| """Skills: library members that produce an artifact rather than an answer. |
| |
| A skill is not a different mechanism. It is a :class:`~distinct_tools.core.ToolSpec` |
| with ``kind="skill"``, registered in the same registry, approved by the same |
| operator policy, selected through the same per-run allowlist, and executed |
| behind the same broker with the same quotas, timeouts and byte caps. The only |
| difference is the shape of the result: a skill's output carries an ``artifact`` |
| mapping, which the agent harness lifts out of the model's context and returns |
| with the run so the user's server session can store and offer it for download. |
| |
| The artifact envelope is one shape everywhere:: |
| |
| {"artifact": {"name": ..., "media_type": ..., "base64": ..., "size_bytes": ...}} |
| |
| Like the local tools, nothing here opens a socket, reads a file, spawns a |
| process or consults a model. ``create_pdf`` builds the PDF bytes in memory from |
| its arguments; ``make_plan`` is a deterministic formatter. Both answer in one |
| call, which is the same viability criterion the local tool set applies below |
| 7B. |
| |
| The plan skill deserves its caveat stated rather than hidden: |
| ``TOOL_CATEGORIES_REVIEW.md`` section 3 records that the one direct small-model |
| measurement found Llama-1B performing *worse* with its own plans than without. |
| ``make_plan`` exists because a written, numbered plan is a deliverable a user |
| asked this network to produce, not because it improves the model. Its |
| description says when not to call it. |
| |
| **The skill repository.** Beside the two skills written in this module there is |
| :mod:`distinct_skills`, a repository of skills held on disk in the Anthropic |
| Agent Skills format: a directory each, with a ``SKILL.md`` carrying the |
| frontmatter a human reads and the description the model reads, a ``tool.json`` |
| carrying the argument schema, and the templates the output is rendered from. |
| :func:`install_skill_repository` turns one of those into exactly the same |
| ``ToolSpec`` with ``kind="skill"`` that ``create_pdf`` is, in the same |
| registry, behind the same broker, refused by the same operator policy. There |
| is no parallel path and no second kind of skill. |
| |
| Three properties are worth stating because the obvious alternative to each is |
| what goes wrong: |
| |
| 1. **The directory is read once, at install time.** ``load_repository`` runs |
| when this module is imported, which is process start-up, and the result is |
| held. A run never touches the disk. This is the same rule |
| :mod:`distinct_tools.mcp` states for ``tools/list`` and for the same reason: |
| ``ToolBroker`` snapshots the registry so a job's capabilities cannot change |
| under it, and a definition re-read per call would defeat that from the other |
| end. The cost is honest and worth naming: a skill edited while the worker is |
| running is not picked up until it restarts. |
| 2. **The definition is digest-pinned.** An edit to any file in a skill |
| directory, template included, changes its digest and is refused at load |
| rather than used. So the failure mode of a tampered file is a skill that |
| does not install, not a skill that quietly does something else. |
| 3. **A skill directory supplies data, never code.** ``tool.json`` names a |
| handler and the name is resolved in a fixed table in |
| :mod:`distinct_skills.handlers`. Importing a module out of the skill folder |
| would make dropping in a directory equivalent to running arbitrary code, |
| which is the property this repository refuses everywhere else. |
| |
| Repository skills are deliberately **not** in |
| :data:`~distinct_tools.INSTALLABLE_SPECS` and therefore not in the ``local`` |
| bundle. The bundle is what a single ``--tools local`` flag approves, and a |
| member that arrived from a directory rather than from this source file should |
| not join it without the operator seeing it by name. They are reachable, and |
| :func:`~distinct_tools.default_registry` installs them, but only for an |
| operator who wrote the exact ``id@version`` ref: the approval is read against |
| ``INSTALLABLE_SPECS`` alone and the repository is then filtered through |
| ``permitted_specs``, which expands no bundle. An MCP server's tools are |
| approved the same way and for the same reason. |
| |
| Two consequences worth stating rather than leaving to be discovered. |
| ``distinct_agent``'s ``--list-tools`` shows the repository as its own group, |
| so an operator can see what there is to name. The server's library picker |
| still reads ``INSTALLABLE_SPECS``, so a repository skill cannot be selected |
| from the web interface yet; that is a change to ``distinct_server``, which |
| this work does not own. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import base64 |
| import zlib |
| from collections.abc import Mapping, Sequence |
|
|
| from .core import Registry, ToolContext, ToolInputError, ToolRef, ToolSpec |
|
|
| try: |
| from distinct_skills import ( |
| HANDLERS, |
| SkillDefinition, |
| SkillError, |
| SkillInputError, |
| describe, |
| load_repository, |
| ) |
|
|
| _IMPORT_FAILURE = "" |
| except ImportError as exc: |
| |
| |
| |
| |
| |
| |
| _IMPORT_FAILURE = f"distinct_skills is not importable ({exc})" |
| |
| |
| SkillError = RuntimeError |
| SkillInputError = ValueError |
|
|
| MAX_PDF_TEXT_CHARACTERS = 12_000 |
| MAX_TITLE_CHARACTERS = 160 |
| MAX_FILENAME_CHARACTERS = 80 |
| MAX_PLAN_STEPS = 20 |
| MAX_STEP_CHARACTERS = 400 |
|
|
| _PAGE_WIDTH = 595 |
| _PAGE_HEIGHT = 842 |
| _MARGIN = 56 |
| _BODY_SIZE = 11 |
| _TITLE_SIZE = 18 |
| _LEADING = 16 |
| _CHARS_PER_LINE = 88 |
| _LINES_PER_PAGE = int((_PAGE_HEIGHT - 2 * _MARGIN - 40) / _LEADING) |
|
|
|
|
| def _safe_filename(value: object, fallback: str) -> str: |
| if not isinstance(value, str) or not value.strip(): |
| return fallback |
| cleaned = "".join( |
| ch for ch in value.strip() if ch.isalnum() or ch in ("-", "_", ".", " ") |
| ).strip() |
| cleaned = cleaned.replace(" ", "-")[:MAX_FILENAME_CHARACTERS].strip(".") or fallback |
| return cleaned |
|
|
|
|
| def _pdf_escape(text: str) -> bytes: |
| """Escape a text run for a PDF literal string, in Latin-1. |
| |
| Characters outside Latin-1 are replaced rather than dropped, so the output |
| is always well-formed even for input the base fonts cannot draw. |
| """ |
|
|
| encoded = text.encode("latin-1", errors="replace") |
| return ( |
| encoded.replace(b"\\", b"\\\\").replace(b"(", b"\\(").replace(b")", b"\\)") |
| ) |
|
|
|
|
| def _wrap(text: str, width: int) -> list[str]: |
| lines: list[str] = [] |
| for raw_line in text.splitlines() or [""]: |
| line = raw_line.rstrip() |
| if not line: |
| lines.append("") |
| continue |
| current = "" |
| for word in line.split(" "): |
| candidate = f"{current} {word}".strip() |
| if len(candidate) <= width: |
| current = candidate |
| continue |
| if current: |
| lines.append(current) |
| while len(word) > width: |
| lines.append(word[:width]) |
| word = word[width:] |
| current = word |
| lines.append(current) |
| return lines |
|
|
|
|
| def _build_pdf(title: str, body: str) -> bytes: |
| """A complete, valid, dependency-free PDF: Helvetica, A4, flate-compressed.""" |
|
|
| wrapped = _wrap(body, _CHARS_PER_LINE) |
| pages: list[list[str]] = [] |
| for index in range(0, max(1, len(wrapped)), _LINES_PER_PAGE): |
| pages.append(wrapped[index : index + _LINES_PER_PAGE]) |
|
|
| content_streams: list[bytes] = [] |
| for page_number, page_lines in enumerate(pages): |
| parts: list[bytes] = [b"BT\n"] |
| cursor = _PAGE_HEIGHT - _MARGIN |
| if page_number == 0 and title: |
| parts.append( |
| b"/F2 %d Tf\n1 0 0 1 %d %d Tm\n(%s) Tj\n" |
| % (_TITLE_SIZE, _MARGIN, cursor, _pdf_escape(title)) |
| ) |
| cursor -= 30 |
| parts.append(b"/F1 %d Tf\n" % _BODY_SIZE) |
| for line in page_lines: |
| parts.append( |
| b"1 0 0 1 %d %d Tm\n(%s) Tj\n" % (_MARGIN, cursor, _pdf_escape(line)) |
| ) |
| cursor -= _LEADING |
| parts.append(b"ET") |
| content_streams.append(zlib.compress(b"".join(parts))) |
|
|
| objects: list[bytes] = [] |
| page_count = len(content_streams) |
| |
| first_page_object = 5 |
| kids = b" ".join( |
| b"%d 0 R" % (first_page_object + index * 2) for index in range(page_count) |
| ) |
| objects.append(b"<< /Type /Catalog /Pages 2 0 R >>") |
| objects.append( |
| b"<< /Type /Pages /Kids [%s] /Count %d >>" % (kids, page_count) |
| ) |
| objects.append(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") |
| objects.append(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>") |
| for index, stream in enumerate(content_streams): |
| content_object = first_page_object + index * 2 + 1 |
| objects.append( |
| b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 %d %d] " |
| b"/Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents %d 0 R >>" |
| % (_PAGE_WIDTH, _PAGE_HEIGHT, content_object) |
| ) |
| objects.append( |
| b"<< /Length %d /Filter /FlateDecode >>\nstream\n" % len(stream) |
| + stream |
| + b"\nendstream" |
| ) |
|
|
| buffer = bytearray(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n") |
| offsets: list[int] = [] |
| for number, obj in enumerate(objects, start=1): |
| offsets.append(len(buffer)) |
| buffer += b"%d 0 obj\n" % number + obj + b"\nendobj\n" |
| xref_offset = len(buffer) |
| buffer += b"xref\n0 %d\n0000000000 65535 f \n" % (len(objects) + 1) |
| for offset in offsets: |
| buffer += b"%010d 00000 n \n" % offset |
| buffer += ( |
| b"trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n" |
| % (len(objects) + 1, xref_offset) |
| ) |
| return bytes(buffer) |
|
|
|
|
| def _create_pdf(arguments: Mapping[str, object], context: ToolContext) -> Mapping[str, object]: |
| del context |
| known = frozenset({"title", "body", "filename"}) |
| unknown = sorted(set(map(str, arguments)) - known) |
| if unknown: |
| raise ToolInputError(f"unsupported arguments: {', '.join(unknown)}") |
| body = arguments.get("body") |
| if not isinstance(body, str) or not body.strip(): |
| raise ToolInputError("body must be a non-empty string") |
| if len(body) > MAX_PDF_TEXT_CHARACTERS: |
| raise ToolInputError( |
| f"body must be at most {MAX_PDF_TEXT_CHARACTERS:,} characters" |
| ) |
| title = arguments.get("title") |
| if title is not None and not isinstance(title, str): |
| raise ToolInputError("title must be a string") |
| title = (title or "").strip()[:MAX_TITLE_CHARACTERS] |
| name = _safe_filename(arguments.get("filename"), "document") |
| if not name.lower().endswith(".pdf"): |
| name = f"{name}.pdf" |
| payload = _build_pdf(title, body) |
| return { |
| "created": True, |
| "pages": max(1, (len(_wrap(body, _CHARS_PER_LINE)) + _LINES_PER_PAGE - 1) // _LINES_PER_PAGE), |
| "artifact": { |
| "name": name, |
| "media_type": "application/pdf", |
| "base64": base64.b64encode(payload).decode("ascii"), |
| "size_bytes": len(payload), |
| }, |
| } |
|
|
|
|
| def _make_plan(arguments: Mapping[str, object], context: ToolContext) -> Mapping[str, object]: |
| del context |
| known = frozenset({"goal", "steps"}) |
| unknown = sorted(set(map(str, arguments)) - known) |
| if unknown: |
| raise ToolInputError(f"unsupported arguments: {', '.join(unknown)}") |
| goal = arguments.get("goal") |
| if not isinstance(goal, str) or not goal.strip(): |
| raise ToolInputError("goal must be a non-empty string") |
| if len(goal) > MAX_STEP_CHARACTERS: |
| raise ToolInputError(f"goal must be at most {MAX_STEP_CHARACTERS} characters") |
| steps = arguments.get("steps") |
| if not isinstance(steps, (list, tuple)) or not steps: |
| raise ToolInputError("steps must be a non-empty list of strings") |
| if len(steps) > MAX_PLAN_STEPS: |
| raise ToolInputError(f"steps must contain at most {MAX_PLAN_STEPS} entries") |
| cleaned: list[str] = [] |
| for step in steps: |
| if not isinstance(step, str) or not step.strip(): |
| raise ToolInputError("every step must be a non-empty string") |
| if len(step) > MAX_STEP_CHARACTERS: |
| raise ToolInputError( |
| f"each step must be at most {MAX_STEP_CHARACTERS} characters" |
| ) |
| cleaned.append(" ".join(step.split())) |
| lines = [f"Plan: {' '.join(goal.split())}", ""] |
| lines += [f"{index}. {step}" for index, step in enumerate(cleaned, start=1)] |
| text = "\n".join(lines) |
| return { |
| "plan": text, |
| "step_count": len(cleaned), |
| "artifact": { |
| "name": "plan.txt", |
| "media_type": "text/plain", |
| "base64": base64.b64encode(text.encode("utf-8")).decode("ascii"), |
| "size_bytes": len(text.encode("utf-8")), |
| }, |
| } |
|
|
|
|
| SKILL_SPECS: tuple[ToolSpec, ...] = ( |
| ToolSpec( |
| tool_id="create_pdf", |
| version="1", |
| kind="skill", |
| description=( |
| "Create a PDF document from text you already have, and return it as a " |
| "downloadable file for the user. Arguments: body (required text, at most " |
| "12,000 characters), title (optional heading), filename (optional). Call " |
| "this only when the user asked for a document or a file; do not call it " |
| "to answer an ordinary question, and do not call it twice for the same " |
| "document." |
| ), |
| input_schema={ |
| "type": "object", |
| "additionalProperties": False, |
| "properties": { |
| "title": {"type": "string", "description": "optional document title"}, |
| "body": {"type": "string", "description": "the document text"}, |
| "filename": {"type": "string", "description": "optional file name"}, |
| }, |
| "required": ["body"], |
| }, |
| ), |
| ToolSpec( |
| tool_id="make_plan", |
| version="1", |
| kind="skill", |
| description=( |
| "Record a numbered plan as a small text artifact the user can keep. " |
| "Arguments: goal (one sentence) and steps (a list of short strings, at " |
| "most 20). Call this only when the user asked for a plan as a " |
| "deliverable; do not call it to organise your own reasoning, and never " |
| "call it more than once per request." |
| ), |
| input_schema={ |
| "type": "object", |
| "additionalProperties": False, |
| "properties": { |
| "goal": {"type": "string", "description": "what the plan achieves"}, |
| "steps": { |
| "type": "array", |
| "items": {"type": "string"}, |
| "description": "ordered steps, each one action", |
| }, |
| }, |
| "required": ["goal", "steps"], |
| }, |
| ), |
| ) |
|
|
|
|
| def register_skills(registry: Registry, *, only: frozenset | None = None) -> None: |
| """Register the skill set explicitly, mirroring ``register_local_tools``. |
| |
| This registers the two skills written in this module. The repository on |
| disk is installed by :func:`install_skill_repository`, which is a separate |
| call because it is a separate decision: these two ship approved by the |
| ``local`` bundle, and a definition read from a directory should not. |
| """ |
|
|
| handlers = { |
| "create_pdf": _create_pdf, |
| "make_plan": _make_plan, |
| } |
| for spec in SKILL_SPECS: |
| if only is not None and spec.ref not in only: |
| continue |
| registry.register(spec, handlers[spec.tool_id]) |
|
|
|
|
| |
| |
| |
|
|
|
|
| def _spec_for(skill: SkillDefinition) -> ToolSpec: |
| """Turn a loaded skill into an ordinary spec, with nothing added. |
| |
| ``origin`` is carried through rather than forced to ``builtin``. The |
| packaged repository is this repository's own text and says so; a directory |
| an operator supplied says something else, and ``ToolSpec.third_party`` |
| then reports true, which is the flag a harness uses to decide whether a |
| description needs fencing before it reaches the prompt. |
| """ |
|
|
| return ToolSpec( |
| tool_id=skill.tool_id, |
| version=skill.version, |
| kind="skill", |
| description=skill.description, |
| input_schema=skill.input_schema, |
| origin=skill.origin, |
| ) |
|
|
|
|
| def _bind(skill: SkillDefinition): |
| """Adapt a repository handler to the broker's handler signature. |
| |
| The repository knows nothing about this framework, deliberately, so its |
| handlers take their own arguments and raise their own input error. The |
| translation happens here, at the one boundary, rather than by having the |
| repository import the framework and risk an import cycle. |
| """ |
|
|
| try: |
| handler = HANDLERS[skill.handler] |
| except KeyError as exc: |
| |
| |
| |
| raise SkillError( |
| f"skill {skill.name!r} names the handler {skill.handler!r}, which this " |
| "repository does not define" |
| ) from exc |
|
|
| def call(arguments: Mapping[str, object], context: ToolContext) -> object: |
| del context |
| try: |
| return handler(arguments, skill) |
| except SkillInputError as exc: |
| raise ToolInputError(str(exc)) from exc |
|
|
| call.__name__ = f"skill_{skill.tool_id}" |
| return call |
|
|
|
|
| def _load_packaged_repository() -> tuple[tuple, tuple, str]: |
| """Read the packaged repository once, and never fail an import over it. |
| |
| A broken or tampered repository must not stop the worker starting, because |
| the rest of the tool framework is unaffected by it and a worker that |
| refuses to boot is a worse outcome than a worker missing four skills. It |
| must also not be silent, so the reason is kept and |
| :func:`describe_skill_repository` prints it. An explicit |
| :func:`install_skill_repository` call raises instead: a caller who asked |
| for the repository is entitled to the exception. |
| """ |
|
|
| if _IMPORT_FAILURE: |
| return (), (), _IMPORT_FAILURE |
| try: |
| skills = load_repository() |
| specs = tuple(_spec_for(skill) for skill in skills) |
| except (SkillError, ValueError, OSError) as exc: |
| return (), (), f"the packaged skill repository was refused: {exc}" |
| return skills, specs, f"{len(skills)} skill(s) loaded and digest-checked" |
|
|
|
|
| |
| |
| |
| REPOSITORY_SKILLS, REPOSITORY_SPECS, REPOSITORY_STATUS = _load_packaged_repository() |
|
|
|
|
| def install_skill_repository( |
| registry: Registry, |
| *, |
| only: frozenset[ToolRef] | None = None, |
| skills: Sequence[SkillDefinition] | None = None, |
| ) -> tuple[ToolSpec, ...]: |
| """Register repository skills into an ordinary registry. |
| |
| ``only`` filters by exact ``ToolRef``, exactly as ``register_skills`` does, |
| so the caller passes whatever the operator approved and nothing else |
| appears. Approval itself is unchanged and happens elsewhere: the operator |
| policy decides, and :class:`ToolBroker` refuses anything unapproved before |
| the job starts. |
| |
| ``skills`` overrides what is installed, for an operator running their own |
| directory: they call :func:`distinct_skills.load_repository` with their own |
| root and pins and pass the result. Loading stays in one place that way, and |
| the disk read is visible at the call site rather than hidden in here. |
| |
| :func:`~distinct_tools.default_registry` already does all of this for an |
| operator who named a ref. A caller building a registry by hand should copy |
| the shape it uses, and the first line most of all:: |
| |
| # INSTALLABLE_SPECS and nothing else: `local` expands over this list. |
| policy = load_policy(INSTALLABLE_SPECS) # operator named status_update@1 |
| approved = frozenset(spec.ref for spec in policy.permitted_specs(INSTALLABLE_SPECS)) |
| repository = frozenset(spec.ref for spec in policy.permitted_specs(REPOSITORY_SPECS)) |
| registry = Registry() |
| register_local_tools(registry, only=approved) |
| register_skills(registry, only=approved) |
| install_skill_repository(registry, only=repository) |
| # The run's own allowlist is the third gate and normally comes from |
| # the server's JobSpec; approving both halves is the standalone case. |
| job = JobPolicy(allowed_tools=approved | repository) |
| broker = ToolBroker(registry, job, job_id=..., operator_policy=policy) |
| |
| An earlier version of this docstring passed ``(*INSTALLABLE_SPECS, |
| *REPOSITORY_SPECS)`` to ``load_policy``, which reads as harmless and is |
| not: the bundle name ``local`` expands over whatever that list contains, so |
| it would have quietly put every skill on disk inside one flag. The |
| two-step above, approve narrowly then filter by ref, is the difference. |
| |
| Raises rather than installing part of a set, and raises rather than |
| installing nothing quietly when the packaged repository could not be read. |
| """ |
|
|
| if not isinstance(registry, Registry): |
| raise TypeError("registry must be a Registry") |
| if skills is None: |
| if not REPOSITORY_SKILLS: |
| raise SkillError(REPOSITORY_STATUS or "no skill repository is available") |
| skills = REPOSITORY_SKILLS |
|
|
| chosen = [] |
| for skill in skills: |
| spec = _spec_for(skill) |
| if only is not None and spec.ref not in only: |
| continue |
| chosen.append((spec, skill)) |
|
|
| |
| |
| for spec, skill in chosen: |
| registry.register(spec, _bind(skill)) |
| return tuple(spec for spec, _ in chosen) |
|
|
|
|
| def describe_skill_repository() -> str: |
| """Render the packaged repository for an operator to read before approving.""" |
|
|
| if not REPOSITORY_SKILLS: |
| return f"No repository skills are installed: {REPOSITORY_STATUS}" |
| return describe(REPOSITORY_SKILLS) |
|
|
|
|
| __all__ = [ |
| "REPOSITORY_SKILLS", |
| "REPOSITORY_SPECS", |
| "REPOSITORY_STATUS", |
| "SKILL_SPECS", |
| "describe_skill_repository", |
| "install_skill_repository", |
| "register_skills", |
| ] |
|
|