apps 1.0.0-beta.1 → 1.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +3 -2
  2. package/dist/authoring/index.d.ts +222 -34
  3. package/dist/authoring/index.js +11 -1
  4. package/dist/{chunk-NFLNA2SC.js → chunk-4OZFMCTT.js} +7 -1
  5. package/dist/{chunk-LWKQDDLZ.js → chunk-A3H32P2J.js} +46 -1
  6. package/dist/chunk-F7GZYEAC.js +41 -0
  7. package/dist/chunk-MUFMJRXR.js +308 -0
  8. package/dist/chunk-S5KFP4JQ.js +20 -0
  9. package/dist/chunk-Y6LLTN7W.js +99 -0
  10. package/dist/chunk-YUZ4TXOY.js +27 -0
  11. package/dist/http/index.d.ts +9 -0
  12. package/dist/http/index.js +6 -0
  13. package/dist/index.d.ts +3 -8
  14. package/dist/index.js +23 -1
  15. package/dist/manifest/contract.d.ts +8 -0
  16. package/dist/manifest/index.d.ts +8 -6
  17. package/dist/manifest/index.js +3 -3
  18. package/dist/mcp/client.d.ts +9 -0
  19. package/dist/mcp/index.d.ts +11 -0
  20. package/dist/mcp/index.js +7 -0
  21. package/dist/openapi/index.d.ts +14 -0
  22. package/dist/openapi/index.js +7 -0
  23. package/dist/openapi/mapper.d.ts +7 -0
  24. package/dist/runner/index.js +36 -51
  25. package/dist/runner/protocol.d.ts +20 -15
  26. package/dist/runner/protocol.js +5 -3
  27. package/dist/runtime/index.d.ts +83 -0
  28. package/dist/runtime/index.js +12 -0
  29. package/dist/workerd/index.d.ts +7 -2
  30. package/dist/workerd/index.js +7 -3
  31. package/package.json +18 -7
  32. package/src/authoring/index.ts +362 -44
  33. package/src/http/index.ts +26 -0
  34. package/src/index.ts +3 -8
  35. package/src/manifest/contract.ts +9 -0
  36. package/src/manifest/index.ts +89 -157
  37. package/src/mcp/client.ts +59 -0
  38. package/src/mcp/index.ts +42 -0
  39. package/src/openapi/index.ts +70 -0
  40. package/src/openapi/mapper.ts +25 -0
  41. package/src/runner/index.ts +50 -87
  42. package/src/runner/protocol.ts +34 -21
  43. package/src/runtime/index.ts +550 -0
  44. package/src/workerd/index.ts +17 -2
  45. package/dist/chunk-J5AVXTHA.js +0 -154
  46. package/dist/chunk-RZA7AXBK.js +0 -107
  47. package/dist/manifest/collect.d.ts +0 -64
  48. package/src/manifest/collect.ts +0 -198
@@ -1,20 +1,26 @@
1
- import { Crypto, Data, type Effect, FileSystem } from "effect";
1
+ import { Data, type Effect, FileSystem } from "effect";
2
2
  import type { AppManifestResult, PublishError } from "../manifest/contract";
3
+ import type {
4
+ CallFailureKind,
5
+ DiscoveredTool,
6
+ ToolCtx,
7
+ ToolCtxEntry,
8
+ } from "../runtime/index";
3
9
 
4
- /** The capability exposed to one tool requirement role. */
5
- export interface ToolCtxEntry {
6
- readonly grant: string;
7
- readonly token?: string;
8
- readonly secrets?: Readonly<Record<string, string>>;
9
- fetch(path: string, init?: RequestInit): Promise<Response>;
10
- }
10
+ export type {
11
+ DiscoveredTool,
12
+ ToolCtx,
13
+ ToolCtxEntry,
14
+ ToolDescriptor,
15
+ } from "../runtime/index";
11
16
 
12
- /** Tool capabilities keyed by requirement role. */
13
- export type ToolCtx = Readonly<Record<string, ToolCtxEntry>>;
17
+ /** Which invocation phase failed. */
18
+ export type CallErrorKind = CallFailureKind;
14
19
 
15
- /** A runner operation failure associated with its target. */
20
+ /** A runner operation failure associated with its target and phase. */
16
21
  export class CallError extends Data.TaggedError("CallError")<{
17
22
  readonly target: string;
23
+ readonly kind: CallErrorKind;
18
24
  readonly reason: string;
19
25
  }> {
20
26
  override get message(): string {
@@ -22,20 +28,27 @@ export class CallError extends Data.TaggedError("CallError")<{
22
28
  }
23
29
  }
24
30
 
31
+ /** A discovery failure associated with one dynamic segment. */
32
+ export class DiscoverError extends Data.TaggedError("DiscoverError")<{
33
+ readonly segment: string;
34
+ readonly reason: string;
35
+ }> {
36
+ override get message(): string {
37
+ return `${this.segment}: ${this.reason}`;
38
+ }
39
+ }
40
+
25
41
  /** The platform protocol implemented by an app execution environment. */
26
42
  export interface AppRunner {
27
- manifest(): Effect.Effect<
28
- AppManifestResult,
29
- PublishError,
30
- Crypto.Crypto | FileSystem.FileSystem
31
- >;
43
+ manifest(): Effect.Effect<AppManifestResult, PublishError, FileSystem.FileSystem>;
32
44
  call(
33
- name: string,
45
+ address: string,
34
46
  input: unknown,
35
47
  ctx: ToolCtx,
36
- ): Effect.Effect<unknown, CallError, Crypto.Crypto | FileSystem.FileSystem>;
48
+ detail?: unknown,
49
+ ): Effect.Effect<unknown, CallError, FileSystem.FileSystem>;
37
50
  discover(
38
- name: string,
39
- binding: unknown,
40
- ): Effect.Effect<unknown, CallError, Crypto.Crypto | FileSystem.FileSystem>;
51
+ segment: string,
52
+ ctx: ToolCtx,
53
+ ): Effect.Effect<readonly DiscoveredTool[], DiscoverError, FileSystem.FileSystem>;
41
54
  }
@@ -0,0 +1,550 @@
1
+ /**
2
+ * The execution core: everything that happens after authored modules
3
+ * load, in any environment. Project a registry into its deterministic
4
+ * manifest, validate a call's input, run the handler, shape the
5
+ * outcome. There is exactly one implementation of this logic; the
6
+ * local runner and the sandboxed workerd guest are both thin doors
7
+ * onto this module.
8
+ *
9
+ * Deliberately dependency-free (no effect, no node builtins): this
10
+ * module bundles into sandboxed guests, where every dependency is
11
+ * paid on each cold start. Digesting uses WebCrypto, which node, bun,
12
+ * and workerd all provide.
13
+ */
14
+
15
+ import type {
16
+ DiscoveredToolSpec,
17
+ DynamicToolsSpec,
18
+ Registry,
19
+ } from "../authoring/index";
20
+ import type {
21
+ AppManifestResult,
22
+ CollectedClaim,
23
+ CollectedRequirement,
24
+ JsonSchemaObject,
25
+ ManifestDynamic,
26
+ ManifestTool,
27
+ } from "../manifest/contract";
28
+
29
+ // ── tool capabilities ─────────────────────────────────────────────────
30
+
31
+ /** The capability exposed to one tool requirement role. */
32
+ export interface ToolCtxEntry {
33
+ readonly grant: string;
34
+ readonly kind: "bearer" | "basic" | "secrets" | "oauth";
35
+ readonly token?: string;
36
+ readonly username?: string;
37
+ readonly password?: string;
38
+ readonly secrets?: Readonly<Record<string, string>>;
39
+ fetch(path: string, init?: RequestInit): Promise<Response>;
40
+ }
41
+
42
+ /** Tool capabilities keyed by requirement role. */
43
+ export type ToolCtx = Readonly<Record<string, ToolCtxEntry>>;
44
+
45
+ // ── outcomes ──────────────────────────────────────────────────────────
46
+
47
+ /** A manifest projection: the app's deterministic description, or why not. */
48
+ export type ManifestOutcome =
49
+ | { readonly ok: true; readonly result: AppManifestResult }
50
+ | { readonly ok: false; readonly problems: readonly string[] };
51
+
52
+ /** Which invocation phase failed. */
53
+ export type CallFailureKind = "input" | "handler";
54
+
55
+ /** One tool invocation's result envelope. */
56
+ export type CallOutcome =
57
+ | { readonly ok: true; readonly value: unknown }
58
+ | {
59
+ readonly ok: false;
60
+ readonly kind: CallFailureKind;
61
+ readonly reason: string;
62
+ };
63
+
64
+ /** Agent-facing descriptor compiled from one discovery result. */
65
+ export interface ToolDescriptor {
66
+ readonly description: string;
67
+ readonly inputSchema: JsonSchemaObject;
68
+ readonly annotations: {
69
+ readonly readOnly?: boolean;
70
+ readonly destructive?: boolean;
71
+ };
72
+ }
73
+
74
+ /** One tool emitted by a dynamic segment discovery. */
75
+ export interface DiscoveredTool {
76
+ readonly segment: string;
77
+ readonly name: string;
78
+ readonly descriptor: ToolDescriptor;
79
+ readonly detail: unknown;
80
+ }
81
+
82
+ /** One dynamic discovery result envelope. */
83
+ export type DiscoverOutcome =
84
+ | { readonly ok: true; readonly value: readonly DiscoveredTool[] }
85
+ | { readonly ok: false; readonly reason: string };
86
+
87
+ const failure = (kind: CallFailureKind, reason: string): CallOutcome => ({
88
+ ok: false,
89
+ kind,
90
+ reason,
91
+ });
92
+
93
+ const errorMessage = (error: unknown): string =>
94
+ error instanceof Error ? error.message : String(error);
95
+
96
+ // ── input schema compilation ──────────────────────────────────────────
97
+
98
+ type JsonValue =
99
+ | null
100
+ | boolean
101
+ | number
102
+ | string
103
+ | readonly JsonValue[]
104
+ | { readonly [key: string]: JsonValue };
105
+
106
+ const isJsonValue = (value: unknown): value is JsonValue => {
107
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
108
+ if (typeof value === "number") return Number.isFinite(value);
109
+ if (Array.isArray(value)) return value.every(isJsonValue);
110
+ if (typeof value !== "object") return false;
111
+ return Object.values(value).every(isJsonValue);
112
+ };
113
+
114
+ interface StandardJsonSchemaCapable {
115
+ readonly "~standard": {
116
+ readonly jsonSchema?: {
117
+ readonly input: (options: { readonly target: string }) => unknown;
118
+ };
119
+ };
120
+ }
121
+
122
+ const standardJsonSchemaOf = (input: object) =>
123
+ "~standard" in input
124
+ ? (input as StandardJsonSchemaCapable)["~standard"].jsonSchema
125
+ : undefined;
126
+
127
+ /** Strict-by-default: authored schemas reject unknown keys unless stated. */
128
+ const strictify = (node: unknown): unknown => {
129
+ if (typeof node !== "object" || node === null) return node;
130
+ if (Array.isArray(node)) return node.map(strictify);
131
+ const object = { ...(node as Record<string, unknown>) };
132
+ if (object.type === "object" && object.additionalProperties === undefined)
133
+ object.additionalProperties = false;
134
+ for (const key of ["items", "anyOf", "oneOf", "allOf"])
135
+ if (key in object) object[key] = strictify(object[key]);
136
+ for (const key of ["properties", "$defs"])
137
+ if (object[key] !== undefined && typeof object[key] === "object" && object[key] !== null)
138
+ object[key] = Object.fromEntries(
139
+ Object.entries(object[key] as Record<string, unknown>).map(([name, value]) => [
140
+ name,
141
+ strictify(value),
142
+ ]),
143
+ );
144
+ return object;
145
+ };
146
+
147
+ type InputSchemaCompilation =
148
+ | { readonly value: JsonSchemaObject }
149
+ | { readonly problem: string };
150
+
151
+ export const compileInputSchema = (input: object, where: string): InputSchemaCompilation => {
152
+ const standard = standardJsonSchemaOf(input);
153
+ if ("~standard" in input && typeof standard?.input !== "function")
154
+ return {
155
+ problem: `${where}: input is a Standard Schema without the jsonSchema extension — use a library that implements it (zod 4+, effect Schema) or pass raw JSON schema`,
156
+ };
157
+ try {
158
+ const compiled = strictify(
159
+ typeof standard?.input === "function"
160
+ ? standard.input({ target: "draft-2020-12" })
161
+ : structuredClone(input),
162
+ );
163
+ if (
164
+ typeof compiled !== "object" ||
165
+ compiled === null ||
166
+ Array.isArray(compiled) ||
167
+ !isJsonValue(compiled)
168
+ )
169
+ throw new Error("expected a JSON object");
170
+ return { value: compiled as JsonSchemaObject };
171
+ } catch (error) {
172
+ return {
173
+ problem: `${where}: input schema could not compile to JSON schema (${errorMessage(error)})`,
174
+ };
175
+ }
176
+ };
177
+
178
+ // ── registry projection ───────────────────────────────────────────────
179
+
180
+ const requirementKey = Symbol.for("executor/apps/requirement");
181
+
182
+ interface RequirementSpecShape {
183
+ readonly service: {
184
+ readonly origin: string;
185
+ readonly auth?: Readonly<Record<string, unknown>>;
186
+ };
187
+ readonly arity: "one" | "optional" | "many";
188
+ readonly grant?: string;
189
+ readonly description?: string;
190
+ }
191
+
192
+ const requirementSpecOf = (value: unknown): RequirementSpecShape | undefined =>
193
+ typeof value === "object" && value !== null && requirementKey in value
194
+ ? ((value as Record<PropertyKey, unknown>)[requirementKey] as RequirementSpecShape)
195
+ : undefined;
196
+
197
+ const segmentOf = (file: string): string => {
198
+ const name = file.slice(file.lastIndexOf("/") + 1);
199
+ return name.endsWith(".ts") ? name.slice(0, -3) : name;
200
+ };
201
+
202
+ const canonicalJson = (value: unknown): string => {
203
+ const sortKeys = (node: unknown): unknown => {
204
+ if (Array.isArray(node)) return node.map(sortKeys);
205
+ if (typeof node === "object" && node !== null)
206
+ return Object.fromEntries(
207
+ Object.entries(node as Record<string, unknown>)
208
+ .sort(([left], [right]) => left.localeCompare(right))
209
+ .map(([key, child]) => [key, sortKeys(child)]),
210
+ );
211
+ return node;
212
+ };
213
+ return JSON.stringify(sortKeys(value));
214
+ };
215
+
216
+ interface CollectedManifest {
217
+ readonly app: { readonly key: string; readonly label: string; readonly description: string };
218
+ readonly services: readonly CollectedClaim[];
219
+ readonly tools: readonly ManifestTool[];
220
+ readonly dynamic: readonly ManifestDynamic[];
221
+ }
222
+
223
+ type RegistryCollection =
224
+ | { readonly problems: readonly string[] }
225
+ | {
226
+ readonly problems: readonly [];
227
+ readonly manifest: CollectedManifest;
228
+ readonly canonical: string;
229
+ };
230
+
231
+ const collectRegistry = (registry: Registry): RegistryCollection => {
232
+ const problems = registry.problems.map((problem) => problem.message);
233
+ const app = registry.app;
234
+
235
+ const claims = new Map<string, CollectedClaim>();
236
+ for (const [origin, registered] of registry.services) {
237
+ if (registered.file !== "app.ts") continue;
238
+ const { decl } = registered;
239
+ claims.set(origin, {
240
+ origin,
241
+ reach: decl.reach ?? [],
242
+ params: decl.params ?? {},
243
+ auth: decl.auth ?? {},
244
+ });
245
+ }
246
+
247
+ const collectRequirement = (
248
+ role: string,
249
+ requirement: unknown,
250
+ where: string,
251
+ ): CollectedRequirement | undefined => {
252
+ const spec = requirementSpecOf(requirement);
253
+ if (spec === undefined) {
254
+ problems.push(`${where}: requirement '${role}' is not a service/requirement value`);
255
+ return undefined;
256
+ }
257
+ const origin = spec.service.origin;
258
+ if (!claims.has(origin))
259
+ problems.push(`${where}: requirement '${role}' names ${origin}, which app.ts does not declare`);
260
+ if (spec.grant !== undefined && !(spec.grant in (spec.service.auth ?? {})))
261
+ problems.push(
262
+ `${where}: requirement '${role}' pins grant '${spec.grant}', not a auth method on ${origin}`,
263
+ );
264
+ return {
265
+ role,
266
+ service: origin,
267
+ arity: spec.arity,
268
+ ...(spec.grant === undefined ? {} : { grant: spec.grant }),
269
+ ...(spec.description === undefined ? {} : { description: spec.description }),
270
+ };
271
+ };
272
+
273
+ if (app === undefined) problems.push("app.ts must call defineApp({...})");
274
+ else if (typeof app.description !== "string" || app.description.trim().length === 0)
275
+ problems.push("app.ts: app description must not be empty");
276
+
277
+ for (const [file, summary] of registry.toolFiles) {
278
+ if (summary.definitions > 1 && summary.hasUnnamedTool)
279
+ problems.push(`${file}: multi-tool files must name every tool`);
280
+ if (registry.dynamic.has(file))
281
+ problems.push(`${file}: a file claims a dynamic tool set or defines tools, never both`);
282
+ }
283
+
284
+ const tools: ManifestTool[] = [];
285
+ if (app !== undefined) {
286
+ for (const [name, registered] of registry.tools) {
287
+ const { spec, file } = registered;
288
+ const address = `${app.key}/${name}`;
289
+ if (typeof spec.description !== "string" || spec.description.trim().length === 0)
290
+ problems.push(`${file}: tool '${name}' description must not be empty`);
291
+ const requires = Object.entries(spec.requirements ?? {})
292
+ .flatMap(([role, requirement]) => {
293
+ const collected = collectRequirement(role, requirement, address);
294
+ return collected === undefined ? [] : [collected];
295
+ })
296
+ .sort((left, right) => left.role.localeCompare(right.role));
297
+ const compiled = compileInputSchema(spec.input, address);
298
+ if ("problem" in compiled) {
299
+ problems.push(compiled.problem);
300
+ continue;
301
+ }
302
+ tools.push({
303
+ address,
304
+ name,
305
+ description: spec.description,
306
+ inputSchema: compiled.value,
307
+ annotations: spec.annotations ?? {},
308
+ requires,
309
+ file,
310
+ });
311
+ }
312
+ }
313
+
314
+ const dynamic: ManifestDynamic[] = [];
315
+ const seenSegments = new Map<string, string>();
316
+ for (const [file, registered] of registry.dynamic) {
317
+ const segment = segmentOf(file);
318
+ const where = `${file}: dynamic tool set '${segment}'`;
319
+ const existing = seenSegments.get(segment);
320
+ if (existing !== undefined) {
321
+ problems.push(`${where}: segment already claimed by ${existing}`);
322
+ continue;
323
+ }
324
+ seenSegments.set(segment, file);
325
+
326
+ const requires = Object.entries(registered.spec.requirements ?? {})
327
+ .flatMap(([role, requirement]) => {
328
+ const collected = collectRequirement(role, requirement, where);
329
+ return collected === undefined ? [] : [collected];
330
+ })
331
+ .sort((left, right) => left.role.localeCompare(right.role));
332
+ dynamic.push({
333
+ segment,
334
+ description: registered.spec.description,
335
+ requires,
336
+ file,
337
+ });
338
+ }
339
+
340
+ if (problems.length > 0 || app === undefined) return { problems };
341
+ const services = [...claims.values()].sort((left, right) =>
342
+ left.origin.localeCompare(right.origin),
343
+ );
344
+ const sortedTools = [...tools].sort((left, right) => left.address.localeCompare(right.address));
345
+ const sortedDynamic = [...dynamic].sort((left, right) =>
346
+ left.segment.localeCompare(right.segment),
347
+ );
348
+ const appResult = { key: app.key, label: app.label, description: app.description };
349
+ const manifest = {
350
+ app: appResult,
351
+ services,
352
+ tools: sortedTools,
353
+ dynamic: sortedDynamic,
354
+ } satisfies CollectedManifest;
355
+ return { problems: [], manifest, canonical: canonicalJson(manifest) };
356
+ };
357
+
358
+ const digestOf = async (canonical: string): Promise<string> => {
359
+ const bytes = new Uint8Array(
360
+ await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical)),
361
+ );
362
+ return Array.from(bytes.slice(0, 8), (byte) => byte.toString(16).padStart(2, "0")).join("");
363
+ };
364
+
365
+ /** Project a loaded registry into the app's deterministic manifest. */
366
+ export const manifestOf = async (registry: Registry): Promise<ManifestOutcome> => {
367
+ const collected = collectRegistry(registry);
368
+ if (collected.problems.length > 0) return { ok: false, problems: collected.problems };
369
+ if (!("manifest" in collected))
370
+ return { ok: false, problems: ["validated registry projection unexpectedly missing"] };
371
+ return {
372
+ ok: true,
373
+ result: { digest: `snap:${await digestOf(collected.canonical)}`, ...collected.manifest },
374
+ };
375
+ };
376
+
377
+ // ── invocation ────────────────────────────────────────────────────────
378
+
379
+ interface StandardValidateCapable {
380
+ readonly "~standard": {
381
+ readonly validate: (value: unknown) =>
382
+ | { readonly value: unknown; readonly issues?: undefined }
383
+ | { readonly issues: ReadonlyArray<{ readonly message: string }> }
384
+ | Promise<
385
+ | { readonly value: unknown; readonly issues?: undefined }
386
+ | { readonly issues: ReadonlyArray<{ readonly message: string }> }
387
+ >;
388
+ };
389
+ }
390
+
391
+ const standardValidateOf = (input: object) =>
392
+ "~standard" in input &&
393
+ typeof (input as StandardValidateCapable)["~standard"].validate === "function"
394
+ ? (input as StandardValidateCapable)["~standard"].validate
395
+ : undefined;
396
+
397
+ type ParsedInput =
398
+ | { readonly ok: true; readonly value: unknown }
399
+ | { readonly ok: false; readonly reason: string };
400
+
401
+ const parseInput = async (schema: object, input: unknown): Promise<ParsedInput> => {
402
+ const validate = standardValidateOf(schema);
403
+ if (validate === undefined)
404
+ return { ok: false, reason: "input schema does not implement Standard Schema validate" };
405
+ try {
406
+ const result = await validate(input);
407
+ if (result.issues !== undefined)
408
+ return {
409
+ ok: false,
410
+ reason: `invalid input (${result.issues.map((issue) => issue.message).join("; ")})`,
411
+ };
412
+ return { ok: true, value: result.value };
413
+ } catch (error) {
414
+ return { ok: false, reason: `input validation threw (${errorMessage(error)})` };
415
+ }
416
+ };
417
+
418
+ const splitDynamicAddress = (
419
+ address: string,
420
+ ): { readonly segment: string; readonly name: string } | undefined => {
421
+ const slash = address.indexOf("/");
422
+ if (slash <= 0 || slash === address.length - 1) return undefined;
423
+ return { segment: address.slice(0, slash), name: address.slice(slash + 1) };
424
+ };
425
+
426
+ const dynamicFor = (
427
+ registry: Registry,
428
+ segment: string,
429
+ ): DynamicToolsSpec | undefined => {
430
+ for (const [file, registered] of registry.dynamic)
431
+ if (segmentOf(file) === segment) return registered.spec;
432
+ return undefined;
433
+ };
434
+
435
+ /** Live schemas from the latest successful discovery, retained per loaded registry. */
436
+ const discoveredSpecs = new WeakMap<
437
+ Registry,
438
+ Map<string, ReadonlyMap<string, DiscoveredToolSpec>>
439
+ >();
440
+
441
+ /** Validate input and run one static or dynamic tool handler. */
442
+ export const callTool = async (
443
+ registry: Registry,
444
+ address: string,
445
+ input: unknown,
446
+ ctx: ToolCtx,
447
+ detail?: unknown,
448
+ ): Promise<CallOutcome> => {
449
+ const dynamic = splitDynamicAddress(address);
450
+ if (dynamic === undefined) {
451
+ if (detail !== undefined)
452
+ return failure("handler", "a static tool takes no discovery detail");
453
+ const registered = registry.tools.get(address);
454
+ if (registered === undefined) return failure("handler", "unknown tool");
455
+ const parsed = await parseInput(registered.spec.input, input);
456
+ if (!parsed.ok) return failure("input", parsed.reason);
457
+ try {
458
+ // SAFETY: ToolSpec intentionally erases authored handler inputs to keep
459
+ // authoring generic-free; validation and ToolCtx establish this call boundary.
460
+ const handler = registered.spec.handler as (
461
+ value: unknown,
462
+ toolCtx: ToolCtx,
463
+ ) => Promise<unknown>;
464
+ return { ok: true, value: await handler(parsed.value, ctx) };
465
+ } catch (error) {
466
+ return failure("handler", `handler failed (${errorMessage(error)})`);
467
+ }
468
+ }
469
+
470
+ if (detail === undefined)
471
+ return failure("handler", "a dynamic tool requires discovery detail");
472
+ const pair = dynamicFor(registry, dynamic.segment);
473
+ if (pair === undefined)
474
+ return failure("handler", `unknown dynamic tool set '${dynamic.segment}'`);
475
+ const discovered = discoveredSpecs.get(registry)?.get(dynamic.segment)?.get(dynamic.name);
476
+ if (discovered === undefined)
477
+ return failure(
478
+ "input",
479
+ `dynamic tool '${dynamic.name}' has no compiled discovery descriptor`,
480
+ );
481
+ const parsed = await parseInput(discovered.input, input);
482
+ if (!parsed.ok) return failure("input", parsed.reason);
483
+ try {
484
+ const call = pair.call as (
485
+ tool: { readonly name: string; readonly detail: unknown },
486
+ value: unknown,
487
+ toolCtx: ToolCtx,
488
+ ) => Promise<unknown>;
489
+ return {
490
+ ok: true,
491
+ value: await call({ name: dynamic.name, detail }, parsed.value, ctx),
492
+ };
493
+ } catch (error) {
494
+ return failure("handler", `dynamic call failed (${errorMessage(error)})`);
495
+ }
496
+ };
497
+
498
+ /** Discover and compile one dynamic segment. */
499
+ export const discoverTool = async (
500
+ registry: Registry,
501
+ segment: string,
502
+ ctx: ToolCtx,
503
+ ): Promise<DiscoverOutcome> => {
504
+ const pair = dynamicFor(registry, segment);
505
+ if (pair === undefined)
506
+ return { ok: false, reason: `unknown dynamic tool set '${segment}'` };
507
+ discoveredSpecs.get(registry)?.delete(segment);
508
+
509
+ let emitted: readonly DiscoveredToolSpec[];
510
+ try {
511
+ const discover = pair.discover as (
512
+ toolCtx: ToolCtx,
513
+ ) => Promise<readonly DiscoveredToolSpec[]>;
514
+ emitted = await discover(ctx);
515
+ } catch (error) {
516
+ return { ok: false, reason: `discover failed (${errorMessage(error)})` };
517
+ }
518
+
519
+ const tools: DiscoveredTool[] = [];
520
+ const live = new Map<string, DiscoveredToolSpec>();
521
+ for (const tool of emitted) {
522
+ if (live.has(tool.name))
523
+ return { ok: false, reason: `discover emitted duplicate tool '${tool.name}'` };
524
+ const compiled = compileInputSchema(tool.input, `${segment}/${tool.name}`);
525
+ if ("problem" in compiled) return { ok: false, reason: compiled.problem };
526
+ live.set(tool.name, tool);
527
+ tools.push({
528
+ segment,
529
+ name: tool.name,
530
+ descriptor: {
531
+ description: tool.description,
532
+ inputSchema: compiled.value,
533
+ annotations: {
534
+ ...(tool.annotations?.readOnly === undefined
535
+ ? {}
536
+ : { readOnly: tool.annotations.readOnly }),
537
+ ...(tool.annotations?.destructive === undefined
538
+ ? {}
539
+ : { destructive: tool.annotations.destructive }),
540
+ },
541
+ },
542
+ detail: tool.detail ?? null,
543
+ });
544
+ }
545
+
546
+ const registryDiscoveries = discoveredSpecs.get(registry) ?? new Map();
547
+ registryDiscoveries.set(segment, live);
548
+ discoveredSpecs.set(registry, registryDiscoveries);
549
+ return { ok: true, value: tools.sort((left, right) => left.name.localeCompare(right.name)) };
550
+ };
@@ -1,2 +1,17 @@
1
- /** Worker-safe registry projection injected into sandboxed app workers. @internal */
2
- export { collectRegistry } from "../manifest/collect";
1
+ /**
2
+ * The execution core re-exported for sandboxed guests. A generated
3
+ * guest module imports the authored files (filling the registry) and
4
+ * exposes these verbs over RPC; the logic itself lives in ../runtime
5
+ * and is identical to what the local runner executes. @internal
6
+ */
7
+ export {
8
+ callTool,
9
+ discoverTool,
10
+ manifestOf,
11
+ type CallOutcome,
12
+ type DiscoverOutcome,
13
+ type DiscoveredTool,
14
+ type ManifestOutcome,
15
+ type ToolCtx,
16
+ type ToolCtxEntry,
17
+ } from "../runtime/index";