Proposal: Member Tool Policy v1
Status: Proposed for Meerkat 0.8.26 Baseline: Meerkatb1f1a9458deb55d4398b894f9b06bdc8695ee832
Scope: Meerkat tool construction, mob desired state, runtime dispatch, public contracts, and downstream policy-provider composition
Decision
Meerkat should finish its existing per-member tool-policy path. It should not build a tool-specific authorization platform. This is the 0.8.26 design.v1 names the first member-tool policy contract,
not an earlier Meerkat release.
V1 introduces one durable MemberToolDeclaration with two deliberately
separate parts:
- Materialization intent says which Meerkat tool categories are composed
into this member’s executor. It answers whether
shell,schedule,workgraph,memory,mob,comms, image generation, or web search exist for the member. - Execution constraints say which calls may cross the final dispatcher boundary. They compose allow-lists, deny-lists, and consequence ceilings conjunctively. No constraint can erase another.
AgentIdentity, profile, comms binding, and session.
Changing tool capability must not require changing the member’s profile.
V1 also adds a small, bounded application-policy hook at the existing
outermost dispatch gate. The hook may only narrow the result of Meerkat’s
static constraints. It evaluates a validated in-memory snapshot and cannot
perform Python, RPC, network, filesystem, or other blocking IO on the dispatch
path. This slice is delivered first so applications can close a live fail-open
without waiting for desired-state convergence work.
V1 does not add ToolInvocationMachine, durable suspended tool execution, or
automatic execution after approval. Ordinary Meerkat and MCP tool calls are
stateless requests. If a call is interrupted, Meerkat does not resume it. If
an external effect may have happened, the outcome is uncertain and the agent
decides whether to call again.
If Meerkat later grows a complete authorization and approval platform, that
platform must govern generic actions rather than tools alone. It requires a
separate proposal.
1. Current truth
The motivating HomeCore proposal identifies a real product problem, but its original Meerkat baseline is stale.MobMemberSpecWire is intentionally a reduced create-if-absent shape.
ensure_member returns an existing member unchanged, and roster reconciliation
retains present identities. Adding one field to that wire type would improve
first-create parity but would not make an update reach an existing member.
The latent desired-state substrate is already better than that reduced wire
shape. IdentityIntentRecord -> DesiredMemberSpec -> DesiredMemberOverlay
already names a stable identity, intent revision, intent digest, tombstone,
session target, and resolved tool policy. V1 activates that substrate through
a public revisioned member-tool update. It does not invent a second member
store or authority.
2. The problems v1 solves
2.1 Capability changes are incorrectly coupled to profile changes
A mob profile participates in the durable member binding:MemberCommsName is
{mob_id}/{role}/{member}, and the role is the profile name used at
construction. An undeclared reassignment therefore changes comms identity and
correctly fails resume. Meerkat 0.8.25 added an explicit, one-shot
resume_from_role migration for a real administrative role change; that
authority does not turn role migration into a capability-update mechanism.
Applications have nevertheless used profile reassignment as a capability
update mechanism. That is the wrong operation. A profile is a role and
construction template, not a mutable bag of grants.
V1 makes member tool materialization and execution policy independently
mutable while profile and comms identity remain fixed. Giving an existing
member shell, or removing schedule, becomes a member-tool declaration
update. An actual role/profile change remains an identity migration and is
outside this proposal, using the existing explicit migration contract.
2.2 Current policy composition can widen by accident
Current profile construction selects oneToolAccessPolicy. In particular,
profile.tools.read_only = true replaces a member AllowList or DenyList
with ReadOnly.
That preserves the no-mutation ceiling, but it can widen a narrow allow-list
to every dispatcher-declared read-only tool. V1 composes both facts:
2.3 Consequence labels do not currently govern dispatch
HomeCore classifies tools into risk tiers and has an approval UX, but its callback dispatch path does not consult those facts. A high-risk label can therefore be true in configuration and irrelevant at execution. The immediate platform defect is not missing approval state. It is the lack of an attachable, authoritative narrowing check immediately before tool IO. V1 supplies that seam first. It does not require the rest of this proposal to land before applications can deny a risky call.2.4 Provider-native tools bypass the dispatcher
Provider-native web search, code execution, computer use, and similar server tools do not traverseAgentToolDispatcher. The existing execution gate
cannot govern them.
V1 makes that boundary explicit. A member governed by a restrictive member
policy must disable provider-native tool capabilities unless the provider
feature later supplies a typed enforcement seam with equivalent guarantees.
This is a declared capability loss, never an implicit runtime surprise.
3. Goals
V1 must provide:- Durable per-member tool materialization without changing profile or comms identity.
- Durable per-member execution constraints that survive restart, resume, and fresh respawn.
- Revisioned compare-and-set updates for existing members.
- One compiler for profile defaults, member declarations, parent delegation ceilings, and the existing session execution policy.
- Conjunctive constraint composition with deny-overrides-allow behavior.
- One outermost call gate shared by every dispatcher-backed surface.
- An early in-process application-policy hook that can narrow but never widen Meerkat’s result.
- A finite evaluation and member-convergence contract, with typed failure rather than silent hangs.
- Explicit operational behavior for provider unavailability and provider-native bypass.
- Exact migration parity against downstream effective behavior, including hardcoded filters.
- No automatic retry or resume of an ordinary tool call.
4. Non-goals
V1 does not:- Define household roles, tool assignments, risk tiers, approver routing, or product UI.
- Turn model visibility into an authorization boundary.
- Make profile reassignment a supported capability-change operation.
- Persist callback closures, clients, credentials, MCP transports, or other process-local implementation material.
- Invent durable state for an ordinary tool handler.
- Resume, replay, or automatically retry a call after process loss.
- Promise exactly-once external effects.
- Build a general policy language.
- Build a tool-specific approval or authorization platform.
- Allow a caller to supply the risk or consequence class that governs itself.
- Treat deployment activation identity as policy content identity.
5. Semantic facts and owners
The stable declaration survives respawn, so it belongs to desired member
identity. The live executor is disposable, so it is recomposed from that
declaration. Session metadata is a resume projection of the resolved
declaration, not a rival desired owner.
6. Public member-tool contract
6.1 Declaration shape
The public domain shape is conceptually:Option<T> must not make absence mean both inherit and unrestricted.
ToolCategoryOverride already carries the required Inherit, Enable, and
Disable distinction. An enabled category still needs its normal injected
runtime authority. For example, enabling mob operator tools without a valid
MobToolAuthorityContext fails typed. A category flag cannot mint operator
authority.
Callback materialization reuses the existing split in desired member material:
the exact name, description, and input schema are durable, while the executable
handler and callback scope remain process-local. Materialization fails typed if
the current host cannot bind every required callback definition. A persisted
definition never becomes an executable closure.
Unrestricted is explicit. DenyNames is intentionally open-world and may
admit a newly materialized name that is not denied. Callers that require a
closed set use AllowNames. V1 does not add a catalog snapshot machine to
make an explicitly open-world declaration look closed.
Constraints must contain at least one constraint. An empty vector is rejected
rather than becoming a second spelling of Unrestricted.
The member declaration binds the stable identity of the application policy,
not one content revision. The injected provider owns the active immutable
snapshot for each PolicyId. Every evaluation reports the exact revision and
digest it used as PolicyEvaluationProvenance. Snapshot replacement is an
atomic provider operation, so a routine policy edit does not require a CAS
update and rematerialization of every governed member. Missing policy identity,
an incoherent snapshot, or absent provenance fails closed.
This is an ownership distinction, not an availability shortcut. The member
declaration owns which policy governs the member. The provider owns the current
content of that policy. Static member constraints remain the durable ceiling,
so an application-policy update can never widen past Meerkat’s constraints.
The provider registry is host-scoped and injected through the normal
MobBuilder and AgentFactory composition path. Every governed executor on one
host shares that registry and its atomic snapshot pointer. Multi-host rollout is
not globally atomic: each host reports the revision and digest it actually
used, and a member refuses readiness on a host that lacks its exact provider and
policy binding. Cross-host rollout ordering belongs to the application’s
deployment authority, not to a hidden Meerkat consensus protocol.
ToolCategoryOverrides is an aggregate carrier, not a claim that all
categories share implementation semantics. Each field lowers through its
existing category owner. Some categories compose dispatchers, some govern
visibility, and provider-native categories require final request sanitization.
6.2 Constraint composition
One compiler resolves declarations in this order:- Profile category and callback defaults plus profile constraints.
- Administrative member declaration.
- Parent delegation ceiling.
- Existing session execution policy.
- Every
AllowNamesconstraint must contain the name. - Any matching
DenyNamesconstraint denies. - Every
ReadOnlyconstraint requires the owning dispatcher to declare the tool read-only. - Unknown mutation class fails a
ReadOnlyconstraint. - Application policy runs only after static constraints admit the call.
- Application policy may deny an admitted call but cannot admit a statically denied call.
Inherit is forbidden in resolved desired material and at the
dispatch seam.
6.3 Tool identity in v1
V1 retains Meerkat’s existing exactToolName selector contract. It does not
pretend that a name is a universal authorization resource identity.
Dispatcher composition already owns which live implementation wins a name.
HomeCore’s MCP names include their server namespace. Replacing a same-name
implementation remains governed by dispatcher catalog and collision rules.
A future cross-resource authorization platform will need typed resource
provenance and action identity. That requirement does not justify creating a
new durable tool-binding catalog for static per-member allow-lists in v1.
7. Updating an existing member
7.1 Revisioned apply
Expose one shared command across Rust, RPC, REST where applicable, and generated SDKs:IdentityIntentRecord.
It does not create a second member-policy row.
The MobHandle form obtains mob_id from the handle. Wire forms carry it
explicitly. AgentIdentity is never interpreted outside that mob scope.
request_id is an idempotency identity recorded in a new, typed
IdentityIntentMutationReceipt. It may share the existing identity store
transaction and physical receipt table, but it does not reuse
IdentityOperationReceipt: those receipts are actuator lost-ACK custody for
session creation, retirement, external binding, and initial delivery, not a
generic mutation ledger. The store atomically compares the desired revision,
writes the new sealed intent when admitted, and records the mutation outcome.
No crash may commit one without the other. Repeating the same request id and
payload returns the recorded result. Reusing it with different payload is a
typed conflict.
The response separates the idempotent mutation result from the live
convergence projection:
Committed { desired_revision }NoChange { desired_revision }RevisionConflict { expected, actual }RequestConflict { request_id }MemberAbsentInvalidDeclaration(reason)
MemberToolCommitOutcome. Repeating the same request id
and payload returns that recorded commit outcome plus a fresh existing
IdentityConvergenceStatus; it never freezes an old convergence projection
into the mutation receipt. A successful CAS remains Committed even if later
rematerialization fails. The convergence projection carries the desired intent
revision and the fresh machine-derived identity reconciliation condition. The
active realization observation also carries the exact intent revision and
material digest from which the current executor was built, so callers can
distinguish desired, active, and divergent state without a parallel tool-only
status authority.
Drain.max_wait is required, non-zero, and bounded by the platform contract.
The existing identity reconciliation vocabulary gains the exact typed facts it
needs for an authorized material update: admission closed, draining, drain
deadline exceeded, replacement failed, and active intent revision. Deadline
expiry derives a specific DrainBlocked convergence condition while an active
turn or call still prevents retirement. New turns remain closed. The old
executor is not silently re-opened under stale policy. If the blocking turn
later ends, level-triggered reconciliation observes that the obstacle is gone
and continues automatically. An operator may instead use a focused identity
convergence command to continue draining under a new finite deadline or
explicitly choose CancelActive, always against the exact desired and active
revisions. Binding, construction, or persistence errors derive
RepairBlocked or the existing typed failure projection; they are not
mislabeled as deadline expiry.
That command is a separate idempotent ResolveIdentityConvergenceBlock
operation carrying its own request id, exact desired revision, observed active
revision, and IdentityConvergenceMode. Its name and authority are identity
generic even though member-tool mutation is its first caller. It changes no
desired declaration and feeds the same generated identity classifier.
The generated MobMachine identity reconciliation path owns the drain
deadline, admission closure, and replacement obligations. The existing
ClassifyIdentityReconciliation decision remains the one total classifier;
member-tool mutation extends its typed observations and decisions rather than
creating a second tool-policy reconciler. Shell code supplies trusted time and
realization observations and realizes retire or cancel effects; it does not
infer convergence from a local timer, waiter, or mutation receipt.
7.2 Convergence by rematerialization
V1 does not hot-edit an arbitrary running dispatcher. It uses the existing identity lifecycle to replace disposable execution material:- Validate the candidate declaration and preflight the current target host’s callback, category, and provider bindings without changing desired or active state.
- Atomically compare-and-set the sealed
IdentityIntentRecordand itsIdentityIntentMutationReceipt. - Observe desired intent revision or material digest diverging from the active
realization and let
MobMachinestop admission of new turns for that member before any fallible replacement work. - Let the active turn drain until its declared deadline, or explicitly cancel it when the caller requested an emergency revocation.
- Retire the old runtime binding.
- Rebuild through
AgentFactoryfrom the latest desired member intent. - Resume the same session under the same
AgentIdentity, profile, member binding, and comms name with a new runtime incarnation and fence. - Report convergence only after the new executor is active.
tool_access_policy bit. A generic
ResumeOverrideField::Tooling or ResumeOverrideField::Tools is not added
because it would collapse
materialization, visibility, execution constraints, and consequence policy.
All respawn, revival, ensure, reconcile, helper, flow-provisioning, and placed
member paths must lower through the same desired-member compiler. A
process-local spawn customizer cannot replace the durable declaration.
7.3 Revocation semantics
A policy update does not retroactively stop IO already admitted by the old executor. The old runtime stops accepting new turns before replacement. A normal update drains the current turn. An emergency update explicitly cancels it and accepts the ordinary uncertainty rules for any external call already in flight. A normal update does not wait forever. If its drain deadline expires, the desired revision remains committed, convergence becomes the machine-derivedDrainBlocked condition, and new turns stay closed until
convergence is resumed or cancellation is explicitly authorized. A timeout
never silently turns a normal update into force-cancel.
The new policy is effective when the replacement executor becomes active.
Until then, the existing IdentityConvergenceStatus reports the exact pending
or divergent identity condition rather than falsely reporting convergence. A
rematerialization failure leaves the desired revision committed and reports
the typed repair-blocking condition with the last active revision.
This is deliberately weaker than instantaneous mid-call revocation and much
simpler than a per-invocation transaction machine. V1 has no requirement that
justifies the latter.
8. The early application narrowing seam
8.1 Contract
Before the desired-state work lands, add an optional provider to the existing outermost execution gate:MobMemberBinding, exact tool name, the gate’s immutable ToolCallView, run
and tool-call correlation, and the exact provider id and policy id. Member
identity is optional only for an explicitly Unmanaged
standalone session, in which case this provider is not called. It does not
carry a caller-authored risk tier as authority.
The hook runs in process after static constraints and immediately before the
inner dispatcher. snapshot is an atomic, non-blocking read of the provider’s
current immutable snapshot. The gate captures that snapshot, its provenance,
and its provider generation once for the call. evaluate is contractually a
pure, non-blocking operation over those captured bytes. The gate constructs an
owned, closed ToolConsequenceRequest before supervisor admission; worker code
never retains a borrow into a live dispatcher stack or a mutable call object.
That contract is still defended mechanically. A host-scoped
PolicyEvaluationSupervisor owns a fixed number of provider partitions, each
with a fixed worker set and bounded admission queue. The provider registry and
partition assignment are bounded at host construction, so one provider cannot
consume another provider’s reserved evaluation capacity. Evaluation uses
those workers with a finite deadline; it never creates one spawn_blocking
task or one operating-system thread per call.
The deadline can return even when one evaluator implementation never returns.
A wedged evaluator may permanently consume its provider partition’s worker
until process restart, but the fixed partition and queue capacities bound that
loss. A mechanical timeout marks that provider partition unhealthy for the
rest of the process; snapshot replacement cannot pretend to reclaim a stuck
thread. A panic is caught at the same boundary, reported typed, and marks that
partition unhealthy. Once a provider generation is semantically unhealthy,
mechanically unhealthy, or saturated, later governed calls fail fast as
policy_indeterminate instead of allocating more tasks or threads. Installing
a newly validated snapshot creates a new provider generation and may recover
a semantic snapshot failure only while its mechanical partition remains
healthy. Any late result from an old generation is fenced and cannot affect the
new generation’s health or verdicts.
Deny produces an ordinary policy_denied tool result and lets the run
continue. Indeterminate, including deadline expiry, produces a typed
run-terminal policy_indeterminate condition. Both perform zero
inner-dispatch calls. They remain distinct because one is a policy verdict the
agent may reason around and the other is an authority or mechanism failure that
the model must not retry within the same run.
The dispatcher contract remains Result<ToolDispatchOutcome, ToolError>.
V1 adds distinct closed errors for policy denial and policy indeterminacy.
Model-driven collection must special-case ToolError::PolicyIndeterminate
before the existing generic conversion of dispatcher errors into ordinary tool
results, lower it to AgentError::PolicyIndeterminate, and route it through
the existing canonical fatal run-terminalization path. Direct external
dispatch returns that same typed fatal error without inventing a synthetic run.
ToolError::PolicyDenied continues through the ordinary tool-result path. No
surface may flatten the indeterminate variant back into text and feed it to the
model.
Each denial or indeterminate outcome emits a typed observation carrying member,
provider and policy identity, snapshot provenance when available, tool name,
arguments digest, run id, and tool-call id. This is a rebuildable operational
projection, not an execution receipt or permission. It lets a host detect
repeated identical denials without adding a policy-specific retry authority.
The provider may consult an immutable application-owned policy snapshot. It
may reuse an existing product gating engine. It cannot:
- Widen the static Meerkat result.
- Change tool visibility.
- Mutate member desired state.
- Invent approval lifecycle state.
- Suspend or resume a tool handler.
- Trust consequence or risk supplied in tool arguments.
- Perform IO or call back into a Python or remote application host.
f596e90f correction makes a configured risk tier override a
conflicting caller tier for known actions, but it is not the enforcement seam:
an action absent from that table still retains the caller’s tier. The v1
provider ignores caller-authored tier data entirely and classifies every
governed call from its captured snapshot, with unknown actions failing closed.
This seam closes the dispatch fail-open without claiming to be a complete
authorization system.
MobKit may reuse mechanics from its existing access engine when compiling the
gateway’s immutable snapshot, but it cannot simply rebind that engine and call
the result done. The current access model treats authenticated operators as
principals, agents as resources, disabled enforcement as allow, and configured
admins as unconditional bypasses. A member-to-tool consequence policy needs a
closed tool action/resource vocabulary, default deny, and no disabled or admin
escape. That is provider composition, not a second member-policy owner. MobKit
must not persist a rival per-member grant and stamp it into one spawn path.
8.2 Compilation input and vocabulary ownership
Phase 0 consumes one closed, typedCompiledApplicationToolPolicy artifact.
It contains the provider id, policy id, monotonic policy revision, canonical
policy digest, closed action and resource vocabulary, resolved consequence
classifications, and default-deny behavior required by the evaluator. The
digest covers canonical compiled bytes, not a source file path or deployment
activation id. A source-bundle digest may be retained separately as
provenance.
The product host owns source policy semantics and vocabulary mapping. For the
first HomeCore adoption, the source of truth is its immutable
shared/data/policy/bundle.N bundle containing policy.json and
compiled-gating.toml, addressed as hc-policy-v1:sha256:<digest>, or a
versioned successor with the same ownership properties. HomeCore’s compiler
must map that product vocabulary into the closed typed artifact and reject
unknown actions, ambiguous resource identities, duplicate conflicting rules,
and non-canonical content.
MobKit owns strict parsing and mechanical lowering of that compiled artifact
into the Meerkat snapshot contract. It must validate the digest and schema, but
it cannot infer missing grants, reinterpret HomeCore vocabulary, or retain a
rival policy store. Meerkat owns the snapshot contract, provider-generation
fencing, and enforcement. This division names the Phase 0 starting artifact
without making HomeCore’s current file format a Meerkat platform contract.
8.3 Availability
Fail-closed consequence policy is correct, but a network dependency on every tool call would be an avoidable household-wide availability hazard. Meerkat’s provider contract requires:- An in-process evaluator bound to the exact provider id and policy id.
- A validated immutable policy snapshot whose revision and digest are reported on every evaluation.
- Binding validation at construction before a governed member becomes ready.
- Typed
policy_indeterminatewhen the required binding is absent, inconsistent, exceeds its evaluation deadline, or fails evaluation. - A run-terminal indeterminate path so the model cannot turn provider failure into an in-run retry or route-around loop.
- Failure scoped to calls bound to that provider, not unrelated members or ungoverned tool paths.
8.4 Provider-native capabilities
The narrowing provider only coversAgentToolDispatcher calls. V1 compiles a
narrow-only SessionLlmRequestPolicy from the resolved member declaration. The
typed provider-parameter owner applies it at the end of
Agent::prepare_calling_llm_request, after effective provider parameters,
defaults, pre-LLM hooks, extraction, model fallback, and retry composition have
all run, on every attempt. The sanitizer disables provider-native capabilities
unless the provider feature supplies an equivalent typed enforcement seam.
Activation certification is useful evidence, but it is not the enforcement
boundary. Resume, fallback, retry, or an explicit request parameter must not
re-enable a native tool after certification.
The sanitizer composes with existing feature materialization. It does not
duplicate or replace MobKit’s image-generation machine gate, Meerkat category
flags, or model capability checks. Those owners decide whether the feature
exists; the final sanitizer can only remove an otherwise available
provider-native capability from the effective request.
Downstream adoption reports the exact lost capabilities. In HomeCore this
includes profiles where web_search = inherit currently enables native search
for gpt-5.6-sol. Certification must surface that change before activation.
9. Stateless calls, approval, and recovery
9.1 Ordinary calls are not resumed
An ordinary tool call is one stateless request to a dispatcher. Meerkat does not persist or resume the handler’s internal execution. If Meerkat crashes before dispatch, no effect occurred. If it crashes after the external effect may have occurred but before a result was committed, the outcome is uncertain. Existing durable-tail recovery holds a danglingtool_use as ambiguous and never synthesizes a result or replays the call.
The agent decides whether to call again using available evidence, tool
semantics, and any idempotency key the external system supports. Meerkat does
not make that decision on the agent’s behalf.
Streaming calls terminate with their process unless their feature protocol
exposes an explicit continuation handle. Detached tools may return a durable
operation or job handle; the operation’s owning authority resumes the
operation. The original tool call remains stateless.
9.2 Approval does not resume execution
ApprovalLifecycleMachine remains the only owner of approval status. An
approval proves a decision, not execution.
V1 does not automatically continue an approved tool call. The v1 narrowing
provider has no RequireApproval verdict. An approval-tier call that lacks
separate authorization is denied. The application may create and route an
approval request through its existing product workflow, and the agent may
later observe the decision and decide whether to submit a fresh call. That
fresh call is evaluated against current policy and current arguments.
Standardizing when an approval authorizes that fresh call, whether it is
one-shot, and how it is consumed belongs to the generic authorization design.
V1 does not hide those semantics inside the tool hook.
9.3 A complete authorization platform is a separate design
If Meerkat standardizes approval-backed execution, it must be a separate cross-resource authorization design with tools as one feature-owned adapter. It must reuseApprovalLifecycleMachine rather than create a second approval
owner in MobKit. This v1 deliberately does not predeclare that platform’s
types, state machine, permit model, or resource taxonomy.
None of those concerns is required to make static member tool policy durable
or to install a narrowing check at dispatch.
10. HomeCore adoption
HomeCore motivates the proposal, but HomeCore configuration is not Meerkat architecture. Adoption proves the platform contract without importing household policy into Meerkat.10.1 Effective baseline
The migration baseline is the final effective result oftools_for_profile(profile, domain), including every post-resolution
restriction. It is not only plugin manifests.
The baseline must include the effects of:
_DISCOVERY_ALLOWED_TOOL_SUFFIXES_TRIAGE_ALLOWED_TOOL_SUFFIXES_LIVE_GOOGLE_READ_SUFFIXES_AGENT_FORBIDDEN_DIRECT_MEMORY_MUTATION_SUFFIXES- the matching semantic-validation logic
- Discovery and triage tool sets become exact member callback-materialization sets plus matching execution constraints.
- The live Google read surface remains a live-feature declaration. It is not relabeled as stable member policy merely because both contain tool names.
- The direct-memory-mutation prohibition remains a named HomeCore-wide invariant, but it compiles into the application narrowing policy. It must not survive as a bypassable post-dispatch filter.
DenyNames snapshot and then falsely presented as future-proof. The migration
gate proves equality for the current catalog; a separate late-discovery test
must prove that a newly introduced tool matching the forbidden suffix is still
denied. Discovery and triage use closed exact allow sets, so newly introduced
names remain excluded without depending on a deny enumeration.
The required gate is an empty, bidirectional, name-by-name before/after diff for
all 17 members, computed separately for model-visible and executable tools.
The live-feature surface has its own equivalent diff. Counts are diagnostics,
not acceptance. In particular, migration must preserve the measured
containment that otherwise changes domain:discovery from 25 to 44 tools and
triage:main from 15 to 26.
10.2 Profile and category migration
HomeCore stops changing profiles merely to change tool capability. Existing members keep their current profile and comms identity. Member materialization declarations carry category differences. Thedomain:home-automation shell case is therefore in scope: an administrative
member declaration may enable shell while the member remains on its existing
profile. The usual shell dispatcher and authority requirements still apply,
and execution constraints remain the final backstop.
If HomeCore actually needs a new role, provider/model template, or comms role,
that is not a tool-policy update. It requires a new identity or a separately
designed identity migration.
10.3 Reversible adoption
Adoption uses a separate candidate activation:- Compile HomeCore’s immutable source bundle into member declarations and the
closed typed
CompiledApplicationToolPolicyartifact. - Validate the active policy snapshot revision and digest plus every required provider binding.
- Dry-run reconciliation against all current identities and report every difference without writing desired state, parking a member, or quarantining an identity.
- Run the 17-member effective-tool parity census.
- Report provider-native capabilities that will be disabled.
- Certify the candidate only when all checks pass.
- Before fleet rollout, apply and then compensate one complete revisioned update on a disposable identity or an explicitly approved canary. Verify exact restoration of desired and active material, session continuity, durable cursor position, and no replay of already-consumed work.
- For every realized member that predates durable identity intent, submit one
full identity declaration through
mob/adopt_member_identity_declaration. The operation requires an explicitexpected_absentprecondition, the exact existing session lineage, complete desired member material, explicit wiring custody, canonical owned wiring when identity custody is selected, declaration provenance, and a convergence mode. Externally managed custody is the default and requires an empty member-owned edge set; it leaves mob-level topology under its existing sole owner. The operation never infers desired state from the roster or transfers topology custody as a migration side effect. Replaying the same request is idempotent; an existing intent row is a typed precondition conflict and is never overwritten. - Roll out member declarations with the revisioned per-member command and verify convergence after each bounded batch.
PolicyId identifies the stable governed policy;
PolicyRevision and PolicyDigest identify the exact content installed in the
provider snapshot. Changed content must advance revision and digest even when
deployment metadata was copied.
11. Delivery sequence
Phase 0: Close the live consequence fail-open
- Add
ToolConsequenceNarrowingPolicyto the outermost dispatcher gate. - Bind an exact in-process provider and validated immutable snapshot at agent construction.
- Add the fixed-capacity
PolicyEvaluationSupervisor, bounded queue, generation fencing, deadline, and fail-fast saturation behavior. - Lower
ToolError::PolicyIndeterminatethrough the canonical fatal run-terminalization path before generic dispatcher-error conversion. - Ignore caller-authored risk tiers as authority.
- Preserve ordinary
policy_deniedand run-terminalpolicy_indeterminateas distinct results, both with zero inner calls. - Project typed provider failure for host-owned health and activation checks.
- Add no-bypass tests for callback, MCP, builtin, and host dispatchers.
- Add final-request provider-native sanitization for governed members.
- Define and validate the closed typed
CompiledApplicationToolPolicyartifact, canonical digest, and source-provenance contract. - Deliver the downstream MobKit gateway adapter that strictly parses and lowers HomeCore’s compiled artifact into this immutable in-process snapshot. HomeCore owns source vocabulary translation and must not satisfy the seam with a Python callback on each call.
Phase 1: Make static policy compositional
- Replace the single resolved-policy alternative with a normalized constraint set.
- Compile current
AllowList,DenyList, andReadOnlydeclarations into it. - Compose profile read-only intent with member and parent constraints instead of replacing them.
- Preserve ordinary access-denied tool-result behavior.
Phase 2: Activate member desired policy
- Add
MemberToolDeclarationto existing desired member material. - Expose compare-and-set apply and convergence status.
- Add an atomic
IdentityIntentMutationReceiptfor idempotent member-tool mutation results without changingIdentityOperationReceiptsemantics. - Extend the existing
ClassifyIdentityReconciliationobservations and decisions; do not add a parallel tool-only convergence authority. - Add per-category and exact callback materialization intent using existing desired material and typed overrides.
- Route respawn, revival, ensure, reconcile, helper, flow, and placed-member construction through the same compiler.
- Rematerialize an existing member without changing profile, comms name, session, or stable identity.
Phase 3: Contract and surface parity
- Generate wire schemas and Rust, Python, TypeScript, and Web SDK shapes.
- Expose declaration reads, apply outcomes, and convergence status.
- Return typed unsupported results on substrates that cannot supply member identity or a required materialization authority.
- Add schema, SDK, and surface parity ratchets.
- Ship the corresponding MobKit client/runtime release as a thin adapter over these Meerkat contracts. MobKit adds no member-policy store or mutation authority; this is new surface work, not only a dependency repin.
Phase 4: HomeCore migration
- Compile the measured effective allow sets, including hardcoded frozensets, into member declarations.
- Install the in-process consequence provider.
- Run dry-run adoption and the 17-member empty-diff gate.
- Adopt pre-existing realized members with the expected-absent full identity declaration before applying later member-tool revisions.
- Declare wiring custody explicitly. MobKit’s fleet-scoped managed topology remains externally managed; adoption must not copy those edges into per-member ownership or mistake non-empty physical topology for member-owned desired state.
- Report provider-native capability loss.
- Activate without profile reassignment.
- Remove legacy Python filters only after the shared gate proves parity.
Separate proposal: Generic governed-action authorization
Approval-backed permits, authenticated decision principals, exact action binding, and cross-resource authorization are not a later phase of this v1. They are a separate architectural decision with tools as one adapter.12. Acceptance gates
Materialization and identity
- Enabling
shellfor one existing member changes neitherAgentIdentity, profile,MobMemberBinding, comms name, nor session id. - The enabled category survives process restart and fresh respawn.
- Enabling a category without required injected authority fails typed.
- Attempting profile reassignment still refuses rather than silently changing identity.
Desired-state update
- Apply with the exact current intent revision commits desired state and then reports convergence independently.
- Repeating the same declaration is a no-op.
- Repeating the same request id and payload returns the recorded commit outcome plus a fresh convergence snapshot.
- Reusing a request id with different payload conflicts.
- A stale revision conflicts without changing desired or live state.
- A declaration or binding preflight failure leaves desired and active state unchanged.
- A crash after the atomic desired CAS but before live replacement is recovered by level-triggered desired-active mismatch on restart, without a client retry.
- A policy-driven rematerialization resume with a boot-scoped role-migration declaration still armed does not migrate the member and converges normally; equal stored and current roles leave the one-shot migration authority inert.
- A failed rematerialization reports desired and active revisions, preserving the fact that desired state committed while the new executor is not active; admission remains closed.
- A finite drain that exceeds its deadline becomes
the typed
DrainBlockedidentity condition, keeps new turns closed, and never silently force-cancels the active turn. - Resuming blocked convergence requires an idempotent command fenced by exact desired and active revisions; it cannot mutate desired policy implicitly.
- Every member construction and recovery ingress uses the same desired compiler.
Constraint composition
AllowNames({a}) AND ReadOnlyadmitsaonly whenais declared read-only.- Profile read-only intent never widens a member allow-list.
- Parent and existing session constraints can only narrow.
DenyNameswins over every matching allow.Constraints([])is rejected rather than treated as unrestricted.- Unresolved
Inheritfails before persistence and dispatch. - A denied call makes zero inner-dispatch calls and remains an ordinary tool error so the run can continue.
Application policy
- The provider receives trusted member and call context, not a caller-selected risk tier.
- A provider-bound member without an exact
MobMemberBindingis rejected at construction. - Policy identity includes both
PolicyProviderIdand provider-localPolicyId. - Updating the provider-owned policy revision and digest does not mutate or rematerialize every member declaration.
- A supplied
risk_tier = r0cannot weaken policy-derivedr3treatment. Denymakes zero inner calls and remains an ordinary tool result.Indeterminatemakes zero inner calls and terminalizes the current run with its distinct typed class.- Model-driven dispatch promotes
ToolError::PolicyIndeterminatebefore the generic tool-result conversion and reaches canonical fatal terminalization. External direct dispatch returns the same typed fatal error without creating a synthetic run. - A deliberately wedged evaluator reaches a finite deadline, marks that provider instance unhealthy, and cannot consume an unbounded number of runtime workers.
- Fixed per-provider evaluator workers and bounded queues cap stuck work and isolate providers. Saturation fails fast, a late old-generation verdict is fenced, semantic failure can recover through a valid new generation, and a mechanical timeout remains unhealthy until process restart.
- Evaluation performs no Python, RPC, network, filesystem, or other blocking IO and uses one validated immutable snapshot.
- Missing or inconsistent required provider binding returns
policy_indeterminateonly for calls governed by that provider. - The injected provider exposes typed failure that the host must include in health and activation certification.
- Deny and indeterminate observations carry enough typed correlation for the host to detect repeated identical attempts without becoming policy truth.
- Each no-bypass integration test has a paired allowed-call control over the same construction path that proves the sentinel inner dispatcher is reachable. The denied path is also demonstrated red under a gate-disabled test mutation, so a vacuous test cannot satisfy the ratchet.
- Final provider-request sanitization prevents resume, fallback, retry, or explicit parameters from re-enabling an ungoverned provider-native tool.
SessionLlmRequestPolicyis applied by the typed provider-parameter owner at the end ofAgent::prepare_calling_llm_requeston every attempt.- HomeCore’s compiler emits canonical closed artifacts deterministically; unknown actions, ambiguous resources, conflicting duplicates, non-canonical content, or digest mismatch fail before provider installation. MobKit performs strict parsing and lowering without semantic inference.
- Every compiler-rejection ratchet is mutation-proven: removing that exact validation makes its targeted negative test fail, while a valid-artifact control still installs through the same path.
Stateless execution and recovery
- No ordinary tool call creates durable invocation-machine state.
- Approval never automatically executes or resumes a call.
- A dangling
tool_useremains ambiguous and is not replayed. - After an uncertain result, the agent decides whether to submit a fresh call.
- Detached operations resume only through their own existing operation or job authority.
HomeCore migration
- Dry-run adoption performs no desired-state write and cannot park or quarantine an identity.
- Adoption defaults every current member to externally managed wiring custody, requires an empty member-owned edge set, and leaves the mob-level desired topology untouched.
- The before/after model-visible and executable-tool diffs are both empty for all 17 members, including all hardcoded frozenset effects. The live-feature surface has its own equivalent parity assertion.
- Diffs are bidirectional and name-by-name; tool counts are diagnostic only.
- A newly introduced tool matching HomeCore’s direct-memory-mutation suffix rule is denied even though it was absent from the migration catalog.
- Discovery remains at 25 tools and triage remains at 15 as diagnostics unless a separately reviewed policy change intentionally changes those sets.
- Intentional additions or removals are separate revisions after the parity activation; they cannot weaken the empty-diff adoption gate.
- A revisioned apply-and-compensate canary restores exact desired and active material without resetting durable cursors or replaying consumed work.
- The activation report lists native web-search loss before cutover.
- A failed candidate leaves the prior activation live.
13. Rejected alternatives
Add only tool_access_policy to MobMemberSpecWire
This affects first creation. It does not update an existing identity because
ensure and reconcile retain present members.
Add ResumeOverrideField::Tooling or ResumeOverrideField::Tools
One switch would collapse materialization, visibility, execution constraints,
and consequence policy. The member declaration drives the existing specific
override bits instead.
This variant is not present in the 0.8.25 baseline and must not be introduced
as a 0.8.26 shortcut. It would not solve HomeCore’s per-member capability case,
and shipping then removing a generated-SDK enum variant would create
compatibility debt for no architectural gain.
Pin application policy content in every member declaration
Pinning revision and digest besidePolicyId would turn each routine
application-policy edit into an N-member desired-state migration. The provider
owns its current immutable policy snapshot and reports its exact content
provenance on every evaluation. The member owns only which stable policy
identity governs it.
Treat Indeterminate as an ordinary retryable tool result
The agent is the consumer of ordinary tool errors. Showing an unavailable or
wedged policy mechanism to the model as a retryable result invites an in-run
retry loop. Indeterminate therefore terminalizes the current run while
Deny remains an ordinary tool result.
Add a policy-specific retry or denial machine
Denied calls perform no external IO and existing run limits bound agentic retries. V1 projects typed denial evidence so the host can detect repeated attempts, but it does not create a second retry authority beside the agent loop and its existing budgets.Change profile to change capability
Profile is the member’s durable role and part of its comms identity. Using it as a mutable grant bundle is the application error v1 removes.Keep tool-category materialization profile-only
That leaves per-membershell and other builtin-category differences
impossible without identity-changing profile reassignment. A separate typed
materialization declaration is the missing semantic level.
Let a Python post-filter remain the final backstop
Application policy may remain application-owned, but its verdict must execute through the shared outermost gate. A direct callback path that skips that gate is still a bypass.Build ToolInvocationMachine
Static allow-list lookup and an ordinary stateless request do not warrant a
durable per-call machine. External uncertainty is handled by existing durable
tail semantics and an agent decision, not automatic replay.
Automatically resume after approval
Approval is a decision, not execution authority. The agent decides whether to make a new call. A future generic authorization platform may define a one-action permit, but it must not masquerade as resumed tool state.Extend the tool seam into a complete authorization platform
Memory writes, comms sends, schedules, devices, WorkGraph mutations, files, network calls, and tools share the deeper problem. A full platform must be resource-generic and gets its own proposal.Fail open when the policy provider is unavailable
Availability does not authorize a risky effect. Keep evaluation in process, require an exact provider binding, and fail closed only at the affected call scope. The injected provider and host own snapshot rollout and health; Meerkat must preserve their typed failure rather than silently widening.Use activation id as policy revision
Deployment identity is useful provenance but does not prove policy content. Canonical content digest and monotonic policy revision remain the policy identity.Quarantine on adoption dry-run disagreement
Candidate disagreement should fail certification before live desired state is changed. After adoption, a real semantic disagreement still fails closed.14. Dogma check
- Authority is singular: desired member tooling lives in the existing identity intent; live executors and session metadata are derived.
- Generated machines own canonical change:
MobMachineowns convergence; shell code only rematerializes the authorized executor. - Shells and projections are mechanical: roster, SDK, health, and audit views report policy but cannot grant it.
- Truth is typed: categories, inheritance, unrestricted access, constraints, provider binding, failures, and convergence are distinct types.
- Composability is feature-owned: applications own product rules; Meerkat owns the shared enforcement seam.
- Surfaces are thin: every surface lowers into the same apply and dispatch paths.
- Policy stays behind its seam: caller-supplied risk never outranks the provider’s resolved policy.
- Terminality is explicit: denied, indeterminate, convergence-failed, and uncertain are not success.
- Contracts ratchet: generated schemas, SDKs, and parity tests cover the production paths they describe.
