@tangle-network/agent-interface 5.9.0 → 5.10.0

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 CHANGED
@@ -424,3 +424,5 @@ must validate source ownership and active generation bindings for its own runtim
424
424
  before admitting a knowledge tool. A profile copied to another owner does not
425
425
  confer access. Hosts unable to resolve the saved pins must fail closed rather than
426
426
  inline JSON into instructions, substitute a newer generation or silently omit them.
427
+
428
+ Profile collection admission uses the shared [contract limits](src/contract-limits.ts) before parsing elements or encoding record keys. Training receipts and knowledge references retain their own typed limits in [the profile schema](src/profile-schema.ts). Opaque metadata is admitted and detached by value as bounded portable data; accessors are refused and cross-key object identity is not preserved. Snapshot validation precedes the final detached copy, preserving explicit undefined fields, canonical hashes and nested isolation. The generic serialized-JSON byte bound is not a whole-profile transport limit.
@@ -1,4 +1,10 @@
1
1
  import type { AgentCandidateJsonValue } from "./agent-candidate.js";
2
+ type ProfilePath = readonly (string | number)[];
3
+ /** Why a value cannot be AgentProfile JSON, with the path of the offending entry. */
4
+ export declare class AgentProfileValueError extends Error {
5
+ readonly path: ProfilePath;
6
+ constructor(path: ProfilePath, reason: string);
7
+ }
2
8
  /**
3
9
  * Normalize profile-shaped data into the canonical JSON domain used for public
4
10
  * profile identity. Undefined object entries are omitted; unsupported values,
@@ -8,5 +14,13 @@ import type { AgentCandidateJsonValue } from "./agent-candidate.js";
8
14
  * profile-level operations that own validation, identity, or diff semantics.
9
15
  */
10
16
  export declare function canonicalAgentProfileValue(value: unknown): AgentCandidateJsonValue | undefined;
17
+ /**
18
+ * Throw the path-specific reason a value refused by the bounded admission walker is
19
+ * not profile JSON. Reads no accessor and throws at the contract node and depth
20
+ * bounds, so hostile input stays cheap; returns only when the refusal was a bound
21
+ * this walker does not model, such as string or entry length.
22
+ */
23
+ export declare function assertAgentProfileValue(value: unknown, path: ProfilePath): void;
11
24
  /** Canonical serialization after applying AgentProfile normalization rules. */
12
25
  export declare function canonicalAgentProfileJson(value: unknown): string | undefined;
26
+ export {};
@@ -1,4 +1,14 @@
1
1
  import { canonicalCandidateJson, isWellFormedUnicode, } from "./agent-candidate-schema-common.js";
2
+ import { CONTRACT_MAX_JSON_DEPTH, CONTRACT_MAX_JSON_NODES } from "./contract-limits.js";
3
+ /** Why a value cannot be AgentProfile JSON, with the path of the offending entry. */
4
+ export class AgentProfileValueError extends Error {
5
+ path;
6
+ constructor(path, reason) {
7
+ super(`AgentProfile ${renderPath(path)} ${reason}`);
8
+ this.path = path;
9
+ this.name = "AgentProfileValueError";
10
+ }
11
+ }
2
12
  /**
3
13
  * Normalize profile-shaped data into the canonical JSON domain used for public
4
14
  * profile identity. Undefined object entries are omitted; unsupported values,
@@ -10,66 +20,57 @@ import { canonicalCandidateJson, isWellFormedUnicode, } from "./agent-candidate-
10
20
  export function canonicalAgentProfileValue(value) {
11
21
  return normalizeAgentProfileValue(value, [], new Set());
12
22
  }
23
+ /**
24
+ * Throw the path-specific reason a value refused by the bounded admission walker is
25
+ * not profile JSON. Reads no accessor and throws at the contract node and depth
26
+ * bounds, so hostile input stays cheap; returns only when the refusal was a bound
27
+ * this walker does not model, such as string or entry length.
28
+ */
29
+ export function assertAgentProfileValue(value, path) {
30
+ normalizeAgentProfileValue(value, path, new Set(), { nodes: CONTRACT_MAX_JSON_NODES, base: path.length });
31
+ }
13
32
  /** Canonical serialization after applying AgentProfile normalization rules. */
14
33
  export function canonicalAgentProfileJson(value) {
15
34
  const normalized = canonicalAgentProfileValue(value);
16
- return normalized === undefined
17
- ? undefined
18
- : canonicalCandidateJson(normalized);
35
+ return normalized === undefined ? undefined : canonicalCandidateJson(normalized);
19
36
  }
20
- function normalizeAgentProfileValue(value, path, ancestors) {
21
- if (value === undefined)
22
- return undefined;
23
- if (value === null ||
24
- typeof value === "boolean" ||
25
- typeof value === "string") {
26
- return value;
27
- }
28
- if (typeof value === "number") {
29
- if (!Number.isFinite(value)) {
30
- throw new Error(`AgentProfile ${renderPath(path)} must be finite`);
31
- }
37
+ function normalizeAgentProfileValue(value, path, ancestors, bounds) {
38
+ const refuse = (reason, at = path) => { throw new AgentProfileValueError(at, reason); };
39
+ if (bounds && --bounds.nodes < 0)
40
+ refuse("exceeds the contract node bound");
41
+ if (value === undefined || value === null || typeof value === "boolean" || typeof value === "string")
32
42
  return value;
33
- }
34
- if (typeof value !== "object") {
35
- throw new Error(`AgentProfile ${renderPath(path)} is not JSON serializable`);
36
- }
37
- if (ancestors.has(value)) {
38
- throw new Error(`AgentProfile ${renderPath(path)} must be acyclic`);
39
- }
43
+ if (typeof value === "number")
44
+ return Number.isFinite(value) ? value : refuse("must be finite");
45
+ if (typeof value !== "object")
46
+ return refuse("is not JSON serializable");
47
+ if (ancestors.has(value))
48
+ refuse("must be acyclic");
40
49
  const prototype = Object.getPrototypeOf(value);
41
- if (!Array.isArray(value) &&
42
- prototype !== Object.prototype &&
43
- prototype !== null) {
44
- throw new Error(`AgentProfile ${renderPath(path)} must be a plain JSON object`);
45
- }
50
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
51
+ refuse("must be a plain JSON object");
52
+ if (bounds && path.length - bounds.base >= CONTRACT_MAX_JSON_DEPTH)
53
+ refuse("exceeds the contract depth");
46
54
  const nextAncestors = new Set(ancestors).add(value);
47
- if (Array.isArray(value)) {
48
- return Array.from({ length: value.length }, (_, index) => {
49
- if (!Object.prototype.hasOwnProperty.call(value, index)) {
50
- throw new Error(`AgentProfile ${renderPath([...path, index])} cannot be a sparse array hole`);
51
- }
52
- const entry = value[index];
53
- const normalized = normalizeAgentProfileValue(entry, [...path, index], nextAncestors);
54
- if (normalized === undefined) {
55
- throw new Error(`AgentProfile ${renderPath([...path, index])} cannot be undefined`);
56
- }
57
- return normalized;
58
- });
59
- }
55
+ // Entries are read through own data descriptors so refusing a value never runs an accessor.
56
+ const entry = (key) => {
57
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
58
+ if (!descriptor)
59
+ return refuse("cannot be a sparse array hole", [...path, key]);
60
+ if (!("value" in descriptor))
61
+ return refuse("must be a data property", [...path, key]);
62
+ const normalized = normalizeAgentProfileValue(descriptor.value, [...path, key], nextAncestors, bounds);
63
+ return normalized === undefined && typeof key === "number" ? refuse("cannot be undefined", [...path, key]) : normalized;
64
+ };
65
+ if (Array.isArray(value))
66
+ return Array.from({ length: value.length }, (_, index) => entry(index));
60
67
  const material = Object.create(null);
61
- for (const [key, entry] of Object.entries(value)) {
62
- if (!isWellFormedUnicode(key)) {
63
- throw new Error(`AgentProfile ${renderPath(path)} has a record key that is not valid Unicode`);
64
- }
65
- const normalized = normalizeAgentProfileValue(entry, [...path, key], nextAncestors);
68
+ for (const key of Object.keys(value)) {
69
+ if (!isWellFormedUnicode(key))
70
+ refuse("has a record key that is not valid Unicode");
71
+ const normalized = entry(key);
66
72
  if (normalized !== undefined) {
67
- Object.defineProperty(material, key, {
68
- value: normalized,
69
- enumerable: true,
70
- configurable: true,
71
- writable: true,
72
- });
73
+ Object.defineProperty(material, key, { value: normalized, enumerable: true, configurable: true, writable: true });
73
74
  }
74
75
  }
75
76
  return material;
@@ -9,7 +9,7 @@ import { agentProfileSchema } from "./profile-schema.js";
9
9
  * existing canonical profile JSON domain fail instead of becoming mutable state.
10
10
  */
11
11
  export function snapshotAgentProfile(value) {
12
- const parsed = agentProfileSchema.parse(structuredClone(value));
12
+ const parsed = structuredClone(agentProfileSchema.parse(value));
13
13
  canonicalAgentProfileValue(parsed);
14
14
  return deepFreeze(parsed);
15
15
  }
@@ -12,23 +12,16 @@ export declare const CONTRACT_MAX_JSON_NODES = 8192;
12
12
  export declare const boundedStringSchema: z.ZodString;
13
13
  export declare const confidentialAttestationQuoteSchema: z.ZodString;
14
14
  export declare const boundedIdentifierSchema: z.ZodString;
15
- /**
16
- * Validate JSON without asking Zod to recursively copy an attacker-sized
17
- * value. This is intentionally iterative so depth and collection limits are
18
- * checked before canonical serialization.
19
- */
15
+ /** Admit ordinary JSON before recursive parsing, copying or hashing. */
20
16
  export declare function isBoundedJsonValue(value: unknown): boolean;
21
- /**
22
- * Validate provider event and terminal content. Unlike ordinary metadata,
23
- * content may contain a single large transcript or tool result. It remains
24
- * finite, plain JSON with the normal structural limits, and its complete JSON
25
- * representation is capped exactly by UTF-8 bytes.
26
- */
17
+ /** Event content permits large strings within the exact serialized UTF-8 budget. */
27
18
  export declare function isBoundedEventContentJson(value: unknown, { omitUndefinedObjectFields }?: {
28
19
  omitUndefinedObjectFields?: boolean;
29
20
  }): boolean;
30
- /** Validate digest input while matching JSON's omission of undefined object fields. */
31
- export declare function isBoundedJsonMaterial(value: unknown): boolean;
21
+ /** Digest material omits undefined object fields; portable profile values require data properties. */
22
+ export declare function isBoundedJsonMaterial(value: unknown, { dataPropertiesOnly }?: {
23
+ dataPropertiesOnly?: boolean;
24
+ }): boolean;
32
25
  export declare const boundedJsonSchema: z.ZodCustom<unknown, unknown>;
33
26
  export declare const boundedJsonRecordSchema: z.ZodCustom<Record<string, unknown>, Record<string, unknown>>;
34
27
  /** One provider event payload or terminal response, bounded as serialized UTF-8 JSON. */
@@ -17,72 +17,9 @@ export const boundedIdentifierSchema = boundedStringSchema
17
17
  .min(1)
18
18
  .max(CONTRACT_MAX_IDENTIFIER_LENGTH)
19
19
  .refine((value) => value.trim() === value, "identifier cannot have outer whitespace");
20
- /**
21
- * Validate JSON without asking Zod to recursively copy an attacker-sized
22
- * value. This is intentionally iterative so depth and collection limits are
23
- * checked before canonical serialization.
24
- */
20
+ /** Admit ordinary JSON before recursive parsing, copying or hashing. */
25
21
  export function isBoundedJsonValue(value) {
26
- const pending = [{ value, depth: 0 }];
27
- const ancestors = new Set();
28
- let nodes = 0;
29
- while (pending.length > 0) {
30
- const item = pending.pop();
31
- if (!item)
32
- continue;
33
- nodes += 1;
34
- if (nodes > CONTRACT_MAX_JSON_NODES)
35
- return false;
36
- const current = item.value;
37
- if (item.leave) {
38
- ancestors.delete(current);
39
- continue;
40
- }
41
- if (current === undefined)
42
- return false;
43
- if (current === null || typeof current === "boolean")
44
- continue;
45
- if (typeof current === "string") {
46
- if (current.length > CONTRACT_MAX_STRING_LENGTH)
47
- return false;
48
- continue;
49
- }
50
- if (typeof current === "number") {
51
- if (!Number.isFinite(current))
52
- return false;
53
- continue;
54
- }
55
- if (typeof current !== "object" || item.depth >= CONTRACT_MAX_JSON_DEPTH) {
56
- return false;
57
- }
58
- if (ancestors.has(current))
59
- return false;
60
- ancestors.add(current);
61
- pending.push({ value: current, depth: item.depth, leave: true });
62
- if (Array.isArray(current)) {
63
- if (current.length > CONTRACT_MAX_ARRAY_LENGTH)
64
- return false;
65
- for (const entry of current) {
66
- pending.push({ value: entry, depth: item.depth + 1 });
67
- }
68
- continue;
69
- }
70
- const prototype = Object.getPrototypeOf(current);
71
- if (prototype !== Object.prototype && prototype !== null)
72
- return false;
73
- const keys = Object.keys(current);
74
- if (keys.length > CONTRACT_MAX_MAP_ENTRIES)
75
- return false;
76
- for (const key of keys) {
77
- if (key.length > CONTRACT_MAX_IDENTIFIER_LENGTH)
78
- return false;
79
- pending.push({
80
- value: current[key],
81
- depth: item.depth + 1,
82
- });
83
- }
84
- }
85
- return true;
22
+ return boundedJson(value);
86
23
  }
87
24
  /** Exact serialized JSON UTF-8 byte count for one string scalar. */
88
25
  function serializedJsonStringBytes(value) {
@@ -122,187 +59,104 @@ function serializedJsonStringBytes(value) {
122
59
  }
123
60
  return bytes;
124
61
  }
125
- /**
126
- * Validate provider event and terminal content. Unlike ordinary metadata,
127
- * content may contain a single large transcript or tool result. It remains
128
- * finite, plain JSON with the normal structural limits, and its complete JSON
129
- * representation is capped exactly by UTF-8 bytes.
130
- */
62
+ /** Event content permits large strings within the exact serialized UTF-8 budget. */
131
63
  export function isBoundedEventContentJson(value, { omitUndefinedObjectFields = false } = {}) {
64
+ return boundedJson(value, { content: true, omitUndefinedObjectFields });
65
+ }
66
+ /** Digest material omits undefined object fields; portable profile values require data properties. */
67
+ export function isBoundedJsonMaterial(value, { dataPropertiesOnly = false } = {}) {
68
+ return boundedJson(value, { material: true, omitUndefinedObjectFields: true, dataPropertiesOnly });
69
+ }
70
+ /** One iterative walker owns structural admission; typed domains keep their own schema limits. */
71
+ function boundedJson(value, options = {}) {
132
72
  const pending = [
133
73
  { value, depth: 0 },
134
74
  ];
135
75
  const ancestors = new Set();
136
76
  let nodes = 0;
137
77
  let bytes = 0;
138
- const addBytes = (additional) => {
139
- bytes += additional;
140
- return bytes <= CONTRACT_MAX_JSON_BYTES;
141
- };
142
- while (pending.length > 0) {
78
+ const addBytes = (count) => !options.content || (bytes += count) <= CONTRACT_MAX_JSON_BYTES;
79
+ while (pending.length) {
143
80
  const item = pending.pop();
144
- if (!item)
145
- continue;
146
81
  const current = item.value;
147
- if (current === undefined && item.omitUndefined === true)
148
- continue;
149
- nodes += 1;
150
- if (nodes > CONTRACT_MAX_JSON_NODES)
82
+ if ((!item.leave || !options.material) && ++nodes > CONTRACT_MAX_JSON_NODES)
151
83
  return false;
84
+ if (current === undefined && item.omitUndefined)
85
+ continue;
152
86
  if (item.leave) {
153
87
  ancestors.delete(current);
154
88
  continue;
155
89
  }
156
- if (current === null) {
157
- if (!addBytes(4))
158
- return false;
159
- continue;
160
- }
161
- if (typeof current === "boolean") {
162
- if (!addBytes(current ? 4 : 5))
90
+ if (current === null || typeof current === "boolean") {
91
+ if (!addBytes(current === null ? 4 : current ? 4 : 5))
163
92
  return false;
164
93
  continue;
165
94
  }
166
95
  if (typeof current === "string") {
167
- if (!addBytes(serializedJsonStringBytes(current)))
96
+ if (options.content ? !addBytes(serializedJsonStringBytes(current)) : current.length > CONTRACT_MAX_STRING_LENGTH)
168
97
  return false;
169
98
  continue;
170
99
  }
171
100
  if (typeof current === "number") {
172
- if (!Number.isFinite(current))
173
- return false;
174
- if (!addBytes(JSON.stringify(current).length))
101
+ if (!Number.isFinite(current) || !addBytes(JSON.stringify(current).length))
175
102
  return false;
176
103
  continue;
177
104
  }
178
- if (typeof current !== "object" || item.depth >= CONTRACT_MAX_JSON_DEPTH) {
179
- return false;
180
- }
181
- if (ancestors.has(current))
105
+ if (typeof current !== "object" || item.depth >= CONTRACT_MAX_JSON_DEPTH || ancestors.has(current))
182
106
  return false;
183
107
  ancestors.add(current);
184
108
  pending.push({ value: current, depth: item.depth, leave: true });
185
- if (Array.isArray(current)) {
186
- if (current.length > CONTRACT_MAX_ARRAY_LENGTH)
187
- return false;
188
- if (!addBytes(2 + Math.max(current.length - 1, 0)))
189
- return false;
190
- const prototype = Object.getPrototypeOf(current);
191
- if (prototype !== Array.prototype && prototype !== null)
192
- return false;
193
- const keys = Reflect.ownKeys(current);
194
- const entryKeys = keys.filter((key) => key !== "length");
195
- if (entryKeys.length !== current.length)
196
- return false;
197
- if (entryKeys.some((key) => {
198
- if (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key))
199
- return true;
200
- const index = Number(key);
201
- return !Number.isSafeInteger(index) || index < 0 || index >= current.length || index >= 4_294_967_295;
202
- })) {
109
+ const array = Array.isArray(current);
110
+ if (array && current.length > CONTRACT_MAX_ARRAY_LENGTH)
111
+ return false;
112
+ const prototype = Object.getPrototypeOf(current);
113
+ if ((!array && prototype !== Object.prototype && prototype !== null) ||
114
+ (array && options.content && prototype !== Array.prototype && prototype !== null))
115
+ return false;
116
+ if (options.content || options.dataPropertiesOnly) {
117
+ const keys = Reflect.ownKeys(current).filter((key) => !array || key !== "length");
118
+ if (array ? keys.length !== current.length : keys.length > CONTRACT_MAX_MAP_ENTRIES)
203
119
  return false;
204
- }
205
- for (const key of entryKeys) {
120
+ let entries = 0;
121
+ for (const key of keys) {
122
+ if (typeof key !== "string" || (!array && key.length > CONTRACT_MAX_IDENTIFIER_LENGTH))
123
+ return false;
124
+ if (array && (!/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= current.length || Number(key) >= 4_294_967_295))
125
+ return false;
206
126
  const descriptor = Object.getOwnPropertyDescriptor(current, key);
207
- if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor))
127
+ if (!descriptor?.enumerable || !("value" in descriptor))
128
+ return false;
129
+ if (!array && descriptor.value === undefined && options.omitUndefinedObjectFields) {
130
+ if (++nodes > CONTRACT_MAX_JSON_NODES)
131
+ return false;
132
+ continue;
133
+ }
134
+ if (++entries > (array ? CONTRACT_MAX_ARRAY_LENGTH : CONTRACT_MAX_MAP_ENTRIES))
135
+ return false;
136
+ if (!array && !addBytes(serializedJsonStringBytes(key) + 1))
208
137
  return false;
209
138
  pending.push({ value: descriptor.value, depth: item.depth + 1 });
210
139
  }
211
- continue;
212
- }
213
- const prototype = Object.getPrototypeOf(current);
214
- if (prototype !== Object.prototype && prototype !== null)
215
- return false;
216
- const entries = [];
217
- for (const key of Reflect.ownKeys(current)) {
218
- if (typeof key !== "string" || key.length > CONTRACT_MAX_IDENTIFIER_LENGTH)
219
- return false;
220
- const descriptor = Object.getOwnPropertyDescriptor(current, key);
221
- if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor))
222
- return false;
223
- if (descriptor.value === undefined && omitUndefinedObjectFields)
224
- continue;
225
- entries.push([key, descriptor]);
226
- if (entries.length > CONTRACT_MAX_MAP_ENTRIES)
140
+ if (!addBytes(2 + Math.max(entries - 1, 0)))
227
141
  return false;
228
142
  }
229
- if (!addBytes(2 + Math.max(entries.length - 1, 0)))
230
- return false;
231
- for (const [key, descriptor] of entries) {
232
- if (!addBytes(serializedJsonStringBytes(key) + 1))
233
- return false;
234
- pending.push({
235
- value: descriptor.value,
236
- depth: item.depth + 1,
237
- omitUndefined: omitUndefinedObjectFields,
238
- });
239
- }
240
- }
241
- return true;
242
- }
243
- /** Validate digest input while matching JSON's omission of undefined object fields. */
244
- export function isBoundedJsonMaterial(value) {
245
- const pending = [{ value, depth: 0 }];
246
- const ancestors = new Set();
247
- let nodes = 0;
248
- while (pending.length > 0) {
249
- const item = pending.pop();
250
- if (!item)
251
- continue;
252
- if (item.leave) {
253
- ancestors.delete(item.value);
254
- continue;
255
- }
256
- if (item.value === undefined) {
257
- if (item.omitUndefined === true)
258
- continue;
259
- return false;
260
- }
261
- nodes += 1;
262
- if (nodes > CONTRACT_MAX_JSON_NODES)
263
- return false;
264
- const current = item.value;
265
- if (current === null || typeof current === "boolean")
266
- continue;
267
- if (typeof current === "string") {
268
- if (current.length > CONTRACT_MAX_STRING_LENGTH)
269
- return false;
270
- continue;
271
- }
272
- if (typeof current === "number") {
273
- if (!Number.isFinite(current))
274
- return false;
275
- continue;
276
- }
277
- if (typeof current !== "object" || item.depth >= CONTRACT_MAX_JSON_DEPTH) {
278
- return false;
279
- }
280
- if (ancestors.has(current))
281
- return false;
282
- ancestors.add(current);
283
- pending.push({ value: current, depth: item.depth, leave: true });
284
- if (Array.isArray(current)) {
285
- if (current.length > CONTRACT_MAX_ARRAY_LENGTH)
286
- return false;
287
- for (const entry of current) {
143
+ else if (array) {
144
+ for (const entry of current)
288
145
  pending.push({ value: entry, depth: item.depth + 1 });
289
- }
290
- continue;
291
146
  }
292
- const prototype = Object.getPrototypeOf(current);
293
- if (prototype !== Object.prototype && prototype !== null)
294
- return false;
295
- const keys = Object.keys(current);
296
- if (keys.length > CONTRACT_MAX_MAP_ENTRIES)
297
- return false;
298
- for (const key of keys) {
299
- if (key.length > CONTRACT_MAX_IDENTIFIER_LENGTH)
147
+ else {
148
+ const keys = Object.keys(current);
149
+ if (keys.length > CONTRACT_MAX_MAP_ENTRIES)
300
150
  return false;
301
- pending.push({
302
- value: current[key],
303
- depth: item.depth + 1,
304
- omitUndefined: true,
305
- });
151
+ for (const key of keys) {
152
+ if (key.length > CONTRACT_MAX_IDENTIFIER_LENGTH)
153
+ return false;
154
+ pending.push({
155
+ value: current[key],
156
+ depth: item.depth + 1,
157
+ omitUndefined: options.omitUndefinedObjectFields,
158
+ });
159
+ }
306
160
  }
307
161
  }
308
162
  return true;
@@ -6,4 +6,7 @@ import { z } from "zod";
6
6
  * sees it, then restore the exact UTF-16 key with defineProperty so all string
7
7
  * keys allowed by `Record<string, ...>` remain data rather than object syntax.
8
8
  */
9
- export declare function ownPropertyRecordSchema<ValueSchema extends z.ZodType>(valueSchema: ValueSchema): z.ZodType<Record<string, z.output<ValueSchema>>>;
9
+ export declare function ownPropertyRecordSchema<ValueSchema extends z.ZodType>(valueSchema: ValueSchema, { maxEntries, prepareValue }?: {
10
+ maxEntries?: number;
11
+ prepareValue?: (key: string, value: unknown) => unknown;
12
+ }): z.ZodType<Record<string, z.output<ValueSchema>>>;
@@ -1,5 +1,7 @@
1
1
  import { z } from "zod";
2
+ import { CONTRACT_MAX_IDENTIFIER_LENGTH, CONTRACT_MAX_MAP_ENTRIES } from "./contract-limits.js";
2
3
  import { isWellFormedUnicode } from "./agent-candidate-schema-common.js";
4
+ import { AgentProfileValueError } from "./agent-profile-canonical.js";
3
5
  /**
4
6
  * Zod records currently assign parsed values onto an ordinary object. The
5
7
  * special own key `__proto__` therefore invokes the legacy prototype setter
@@ -7,8 +9,48 @@ import { isWellFormedUnicode } from "./agent-candidate-schema-common.js";
7
9
  * sees it, then restore the exact UTF-16 key with defineProperty so all string
8
10
  * keys allowed by `Record<string, ...>` remain data rather than object syntax.
9
11
  */
10
- export function ownPropertyRecordSchema(valueSchema) {
11
- return z.preprocess(encodeOwnRecordKeys, z.record(z
12
+ export function ownPropertyRecordSchema(valueSchema, { maxEntries = CONTRACT_MAX_MAP_ENTRIES, prepareValue } = {}) {
13
+ return z.preprocess((value, context) => {
14
+ if (value === null || typeof value !== "object" || Array.isArray(value))
15
+ return value;
16
+ const prototype = Object.getPrototypeOf(value);
17
+ if (prototype !== Object.prototype && prototype !== null)
18
+ return [];
19
+ let entries = 0;
20
+ for (const key in value) {
21
+ if (!Object.hasOwn(value, key))
22
+ continue;
23
+ if (++entries > maxEntries || key.length > CONTRACT_MAX_IDENTIFIER_LENGTH) {
24
+ context.addIssue({ code: "custom", message: "record exceeds the contract bounds" });
25
+ return z.NEVER;
26
+ }
27
+ }
28
+ const encoded = Object.create(null);
29
+ for (const key of Object.keys(value)) {
30
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
31
+ if (!descriptor || !("value" in descriptor)) {
32
+ context.addIssue({ code: "custom", message: "record entries must be data properties" });
33
+ return z.NEVER;
34
+ }
35
+ let entry;
36
+ try {
37
+ entry = prepareValue ? prepareValue(key, descriptor.value) : descriptor.value;
38
+ }
39
+ catch (error) {
40
+ // Only a profile-value refusal is a validation issue; any other throw is a defect.
41
+ if (!(error instanceof AgentProfileValueError))
42
+ throw error;
43
+ context.addIssue({ code: "custom", path: [...error.path], message: error.message });
44
+ return z.NEVER;
45
+ }
46
+ if (entry === z.NEVER) {
47
+ context.addIssue({ code: "custom", message: "record value exceeds the contract bounds or is not portable JSON" });
48
+ return z.NEVER;
49
+ }
50
+ encoded[encodeRecordKey(key)] = entry;
51
+ }
52
+ return encoded;
53
+ }, z.record(z
12
54
  .string()
13
55
  .regex(/^u(?:[0-9a-f]{4})*$/, "record keys must contain valid Unicode"), valueSchema).transform((record) => {
14
56
  const restored = {};
@@ -23,19 +65,6 @@ export function ownPropertyRecordSchema(valueSchema) {
23
65
  return restored;
24
66
  }));
25
67
  }
26
- function encodeOwnRecordKeys(value) {
27
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
28
- return value;
29
- }
30
- const prototype = Object.getPrototypeOf(value);
31
- if (prototype !== Object.prototype && prototype !== null)
32
- return [];
33
- const encoded = Object.create(null);
34
- for (const [key, entry] of Object.entries(value)) {
35
- encoded[encodeRecordKey(key)] = entry;
36
- }
37
- return encoded;
38
- }
39
68
  function encodeRecordKey(value) {
40
69
  let encoded = isWellFormedUnicode(value) ? "u" : "i";
41
70
  for (let index = 0; index < value.length; index += 1) {
@@ -1,5 +1,6 @@
1
1
  import { agentCandidateSecretRefSchema } from "./agent-candidate-schema-common.js";
2
2
  import { z } from "zod";
3
+ import { CONTRACT_MAX_ARRAY_LENGTH, isBoundedJsonMaterial } from "./contract-limits.js";
3
4
  import { ownPropertyRecordSchema } from "./own-property-record.js";
4
5
  import { ProxySecretsSchema } from "./proxy-secrets.js";
5
6
  import { agentProfileKnowledgeRefsSchema } from "./profile-knowledge.js";
@@ -7,9 +8,24 @@ export { agentProfileKnowledgeRefSchema, agentProfileKnowledgeRefsSchema, } from
7
8
  import { REASONING_EFFORTS, TRAINED_MODEL_PREFIX, trainedModelIdForArtifact, } from "./agent-profile.js";
8
9
  import { canonicalCandidateDigest, sha256DigestSchema, environmentNameSchema, headerNameSchema, isSafeExecutable, isSafeRelativePath, isWellFormedUnicode, looksLikeCredential, } from "./agent-candidate-schema-common.js";
9
10
  import { harnessTypeSchema } from "./harness.js";
10
- import { canonicalAgentProfileJson } from "./agent-profile-canonical.js";
11
+ import { AgentProfileValueError, assertAgentProfileValue, canonicalAgentProfileJson } from "./agent-profile-canonical.js";
11
12
  import { harnessHonorsEffort, harnessHonorsModel, harnessModelSupport, harnessReasoningEfforts, harnessSystemPromptIntents, resolveModelProvider, } from "./harness-capabilities.js";
12
13
  import { SANDBOX_SIZE_PRESET_NAMES, } from "./sandbox-size.js";
14
+ const profileOpaqueValueSchema = z.unknown().transform((value, context) => {
15
+ if (value === undefined || isBoundedJsonMaterial(value, { dataPropertiesOnly: true }))
16
+ return structuredClone(value);
17
+ try {
18
+ assertAgentProfileValue(value, []);
19
+ }
20
+ catch (error) {
21
+ if (!(error instanceof AgentProfileValueError))
22
+ throw error;
23
+ context.addIssue({ code: "custom", path: [...error.path], message: error.message });
24
+ return z.NEVER;
25
+ }
26
+ context.addIssue({ code: "custom", message: "profile metadata exceeds contract bounds or is not portable JSON" });
27
+ return z.NEVER;
28
+ });
13
29
  export const agentProfilePermissionValueSchema = z.enum([
14
30
  "allow",
15
31
  "deny",
@@ -41,11 +57,11 @@ export const agentProfileFileMountSchema = z.strictObject({
41
57
  });
42
58
  export const agentProfileResourcesSchema = z.strictObject({
43
59
  knowledge: agentProfileKnowledgeRefsSchema.optional(),
44
- files: z.array(agentProfileFileMountSchema).optional(),
45
- tools: z.array(agentProfileResourceRefSchema).optional(),
46
- skills: z.array(agentProfileResourceRefSchema).optional(),
47
- agents: z.array(agentProfileResourceRefSchema).optional(),
48
- commands: z.array(agentProfileResourceRefSchema).optional(),
60
+ files: z.array(agentProfileFileMountSchema).max(CONTRACT_MAX_ARRAY_LENGTH).optional(),
61
+ tools: z.array(agentProfileResourceRefSchema).max(CONTRACT_MAX_ARRAY_LENGTH).optional(),
62
+ skills: z.array(agentProfileResourceRefSchema).max(CONTRACT_MAX_ARRAY_LENGTH).optional(),
63
+ agents: z.array(agentProfileResourceRefSchema).max(CONTRACT_MAX_ARRAY_LENGTH).optional(),
64
+ commands: z.array(agentProfileResourceRefSchema).max(CONTRACT_MAX_ARRAY_LENGTH).optional(),
49
65
  instructions: z
50
66
  .union([z.string(), agentProfileResourceRefSchema])
51
67
  .optional(),
@@ -88,7 +104,7 @@ export const agentProfileModelHintsBaseSchema = z.strictObject({
88
104
  maxVisibleOutputTokens: z.number().int().positive().optional(),
89
105
  maxReasoningTokens: z.number().int().positive().optional(),
90
106
  maxTotalOutputTokens: z.number().int().positive().optional(),
91
- metadata: ownPropertyRecordSchema(z.unknown()).optional(),
107
+ metadata: ownPropertyRecordSchema(profileOpaqueValueSchema).optional(),
92
108
  });
93
109
  export const agentProfileModelHintsSchema = agentProfileModelHintsBaseSchema.superRefine(enforceModelTokenBounds);
94
110
  /** Ordered account selection policy, without credentials or ambient model defaults. */
@@ -113,7 +129,7 @@ export const agentProfileSeatSchema = z.strictObject({
113
129
  export const agentProfilePromptSchema = z.strictObject({
114
130
  systemPrompt: z.string().optional(),
115
131
  appendSystemPrompt: z.string().optional(),
116
- instructions: z.array(z.string()).optional(),
132
+ instructions: z.array(z.string()).max(CONTRACT_MAX_ARRAY_LENGTH).optional(),
117
133
  });
118
134
  const controlCharacterPattern = /[\u0000-\u001f\u007f]/;
119
135
  const secretCapableNamePattern = /(?:^|[_-])(?:api[_-]?key|access[_-]?key|private[_-]?key|token|secret|password|credentials?|authorization|cookie|database[_-]?url|dsn|pat)(?:[_-]|$)/i;
@@ -166,7 +182,7 @@ export const agentSubagentProfileSchema = z.strictObject({
166
182
  tools: ownPropertyRecordSchema(z.boolean()).optional(),
167
183
  permissions: ownPropertyRecordSchema(agentProfilePermissionSchema).optional(),
168
184
  maxSteps: z.number().optional(),
169
- metadata: ownPropertyRecordSchema(z.unknown()).optional(),
185
+ metadata: ownPropertyRecordSchema(profileOpaqueValueSchema).optional(),
170
186
  });
171
187
  export const agentProfileHookCommandSchema = z.strictObject({
172
188
  command: z.string(),
@@ -181,7 +197,7 @@ export const agentProfileModeSchema = z.strictObject({
181
197
  prompt: z.string().optional(),
182
198
  tools: ownPropertyRecordSchema(z.boolean()).optional(),
183
199
  permissions: ownPropertyRecordSchema(agentProfilePermissionSchema).optional(),
184
- metadata: ownPropertyRecordSchema(z.unknown()).optional(),
200
+ metadata: ownPropertyRecordSchema(profileOpaqueValueSchema).optional(),
185
201
  });
186
202
  export const agentProfileConfidentialSchema = z.strictObject({
187
203
  tee: z.string().optional(),
@@ -204,7 +220,7 @@ const remoteMcpUrlSchema = nonBlankMcpValueSchema.refine((value) => {
204
220
  const agentProfileLocalMcpServerSchema = z.strictObject({
205
221
  transport: z.literal("stdio").optional(),
206
222
  command: nonBlankMcpValueSchema.refine(isSafeExecutable, "local MCP command must be a canonical public executable"),
207
- args: z.array(agentProfileConfigValueSchema).optional(),
223
+ args: z.array(agentProfileConfigValueSchema).max(CONTRACT_MAX_ARRAY_LENGTH).optional(),
208
224
  env: agentProfileEnvironmentSchema.optional(),
209
225
  cwd: z
210
226
  .string()
@@ -213,7 +229,7 @@ const agentProfileLocalMcpServerSchema = z.strictObject({
213
229
  url: z.undefined().optional(),
214
230
  headers: z.undefined().optional(),
215
231
  enabled: z.literal(true).optional(),
216
- metadata: ownPropertyRecordSchema(z.unknown()).optional(),
232
+ metadata: ownPropertyRecordSchema(profileOpaqueValueSchema).optional(),
217
233
  });
218
234
  const agentProfileRemoteMcpServerSchema = z.strictObject({
219
235
  transport: z.enum(["sse", "http"]).optional(),
@@ -224,7 +240,7 @@ const agentProfileRemoteMcpServerSchema = z.strictObject({
224
240
  url: remoteMcpUrlSchema,
225
241
  headers: agentProfileHeaderSchema.optional(),
226
242
  enabled: z.literal(true).optional(),
227
- metadata: ownPropertyRecordSchema(z.unknown()).optional(),
243
+ metadata: ownPropertyRecordSchema(profileOpaqueValueSchema).optional(),
228
244
  });
229
245
  const agentProfileDisabledMcpServerSchema = z.strictObject({
230
246
  enabled: z.literal(false),
@@ -235,7 +251,7 @@ const agentProfileDisabledMcpServerSchema = z.strictObject({
235
251
  cwd: z.undefined().optional(),
236
252
  url: z.undefined().optional(),
237
253
  headers: z.undefined().optional(),
238
- metadata: ownPropertyRecordSchema(z.unknown()).optional(),
254
+ metadata: ownPropertyRecordSchema(profileOpaqueValueSchema).optional(),
239
255
  });
240
256
  export const agentProfileMcpServerSchema = z.union([
241
257
  agentProfileLocalMcpServerSchema,
@@ -244,10 +260,10 @@ export const agentProfileMcpServerSchema = z.union([
244
260
  ]);
245
261
  export const agentProfileConnectionSchema = z.strictObject({
246
262
  connectionId: z.string().min(1),
247
- capabilities: z.array(z.string().min(1)).min(1),
263
+ capabilities: z.array(z.string().min(1)).max(CONTRACT_MAX_ARRAY_LENGTH).min(1),
248
264
  alias: z.string().min(1).optional(),
249
265
  });
250
- const removeListSchema = z.union([z.literal(true), z.array(z.string().min(1))]);
266
+ const removeListSchema = z.union([z.literal(true), z.array(z.string().min(1)).max(CONTRACT_MAX_ARRAY_LENGTH)]);
251
267
  export const agentProfilePromptRemovalSchema = z.strictObject({
252
268
  systemPrompt: z.literal(true).optional(),
253
269
  appendSystemPrompt: z.literal(true).optional(),
@@ -311,10 +327,7 @@ export const agentTrainingDatasetIdentitySchema = z.strictObject({
311
327
  context.addIssue({ code: "custom", path: ["taskSetDigest"], message: "training task inventory digest mismatch" });
312
328
  }
313
329
  });
314
- export const agentTrainingParametersSchema = ownPropertyRecordSchema(z.union([z.string().max(2_048).pipe(publicProfileConfigStringSchema), z.number().finite(), z.boolean(), z.null()])).superRefine((parameters, context) => {
315
- if (Object.keys(parameters).length > 100) {
316
- context.addIssue({ code: "custom", message: "at most 100 public training parameters are allowed" });
317
- }
330
+ export const agentTrainingParametersSchema = ownPropertyRecordSchema(z.union([z.string().max(2_048).pipe(publicProfileConfigStringSchema), z.number().finite(), z.boolean(), z.null()]), { maxEntries: 100 }).superRefine((parameters, context) => {
318
331
  for (const name of Object.keys(parameters)) {
319
332
  if (!trainingIdentitySchema.safeParse(name).success || isCredentialBearingProfileConfigName(name)) {
320
333
  context.addIssue({ code: "custom", path: [name], message: "training parameter name must be public and non-credential" });
@@ -359,13 +372,25 @@ export const agentProfileTrainingSchema = z.strictObject({
359
372
  }
360
373
  }
361
374
  });
362
- export const agentProfileMetadataSchema = ownPropertyRecordSchema(z.unknown()).superRefine((metadata, context) => {
375
+ export const agentProfileMetadataSchema = ownPropertyRecordSchema(z.unknown(), {
376
+ prepareValue: (key, value) => {
377
+ if (key === "training")
378
+ return value;
379
+ if (value === undefined || isBoundedJsonMaterial(value, { dataPropertiesOnly: true }))
380
+ return structuredClone(value);
381
+ assertAgentProfileValue(value, [key]);
382
+ return z.NEVER;
383
+ },
384
+ }).superRefine((metadata, context) => {
363
385
  if (!Object.hasOwn(metadata, "training"))
364
386
  return;
365
387
  const result = agentProfileTrainingSchema.safeParse(metadata.training);
366
- if (!result.success) {
388
+ if (result.success)
389
+ metadata.training = result.data;
390
+ else {
391
+ delete metadata.training;
367
392
  for (const issue of result.error.issues) {
368
- context.addIssue({ code: "custom", path: ["training", ...issue.path], message: issue.message });
393
+ context.addIssue({ code: "custom", path: ["training", ...issue.path], message: issue.message, continue: false });
369
394
  }
370
395
  }
371
396
  });
@@ -379,23 +404,23 @@ export const agentProfileSchema = z
379
404
  name: z.string().optional(),
380
405
  description: z.string().optional(),
381
406
  version: z.string().optional(),
382
- tags: z.array(z.string()).optional(),
407
+ tags: z.array(z.string()).max(CONTRACT_MAX_ARRAY_LENGTH).optional(),
383
408
  prompt: agentProfilePromptSchema.optional(),
384
409
  model: agentProfileModelHintsSchema.optional(),
385
410
  harness: harnessTypeSchema.optional(),
386
- seats: z.array(agentProfileSeatSchema).nonempty().optional(),
411
+ seats: z.array(agentProfileSeatSchema).max(CONTRACT_MAX_ARRAY_LENGTH).nonempty().optional(),
387
412
  proxySecrets: ProxySecretsSchema.optional(),
388
413
  permissions: ownPropertyRecordSchema(agentProfilePermissionSchema).optional(),
389
414
  tools: ownPropertyRecordSchema(z.boolean()).optional(),
390
415
  mcp: ownPropertyRecordSchema(agentProfileMcpServerSchema).optional(),
391
- connections: z.array(agentProfileConnectionSchema).optional(),
416
+ connections: z.array(agentProfileConnectionSchema).max(CONTRACT_MAX_ARRAY_LENGTH).optional(),
392
417
  subagents: ownPropertyRecordSchema(agentSubagentProfileSchema).optional(),
393
418
  resources: agentProfileResourcesSchema.optional(),
394
- hooks: ownPropertyRecordSchema(z.array(agentProfileHookCommandSchema)).optional(),
419
+ hooks: ownPropertyRecordSchema(z.array(agentProfileHookCommandSchema).max(CONTRACT_MAX_ARRAY_LENGTH)).optional(),
395
420
  modes: ownPropertyRecordSchema(agentProfileModeSchema).optional(),
396
421
  confidential: agentProfileConfidentialSchema.optional(),
397
422
  metadata: agentProfileMetadataSchema.optional(),
398
- extensions: ownPropertyRecordSchema(z.union([ownPropertyRecordSchema(z.unknown()), z.undefined()])).optional(),
423
+ extensions: ownPropertyRecordSchema(z.union([ownPropertyRecordSchema(profileOpaqueValueSchema), z.undefined()])).optional(),
399
424
  })
400
425
  .superRefine((profile, context) => {
401
426
  validateNestedRecordKeys(profile, context, [], new Set());
@@ -533,13 +558,13 @@ export const agentProfileDiffSchema = z.strictObject({
533
558
  "optimizer",
534
559
  "compound",
535
560
  ]),
536
- artifacts: z.array(z.string().min(1)).optional(),
537
- notes: z.array(z.string()).optional(),
561
+ artifacts: z.array(z.string().min(1)).max(CONTRACT_MAX_ARRAY_LENGTH).optional(),
562
+ notes: z.array(z.string()).max(CONTRACT_MAX_ARRAY_LENGTH).optional(),
538
563
  })
539
564
  .optional(),
540
565
  set: agentProfileSchema.optional(),
541
566
  remove: agentProfileDiffRemovalSchema.optional(),
542
- metadata: ownPropertyRecordSchema(z.unknown()).optional(),
567
+ metadata: ownPropertyRecordSchema(profileOpaqueValueSchema).optional(),
543
568
  });
544
569
  const _agentProfileSchemaMatchesInterface = true;
545
570
  void _agentProfileSchemaMatchesInterface;
@@ -57,9 +57,8 @@ export const ProxySecretsSchema = z.array(ProxySecretBindingSchema).max(CONTRACT
57
57
  * Strings select an API-verified parent sandbox; null explicitly selects independent issuance.
58
58
  * When present, every destination in the final effective selection must have exactly one key.
59
59
  */
60
- export const ProxySecretParentsSchema = ownPropertyRecordSchema(boundedIdentifierSchema.nullable())
61
- .refine((parents) => Object.keys(parents).length <= CONTRACT_MAX_ARRAY_LENGTH &&
62
- Object.keys(parents).every((name) => envNameSchema.safeParse(name).success), "invalid parent selection keys");
60
+ export const ProxySecretParentsSchema = ownPropertyRecordSchema(boundedIdentifierSchema.nullable(), { maxEntries: CONTRACT_MAX_ARRAY_LENGTH })
61
+ .refine((parents) => Object.keys(parents).every((name) => envNameSchema.safeParse(name).success), "invalid parent selection keys");
63
62
  /** Shape and totality only; the API verifies issuer authority and durable immutable pins. */
64
63
  export function isProxySecretParentsTotal(bindings, parents) {
65
64
  const selection = ProxySecretsSchema.safeParse(bindings);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-interface",
3
- "version": "5.9.0",
3
+ "version": "5.10.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",