ConceptsResources

Resources

Who runs each operator, and what it is allowed to do beyond reading. Three closed places carry it, and scripts/validate-resources.mjs keeps them from drifting apart:

  • resources/tools.json — the closed registry of tools. A tool absent from this file does not exist for an operator.
  • resources/agents/profiles/<runtime>.json — one file per runtime, openai.json and claude.json. A profile carries a model, an isolation, its capabilities (what the runtime can do here) and its permits (what an operator on that profile is allowed to use), stated for every tool in the registry.
  • operators/<id>/operator.json → resources — the one profile that runs the operator end to end, whether it is grammarBound, and tools: the map of @tools/<id> to the one mode it runs in. There is no central assignment file.

A tool is not an alias

An alias is what an operator may read: alias/alias.json resolves it to an exact location, and operator.md’s Context table declares every one the operator is allowed to open. A tool is what the operator may do beyond reading — write a file, run a command, drive a browser, probe an endpoint, resolve a sealed credential.

Both are written in the same Steps cells, so one table says where the evidence came from and what made it: a tool goes into the Reads cell of the step that calls it to obtain evidence, and into the Writes cell of the step that changes something with it. @tools/fileread is the exception that needs no step, because reading the Context aliases is what every step already does.

Two rules close the loop, both enforced by scripts/validate-alias.mjs:

  • a Steps cell may only name a tool the package declares in operator.json → resources.tools;
  • every declared tool other than @tools/fileread must be named by at least one step, in both the English file and its Vietnamese mirror, in the same rows.

So a grant nobody uses cannot survive in a manifest, and a step cannot reach for a capability the manifest never asked for.

Modes

A tool is never declared bare. Each one publishes the modes an operator may declare, and the mode is the whole permission: @tools/git: read cannot commit, @tools/git: commit-session-branch commits once to session/<sessionId> and never to a person’s branch, @tools/git: merge-and-push is the only mode that reaches a remote and still may not rebase, force, or skip a hook. @tools/database: namespaced-write may insert and delete only rows carrying the run namespace and is_uat=true. @tools/print: decision-points hands the person the artifact a decision or a verdict rests on, at the moment it is recorded, and records the same artifact in the receipt; when the decision is the person’s, those artifacts are the options — rendered candidates, one per option and at least three for a composition or taste choice, with a one-question message — never prose alternatives. never is not a declaration: a tool the operator does not use is left out of the map entirely.

Capability and permission stay different facts. Capability belongs to the runtime; permission is a policy about the operator. sol-fresh and sol-reviewer run the same model, but the reviewer is permitted only what a reviewer needs, “because a reviewer that produces is no longer a reviewer.” A tool absent from resources.tools is unavailable even where the profile would permit it.

What each runtime provides

resources/tools.json states, per tool, whether each runtime supports it and by what means. A runtime that does not support a tool cannot run an operator that declares it: the processor runs the profile equivalent on a runtime that does, or reports the gap.

ToolModesCodex (openai)Claude Code (claude)
filereadcontext-aliasesyesyes
sourcewritedeclared-write-setyesyes
gitread, commit-session-branch, merge-and-pushyesyes
shelldeclared-commandsyesyes
websearchboundedyesyes
imagegenjudged, requiredyesno
visualizehtmlyesyes
browsercontrolrequiredyesyes
httpprobeyesyes
registryreadyesyes
containerread, operateyesyes
ciread, dispatchyesyes
objectstorageread, writeyesyes
secretsresolve-by-nameyesyes
databaseread, namespaced-writeyesyes

imagegen is the one gap today: no Claude profile can make artwork.

Artwork versus rendering

imagegen and visualize are different acts and the registry keeps them apart:

  • imagegen produces artwork — a digital image from an image model, made to one stated claim.
  • visualize renders HTML for a person to look at — direction candidates, audit sheets, alternative comparisons, previews — written under response/artifacts/ and linked from response.md. It needs no model grant, and every runtime does it.

An operator that draws a comparison page is visualizing, not generating an image. Only three image modes exist:

ModeWhereWhat it means
absentEvery operator not listed belowThe operator produces no artwork at all
judgedfrontend.direction.decide, frontend.source.applyThe operator decides for itself, from the direction, whether a region reads empty enough to need artwork, records why, and never decorates
requiredcontent.generateArtwork is a step of the job: made to a stated claim and inspected for fidelity to it

Where the runtime cannot make artwork, a judged step records that none could be made and the branch continues; a required step blocks with the operator’s own code.

Research, and where it is legitimate

@tools/websearch: bounded is declared only by the operators that have a step where closing a gap from outside the tree is honest work: the decision operators, the content brief, the surface audit’s comparison against a published rule, and the UAT run against a third-party service. Research is bounded by the exact gap the step names, records every source with its URL, and never copies a page, brand, palette, or component anatomy. A presentation value the knowledge does not publish is RULE_MISSING, never a research task. An operator with no such step — every applier, publisher, verifier and operator of running systems — does not declare the tool at all.

Grammar binding

resources.grammarBound is true for the four frontend operators. A missing reusable capability is GRAMMAR_REQUIRED or COMMON_CAPABILITY_MISSING, never a local imitation.

One agent per operator, at most three at once

resources/orchestrator.json fixes the orchestration:

One invocation of one operator is one agent, created fresh on the operator’s own profile with the aliases its Context table declares and nothing else.

The orchestrator owns dispatch: it writes the branch’s request.json, validates it, starts the agent, validates the response, and follows routing.json. It never merges two operators into one agent and never lets an agent start another — a step that pauses with waiting is served by a second fresh agent, with its own request/ and response/ inside the branch.

At most three agents run at once, and branches of one step run concurrently only when their writes touch no common alias. Two agents may read one checkout; only one may hold it for writing. business.decide is exclusive per businesses root, platform.operate per runtime owner.

Profile equivalents

An operator binds one profile. When the processor runs on a runtime that lacks that profile, it runs the equivalent profile of its own runtime and response.json records both boundProfile and ranProfile, so an audit can tell a stand-in from the binding. The pairs are symmetric and always cross runtimes:

Bound profileEquivalent
sol-freshopus
lunasonnet
sol-reviewerfable

The equivalent runs with the tools the operator declares that its own profile permits and its runtime supports. A tool the runtime does not support is unavailable for that run.

Changing any of it

A profile’s model or permits, an operator’s profile, and any declared tool or mode are owner decisions. Changing one is an edit in the profile file and in the operator’s operator.json, the matching Steps rows in both languages, the row in resources/INDEX.md, plus a green npm test. Adding a tool to the registry means stating it in every profile explicitly, because the validator refuses a profile that leaves a tool unstated.