@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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { type BrandProfile } from "@yanlinglabs/winter-agent-sdk";
|
|
2
|
+
/** A JSON-Schema object, in the subset the descriptors use. */
|
|
3
|
+
export interface JsonSchemaObject {
|
|
4
|
+
type: "object";
|
|
5
|
+
properties: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
6
|
+
required?: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/** What a handler answers with — the MCP content shape, identical on both branches. */
|
|
9
|
+
export interface WinterMcpToolResult {
|
|
10
|
+
content: Array<{
|
|
11
|
+
type: "text";
|
|
12
|
+
text: string;
|
|
13
|
+
}>;
|
|
14
|
+
isError?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A tool handler, and the vendor's own second argument (item 15).
|
|
18
|
+
*
|
|
19
|
+
* `extra` IS FORWARDED, NOT DROPPED — but what it CARRIES is a measurement, not an assumption. The
|
|
20
|
+
* carry that produced this change asked for it so the official branch could derive WS-10 §12's retry
|
|
21
|
+
* key (the (session, tool-call id) pair a retry must allocate the SAME message id from), and the
|
|
22
|
+
* whole-branch review was right to say that must be checked first: the in-process server's `extra` is
|
|
23
|
+
* the MCP REQUEST CONTEXT — the JSON-RPC request id and `_meta` — which is not the model's
|
|
24
|
+
* `tool_use_id`. `test/official/runtime-aliases.test.ts` records what the pinned runtime actually
|
|
25
|
+
* puts there; see that test and this module's own note below for the answer.
|
|
26
|
+
*
|
|
27
|
+
* Forwarding it is worth doing either way: it is the only channel the vendor gives a tool for request
|
|
28
|
+
* context, dropping it is unrecoverable at the handler, and a handler that does not want it simply
|
|
29
|
+
* declares one parameter.
|
|
30
|
+
*/
|
|
31
|
+
export type WinterMcpHandler = (args: unknown, extra?: unknown) => Promise<WinterMcpToolResult>;
|
|
32
|
+
/** One tool on the standing server. Everything a branch needs to register it, and nothing branch-specific. */
|
|
33
|
+
export interface WinterMcpToolDescriptor {
|
|
34
|
+
/** The bare tool name (`send_message`); the canonical name is derived from it and the brand. */
|
|
35
|
+
tool: string;
|
|
36
|
+
description: string;
|
|
37
|
+
inputSchema: JsonSchemaObject;
|
|
38
|
+
outputSchema?: JsonSchemaObject;
|
|
39
|
+
annotations?: Readonly<Record<string, unknown>>;
|
|
40
|
+
/** WS-09 §10's exposure map. The canonical twins of aliased built-ins are deferred (§7). */
|
|
41
|
+
exposure: "eager" | "deferred" | "hidden";
|
|
42
|
+
/** The permission identity both branches must agree on (WS-07). */
|
|
43
|
+
permissionClass: string;
|
|
44
|
+
handler: WinterMcpHandler;
|
|
45
|
+
}
|
|
46
|
+
/** The standing server, as one descriptor. `name` is the brand's, so `mcp__<name>__<tool>` follows. */
|
|
47
|
+
export interface WinterMcpServerDescriptor {
|
|
48
|
+
name: string;
|
|
49
|
+
version: string;
|
|
50
|
+
tools: readonly WinterMcpToolDescriptor[];
|
|
51
|
+
}
|
|
52
|
+
/** The two messaging handlers §7's aliases resolve to. Their implementations are the router's. */
|
|
53
|
+
export interface MessagingHandlers {
|
|
54
|
+
sendMessage: WinterMcpHandler;
|
|
55
|
+
listAgents: WinterMcpHandler;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The messaging tools, with the NATIVE schemas mirrored (§7: "handlers MUST accept the native
|
|
59
|
+
* argument schemas exactly").
|
|
60
|
+
*
|
|
61
|
+
* `deferred` on both: they are the canonical twins of aliased built-ins, and §7 wants the model to
|
|
62
|
+
* see one `SendMessage`, not two.
|
|
63
|
+
*/
|
|
64
|
+
export declare function messagingToolDescriptors(handlers: MessagingHandlers): readonly WinterMcpToolDescriptor[];
|
|
65
|
+
/**
|
|
66
|
+
* §11's refusal: no advisor on this server, on this branch.
|
|
67
|
+
*
|
|
68
|
+
* Thrown rather than filtered. A silent filter would leave a host believing its advisor was
|
|
69
|
+
* registered and wondering why the model never calls it; the whole point of D29's split is that each
|
|
70
|
+
* branch's advisor has a DIFFERENT backing, and a host that tried to register one here has a
|
|
71
|
+
* misunderstanding worth surfacing.
|
|
72
|
+
*/
|
|
73
|
+
export declare function assertNoAdvisor(tools: readonly WinterMcpToolDescriptor[], branchLabel: string): void;
|
|
74
|
+
/**
|
|
75
|
+
* Builds the standing server descriptor for a session.
|
|
76
|
+
*
|
|
77
|
+
* The capability plugins (browser, computer, office) are passed IN rather than declared here: WS-06
|
|
78
|
+
* owns their exact names and schemas, and a copy of them in the router would be a second declaration
|
|
79
|
+
* to drift. What the router owns is that they are registered under the same canonical names, with the
|
|
80
|
+
* same identity, on both branches — which is what `canonicalToolNames` below makes checkable.
|
|
81
|
+
*/
|
|
82
|
+
export declare function winterMcpServerDescriptor(args: {
|
|
83
|
+
brand: Pick<BrandProfile, "mcpServerName">;
|
|
84
|
+
messaging: MessagingHandlers;
|
|
85
|
+
capabilities?: readonly WinterMcpToolDescriptor[];
|
|
86
|
+
version?: string;
|
|
87
|
+
branchLabel: string;
|
|
88
|
+
}): WinterMcpServerDescriptor;
|
|
89
|
+
/** The canonical names a descriptor registers — the identity both branches must agree on. */
|
|
90
|
+
export declare function canonicalToolNames(descriptor: WinterMcpServerDescriptor, brand: Pick<BrandProfile, "mcpServerName">): readonly string[];
|
|
91
|
+
/** The subset of the injected official module this needs. Duck-typed: the seam declares only `query`. */
|
|
92
|
+
export interface OfficialMcpModule {
|
|
93
|
+
createSdkMcpServer?: (options: {
|
|
94
|
+
name: string;
|
|
95
|
+
version?: string;
|
|
96
|
+
tools?: unknown[];
|
|
97
|
+
instructions?: string;
|
|
98
|
+
}) => unknown;
|
|
99
|
+
tool?: (name: string, description: string, inputSchema: unknown, handler: (args: unknown, extra: unknown) => Promise<unknown>, extras?: unknown) => unknown;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Converts a JSON-Schema object into whatever the official in-process server constructor expects.
|
|
103
|
+
*
|
|
104
|
+
* INJECTED, NOT IMPLEMENTED HERE — see this module's header. A host writes it once against the
|
|
105
|
+
* validator it already has as the official SDK's peer.
|
|
106
|
+
*/
|
|
107
|
+
export type InputShapeFactory = (schema: JsonSchemaObject) => unknown;
|
|
108
|
+
/**
|
|
109
|
+
* Registers the standing server into the official branch, returning the `mcpServers` entry.
|
|
110
|
+
*
|
|
111
|
+
* A MISSING CONSTRUCTOR IS A TYPED FAILURE. An injected module without `createSdkMcpServer` is a
|
|
112
|
+
* module this branch cannot register a server into at all, and continuing would produce a session
|
|
113
|
+
* whose aliases resolve to a tool that does not exist — the model's `SendMessage` would fail at the
|
|
114
|
+
* one moment it matters (WS-17 rows 1–2).
|
|
115
|
+
*/
|
|
116
|
+
export declare function materializeOfficialMcpServer(args: {
|
|
117
|
+
descriptor: WinterMcpServerDescriptor;
|
|
118
|
+
module: OfficialMcpModule;
|
|
119
|
+
toInputShape: InputShapeFactory;
|
|
120
|
+
branchLabel: string;
|
|
121
|
+
}): unknown;
|
|
122
|
+
/** `Options.mcpServers` for this branch: one entry, keyed by the brand's own server name. */
|
|
123
|
+
/** The descriptor fields the official branch's registration cannot carry (review r1, n3). */
|
|
124
|
+
export declare const OFFICIAL_MATERIALIZATION_DROPS: readonly string[];
|
|
125
|
+
export declare function officialMcpServers(args: {
|
|
126
|
+
descriptor: WinterMcpServerDescriptor;
|
|
127
|
+
module: OfficialMcpModule;
|
|
128
|
+
toInputShape: InputShapeFactory;
|
|
129
|
+
branchLabel: string;
|
|
130
|
+
}): Record<string, unknown>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { mcpToolName, type BrandProfile } from "@yanlinglabs/winter-agent-sdk";
|
|
2
|
+
import type { OfficialOptions } from "../seams/official-sdk-shapes.js";
|
|
3
|
+
import type { OptionsTemplateInput } from "../seams/official-adapter.js";
|
|
4
|
+
import { type OfficialPermissionMode } from "./callbacks.js";
|
|
5
|
+
import type { OfficialApprovalBridge } from "./callbacks.js";
|
|
6
|
+
import { type ContainmentPolicy } from "./containment.js";
|
|
7
|
+
/** The vendor's own preset name — a Claude-mirroring literal, fixed (WS-01 §5 / D16). */
|
|
8
|
+
export declare const PINNED_SYSTEM_PROMPT_PRESET = "claude_code";
|
|
9
|
+
/**
|
|
10
|
+
* §2's pinned auto-memory load cap, "kept in the compatibility profile".
|
|
11
|
+
*
|
|
12
|
+
* DATA, NOT AN OPTION: the pinned runtime exposes no knob for it, so the cap is something the HOST
|
|
13
|
+
* applies when it writes the shared memory file. It lives here because this is where a reader looks
|
|
14
|
+
* for the memory contract, and because a number that exists only in a spec sentence is a number that
|
|
15
|
+
* gets rounded.
|
|
16
|
+
*/
|
|
17
|
+
export declare const AUTO_MEMORY_LOAD_CAP: {
|
|
18
|
+
readonly lines: 200;
|
|
19
|
+
readonly bytes: number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* §16 OPEN QUESTION 1, FIXED HERE, PER MODE, WITH THE REASON.
|
|
23
|
+
*
|
|
24
|
+
* "`excludeDynamicSections` default: report §124 shows `true` in the safe template; dynamic sections
|
|
25
|
+
* carry env/context blocks the host may want. The per-mode default is left to the mode-host policy
|
|
26
|
+
* and MUST BE FIXED BEFORE GOLDEN CAPTURES FREEZE."
|
|
27
|
+
*
|
|
28
|
+
* FIXED AT `true` FOR EVERY MODE. The argument is §4's, not a preference: "whether project context is
|
|
29
|
+
* appended to the system prompt or injected as a Winter-owned context message is a single host-wide
|
|
30
|
+
* choice, and BOTH BRANCHES MUST USE THE SAME CHOSEN SEMANTICS SO THEY AGREE WITH EACH OTHER". The
|
|
31
|
+
* vendor's dynamic sections are assembled by the vendor runtime from its own view of the environment
|
|
32
|
+
* and its own project-context conventions; the other branch cannot reproduce them, so leaving them on
|
|
33
|
+
* makes the two branches disagree in the one place a handoff makes visible — mid-session, to a user
|
|
34
|
+
* who just switched runtimes. The deterministic append (§4's ordered build) is what carries the
|
|
35
|
+
* context instead, identically on both.
|
|
36
|
+
*
|
|
37
|
+
* A HOST CAN STILL OVERRIDE, per mode, and the golden captures show which value each mode was built
|
|
38
|
+
* with — so a change is a diff rather than a discovery.
|
|
39
|
+
*/
|
|
40
|
+
export declare const DEFAULT_EXCLUDE_DYNAMIC_SECTIONS: Readonly<Record<OptionsTemplateInput["mode"], boolean>>;
|
|
41
|
+
/** Host policy for the template — everything §2 leaves to the mode host (WS-15) or to the session. */
|
|
42
|
+
export interface OptionsTemplatePolicy {
|
|
43
|
+
/** §4's deterministically-built instructions, appended to the pinned preset. */
|
|
44
|
+
systemPromptAppend?: string;
|
|
45
|
+
/** §16 q1. Absent → `DEFAULT_EXCLUDE_DYNAMIC_SECTIONS` for the mode. */
|
|
46
|
+
excludeDynamicSections?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* §5: default `"batched"`; sessions that ADVERTISE CROSS-RUNTIME HANDOFF must use `"eager"` — it
|
|
49
|
+
* narrows the mirror-lag window. Derived from `advertisesHandoff` rather than set directly, so the
|
|
50
|
+
* rule is expressed once and cannot be half-applied.
|
|
51
|
+
*/
|
|
52
|
+
advertisesHandoff?: boolean;
|
|
53
|
+
/** Additional flag-layer settings the host has already checked against the runtime's schema (§2). */
|
|
54
|
+
settings?: Readonly<Record<string, unknown>>;
|
|
55
|
+
/** §11's server entry, already materialized. */
|
|
56
|
+
mcpServers?: Readonly<Record<string, unknown>>;
|
|
57
|
+
/** §10's bridge. Absent -> a fail-closed one is installed, because the invariants require one. */
|
|
58
|
+
canUseTool?: OfficialApprovalBridge;
|
|
59
|
+
/** The session's permission mode, for the fail-closed bridge the template installs. */
|
|
60
|
+
permissionMode?: OfficialPermissionMode;
|
|
61
|
+
/** §10's hook bridge (WS-08 owns its contract). */
|
|
62
|
+
hooks?: unknown;
|
|
63
|
+
/** §3's built child environment. Built by `buildChildEnv`, because only it has the credentials. */
|
|
64
|
+
env?: Readonly<Record<string, string>>;
|
|
65
|
+
/** The creation transaction's pre-allocated backend id (WS-16). */
|
|
66
|
+
sessionId?: string;
|
|
67
|
+
/** Resume/fork (§5.1): a fork registers as a NEW record and inherits no handoff certification. */
|
|
68
|
+
resume?: string;
|
|
69
|
+
forkSession?: boolean;
|
|
70
|
+
containment?: ContainmentPolicy;
|
|
71
|
+
/** §5.1: the ONLY documented `extraArgs` route. Anything else is refused. */
|
|
72
|
+
appendSystemPromptFile?: string;
|
|
73
|
+
/** Extra tool names this deployment denies, merged with the containment floor's own list. */
|
|
74
|
+
additionalDisallowedTools?: readonly string[];
|
|
75
|
+
}
|
|
76
|
+
/** Puts the containment matchers FIRST, then whatever the host installed for the same events. */
|
|
77
|
+
export declare function mergeHooks(ours: Record<string, unknown[]>, hostHooks: unknown): Record<string, unknown[]>;
|
|
78
|
+
/** The settings the flag layer must carry for this branch to behave (see this module's header). */
|
|
79
|
+
export declare function brandedFlagSettings(args: {
|
|
80
|
+
brand: Pick<BrandProfile, "projectDirName">;
|
|
81
|
+
autoMemoryDirectory: string;
|
|
82
|
+
extra?: Readonly<Record<string, unknown>>;
|
|
83
|
+
}): Record<string, unknown>;
|
|
84
|
+
/**
|
|
85
|
+
* Builds §2's template.
|
|
86
|
+
*
|
|
87
|
+
* Nothing here reads ambient state: every value is either an argument, a brand derivation, or a
|
|
88
|
+
* constant this file declares. That is what makes the golden captures meaningful — two runs of the
|
|
89
|
+
* same input produce the same object, on any machine.
|
|
90
|
+
*/
|
|
91
|
+
export declare function buildOfficialOptions(input: OptionsTemplateInput, policy?: OptionsTemplatePolicy): OfficialOptions;
|
|
92
|
+
/**
|
|
93
|
+
* §5.1's withheld options and §2's required ones, as refusals.
|
|
94
|
+
*
|
|
95
|
+
* WHY A VALIDATOR AND NOT JUST A CAREFUL BUILDER: `launch()` accepts an options object the CALLER
|
|
96
|
+
* built (the seam's `OfficialLaunchPlan.options`), so the builder's care protects only the callers who
|
|
97
|
+
* used it. These are the invariants that must hold for anything this adapter launches.
|
|
98
|
+
*/
|
|
99
|
+
export declare function assertOptionsInvariants(options: OfficialOptions, branchLabel: string): void;
|
|
100
|
+
/**
|
|
101
|
+
* A golden-capturable view of a built options object (§15's "options-template golden captures").
|
|
102
|
+
*
|
|
103
|
+
* FUNCTIONS AND INSTANCES BECOME MARKERS, because their identity is not stable across runs and their
|
|
104
|
+
* bodies are not what the capture is about. What the capture pins is the SHAPE: which fields are set,
|
|
105
|
+
* to which literal values, with which brand-derived names — and, for the three things that are
|
|
106
|
+
* objects with behaviour, that they are present and which one they are.
|
|
107
|
+
*/
|
|
108
|
+
export declare function captureOptions(options: OfficialOptions): Record<string, unknown>;
|
|
109
|
+
/** The canonical name an aliased built-in resolves to — re-exported so a capture reader has one import. */
|
|
110
|
+
export { mcpToolName };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
import type { BrandProfile } from "@yanlinglabs/winter-agent-sdk";
|
|
3
|
+
import type { RuntimeDirectoryEntry, RuntimeDirectoryStore } from "../seams/directory-store.js";
|
|
4
|
+
import type { SerializedRuntimeAddress } from "../seams/messaging-contract.js";
|
|
5
|
+
import type { OfficialLaunchProfile } from "../seams/official-adapter.js";
|
|
6
|
+
import type { OfficialSpawnClaudeCodeProcess, OfficialSpawnOptions } from "../seams/official-sdk-shapes.js";
|
|
7
|
+
import { type OfficialBranchError } from "./errors.js";
|
|
8
|
+
import { type ObservedLocalWriteRoot } from "./spool.js";
|
|
9
|
+
/** WS-14 §9: "PID **plus process start identity** (never bare PID)" — an OS recycles pids. */
|
|
10
|
+
export interface ProcessIdentity {
|
|
11
|
+
pid: number;
|
|
12
|
+
/** ISO-8601. The pair is the identity; either half alone revalidates a stranger. */
|
|
13
|
+
startedAt: string;
|
|
14
|
+
}
|
|
15
|
+
/** What the proxy observed about one generation, and what §6 rule 2 requires to be durable. */
|
|
16
|
+
export interface SpawnObservation {
|
|
17
|
+
root: ObservedLocalWriteRoot;
|
|
18
|
+
processIdentity: ProcessIdentity;
|
|
19
|
+
command: string;
|
|
20
|
+
args: readonly string[];
|
|
21
|
+
cwd?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Where the observation is written durably.
|
|
25
|
+
*
|
|
26
|
+
* A SEAM RATHER THAN A DIRECT STORE CALL, because the sink differs by caller: the adapter's default
|
|
27
|
+
* writes `RuntimeDirectoryEntry.configDir`/`processIdentity` through the spine's directory store
|
|
28
|
+
* (`directoryRecordSink` below), a host with its own record writes there, and a test records into an
|
|
29
|
+
* array. `clear` is §6 rule 5's other half and is optional: a sink that cannot forget is still a
|
|
30
|
+
* legal sink, it just keeps a stale root.
|
|
31
|
+
*/
|
|
32
|
+
export interface SpawnRecordSink {
|
|
33
|
+
record(observation: SpawnObservation): Promise<void> | void;
|
|
34
|
+
clear?(observation: SpawnObservation): Promise<void> | void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* §6 rule 3's collaborator: transcript-only reconciliation (or a quarantine snapshot) of the recorded
|
|
38
|
+
* root, run BEFORE the exit is forwarded.
|
|
39
|
+
*
|
|
40
|
+
* DEFAULT IS A NO-OP, deliberately. The reconciler belongs to the store lane (WS-05 §12's barrier);
|
|
41
|
+
* the proxy owns only the ORDERING, and an ordering with nothing in the middle is still the correct
|
|
42
|
+
* ordering. A no-op that runs at the right moment is honest; a proxy that refused to spawn without a
|
|
43
|
+
* reconciler would make the ordering untestable until another lane lands.
|
|
44
|
+
*
|
|
45
|
+
* "Reconciliation touches transcript/subagent files ONLY" (rule 4) — the proxy hands the reconciler
|
|
46
|
+
* the root and the exit, never the environment it was spawned with.
|
|
47
|
+
*/
|
|
48
|
+
export type TranscriptReconcile = (input: {
|
|
49
|
+
observation: SpawnObservation;
|
|
50
|
+
exit: {
|
|
51
|
+
code: number | null;
|
|
52
|
+
signal: string | null;
|
|
53
|
+
};
|
|
54
|
+
}) => Promise<void> | void;
|
|
55
|
+
/** The child process handle the proxy wraps — the members it drives, in Node's own shape. */
|
|
56
|
+
export interface SpawnedChildProcess {
|
|
57
|
+
readonly pid?: number | undefined;
|
|
58
|
+
stdin: Writable | null;
|
|
59
|
+
stdout: Readable | null;
|
|
60
|
+
stderr: Readable | null;
|
|
61
|
+
kill(signal?: string): boolean;
|
|
62
|
+
on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
63
|
+
on(event: "error", listener: (error: Error) => void): unknown;
|
|
64
|
+
}
|
|
65
|
+
/** How the proxy actually starts a process. Injected so a test never spawns a real binary. */
|
|
66
|
+
export type SpawnChild = (options: OfficialSpawnOptions & {
|
|
67
|
+
argv0: string;
|
|
68
|
+
}) => SpawnedChildProcess;
|
|
69
|
+
/** The proxy plus the handles a launch needs to read what it observed. */
|
|
70
|
+
export interface SupervisedSpawnProxy {
|
|
71
|
+
/** The hook itself, in the official runtime's own shape. */
|
|
72
|
+
readonly spawn: OfficialSpawnClaudeCodeProcess;
|
|
73
|
+
/** The observation of the most recent spawn, once it has happened. */
|
|
74
|
+
readonly observation: SpawnObservation | undefined;
|
|
75
|
+
/** Resolves when §6 rule 2's record has settled (rejects if it failed). */
|
|
76
|
+
whenRecorded(): Promise<void>;
|
|
77
|
+
/** Resolves when the exit gate has opened — reconciliation done, cleanup verified, record cleared. */
|
|
78
|
+
whenSettled(): Promise<void>;
|
|
79
|
+
/** Everything stderr produced, bounded (§6 rule 6). */
|
|
80
|
+
readonly stderrTail: string;
|
|
81
|
+
}
|
|
82
|
+
export interface SupervisedSpawnProxyOptions {
|
|
83
|
+
brand: Pick<BrandProfile, "processLabel" | "envPrefix">;
|
|
84
|
+
/** Which profile this generation was launched as, and what config dir it was configured with (§1). */
|
|
85
|
+
profile: OfficialLaunchProfile;
|
|
86
|
+
configuredConfigDir: string;
|
|
87
|
+
sink: SpawnRecordSink;
|
|
88
|
+
reconcile?: TranscriptReconcile;
|
|
89
|
+
spawnChild?: SpawnChild;
|
|
90
|
+
/** Verified cleanup (§6 rule 5). Default: the recorded root no longer exists on disk. */
|
|
91
|
+
verifyCleanup?: (observation: SpawnObservation) => Promise<boolean> | boolean;
|
|
92
|
+
/** Typed crash classes are handed here as they are classified (§6 rule 6, §9). */
|
|
93
|
+
onCrash?: (error: OfficialBranchError) => void;
|
|
94
|
+
/** Bound on the retained stderr (a runaway child must not turn a message into a leak). */
|
|
95
|
+
stderrTailBytes?: number;
|
|
96
|
+
/**
|
|
97
|
+
* How long §6 rule 2's record may take to settle before the generation is ended (review r1, M4).
|
|
98
|
+
*
|
|
99
|
+
* The module always handled a record that FAILS; it did not handle one that HANGS, and a hanging
|
|
100
|
+
* sink left the child alive, silent and unobservable forever — no bytes, no exit, no error. Every
|
|
101
|
+
* wait in a supervisor is bounded or it is a hang with better manners.
|
|
102
|
+
*/
|
|
103
|
+
recordTimeoutMs?: number;
|
|
104
|
+
/**
|
|
105
|
+
* How long after the child's exit the gate waits for stdout to close (review r1, M4).
|
|
106
|
+
*
|
|
107
|
+
* The gate needs BOTH the exit and the stdout end, because a transcript's last frames arrive on the
|
|
108
|
+
* way out. A child that exits with its pipe held open (a surviving grandchild inherits it) would
|
|
109
|
+
* otherwise stall the gate forever: reconciliation never running, `whenSettled()` never resolving.
|
|
110
|
+
* After this grace the exit is forwarded anyway and the `stdout-unterminated` crash class says so.
|
|
111
|
+
*/
|
|
112
|
+
stdoutGraceMs?: number;
|
|
113
|
+
now?: () => Date;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Kept as a no-op-safe warm-up so an existing caller keeps working; nothing REQUIRES it any more.
|
|
117
|
+
*
|
|
118
|
+
* A host that wants the resolution cost paid at construction rather than at the first spawn can still
|
|
119
|
+
* call it, and the adapter does.
|
|
120
|
+
*/
|
|
121
|
+
export declare function prepareDefaultSpawn(): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Builds the supervised proxy (§6). One proxy per GENERATION: it holds that generation's observation,
|
|
124
|
+
* its stderr tail and its exit gate, and none of those are meaningful across two children.
|
|
125
|
+
*/
|
|
126
|
+
export declare function createSupervisedSpawnProxy(options: SupervisedSpawnProxyOptions): SupervisedSpawnProxy;
|
|
127
|
+
/**
|
|
128
|
+
* The default sink: §6 rule 2's record written onto the directory entry the spine defined for it.
|
|
129
|
+
*
|
|
130
|
+
* `RuntimeDirectoryEntry.configDir` and `.processIdentity` exist precisely for this (spine fix round
|
|
131
|
+
* 2, NEW-1: "WRITTEN BY Lane A's spawn proxy before it returns the process; READ BY Lane B's
|
|
132
|
+
* `recover()`"), and rule 5's clear is an upsert WITHOUT the two keys — the spine's in-memory store
|
|
133
|
+
* treats an absent key as absent rather than as `undefined`, which is what makes "cleared" a state a
|
|
134
|
+
* reader can see.
|
|
135
|
+
*
|
|
136
|
+
* A MISSING ENTRY IS AN ERROR, not a silent no-op: the address is handed in by the launch, so an
|
|
137
|
+
* entry that is not there means the caller recorded the session under a different address and the
|
|
138
|
+
* root would be written nowhere.
|
|
139
|
+
*/
|
|
140
|
+
export declare function directoryRecordSink(args: {
|
|
141
|
+
store: RuntimeDirectoryStore;
|
|
142
|
+
address: SerializedRuntimeAddress;
|
|
143
|
+
seed?: () => RuntimeDirectoryEntry;
|
|
144
|
+
}): SpawnRecordSink;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { BrandProfile } from "@yanlinglabs/winter-agent-sdk";
|
|
2
|
+
import type { OfficialLaunchProfile } from "../seams/official-adapter.js";
|
|
3
|
+
/**
|
|
4
|
+
* The spool's path segments under the brand home.
|
|
5
|
+
*
|
|
6
|
+
* BRAND-NEUTRAL BY CONSTRUCTION: the product-owned part of the spool path is the HOME DIRECTORY
|
|
7
|
+
* itself, which the caller passes in already resolved from `brand.homeDirName` (review r1, n4: the
|
|
8
|
+
* previous sentence spelled one product's home in prose, and the brand gate's own header says a raw
|
|
9
|
+
* occurrence includes comments even where its regex cannot see one).
|
|
10
|
+
* These two segments name a ROLE ("the runtimes we host", "the spool the official agent lives in"),
|
|
11
|
+
* so a reuser inherits them unchanged and the brand gate has nothing to match.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SPOOL_SEGMENTS: readonly ["runtimes", "official-agent-spool"];
|
|
14
|
+
/** WS-16's `activeLocalWriteRoot` kinds — one per launch profile. */
|
|
15
|
+
export type LocalWriteRootKind = "official-spool" | "sdk-resume-staging";
|
|
16
|
+
/**
|
|
17
|
+
* The durable record §6 rule 2 demands: WHICH root this generation actually got, and of which kind.
|
|
18
|
+
*
|
|
19
|
+
* The pair travels together because neither half is derivable from the other at the point it matters:
|
|
20
|
+
* a reconciler holding only a path cannot tell a spool from a staging root it must not delete, and a
|
|
21
|
+
* reconciler holding only a kind has nothing to open.
|
|
22
|
+
*/
|
|
23
|
+
export interface ObservedLocalWriteRoot {
|
|
24
|
+
configDir: string;
|
|
25
|
+
kind: LocalWriteRootKind;
|
|
26
|
+
profile: OfficialLaunchProfile;
|
|
27
|
+
}
|
|
28
|
+
/** `<home>/runtimes/official-agent-spool` — profile 1's configured value (§1, §3). */
|
|
29
|
+
export declare function officialSpoolRoot(home: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Classifies an OBSERVED `CLAUDE_CONFIG_DIR`.
|
|
32
|
+
*
|
|
33
|
+
* The classification is by SHAPE, not by which profile we asked for, because the whole point of §1's
|
|
34
|
+
* authority rule is that the two can disagree: a session configured `fresh-spool` that resumes out of
|
|
35
|
+
* the store is handed a staging root by the wrapper, and the record has to say `sdk-resume-staging`
|
|
36
|
+
* or the reconciler will look in the wrong place after a crash.
|
|
37
|
+
*/
|
|
38
|
+
export declare function classifyLocalWriteRoot(configDir: string): ObservedLocalWriteRoot;
|
|
39
|
+
/**
|
|
40
|
+
* §6 rule 2's "VALIDATE and durably record": the checks that run before the record is written.
|
|
41
|
+
*
|
|
42
|
+
* Two refusals, and each is a real failure mode rather than defensive noise:
|
|
43
|
+
*
|
|
44
|
+
* * AN ABSENT VALUE means the child is about to write its transcript somewhere we did not choose
|
|
45
|
+
* and cannot name — under the SDK parent's own `CLAUDE_CONFIG_DIR`, or `~/.claude` when there is
|
|
46
|
+
* none. That is the exact leak §3 forbids ("any variable pointing into `~/.claude`" must not
|
|
47
|
+
* appear) and it is unrecoverable after the fact, so it fails the spawn.
|
|
48
|
+
* * A `fresh-spool` GENERATION THAT DID NOT GET THE SPOOL is either a wrapper we do not understand
|
|
49
|
+
* or an env allowlist that was overwritten downstream. Both mean the recorded root and the real
|
|
50
|
+
* one have diverged, which is the corruption §1 says breaks crash recovery.
|
|
51
|
+
*
|
|
52
|
+
* A `store-backed-resume` generation is deliberately NOT required to match anything: the staging root
|
|
53
|
+
* is the wrapper's own uuid and we could not have predicted it. It only has to LOOK like one.
|
|
54
|
+
*/
|
|
55
|
+
export declare function validateObservedConfigDir(args: {
|
|
56
|
+
observed: string | undefined;
|
|
57
|
+
configured: string;
|
|
58
|
+
profile: OfficialLaunchProfile;
|
|
59
|
+
brand: Pick<BrandProfile, "processLabel">;
|
|
60
|
+
}): ObservedLocalWriteRoot;
|
|
61
|
+
/**
|
|
62
|
+
* WS-14 §3's `CLAUDE_CODE_TMPDIR` and what the engine does with it — reported HONESTLY (WS-17 row 15).
|
|
63
|
+
*
|
|
64
|
+
* "Engine appends its literal `claude-<uid>`" — so a host that reports only the configured value is
|
|
65
|
+
* telling a user the wrong directory. Both are returned, plus the reason the second one cannot be
|
|
66
|
+
* moved, so a capability matrix can print the truth without re-deriving it.
|
|
67
|
+
*/
|
|
68
|
+
export interface VendorTempRootReport {
|
|
69
|
+
/** What we set: the shared per-user temp root, derived by the host from `brand.tempRootName`. */
|
|
70
|
+
configured: string;
|
|
71
|
+
/** Where the engine actually writes: the configured root plus its own fixed segment. */
|
|
72
|
+
engineComputed: string;
|
|
73
|
+
/** The vendor literal in that path. */
|
|
74
|
+
vendorSegment: string;
|
|
75
|
+
note: string;
|
|
76
|
+
}
|
|
77
|
+
export declare function vendorTempRootReport(args: {
|
|
78
|
+
sharedTempRoot: string;
|
|
79
|
+
uid: number | string;
|
|
80
|
+
}): VendorTempRootReport;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { OfficialCrashClass } from "./errors.js";
|
|
2
|
+
import { OfficialInterruptedError } from "./errors.js";
|
|
3
|
+
import type { ProcessIdentity } from "./spawn-proxy.js";
|
|
4
|
+
/**
|
|
5
|
+
* §9's per-session quotas.
|
|
6
|
+
*
|
|
7
|
+
* ANTHROPIC'S OWN PLANNING FIGURES, carried as initial defaults and labelled as what they are: "these
|
|
8
|
+
* are capacity-planning numbers, NOT guarantees; Winter's enforced per-session quotas need product
|
|
9
|
+
* tuning before release" (§16 open question 3). They ship as data so a host can read and override
|
|
10
|
+
* them; nothing in this package enforces them, because enforcement is the daemon's (Phase 8).
|
|
11
|
+
*/
|
|
12
|
+
export declare const DEFAULT_SESSION_QUOTAS: {
|
|
13
|
+
readonly memoryBytes: number;
|
|
14
|
+
readonly diskBytes: number;
|
|
15
|
+
readonly cpus: 1;
|
|
16
|
+
readonly provenance: "Anthropic's published capacity-planning figures, adopted as initial defaults; not guarantees and not yet product-tuned (WS-14 §9/§16)";
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Does a live process still belong to the identity we recorded?
|
|
20
|
+
*
|
|
21
|
+
* BOTH HALVES MUST MATCH. A pid alone revalidates a recycled stranger; a start time alone matches
|
|
22
|
+
* nothing at all. Anything absent is "not revalidated" rather than "assume yes" — an unrevalidated
|
|
23
|
+
* handle is marked unavailable by the directory's own recovery (WS-15 §6.4 step 2), which is a
|
|
24
|
+
* recoverable state, while a wrongly-revalidated one silently steers a stranger's process.
|
|
25
|
+
*/
|
|
26
|
+
export declare function revalidateProcessIdentity(recorded: ProcessIdentity | undefined, observed: ProcessIdentity | undefined): boolean;
|
|
27
|
+
/** §9's host gestures. */
|
|
28
|
+
export type CancellationGesture = "stop-turn" | "stop-background-task" | "end-session" | "kill-process";
|
|
29
|
+
/** What a gesture maps onto, as data — so a host renders §9's table rather than re-deriving it. */
|
|
30
|
+
export interface CancellationMapping {
|
|
31
|
+
gesture: CancellationGesture;
|
|
32
|
+
operation: string;
|
|
33
|
+
effect: string;
|
|
34
|
+
/** True when this gesture must not run before the §6 reconciliation barrier has completed. */
|
|
35
|
+
afterReconciliationBarrier: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare const CANCELLATION_MAPPINGS: readonly CancellationMapping[];
|
|
38
|
+
export declare function cancellationMappingFor(gesture: CancellationGesture): CancellationMapping;
|
|
39
|
+
/** The typed error a gesture produces when it ends a generation (§13's interrupt/cancellation class). */
|
|
40
|
+
export declare function interruptionFor(gesture: CancellationGesture, branchLabel: string): OfficialInterruptedError;
|
|
41
|
+
/**
|
|
42
|
+
* §9's distinction, as a predicate: is this a PROCESS failure or a TURN failure?
|
|
43
|
+
*
|
|
44
|
+
* The projector branches on it, and the two must never collapse: a turn that ended `is_error: true`
|
|
45
|
+
* leaves a healthy child that can take the next message, while a crash class means there is nothing
|
|
46
|
+
* left to send to.
|
|
47
|
+
*/
|
|
48
|
+
export declare function crashClassOf(error: unknown): OfficialCrashClass | undefined;
|
|
49
|
+
export declare function isProcessCrash(error: unknown): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { OfficialContainmentBreachError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tools whose effect is a filesystem write, and therefore worth a snapshot pair.
|
|
4
|
+
*
|
|
5
|
+
* `WebFetch` is here for a reason that is not a file write (review r3, NEW-11): a saved WebFetch
|
|
6
|
+
* approval is §8's one writer with no tool call of its own, and its durable rule update lands in the
|
|
7
|
+
* vendor's own settings file. Watching the call it rides on is the only post-hoc cover it can have.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SWEPT_TOOLS: readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* THE HOOK EVENTS A TOOL'S FILESYSTEM EFFECT CAN BE FOLLOWED BY (review r3, NEW-9).
|
|
12
|
+
*
|
|
13
|
+
* Enumerated from the pinned union rather than guessed: `HOOK_EVENTS` in the 0.3.250 declarations
|
|
14
|
+
* carries `PostToolUse`, `PostToolUseFailure` and `PostToolBatch` as the three events that follow a
|
|
15
|
+
* tool call, and the sweep registered only the first. MEASURED: a command with a side effect and a
|
|
16
|
+
* nonzero exit (`mkdir …; exit 1`) fires `PostToolUseFailure` and NO `PostToolUse`, so the baseline
|
|
17
|
+
* taken in `pre` was never consumed, the artifact survived the session, and `existsSync(<cwd>/.claude)`
|
|
18
|
+
* — row 14's own predicate — was true with both containment layers installed.
|
|
19
|
+
*
|
|
20
|
+
* `PostToolBatch` carries `tool_calls[]` rather than one `tool_use_id`, so the handler reads both
|
|
21
|
+
* shapes.
|
|
22
|
+
*/
|
|
23
|
+
export declare const POST_TOOL_EVENTS: readonly string[];
|
|
24
|
+
/** One breach: what appeared, from which call, and whether the sweep managed to remove it. */
|
|
25
|
+
export interface ContainmentBreach {
|
|
26
|
+
toolName: string;
|
|
27
|
+
toolUseId: string;
|
|
28
|
+
/** The paths this call created that carry a forbidden name. */
|
|
29
|
+
created: readonly string[];
|
|
30
|
+
/** The subset the sweep removed. Anything else is named in `retained`. */
|
|
31
|
+
removed: readonly string[];
|
|
32
|
+
retained: readonly string[];
|
|
33
|
+
at: string;
|
|
34
|
+
}
|
|
35
|
+
/** Every forbidden-named path under `root`, folded-compared, bounded. */
|
|
36
|
+
export declare function forbiddenArtifactsUnder(root: string, depth?: number, maxDepth?: number): string[];
|
|
37
|
+
export interface ContainmentSweepOptions {
|
|
38
|
+
/** The session's working directory. */
|
|
39
|
+
cwd: string;
|
|
40
|
+
/** WS-14 §14's diagnostics label, so a breach error reads like every other error in the package. */
|
|
41
|
+
branchLabel?: string;
|
|
42
|
+
/** How deep below each root the walk goes. Default 6; see `MAX_DEPTH`'s own note. */
|
|
43
|
+
maxDepth?: number;
|
|
44
|
+
/** The session's home, so `~/.claude/plans` is covered as well as the project (WS-17 row 14). */
|
|
45
|
+
home?: string;
|
|
46
|
+
/** Where a breach is reported. The adapter forwards it to the host and keeps it on the session. */
|
|
47
|
+
onBreach?: (breach: ContainmentBreach, error: OfficialContainmentBreachError) => void;
|
|
48
|
+
now?: () => Date;
|
|
49
|
+
}
|
|
50
|
+
export interface ContainmentSweep {
|
|
51
|
+
/** The hook matchers to merge into `Options.hooks` — one PreToolUse, one PostToolUse. */
|
|
52
|
+
hooks: Record<string, Array<{
|
|
53
|
+
hooks: Array<(input: unknown) => Promise<unknown>>;
|
|
54
|
+
}>>;
|
|
55
|
+
/** Every breach this session has seen, in order. */
|
|
56
|
+
readonly breaches: readonly ContainmentBreach[];
|
|
57
|
+
}
|
|
58
|
+
/** Builds the sweep's two hooks. One sweep per SESSION: the snapshots are keyed by tool-use id. */
|
|
59
|
+
export declare function createContainmentSweep(options: ContainmentSweepOptions): ContainmentSweep;
|
|
60
|
+
/** The three names the sweep looks for, restated for a caller that wants to report them. */
|
|
61
|
+
export declare const SWEPT_TARGETS: {
|
|
62
|
+
readonly instructionsFile: "CLAUDE.md";
|
|
63
|
+
readonly projectDir: ".claude";
|
|
64
|
+
readonly userPlansDir: ".claude/plans";
|
|
65
|
+
};
|