@agent-surface/core 0.1.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/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/index.d.ts +796 -0
- package/dist/index.js +3271 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3271 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var DEFAULT_LIMITS = {
|
|
3
|
+
maxComponentDescription: 500,
|
|
4
|
+
maxCapabilityDescription: 300,
|
|
5
|
+
maxMetaBytes: 2048,
|
|
6
|
+
maxOutputBytes: 32768,
|
|
7
|
+
maxSchemaBytes: 16384,
|
|
8
|
+
maxSchemaDepth: 8,
|
|
9
|
+
observationTimeoutMs: 5e3,
|
|
10
|
+
actionTimeoutMs: 1e4,
|
|
11
|
+
procedureTimeoutMs: 3e4,
|
|
12
|
+
actionQueueDepth: 2,
|
|
13
|
+
maxConcurrentObservationsPerConsumer: 8,
|
|
14
|
+
maxConcurrentObservationsTotal: 32,
|
|
15
|
+
maxQueuedObservationsPerConsumer: 8,
|
|
16
|
+
dedupeCacheSize: 200,
|
|
17
|
+
dedupeCacheTtlMs: 6e5,
|
|
18
|
+
tombstoneSize: 100,
|
|
19
|
+
tombstoneTtlMs: 3e5,
|
|
20
|
+
confirmationTtlMs: 12e4,
|
|
21
|
+
maxPendingConfirmations: 32
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/errors.ts
|
|
25
|
+
var AGENT_CAPABILITY_ERROR_CODES = [
|
|
26
|
+
"CAPABILITY_NOT_FOUND",
|
|
27
|
+
"CAPABILITY_NOT_AVAILABLE",
|
|
28
|
+
"AMBIGUOUS_INSTANCE",
|
|
29
|
+
"COMPONENT_UNMOUNTED",
|
|
30
|
+
"STALE_CAPABILITY",
|
|
31
|
+
"INVOCATION_CONFLICT",
|
|
32
|
+
"INVALID_INPUT",
|
|
33
|
+
"NOT_AUTHENTICATED",
|
|
34
|
+
"NOT_AUTHORIZED",
|
|
35
|
+
"PRECONDITION_FAILED",
|
|
36
|
+
"CONFIRMATION_REQUIRED",
|
|
37
|
+
"CONFIRMATION_INVALID",
|
|
38
|
+
"RATE_LIMITED",
|
|
39
|
+
"TIMEOUT",
|
|
40
|
+
"CANCELLED",
|
|
41
|
+
"EXECUTION_FAILED"
|
|
42
|
+
];
|
|
43
|
+
var AgentSurfaceError = class extends Error {
|
|
44
|
+
payload;
|
|
45
|
+
constructor(payload, opts) {
|
|
46
|
+
super(payload.message, opts);
|
|
47
|
+
this.name = "AgentSurfaceError";
|
|
48
|
+
this.payload = payload;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function isAgentSurfaceError(e) {
|
|
52
|
+
return e instanceof AgentSurfaceError || typeof e === "object" && e !== null && e.name === "AgentSurfaceError" && typeof e.payload === "object";
|
|
53
|
+
}
|
|
54
|
+
var AgentSurfaceDefinitionError = class extends Error {
|
|
55
|
+
code;
|
|
56
|
+
constructor(code, message) {
|
|
57
|
+
super(`[${code}] ${message}`);
|
|
58
|
+
this.name = "AgentSurfaceDefinitionError";
|
|
59
|
+
this.code = code;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/ids.ts
|
|
64
|
+
var MAX_ID_LENGTH = 128;
|
|
65
|
+
var SEGMENT_RE = /^[a-z][a-z0-9-]*$/;
|
|
66
|
+
var CAPABILITY_NAME_RE = /^[a-z][A-Za-z0-9]*$/;
|
|
67
|
+
var INSTANCE_ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
68
|
+
function isValidComponentType(type) {
|
|
69
|
+
if (type.length === 0 || type.length > MAX_ID_LENGTH) return false;
|
|
70
|
+
return type.split(".").every((seg) => SEGMENT_RE.test(seg));
|
|
71
|
+
}
|
|
72
|
+
function isValidCapabilityName(name) {
|
|
73
|
+
return CAPABILITY_NAME_RE.test(name);
|
|
74
|
+
}
|
|
75
|
+
function isValidInstanceId(id) {
|
|
76
|
+
return id.length > 0 && id.length <= MAX_ID_LENGTH && INSTANCE_ID_RE.test(id);
|
|
77
|
+
}
|
|
78
|
+
function formatViewCapabilityId(componentType, name) {
|
|
79
|
+
return `view:${componentType}.${name}`;
|
|
80
|
+
}
|
|
81
|
+
function formatDomainCapabilityId(path) {
|
|
82
|
+
return `domain:${path}`;
|
|
83
|
+
}
|
|
84
|
+
function parseCapabilityId(id) {
|
|
85
|
+
if (id.length > MAX_ID_LENGTH) return void 0;
|
|
86
|
+
if (id.startsWith("view:")) {
|
|
87
|
+
const rest = id.slice("view:".length);
|
|
88
|
+
const lastDot = rest.lastIndexOf(".");
|
|
89
|
+
if (lastDot <= 0) return void 0;
|
|
90
|
+
const componentType = rest.slice(0, lastDot);
|
|
91
|
+
const name = rest.slice(lastDot + 1);
|
|
92
|
+
if (!isValidComponentType(componentType) || !isValidCapabilityName(name)) {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
return { plane: "view", componentType, name };
|
|
96
|
+
}
|
|
97
|
+
if (id.startsWith("domain:")) {
|
|
98
|
+
const path = id.slice("domain:".length);
|
|
99
|
+
if (path.length === 0) return void 0;
|
|
100
|
+
return { plane: "domain", path };
|
|
101
|
+
}
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
var MAX_WIRE_NAME_LENGTH = 64;
|
|
105
|
+
function fnv1aHash36(input) {
|
|
106
|
+
let hash = 2166136261;
|
|
107
|
+
for (let i = 0; i < input.length; i++) {
|
|
108
|
+
hash ^= input.charCodeAt(i);
|
|
109
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
110
|
+
}
|
|
111
|
+
return hash.toString(36).padStart(7, "0").slice(0, 7);
|
|
112
|
+
}
|
|
113
|
+
function encodeWireName(id) {
|
|
114
|
+
return encodeWireNameForInstance(id);
|
|
115
|
+
}
|
|
116
|
+
function encodeWireNameForInstance(id, instanceId) {
|
|
117
|
+
const raw = id.replace(":", "_").replaceAll(".", "__") + (instanceId ? `_at_${instanceId}` : "");
|
|
118
|
+
if (raw.length <= MAX_WIRE_NAME_LENGTH) return raw;
|
|
119
|
+
return `${raw.slice(0, 56)}_${fnv1aHash36(`${id}#${instanceId ?? ""}`)}`;
|
|
120
|
+
}
|
|
121
|
+
function decodeWireName(name) {
|
|
122
|
+
const planeEnd = name.indexOf("_");
|
|
123
|
+
if (planeEnd <= 0) return void 0;
|
|
124
|
+
const plane = name.slice(0, planeEnd);
|
|
125
|
+
if (plane !== "view" && plane !== "domain") return void 0;
|
|
126
|
+
const rest = name.slice(planeEnd + 1).replaceAll("__", ".");
|
|
127
|
+
return `${plane}:${rest}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/utils.ts
|
|
131
|
+
function jsonDeepEqual(a, b) {
|
|
132
|
+
if (a === b) return true;
|
|
133
|
+
if (a === void 0 || b === void 0) return false;
|
|
134
|
+
if (typeof a !== typeof b || a === null || b === null) return false;
|
|
135
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
136
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => jsonDeepEqual(v, b[i]));
|
|
137
|
+
}
|
|
138
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
139
|
+
const ka = Object.keys(a).sort();
|
|
140
|
+
const kb = Object.keys(b).sort();
|
|
141
|
+
return ka.length === kb.length && ka.every(
|
|
142
|
+
(k, i) => k === kb[i] && jsonDeepEqual(
|
|
143
|
+
a[k],
|
|
144
|
+
b[k]
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
function deepFreeze(value) {
|
|
151
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
152
|
+
Object.freeze(value);
|
|
153
|
+
for (const key of Object.keys(value)) {
|
|
154
|
+
deepFreeze(value[key]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
function jsonClone(value) {
|
|
160
|
+
return value === void 0 ? value : JSON.parse(JSON.stringify(value));
|
|
161
|
+
}
|
|
162
|
+
function isJsonValue(value, depth = 0) {
|
|
163
|
+
if (depth > 64) return false;
|
|
164
|
+
if (value === null) return true;
|
|
165
|
+
const t = typeof value;
|
|
166
|
+
if (t === "string" || t === "boolean") return true;
|
|
167
|
+
if (t === "number") return Number.isFinite(value);
|
|
168
|
+
if (Array.isArray(value)) return value.every((v) => isJsonValue(v, depth + 1));
|
|
169
|
+
if (t === "object") {
|
|
170
|
+
const proto = Object.getPrototypeOf(value);
|
|
171
|
+
if (proto !== Object.prototype && proto !== null) return false;
|
|
172
|
+
return Object.values(value).every(
|
|
173
|
+
(v) => v === void 0 || isJsonValue(v, depth + 1)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
function byteLength(value) {
|
|
179
|
+
const s = JSON.stringify(value);
|
|
180
|
+
return s === void 0 ? 0 : s.length;
|
|
181
|
+
}
|
|
182
|
+
var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
183
|
+
function randomBase62(length) {
|
|
184
|
+
let out = "";
|
|
185
|
+
for (let i = 0; i < length; i++) {
|
|
186
|
+
out += ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
function truncate(s, max) {
|
|
191
|
+
return s.length <= max ? s : s.slice(0, Math.max(0, max - 1)) + "\u2026";
|
|
192
|
+
}
|
|
193
|
+
function canonicalJson(value) {
|
|
194
|
+
if (value === void 0 || value === null) return "null";
|
|
195
|
+
const t = typeof value;
|
|
196
|
+
if (t === "number") {
|
|
197
|
+
if (!Number.isFinite(value)) {
|
|
198
|
+
throw new Error("canonicalJson: non-finite numbers are not JsonValues");
|
|
199
|
+
}
|
|
200
|
+
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
201
|
+
}
|
|
202
|
+
if (t === "string" || t === "boolean") return JSON.stringify(value);
|
|
203
|
+
if (Array.isArray(value)) {
|
|
204
|
+
return `[${value.map((v) => canonicalJson(v ?? null)).join(",")}]`;
|
|
205
|
+
}
|
|
206
|
+
const entries = Object.keys(value).sort().filter((k) => value[k] !== void 0).map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`);
|
|
207
|
+
return `{${entries.join(",")}}`;
|
|
208
|
+
}
|
|
209
|
+
function fnv1a64(input) {
|
|
210
|
+
let hash = 0xcbf29ce484222325n;
|
|
211
|
+
const prime = 0x100000001b3n;
|
|
212
|
+
for (let i = 0; i < input.length; i++) {
|
|
213
|
+
hash ^= BigInt(input.charCodeAt(i));
|
|
214
|
+
hash = hash * prime & 0xffffffffffffffffn;
|
|
215
|
+
}
|
|
216
|
+
return hash.toString(16).padStart(16, "0");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/schema.ts
|
|
220
|
+
var AgentSchemaError = class extends Error {
|
|
221
|
+
issues;
|
|
222
|
+
constructor(issues) {
|
|
223
|
+
super(issues.map((i) => `${i.path || "$"}: ${i.message}`).join("; ") || "Invalid value");
|
|
224
|
+
this.name = "AgentSchemaError";
|
|
225
|
+
this.issues = issues;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
function fromStandardSchema(schema, options) {
|
|
229
|
+
return {
|
|
230
|
+
jsonSchema: options.jsonSchema,
|
|
231
|
+
parse(value) {
|
|
232
|
+
const result = schema["~standard"].validate(value);
|
|
233
|
+
if (result instanceof Promise) {
|
|
234
|
+
throw new AgentSchemaError([
|
|
235
|
+
{ path: "", message: "Async schema validation is not supported in v0.1" }
|
|
236
|
+
]);
|
|
237
|
+
}
|
|
238
|
+
if (result.issues) {
|
|
239
|
+
throw new AgentSchemaError(
|
|
240
|
+
result.issues.map((issue) => ({
|
|
241
|
+
path: (issue.path ?? []).map((p) => String(typeof p === "object" && p !== null && "key" in p ? p.key : p)).join("."),
|
|
242
|
+
message: issue.message
|
|
243
|
+
}))
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return result.value;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function fromJsonSchema(schema) {
|
|
251
|
+
return {
|
|
252
|
+
jsonSchema: schema,
|
|
253
|
+
parse(value) {
|
|
254
|
+
const issues = validateValueAgainstSchema(value, schema, schema, "");
|
|
255
|
+
if (issues.length > 0) throw new AgentSchemaError(issues);
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
var emptyObjectSchema = fromJsonSchema({
|
|
261
|
+
type: "object",
|
|
262
|
+
properties: {},
|
|
263
|
+
additionalProperties: false
|
|
264
|
+
});
|
|
265
|
+
var ALLOWED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
266
|
+
"type",
|
|
267
|
+
"enum",
|
|
268
|
+
"const",
|
|
269
|
+
// objects
|
|
270
|
+
"properties",
|
|
271
|
+
"required",
|
|
272
|
+
"additionalProperties",
|
|
273
|
+
// arrays
|
|
274
|
+
"items",
|
|
275
|
+
"minItems",
|
|
276
|
+
"maxItems",
|
|
277
|
+
"uniqueItems",
|
|
278
|
+
// strings
|
|
279
|
+
"minLength",
|
|
280
|
+
"maxLength",
|
|
281
|
+
"pattern",
|
|
282
|
+
"format",
|
|
283
|
+
// numbers
|
|
284
|
+
"minimum",
|
|
285
|
+
"maximum",
|
|
286
|
+
"exclusiveMinimum",
|
|
287
|
+
"exclusiveMaximum",
|
|
288
|
+
"multipleOf",
|
|
289
|
+
// unions
|
|
290
|
+
"anyOf",
|
|
291
|
+
// annotations
|
|
292
|
+
"description",
|
|
293
|
+
"default",
|
|
294
|
+
"examples",
|
|
295
|
+
"title",
|
|
296
|
+
"deprecated",
|
|
297
|
+
// refs
|
|
298
|
+
"$defs",
|
|
299
|
+
"$ref",
|
|
300
|
+
// tolerated (converter noise), ignored at validation time
|
|
301
|
+
"$schema",
|
|
302
|
+
"$id"
|
|
303
|
+
]);
|
|
304
|
+
var REJECTED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
305
|
+
"oneOf",
|
|
306
|
+
"allOf",
|
|
307
|
+
"not",
|
|
308
|
+
"if",
|
|
309
|
+
"then",
|
|
310
|
+
"else",
|
|
311
|
+
"patternProperties",
|
|
312
|
+
"dependentRequired",
|
|
313
|
+
"dependentSchemas",
|
|
314
|
+
"unevaluatedProperties",
|
|
315
|
+
"unevaluatedItems",
|
|
316
|
+
"prefixItems",
|
|
317
|
+
"contains",
|
|
318
|
+
"propertyNames"
|
|
319
|
+
]);
|
|
320
|
+
var ALLOWED_TYPES = /* @__PURE__ */ new Set([
|
|
321
|
+
"object",
|
|
322
|
+
"array",
|
|
323
|
+
"string",
|
|
324
|
+
"number",
|
|
325
|
+
"integer",
|
|
326
|
+
"boolean",
|
|
327
|
+
"null"
|
|
328
|
+
]);
|
|
329
|
+
var ALLOWED_FORMATS = /* @__PURE__ */ new Set(["date-time", "date", "uuid", "email", "uri"]);
|
|
330
|
+
function validateJsonSchemaDocument(schema, limits) {
|
|
331
|
+
const size = byteLength(schema);
|
|
332
|
+
if (size > limits.maxSchemaBytes) {
|
|
333
|
+
return { ok: false, reason: `schema serializes to ${size} bytes (max ${limits.maxSchemaBytes})` };
|
|
334
|
+
}
|
|
335
|
+
return walkSchemaDocument(schema, "", 0, limits.maxSchemaDepth);
|
|
336
|
+
}
|
|
337
|
+
function walkSchemaDocument(node, path, depth, maxDepth) {
|
|
338
|
+
if (depth > maxDepth) {
|
|
339
|
+
return { ok: false, reason: `schema nesting exceeds depth ${maxDepth} at ${path || "$"}` };
|
|
340
|
+
}
|
|
341
|
+
if (typeof node === "boolean") {
|
|
342
|
+
return { ok: false, reason: `boolean schema not supported at ${path || "$"}` };
|
|
343
|
+
}
|
|
344
|
+
if (typeof node !== "object" || node === null || Array.isArray(node)) {
|
|
345
|
+
return { ok: false, reason: `schema must be an object at ${path || "$"}` };
|
|
346
|
+
}
|
|
347
|
+
const obj = node;
|
|
348
|
+
for (const key of Object.keys(obj)) {
|
|
349
|
+
if (REJECTED_KEYWORDS.has(key) || !ALLOWED_KEYWORDS.has(key)) {
|
|
350
|
+
return { ok: false, reason: `unsupported keyword "${key}" at ${path || "$"}` };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if ("$ref" in obj) {
|
|
354
|
+
const ref = obj.$ref;
|
|
355
|
+
if (typeof ref !== "string" || !ref.startsWith("#/$defs/")) {
|
|
356
|
+
return { ok: false, reason: `only internal "#/$defs/..." refs are supported at ${path || "$"}` };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if ("type" in obj) {
|
|
360
|
+
const t = obj.type;
|
|
361
|
+
const types = Array.isArray(t) ? t : [t];
|
|
362
|
+
for (const one of types) {
|
|
363
|
+
if (typeof one !== "string" || !ALLOWED_TYPES.has(one)) {
|
|
364
|
+
return { ok: false, reason: `unsupported type "${String(one)}" at ${path || "$"}` };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if ("format" in obj) {
|
|
369
|
+
const f = obj.format;
|
|
370
|
+
if (typeof f !== "string" || !ALLOWED_FORMATS.has(f)) {
|
|
371
|
+
return { ok: false, reason: `unsupported format "${String(obj.format)}" at ${path || "$"}` };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if ("additionalProperties" in obj && typeof obj.additionalProperties !== "boolean") {
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
reason: `additionalProperties must be a boolean at ${path || "$"}`
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if ("items" in obj) {
|
|
381
|
+
if (Array.isArray(obj.items)) {
|
|
382
|
+
return { ok: false, reason: `tuple "items" arrays are not supported at ${path || "$"}` };
|
|
383
|
+
}
|
|
384
|
+
const r = walkSchemaDocument(obj.items, `${path}.items`, depth + 1, maxDepth);
|
|
385
|
+
if (!r.ok) return r;
|
|
386
|
+
}
|
|
387
|
+
if ("properties" in obj) {
|
|
388
|
+
const props = obj.properties;
|
|
389
|
+
if (typeof props !== "object" || props === null || Array.isArray(props)) {
|
|
390
|
+
return { ok: false, reason: `properties must be an object at ${path || "$"}` };
|
|
391
|
+
}
|
|
392
|
+
for (const [name, sub] of Object.entries(props)) {
|
|
393
|
+
const r = walkSchemaDocument(sub, `${path}.properties.${name}`, depth + 1, maxDepth);
|
|
394
|
+
if (!r.ok) return r;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if ("anyOf" in obj) {
|
|
398
|
+
if (!Array.isArray(obj.anyOf) || obj.anyOf.length === 0) {
|
|
399
|
+
return { ok: false, reason: `anyOf must be a non-empty array at ${path || "$"}` };
|
|
400
|
+
}
|
|
401
|
+
for (let i = 0; i < obj.anyOf.length; i++) {
|
|
402
|
+
const r = walkSchemaDocument(obj.anyOf[i], `${path}.anyOf[${i}]`, depth + 1, maxDepth);
|
|
403
|
+
if (!r.ok) return r;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if ("$defs" in obj) {
|
|
407
|
+
const defs = obj.$defs;
|
|
408
|
+
if (typeof defs !== "object" || defs === null || Array.isArray(defs)) {
|
|
409
|
+
return { ok: false, reason: `$defs must be an object at ${path || "$"}` };
|
|
410
|
+
}
|
|
411
|
+
for (const [name, sub] of Object.entries(defs)) {
|
|
412
|
+
const r = walkSchemaDocument(sub, `${path}.$defs.${name}`, depth + 1, maxDepth);
|
|
413
|
+
if (!r.ok) return r;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return { ok: true };
|
|
417
|
+
}
|
|
418
|
+
var FORMAT_VALIDATORS = {
|
|
419
|
+
"date-time": (s) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.test(s),
|
|
420
|
+
date: (s) => /^\d{4}-\d{2}-\d{2}$/.test(s),
|
|
421
|
+
uuid: (s) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
|
|
422
|
+
email: (s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s),
|
|
423
|
+
uri: (s) => /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(s)
|
|
424
|
+
};
|
|
425
|
+
function typeOfValue(value) {
|
|
426
|
+
if (value === null) return "null";
|
|
427
|
+
if (Array.isArray(value)) return "array";
|
|
428
|
+
const t = typeof value;
|
|
429
|
+
if (t === "number") return "number";
|
|
430
|
+
return t;
|
|
431
|
+
}
|
|
432
|
+
function matchesType(value, type) {
|
|
433
|
+
switch (type) {
|
|
434
|
+
case "object":
|
|
435
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
436
|
+
case "array":
|
|
437
|
+
return Array.isArray(value);
|
|
438
|
+
case "string":
|
|
439
|
+
return typeof value === "string";
|
|
440
|
+
case "number":
|
|
441
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
442
|
+
case "integer":
|
|
443
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
444
|
+
case "boolean":
|
|
445
|
+
return typeof value === "boolean";
|
|
446
|
+
case "null":
|
|
447
|
+
return value === null;
|
|
448
|
+
default:
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function validateValueAgainstSchema(value, schema, root, path) {
|
|
453
|
+
if (typeof schema !== "object" || schema === null) return [];
|
|
454
|
+
let node = schema;
|
|
455
|
+
if (typeof node.$ref === "string") {
|
|
456
|
+
const ref = node.$ref;
|
|
457
|
+
const defName = ref.slice("#/$defs/".length);
|
|
458
|
+
const defs = root.$defs;
|
|
459
|
+
const resolved = defs?.[defName];
|
|
460
|
+
if (typeof resolved !== "object" || resolved === null) {
|
|
461
|
+
return [{ path, message: `unresolvable $ref "${ref}"` }];
|
|
462
|
+
}
|
|
463
|
+
node = resolved;
|
|
464
|
+
}
|
|
465
|
+
const issues = [];
|
|
466
|
+
if ("const" in node) {
|
|
467
|
+
if (!jsonDeepEqual(value, node.const)) {
|
|
468
|
+
issues.push({ path, message: `must equal the constant ${JSON.stringify(node.const)}` });
|
|
469
|
+
return issues;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (Array.isArray(node.enum)) {
|
|
473
|
+
const ok = node.enum.some((candidate) => jsonDeepEqual(value, candidate));
|
|
474
|
+
if (!ok) {
|
|
475
|
+
issues.push({ path, message: `must be one of ${JSON.stringify(node.enum)}` });
|
|
476
|
+
return issues;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (Array.isArray(node.anyOf)) {
|
|
480
|
+
const anyOk = node.anyOf.some(
|
|
481
|
+
(branch) => validateValueAgainstSchema(value, branch, root, path).length === 0
|
|
482
|
+
);
|
|
483
|
+
if (!anyOk) {
|
|
484
|
+
issues.push({ path, message: "does not match any allowed variant" });
|
|
485
|
+
return issues;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if ("type" in node) {
|
|
489
|
+
const types = Array.isArray(node.type) ? node.type : [node.type];
|
|
490
|
+
const ok = types.some((t) => typeof t === "string" && matchesType(value, t));
|
|
491
|
+
if (!ok) {
|
|
492
|
+
issues.push({
|
|
493
|
+
path,
|
|
494
|
+
message: `expected ${types.join(" | ")}, got ${typeOfValue(value)}`
|
|
495
|
+
});
|
|
496
|
+
return issues;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (typeof value === "string") {
|
|
500
|
+
if (typeof node.minLength === "number" && value.length < node.minLength) {
|
|
501
|
+
issues.push({ path, message: `must be at least ${node.minLength} characters` });
|
|
502
|
+
}
|
|
503
|
+
if (typeof node.maxLength === "number" && value.length > node.maxLength) {
|
|
504
|
+
issues.push({ path, message: `must be at most ${node.maxLength} characters` });
|
|
505
|
+
}
|
|
506
|
+
if (typeof node.pattern === "string") {
|
|
507
|
+
let re;
|
|
508
|
+
try {
|
|
509
|
+
re = new RegExp(node.pattern);
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
if (re && !re.test(value)) {
|
|
513
|
+
issues.push({ path, message: `must match pattern ${node.pattern}` });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (typeof node.format === "string") {
|
|
517
|
+
const check = FORMAT_VALIDATORS[node.format];
|
|
518
|
+
if (check && !check(value)) {
|
|
519
|
+
issues.push({ path, message: `must be a valid ${node.format}` });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (typeof value === "number") {
|
|
524
|
+
if (typeof node.minimum === "number" && value < node.minimum) {
|
|
525
|
+
issues.push({ path, message: `must be >= ${node.minimum}` });
|
|
526
|
+
}
|
|
527
|
+
if (typeof node.maximum === "number" && value > node.maximum) {
|
|
528
|
+
issues.push({ path, message: `must be <= ${node.maximum}` });
|
|
529
|
+
}
|
|
530
|
+
if (typeof node.exclusiveMinimum === "number" && value <= node.exclusiveMinimum) {
|
|
531
|
+
issues.push({ path, message: `must be > ${node.exclusiveMinimum}` });
|
|
532
|
+
}
|
|
533
|
+
if (typeof node.exclusiveMaximum === "number" && value >= node.exclusiveMaximum) {
|
|
534
|
+
issues.push({ path, message: `must be < ${node.exclusiveMaximum}` });
|
|
535
|
+
}
|
|
536
|
+
if (typeof node.multipleOf === "number" && node.multipleOf > 0) {
|
|
537
|
+
const quotient = value / node.multipleOf;
|
|
538
|
+
if (Math.abs(quotient - Math.round(quotient)) > 1e-9) {
|
|
539
|
+
issues.push({ path, message: `must be a multiple of ${node.multipleOf}` });
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (Array.isArray(value)) {
|
|
544
|
+
if (typeof node.minItems === "number" && value.length < node.minItems) {
|
|
545
|
+
issues.push({ path, message: `must have at least ${node.minItems} items` });
|
|
546
|
+
}
|
|
547
|
+
if (typeof node.maxItems === "number" && value.length > node.maxItems) {
|
|
548
|
+
issues.push({ path, message: `must have at most ${node.maxItems} items` });
|
|
549
|
+
}
|
|
550
|
+
if (node.uniqueItems === true) {
|
|
551
|
+
const seen = /* @__PURE__ */ new Set();
|
|
552
|
+
for (const item of value) {
|
|
553
|
+
const key = JSON.stringify(item);
|
|
554
|
+
if (seen.has(key)) {
|
|
555
|
+
issues.push({ path, message: "items must be unique" });
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
seen.add(key);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (node.items !== void 0) {
|
|
562
|
+
value.forEach((item, i) => {
|
|
563
|
+
issues.push(...validateValueAgainstSchema(item, node.items, root, `${path}[${i}]`));
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
568
|
+
const record = value;
|
|
569
|
+
const props = node.properties ?? {};
|
|
570
|
+
if (Array.isArray(node.required)) {
|
|
571
|
+
for (const req of node.required) {
|
|
572
|
+
if (typeof req === "string" && record[req] === void 0) {
|
|
573
|
+
issues.push({ path: path ? `${path}.${req}` : req, message: "is required" });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
for (const [name, sub] of Object.entries(props)) {
|
|
578
|
+
if (record[name] !== void 0) {
|
|
579
|
+
issues.push(
|
|
580
|
+
...validateValueAgainstSchema(record[name], sub, root, path ? `${path}.${name}` : name)
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (node.additionalProperties === false) {
|
|
585
|
+
for (const key of Object.keys(record)) {
|
|
586
|
+
if (!(key in props)) {
|
|
587
|
+
issues.push({
|
|
588
|
+
path: path ? `${path}.${key}` : key,
|
|
589
|
+
message: "is not an allowed property"
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return issues;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/definition.ts
|
|
599
|
+
function observation(def) {
|
|
600
|
+
return def;
|
|
601
|
+
}
|
|
602
|
+
function action(def) {
|
|
603
|
+
return def;
|
|
604
|
+
}
|
|
605
|
+
function defineAgentComponent(def) {
|
|
606
|
+
return def;
|
|
607
|
+
}
|
|
608
|
+
var COMPONENT_KEYS = /* @__PURE__ */ new Set([
|
|
609
|
+
"type",
|
|
610
|
+
"instanceId",
|
|
611
|
+
"description",
|
|
612
|
+
"parent",
|
|
613
|
+
"meta",
|
|
614
|
+
"internal",
|
|
615
|
+
"policies",
|
|
616
|
+
"origin",
|
|
617
|
+
"priority",
|
|
618
|
+
"enabled",
|
|
619
|
+
"observations",
|
|
620
|
+
"actions",
|
|
621
|
+
"procedures"
|
|
622
|
+
]);
|
|
623
|
+
var OBSERVATION_KEYS = /* @__PURE__ */ new Set([
|
|
624
|
+
"description",
|
|
625
|
+
"output",
|
|
626
|
+
"read",
|
|
627
|
+
"when",
|
|
628
|
+
"unavailableReason",
|
|
629
|
+
"policies",
|
|
630
|
+
"meta",
|
|
631
|
+
"timeoutMs"
|
|
632
|
+
]);
|
|
633
|
+
var ACTION_KEYS = /* @__PURE__ */ new Set([
|
|
634
|
+
"description",
|
|
635
|
+
"input",
|
|
636
|
+
"output",
|
|
637
|
+
"effect",
|
|
638
|
+
"idempotent",
|
|
639
|
+
"reversible",
|
|
640
|
+
"confirmation",
|
|
641
|
+
"audit",
|
|
642
|
+
"when",
|
|
643
|
+
"unavailableReason",
|
|
644
|
+
"precondition",
|
|
645
|
+
"execute",
|
|
646
|
+
"policies",
|
|
647
|
+
"meta",
|
|
648
|
+
"timeoutMs"
|
|
649
|
+
]);
|
|
650
|
+
var VIEW_EFFECTS = /* @__PURE__ */ new Set(["local-state", "navigation"]);
|
|
651
|
+
var SERVER_EFFECTS = /* @__PURE__ */ new Set([
|
|
652
|
+
"server-query",
|
|
653
|
+
"server-mutation",
|
|
654
|
+
"external-side-effect",
|
|
655
|
+
"destructive"
|
|
656
|
+
]);
|
|
657
|
+
function fail(code, message) {
|
|
658
|
+
throw new AgentSurfaceDefinitionError(code, message);
|
|
659
|
+
}
|
|
660
|
+
function checkMeta(meta, where, limits) {
|
|
661
|
+
if (meta === void 0) return;
|
|
662
|
+
if (!isJsonValue(meta) || typeof meta !== "object" || Array.isArray(meta)) {
|
|
663
|
+
fail("INVALID_DEFINITION", `${where}: meta must be a JsonValue record`);
|
|
664
|
+
}
|
|
665
|
+
if (byteLength(meta) > limits.maxMetaBytes) {
|
|
666
|
+
fail("LIMIT_EXCEEDED", `${where}: meta exceeds ${limits.maxMetaBytes} bytes`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function checkSchema(schema, where, limits) {
|
|
670
|
+
if (schema === void 0) return;
|
|
671
|
+
if (typeof schema !== "object" || schema === null || typeof schema.parse !== "function" || typeof schema.jsonSchema !== "object") {
|
|
672
|
+
fail("INVALID_DEFINITION", `${where}: expected an AgentSchema ({ jsonSchema, parse })`);
|
|
673
|
+
}
|
|
674
|
+
const result = validateJsonSchemaDocument(schema.jsonSchema, limits);
|
|
675
|
+
if (!result.ok) fail("UNSUPPORTED_SCHEMA", `${where}: ${result.reason}`);
|
|
676
|
+
}
|
|
677
|
+
function validateComponentDefinition(def, limits, opts) {
|
|
678
|
+
if (typeof def !== "object" || def === null) {
|
|
679
|
+
fail("INVALID_DEFINITION", "definition must be an object");
|
|
680
|
+
}
|
|
681
|
+
for (const key of Object.keys(def)) {
|
|
682
|
+
if (!COMPONENT_KEYS.has(key)) {
|
|
683
|
+
fail("INVALID_DEFINITION", `unknown definition field "${key}"`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (typeof def.type !== "string" || !isValidComponentType(def.type)) {
|
|
687
|
+
fail("INVALID_ID", `invalid component type "${String(def.type)}"`);
|
|
688
|
+
}
|
|
689
|
+
const instanceId = def.instanceId ?? "default";
|
|
690
|
+
if (!isValidInstanceId(instanceId)) {
|
|
691
|
+
fail("INVALID_ID", `invalid instanceId "${instanceId}" for component "${def.type}"`);
|
|
692
|
+
}
|
|
693
|
+
if (typeof def.description !== "string" || def.description.trim().length === 0) {
|
|
694
|
+
fail("INVALID_DEFINITION", `component "${def.type}": description is required and must be non-empty`);
|
|
695
|
+
}
|
|
696
|
+
if (def.description.length > limits.maxComponentDescription) {
|
|
697
|
+
fail(
|
|
698
|
+
"LIMIT_EXCEEDED",
|
|
699
|
+
`component "${def.type}": description exceeds ${limits.maxComponentDescription} chars`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if (def.parent !== void 0) {
|
|
703
|
+
if (typeof def.parent !== "object" || def.parent === null || typeof def.parent.type !== "string" || !isValidComponentType(def.parent.type) || def.parent.instanceId !== void 0 && !isValidInstanceId(def.parent.instanceId)) {
|
|
704
|
+
fail("INVALID_DEFINITION", `component "${def.type}": invalid parent link`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
checkMeta(def.meta, `component "${def.type}"`, limits);
|
|
708
|
+
if (def.priority !== void 0 && typeof def.priority !== "number") {
|
|
709
|
+
fail("INVALID_DEFINITION", `component "${def.type}": priority must be a number`);
|
|
710
|
+
}
|
|
711
|
+
if (def.origin !== void 0 && typeof def.origin !== "string") {
|
|
712
|
+
fail("INVALID_DEFINITION", `component "${def.type}": origin must be a string`);
|
|
713
|
+
}
|
|
714
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
715
|
+
const checkName = (name, kind) => {
|
|
716
|
+
if (!isValidCapabilityName(name)) {
|
|
717
|
+
fail("INVALID_ID", `component "${def.type}": invalid ${kind} name "${name}"`);
|
|
718
|
+
}
|
|
719
|
+
const capabilityId = formatViewCapabilityId(def.type, name);
|
|
720
|
+
if (capabilityId.length > MAX_ID_LENGTH) {
|
|
721
|
+
fail("INVALID_ID", `capability id "${capabilityId}" exceeds ${MAX_ID_LENGTH} chars`);
|
|
722
|
+
}
|
|
723
|
+
if (seenNames.has(name)) {
|
|
724
|
+
fail("DUPLICATE_CAPABILITY", `component "${def.type}": duplicate capability name "${name}"`);
|
|
725
|
+
}
|
|
726
|
+
seenNames.add(name);
|
|
727
|
+
};
|
|
728
|
+
for (const [name, obs] of Object.entries(def.observations ?? {})) {
|
|
729
|
+
checkName(name, "observation");
|
|
730
|
+
const where = `observation "${def.type}.${name}"`;
|
|
731
|
+
for (const key of Object.keys(obs)) {
|
|
732
|
+
if (!OBSERVATION_KEYS.has(key)) fail("INVALID_DEFINITION", `${where}: unknown field "${key}"`);
|
|
733
|
+
}
|
|
734
|
+
if (typeof obs.description !== "string" || obs.description.trim().length === 0) {
|
|
735
|
+
fail("INVALID_DEFINITION", `${where}: description is required`);
|
|
736
|
+
}
|
|
737
|
+
if (obs.description.length > limits.maxCapabilityDescription) {
|
|
738
|
+
fail("LIMIT_EXCEEDED", `${where}: description exceeds ${limits.maxCapabilityDescription} chars`);
|
|
739
|
+
}
|
|
740
|
+
if (typeof obs.read !== "function") fail("INVALID_DEFINITION", `${where}: read() is required`);
|
|
741
|
+
checkSchema(obs.output, `${where} output`, limits);
|
|
742
|
+
if (obs.output === void 0) fail("INVALID_DEFINITION", `${where}: output schema is required`);
|
|
743
|
+
checkMeta(obs.meta, where, limits);
|
|
744
|
+
}
|
|
745
|
+
for (const [name, act] of Object.entries(def.actions ?? {})) {
|
|
746
|
+
checkName(name, "action");
|
|
747
|
+
const where = `action "${def.type}.${name}"`;
|
|
748
|
+
for (const key of Object.keys(act)) {
|
|
749
|
+
if (!ACTION_KEYS.has(key)) fail("INVALID_DEFINITION", `${where}: unknown field "${key}"`);
|
|
750
|
+
}
|
|
751
|
+
if (typeof act.description !== "string" || act.description.trim().length === 0) {
|
|
752
|
+
fail("INVALID_DEFINITION", `${where}: description is required`);
|
|
753
|
+
}
|
|
754
|
+
if (act.description.length > limits.maxCapabilityDescription) {
|
|
755
|
+
fail("LIMIT_EXCEEDED", `${where}: description exceeds ${limits.maxCapabilityDescription} chars`);
|
|
756
|
+
}
|
|
757
|
+
if (typeof act.execute !== "function") fail("INVALID_DEFINITION", `${where}: execute() is required`);
|
|
758
|
+
if (!VIEW_EFFECTS.has(act.effect)) {
|
|
759
|
+
if (SERVER_EFFECTS.has(act.effect)) {
|
|
760
|
+
fail(
|
|
761
|
+
"PLANE_VIOLATION",
|
|
762
|
+
`${where}: view actions cannot declare server effect "${act.effect}" \u2014 define an oRPC procedure and reference it (docs/05)`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
fail("INVALID_DEFINITION", `${where}: effect must be "local-state" or "navigation"`);
|
|
766
|
+
}
|
|
767
|
+
if (act.confirmation !== void 0 && !["never", "optional", "required"].includes(act.confirmation)) {
|
|
768
|
+
fail("INVALID_DEFINITION", `${where}: invalid confirmation "${act.confirmation}"`);
|
|
769
|
+
}
|
|
770
|
+
if (act.audit !== void 0 && !["none", "metadata", "full"].includes(act.audit)) {
|
|
771
|
+
fail("INVALID_DEFINITION", `${where}: invalid audit level "${act.audit}"`);
|
|
772
|
+
}
|
|
773
|
+
if (act.input === void 0) fail("INVALID_DEFINITION", `${where}: input schema is required`);
|
|
774
|
+
checkSchema(act.input, `${where} input`, limits);
|
|
775
|
+
checkSchema(act.output, `${where} output`, limits);
|
|
776
|
+
checkMeta(act.meta, where, limits);
|
|
777
|
+
}
|
|
778
|
+
const procedures = def.procedures ?? [];
|
|
779
|
+
if (procedures.length > 0 && !opts.hasProcedureExecutor) {
|
|
780
|
+
fail(
|
|
781
|
+
"PLANE_VIOLATION",
|
|
782
|
+
`component "${def.type}": procedure bindings require an installed procedure executor (registry.setProcedureExecutor)`
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
for (const binding of procedures) {
|
|
786
|
+
if (typeof binding !== "object" || binding === null || binding.kind !== "procedure-binding") {
|
|
787
|
+
fail("INVALID_DEFINITION", `component "${def.type}": invalid procedure binding`);
|
|
788
|
+
}
|
|
789
|
+
const ref = binding.ref;
|
|
790
|
+
if (typeof ref !== "object" || ref === null || typeof ref.path !== "string" || ref.path.length === 0 || typeof ref.id !== "string" || ref.id !== `domain:${ref.path}` || typeof ref.description !== "string") {
|
|
791
|
+
fail("INVALID_DEFINITION", `component "${def.type}": procedure binding has an invalid ref`);
|
|
792
|
+
}
|
|
793
|
+
if (!SERVER_EFFECTS.has(ref.effect)) {
|
|
794
|
+
fail(
|
|
795
|
+
"PLANE_VIOLATION",
|
|
796
|
+
`procedure "${ref.path}": effect must be one of server-query | server-mutation | external-side-effect | destructive`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
if (typeof binding.reducedInputSchema !== "object" || binding.reducedInputSchema === null) {
|
|
800
|
+
fail("INVALID_DEFINITION", `procedure "${ref.path}": missing reduced input schema`);
|
|
801
|
+
}
|
|
802
|
+
if (binding.config.confirmation !== void 0 && !["optional", "required"].includes(binding.config.confirmation)) {
|
|
803
|
+
fail("INVALID_DEFINITION", `procedure "${ref.path}": invalid confirmation escalation`);
|
|
804
|
+
}
|
|
805
|
+
checkMeta(binding.config.meta, `procedure "${ref.path}"`, limits);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/policy.ts
|
|
810
|
+
var CONFIRMATION_ESCALATION = /* @__PURE__ */ Symbol("agent-surface.confirmation-escalation");
|
|
811
|
+
function evaluateDiscovery(policies, ctx) {
|
|
812
|
+
let disable;
|
|
813
|
+
for (const policy of policies) {
|
|
814
|
+
if (!policy.onDiscovery) continue;
|
|
815
|
+
let decision;
|
|
816
|
+
try {
|
|
817
|
+
decision = policy.onDiscovery(ctx);
|
|
818
|
+
} catch {
|
|
819
|
+
return { decision: "hide" };
|
|
820
|
+
}
|
|
821
|
+
if (decision.decision === "hide") return decision;
|
|
822
|
+
if (decision.decision === "disable" && !disable) disable = decision;
|
|
823
|
+
}
|
|
824
|
+
return disable ?? { decision: "expose" };
|
|
825
|
+
}
|
|
826
|
+
function composeAuthorizeChain(policies, ctx, core) {
|
|
827
|
+
let index = -1;
|
|
828
|
+
const dispatch = (i) => {
|
|
829
|
+
if (i <= index) {
|
|
830
|
+
return Promise.reject(new Error("policy next() called multiple times"));
|
|
831
|
+
}
|
|
832
|
+
index = i;
|
|
833
|
+
const policy = policies[i];
|
|
834
|
+
if (!policy) return core();
|
|
835
|
+
if (!policy.onAuthorize) return dispatch(i + 1);
|
|
836
|
+
return policy.onAuthorize(ctx, () => dispatch(i + 1));
|
|
837
|
+
};
|
|
838
|
+
return dispatch(0);
|
|
839
|
+
}
|
|
840
|
+
function composeInvokeChain(policies, ctx, core) {
|
|
841
|
+
let index = -1;
|
|
842
|
+
const dispatch = (i) => {
|
|
843
|
+
if (i <= index) {
|
|
844
|
+
return Promise.reject(new Error("policy next() called multiple times"));
|
|
845
|
+
}
|
|
846
|
+
index = i;
|
|
847
|
+
const policy = policies[i];
|
|
848
|
+
if (!policy) return core();
|
|
849
|
+
if (!policy.onInvoke) return dispatch(i + 1);
|
|
850
|
+
return policy.onInvoke(ctx, () => dispatch(i + 1));
|
|
851
|
+
};
|
|
852
|
+
return dispatch(0);
|
|
853
|
+
}
|
|
854
|
+
function authenticated(opts) {
|
|
855
|
+
const key = opts?.key ?? "user";
|
|
856
|
+
return {
|
|
857
|
+
name: "authenticated",
|
|
858
|
+
onDiscovery(ctx) {
|
|
859
|
+
return ctx.host[key] ? { decision: "expose" } : { decision: "hide" };
|
|
860
|
+
},
|
|
861
|
+
async onAuthorize(ctx, next) {
|
|
862
|
+
if (!ctx.host[key]) {
|
|
863
|
+
throw new AgentSurfaceError({
|
|
864
|
+
code: "NOT_AUTHENTICATED",
|
|
865
|
+
message: "Sign-in is required before this capability can be used. Ask the user to sign in.",
|
|
866
|
+
retry: "no"
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
return next();
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function hasPermission(permission, check) {
|
|
874
|
+
return {
|
|
875
|
+
name: `has-permission(${permission})`,
|
|
876
|
+
onDiscovery(ctx) {
|
|
877
|
+
return check({ ...ctx.host }, permission) ? { decision: "expose" } : { decision: "hide" };
|
|
878
|
+
},
|
|
879
|
+
async onAuthorize(ctx, next) {
|
|
880
|
+
if (!check({ ...ctx.host }, permission)) {
|
|
881
|
+
throw new AgentSurfaceError({
|
|
882
|
+
code: "NOT_AUTHORIZED",
|
|
883
|
+
message: "The current user is not authorized to use this capability.",
|
|
884
|
+
retry: "no",
|
|
885
|
+
details: { origin: "client" }
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
return next();
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function tenantBoundary(opts) {
|
|
893
|
+
const matches = (ctx) => {
|
|
894
|
+
const current = opts.current({ ...ctx.host });
|
|
895
|
+
const expected = opts.expected(ctx);
|
|
896
|
+
return expected === void 0 || current === expected;
|
|
897
|
+
};
|
|
898
|
+
return {
|
|
899
|
+
name: "tenant-boundary",
|
|
900
|
+
onDiscovery(ctx) {
|
|
901
|
+
return matches(ctx) ? { decision: "expose" } : { decision: "hide" };
|
|
902
|
+
},
|
|
903
|
+
async onAuthorize(ctx, next) {
|
|
904
|
+
if (!matches(ctx)) {
|
|
905
|
+
throw new AgentSurfaceError({
|
|
906
|
+
code: "NOT_AUTHORIZED",
|
|
907
|
+
message: "This capability belongs to a different tenant.",
|
|
908
|
+
retry: "no",
|
|
909
|
+
details: { origin: "client" }
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
return next();
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
function environment(allowed) {
|
|
917
|
+
return {
|
|
918
|
+
name: "environment",
|
|
919
|
+
onDiscovery(ctx) {
|
|
920
|
+
return allowed.includes(ctx.environment) ? { decision: "expose" } : { decision: "hide" };
|
|
921
|
+
},
|
|
922
|
+
async onAuthorize(ctx, next) {
|
|
923
|
+
if (!allowed.includes(ctx.environment)) {
|
|
924
|
+
throw new AgentSurfaceError({
|
|
925
|
+
code: "CAPABILITY_NOT_FOUND",
|
|
926
|
+
message: "This capability does not exist in the current surface.",
|
|
927
|
+
retry: "after-refresh"
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
return next();
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function rateLimit(opts) {
|
|
935
|
+
const hits = /* @__PURE__ */ new Map();
|
|
936
|
+
return {
|
|
937
|
+
name: "rate-limit",
|
|
938
|
+
async onAuthorize(ctx, next) {
|
|
939
|
+
const key = `${ctx.consumer.kind}:${ctx.consumer.id} ${ctx.capabilityId}`;
|
|
940
|
+
const now = ctx.now();
|
|
941
|
+
const windowStart = now - opts.windowMs;
|
|
942
|
+
const list = (hits.get(key) ?? []).filter((t) => t > windowStart);
|
|
943
|
+
if (list.length >= opts.limit) {
|
|
944
|
+
const retryAfterMs = Math.max(0, (list[0] ?? now) + opts.windowMs - now);
|
|
945
|
+
throw new AgentSurfaceError({
|
|
946
|
+
code: "RATE_LIMITED",
|
|
947
|
+
message: "Too many calls to this capability. Wait before retrying.",
|
|
948
|
+
retry: "after-delay",
|
|
949
|
+
details: { reason: "rate", retryAfterMs }
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
list.push(now);
|
|
953
|
+
hits.set(key, list);
|
|
954
|
+
return next();
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
function requireConfirmation(opts) {
|
|
959
|
+
const policy = {
|
|
960
|
+
name: "require-confirmation"
|
|
961
|
+
};
|
|
962
|
+
policy[CONFIRMATION_ESCALATION] = { if: opts?.if, summary: opts?.summary };
|
|
963
|
+
return policy;
|
|
964
|
+
}
|
|
965
|
+
function audit(sink, level = "metadata") {
|
|
966
|
+
return {
|
|
967
|
+
name: "audit",
|
|
968
|
+
async onInvoke(ctx, next) {
|
|
969
|
+
const startedAt = ctx.now();
|
|
970
|
+
sink?.record({
|
|
971
|
+
at: new Date(startedAt).toISOString(),
|
|
972
|
+
type: "invocation-started",
|
|
973
|
+
capabilityId: ctx.capabilityId,
|
|
974
|
+
registrationId: ctx.registrationId,
|
|
975
|
+
invocationId: ctx.invocationId,
|
|
976
|
+
consumerId: ctx.consumer.id,
|
|
977
|
+
...level === "full" ? { payload: { input: ctx.effectiveInput } } : {}
|
|
978
|
+
});
|
|
979
|
+
const result = await next();
|
|
980
|
+
sink?.record({
|
|
981
|
+
at: new Date(ctx.now()).toISOString(),
|
|
982
|
+
type: "invocation-settled",
|
|
983
|
+
capabilityId: ctx.capabilityId,
|
|
984
|
+
registrationId: ctx.registrationId,
|
|
985
|
+
invocationId: ctx.invocationId,
|
|
986
|
+
consumerId: ctx.consumer.id,
|
|
987
|
+
status: result.status,
|
|
988
|
+
...result.status === "error" ? { code: result.error.code } : {},
|
|
989
|
+
durationMs: ctx.now() - startedAt,
|
|
990
|
+
...level === "full" && result.status === "ok" && result.output !== void 0 ? { payload: { output: result.output } } : {}
|
|
991
|
+
});
|
|
992
|
+
return result;
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// src/audit.ts
|
|
998
|
+
function memoryAuditSink(opts) {
|
|
999
|
+
const capacity = opts?.capacity ?? 1e3;
|
|
1000
|
+
const buffer = [];
|
|
1001
|
+
return {
|
|
1002
|
+
record(event) {
|
|
1003
|
+
buffer.push(event);
|
|
1004
|
+
if (buffer.length > capacity) buffer.splice(0, buffer.length - capacity);
|
|
1005
|
+
},
|
|
1006
|
+
events() {
|
|
1007
|
+
return [...buffer];
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
function consoleAuditSink() {
|
|
1012
|
+
return {
|
|
1013
|
+
record(event) {
|
|
1014
|
+
console.debug("[agent-surface audit]", event.type, event);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
function safeRecord(sink, event) {
|
|
1019
|
+
if (!sink) return;
|
|
1020
|
+
try {
|
|
1021
|
+
sink.record(event);
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
console.error("[agent-surface] audit sink threw", err);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// src/events.ts
|
|
1028
|
+
var EventDispatcher = class {
|
|
1029
|
+
constructor(reportError) {
|
|
1030
|
+
this.reportError = reportError;
|
|
1031
|
+
}
|
|
1032
|
+
reportError;
|
|
1033
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1034
|
+
queue = [];
|
|
1035
|
+
draining = false;
|
|
1036
|
+
subscribe(listener) {
|
|
1037
|
+
this.listeners.add(listener);
|
|
1038
|
+
return () => {
|
|
1039
|
+
this.listeners.delete(listener);
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
emit(event) {
|
|
1043
|
+
this.queue.push(event);
|
|
1044
|
+
if (this.draining) return;
|
|
1045
|
+
this.draining = true;
|
|
1046
|
+
try {
|
|
1047
|
+
let next;
|
|
1048
|
+
while ((next = this.queue.shift()) !== void 0) {
|
|
1049
|
+
for (const listener of [...this.listeners]) {
|
|
1050
|
+
try {
|
|
1051
|
+
listener(next);
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
this.reportError(err);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
} finally {
|
|
1058
|
+
this.draining = false;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
clear() {
|
|
1062
|
+
this.listeners.clear();
|
|
1063
|
+
this.queue.length = 0;
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
// src/confirmation.ts
|
|
1068
|
+
var MAX_RETAINED_RESOLVED = 200;
|
|
1069
|
+
var ConfirmationStore = class {
|
|
1070
|
+
constructor(opts) {
|
|
1071
|
+
this.opts = opts;
|
|
1072
|
+
}
|
|
1073
|
+
opts;
|
|
1074
|
+
records = /* @__PURE__ */ new Map();
|
|
1075
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1076
|
+
/** Creates (or re-uses a matching pending) confirmation record.
|
|
1077
|
+
* Returns "overflow" when the bounded pending store is full (D24):
|
|
1078
|
+
* the caller fails RATE_LIMITED and no record is created. */
|
|
1079
|
+
request(request) {
|
|
1080
|
+
for (const record2 of this.records.values()) {
|
|
1081
|
+
if (record2.state === "pending" && record2.digest === request.digest) {
|
|
1082
|
+
return this.view(record2);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (this.pendingCount() >= this.opts.maxPending) return "overflow";
|
|
1086
|
+
const now = this.opts.now();
|
|
1087
|
+
const record = {
|
|
1088
|
+
confirmationId: `cnf_${randomBase62(12)}`,
|
|
1089
|
+
capabilityId: request.capabilityId,
|
|
1090
|
+
registrationId: request.registrationId,
|
|
1091
|
+
consumerKey: request.consumerKey,
|
|
1092
|
+
effect: request.effect,
|
|
1093
|
+
summary: request.summary,
|
|
1094
|
+
input: request.input,
|
|
1095
|
+
digest: request.digest,
|
|
1096
|
+
requestedAt: new Date(now).toISOString(),
|
|
1097
|
+
expiresAt: new Date(now + this.opts.ttlMs).toISOString(),
|
|
1098
|
+
state: "pending",
|
|
1099
|
+
waiters: []
|
|
1100
|
+
};
|
|
1101
|
+
record.timer = setTimeout(() => this.expire(record.confirmationId), this.opts.ttlMs);
|
|
1102
|
+
this.records.set(record.confirmationId, record);
|
|
1103
|
+
this.trim();
|
|
1104
|
+
this.opts.emit({
|
|
1105
|
+
type: "confirmation-requested",
|
|
1106
|
+
confirmationId: record.confirmationId,
|
|
1107
|
+
capabilityId: record.capabilityId,
|
|
1108
|
+
expiresAt: record.expiresAt
|
|
1109
|
+
});
|
|
1110
|
+
this.opts.audit({
|
|
1111
|
+
type: "confirmation-requested",
|
|
1112
|
+
capabilityId: record.capabilityId,
|
|
1113
|
+
registrationId: record.registrationId,
|
|
1114
|
+
consumerId: record.consumerKey,
|
|
1115
|
+
invocationId: void 0
|
|
1116
|
+
});
|
|
1117
|
+
this.notify();
|
|
1118
|
+
return this.view(record);
|
|
1119
|
+
}
|
|
1120
|
+
pendingCount() {
|
|
1121
|
+
let count = 0;
|
|
1122
|
+
for (const record of this.records.values()) {
|
|
1123
|
+
if (record.state === "pending") count += 1;
|
|
1124
|
+
}
|
|
1125
|
+
return count;
|
|
1126
|
+
}
|
|
1127
|
+
resolve(confirmationId, resolution) {
|
|
1128
|
+
const record = this.records.get(confirmationId);
|
|
1129
|
+
if (!record || record.state !== "pending") return;
|
|
1130
|
+
if (record.timer) clearTimeout(record.timer);
|
|
1131
|
+
if (resolution.approved) {
|
|
1132
|
+
record.state = "approved";
|
|
1133
|
+
record.approvedAt = new Date(this.opts.now()).toISOString();
|
|
1134
|
+
this.opts.emit({ type: "confirmation-resolved", confirmationId, outcome: "approved" });
|
|
1135
|
+
this.opts.audit({
|
|
1136
|
+
type: "confirmation-approved",
|
|
1137
|
+
capabilityId: record.capabilityId,
|
|
1138
|
+
registrationId: record.registrationId,
|
|
1139
|
+
consumerId: record.consumerKey
|
|
1140
|
+
});
|
|
1141
|
+
this.settleWaiters(record, "approved");
|
|
1142
|
+
} else {
|
|
1143
|
+
record.state = "denied";
|
|
1144
|
+
record.denyReason = resolution.reason;
|
|
1145
|
+
this.opts.emit({ type: "confirmation-resolved", confirmationId, outcome: "denied" });
|
|
1146
|
+
this.opts.audit({
|
|
1147
|
+
type: "confirmation-denied",
|
|
1148
|
+
capabilityId: record.capabilityId,
|
|
1149
|
+
registrationId: record.registrationId,
|
|
1150
|
+
consumerId: record.consumerKey
|
|
1151
|
+
});
|
|
1152
|
+
this.settleWaiters(record, "denied");
|
|
1153
|
+
}
|
|
1154
|
+
this.notify();
|
|
1155
|
+
}
|
|
1156
|
+
expire(confirmationId) {
|
|
1157
|
+
const record = this.records.get(confirmationId);
|
|
1158
|
+
if (!record || record.state !== "pending") return;
|
|
1159
|
+
if (record.timer) clearTimeout(record.timer);
|
|
1160
|
+
record.state = "expired";
|
|
1161
|
+
record.expiresAt = new Date(this.opts.now()).toISOString();
|
|
1162
|
+
this.opts.emit({ type: "confirmation-resolved", confirmationId, outcome: "expired" });
|
|
1163
|
+
this.opts.audit({
|
|
1164
|
+
type: "confirmation-expired",
|
|
1165
|
+
capabilityId: record.capabilityId,
|
|
1166
|
+
registrationId: record.registrationId,
|
|
1167
|
+
consumerId: record.consumerKey
|
|
1168
|
+
});
|
|
1169
|
+
this.settleWaiters(record, "expired");
|
|
1170
|
+
this.notify();
|
|
1171
|
+
}
|
|
1172
|
+
/** Evidence validation + single-use consumption (docs/06 rules 2–5).
|
|
1173
|
+
* Matching is digest-first AND exact-value on the effective input —
|
|
1174
|
+
* never hash-only (AS-CONFIRM-002). */
|
|
1175
|
+
consume(evidence) {
|
|
1176
|
+
const record = this.records.get(evidence.confirmationId);
|
|
1177
|
+
if (!record) return { ok: false, kind: "invalid", reason: "mismatch" };
|
|
1178
|
+
const matches = record.digest === evidence.digest && jsonDeepEqual(record.input, evidence.input);
|
|
1179
|
+
switch (record.state) {
|
|
1180
|
+
case "pending":
|
|
1181
|
+
return matches ? { ok: false, kind: "pending-again", record: this.view(record) } : { ok: false, kind: "invalid", reason: "mismatch" };
|
|
1182
|
+
case "denied":
|
|
1183
|
+
return { ok: false, kind: "invalid", reason: "denied" };
|
|
1184
|
+
case "expired":
|
|
1185
|
+
return { ok: false, kind: "invalid", reason: "expired" };
|
|
1186
|
+
case "consumed":
|
|
1187
|
+
return { ok: false, kind: "invalid", reason: "consumed" };
|
|
1188
|
+
case "approved": {
|
|
1189
|
+
if (Date.parse(record.expiresAt) < this.opts.now()) {
|
|
1190
|
+
record.state = "expired";
|
|
1191
|
+
return { ok: false, kind: "invalid", reason: "expired" };
|
|
1192
|
+
}
|
|
1193
|
+
if (!matches) return { ok: false, kind: "invalid", reason: "mismatch" };
|
|
1194
|
+
record.state = "consumed";
|
|
1195
|
+
this.opts.audit({
|
|
1196
|
+
type: "confirmation-consumed",
|
|
1197
|
+
capabilityId: record.capabilityId,
|
|
1198
|
+
registrationId: record.registrationId,
|
|
1199
|
+
consumerId: record.consumerKey
|
|
1200
|
+
});
|
|
1201
|
+
return { ok: true, approvedAt: record.approvedAt ?? record.requestedAt };
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
pending() {
|
|
1206
|
+
return [...this.records.values()].filter((r) => r.state === "pending").map((r) => this.view(r));
|
|
1207
|
+
}
|
|
1208
|
+
waitFor(confirmationId, opts) {
|
|
1209
|
+
const record = this.records.get(confirmationId);
|
|
1210
|
+
if (!record) return Promise.resolve("expired");
|
|
1211
|
+
if (record.state === "approved" || record.state === "consumed") return Promise.resolve("approved");
|
|
1212
|
+
if (record.state === "denied") return Promise.resolve("denied");
|
|
1213
|
+
if (record.state === "expired") return Promise.resolve("expired");
|
|
1214
|
+
return new Promise((resolvePromise) => {
|
|
1215
|
+
const waiter = (outcome) => resolvePromise(outcome);
|
|
1216
|
+
record.waiters.push(waiter);
|
|
1217
|
+
opts?.signal?.addEventListener(
|
|
1218
|
+
"abort",
|
|
1219
|
+
() => {
|
|
1220
|
+
const i = record.waiters.indexOf(waiter);
|
|
1221
|
+
if (i >= 0) record.waiters.splice(i, 1);
|
|
1222
|
+
resolvePromise("expired");
|
|
1223
|
+
},
|
|
1224
|
+
{ once: true }
|
|
1225
|
+
);
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
subscribe(listener) {
|
|
1229
|
+
this.listeners.add(listener);
|
|
1230
|
+
return () => {
|
|
1231
|
+
this.listeners.delete(listener);
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
/** Expires every pending record (dispose path). */
|
|
1235
|
+
disposeAll() {
|
|
1236
|
+
for (const record of [...this.records.values()]) {
|
|
1237
|
+
if (record.state === "pending") this.expire(record.confirmationId);
|
|
1238
|
+
}
|
|
1239
|
+
this.listeners.clear();
|
|
1240
|
+
}
|
|
1241
|
+
controller() {
|
|
1242
|
+
return {
|
|
1243
|
+
pending: () => this.pending(),
|
|
1244
|
+
resolve: (id, resolution) => this.resolve(id, resolution),
|
|
1245
|
+
waitFor: (id, opts) => this.waitFor(id, opts),
|
|
1246
|
+
subscribe: (listener) => this.subscribe(listener),
|
|
1247
|
+
forceExpire: (id) => this.expire(id)
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
view(record) {
|
|
1251
|
+
return {
|
|
1252
|
+
confirmationId: record.confirmationId,
|
|
1253
|
+
capabilityId: record.capabilityId,
|
|
1254
|
+
registrationId: record.registrationId,
|
|
1255
|
+
consumerKey: record.consumerKey,
|
|
1256
|
+
effect: record.effect,
|
|
1257
|
+
summary: record.summary,
|
|
1258
|
+
input: record.input,
|
|
1259
|
+
requestedAt: record.requestedAt,
|
|
1260
|
+
expiresAt: record.expiresAt
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
settleWaiters(record, outcome) {
|
|
1264
|
+
const waiters = record.waiters.splice(0);
|
|
1265
|
+
for (const waiter of waiters) waiter(outcome);
|
|
1266
|
+
}
|
|
1267
|
+
notify() {
|
|
1268
|
+
const snapshot = this.pending();
|
|
1269
|
+
for (const listener of [...this.listeners]) {
|
|
1270
|
+
try {
|
|
1271
|
+
listener(snapshot);
|
|
1272
|
+
} catch {
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
trim() {
|
|
1277
|
+
const resolved = [...this.records.values()].filter((r) => r.state !== "pending");
|
|
1278
|
+
if (resolved.length <= MAX_RETAINED_RESOLVED) return;
|
|
1279
|
+
for (const record of resolved.slice(0, resolved.length - MAX_RETAINED_RESOLVED)) {
|
|
1280
|
+
this.records.delete(record.confirmationId);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
// src/internal.ts
|
|
1286
|
+
var DevDefectError = class extends Error {
|
|
1287
|
+
constructor(message) {
|
|
1288
|
+
super(message);
|
|
1289
|
+
this.name = "AgentSurfaceDevDefectError";
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
function componentKey(type, instanceId) {
|
|
1293
|
+
return `${type}\0${instanceId}`;
|
|
1294
|
+
}
|
|
1295
|
+
var CONFIRMATION_RANK = {
|
|
1296
|
+
never: 0,
|
|
1297
|
+
optional: 1,
|
|
1298
|
+
required: 2
|
|
1299
|
+
};
|
|
1300
|
+
function maxConfirmation(...levels) {
|
|
1301
|
+
return levels.reduce((acc, l) => CONFIRMATION_RANK[l] > CONFIRMATION_RANK[acc] ? l : acc, "never");
|
|
1302
|
+
}
|
|
1303
|
+
function defaultConfirmationFor(effect) {
|
|
1304
|
+
switch (effect) {
|
|
1305
|
+
case "server-query":
|
|
1306
|
+
return "never";
|
|
1307
|
+
case "server-mutation":
|
|
1308
|
+
return "optional";
|
|
1309
|
+
case "external-side-effect":
|
|
1310
|
+
case "destructive":
|
|
1311
|
+
return "required";
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
function defaultAuditFor(effect) {
|
|
1315
|
+
return effect === "external-side-effect" || effect === "destructive" ? "full" : "metadata";
|
|
1316
|
+
}
|
|
1317
|
+
var registrationCounter = 0;
|
|
1318
|
+
function nextRegistrationId(random) {
|
|
1319
|
+
registrationCounter += 1;
|
|
1320
|
+
return `reg_${registrationCounter.toString(36).padStart(4, "0")}${random()}`;
|
|
1321
|
+
}
|
|
1322
|
+
function normalizeRegistration(def, id) {
|
|
1323
|
+
const instanceId = def.instanceId ?? "default";
|
|
1324
|
+
const observations = /* @__PURE__ */ new Map();
|
|
1325
|
+
for (const [name, obs] of Object.entries(def.observations ?? {})) {
|
|
1326
|
+
observations.set(name, {
|
|
1327
|
+
kind: "observation",
|
|
1328
|
+
name,
|
|
1329
|
+
capabilityId: formatViewCapabilityId(def.type, name),
|
|
1330
|
+
description: obs.description,
|
|
1331
|
+
outputSchema: obs.output,
|
|
1332
|
+
jsonSchema: jsonClone(obs.output.jsonSchema),
|
|
1333
|
+
meta: obs.meta ? jsonClone(obs.meta) : void 0,
|
|
1334
|
+
timeoutMs: obs.timeoutMs,
|
|
1335
|
+
policies: [...obs.policies ?? []],
|
|
1336
|
+
auditLevel: "none"
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
const actions = /* @__PURE__ */ new Map();
|
|
1340
|
+
for (const [name, act] of Object.entries(def.actions ?? {})) {
|
|
1341
|
+
actions.set(name, {
|
|
1342
|
+
kind: "action",
|
|
1343
|
+
name,
|
|
1344
|
+
capabilityId: formatViewCapabilityId(def.type, name),
|
|
1345
|
+
description: act.description,
|
|
1346
|
+
inputSchema: act.input,
|
|
1347
|
+
inputJsonSchema: jsonClone(act.input.jsonSchema),
|
|
1348
|
+
outputSchema: act.output,
|
|
1349
|
+
outputJsonSchema: act.output ? jsonClone(act.output.jsonSchema) : void 0,
|
|
1350
|
+
effect: act.effect,
|
|
1351
|
+
idempotent: act.idempotent ?? false,
|
|
1352
|
+
reversible: act.reversible ?? true,
|
|
1353
|
+
confirmation: act.confirmation ?? "never",
|
|
1354
|
+
auditLevel: act.audit ?? "metadata",
|
|
1355
|
+
meta: act.meta ? jsonClone(act.meta) : void 0,
|
|
1356
|
+
timeoutMs: act.timeoutMs,
|
|
1357
|
+
policies: [...act.policies ?? []]
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
const hasView = observations.size > 0 || actions.size > 0;
|
|
1361
|
+
const procedures = (def.procedures ?? []).map((binding) => {
|
|
1362
|
+
const boundKeys = [...binding.boundKeys];
|
|
1363
|
+
const overridable = new Set(binding.config.overridableFields ?? []);
|
|
1364
|
+
const lockedKeys = binding.lockedKeys ? [...binding.lockedKeys] : boundKeys.filter((k) => !overridable.has(k));
|
|
1365
|
+
const effect = binding.ref.effect;
|
|
1366
|
+
return {
|
|
1367
|
+
kind: "procedure",
|
|
1368
|
+
binding,
|
|
1369
|
+
capabilityId: binding.ref.id,
|
|
1370
|
+
path: binding.ref.path,
|
|
1371
|
+
effect,
|
|
1372
|
+
requiresApproval: binding.ref.requiresApproval === true,
|
|
1373
|
+
baseDescription: binding.ref.description,
|
|
1374
|
+
fullInputSchema: jsonClone(binding.ref.inputSchema),
|
|
1375
|
+
reducedInputSchema: jsonClone(binding.reducedInputSchema),
|
|
1376
|
+
outputJsonSchema: binding.ref.outputSchema ? jsonClone(binding.ref.outputSchema) : void 0,
|
|
1377
|
+
boundKeys,
|
|
1378
|
+
lockedKeys,
|
|
1379
|
+
overridableKeys: overridable,
|
|
1380
|
+
confirmationFloor: maxConfirmation(
|
|
1381
|
+
defaultConfirmationFor(effect),
|
|
1382
|
+
binding.config.confirmation ?? "never",
|
|
1383
|
+
binding.ref.requiresApproval === true ? "required" : "never"
|
|
1384
|
+
),
|
|
1385
|
+
idempotent: effect === "server-query",
|
|
1386
|
+
auditLevel: defaultAuditFor(effect),
|
|
1387
|
+
meta: binding.config.meta ? jsonClone(binding.config.meta) : void 0,
|
|
1388
|
+
policies: [...binding.config.policies ?? []],
|
|
1389
|
+
contextLink: binding.contextLink ?? (hasView ? { type: def.type, instanceId } : void 0)
|
|
1390
|
+
};
|
|
1391
|
+
});
|
|
1392
|
+
return {
|
|
1393
|
+
id,
|
|
1394
|
+
key: componentKey(def.type, instanceId),
|
|
1395
|
+
type: def.type,
|
|
1396
|
+
instanceId,
|
|
1397
|
+
description: def.description,
|
|
1398
|
+
parent: def.parent ? { type: def.parent.type, instanceId: def.parent.instanceId ?? "default" } : void 0,
|
|
1399
|
+
meta: def.meta ? jsonClone(def.meta) : void 0,
|
|
1400
|
+
internal: Object.freeze({ ...def.internal ?? {} }),
|
|
1401
|
+
origin: def.origin ?? "first-party",
|
|
1402
|
+
priority: def.priority ?? 0,
|
|
1403
|
+
definition: def,
|
|
1404
|
+
componentPolicies: [...def.policies ?? []],
|
|
1405
|
+
observations,
|
|
1406
|
+
actions,
|
|
1407
|
+
procedures,
|
|
1408
|
+
procedureOnly: !hasView && procedures.length > 0,
|
|
1409
|
+
status: "active",
|
|
1410
|
+
enabled: def.enabled !== false,
|
|
1411
|
+
availabilityOverrides: /* @__PURE__ */ new Map(),
|
|
1412
|
+
inFlight: /* @__PURE__ */ new Set(),
|
|
1413
|
+
actionQueue: { running: false, waiting: [] }
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function liveAvailabilityHooks(reg, cap) {
|
|
1417
|
+
if (cap.kind === "observation") {
|
|
1418
|
+
const live = reg.definition.observations?.[cap.name];
|
|
1419
|
+
return { when: live?.when, unavailableReason: live?.unavailableReason };
|
|
1420
|
+
}
|
|
1421
|
+
if (cap.kind === "action") {
|
|
1422
|
+
const live = reg.definition.actions?.[cap.name];
|
|
1423
|
+
return { when: live?.when, unavailableReason: live?.unavailableReason };
|
|
1424
|
+
}
|
|
1425
|
+
return { when: cap.binding.config.when, unavailableReason: cap.binding.config.unavailableReason };
|
|
1426
|
+
}
|
|
1427
|
+
function computeAvailability(internals, reg, cap) {
|
|
1428
|
+
if (reg.status !== "active") {
|
|
1429
|
+
return { available: false, reason: "component-unregistered" };
|
|
1430
|
+
}
|
|
1431
|
+
if (!reg.enabled) {
|
|
1432
|
+
return { available: false, reason: "component-disabled" };
|
|
1433
|
+
}
|
|
1434
|
+
const overrideKey = cap.kind === "procedure" ? cap.path : cap.name;
|
|
1435
|
+
const override = reg.availabilityOverrides.get(overrideKey) ?? reg.availabilityOverrides.get(cap.capabilityId);
|
|
1436
|
+
if (override && override.available === false) {
|
|
1437
|
+
return { available: false, reason: override.reason ?? "unavailable" };
|
|
1438
|
+
}
|
|
1439
|
+
const hooks = liveAvailabilityHooks(reg, cap);
|
|
1440
|
+
if (hooks.when) {
|
|
1441
|
+
let result;
|
|
1442
|
+
try {
|
|
1443
|
+
result = hooks.when() !== false;
|
|
1444
|
+
} catch (err) {
|
|
1445
|
+
internals.devWarn(
|
|
1446
|
+
`[agent-surface] when() threw for ${cap.capabilityId}; treating as unavailable`,
|
|
1447
|
+
err
|
|
1448
|
+
);
|
|
1449
|
+
return { available: false, reason: "when-error" };
|
|
1450
|
+
}
|
|
1451
|
+
if (!result) {
|
|
1452
|
+
let reason = "Currently unavailable";
|
|
1453
|
+
const ur = hooks.unavailableReason;
|
|
1454
|
+
try {
|
|
1455
|
+
if (typeof ur === "function") reason = ur();
|
|
1456
|
+
else if (typeof ur === "string") reason = ur;
|
|
1457
|
+
} catch {
|
|
1458
|
+
}
|
|
1459
|
+
return { available: false, reason };
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
return { available: true };
|
|
1463
|
+
}
|
|
1464
|
+
function policiesFor(internals, reg, cap) {
|
|
1465
|
+
return [...internals.registryPolicies, ...reg.componentPolicies, ...cap.policies];
|
|
1466
|
+
}
|
|
1467
|
+
function buildPolicyContext(internals, reg, cap, consumer, host) {
|
|
1468
|
+
return {
|
|
1469
|
+
capabilityId: cap.capabilityId,
|
|
1470
|
+
plane: cap.kind === "procedure" ? "domain" : "view",
|
|
1471
|
+
kind: cap.kind,
|
|
1472
|
+
effect: cap.kind === "observation" ? "read" : cap.effect,
|
|
1473
|
+
registrationId: reg.id,
|
|
1474
|
+
consumer,
|
|
1475
|
+
host,
|
|
1476
|
+
meta: { component: reg.meta, capability: cap.meta },
|
|
1477
|
+
internal: reg.internal,
|
|
1478
|
+
environment: internals.environment,
|
|
1479
|
+
now: () => internals.now()
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
function consumerKeyOf(consumer) {
|
|
1483
|
+
return `${consumer.kind}:${consumer.id}`;
|
|
1484
|
+
}
|
|
1485
|
+
function pruneTombstones(internals) {
|
|
1486
|
+
const now = internals.now();
|
|
1487
|
+
for (const [id, tomb] of internals.tombstones) {
|
|
1488
|
+
if (tomb.expiresAt <= now) internals.tombstones.delete(id);
|
|
1489
|
+
}
|
|
1490
|
+
while (internals.tombstones.size > internals.limits.tombstoneSize) {
|
|
1491
|
+
const oldest = internals.tombstones.keys().next().value;
|
|
1492
|
+
if (oldest === void 0) break;
|
|
1493
|
+
internals.tombstones.delete(oldest);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
function addTombstone(internals, reg) {
|
|
1497
|
+
const capabilityIds = /* @__PURE__ */ new Set();
|
|
1498
|
+
for (const obs of reg.observations.values()) capabilityIds.add(obs.capabilityId);
|
|
1499
|
+
for (const act of reg.actions.values()) capabilityIds.add(act.capabilityId);
|
|
1500
|
+
for (const proc of reg.procedures) capabilityIds.add(proc.capabilityId);
|
|
1501
|
+
internals.tombstones.set(reg.id, {
|
|
1502
|
+
registrationId: reg.id,
|
|
1503
|
+
type: reg.type,
|
|
1504
|
+
instanceId: reg.instanceId,
|
|
1505
|
+
capabilityIds,
|
|
1506
|
+
expiresAt: internals.now() + internals.limits.tombstoneTtlMs
|
|
1507
|
+
});
|
|
1508
|
+
pruneTombstones(internals);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// src/invoke.ts
|
|
1512
|
+
var DEFAULT_CONSUMER = { id: "anonymous", kind: "embedded" };
|
|
1513
|
+
function notFound() {
|
|
1514
|
+
return {
|
|
1515
|
+
code: "CAPABILITY_NOT_FOUND",
|
|
1516
|
+
message: "This capability does not exist in the current surface. Refresh the surface catalog before the next step.",
|
|
1517
|
+
retry: "after-refresh"
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
function notAvailable(reason) {
|
|
1521
|
+
return {
|
|
1522
|
+
code: "CAPABILITY_NOT_AVAILABLE",
|
|
1523
|
+
message: `This capability exists but is currently unavailable${reason ? `: ${reason}` : ""}. Perform the enabling step first, then refresh.`,
|
|
1524
|
+
retry: "after-refresh",
|
|
1525
|
+
...reason !== void 0 ? { details: { reason } } : {}
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
function unmounted(phase) {
|
|
1529
|
+
return {
|
|
1530
|
+
code: "COMPONENT_UNMOUNTED",
|
|
1531
|
+
message: phase === "mid-flight" ? "The owning view unmounted while this capability was executing. Verify state before repeating a non-idempotent action." : "The owning view is no longer mounted. Refresh the surface catalog.",
|
|
1532
|
+
retry: "after-refresh",
|
|
1533
|
+
details: { phase }
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
function stale(reason, liveRegistrationId) {
|
|
1537
|
+
return {
|
|
1538
|
+
code: "STALE_CAPABILITY",
|
|
1539
|
+
message: "The invocation references a superseded surface snapshot. Refresh the catalog and re-resolve the target.",
|
|
1540
|
+
retry: "after-refresh",
|
|
1541
|
+
details: { reason, ...liveRegistrationId ? { liveRegistrationId } : {} }
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
function invocationConflict() {
|
|
1545
|
+
return {
|
|
1546
|
+
code: "INVOCATION_CONFLICT",
|
|
1547
|
+
message: "This invocation id was already used for a different request. Use a fresh invocation id if the new request is intentional.",
|
|
1548
|
+
retry: "with-changes",
|
|
1549
|
+
details: { reason: "id-reused-with-different-request" }
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
function queueFull(retryAfterMs) {
|
|
1553
|
+
return {
|
|
1554
|
+
code: "RATE_LIMITED",
|
|
1555
|
+
message: "The queue for this capability is full. Retry shortly.",
|
|
1556
|
+
retry: "after-delay",
|
|
1557
|
+
details: { reason: "queue-full", retryAfterMs }
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
function cancelled(message) {
|
|
1561
|
+
return { code: "CANCELLED", message, retry: "yes" };
|
|
1562
|
+
}
|
|
1563
|
+
function executionFailed(reason, opts) {
|
|
1564
|
+
const messages = {
|
|
1565
|
+
"handler-error": "The capability failed to execute.",
|
|
1566
|
+
"output-invalid": "The capability produced an invalid output.",
|
|
1567
|
+
"output-too-large": "The capability produced an output exceeding the size limit.",
|
|
1568
|
+
transport: "The server call failed."
|
|
1569
|
+
};
|
|
1570
|
+
return {
|
|
1571
|
+
code: "EXECUTION_FAILED",
|
|
1572
|
+
message: messages[reason] ?? "The capability failed to execute.",
|
|
1573
|
+
retry: opts?.transient ? "after-delay" : "no",
|
|
1574
|
+
details: {
|
|
1575
|
+
reason,
|
|
1576
|
+
...opts?.transient ? { transient: true, retryAfterMs: 1e3 } : {}
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function requestFingerprint(request) {
|
|
1581
|
+
return fnv1a64(
|
|
1582
|
+
canonicalJson({
|
|
1583
|
+
capabilityId: request.capabilityId,
|
|
1584
|
+
registrationId: request.registrationId ?? null,
|
|
1585
|
+
instanceId: request.instanceId ?? null,
|
|
1586
|
+
surfaceVersion: request.surfaceVersion ?? null,
|
|
1587
|
+
input: request.input ?? null,
|
|
1588
|
+
confirmationId: request.confirmationId ?? null
|
|
1589
|
+
})
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
function performInvoke(internals, request, options) {
|
|
1593
|
+
if (internals.disposed) {
|
|
1594
|
+
throw new Error("invoke() called on a disposed registry");
|
|
1595
|
+
}
|
|
1596
|
+
const invocationId = request.invocationId ?? `inv_${randomBase62(12)}`;
|
|
1597
|
+
const consumer = options?.consumer ?? DEFAULT_CONSUMER;
|
|
1598
|
+
const consumerKey = consumerKeyOf(consumer);
|
|
1599
|
+
const fingerprint = requestFingerprint(request);
|
|
1600
|
+
const dedupeKey = `${consumerKey} ${invocationId}`;
|
|
1601
|
+
pruneDedupe(internals);
|
|
1602
|
+
const existing = internals.dedupe.get(dedupeKey);
|
|
1603
|
+
if (existing) {
|
|
1604
|
+
if (existing.kind === "inflight") {
|
|
1605
|
+
if (existing.fingerprint === fingerprint) return existing.promise;
|
|
1606
|
+
return Promise.resolve(conflictResult(internals, request, invocationId, consumer));
|
|
1607
|
+
}
|
|
1608
|
+
if (existing.expiresAt > internals.now()) {
|
|
1609
|
+
if (existing.fingerprint === fingerprint) return Promise.resolve(existing.result);
|
|
1610
|
+
return Promise.resolve(conflictResult(internals, request, invocationId, consumer));
|
|
1611
|
+
}
|
|
1612
|
+
internals.dedupe.delete(dedupeKey);
|
|
1613
|
+
}
|
|
1614
|
+
const promise = runPipeline(internals, request, invocationId, consumer, consumerKey, options);
|
|
1615
|
+
internals.dedupe.set(dedupeKey, { kind: "inflight", fingerprint, promise });
|
|
1616
|
+
promise.then(
|
|
1617
|
+
(result) => {
|
|
1618
|
+
const terminal = result.status === "ok" || result.error.code !== "CONFIRMATION_REQUIRED" && result.error.code !== "RATE_LIMITED";
|
|
1619
|
+
if (terminal) {
|
|
1620
|
+
internals.dedupe.set(dedupeKey, {
|
|
1621
|
+
kind: "terminal",
|
|
1622
|
+
fingerprint,
|
|
1623
|
+
result,
|
|
1624
|
+
expiresAt: internals.now() + internals.limits.dedupeCacheTtlMs
|
|
1625
|
+
});
|
|
1626
|
+
pruneDedupe(internals);
|
|
1627
|
+
} else {
|
|
1628
|
+
internals.dedupe.delete(dedupeKey);
|
|
1629
|
+
}
|
|
1630
|
+
},
|
|
1631
|
+
() => {
|
|
1632
|
+
internals.dedupe.delete(dedupeKey);
|
|
1633
|
+
}
|
|
1634
|
+
);
|
|
1635
|
+
return promise;
|
|
1636
|
+
}
|
|
1637
|
+
function conflictResult(internals, request, invocationId, consumer) {
|
|
1638
|
+
internals.emit({
|
|
1639
|
+
type: "invocation-started",
|
|
1640
|
+
invocationId,
|
|
1641
|
+
capabilityId: request.capabilityId,
|
|
1642
|
+
consumerId: consumer.id
|
|
1643
|
+
});
|
|
1644
|
+
const error = invocationConflict();
|
|
1645
|
+
const result = {
|
|
1646
|
+
status: "error",
|
|
1647
|
+
invocationId,
|
|
1648
|
+
capabilityId: request.capabilityId,
|
|
1649
|
+
error,
|
|
1650
|
+
surfaceVersion: String(internals.version)
|
|
1651
|
+
};
|
|
1652
|
+
internals.emit({
|
|
1653
|
+
type: "invocation-settled",
|
|
1654
|
+
invocationId,
|
|
1655
|
+
capabilityId: request.capabilityId,
|
|
1656
|
+
status: "error",
|
|
1657
|
+
code: error.code,
|
|
1658
|
+
durationMs: 0
|
|
1659
|
+
});
|
|
1660
|
+
internals.recordAudit({
|
|
1661
|
+
type: "invocation-settled",
|
|
1662
|
+
capabilityId: request.capabilityId,
|
|
1663
|
+
invocationId,
|
|
1664
|
+
consumerId: consumerKeyOf(consumer),
|
|
1665
|
+
status: "error",
|
|
1666
|
+
code: error.code,
|
|
1667
|
+
durationMs: 0
|
|
1668
|
+
});
|
|
1669
|
+
return result;
|
|
1670
|
+
}
|
|
1671
|
+
function pruneDedupe(internals) {
|
|
1672
|
+
const now = internals.now();
|
|
1673
|
+
for (const [id, entry] of internals.dedupe) {
|
|
1674
|
+
if (entry.kind === "terminal" && entry.expiresAt <= now) internals.dedupe.delete(id);
|
|
1675
|
+
}
|
|
1676
|
+
while (internals.dedupe.size > internals.limits.dedupeCacheSize) {
|
|
1677
|
+
const oldest = internals.dedupe.keys().next().value;
|
|
1678
|
+
if (oldest === void 0) break;
|
|
1679
|
+
const entry = internals.dedupe.get(oldest);
|
|
1680
|
+
if (entry?.kind === "inflight") break;
|
|
1681
|
+
internals.dedupe.delete(oldest);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
async function runPipeline(internals, request, invocationId, consumer, consumerKey, options) {
|
|
1685
|
+
const startVersion = internals.version;
|
|
1686
|
+
const startedAt = internals.now();
|
|
1687
|
+
internals.emit({
|
|
1688
|
+
type: "invocation-started",
|
|
1689
|
+
invocationId,
|
|
1690
|
+
capabilityId: request.capabilityId,
|
|
1691
|
+
consumerId: consumer.id
|
|
1692
|
+
});
|
|
1693
|
+
let resolvedAuditLevel = "metadata";
|
|
1694
|
+
let resolvedRegistrationId;
|
|
1695
|
+
let inputForAudit;
|
|
1696
|
+
let outputForAudit;
|
|
1697
|
+
let queueWaitMsForAudit;
|
|
1698
|
+
let executionMsForAudit;
|
|
1699
|
+
const finalize = (body) => {
|
|
1700
|
+
const surfaceVersion = String(internals.version);
|
|
1701
|
+
const surfaceChanged = internals.version !== startVersion ? true : void 0;
|
|
1702
|
+
const result = body.status === "ok" ? {
|
|
1703
|
+
status: "ok",
|
|
1704
|
+
invocationId,
|
|
1705
|
+
capabilityId: request.capabilityId,
|
|
1706
|
+
...body.output !== void 0 ? { output: body.output } : {},
|
|
1707
|
+
surfaceVersion,
|
|
1708
|
+
...surfaceChanged ? { surfaceChanged } : {}
|
|
1709
|
+
} : {
|
|
1710
|
+
status: "error",
|
|
1711
|
+
invocationId,
|
|
1712
|
+
capabilityId: request.capabilityId,
|
|
1713
|
+
error: body.error,
|
|
1714
|
+
surfaceVersion,
|
|
1715
|
+
...surfaceChanged ? { surfaceChanged } : {}
|
|
1716
|
+
};
|
|
1717
|
+
const durationMs = internals.now() - startedAt;
|
|
1718
|
+
internals.emit({
|
|
1719
|
+
type: "invocation-settled",
|
|
1720
|
+
invocationId,
|
|
1721
|
+
capabilityId: request.capabilityId,
|
|
1722
|
+
status: result.status,
|
|
1723
|
+
...result.status === "error" ? { code: result.error.code } : {},
|
|
1724
|
+
durationMs
|
|
1725
|
+
});
|
|
1726
|
+
if (resolvedAuditLevel !== "none") {
|
|
1727
|
+
internals.recordAudit({
|
|
1728
|
+
type: "invocation-settled",
|
|
1729
|
+
capabilityId: request.capabilityId,
|
|
1730
|
+
registrationId: resolvedRegistrationId,
|
|
1731
|
+
invocationId,
|
|
1732
|
+
consumerId: consumerKey,
|
|
1733
|
+
status: result.status,
|
|
1734
|
+
...result.status === "error" ? { code: result.error.code } : {},
|
|
1735
|
+
durationMs,
|
|
1736
|
+
...queueWaitMsForAudit !== void 0 ? { queueWaitMs: queueWaitMsForAudit } : {},
|
|
1737
|
+
...executionMsForAudit !== void 0 ? { executionMs: executionMsForAudit } : {},
|
|
1738
|
+
...resolvedAuditLevel === "full" ? {
|
|
1739
|
+
payload: {
|
|
1740
|
+
...inputForAudit !== void 0 ? { input: inputForAudit } : {},
|
|
1741
|
+
...outputForAudit !== void 0 ? { output: outputForAudit } : {}
|
|
1742
|
+
}
|
|
1743
|
+
} : {}
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
return result;
|
|
1747
|
+
};
|
|
1748
|
+
try {
|
|
1749
|
+
const resolved = resolveTarget(internals, request);
|
|
1750
|
+
if ("error" in resolved) return finalize({ status: "error", error: resolved.error });
|
|
1751
|
+
const { reg, cap } = resolved;
|
|
1752
|
+
resolvedRegistrationId = reg.id;
|
|
1753
|
+
resolvedAuditLevel = cap.auditLevel;
|
|
1754
|
+
if (request.surfaceVersion !== void 0 && request.surfaceVersion !== String(internals.version) && cap.kind === "procedure" && (cap.effect === "destructive" || cap.effect === "external-side-effect")) {
|
|
1755
|
+
return finalize({ status: "error", error: stale("surface-version-mismatch") });
|
|
1756
|
+
}
|
|
1757
|
+
if (resolvedAuditLevel !== "none") {
|
|
1758
|
+
internals.recordAudit({
|
|
1759
|
+
type: "invocation-started",
|
|
1760
|
+
capabilityId: cap.capabilityId,
|
|
1761
|
+
registrationId: reg.id,
|
|
1762
|
+
invocationId,
|
|
1763
|
+
consumerId: consumerKey
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
const availability = computeAvailability(internals, reg, cap);
|
|
1767
|
+
if (!availability.available) {
|
|
1768
|
+
return finalize({ status: "error", error: notAvailable(availability.reason) });
|
|
1769
|
+
}
|
|
1770
|
+
const host = internals.host();
|
|
1771
|
+
const chain = policiesFor(internals, reg, cap);
|
|
1772
|
+
const policyCtx = buildPolicyContext(internals, reg, cap, consumer, host);
|
|
1773
|
+
const discovery = evaluateDiscovery(
|
|
1774
|
+
chain.filter((p) => !p.onAuthorize && !p.onInvoke),
|
|
1775
|
+
policyCtx
|
|
1776
|
+
);
|
|
1777
|
+
if (discovery.decision === "hide") {
|
|
1778
|
+
return finalize({ status: "error", error: notFound() });
|
|
1779
|
+
}
|
|
1780
|
+
if (discovery.decision === "disable") {
|
|
1781
|
+
return finalize({ status: "error", error: notAvailable(discovery.reason) });
|
|
1782
|
+
}
|
|
1783
|
+
const escalations = chain.map((p) => p[CONFIRMATION_ESCALATION]).filter((e) => e !== void 0);
|
|
1784
|
+
const core = () => executeCore(internals, {
|
|
1785
|
+
request,
|
|
1786
|
+
invocationId,
|
|
1787
|
+
consumer,
|
|
1788
|
+
consumerKey,
|
|
1789
|
+
host,
|
|
1790
|
+
reg,
|
|
1791
|
+
cap,
|
|
1792
|
+
chain,
|
|
1793
|
+
policyCtx,
|
|
1794
|
+
escalations,
|
|
1795
|
+
options,
|
|
1796
|
+
finalize,
|
|
1797
|
+
setAuditPayload: (input, output) => {
|
|
1798
|
+
if (input !== void 0) inputForAudit = input;
|
|
1799
|
+
if (output !== void 0) outputForAudit = output;
|
|
1800
|
+
},
|
|
1801
|
+
setTimings: (timings) => {
|
|
1802
|
+
if (timings.queueWaitMs !== void 0) queueWaitMsForAudit = timings.queueWaitMs;
|
|
1803
|
+
if (timings.executionMs !== void 0) executionMsForAudit = timings.executionMs;
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
try {
|
|
1807
|
+
return await composeAuthorizeChain(chain, policyCtx, core);
|
|
1808
|
+
} catch (err) {
|
|
1809
|
+
if (isAgentSurfaceError(err)) {
|
|
1810
|
+
return finalize({ status: "error", error: err.payload });
|
|
1811
|
+
}
|
|
1812
|
+
throw err;
|
|
1813
|
+
}
|
|
1814
|
+
} catch (err) {
|
|
1815
|
+
if (err instanceof DevDefectError) throw err;
|
|
1816
|
+
if (isAgentSurfaceError(err)) {
|
|
1817
|
+
return finalize({ status: "error", error: err.payload });
|
|
1818
|
+
}
|
|
1819
|
+
internals.devError("[agent-surface] invocation pipeline failure", err);
|
|
1820
|
+
return finalize({ status: "error", error: executionFailed("handler-error") });
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
function resolveTarget(internals, request) {
|
|
1824
|
+
const parsed = parseCapabilityId(request.capabilityId);
|
|
1825
|
+
if (!parsed) return { error: notFound() };
|
|
1826
|
+
let candidates = [];
|
|
1827
|
+
if (parsed.plane === "view") {
|
|
1828
|
+
for (const reg of internals.registrations.values()) {
|
|
1829
|
+
if (reg.status !== "active" || reg.type !== parsed.componentType) continue;
|
|
1830
|
+
const cap = reg.observations.get(parsed.name) ?? reg.actions.get(parsed.name);
|
|
1831
|
+
if (cap) candidates.push({ reg, cap });
|
|
1832
|
+
}
|
|
1833
|
+
} else {
|
|
1834
|
+
for (const reg of internals.registrations.values()) {
|
|
1835
|
+
if (reg.status !== "active") continue;
|
|
1836
|
+
for (const proc of reg.procedures) {
|
|
1837
|
+
if (proc.path === parsed.path) candidates.push({ reg, cap: proc });
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
if (request.instanceId !== void 0) {
|
|
1842
|
+
candidates = candidates.filter((c) => c.reg.instanceId === request.instanceId);
|
|
1843
|
+
}
|
|
1844
|
+
candidates.sort(
|
|
1845
|
+
(a, b) => a.reg.instanceId < b.reg.instanceId ? -1 : a.reg.instanceId > b.reg.instanceId ? 1 : 0
|
|
1846
|
+
);
|
|
1847
|
+
if (request.registrationId !== void 0) {
|
|
1848
|
+
const live = candidates.find((c) => c.reg.id === request.registrationId);
|
|
1849
|
+
if (live) return live;
|
|
1850
|
+
const tombstone = internals.tombstones.get(request.registrationId);
|
|
1851
|
+
const tombstoned = tombstone !== void 0 && tombstone.expiresAt > internals.now();
|
|
1852
|
+
if (candidates.length > 0) {
|
|
1853
|
+
const reason = tombstoned ? "registration-replaced" : "surface-reloaded";
|
|
1854
|
+
return { error: stale(reason, candidates[0]?.reg.id) };
|
|
1855
|
+
}
|
|
1856
|
+
if (tombstoned) {
|
|
1857
|
+
return { error: unmounted("resolve") };
|
|
1858
|
+
}
|
|
1859
|
+
return { error: notFound() };
|
|
1860
|
+
}
|
|
1861
|
+
if (candidates.length === 0) {
|
|
1862
|
+
for (const tomb of internals.tombstones.values()) {
|
|
1863
|
+
if (tomb.expiresAt <= internals.now()) continue;
|
|
1864
|
+
if (tomb.capabilityIds.has(request.capabilityId)) {
|
|
1865
|
+
return { error: unmounted("resolve") };
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
return { error: notFound() };
|
|
1869
|
+
}
|
|
1870
|
+
if (candidates.length > 1) {
|
|
1871
|
+
const instances = candidates.map((c) => {
|
|
1872
|
+
const entry = {
|
|
1873
|
+
instanceId: c.reg.instanceId,
|
|
1874
|
+
registrationId: c.reg.id
|
|
1875
|
+
};
|
|
1876
|
+
if (c.cap.kind === "procedure") {
|
|
1877
|
+
if (c.cap.contextLink) entry.context = { ...c.cap.contextLink };
|
|
1878
|
+
} else {
|
|
1879
|
+
entry.description = c.reg.description;
|
|
1880
|
+
}
|
|
1881
|
+
return entry;
|
|
1882
|
+
});
|
|
1883
|
+
return {
|
|
1884
|
+
error: {
|
|
1885
|
+
code: "AMBIGUOUS_INSTANCE",
|
|
1886
|
+
message: "More than one live instance matches this capability. Re-issue the call with an explicit instanceId or registrationId.",
|
|
1887
|
+
retry: "with-changes",
|
|
1888
|
+
details: { instances }
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
return candidates[0];
|
|
1893
|
+
}
|
|
1894
|
+
async function executeCore(internals, args) {
|
|
1895
|
+
const { cap } = args;
|
|
1896
|
+
if (cap.kind === "observation") return executeObservation(internals, args, cap);
|
|
1897
|
+
if (cap.kind === "action") return executeAction(internals, args, cap);
|
|
1898
|
+
return executeProcedure(internals, args, cap);
|
|
1899
|
+
}
|
|
1900
|
+
function runInvokePolicies(args, effectiveInput, downstream) {
|
|
1901
|
+
const invokeCtx = {
|
|
1902
|
+
...args.policyCtx,
|
|
1903
|
+
invocationId: args.invocationId,
|
|
1904
|
+
effectiveInput
|
|
1905
|
+
};
|
|
1906
|
+
return composeInvokeChain(args.chain, invokeCtx, downstream);
|
|
1907
|
+
}
|
|
1908
|
+
async function executeObservation(internals, args, cap) {
|
|
1909
|
+
const { reg, invocationId, consumer, consumerKey, host, options, finalize } = args;
|
|
1910
|
+
const readCtx = {
|
|
1911
|
+
capabilityId: cap.capabilityId,
|
|
1912
|
+
registrationId: reg.id,
|
|
1913
|
+
consumer,
|
|
1914
|
+
host
|
|
1915
|
+
};
|
|
1916
|
+
const run = async () => {
|
|
1917
|
+
const queueStart = internals.now();
|
|
1918
|
+
const slot = await acquireObservationSlot(internals, consumerKey);
|
|
1919
|
+
args.setTimings({ queueWaitMs: internals.now() - queueStart });
|
|
1920
|
+
if (slot === "overflow") {
|
|
1921
|
+
return finalize({ status: "error", error: queueFull(250) });
|
|
1922
|
+
}
|
|
1923
|
+
if (slot === "cancelled") {
|
|
1924
|
+
return finalize({
|
|
1925
|
+
status: "error",
|
|
1926
|
+
error: { ...cancelled("The registry was disposed."), retry: "no" }
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
try {
|
|
1930
|
+
const timeoutMs = options?.timeoutMs ?? cap.timeoutMs ?? internals.limits.observationTimeoutMs;
|
|
1931
|
+
const executeStart = internals.now();
|
|
1932
|
+
const outcome = await executeWithGuards(internals, reg, {
|
|
1933
|
+
invocationId,
|
|
1934
|
+
capabilityId: cap.capabilityId,
|
|
1935
|
+
timeoutMs,
|
|
1936
|
+
externalSignal: options?.signal,
|
|
1937
|
+
idempotent: true,
|
|
1938
|
+
run: () => {
|
|
1939
|
+
const live = reg.definition.observations?.[cap.name];
|
|
1940
|
+
if (!live) throw new Error("observation handler missing");
|
|
1941
|
+
return live.read(readCtx);
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
args.setTimings({ executionMs: internals.now() - executeStart });
|
|
1945
|
+
if (!outcome.ok) return finalize({ status: "error", error: outcome.payload });
|
|
1946
|
+
const output = settleOutput(internals, outcome.value, cap.outputSchema);
|
|
1947
|
+
if ("error" in output) return finalize({ status: "error", error: output.error });
|
|
1948
|
+
return finalize({ status: "ok", output: output.value });
|
|
1949
|
+
} finally {
|
|
1950
|
+
releaseObservationSlot(internals, consumerKey);
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
return runInvokePolicies(args, {}, run);
|
|
1954
|
+
}
|
|
1955
|
+
async function executeAction(internals, args, cap) {
|
|
1956
|
+
const { request, reg, invocationId, consumer, host, options, finalize } = args;
|
|
1957
|
+
let parsedInput;
|
|
1958
|
+
try {
|
|
1959
|
+
parsedInput = cap.inputSchema.parse(request.input);
|
|
1960
|
+
} catch (err) {
|
|
1961
|
+
return finalize({ status: "error", error: invalidInput(err) });
|
|
1962
|
+
}
|
|
1963
|
+
args.setAuditPayload(parsedInput, void 0);
|
|
1964
|
+
const readCtx = {
|
|
1965
|
+
capabilityId: cap.capabilityId,
|
|
1966
|
+
registrationId: reg.id,
|
|
1967
|
+
consumer,
|
|
1968
|
+
host
|
|
1969
|
+
};
|
|
1970
|
+
const run = async () => {
|
|
1971
|
+
const confirmation = gateConfirmation(internals, {
|
|
1972
|
+
...args,
|
|
1973
|
+
effectiveInput: parsedInput,
|
|
1974
|
+
declared: cap.confirmation,
|
|
1975
|
+
description: cap.description,
|
|
1976
|
+
effect: cap.effect
|
|
1977
|
+
});
|
|
1978
|
+
if ("error" in confirmation) return finalize({ status: "error", error: confirmation.error });
|
|
1979
|
+
const livePrecondition = reg.definition.actions?.[cap.name]?.precondition;
|
|
1980
|
+
if (livePrecondition) {
|
|
1981
|
+
try {
|
|
1982
|
+
const failure = livePrecondition(parsedInput, readCtx);
|
|
1983
|
+
if (failure && typeof failure.message === "string") {
|
|
1984
|
+
return finalize({
|
|
1985
|
+
status: "error",
|
|
1986
|
+
error: preconditionFailed(failure.message, failure.details)
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
} catch (err) {
|
|
1990
|
+
if (isAgentSurfaceError(err)) return finalize({ status: "error", error: err.payload });
|
|
1991
|
+
if (!(err instanceof Error) && typeof err === "object" && err !== null && typeof err.message === "string") {
|
|
1992
|
+
const failure = err;
|
|
1993
|
+
return finalize({
|
|
1994
|
+
status: "error",
|
|
1995
|
+
error: preconditionFailed(failure.message, failure.details)
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
internals.devError(`[agent-surface] precondition threw for ${cap.capabilityId}`, err);
|
|
1999
|
+
return finalize({ status: "error", error: executionFailed("handler-error") });
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
const queueStart = internals.now();
|
|
2003
|
+
const slot = await acquireActionSlot(internals, reg);
|
|
2004
|
+
args.setTimings({ queueWaitMs: internals.now() - queueStart });
|
|
2005
|
+
if (slot === "overflow") {
|
|
2006
|
+
return finalize({ status: "error", error: queueFull(250) });
|
|
2007
|
+
}
|
|
2008
|
+
try {
|
|
2009
|
+
const timeoutMs = options?.timeoutMs ?? cap.timeoutMs ?? internals.limits.actionTimeoutMs;
|
|
2010
|
+
const executeStart = internals.now();
|
|
2011
|
+
const outcome = await executeWithGuards(internals, reg, {
|
|
2012
|
+
invocationId,
|
|
2013
|
+
capabilityId: cap.capabilityId,
|
|
2014
|
+
timeoutMs,
|
|
2015
|
+
externalSignal: options?.signal,
|
|
2016
|
+
idempotent: cap.idempotent,
|
|
2017
|
+
navigationSettlement: cap.effect === "navigation",
|
|
2018
|
+
run: (signal) => {
|
|
2019
|
+
const live = reg.definition.actions?.[cap.name];
|
|
2020
|
+
if (!live) throw new Error("action handler missing");
|
|
2021
|
+
const actionCtx = {
|
|
2022
|
+
...readCtx,
|
|
2023
|
+
invocationId,
|
|
2024
|
+
signal,
|
|
2025
|
+
...confirmation.evidence ? { confirmation: confirmation.evidence } : {}
|
|
2026
|
+
};
|
|
2027
|
+
return live.execute(parsedInput, actionCtx);
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
args.setTimings({ executionMs: internals.now() - executeStart });
|
|
2031
|
+
if (!outcome.ok) return finalize({ status: "error", error: outcome.payload });
|
|
2032
|
+
const output = settleOutput(internals, outcome.value, cap.outputSchema);
|
|
2033
|
+
if ("error" in output) return finalize({ status: "error", error: output.error });
|
|
2034
|
+
args.setAuditPayload(void 0, output.value);
|
|
2035
|
+
return finalize({ status: "ok", output: output.value });
|
|
2036
|
+
} finally {
|
|
2037
|
+
releaseActionSlot(reg);
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
return runInvokePolicies(args, parsedInput, run);
|
|
2041
|
+
}
|
|
2042
|
+
async function executeProcedure(internals, args, cap) {
|
|
2043
|
+
const { request, reg, invocationId, consumer, options, finalize } = args;
|
|
2044
|
+
const agentInput = request.input ?? {};
|
|
2045
|
+
if (typeof agentInput !== "object" || agentInput === null || Array.isArray(agentInput)) {
|
|
2046
|
+
return finalize({
|
|
2047
|
+
status: "error",
|
|
2048
|
+
error: invalidInput(new AgentSchemaError([{ path: "", message: "input must be an object" }]))
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
const suppliedLocked = Object.keys(agentInput).filter((k) => cap.lockedKeys.includes(k));
|
|
2052
|
+
if (suppliedLocked.length > 0) {
|
|
2053
|
+
return finalize({
|
|
2054
|
+
status: "error",
|
|
2055
|
+
error: {
|
|
2056
|
+
code: "INVALID_INPUT",
|
|
2057
|
+
message: "Some fields are bound to the application's UI state and cannot be supplied by the agent. Omit them and retry.",
|
|
2058
|
+
retry: "with-changes",
|
|
2059
|
+
details: { lockedFields: suppliedLocked }
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
try {
|
|
2064
|
+
fromJsonSchema(cap.reducedInputSchema).parse(agentInput);
|
|
2065
|
+
} catch (err) {
|
|
2066
|
+
return finalize({ status: "error", error: invalidInput(err) });
|
|
2067
|
+
}
|
|
2068
|
+
let bound = {};
|
|
2069
|
+
const bind = cap.binding.config.bind;
|
|
2070
|
+
if (bind) {
|
|
2071
|
+
try {
|
|
2072
|
+
bound = bind() ?? {};
|
|
2073
|
+
} catch (err) {
|
|
2074
|
+
internals.devWarn(`[agent-surface] bind() threw for ${cap.capabilityId}`, err);
|
|
2075
|
+
return finalize({ status: "error", error: bindingFailed() });
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
const effective = {};
|
|
2079
|
+
for (const [key, value] of Object.entries(agentInput)) {
|
|
2080
|
+
if (!cap.lockedKeys.includes(key)) effective[key] = value;
|
|
2081
|
+
}
|
|
2082
|
+
for (const key of cap.boundKeys) {
|
|
2083
|
+
const agentSupplied = cap.overridableKeys.has(key) && agentInput[key] !== void 0;
|
|
2084
|
+
if (!agentSupplied && bound[key] !== void 0) effective[key] = bound[key];
|
|
2085
|
+
}
|
|
2086
|
+
try {
|
|
2087
|
+
fromJsonSchema(cap.fullInputSchema).parse(effective);
|
|
2088
|
+
} catch (err) {
|
|
2089
|
+
internals.devWarn(
|
|
2090
|
+
`[agent-surface] merged input for ${cap.capabilityId} failed full-schema validation`,
|
|
2091
|
+
err
|
|
2092
|
+
);
|
|
2093
|
+
return finalize({ status: "error", error: bindingFailed() });
|
|
2094
|
+
}
|
|
2095
|
+
args.setAuditPayload(effective, void 0);
|
|
2096
|
+
const run = async () => {
|
|
2097
|
+
const confirmation = gateConfirmation(internals, {
|
|
2098
|
+
...args,
|
|
2099
|
+
effectiveInput: effective,
|
|
2100
|
+
declared: cap.confirmationFloor,
|
|
2101
|
+
description: cap.baseDescription,
|
|
2102
|
+
effect: cap.effect
|
|
2103
|
+
});
|
|
2104
|
+
if ("error" in confirmation) return finalize({ status: "error", error: confirmation.error });
|
|
2105
|
+
const executor = internals.executor;
|
|
2106
|
+
if (!executor) {
|
|
2107
|
+
return finalize({ status: "error", error: executionFailed("transport") });
|
|
2108
|
+
}
|
|
2109
|
+
const timeoutMs = options?.timeoutMs ?? internals.limits.procedureTimeoutMs;
|
|
2110
|
+
const executeStart = internals.now();
|
|
2111
|
+
const outcome = await executeWithGuards(internals, reg, {
|
|
2112
|
+
invocationId,
|
|
2113
|
+
capabilityId: cap.capabilityId,
|
|
2114
|
+
timeoutMs,
|
|
2115
|
+
externalSignal: options?.signal,
|
|
2116
|
+
idempotent: cap.idempotent,
|
|
2117
|
+
run: (signal) => executor.execute({
|
|
2118
|
+
path: cap.path,
|
|
2119
|
+
input: effective,
|
|
2120
|
+
info: {
|
|
2121
|
+
invocationId,
|
|
2122
|
+
consumer,
|
|
2123
|
+
signal,
|
|
2124
|
+
...confirmation.evidence ? { confirmation: confirmation.evidence } : {}
|
|
2125
|
+
}
|
|
2126
|
+
}),
|
|
2127
|
+
procedureErrors: true
|
|
2128
|
+
});
|
|
2129
|
+
args.setTimings({ executionMs: internals.now() - executeStart });
|
|
2130
|
+
if (!outcome.ok) return finalize({ status: "error", error: outcome.payload });
|
|
2131
|
+
const output = settleOutput(
|
|
2132
|
+
internals,
|
|
2133
|
+
outcome.value,
|
|
2134
|
+
cap.outputJsonSchema ? fromJsonSchema(cap.outputJsonSchema) : void 0
|
|
2135
|
+
);
|
|
2136
|
+
if ("error" in output) return finalize({ status: "error", error: output.error });
|
|
2137
|
+
args.setAuditPayload(void 0, output.value);
|
|
2138
|
+
return finalize({ status: "ok", output: output.value });
|
|
2139
|
+
};
|
|
2140
|
+
return runInvokePolicies(args, effective, run);
|
|
2141
|
+
}
|
|
2142
|
+
function gateConfirmation(internals, args) {
|
|
2143
|
+
const { request, reg, cap, consumerKey, escalations, effectiveInput, declared } = args;
|
|
2144
|
+
const activeEscalations = escalations.filter((e) => {
|
|
2145
|
+
if (!e.if) return true;
|
|
2146
|
+
try {
|
|
2147
|
+
return e.if({ ...args.policyCtx, effectiveInput });
|
|
2148
|
+
} catch {
|
|
2149
|
+
return true;
|
|
2150
|
+
}
|
|
2151
|
+
});
|
|
2152
|
+
const effective = maxConfirmation(declared, activeEscalations.length > 0 ? "required" : "never");
|
|
2153
|
+
if (effective !== "required") return {};
|
|
2154
|
+
const summaryComposer = activeEscalations.find((e) => e.summary)?.summary;
|
|
2155
|
+
let summary;
|
|
2156
|
+
try {
|
|
2157
|
+
summary = summaryComposer ? summaryComposer(effectiveInput) : `${args.description} \u2014 input: ${JSON.stringify(effectiveInput)}`;
|
|
2158
|
+
} catch {
|
|
2159
|
+
summary = args.description;
|
|
2160
|
+
}
|
|
2161
|
+
summary = truncate(summary, 300);
|
|
2162
|
+
const digest = canonicalJson({
|
|
2163
|
+
surfaceId: internals.surfaceId,
|
|
2164
|
+
registrationId: reg.id,
|
|
2165
|
+
capabilityId: cap.capabilityId,
|
|
2166
|
+
consumerKey,
|
|
2167
|
+
effectiveInput,
|
|
2168
|
+
effect: args.effect
|
|
2169
|
+
});
|
|
2170
|
+
if (request.confirmationId) {
|
|
2171
|
+
const consumed = internals.confirmations.consume({
|
|
2172
|
+
confirmationId: request.confirmationId,
|
|
2173
|
+
digest,
|
|
2174
|
+
input: effectiveInput
|
|
2175
|
+
});
|
|
2176
|
+
if (consumed.ok) {
|
|
2177
|
+
return { evidence: { id: request.confirmationId, approvedAt: consumed.approvedAt } };
|
|
2178
|
+
}
|
|
2179
|
+
if (consumed.kind === "pending-again") {
|
|
2180
|
+
return { error: confirmationRequired(consumed.record, args.effect) };
|
|
2181
|
+
}
|
|
2182
|
+
return {
|
|
2183
|
+
error: {
|
|
2184
|
+
code: "CONFIRMATION_INVALID",
|
|
2185
|
+
message: consumed.reason === "denied" ? "The user declined this action. Do not retry; respect the decision." : consumed.reason === "expired" ? "The confirmation expired. Request a fresh confirmation." : consumed.reason === "consumed" ? "This confirmation was already used. Request a fresh confirmation if the action is still needed." : "The confirmation does not match this exact invocation.",
|
|
2186
|
+
retry: consumed.reason === "expired" ? "with-confirmation" : "no",
|
|
2187
|
+
details: { reason: consumed.reason }
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
const record = internals.confirmations.request({
|
|
2192
|
+
capabilityId: cap.capabilityId,
|
|
2193
|
+
registrationId: reg.id,
|
|
2194
|
+
consumerKey,
|
|
2195
|
+
effect: args.policyCtx.effect,
|
|
2196
|
+
input: effectiveInput,
|
|
2197
|
+
summary,
|
|
2198
|
+
digest
|
|
2199
|
+
});
|
|
2200
|
+
if (record === "overflow") {
|
|
2201
|
+
return { error: queueFull(1e3) };
|
|
2202
|
+
}
|
|
2203
|
+
return { error: confirmationRequired(record, args.effect) };
|
|
2204
|
+
}
|
|
2205
|
+
function confirmationRequired(record, effect) {
|
|
2206
|
+
return {
|
|
2207
|
+
code: "CONFIRMATION_REQUIRED",
|
|
2208
|
+
message: "User approval is required for this action. Wait for the user to resolve the confirmation, then retry with the confirmationId.",
|
|
2209
|
+
retry: "with-confirmation",
|
|
2210
|
+
details: {
|
|
2211
|
+
confirmationId: record.confirmationId,
|
|
2212
|
+
summary: record.summary,
|
|
2213
|
+
expiresAt: record.expiresAt,
|
|
2214
|
+
effect,
|
|
2215
|
+
origin: "client"
|
|
2216
|
+
}
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
function invalidInput(err) {
|
|
2220
|
+
const issues = err instanceof AgentSchemaError ? err.issues.map((i) => ({ path: i.path, message: i.message })) : [{ path: "", message: "Input failed schema validation" }];
|
|
2221
|
+
return {
|
|
2222
|
+
code: "INVALID_INPUT",
|
|
2223
|
+
message: "The input does not match the capability's schema. Fix the listed issues and retry.",
|
|
2224
|
+
retry: "with-changes",
|
|
2225
|
+
details: { issues }
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
function preconditionFailed(message, details) {
|
|
2229
|
+
return {
|
|
2230
|
+
code: "PRECONDITION_FAILED",
|
|
2231
|
+
message: truncate(message, 300),
|
|
2232
|
+
retry: "with-changes",
|
|
2233
|
+
...details ? { details } : {}
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
function bindingFailed() {
|
|
2237
|
+
return {
|
|
2238
|
+
code: "PRECONDITION_FAILED",
|
|
2239
|
+
message: "The UI-derived input binding could not be evaluated. Refresh the surface and check availability before retrying.",
|
|
2240
|
+
retry: "after-refresh",
|
|
2241
|
+
details: { reason: "binding-failed" }
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
function settleOutput(internals, value, schema) {
|
|
2245
|
+
if (value === void 0) return {};
|
|
2246
|
+
let parsed = value;
|
|
2247
|
+
if (schema) {
|
|
2248
|
+
try {
|
|
2249
|
+
parsed = schema.parse(value);
|
|
2250
|
+
} catch (err) {
|
|
2251
|
+
internals.devError("[agent-surface] output failed schema validation", err);
|
|
2252
|
+
return { error: executionFailed("output-invalid") };
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
if (!isJsonValue(parsed)) {
|
|
2256
|
+
if (internals.environment !== "production") {
|
|
2257
|
+
throw new DevDefectError(
|
|
2258
|
+
"capability output is not a JsonValue (functions, symbols, bigints, Dates, or cycles are defects \u2014 docs/03 \xA7serialization)"
|
|
2259
|
+
);
|
|
2260
|
+
}
|
|
2261
|
+
return { error: executionFailed("output-invalid") };
|
|
2262
|
+
}
|
|
2263
|
+
let serialized;
|
|
2264
|
+
try {
|
|
2265
|
+
serialized = JSON.stringify(parsed);
|
|
2266
|
+
} catch {
|
|
2267
|
+
if (internals.environment !== "production") {
|
|
2268
|
+
throw new DevDefectError("capability output cannot be serialized to JSON");
|
|
2269
|
+
}
|
|
2270
|
+
return { error: executionFailed("output-invalid") };
|
|
2271
|
+
}
|
|
2272
|
+
if (serialized.length > internals.limits.maxOutputBytes) {
|
|
2273
|
+
return { error: executionFailed("output-too-large") };
|
|
2274
|
+
}
|
|
2275
|
+
return { value: parsed };
|
|
2276
|
+
}
|
|
2277
|
+
function executeWithGuards(internals, reg, opts) {
|
|
2278
|
+
return new Promise((resolve) => {
|
|
2279
|
+
const controller = new AbortController();
|
|
2280
|
+
let settled = false;
|
|
2281
|
+
let timer;
|
|
2282
|
+
const entry = {
|
|
2283
|
+
onUnregister() {
|
|
2284
|
+
controller.abort();
|
|
2285
|
+
if (!opts.navigationSettlement) {
|
|
2286
|
+
finish({ ok: false, payload: unmounted("mid-flight") });
|
|
2287
|
+
}
|
|
2288
|
+
},
|
|
2289
|
+
onDispose() {
|
|
2290
|
+
controller.abort();
|
|
2291
|
+
finish({
|
|
2292
|
+
ok: false,
|
|
2293
|
+
payload: { code: "CANCELLED", message: "The registry was disposed.", retry: "no" }
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
const onExternalAbort = () => {
|
|
2298
|
+
controller.abort();
|
|
2299
|
+
finish({
|
|
2300
|
+
ok: false,
|
|
2301
|
+
payload: cancelled("The invocation was cancelled by the host.")
|
|
2302
|
+
});
|
|
2303
|
+
};
|
|
2304
|
+
const finish = (outcome) => {
|
|
2305
|
+
if (settled) return false;
|
|
2306
|
+
settled = true;
|
|
2307
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2308
|
+
reg.inFlight.delete(entry);
|
|
2309
|
+
opts.externalSignal?.removeEventListener("abort", onExternalAbort);
|
|
2310
|
+
resolve(outcome);
|
|
2311
|
+
return true;
|
|
2312
|
+
};
|
|
2313
|
+
const lateSettlement = () => {
|
|
2314
|
+
internals.recordAudit({
|
|
2315
|
+
type: "late-settlement",
|
|
2316
|
+
capabilityId: opts.capabilityId,
|
|
2317
|
+
registrationId: reg.id,
|
|
2318
|
+
invocationId: opts.invocationId
|
|
2319
|
+
});
|
|
2320
|
+
};
|
|
2321
|
+
const handlerError = (err) => {
|
|
2322
|
+
if (isAgentSurfaceError(err)) return { ok: false, payload: err.payload };
|
|
2323
|
+
if (opts.navigationSettlement && controller.signal.aborted) {
|
|
2324
|
+
return { ok: false, payload: cancelled("The navigation was abandoned after its owner unmounted.") };
|
|
2325
|
+
}
|
|
2326
|
+
internals.devError(`[agent-surface] handler failed for ${opts.capabilityId}`, err);
|
|
2327
|
+
return {
|
|
2328
|
+
ok: false,
|
|
2329
|
+
payload: executionFailed(opts.procedureErrors ? "transport" : "handler-error", {
|
|
2330
|
+
transient: opts.procedureErrors === true && typeof err === "object" && err !== null && err.transient === true
|
|
2331
|
+
})
|
|
2332
|
+
};
|
|
2333
|
+
};
|
|
2334
|
+
if (internals.disposed) {
|
|
2335
|
+
resolve({
|
|
2336
|
+
ok: false,
|
|
2337
|
+
payload: { code: "CANCELLED", message: "The registry was disposed.", retry: "no" }
|
|
2338
|
+
});
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
if (reg.status !== "active") {
|
|
2342
|
+
resolve({ ok: false, payload: unmounted("mid-flight") });
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
if (opts.externalSignal?.aborted) {
|
|
2346
|
+
resolve({
|
|
2347
|
+
ok: false,
|
|
2348
|
+
payload: cancelled("The invocation was cancelled by the host.")
|
|
2349
|
+
});
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
opts.externalSignal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
2353
|
+
timer = setTimeout(() => {
|
|
2354
|
+
controller.abort();
|
|
2355
|
+
finish({
|
|
2356
|
+
ok: false,
|
|
2357
|
+
payload: {
|
|
2358
|
+
code: "TIMEOUT",
|
|
2359
|
+
message: opts.idempotent ? "The capability timed out. It is idempotent; retrying with a new invocationId is safe." : "The capability timed out and side effects may or may not have occurred. Verify state with an observation before repeating.",
|
|
2360
|
+
retry: opts.idempotent ? "yes" : "no",
|
|
2361
|
+
details: { timeoutMs: opts.timeoutMs, idempotent: opts.idempotent }
|
|
2362
|
+
}
|
|
2363
|
+
});
|
|
2364
|
+
}, opts.timeoutMs);
|
|
2365
|
+
reg.inFlight.add(entry);
|
|
2366
|
+
let returned;
|
|
2367
|
+
try {
|
|
2368
|
+
returned = opts.run(controller.signal);
|
|
2369
|
+
} catch (err) {
|
|
2370
|
+
finish(handlerError(err));
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
if (returned !== null && (typeof returned === "object" || typeof returned === "function") && typeof returned.then === "function") {
|
|
2374
|
+
returned.then(
|
|
2375
|
+
(value) => {
|
|
2376
|
+
if (!finish({ ok: true, value })) lateSettlement();
|
|
2377
|
+
},
|
|
2378
|
+
(err) => {
|
|
2379
|
+
if (!finish(handlerError(err))) lateSettlement();
|
|
2380
|
+
}
|
|
2381
|
+
);
|
|
2382
|
+
} else {
|
|
2383
|
+
finish({ ok: true, value: returned });
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
async function acquireActionSlot(internals, reg) {
|
|
2388
|
+
if (!reg.actionQueue.running) {
|
|
2389
|
+
reg.actionQueue.running = true;
|
|
2390
|
+
return "ok";
|
|
2391
|
+
}
|
|
2392
|
+
if (reg.actionQueue.waiting.length >= internals.limits.actionQueueDepth) {
|
|
2393
|
+
return "overflow";
|
|
2394
|
+
}
|
|
2395
|
+
await new Promise((resolve) => reg.actionQueue.waiting.push(resolve));
|
|
2396
|
+
return "ok";
|
|
2397
|
+
}
|
|
2398
|
+
function releaseActionSlot(reg) {
|
|
2399
|
+
const next = reg.actionQueue.waiting.shift();
|
|
2400
|
+
if (next) next();
|
|
2401
|
+
else reg.actionQueue.running = false;
|
|
2402
|
+
}
|
|
2403
|
+
function acquireObservationSlot(internals, consumerKey) {
|
|
2404
|
+
const adm = internals.observationAdmission;
|
|
2405
|
+
const perCap = internals.limits.maxConcurrentObservationsPerConsumer;
|
|
2406
|
+
const totalCap = internals.limits.maxConcurrentObservationsTotal;
|
|
2407
|
+
const held = adm.perConsumer.get(consumerKey) ?? 0;
|
|
2408
|
+
if (held < perCap && adm.total < totalCap) {
|
|
2409
|
+
adm.perConsumer.set(consumerKey, held + 1);
|
|
2410
|
+
adm.total += 1;
|
|
2411
|
+
return Promise.resolve("ok");
|
|
2412
|
+
}
|
|
2413
|
+
let queued = 0;
|
|
2414
|
+
for (const waiter of adm.waiting) {
|
|
2415
|
+
if (waiter.consumerKey === consumerKey) queued += 1;
|
|
2416
|
+
}
|
|
2417
|
+
if (queued >= internals.limits.maxQueuedObservationsPerConsumer) {
|
|
2418
|
+
return Promise.resolve("overflow");
|
|
2419
|
+
}
|
|
2420
|
+
return new Promise((resolve) => {
|
|
2421
|
+
adm.waiting.push({
|
|
2422
|
+
consumerKey,
|
|
2423
|
+
admit: (admitted) => resolve(admitted ? "ok" : "cancelled")
|
|
2424
|
+
});
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
function releaseObservationSlot(internals, consumerKey) {
|
|
2428
|
+
const adm = internals.observationAdmission;
|
|
2429
|
+
adm.total = Math.max(0, adm.total - 1);
|
|
2430
|
+
const held = adm.perConsumer.get(consumerKey) ?? 0;
|
|
2431
|
+
if (held <= 1) adm.perConsumer.delete(consumerKey);
|
|
2432
|
+
else adm.perConsumer.set(consumerKey, held - 1);
|
|
2433
|
+
const perCap = internals.limits.maxConcurrentObservationsPerConsumer;
|
|
2434
|
+
const totalCap = internals.limits.maxConcurrentObservationsTotal;
|
|
2435
|
+
for (let i = 0; i < adm.waiting.length; i++) {
|
|
2436
|
+
const waiter = adm.waiting[i];
|
|
2437
|
+
if (!waiter) continue;
|
|
2438
|
+
const waiterHeld = adm.perConsumer.get(waiter.consumerKey) ?? 0;
|
|
2439
|
+
if (waiterHeld < perCap && adm.total < totalCap) {
|
|
2440
|
+
adm.waiting.splice(i, 1);
|
|
2441
|
+
adm.perConsumer.set(waiter.consumerKey, waiterHeld + 1);
|
|
2442
|
+
adm.total += 1;
|
|
2443
|
+
waiter.admit(true);
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
function drainObservationQueues(internals) {
|
|
2449
|
+
const adm = internals.observationAdmission;
|
|
2450
|
+
const waiting = adm.waiting.splice(0);
|
|
2451
|
+
for (const waiter of waiting) waiter.admit(false);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
// src/snapshot.ts
|
|
2455
|
+
var DEFAULT_CONSUMER2 = { id: "anonymous", kind: "embedded" };
|
|
2456
|
+
function matchesScope(type, scope) {
|
|
2457
|
+
if (!scope || scope.length === 0) return true;
|
|
2458
|
+
return scope.some((prefix) => type === prefix || type.startsWith(`${prefix}.`));
|
|
2459
|
+
}
|
|
2460
|
+
function sortRegistrations(regs) {
|
|
2461
|
+
return regs.sort((a, b) => {
|
|
2462
|
+
if (a.priority !== b.priority) return b.priority - a.priority;
|
|
2463
|
+
if (a.type !== b.type) return a.type < b.type ? -1 : 1;
|
|
2464
|
+
return a.instanceId < b.instanceId ? -1 : a.instanceId > b.instanceId ? 1 : 0;
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
function createSnapshot(internals, ctx) {
|
|
2468
|
+
const consumer = ctx?.consumer ?? DEFAULT_CONSUMER2;
|
|
2469
|
+
const includeUnavailable = ctx?.includeUnavailable ?? true;
|
|
2470
|
+
const host = internals.host();
|
|
2471
|
+
const regs = sortRegistrations(
|
|
2472
|
+
[...internals.registrations.values()].filter((r) => r.status === "active")
|
|
2473
|
+
);
|
|
2474
|
+
const components = [];
|
|
2475
|
+
const componentPriority = [];
|
|
2476
|
+
const procedures = [];
|
|
2477
|
+
for (const reg of regs) {
|
|
2478
|
+
const inScopeForComponents = matchesScope(reg.type, ctx?.scope);
|
|
2479
|
+
if (!reg.procedureOnly && inScopeForComponents) {
|
|
2480
|
+
const observations = [];
|
|
2481
|
+
const actions = [];
|
|
2482
|
+
let definedCount = 0;
|
|
2483
|
+
let hiddenCount = 0;
|
|
2484
|
+
for (const obs of reg.observations.values()) {
|
|
2485
|
+
definedCount += 1;
|
|
2486
|
+
const chain = policiesFor(internals, reg, obs);
|
|
2487
|
+
const policyCtx = buildPolicyContext(internals, reg, obs, consumer, host);
|
|
2488
|
+
const decision = evaluateDiscovery(chain, policyCtx);
|
|
2489
|
+
if (decision.decision === "hide") {
|
|
2490
|
+
hiddenCount += 1;
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
const availability = computeAvailability(internals, reg, obs);
|
|
2494
|
+
const available = availability.available && decision.decision === "expose";
|
|
2495
|
+
const reason = decision.decision === "disable" ? decision.reason : availability.reason;
|
|
2496
|
+
if (!available && !includeUnavailable) continue;
|
|
2497
|
+
observations.push({
|
|
2498
|
+
capabilityId: obs.capabilityId,
|
|
2499
|
+
name: obs.name,
|
|
2500
|
+
description: obs.description,
|
|
2501
|
+
outputSchema: obs.jsonSchema,
|
|
2502
|
+
available,
|
|
2503
|
+
...available ? {} : { unavailableReason: reason },
|
|
2504
|
+
...obs.meta ? { meta: obs.meta } : {}
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
for (const act of reg.actions.values()) {
|
|
2508
|
+
definedCount += 1;
|
|
2509
|
+
const chain = policiesFor(internals, reg, act);
|
|
2510
|
+
const policyCtx = buildPolicyContext(internals, reg, act, consumer, host);
|
|
2511
|
+
const decision = evaluateDiscovery(chain, policyCtx);
|
|
2512
|
+
if (decision.decision === "hide") {
|
|
2513
|
+
hiddenCount += 1;
|
|
2514
|
+
continue;
|
|
2515
|
+
}
|
|
2516
|
+
const availability = computeAvailability(internals, reg, act);
|
|
2517
|
+
const available = availability.available && decision.decision === "expose";
|
|
2518
|
+
const reason = decision.decision === "disable" ? decision.reason : availability.reason;
|
|
2519
|
+
if (!available && !includeUnavailable) continue;
|
|
2520
|
+
actions.push({
|
|
2521
|
+
capabilityId: act.capabilityId,
|
|
2522
|
+
name: act.name,
|
|
2523
|
+
description: act.description,
|
|
2524
|
+
inputSchema: act.inputJsonSchema,
|
|
2525
|
+
...act.outputJsonSchema ? { outputSchema: act.outputJsonSchema } : {},
|
|
2526
|
+
effect: act.effect,
|
|
2527
|
+
idempotent: act.idempotent,
|
|
2528
|
+
reversible: act.reversible,
|
|
2529
|
+
confirmation: act.confirmation,
|
|
2530
|
+
available,
|
|
2531
|
+
...available ? {} : { unavailableReason: reason },
|
|
2532
|
+
...act.meta ? { meta: act.meta } : {}
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
const allHidden = definedCount > 0 && hiddenCount === definedCount;
|
|
2536
|
+
if (!allHidden) {
|
|
2537
|
+
components.push({
|
|
2538
|
+
type: reg.type,
|
|
2539
|
+
instanceId: reg.instanceId,
|
|
2540
|
+
registrationId: reg.id,
|
|
2541
|
+
description: reg.description,
|
|
2542
|
+
...reg.parent ? { parent: reg.parent } : {},
|
|
2543
|
+
...reg.meta ? { meta: reg.meta } : {},
|
|
2544
|
+
observations,
|
|
2545
|
+
actions
|
|
2546
|
+
});
|
|
2547
|
+
componentPriority.push(reg.priority);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
for (const proc of reg.procedures) {
|
|
2551
|
+
const scopeMatch = proc.contextLink ? matchesScope(proc.contextLink.type, ctx?.scope) : matchesScope(proc.path, ctx?.scope);
|
|
2552
|
+
if (!scopeMatch) continue;
|
|
2553
|
+
const chain = policiesFor(internals, reg, proc);
|
|
2554
|
+
const policyCtx = buildPolicyContext(internals, reg, proc, consumer, host);
|
|
2555
|
+
const decision = evaluateDiscovery(chain, policyCtx);
|
|
2556
|
+
if (decision.decision === "hide") continue;
|
|
2557
|
+
const availability = computeAvailability(internals, reg, proc);
|
|
2558
|
+
const available = availability.available && decision.decision === "expose";
|
|
2559
|
+
const reason = decision.decision === "disable" ? decision.reason : availability.reason;
|
|
2560
|
+
if (!available && !includeUnavailable) continue;
|
|
2561
|
+
let description = proc.baseDescription;
|
|
2562
|
+
const describe = proc.binding.config.describe;
|
|
2563
|
+
if (describe) {
|
|
2564
|
+
try {
|
|
2565
|
+
const contextual = describe();
|
|
2566
|
+
if (contextual) description = `${description} ${contextual}`.trim();
|
|
2567
|
+
} catch {
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
procedures.push({
|
|
2571
|
+
procedureId: proc.capabilityId,
|
|
2572
|
+
description,
|
|
2573
|
+
inputSchema: proc.reducedInputSchema,
|
|
2574
|
+
...proc.outputJsonSchema ? { outputSchema: proc.outputJsonSchema } : {},
|
|
2575
|
+
effect: proc.effect,
|
|
2576
|
+
confirmation: proc.confirmationFloor,
|
|
2577
|
+
available,
|
|
2578
|
+
...available ? {} : { unavailableReason: reason },
|
|
2579
|
+
boundFields: proc.boundKeys.map((path) => ({
|
|
2580
|
+
path,
|
|
2581
|
+
locked: proc.lockedKeys.includes(path),
|
|
2582
|
+
source: "ui-state"
|
|
2583
|
+
})),
|
|
2584
|
+
registrationId: reg.id,
|
|
2585
|
+
...proc.contextLink ? { context: proc.contextLink } : {},
|
|
2586
|
+
...proc.meta ? { meta: proc.meta } : {}
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
let dropped = 0;
|
|
2591
|
+
const budget = ctx?.budget;
|
|
2592
|
+
if (budget?.maxComponents !== void 0 && components.length > budget.maxComponents) {
|
|
2593
|
+
dropped += components.length - budget.maxComponents;
|
|
2594
|
+
dropLowestPriority(components, componentPriority, components.length - budget.maxComponents);
|
|
2595
|
+
}
|
|
2596
|
+
if (budget?.maxBytes !== void 0) {
|
|
2597
|
+
while (components.length > 0 && byteLength(components) > budget.maxBytes) {
|
|
2598
|
+
dropLowestPriority(components, componentPriority, 1);
|
|
2599
|
+
dropped += 1;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
const snapshot = {
|
|
2603
|
+
surfaceId: internals.surfaceId,
|
|
2604
|
+
surfaceVersion: String(internals.version),
|
|
2605
|
+
capturedAt: new Date(internals.now()).toISOString(),
|
|
2606
|
+
...internals.routeFn?.() ? { route: internals.routeFn() } : {},
|
|
2607
|
+
components,
|
|
2608
|
+
procedures,
|
|
2609
|
+
...dropped > 0 ? { truncated: { droppedComponents: dropped } } : {}
|
|
2610
|
+
};
|
|
2611
|
+
return deepFreeze(snapshot);
|
|
2612
|
+
}
|
|
2613
|
+
function dropLowestPriority(components, priorities, count) {
|
|
2614
|
+
for (let n = 0; n < count && components.length > 0; n++) {
|
|
2615
|
+
let lowestIndex = 0;
|
|
2616
|
+
for (let i = 1; i < priorities.length; i++) {
|
|
2617
|
+
if ((priorities[i] ?? 0) <= (priorities[lowestIndex] ?? 0)) lowestIndex = i;
|
|
2618
|
+
}
|
|
2619
|
+
components.splice(lowestIndex, 1);
|
|
2620
|
+
priorities.splice(lowestIndex, 1);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// src/registry.ts
|
|
2625
|
+
function createAgentSurfaceRegistry(options) {
|
|
2626
|
+
const environment2 = options?.environment ?? "production";
|
|
2627
|
+
const limits = { ...DEFAULT_LIMITS, ...options?.limits ?? {} };
|
|
2628
|
+
const now = options?.now ?? (() => Date.now());
|
|
2629
|
+
const auditSink = options?.audit ?? (environment2 === "development" ? combineSinks(memoryAuditSink(), consoleAuditSink()) : memoryAuditSink());
|
|
2630
|
+
let surfaceChangedScheduled = false;
|
|
2631
|
+
const dispatcher = new EventDispatcher((err) => {
|
|
2632
|
+
if (environment2 === "development") {
|
|
2633
|
+
console.error("[agent-surface] event listener threw", err);
|
|
2634
|
+
}
|
|
2635
|
+
});
|
|
2636
|
+
const internals = {
|
|
2637
|
+
environment: environment2,
|
|
2638
|
+
limits,
|
|
2639
|
+
surfaceId: `srf_${randomBase62(22)}`,
|
|
2640
|
+
version: 0,
|
|
2641
|
+
registrations: /* @__PURE__ */ new Map(),
|
|
2642
|
+
byKey: /* @__PURE__ */ new Map(),
|
|
2643
|
+
tombstones: /* @__PURE__ */ new Map(),
|
|
2644
|
+
dedupe: /* @__PURE__ */ new Map(),
|
|
2645
|
+
observationAdmission: { total: 0, perConsumer: /* @__PURE__ */ new Map(), waiting: [] },
|
|
2646
|
+
dispatcher,
|
|
2647
|
+
confirmations: void 0,
|
|
2648
|
+
// set below
|
|
2649
|
+
executor: void 0,
|
|
2650
|
+
disposed: false,
|
|
2651
|
+
registryPolicies: [...options?.policies ?? []],
|
|
2652
|
+
auditSink,
|
|
2653
|
+
contextFn: options?.context,
|
|
2654
|
+
routeFn: options?.route,
|
|
2655
|
+
now,
|
|
2656
|
+
bumpVersion() {
|
|
2657
|
+
internals.version += 1;
|
|
2658
|
+
if (!surfaceChangedScheduled) {
|
|
2659
|
+
surfaceChangedScheduled = true;
|
|
2660
|
+
queueMicrotask(() => {
|
|
2661
|
+
surfaceChangedScheduled = false;
|
|
2662
|
+
if (internals.disposed) return;
|
|
2663
|
+
internals.emit({ type: "surface-changed", surfaceVersion: String(internals.version) });
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
},
|
|
2667
|
+
emit(event) {
|
|
2668
|
+
dispatcher.emit(event);
|
|
2669
|
+
},
|
|
2670
|
+
recordAudit(event) {
|
|
2671
|
+
safeRecord(auditSink, { at: new Date(now()).toISOString(), ...event });
|
|
2672
|
+
},
|
|
2673
|
+
host() {
|
|
2674
|
+
try {
|
|
2675
|
+
return internals.contextFn?.() ?? {};
|
|
2676
|
+
} catch (err) {
|
|
2677
|
+
internals.devWarn("[agent-surface] RegistryOptions.context() threw", err);
|
|
2678
|
+
return {};
|
|
2679
|
+
}
|
|
2680
|
+
},
|
|
2681
|
+
devWarn(...args) {
|
|
2682
|
+
if (environment2 === "development") {
|
|
2683
|
+
console.warn(...args);
|
|
2684
|
+
}
|
|
2685
|
+
},
|
|
2686
|
+
devError(...args) {
|
|
2687
|
+
if (environment2 === "development") {
|
|
2688
|
+
console.error(...args);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
};
|
|
2692
|
+
internals.confirmations = new ConfirmationStore({
|
|
2693
|
+
ttlMs: limits.confirmationTtlMs,
|
|
2694
|
+
maxPending: limits.maxPendingConfirmations,
|
|
2695
|
+
now,
|
|
2696
|
+
emit: (event) => internals.emit(event),
|
|
2697
|
+
audit: (event) => internals.recordAudit(event)
|
|
2698
|
+
});
|
|
2699
|
+
const onDuplicateInstance = options?.onDuplicateInstance ?? "reject";
|
|
2700
|
+
const duplicateSuffixPolicy = options?.duplicateSuffixPolicy ?? "warn";
|
|
2701
|
+
function deadHandle() {
|
|
2702
|
+
const id = nextRegistrationId(() => randomBase62(6));
|
|
2703
|
+
return {
|
|
2704
|
+
registrationId: id,
|
|
2705
|
+
status: "rejected",
|
|
2706
|
+
update() {
|
|
2707
|
+
internals.devWarn("[agent-surface] update() called on a rejected registration handle");
|
|
2708
|
+
},
|
|
2709
|
+
invalidate() {
|
|
2710
|
+
internals.devWarn("[agent-surface] invalidate() called on a rejected registration handle");
|
|
2711
|
+
},
|
|
2712
|
+
unregister() {
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
function unregisterInternal(reg) {
|
|
2717
|
+
if (reg.status !== "active") return;
|
|
2718
|
+
reg.status = "unregistered";
|
|
2719
|
+
internals.registrations.delete(reg.id);
|
|
2720
|
+
if (internals.byKey.get(reg.key) === reg.id) internals.byKey.delete(reg.key);
|
|
2721
|
+
addTombstone(internals, reg);
|
|
2722
|
+
for (const entry of [...reg.inFlight]) {
|
|
2723
|
+
entry.onUnregister();
|
|
2724
|
+
}
|
|
2725
|
+
internals.bumpVersion();
|
|
2726
|
+
internals.emit({
|
|
2727
|
+
type: "component-unregistered",
|
|
2728
|
+
registrationId: reg.id,
|
|
2729
|
+
componentType: reg.type,
|
|
2730
|
+
instanceId: reg.instanceId
|
|
2731
|
+
});
|
|
2732
|
+
internals.recordAudit({
|
|
2733
|
+
type: "unregistration",
|
|
2734
|
+
registrationId: reg.id,
|
|
2735
|
+
capabilityId: void 0
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
function checkSuffixCollisions(def) {
|
|
2739
|
+
if (duplicateSuffixPolicy === "off") return;
|
|
2740
|
+
const paths = internals.executor?.paths;
|
|
2741
|
+
if (!paths || paths.length === 0) return;
|
|
2742
|
+
const names = [
|
|
2743
|
+
...Object.keys(def.observations ?? {}),
|
|
2744
|
+
...Object.keys(def.actions ?? {})
|
|
2745
|
+
];
|
|
2746
|
+
for (const name of names) {
|
|
2747
|
+
const candidatePath = `${def.type}.${name}`;
|
|
2748
|
+
if (paths.includes(candidatePath)) {
|
|
2749
|
+
const viewCapabilityId = formatViewCapabilityId(def.type, name);
|
|
2750
|
+
const domainProcedureId = `domain:${candidatePath}`;
|
|
2751
|
+
if (duplicateSuffixPolicy === "error") {
|
|
2752
|
+
throw new AgentSurfaceDefinitionError(
|
|
2753
|
+
"PLANE_VIOLATION",
|
|
2754
|
+
`view capability "${viewCapabilityId}" collides with domain procedure "${domainProcedureId}" \u2014 reference the procedure instead of redefining it (docs/05)`
|
|
2755
|
+
);
|
|
2756
|
+
}
|
|
2757
|
+
internals.devWarn(
|
|
2758
|
+
`[agent-surface] suspicious suffix collision: "${viewCapabilityId}" vs "${domainProcedureId}"`
|
|
2759
|
+
);
|
|
2760
|
+
internals.emit({ type: "collision-suspected", viewCapabilityId, domainProcedureId });
|
|
2761
|
+
internals.recordAudit({
|
|
2762
|
+
type: "collision-suspected",
|
|
2763
|
+
capabilityId: viewCapabilityId
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
const registry = {
|
|
2769
|
+
surfaceId: internals.surfaceId,
|
|
2770
|
+
register(definition) {
|
|
2771
|
+
if (internals.disposed) throw new Error("register() called on a disposed registry");
|
|
2772
|
+
validateComponentDefinition(definition, limits, {
|
|
2773
|
+
hasProcedureExecutor: internals.executor !== void 0
|
|
2774
|
+
});
|
|
2775
|
+
checkSuffixCollisions(definition);
|
|
2776
|
+
const instanceId = definition.instanceId ?? "default";
|
|
2777
|
+
if (options?.onRegister) {
|
|
2778
|
+
let verdict = "accept";
|
|
2779
|
+
try {
|
|
2780
|
+
verdict = options.onRegister({
|
|
2781
|
+
definition,
|
|
2782
|
+
...environment2 === "development" ? { stack: new Error().stack } : {}
|
|
2783
|
+
});
|
|
2784
|
+
} catch (err) {
|
|
2785
|
+
internals.devError("[agent-surface] onRegister guard threw; rejecting", err);
|
|
2786
|
+
verdict = "reject";
|
|
2787
|
+
}
|
|
2788
|
+
if (verdict === "reject") {
|
|
2789
|
+
internals.emit({
|
|
2790
|
+
type: "component-rejected",
|
|
2791
|
+
componentType: definition.type,
|
|
2792
|
+
instanceId,
|
|
2793
|
+
reason: "guard"
|
|
2794
|
+
});
|
|
2795
|
+
internals.recordAudit({ type: "registration-rejected" });
|
|
2796
|
+
internals.devError(
|
|
2797
|
+
`[agent-surface] registration of "${definition.type}" (${instanceId}) rejected by guard`
|
|
2798
|
+
);
|
|
2799
|
+
return deadHandle();
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
const key = componentKey(definition.type, instanceId);
|
|
2803
|
+
const existingId = internals.byKey.get(key);
|
|
2804
|
+
if (existingId !== void 0) {
|
|
2805
|
+
if (onDuplicateInstance === "reject") {
|
|
2806
|
+
internals.emit({
|
|
2807
|
+
type: "component-rejected",
|
|
2808
|
+
componentType: definition.type,
|
|
2809
|
+
instanceId,
|
|
2810
|
+
reason: "duplicate"
|
|
2811
|
+
});
|
|
2812
|
+
internals.recordAudit({ type: "registration-rejected" });
|
|
2813
|
+
internals.devError(
|
|
2814
|
+
`[agent-surface] duplicate registration of "${definition.type}" (${instanceId}); first-wins (onDuplicateInstance: "reject")`
|
|
2815
|
+
);
|
|
2816
|
+
return deadHandle();
|
|
2817
|
+
}
|
|
2818
|
+
const existing = internals.registrations.get(existingId);
|
|
2819
|
+
if (existing) unregisterInternal(existing);
|
|
2820
|
+
}
|
|
2821
|
+
const reg = normalizeRegistration(definition, nextRegistrationId(() => randomBase62(6)));
|
|
2822
|
+
internals.registrations.set(reg.id, reg);
|
|
2823
|
+
internals.byKey.set(reg.key, reg.id);
|
|
2824
|
+
internals.bumpVersion();
|
|
2825
|
+
internals.emit({
|
|
2826
|
+
type: "component-registered",
|
|
2827
|
+
registrationId: reg.id,
|
|
2828
|
+
componentType: reg.type,
|
|
2829
|
+
instanceId: reg.instanceId
|
|
2830
|
+
});
|
|
2831
|
+
internals.recordAudit({ type: "registration", registrationId: reg.id });
|
|
2832
|
+
return {
|
|
2833
|
+
get registrationId() {
|
|
2834
|
+
return reg.id;
|
|
2835
|
+
},
|
|
2836
|
+
get status() {
|
|
2837
|
+
return reg.status === "active" ? "active" : "unregistered";
|
|
2838
|
+
},
|
|
2839
|
+
update(patch) {
|
|
2840
|
+
if (reg.status !== "active") {
|
|
2841
|
+
internals.devWarn(
|
|
2842
|
+
`[agent-surface] update() called after unregistration of "${reg.type}"`
|
|
2843
|
+
);
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
let changed = false;
|
|
2847
|
+
if (patch.enabled !== void 0 && patch.enabled !== reg.enabled) {
|
|
2848
|
+
reg.enabled = patch.enabled;
|
|
2849
|
+
changed = true;
|
|
2850
|
+
}
|
|
2851
|
+
if (patch.availability) {
|
|
2852
|
+
for (const [name, value] of Object.entries(patch.availability)) {
|
|
2853
|
+
const prev = reg.availabilityOverrides.get(name);
|
|
2854
|
+
if (!prev || prev.available !== value.available || prev.reason !== value.reason) {
|
|
2855
|
+
reg.availabilityOverrides.set(name, {
|
|
2856
|
+
available: value.available,
|
|
2857
|
+
...value.reason !== void 0 ? { reason: value.reason } : {}
|
|
2858
|
+
});
|
|
2859
|
+
changed = true;
|
|
2860
|
+
const capabilityId = reg.observations.get(name)?.capabilityId ?? reg.actions.get(name)?.capabilityId ?? reg.procedures.find((p) => p.path === name)?.capabilityId ?? name;
|
|
2861
|
+
internals.emit({
|
|
2862
|
+
type: "availability-changed",
|
|
2863
|
+
registrationId: reg.id,
|
|
2864
|
+
capabilityId,
|
|
2865
|
+
available: value.available
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
if (changed) internals.bumpVersion();
|
|
2871
|
+
},
|
|
2872
|
+
invalidate() {
|
|
2873
|
+
if (reg.status !== "active") return;
|
|
2874
|
+
internals.bumpVersion();
|
|
2875
|
+
},
|
|
2876
|
+
unregister() {
|
|
2877
|
+
unregisterInternal(reg);
|
|
2878
|
+
}
|
|
2879
|
+
};
|
|
2880
|
+
},
|
|
2881
|
+
snapshot(context) {
|
|
2882
|
+
if (internals.disposed) throw new Error("snapshot() called on a disposed registry");
|
|
2883
|
+
return createSnapshot(internals, context);
|
|
2884
|
+
},
|
|
2885
|
+
invoke(request, invokeOptions) {
|
|
2886
|
+
return performInvoke(internals, request, invokeOptions);
|
|
2887
|
+
},
|
|
2888
|
+
subscribe(listener) {
|
|
2889
|
+
return dispatcher.subscribe(listener);
|
|
2890
|
+
},
|
|
2891
|
+
confirmations: internals.confirmations.controller(),
|
|
2892
|
+
setProcedureExecutor(executor) {
|
|
2893
|
+
internals.executor = executor;
|
|
2894
|
+
},
|
|
2895
|
+
getVersion() {
|
|
2896
|
+
return String(internals.version);
|
|
2897
|
+
},
|
|
2898
|
+
dispose() {
|
|
2899
|
+
if (internals.disposed) return;
|
|
2900
|
+
for (const reg of [...internals.registrations.values()]) {
|
|
2901
|
+
for (const entry of [...reg.inFlight]) {
|
|
2902
|
+
entry.onDispose();
|
|
2903
|
+
}
|
|
2904
|
+
reg.status = "unregistered";
|
|
2905
|
+
}
|
|
2906
|
+
drainObservationQueues(internals);
|
|
2907
|
+
internals.registrations.clear();
|
|
2908
|
+
internals.byKey.clear();
|
|
2909
|
+
internals.confirmations.disposeAll();
|
|
2910
|
+
internals.disposed = true;
|
|
2911
|
+
dispatcher.clear();
|
|
2912
|
+
}
|
|
2913
|
+
};
|
|
2914
|
+
return registry;
|
|
2915
|
+
}
|
|
2916
|
+
function combineSinks(...sinks) {
|
|
2917
|
+
return {
|
|
2918
|
+
record(event) {
|
|
2919
|
+
for (const sink of sinks) safeRecord(sink, event);
|
|
2920
|
+
}
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
// src/toolset.ts
|
|
2925
|
+
var EMPTY_INPUT_SCHEMA = {
|
|
2926
|
+
type: "object",
|
|
2927
|
+
properties: {},
|
|
2928
|
+
additionalProperties: false
|
|
2929
|
+
};
|
|
2930
|
+
function describePrefix(plane, effect, confirmation, available, unavailableReason) {
|
|
2931
|
+
const parts = [plane, effect];
|
|
2932
|
+
if (confirmation === "required") parts.push("requires confirmation");
|
|
2933
|
+
let prefix = `[${parts.join(" \xB7 ")}]`;
|
|
2934
|
+
if (!available) {
|
|
2935
|
+
prefix += ` [currently unavailable${unavailableReason ? `: ${unavailableReason}` : ""}]`;
|
|
2936
|
+
}
|
|
2937
|
+
return prefix;
|
|
2938
|
+
}
|
|
2939
|
+
function createAgentToolset(registry, options) {
|
|
2940
|
+
const mode = options.mode ?? "direct";
|
|
2941
|
+
if (options.confirmations === void 0 && options.topology === void 0) {
|
|
2942
|
+
throw new Error(
|
|
2943
|
+
"createAgentToolset: declare a topology ('embedded' | 'remote') or an explicit confirmations mode ('wait' | 'two-phase'). Embedded loops default to 'wait', remote loops to 'two-phase' (docs/09 \xA7confirmation-topology)."
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2946
|
+
const confirmationsMode = options.confirmations ?? (options.topology === "remote" ? "two-phase" : "wait");
|
|
2947
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2948
|
+
const pendingWaits = /* @__PURE__ */ new Set();
|
|
2949
|
+
let disposed = false;
|
|
2950
|
+
let cachedVersion;
|
|
2951
|
+
let cachedTools;
|
|
2952
|
+
let cachedSignature;
|
|
2953
|
+
async function waitForConfirmation(confirmationId) {
|
|
2954
|
+
if (disposed) return;
|
|
2955
|
+
const controller = new AbortController();
|
|
2956
|
+
pendingWaits.add(controller);
|
|
2957
|
+
try {
|
|
2958
|
+
await registry.confirmations.waitFor(confirmationId, { signal: controller.signal });
|
|
2959
|
+
} finally {
|
|
2960
|
+
pendingWaits.delete(controller);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
async function invokeThroughSurface(entry, input, toolCallId) {
|
|
2964
|
+
const invocationId = toolCallId ?? `inv_${randomBase62(12)}`;
|
|
2965
|
+
const base = {
|
|
2966
|
+
invocationId,
|
|
2967
|
+
capabilityId: entry.capabilityId,
|
|
2968
|
+
...entry.instanceId !== void 0 ? { instanceId: entry.instanceId } : {},
|
|
2969
|
+
registrationId: entry.registrationId,
|
|
2970
|
+
surfaceVersion: entry.surfaceVersion,
|
|
2971
|
+
...input !== void 0 ? { input } : {}
|
|
2972
|
+
};
|
|
2973
|
+
let result = await registry.invoke(base, { consumer: options.consumer });
|
|
2974
|
+
if (confirmationsMode === "wait" && result.status === "error" && result.error.code === "CONFIRMATION_REQUIRED") {
|
|
2975
|
+
const confirmationId = result.error.details?.confirmationId;
|
|
2976
|
+
if (typeof confirmationId === "string") {
|
|
2977
|
+
await waitForConfirmation(confirmationId);
|
|
2978
|
+
if (disposed) return result;
|
|
2979
|
+
result = await registry.invoke(
|
|
2980
|
+
{ ...base, confirmationId },
|
|
2981
|
+
{ consumer: options.consumer }
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
return result;
|
|
2986
|
+
}
|
|
2987
|
+
function buildDirectTools() {
|
|
2988
|
+
const snapshot = registry.snapshot({
|
|
2989
|
+
consumer: options.consumer,
|
|
2990
|
+
...options.scope ? { scope: options.scope } : {},
|
|
2991
|
+
includeUnavailable: true
|
|
2992
|
+
});
|
|
2993
|
+
const tools = [];
|
|
2994
|
+
const push = (capabilityId, kind, registrationId, instanceId, description, inputSchema, nameSuffix) => {
|
|
2995
|
+
const entry = {
|
|
2996
|
+
capabilityId,
|
|
2997
|
+
registrationId,
|
|
2998
|
+
...instanceId !== void 0 ? { instanceId } : {},
|
|
2999
|
+
surfaceVersion: snapshot.surfaceVersion,
|
|
3000
|
+
kind
|
|
3001
|
+
};
|
|
3002
|
+
tools.push({
|
|
3003
|
+
// Providers require unique tool names: multi-instance capabilities
|
|
3004
|
+
// are disambiguated with an `_at_<instance>` suffix (docs/09).
|
|
3005
|
+
name: encodeWireNameForInstance(capabilityId, nameSuffix ?? instanceId),
|
|
3006
|
+
description,
|
|
3007
|
+
inputSchema,
|
|
3008
|
+
execute: (input, call) => invokeThroughSurface(entry, input, call.toolCallId)
|
|
3009
|
+
});
|
|
3010
|
+
};
|
|
3011
|
+
for (const component of snapshot.components) {
|
|
3012
|
+
const multiInstance = snapshot.components.filter((c) => c.type === component.type).length > 1;
|
|
3013
|
+
const instanceId = multiInstance ? component.instanceId : void 0;
|
|
3014
|
+
for (const obs of component.observations) {
|
|
3015
|
+
push(
|
|
3016
|
+
obs.capabilityId,
|
|
3017
|
+
"observation",
|
|
3018
|
+
component.registrationId,
|
|
3019
|
+
instanceId,
|
|
3020
|
+
`${describePrefix("view", "read", "never", obs.available, obs.unavailableReason)} ${obs.description}`,
|
|
3021
|
+
EMPTY_INPUT_SCHEMA
|
|
3022
|
+
);
|
|
3023
|
+
}
|
|
3024
|
+
for (const act of component.actions) {
|
|
3025
|
+
push(
|
|
3026
|
+
act.capabilityId,
|
|
3027
|
+
"action",
|
|
3028
|
+
component.registrationId,
|
|
3029
|
+
instanceId,
|
|
3030
|
+
`${describePrefix("view", act.effect, act.confirmation, act.available, act.unavailableReason)} ${act.description}`,
|
|
3031
|
+
act.inputSchema
|
|
3032
|
+
);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
const procedureCounts = /* @__PURE__ */ new Map();
|
|
3036
|
+
for (const proc of snapshot.procedures) {
|
|
3037
|
+
procedureCounts.set(proc.procedureId, (procedureCounts.get(proc.procedureId) ?? 0) + 1);
|
|
3038
|
+
}
|
|
3039
|
+
for (const proc of snapshot.procedures) {
|
|
3040
|
+
const needsSuffix = (procedureCounts.get(proc.procedureId) ?? 0) > 1;
|
|
3041
|
+
push(
|
|
3042
|
+
proc.procedureId,
|
|
3043
|
+
"procedure",
|
|
3044
|
+
proc.registrationId,
|
|
3045
|
+
void 0,
|
|
3046
|
+
`${describePrefix("domain", proc.effect, proc.confirmation, proc.available, proc.unavailableReason)} ${proc.description}`,
|
|
3047
|
+
proc.inputSchema,
|
|
3048
|
+
needsSuffix ? proc.context?.instanceId ?? proc.registrationId.replace(/[^A-Za-z0-9_-]/g, "") : void 0
|
|
3049
|
+
);
|
|
3050
|
+
}
|
|
3051
|
+
return tools;
|
|
3052
|
+
}
|
|
3053
|
+
function buildMetaTools() {
|
|
3054
|
+
const snapshotFor = () => registry.snapshot({
|
|
3055
|
+
consumer: options.consumer,
|
|
3056
|
+
...options.scope ? { scope: options.scope } : {}
|
|
3057
|
+
});
|
|
3058
|
+
return [
|
|
3059
|
+
{
|
|
3060
|
+
name: "surface_discover",
|
|
3061
|
+
description: "[meta] Discover the current agent surface: components, capabilities, procedures, availability, schemas.",
|
|
3062
|
+
inputSchema: {
|
|
3063
|
+
type: "object",
|
|
3064
|
+
properties: { scope: { type: "array", items: { type: "string" } } },
|
|
3065
|
+
additionalProperties: false
|
|
3066
|
+
},
|
|
3067
|
+
async execute(input) {
|
|
3068
|
+
const scope = input?.scope;
|
|
3069
|
+
const snapshot = registry.snapshot({
|
|
3070
|
+
consumer: options.consumer,
|
|
3071
|
+
...scope ? { scope } : options.scope ? { scope: options.scope } : {}
|
|
3072
|
+
});
|
|
3073
|
+
return {
|
|
3074
|
+
status: "ok",
|
|
3075
|
+
invocationId: `inv_${randomBase62(12)}`,
|
|
3076
|
+
capabilityId: "meta:surface.discover",
|
|
3077
|
+
output: JSON.parse(JSON.stringify(snapshot)),
|
|
3078
|
+
surfaceVersion: snapshot.surfaceVersion
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
},
|
|
3082
|
+
{
|
|
3083
|
+
name: "surface_read",
|
|
3084
|
+
description: "[meta] Invoke an observation by capabilityId and return its output.",
|
|
3085
|
+
inputSchema: {
|
|
3086
|
+
type: "object",
|
|
3087
|
+
properties: {
|
|
3088
|
+
capabilityId: { type: "string" },
|
|
3089
|
+
instanceId: { type: "string" }
|
|
3090
|
+
},
|
|
3091
|
+
required: ["capabilityId"],
|
|
3092
|
+
additionalProperties: false
|
|
3093
|
+
},
|
|
3094
|
+
async execute(input, call) {
|
|
3095
|
+
const req = input;
|
|
3096
|
+
const snapshot = snapshotFor();
|
|
3097
|
+
return invokeThroughSurface(
|
|
3098
|
+
{
|
|
3099
|
+
capabilityId: req.capabilityId,
|
|
3100
|
+
registrationId: findRegistrationId(snapshot, req.capabilityId, req.instanceId) ?? "",
|
|
3101
|
+
...req.instanceId !== void 0 ? { instanceId: req.instanceId } : {},
|
|
3102
|
+
surfaceVersion: snapshot.surfaceVersion,
|
|
3103
|
+
kind: "observation"
|
|
3104
|
+
},
|
|
3105
|
+
void 0,
|
|
3106
|
+
call.toolCallId
|
|
3107
|
+
);
|
|
3108
|
+
}
|
|
3109
|
+
},
|
|
3110
|
+
{
|
|
3111
|
+
name: "surface_act",
|
|
3112
|
+
description: "[meta] Invoke an action or procedure by capabilityId.",
|
|
3113
|
+
inputSchema: {
|
|
3114
|
+
type: "object",
|
|
3115
|
+
properties: {
|
|
3116
|
+
capabilityId: { type: "string" },
|
|
3117
|
+
instanceId: { type: "string" },
|
|
3118
|
+
input: {},
|
|
3119
|
+
invocationId: { type: "string" },
|
|
3120
|
+
confirmationId: { type: "string" }
|
|
3121
|
+
},
|
|
3122
|
+
required: ["capabilityId"],
|
|
3123
|
+
additionalProperties: false
|
|
3124
|
+
},
|
|
3125
|
+
async execute(input, call) {
|
|
3126
|
+
const req = input;
|
|
3127
|
+
const snapshot = snapshotFor();
|
|
3128
|
+
const invocationId = req.invocationId ?? call.toolCallId ?? `inv_${randomBase62(12)}`;
|
|
3129
|
+
let result = await registry.invoke(
|
|
3130
|
+
{
|
|
3131
|
+
invocationId,
|
|
3132
|
+
capabilityId: req.capabilityId,
|
|
3133
|
+
...req.instanceId !== void 0 ? { instanceId: req.instanceId } : {},
|
|
3134
|
+
...findRegistrationId(snapshot, req.capabilityId, req.instanceId) ? { registrationId: findRegistrationId(snapshot, req.capabilityId, req.instanceId) } : {},
|
|
3135
|
+
surfaceVersion: snapshot.surfaceVersion,
|
|
3136
|
+
...req.input !== void 0 ? { input: req.input } : {},
|
|
3137
|
+
...req.confirmationId !== void 0 ? { confirmationId: req.confirmationId } : {}
|
|
3138
|
+
},
|
|
3139
|
+
{ consumer: options.consumer }
|
|
3140
|
+
);
|
|
3141
|
+
if (confirmationsMode === "wait" && result.status === "error" && result.error.code === "CONFIRMATION_REQUIRED" && typeof result.error.details?.confirmationId === "string") {
|
|
3142
|
+
const confirmationId = result.error.details.confirmationId;
|
|
3143
|
+
await waitForConfirmation(confirmationId);
|
|
3144
|
+
if (disposed) return result;
|
|
3145
|
+
result = await registry.invoke(
|
|
3146
|
+
{
|
|
3147
|
+
invocationId,
|
|
3148
|
+
capabilityId: req.capabilityId,
|
|
3149
|
+
...req.instanceId !== void 0 ? { instanceId: req.instanceId } : {},
|
|
3150
|
+
surfaceVersion: snapshot.surfaceVersion,
|
|
3151
|
+
...req.input !== void 0 ? { input: req.input } : {},
|
|
3152
|
+
confirmationId
|
|
3153
|
+
},
|
|
3154
|
+
{ consumer: options.consumer }
|
|
3155
|
+
);
|
|
3156
|
+
}
|
|
3157
|
+
return result;
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
];
|
|
3161
|
+
}
|
|
3162
|
+
function computeTools() {
|
|
3163
|
+
if (mode === "meta") {
|
|
3164
|
+
cachedTools ??= buildMetaTools();
|
|
3165
|
+
return cachedTools;
|
|
3166
|
+
}
|
|
3167
|
+
const version = registry.getVersion();
|
|
3168
|
+
if (cachedTools && cachedVersion === version) return cachedTools;
|
|
3169
|
+
cachedTools = buildDirectTools();
|
|
3170
|
+
cachedVersion = version;
|
|
3171
|
+
return cachedTools;
|
|
3172
|
+
}
|
|
3173
|
+
function signatureOf(tools) {
|
|
3174
|
+
return JSON.stringify(
|
|
3175
|
+
tools.map((t) => [t.name, t.description, t.inputSchema])
|
|
3176
|
+
);
|
|
3177
|
+
}
|
|
3178
|
+
const unsubscribe = registry.subscribe((event) => {
|
|
3179
|
+
if (disposed || event.type !== "surface-changed") return;
|
|
3180
|
+
cachedVersion = void 0;
|
|
3181
|
+
const tools = computeTools();
|
|
3182
|
+
const signature = signatureOf(tools);
|
|
3183
|
+
if (signature === cachedSignature) return;
|
|
3184
|
+
cachedSignature = signature;
|
|
3185
|
+
for (const listener of [...listeners]) {
|
|
3186
|
+
try {
|
|
3187
|
+
listener(tools);
|
|
3188
|
+
} catch {
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
});
|
|
3192
|
+
return {
|
|
3193
|
+
tools() {
|
|
3194
|
+
const tools = computeTools();
|
|
3195
|
+
cachedSignature ??= signatureOf(tools);
|
|
3196
|
+
return tools;
|
|
3197
|
+
},
|
|
3198
|
+
subscribe(listener) {
|
|
3199
|
+
listeners.add(listener);
|
|
3200
|
+
return () => {
|
|
3201
|
+
listeners.delete(listener);
|
|
3202
|
+
};
|
|
3203
|
+
},
|
|
3204
|
+
dispose() {
|
|
3205
|
+
disposed = true;
|
|
3206
|
+
unsubscribe();
|
|
3207
|
+
listeners.clear();
|
|
3208
|
+
for (const controller of [...pendingWaits]) controller.abort();
|
|
3209
|
+
pendingWaits.clear();
|
|
3210
|
+
}
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
function findRegistrationId(snapshot, capabilityId, instanceId) {
|
|
3214
|
+
const matches = [];
|
|
3215
|
+
for (const component of snapshot.components) {
|
|
3216
|
+
if (instanceId !== void 0 && component.instanceId !== instanceId) continue;
|
|
3217
|
+
const all = [
|
|
3218
|
+
...component.observations,
|
|
3219
|
+
...component.actions
|
|
3220
|
+
];
|
|
3221
|
+
if (all.some((c) => c.capabilityId === capabilityId)) matches.push(component.registrationId);
|
|
3222
|
+
}
|
|
3223
|
+
for (const proc of snapshot.procedures) {
|
|
3224
|
+
if (proc.procedureId === capabilityId) matches.push(proc.registrationId);
|
|
3225
|
+
}
|
|
3226
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
3227
|
+
}
|
|
3228
|
+
export {
|
|
3229
|
+
AGENT_CAPABILITY_ERROR_CODES,
|
|
3230
|
+
AgentSchemaError,
|
|
3231
|
+
AgentSurfaceDefinitionError,
|
|
3232
|
+
AgentSurfaceError,
|
|
3233
|
+
CONFIRMATION_ESCALATION,
|
|
3234
|
+
DEFAULT_LIMITS,
|
|
3235
|
+
MAX_ID_LENGTH,
|
|
3236
|
+
MAX_WIRE_NAME_LENGTH,
|
|
3237
|
+
action,
|
|
3238
|
+
audit,
|
|
3239
|
+
authenticated,
|
|
3240
|
+
composeInvokeChain,
|
|
3241
|
+
consoleAuditSink,
|
|
3242
|
+
createAgentSurfaceRegistry,
|
|
3243
|
+
createAgentToolset,
|
|
3244
|
+
decodeWireName,
|
|
3245
|
+
defineAgentComponent,
|
|
3246
|
+
emptyObjectSchema,
|
|
3247
|
+
encodeWireName,
|
|
3248
|
+
encodeWireNameForInstance,
|
|
3249
|
+
environment,
|
|
3250
|
+
evaluateDiscovery,
|
|
3251
|
+
formatDomainCapabilityId,
|
|
3252
|
+
formatViewCapabilityId,
|
|
3253
|
+
fromJsonSchema,
|
|
3254
|
+
fromStandardSchema,
|
|
3255
|
+
hasPermission,
|
|
3256
|
+
isAgentSurfaceError,
|
|
3257
|
+
isValidCapabilityName,
|
|
3258
|
+
isValidComponentType,
|
|
3259
|
+
isValidInstanceId,
|
|
3260
|
+
jsonDeepEqual,
|
|
3261
|
+
memoryAuditSink,
|
|
3262
|
+
observation,
|
|
3263
|
+
parseCapabilityId,
|
|
3264
|
+
rateLimit,
|
|
3265
|
+
requireConfirmation,
|
|
3266
|
+
tenantBoundary,
|
|
3267
|
+
validateComponentDefinition,
|
|
3268
|
+
validateJsonSchemaDocument,
|
|
3269
|
+
validateValueAgainstSchema
|
|
3270
|
+
};
|
|
3271
|
+
//# sourceMappingURL=index.js.map
|