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.
- package/dist/authoring/index.d.ts +254 -37
- package/dist/authoring/index.js +11 -1
- package/dist/{chunk-OGCPVWAZ.js → chunk-2DVKSPQH.js} +134 -13
- package/dist/{chunk-NFLNA2SC.js → chunk-4OZFMCTT.js} +7 -1
- package/dist/{chunk-LWKQDDLZ.js → chunk-535V5K64.js} +61 -4
- package/dist/chunk-DP6UJCMX.js +27 -0
- package/dist/chunk-QA72XJZU.js +41 -0
- package/dist/chunk-S5KFP4JQ.js +20 -0
- package/dist/{chunk-646MS7DD.js → chunk-XYT6GZ7M.js} +7 -7
- package/dist/http/index.d.ts +9 -0
- package/dist/http/index.js +6 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +23 -1
- package/dist/manifest/contract.d.ts +8 -0
- package/dist/manifest/index.js +3 -3
- package/dist/mcp/client.d.ts +9 -0
- package/dist/mcp/index.d.ts +11 -0
- package/dist/mcp/index.js +7 -0
- package/dist/openapi/index.d.ts +14 -0
- package/dist/openapi/index.js +7 -0
- package/dist/openapi/mapper.d.ts +7 -0
- package/dist/runner/index.js +29 -15
- package/dist/runner/protocol.d.ts +18 -5
- package/dist/runner/protocol.js +5 -3
- package/dist/runtime/index.d.ts +42 -5
- package/dist/runtime/index.js +3 -1
- package/dist/workerd/index.d.ts +1 -1
- package/dist/workerd/index.js +1 -1
- package/package.json +13 -6
- package/src/authoring/index.ts +395 -49
- package/src/http/index.ts +26 -0
- package/src/index.ts +3 -0
- package/src/manifest/contract.ts +9 -0
- package/src/mcp/client.ts +59 -0
- package/src/mcp/index.ts +42 -0
- package/src/openapi/index.ts +70 -0
- package/src/openapi/mapper.ts +25 -0
- package/src/runner/index.ts +35 -12
- package/src/runner/protocol.ts +32 -7
- package/src/runtime/index.ts +216 -25
- 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.
|
|
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
|
}
|
package/src/authoring/index.ts
CHANGED
|
@@ -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
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
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
|
|
136
|
-
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
152
|
-
*
|
|
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]:
|
|
350
|
+
readonly [DynamicKey]: DynamicToolsSpec | undefined;
|
|
156
351
|
}
|
|
157
352
|
|
|
158
|
-
/**
|
|
159
|
-
export
|
|
160
|
-
|
|
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,
|
|
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 = <
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
package/src/manifest/contract.ts
CHANGED
|
@@ -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. */
|