apps 1.0.0-beta.2 → 1.0.0-beta.4

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 (41) hide show
  1. package/dist/authoring/index.d.ts +254 -37
  2. package/dist/authoring/index.js +11 -1
  3. package/dist/{chunk-OGCPVWAZ.js → chunk-2DVKSPQH.js} +134 -13
  4. package/dist/{chunk-NFLNA2SC.js → chunk-4OZFMCTT.js} +7 -1
  5. package/dist/{chunk-LWKQDDLZ.js → chunk-535V5K64.js} +61 -4
  6. package/dist/chunk-DP6UJCMX.js +27 -0
  7. package/dist/chunk-QA72XJZU.js +41 -0
  8. package/dist/chunk-S5KFP4JQ.js +20 -0
  9. package/dist/{chunk-646MS7DD.js → chunk-XYT6GZ7M.js} +7 -7
  10. package/dist/http/index.d.ts +9 -0
  11. package/dist/http/index.js +6 -0
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.js +23 -1
  14. package/dist/manifest/contract.d.ts +8 -0
  15. package/dist/manifest/index.js +3 -3
  16. package/dist/mcp/client.d.ts +9 -0
  17. package/dist/mcp/index.d.ts +11 -0
  18. package/dist/mcp/index.js +7 -0
  19. package/dist/openapi/index.d.ts +14 -0
  20. package/dist/openapi/index.js +7 -0
  21. package/dist/openapi/mapper.d.ts +7 -0
  22. package/dist/runner/index.js +29 -15
  23. package/dist/runner/protocol.d.ts +18 -5
  24. package/dist/runner/protocol.js +5 -3
  25. package/dist/runtime/index.d.ts +42 -5
  26. package/dist/runtime/index.js +3 -1
  27. package/dist/workerd/index.d.ts +1 -1
  28. package/dist/workerd/index.js +1 -1
  29. package/package.json +13 -6
  30. package/src/authoring/index.ts +395 -49
  31. package/src/http/index.ts +26 -0
  32. package/src/index.ts +3 -0
  33. package/src/manifest/contract.ts +9 -0
  34. package/src/mcp/client.ts +59 -0
  35. package/src/mcp/index.ts +42 -0
  36. package/src/openapi/index.ts +70 -0
  37. package/src/openapi/mapper.ts +25 -0
  38. package/src/runner/index.ts +35 -12
  39. package/src/runner/protocol.ts +32 -7
  40. package/src/runtime/index.ts +216 -25
  41. package/src/workerd/index.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apps",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.4",
4
4
  "description": "Author executor apps: declare services, auth, and tools in plain TypeScript; the executor platform runs, validates, and serves them.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,6 +44,18 @@
44
44
  "types": "./dist/runner/protocol.d.ts",
45
45
  "default": "./dist/runner/protocol.js"
46
46
  },
47
+ "./mcp": {
48
+ "types": "./dist/mcp/index.d.ts",
49
+ "default": "./dist/mcp/index.js"
50
+ },
51
+ "./openapi": {
52
+ "types": "./dist/openapi/index.d.ts",
53
+ "default": "./dist/openapi/index.js"
54
+ },
55
+ "./http": {
56
+ "types": "./dist/http/index.d.ts",
57
+ "default": "./dist/http/index.js"
58
+ },
47
59
  "./workerd": {
48
60
  "types": "./dist/workerd/index.d.ts",
49
61
  "default": "./dist/workerd/index.js"
@@ -56,10 +68,5 @@
56
68
  "effect": {
57
69
  "optional": false
58
70
  }
59
- },
60
- "devDependencies": {
61
- "@types/node": "22",
62
- "esbuild": "0.25",
63
- "typescript": "5.9.3"
64
71
  }
65
72
  }
@@ -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;
@@ -98,23 +126,68 @@ export interface RequirementSpec {
98
126
  }
99
127
 
100
128
  const RequirementKey: unique symbol = Symbol.for("executor/apps/requirement");
129
+ const ServiceDeclKey: unique symbol = Symbol.for("executor/apps/service-decl");
130
+
131
+ declare const RequirementPhantom: unique symbol;
132
+
133
+ /** Requirement arity, tracked at the type level so handler contexts
134
+ * know whether a role arrives as one entry, maybe-one, or a list. */
135
+ export type RequirementArity = "one" | "optional" | "many";
101
136
 
102
137
  /**
103
138
  * A requirement value in a tool's `requirements` record. A Service IS
104
139
  * a requirement (bare value = arity "one"); .optional()/.array()
105
140
  * return terminal Requirements so nonsense chains do not typecheck.
141
+ * The phantom property is never assigned — it exists so auth-compose
142
+ * callbacks and handler contexts can recover the service's grants, an
143
+ * auth-door pin, and the arity at the type level.
106
144
  */
107
- export interface Requirement<D extends ServiceDecl = ServiceDecl> {
145
+ export interface Requirement<
146
+ D extends ServiceDecl = ServiceDecl,
147
+ G extends string | undefined = string | undefined,
148
+ A extends RequirementArity = RequirementArity,
149
+ > {
108
150
  readonly [RequirementKey]: RequirementSpec;
109
- describe(text: string): Requirement<D>;
151
+ readonly [RequirementPhantom]?: {
152
+ readonly decl: D;
153
+ readonly pinned: G;
154
+ readonly arity: A;
155
+ };
156
+ describe(text: string): Requirement<D, G, A>;
157
+ }
158
+
159
+ /**
160
+ * A grant-pinned requirement: what `service.auth.<name>` returns. A
161
+ * deliberately narrower type than Service — it can shape arity and
162
+ * describe itself, but carries no `auth` (pinning twice is nonsense)
163
+ * and no declaration access. Pinning is property access because the
164
+ * grants are static facts of the declaration: `github.auth.pat`
165
+ * autocompletes and a wrong name fails compilation.
166
+ */
167
+ export interface PinnedRequirement<
168
+ D extends ServiceDecl = ServiceDecl,
169
+ N extends string = string,
170
+ > extends Requirement<D, N, "one"> {
171
+ optional(): Requirement<D, N, "optional">;
172
+ array(): Requirement<D, N, "many">;
173
+ describe(text: string): PinnedRequirement<D, N>;
110
174
  }
111
175
 
112
- export interface Service<D extends ServiceDecl = ServiceDecl> extends Requirement<D> {
113
- readonly decl: D;
114
- optional(): Requirement<D>;
115
- array(): Requirement<D>;
116
- /** Pin one auth method: this tool supports only the named grant. */
117
- grant(name: keyof NonNullable<D["auth"]> & string): Service<D>;
176
+ /** The grant-pinned doors of a service, keyed by declared auth method. */
177
+ export type ServiceAuth<D extends ServiceDecl> = {
178
+ readonly [N in keyof NonNullable<D["auth"]> & string]: PinnedRequirement<D, N>;
179
+ };
180
+
181
+ export interface Service<D extends ServiceDecl = ServiceDecl>
182
+ extends Requirement<D, undefined, "one"> {
183
+ /** Symbol-keyed so author IntelliSense shows only authoring verbs;
184
+ * the declaration is plumbing the manifest reads at collection. */
185
+ readonly [ServiceDeclKey]: D;
186
+ /** Pin one auth method: `github.auth.pat` is this tool supporting
187
+ * only that grant. */
188
+ readonly auth: ServiceAuth<D>;
189
+ optional(): Requirement<D, undefined, "optional">;
190
+ array(): Requirement<D, undefined, "many">;
118
191
  describe(text: string): Service<D>;
119
192
  }
120
193
 
@@ -125,39 +198,213 @@ export interface Problem {
125
198
 
126
199
  // ── dynamic tool sets ─────────────────────────────────────────────────
127
200
 
201
+ /** One connected role, discriminated by the declared auth method kind. */
202
+ export type AuthRoleEntry<D extends ServiceDecl> = {
203
+ [N in keyof NonNullable<D["auth"]> & string]: NonNullable<D["auth"]>[N] extends SecretsMethod<
204
+ infer F
205
+ >
206
+ ? {
207
+ readonly grant: N;
208
+ readonly kind: "secrets";
209
+ readonly secrets: { readonly [K in keyof F & string]: string };
210
+ }
211
+ : NonNullable<D["auth"]>[N] extends BasicMethod
212
+ ? {
213
+ readonly grant: N;
214
+ readonly kind: "basic";
215
+ readonly username: string;
216
+ readonly password: string;
217
+ }
218
+ : { readonly grant: N; readonly kind: "bearer" | "oauth"; readonly token: string };
219
+ }[keyof NonNullable<D["auth"]> & string];
220
+
221
+ /** Kind-discriminated credential material accepted by surface helpers. */
222
+ export type RoleMaterial =
223
+ | { readonly kind: "bearer" | "oauth"; readonly token: string }
224
+ | { readonly kind: "basic"; readonly username: string; readonly password: string }
225
+ | { readonly kind: "secrets"; readonly secrets: Readonly<Record<string, string>> };
226
+
227
+ /** Recover the connected role type produced by a service requirement. */
228
+ export type RoleOf<
229
+ S,
230
+ G extends string | undefined = undefined,
231
+ > = S extends Requirement<infer D, infer Pin>
232
+ ? [G] extends [string]
233
+ ? Extract<AuthRoleEntry<D>, { grant: G }>
234
+ : Pin extends string
235
+ ? Extract<AuthRoleEntry<D>, { grant: Pin }>
236
+ : AuthRoleEntry<D>
237
+ : never;
238
+
239
+ /** Role record received by an authored authentication placement callback. */
240
+ export type AuthRoles<Reqs extends Readonly<Record<string, Requirement>>> = {
241
+ readonly [K in keyof Reqs]: NonNullable<Reqs[K][typeof RequirementPhantom]> extends {
242
+ readonly decl: infer D extends ServiceDecl;
243
+ readonly pinned: infer G;
244
+ }
245
+ ? G extends string
246
+ ? Extract<AuthRoleEntry<D>, { grant: G }>
247
+ : AuthRoleEntry<D>
248
+ : never;
249
+ };
250
+
251
+ /** Request placement returned by an authored dynamic auth callback. */
252
+ export interface AuthPlacement {
253
+ readonly headers?: HeadersInit;
254
+ readonly searchParams?: URLSearchParams | Readonly<Record<string, string>>;
255
+ }
256
+
257
+ export type AuthComposeFn<Reqs extends Readonly<Record<string, Requirement>> = never> = (
258
+ roles: AuthRoles<Reqs>,
259
+ ) => AuthPlacement | undefined | void;
260
+
261
+ export type HasSecretsGrant<D extends ServiceDecl> = true extends (
262
+ NonNullable<D["auth"]>[keyof NonNullable<D["auth"]>] extends infer M
263
+ ? M extends SecretsMethod
264
+ ? true
265
+ : false
266
+ : never
267
+ )
268
+ ? true
269
+ : false;
270
+
271
+ /** Whether an OpenAPI wrapper must state credential placement explicitly. */
272
+ export type RequiresAuthCompose<Reqs extends Readonly<Record<string, Requirement>>> = true extends {
273
+ [K in keyof Reqs]: NonNullable<Reqs[K][typeof RequirementPhantom]> extends {
274
+ readonly decl: infer D extends ServiceDecl;
275
+ readonly pinned: infer G;
276
+ }
277
+ ? G extends keyof NonNullable<D["auth"]> & string
278
+ ? NonNullable<D["auth"]>[G] extends SecretsMethod
279
+ ? true
280
+ : false
281
+ : HasSecretsGrant<D>
282
+ : never;
283
+ }[keyof Reqs]
284
+ ? true
285
+ : false;
286
+
287
+ /** Record a wrapper-specific publish problem on the active authored file. @internal */
288
+ export const recordAuthoringProblem = (message: string): void => {
289
+ const file = sourceFileFor("wrapper");
290
+ activeRegistry.problems.push({ message: `${file}: ${message}` });
291
+ };
292
+
293
+ /** Operation curation for a dynamic tool set. Exactly one of allow/block. */
294
+ export type OperationSelection =
295
+ | { readonly allow: readonly string[]; readonly block?: undefined }
296
+ | { readonly block: readonly string[]; readonly allow?: undefined };
297
+
298
+ /** One tool emitted by a dynamic discovery pass. */
299
+ export interface DiscoveredToolSpec {
300
+ readonly name: string;
301
+ readonly description: string;
302
+ readonly input: SchemaLike;
303
+ readonly annotations?: ToolAnnotations;
304
+ readonly detail?: unknown;
305
+ }
306
+
128
307
  /**
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.
308
+ * The generic authored shape of a dynamic pair: ctx is inferred from
309
+ * the requirements record exactly as for static tools. Discovered
310
+ * inputs are runtime data (the schema arrives from the upstream at
311
+ * discover time), so `call`'s input is honestly `unknown` — the
312
+ * runtime validates against the discovered descriptor before calling.
134
313
  */
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
- };
314
+ export interface DynamicToolsDecl<
315
+ Reqs extends Readonly<Record<string, Requirement>> | undefined =
316
+ | Readonly<Record<string, Requirement>>
317
+ | undefined,
318
+ > {
319
+ readonly description: string;
320
+ readonly requirements?: Reqs;
321
+ discover(args: { readonly ctx: HandlerCtx<Reqs> }): Promise<readonly DiscoveredToolSpec[]>;
322
+ call(args: {
323
+ readonly tool: { readonly name: string; readonly detail: unknown };
324
+ readonly input: unknown;
325
+ readonly ctx: HandlerCtx<Reqs>;
326
+ }): Promise<unknown>;
327
+ }
328
+
329
+ /**
330
+ * The erased registry row of a dynamic pair — same storage boundary
331
+ * as ToolSpec. App code: runs where handlers run (in-process locally,
332
+ * inside the isolate on workerd) and places credentials itself from
333
+ * ctx, like any handler. `discover` interrogates the tool source;
334
+ * `call` executes one discovered tool from the identity and detail
335
+ * discover emitted.
336
+ */
337
+ export interface DynamicToolsSpec {
338
+ readonly description: string;
339
+ readonly requirements?: Readonly<Record<string, Requirement>>;
340
+ discover(args: never): Promise<readonly DiscoveredToolSpec[]>;
341
+ call(args: never): Promise<unknown>;
342
+ }
148
343
 
149
344
  const DynamicKey: unique symbol = Symbol.for("executor/apps/dynamic");
150
345
 
151
- /** The value fromMCP/fromOpenAPI return: an opaque claim handle a tool
152
- * file default-exports. The spec is undefined when the claim was
346
+ /** The value defineDynamicTools returns: an opaque handle a tool file
347
+ * default-exports. The spec is undefined when the definition was
153
348
  * invalid (a problem was recorded instead). */
154
349
  export interface DynamicToolSet {
155
- readonly [DynamicKey]: DynamicClaimSpec | undefined;
350
+ readonly [DynamicKey]: DynamicToolsSpec | undefined;
156
351
  }
157
352
 
158
- /** A registered dynamic claim and its source attribution. @internal */
159
- export interface RegisteredDynamic {
160
- readonly spec: DynamicClaimSpec;
353
+ /** Reads a dynamic spec from a tool file's default export. @internal */
354
+ export const dynamicSpecOf = (value: unknown): DynamicToolsSpec | undefined =>
355
+ typeof value === "object" && value !== null && DynamicKey in value
356
+ ? (value as DynamicToolSet)[DynamicKey]
357
+ : undefined;
358
+
359
+ /**
360
+ * The dynamic-tools primitive: an author-written discover/call pair
361
+ * whose tool list is interrogated at runtime, per connection, instead
362
+ * of authored one file at a time. The file's name becomes the address
363
+ * segment (appKey/<stem>/<toolName>); tools land in the catalog after
364
+ * discovery and never block a save. fromMCP/fromOpenAPI are shipped
365
+ * pairs over this same primitive.
366
+ */
367
+ export const defineDynamicTools = <
368
+ const Reqs extends Readonly<Record<string, Requirement>> | undefined = undefined,
369
+ >(decl: DynamicToolsDecl<Reqs>): DynamicToolSet => {
370
+ // SAFETY: same erasure boundary as defineTool — members identical,
371
+ // handler parameter types narrow to the registry's untyped floor.
372
+ const spec = decl as unknown as DynamicToolsSpec;
373
+ const file = sourceFileFor("defineDynamicTools(...)");
374
+ const problems: string[] = [];
375
+
376
+ if (typeof spec.description !== "string" || spec.description.trim().length === 0)
377
+ problems.push("dynamic tool set description must not be empty");
378
+
379
+ const roles = Object.entries(spec.requirements ?? {});
380
+ if (roles.length > 1) problems.push("a dynamic tool set takes exactly one requirement role");
381
+ else if (roles.length === 1) {
382
+ const [role, value] = roles[0]!;
383
+ const requirementSpec = requirementSpecOf(value);
384
+ if (requirementSpec === undefined)
385
+ problems.push(`requirement '${role}' is not a service/requirement value`);
386
+ else if (requirementSpec.arity === "many")
387
+ problems.push(
388
+ `requirement '${role}' cannot be .array() — multiple accounts expand the catalog per connection, never merge one list`,
389
+ );
390
+ }
391
+
392
+ if (problems.length > 0) {
393
+ activeRegistry.problems.push(...problems.map((message) => ({ message: `${file}: ${message}` })));
394
+ return { [DynamicKey]: undefined };
395
+ }
396
+
397
+ if (activeRegistry.dynamic.has(file))
398
+ activeRegistry.problems.push({
399
+ message: `${file}: one dynamic tool set per file — its address segment is the file's name`,
400
+ });
401
+ else activeRegistry.dynamic.set(file, { spec, file });
402
+ return { [DynamicKey]: spec };
403
+ };
404
+
405
+ /** A registered dynamic tool set and its source attribution. @internal */
406
+ export interface DynamicRegistration {
407
+ readonly spec: DynamicToolsSpec;
161
408
  readonly file: string;
162
409
  }
163
410
 
@@ -178,7 +425,7 @@ export interface Registry {
178
425
  app: AppManifest | undefined;
179
426
  readonly services: Map<string, RegisteredService>;
180
427
  readonly tools: Map<string, RegisteredTool>;
181
- readonly dynamic: Map<string, RegisteredDynamic>;
428
+ readonly dynamic: Map<string, DynamicRegistration>;
182
429
  readonly problems: Problem[];
183
430
  readonly toolFiles: Map<
184
431
  string,
@@ -224,19 +471,36 @@ const sourceFileFor = (claim: string): string => {
224
471
  return "<unattributed>";
225
472
  };
226
473
 
227
- const makeTerminal = <D extends ServiceDecl>(spec: RequirementSpec): Requirement<D> => ({
474
+ const makeTerminal = <
475
+ D extends ServiceDecl,
476
+ G extends string | undefined,
477
+ A extends RequirementArity,
478
+ >(spec: RequirementSpec): Requirement<D, G, A> => ({
228
479
  [RequirementKey]: spec,
229
- describe: (text) => makeTerminal<D>({ ...spec, description: text }),
480
+ describe: (text) => makeTerminal<D, G, A>({ ...spec, description: text }),
230
481
  });
231
482
 
232
483
  export const service = <const D extends ServiceDecl>(decl: D): Service<D> => {
233
484
  const file = sourceFileFor("service(...)");
485
+ const makePinned = <N extends keyof NonNullable<D["auth"]> & string>(
486
+ spec: RequirementSpec,
487
+ ): PinnedRequirement<D, N> => ({
488
+ [RequirementKey]: spec,
489
+ optional: () => makeTerminal<D, N, "optional">({ ...spec, arity: "optional" }),
490
+ array: () => makeTerminal<D, N, "many">({ ...spec, arity: "many" }),
491
+ describe: (text) => makePinned<N>({ ...spec, description: text }),
492
+ });
234
493
  const make = (spec: RequirementSpec): Service<D> => ({
235
494
  [RequirementKey]: spec,
236
- decl,
237
- optional: () => makeTerminal<D>({ ...spec, arity: "optional" }),
238
- array: () => makeTerminal<D>({ ...spec, arity: "many" }),
239
- grant: (name) => make({ ...spec, grant: name }),
495
+ [ServiceDeclKey]: decl,
496
+ auth: Object.fromEntries(
497
+ Object.keys(decl.auth ?? {}).map((name) => [
498
+ name,
499
+ makePinned({ ...spec, grant: name }),
500
+ ]),
501
+ ) as ServiceAuth<D>,
502
+ optional: () => makeTerminal<D, undefined, "optional">({ ...spec, arity: "optional" }),
503
+ array: () => makeTerminal<D, undefined, "many">({ ...spec, arity: "many" }),
240
504
  describe: (text) => make({ ...spec, description: text }),
241
505
  });
242
506
  const value = make({ service: decl, arity: "one" });
@@ -249,11 +513,7 @@ const isRequirement = (value: unknown): value is Requirement =>
249
513
  typeof value === "object" && value !== null && RequirementKey in value;
250
514
 
251
515
  const isService = (value: unknown): value is Service =>
252
- isRequirement(value) &&
253
- "decl" in value &&
254
- "optional" in value &&
255
- "array" in value &&
256
- "grant" in value;
516
+ isRequirement(value) && ServiceDeclKey in value;
257
517
 
258
518
  export const requirementSpecOf = (value: unknown): RequirementSpec | undefined =>
259
519
  isService(value) || isRequirement(value) ? value[RequirementKey] : undefined;
@@ -284,6 +544,47 @@ export const defineApp = (manifest: {
284
544
  /** Any Standard Schema (zod preferred) or a raw JSON-schema object. */
285
545
  export type SchemaLike = object;
286
546
 
547
+ /**
548
+ * The validated value a Standard Schema produces, recovered from its
549
+ * phantom types (zod 4 and effect Schema both publish them). A raw
550
+ * JSON-schema object carries no phantom, so its input is `unknown` —
551
+ * stated weakness, not silent weakness.
552
+ */
553
+ export type InferSchema<S> = S extends {
554
+ readonly "~standard": { readonly types?: infer T };
555
+ }
556
+ ? NonNullable<T> extends { readonly output: infer O }
557
+ ? O
558
+ : unknown
559
+ : unknown;
560
+
561
+ /**
562
+ * The capability a handler receives for one connected requirement
563
+ * role: the grant's kind-discriminated material (narrowed by any
564
+ * auth-door pin) plus the spending fetch. Arity shapes the value —
565
+ * `.optional()` may be absent, `.array()` is every connected account.
566
+ */
567
+ export type HandlerRole<R> = R extends Requirement<infer D, infer G, infer A>
568
+ ? [A] extends ["many"]
569
+ ? ReadonlyArray<HandlerRoleEntry<D, G>>
570
+ : [A] extends ["optional"]
571
+ ? HandlerRoleEntry<D, G> | undefined
572
+ : HandlerRoleEntry<D, G>
573
+ : never;
574
+
575
+ /** One connected role entry: declared-auth material + capability fetch. */
576
+ export type HandlerRoleEntry<
577
+ D extends ServiceDecl,
578
+ G extends string | undefined,
579
+ > = ([G] extends [string] ? Extract<AuthRoleEntry<D>, { grant: G }> : AuthRoleEntry<D>) & {
580
+ fetch(path: string, init?: RequestInit): Promise<Response>;
581
+ };
582
+
583
+ /** The ctx record a handler receives, keyed by requirement role. */
584
+ export type HandlerCtx<Reqs> = [Reqs] extends [Readonly<Record<string, Requirement>>]
585
+ ? { readonly [K in keyof Reqs]: HandlerRole<Reqs[K]> }
586
+ : Readonly<Record<never, never>>;
587
+
287
588
  export interface ToolAnnotations {
288
589
  readonly readOnly?: boolean;
289
590
  readonly destructive?: boolean;
@@ -291,15 +592,49 @@ export interface ToolAnnotations {
291
592
  readonly scopes?: readonly string[];
292
593
  }
293
594
 
294
- export interface ToolSpec {
595
+ /**
596
+ * The generic authored shape: input/ctx/output are all inferred — the
597
+ * handler's input from the Standard Schema, ctx from the requirements
598
+ * record (grants, pins, arity), and the return type checked against
599
+ * the output schema when one is stated. The handler takes ONE
600
+ * destructurable argument so unused halves never demand an underscore:
601
+ * `handler: async ({ input }) => ...` / `async ({ ctx }) => ...`.
602
+ */
603
+ export interface ToolDecl<
604
+ Input extends SchemaLike = SchemaLike,
605
+ Reqs extends Readonly<Record<string, Requirement>> | undefined =
606
+ | Readonly<Record<string, Requirement>>
607
+ | undefined,
608
+ Output extends SchemaLike | undefined = SchemaLike | undefined,
609
+ > {
295
610
  /** Address segment. Single-tool files may omit it (filename). */
611
+ readonly name?: string;
612
+ readonly description: string;
613
+ readonly requirements?: Reqs;
614
+ readonly input: Input;
615
+ readonly output?: Output;
616
+ readonly annotations?: ToolAnnotations;
617
+ handler(args: {
618
+ readonly input: InferSchema<Input>;
619
+ readonly ctx: HandlerCtx<Reqs>;
620
+ }): Promise<Output extends SchemaLike ? InferSchema<Output> : unknown>;
621
+ }
622
+
623
+ /**
624
+ * The erased registry row. Registration drops the generics — storage
625
+ * is untyped by design, the same boundary every schema library draws
626
+ * between its typed builders and its runtime representation. The
627
+ * runtime re-establishes safety by validating input before the
628
+ * handler runs and shaping ctx from resolved connections.
629
+ */
630
+ export interface ToolSpec {
296
631
  readonly name?: string;
297
632
  readonly description: string;
298
633
  readonly requirements?: Readonly<Record<string, Requirement>>;
299
634
  readonly input: SchemaLike;
300
635
  readonly output?: SchemaLike;
301
636
  readonly annotations?: ToolAnnotations;
302
- handler(input: never, ctx: never): Promise<unknown>;
637
+ handler(args: never): Promise<unknown>;
303
638
  }
304
639
 
305
640
  const ToolKey: unique symbol = Symbol.for("executor/apps/tool");
@@ -308,7 +643,18 @@ export interface Tool {
308
643
  readonly [ToolKey]: ToolSpec;
309
644
  }
310
645
 
311
- export const defineTool = (spec: ToolSpec): Tool => {
646
+ export const defineTool = <
647
+ const Input extends SchemaLike,
648
+ const Reqs extends Readonly<Record<string, Requirement>> | undefined = undefined,
649
+ const Output extends SchemaLike | undefined = undefined,
650
+ >(decl: ToolDecl<Input, Reqs, Output>): Tool => {
651
+ // SAFETY: the erased row keeps the same members; only the handler's
652
+ // parameter types narrow (never is the bottom of every inference).
653
+ const spec = decl as unknown as ToolSpec;
654
+ return registerTool(spec);
655
+ };
656
+
657
+ const registerTool = (spec: ToolSpec): Tool => {
312
658
  const file = sourceFileFor("defineTool(...)");
313
659
  const value: Tool = { [ToolKey]: spec };
314
660
  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 +1,4 @@
1
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. */