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
@@ -5,6 +5,24 @@
5
5
  */
6
6
 
7
7
  // ── auth options (auth methods) ──────────────────────────────────────────────
8
+ //
9
+ // An auth method states the credential's kind, never its placement.
10
+ // bearer/basic/oauth have protocol-defined spend semantics. secrets()
11
+ // is the anonymous bag whose spend must be authored at the surface.
12
+
13
+ /** A single opaque credential with RFC 6750 bearer semantics. */
14
+ export interface BearerMethod {
15
+ readonly kind: "bearer";
16
+ readonly label: string;
17
+ readonly help?: string;
18
+ }
19
+
20
+ /** An RFC 7617 username/password pair. */
21
+ export interface BasicMethod {
22
+ readonly kind: "basic";
23
+ readonly label: string;
24
+ readonly help?: string;
25
+ }
8
26
 
9
27
  export interface SecretsMethod<
10
28
  F extends Readonly<Record<string, SecretField>> = Readonly<Record<string, SecretField>>,
@@ -24,7 +42,7 @@ export interface SecretField {
24
42
  }
25
43
 
26
44
  /**
27
- * An oauth auth method is a secrets-shaped auth method with acquisition strategies
45
+ * An oauth auth method is a bearer method with acquisition strategies
28
46
  * layered on: the material contract is always {token, refresh_token?,
29
47
  * expires_at?}, and the host fills it by the best strategy it can offer
30
48
  * — authorization-code ceremony, device-code ceremony, or paste. Paste
@@ -49,7 +67,17 @@ export interface OAuthMethod {
49
67
  readonly paste?: false;
50
68
  }
51
69
 
52
- export type AuthMethod = SecretsMethod | OAuthMethod;
70
+ export type AuthMethod = BearerMethod | BasicMethod | SecretsMethod | OAuthMethod;
71
+
72
+ export const bearer = (opts: {
73
+ readonly label: string;
74
+ readonly help?: string;
75
+ }): BearerMethod => ({ kind: "bearer", ...opts });
76
+
77
+ export const basic = (opts: {
78
+ readonly label: string;
79
+ readonly help?: string;
80
+ }): BasicMethod => ({ kind: "basic", ...opts });
53
81
 
54
82
  export const secrets = <const F extends Readonly<Record<string, SecretField>>>(opts: {
55
83
  readonly label: string;
@@ -99,23 +127,44 @@ export interface RequirementSpec {
99
127
 
100
128
  const RequirementKey: unique symbol = Symbol.for("executor/apps/requirement");
101
129
 
130
+ declare const RequirementPhantom: unique symbol;
131
+
132
+ /** Requirement arity, tracked at the type level so handler contexts
133
+ * know whether a role arrives as one entry, maybe-one, or a list. */
134
+ export type RequirementArity = "one" | "optional" | "many";
135
+
102
136
  /**
103
137
  * A requirement value in a tool's `requirements` record. A Service IS
104
138
  * a requirement (bare value = arity "one"); .optional()/.array()
105
139
  * return terminal Requirements so nonsense chains do not typecheck.
140
+ * The phantom property is never assigned — it exists so auth-compose
141
+ * callbacks and handler contexts can recover the service's grants, a
142
+ * .grant() pin, and the arity at the type level.
106
143
  */
107
- export interface Requirement<D extends ServiceDecl = ServiceDecl> {
144
+ export interface Requirement<
145
+ D extends ServiceDecl = ServiceDecl,
146
+ G extends string | undefined = string | undefined,
147
+ A extends RequirementArity = RequirementArity,
148
+ > {
108
149
  readonly [RequirementKey]: RequirementSpec;
109
- describe(text: string): Requirement<D>;
150
+ readonly [RequirementPhantom]?: {
151
+ readonly decl: D;
152
+ readonly pinned: G;
153
+ readonly arity: A;
154
+ };
155
+ describe(text: string): Requirement<D, G, A>;
110
156
  }
111
157
 
112
- export interface Service<D extends ServiceDecl = ServiceDecl> extends Requirement<D> {
158
+ export interface Service<
159
+ D extends ServiceDecl = ServiceDecl,
160
+ G extends (keyof NonNullable<D["auth"]> & string) | undefined = undefined,
161
+ > extends Requirement<D, G, "one"> {
113
162
  readonly decl: D;
114
- optional(): Requirement<D>;
115
- array(): Requirement<D>;
163
+ optional(): Requirement<D, G, "optional">;
164
+ array(): Requirement<D, G, "many">;
116
165
  /** Pin one auth method: this tool supports only the named grant. */
117
- grant(name: keyof NonNullable<D["auth"]> & string): Service<D>;
118
- describe(text: string): Service<D>;
166
+ grant<N extends keyof NonNullable<D["auth"]> & string>(name: N): Service<D, N>;
167
+ describe(text: string): Service<D, G>;
119
168
  }
120
169
 
121
170
  /** A publish-time authoring problem accumulated without interrupting defines. */
@@ -125,39 +174,217 @@ export interface Problem {
125
174
 
126
175
  // ── dynamic tool sets ─────────────────────────────────────────────────
127
176
 
177
+ /** One connected role, discriminated by the declared auth method kind. */
178
+ export type AuthRoleEntry<D extends ServiceDecl> = {
179
+ [N in keyof NonNullable<D["auth"]> & string]: NonNullable<D["auth"]>[N] extends SecretsMethod<
180
+ infer F
181
+ >
182
+ ? {
183
+ readonly grant: N;
184
+ readonly kind: "secrets";
185
+ readonly secrets: { readonly [K in keyof F & string]: string };
186
+ }
187
+ : NonNullable<D["auth"]>[N] extends BasicMethod
188
+ ? {
189
+ readonly grant: N;
190
+ readonly kind: "basic";
191
+ readonly username: string;
192
+ readonly password: string;
193
+ }
194
+ : { readonly grant: N; readonly kind: "bearer" | "oauth"; readonly token: string };
195
+ }[keyof NonNullable<D["auth"]> & string];
196
+
197
+ /** Kind-discriminated credential material accepted by surface helpers. */
198
+ export type RoleMaterial =
199
+ | { readonly kind: "bearer" | "oauth"; readonly token: string }
200
+ | { readonly kind: "basic"; readonly username: string; readonly password: string }
201
+ | { readonly kind: "secrets"; readonly secrets: Readonly<Record<string, string>> };
202
+
203
+ /** Recover the connected role type produced by a service requirement. */
204
+ export type RoleOf<
205
+ S,
206
+ G extends string | undefined = undefined,
207
+ > = S extends Requirement<infer D, infer Pin>
208
+ ? [G] extends [string]
209
+ ? Extract<AuthRoleEntry<D>, { grant: G }>
210
+ : Pin extends string
211
+ ? Extract<AuthRoleEntry<D>, { grant: Pin }>
212
+ : AuthRoleEntry<D>
213
+ : never;
214
+
215
+ /** Role record received by an authored authentication placement callback. */
216
+ export type AuthRoles<Reqs extends Readonly<Record<string, Requirement>>> = {
217
+ readonly [K in keyof Reqs]: NonNullable<Reqs[K][typeof RequirementPhantom]> extends {
218
+ readonly decl: infer D extends ServiceDecl;
219
+ readonly pinned: infer G;
220
+ }
221
+ ? G extends string
222
+ ? Extract<AuthRoleEntry<D>, { grant: G }>
223
+ : AuthRoleEntry<D>
224
+ : never;
225
+ };
226
+
227
+ /** Request placement returned by an authored dynamic auth callback. */
228
+ export interface AuthPlacement {
229
+ readonly headers?: HeadersInit;
230
+ readonly searchParams?: URLSearchParams | Readonly<Record<string, string>>;
231
+ }
232
+
233
+ export type AuthComposeFn<Reqs extends Readonly<Record<string, Requirement>> = never> = (
234
+ roles: AuthRoles<Reqs>,
235
+ ) => AuthPlacement | undefined | void;
236
+
237
+ export type HasSecretsGrant<D extends ServiceDecl> = true extends (
238
+ NonNullable<D["auth"]>[keyof NonNullable<D["auth"]>] extends infer M
239
+ ? M extends SecretsMethod
240
+ ? true
241
+ : false
242
+ : never
243
+ )
244
+ ? true
245
+ : false;
246
+
247
+ /** Whether an OpenAPI wrapper must state credential placement explicitly. */
248
+ export type RequiresAuthCompose<Reqs extends Readonly<Record<string, Requirement>>> = true extends {
249
+ [K in keyof Reqs]: NonNullable<Reqs[K][typeof RequirementPhantom]> extends {
250
+ readonly decl: infer D extends ServiceDecl;
251
+ readonly pinned: infer G;
252
+ }
253
+ ? G extends keyof NonNullable<D["auth"]> & string
254
+ ? NonNullable<D["auth"]>[G] extends SecretsMethod
255
+ ? true
256
+ : false
257
+ : HasSecretsGrant<D>
258
+ : never;
259
+ }[keyof Reqs]
260
+ ? true
261
+ : false;
262
+
263
+ /** Record a wrapper-specific publish problem on the active authored file. @internal */
264
+ export const recordAuthoringProblem = (message: string): void => {
265
+ const file = sourceFileFor("wrapper");
266
+ activeRegistry.problems.push({ message: `${file}: ${message}` });
267
+ };
268
+
269
+ /** Operation curation for a dynamic tool set. Exactly one of allow/block. */
270
+ export type OperationSelection =
271
+ | { readonly allow: readonly string[]; readonly block?: undefined }
272
+ | { readonly block: readonly string[]; readonly allow?: undefined };
273
+
274
+ /** One tool emitted by a dynamic discovery pass. */
275
+ export interface DiscoveredToolSpec {
276
+ readonly name: string;
277
+ readonly description: string;
278
+ readonly input: SchemaLike;
279
+ readonly annotations?: ToolAnnotations;
280
+ readonly detail?: unknown;
281
+ }
282
+
128
283
  /**
129
- * A dynamic tool-set claim: tools discovered from a live surface (an
130
- * MCP server's tool list, an OpenAPI document) rather than authored one
131
- * file at a time. The claim is recorded at publish time; discovery runs
132
- * later, per connection — an app with zero static tools and one claim
133
- * saves green, and listing never blocks on an upstream.
284
+ * The generic authored shape of a dynamic pair: ctx is inferred from
285
+ * the requirements record exactly as for static tools. Discovered
286
+ * inputs are runtime data (the schema arrives from the upstream at
287
+ * discover time), so `call`'s input is honestly `unknown` — the
288
+ * runtime validates against the discovered descriptor before calling.
134
289
  */
135
- export type DynamicClaimSpec =
136
- | {
137
- readonly kind: "mcp";
138
- readonly requirement: RequirementSpec;
139
- /** MCP endpoint URL. Absent = the service origin at its root path. */
140
- readonly endpoint?: string;
141
- }
142
- | {
143
- readonly kind: "openapi";
144
- readonly requirement: RequirementSpec;
145
- /** OpenAPI document: an absolute URL or an app-relative path. */
146
- readonly spec: string;
147
- };
290
+ export interface DynamicToolsDecl<
291
+ Reqs extends Readonly<Record<string, Requirement>> | undefined =
292
+ | Readonly<Record<string, Requirement>>
293
+ | undefined,
294
+ > {
295
+ readonly description: string;
296
+ readonly requirements?: Reqs;
297
+ discover(ctx: HandlerCtx<Reqs>): Promise<readonly DiscoveredToolSpec[]>;
298
+ call(
299
+ tool: { readonly name: string; readonly detail: unknown },
300
+ input: unknown,
301
+ ctx: HandlerCtx<Reqs>,
302
+ ): Promise<unknown>;
303
+ }
304
+
305
+ /**
306
+ * The erased registry row of a dynamic pair — same storage boundary
307
+ * as ToolSpec. App code: runs where handlers run (in-process locally,
308
+ * inside the isolate on workerd) and places credentials itself from
309
+ * ctx, like any handler. `discover` interrogates the tool source;
310
+ * `call` executes one discovered tool from the identity and detail
311
+ * discover emitted.
312
+ */
313
+ export interface DynamicToolsSpec {
314
+ readonly description: string;
315
+ readonly requirements?: Readonly<Record<string, Requirement>>;
316
+ discover(ctx: never): Promise<readonly DiscoveredToolSpec[]>;
317
+ call(
318
+ tool: { readonly name: string; readonly detail: unknown },
319
+ input: never,
320
+ ctx: never,
321
+ ): Promise<unknown>;
322
+ }
148
323
 
149
324
  const DynamicKey: unique symbol = Symbol.for("executor/apps/dynamic");
150
325
 
151
- /** The value fromMCP/fromOpenAPI return: an opaque claim handle a tool
152
- * file default-exports. The spec is undefined when the claim was
326
+ /** The value defineDynamicTools returns: an opaque handle a tool file
327
+ * default-exports. The spec is undefined when the definition was
153
328
  * invalid (a problem was recorded instead). */
154
329
  export interface DynamicToolSet {
155
- readonly [DynamicKey]: DynamicClaimSpec | undefined;
330
+ readonly [DynamicKey]: DynamicToolsSpec | undefined;
156
331
  }
157
332
 
158
- /** A registered dynamic claim and its source attribution. @internal */
159
- export interface RegisteredDynamic {
160
- readonly spec: DynamicClaimSpec;
333
+ /** Reads a dynamic spec from a tool file's default export. @internal */
334
+ export const dynamicSpecOf = (value: unknown): DynamicToolsSpec | undefined =>
335
+ typeof value === "object" && value !== null && DynamicKey in value
336
+ ? (value as DynamicToolSet)[DynamicKey]
337
+ : undefined;
338
+
339
+ /**
340
+ * The dynamic-tools primitive: an author-written discover/call pair
341
+ * whose tool list is interrogated at runtime, per connection, instead
342
+ * of authored one file at a time. The file's name becomes the address
343
+ * segment (appKey/<stem>/<toolName>); tools land in the catalog after
344
+ * discovery and never block a save. fromMCP/fromOpenAPI are shipped
345
+ * pairs over this same primitive.
346
+ */
347
+ export const defineDynamicTools = <
348
+ const Reqs extends Readonly<Record<string, Requirement>> | undefined = undefined,
349
+ >(decl: DynamicToolsDecl<Reqs>): DynamicToolSet => {
350
+ // SAFETY: same erasure boundary as defineTool — members identical,
351
+ // handler parameter types narrow to the registry's untyped floor.
352
+ const spec = decl as unknown as DynamicToolsSpec;
353
+ const file = sourceFileFor("defineDynamicTools(...)");
354
+ const problems: string[] = [];
355
+
356
+ if (typeof spec.description !== "string" || spec.description.trim().length === 0)
357
+ problems.push("dynamic tool set description must not be empty");
358
+
359
+ const roles = Object.entries(spec.requirements ?? {});
360
+ if (roles.length > 1) problems.push("a dynamic tool set takes exactly one requirement role");
361
+ else if (roles.length === 1) {
362
+ const [role, value] = roles[0]!;
363
+ const requirementSpec = requirementSpecOf(value);
364
+ if (requirementSpec === undefined)
365
+ problems.push(`requirement '${role}' is not a service/requirement value`);
366
+ else if (requirementSpec.arity === "many")
367
+ problems.push(
368
+ `requirement '${role}' cannot be .array() — multiple accounts expand the catalog per connection, never merge one list`,
369
+ );
370
+ }
371
+
372
+ if (problems.length > 0) {
373
+ activeRegistry.problems.push(...problems.map((message) => ({ message: `${file}: ${message}` })));
374
+ return { [DynamicKey]: undefined };
375
+ }
376
+
377
+ if (activeRegistry.dynamic.has(file))
378
+ activeRegistry.problems.push({
379
+ message: `${file}: one dynamic tool set per file — its address segment is the file's name`,
380
+ });
381
+ else activeRegistry.dynamic.set(file, { spec, file });
382
+ return { [DynamicKey]: spec };
383
+ };
384
+
385
+ /** A registered dynamic tool set and its source attribution. @internal */
386
+ export interface DynamicRegistration {
387
+ readonly spec: DynamicToolsSpec;
161
388
  readonly file: string;
162
389
  }
163
390
 
@@ -178,7 +405,7 @@ export interface Registry {
178
405
  app: AppManifest | undefined;
179
406
  readonly services: Map<string, RegisteredService>;
180
407
  readonly tools: Map<string, RegisteredTool>;
181
- readonly dynamic: Map<string, RegisteredDynamic>;
408
+ readonly dynamic: Map<string, DynamicRegistration>;
182
409
  readonly problems: Problem[];
183
410
  readonly toolFiles: Map<
184
411
  string,
@@ -224,22 +451,29 @@ const sourceFileFor = (claim: string): string => {
224
451
  return "<unattributed>";
225
452
  };
226
453
 
227
- const makeTerminal = <D extends ServiceDecl>(spec: RequirementSpec): Requirement<D> => ({
454
+ const makeTerminal = <
455
+ D extends ServiceDecl,
456
+ G extends string | undefined,
457
+ A extends RequirementArity,
458
+ >(spec: RequirementSpec): Requirement<D, G, A> => ({
228
459
  [RequirementKey]: spec,
229
- describe: (text) => makeTerminal<D>({ ...spec, description: text }),
460
+ describe: (text) => makeTerminal<D, G, A>({ ...spec, description: text }),
230
461
  });
231
462
 
232
463
  export const service = <const D extends ServiceDecl>(decl: D): Service<D> => {
233
464
  const file = sourceFileFor("service(...)");
234
- const make = (spec: RequirementSpec): Service<D> => ({
465
+ const make = <G extends (keyof NonNullable<D["auth"]> & string) | undefined>(
466
+ spec: RequirementSpec,
467
+ ): Service<D, G> => ({
235
468
  [RequirementKey]: spec,
236
469
  decl,
237
- optional: () => makeTerminal<D>({ ...spec, arity: "optional" }),
238
- array: () => makeTerminal<D>({ ...spec, arity: "many" }),
239
- grant: (name) => make({ ...spec, grant: name }),
240
- describe: (text) => make({ ...spec, description: text }),
470
+ optional: () => makeTerminal<D, G, "optional">({ ...spec, arity: "optional" }),
471
+ array: () => makeTerminal<D, G, "many">({ ...spec, arity: "many" }),
472
+ grant: <N extends keyof NonNullable<D["auth"]> & string>(name: N) =>
473
+ make<N>({ ...spec, grant: name }),
474
+ describe: (text) => make<G>({ ...spec, description: text }),
241
475
  });
242
- const value = make({ service: decl, arity: "one" });
476
+ const value = make<undefined>({ service: decl, arity: "one" });
243
477
  if (!activeRegistry.services.has(decl.origin))
244
478
  activeRegistry.services.set(decl.origin, { decl, file });
245
479
  return value;
@@ -284,6 +518,47 @@ export const defineApp = (manifest: {
284
518
  /** Any Standard Schema (zod preferred) or a raw JSON-schema object. */
285
519
  export type SchemaLike = object;
286
520
 
521
+ /**
522
+ * The validated value a Standard Schema produces, recovered from its
523
+ * phantom types (zod 4 and effect Schema both publish them). A raw
524
+ * JSON-schema object carries no phantom, so its input is `unknown` —
525
+ * stated weakness, not silent weakness.
526
+ */
527
+ export type InferSchema<S> = S extends {
528
+ readonly "~standard": { readonly types?: infer T };
529
+ }
530
+ ? NonNullable<T> extends { readonly output: infer O }
531
+ ? O
532
+ : unknown
533
+ : unknown;
534
+
535
+ /**
536
+ * The capability a handler receives for one connected requirement
537
+ * role: the grant's kind-discriminated material (narrowed by any
538
+ * .grant() pin) plus the spending fetch. Arity shapes the value —
539
+ * `.optional()` may be absent, `.array()` is every connected account.
540
+ */
541
+ export type HandlerRole<R> = R extends Requirement<infer D, infer G, infer A>
542
+ ? [A] extends ["many"]
543
+ ? ReadonlyArray<HandlerRoleEntry<D, G>>
544
+ : [A] extends ["optional"]
545
+ ? HandlerRoleEntry<D, G> | undefined
546
+ : HandlerRoleEntry<D, G>
547
+ : never;
548
+
549
+ /** One connected role entry: declared-auth material + capability fetch. */
550
+ export type HandlerRoleEntry<
551
+ D extends ServiceDecl,
552
+ G extends string | undefined,
553
+ > = ([G] extends [string] ? Extract<AuthRoleEntry<D>, { grant: G }> : AuthRoleEntry<D>) & {
554
+ fetch(path: string, init?: RequestInit): Promise<Response>;
555
+ };
556
+
557
+ /** The ctx record a handler receives, keyed by requirement role. */
558
+ export type HandlerCtx<Reqs> = [Reqs] extends [Readonly<Record<string, Requirement>>]
559
+ ? { readonly [K in keyof Reqs]: HandlerRole<Reqs[K]> }
560
+ : Readonly<Record<never, never>>;
561
+
287
562
  export interface ToolAnnotations {
288
563
  readonly readOnly?: boolean;
289
564
  readonly destructive?: boolean;
@@ -291,8 +566,40 @@ export interface ToolAnnotations {
291
566
  readonly scopes?: readonly string[];
292
567
  }
293
568
 
294
- export interface ToolSpec {
569
+ /**
570
+ * The generic authored shape: input/ctx/output are all inferred — the
571
+ * handler's input from the Standard Schema, ctx from the requirements
572
+ * record (grants, pins, arity), and the return type checked against
573
+ * the output schema when one is stated.
574
+ */
575
+ export interface ToolDecl<
576
+ Input extends SchemaLike = SchemaLike,
577
+ Reqs extends Readonly<Record<string, Requirement>> | undefined =
578
+ | Readonly<Record<string, Requirement>>
579
+ | undefined,
580
+ Output extends SchemaLike | undefined = SchemaLike | undefined,
581
+ > {
295
582
  /** Address segment. Single-tool files may omit it (filename). */
583
+ readonly name?: string;
584
+ readonly description: string;
585
+ readonly requirements?: Reqs;
586
+ readonly input: Input;
587
+ readonly output?: Output;
588
+ readonly annotations?: ToolAnnotations;
589
+ handler(
590
+ input: InferSchema<Input>,
591
+ ctx: HandlerCtx<Reqs>,
592
+ ): Promise<Output extends SchemaLike ? InferSchema<Output> : unknown>;
593
+ }
594
+
595
+ /**
596
+ * The erased registry row. Registration drops the generics — storage
597
+ * is untyped by design, the same boundary every schema library draws
598
+ * between its typed builders and its runtime representation. The
599
+ * runtime re-establishes safety by validating input before the
600
+ * handler runs and shaping ctx from resolved connections.
601
+ */
602
+ export interface ToolSpec {
296
603
  readonly name?: string;
297
604
  readonly description: string;
298
605
  readonly requirements?: Readonly<Record<string, Requirement>>;
@@ -308,7 +615,18 @@ export interface Tool {
308
615
  readonly [ToolKey]: ToolSpec;
309
616
  }
310
617
 
311
- export const defineTool = (spec: ToolSpec): Tool => {
618
+ export const defineTool = <
619
+ const Input extends SchemaLike,
620
+ const Reqs extends Readonly<Record<string, Requirement>> | undefined = undefined,
621
+ const Output extends SchemaLike | undefined = undefined,
622
+ >(decl: ToolDecl<Input, Reqs, Output>): Tool => {
623
+ // SAFETY: the erased row keeps the same members; only the handler's
624
+ // parameter types narrow (never is the bottom of every inference).
625
+ const spec = decl as unknown as ToolSpec;
626
+ return registerTool(spec);
627
+ };
628
+
629
+ const registerTool = (spec: ToolSpec): Tool => {
312
630
  const file = sourceFileFor("defineTool(...)");
313
631
  const value: Tool = { [ToolKey]: spec };
314
632
  const fileName = file.slice(file.lastIndexOf("/") + 1);
@@ -0,0 +1,26 @@
1
+ /** HTTP credential placement helpers for kind-bearing connected roles. */
2
+
3
+ import type { RoleMaterial } from "../authoring/index";
4
+
5
+ export type HttpSpendableRole = Extract<
6
+ RoleMaterial,
7
+ { kind: "bearer" | "oauth" | "basic" }
8
+ >;
9
+
10
+ /** Build the protocol-defined Authorization header for a connected role. */
11
+ export const authHeaders = (role: HttpSpendableRole): { readonly authorization: string } => {
12
+ switch (role.kind) {
13
+ case "bearer":
14
+ case "oauth":
15
+ return { authorization: `Bearer ${role.token}` };
16
+ case "basic":
17
+ return { authorization: `Basic ${base64(`${role.username}:${role.password}`)}` };
18
+ }
19
+ };
20
+
21
+ const base64 = (value: string): string => {
22
+ const bytes = new TextEncoder().encode(value);
23
+ let binary = "";
24
+ for (const byte of bytes) binary += String.fromCharCode(byte);
25
+ return btoa(binary);
26
+ };
package/src/index.ts CHANGED
@@ -1,9 +1,4 @@
1
- /**
2
- * The authoring surface: what app code imports. Pure and synchronous —
3
- * no effect dependency, no node builtins — so authored files run
4
- * unmodified in any execution environment, including sandboxed workers.
5
- *
6
- * Host-side machinery lives behind subpaths: "apps/manifest" loads and
7
- * validates authored sources, "apps/runner" executes them.
8
- */
9
1
  export * from "./authoring/index";
2
+ export * from "./http/index";
3
+ export * from "./mcp/index";
4
+ export * from "./openapi/index";
@@ -37,12 +37,21 @@ export interface ManifestTool {
37
37
  readonly file: string;
38
38
  }
39
39
 
40
+ /** A dynamic address segment projected without endpoint or specification data. */
41
+ export interface ManifestDynamic {
42
+ readonly segment: string;
43
+ readonly description: string;
44
+ readonly requires: readonly CollectedRequirement[];
45
+ readonly file: string;
46
+ }
47
+
40
48
  /** The deterministic public description of an executor app. */
41
49
  export interface AppManifestResult {
42
50
  readonly digest: string;
43
51
  readonly app: { readonly key: string; readonly label: string; readonly description: string };
44
52
  readonly services: readonly CollectedClaim[];
45
53
  readonly tools: readonly ManifestTool[];
54
+ readonly dynamic: readonly ManifestDynamic[];
46
55
  }
47
56
 
48
57
  /** All publish-time authoring problems from one registry load. */