@yanlinglabs/winter-runtime-sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/directory/directory.d.ts +55 -0
- package/dist/directory/entries.d.ts +82 -0
- package/dist/directory/recovery.d.ts +49 -0
- package/dist/door.d.ts +247 -0
- package/dist/errors.d.ts +104 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +6679 -0
- package/dist/messaging/attribution.d.ts +47 -0
- package/dist/messaging/dispatch.d.ts +78 -0
- package/dist/messaging/handlers.d.ts +56 -0
- package/dist/messaging/inbound.d.ts +110 -0
- package/dist/messaging/index.d.ts +39 -0
- package/dist/messaging/official-adapter.d.ts +36 -0
- package/dist/messaging/router.d.ts +101 -0
- package/dist/messaging/sessions.d.ts +49 -0
- package/dist/messaging/winter-adapter.d.ts +47 -0
- package/dist/native-args.d.ts +76 -0
- package/dist/official/adapter.d.ts +88 -0
- package/dist/official/aliases.d.ts +48 -0
- package/dist/official/auth.d.ts +117 -0
- package/dist/official/branding.d.ts +31 -0
- package/dist/official/callbacks.d.ts +143 -0
- package/dist/official/containment.d.ts +130 -0
- package/dist/official/env-allowlist.d.ts +237 -0
- package/dist/official/env-registry-rule.d.ts +12 -0
- package/dist/official/env-registry.d.ts +3 -0
- package/dist/official/errors.d.ts +250 -0
- package/dist/official/index.d.ts +31 -0
- package/dist/official/mcp-descriptors.d.ts +130 -0
- package/dist/official/options-template.d.ts +110 -0
- package/dist/official/spawn-proxy.d.ts +144 -0
- package/dist/official/spool.d.ts +80 -0
- package/dist/official/supervision.d.ts +49 -0
- package/dist/official/sweep.d.ts +65 -0
- package/dist/sdk.d.ts +214 -0
- package/dist/seams/context.d.ts +26 -0
- package/dist/seams/directory-store.d.ts +274 -0
- package/dist/seams/directory.d.ts +46 -0
- package/dist/seams/global-messaging.d.ts +30 -0
- package/dist/seams/handoff.d.ts +77 -0
- package/dist/seams/index.d.ts +11 -0
- package/dist/seams/keychain.d.ts +11 -0
- package/dist/seams/materialized-resume.d.ts +46 -0
- package/dist/seams/messaging-contract.d.ts +29 -0
- package/dist/seams/official-adapter.d.ts +125 -0
- package/dist/seams/official-sdk-shapes.d.ts +126 -0
- package/dist/seams/stubs.d.ts +34 -0
- package/dist/selection/child-runtime.d.ts +81 -0
- package/dist/selection/runtime-selection.d.ts +217 -0
- package/dist/selection/select-runtime.d.ts +213 -0
- package/dist/store/handoff-barrier.d.ts +238 -0
- package/dist/store/index.d.ts +11 -0
- package/dist/store/materialized-resume.d.ts +100 -0
- package/dist/store/pinned-probes.d.ts +17 -0
- package/dist/store/reconcile.d.ts +157 -0
- package/dist/store/temp-continuity.d.ts +92 -0
- package/dist/store/wiring.d.ts +250 -0
- package/dist/vendor-paths.d.ts +21 -0
- package/dist/version-matrix.d.ts +84 -0
- package/docs/conformance-rows.md +195 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yanlingLabs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# winter-runtime-sdk
|
|
2
|
+
|
|
3
|
+
One door over two agent runtimes. A host that wants both the Winter Agent SDK and the official
|
|
4
|
+
Claude Agent SDK talks to this package as a single SDK: the same `query()`, the same `Options`, the
|
|
5
|
+
same closed `SDKMessage` union — plus runtime-selection inputs.
|
|
6
|
+
|
|
7
|
+
Decision record: WS-00 D19 (2026-09-05). Boundaries that do not move:
|
|
8
|
+
|
|
9
|
+
- `@yanlinglabs/winter-agent-sdk` stands alone for Winter-only hosts and never learns this package
|
|
10
|
+
or the official runtime exists.
|
|
11
|
+
- This package is a selector and an adapter, never a translation layer: Options and the message
|
|
12
|
+
stream pass through verbatim. It owns runtime selection (the D13 rule), the official-SDK adapter
|
|
13
|
+
(Options template, spool env, supervised spawn proxy, mirror errors, tool aliases and deny floor,
|
|
14
|
+
builtin-path containment, Winter MCP plugin registration), shared session-store wiring, the
|
|
15
|
+
cross-runtime handoff barrier with the materialized-resume decoration doors, and the runtime
|
|
16
|
+
directory plus cross-runtime messaging router.
|
|
17
|
+
- The host vendors all three packages directly (`winter-runtime-sdk`, `winter-agent-sdk`,
|
|
18
|
+
`claude-agent-sdk`); this package declares the two SDKs as peer dependencies and receives their
|
|
19
|
+
module instances by injection, so a host that never creates a Claude session never loads the
|
|
20
|
+
official runtime and no SDK is ever instantiated twice.
|
|
21
|
+
- A `brand` profile flows through unchanged (Winter defaults); Claude Code's own literals stay fixed.
|
|
22
|
+
|
|
23
|
+
Status: Phase 7b, all four lanes landed and the door routed. The spine (the package scaffold, the
|
|
24
|
+
contract re-export, the `createRuntimeSdk` constructor with its version matrix, the seams, the test
|
|
25
|
+
harness and CI) and the four lanes behind those seams — the official-SDK adapter, the runtime
|
|
26
|
+
directory and messaging router, the store wiring and handoff barrier, and runtime selection — are on
|
|
27
|
+
`main`, with WS-17's eighteen router-owned rows proven and cited in `docs/conformance-rows.md`. See
|
|
28
|
+
`docs/architecture.md` for the ownership map, the pinned interfaces and how this package consumes the
|
|
29
|
+
Winter SDK before its first publish.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## The door: `query()` over both runtimes
|
|
34
|
+
|
|
35
|
+
`RuntimeSdk.query()` routes by the session's DECIDED `RuntimeSelection`, and each leg returns its own
|
|
36
|
+
runtime's handle untouched.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// The Winter leg: exactly what it always was. No runtime input, so nothing is decided and nothing
|
|
40
|
+
// is stripped — the caller's own `options` object is forwarded by reference.
|
|
41
|
+
for await (const message of sdk.query({ prompt: "hello" })) { /* SdkMessage */ }
|
|
42
|
+
|
|
43
|
+
// The official leg: a `claude-agent` selection, plus what only a host can answer.
|
|
44
|
+
const query = sdk.query({
|
|
45
|
+
prompt: turns, // string, or an AsyncIterable<string>
|
|
46
|
+
options: {
|
|
47
|
+
cwd: "/work/repo",
|
|
48
|
+
provider: { providerId: "anthropic", authRef: { kind: "keychain", account: "anthropic:default" } },
|
|
49
|
+
runtime: {
|
|
50
|
+
selection, // the session's PERSISTED choice (D13)
|
|
51
|
+
official: {
|
|
52
|
+
sessionId: "s-42", // its directory row is `session:s-42`
|
|
53
|
+
base: minimalOsEnvironmentFrom(process.env),
|
|
54
|
+
mcpServers: officialMcpServers({ /* … */ }),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**What the host passes, and why the Winter leg needs none of it.** A `winter-agent` session is served
|
|
62
|
+
in-process by an SDK that already reads everything it needs from `Options`. A `claude-agent` session is
|
|
63
|
+
a supervised CHILD PROCESS with a durable row of its own, so the door needs three things `Options` has
|
|
64
|
+
no field for: the **session id** its directory row is addressed by (WS-14 §6 rule 2's record is written
|
|
65
|
+
onto that address, and the messaging registry attaches under it), the **minimal OS environment**
|
|
66
|
+
(WS-14 §3's child env is a REPLACEMENT built from an allowlist — nothing inherits, so nothing is read
|
|
67
|
+
from `process.env` by this package, and a `base` without `HOME` is **refused**, because the runtime
|
|
68
|
+
resolves `os.homedir()` through the OS user database when it is missing and `CLAUDE_CONFIG_DIR` cannot
|
|
69
|
+
scope that), and the **vendored runtime path** (§5.1: never the user's
|
|
70
|
+
installed binary; give it once as `createRuntimeSdk({ vendoredOfficialRuntime })` or per query as
|
|
71
|
+
`options.pathToClaudeCodeExecutable`). Everything else has a default that is either derived from the
|
|
72
|
+
brand or read from the pinned contract you already fill in: credentials come from
|
|
73
|
+
`options.provider.authRef` through your own `KeychainSeam`, the spool from the resolved Winter home,
|
|
74
|
+
the session store from the one shared instance both branches use.
|
|
75
|
+
|
|
76
|
+
**What `SessionKey` a door-opened session has.** `sdk.handoff(session, to)` and every store-facing API
|
|
77
|
+
take a `SessionKey`, and both halves of it are chosen by the door rather than by you: `projectKey` is
|
|
78
|
+
the transcript project key the door set on the child (see "the transcript key" below) and `sessionId`
|
|
79
|
+
is the **backend uuid the vendor allocated**, not `runtime.official.sessionId`. The door records that
|
|
80
|
+
uuid on the session's directory row as `backendSessionId` the moment the runtime reports it at
|
|
81
|
+
`system/init`, so
|
|
82
|
+
`sdk.handoff({ projectKey, sessionId: (await sdk.directory.get("session:s-42"))!.backendSessionId! }, "winter-agent")`
|
|
83
|
+
is the route. WS-15 §6.2's cold resume of an exited official session reads the same field.
|
|
84
|
+
|
|
85
|
+
**The transcript key** (R-7b-13). The official child is given
|
|
86
|
+
`CLAUDE_CODE_PROJECT_DIR_NAME`, and it defaults to the Winter SDK's own
|
|
87
|
+
`transcriptProjectKey(options.cwd)` — read off the peer you injected, never re-derived — so both
|
|
88
|
+
branches write under one project directory for one working directory, and the auto-memory directory
|
|
89
|
+
(WS-14 §2's ONE shared directory) derives from the same key. It is also the `projectKey` half of the
|
|
90
|
+
`SessionKey` above. **The pinned runtime validates that variable against `^[A-Za-z0-9_-]{1,64}$` and
|
|
91
|
+
silently substitutes its own cwd-derived name when it does not match**, so the door refuses a key it
|
|
92
|
+
would reject — including its own default — rather than letting the row, the environment and the memory
|
|
93
|
+
directory name a transcript that is somewhere else. Two consequences for a host: a deep working
|
|
94
|
+
directory (a sanitized path over 64 characters) needs an explicit short `runtime.official.projectKey`,
|
|
95
|
+
and the vendor's own fallback key is built from the **realpath** of `cwd` (`/private/var/…` on macOS)
|
|
96
|
+
while `transcriptProjectKey` takes the path as given — so pass the key explicitly rather than relying
|
|
97
|
+
on either default when the two could differ.
|
|
98
|
+
|
|
99
|
+
**What the persisted selection means.** `runtime.selection` is "what this session's record says", so a
|
|
100
|
+
selection that DISAGREES with the record is a request to change runtime — and D13 answers that with
|
|
101
|
+
the certified handoff (`sdk.handoff(session, to)`) or a visible fork, never by serving the new runtime
|
|
102
|
+
on the old transcript. The door refuses with `RuntimeHandoffRequiredError`: in-process on both legs
|
|
103
|
+
(pass `runtime.sessionId` so it can hold you to it) and, on the official leg, against the DURABLE
|
|
104
|
+
directory row before a credential is read or a child spawns. On a session with no record yet, the
|
|
105
|
+
decided selection is PERSISTED at creation, by the door.
|
|
106
|
+
|
|
107
|
+
**What each leg returns.** The Winter peer's `Query` on one side and the official SDK's own `Query` on
|
|
108
|
+
the other — verbatim, both of them. They are different types (the Winter handle carries `messaging`
|
|
109
|
+
and `listModelFamilies`; the official one carries a dozen members this package deliberately never
|
|
110
|
+
names on its published surface), so `query()` is overloaded: a call with **no** `options.runtime` can
|
|
111
|
+
only reach the Winter leg and is typed `Query`, and a call that passes one is typed
|
|
112
|
+
`Query | OfficialQuery`. `isOfficialQuery(handle)` narrows it. On the official leg the launch happens
|
|
113
|
+
at the FIRST PULL — the same lazy spawn the vendor's own `query()` performs — because WS-14 §12's
|
|
114
|
+
"credentials are fetched at spawn" is asynchronous and `query()` returns a handle rather than a
|
|
115
|
+
promise for one; `close()` before the first pull starts nothing at all.
|
|
116
|
+
|
|
117
|
+
**A live session's input stream is how messages reach it.** R-7b-4: delivery into a live session of
|
|
118
|
+
either runtime is a push into that session's input stream. Pass an `AsyncIterable<string>` prompt and
|
|
119
|
+
the door owns that stream — your turns and the router's deliveries interleave in order, with your own
|
|
120
|
+
backpressure preserved — and the session is attached to `sdk.messaging` as a live receiver. Pass a
|
|
121
|
+
**string** prompt and the vendor runs one turn and exits: the session is still recorded in the
|
|
122
|
+
directory, but there is nothing to push into, so it is not attached and delivery to it answers
|
|
123
|
+
`unavailable` rather than pretending.
|
|
124
|
+
|
|
125
|
+
**A session's END is recorded too**, and it changes what a delivery gets. When the message stream
|
|
126
|
+
completes (or `close()` runs) the door detaches the handle, closes the stream and records the row
|
|
127
|
+
`exited` — `unavailable` on a stream that ended in a fault. So a streaming session whose input has
|
|
128
|
+
ended answers **`unavailable` (non-retryable)** exactly like a string-prompted one, never
|
|
129
|
+
`delivery_uncertain`: "the write may have landed" is not an honest answer for a session where nothing
|
|
130
|
+
can land. `sdk.messaging.listReachable` stops listing a session at the same moment (WS-10 §10.2: a
|
|
131
|
+
listing does not enumerate exited transcripts), and a launch that refuses synchronously leaves no row
|
|
132
|
+
at all.
|
|
133
|
+
|
|
134
|
+
**The official branch disables the runtime's remote feature configuration by default** (R-7b-11).
|
|
135
|
+
Every official child gets `TRAFFIC_OPT_OUT_VARIABLES` — the four names are exported, so read them
|
|
136
|
+
rather than trusting this sentence. Measured on the pin, same binary and same options: 25 advertised
|
|
137
|
+
tools with the fetch, 21 without; `DesignSync`, `Monitor`, `PushNotification` and
|
|
138
|
+
`advisor_20260301:advisor` appear only when a CDN answers. A tool surface that moves with no version
|
|
139
|
+
moving is not a pinned artifact, so this is on unless you say otherwise: `remoteConfig: "allow"` (per
|
|
140
|
+
query on `runtime.official`, or deployment-wide on `createRuntimeSdk({ official: { env: { … } } })`)
|
|
141
|
+
opts back in, and the choice is recorded on the session's directory row as
|
|
142
|
+
`RuntimeDirectoryEntry.remoteConfig`.
|
|
143
|
+
|
|
144
|
+
**The materialized-resume PREFERRED door is open for the pinned runtime, by measurement** (R-7b-12).
|
|
145
|
+
WS-17 §8's four probes pass against 0.3.250 on darwin-arm64 and linux-x64, so a handle over that peer
|
|
146
|
+
decorates the materialized copy and leaves the canonical file byte-pure; any other version — or no
|
|
147
|
+
official peer — gets the always-available FALLBACK door (one labelled entry appended after the
|
|
148
|
+
destination confirms). The verdict is data (`materializedResumeReportForPin`), keyed by version and
|
|
149
|
+
re-derived in CI against the real artifact, so a pin bump is a reviewed event rather than an inherited
|
|
150
|
+
answer. A host that measured its own pin passes `handoff: { decorationReport }` and wins.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## What a host needs to know before wiring this
|
|
155
|
+
|
|
156
|
+
Each of these is behaviour you cannot discover from the type signatures, and each has a test behind
|
|
157
|
+
it. They are stated here because a host that learns them from an incident learns them expensively.
|
|
158
|
+
|
|
159
|
+
**Inbound messaging to an official session is FAIL-CLOSED until you wire a permission class.** WS-10
|
|
160
|
+
§13 decides delivery from the sender's and receiver's permission classes, and the official runtime
|
|
161
|
+
exposes no way to read its own. So `official.permissionClass` — passed as
|
|
162
|
+
`createRuntimeSdk({ messaging: { messaging: { official: { permissionClass } } } })` — is the ONLY way
|
|
163
|
+
that class is ever known, and without it **every message to every official session is HELD**, never
|
|
164
|
+
delivered. The hold is *default-kind*: it is released the moment the class becomes knowable, and it
|
|
165
|
+
expires under §13's five-minute dialog window. Two things follow that you should design for: **the
|
|
166
|
+
expiry sweep is LAZY** — a held message's receipt is rewritten to `refused` when something next
|
|
167
|
+
addresses that receiver, not on a timer of its own, so between the fifth minute and the next send the
|
|
168
|
+
receipt still reads `held`; and **a hook that throws is an answer, not a crash** — it falls through to
|
|
169
|
+
`unknown`, which holds.
|
|
170
|
+
|
|
171
|
+
**Session-name leases are global by construction.** A display name that has been used is remembered
|
|
172
|
+
after the object is gone, so addressing it earns "that referred to something that has gone" rather
|
|
173
|
+
than "no such agent" (WS-10 §11 rule 5). Rule 5 governs children within their owning conversation;
|
|
174
|
+
sessions are global. Nothing forgets by itself: `RuntimeDirectoryOptions.retention` is where you set
|
|
175
|
+
how long a released lease and a receipted delivery record survive, and **absent means forever**,
|
|
176
|
+
because forgetting a lease changes what a model is told about a name it can no longer reach.
|
|
177
|
+
|
|
178
|
+
**The containment floor is a scan, not a sandbox.** The pre-hoc permission floor refuses any call
|
|
179
|
+
whose ARGUMENTS name a forbidden target (`CLAUDE.md`, `.claude/`, `~/.claude/plans`), case-folded and
|
|
180
|
+
NFKC-normalized for path fields and quote-stripped for command text. **Shell-escape and constructed
|
|
181
|
+
name spellings are caught POST-HOC**, by a sweep registered on `PostToolUse`, `PostToolUseFailure`
|
|
182
|
+
and `PostToolBatch` that snapshots the forbidden names under the session's cwd and the child's HOME,
|
|
183
|
+
removes what APPEARED under its roots during the call, records a typed breach and ends the turn — its
|
|
184
|
+
diff is TIME-BASED rather than causal, so under the child's HOME a vendor home created by something
|
|
185
|
+
else during a long call is removed and attributed to that call (narrow: an existing one is in every
|
|
186
|
+
baseline and is never touched). The sweep walks both
|
|
187
|
+
roots to a bounded depth (6 by default) around every filesystem-touching call, so **it costs a walk
|
|
188
|
+
per call**: on a large tree that is the dominant cost of the floor, and an incremental/fs-events
|
|
189
|
+
design is the follow-up. It sees the synchronously-visible effects of the call it brackets; a
|
|
190
|
+
background write that lands later is caught opportunistically by the next swept call.
|
|
191
|
+
|
|
192
|
+
**A host `PreToolUse` hook that answers `allow` makes 0.3.250 skip `canUseTool` for that call.** The
|
|
193
|
+
floor runs first and any deny wins, so containment is unaffected — but your broker will not see that
|
|
194
|
+
call, which matters if you were counting on it for audit.
|
|
195
|
+
|
|
196
|
+
**The extras door is a positive allowlist with two closed escape hatches.** `configuredExtras` admits
|
|
197
|
+
only names the pinned artifact's own environment registry declares AND that an independent rule
|
|
198
|
+
classifies as non-credential; anything auth-shaped is refused with a sentence naming why, and a
|
|
199
|
+
deployment that has REVIEWED a specific credential-shaped variable names it in
|
|
200
|
+
`reviewedCredentialShapedExtras` — one name at a time, never a wildcard.
|
|
201
|
+
|
|
202
|
+
A second class is refused **by name**, and the set is exported so you can read it rather than trust a
|
|
203
|
+
description: `EXECUTION_INDIRECTION_ENV_NAMES` and `EXECUTION_INDIRECTION_ENV_PREFIXES`
|
|
204
|
+
(`src/official/`). It is the **pinned artifact's own scrub list** — the environment the runtime strips
|
|
205
|
+
before running its policy helper, so the definition of "changes how the child executes code" is the
|
|
206
|
+
vendor's rather than ours — plus that runtime's own doors: `CLAUDE_CODE_SHELL` (the Bash tool's
|
|
207
|
+
shell), `CLAUDE_ENV_FILE` (sourced into every Bash call), the settings paths and plugin directories
|
|
208
|
+
(settings carry `hooks`, `apiKeyHelper` and `env`; plugins are code), the package-manager config files,
|
|
209
|
+
and the binary paths it executes. Whole prefixes are refused where a closed list cannot work:
|
|
210
|
+
`LD_*`, `DYLD_*`, `BASH_FUNC_*`, `PYTHON*`, `PERL5*`, `RUBY*`, `LUA_*`, `DOTNET_*`, `COR*`,
|
|
211
|
+
`APPDOMAIN_MANAGER_*` and **all of `GIT_*`** (git reads `GIT_CONFIG_*` for a `credential.helper` and
|
|
212
|
+
runs `GIT_SSH_COMMAND`, `GIT_EXTERNAL_DIFF` and `GIT_ASKPASS`).
|
|
213
|
+
|
|
214
|
+
Neither of the other two rules can see this class: these names are not credential-SHAPED, and the
|
|
215
|
+
pinned registry legitimately declares many of them, because the runtime really does read them — which
|
|
216
|
+
is why "the registry declares it" cannot be the whole test. Two of them were measured on the pin doing
|
|
217
|
+
exactly what the class describes before they were refused (a planted `CLAUDE_CODE_SHELL` ran as the
|
|
218
|
+
Bash tool's shell 114 times in one session; a planted `CLAUDE_ENV_FILE` was sourced into every Bash
|
|
219
|
+
call — `BASH_ENV` by another door). The reviewed door for this class is `reviewedExecutionExtras`,
|
|
220
|
+
again one name at a time, and a drift gate fails the suite when a pin bump adds a registry name of
|
|
221
|
+
this shape that nothing has classified.
|
|
222
|
+
|
|
223
|
+
**The approval bridge is fail-closed when no broker is configured.** A host MUST supply a broker: with
|
|
224
|
+
none, every call that reaches the bridge is denied. That is deliberate — the alternative is a session
|
|
225
|
+
that approves its own tool calls — and it means "I did not wire a broker yet" behaves like "deny", not
|
|
226
|
+
like "allow".
|
|
227
|
+
|
|
228
|
+
**`interrupt()` stops the turn.** On a streaming turn it ends the generation by throwing, which is
|
|
229
|
+
what a projector (Phase 8) must expect: an interrupted session's stream terminates with an error
|
|
230
|
+
rather than a result.
|
|
231
|
+
|
|
232
|
+
**A handoff will not check that the destination can serve the session unless you give it a catalog.**
|
|
233
|
+
`HandoffPlan.selection` reports `unreviewed` by default, because only the host holds the model catalog
|
|
234
|
+
and the credential map. Passing `createRuntimeSdk({ handoff: { selectionInputFor } })` turns it on:
|
|
235
|
+
`plan()` then asks the selector whether the RECORDED row is still servable and whether the destination
|
|
236
|
+
branch can serve it, and a plan that cannot be served carries a typed refusal instead — `execute()`
|
|
237
|
+
offers the lossy fork before the lease is taken, rather than after the drain and the staged copy.
|
|
238
|
+
|
|
239
|
+
**A handoff can deliberately leak one staging directory.** If the destination confirms init and the
|
|
240
|
+
producer record then fails to write, or if the destination throws while starting against the copy it
|
|
241
|
+
was handed, the `claude-resume-<uuid>` staging root SURVIVES — the destination may be reading it, and
|
|
242
|
+
deleting a live child's `CLAUDE_CONFIG_DIR` is worse than leaving a directory behind. It is locatable
|
|
243
|
+
at `outcome.target.stagingRoot` and belongs to your retention pass.
|
|
244
|
+
|
|
245
|
+
**"Exactly one runtime owns a session" is a convention here, not a mechanism.** The barrier moves
|
|
246
|
+
ownership only after the destination confirms, and the transcript's producer record is authoritative —
|
|
247
|
+
but the store's writer lease is re-entrant per pid and this router hosts both branches in one process,
|
|
248
|
+
so a determined host can still write from the source after a handoff. A real guard needs a
|
|
249
|
+
writer-lease generation the SDK store does not have; it is on the SDK's list.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ChildLike } from "@yanlinglabs/winter-agent-sdk/messaging";
|
|
2
|
+
import type { SeamContext } from "../seams/context.js";
|
|
3
|
+
import type { DirectoryResolution, DirectoryResolutionContext, RuntimeDirectory } from "../seams/directory.js";
|
|
4
|
+
import type { RuntimeDirectoryEntry } from "../seams/directory-store.js";
|
|
5
|
+
import type { DeliveryOutcome, GlobalAgentMessage, ListedRuntimeObject, SerializedRuntimeAddress } from "../seams/messaging-contract.js";
|
|
6
|
+
import { type RuntimeDirectoryRecoveryHooks, type RuntimeDirectoryRetention } from "./recovery.js";
|
|
7
|
+
/** What a caller may configure. Every field has an answer that is correct when it is absent. */
|
|
8
|
+
export interface RuntimeDirectoryOptions extends RuntimeDirectoryRecoveryHooks {
|
|
9
|
+
/** Injected so a test never races a real clock. */
|
|
10
|
+
now?: () => number;
|
|
11
|
+
/**
|
|
12
|
+
* How a `ChildLike` built from a directory row delivers, for the two doors that interface requires.
|
|
13
|
+
*
|
|
14
|
+
* The router core never uses them (it calls `adapter.steerChild`/`resumeChild` and reads `ChildLike`
|
|
15
|
+
* only for `status()` and its record) — see `entryToChildLike`. Absent means those two doors answer
|
|
16
|
+
* a typed non-retryable `unavailable` rather than throwing, so a caller that used the interface it
|
|
17
|
+
* was handed gets an outcome instead of a crash.
|
|
18
|
+
*/
|
|
19
|
+
deliverToChild?: (entry: RuntimeDirectoryEntry, message: GlobalAgentMessage) => Promise<DeliveryOutcome>;
|
|
20
|
+
/**
|
|
21
|
+
* WS-10 §13's caps on the two durable sinks, applied by `recover()` at step 6 (Lane B fix r1, n3).
|
|
22
|
+
*
|
|
23
|
+
* ABSENT MEANS BOTH SINKS KEEP EVERYTHING — see `RuntimeDirectoryRetention`: forgetting a released
|
|
24
|
+
* name lease changes what a model is told about a name it can no longer reach, so the router will
|
|
25
|
+
* not choose a horizon for a host that did not state one.
|
|
26
|
+
*/
|
|
27
|
+
retention?: RuntimeDirectoryRetention;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The directory, plus the two views the messaging router needs and the seam does not name.
|
|
31
|
+
*
|
|
32
|
+
* `RuntimeDirectory` is what `RuntimeSdk.directory` exposes; a host needs nothing more. The router
|
|
33
|
+
* lives in the same package and needs the SAME snapshot resolution used, or its listing and its
|
|
34
|
+
* resolution could disagree about a row that changed between two `load()` calls.
|
|
35
|
+
*/
|
|
36
|
+
export interface RuntimeDirectoryHandle extends RuntimeDirectory {
|
|
37
|
+
/** One consistent read of the store, with the caller-scoped views built from it. */
|
|
38
|
+
snapshot(caller: {
|
|
39
|
+
owningSessionId: string;
|
|
40
|
+
}): Promise<DirectorySnapshot>;
|
|
41
|
+
/** Resolve against an ALREADY-TAKEN snapshot — the router resolves and delivers over one read. */
|
|
42
|
+
resolveIn(snapshot: DirectorySnapshot, to: string, context: DirectoryResolutionContext): Promise<DirectoryResolution>;
|
|
43
|
+
}
|
|
44
|
+
export interface DirectorySnapshot {
|
|
45
|
+
/** Every entry the store held at the moment of the read. */
|
|
46
|
+
readonly entries: readonly RuntimeDirectoryEntry[];
|
|
47
|
+
readonly byAddress: ReadonlyMap<SerializedRuntimeAddress, RuntimeDirectoryEntry>;
|
|
48
|
+
/** WS-10 §11's resolution input: this caller's own children, as the shared `ChildLike` boundary. */
|
|
49
|
+
readonly children: readonly ChildLike[];
|
|
50
|
+
/** Every object this caller may ADDRESS (includes exited sessions — WS-15 §6.2 has a row for them). */
|
|
51
|
+
readonly resolvable: readonly ListedRuntimeObject[];
|
|
52
|
+
/** Every object a listing may SHOW (WS-10 §10.2 — never an exited session). */
|
|
53
|
+
readonly listable: readonly ListedRuntimeObject[];
|
|
54
|
+
}
|
|
55
|
+
export declare function createRuntimeDirectory(context: SeamContext, options?: RuntimeDirectoryOptions): RuntimeDirectoryHandle;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ChildLike } from "@yanlinglabs/winter-agent-sdk/messaging";
|
|
2
|
+
import type { RuntimeDirectoryEntry } from "../seams/directory-store.js";
|
|
3
|
+
import type { DeliveryOutcome, GlobalAgentMessage, ListedRuntimeObject, RuntimeAddress, SerializedRuntimeAddress } from "../seams/messaging-contract.js";
|
|
4
|
+
/**
|
|
5
|
+
* The session that OWNS an address (WS-10 §10.3, §11 rule 2).
|
|
6
|
+
*
|
|
7
|
+
* For a session that is the session itself; for an agent it is the parent, never the child — "a child
|
|
8
|
+
* is only addressable within its owning parent", and every fence in this package is that one
|
|
9
|
+
* sentence applied to a concrete pair of ids.
|
|
10
|
+
*/
|
|
11
|
+
export declare function owningSessionIdOf(address: RuntimeAddress): string;
|
|
12
|
+
/** WS-10 §11's listing row for one entry. The row IS the directory record — nothing is inferred. */
|
|
13
|
+
export declare function entryToListedRuntimeObject(entry: RuntimeDirectoryEntry): ListedRuntimeObject;
|
|
14
|
+
/** `starting`/`running` are live for resolution's purposes; everything else is terminal or worse. */
|
|
15
|
+
export declare function isLiveStatus(status: RuntimeDirectoryEntry["status"]): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* WS-10 §10.2's LISTING eligibility — deliberately narrower than resolution's.
|
|
18
|
+
*
|
|
19
|
+
* "It lists what `SendMessage` can currently reach — children of this parent, teammates, eligible
|
|
20
|
+
* LIVE peer sessions — and does NOT enumerate exited transcripts on disk." A terminal CHILD is still
|
|
21
|
+
* listed (it is resumable through its owner, and the subpath's own child row says so with
|
|
22
|
+
* `capabilities.resume`); an exited SESSION is not, because listing one is the enumeration that
|
|
23
|
+
* sentence forbids. Resolution still reaches an exited session by canonical address — WS-15 §6.2's
|
|
24
|
+
* routing table has a row for delivering to one — which is exactly why these are two functions and
|
|
25
|
+
* not one.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isListableFrom(entry: RuntimeDirectoryEntry, callerOwningSessionId: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* What RESOLUTION may see: every non-archived object this caller could legitimately address.
|
|
30
|
+
*
|
|
31
|
+
* An archived session "refuses until a deliberate user/product resume unarchives it" (WS-15 §6.2), so
|
|
32
|
+
* it is not resolvable at all — the refusal belongs at the delivery door only for objects a caller can
|
|
33
|
+
* name, and an archived row is not one of them.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isResolvableFrom(entry: RuntimeDirectoryEntry, callerOwningSessionId: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* A `ChildLike` view of a child entry, so WS-10 §11's SHARED resolution runs over directory rows.
|
|
38
|
+
*
|
|
39
|
+
* `steer`/`resume` are the two doors `ChildLike` requires. The router never calls them — the core's
|
|
40
|
+
* own `deliverEnvelope` calls `adapter.steerChild`/`adapter.resumeChild` and uses `ChildLike` only
|
|
41
|
+
* for `status()` and the three `record` fields — so they are wired to the caller's own delivery
|
|
42
|
+
* function rather than left as throws: a `ChildLike` that threw would be a trap for any future caller
|
|
43
|
+
* that reasonably used the interface it was handed.
|
|
44
|
+
*
|
|
45
|
+
* `permission.effectiveMode` is the one member with no source on a directory row, and it is
|
|
46
|
+
* UNOBSERVABLE by construction: the only place the subpath reads it is
|
|
47
|
+
* `childToListedRuntimeObject`'s `mode`, and this package never renders a child through that function
|
|
48
|
+
* — `resolve()` maps every candidate back to `entryToListedRuntimeObject`, which reads the entry's own
|
|
49
|
+
* `mode`. It is stated here rather than left to be discovered, because a future caller that DID
|
|
50
|
+
* render a `ChildLike` directly would otherwise show every child the same made-up mode.
|
|
51
|
+
*/
|
|
52
|
+
export declare function entryToChildLike(entry: RuntimeDirectoryEntry, deliver: (entry: RuntimeDirectoryEntry, msg: GlobalAgentMessage) => Promise<DeliveryOutcome>): ChildLike;
|
|
53
|
+
/**
|
|
54
|
+
* THE ONE FIELD-LEVEL MERGE RULE IN THIS PACKAGE, and it exists because two lanes write one row.
|
|
55
|
+
*
|
|
56
|
+
* Lane A's supervised spawn proxy records WS-14 §6 rule 2's observed `CLAUDE_CONFIG_DIR` and §9's
|
|
57
|
+
* pid-plus-start-identity onto the LAUNCHED SESSION's own directory entry, through the store's
|
|
58
|
+
* `upsert` — which is a full REPLACE. A host that later records a status change through
|
|
59
|
+
* `RuntimeDirectory.record()` builds its entry from what IT knows, which is never those two fields:
|
|
60
|
+
* a plain `upsert` would drop them, and WS-15 §6.4 step 2 (revalidate process identity) plus §6 rule
|
|
61
|
+
* 5 (clear the recorded root only after verified cleanup) would then be reading a row that lost its
|
|
62
|
+
* evidence — silently, with nothing failing at the time.
|
|
63
|
+
*
|
|
64
|
+
* SO THE RULE IS NARROW AND STATED: exactly the two ADAPTER-OWNED fields are carried forward when the
|
|
65
|
+
* incoming entry omits them. Everything else is the caller's, including a caller's `undefined` for a
|
|
66
|
+
* field it owns, because a merge that preserved every absent field would make a display name or a
|
|
67
|
+
* parent impossible to clear. Lane A's own `clear()` writes through the STORE (not through this
|
|
68
|
+
* door), so rule 5's clearing still works exactly as it did.
|
|
69
|
+
*/
|
|
70
|
+
export declare function mergeAdapterOwnedFields(incoming: RuntimeDirectoryEntry, existing: RuntimeDirectoryEntry | undefined): RuntimeDirectoryEntry;
|
|
71
|
+
/** Many rows, one mapping — so no caller ever writes the `.map(entryToListedRuntimeObject)` itself. */
|
|
72
|
+
export declare function entryToListedRuntimeObjectList(entries: readonly RuntimeDirectoryEntry[]): ListedRuntimeObject[];
|
|
73
|
+
/**
|
|
74
|
+
* The parent's canonical address for a child entry.
|
|
75
|
+
*
|
|
76
|
+
* `parentAddress` is optional on the record, so the fallback derives it from the child's own parsed
|
|
77
|
+
* address — through the SUBPATH's serializer, never a template literal, so this package has exactly
|
|
78
|
+
* one spelling of WS-10 §11's serialization.
|
|
79
|
+
*/
|
|
80
|
+
export declare function parentAddressOf(entry: RuntimeDirectoryEntry): SerializedRuntimeAddress | undefined;
|
|
81
|
+
/** The canonical address of a session id — the same one spelling. */
|
|
82
|
+
export declare function sessionAddressOf(winterSessionId: string): SerializedRuntimeAddress;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { RuntimeDirectoryRecovery } from "../seams/directory.js";
|
|
2
|
+
import type { RuntimeDirectoryEntry, RuntimeDirectoryStore } from "../seams/directory-store.js";
|
|
3
|
+
export interface RuntimeDirectoryRecoveryHooks {
|
|
4
|
+
/**
|
|
5
|
+
* WS-15 §6.4 step 2 / WS-14 §9: is the recorded `{pid, startedAt}` still that same process?
|
|
6
|
+
*
|
|
7
|
+
* A BARE PID IS NEVER ENOUGH — an OS recycles them — so an implementation must compare the start
|
|
8
|
+
* identity too, which is why the field is a pair. Absent: nothing revalidates.
|
|
9
|
+
*/
|
|
10
|
+
revalidateProcessIdentity?: (entry: RuntimeDirectoryEntry) => Promise<boolean> | boolean;
|
|
11
|
+
/**
|
|
12
|
+
* WS-15 §6.4 step 3: "where policy permits" — the host's call, never the router's.
|
|
13
|
+
*
|
|
14
|
+
* THIS HOOK IS THE READER OF §6 RULE 2's DURABLE ROOT (whole-branch review, F-5). `entry.configDir`
|
|
15
|
+
* is the `CLAUDE_CONFIG_DIR` the child was OBSERVED to get — for a store-backed resume, a
|
|
16
|
+
* `claude-resume-<uuid>` staging root that "the default spawner exposes no post-cleanup lookup
|
|
17
|
+
* for". The field exists precisely for the case where the process that knew it is gone, which is
|
|
18
|
+
* this one, and nothing inside this package can act on it: reconciling or cleaning a vendor root
|
|
19
|
+
* after a crash is a policy decision (WS-05 §12's barrier does it for a live handoff; a restart is
|
|
20
|
+
* the host's). So it travels on the entry, the step's own report counts the rows that carry one,
|
|
21
|
+
* and a host that reattaches is expected to read it.
|
|
22
|
+
*/
|
|
23
|
+
reattachSupervised?: (entry: RuntimeDirectoryEntry) => Promise<"reattached" | "skipped"> | "reattached" | "skipped";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* How long the two durable sinks REMEMBER, in milliseconds (Lane B fix r1, n3).
|
|
27
|
+
*
|
|
28
|
+
* BOTH DEFAULT TO "FOREVER" (`undefined`), and that is deliberate rather than lazy: forgetting is a
|
|
29
|
+
* product decision with a visible model-facing consequence, so the router will not make it for a host
|
|
30
|
+
* that did not ask. `nameLeases` in particular is exactly how long a stale name keeps answering "that
|
|
31
|
+
* referred to something that has gone" instead of "no such agent" (WS-10 §11 rule 5).
|
|
32
|
+
*
|
|
33
|
+
* Neither number can delete evidence: a claimed-but-unreceipted delivery (WS-15 §6.4 step 5) and a
|
|
34
|
+
* HELD lease are excluded by the stores' own doors, at every retention.
|
|
35
|
+
*/
|
|
36
|
+
export interface RuntimeDirectoryRetention {
|
|
37
|
+
/** Receipted delivery records older than this are dropped at recovery. Absent = kept forever. */
|
|
38
|
+
deliveries?: number;
|
|
39
|
+
/** Released name leases released longer ago than this are dropped. Absent = kept forever. */
|
|
40
|
+
nameLeases?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface RecoverDirectoryInput {
|
|
43
|
+
store: RuntimeDirectoryStore;
|
|
44
|
+
now: () => number;
|
|
45
|
+
hooks: RuntimeDirectoryRecoveryHooks;
|
|
46
|
+
/** WS-10 §13's caps, applied at step 6. Absent = both sinks keep everything. */
|
|
47
|
+
retention?: RuntimeDirectoryRetention;
|
|
48
|
+
}
|
|
49
|
+
export declare function recoverDirectory(input: RecoverDirectoryInput): Promise<RuntimeDirectoryRecovery>;
|