apps 1.0.0-beta.1 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,363 @@
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 { Registry, ToolSpec } from "../authoring/index";
16
+ import type {
17
+ AppManifestResult,
18
+ CollectedClaim,
19
+ CollectedRequirement,
20
+ JsonSchemaObject,
21
+ ManifestTool,
22
+ } from "../manifest/contract";
23
+
24
+ // ── tool capabilities ─────────────────────────────────────────────────
25
+
26
+ /** The capability exposed to one tool requirement role. */
27
+ export interface ToolCtxEntry {
28
+ readonly grant: string;
29
+ readonly token?: string;
30
+ readonly secrets?: Readonly<Record<string, string>>;
31
+ fetch(path: string, init?: RequestInit): Promise<Response>;
32
+ }
33
+
34
+ /** Tool capabilities keyed by requirement role. */
35
+ export type ToolCtx = Readonly<Record<string, ToolCtxEntry>>;
36
+
37
+ // ── outcomes ──────────────────────────────────────────────────────────
38
+
39
+ /** A manifest projection: the app's deterministic description, or why not. */
40
+ export type ManifestOutcome =
41
+ | { readonly ok: true; readonly result: AppManifestResult }
42
+ | { readonly ok: false; readonly problems: readonly string[] };
43
+
44
+ /** One tool invocation's result envelope. */
45
+ export type CallOutcome =
46
+ | { readonly ok: true; readonly value: unknown }
47
+ | { readonly ok: false; readonly reason: string };
48
+
49
+ const failure = (reason: string): CallOutcome => ({ ok: false, reason });
50
+
51
+ const errorMessage = (error: unknown): string =>
52
+ error instanceof Error ? error.message : String(error);
53
+
54
+ // ── input schema compilation ──────────────────────────────────────────
55
+
56
+ type JsonValue =
57
+ | null
58
+ | boolean
59
+ | number
60
+ | string
61
+ | readonly JsonValue[]
62
+ | { readonly [key: string]: JsonValue };
63
+
64
+ const isJsonValue = (value: unknown): value is JsonValue => {
65
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
66
+ if (typeof value === "number") return Number.isFinite(value);
67
+ if (Array.isArray(value)) return value.every(isJsonValue);
68
+ if (typeof value !== "object") return false;
69
+ return Object.values(value).every(isJsonValue);
70
+ };
71
+
72
+ interface StandardJsonSchemaCapable {
73
+ readonly "~standard": {
74
+ readonly jsonSchema?: {
75
+ readonly input: (options: { readonly target: string }) => unknown;
76
+ };
77
+ };
78
+ }
79
+
80
+ const standardJsonSchemaOf = (input: object) =>
81
+ "~standard" in input
82
+ ? (input as StandardJsonSchemaCapable)["~standard"].jsonSchema
83
+ : undefined;
84
+
85
+ /** Strict-by-default: authored schemas reject unknown keys unless stated. */
86
+ const strictify = (node: unknown): unknown => {
87
+ if (typeof node !== "object" || node === null) return node;
88
+ if (Array.isArray(node)) return node.map(strictify);
89
+ const object = { ...(node as Record<string, unknown>) };
90
+ if (object.type === "object" && object.additionalProperties === undefined)
91
+ object.additionalProperties = false;
92
+ for (const key of ["items", "anyOf", "oneOf", "allOf"])
93
+ if (key in object) object[key] = strictify(object[key]);
94
+ for (const key of ["properties", "$defs"])
95
+ if (object[key] !== undefined && typeof object[key] === "object" && object[key] !== null)
96
+ object[key] = Object.fromEntries(
97
+ Object.entries(object[key] as Record<string, unknown>).map(([name, value]) => [
98
+ name,
99
+ strictify(value),
100
+ ]),
101
+ );
102
+ return object;
103
+ };
104
+
105
+ type InputSchemaCompilation =
106
+ | { readonly value: JsonSchemaObject }
107
+ | { readonly problem: string };
108
+
109
+ const compileInputSchema = (input: object, where: string): InputSchemaCompilation => {
110
+ const standard = standardJsonSchemaOf(input);
111
+ if ("~standard" in input && typeof standard?.input !== "function")
112
+ return {
113
+ 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`,
114
+ };
115
+ try {
116
+ const compiled = strictify(
117
+ typeof standard?.input === "function"
118
+ ? standard.input({ target: "draft-2020-12" })
119
+ : structuredClone(input),
120
+ );
121
+ if (
122
+ typeof compiled !== "object" ||
123
+ compiled === null ||
124
+ Array.isArray(compiled) ||
125
+ !isJsonValue(compiled)
126
+ )
127
+ throw new Error("expected a JSON object");
128
+ return { value: compiled as JsonSchemaObject };
129
+ } catch (error) {
130
+ return {
131
+ problem: `${where}: input schema could not compile to JSON schema (${errorMessage(error)})`,
132
+ };
133
+ }
134
+ };
135
+
136
+ // ── registry projection ───────────────────────────────────────────────
137
+
138
+ const requirementKey = Symbol.for("executor/apps/requirement");
139
+
140
+ interface RequirementSpecShape {
141
+ readonly service: {
142
+ readonly origin: string;
143
+ readonly auth?: Readonly<Record<string, unknown>>;
144
+ };
145
+ readonly arity: "one" | "optional" | "many";
146
+ readonly grant?: string;
147
+ readonly description?: string;
148
+ }
149
+
150
+ const requirementSpecOf = (value: unknown): RequirementSpecShape | undefined =>
151
+ typeof value === "object" && value !== null && requirementKey in value
152
+ ? ((value as Record<PropertyKey, unknown>)[requirementKey] as RequirementSpecShape)
153
+ : undefined;
154
+
155
+ const canonicalJson = (value: unknown): string => {
156
+ const sortKeys = (node: unknown): unknown => {
157
+ if (Array.isArray(node)) return node.map(sortKeys);
158
+ if (typeof node === "object" && node !== null)
159
+ return Object.fromEntries(
160
+ Object.entries(node as Record<string, unknown>)
161
+ .sort(([left], [right]) => left.localeCompare(right))
162
+ .map(([key, child]) => [key, sortKeys(child)]),
163
+ );
164
+ return node;
165
+ };
166
+ return JSON.stringify(sortKeys(value));
167
+ };
168
+
169
+ interface CollectedManifest {
170
+ readonly app: { readonly key: string; readonly label: string; readonly description: string };
171
+ readonly services: readonly CollectedClaim[];
172
+ readonly tools: readonly ManifestTool[];
173
+ }
174
+
175
+ type RegistryCollection =
176
+ | { readonly problems: readonly string[] }
177
+ | {
178
+ readonly problems: readonly [];
179
+ readonly manifest: CollectedManifest;
180
+ readonly canonical: string;
181
+ };
182
+
183
+ const collectRegistry = (registry: Registry): RegistryCollection => {
184
+ const problems = registry.problems.map((problem) => problem.message);
185
+ const app = registry.app;
186
+
187
+ const claims = new Map<string, CollectedClaim>();
188
+ for (const [origin, registered] of registry.services) {
189
+ if (registered.file !== "app.ts") continue;
190
+ const { decl } = registered;
191
+ claims.set(origin, {
192
+ origin,
193
+ reach: decl.reach ?? [],
194
+ params: decl.params ?? {},
195
+ auth: decl.auth ?? {},
196
+ });
197
+ }
198
+
199
+ const collectRequirement = (
200
+ role: string,
201
+ requirement: unknown,
202
+ where: string,
203
+ ): CollectedRequirement | undefined => {
204
+ const spec = requirementSpecOf(requirement);
205
+ if (spec === undefined) {
206
+ problems.push(`${where}: requirement '${role}' is not a service/requirement value`);
207
+ return undefined;
208
+ }
209
+ const origin = spec.service.origin;
210
+ if (!claims.has(origin))
211
+ problems.push(`${where}: requirement '${role}' names ${origin}, which app.ts does not declare`);
212
+ if (spec.grant !== undefined && !(spec.grant in (spec.service.auth ?? {})))
213
+ problems.push(
214
+ `${where}: requirement '${role}' pins grant '${spec.grant}', not a auth method on ${origin}`,
215
+ );
216
+ return {
217
+ role,
218
+ service: origin,
219
+ arity: spec.arity,
220
+ ...(spec.grant === undefined ? {} : { grant: spec.grant }),
221
+ ...(spec.description === undefined ? {} : { description: spec.description }),
222
+ };
223
+ };
224
+
225
+ if (app === undefined) problems.push("app.ts must call defineApp({...})");
226
+ else if (typeof app.description !== "string" || app.description.trim().length === 0)
227
+ problems.push("app.ts: app description must not be empty");
228
+
229
+ for (const [file, summary] of registry.toolFiles) {
230
+ if (summary.definitions > 1 && summary.hasUnnamedTool)
231
+ problems.push(`${file}: multi-tool files must name every tool`);
232
+ }
233
+
234
+ const tools: ManifestTool[] = [];
235
+ if (app !== undefined) {
236
+ for (const [name, registered] of registry.tools) {
237
+ const { spec, file } = registered;
238
+ const address = `${app.key}/${name}`;
239
+ if (typeof spec.description !== "string" || spec.description.trim().length === 0)
240
+ problems.push(`${file}: tool '${name}' description must not be empty`);
241
+ const requires = Object.entries(spec.requirements ?? {})
242
+ .flatMap(([role, requirement]) => {
243
+ const collected = collectRequirement(role, requirement, address);
244
+ return collected === undefined ? [] : [collected];
245
+ })
246
+ .sort((left, right) => left.role.localeCompare(right.role));
247
+ const compiled = compileInputSchema(spec.input, address);
248
+ if ("problem" in compiled) {
249
+ problems.push(compiled.problem);
250
+ continue;
251
+ }
252
+ tools.push({
253
+ address,
254
+ name,
255
+ description: spec.description,
256
+ inputSchema: compiled.value,
257
+ annotations: spec.annotations ?? {},
258
+ requires,
259
+ file,
260
+ });
261
+ }
262
+ }
263
+
264
+ if (problems.length > 0 || app === undefined) return { problems };
265
+ const services = [...claims.values()].sort((left, right) =>
266
+ left.origin.localeCompare(right.origin),
267
+ );
268
+ const sortedTools = [...tools].sort((left, right) => left.address.localeCompare(right.address));
269
+ const appResult = { key: app.key, label: app.label, description: app.description };
270
+ const manifest = { app: appResult, services, tools: sortedTools } satisfies CollectedManifest;
271
+ return { problems: [], manifest, canonical: canonicalJson(manifest) };
272
+ };
273
+
274
+ const digestOf = async (canonical: string): Promise<string> => {
275
+ const bytes = new Uint8Array(
276
+ await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical)),
277
+ );
278
+ return Array.from(bytes.slice(0, 8), (byte) => byte.toString(16).padStart(2, "0")).join("");
279
+ };
280
+
281
+ /** Project a loaded registry into the app's deterministic manifest. */
282
+ export const manifestOf = async (registry: Registry): Promise<ManifestOutcome> => {
283
+ const collected = collectRegistry(registry);
284
+ if (collected.problems.length > 0) return { ok: false, problems: collected.problems };
285
+ if (!("manifest" in collected))
286
+ return { ok: false, problems: ["validated registry projection unexpectedly missing"] };
287
+ return {
288
+ ok: true,
289
+ result: { digest: `snap:${await digestOf(collected.canonical)}`, ...collected.manifest },
290
+ };
291
+ };
292
+
293
+ // ── invocation ────────────────────────────────────────────────────────
294
+
295
+ interface StandardValidateCapable {
296
+ readonly "~standard": {
297
+ readonly validate: (value: unknown) =>
298
+ | { readonly value: unknown; readonly issues?: undefined }
299
+ | { readonly issues: ReadonlyArray<{ readonly message: string }> }
300
+ | Promise<
301
+ | { readonly value: unknown; readonly issues?: undefined }
302
+ | { readonly issues: ReadonlyArray<{ readonly message: string }> }
303
+ >;
304
+ };
305
+ }
306
+
307
+ const standardValidateOf = (input: object) =>
308
+ "~standard" in input &&
309
+ typeof (input as StandardValidateCapable)["~standard"].validate === "function"
310
+ ? (input as StandardValidateCapable)["~standard"].validate
311
+ : undefined;
312
+
313
+ type ParsedInput =
314
+ | { readonly ok: true; readonly value: unknown }
315
+ | { readonly ok: false; readonly reason: string };
316
+
317
+ const parseInput = async (spec: ToolSpec, input: unknown): Promise<ParsedInput> => {
318
+ const validate = standardValidateOf(spec.input);
319
+ if (validate === undefined)
320
+ return { ok: false, reason: "input schema does not implement Standard Schema validate" };
321
+ try {
322
+ const result = await validate(input);
323
+ if (result.issues !== undefined)
324
+ return {
325
+ ok: false,
326
+ reason: `invalid input (${result.issues.map((issue) => issue.message).join("; ")})`,
327
+ };
328
+ return { ok: true, value: result.value };
329
+ } catch (error) {
330
+ return { ok: false, reason: `input validation threw (${errorMessage(error)})` };
331
+ }
332
+ };
333
+
334
+ /** Validate input and run one registered tool's handler. */
335
+ export const callTool = async (
336
+ registry: Registry,
337
+ name: string,
338
+ input: unknown,
339
+ ctx: ToolCtx,
340
+ ): Promise<CallOutcome> => {
341
+ const registered = registry.tools.get(name);
342
+ if (registered === undefined) return failure("unknown tool");
343
+ const parsed = await parseInput(registered.spec, input);
344
+ if (!parsed.ok) return parsed;
345
+ try {
346
+ // SAFETY: ToolSpec intentionally erases authored handler inputs to keep
347
+ // authoring generic-free; validation and ToolCtx establish this call boundary.
348
+ const handler = registered.spec.handler as (
349
+ value: unknown,
350
+ toolCtx: ToolCtx,
351
+ ) => Promise<unknown>;
352
+ return { ok: true, value: await handler(parsed.value, ctx) };
353
+ } catch (error) {
354
+ return failure(`handler failed (${errorMessage(error)})`);
355
+ }
356
+ };
357
+
358
+ /** Discover one dynamic tool set's current segment. Unbuilt: always fails. */
359
+ export const discoverTool = async (
360
+ _registry: Registry,
361
+ _name: string,
362
+ _binding: unknown,
363
+ ): Promise<CallOutcome> => failure("no dynamic tools yet");
@@ -1,2 +1,15 @@
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 ManifestOutcome,
13
+ type ToolCtx,
14
+ type ToolCtxEntry,
15
+ } from "../runtime/index";
@@ -1,154 +0,0 @@
1
- import {
2
- __enterFile,
3
- __leaveFile,
4
- newRegistry,
5
- setActiveRegistry
6
- } from "./chunk-LWKQDDLZ.js";
7
- import {
8
- JsonSchemaObject,
9
- PublishError
10
- } from "./chunk-B7NEUIJL.js";
11
- import {
12
- collectRegistry
13
- } from "./chunk-RZA7AXBK.js";
14
-
15
- // src/manifest/index.ts
16
- import { fileURLToPath, pathToFileURL } from "node:url";
17
- import { Crypto, Effect, FileSystem, Schema, Semaphore } from "effect";
18
- var hasStandardJsonSchema = (input) => "~standard" in input && typeof input["~standard"].jsonSchema?.input === "function";
19
- var compileInputSchema = (input, where) => {
20
- if ("~standard" in input) {
21
- if (!hasStandardJsonSchema(input))
22
- return {
23
- problem: `${where}: input is a Standard Schema without the jsonSchema extension \u2014 use a library that implements it (zod 4+, effect Schema) or pass raw JSON schema`
24
- };
25
- }
26
- try {
27
- const compiled = strictify(
28
- hasStandardJsonSchema(input) ? input["~standard"].jsonSchema.input({ target: "draft-2020-12" }) : structuredClone(input)
29
- );
30
- return { value: Schema.decodeUnknownSync(JsonSchemaObject)(compiled) };
31
- } catch (error) {
32
- return { problem: schemaPublishProblem(where, error) };
33
- }
34
- };
35
- var schemaPublishProblem = (where, error) => `${where}: input schema could not compile to JSON schema (${error instanceof Error ? error.message : String(error)})`;
36
- var strictify = (node) => {
37
- if (typeof node !== "object" || node === null) return node;
38
- if (Array.isArray(node)) return node.map(strictify);
39
- const object = { ...node };
40
- if (object.type === "object" && object.additionalProperties === void 0)
41
- object.additionalProperties = false;
42
- for (const key of ["items", "anyOf", "oneOf", "allOf"])
43
- if (key in object) object[key] = strictify(object[key]);
44
- for (const key of ["properties", "$defs"])
45
- if (object[key] !== void 0 && typeof object[key] === "object" && object[key] !== null)
46
- object[key] = Object.fromEntries(
47
- Object.entries(object[key]).map(([name, value]) => [
48
- name,
49
- strictify(value)
50
- ])
51
- );
52
- return object;
53
- };
54
- var digestOf = (content) => Effect.gen(function* () {
55
- const crypto = yield* Crypto.Crypto;
56
- const bytes = yield* Effect.orDie(
57
- crypto.digest("SHA-256", new TextEncoder().encode(content))
58
- );
59
- return Array.from(bytes.slice(0, 8), (byte) => byte.toString(16).padStart(2, "0")).join("");
60
- });
61
- var loadPermit = Semaphore.makeUnsafe(1);
62
- var runNonce = 0;
63
- var baseAppImports = /* @__PURE__ */ new Set();
64
- var moduleFailure = (where, error) => new PublishError({
65
- problems: [
66
- `${where}: failed to execute (${error instanceof Error ? error.message : String(error)})`
67
- ]
68
- });
69
- var canonicalModuleUrl = (path, where) => Effect.gen(function* () {
70
- const fs = yield* FileSystem.FileSystem;
71
- const realPath = yield* fs.realPath(path).pipe(
72
- Effect.mapError((error) => moduleFailure(where, error))
73
- );
74
- return pathToFileURL(realPath);
75
- });
76
- var importModule = (specifier, where) => Effect.tryPromise({
77
- try: () => import(specifier).then(() => void 0),
78
- catch: (error) => moduleFailure(where, error)
79
- });
80
- var recordModuleProblems = (registry, effect) => effect.pipe(
81
- Effect.catchTag(
82
- "PublishError",
83
- (error) => Effect.sync(() => {
84
- registry.problems.push(...error.problems.map((message) => ({ message })));
85
- })
86
- )
87
- );
88
- var executeModules = (appDir, files) => loadPermit.withPermit(
89
- Effect.uninterruptibleMask(
90
- (restore) => Effect.gen(function* () {
91
- const registry = newRegistry();
92
- const previous = yield* Effect.sync(() => setActiveRegistry(registry));
93
- const nonce = yield* Effect.sync(() => ++runNonce);
94
- const execute = Effect.gen(function* () {
95
- __enterFile("app.ts");
96
- yield* recordModuleProblems(
97
- registry,
98
- Effect.gen(function* () {
99
- const appUrl = yield* canonicalModuleUrl(`${appDir}/app.ts`, "app.ts");
100
- const baseUrl = appUrl.href;
101
- const specifier = baseAppImports.has(baseUrl) ? `${fileURLToPath(appUrl)}?m=${nonce}` : baseUrl;
102
- yield* importModule(specifier, "app.ts");
103
- yield* Effect.sync(() => baseAppImports.add(baseUrl));
104
- })
105
- );
106
- for (const file of files) {
107
- const where = `tools/${file}`;
108
- __enterFile(where);
109
- yield* recordModuleProblems(
110
- registry,
111
- Effect.gen(function* () {
112
- const url = yield* canonicalModuleUrl(`${appDir}/tools/${file}`, where);
113
- yield* importModule(`${fileURLToPath(url)}?m=${nonce}`, where);
114
- })
115
- );
116
- }
117
- });
118
- yield* restore(execute).pipe(
119
- Effect.ensuring(
120
- Effect.sync(() => {
121
- __leaveFile();
122
- setActiveRegistry(previous);
123
- })
124
- )
125
- );
126
- return registry;
127
- })
128
- )
129
- );
130
- var loadApp = (appDir) => Effect.gen(function* () {
131
- const fs = yield* FileSystem.FileSystem;
132
- const files = (yield* fs.readDirectory(`${appDir}/tools`).pipe(
133
- Effect.mapError((error) => new PublishError({ problems: [`tools/: ${error.message}`] }))
134
- )).filter((file) => file.endsWith(".ts")).sort();
135
- const registry = yield* executeModules(appDir, files);
136
- const collected = collectRegistry(registry, compileInputSchema);
137
- if (collected.problems.length > 0)
138
- return yield* new PublishError({ problems: collected.problems });
139
- if (!("manifest" in collected))
140
- return yield* Effect.die("validated registry projection unexpectedly missing");
141
- return {
142
- registry,
143
- result: {
144
- digest: `snap:${yield* digestOf(collected.canonical)}`,
145
- ...collected.manifest
146
- }
147
- };
148
- });
149
- var manifest = (appDir) => Effect.map(loadApp(appDir), ({ result }) => result);
150
-
151
- export {
152
- loadApp,
153
- manifest
154
- };
@@ -1,107 +0,0 @@
1
- // src/manifest/collect.ts
2
- var collectRegistry = (registry, compileInputSchema) => {
3
- const requirementKey = Symbol.for("executor/apps/requirement");
4
- const problems = registry.problems.map((problem) => problem.message);
5
- const app = registry.app;
6
- const requirementSpecOf = (value) => typeof value === "object" && value !== null && requirementKey in value ? value[requirementKey] : void 0;
7
- const canonicalJson = (value) => {
8
- const sortKeys = (node) => {
9
- if (Array.isArray(node)) return node.map(sortKeys);
10
- if (typeof node === "object" && node !== null)
11
- return Object.fromEntries(
12
- Object.entries(node).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, sortKeys(child)])
13
- );
14
- return node;
15
- };
16
- return JSON.stringify(sortKeys(value));
17
- };
18
- const claims = /* @__PURE__ */ new Map();
19
- for (const [origin, registered] of registry.services) {
20
- if (registered.file !== "app.ts") continue;
21
- const { decl } = registered;
22
- claims.set(origin, {
23
- origin,
24
- reach: decl.reach ?? [],
25
- params: decl.params ?? {},
26
- auth: decl.auth ?? {}
27
- });
28
- }
29
- const collectRequirement = (role, requirement, where) => {
30
- const spec = requirementSpecOf(requirement);
31
- if (spec === void 0) {
32
- problems.push(`${where}: requirement '${role}' is not a service/requirement value`);
33
- return void 0;
34
- }
35
- const origin = spec.service.origin;
36
- if (!claims.has(origin))
37
- problems.push(`${where}: requirement '${role}' names ${origin}, which app.ts does not declare`);
38
- if (spec.grant !== void 0 && !(spec.grant in (spec.service.auth ?? {})))
39
- problems.push(
40
- `${where}: requirement '${role}' pins grant '${spec.grant}', not a auth method on ${origin}`
41
- );
42
- return {
43
- role,
44
- service: origin,
45
- arity: spec.arity,
46
- ...spec.grant === void 0 ? {} : { grant: spec.grant },
47
- ...spec.description === void 0 ? {} : { description: spec.description }
48
- };
49
- };
50
- if (app === void 0) problems.push("app.ts must call defineApp({...})");
51
- else if (typeof app.description !== "string" || app.description.trim().length === 0)
52
- problems.push("app.ts: app description must not be empty");
53
- for (const [file, summary] of registry.toolFiles) {
54
- if (summary.definitions > 1 && summary.hasUnnamedTool)
55
- problems.push(`${file}: multi-tool files must name every tool`);
56
- }
57
- const tools = [];
58
- if (app !== void 0) {
59
- for (const [name, registered] of registry.tools) {
60
- const { spec, file } = registered;
61
- const address = `${app.key}/${name}`;
62
- if (typeof spec.description !== "string" || spec.description.trim().length === 0)
63
- problems.push(`${file}: tool '${name}' description must not be empty`);
64
- const requires = Object.entries(spec.requirements ?? {}).flatMap(([role, requirement]) => {
65
- const collected = collectRequirement(role, requirement, address);
66
- return collected === void 0 ? [] : [collected];
67
- }).sort((left, right) => left.role.localeCompare(right.role));
68
- let compiled;
69
- try {
70
- compiled = compileInputSchema(spec.input, address);
71
- } catch (error) {
72
- compiled = {
73
- problem: `${address}: input schema could not compile to JSON schema (${error instanceof Error ? error.message : String(error)})`
74
- };
75
- }
76
- if ("problem" in compiled) {
77
- problems.push(compiled.problem);
78
- continue;
79
- }
80
- tools.push({
81
- address,
82
- name,
83
- description: spec.description,
84
- inputSchema: compiled.value,
85
- annotations: spec.annotations ?? {},
86
- requires,
87
- file
88
- });
89
- }
90
- }
91
- if (problems.length > 0 || app === void 0) return { problems };
92
- const services = [...claims.values()].sort(
93
- (left, right) => left.origin.localeCompare(right.origin)
94
- );
95
- const sortedTools = [...tools].sort((left, right) => left.address.localeCompare(right.address));
96
- const appResult = { key: app.key, label: app.label, description: app.description };
97
- const manifest = { app: appResult, services, tools: sortedTools };
98
- return {
99
- problems: [],
100
- manifest,
101
- canonical: canonicalJson(manifest)
102
- };
103
- };
104
-
105
- export {
106
- collectRegistry
107
- };