apps 1.0.0-beta.2 → 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.
- package/dist/authoring/index.d.ts +222 -34
- package/dist/authoring/index.js +11 -1
- package/dist/{chunk-NFLNA2SC.js → chunk-4OZFMCTT.js} +7 -1
- package/dist/{chunk-LWKQDDLZ.js → chunk-A3H32P2J.js} +46 -1
- package/dist/chunk-F7GZYEAC.js +41 -0
- package/dist/{chunk-OGCPVWAZ.js → chunk-MUFMJRXR.js} +130 -13
- package/dist/chunk-S5KFP4JQ.js +20 -0
- package/dist/{chunk-646MS7DD.js → chunk-Y6LLTN7W.js} +7 -7
- package/dist/chunk-YUZ4TXOY.js +27 -0
- 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 +362 -44
- 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 +209 -22
- package/src/workerd/index.ts +2 -0
package/dist/workerd/index.js
CHANGED
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.3",
|
|
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;
|
|
@@ -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<
|
|
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
|
-
|
|
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<
|
|
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
|
|
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
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
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
|
|
136
|
-
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
152
|
-
*
|
|
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]:
|
|
330
|
+
readonly [DynamicKey]: DynamicToolsSpec | undefined;
|
|
156
331
|
}
|
|
157
332
|
|
|
158
|
-
/**
|
|
159
|
-
export
|
|
160
|
-
|
|
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,
|
|
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 = <
|
|
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 = (
|
|
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:
|
|
240
|
-
|
|
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
|
-
|
|
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 =
|
|
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
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. */
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Stub-contract MCP client used by the fromMCP dynamic pair. */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AuthComposeFn,
|
|
5
|
+
DiscoveredToolSpec,
|
|
6
|
+
OperationSelection,
|
|
7
|
+
} from "../authoring/index";
|
|
8
|
+
|
|
9
|
+
/** Normalize authored placement, or apply MCP's bearer default. */
|
|
10
|
+
export const composePlacement = (
|
|
11
|
+
auth: AuthComposeFn<never> | undefined,
|
|
12
|
+
ctx: unknown,
|
|
13
|
+
): { headers: Headers; searchParams: URLSearchParams } => {
|
|
14
|
+
const headers = new Headers();
|
|
15
|
+
const searchParams = new URLSearchParams();
|
|
16
|
+
const roles = ctx as Record<string, { token?: string }> | undefined;
|
|
17
|
+
|
|
18
|
+
if (auth === undefined) {
|
|
19
|
+
const role = roles === undefined ? undefined : Object.values(roles)[0];
|
|
20
|
+
if (role?.token !== undefined) headers.set("authorization", `Bearer ${role.token}`);
|
|
21
|
+
return { headers, searchParams };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const placement = (auth as (roles: unknown) => unknown)(roles ?? {});
|
|
25
|
+
if (placement !== undefined && placement !== null && typeof placement === "object") {
|
|
26
|
+
const normalized = placement as {
|
|
27
|
+
readonly headers?: HeadersInit;
|
|
28
|
+
readonly searchParams?: URLSearchParams | Readonly<Record<string, string>>;
|
|
29
|
+
};
|
|
30
|
+
if (normalized.headers !== undefined)
|
|
31
|
+
new Headers(normalized.headers).forEach((value, key) => headers.set(key, value));
|
|
32
|
+
if (normalized.searchParams !== undefined)
|
|
33
|
+
new URLSearchParams(
|
|
34
|
+
normalized.searchParams instanceof URLSearchParams
|
|
35
|
+
? normalized.searchParams
|
|
36
|
+
: { ...normalized.searchParams },
|
|
37
|
+
).forEach((value, key) => searchParams.set(key, value));
|
|
38
|
+
}
|
|
39
|
+
return { headers, searchParams };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const mcpDiscover = async (
|
|
43
|
+
_endpoint: string,
|
|
44
|
+
_ctx: unknown,
|
|
45
|
+
_auth: AuthComposeFn<never> | undefined,
|
|
46
|
+
_operations: OperationSelection | undefined,
|
|
47
|
+
): Promise<readonly DiscoveredToolSpec[]> => {
|
|
48
|
+
throw new Error("mcp client not built yet");
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const mcpCall = async (
|
|
52
|
+
_endpoint: string,
|
|
53
|
+
_tool: string,
|
|
54
|
+
_input: unknown,
|
|
55
|
+
_ctx: unknown,
|
|
56
|
+
_auth: AuthComposeFn<never> | undefined,
|
|
57
|
+
): Promise<unknown> => {
|
|
58
|
+
throw new Error("mcp client not built yet");
|
|
59
|
+
};
|