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.