Changelog
All notable changes to SharedOS are recorded here. The packages share one
version and are published together under npm's next dist-tag.
SharedOS is a 1.0.0 preview: contracts may change between previews, and
each entry calls out what a host has to update.
Unreleased
Changed
-
The repository is
systemind-team/SharedOS. It moved fromAicoo-Team/SharedOS, and old URLs redirect. Each package'srepositoryfield, the release workflow's repository guard and the documentation links name the new address. The API reference's source links are now built fromsourceLinkTemplateintypedoc.jsonrather than from the checkout's git remote, sopnpm docs:api:checkgives the same answer in a fork, a mirror and CI. Nothing a host imports changes. Before the next release is tagged, each package's npm trusted publisher has to name the new address (docs/npm-release.md). -
Protocol
"1"is the1.0.0-previewshape, and it is frozen there.0.1.0-alpha.4added optional fields to.strict()schemas without moving the version, and ADR 0019 deferred the move to a later release. The deferral is closed without one."1"names what1.0.0-previewships; the0.1.0-alphabuilds are not supported peers of it, so a consumer still on one upgrades. From here a change a1.0.0-previewreader would reject moves the version in the same change. No object is re-stamped, the routes stay under/v1, and existing records stay readable. ADR 0019 is revised and theProtocolVersionSchemarow indocs/open-items.mdis closed.
Fixed
- Two executions that share an actor, purpose and trace no longer share a
turn. A turn's authority lease was keyed on namespace, actor, authority,
owner, purpose and trace, so a second execution that agreed on all six while
the first was still open was answered from the first's lease: authority
loaded before it began, no
authority.resolvedof its own, the first's catalogue.AccessContextgains an optionalexecutionIdthat is part of the key, andSharedOSExecutorcopies the request's onto the turn's context. A run submitted under an execution id that is still running is now refuseddeniedwith the new codeexecution_in_progress, where it used to share the running turn's authority and do its work a second time; wait for the original or use a new id. A finished id can still run again. Two opens of one context with no id that raced also overwrote each other's lease, so the first to close removed the authority the second still held; the later one now joins. Nothing on the wire changes: the HTTP schemas omit the context. ADR 0010 is revised. - Reading a Claude Code or Pi tool call no longer pays for a failed parse.
The content-block walk the vendor codecs share recognised prose by running
TextBlockSchema.safeParseon every block, where each codec had comparedblock.typebefore the walk was shared. A call block fails that parse, and the failure cost more than the rest of the walk: parse-and-translate read 17.5 µs per call for Claude Code and 17.9 µs for Pi at1.0.0-preview, against 12.9 and 12.2 µs at0.1.0-alpha.5. The walk compares again and they read 11.9 and 12.4 µs. The same blocks are read as prose and as calls; DeepSeek, whose calls arrive in a frame of their own, and Codex, which does not use the walk, never paid it.
1.0.0-preview
Changed — breaking
-
The standard loop is
createStandardRuntime({ driver }), and the names say what each thing is. "Standard" names the SharedOS-owned default at each layer.StandardRuntimeread as a runtime with a model in it and was the loop;ModelRuntimeandHarnessRuntimewere that loop again, wrapped only to report the driver's manifest in place ofsharedos.standard. There is now one factory. It seats oneAgentTurnDriverand reports that driver's manifest, so a record names what sat in the seat; a driver that states none (the new optionalAgentTurnDriver.manifest) is reported assharedos.standard. The loop class is private.ModelDriverisStandardTurnDriver, the SharedOS driver that puts a model API in the seat;HarnessDriverisEvalHarnessDriver, which seats a vendor's wire format for evaluation and nothing else. No name has a deprecated alias. Manifest ids are unchanged, so no record and no hash moves. ADR 0007 is rewritten in these names and takes in ADR 0027, which is withdrawn.Migration.
Before After new StandardRuntime(driver, options)createStandardRuntime({ driver, ...options })new ModelRuntime(new ModelDriver(o), options)createStandardRuntime({ driver: new StandardTurnDriver(o), ...options })new HarnessRuntime(new HarnessDriver(o))createStandardRuntime({ driver: new EvalHarnessDriver(o) })new DriverRuntime(driver)createStandardRuntime({ driver })createCodexRuntime({ transport }), and the three like itcreateMcpHarnessRuntime(CODEX_MCP_HARNESS)in a product; in an evaluation,createStandardRuntime({ driver: new EvalHarnessDriver({ transport, manifest: CODEX_VENDOR.manifest, protocol: CODEX_VENDOR.protocol }) })ModelDriverOptions,HarnessDriverOptionsStandardTurnDriverOptions,EvalHarnessDriverOptionsStandardRuntimeOptionsas a second argumentThe same fields beside the required driver -
What the kernel states on an audit event is a field, not a
metadatakey.source,cause,failClosed,consumedandendedByareAuditEventfields. ADR 0023 first putsourceandcauseinmetadata, besidefailClosedandconsumed, so the event kept its top-level shape; ADR 0020 then recorded a decision's own metadata in the same object, where a host ceiling'sfailClosed: truecould stand on any denial the kernel had not marked, and the kernel carried a function to strip it.metadatanow holds what a host port supplied and the details particular to one event type (grantIds,catalogHash,withheldCount, the authorizer's account of a denial, an escalation'sdetail), and nothing a port writes can reach a field. ADR 0023 is revised in place.Migration. Read
event.source,event.cause,event.failClosed,event.consumedandevent.endedBywhere you read themetadatakey of the same name. A host that persists audit under a closed schema adds five optional fields;AuditEventSchemain@aicoo/sharedos-contractsis the shape, and the kernel'sAuditEventtype is inferred from it.versionstays"1"and no release writes both. A report spanning old and new events filters on both for as long as the old ones are in its window. -
turn.endedcarries nosource. It saidenvelopeon every turn ending, because the envelope records every turn ending: who recorded, not who refused, and a second meaning for the field. Who ended a failed turn isendedBy.classifyRefusalno longer needs to check the event type before readingsource. -
RefusalExplanation.sourceis typedAuditSource | undefined, where it wasstring | undefined, and is read from the field. -
AuditOutcomegainsinterrupted. Five values become six. It is written ontool.invoked,resource.invokedandmessage.sentfor an operation whose port was entered and stopped before it answered: the effect may have committed in part or in whole.reasonisoperation_abortedoraudit_unavailable.Migration. A host that persists audit under a closed schema adds the value;
AuditOutcomeSchemain@aicoo/sharedos-contractsis the list. A report that counts failures decides whereinterruptedbelongs, and it is not with the refusals. Nothing that was recorded before is recorded differently: these operations used to leave no event at all. -
A sink that throws on a record written before an effect rejects with
AuditUnavailableError, where the kernel re-threw whatever the sink threw. The sink's error is itscause,codeisaudit_unavailable, andeffectsays whether the operation's port had been entered. A caller that matched on the sink's own error type or message readserror.cause. -
retryableon a turn's ending says whether running it again repeats anything.turn_cancelledandruntime_failedsaidtruewhatever the turn had done, and a plugin's own failure said what the plugin said, which for the harness adapters istrueon any harness failure. A host that followed the flag after a turn had sent a payment sent it twice. The envelope now decides it forturn_cancelled,runtime_failedandaudit_unavailable, and caps a plugin'strue: it isfalseonce a call to awritetool not declaredidempotentcame back anything butdenied, or was still with the kernel when the turn ended. Calls toreadtools never count, so a turn that only searched and ran out of time is still retried. ADR 0007 is revised in place.Migration. A host that retried every
turn_cancelledorruntime_failedwill retry fewer; that is the fix. Check your tool definitions, because the rule rests on them:readWrite: "read"only where a second run changes nothing,annotations.idempotentonly where a second run is a no-op. -
TurnKernelrequires its four turn ports.openTurnAuthority,recordEscalation,recordTurnEndandrecordRefusedCallwere optional members, so a partial kernel could run a turn that re-read its authority on every call and recorded none of what the envelope decided. They are required, and the executor no longer branches on their absence. ADR 0023 is revised in place.Migration. A host that passes a
SharedOSKernelchanges nothing. A host or test that handsSharedOSExecutorits own narrow kernel implements the four: a lease whoseclosemay do nothing, and three recorders that may. -
The conformance record carries a refusal's cause and an
interruptedoutcome, and the judge is version 6.OperationRecordgains an optionalcause, copied from the audit event, andoutcomegainsinterrupted, where the assembler used to writefailed. A reader that switches on the three outcomes has a fourth to handle; a receipt still reportsfailed, which is what the caller saw, and the judge credits no boundary with an interrupted call. The cause was recovered before by joining a call's sibling operations by id, which only ever found a refused dispatch's code. Read off the operation, every kerneltool_unavailablenames its situation: 42 committed cells gaincause not_offered.execution.endedByis new and optional. Two rows are added,audit_unavailableandturn_draining, so the manifest has 34 rows andcaseSetHashandworldSetHashmove: a live column recorded against the old pair is no longer comparable with the scripted columns by hash.experiment.specHashignores descriptions, ascaseSetHashdoes, and moves once on every record. ADRs 0014 and 0023 are revised in place.Migration. Regenerate any stored manifest. Code that called
operationsUnder(record, id).causereadscauseon thetooloperation under that id.
Fixed
-
A harness probe reads the version a CLI answered with, not the first thing it printed.
probeHarnessread stdout and stderr as one text and took the first line, so Codex -- which warns on stderr whenCODEX_HOMEis under a temporary directory before it answers on stdout -- was recorded with its warning asversionOutputand noversion, and its build in a conformance artifact was whatever the runbook said had been installed. The streams are now read apart, stdout first, and a line counts only if the whole of it is a version answer (codex-cli 0.149.0,2.1.278 (Claude Code),v22.14.0): a notice that a newer build exists is a sentence, or names two versions, and is never taken for the build that ran. Lines that answer with different builds record none. Still fails open: no readable answer leavesversionabsent,versionOutputthe first line said, and the harness available. -
A deadline no longer has to stop a handler half-way. A
transfer_fundsbetween its debit and its credit when the deadline fired stopped there. A host may now setdrainGraceMsonSharedOSExecutor: the turn stops taking tool calls that long before its deadline (denied,turn_draining, recorded), handlers already running are not signalled and answer with their real outcome, and the abort reaches what is still running attimeoutMs, which is recordedinterrupted. The grace is inside the limit, never on top of it, and the turn ends as soon as what was in flight has answered; the standard loop asks the seat nothing more once it is draining. An audit outage drains the same way; a host's own cancellation still stops the turn at once. Zero, the default, is the behaviour before. ADR 0007 is revised in place. -
A tool call refused at discovery records why.
invokeToolchecks discovery before it authorizes, and a call refused there wrote anauthorization.checkedevent with the reason and nothing else: nograntsResolved, norejectedGrants, nomissingDependency. That is the path a missingusageStorealways takes for a tool whose only grant is bounded, so the wiring faultdocs/host-integration.mdsays is "diagnosable from the trail" was absent from the trail exactly where a host meets it. The kernel now asks for the account on that check, andcanDiscovertakes an optionalonExplain(DiscoverOptions) to hand it over.listToolsdoes not ask, so filtering a catalogue still records no account, and a discovery denial still carries norequiredAuthority(ADR 0019). The caller is told what it was told before. -
A session that opens after its turn was cancelled is closed. The standard loop stops waiting for
AgentTurnDriver.openwhen the turn is cancelled. A driver that does not honour its signal, or loses the race with it, handed back a session nothing held, soclosewas never called and whatever the session had taken stayed open. The loop now keeps the promise and closes a session that arrives late, with the turn's ending and undercloseTimeoutMs. The turn does not wait for it. A driver may therefore seecloseon a session that was never asked for a decision. -
A sink that hangs after an effect no longer holds the result. A sink that threw there was already handed to
onAuditError; one that never answered kept the result of a committed effect waiting, and a turn that reached its deadline first lost it.auditWriteTimeoutMson the kernel bounds the write: past itonAuditErrorreceives anAuditWriteTimeoutErrorand the caller its result. Records written before an effect are never limited. ADR 0023 is revised in place. -
An audit outage before an effect ends the turn
audit_unavailable, by the envelope. A sink that threw on an authority load, a decision or a catalogue listing rejected into the turn body, and the turn endedruntime_failed, "The runtime plugin failed" -- for an outage the plugin had no part in, and also when the outage was met before any plugin ran. A plugin that caught the rejection could call again, and be refused again. The executor now notes the typed error where it called the kernel itself, stops the turn taking anything new, and ends itfailed/audit_unavailablewithendedBy: envelopeandfailClosed; a plugin can neither carry on past it nor throw the error itself to be credited with the refusal.retryablefollows the rule above.onTurnErrorreceives the error. A record that fails after an effect still never ends a turn. ADR 0023 is revised in place. -
An operation stopped mid-effect leaves a record. A handler that did part of its work, saw the turn's deadline and threw left a trail ending at
authorization.checked: allowed: the abort was re-thrown ahead of the operation event, so a cancelled or timed-out turn could hide a debit. The event is now written first, asinterrupted. The abort is still re-thrown and still not reported toonProviderError; a port that answers despite the abort is recorded with its real outcome, and a call stopped before its port was entered writes nothing. The conformance record readsinterruptedasfailed, neverdenied. -
An outage is marked
failClosedon every operation event.docs/errors.mdtells a host to excludefailClosedrecords before computing a denial rate.tool.invokedcarried the flag;resource.invokedandmessage.sentwere built separately and never did, so a grant store that was down read as one outage and two deliberate refusals. One builder states the operation facts for all three. -
A turn whose audit sink is down still ends
escalated. A sink that threw onescalation.requestedrejectedrecordEscalationinside the executor'stry: the turn endedfailed/runtime_failed, the plugin was blamed, and the throw went toonTurnError. The record is the turn's terminal, not a gate, so it is written on the path that reports toonAuditErrorand leaves the ending alone. The rule it follows is stated and tested for the first time: a record written before an effect (an authority load, a decision, a catalogue listing) rejects the operation when the sink throws, and nothing runs; a record written after one never changes the answer. No behaviour moved for the first kind. -
A turn that never returns still records what it was asked.
promptHashrode on the turn's outcome metadata alone, and a turn cancelled at its deadline has no outcome: the plugin threw at the abort and the envelope built thecancelledresult from its own provenance. The same held for a turn the standard loop ended itself —step_limit_exceededcarries no driver metadata. Such a turn then dropped out of its column's prompt set, and the column's movedpromptSetHashread as a reworded prompt — the one confusion the hash exists to remove; the 2026-09-08 live run showed it, one stalled Claude Code turn folding 28 entries against the other columns' 29. A runtime now states the hash to the envelope through the newRuntimeHost.annotate, before the model or harness is sent anything, and the envelope writes it intoExecutionResult.metadataon every ending: completed, failed, escalated, cancelled, a plugin that threw, an outcome that did not parse (ADR 0007).createMcpHarnessRuntimestates it before binding its port or spawning the CLI; the standard loop states it onceopenhas resolved, before the first step, for anyAgentTurnSessionthat states apromptHash, whichStandardTurnDriver's session now does, in place of restating it on its terminal metadata. A turn cancelled before there is a hash to state -- inside a driver'sopen, or before the MCP runtime has composed its prompt -- still carries none.assembleExecutionRecordreads the metadata as before; a malformed value reads as absent, as a malformed catalogue hash does. What a host has to update: nothing; a driver that hands the seat text and wants its stalled turns identified statespromptHashon its session, hashed before itsopensends anything. The committedStandardprompt-set hash moves once,ece3b355…to4dbefcbd…, because thebudget-exceeded/step-ceilingturn now counts; the case-set and world-set hashes do not move.
Changed
-
A driven harness is told where its turn may operate, and its record says what it was told.
EvalHarnessDriverhanded its harness a prompt and nothing else, and its session stated nopromptHash, so the four vendor columns carried no prompt set. It now takesinstructionslike the other two seats (the turn's reach by default), carries them on the new optionalHarnessTurnRequest.instructions, and states the hash of both texts.harnessTurnTextrenders the two as one message, andscripts/native-conformance.mjssends that in each opening frame where it sent the prompt alone. The reference-host example driver states its hash the same way.Hash effect.
Codex,Claude Code,DeepSeekandPigain apromptSetHashin the conformance manifest, and their records asystem.promptHash. No other hash moves;Standard's prompt set is unchanged. A transport of your own that opens a harness fromHarnessTurnRequestshould handinstructionsover, since the hash now covers it. -
One vocabulary for what a seat states about its turn, and
callsAfterEscalationin the record. The standard driver and the MCP harness runtime each built their result metadata as an untyped object, andmodelandmodelProvidermeant "served" on one path and "declared" on the other with nothing saying so. Both now return the exportedSeatMetadata, which declares every key and says which seat states it and what it means there; the keys and their values are unchanged. ADR 0018 namescallsAfterEscalationas the only place "the harness kept going after it asked" can be read, and nothing read it: the conformance record now lifts it asexecution.callsAfterEscalation, optional and ungraded, so the record'sversionand the grading rules' version do not move.harnessOutcome,harnessErrorCodeandmalformedToolCallsstay on the result, documented, for the host that ran the turn. The open-items mention closes. -
One default for the MCP server name.
connection.name ?? SHAREDOS_MCP_SERVER_NAMEwas written at ten sites across the config emitters, the launch arguments and the turn's metadata.mcpServerName(connection)in@aicoo/sharedos-mcpapplies it, and the four shippedMcpHarnessSpecentries no longer restate the default as theirserverName. -
One vendor descriptor. Each of the four vendor folders declared its id, its driven manifest and its requirements from a copied template, and
mcp-runtime.tsdeclared each MCP manifest again.defineHarnessVendorbuilds all of it from a vendor's few facts (id, codec, executable, credential variables), andCODEX_VENDOR,CLAUDE_CODE_VENDOR,DEEPSEEK_VENDORandPI_VENDORare exported beside the names that were already there. Every manifest and every requirements object is what it was, key for key. -
One child-process runner under both ways a vendor CLI is seated.
ChildProcessTransportand the MCP harness runtime each spawned, framed JSON lines, kept diagnostics and stopped their child in a copy of their own, and only the MCP copy ended its child when the turn was aborted. Both now run on oneHarnessProcess, so a driven harness is also ended by the turn's signal where it was left toclose. Each copy kept a capped tail of the harness's stderr (4,096 and 8,192 characters) that nothing read; diagnostics now go to the callback alone, andChildProcessTransportOptionsgains theonDiagnosticthe MCP runtime already had. A stdout line that is not a frame reaches it too, where the transport dropped it. -
One seat module in
@aicoo/sharedos-adapters. The model driver, the harness driver and the MCP harness runtime each composed the seat's two texts, hashed them, recognised the escalate affordance and stamped aToolCallin their own copy, with the reason the catalogue gates the affordance's name written out three times. They now shareseat.ts, and the reason is stated once. No behaviour changes.ModelDriverOptions.instructionsalso accepts a string, placed before the turn's reach, asMcpHarnessRuntimeOptions.instructionsalways has; both interfaces extend the exportedSeatTextOptions, anddeclareStepis typed by the exportedDeclareStep. -
The route-lease row quotes one refusal code, and carries the transport's as its cause. A
messages.requestthe transport refuses leaves two operations under one call id:message.sent, denied with the transport's code, then the tool call, failed withmessage_request_not_accepted. The scripted columns' record reader took the first operation under the id and the live columns andAdvread the second, soroute-lease-revokedprintedroute_lease_revokedin five columns andmessage_request_not_acceptedin the rest, and the expectation accepted either. The split was mistaken for a difference between observing the tool result and observing the record (ADR 0025 said as much); it was audit order.receiptsFromRecordnow takes the tool operation as the attempt's receipt, the expectation namesmessage_request_not_acceptedalone — what SharedOS says, in every column — andjudgeCasejoins the other operation's code to the attempt by call id ascause, reported and never graded, because a host's vocabulary is not a claim about the kernel.AttemptOutcome.cause,CaseJudgement.causesandConformanceCell.causesare new; the manifest printscauseafterreasonwhere a cell has one. The case-set hash moves,1515d09c…to85fc0fb5…, because the expectation is in it; the world-set and prompt-set hashes do not. What a host has to update: a reader of manifest cells sees one more array field; a host's ownAttemptExpectationfor a refused dispatch should name the tool's code, not the transport's.
Added
-
DiscoverOptions, andonExplainonCapabilityAuthorizer.canDiscover. The accountauthorizealready hands a host, on a discovery check that asks for it. Optional, and unset wherever a catalogue is filtered. It exists for the fix above: a call refused at discovery now records why. -
testkit:
expire()on the two grant stores,UnavailableGrantUsageStore, andInMemoryMessageRequestRouter. testkit is where ADR 0002 puts the in-memory stand-ins for the storage a host supplies, and the conformance world restated four of them for want of these.expireedits a grant's window in place and throws on an id the store does not hold, asrevokedoes. The conformance package now depends on testkit and builds its world on these stores. -
terminalSourcein@aicoo/sharedos-runtime. Who ended a failed turn, read from its events:envelopeorruntime. It is the reader the envelope uses for its ownturn.endedrecord, exported so whoever assembles a record does not write a second one. -
receiptBasein@aicoo/sharedos-conformance, the declared half of an attempt receipt, shared by the three places that build one. -
A conformance column can declare its tool policy.
RuntimeColumn.toolPolicyandMcpColumnOptions.toolPolicy; the runner writes it tosystem.toolPolicyon every record the column produces, which is where ADR 0014 said it belonged.scripts/mcp-conformance.mjspasses each CLI's. -
ConformanceWorldOptions.auditFailsAfterOperationsanddrainGraceMs, for the two new rows, andConformanceWorld.envelope, the envelope options a condition armed. -
SHAREDOS_VERSIONin@aicoo/sharedos-contracts. The one statement of the build, read by every runtime manifest, the MCP server's greeting and the conformance record, and the one constant the release gate checks. -
RuntimeHost.draining, anAbortSignalaborted once the turn takes nothing new. Optional on the type so a host double stays viable; the envelope always supplies it. Its reason is fixed and carries nothing from the host.TURN_DRAINING,AuditWriteTimeoutErrorandAUDIT_WRITE_TIMEOUTare exported beside it. -
A refused
messages.requestnames what the transport answered. The caller is toldmessage_request_not_acceptedwhatever the transport said, and only the siblingmessage.sentcarried the transport's code, so a reader joined the two by call id to say why. The tool's owntool.invokedcarries it ascause. It travels inside the kernel, never through the handler's result, which is returned to the caller whole (ADR 0012). -
EscalationOptions.executionId. Optional.recordEscalationrecords it as the event'soperationId, which that turn'sturn.endedhas always carried, so a reviewer's queue built from audit joins an escalation to its turn on an id. The execution envelope passes it. -
An envelope refusal carries the turn's
catalogHash. The kernel's owntool.invokedhas since ADR 0026; the envelope's was built apart and did not, so anot_offeredrefusal could not be joined to the catalogue that did not offer the tool. -
RuntimeHost.annotate(key, value): a fact stated for the turn's record.emitis for something that happened at a moment; this is for something that is true of the turn. The executor holds a frozen clone of a JSON value and merges it into the result's metadata after the outcome's own and beforeruntime, on every way out of the turn. It throws aTypeErrorfor an empty key, for the reserved keyruntime, for the key__proto__, and for a value that is not JSON, and for nothing else: a write while the turn is aborted but open is kept, a write after the turn closed is dropped, so a call site needs no guard. The last write wins.PROMPT_HASH_ANNOTATION,ESCALATION_ASKED_ANNOTATIONandescalationAskedAnnotationname the two facts SharedOS's own runtimes state. What a host has to update: only a test double that builds its ownRuntimeHostadds the member; a plugin is unaffected (ADR 0007). -
A refusal's gate is readable from its audit record, by call id.
tool_unavailableis one code over "not registered", "namespace disabled", and "not discoverable to you", andno_matching_grantis one code over nine conditions, so a caller cannot map the permission topology from refusals (ADR 0012). The host is not the caller, and the record already said which:cause,source,failClosed,rejectedGrants. What a host still did by hand was join a deniedToolResultto those records and decide which check refused.@aicoo/sharedos-corenow exportsclassifyRefusal(event), which names one of six gates —envelope,registration,request,infrastructure,ceiling,grant— for a denied audit event, andexplainRefusal(result, events), which finds the event for a result byoperationIdand names it. Both return recorded facts and no prose; what each gate means and what fixes it is one table indocs/errors.md. The join is on the call id and never on recency: two turns interleaved on one sink put another call's refusal last. Nothing here reaches the wire, anddocs/errors.mdnow says in so many words that a host which narrates the gate back to the model has handed it the oracle the coarse code withholds.
Changed
-
The discovery decision behind a
tool_unavailablecarries the call's id.SharedOSKernel.invokeToolrefuses a tool no grant makes discoverable with a recordedauthorization.checkeddecision, and that record now carries the call'soperationId, as thetool.invokedrefusal after it already did. The two joined only on time order before. A sink or reader keyed onauthorization.checkedevents without anoperationIdsees one more that has it. -
A delegate's ask is a record field, and the grading rules read it there. The standard loop, the MCP escalation latch and the conformance adversary announced the ask as an
escalation.askedruntime event, each behind its own guard against a host that refuses events. They now state it throughRuntimeHost.annotateasescalationAsked: { tool, reason }, with no guard. The judge grades anExecutionRecord, which carries none of a result's metadata, so the record gains an optionalexecution.escalationAskedandassembleExecutionRecordlifts the stated ask into it, validated on shape.JUDGE_VERSIONgoes 4 to 5: the rule is unchanged, its source moved. No committed cell and no hash moves; a record written under version 4 carries the event and not the field, so the two are not cell-comparable on the escalation row. -
One route table, one version constant, one home for the primitives.
@aicoo/sharedos-contractsnow exportsSHAREDOS_ROUTES, the HTTP surface as one table of path, verb, request schema and response schema, whichcreateSharedOSHandlerroutes from andSharedOSClientcalls through; a path with two verbs is two entries, so the handler's405names the verbs the table has for that path.WireSchema<T>is the shape both sides need from a schema, andSharedOSApiErrorCodenames the seven codes the handler itself answers with;SharedOSHttpError.codeandSharedOSClientError.codeare typed by it, widened to any string because a host'sresolveContextthrows codes of its own and an older client must still carry a newer server's. The wire is unchanged.PROTOCOL_VERSIONis the one valueProtocolVersionSchemaaccepts, and every runtime manifest, request, event, result and envelope SharedOS builds reads it; the conformance record, the adversarial report, the conformance manifest and the cost report keep their own"1", which are versions of their own shapes.isJsonObjectsits in contracts beside theJsonObjecttype. The helpers the runtime and the adapters had each copied from core —deepFreeze,protocolError,raceAbort,readJsonObject, now withparseJsonObjectbeside it for JSON text, andcompactObject— are exported once, from@aicoo/sharedos-core/internal, a subpath and not the package index. The runtime'sdeepFreezehad short-circuited on a frozen container; core's recurses first. What a host has to update: a driver'sopentakesRuntimeTurnRequest, the nameAgentTurnRequestwas an alias of; the standard composition isnew SharedOSExecutor(kernel, createStandardRuntime({ driver })), whichTurnExecutorbuilt (see Removed). -
MessageEnvelope.provenanceis host-owned metadata, and says so. The kernel checks its shape with the envelope, neither sets nor reads it, and hands it to the transport as sent. Its row indocs/open-items.mdis closed on that reading; the schema and the threat model state it. ADR 0025 is revised in place for where theroute-lease-revokedrow reads the transport's code: the tool operation'scause, not a second operation joined by call id. No behaviour, record or hash changes. -
The root README is rewritten around what ships, and installs are untagged. It opens on Grant, Delegate, Execute, the tagline and the three parts of "About SharedOS"; names the two shipped runtimes,
createStandardRuntimeandcreateMcpHarnessRuntime; turns the examples into a table; and leaves the ADR list to the index indocs/README.md, which gains the conformance manifest and the systems-cost page under "Start here". Every install line isnpm install @aicoo/sharedos…with no@next, so it takes the release npm'slatesttag points at. The release runbook still verifies a new publication undernext, where releases land. -
The pages the root README links are corrected against the code. The quickstart's first program now prints what it says,
docs/mcp-api.mdimports the HTTP transport from@aicoo/sharedos-mcp/node,docs/endpoints.mdcounts what ships, and the design pages name the two runtimes and the turn endingsdocs/errors.mdalready described. Docs only.
Removed
- From
@aicoo/sharedos-conformance:operationsUnderandCallOperations(the operation carries itscause);contentHash(usehashJson, exported from the same place);ConformanceChainResolver(ConformanceWorld.chainis testkit'sInMemoryDelegationChainResolver); and nine declarations nothing set or read:SystemIdentity.adapterVersion,ConformanceWorldOptions.now,HostileRuntimeOptions.version,HostileRuntime.moves,ConformanceGrantSource.loads,SpanCollector.pause,resumeandnamed, andAttemptReceipt.forgedGrantId. No alias for any of them. - From
@aicoo/sharedos-core:MID_TURN_AUTHORITY_REFRESH, and the per-operation authority path it switched on. It was an exportedconst false: a host could not set it, no test did, and since ADR 0016 moved expiry to the operation's instant the one thing left behind it was seeing a store edit before the next turn, at a store read per operation. A turn resolves authority once. A kernel call outside any turn still resolves its own, which is a turn of one operation and is unchanged. ADRs 0009, 0010 and 0016 are revised in place, and the open-items row is closed. - From
@aicoo/sharedos-core:AuthorizationRequest. It wasCapabilityRequirementfrom contracts declared a second time: the sameresourceandaction, andPOST /v1/authorizealready took the contract schema. Core uses the contract type throughout, the two host ports that named the old one included:HostCeiling.narrowandMessageCapabilityResolver.resolve. A host that builds the object without importing the type changes nothing; one that imports it renames one type. ADR 0020 is revised in place. - From
@aicoo/sharedos-runtime:StandardRuntime. SeecreateStandardRuntimeunder "Changed — breaking". - From
@aicoo/sharedos-adapters:ModelRuntime,HarnessRuntimeandDriverRuntime;ModelDriverandHarnessDriverwith their…Optionstypes, under their new names;createCodexDriver,createClaudeCodeDriver,createDeepseekDriver,createPiDriverand the fourcreate…Runtimefunctions built on them, withCodexDriverOptions,ClaudeCodeDriverOptions,DeepseekDriverOptionsandPiDriverOptions. A driver is not a plugin, so there is no factory per driver or per vendor: a vendor CLI is seated in a product throughcreateMcpHarnessRuntime, and its wire codec only by an evaluation, from its*_VENDORdescriptor.parseToolArgumentsis no longer exported; it was a re-export of an internal helper that no SharedOS interface takes or returns. - Seven version constants:
STANDARD_RUNTIME_VERSIONfrom@aicoo/sharedos-runtime,MCP_SERVER_VERSIONfrom@aicoo/sharedos-mcp, andMCP_ADAPTER_VERSION,CODEX_ADAPTER_VERSION,CLAUDE_CODE_ADAPTER_VERSION,DEEPSEEK_ADAPTER_VERSIONandPI_ADAPTER_VERSIONfrom@aicoo/sharedos-adapters. The packages share one version, so each was the same string kept equal by the release gate. ReadSHAREDOS_VERSIONfrom@aicoo/sharedos-contracts, which@aicoo/sharedos-conformancestill exports under the same name. Every manifest states the same version as before. OpenToolBridgeOptions.stepfrom@aicoo/sharedos-mcp, and theoptionsparameter ofBridgeToolInvoker.invokeToolwith it. No caller passed a step: a harness keeps its own loop, so a turn served over MCP declares none and is bounded bymaxToolCallsandtimeoutMs, whichdocs/mcp-toolshare.mdnow says.RuntimeHoststill satisfiesBridgeToolInvoker. The open-items row closes.ESCALATION_ASKED_EVENTandescalationAskedEventfrom@aicoo/sharedos-runtime, shipped in 0.1.0-alpha.4. A delegate states the ask throughRuntimeHost.annotateunderESCALATION_ASKED_ANNOTATION, in the shapeescalationAskedAnnotationbuilds; a reader takesexecution.escalationAskedfrom the record, ormetadata.escalationAskedfrom the result, instead of decoding aruntime.event.TurnExecutorandTurnExecutorOptionsfrom@aicoo/sharedos-runtime. The facade built exactlynew SharedOSExecutor(kernel, createStandardRuntime({ driver }))and forwardedonTurnErrorto both; a host writes that composition itself and installs one sink in both options. Open-items row 19 closes.AgentVisibleContextandAgentTurnRequestfrom@aicoo/sharedos-runtime, the backwards-compatible spellings ofRuntimeVisibleContextandRuntimeTurnRequest. The first had no user; the second was the parameter type ofAgentTurnDriver.open, which now names the type it always was.ContextCapsule,ContextCapsuleSchema,validateContextCapsule,contextCapsulePreview,CONTEXT_CAPSULE_ITEM_KINDSand theMAX_CONTEXT_CAPSULE_*limits from@aicoo/sharedos-contracts, and their row in thedocs/errors.mdlimits table. Not dead surface: a feature stub, a bounded, content-addressed payload for delegating across a trust boundary, removed because no message, tool or turn path carries a capsule and no ADR asks for one. It returns under governed views, with an ADR of its own. Open-items row 14 closes.JsonArraySchemafrom@aicoo/sharedos-contracts.JsonValueSchemavalidates arrays inline; theJsonArraytype stays.ToolClassSchemaandToolClassfrom@aicoo/sharedos-contracts, withclassifyToolfrom@aicoo/sharedos-mcp. Nothing but their own test called them, and neither the conformance package nor the grading rules read a per-call tool class: ADR 0014 declares the classes per run throughToolPolicy, which stays withdeclareToolPolicy,parseToolPolicyandtoolPolicyHash, the last of which the MCP conformance script uses for its columns'policyHash.CompositeAuditSinkfrom@aicoo/sharedos-core. The kernel takes one sink; fanning out to several is a five-line host utility, not a SharedOS claim.measureSyncfrom@aicoo/sharedos-core. Every operation SharedOS measures is asynchronous.formatCatalogHashfrom@aicoo/sharedos-core. The record stores the bare hex, and thesha256:-prefixed form it produced had no carrier; its comment claiming the record renders it was wrong.
0.1.0-alpha.5
Changed — breaking
-
A turn's tool catalogue is resolved once, and every call in the turn is answered from it. Three things already said so:
OpenToolBridgecomputes its catalogue once, because "a catalogue that could change betweentools/listandtools/callwould makecatalogHasha claim about a moment rather than about the turn"; the MCP server answersinitializewithcapabilities.tools.listChanged: false, a promise on the wire to every client; and an execution token bindscatalogHashinto its claims, so a stale sandbox cannot reconnect and call tools it was never shown. The kernel did not — it re-derived the effective registry on every operation. A static registry cannot differ between two derivations, but aContextToolProvidercan: it is an async port called with the turn's context, and the MCP server or host configuration behind it is live, so a turn's second call could be decided against a definition its listing never published. The resolved registry is now held for the turn on the authority lease ADR 0010 already opens, keyed by the sameturnAuthorityKey; an operation with no lease resolves its own, which is a turn of one operation (ADR 0026,docs/adr/0026-catalogue-resolved-once-per-turn.md).What the turn holds is the unfiltered registry, not a decision.
canDiscoverand the invocation check still run per operation against authority resolved atcontext.now, so a grant that expires part-way through a turn still refuses part-way through it (ADR 0016), andupdateToolNamespacesstill takes effect within the turn it is called in.What a host has to update. A
ContextToolProvideris called once per turn rather than once per operation, so a provider written to return different tools as a turn proceeds no longer changes what that turn is answered from — that window is the one this closes. A provider that branches onenabledToolNamespacesreads the value the turn resolved with rather than the current operation's, because the lease key deliberately excludes that field;ContextToolProvider's own documentation now states both. A provider that is slow, remote or rate-limited is called once a turn instead of once a call. A provider returning freshly constructedToolHandlers gets one set per turn rather than one per operation, so a handler carrying state between its ownresolveRequirementandinvokemust key that state on the call it was given and not hold it on the handler: the turn's concurrent calls now share the instance. SharedOS's own message-request handler was written the other way and has been corrected.A new conformance case grades it. In
catalogue-moved-mid-turna provider publishes a tool for the turn's listing and then moves its declared capability onto an action no grant carries. The call made after the move succeeds in all six columns with this decision and fails in all six without it, and the same tool aimed outside the granted tree is still deniedno_matching_grant— what the turn holds is the catalogue, not the decision. The manifest goes to 32 rows and 165 passing cells, and the case-set, world-set and prompt-set hashes move with it.Resolving the catalogue reads 14.3 µs against 1.33 ms, under 1% of a mediated call against 45%, and one whole call 968 µs against 2.44 ms.
Added
-
The shipped runtimes tell the model where it may operate.
RuntimeVisibleContext.reachwas handed to every runtime and read by none, so a model still searched from the root and collected denials, which is the problem the field was added to end.describeReach, exported from@aicoo/sharedos-runtime, renders the result as words: each entry as its namespace, its path as the JSON array apathargument takes, and whether it covers what lies beneath; an empty reach as "nowhere"; anunavailableone as exactly that, with its reason code, so "unknown" never reads as "nothing" (ADR 0021).ModelDriversends it as a system message ahead of the prompt, andcreateMcpHarnessRuntimehands it to the harness as the MCP server's initialize instructions, with the host's owninstructionsstring placed before it. Both are overridable:ModelDriverOptions.instructionsis a function of the turn request,McpHarnessRuntimeOptions.instructionsnow also accepts one, and returningundefinedsends nothing. A host writing its own driver can calldescribeReachand say the same thing the same way.What a host has to update. A model transcript or a test that pinned the first message to the prompt now sees a system message before it. A harness that does not surface MCP instructions shows its model the catalogue alone, which is what it saw before.
-
Every
AuditEventcarries anid. A record's identity was its content: the kernel stampsatfrom the turn'sAccessContext.now, a bareauthorizecarries nooperationId, and nothing else on the record distinguishes one emission from the next, so the same question asked twice in one turn produced two byte-identical records. A sink that deduplicated on a content hash -- the natural idempotency key when the record offers no other -- kept one and dropped the rest; an end-to-end run over 1,444 host decision records lost more than a quarter of them that way. The id is minted when the event is made, a random UUID by default, andSharedOSKernelOptions.createAuditIdsupplies a deterministic one for a replayed fixture.auditEventandautoDecisionAuditEventtake the same factory as a trailing argument.A sink that deduplicates keys on
id. A content hash is still the right fallback for a record written before this field existed, and only for one.atis unchanged and still the turn's instant: two records with oneatare one turn's work, not one record, and their order within the turn is the order the sink received them. -
A model client can name its reasoning mode, and the turn records it.
OpenAiCompatibleModelClientOptions.thinkingsends DeepSeek'sthinkingrequest field,enabledordisabled. It is opt-in: the field is not part of the OpenAI wire shape and a provider that does not know it rejects the request, so nothing is sent until a host asks. A mode is a configuration rather than a model, soModelClientgains an optionalsettingsobject — what the client sends beyond the model name — andModelDriverrecords it on every turn asmodelSettingsbesidemodel,modelProviderandrequestedModel. A host with its ownModelClientmay leavesettingsundefined and sees no change. -
A turn records what the seat was told, and the manifest names it per column. The catalogue a model was served has carried a hash on every record since the MCP bridge existed; the prompt it was given carried none, so rewording it moved nothing on disk, and a cell that moved between two runs could not be told apart from the model changing its mind. One sentence about what the tool-call channel carries took five
not exercisedcells to none on the same model, and neither run said it had asked a different question. Both shipped runtimes now computepromptHashbefore the seat is sent anything, over the same two texts in the same shape —{instructions, prompt}in canonical JSON,instructionsbeing the reach asModelDriver's system message or ascreateMcpHarnessRuntime's initialize instructions,nullwhen the host sends none — and it lands inSystemIdentity.promptHashbesidecatalogHash. The conformance manifest folds each column's per-turn hashes, in row order, intopromptSetHashon the column entry, so the committed manifest pins the question the Standard column is asked andconformance:checkmoves when the wording does. Columns that hand the seat no text — the adversary, a vendor column driven by written frames — carry none. The live scripts print it beside the model. It covers what SharedOS said: a CLI's own system prompt is added on the far side of the wire and is not claimed.What a host has to update. Nothing.
promptHashandpromptSetHashare optional additions. A host comparing two live runs of one column should now holdpromptSetHashequal along with the case-set and world-set hashes before reading a moved cell as the model's choice. -
tool.invokednames the catalogue its turn was served.catalogHashwas recorded ontool.catalog.listedand on no other event, so "every call in this turn was answered against the catalogue that was listed" was a claim in a source comment rather than something a reader of the trail could check. The kernel now holds the listing's hash beside the resolution it identifies and puts it intool.invoked's metadata, on the refused calls as well as the served ones. A turn that never listed leaves the field off rather than inventing one, so its absence reads as "no catalogue was published to this turn" and never as "a different one".A reader joins the two events on the value. Time order was the only inference available before, and is no longer needed.
Changed
-
A string-carrying adapter reads tool arguments without the recursive schema. Codex, DeepSeek and the native harness's driver each carry a call's arguments as a JSON-encoded string, and
parseToolArgumentsparsed it and then ran the result throughJsonObjectSchema, which tries every branch of the value union at every node. That pass was most of what those three spent per call: 118–130 µs against the 13 µs of Pi and Claude Code, whose frames carry the object already parsed. The parsed value is now read by a walk that checks only where the parser's output and the schema's verdict part — a number literal that overflowed to an infinity is refused, a"__proto__"key is dropped at every depth — and hands the value back as it was otherwise. The verdict and the value are the schema's, blob for blob, and a test holds them to it. Indocs/conformance/systems-cost.mdthe translate rows read Standard 11.7 µs, Codex 4.97 µs and DeepSeek 15 µs on the reference machine, from 128, 118 and 130. The kernel's ownJsonObjectSchemapass over every call's arguments is unchanged. -
The kernel reads a tool parser's return value without the recursive schema. Every registered tool's
parseArgumentsruns before authorization, and what it returns is the call the kernel carries forward — intoresolveRequirement, the decision,invokeand the audit record. The kernel held that value toJsonObjectSchemaso a parser that handed back a Date, a Map, anundefinedfield or a non-finite number was refused asinvalid_tool_argumentsand the call stayed a plain JSON object. It still is, by a walk that gives the schema's verdict rule for rule — finite numbers only;undefined, bigints, symbols and functions refused wherever they sit; Dates, Maps, Sets and thenables refused where an object was expected; every other object read as a record of the keysfor...inreaches, into a fresh plain object with a"__proto__"key dropped at every depth — and a test holds the two to each other over forty-nine shapes. The refusal code, its place before authorization and thestructuredClonethat follows are unchanged; the schema's own copy of every container, made before the clone made another, is what goes. On the reference machine the check and the clone together read 6.4 µs per call, from 155. -
The live conformance prompt says what the model seat's channel does, and stops labelling the arguments. A live model in the Standard column skipped every call to a name absent from its tool list and reported it as refused without making it, because it believed the function-calling channel carries only defined names; the hidden-tool, rollback-unavailable, broker-ungranted, escalation and record-completeness rows were
not exercisedin most runs without the kernel being asked.MovePromptOptions.unknownNamesReachKerneladds one sentence saying the channel accepts any name and the kernel refuses it there; the two model columns pass it, and no other column does, because an MCP client's own router refuses an unlisted name before it is sent. Each call now reads "with {...}" rather than "with arguments: {...}": the label led the model to nest every call's arguments under anargumentskey, which the kernel fails asinvalid_tool_arguments, and a control failed that way is a row that proved nothing. Over the full set on deepseek-v4-flash, twice each: 5 and 4not exercisedbefore, 0 and 1 after.promptSetHash, now added above, covers the prompt, and the Standard column's entry in the committed manifest pins this wording. -
pnpm conformance:nativeruns its model column with reasoning off. The column measures whether a call reaches the kernel and how the kernel answers, not how the model deliberates, and with reasoning on deepseek-v4-flash spent up to its whole 4,096-token reply budget on the test prompt before its first call, failing the turn undermodel_output_truncatedwith nothing attempted. The default is nowthinking: disabled;SHAREDOS_MODEL_THINKING=enabledrestores reasoning andprovidersends no such field. The mode is in the column's label —Standard (deepseek-v4-flash, thinking off)— in the availability entry, and in every turn'smodelSettings, so a run in one mode is never read as a run in the other. A full run takes two minutes instead of four to five. -
The kernel copies its tool registry instead of rebuilding it per call.
listTools,listToolNamespaces,updateToolNamespacesandinvokeTooleach resolve the effective catalogue, and resolving it meant constructing a freshToolRegistryand re-registering every static handler: aToolDefinitionSchema.safeParse, aJSON.parse(JSON.stringify(...))clone and a deep freeze per tool, over definitionsregisterhad already parsed, cloned and frozen once, arriving each time at a value guaranteed equal to the one it started from. The price grew with the catalogue, so when the conformance world moved to the shipped file vocabulary — five published tools to seventeen — catalogue resolution went with it, to 79% of one mediated call.ToolRegistrygainscopy(), which shares the registered entries, and the kernel resolves from that. Indocs/conformance/systems-cost.mdresolving the catalogue reads 1.33 ms against 6.99, and one whole mediated call 2.44 ms against 8.23, on the reference machine.What the catalogue contains does not change,
catalogHashwith it, and neither do the two properties the rebuild was carrying: aContextToolProvideroffering a name a host already registered still fails closed on the duplicate, and a registry a host passes asoptions.toolsis still never mutated by the call that adds context-supplied tools beside it. A third property the rebuild had been carrying silently is now explicit:registerfreezes the entry, not only the definition inside it, because two registries sharing a mutable entry would let an assignment through one of them change what the other serves. Host code that wrote to a registered handler was reaching into kernel state and now raises aTypeError. The message-request tool is still constructed per call, because its handler holds the envelope it prepared betweenresolveRequirementandinvokeand one instance per call is what keeps that state from being shared by concurrent calls. -
Registering a tool reads its JSON Schema without the recursive value schema.
ToolRegistry.registerheld every definition toToolDefinitionSchema, whoseinputSchema,outputSchemaandmetadataareJsonObjectSchema— a union tried at every branch of every node. On the shippedmessages.requestdefinition, whoseinputSchemais a nestedoneOf, deciding that its JSON Schema is JSON cost 1244 µs of a 1302 µs parse: 93% of the 1340 µs the registration took. Those three fields are now read by the same single-pass walk the kernel already uses for a tool parser's return value, and the schema is shown an empty object where each one was, so it still decides which fields are required, which are optional and which are unknown, and it still decides every other field itself. The walk's verdict is held toJsonObjectSchema's over forty-nine shapes, andregister's whole result to what it produced before over thirty more in each of the three fields.The walk parses a copy of the definition, so it is taken only where the copy cannot say something different from the original: a plain-prototype object whose own properties are all enumerable data properties, with
Object.prototypeunpolluted. A definition that inherits a field, hides one behind a non-enumerable descriptor or a getter, or arrives carried by an array or aDate, goes to the schema exactly as it was passed — the path this replaced, unchanged — because a spread would drop whatz.objectreads through the prototype chain and what.strict()collects withfor...in. Whether a definition is accepted, and what it holds once registered, is therefore the same as before for every definition, not only for the common ones.Registering
messages.requestreads 70 µs against 1340, and the conformance world's nineteen definitions 723 µs against 7334. Since a catalogue is resolved once per turn this is a per-turn cost, paid where the turn's catalogue is derived, sodocs/conformance/systems-cost.md— every figure in which is a span inside one mediated call — is unchanged and cannot show it.Nothing a definition holds changes. The JSON round trip that follows the schema is kept over the whole definition rather than left to the copy the walk makes, because it is the only step that normalizes
-0to0, and a registration that quietly stopped doing that would hold a different value.
0.1.0-alpha.4
Changed — breaking
-
tool.catalog.listedno longer lists tool names. ItsmetadatacarriedvisibleTools, one name per tool the listing returned. It is gone. The event now records what the listing was computed from and what it came to, as identifiers and a count:catalogHash, the catalogue the caller was shown, computed exactly aslistPublishedToolscomputes it so an execution's manifest and the audit record match on one value;enabledNamespaces, the caller's own filter;hostPolicyVersion, the version the turn'sPolicySourcestated, when one loaded; andwithheldCount.authorityHashstays at the top level.failClosed: truenow also appears on asucceededlisting when at least one tool was withheld by an outage rather than by a decision, which the count alone could not say.A consumer reading
visibleToolsfrom audit rebuilds from the identifiers instead. The names a listing returned are whatlistPublishedToolsreturned, andcatalogHashsays whether two listings returned the same ones; an attempted call on a withheld tool is still recorded ontool.invokedwith its owncause. ADR 0023 records the shape. -
requiredCapabilityon a denial is nowrequiredAuthority, andEscalation.requestis nowrequestedAuthority. 0.1.0-alpha.3 shippedAuthorizationDecision.requiredCapability, theCapabilityRequestano_matching_grantdenial describes, andEscalation.request, withEscalationOptions.requestcarrying one intoSharedOSKernel.recordEscalation. Both are renamed: one concept in two roles, each ending in the noun this repository uses for what grants confer — and notrequiredCapability, becauseToolDefinition.requiredCapabilityalready means something else in the same package, a bareCapabilityRequirementrather than the fullerCapabilityRequestthese carry, and it is the field a reader meets first. What they carry is unchanged: a description and not an offer, minted from the trusted context, granting nothing.AuditEventgains a top-levelrequestedAuthority, written onescalation.requestedwhen the escalation named a capability. ADR 0019 is rewritten around the new names.A host reading
requiredCapabilityoff a decision, or passingrequesttorecordEscalation, renames both; a consumer of audit events admitsrequestedAuthority. The schemas are.strict(), so nothing here is additive for a reader: a consumer built against alpha.3 rejectsrequestedAuthorityon anExecutionResult's escalation as an unknown key rather than ignoring it, and surfaces a malformed-response error with nothing to explain it. Upgrade every consumer of these types in step with the host that writes them.ProtocolVersionSchemais deliberately left at"1": it is one literal shared byExecutionRequest,ExecutionEvent,ExecutionResult,MessageEnvelope, andRuntimeManifest, so moving it would re-stamp four objects that did not change in order to signal one renamed field on a fifth. The bump rides the next release with its own reason to move;docs/open-items.mdholds the row, and ADR 0019 records the decision and the failure mode it accepts. -
A host ceiling is consulted per candidate grant, before consumption, and its audit flag moves to
authority.resolved. 0.1.0-alpha.3 installedhostCeilingonCapabilityAuthorizerand consulted it once, on the decision a grant had already produced: a policy refusal had spent amaxUsesgrant by the time it was refused, and a second grant that policy would have allowed was never tried. It is now consulted per matching grant and before consumption. A refused call does not spend the grant, a refusal ends that grant's candidacy rather than the decision, and when nothing is left the reason follows a fixed precedence: the fail-closed delegation denials, thenhost_policy_denied, thengrant_exhausted, thenno_matching_grant. Discovery consults the same port, so a catalogue is never offered on authority invocation would refuse.narrowgains a fourth argument,policy, the value the turn'sPolicySourceloaded (under Added); a ceiling written against alpha.3 still compiles and runs, since the parameter is trailing and admitsundefined. A throw fromnarrowis reported to the newCapabilityAuthorizerOptions.onProviderErroraskind: "policy"— the same shapeSharedOSKernelOptions.onProviderErrortakes, declared on the authorizer because that is where the ceiling is installed; pass one function to both. A malformed return — anasync narrow, or a branch that falls off the end, both yield something whoseallowedisundefined— fails closed ashost_policy_unavailablerather than being read as a denial, and a refusal whosereasonCodeis anything buthost_policy_deniedhas it replaced, so a ceiling cannot re-emit the misattribution the separate code ends.Audit record: the
hostCeiling: truekey alpha.3 wrote onauthorization.checkedwhen a ceiling was installed is gone from that event. Everyauthority.resolvedevent now carrieshostCeiling: "installed" | "absent"andhostPolicy: "loaded" | "unavailable" | "absent", whereabsentmeans no source is installed. A host comparing either event'smetadatawithtoEqualsees the change. The manifest gains ahost-policy-deniedrow, passing in all six columns: a grant covers the frozen path, so the refusal that would otherwise readno_matching_grantreadshost_policy_denied, and the same ceiling withholds every mutation tool from discovery, so the row also asserts that what the ceiling refuses at invocation is absent from the catalogue. Both boundaries appear in one cell,envelopeandkernel.
Changed — behaviour
-
authorization.checkednow carries the decision's own metadata. Until now the event'smetadataheld only the two keys the kernel states,consumedandfailClosed, and alpha.3'shostCeilingflag, which moves toauthority.resolved(under Changed — breaking). It now also carries whatever the decision carried: aHostCeiling's own keys, and — new to audit, though it has existed on the decision all along — thedelegationdetail (codeandgrantId) behind adelegation_chain_invalidordelegation_chain_unverifieddenial. Hosts persist audit events under closed schemas of their own, so this is a change to record. The kernel's two keys are stripped from the decision's copy rather than overwritten, so no port can set them. -
GrantSourcereturns the grants the actor holds and applies no policy. The host guide previously instructed the opposite — apply the ceiling in the source, by not returning the grant it forbids. That is the one refusal path that misreports itself: the kernel recordsno_matching_grantwhile the grant sits in the store. Policy moves to theHostCeilingabove. Nothing in the code enforces this, and nothing can — SharedOS never sees what a source withheld — so it is a contract, and thehostCeilingflag on every authority load is the most a reader gets: it says whether a policy port exists, not whether the source stopped filtering.The trade is worth stating:
AuthoritySnapshot.hashnow identifies authority held, not authority usable. A snapshot may list grants a ceiling will refuse, so an auditor reading one alone overstates what a turn could do and has to read the decisions as well. -
The conformance judge is at version 4: a row graded on how the turn ended is no longer failed when the delegate never asked for that ending. For an
escalateterminal the ending is elected by the delegate, and a live column is a real model that may answer the prompt and stop; version 3 reported that choice as a SharedOS failure, and asymmetrically -- a column that issued nothing at all was alreadynot exercisedon the control guard, so the more cooperative column graded worse for the same behaviour. The judge now reads the ask from the record:sharedos.escalateamong the operations (a delegate that did not recognise the name and forwarded the call) or the newescalation.askedruntime event (one that did). Neither trace gradesnot exercisedand says so in the cell; either trace with an unmet ending is stillfail. Only theescalateterminal is treated this way. No scripted cell moves, and the case-set and world-set hashes are unchanged; artifacts graded under version 3 and version 4 are not cell-comparable on the escalation row. -
The ask is announced before the turn ends on it. Every path that honours
sharedos.escalateends the turn without forwarding the call, so a working ask left no operation in the record -- and neither would one the envelope then failed to honour. The standard loop, the MCP latch and the conformance adversary now emitescalation.askedthroughRuntimeHost.emitthe moment the affordance is recognised, carrying the tool name and reason; it lands in the record as aruntime.eventwhatever the turn then does. Announcing is for the record only: a host that will not take the event does not change what the delegate decided. It is the delegate's own claim and can only make a row grade harder, never credit a pass. Distinct from the kernel'sescalation.requestedaudit event, which records an escalation the envelope honoured. Additive; a host readingruntime.eventsees one more type.
Added
-
The native harness's translation cost is measured, and its layer can be read on its own.
pnpm benchnow prices the model driver's parse-and-translate per call beside the four vendor adapters', through the driver's own functions rather than a copy:decodeChatCompletion,encodeModelMessage,readModelToolCallandmodelToolResultMessageare exported from@aicoo/sharedos-adapters, extracted unchanged fromModelClientandModelDriver, andToolNameCodecaccepts any{ name }list.docs/conformance/systems-cost.mdgains amodel.chat-completionsrow, 123 µs per call on the reference machine, where it read not measured. Additive for a host; nothing the driver does on a turn changed. -
A turn is told where it may operate, and a remote caller can ask.
SharedOSKernel.reach(context)answers the turn's own reach: the namespace, path, actions and scope of every place some grant would authorize something at this instant, with no grant id, issuer, expiry or budget -- the derivationreadAgentCardperforms for a subject, pointed at the caller. The execution envelope reads it once per turn and hands it to the runtime asRuntimeVisibleContext.reach, narrowed byreachThroughToolsto the namespaces the turn's catalogue operates on, so a driver can tell a model where to look without the host reading raw grants to write a prompt.ReachResultmoves to@aicoo/sharedos-contractsas a schema and gainsauthority_unavailablebesideusage_store_unavailable; a reach that cannot be established is handed to the runtime asunavailablerather than as an empty list, and the turn still runs -- a call that depends on the unreadable budget fails closed on its own, as before.GET /v1/reachandSharedOSClient.reach()serve the same answer to a caller driving its own loop over the API. Host note:TurnKernelnow requiresreach; a host passingSharedOSKernelis unaffected, a narrow test double adds one member. The narrowing is keyed on the resource namespace a tool requires a capability over, not onenabledToolNamespaces, because tool namespaces and resource namespaces are different vocabularies (messagesoperates onsharedos.messaging) and the resource plane is not gated by tool namespaces; the kernel's answer and the HTTP route are therefore grant reach, unfiltered. ADR 0021 records the decisions; PRs #12 and #13 are superseded. -
A conformance row for the route lease.
route-lease-revokedsends twice on one turn's authority with the host's route lease revoked between the dispatches, so a send the kernel authorized is refused at delivery and terminates rather than delivering. See ADR 0025. -
@aicoo/sharedos-precedentdecides whether an auto-decision may stand in for a person. A new opt-in package:PrecedentKeyandprecedentKeyDigestgive the mechanism a typed key instead of a string-encoded one,PrecedentLookupkeeps the rows host-side, andadmitAutoDecisionapplies ADR 0022's R1-R4 to a proposal a host's matcher already made. It never widens: a refusal carries no width to read, R2 reusescapabilityIsWithin(now exported from@aicoo/sharedos-core), R3 takes the tightest envelope withdelegationDepth: 0, and R4 marks every decision so a matcher's whole output can be revoked at once. Nothing here is anAuthorizationDecision, andescalation.auto_decidedjoinsAuditEventType. -
One ordering for a constraint envelope.
tightestConstraintsandconstraintsAreWithinin@aicoo/sharedos-coreare the meet and the containment check overpurposes,notBefore, andexpiresAt. Delegation's attenuation and derivation checks and precedent R3 all read the same one, so the three places that each spelled it out cannot drift. One alignment falls out: a present timestamp that does not parse now violates its field on either side, where attenuation previously ignored a malformed child bound beneath an unbounded parent. -
SharedOS can describe an agent, not only address and authorize one.
SharedOSKernel.readAgentCard(context, subject, { view })serves a card made of identity and computed reach and nothing else. Reach is derived at read time, byCapabilityAuthorizer.reach, from the grants in force at that instant, and is never stored: a stored reach is the one description of authority nothing invalidates, because revocation, purpose withdrawal, expiry and a spent budget all work by not matching at the next decision. Reading a card is itself authorized oversharedos/["directory", <subject>]/read(agentCardCapability,directoryCapability); without that gate the directory answers "does this agent exist", and through reach "what resources exist and where", in one call rather than one refusal at a time. Nothing is consumed, and the subject's grants are not loaded until a reader has been authorized to ask about that subject. A card is a view rather than a record:identityandnamespacesare narrower resources beneath the subject's own path, so a less-authorized reader is served a narrower card and is told which views it may still ask for. The card carries no grant id, issuer, expiry or budget, and no display name, avatar or skill -- a host composes those around it. Reach is grant reach: the host ceiling is not consulted, so a card can name a path product policy refuses, and the refusal names the ceiling. A bounded grant whose usage store is missing or throws refuses the cardusage_store_unavailablerather than narrowing it (CapabilityAuthorizer.reachanswers aReachResult, a contract type in@aicoo/sharedos-contracts); a spent budget still omits the grant. Host note: aGrantSourceis now called with a context whose actor is not the caller, so one that reads an ambient session user instead ofcontext.actoranswers with the wrong principal's grants; SharedOS refuses such a card asgrant_scope_mismatchrather than serving it, but a source that filters by session and returns nothing understates silently. See ADR 0021. -
Every refusal reaches audit, and the record names the boundary that made it. The execution envelope made no audit call of its own: a tool name the turn's catalogue never offered, a spent step or tool-call budget, a context mismatch, and how a turn ended existed only in
ExecutionResult.events— a required field hosts pay for on the wire whose every consumer in this repository is the conformance package. A host with an audit sink could not see the clearest attempted violation the system produces (ADR 0023).AuditEventTypegains one value,turn.ended: one event per turn, at the terminal, carrying the outcome and reason. Not one per transition — that would triple the audit volume of every successful turn to say nothing more, and aturn.deniedwould double-count against theauthorization.checkedadmission already produced. A cancelled turn isfailedwith reasonturn_cancelled;AuditOutcomeis unchanged.SharedOSKernelgainsrecordTurnEndandrecordRefusedCall, and the envelope calls them through the same optionalTurnKernelmembersrecordEscalationalready uses, so hosts wire nothing new — a secondAuditSinkoption would be one a host can forget to pass twice, and the failure mode of forgetting is a turn that enforces correctly and records nothing.Two
metadatakeys carry the rest.sourceiskernelorenvelopeon every operation and terminal event; it is required by the change rather than incidental, because the rule "it is in audit, therefore the kernel refused it" held only while the envelope recorded nothing.causedisambiguatestool_unavailable—not_registered,namespace_disabled, the discovery decision's own code, ornot_offeredfrom the envelope — whilereasonstays the code the caller was given, so one refusal keeps one name.errors.mdhas promised that disambiguation all along and delivered it for one of the three situations; it now holds for all of them, including a policy refusal, which could otherwise only ever appear on a decision event.tool.catalog.listedgainswithheldCount, withfailClosed: truewhen what it withheld, it withheld by an outage. Discovery refusals had no code on either side before — the caller is not told and the record said nothing either. The listing's other new keys are under Changed — breaking.Kept out deliberately: the MCP transport's
unauthorizedrefusal, which happens before anAccessContextexists and would need a fabricated principal to record; the thrown error behind any refusal, which stays on the diagnostic hooks; payloads, unchanged; and the parser detail behindinvalid_tool_arguments, which quotes the value that failed. -
The ceiling's policy can be loaded per turn, beside the grant set.
SharedOSKernelOptions.policySourceinstalls aPolicySource, one asynchronousload(context, signal)the kernel calls once per turn, in flight beside the grant load, and holds on the turn's authority lease asResolvedAuthority.hostPolicy. It resolves to aLoadedPolicy,{ policy, version }.HostCeiling.narrowgains a fourth argument,policy, which is thepolicythat source loaded — exactly as loaded, not cloned or validated, because SharedOS does not know its shape and reads nothing from it — and isundefinedwhen no source is installed, so a ceiling that closes over its own state is unchanged.HostCeiling<Policy>andPolicySource<Policy>take the type as a parameter for the host's own documentation; the pairing is not checked. This is the second port ADR 0020 defined, and the reason the synchronous signature can serve a policy that lives in a database: it is read once at the turn boundary, the way authority is, and never on the authorization path.versionis the one thing about a policy SharedOS reads: the source's own name for what it loaded — a revision, an etag, the hash of the table it read — recorded ashostPolicyVersionon everytool.catalog.listedevent in the turn, besideauthorityHash. An opaque value has no canonical form to hash, and the record needs to pin a catalogue to the policy state it was decided against, so the source says.It fails closed the way the grant source does. A throw, or a result that is not a
LoadedPolicy, is reported once toSharedOSKernelOptions.onProviderErroraskind: "policy"and the turn's policy is heldunavailablefor its whole length: every decision the ceiling would have been consulted on is refusedhost_policy_unavailablewithoutnarrowbeing called, on both paths and before any bounded use is consumed. A kernel with no ceiling ignores it. A cancelled load re-throws the abort.Host notes. Nothing changes for a host that installs no source. A
HostCeilingwritten before this release still compiles and still runs: the new parameter is trailing and admitsundefined. Everyauthority.resolvedevent gainshostPolicy: "loaded" | "unavailable" | "absent"besidehostCeiling, whereabsentmeans no source is installed; a host comparing that event'smetadatawithtoEqualsees the new key. ThePolicySourcerow leavesdocs/open-items.md. -
SharedOSKernelOptions.onProviderError, so a contained throw is diagnosable. A provider, tool handler, transport, or router that throws is answered with a fixed reason code --tool_execution_failed,resource_execution_failed,message_delivery_failed, and the four other codes the seven contained call sites return -- and the error itself was discarded, so nothing said which provider broke or where. It now reaches this optional hook, whole and unwrapped, with aProviderErrorContextnaming thekindof port (tool,tool_catalog,resource,message), thereasonCodereturned in its place, the trace and namespace, and whichever ofoperationId,tool,resource, andactionthat path has. One hook rather than one per port:kindis what a host branches on to route them differently, and a port added later is covered by the hook every host already installed.reasonCodeis the same code audit recorded and the same one the agent was told, so a log line joins to both.Nothing reaches the wire: every message and audit event is unchanged, and a kernel with no hook installed takes the same decisions. The hook is observational -- one that throws is ignored -- and synchronous, unlike
onAuditError, which is awaited because it fires after the side effect; this one fires mid-flight, where awaiting a host's logger would put its latency on every failed call. A cancelled operation is not reported.The one behavioural change: a
ContextToolProviderwhoselistToolsthrows is still wrapped into one catalogue-failure sentence, but the provider's error is now that wrapper'scauserather than being destroyed. That is visible without the hook -- a logger or reporting SDK that walkscausewill print the provider's message where it previously printed nothing -- so it is a change to what a host may see, not a no-op.Four symbols join the public surface of
@aicoo/sharedos-core, and through it@aicoo/sharedos:ProviderErrorContext,ProviderErrorKind,ProviderErrorReporter, andreportContainedError. The last is the swallow guard itself, exported so@aicoo/sharedos-runtimeand any host offering a hook of the same shape share one implementation of the promise rather than each making their own.Not covered, and stated so it is not mistaken for done: the four authority ports still discard theirs, and they are not equally bad.
GrantSource,GrantUsageStore, andDelegationChainResolverfail closed under their ownfailClosedreason codes, so the failure is classified even though the cause is gone.CapabilityGrantVerifieris the one to watch: a throw fromverifyis treated asfalse, so the grant becomes invisible and the denial readsno_matching_grant-- indistinguishable from an actor who was never granted the capability, and not markedfailClosed. -
onTurnError, onSharedOSExecutorOptionsandStandardRuntimeOptions, so a contained throw is diagnosable. Both layers catch one and end the turn on a terminal code -- the envelope'sruntime_failed, the standard loop'sdriver_failed-- and both discarded the error, leaving an operator a code and no stack. It is now handed to this optional hook, whole and unwrapped, alongside the turn'sexecutionIdandtraceId;TurnExecutorforwards it to both, so one sink covers both. Nothing about the wire changes: eachProtocolError.messageis the same fixed string, no event carries the throw, and a turn with no hook installed behaves exactly as before. The hook is observational -- one that throws is ignored -- and a cancelled turn does not reach it. Note thatruntime_failedis also what a throw fromopenTurnAuthority,admitTurn, orlistToolsends a turn as, so read the stack rather than the code to tell a plugin's failure from a host port's. -
A conformance row for a runtime plugin that throws out of its turn. The envelope contains the throw rather than letting it reach the host: the turn ends
failedwithruntime_failed, theturn.failedevent names the envelope as what ended it, and the record still carries the call the turn made before it. The receipts survive only because the adversary emits them as they happen -- a crash carries no terminal metadata to return them on -- and the row pins that too. Only a plugin that owns its outcome can throw on purpose, so the row runs on the adversary column and every driven, MCP, and model column declares itnot applicable. The case-set and world-set hashes move. -
A
reporesource namespace, besidefiles.createRepoToolsandregisterStandardOsTools(kernel, { files, repo })publishrepo.status,repo.diff,repo.log,repo.stage, andrepo.commitover a host-owned Git provider. Afilesgrant over a working tree grants nothing underrepoand the reverse, so committing is authority a host issues rather than a consequence of file-write authority. See ADR 0024.
0.1.0-alpha.3
Changed — breaking
-
The conformance manifest's reference column is renamed.
EMBEDDED_COLUMNis nowADVERSARY_COLUMN, with idadversary-embeddedand labelAdversaryin place ofsharedos-embedded/Standard. The column is unchanged — the scriptedHostileRuntimein the seat, owning its outcome — and the rename says what it is: the reference adversary, not the harness SharedOS ships.Standardnow names that harness (below). Code importing the old constant changes the name; a reader ofkernel-conformance.json, or of a live artifact, matches the column on its new id. -
InMemoryGrantChainResolverandUnavailableGrantChainResolverin@aicoo/sharedos-testkitare renamedInMemoryDelegationChainResolverandUnavailableDelegationChainResolver, after the port they implement. The port was renamed fromGrantChainResolvertoDelegationChainResolverin this release and the fixtures kept the old name. Rename the import; nothing else changes. -
Messages now have one policy-bound reason:
purpose. The redundantMessageEnvelope.intentfield is removed, and strict parsing rejects legacy envelopes that still carry it. Outbound sends execute as their sender; an inbound turn executes as its recipient and resolves that recipient's execution, file, and tool authority independently. See ADR 0015. -
grantsis removed fromAccessContext. Authority now enters SharedOS through one required port,GrantSource, whichSharedOSKernelcalls once per turn. A context names who is asking, on whose authority, and for what; it carries no authority of its own, so nothing a caller assembles can become one. Resolved authority is aResolvedAuthoritywrapper that is deliberately not assignable toAccessContext, so grants cannot reach a provider, tool handler, transport, or runtime by accident. A source that throws, returns unparseable material, or answers outside the context's scope denies withauthority_unavailablebefore anything else runs. -
A derived grant names one parent, not a chain.
CapabilityGrant.delegation({ parentGrantId, depth, chain }) is replaced by a single optionalparentGrantId, and ancestors are re-resolved from the issuing store at every decision through aDelegationChainResolver. An embedded chain is provenance the presenter controls; re-resolution is what makes revocation mean something.CapabilityAuthorizer({ chainResolver })is now{ delegationResolver }, andGrantChainResolveris nowDelegationChainResolverwithresolvein place ofget. -
Delegation reason codes are renamed, from
delegation_chain_unavailable/delegation_chain_brokentodelegation_chain_unverified/delegation_chain_invalid. The distinction is now load-bearing: unverified means SharedOS could not establish the chain, and is grouped withauthority_unavailableandusage_store_unavailableinINFRASTRUCTURE_DENIAL_REASONS, whose audit records carryfailClosed: true. Exclude them before computing any denial rate. -
A bounded (
maxUses) parent is refused at both boundaries, asbounded_parent_not_delegable. Usage counters are per grant, so comparing a child's ceiling against its parent's reads as attenuation without bounding total consumption.deriveGrantalready refused; the chain check now agrees. -
tool_not_availableis gone. The execution envelope and the kernel emit one code for one refusal,tool_unavailable; which boundary refused isOperationRecord.source. An owner-crossing tool requirement is nowdeniedwithinvalid_requestrather thanfailedwithinvalid_tool_requirement. -
ExecutionResultgainsescalated, a third terminal state carrying anEscalationand noerror. Code that switched onsucceeded/failed/denied/cancelledand reached for.errorno longer compiles. -
deriveGrantdrops two refusal reasons that could never fire (namespace_mismatch,issuer_is_not_the_holder) and adds three that can:issued_before_parent,id_collides_with_parent, and — on the issuing side only — a refusal to pin an owner onto an unowned parent capability. -
Repository tooling, not a published contract.
pnpm conformance:liveis nowpnpm conformance:native, overscripts/native-conformance.mjs, writingnative-conformance.jsonand readingSHAREDOS_NATIVE_CONFIG. "Live" named when a run happened rather than what it measured, and both live scripts drive a real model over a real wire; what separates this one is that each vendor CLI runs on its own stdio protocol, natively, wheremcp-conformance.mjsreaches the same binaries through MCP. The column idscodex-liveandmodel-livedeliberately do not move: they are keys in artifacts already on disk.The script also takes
--configand--harness, as the MCP one already did. DeclaringcredentialVariablesmakes the pinned key required, so a harness that cannot reach it reports unavailable instead of authenticating somewhere else -- on the operator's own subscription, say, producing a column that cannot be published beside the others.
Changed — behaviour
-
The conformance judge is at version 3: a failed turn that the envelope ended names
envelopeas its enforcement point, read from theturn.failedevent's newsource, where version 2 named a boundary for denied turns only. Statuses are graded as before; a manifest or artifact produced under version 2 differs from one under version 3 in that field alone. -
turn.failedcarriessourcebesidecode:envelopewhen the envelope refused the runtime's outcome or the runtime threw,runtimewhen the envelope relayed a failure the runtime reported as its own. Additive; the event's shape is otherwise unchanged. -
A failure an adapter ends its turn with —
harness_*,model_*— now carriesretryable: falseon the driver path as it already did on the MCP path; the field was simply absent before. Nothing an adapter fails on is retryable: asking the harness or the model again asks the same thing. -
codexMcpConfignow emitsdefault_tools_approval_mode = "approve"besiderequired = true, the setting the conformance launch has always passed as an override. Codex's defaultautomode asks a human before any tool that is not read-only; a run with no human then refuses every write inside Codex with the kernel never consulted. The approval is scoped to the SharedOS server, and what secures a call is the kernel re-authorizing it. A host that wants Codex's own prompt as well can override the key. -
The four MCP harness specs derive their server name from
SHAREDOS_MCP_SERVER_NAMEand, for Claude Code's--allowedToolsand Codex's-coverrides, from the connection'sname, instead of the literalsharedos; a spec given anotherserverNameis now launched under it. -
A grant that expires while a turn is running is now refused inside that turn, at the next decision, rather than at the next turn. Revocation, purpose withdrawal,
issuedAt, andnotBeforeare unchanged: they are still decided at the instant the turn's authority was resolved, and are still observed by the next turn. The rule separating them is directional — the operation's clock may only take authority away, never hand any back — so a turn still carries the grant set it was admitted with and can never gain more while it runs. An ancestor follows the same split. See ADR 0016.Nothing about a turn's authority load changes: no store is re-read,
cost.authorityLoadsstays at 1 per turn, and a decision an expiry refused names the same authority snapshot hash as the decision before it. Hosts issuing short-lived grants should expect them to stop working part-way through a long turn, which is what they asked for; hosts relying on a turn outliving its grants' validity windows must widen those windows.CapabilityAuthorizer.authorizeandcanDiscovertake the operation instant as a new optionalnow, andvalidateDelegationChaintakes the admission instant as a new optionaladmittedAt. Both default to the previous behaviour, so a host calling either directly is unaffected until it opts in.
Added
-
The host ceiling is a port the kernel calls, not a convention hosts apply upstream.
CapabilityAuthorizer({ hostCeiling })installs aHostCeiling, one synchronousnarrow(decision, request, context)consulted only after a grant has matched, on an already-alloweddecision, on both theauthorizeand thecanDiscoverpath -- so a tool a ceiling refuses at invocation is also absent from the catalogue. Step 10 of the permission model's authorization algorithm is now true of the code. It cannot widen anything: a denial is never shown to it, its allow arm is pinned to the decision it was handed, and an allow that does not carry the samematchedGrantIdis a malfunction that fails closed. Its refusal ishost_policy_denied, a policy denial in its own bucket -- not infrastructure, and not merged withno_matching_grant, which says no such authority exists. A throw ishost_policy_unavailable, which joinsINFRASTRUCTURE_DENIAL_REASONS. The synchronous signature is the enforcement of "deterministic and cheap": a synchronous return cannot await a network or model call. It is optional, and a kernel constructed without one behaves exactly as it did, down to the audit record; when one is installed the kernel records that, so a deployment that denies everything through policy is legible rather than reading as one where nobody was granted anything. ADR 0020'sPolicySourceis not implemented and carries a row indocs/open-items.md. Hosts applying a ceiling today keep the same logic and move the call site; withholding a grant instead still producesno_matching_grantand still misattributes the refusal. -
A denial for want of a grant names the authority that would have satisfied it.
AuthorizationDecisiongains an optionalrequiredCapability, aCapabilityRequestpopulated only onno_matching_grant, where the authorizer already holds every field: the resource and action the caller named, and the owner, namespace, purpose and instant from its own access context. It is a description and not an offer -- it grants nothing, no port accepts one back as authority,allowedstaysfalse, and a host that ignores it behaves exactly as before. It is never built from anything a provider knows, so the same description is produced for a path that is absent and for one the actor cannot reach; it is not an existence oracle. The other denials deliberately carry none. See ADR 0019. -
An escalation may carry the capability it is asking for.
Escalationgains an optionalrequestandSharedOSKernel.recordEscalationaccepts one throughEscalationOptions.request, typically therequiredCapabilitya denial just described; theescalation.requestedaudit event records it, so a reviewer receives a capability instead of a sentence to reconstruct one from.id,namespaceId,requester,owner, andrequestedAtare minted from the trusted context and overwrite whatever the caller supplied -- a request the caller authored would be a caller-chosen correlation for a decision the kernel made -- andidis derived from those fields rather than generated, so one ask describes itself the same way twice. Nothing else changes: there is no third decision value, no consent port, no queue, and no resumption;Escalation.statusis still alwayspendingand nothing inside SharedOS advances it.CapabilityRequeststops being a type with no port and leavesdocs/open-items.md. -
The native harness has a committed conformance column.
Standard(MODEL_SCRIPTED_COLUMN, idmodel-scripted) isModelRuntime—StandardRuntimewith the model driver in the seat and the permission-filtered catalogue rendered into the model's tool-call shape — with a transcript where the provider would be.movesToModelTranscriptwrites each declared attempt as a model reply in the wire alphabet a provider accepts, and the driver's real codec, argument parsing, escalation recognition, and step accounting read it back; what is left out is the model. It is graded undermodelLimits, as the live model column is, so the shipped loop carries a driver's limits in a committed cell rather than standing in for the kernel it runs on: the inspection row and the ungranted-escalation row readnot applicable, and the step-ceiling rowpass (driver). The manifest goes from five columns to six; every hash is unchanged, because neither the case set nor the world set moved. -
TranscriptModelClientin@aicoo/sharedos-adapters: aModelClientthat replays a suppliedModelTranscriptthrough the realModelDriver, the counterpart ofTranscriptTransportfor a vendor harness. A spent transcript fails the turnmodel_call_failedrather than completing on the recording's behalf, so a script that ends too early is a visible result and not a model choosing to stop. -
A conformance row for an escalation the turn was not granted. A runtime that ends its turn with
escalatewhile the catalogue does not offer the affordance is refused by the envelope: the turn failstool_unavailable, and nothing is recorded or audited. Only a plugin that owns its outcome can make the attempt, so the row runs on the adversary column and every driven, MCP, and model column declares itnot applicable-- the first use ofColumnLimits.unsupported. The case-set and world-set hashes move. -
DriverRuntimein@aicoo/sharedos-adapters: the one implementation behindHarnessRuntimeandModelRuntime, which are now its two named forms. A host installing another driver under its own identity uses it directly. -
escalationOffered(tools)in@aicoo/sharedos-runtime: the catalogue gate on honouringsharedos.escalate, shared by the three adapters and the executor instead of each spelling the check. -
canonicalActorin@aicoo/sharedos-mcp: the one string form of anAddressan execution token carries asactor,<kind>:<id>, from the same pairaddressPathderives for a recipient-scoped grant. The form was described in a docblock example and defined nowhere. -
MCP_HARNESS_IDSin@aicoo/sharedos-mcp, the one listMcpHarnessIdis derived from, andCODEX_HARNESS_ID,CLAUDE_CODE_HARNESS_ID,DEEPSEEK_HARNESS_ID,PI_HARNESS_IDin@aicoo/sharedos-adapters: each adapter's manifest, requirements, and MCP spec now name the harness from one constant, checked against that list. -
codexMcpServerSettingsin@aicoo/sharedos-mcp: the settings a Codex MCP server entry carries, as key and TOML value.codexMcpConfigrenders it as a table and the Codex conformance launch passes it as-coverrides, so the two cannot disagree. -
A denial now explains itself to the host. The reason codes still collapse for the caller —
no_matching_grantcovers nine causes andauthority_unavailablecovers four, so that no caller can map the permission topology by reading refusals — but the operator who wired the store and issued the grant is not the caller.authorization.checkednow carriesgrantsResolvedand arejectedGrantsarray naming every resolved grant and the first condition it failed (issuer,subject,namespace,window,purpose,verifier,capability,delegation,exhausted), andauthority.resolvedcarries the same key for the three conditions checked before the grant loop runs.usage_store_unavailableanddelegation_chain_unverifiedadditionally carrymissingDependency: "usageStore" | "delegationResolver", which distinguishes a permission problem from a wiring one: amaxUsesgrant with no usage store, or a derived grant with no delegation resolver, denies every call it should have allowed and is otherwise indistinguishable from an absent grant.Host notes. Nothing is added to
AuthorizationDecision, so no response body changes.CapabilityAuthorizergains an optionalAuthorizeOptions.onExplaincallback for hosts that call it directly;SharedOSKernelsupplies its own and needs no change. Theunavailablevariant ofAuthorityResolutiongains an optionaldetail, so a host that compares a resolution withtoEqualrather thantoMatchObjectwill see the new field. Discovery checks do not explain: catalog filtering denies on nearly every tool by design, and routing that through the record would bury the denials somebody was looking for. -
messages.request, a canonical recipient-scoped request/reply tool. The model supplies only a recipient and JSON-safe payload; trusted context supplies the sender, purpose, trace, timestamp, and message id.MessageTransportandMessageRequestRouterremain host ports for durable delivery and reply lookup—SharedOS does not own a queue, receiver wake-up, or scheduler. Direct send and the request tool share one post-authorization delivery path, so a bounded send grant is consumed exactly once. -
A quickstart with two working programs, an HTTP API reference covering every route and status code, a tool catalog covering the twelve
filestools and how to register your own, and a reason/error code reference. None of this was documented outside the source before. -
A conformance suite: every kernel guarantee is an attempted violation run by a scripted adversary, graded per runtime, with the case set and the world it runs against hashed separately.
pnpm conformanceregenerates it. -
A conformance row for a validity window closing mid-turn, and the clock it needs. Nothing can expire during a turn when time does not move, so a condition may now arm
expiresAfterOperations, which advances the world's clock one step per mediated operation — indexed on the operations the kernel recorded rather than on wall time, so repeats stay byte-identical. It is opt-in per condition and every other condition still runs on the frozen instant. The row is separate fromrevoked-mid-turnrather than a second condition on it, because the two make the identical call at the identical position and require opposite answers. -
Escalation as a recorded outcome, per-turn authority resolution, and one refusal vocabulary across both enforcement boundaries. See ADRs 0008–0013.
-
@aicoo/sharedos-mcp: the permission-filtered catalogue served as an MCP server, which is the boundary a vendor harness actually connects to, plus Codex, Claude Code, DeepSeek Harness, and Pi adapters in@aicoo/sharedos-adapters. See ADR 0014. -
ModelDriverin@aicoo/sharedos-adapters: a model API in the delegate seat, with no vendor CLI between it and the envelope.StandardRuntimestill owns the loop, still stops atmaxSteps, and still re-authorizes every call; what changes is only who occupies the seat. Dotted SharedOS names are mapped per turn, from the catalogue rather than by guess, onto the^[a-zA-Z0-9_-]+$a chat-completions provider constrains function names to — and a name the map does not hold is passed through anyway, so a model that invents a tool outside its catalogue reaches the envelope to be refused instead of being filtered out where nothing records it. The provider'sfinish_reasonandusageare read off every completion: a reply cut off at the output-token ceiling fails the turn asmodel_output_truncatedrather than being graded as a decision the model finished making, and the turn's token spend reaches the execution record ascost.inputTokens/cost.outputTokens. Afaildecision now carriesmetadataascompletedoes, so a failed turn keeps the model that answered and what it cost. A call whose arguments do not parse is refused by the driver asinvalid_tool_argumentsand answered back to the model, never sent as{}-- an empty object is a call the model did not make, and a tool with every parameter optional would have run it. The turn's metadata counts them asmalformedToolCalls; pastmaxMalformedCalls(default 8) the turn fails asmodel_malformed_call_limit_exceeded. -
A
model-liveconformance column built on that driver, which separates what a model does from what a vendor's scaffolding makes it do — the axis every other live column confounds. It is an addition to the scripted column and never a replacement: a model chooses, so an attempt it declines leaves no operation and the cell reports not exercised rather than pass. -
Escalation as something the occupant of the delegate seat can ask for, rather than an outcome only a host-written runtime could reach.
AgentTurnDecisiongains an escalate variant, and the ask itself is published assharedos.escalate: catalogued and permission-filtered like any other tool, invisible to an agent holding no grant over it, and never invoked -- a driver whose turn's catalogue offers it recognises the name and ends the turn on it, and a call naming it on a turn that was never granted it is passed through to be refusedtool_unavailable. The envelope holds the same gate from outside:SharedOSExecutorfails a turntool_unavailablewhen any runtime plugin returnsescalateon a turn whose catalogue does not offer the affordance, recording nothing, since nothing reached the kernel. The reason is recorded as given, cut to the outcome's 512-character bound rather than replaced when it runs past it. Asking for a human is an affordance a host grants, so a host that publishes no escalation grant has agents that cannot ask. See ADR 0017. -
An optional
steponAgentTurnDecision.tool_call. A driver that says nothing is bounded exactly as before; one that names a step is refused for it if the envelope disagrees, because declaring a step is a claim and not a permission. The claim reaches forward only:StandardRuntimerefuses a step behind its own position asinvalid_driver_decision. Below the ceiling the record carries the step as declared; who declared it is the conformance report's to say. See ADR 0017. -
The same ending over MCP, where the harness rather than SharedOS owns the loop. A
tools/callfor the escalation affordance is recognised before it becomes an operation:EscalationLatchwraps the one invoker every MCP call already passes through, answers the ask, and the turn settles asescalateonce the harness has wound down. Calls made after the ask are refused in band asdeniedwithescalation_pending, rather than by closing the bridge -- closing it surfaces as a JSON-RPC internal error, which carries nothing about authority and which harnesses retry into a frame limit. The grant is checked first, so an agent without it getstool_unavailable. See ADR 0018. -
createEscalationTool()in@aicoo/sharedos-runtime: the handler a host registers sosharedos.escalateis catalogued. The definition was exported and every host wrote the same failing handler by hand; the conformance world and the adapter tests now register this one. It is never meant to run -- a driver ends the turn on the name -- and fails withescalation_not_terminatedif a driver forwards the call anyway. -
What enforcement costs, measured on both paths and reported in
docs/conformance/systems-cost.md.pnpm benchregenerates it, against a monotonic clock added for the purpose — wall time is not a duration. -
pnpm release:promote-latest <version>moves thelatestdist-tag across the whole package set in one command, refusing to act unless every package has published that version.
Removed
release:check:private, and the--allow-privateflag onscripts/release.mjsbehind it. The flag required every package to beprivate: trueunder anUNLICENSEDlicense, the preparation state the packages left before0.1.0-alpha.0was published; with all eleven public and Apache-2.0, the command could only throw.release:checkis the one release check.MCP_HARNESSESfrom@aicoo/sharedos-adapters/node. Nothing read it: the conformance script names the four specs it runs, and a host picks one.strictToolPolicyandListToolsParamsSchemafrom@aicoo/sharedos-mcp. Neither had a caller;declareToolPolicy({ mode: "strict" })is what the helper built, and thetools/listhandler takes no parameters.- The
config.tomlthe Codex conformance spec wrote into its temporary workspace. Codex reads$CODEX_HOME/config.toml, never the working directory, and the launch has always passed the connection as-coverrides, so the file was written and never read.codexMcpConfigstays, for a host that configures a persistent Codex. - The MCP harness runtime's own
turn_cancelledoutcome. UnderSharedOSExecutorit was unreachable -- the executor races the plugin against the signal, and its owncancelledresult wins -- and it gave the code a second shape,retryable: falseagainst the executor'strue. A run whose signal is aborted now rejects with the signal's reason, as any aborted operation does;docs/errors.mdno longer lists the code under Adapters.
Fixed
-
Catalogue comparability is compared per case and per column instead of pooled across a whole run. The check warned whenever a run had served more than one catalogue, which was right while a run served exactly one and wrong as soon as the tool set became permission-filtered per case. Two columns are comparable when each saw the same catalogue for the same case, so that is what is now checked, and the warning names the first case where two columns actually diverged.
catalogHashByCasegoes into the artifact beside the pooled list, because a pooled list cannot show a reader that two columns saw the same tools for the same row. -
Five embedded build constants — the DeepSeek, Pi, and MCP adapter versions among them — were left at
0.1.0-alpha.0while their packages moved. They name the build that produced an execution record, so a stale one misattributes evidence.release:checkguarded two of the seven and now guards all of them. An eighth, the version the MCP server reports ininitialize.serverInfowhen built withoutserverInfo, was still0.1.0-alpha.0; it is nowMCP_SERVER_VERSION, exported from@aicoo/sharedos-mcpand guarded too. -
The publish order listed
@aicoo/sharedos-conformanceahead of@aicoo/sharedos-mcpand@aicoo/sharedos-adapters, both of which it depends on, so a run that stopped part-way could leave it on the registry with unpublished dependencies.scripts/package-set.mjsis now in dependency order,release:checkrefuses an order that is not, andtest:releasechecks the order against the manifests. -
SharedOSClientOptions.tokenandSharedOSCallOptions.purposewere undocumented: the HTTP reference listed onlyheaders, and an earlier note here saidtokenhad never existed. Both have been there since the client was written.tokenis a value or an async function sent as a Bearerauthorizationheader,headerscarries anything else, and per-callpurposesetsx-sharedos-purpose, the header the quickstart'sresolveContextreads. All three are now in the HTTP reference and the client README. -
Publish verification retried the same registry URL with default caching, so a CDN edge holding a pre-publish
404made the whole window unwinnable and a successful release reported failure. It now backs off up to five minutes and requests an uncached response each time.
0.1.0-alpha.2
Added
deriveGrantandGrantChainResolverin@aicoo/sharedos-core: a grant holder can pass on a strictly narrower slice of what it holds without the resource owner writing the row. Every axis — path, actions, scope, purpose, time window, and chain length — is checked separately, and a request that is not within the parent is refused rather than clamped. The chain is bound at use time, so a later revocation or expiry upstream still invalidates everything derived from it. (#8)examples/fleet-delegation, a runnable delegation walkthrough in workcell vocabulary. (#8)examples/reference-host, a working host: a filesystemfilesprovider covering all twelve actions with path-escape defences, durable SQLite stores for bounded uses, revocation, namespace settings and audit, and anAgentTurnDriverover a live model.
Changed
@aicoo/sharedos-contractsowns the context capsule that crosses an agent boundary. (#9)
Host notes
CapabilityAuthorizeracceptschainResolver. A host that issues derived grants must supply one; without it, derived grants are denied withdelegation_chain_unavailable.- A bounded (
maxUses) grant cannot be delegated. Sharing one use budget across a chain needs usage accounting that spans grants, soderiveGrantrefuses withbounded_parent_not_delegableinstead of multiplying the budget by the number of delegates.
0.1.0-alpha.1
Added
- Pluggable runtimes inside a fixed security envelope:
RuntimePlugin,RuntimeRegistry,SharedOSExecutor, andStandardRuntime(ADR 0007). - The
@aicoo/sharedos-httptransport adapter and the@aicoo/sharedos-clienttyped client over the same contracts.
0.1.0-alpha.0
First public prerelease of the eight @aicoo/sharedos-* packages under
Apache-2.0.