@yanlinglabs/winter-runtime-sdk 0.0.2 → 0.0.4
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/door.d.ts +23 -6
- package/dist/index-294jzb3e.js +1434 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +62 -1328
- package/dist/official/auth.d.ts +11 -0
- package/dist/selection/runtime-selection.d.ts +12 -1
- 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
package/dist/official/auth.d.ts
CHANGED
|
@@ -25,6 +25,17 @@ export declare const AUTH_FAMILY_VARIABLES: {
|
|
|
25
25
|
readonly "console-oauth": readonly ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL"];
|
|
26
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
27
|
readonly vertex: readonly ["CLAUDE_CODE_USE_VERTEX", "ANTHROPIC_VERTEX_PROJECT_ID", "CLOUD_ML_REGION", "GOOGLE_APPLICATION_CREDENTIALS", "ANTHROPIC_VERTEX_BASE_URL"];
|
|
28
|
+
/**
|
|
29
|
+
* router 0.0.4, C1: the Anthropic Console CLI's own PROFILE. Both names are NON-SECRET (a profile
|
|
30
|
+
* name and a directory path, not material) and NEITHER is a bearer credential — the profile store the
|
|
31
|
+
* CLI already manages at `ANTHROPIC_CONFIG_DIR` owns the token, so this family injects nothing that
|
|
32
|
+
* could re-point billing by itself. `officialCredentialPlan` (`door.ts`) treats it exactly like
|
|
33
|
+
* `claude-oauth`/`local-none`: an EMPTY plan, even when `provider.authRef` resolves to material, so a
|
|
34
|
+
* host that also has an API key configured never leaks it in here by accident. `ANTHROPIC_API_KEY`
|
|
35
|
+
* and `ANTHROPIC_AUTH_TOKEN` are refused for this family by the same family-membership check every
|
|
36
|
+
* other row already gets (§12's "exactly one auth family").
|
|
37
|
+
*/
|
|
38
|
+
readonly "console-profile": readonly ["ANTHROPIC_PROFILE", "ANTHROPIC_CONFIG_DIR"];
|
|
28
39
|
/**
|
|
29
40
|
* §12's last row: NONE. "Claude OAuth (D14-gated) — stored subscription credentials live inside the
|
|
30
41
|
* spool namespace; no env credential injected." The empty set is the rule, not an oversight.
|
|
@@ -34,7 +34,18 @@ export interface RuntimeSelection {
|
|
|
34
34
|
*/
|
|
35
35
|
modelRef: string;
|
|
36
36
|
family: string;
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* `"console-profile"` (router 0.0.4, C1): the Anthropic Console CLI's own PROFILE — `ANTHROPIC_PROFILE`
|
|
39
|
+
* + `ANTHROPIC_CONFIG_DIR`, no bearer credential in the child at all (the profile store the CLI already
|
|
40
|
+
* manages owns the token). Distinct from `"console-oauth"`, which injects a bearer pair the router
|
|
41
|
+
* itself resolves (`ANTHROPIC_AUTH_TOKEN` + `ANTHROPIC_BASE_URL`) — reusing that family would either
|
|
42
|
+
* demand a token this flow never has or let the two mechanisms silently race on precedence. Reachable
|
|
43
|
+
* only by explicit declaration, the same as `claude-oauth`: never inferred from a credential ref's
|
|
44
|
+
* storage kind (`authFamilyFromRefKind`) and not a member of `OFFICIAL_SERVED_AUTH_FAMILIES`, so the
|
|
45
|
+
* D13 auto-selector never routes to it on its own — a host that wants this family builds the
|
|
46
|
+
* `RuntimeSelection` for it directly, the same shape a `custom` or `claude-oauth` host already does.
|
|
47
|
+
*/
|
|
48
|
+
authFamily: "api-key" | "cloud-credential-chain" | "claude-oauth" | "console-oauth" | "console-profile" | "local-none" | "custom";
|
|
38
49
|
sdkVersion: string;
|
|
39
50
|
engineVersion?: string;
|
|
40
51
|
reason: string;
|
|
@@ -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-294jzb3e.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.4",
|
|
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": {
|