ComponentOp::GrantAffordance{subject, affordance} and RevokeAffordance
resolve beside GrantAuthority/RevokeAuthority: a retired or externally
controlled subject, or an unknown catalog entry, is refused (the new
Mismatch::GrantsOutsideControl); a grant already held or a revoke not held is
NoOperationEffect; a revoke that would leave a controlled subject with no
affordance at all is refused with the new Mismatch::WouldLeaveSubjectMute,
because derive_opportunities requires at least one. Applying either writes
state.affordance_grants directly, re-deciding the same three refusals against
committed state. confine_to_ground now refuses both, unconditionally, beside
Retire: a grant is authority over the catalog, not structure over ground, so
the owner (including the seed lane, the owner in Draft) and the play
authority are the only callers.
PATCH_TOOLS gains grant_affordance and revoke_affordance (Operate, subject
plus an affordance reference); catalog counts move to (7, 34, 43). Same
consumer.v6 / controller_work.v17 as Cuts 3 and 4.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ControllerAssignment::Retired is a new value of the existing controller
assignment map, not a second partition: id() and mode() return None for it
like a mirror, so derive_opportunities mints nothing and it is never an
audience once its position is gone. expected_caller becomes Option<CallerId>
(None only for Retired; a mirror still returns Some, the one caller that
could ever act for it) and both exact-caller comparison sites move to
Some(&caller).
ComponentOp::Retire{subject} resolves beside Relocate: refuses an already
retired subject with NoOperationEffect and a mirror with the new
Mismatch::RetiresAMirror. Applying it sets the assignment to Retired and
removes the subject's positions and affordance_grants entries, so a witness
at its former place never reaches it while its holdings, commitments, and
knowledge stay untouched. confine_to_ground refuses Retire unconditionally
for every confined author, the same unconditional shape require_ruler already
gave a Ruled fact and a Mint: the owner and the play authority are the two
callers left. SubjectSnapshot gains pub retired: bool, set in snapshot().
PATCH_TOOLS gains retire (Operate, one subject field); catalog counts move to
(7, 32, 41). Schema and controller_work row version are unchanged: this rides
Cut 3's consumer.v6 / controller_work.v17.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SystemCapability::Play is a new system capability, minted only by the new
PlayPort (mailbox.rs): unconfined, declares in Active with no answer, may
advance time, and is the sole author of FactStanding::Ruled and
ComponentOp::Mint (quantity with no evidence receipt). One new admission rule,
require_ruler, refuses a Ruled fact or a Mint from any other caller in both
reduce and apply_effect; the evidence gate on Canonical facts and Admit is
untouched for every author, Play included. controller_work rows bump to v17
and the consumer schema bumps to consumer.v6 in the same commit, since the
shape change and the version are one bump.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Run 7's seed authored six distinct people with memories, reads, and worded
promises, and every declare_subject carried an empty controller: the field
had no description and the tool result said only "recorded as a gap". The
controller variant now describes its two shapes, and a decode failure's
reason goes back to the model in the tool result. The SDK lowering renders a
later user turn into its transcript, so a refusal continuation reaches the
sidecar instead of faulting.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Plan step 12. PersonaMaterial is the ontology's thirteenth row, built:
values, voice, memories, and reads keyed by subject, set whole by
set_persona_material, refused for padded text or a doubled read, no-op when
identical, outside every scope digest. A commitment carries a statement of
what is promised, required at authoring; an affordance-made promise states
the action it came through. The Projector context carries material by label,
each promise's statement, and who else stands in the same place; the
Persona's identity is label, voice, and values. Schemas move to consumer.v3.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The first SDK-backed seeded runs. Run 1 refused both seed sessions: the
sidecar's schema conversion dropped every field description, `due` was an
undescribed integer against a clock the model never saw, and the subject
tool did not say a grant is required. Run 2 seeded six people in one round
and ticked three times.
A refusal used to reopen the session with an empty draft and a prompt that
named sites from a draft the model could no longer see. It now continues
the same conversation: `Refusal { after_round, mismatches }` on both
authoring checkpoints (controller_work.v12), the refusal as the next user
turn, and the draft rebuilt by one DraftFold shared by evaluator and oracle
from the calls after it. The harness takes the connector only when its
address is set, so an all-claude run needs none.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The sidecar registered a closed Zod schema, so the SDK judged arguments
before Rust ever saw them: an extra key was stripped, a missing required
property was refused with an MCP error, and the call never reached the
handler. The connector path decodes the same argument in Rust, where
`deny_unknown_fields` and required-field decoding turn a bad argument
into the gap the evaluator records.
`toolInputSchema` now builds a loose object whose every property
catches, so nothing is stripped and nothing is refused. The conversion
survives only to advertise: the in-process MCP server derives the schema
the model sees from this object, and that schema still carries every
property, type, bound, and `required` name the emitter produced. What is
deleted is the strictness that made it a second validator -- `.strict()`,
the closed-object requirement, and the every-property-is-required
requirement.
One case cannot be carried through as written: a value outside a
property's own type empties that property, so it reaches Rust as an
absent field rather than a wrong one. Rust still names the gap; the SDK
still does not answer the call.
The undispatched-tool_use protocol violation stays, because no argument
reaches it any more but a call never offered to the handler still does.
`resultFault` is the result-subtype mapping lifted out of `runQuery`, so
`error_max_turns` becoming an ordinary round and every other subtype
becoming its named fault are both asserted without a credential.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tests only. Per lane, an adversarial call sequence — decode failure, duplicate
terminal, call after terminal, unknown tool name, oversized argument — is
answered by the oracle with exactly the strings the evaluator recomputes for the
same transcript, and the transcripts the oracle refuses are the transcripts the
evaluator refuses. Identity is pinned across every request builder and one
checkpoint variant. The wire's closed enums, the fault disposition table, the
lowering gate table, the dispatch-order gate, the receipt's contents, and the
routing refusal each get a test.
Two recorded findings, characterized and not fixed:
- The emitted grammar's top-level object is not strict in Zod, so a property the
connector's `deny_unknown_fields` decoders record as a gap is silently
stripped and the call succeeds; and a missing or mistyped required property is
answered by the SDK itself, leaving a registered `tool_use` block with no
dispatch, which `assembleEvents` raises as `protocol_violation` and Rust maps
to an integrity violation. Both are proven against the real in-process MCP
server with no credential and no network.
- A lend refused by the caller-identity or expiry gate is never reclaimed: those
gates run before `take_oracle`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`sidecar/claude-sdk` is the repo's second TypeScript package, wired to the
root only through `npm --prefix`. It reads length-prefixed MessagePack
frames on stdin, runs one SDK query, forwards every tool call to Rust, and
returns Rust's answer to the model verbatim. It computes no tool result,
reads no credential, logs no prompt, argument, or result, and writes nothing
outside its own stdout.
The query is opened with a custom system prompt, `settingSources: []`,
`tools: []`, `strictMcpConfig`, `persistSession: false`, and `alwaysLoad` on
every tool, so no CLAUDE.md, settings file, output style, built-in tool, or
persisted transcript reaches a caller-derived prompt.
`schema.ts` converts exactly the grammar `tool_schema.rs` emits and throws on
anything else, so an emitter that outgrows it fails a test rather than
registering a looser tool. Two Rust tests publish the fixtures the sidecar
tests read — the frame bytes and every real `parameters_json` — so the halves
cannot drift apart silently.
Dependencies are exact-pinned: @anthropic-ai/claude-agent-sdk 0.3.261,
@msgpack/msgpack 3.1.3, zod 4.5.4, and dev typescript 5.9.3 with
@types/node 24.13.3. `engines.node` is the installed major.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`SdkInferencePort` runs one Claude Agent SDK query per round through a Node
sidecar and returns it as one `InferenceOutput`. It prepares through the
same `prepare_invocation` the connector port uses, so there is one identity
scheme and every `integrity_is_valid` variant is unchanged.
The sidecar computes no tool result. Every call is answered by
`ToolResultOracle`, which is the lane's own evaluator fold replayed over the
calls made so far — the same function that will recompute the string at
re-derivation. A lane lends its oracle keyed by the prepared request id
immediately before `infer`; the connector port ignores the lend.
Rust owns the transcript lowering, the receipt digest, and the fault
disposition. The sidecar names a closed set of reasons and cannot assert an
integrity violation, a request identity, or a tool result.
`open_inference` routes each configured model: a `GHOSTLIGHT_SDK_MODEL_PREFIX`
model reaches the SDK port, anything else the connector, and a model no
built backend claims fails at open rather than at its first tick. Ghostlight
reads no credential for this transport.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>