@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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -0
  3. package/dist/directory/directory.d.ts +55 -0
  4. package/dist/directory/entries.d.ts +82 -0
  5. package/dist/directory/recovery.d.ts +49 -0
  6. package/dist/door.d.ts +247 -0
  7. package/dist/errors.d.ts +104 -0
  8. package/dist/index.d.ts +25 -0
  9. package/dist/index.js +6679 -0
  10. package/dist/messaging/attribution.d.ts +47 -0
  11. package/dist/messaging/dispatch.d.ts +78 -0
  12. package/dist/messaging/handlers.d.ts +56 -0
  13. package/dist/messaging/inbound.d.ts +110 -0
  14. package/dist/messaging/index.d.ts +39 -0
  15. package/dist/messaging/official-adapter.d.ts +36 -0
  16. package/dist/messaging/router.d.ts +101 -0
  17. package/dist/messaging/sessions.d.ts +49 -0
  18. package/dist/messaging/winter-adapter.d.ts +47 -0
  19. package/dist/native-args.d.ts +76 -0
  20. package/dist/official/adapter.d.ts +88 -0
  21. package/dist/official/aliases.d.ts +48 -0
  22. package/dist/official/auth.d.ts +117 -0
  23. package/dist/official/branding.d.ts +31 -0
  24. package/dist/official/callbacks.d.ts +143 -0
  25. package/dist/official/containment.d.ts +130 -0
  26. package/dist/official/env-allowlist.d.ts +237 -0
  27. package/dist/official/env-registry-rule.d.ts +12 -0
  28. package/dist/official/env-registry.d.ts +3 -0
  29. package/dist/official/errors.d.ts +250 -0
  30. package/dist/official/index.d.ts +31 -0
  31. package/dist/official/mcp-descriptors.d.ts +130 -0
  32. package/dist/official/options-template.d.ts +110 -0
  33. package/dist/official/spawn-proxy.d.ts +144 -0
  34. package/dist/official/spool.d.ts +80 -0
  35. package/dist/official/supervision.d.ts +49 -0
  36. package/dist/official/sweep.d.ts +65 -0
  37. package/dist/sdk.d.ts +214 -0
  38. package/dist/seams/context.d.ts +26 -0
  39. package/dist/seams/directory-store.d.ts +274 -0
  40. package/dist/seams/directory.d.ts +46 -0
  41. package/dist/seams/global-messaging.d.ts +30 -0
  42. package/dist/seams/handoff.d.ts +77 -0
  43. package/dist/seams/index.d.ts +11 -0
  44. package/dist/seams/keychain.d.ts +11 -0
  45. package/dist/seams/materialized-resume.d.ts +46 -0
  46. package/dist/seams/messaging-contract.d.ts +29 -0
  47. package/dist/seams/official-adapter.d.ts +125 -0
  48. package/dist/seams/official-sdk-shapes.d.ts +126 -0
  49. package/dist/seams/stubs.d.ts +34 -0
  50. package/dist/selection/child-runtime.d.ts +81 -0
  51. package/dist/selection/runtime-selection.d.ts +217 -0
  52. package/dist/selection/select-runtime.d.ts +213 -0
  53. package/dist/store/handoff-barrier.d.ts +238 -0
  54. package/dist/store/index.d.ts +11 -0
  55. package/dist/store/materialized-resume.d.ts +100 -0
  56. package/dist/store/pinned-probes.d.ts +17 -0
  57. package/dist/store/reconcile.d.ts +157 -0
  58. package/dist/store/temp-continuity.d.ts +92 -0
  59. package/dist/store/wiring.d.ts +250 -0
  60. package/dist/vendor-paths.d.ts +21 -0
  61. package/dist/version-matrix.d.ts +84 -0
  62. package/docs/conformance-rows.md +195 -0
  63. package/package.json +65 -0
@@ -0,0 +1,88 @@
1
+ import type { SeamContextWithDirectory } from "../seams/context.js";
2
+ import type { OfficialAdapter, OfficialLaunchPlan, OfficialResumePlan, OfficialSession, OptionsTemplateInput } from "../seams/official-adapter.js";
3
+ import { type OfficialPermissionMode } from "./callbacks.js";
4
+ import { type ContainmentPolicy } from "./containment.js";
5
+ import { type ContainmentBreach } from "./sweep.js";
6
+ import { type OfficialEnvInput, type OfficialEnvPolicy } from "./env-allowlist.js";
7
+ import { type OptionsTemplatePolicy } from "./options-template.js";
8
+ import type { OfficialContainmentBreachError } from "./errors.js";
9
+ import { type SpawnChild, type SpawnObservation, type SpawnRecordSink, type SupervisedSpawnProxy, type TranscriptReconcile } from "./spawn-proxy.js";
10
+ import type { OfficialBranchError } from "./errors.js";
11
+ /** Everything the adapter needs that is neither a seam nor a per-launch value. */
12
+ export interface OfficialAdapterPolicy {
13
+ /** Per-launch template policy: an object, or a function of the template input. */
14
+ options?: OptionsTemplatePolicy | ((input: OptionsTemplateInput) => OptionsTemplatePolicy);
15
+ env?: OfficialEnvPolicy;
16
+ /** Where §6 rule 2's record goes. Default: nowhere durable, and `launch` says so if it matters. */
17
+ sink?: SpawnRecordSink;
18
+ /** §6 rule 3's collaborator (the store lane's). Default: a no-op that still runs at the right moment. */
19
+ reconcile?: TranscriptReconcile;
20
+ /** §6 rule 5. Default: `undefined` → cleanup is treated as verified (an in-memory sink has nothing to verify). */
21
+ verifyCleanup?: (observation: SpawnObservation) => Promise<boolean> | boolean;
22
+ /** Injected for tests; production uses Node's own spawn with the brand's process label as argv0. */
23
+ spawnChild?: SpawnChild;
24
+ onCrash?: (error: OfficialBranchError) => void;
25
+ /** §8's post-hoc sweep found a vendor-named path a call created (review r2, NEW-3). */
26
+ onContainmentBreach?: (breach: ContainmentBreach, error: OfficialContainmentBreachError) => void;
27
+ /** §8's dispositions, threaded into the floor this adapter installs on every launch. */
28
+ containment?: ContainmentPolicy;
29
+ /** The session's permission mode, for the bridge the adapter installs when the caller supplied none. */
30
+ permissionMode?: OfficialPermissionMode;
31
+ now?: () => Date;
32
+ }
33
+ /** A live official generation, plus the two handles the seam's shape cannot carry. */
34
+ export interface OfficialSessionHandle extends OfficialSession {
35
+ /** Resolves with the OBSERVED root once the child has spawned (§1's authoritative value). */
36
+ whenObserved(): Promise<string>;
37
+ /** WS-14 §9: stops the foreground turn, preserving background agents (`perTaskStopAffordance`). */
38
+ interrupt(): Promise<unknown>;
39
+ /** The generation's proxy — its stderr tail, its record, its exit gate. */
40
+ readonly supervisor: SupervisedSpawnProxy;
41
+ /** §8's post-hoc sweep findings for this session (review r2, NEW-3). Empty is the normal case. */
42
+ readonly containmentBreaches: readonly ContainmentBreach[];
43
+ }
44
+ export interface OfficialAdapterHandle extends OfficialAdapter {
45
+ launch(plan: OfficialLaunchPlan): OfficialSessionHandle;
46
+ resume(plan: OfficialResumePlan): OfficialSessionHandle;
47
+ /**
48
+ * The seam's `buildChildEnv`, widened to the input the env builder actually accepts.
49
+ *
50
+ * `OfficialEnvInput` extends the spine's `EnvInput` with §3's two per-session vendor variables
51
+ * (the transcript project key and the shared temp root), which the seam does not name. Method
52
+ * parameters are bivariant, so this stays assignable to the seam while letting a caller that HAS
53
+ * those values pass them without a cast.
54
+ */
55
+ buildChildEnv(input: OfficialEnvInput): Record<string, string>;
56
+ /** Prepares the default child starter. Idempotent; a test injecting `spawnChild` never needs it. */
57
+ ready(): Promise<void>;
58
+ }
59
+ /** §5's health facts a handoff decision is made from. */
60
+ export interface OfficialSessionHealth {
61
+ /** WS-14 §6: "any session advertising cross-runtime handoff MUST launch through this proxy from its FIRST generation". */
62
+ launchedThroughProxy: boolean;
63
+ /** §1's recorded active local-write root, as observed. Absent for a default-spawn session. */
64
+ recordedLocalWriteRoot?: string;
65
+ /** §5: `mirror_error` sets `repair-required` and blocks handoff until the store is reconciled. */
66
+ transcriptHealth: "ok" | "repair-required";
67
+ }
68
+ export type HandoffEligibility = {
69
+ eligible: true;
70
+ } | {
71
+ eligible: false;
72
+ reason: "default-spawn-mirror-error" | "repair-required" | "no-recorded-root";
73
+ detail: string;
74
+ };
75
+ /**
76
+ * §5's handoff refusals, as one decision (WS-17 row 15's "default-spawn `mirror_error` handoff
77
+ * refusal").
78
+ *
79
+ * THE DEFAULT-SPAWN CASE IS THE INTERESTING ONE and it is a REFUSAL, not a fallback: "a resumed
80
+ * default-spawn session with `mirror_error` keeps the official owner or stays `repair-required`;
81
+ * SCANNING TEMP DIRECTORIES BY RECENCY IS FORBIDDEN". A session that did not launch through the proxy
82
+ * has no recorded root, and the only other way to find its `claude-resume-*` staging directory is to
83
+ * guess by mtime — which is how a reconciler ends up merging a DIFFERENT session's transcript into
84
+ * this one. There is no safe answer, so the answer is no.
85
+ */
86
+ export declare function officialHandoffEligibility(health: OfficialSessionHealth): HandoffEligibility;
87
+ /** Builds the adapter. `context` is the spine's one seam context — see `src/seams/context.ts`. */
88
+ export declare function createOfficialAdapter(context: SeamContextWithDirectory, policy?: OfficialAdapterPolicy): OfficialAdapterHandle;
@@ -0,0 +1,48 @@
1
+ import { type BrandProfile } from "@yanlinglabs/winter-agent-sdk";
2
+ /** The two built-ins WS-14 §7 redirects, and the canonical tool each resolves to. */
3
+ export declare const ALIASED_BUILTINS: readonly [{
4
+ readonly builtin: "SendMessage";
5
+ readonly tool: "send_message";
6
+ }, {
7
+ readonly builtin: "ListAgents";
8
+ readonly tool: "list_agents";
9
+ }];
10
+ export type AliasedBuiltin = (typeof ALIASED_BUILTINS)[number]["builtin"];
11
+ /** §2's `toolAliases` value: `{ SendMessage: "mcp__<server>__send_message", … }`. */
12
+ export declare function officialToolAliases(brand: Pick<BrandProfile, "mcpServerName">): Record<string, string>;
13
+ /** The canonical name one aliased built-in resolves to (single hop — never a chain). */
14
+ export declare function aliasTargetFor(builtin: AliasedBuiltin, brand: Pick<BrandProfile, "mcpServerName">): string;
15
+ export { LIST_AGENTS_FIELD_MAX, NATIVE_LIST_AGENTS_OUTPUT_SCHEMA, NATIVE_LIST_AGENTS_SCHEMA, NATIVE_SEND_MESSAGE_SCHEMA, SEND_MESSAGE_SUMMARY_MAX, SEND_MESSAGE_TO_MAX, acceptNativeListAgentsArgs, acceptNativeSendMessageArgs, } from "../native-args.js";
16
+ export type { NativeArgsResult, NativeListAgentsArgs, NativeSendMessageArgs } from "../native-args.js";
17
+ /**
18
+ * How the canonical duplicates are exposed (§7's "SHOULD be deferred/hidden").
19
+ *
20
+ * `deferred` and not `hidden`: a hidden tool is unreachable by name, and the canonical name is
21
+ * exactly what a HOST-INTERNAL caller (and the router's own cross-runtime path) addresses. Deferring
22
+ * keeps it addressable while keeping it out of the default advertised set — which is the behaviour
23
+ * the Winter branch's own canonical twin already declares at its source.
24
+ *
25
+ * ON THE OFFICIAL BRANCH THIS IS AN INTENT, NOT AN ENFORCEMENT, and the distinction is recorded
26
+ * rather than papered over: the pinned runtime decides MCP-tool visibility itself, and the capture
27
+ * above shows both duplicates advertised in a session with no Tool Search active. WS-17 row 2 asks
28
+ * for exactly that measurement ("canonical MCP duplicate deferred/hidden visibility; behaviour
29
+ * without Tool Search"), and `test/official/runtime-aliases.test.ts` records what 0.3.250 does rather
30
+ * than asserting what we wish it did.
31
+ */
32
+ export declare const CANONICAL_DUPLICATE_EXPOSURE: "deferred";
33
+ /**
34
+ * BOTH NAMES A DENY RULE MUST CARRY — and this is a MEASUREMENT, not a belt-and-braces habit.
35
+ *
36
+ * Driven against the pinned 0.3.250 runtime (`test/official/runtime-aliases.test.ts`, row 3):
37
+ *
38
+ * `disallowedTools: ["SendMessage"]` → the model emits `SendMessage`, the
39
+ * alias resolves, AND THE HANDLER RUNS.
40
+ * `disallowedTools: [<the canonical target>]` → the call is blocked.
41
+ *
42
+ * So the deny check happens AFTER alias resolution, against the resolved name. A host that denied
43
+ * only the built-in — the obvious reading of "deny `SendMessage`" — would have a deny rule that does
44
+ * nothing at all, silently, on this branch only. §7's "aliases are NOT a security boundary" is
45
+ * exactly this fact, and this function is how a caller stops tripping over it: one built-in in, both
46
+ * names out.
47
+ */
48
+ export declare function aliasDenyNames(builtin: AliasedBuiltin, brand: Pick<BrandProfile, "mcpServerName">): readonly string[];
@@ -0,0 +1,117 @@
1
+ import type { CredentialRef } from "@yanlinglabs/winter-agent-sdk";
2
+ import type { KeychainSeam } from "../seams/keychain.js";
3
+ import type { RuntimeSelection } from "../selection/runtime-selection.js";
4
+ /** `RuntimeSelection.authFamily` — restated as a local alias so the tables below read as tables. */
5
+ export type AuthFamily = RuntimeSelection["authFamily"];
6
+ /**
7
+ * The credential-shaped variables ONE auth family may set, per §12's table.
8
+ *
9
+ * `cloud-credential-chain` is deliberately split by provider: Bedrock and Vertex are two different
10
+ * variable sets and a session is on one of them, never both. The split key is `providerId`, which is
11
+ * the persisted selection's own field — never an ambient scan of the environment (WS-14's Phase 6
12
+ * amendment: "a pinned alias resolves to the `anthropic` provider ONLY when a credential ref for it
13
+ * is configured, and never by ambient environment scan").
14
+ */
15
+ export declare const AUTH_FAMILY_VARIABLES: {
16
+ readonly "api-key": readonly ["ANTHROPIC_API_KEY"];
17
+ /**
18
+ * A bearer credential (Console OAuth, or an approved gateway).
19
+ *
20
+ * THE FULL PAIR, ALWAYS — §12's gateway caveat: "an explicit gateway credential replaces
21
+ * subscription login, but `ANTHROPIC_BASE_URL` alone can leave a stored OAuth credential active;
22
+ * gateway configs MUST set the full credential pair." So the endpoint is part of this family's set
23
+ * rather than a separate knob, and the validator below refuses a base URL with no token.
24
+ */
25
+ readonly "console-oauth": readonly ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL"];
26
+ readonly bedrock: readonly ["CLAUDE_CODE_USE_BEDROCK", "AWS_REGION", "AWS_PROFILE", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_BEARER_TOKEN_BEDROCK", "ANTHROPIC_BEDROCK_BASE_URL"];
27
+ readonly vertex: readonly ["CLAUDE_CODE_USE_VERTEX", "ANTHROPIC_VERTEX_PROJECT_ID", "CLOUD_ML_REGION", "GOOGLE_APPLICATION_CREDENTIALS", "ANTHROPIC_VERTEX_BASE_URL"];
28
+ /**
29
+ * §12's last row: NONE. "Claude OAuth (D14-gated) — stored subscription credentials live inside the
30
+ * spool namespace; no env credential injected." The empty set is the rule, not an oversight.
31
+ */
32
+ readonly "claude-oauth": readonly [];
33
+ /** A runtime with no credential at all (a local endpoint). Nothing to inject. */
34
+ readonly "local-none": readonly [];
35
+ };
36
+ export type AuthVariableSetKey = keyof typeof AUTH_FAMILY_VARIABLES;
37
+ /**
38
+ * WHAT "CREDENTIAL-BEARING" MEANS, as a SHAPE rather than as a list (review r2, NEW-2).
39
+ *
40
+ * The family tables above are sixteen names. The pinned runtime reads thirty-odd, and eight of the
41
+ * ones it reads were measured travelling through `configuredExtras` into an `api-key` session —
42
+ * including a complete second provider family (`CLAUDE_CODE_USE_FOUNDRY` + `ANTHROPIC_FOUNDRY_API_KEY`),
43
+ * an `Authorization: Bearer` header injected wholesale through `ANTHROPIC_CUSTOM_HEADERS`, and
44
+ * `CLAUDE_CODE_OAUTH_REFRESH_TOKEN`, which walks around D14's "the Claude OAuth family injects NO
45
+ * credential variable" because `NEVER_INJECTED_AUTH_VARIABLES` names exactly one variable.
46
+ *
47
+ * A list can only ever refuse what someone already thought of; the runtime's own precedence order is
48
+ * what makes that fatal (cloud-provider credentials resolve ABOVE the token and the key, so one
49
+ * variable re-points billing, rate-limits and audit at an account the persisted selection does not
50
+ * name). So the universe is a SHAPE: every vendor/cloud auth prefix the artifact uses, plus the
51
+ * generic credential suffixes. Anything matching it is refused from the extras door unless it is a
52
+ * variable this session's own family sets.
53
+ */
54
+ export declare const AUTH_SHAPED_RE: RegExp;
55
+ /** True when a name is credential-bearing by shape, whatever table it is or is not in. */
56
+ export declare function isAuthShapedVariable(name: string): boolean;
57
+ /** Every credential-shaped name any family may set — the env allowlist's auth section. */
58
+ export declare const ALL_AUTH_VARIABLES: readonly string[];
59
+ /**
60
+ * NEVER INJECTED, on any branch, in any family (§3, §5.1, §12; WS-01 §2.5).
61
+ *
62
+ * The subscription OAuth token is the one credential the runtime can pick up from the environment
63
+ * that Winter must never place there: D14 gates the OAuth branch entirely, and the supported flow
64
+ * puts its stored credentials INSIDE THE SPOOL rather than in a variable.
65
+ */
66
+ export declare const NEVER_INJECTED_AUTH_VARIABLES: readonly string[];
67
+ /** Which variable set a selection's family uses, `providerId` deciding the cloud split. */
68
+ export declare function authVariableSetKey(selection: Pick<RuntimeSelection, "authFamily" | "providerId">): AuthVariableSetKey | "custom";
69
+ /**
70
+ * The names this session's family is allowed to set.
71
+ *
72
+ * `custom` returns `undefined` rather than a set: a host that declares a custom auth family is saying
73
+ * "I name these variables myself", and the env builder validates them against the FORBIDDEN list
74
+ * instead of an allowlist. That is a deliberate hole with a fence around it — §3's "anything else is
75
+ * a deliberate, documented addition" — and it is the only family for which the set is open.
76
+ */
77
+ export declare function allowedAuthVariables(selection: Pick<RuntimeSelection, "authFamily" | "providerId">): readonly string[] | undefined;
78
+ /** D14's ship gate: the OAuth branch is built but publicly gated pending written approval. */
79
+ export interface ClaudeOauthGate {
80
+ /** Default FALSE everywhere. "Until approval exists, the shippable branch uses API-key/cloud/gateway auth only." */
81
+ approved: boolean;
82
+ }
83
+ /**
84
+ * Validates the credential material a caller assembled for this session, before it reaches a child.
85
+ *
86
+ * Four refusals, each naming a real way the "exactly one family" rule breaks:
87
+ * 1. a variable outside the family's set (two families at once, decided silently by precedence);
88
+ * 2. `CLAUDE_CODE_OAUTH_TOKEN`, ever;
89
+ * 3. a bearer family with a base URL and no token (the §12 gateway caveat's exact failure: the
90
+ * endpoint moves and a STORED subscription credential stays active behind it);
91
+ * 4. `claude-oauth` while the D14 gate is closed, or with any variable at all (that family injects
92
+ * none by definition).
93
+ */
94
+ export declare function validateAuthEnvironment(args: {
95
+ selection: Pick<RuntimeSelection, "authFamily" | "providerId">;
96
+ credentials: Readonly<Record<string, string>>;
97
+ gate: ClaudeOauthGate;
98
+ branchLabel: string;
99
+ }): void;
100
+ /** One variable's credential source: the name to set, and the ref whose material fills it. */
101
+ export interface AuthCredentialPlan {
102
+ variable: string;
103
+ ref: CredentialRef;
104
+ }
105
+ /**
106
+ * Fetches this session's credential material AT SPAWN (§12) and returns it for immediate injection.
107
+ *
108
+ * THE ONLY KEYCHAIN READ IN THIS PACKAGE, and it returns rather than stores. A missing ref is a
109
+ * TYPED refusal and not a silent omission: a child launched with an empty auth family falls through
110
+ * the runtime's precedence table to whatever the spool happens to hold, which is the accident §12
111
+ * exists to prevent.
112
+ */
113
+ export declare function fetchAuthCredentials(args: {
114
+ plan: readonly AuthCredentialPlan[];
115
+ keychain: KeychainSeam;
116
+ branchLabel: string;
117
+ }): Promise<Record<string, string>>;
@@ -0,0 +1,31 @@
1
+ import type { BrandProfile } from "@yanlinglabs/winter-agent-sdk";
2
+ /**
3
+ * The internal diagnostics label for this branch (WS-14 §14, WS-01 §5).
4
+ *
5
+ * `<processLabel>-claude-agent` — the Winter-owned half comes from the profile, the Claude-mirroring
6
+ * half is fixed (WS-01 §5/D16: vendor literals are never rebranded, because renaming them would be a
7
+ * lie about which runtime is executing).
8
+ */
9
+ export declare function officialBranchLabel(brand: Pick<BrandProfile, "processLabel">): string;
10
+ /** One disclosed vendor-named artifact: what it is, where it shows up, and why it cannot be renamed. */
11
+ export interface OfficialDisclosure {
12
+ /** A stable id so a capability matrix can key rows off it rather than off prose. */
13
+ id: "signed-binary-identity" | "spool-config-file" | "nested-engine-temp" | "resume-staging" | "vendor-telemetry-defaults" | "extraction-cache";
14
+ /** The vendor literal a user or an operator will actually see. */
15
+ literal: string;
16
+ /** Where it appears. */
17
+ where: string;
18
+ /** Why Winter does not and must not rename it. */
19
+ why: string;
20
+ }
21
+ /**
22
+ * WS-14 §14's disclosure set, as data.
23
+ *
24
+ * Every entry is a place the official branch is visibly Anthropic's, and the honest caveat D12
25
+ * attaches to the process label ("the Mach-O stays signed `com.anthropic.claude-code`; some process
26
+ * viewers, crash reports and code-signing displays reveal the real identity") is the first row. The
27
+ * last row is the one that is disclosed as AVOIDED rather than present, because WS-02 ships the
28
+ * native package as a normal resource and a disclosure that quietly dropped it would read, later, as
29
+ * a claim that the cache never existed.
30
+ */
31
+ export declare const OFFICIAL_DISCLOSURES: readonly OfficialDisclosure[];
@@ -0,0 +1,143 @@
1
+ import type { BrandProfile, PermissionResult, PermissionUpdate } from "@yanlinglabs/winter-agent-sdk";
2
+ import { type ContainmentPolicy } from "./containment.js";
3
+ /** The permission modes a host session can be in. Mirrors the pinned runtime's own vocabulary. */
4
+ export type OfficialPermissionMode = "default" | "plan" | "acceptEdits" | "bypassPermissions" | "dontAsk";
5
+ /** Everything the pinned `CanUseTool` hands a callback, carried through to the broker unchanged. */
6
+ export interface ApprovalRequest {
7
+ toolName: string;
8
+ input: Record<string, unknown>;
9
+ signal: AbortSignal;
10
+ requestId: string;
11
+ toolUseID: string;
12
+ agentID?: string;
13
+ suggestions?: PermissionUpdate[];
14
+ blockedPath?: string;
15
+ decisionReason?: string;
16
+ title?: string;
17
+ displayName?: string;
18
+ description?: string;
19
+ matchedAskRule?: {
20
+ source: string;
21
+ toolName: string;
22
+ ruleContent?: string;
23
+ };
24
+ }
25
+ /** The host's broker (WS-15 §7 / Phase 8). It answers with a typed result — never `null`, ever. */
26
+ export type ApprovalBroker = (request: ApprovalRequest) => Promise<PermissionResult>;
27
+ /** The bridge itself: assignable to either SDK's `canUseTool`, and strictly narrower than both. */
28
+ export type OfficialApprovalBridge = (toolName: string, input: Record<string, unknown>, options: Omit<ApprovalRequest, "toolName" | "input">) => Promise<PermissionResult>;
29
+ export interface ApprovalBridgeOptions {
30
+ broker: ApprovalBroker;
31
+ brand: Pick<BrandProfile, "projectDirName" | "processLabel">;
32
+ /** The session's mode. `dontAsk` never reaches the broker (§10). */
33
+ mode: OfficialPermissionMode;
34
+ /** §8's dispositions. The floor reads them; `projectDirName` is filled from `brand` if absent. */
35
+ containment?: ContainmentPolicy;
36
+ /** Called with every decision, so a host can log or count without wrapping the broker. */
37
+ onDecision?: (decision: {
38
+ request: ApprovalRequest;
39
+ result: PermissionResult;
40
+ source: DecisionSource;
41
+ }) => void;
42
+ }
43
+ /** Where a decision came from — what a row-14 tally is built out of. */
44
+ export type DecisionSource = "containment-floor" | "broker" | "dont-ask" | "broker-approval-stripped";
45
+ /**
46
+ * Builds `Options.canUseTool` for the official branch.
47
+ *
48
+ * ORDER: containment floor → mode → broker. Every arm returns a typed result, and the function's own
49
+ * return type is what guarantees it.
50
+ */
51
+ export declare function createApprovalBridge(options: ApprovalBridgeOptions): OfficialApprovalBridge;
52
+ /**
53
+ * THE MARKS THAT MAKE THE FLOOR VISIBLE (review r2, NEW-1) — visible, and no longer PROOF.
54
+ *
55
+ * `assertOptionsInvariants` has to be able to answer "does this options object carry the floor?" for
56
+ * an object it did not build — that is the whole point of a function whose doc says it validates what
57
+ * `launch()` is HANDED. A structural guess ("some PreToolUse matcher exists") would pass for any hook
58
+ * at all, so the floor's own callback and the approval bridge each carry a symbol — `Symbol.for`, so a
59
+ * host reading an options object can recognise the two by name whichever copy of this package stamped
60
+ * them.
61
+ *
62
+ * WHAT A MARK IS NOW (review r3 NEW-11, review r4 NEW-18): a LABEL. It is an exported `Symbol.for` key,
63
+ * so any caller can stamp it, and both marks have been measured being stamped to some effect. The
64
+ * answer to "is this the floor / the bridge" is the identity check further down, never the mark.
65
+ */
66
+ export declare const CONTAINMENT_FLOOR_MARK: unique symbol;
67
+ export declare const APPROVAL_BRIDGE_MARK: unique symbol;
68
+ /** True when this function carries the mark — a label a host can read, NOT proof that this package made it. */
69
+ export declare function carriesMark(value: unknown, mark: symbol): boolean;
70
+ export declare function isOurApprovalBridge(value: unknown): value is OfficialApprovalBridge;
71
+ /** True only for a hook `createContainmentHooks` built. A hook merely stamped with the mark is NOT the floor. */
72
+ export declare function isOurContainmentHook(value: unknown): boolean;
73
+ /** The subset of the hook contract this needs, declared structurally (the peer is never imported). */
74
+ export interface PreToolUseHookInput {
75
+ hook_event_name?: string;
76
+ tool_name?: string;
77
+ tool_input?: unknown;
78
+ }
79
+ export type OfficialHookOutput = {
80
+ hookSpecificOutput?: {
81
+ hookEventName: "PreToolUse";
82
+ permissionDecision?: "allow" | "deny" | "ask" | "defer";
83
+ permissionDecisionReason?: string;
84
+ };
85
+ decision?: "block";
86
+ stopReason?: string;
87
+ };
88
+ export interface ContainmentHooksOptions {
89
+ brand: Pick<BrandProfile, "projectDirName">;
90
+ containment?: ContainmentPolicy;
91
+ onDecision?: (decision: {
92
+ tool: string;
93
+ target: string;
94
+ reason: string;
95
+ }) => void;
96
+ }
97
+ /**
98
+ * The `hooks` value §8's containment needs: one `PreToolUse` matcher applying the same floor.
99
+ *
100
+ * A DENY HERE IS THE STRONGEST ONE THE RUNTIME OFFERS: `permissionDecision: "deny"` stops the call
101
+ * before it runs and returns the reason to the model, and unlike the callback it is invoked for every
102
+ * tool — including the ones that skip `canUseTool` entirely.
103
+ */
104
+ export declare function createContainmentHooks(options: ContainmentHooksOptions): Record<string, Array<{
105
+ hooks: Array<(input: unknown) => Promise<OfficialHookOutput>>;
106
+ }>>;
107
+ /** A decision that was made before a restart, replayed against the session it was made for. */
108
+ export interface ResumedDecision {
109
+ requestId: string;
110
+ toolUseID: string;
111
+ sessionId: string;
112
+ /** The mode/policy version the decision was made under. */
113
+ policyVersion: string;
114
+ /** Paths the decision covered, already normalized by the host. */
115
+ normalizedPaths: readonly string[];
116
+ /** Which runtime owned the session when the decision was made (WS-05 §12's writer lease). */
117
+ runtimeOwner: string;
118
+ }
119
+ /** The live facts a resumed decision is revalidated against. */
120
+ export type DecisionContext = Omit<ResumedDecision, "requestId">;
121
+ /**
122
+ * §10's revalidation, as a predicate.
123
+ *
124
+ * "A decision resumed after restart revalidates SESSION, TOOL CALL, MODE/POLICY VERSION, NORMALIZED
125
+ * PATHS, and RUNTIME OWNERSHIP before executing." All five, and the list is why this is a function
126
+ * rather than an `===`: four-out-of-five is an approval for a call the user never saw.
127
+ */
128
+ export declare function revalidateResumedDecision(decision: ResumedDecision, live: DecisionContext): {
129
+ valid: true;
130
+ } | {
131
+ valid: false;
132
+ reason: string;
133
+ };
134
+ /**
135
+ * "FIRST RESPONSE WINS" — a one-shot latch per `requestId`.
136
+ *
137
+ * A broker that answers twice (a user click plus a restored decision, say) must not produce two
138
+ * control responses: the second is dropped, and the latch says so rather than silently discarding.
139
+ */
140
+ export declare function createFirstResponseWins(): {
141
+ claim(requestId: string): boolean;
142
+ claimed(): readonly string[];
143
+ };
@@ -0,0 +1,130 @@
1
+ import type { BrandProfile } from "@yanlinglabs/winter-agent-sdk";
2
+ import { type AliasedBuiltin } from "./aliases.js";
3
+ /** The three vendor-named targets nothing may create (WS-17 row 14). */
4
+ export declare const FORBIDDEN_TARGETS: {
5
+ readonly instructionsFile: "CLAUDE.md";
6
+ readonly projectDir: ".claude";
7
+ readonly userPlansDir: ".claude/plans";
8
+ };
9
+ /** Where each native writer is redirected — the product's own project directory (§8's table). */
10
+ export interface ContainmentPaths {
11
+ worktrees: string;
12
+ workflows: string;
13
+ plans: string;
14
+ localSettings: string;
15
+ }
16
+ export declare function containmentPaths(brand: Pick<BrandProfile, "projectDirName">): ContainmentPaths;
17
+ /** One row of §8's disposition table, as data a capability matrix can render. */
18
+ export interface ContainmentDisposition {
19
+ writer: string;
20
+ claudeNamedTarget: string;
21
+ /** What §8 SAYS should happen to this writer. */
22
+ disposition: "redirect" | "disable" | "owned-by-product" | "deny";
23
+ target?: string;
24
+ /**
25
+ * What THIS PACKAGE actually does about it (review r1, M2).
26
+ *
27
+ * The distinction is the finding: a row can say `redirect` while the router — which implements no
28
+ * tools — has nothing to redirect to, and a reader of the table alone would believe the writer was
29
+ * handled. `floor-deny` means the permission floor refuses the call; `deny-list` means the name is
30
+ * in `disallowedTools`; `approval-stripped` means the durable permission update is dropped at the
31
+ * bridge; `host-implementation` means the host installed the replacement and owns it from there.
32
+ */
33
+ enforcement: "floor-deny" | "deny-list" | "approval-stripped" | "host-implementation" | "host-ui";
34
+ note: string;
35
+ }
36
+ /**
37
+ * §16 open question 2, FIXED HERE WITH ITS REASON RECORDED.
38
+ *
39
+ * "Saved `WebFetch` approvals: redirect versus disable is left open until WS-07 decides whether
40
+ * durable approvals on this branch share the product's rules store or the project settings file;
41
+ * either way the `.claude/` write is denied."
42
+ *
43
+ * `disable` IS THE ONLY VALUE THIS BRANCH CAN HONOUR, and that is a measurement rather than a
44
+ * preference (review r3, NEW-11). `redirect` was documented as "durable approvals are routed into the
45
+ * product's own project settings file" — and nothing routed them: the branch has no way to make the
46
+ * runtime write elsewhere, so passing the durable update through means the RUNTIME writes
47
+ * `<cwd>/.claude/settings.local.json`. Measured, with an ordinary host broker and no forgery: that
48
+ * file was created and the product's own never was. §8's own sentence is "either way the `.claude/`
49
+ * write is denied", so the value that cannot deny it is refused with a typed error until a host
50
+ * replacement exists — `disable` still keeps the approval for the session's lifetime and leaves
51
+ * WS-07's open question open.
52
+ */
53
+ export type SavedApprovalDisposition = "disable" | "redirect";
54
+ export interface ContainmentPolicy {
55
+ savedWebFetchApprovals?: SavedApprovalDisposition;
56
+ /**
57
+ * The two §8 rows whose disposition is REDIRECT, and the honest fact about who can perform one.
58
+ *
59
+ * REVIEW r1, M2 — MEASURED. `EnterWorktree`, `Agent`/`Task` with `isolation: "worktree"` and named
60
+ * `Workflow` resolution were all reaching the floor and being ALLOWED: the router implements no
61
+ * tools, so it has nothing to redirect them TO, and the disposition table said "redirect" while the
62
+ * code did nothing. In a non-git working directory they then failed for an unrelated reason and the
63
+ * proof read as containment.
64
+ *
65
+ * So the router's own enforcement is a DENY until the host installs the schema-compatible
66
+ * replacement §8 describes — `"host-replacement"` is the host saying it has, and only then does the
67
+ * vendor's own writer become the host's problem rather than a vendor-named write.
68
+ */
69
+ worktrees?: "deny" | "host-replacement";
70
+ workflows?: "deny" | "host-replacement";
71
+ /** The product's project directory, so a refusal can NAME where the replacement lives. */
72
+ projectDirName?: string;
73
+ /**
74
+ * Aliased built-ins this deployment denies.
75
+ *
76
+ * Each expands to BOTH names, because the pinned runtime checks the deny list AFTER alias
77
+ * resolution — see `aliasDenyNames` for the measurement. A host that listed only the built-in would
78
+ * have a rule that does nothing.
79
+ */
80
+ deniedAliasedBuiltins?: readonly AliasedBuiltin[];
81
+ }
82
+ /**
83
+ * The saved-approval disposition, refusing the value the branch cannot honour (review r3, NEW-11).
84
+ *
85
+ * A typed refusal rather than a silent downgrade: a host that asked for `redirect` asked for a
86
+ * behaviour, and quietly giving it `disable` would leave it believing durable approvals are being
87
+ * persisted somewhere.
88
+ */
89
+ export declare function resolveSavedApprovalDisposition(policy: ContainmentPolicy, branchLabel: string): SavedApprovalDisposition;
90
+ export declare function containmentDispositions(brand: Pick<BrandProfile, "projectDirName" | "productName">, policy?: ContainmentPolicy): readonly ContainmentDisposition[];
91
+ /**
92
+ * Tools whose NAME-based route is closed on this branch (§7's `disallowedTools`).
93
+ *
94
+ * A SHORT LIST ON PURPOSE. `disallowedTools` covers the paths aliases miss, but it is still a name
95
+ * mechanism: the floor below is what actually holds. What is listed here are the writers whose whole
96
+ * PURPOSE is a vendor-named path and which have no redirected equivalent on this branch.
97
+ */
98
+ export declare function officialDisallowedTools(policy?: ContainmentPolicy, brand?: Pick<BrandProfile, "mcpServerName">): readonly string[];
99
+ /** A decision from the containment floor. `deny` carries the sentence the model is shown. */
100
+ export type ContainmentDecision = {
101
+ allow: true;
102
+ } | {
103
+ allow: false;
104
+ reason: string;
105
+ target: string;
106
+ };
107
+ /**
108
+ * Does this path create or write one of the three forbidden targets?
109
+ *
110
+ * SEGMENT MATCHING, never substring: `.claude` must not match `.claude-backup`, and `CLAUDE.md` must
111
+ * not match `MY_CLAUDE.mdx`. The user-level plans directory is matched anywhere (it is an absolute
112
+ * path under the vendor home, which §3 already keeps out of the environment — this is the belt).
113
+ * Every comparison goes through `fold` — see its own note for the measurement that made that
114
+ * mandatory.
115
+ */
116
+ export declare function targetsForbiddenPath(rawPath: string): {
117
+ forbidden: boolean;
118
+ target: string;
119
+ };
120
+ /**
121
+ * THE ONE DEFINITION OF "the vendor's user-level home" IN THIS PACKAGE (review r2, NEW-4).
122
+ *
123
+ * It lived in three places and C1 folded only one of them, so `/Users/dev/.Claude/ca.pem` passed the
124
+ * env value check, a `PATH` entry under `~/.Claude/plugins/.../bin` survived the sanitizer, and
125
+ * `/tmp/.Claude/claude-resume-abc` was accepted as a staging root — all on the filesystem where those
126
+ * ARE the same directory. Case-insensitive, exported, and imported by `env-allowlist.ts` and
127
+ * `spool.ts` rather than re-spelled.
128
+ */
129
+ export declare const VENDOR_HOME_SEGMENT_RE: RegExp;
130
+ export declare function containmentDecisionFor(toolName: string, input: Record<string, unknown>, policy?: ContainmentPolicy): ContainmentDecision;