Skip to content

core.factory

core.factory

Zone A — the dynamic agent factory + base role library (BUILD-SPEC §9, §10).

Mints personalized agents on demand from base role templates, each framed by the Constitution (Invariant 6) and bounded by a hard scope ceiling: capability flows only from role.scope ∩ PRE_DECLARED_MAX, resolved at mint and enforced at dispatch via object-capability handles. Beyond-scope requests route to the human gate (ops.gate), never to a privileged agent. Honors docs/design-notes/skills-and-scope.md.

BASE_ROLES = {(r.name): r for r in _ROLES} module-attribute

PRE_DECLARED_MAX = frozenset({'run_python'}) module-attribute

AgentFactory dataclass

server = None class-attribute instance-attribute

tools = field(default_factory=ToolRegistry) class-attribute instance-attribute

gate = field(default_factory=HumanGate) class-attribute instance-attribute

roles = field(default_factory=(lambda: dict(BASE_ROLES))) class-attribute instance-attribute

agent_registry = None class-attribute instance-attribute

secrets = None class-attribute instance-attribute

attestor = None class-attribute instance-attribute

grant_roles = frozenset() class-attribute instance-attribute

token_ttl = '10m' class-attribute instance-attribute

mint(role_name, *, requested_tools=frozenset(), name=None, persist=False)

Mint an agent for role_name. If requested_tools reaches beyond the role's resolved scope ceiling, route to the human gate instead of minting (§10).

MintedAgent dataclass

name instance-attribute

role instance-attribute

scope instance-attribute

dispatcher instance-attribute

gate instance-attribute

server = None class-attribute instance-attribute

ephemeral = True class-attribute instance-attribute

token = field(default=None, repr=False) class-attribute instance-attribute

accessor = None class-attribute instance-attribute

build_context(task, *, history=None)

Constitution outermost (Invariant 6); the role nests inside; task last. The credential token is deliberately NOT here — the model advises, code reads secrets (read_secret).

grant(minted)

Bind an ephemeral scoped token (the supervisor-mints-at-dispatch path, §2 step 3). Holds the credential off the prompt; keeps only the non-secret accessor for the audit.

read_secret(name)

Read a scoped secret using this agent's grant (§2 step 4). CODE-ONLY: the orchestration around the agent calls this to fetch data the agent's REASONING then uses; the MODEL never calls it and never sees the token. get_secret presents the token to Vault, which enforces the role's policy and raises VaultPermissionDenied for an out-of-scope path — the agent learns nothing beyond 'denied' (§2 step 5). No grant at all => RuntimeError.

respond(task, *, history=None, judge=None, think=None)

Advisory path: generate + run the Constitution pre-return check (§4).

invoke(tool_id, args)

Action path: dispatch a tool the agent is scoped for. An out-of-scope id is unreachable in the dispatcher → route to the human gate and refuse (§10).

AgentRegistry dataclass

path instance-attribute

promote(name, role, scope, tier)

get(name)

list()

close()

PersistedAgent dataclass

name instance-attribute

role instance-attribute

scope instance-attribute

tier instance-attribute

created_at instance-attribute

RoleTemplate dataclass

name instance-attribute

prompt_fragment instance-attribute

default_tier = 'routine' class-attribute instance-attribute

scope = frozenset() class-attribute instance-attribute

skills = () class-attribute instance-attribute

ToolDispatcher dataclass

Holds ONLY the in-scope tool handles — the object-capability enforcement point.

scope property

can_invoke(tool_id)

invoke(tool_id, args)

ToolNotInScopeError

Bases: RuntimeError

A tool id was invoked that is not among the agent's in-scope handles (§10).

ToolRegistry dataclass

All registered tools, independent of any role/skill (skills bind tools by name).

register(spec)

get(tool_id)

ids()

ToolResult dataclass

tool_id instance-attribute

ok instance-attribute

data instance-attribute

ToolSpec dataclass

id instance-attribute

description instance-attribute

handler instance-attribute

sandboxed = False class-attribute instance-attribute

build_factory(config=None, *, broker=None)

Wire a factory against the real model server + default tool registry (run_python is available only if a sandbox broker is supplied). When [secrets] is enabled, also wire the credential-grant path: a backend (mint authority), the attestor (records accessors), and the grant_roles opt-in set — so a minted agent in a granted role carries an ephemeral token.

build_default_registry(broker=None)

The Phase-5 tool set. run_python runs code in the §11 sandbox (powerless, returns data). It is registered only if a broker is provided; with no broker there are no executable tools at all.

dispatcher_for(scope, registry)

Build a dispatcher holding handles for exactly the in-scope tools present in the registry — nothing else is reachable.

factory

Dynamic agent factory (BUILD-SPEC §10).

Mints a personalized agent on demand as nested frames, outermost-first (Invariant 6): Constitution → base role template → task then resolves the tool scope and binds a dispatcher holding ONLY the in-scope handles.

The scope ceiling is the whole safety story: a minted agent can never exceed its template's scope, never be granted scope beyond PRE_DECLARED_MAX, and always inherits the Constitution. A request for capability beyond that is routed to the human gate, never satisfied by minting a privileged agent (§10). Capability is checked twice, by two subsystems, at two times: at mint (scope resolution) and at dispatch (object-capability). The advisory path (respond) and the action path (invoke) are kept separate — model advises, code acts (Invariant 3).

MintedAgent dataclass

name instance-attribute
role instance-attribute
scope instance-attribute
dispatcher instance-attribute
gate instance-attribute
server = None class-attribute instance-attribute
ephemeral = True class-attribute instance-attribute
token = field(default=None, repr=False) class-attribute instance-attribute
accessor = None class-attribute instance-attribute
build_context(task, *, history=None)

Constitution outermost (Invariant 6); the role nests inside; task last. The credential token is deliberately NOT here — the model advises, code reads secrets (read_secret).

grant(minted)

Bind an ephemeral scoped token (the supervisor-mints-at-dispatch path, §2 step 3). Holds the credential off the prompt; keeps only the non-secret accessor for the audit.

read_secret(name)

Read a scoped secret using this agent's grant (§2 step 4). CODE-ONLY: the orchestration around the agent calls this to fetch data the agent's REASONING then uses; the MODEL never calls it and never sees the token. get_secret presents the token to Vault, which enforces the role's policy and raises VaultPermissionDenied for an out-of-scope path — the agent learns nothing beyond 'denied' (§2 step 5). No grant at all => RuntimeError.

respond(task, *, history=None, judge=None, think=None)

Advisory path: generate + run the Constitution pre-return check (§4).

invoke(tool_id, args)

Action path: dispatch a tool the agent is scoped for. An out-of-scope id is unreachable in the dispatcher → route to the human gate and refuse (§10).

AgentFactory dataclass

server = None class-attribute instance-attribute
tools = field(default_factory=ToolRegistry) class-attribute instance-attribute
gate = field(default_factory=HumanGate) class-attribute instance-attribute
roles = field(default_factory=(lambda: dict(BASE_ROLES))) class-attribute instance-attribute
agent_registry = None class-attribute instance-attribute
secrets = None class-attribute instance-attribute
attestor = None class-attribute instance-attribute
grant_roles = frozenset() class-attribute instance-attribute
token_ttl = '10m' class-attribute instance-attribute
mint(role_name, *, requested_tools=frozenset(), name=None, persist=False)

Mint an agent for role_name. If requested_tools reaches beyond the role's resolved scope ceiling, route to the human gate instead of minting (§10).

build_factory(config=None, *, broker=None)

Wire a factory against the real model server + default tool registry (run_python is available only if a sandbox broker is supplied). When [secrets] is enabled, also wire the credential-grant path: a backend (mint authority), the attestor (records accessors), and the grant_roles opt-in set — so a minted agent in a granted role carries an ephemeral token.

registry

Persisted minted-agent registry (BUILD-SPEC §8, §10).

Minted agents are ephemeral by default — spun up, used, discarded. The owner may PROMOTE a useful one to a persistent named agent recorded here (SQLite, the transactional store for the agent registry per CONVENTIONS). Phase 5 records identity + resolved scope; per-agent memory grows in later phases.

PersistedAgent dataclass

name instance-attribute
role instance-attribute
scope instance-attribute
tier instance-attribute
created_at instance-attribute

AgentRegistry dataclass

path instance-attribute
promote(name, role, scope, tier)
get(name)
list()
close()

roles

Base role library + the scope ceiling (BUILD-SPEC §9, §10; skills-and-scope).

A role template declares two SEPARATE things (never conflated): the instructional frame (prompt + default tier + instructional skills = context) and the capability ceiling (scope = the tool ids the role MAY use). Capability flows only from scope ∩ MAX, resolved at mint and enforced at dispatch — a skill can never widen it.

PRE_DECLARED_MAX = frozenset({'run_python'}) module-attribute

BASE_ROLES = {(r.name): r for r in _ROLES} module-attribute

RoleTemplate dataclass

name instance-attribute
prompt_fragment instance-attribute
default_tier = 'routine' class-attribute instance-attribute
scope = frozenset() class-attribute instance-attribute
skills = () class-attribute instance-attribute

tools

Tools — capability as object-capability handles (BUILD-SPEC §10, §11; skills-and-scope).

A tool is a narrow HANDLE an agent is given, not a flag it is granted (the telemetry store-layer precedent: "the wrong access is impossible, not discouraged"). A minted agent's dispatcher holds ONLY the handles for tools inside its resolved scope; a tool id that isn't there resolves to nothing → the call is refused and routed to the human gate. Out-of-scope is unreachable, not "checked then maybe allowed".

Tools are deterministic CODE (model advises, code acts — Invariant 3). A tool that runs code does so through the Phase-4 sandbox broker (powerless: no creds/network/vault — Invariant 4). The action path (the dispatcher) is deliberately separate from an agent's advisory respond().

ToolHandler = Callable[[dict[str, Any]], dict[str, Any]] module-attribute

ToolSpec dataclass

id instance-attribute
description instance-attribute
handler instance-attribute
sandboxed = False class-attribute instance-attribute

ToolResult dataclass

tool_id instance-attribute
ok instance-attribute
data instance-attribute

ToolNotInScopeError

Bases: RuntimeError

A tool id was invoked that is not among the agent's in-scope handles (§10).

ToolRegistry dataclass

All registered tools, independent of any role/skill (skills bind tools by name).

register(spec)
get(tool_id)
ids()

ToolDispatcher dataclass

Holds ONLY the in-scope tool handles — the object-capability enforcement point.

scope property
can_invoke(tool_id)
invoke(tool_id, args)

dispatcher_for(scope, registry)

Build a dispatcher holding handles for exactly the in-scope tools present in the registry — nothing else is reachable.

build_default_registry(broker=None)

The Phase-5 tool set. run_python runs code in the §11 sandbox (powerless, returns data). It is registered only if a broker is provided; with no broker there are no executable tools at all.