@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.
Files changed (151) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +1 -0
  3. package/README.md +124 -2
  4. package/THIRD_PARTY_NOTICES.md +66 -0
  5. package/ZCode-LICENSE +202 -0
  6. package/dist/index.mjs +4871 -0
  7. package/dist/native-command-plan.LICENSE.txt +221 -0
  8. package/dist/native-command-plan.json +16 -0
  9. package/dist/native-command-plan.mjs +8 -0
  10. package/dist/rpc-entry.mjs +5682 -0
  11. package/package.json +169 -4
  12. package/src/native/a2a-receipt-source.mjs +19 -0
  13. package/src/native/a2a-tools.mjs +517 -0
  14. package/src/native/activity-status.mjs +30 -0
  15. package/src/native/artifact-codec.mjs +481 -0
  16. package/src/native/artifact-store.mjs +1162 -0
  17. package/src/native/assistant-stream.mjs +76 -0
  18. package/src/native/background-command-port.mjs +95 -0
  19. package/src/native/browser-action-observation.mjs +43 -0
  20. package/src/native/browser-availability.mjs +22 -0
  21. package/src/native/browser-canvas-metrics.mjs +71 -0
  22. package/src/native/browser-dom.mjs +193 -0
  23. package/src/native/browser-playwright.mjs +1249 -0
  24. package/src/native/browser-vision.mjs +369 -0
  25. package/src/native/browser-workspace-policy.mjs +22 -0
  26. package/src/native/browser-workspace-protocol.mjs +95 -0
  27. package/src/native/browser-workspace-server.mjs +164 -0
  28. package/src/native/browser-workspace.mjs +355 -0
  29. package/src/native/captured-source.mjs +3 -0
  30. package/src/native/child-budget-allocation.mjs +87 -0
  31. package/src/native/child-capabilities.mjs +35 -0
  32. package/src/native/child-command-resource.mjs +20 -0
  33. package/src/native/child-history.mjs +188 -0
  34. package/src/native/child-merge.mjs +98 -0
  35. package/src/native/child-model-surface.mjs +19 -0
  36. package/src/native/child-observation.mjs +100 -0
  37. package/src/native/child-owner.mjs +620 -0
  38. package/src/native/child-publication.mjs +76 -0
  39. package/src/native/child-readers.mjs +366 -0
  40. package/src/native/child-tools.mjs +46 -0
  41. package/src/native/child-workspace-binding.mjs +29 -0
  42. package/src/native/child-workspace-tools.mjs +1 -0
  43. package/src/native/child-workspaces.mjs +655 -0
  44. package/src/native/children.mjs +365 -0
  45. package/src/native/command-plan-worker.mjs +14 -0
  46. package/src/native/command-plan.mjs +31 -0
  47. package/src/native/command-progress.mjs +64 -0
  48. package/src/native/command-shell.mjs +9 -0
  49. package/src/native/command-workspace-observation.mjs +36 -0
  50. package/src/native/component-tools.mjs +280 -0
  51. package/src/native/context-counter.mjs +133 -0
  52. package/src/native/context-markers.mjs +51 -0
  53. package/src/native/context-profile.mjs +72 -0
  54. package/src/native/context-status.mjs +27 -0
  55. package/src/native/conversation-command-control.mjs +133 -0
  56. package/src/native/delivery-timeline.mjs +39 -0
  57. package/src/native/dsh-background-ownership.mjs +102 -0
  58. package/src/native/dsh-managed-ownership.mjs +23 -0
  59. package/src/native/dsh-turn-contract.mjs +3 -0
  60. package/src/native/dsh-turn-ownership.mjs +54 -0
  61. package/src/native/dsh-turn-runtime.mjs +498 -0
  62. package/src/native/e2b-background-supervisor.mjs +347 -0
  63. package/src/native/e2b-background.mjs +368 -0
  64. package/src/native/e2b-command-state.mjs +40 -0
  65. package/src/native/e2b-command-tools.mjs +486 -0
  66. package/src/native/e2b-sandbox-owner.mjs +227 -0
  67. package/src/native/hook-executor.mjs +249 -0
  68. package/src/native/instruction-assembly.mjs +60 -0
  69. package/src/native/instruction-context.mjs +54 -0
  70. package/src/native/local-background-supervisor.mjs +108 -0
  71. package/src/native/local-background.mjs +207 -0
  72. package/src/native/local-command-evidence.mjs +57 -0
  73. package/src/native/local-command-policy-worker.mjs +197 -0
  74. package/src/native/local-command-policy.mjs +46 -0
  75. package/src/native/local-command-state.mjs +177 -0
  76. package/src/native/local-command-tools.mjs +302 -0
  77. package/src/native/local-execution-profile.mjs +36 -0
  78. package/src/native/local-output-policy.mjs +92 -0
  79. package/src/native/local-process-bin/darwin-arm64/launcher +0 -0
  80. package/src/native/local-process-bin/darwin-x64/launcher +0 -0
  81. package/src/native/local-process-bin/linux-arm64/launcher +0 -0
  82. package/src/native/local-process-bin/linux-x64/launcher +0 -0
  83. package/src/native/local-process-launcher.c +183 -0
  84. package/src/native/local-process-licenses/GCC-COPYING.RUNTIME +72 -0
  85. package/src/native/local-process-licenses/GCC-COPYING3 +674 -0
  86. package/src/native/local-process-licenses/NOTICE +21 -0
  87. package/src/native/local-process-licenses/musl-COPYRIGHT +193 -0
  88. package/src/native/local-process-manifest.json +32 -0
  89. package/src/native/local-process.mjs +246 -0
  90. package/src/native/local-resource-profile.mjs +32 -0
  91. package/src/native/memory-candidate.mjs +121 -0
  92. package/src/native/memory-extraction-run.mjs +60 -0
  93. package/src/native/memory-extraction-tools.mjs +116 -0
  94. package/src/native/memory-recall.mjs +65 -0
  95. package/src/native/memory-store.mjs +334 -0
  96. package/src/native/memory-tools.mjs +329 -0
  97. package/src/native/model-anthropic-cache.mjs +49 -0
  98. package/src/native/model-anthropic-tool-search.mjs +85 -0
  99. package/src/native/model-anthropic.mjs +753 -0
  100. package/src/native/model-chat-usage.mjs +24 -0
  101. package/src/native/model-inline-reasoning.mjs +111 -0
  102. package/src/native/model-media.mjs +174 -0
  103. package/src/native/model-normalize.mjs +228 -0
  104. package/src/native/model-openai-chat.mjs +561 -0
  105. package/src/native/model-port.mjs +361 -0
  106. package/src/native/model-provider-compat.mjs +129 -0
  107. package/src/native/model-replay.mjs +153 -0
  108. package/src/native/model-request.mjs +15 -0
  109. package/src/native/model-responses.mjs +1205 -0
  110. package/src/native/model-sse.mjs +171 -0
  111. package/src/native/model-surface-host.mjs +27 -0
  112. package/src/native/model-tool-search-policy.mjs +16 -0
  113. package/src/native/model-transport-observation.mjs +27 -0
  114. package/src/native/owned-child.mjs +114 -0
  115. package/src/native/owned-command.mjs +106 -0
  116. package/src/native/python-syntax-worker.mjs +60 -0
  117. package/src/native/python-syntax.mjs +50 -0
  118. package/src/native/read-interruption.mjs +16 -0
  119. package/src/native/result-stream.mjs +63 -0
  120. package/src/native/ruff-ast.mjs +95 -0
  121. package/src/native/run-observation.mjs +128 -0
  122. package/src/native/session-delivery-journal.mjs +96 -0
  123. package/src/native/session-delivery.mjs +434 -0
  124. package/src/native/shell-analysis.mjs +50 -0
  125. package/src/native/skill-disclosure.mjs +404 -0
  126. package/src/native/skill-events.mjs +164 -0
  127. package/src/native/skill-tools.mjs +813 -0
  128. package/src/native/source-page.mjs +73 -0
  129. package/src/native/sqlite-session-inbox.mjs +42 -0
  130. package/src/native/sqlite-store.mjs +617 -0
  131. package/src/native/tool-authority.mjs +99 -0
  132. package/src/native/tool-guidance.mjs +12 -0
  133. package/src/native/tool-observation.mjs +75 -0
  134. package/src/native/tool-states.mjs +14 -0
  135. package/src/native/verification-authority.mjs +37 -0
  136. package/src/native/web-tools.mjs +643 -0
  137. package/src/native/workspace-checkpoints.mjs +312 -0
  138. package/src/native/workspace-context.mjs +61 -0
  139. package/src/native/workspace-display.mjs +39 -0
  140. package/src/native/workspace-edit-match.mjs +19 -0
  141. package/src/native/workspace-edit-worker.mjs +15 -0
  142. package/src/native/workspace-editing.mjs +447 -0
  143. package/src/native/workspace-image.mjs +72 -0
  144. package/src/native/workspace-instructions.mjs +107 -0
  145. package/src/native/workspace-mutation-lock.mjs +74 -0
  146. package/src/native/workspace-patch-worker.mjs +17 -0
  147. package/src/native/workspace-patch.mjs +17 -0
  148. package/src/native/workspace-paths.mjs +225 -0
  149. package/src/native/workspace-state.mjs +272 -0
  150. package/src/native/workspace-subject-observer.mjs +84 -0
  151. 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. */