@kingsoftcloud/agentkit-harness-node 0.0.0-stage → 0.0.2
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 +201 -0
- package/NOTICE +1 -0
- package/README.md +124 -2
- package/THIRD_PARTY_NOTICES.md +66 -0
- package/ZCode-LICENSE +202 -0
- package/dist/index.mjs +4871 -0
- package/dist/native-command-plan.LICENSE.txt +221 -0
- package/dist/native-command-plan.json +16 -0
- package/dist/native-command-plan.mjs +8 -0
- package/dist/rpc-entry.mjs +5682 -0
- package/package.json +169 -4
- package/src/native/a2a-receipt-source.mjs +19 -0
- package/src/native/a2a-tools.mjs +517 -0
- package/src/native/activity-status.mjs +30 -0
- package/src/native/artifact-codec.mjs +481 -0
- package/src/native/artifact-store.mjs +1162 -0
- package/src/native/assistant-stream.mjs +76 -0
- package/src/native/background-command-port.mjs +95 -0
- package/src/native/browser-action-observation.mjs +43 -0
- package/src/native/browser-availability.mjs +22 -0
- package/src/native/browser-canvas-metrics.mjs +71 -0
- package/src/native/browser-dom.mjs +193 -0
- package/src/native/browser-playwright.mjs +1249 -0
- package/src/native/browser-vision.mjs +369 -0
- package/src/native/browser-workspace-policy.mjs +22 -0
- package/src/native/browser-workspace-protocol.mjs +95 -0
- package/src/native/browser-workspace-server.mjs +164 -0
- package/src/native/browser-workspace.mjs +355 -0
- package/src/native/captured-source.mjs +3 -0
- package/src/native/child-budget-allocation.mjs +87 -0
- package/src/native/child-capabilities.mjs +35 -0
- package/src/native/child-command-resource.mjs +20 -0
- package/src/native/child-history.mjs +188 -0
- package/src/native/child-merge.mjs +98 -0
- package/src/native/child-model-surface.mjs +19 -0
- package/src/native/child-observation.mjs +100 -0
- package/src/native/child-owner.mjs +620 -0
- package/src/native/child-publication.mjs +76 -0
- package/src/native/child-readers.mjs +366 -0
- package/src/native/child-tools.mjs +46 -0
- package/src/native/child-workspace-binding.mjs +29 -0
- package/src/native/child-workspace-tools.mjs +1 -0
- package/src/native/child-workspaces.mjs +655 -0
- package/src/native/children.mjs +365 -0
- package/src/native/command-plan-worker.mjs +14 -0
- package/src/native/command-plan.mjs +31 -0
- package/src/native/command-progress.mjs +64 -0
- package/src/native/command-shell.mjs +9 -0
- package/src/native/command-workspace-observation.mjs +36 -0
- package/src/native/component-tools.mjs +280 -0
- package/src/native/context-counter.mjs +133 -0
- package/src/native/context-markers.mjs +51 -0
- package/src/native/context-profile.mjs +72 -0
- package/src/native/context-status.mjs +27 -0
- package/src/native/conversation-command-control.mjs +133 -0
- package/src/native/delivery-timeline.mjs +39 -0
- package/src/native/dsh-background-ownership.mjs +102 -0
- package/src/native/dsh-managed-ownership.mjs +23 -0
- package/src/native/dsh-turn-contract.mjs +3 -0
- package/src/native/dsh-turn-ownership.mjs +54 -0
- package/src/native/dsh-turn-runtime.mjs +498 -0
- package/src/native/e2b-background-supervisor.mjs +347 -0
- package/src/native/e2b-background.mjs +368 -0
- package/src/native/e2b-command-state.mjs +40 -0
- package/src/native/e2b-command-tools.mjs +486 -0
- package/src/native/e2b-sandbox-owner.mjs +227 -0
- package/src/native/hook-executor.mjs +249 -0
- package/src/native/instruction-assembly.mjs +60 -0
- package/src/native/instruction-context.mjs +54 -0
- package/src/native/local-background-supervisor.mjs +108 -0
- package/src/native/local-background.mjs +207 -0
- package/src/native/local-command-evidence.mjs +57 -0
- package/src/native/local-command-policy-worker.mjs +197 -0
- package/src/native/local-command-policy.mjs +46 -0
- package/src/native/local-command-state.mjs +177 -0
- package/src/native/local-command-tools.mjs +302 -0
- package/src/native/local-execution-profile.mjs +36 -0
- package/src/native/local-output-policy.mjs +92 -0
- package/src/native/local-process-bin/darwin-arm64/launcher +0 -0
- package/src/native/local-process-bin/darwin-x64/launcher +0 -0
- package/src/native/local-process-bin/linux-arm64/launcher +0 -0
- package/src/native/local-process-bin/linux-x64/launcher +0 -0
- package/src/native/local-process-launcher.c +183 -0
- package/src/native/local-process-licenses/GCC-COPYING.RUNTIME +72 -0
- package/src/native/local-process-licenses/GCC-COPYING3 +674 -0
- package/src/native/local-process-licenses/NOTICE +21 -0
- package/src/native/local-process-licenses/musl-COPYRIGHT +193 -0
- package/src/native/local-process-manifest.json +32 -0
- package/src/native/local-process.mjs +246 -0
- package/src/native/local-resource-profile.mjs +32 -0
- package/src/native/memory-candidate.mjs +121 -0
- package/src/native/memory-extraction-run.mjs +60 -0
- package/src/native/memory-extraction-tools.mjs +116 -0
- package/src/native/memory-recall.mjs +65 -0
- package/src/native/memory-store.mjs +334 -0
- package/src/native/memory-tools.mjs +329 -0
- package/src/native/model-anthropic-cache.mjs +49 -0
- package/src/native/model-anthropic-tool-search.mjs +85 -0
- package/src/native/model-anthropic.mjs +753 -0
- package/src/native/model-chat-usage.mjs +24 -0
- package/src/native/model-inline-reasoning.mjs +111 -0
- package/src/native/model-media.mjs +174 -0
- package/src/native/model-normalize.mjs +228 -0
- package/src/native/model-openai-chat.mjs +561 -0
- package/src/native/model-port.mjs +361 -0
- package/src/native/model-provider-compat.mjs +129 -0
- package/src/native/model-replay.mjs +153 -0
- package/src/native/model-request.mjs +15 -0
- package/src/native/model-responses.mjs +1205 -0
- package/src/native/model-sse.mjs +171 -0
- package/src/native/model-surface-host.mjs +27 -0
- package/src/native/model-tool-search-policy.mjs +16 -0
- package/src/native/model-transport-observation.mjs +27 -0
- package/src/native/owned-child.mjs +114 -0
- package/src/native/owned-command.mjs +106 -0
- package/src/native/python-syntax-worker.mjs +60 -0
- package/src/native/python-syntax.mjs +50 -0
- package/src/native/read-interruption.mjs +16 -0
- package/src/native/result-stream.mjs +63 -0
- package/src/native/ruff-ast.mjs +95 -0
- package/src/native/run-observation.mjs +128 -0
- package/src/native/session-delivery-journal.mjs +96 -0
- package/src/native/session-delivery.mjs +434 -0
- package/src/native/shell-analysis.mjs +50 -0
- package/src/native/skill-disclosure.mjs +404 -0
- package/src/native/skill-events.mjs +164 -0
- package/src/native/skill-tools.mjs +813 -0
- package/src/native/source-page.mjs +73 -0
- package/src/native/sqlite-session-inbox.mjs +42 -0
- package/src/native/sqlite-store.mjs +617 -0
- package/src/native/tool-authority.mjs +99 -0
- package/src/native/tool-guidance.mjs +12 -0
- package/src/native/tool-observation.mjs +75 -0
- package/src/native/tool-states.mjs +14 -0
- package/src/native/verification-authority.mjs +37 -0
- package/src/native/web-tools.mjs +643 -0
- package/src/native/workspace-checkpoints.mjs +312 -0
- package/src/native/workspace-context.mjs +61 -0
- package/src/native/workspace-display.mjs +39 -0
- package/src/native/workspace-edit-match.mjs +19 -0
- package/src/native/workspace-edit-worker.mjs +15 -0
- package/src/native/workspace-editing.mjs +447 -0
- package/src/native/workspace-image.mjs +72 -0
- package/src/native/workspace-instructions.mjs +107 -0
- package/src/native/workspace-mutation-lock.mjs +74 -0
- package/src/native/workspace-patch-worker.mjs +17 -0
- package/src/native/workspace-patch.mjs +17 -0
- package/src/native/workspace-paths.mjs +225 -0
- package/src/native/workspace-state.mjs +272 -0
- package/src/native/workspace-subject-observer.mjs +84 -0
- package/src/native/workspace-tools.mjs +1740 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the Native artifact store.
|
|
3
|
+
*
|
|
4
|
+
* No filesystem, no I/O. Everything here must be deterministic and side
|
|
5
|
+
* effect free so it can be unit tested in isolation. The serialization and
|
|
6
|
+
* byte-digest algorithms are intentionally local: they do not need to match
|
|
7
|
+
* the Python oracle's wire format, only the permission, idempotency,
|
|
8
|
+
* integrity and pagination semantics (see contracts.mts ArtifactPort).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
12
|
+
import { parseNativeJson } from '@kingsoftcloud/agentkit-harness-core/model-json';
|
|
13
|
+
|
|
14
|
+
/** Reject anything that is not a non-empty trimmed string. */
|
|
15
|
+
export function assertNonEmptyString(value, code) {
|
|
16
|
+
if (typeof value !== 'string' || value.length === 0 || value.trim().length === 0) {
|
|
17
|
+
throw Object.assign(new Error(code), { code });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** sha256 of a byte buffer, returned as lowercase hex (no prefix). */
|
|
22
|
+
export function sha256Hex(bytes) {
|
|
23
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** sha256 of a string's UTF-8 bytes, returned as lowercase hex (no prefix). */
|
|
27
|
+
export function sha256HexOfString(text) {
|
|
28
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Canonical JSON for an arbitrary JSON value: sorted keys, no whitespace,
|
|
33
|
+
* no duplicate keys. Throws on non-JSON values (functions, symbols, cycles,
|
|
34
|
+
* non-plain prototypes) so callers cannot smuggle unstructured data.
|
|
35
|
+
*/
|
|
36
|
+
export function canonicalStringify(value) {
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const write = (item) => {
|
|
39
|
+
if (item === null) return 'null';
|
|
40
|
+
if (typeof item === 'string') return JSON.stringify(item);
|
|
41
|
+
if (typeof item === 'boolean') return item ? 'true' : 'false';
|
|
42
|
+
if (typeof item === 'bigint') return item.toString();
|
|
43
|
+
if (typeof item === 'number') {
|
|
44
|
+
if (!Number.isFinite(item)) throw Object.assign(new Error('non_json_number'), { code: 'non_json_number' });
|
|
45
|
+
if (Object.is(item, -0)) return '-0.0';
|
|
46
|
+
if (Number.isInteger(item) && !Number.isSafeInteger(item)) return item.toExponential();
|
|
47
|
+
return JSON.stringify(item);
|
|
48
|
+
}
|
|
49
|
+
if (typeof item !== 'object') {
|
|
50
|
+
throw Object.assign(new Error('non_json_value'), { code: 'non_json_value' });
|
|
51
|
+
}
|
|
52
|
+
if (seen.has(item)) throw Object.assign(new Error('json_cycle'), { code: 'json_cycle' });
|
|
53
|
+
seen.add(item);
|
|
54
|
+
let out;
|
|
55
|
+
if (Array.isArray(item)) {
|
|
56
|
+
out = '[' + item.map(write).join(',') + ']';
|
|
57
|
+
} else {
|
|
58
|
+
const proto = Object.getPrototypeOf(item);
|
|
59
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
60
|
+
throw Object.assign(new Error('non_json_object'), { code: 'non_json_object' });
|
|
61
|
+
}
|
|
62
|
+
const keys = Object.keys(item);
|
|
63
|
+
keys.sort();
|
|
64
|
+
out = '{' + keys.map((k) => JSON.stringify(k) + ':' + write(item[k])).join(',') + '}';
|
|
65
|
+
}
|
|
66
|
+
seen.delete(item);
|
|
67
|
+
return out;
|
|
68
|
+
};
|
|
69
|
+
return write(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* JSON number grammar per RFC 8259: [ minus ] int [ frac ] [ exp ], where int
|
|
76
|
+
* is a single 0 or a non-zero digit followed by digits, frac is a dot followed
|
|
77
|
+
* by one or more digits, and exp is e/E, an optional sign, and one or more
|
|
78
|
+
* digits. This rejects what Number() silently coerces but JSON does not allow:
|
|
79
|
+
* hex (0x10), a leading plus (+1), leading zeros (01), a trailing dot (1.),
|
|
80
|
+
* a leading dot (.1), and an empty exponent (1e). Number() is still applied
|
|
81
|
+
* afterward so an oversized but grammatically valid exponent (1e400) that
|
|
82
|
+
* overflows to Infinity is rejected rather than admitted as non-finite.
|
|
83
|
+
*/
|
|
84
|
+
const JSON_NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate that a value is a plain JSON object (ObjectValue). Rejects
|
|
88
|
+
* arrays, null, prototype-polluting keys and objects whose prototype was
|
|
89
|
+
* tampered (e.g. via an object literal `__proto__` setter). Used for
|
|
90
|
+
* caller-supplied metadata, which must never reach storage unvalidated.
|
|
91
|
+
*/
|
|
92
|
+
export function validateMetadataObject(value, code = 'invalid_artifact_metadata') {
|
|
93
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
94
|
+
throw Object.assign(new Error(code), { code });
|
|
95
|
+
}
|
|
96
|
+
const walk = (node) => {
|
|
97
|
+
if (node === null || typeof node !== 'object') return;
|
|
98
|
+
if (Array.isArray(node)) {
|
|
99
|
+
const arrProto = Object.getPrototypeOf(node);
|
|
100
|
+
if (arrProto !== Array.prototype) {
|
|
101
|
+
throw Object.assign(new Error(code), { code });
|
|
102
|
+
}
|
|
103
|
+
for (const item of node) walk(item);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const proto = Object.getPrototypeOf(node);
|
|
107
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
108
|
+
throw Object.assign(new Error(code), { code });
|
|
109
|
+
}
|
|
110
|
+
for (const key of Object.keys(node)) {
|
|
111
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
112
|
+
throw Object.assign(new Error(code), { code });
|
|
113
|
+
}
|
|
114
|
+
walk(node[key]);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
walk(value);
|
|
118
|
+
// Round-trip through canonical JSON to reject non-JSON values found by
|
|
119
|
+
// traversal and normalize to a plain object.
|
|
120
|
+
return parseNativeJson(canonicalStringify(value));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Strict JSON parser that rejects duplicate keys and prototype-polluting
|
|
125
|
+
* keys. Used when reading our own sidecar metadata, which we wrote, but
|
|
126
|
+
* which a tampered filesystem could have rewritten. Hand-written descent
|
|
127
|
+
* parser so duplicate-key detection is exact; string escapes are delegated
|
|
128
|
+
* to JSON.parse on the raw quoted token so surrogate pairs stay correct.
|
|
129
|
+
*/
|
|
130
|
+
export function safeJsonParse(text, code = 'invalid_artifact_record') {
|
|
131
|
+
if (typeof text !== 'string') {
|
|
132
|
+
throw Object.assign(new Error(code), { code });
|
|
133
|
+
}
|
|
134
|
+
let pos = 0;
|
|
135
|
+
const fail = () => { throw Object.assign(new Error(code), { code }); };
|
|
136
|
+
const skipWs = () => {
|
|
137
|
+
while (pos < text.length) {
|
|
138
|
+
const c = text.charCodeAt(pos);
|
|
139
|
+
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) pos += 1;
|
|
140
|
+
else break;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const parseValue = () => {
|
|
144
|
+
skipWs();
|
|
145
|
+
if (pos >= text.length) fail();
|
|
146
|
+
const c = text[pos];
|
|
147
|
+
if (c === '{') return parseObject();
|
|
148
|
+
if (c === '[') return parseArray();
|
|
149
|
+
if (c === '"') return parseString();
|
|
150
|
+
return parseLiteral();
|
|
151
|
+
};
|
|
152
|
+
const parseObject = () => {
|
|
153
|
+
pos += 1; // {
|
|
154
|
+
const obj = Object.create(null);
|
|
155
|
+
const seen = new Set();
|
|
156
|
+
skipWs();
|
|
157
|
+
if (pos < text.length && text[pos] === '}') { pos += 1; return obj; }
|
|
158
|
+
while (true) {
|
|
159
|
+
skipWs();
|
|
160
|
+
if (pos >= text.length || text[pos] !== '"') fail();
|
|
161
|
+
const key = parseString();
|
|
162
|
+
if (FORBIDDEN_KEYS.has(key)) fail();
|
|
163
|
+
if (seen.has(key)) fail();
|
|
164
|
+
seen.add(key);
|
|
165
|
+
skipWs();
|
|
166
|
+
if (pos >= text.length || text[pos] !== ':') fail();
|
|
167
|
+
pos += 1; // :
|
|
168
|
+
obj[key] = parseValue();
|
|
169
|
+
skipWs();
|
|
170
|
+
if (pos >= text.length) fail();
|
|
171
|
+
if (text[pos] === ',') { pos += 1; continue; }
|
|
172
|
+
if (text[pos] === '}') { pos += 1; break; }
|
|
173
|
+
fail();
|
|
174
|
+
}
|
|
175
|
+
return obj;
|
|
176
|
+
};
|
|
177
|
+
const parseArray = () => {
|
|
178
|
+
pos += 1; // [
|
|
179
|
+
const arr = [];
|
|
180
|
+
skipWs();
|
|
181
|
+
if (pos < text.length && text[pos] === ']') { pos += 1; return arr; }
|
|
182
|
+
while (true) {
|
|
183
|
+
arr.push(parseValue());
|
|
184
|
+
skipWs();
|
|
185
|
+
if (pos >= text.length) fail();
|
|
186
|
+
if (text[pos] === ',') { pos += 1; continue; }
|
|
187
|
+
if (text[pos] === ']') { pos += 1; break; }
|
|
188
|
+
fail();
|
|
189
|
+
}
|
|
190
|
+
return arr;
|
|
191
|
+
};
|
|
192
|
+
const parseString = () => {
|
|
193
|
+
if (text[pos] !== '"') fail();
|
|
194
|
+
let end = pos + 1;
|
|
195
|
+
while (end < text.length) {
|
|
196
|
+
const c = text.charCodeAt(end);
|
|
197
|
+
if (c === 0x5c) { end += 2; continue; } // skip escaped char
|
|
198
|
+
if (c === 0x22) break; // closing quote
|
|
199
|
+
end += 1;
|
|
200
|
+
}
|
|
201
|
+
if (end >= text.length) fail();
|
|
202
|
+
const token = text.slice(pos, end + 1);
|
|
203
|
+
pos = end + 1;
|
|
204
|
+
// Delegate unescaping to JSON.parse on the raw quoted token (surrogate
|
|
205
|
+
// pairs in \uXXXX stay correct), but coerce any escape it rejects (e.g.
|
|
206
|
+
// \z, \x41, a truncated \u00) into the stable `code` rather than letting
|
|
207
|
+
// a bare SyntaxError escape this parser without an error code.
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(token);
|
|
210
|
+
} catch {
|
|
211
|
+
fail();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const parseLiteral = () => {
|
|
215
|
+
const start = pos;
|
|
216
|
+
while (pos < text.length && !' \t\n\r{}[],:"'.includes(text[pos])) pos += 1;
|
|
217
|
+
const chunk = text.slice(start, pos);
|
|
218
|
+
if (chunk === 'true') return true;
|
|
219
|
+
if (chunk === 'false') return false;
|
|
220
|
+
if (chunk === 'null') return null;
|
|
221
|
+
// Enforce the JSON number grammar (see JSON_NUMBER) instead of trusting
|
|
222
|
+
// Number(), which coerces 0x10/+1/01/1./.1 into numbers JSON.parse rejects.
|
|
223
|
+
if (chunk === '' || !JSON_NUMBER.test(chunk)) fail();
|
|
224
|
+
if (!/[.eE]/.test(chunk) && !Number.isSafeInteger(Number(chunk))) {
|
|
225
|
+
if (chunk.replace(/^-/, '').length > 4300) fail();
|
|
226
|
+
return BigInt(chunk);
|
|
227
|
+
}
|
|
228
|
+
if (!Number.isFinite(Number(chunk))) fail();
|
|
229
|
+
return Number(chunk);
|
|
230
|
+
};
|
|
231
|
+
const root = parseValue();
|
|
232
|
+
skipWs();
|
|
233
|
+
if (pos !== text.length) fail(); // trailing garbage
|
|
234
|
+
return root;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Validate a RunScope and return its four string values. The four fields
|
|
239
|
+
* are required and must exactly match the trusted factory binding on every
|
|
240
|
+
* request; they are never concatenated into a path directly.
|
|
241
|
+
*/
|
|
242
|
+
export function validateScope(scope) {
|
|
243
|
+
if (!scope || typeof scope !== 'object') {
|
|
244
|
+
throw Object.assign(new Error('invalid_scope'), { code: 'invalid_scope' });
|
|
245
|
+
}
|
|
246
|
+
const names = ['tenantId', 'workspaceId', 'userId', 'agentId'];
|
|
247
|
+
const values = names.map((p) => scope[p]);
|
|
248
|
+
if (Object.keys(scope).length !== 4 || values.some((v) => typeof v !== 'string' || v.length === 0 || v.trim().length === 0)) {
|
|
249
|
+
throw Object.assign(new Error('invalid_scope'), { code: 'invalid_scope' });
|
|
250
|
+
}
|
|
251
|
+
return values;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** True when two RunScope objects are exactly equal across all four fields. */
|
|
255
|
+
export function scopeEquals(a, b) {
|
|
256
|
+
return !!(a && b && a.tenantId === b.tenantId && a.workspaceId === b.workspaceId && a.userId === b.userId && a.agentId === b.agentId);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Hex digest for a scope, used as the scope directory name. */
|
|
260
|
+
export function scopeDigest(scope) {
|
|
261
|
+
return sha256HexOfString(canonicalStringify(validateScope(scope)));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Hex digest for a run within a scope, used as the run directory name. */
|
|
265
|
+
export function runDigest(scope, runId) {
|
|
266
|
+
assertNonEmptyString(runId, 'invalid_run_id');
|
|
267
|
+
return sha256HexOfString(canonicalStringify([validateScope(scope), runId]));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Hex digest for an operation identity (operationKey within a scope+run).
|
|
272
|
+
* Same identity + same content/type/metadata is idempotent; same identity
|
|
273
|
+
* with different content/type/metadata is a conflict.
|
|
274
|
+
*/
|
|
275
|
+
export function identityDigest(scope, runId, operationKey) {
|
|
276
|
+
assertNonEmptyString(runId, 'invalid_run_id');
|
|
277
|
+
assertNonEmptyString(operationKey, 'invalid_operation_key');
|
|
278
|
+
return sha256HexOfString(canonicalStringify([validateScope(scope), runId, operationKey]));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Validate mediaType: non-empty, <=160 chars, no CR/LF (matches source). */
|
|
282
|
+
export function validateMediaType(mediaType) {
|
|
283
|
+
if (typeof mediaType !== 'string' || mediaType.length === 0 || mediaType.length > 160) {
|
|
284
|
+
throw Object.assign(new Error('invalid_artifact_media_type'), { code: 'invalid_artifact_media_type' });
|
|
285
|
+
}
|
|
286
|
+
if (/[\r\n]/.test(mediaType)) {
|
|
287
|
+
throw Object.assign(new Error('invalid_artifact_media_type'), { code: 'invalid_artifact_media_type' });
|
|
288
|
+
}
|
|
289
|
+
return mediaType;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Validate an ArtifactRef's shape (used on read). Exact match is enforced later. */
|
|
293
|
+
export function validateRef(ref) {
|
|
294
|
+
if (!ref || typeof ref !== 'object') {
|
|
295
|
+
throw Object.assign(new Error('invalid_artifact_ref'), { code: 'invalid_artifact_ref' });
|
|
296
|
+
}
|
|
297
|
+
const { uri, digest, mediaType, sizeBytes, metadata } = ref;
|
|
298
|
+
if (typeof uri !== 'string' || uri.length === 0 || uri.length > 2048 || !/^artifact:\/\/\S+$/.test(uri)) {
|
|
299
|
+
throw Object.assign(new Error('invalid_artifact_ref'), { code: 'invalid_artifact_ref' });
|
|
300
|
+
}
|
|
301
|
+
if (typeof digest !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(digest)) {
|
|
302
|
+
throw Object.assign(new Error('invalid_artifact_ref'), { code: 'invalid_artifact_ref' });
|
|
303
|
+
}
|
|
304
|
+
validateMediaType(mediaType);
|
|
305
|
+
if (!Number.isInteger(sizeBytes) || sizeBytes < 0 || sizeBytes > Number.MAX_SAFE_INTEGER) {
|
|
306
|
+
throw Object.assign(new Error('invalid_artifact_ref'), { code: 'invalid_artifact_ref' });
|
|
307
|
+
}
|
|
308
|
+
if (metadata === undefined || metadata === null) {
|
|
309
|
+
throw Object.assign(new Error('invalid_artifact_ref'), { code: 'invalid_artifact_ref' });
|
|
310
|
+
}
|
|
311
|
+
validateMetadataObject(metadata, 'invalid_artifact_ref');
|
|
312
|
+
return ref;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Parse artifact://{scopeDigest}/{runDigest}/{identityDigest} safely. */
|
|
316
|
+
export function parseArtifactUri(uri) {
|
|
317
|
+
const match = /^artifact:\/\/([0-9a-f]{64})\/([0-9a-f]{64})\/([0-9a-f]{64})$/.exec(uri);
|
|
318
|
+
if (!match) {
|
|
319
|
+
throw Object.assign(new Error('invalid_artifact_uri'), { code: 'invalid_artifact_uri' });
|
|
320
|
+
}
|
|
321
|
+
return { scopeDigest: match[1], runDigest: match[2], identityDigest: match[3] };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Build the canonical URI for a sealed artifact. */
|
|
325
|
+
export function buildArtifactUri(scopeDigestValue, runDigestValue, identityDigestValue) {
|
|
326
|
+
return `artifact://${scopeDigestValue}/${runDigestValue}/${identityDigestValue}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* True when text contains an unpaired UTF-16 surrogate: a high surrogate
|
|
331
|
+
* (U+D800–U+DBFF) not followed by a low surrogate, or a low surrogate
|
|
332
|
+
* (U+DC00–U+DFFF) not preceded by a high surrogate. Buffer.from(text,'utf8')
|
|
333
|
+
* silently replaces every unpaired surrogate with U+FFFD, so a store
|
|
334
|
+
* write/read would change the user's bytes without any error. Detecting this
|
|
335
|
+
* before encoding lets us reject the content rather than corrupt it. Valid
|
|
336
|
+
* surrogate pairs (astral characters such as U+1D400) pass untouched.
|
|
337
|
+
*/
|
|
338
|
+
function hasUnpairedSurrogate(text) {
|
|
339
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
340
|
+
const c = text.charCodeAt(i);
|
|
341
|
+
if (c >= 0xD800 && c <= 0xDBFF) {
|
|
342
|
+
// High surrogate must be immediately followed by a low surrogate.
|
|
343
|
+
if (i + 1 >= text.length) return true; // high at end of string
|
|
344
|
+
const next = text.charCodeAt(i + 1);
|
|
345
|
+
if (next < 0xDC00 || next > 0xDFFF) return true; // high without low
|
|
346
|
+
i += 1; // consume the paired low surrogate
|
|
347
|
+
} else if (c >= 0xDC00 && c <= 0xDFFF) {
|
|
348
|
+
return true; // low surrogate without a preceding high
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** UTF-8 encode without truncation or replacement; reject unpaired surrogates. */
|
|
355
|
+
export function encodeUtf8(text) {
|
|
356
|
+
if (typeof text !== 'string') {
|
|
357
|
+
throw Object.assign(new Error('invalid_artifact_content'), { code: 'invalid_artifact_content' });
|
|
358
|
+
}
|
|
359
|
+
if (hasUnpairedSurrogate(text)) {
|
|
360
|
+
throw Object.assign(new Error('invalid_artifact_content'), { code: 'invalid_artifact_content' });
|
|
361
|
+
}
|
|
362
|
+
return Buffer.from(text, 'utf8');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* UTF-8 decode in fatal mode (never substitute replacement characters) while
|
|
367
|
+
* preserving a leading BOM. The default TextDecoder consumes a leading U+FEFF,
|
|
368
|
+
* so a stored BOM would not round-trip; ignoreBOM keeps it in the output,
|
|
369
|
+
* matching Python's strict payload.decode('utf-8') and the workspace-editing
|
|
370
|
+
* reader. Invalid byte sequences still throw artifact_content_not_utf8.
|
|
371
|
+
*/
|
|
372
|
+
export function decodeUtf8Fatal(bytes) {
|
|
373
|
+
try {
|
|
374
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw Object.assign(new Error('artifact_content_not_utf8'), { code: 'artifact_content_not_utf8' });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Count Unicode code points (aligns with Python len(str) / offset_chars). */
|
|
381
|
+
export function codePointLength(text) {
|
|
382
|
+
let count = 0;
|
|
383
|
+
for (const _ of text) count += 1; // eslint-disable-line no-unused-vars
|
|
384
|
+
return count;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Slice text by Unicode code point index, aligning with Python
|
|
389
|
+
* text[offset:offset+limit]. Returns { content, nextOffset } where nextOffset
|
|
390
|
+
* is null when the page reached the end. Rejects negative / fractional /
|
|
391
|
+
* out-of-bounds offsets and non-positive / oversized limits.
|
|
392
|
+
*/
|
|
393
|
+
export function sliceByCodePoints(text, offset, limit, maxLimit) {
|
|
394
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
395
|
+
throw Object.assign(new Error('invalid_artifact_offset'), { code: 'invalid_artifact_offset' });
|
|
396
|
+
}
|
|
397
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > maxLimit) {
|
|
398
|
+
throw Object.assign(new Error('invalid_artifact_limit'), { code: 'invalid_artifact_limit' });
|
|
399
|
+
}
|
|
400
|
+
const total = codePointLength(text);
|
|
401
|
+
if (offset > total) {
|
|
402
|
+
throw Object.assign(new Error('artifact_offset_beyond_content'), { code: 'artifact_offset_beyond_content' });
|
|
403
|
+
}
|
|
404
|
+
const end = Math.min(offset + limit, total);
|
|
405
|
+
const chars = Array.from(text);
|
|
406
|
+
const content = chars.slice(offset, end).join('');
|
|
407
|
+
const nextOffset = end >= total ? null : end;
|
|
408
|
+
return { content, nextOffset };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Validate a positive integer option with a default and upper bound. */
|
|
412
|
+
export function validateLimitOption(value, name, defaultValue, max) {
|
|
413
|
+
if (value === undefined || value === null) return defaultValue;
|
|
414
|
+
if (!Number.isInteger(value) || value < 1 || value > max) {
|
|
415
|
+
throw Object.assign(new Error(`invalid_${name}`), { code: `invalid_${name}` });
|
|
416
|
+
}
|
|
417
|
+
return value;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Validate a byte-size limit option (positive integer). */
|
|
421
|
+
export function validateBytesOption(value, name, defaultValue, max) {
|
|
422
|
+
if (value === undefined || value === null) return defaultValue;
|
|
423
|
+
if (!Number.isInteger(value) || value < 1 || value > max) {
|
|
424
|
+
throw Object.assign(new Error(`invalid_${name}`), { code: `invalid_${name}` });
|
|
425
|
+
}
|
|
426
|
+
return value;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** True when two JSON values are structurally equal under canonical form. */
|
|
430
|
+
export function jsonEquals(a, b) {
|
|
431
|
+
return canonicalStringify(a) === canonicalStringify(b);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Short random hex nonce for unique temp file names across processes. */
|
|
435
|
+
export function tempNonce() {
|
|
436
|
+
return randomBytes(9).toString('hex') + process.pid.toString(36);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Strict validator for a durable sidecar record read back from disk. Every
|
|
441
|
+
* field is type-checked without coercion (no Number()/String()), and the
|
|
442
|
+
* uri/digest/mediaType/sizeBytes/metadata shape must match the contract.
|
|
443
|
+
* The caller additionally asserts that the uri's scope/run/identity match
|
|
444
|
+
* the on-disk path. Returns the validated, plain-prototype record.
|
|
445
|
+
*
|
|
446
|
+
* @param {string} text Raw sidecar text.
|
|
447
|
+
* @param {string} code Error code to throw on any failure.
|
|
448
|
+
* @returns {{ uri: string, digest: string, mediaType: string, sizeBytes: number, metadata: object, createdAt?: string }}
|
|
449
|
+
*/
|
|
450
|
+
export function validateDurableRecord(text, code = 'invalid_artifact_record') {
|
|
451
|
+
const meta = safeJsonParse(text, code);
|
|
452
|
+
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
453
|
+
throw Object.assign(new Error(code), { code });
|
|
454
|
+
}
|
|
455
|
+
const { uri, digest, mediaType, sizeBytes, metadata, createdAt } = meta;
|
|
456
|
+
if (typeof uri !== 'string') {
|
|
457
|
+
throw Object.assign(new Error(code), { code });
|
|
458
|
+
}
|
|
459
|
+
const m = /^artifact:\/\/([0-9a-f]{64})\/([0-9a-f]{64})\/([0-9a-f]{64})$/.exec(uri);
|
|
460
|
+
if (!m) throw Object.assign(new Error(code), { code });
|
|
461
|
+
if (typeof digest !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(digest)) {
|
|
462
|
+
throw Object.assign(new Error(code), { code });
|
|
463
|
+
}
|
|
464
|
+
if (typeof mediaType !== 'string' || mediaType.length === 0 || mediaType.length > 160 || /[\r\n]/.test(mediaType)) {
|
|
465
|
+
throw Object.assign(new Error(code), { code });
|
|
466
|
+
}
|
|
467
|
+
if (typeof sizeBytes !== 'number' || !Number.isInteger(sizeBytes) || sizeBytes < 0 || sizeBytes > Number.MAX_SAFE_INTEGER) {
|
|
468
|
+
throw Object.assign(new Error(code), { code });
|
|
469
|
+
}
|
|
470
|
+
if (metadata === undefined || metadata === null || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
471
|
+
throw Object.assign(new Error(code), { code });
|
|
472
|
+
}
|
|
473
|
+
// Reject prototype-polluting / non-plain metadata in a stored record.
|
|
474
|
+
validateMetadataObject(metadata, code);
|
|
475
|
+
if (createdAt !== undefined && (typeof createdAt !== 'string' || createdAt.length === 0 || createdAt.length > 64)) {
|
|
476
|
+
throw Object.assign(new Error(code), { code });
|
|
477
|
+
}
|
|
478
|
+
return { uri, digest, mediaType, sizeBytes, metadata, createdAt };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** True when two JSON values are structurally equal under canonical form. */
|