Documentation
¶
Overview ¶
Package moduletools adapts detached module capabilities into agent tools.
This is the seam the whole architecture exists for. A module declares capabilities; the host turns each enabled one into a tool the agent can call, so "assay my last three Claude sessions" reaches sessions.assay without the agent knowing anything about Midden, and without Midden being linked into this binary.
The host stays in charge of everything that matters. It decides which capabilities become tools, supplies filesystem roots and subprocess binaries per invocation, enforces bounds, and validates what comes back. A module contributes a description and a schema; it does not gain authority by being installed.
Index ¶
- Constants
- func ApprovalSatisfied(dec ApprovalDecision, rec *modprotov2.ApprovalRecord) bool
- func Disabled(dir string) bool
- func GrantRoots(d *modproto.Descriptor, home, workspace string) map[string]modproto.Root
- func GrantRootsWithSourceOverrides(d *modproto.Descriptor, home, workspace string, sourceRoots map[string]string) map[string]modproto.Root
- func KnowledgeLoader(installed []Installed) func(string) (string, string, []string)
- func KnownModules(installed []Installed) map[string]bool
- func MayBill(c modproto.Capability) bool
- func MayBillUnder(_ *modproto.Descriptor, c modproto.Capability) bool
- func ModuleDir(home, binary string) (string, bool)
- func ModulesDir(home string) string
- func NeedsApproval(d *modproto.Descriptor, c modproto.Capability, approved bool) bool
- func New(r *module.Runner, d *modproto.Descriptor, home, workspace string) []toolshared.Tool
- func NormalizeExplicitSourceRoot(name, path string) (string, bool)
- func PlaceInput(req *modproto.Request, raw json.RawMessage, humanApproved bool) error
- func PreApproved(moduleID, capabilityID string) bool
- func SetDisabled(dir string, off bool) error
- func StandingApprovalWarnings() []string
- func ToolName(moduleID, capabilityID string) string
- type ApprovalDecision
- type CapabilityTool
- type Document
- type HostConformance
- type Installed
- type Knowledge
- type KnowledgeTool
- type V2
Constants ¶
const ArtifactMarker = "@artifact "
ArtifactMarker prefixes the one-line JSON description of a produced artefact. The cockpit scans assistant output for it to render artefact cards; the model reads the same line as text, so both see identical facts.
const EnvApproveCapabilities = "FACET_STUDIO_APPROVE_CAPABILITIES"
EnvApproveCapabilities names capabilities the operator has already decided about, as a comma-separated list of "<module>/<capability>".
It exists because enforcing the approval guarantee made the operator's actual goal impossible. "Create a video from simple chatting" goes through a cost_known:false capability every time; the agent cannot approve, because an approval arriving through the model is stripped by design; and approval is per-call and remembered nowhere. So chat could never complete that journey again -- the control was correct and the product stopped working.
This is the operator saying, once and explicitly, "I have read what this capability declares and I accept it". It is not the host inferring anything, and it is not the model deciding: a standing approval can only be set by someone with access to the environment the host runs in.
Deliberately NOT a wildcard. "Approve everything this module ever adds" is not a decision anyone can make in advance -- the module updates and the approval silently covers capabilities that did not exist when it was written. Each entry names one capability.
Variables ¶
This section is empty.
Functions ¶
func ApprovalSatisfied ¶
func ApprovalSatisfied(dec ApprovalDecision, rec *modprotov2.ApprovalRecord) bool
ApprovalSatisfied reports whether an approval record covers a decision.
EVERY reason must be covered. A record granting "chargeable" does not authorise an irreversible step: the person approved a charge, and approving one reason is not approving another they were never shown. Treating any approval as blanket is how a gate with reasons becomes a gate with one bit again.
func Disabled ¶
Disabled reports whether the module installed at dir has been turned off.
A module directory that cannot be read is treated as ENABLED: refusing to run a module because the host could not stat a marker would be a silent capability loss, and the failure to read is itself surfaced elsewhere.
func GrantRoots ¶
GrantRoots maps the logical root NAMES a module declared onto real absolute paths, for one invocation.
This is where "installing a module grants nothing" is concrete: a module declares names, the host decides what each points at, source stores are supplied read-only, and each module's own state is confined to its own directory so modules cannot see or corrupt each other's.
func GrantRootsWithSourceOverrides ¶
func GrantRootsWithSourceOverrides( d *modproto.Descriptor, home, workspace string, sourceRoots map[string]string, ) map[string]modproto.Root
GrantRootsWithSourceOverrides is the local invocation/UAT seam for explicit source stores. A non-nil map disables ambient user-home source discovery; invalid or unavailable entries are omitted rather than replaced.
func KnowledgeLoader ¶
KnowledgeLoader returns a function that composes one module's knowledge.
Discovery happens ONCE, here, because describing every installed module is a subprocess per module and a turn must not pay that. The returned closure is called per turn with whatever module the user selected.
It returns empty for an empty selection, which is the layered-composition rule made concrete: an installed module costs one line of capability summary until someone points the agent at it, and only then do its overlay and skills enter context.
Returns nil when nothing is installed, so a caller can tell "no modules" from "a module that contributes nothing".
func KnownModules ¶
KnownModules reports which module ids are installed, lowercased.
Exposed so a caller that must decide whether to ACT on a selection -- rather than merely load its knowledge -- can ask without repeating discovery.
func MayBill ¶
func MayBill(c modproto.Capability) bool
MayBill reports whether a capability must be treated as possibly-billing.
It reads CostKnown, and that is a HOST POLICY rather than a reading of what the field means. cost_known declares whether a NUMERIC AMOUNT IS KNOWN -- nothing more. It does not mean "may spend money", and the owning lane proved it by running: a tool declaring cost_known TRUE and network TRUE requires no consent at all.
So this function is the host over-gating on the only signal v1 carries. The fact it actually wants -- "may incur a monetary charge", independent of whether the amount is known -- is not expressible in v1 and is a successor contract concern (may_charge). Until then, unknown amount is the closest available proxy, and it is deliberately the pessimistic one.
This comment previously said MayBill "reports whether a capability could actually spend money. It is CostKnown" -- stating the superseded equivalence as the definition. The BEHAVIOUR was and is correct; the justification taught the wrong rule, in the file someone copies the pattern from.
Two attempts at a structural rule are recorded below because both were wrong in ways that mattered, and the next person will be tempted by the same idea.
Attempt one: treat local && !network && a local provider as unable to bill, so Facet's creative.tools.estimate -- which declares cost_known:false while also declaring local, no network, provider "local", and whose own summary says "never bills" -- would stop being gated. It worked for that case and let Midden's content.produce through UNGATED, which genuinely spends by shelling out to an AI CLI. The module never touches the network; the subprocess does.
Attempt two: add "the module declared subprocess binaries" to the test. That re-gated content.produce and ALSO gated creative.tools.estimate, because Facet declares ffmpeg, ffprobe, node and npx -- none of which can charge anything. Module-level permissions are too coarse to answer a per-capability question.
There is no honest structural rule available. paid_providers does not help either: Midden declares none and spends; Facet declares five but not on estimate. So the host takes the module at its word.
Note what "at its word" now means, because it changed. Under the settled meaning of cost_known, Facet's creative.tools.estimate declaring cost_known:false while its summary says it never bills is NOT a contradiction: the amount really is unknown (their estimate returns a null cost), and never billing is a separate fact the field cannot carry. The host still gates it, and that is the host being pessimistic on a proxy signal -- not the module contradicting itself. Any warning phrased as "one of these is wrong" is describing a v1 expressiveness gap, not an author error.
The cost of guessing wrong is asymmetric and that decides it: a wrongly gated free capability is an annoying extra click, and a wrongly ungated paid one spends the user's money without asking.
func MayBillUnder ¶
func MayBillUnder(_ *modproto.Descriptor, c modproto.Capability) bool
MayBillUnder exists so callers can pass the descriptor without caring whether the decision uses it. It does not, today, for the reasons above -- but the two attempts that failed both needed it, and a future signal that genuinely answers the question would be per-capability and live here.
func ModuleDir ¶
ModuleDir is the directory an installed module occupies.
It resolves from the BINARY rather than from the module id, because a module may be installed as <modules>/<id>/<binary> or as a bare binary directly in <modules>/ -- and a bare binary has no directory of its own to disable.
func ModulesDir ¶
ModulesDir is where installed modules live under the host state root. Only the host constructs this path; a module never needs to know it.
func NeedsApproval ¶
func NeedsApproval(d *modproto.Descriptor, c modproto.Capability, approved bool) bool
NeedsApproval reports whether this capability must be refused before it runs.
It is the ONE place the approval policy is composed. Both callers -- the agent path (adapter.go) and the cockpit path (api/modules.go) -- ask this rather than each rebuilding "may bill AND nobody approved".
They were not sharing it, and the shape of that divergence is the one this codebase keeps finding: five decisions duplicated across the CLI, the agent and the cockpit, and every place they diverged had produced a bug (grantRoots, ApplyGrants, the seed request shape, DeadlineMS, PartialInstall). The two approval gates had not diverged yet. Sharing the composition is what keeps a later fix from landing on one caller and not the other.
The approval SOURCE stays with the caller, deliberately, because the two are genuinely different questions and collapsing them would be wrong:
- the agent path has no per-call approval to consult. An approval arriving through the model is stripped by design, so its only source is the operator's standing decision in the host environment.
- the cockpit path has a real person pressing a button against the effects the capability declares, which is a per-request fact.
So the caller answers "did someone approve THIS", and this function answers "does it need approval at all".
func New ¶
func New(r *module.Runner, d *modproto.Descriptor, home, workspace string) []toolshared.Tool
New builds tools for every capability a module declares.
One tool per capability rather than one tool per module: the agent picks by what it wants to do, and each capability carries its own schema and its own declared effects, which is what makes per-capability approval possible.
func NormalizeExplicitSourceRoot ¶
NormalizeExplicitSourceRoot validates the concrete shape expected by each source store. OpenCode is a database file; Claude and Copilot are directories.
func PlaceInput ¶
PlaceInput puts a caller-supplied JSON object into a request.
A capability's RequestSchema describes Request.Input, but some modules read arguments beside Input at the request root, so an explicit "input" key is used verbatim and sibling keys pass through at the root. Host-owned fields are never overwritten, so a caller cannot widen roots, forge a request ID, or extend a deadline by naming one.
humanApproved says whether a PERSON authorized this specific invocation -- true only for a deliberate act in the cockpit, never for anything the model produced. It decides whether an approval claim in the arguments is carried or stripped.
The distinction is a parameter rather than a convention because the two callers look identical at the call site and mean opposite things: an agent tool call is the model's word, and a click on "Approve and run" is the user's. A convention would be one refactor away from silently treating them the same, which is exactly the bug this exists to prevent.
func PreApproved ¶
PreApproved reports whether the operator has standing approval on file for exactly this module and capability.
Unset means unchanged: everything that may bill still gates. A host that stopped gating because a variable was absent would turn the guarantee off for everyone who never sets it.
func SetDisabled ¶
SetDisabled turns a module off or on.
Disabling does NOT uninstall: the binary, its state and its declared content stay exactly where they are, so re-enabling restores what the user had rather than re-fetching it.
func StandingApprovalWarnings ¶
func StandingApprovalWarnings() []string
StandingApprovalWarnings reports entries the host could not use.
A malformed entry must be VISIBLE. Ignoring it silently leaves the operator believing they approved something they did not, and they only discover otherwise when the agent refuses -- at which point the setting looks broken rather than mistyped.
Types ¶
type ApprovalDecision ¶
type ApprovalDecision struct {
Required bool
// Reasons are drawn from the frozen legal set: chargeable, irreversible,
// external_write, product_checkpoint.
Reasons []string
}
ApprovalDecision is why a v2 invocation needs approval, or why it does not.
Distinct from the v1 MayBill boolean on purpose. v1 could say only "possibly-billing", so a free-but-irreversible step was ungated entirely and a cost-checking tool was gated as though it spent money. v2 carries the REASON (section 6), and the reason is what a person is actually being asked about.
func NeedsApprovalV2 ¶
func NeedsApprovalV2( d *modprotov2.Descriptor, c *modprotov2.Capability, input map[string]any, ) ApprovalDecision
NeedsApprovalV2 decides approval for ONE invocation of a v2 capability, against the request that is about to be sent.
THE INPUT MATTERS, AND THAT IS THE POINT. v1's gate read a static boolean, so a capability that charges for 12 of 19 kinds was gated for all 19 -- permanently over-gating the 7 free ones. Section 2a rule 2 evaluates the capability's condition against the actual request, so the free kinds stop being gated at the layer that acts.
IT READS CAPABILITY EFFECTS, NOT OPERATION EFFECTS (section 2). The Operation is where the semantics live; the capability is where the gate reads. That ordering is why the no-weakening check exists at all: a capability weaker than its Operation would disable this gate while the Operation still read correct to a reviewer. So this function assumes conformance has already run, and the caller is responsible for that -- see HostConformance.
A capability projecting NO Operation is still gated. A registry read transforms no product material and is still invocable, so its declared effects are still the thing the host acts on.
type CapabilityTool ¶
type CapabilityTool struct {
// contains filtered or unexported fields
}
CapabilityTool exposes one module capability as an agent tool.
func (*CapabilityTool) Description ¶
func (t *CapabilityTool) Description() string
Description is what the agent reads when deciding whether to call this.
It states the declared effects in plain language, because the model choosing a cheap local capability over an expensive networked one is the first line of cost control -- long before the approval prompt, which is the last.
func (*CapabilityTool) Execute ¶
func (t *CapabilityTool) Execute(ctx context.Context, args map[string]any) *toolshared.ToolResult
Execute invokes the capability as a bounded detached process.
func (*CapabilityTool) Name ¶
func (t *CapabilityTool) Name() string
Name is the tool name the model sees.
Capability IDs are already namespaced by module ("creative.tools.run", "sessions.assay"), but dots are not universally safe in tool names across providers, so they become underscores. The module ID is prefixed so two modules offering a similarly-named capability cannot collide.
func (*CapabilityTool) Parameters ¶
func (t *CapabilityTool) Parameters() map[string]any
Parameters is the capability's declared request schema.
The module's own schema is handed to the model directly rather than being re-described by the host: the module owns its domain, and any translation here would be a second source of truth that could drift.
type Document ¶
type Document struct {
ModuleID string
Version string
ID string
Title string
Path string
Digest string
Tokens int
Content string
}
Document is one verified piece of module knowledge.
func (Document) Provenance ¶
Provenance is the attribution line recorded for anything entering agent context, so a later reader can tell where an instruction came from.
type HostConformance ¶
type HostConformance struct {
Module string
Findings []modprotov2.Finding
}
HostConformance is the host's verdict on ONE described module, and it is the thing a caller must consult before acting on any v2 declaration.
WHY A TYPE RATHER THAN AN ERROR. A conformance run produces per-target findings, and the operator ruling is that conformance is evaluated PER TARGET independently: a projection may not claim conformance while silently weakening a mandatory requirement, and one module's failure says nothing about another's. An error return would collapse that into a single boolean at the call site.
func CheckHostConformance ¶
func CheckHostConformance(a modprotov2.Accepted) HostConformance
CheckHostConformance closes the triangle at the host:
product semantic declaration -> v2 projection -> host interpretation
THE HOST MUST FAIL MECHANICALLY IF THE PROJECTION WEAKENS AN ACCEPTED SEMANTIC. This is the one rule whose violation is invisible at the layer being reviewed: the Operation reads correct while the gate, which reads capability effects, fires on nothing. So the check runs here, before any capability of this module reaches NeedsApprovalV2.
It takes an Accepted rather than a *Descriptor, which is not a stylistic choice: an Accepted can only be produced by modprotov2.Evaluate, so the contract pin has necessarily already run. The ordering the operator requires -- gate before reliance -- is therefore enforced by the type system rather than by this comment.
func (HostConformance) Conforms ¶
func (h HostConformance) Conforms() bool
Conforms reports whether the host may act on this module's v2 declarations.
func (HostConformance) Refusal ¶
func (h HostConformance) Refusal() string
Refusal renders the verdict for a person, with reason AND remedy.
Every finding is listed rather than only the first. A validator that reports one problem at a time turns a single bad descriptor into as many rebuild cycles as it has defects, and the author cannot see whether they are unrelated or one cause.
type Installed ¶
type Installed struct {
Runner *module.Runner
Descriptor *modproto.Descriptor
Warnings []string
// HostWarnings are the HOST's findings about this module, kept apart from
// what the module said about itself.
//
// They were merged, which made a stale self-diagnostic from a module and a
// digest mismatch found by the host look identical on the Modules page --
// and one of those the operator can act on. The invoke path had already
// drawn this line; discovery had not.
HostWarnings []string
// V2 is the module's behavioural-contract standing: the pin outcome, and
// the host's no-weakening verdict when the pin passed.
//
// Always populated, including for v1 modules and refusals. A nil-means-v1
// field would make three outcomes share one observable, and telling them
// apart is the entire job of the pin.
V2 V2
// Disabled is set when the user has turned this module off. It is still
// DISCOVERED -- the Modules page must show it, with its capabilities, so
// there is something to turn back on -- but it contributes no tools.
Disabled bool
// Err is set when the module could not describe itself. A broken module is
// reported rather than fatal: one bad install must not hide the others.
Err error
}
Installed is one discovered module.
func Discover ¶
Discover finds installed modules and asks each to describe itself.
Installation is a file on disk plus a successful describe. There is no registry file to drift out of sync with the executable: the binary is the source of truth about what it can do.
func RegisterTools ¶
func RegisterTools(ctx context.Context, home, workspace string, register func(toolshared.Tool)) ([]string, []Installed)
RegisterTools turns every capability of every installed module into an agent tool, and returns a one-line summary per module for the agent's context.
This is where "modules contribute tools" becomes real. The agent gains the ability to call a capability without knowing what a module is; the host keeps every decision about authority. The discovered set is returned alongside the summaries so a caller can compose module knowledge later without describing every module a second time -- each describe is a subprocess, and a per-turn rediscovery would make selecting a module cost more than using it.
type Knowledge ¶
type Knowledge struct {
// Overlays are module-authored instruction documents, already verified.
Overlays []Document
// Skills are progressively loaded module knowledge, already verified.
Skills []Document
// Warnings record content that was declared but could not be used. They are
// surfaced rather than swallowed: a silently missing overlay means the agent
// behaves differently with no visible reason.
Warnings []string
}
Knowledge is the module content composed into one turn.
func LoadKnowledge ¶
LoadKnowledge composes the selected module's overlay and skills.
selectedModules is the user's explicit selection -- the connector gesture. When it is empty nothing is loaded, which is the point: an installed module costs one line of capability summary until someone asks for it.
requestedSkills narrows further. When a capability is in play the host passes the skill IDs that capability declares, so a turn loads only the knowledge that turn needs.
type KnowledgeTool ¶
type KnowledgeTool struct {
// contains filtered or unexported fields
}
KnowledgeTool exposes the existing digest-verified loader as an ordinary, read-only tool. A module selection is not required.
func (*KnowledgeTool) Description ¶
func (t *KnowledgeTool) Description() string
func (*KnowledgeTool) Execute ¶
func (t *KnowledgeTool) Execute(_ context.Context, args map[string]any) *toolshared.ToolResult
func (*KnowledgeTool) Name ¶
func (t *KnowledgeTool) Name() string
func (*KnowledgeTool) Parameters ¶
func (t *KnowledgeTool) Parameters() map[string]any
type V2 ¶
type V2 struct {
// Decision carries the pin outcome. Its Pin.Reason and Pin.Remedy explain a
// refusal to a person.
Decision modprotov2.Decision
// Conformance is the host's no-weakening verdict, and is only populated
// when the pin returned v2. There is nothing to check on a v1 module: its
// descriptor has no Operation layer to compare a capability against.
Conformance *HostConformance
}
V2 is a discovered module's v2 standing: which contract governs it, and whether it conforms.
Present on every Installed, including v1 modules and refusals, because the alternative -- a nil field meaning "v1, probably" -- makes three outcomes share one observable. A caller must be able to tell "no v2 declaration" from "declared something this host refuses" from "not evaluated at all".
func (V2) MayRelyOnV2 ¶
MayRelyOnV2 reports whether this module's v2 declarations may be acted upon.
BOTH conditions, and the conjunction is the point. Passing the pin means the module claims v2; conforming means its projection does not weaken what it claims. A module that passes the pin and fails conformance is MORE dangerous than a v1 module, not less: it has been admitted to the v2 path where the gate reads capability effects, while declaring capability effects weaker than its own Operations.