@yanlinglabs/winter-runtime-sdk 0.0.2 → 0.0.3
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/README.md +11 -2
- package/dist/index-mfd2rg7x.js +1426 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +59 -1327
- package/dist/testing/capture-env.d.ts +41 -0
- package/dist/testing/hermetic.d.ts +33 -0
- package/dist/testing/host.d.ts +5 -0
- package/dist/testing/host.js +139 -0
- package/dist/testing/peers.d.ts +45 -0
- package/package.json +14 -2
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE FOUR TRAFFIC OPT-OUTS THAT MAKE A CHILD RUNTIME ACTUALLY HERMETIC (whole-branch review, F-1).
|
|
3
|
+
*
|
|
4
|
+
* HERMETICITY IS A PROPERTY OF THE CHILD ENVIRONMENT, NOT OF THE FAKE. The loopback fake captures the
|
|
5
|
+
* MODEL endpoint and nothing else; the pinned artifact also fetches REMOTE FEATURE CONFIGURATION
|
|
6
|
+
* (`cdn.growthbook.io`), telemetry, error reports and update checks, none of which pass through
|
|
7
|
+
* `ANTHROPIC_BASE_URL`. Measured on this pin, same binary, same options, same fake: with these four
|
|
8
|
+
* unset the request carries 25 tools; with them set, 21 — `DesignSync`, `Monitor`,
|
|
9
|
+
* `PushNotification` and `advisor_20260301:advisor` appear ONLY when the remote flag fetch succeeds.
|
|
10
|
+
*
|
|
11
|
+
* SO A TEST WITHOUT THESE IS NOT MEASURING THE PIN. It is measuring the pin plus whatever a CDN said
|
|
12
|
+
* this minute, which is (a) a different answer on a different day, (b) a suite that goes red when the
|
|
13
|
+
* fetch times out — the "flake seen twice in ~20 runs" — and (c) a `docs/probes/` record whose "no
|
|
14
|
+
* network" line is false.
|
|
15
|
+
*
|
|
16
|
+
* THE SAME OBJECT THE PRODUCTION ENV BUILDER SETS (R-7b-11). It used to be a test-only copy handed to
|
|
17
|
+
* `configuredExtras`, and a copy is exactly how a test bed and a shipped session end up measuring two
|
|
18
|
+
* different artifacts: this re-export is what makes "the beds run what a host runs" checkable by
|
|
19
|
+
* identity rather than by reading two lists.
|
|
20
|
+
*/
|
|
21
|
+
export { TRAFFIC_OPT_OUT_VARIABLES as HERMETIC_TRAFFIC_OPT_OUTS } from "../official/env-allowlist.js";
|
|
22
|
+
/**
|
|
23
|
+
* The minimal environment that points an official-SDK session at a loopback fake (R-7b-6).
|
|
24
|
+
*
|
|
25
|
+
* A REPLACEMENT, never a spread of `process.env` — WS-14 §3's own rule for the child environment, and
|
|
26
|
+
* the same reason the SDK repository's capture harness sets `HOME`: a throwaway is required because
|
|
27
|
+
* `os.homedir()` falls back to the OS user database and would otherwise reach the real `~/.claude`
|
|
28
|
+
* regardless of `CLAUDE_CONFIG_DIR`.
|
|
29
|
+
*
|
|
30
|
+
* PLUS THE FOUR OPT-OUTS ABOVE, unconditionally, because "hermetic" has to mean the whole child and
|
|
31
|
+
* not just its model endpoint (F-1). `allowRemoteConfig` is the ONE deliberate escape hatch: the D29
|
|
32
|
+
* probe's non-hermetic leg uses it to observe what remote configuration adds, and it is spelled at
|
|
33
|
+
* the call site so a reader can see which legs are which.
|
|
34
|
+
*/
|
|
35
|
+
export declare function officialCaptureEnv(input: {
|
|
36
|
+
baseUrl: string;
|
|
37
|
+
apiKey?: string;
|
|
38
|
+
claudeConfigDir: string;
|
|
39
|
+
home: string;
|
|
40
|
+
allowRemoteConfig?: boolean;
|
|
41
|
+
}): Record<string, string>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CredentialRef } from "@yanlinglabs/winter-agent-sdk";
|
|
2
|
+
import type { KeychainSeam } from "../seams/keychain.js";
|
|
3
|
+
export interface FakeKeychain extends KeychainSeam {
|
|
4
|
+
/** Stores material for a ref, keyed the same way `read` looks it up. */
|
|
5
|
+
set(ref: CredentialRef, material: string): void;
|
|
6
|
+
/** Every `read` this double served, in order — so a test can prove the fetch happened at spawn. */
|
|
7
|
+
readonly reads: CredentialRef[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* An in-memory `KeychainSeam`. NEVER `Bun.secrets`, never the OS keychain.
|
|
11
|
+
*
|
|
12
|
+
* An unset ref reads `undefined`, which is the seam's own documented "the host has none" answer —
|
|
13
|
+
* so the missing-credential path is testable without arranging for a real keychain to be empty.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createFakeKeychain(initial?: Array<{
|
|
16
|
+
ref: CredentialRef;
|
|
17
|
+
material: string;
|
|
18
|
+
}>): FakeKeychain;
|
|
19
|
+
/**
|
|
20
|
+
* A fresh temp directory that is removed whatever the body does.
|
|
21
|
+
*
|
|
22
|
+
* `mkdtemp` under the OS temp root, never a path built from a home directory: the point is a
|
|
23
|
+
* directory this process created and this process owns.
|
|
24
|
+
*/
|
|
25
|
+
export declare function withTempDir<T>(prefix: string, fn: (dir: string) => Promise<T>): Promise<T>;
|
|
26
|
+
/**
|
|
27
|
+
* The home directories a hermetic official-branch test needs: a throwaway `HOME` and a throwaway
|
|
28
|
+
* `CLAUDE_CONFIG_DIR`, both under one temp root that is removed in a `finally`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function withHermeticHomes<T>(fn: (homes: {
|
|
31
|
+
home: string;
|
|
32
|
+
claudeConfigDir: string;
|
|
33
|
+
}) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createFakeKeychain, withHermeticHomes, withTempDir } from "./hermetic.js";
|
|
2
|
+
export type { FakeKeychain } from "./hermetic.js";
|
|
3
|
+
export { createFakeClaudePeer, createFakeWinterPeer } from "./peers.js";
|
|
4
|
+
export type { FakeClaudePeerOptions, FakeWinterPeer, FakeWinterPeerOptions, RecordedQueryCall } from "./peers.js";
|
|
5
|
+
export { HERMETIC_TRAFFIC_OPT_OUTS, officialCaptureEnv } from "./capture-env.js";
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TRAFFIC_OPT_OUT_VARIABLES
|
|
3
|
+
} from "../index-mfd2rg7x.js";
|
|
4
|
+
|
|
5
|
+
// src/testing/hermetic.ts
|
|
6
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
function keyOf(ref) {
|
|
10
|
+
switch (ref.kind) {
|
|
11
|
+
case "keychain":
|
|
12
|
+
return `keychain:${ref.service ?? ""}:${ref.account}`;
|
|
13
|
+
case "env":
|
|
14
|
+
return `env:${ref.name}`;
|
|
15
|
+
case "file":
|
|
16
|
+
return `file:${ref.path}:${ref.format}:${ref.profile ?? ""}`;
|
|
17
|
+
case "inline":
|
|
18
|
+
return `inline:${ref.value}`;
|
|
19
|
+
default:
|
|
20
|
+
return ref.kind;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function createFakeKeychain(initial = []) {
|
|
24
|
+
const store = new Map;
|
|
25
|
+
const reads = [];
|
|
26
|
+
for (const entry of initial)
|
|
27
|
+
store.set(keyOf(entry.ref), entry.material);
|
|
28
|
+
return {
|
|
29
|
+
reads,
|
|
30
|
+
set(ref, material) {
|
|
31
|
+
store.set(keyOf(ref), material);
|
|
32
|
+
},
|
|
33
|
+
async read(ref) {
|
|
34
|
+
reads.push(ref);
|
|
35
|
+
return store.get(keyOf(ref));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function withTempDir(prefix, fn) {
|
|
40
|
+
const dir = mkdtempSync(join(tmpdir(), `winter-runtime-sdk-${prefix}-`));
|
|
41
|
+
try {
|
|
42
|
+
return await fn(dir);
|
|
43
|
+
} finally {
|
|
44
|
+
rmSync(dir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function withHermeticHomes(fn) {
|
|
48
|
+
return withTempDir("homes", async (root) => {
|
|
49
|
+
const home = mkdtempSync(join(root, "home-"));
|
|
50
|
+
const claudeConfigDir = mkdtempSync(join(root, "claude-config-"));
|
|
51
|
+
return fn({ home, claudeConfigDir });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// src/testing/peers.ts
|
|
55
|
+
import { InvalidBrandError, resolveBrand, transcriptProjectKey } from "@yanlinglabs/winter-agent-sdk";
|
|
56
|
+
var defaultMessage = () => ({
|
|
57
|
+
type: "result",
|
|
58
|
+
subtype: "success",
|
|
59
|
+
is_error: false,
|
|
60
|
+
result: "ok"
|
|
61
|
+
});
|
|
62
|
+
function scriptedQuery(messages) {
|
|
63
|
+
const unsupported = (name) => async () => {
|
|
64
|
+
throw new Error(`fake winter peer: Query.${name}() is not scripted`);
|
|
65
|
+
};
|
|
66
|
+
const generator = async function* () {
|
|
67
|
+
for (const message of messages)
|
|
68
|
+
yield message;
|
|
69
|
+
}();
|
|
70
|
+
const query = {
|
|
71
|
+
next: (...args) => generator.next(...args),
|
|
72
|
+
return: (value) => generator.return(value),
|
|
73
|
+
throw: (error) => generator.throw(error),
|
|
74
|
+
[Symbol.asyncIterator]() {
|
|
75
|
+
return query;
|
|
76
|
+
},
|
|
77
|
+
async[Symbol.asyncDispose]() {
|
|
78
|
+
await generator.return(undefined);
|
|
79
|
+
},
|
|
80
|
+
interrupt: unsupported("interrupt"),
|
|
81
|
+
setModel: unsupported("setModel"),
|
|
82
|
+
supportedModels: unsupported("supportedModels"),
|
|
83
|
+
listModelFamilies: unsupported("listModelFamilies"),
|
|
84
|
+
accountInfo: unsupported("accountInfo"),
|
|
85
|
+
rewindFiles: unsupported("rewindFiles"),
|
|
86
|
+
setPermissionMode: unsupported("setPermissionMode"),
|
|
87
|
+
messaging: {
|
|
88
|
+
listReachable: unsupported("messaging.listReachable"),
|
|
89
|
+
deliver: unsupported("messaging.deliver"),
|
|
90
|
+
steerChild: unsupported("messaging.steerChild"),
|
|
91
|
+
resumeChild: unsupported("messaging.resumeChild"),
|
|
92
|
+
subscribeIdle: unsupported("messaging.subscribeIdle"),
|
|
93
|
+
senderClass: unsupported("messaging.senderClass"),
|
|
94
|
+
readNotifications: unsupported("messaging.readNotifications"),
|
|
95
|
+
onIdleNotice: () => {
|
|
96
|
+
throw new Error("fake winter peer: Query.messaging.onIdleNotice() is not scripted");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
return query;
|
|
101
|
+
}
|
|
102
|
+
function createFakeWinterPeer(options = {}) {
|
|
103
|
+
const calls = [];
|
|
104
|
+
const scripted = options.messages ?? [defaultMessage()];
|
|
105
|
+
const namespace = {
|
|
106
|
+
SDK_VERSION: options.packageVersion ?? "0.0.3",
|
|
107
|
+
PROTOCOL_VERSION: options.protocolVersion ?? "1.0",
|
|
108
|
+
resolveBrand,
|
|
109
|
+
InvalidBrandError,
|
|
110
|
+
transcriptProjectKey,
|
|
111
|
+
query: (args) => {
|
|
112
|
+
calls.push({ prompt: args.prompt, options: args.options });
|
|
113
|
+
return options.query === undefined ? scriptedQuery(scripted) : options.query(args);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
return { peer: namespace, calls, scripted };
|
|
117
|
+
}
|
|
118
|
+
function createFakeClaudePeer(options = {}) {
|
|
119
|
+
return { version: options.packageVersion ?? "0.3.250" };
|
|
120
|
+
}
|
|
121
|
+
// src/testing/capture-env.ts
|
|
122
|
+
function officialCaptureEnv(input) {
|
|
123
|
+
return {
|
|
124
|
+
ANTHROPIC_BASE_URL: input.baseUrl,
|
|
125
|
+
ANTHROPIC_API_KEY: input.apiKey ?? "sk-ant-fake-hermetic-key",
|
|
126
|
+
CLAUDE_CONFIG_DIR: input.claudeConfigDir,
|
|
127
|
+
HOME: input.home,
|
|
128
|
+
...input.allowRemoteConfig === true ? {} : TRAFFIC_OPT_OUT_VARIABLES
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export {
|
|
132
|
+
withTempDir,
|
|
133
|
+
withHermeticHomes,
|
|
134
|
+
officialCaptureEnv,
|
|
135
|
+
createFakeWinterPeer,
|
|
136
|
+
createFakeKeychain,
|
|
137
|
+
createFakeClaudePeer,
|
|
138
|
+
TRAFFIC_OPT_OUT_VARIABLES as HERMETIC_TRAFFIC_OPT_OUTS
|
|
139
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Options, Query, SdkMessage } from "@yanlinglabs/winter-agent-sdk";
|
|
2
|
+
import type { RuntimeSdkPeers } from "../sdk.js";
|
|
3
|
+
export interface RecordedQueryCall {
|
|
4
|
+
prompt: string | AsyncIterable<string>;
|
|
5
|
+
options: Options;
|
|
6
|
+
}
|
|
7
|
+
export interface FakeWinterPeer {
|
|
8
|
+
/** Pass this as `peers.winter`. */
|
|
9
|
+
peer: RuntimeSdkPeers["winter"];
|
|
10
|
+
/** Every `query()` the router made, in order, with the values it forwarded. */
|
|
11
|
+
calls: RecordedQueryCall[];
|
|
12
|
+
/** The exact message objects the fake yields — for identity assertions on the stream. */
|
|
13
|
+
scripted: SdkMessage[];
|
|
14
|
+
}
|
|
15
|
+
export interface FakeWinterPeerOptions {
|
|
16
|
+
/**
|
|
17
|
+
* The package version the fake reports as its own identity (probe step 1, `peer-export`).
|
|
18
|
+
* Defaults to a version INSIDE the matrix so a test that does not care about versions constructs
|
|
19
|
+
* cleanly; pass an out-of-range value to exercise the refusal.
|
|
20
|
+
*/
|
|
21
|
+
packageVersion?: string;
|
|
22
|
+
/** Defaults to the one protocol version this router is tested against. */
|
|
23
|
+
protocolVersion?: string;
|
|
24
|
+
/** What the returned `Query` yields, in order. Defaults to one result-shaped message. */
|
|
25
|
+
messages?: SdkMessage[];
|
|
26
|
+
/** Called instead of the default generator, when a test needs the Query itself to misbehave. */
|
|
27
|
+
query?: (args: {
|
|
28
|
+
prompt: string | AsyncIterable<string>;
|
|
29
|
+
options: Options;
|
|
30
|
+
}) => Query;
|
|
31
|
+
}
|
|
32
|
+
/** A Winter peer that records what the router forwarded. See this module's header for the cast. */
|
|
33
|
+
export declare function createFakeWinterPeer(options?: FakeWinterPeerOptions): FakeWinterPeer;
|
|
34
|
+
export interface FakeClaudePeerOptions {
|
|
35
|
+
/** Defaults to the exact pin in the matrix. */
|
|
36
|
+
packageVersion?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* An official peer, for matrix tests only.
|
|
40
|
+
*
|
|
41
|
+
* It exports a version identity ON PURPOSE: without one, the matrix's second probe would resolve the
|
|
42
|
+
* REAL `@anthropic-ai/claude-agent-sdk@0.3.250` that this repository installs as a dev dependency,
|
|
43
|
+
* and an "out of range" test would pass for the wrong reason.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createFakeClaudePeer(options?: FakeClaudePeerOptions): NonNullable<RuntimeSdkPeers["claude"]>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yanlinglabs/winter-runtime-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "One door over the Winter Agent SDK and the official Claude Agent SDK: runtime selection, the official-SDK adapter, the shared session store, the handoff barrier and the cross-runtime messaging router.",
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
".": {
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
23
|
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./testing": {
|
|
26
|
+
"types": "./dist/testing/host.d.ts",
|
|
27
|
+
"default": "./dist/testing/host.js"
|
|
24
28
|
}
|
|
25
29
|
},
|
|
26
30
|
"files": [
|
|
@@ -39,11 +43,19 @@
|
|
|
39
43
|
},
|
|
40
44
|
"peerDependencies": {
|
|
41
45
|
"@anthropic-ai/claude-agent-sdk": "0.3.250",
|
|
42
|
-
"@yanlinglabs/winter-agent-sdk": ">=0.0.3 <0.1.0"
|
|
46
|
+
"@yanlinglabs/winter-agent-sdk": ">=0.0.3 <0.1.0",
|
|
47
|
+
"@yanlinglabs/winter-conformance": ">=0.0.3 <0.1.0",
|
|
48
|
+
"@yanlinglabs/winter-provider-conformance": ">=0.0.3 <0.1.0"
|
|
43
49
|
},
|
|
44
50
|
"peerDependenciesMeta": {
|
|
45
51
|
"@anthropic-ai/claude-agent-sdk": {
|
|
46
52
|
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"@yanlinglabs/winter-conformance": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"@yanlinglabs/winter-provider-conformance": {
|
|
58
|
+
"optional": true
|
|
47
59
|
}
|
|
48
60
|
},
|
|
49
61
|
"devDependencies": {
|