@procedures/client 0.1.0 → 0.2.0

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 (118) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1 -1
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/http/server/transport.d.ts.map +1 -1
  5. package/dist/adapters/http/server/transport.js +1 -0
  6. package/dist/adapters/http/server/transport.js.map +1 -1
  7. package/dist/browser.d.ts +5 -2
  8. package/dist/browser.d.ts.map +1 -1
  9. package/dist/browser.js +4 -1
  10. package/dist/browser.js.map +1 -1
  11. package/dist/client/client.d.ts +9 -1
  12. package/dist/client/client.d.ts.map +1 -1
  13. package/dist/client/client.js +27 -6
  14. package/dist/client/client.js.map +1 -1
  15. package/dist/client/types.d.ts +9 -0
  16. package/dist/client/types.d.ts.map +1 -1
  17. package/dist/client/types.js.map +1 -1
  18. package/dist/procedures/capability.d.ts +111 -0
  19. package/dist/procedures/capability.d.ts.map +1 -0
  20. package/dist/procedures/capability.js +269 -0
  21. package/dist/procedures/capability.js.map +1 -0
  22. package/dist/procedures/cli/discover.d.ts +3 -3
  23. package/dist/procedures/cli/discover.js +3 -3
  24. package/dist/procedures/cli/index.js +5 -3
  25. package/dist/procedures/cli/index.js.map +1 -1
  26. package/dist/procedures/cli/init.js +1 -1
  27. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  28. package/dist/procedures/collection/procedures.js +12 -0
  29. package/dist/procedures/collection/procedures.js.map +1 -1
  30. package/dist/procedures/core/array.d.ts.map +1 -1
  31. package/dist/procedures/core/array.js +3 -1
  32. package/dist/procedures/core/array.js.map +1 -1
  33. package/dist/procedures/core/comparison.d.ts.map +1 -1
  34. package/dist/procedures/core/comparison.js +3 -1
  35. package/dist/procedures/core/comparison.js.map +1 -1
  36. package/dist/procedures/core/index.d.ts.map +1 -1
  37. package/dist/procedures/core/index.js +5 -2
  38. package/dist/procedures/core/index.js.map +1 -1
  39. package/dist/procedures/core/math.d.ts.map +1 -1
  40. package/dist/procedures/core/math.js +3 -1
  41. package/dist/procedures/core/math.js.map +1 -1
  42. package/dist/procedures/core/meta.d.ts.map +1 -1
  43. package/dist/procedures/core/meta.js +11 -6
  44. package/dist/procedures/core/meta.js.map +1 -1
  45. package/dist/procedures/core/object.d.ts.map +1 -1
  46. package/dist/procedures/core/object.js +3 -1
  47. package/dist/procedures/core/object.js.map +1 -1
  48. package/dist/procedures/core/string.d.ts.map +1 -1
  49. package/dist/procedures/core/string.js +3 -1
  50. package/dist/procedures/core/string.js.map +1 -1
  51. package/dist/procedures/core/type.d.ts.map +1 -1
  52. package/dist/procedures/core/type.js +3 -1
  53. package/dist/procedures/core/type.js.map +1 -1
  54. package/dist/procedures/define-procedure.d.ts.map +1 -1
  55. package/dist/procedures/define-procedure.js +5 -4
  56. package/dist/procedures/define-procedure.js.map +1 -1
  57. package/dist/procedures/effects.d.ts +15 -0
  58. package/dist/procedures/effects.d.ts.map +1 -0
  59. package/dist/procedures/effects.js +18 -0
  60. package/dist/procedures/effects.js.map +1 -0
  61. package/dist/procedures/index.d.ts +5 -2
  62. package/dist/procedures/index.d.ts.map +1 -1
  63. package/dist/procedures/index.js +8 -23
  64. package/dist/procedures/index.js.map +1 -1
  65. package/dist/procedures/interceptors.d.ts.map +1 -1
  66. package/dist/procedures/interceptors.js +2 -1
  67. package/dist/procedures/interceptors.js.map +1 -1
  68. package/dist/procedures/invoke.d.ts +31 -3
  69. package/dist/procedures/invoke.d.ts.map +1 -1
  70. package/dist/procedures/invoke.js +78 -12
  71. package/dist/procedures/invoke.js.map +1 -1
  72. package/dist/procedures/module.d.ts +13 -8
  73. package/dist/procedures/module.d.ts.map +1 -1
  74. package/dist/procedures/module.js +19 -9
  75. package/dist/procedures/module.js.map +1 -1
  76. package/dist/procedures/types.d.ts +100 -2
  77. package/dist/procedures/types.d.ts.map +1 -1
  78. package/dist/procedures/types.js.map +1 -1
  79. package/dist/register.d.ts +8 -0
  80. package/dist/register.d.ts.map +1 -0
  81. package/dist/register.js +9 -0
  82. package/dist/register.js.map +1 -0
  83. package/dist/server/host-policy.d.ts +24 -2
  84. package/dist/server/host-policy.d.ts.map +1 -1
  85. package/dist/server/host-policy.js +22 -0
  86. package/dist/server/host-policy.js.map +1 -1
  87. package/dist/server/procedure-server.d.ts +16 -2
  88. package/dist/server/procedure-server.d.ts.map +1 -1
  89. package/dist/server/procedure-server.js +26 -1
  90. package/dist/server/procedure-server.js.map +1 -1
  91. package/package.json +6 -4
  92. package/src/adapters/http/server/transport.ts +1 -0
  93. package/src/browser.ts +28 -0
  94. package/src/client/client.ts +33 -6
  95. package/src/client/types.ts +11 -0
  96. package/src/procedures/capability.ts +333 -0
  97. package/src/procedures/cli/discover.ts +3 -3
  98. package/src/procedures/cli/index.ts +5 -3
  99. package/src/procedures/cli/init.ts +1 -1
  100. package/src/procedures/collection/procedures.ts +12 -0
  101. package/src/procedures/core/array.ts +3 -1
  102. package/src/procedures/core/comparison.ts +3 -1
  103. package/src/procedures/core/index.ts +5 -2
  104. package/src/procedures/core/math.ts +3 -1
  105. package/src/procedures/core/meta.ts +11 -6
  106. package/src/procedures/core/object.ts +3 -1
  107. package/src/procedures/core/string.ts +3 -1
  108. package/src/procedures/core/type.ts +3 -1
  109. package/src/procedures/define-procedure.ts +5 -4
  110. package/src/procedures/effects.ts +24 -0
  111. package/src/procedures/index.ts +33 -22
  112. package/src/procedures/interceptors.ts +2 -1
  113. package/src/procedures/invoke.ts +106 -13
  114. package/src/procedures/module.ts +19 -9
  115. package/src/procedures/types.ts +113 -2
  116. package/src/register.ts +10 -0
  117. package/src/server/host-policy.ts +44 -2
  118. package/src/server/procedure-server.ts +44 -2
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Effect declarations for groups of procedures (CAPABILITIES-2026-10). This file imports only
3
+ * types, so each file of procedures can use it without an import cycle.
4
+ */
5
+
6
+ import type { AnyProcedure, Effects } from "./types.js";
7
+
8
+ /** The effects of a procedure that reads and changes nothing: its output comes from its input. */
9
+ export const PURE_EFFECTS: Effects = Object.freeze({ reads: Object.freeze([]), writes: Object.freeze([]) });
10
+
11
+ /** The effects of a procedure that reads the procedure registry and changes nothing. */
12
+ export const REGISTRY_READ_EFFECTS: Effects = Object.freeze({ reads: Object.freeze(["registry"]), writes: Object.freeze([]) });
13
+
14
+ /**
15
+ * This function gives copies of the procedures with the effects in their metadata. A procedure
16
+ * that declares its own effects keeps them.
17
+ */
18
+ export function withEffects<T extends AnyProcedure>(effects: Effects, procedures: readonly T[]): T[] {
19
+ return procedures.map((procedure) =>
20
+ (procedure.metadata as { effects?: unknown } | undefined)?.effects !== undefined
21
+ ? procedure
22
+ : ({ ...procedure, metadata: { ...procedure.metadata, effects } } as T)
23
+ );
24
+ }
@@ -24,11 +24,39 @@ export type {
24
24
  ProcedureModule,
25
25
  RegistrationOptions,
26
26
  ProcedureRegistryLike,
27
+ Effects,
28
+ PermittedEffects,
29
+ Register,
30
+ ClearanceOf,
31
+ ResourcesOf,
32
+ Capability,
33
+ CapabilityRequest,
34
+ ClearanceOrder,
35
+ NestedCallOptions,
27
36
  } from "./types.js";
28
37
 
29
38
  // Type utilities
30
39
  export { pathToKey, keyToPath } from "./types.js";
31
40
 
41
+ // Capabilities (CAPABILITIES-2026-10)
42
+ export {
43
+ STANDARD_RESOURCES,
44
+ UNLIMITED_CAPABILITY,
45
+ ladder,
46
+ effectsOf,
47
+ isReadOnly,
48
+ isDestructive,
49
+ isOpenWorld,
50
+ toolHints,
51
+ narrowCapability,
52
+ capabilityRefusal,
53
+ examinesNestedCall,
54
+ explainInvocation,
55
+ checkEffects,
56
+ } from "./capability.js";
57
+ export { PURE_EFFECTS, REGISTRY_READ_EFFECTS, withEffects } from "./effects.js";
58
+ export type { ToolHints, CapabilityRefusal, CapabilityRefusalReason, InvocationExplanation, EffectsViolation } from "./capability.js";
59
+
32
60
  // Registry
33
61
  export { ProcedureRegistry, RegistryError, PROCEDURE_REGISTRY, assertValidPath } from "./registry.js";
34
62
 
@@ -61,6 +89,7 @@ export {
61
89
  moduleClosure,
62
90
  registerModules,
63
91
  createRegistry,
92
+ registerClientProcedures,
64
93
  } from "./module.js";
65
94
  export type { ModuleInput } from "./module.js";
66
95
 
@@ -202,27 +231,9 @@ export type {
202
231
  // procedures moved to @procedures/store (roadmap 3.2): the program store.
203
232
 
204
233
  // =============================================================================
205
- // Auto-register core procedures
234
+ // Registration
206
235
  // =============================================================================
207
236
 
208
- import { coreProcedures as _coreProcedures } from "./core/index.js";
209
- import { metaProcedures as _metaProcedures } from "./define-procedure.js";
210
- import { PROCEDURE_REGISTRY } from "./registry.js";
211
-
212
- // Register core procedures when this module is imported
213
- // This ensures client.* procedures are available without explicit registration
214
- try {
215
- for (const proc of _coreProcedures) {
216
- if (!PROCEDURE_REGISTRY.has(proc.path)) {
217
- PROCEDURE_REGISTRY.register(proc);
218
- }
219
- }
220
- // Also register meta-procedures
221
- for (const proc of _metaProcedures) {
222
- if (!PROCEDURE_REGISTRY.has(proc.path)) {
223
- PROCEDURE_REGISTRY.register(proc);
224
- }
225
- }
226
- } catch {
227
- // Ignore errors during auto-registration (e.g., if already registered)
228
- }
237
+ // Importing this package registers nothing (ARCHITECTURE-PROPOSALS-2026-10, P9). The
238
+ // "./register" entry registers the procedures of `clientModule` in the global registry, and
239
+ // `createRegistry()` adds them to each new registry.
@@ -98,7 +98,8 @@ export function timeoutInterceptor(options: TimeoutInterceptorOptions): Intercep
98
98
 
99
99
  let output: ProcedureOutput<unknown>;
100
100
  try {
101
- output = await Promise.race([next({ signal }), deadline]);
101
+ // The deadline goes into the capability: the nested calls and the remote calls see it
102
+ output = await Promise.race([next({ signal, capability: { timeoutMs: ms } }), deadline]);
102
103
  } catch (error) {
103
104
  stop();
104
105
  throw error;
@@ -13,14 +13,24 @@
13
13
  * Interceptors run around each invocation, the nested calls too (roadmap 1.2): policy, tracing,
14
14
  * timeouts and metrics for every host. `interceptors.ts` has a timeout, a tracing and a metrics
15
15
  * interceptor.
16
+ *
17
+ * Each invocation operates with a capability (CAPABILITIES-2026-10, `capability.ts`). A nested
18
+ * call gets the same capability or a narrower one. The admission refuses a procedure whose
19
+ * declared effects the capability does not permit (`FORBIDDEN`). The host can give resources
20
+ * for the capability (`ctx.resources`) and a filter of the output that leaves the host.
16
21
  */
17
22
 
18
23
  import type {
19
24
  AnyProcedure,
25
+ Capability,
26
+ CapabilityRequest,
27
+ ClearanceOrder,
28
+ NestedCallOptions,
20
29
  ProcedureClient,
21
30
  ProcedureContext,
22
31
  ProcedurePath,
23
32
  RepositoryProvider,
33
+ ResourcesOf,
24
34
  } from "./types.js";
25
35
  import { pathToKey } from "./types.js";
26
36
  import { validateSchema } from "./schema.js";
@@ -28,6 +38,8 @@ import type { ProcedureRegistry } from "./registry.js";
28
38
  import { PROCEDURE_REGISTRY } from "./registry.js";
29
39
  import { nestedCallRefusal } from "./policy.js";
30
40
  import { scopeOf, withScope, type HydrationEvent } from "./ref.js";
41
+ import { UNLIMITED_CAPABILITY, capabilityRefusal, examinesNestedCall, narrowCapability, type CapabilityRefusalReason } from "./capability.js";
42
+ import { timeoutInterceptor } from "./interceptors.js";
31
43
  import type { EventBus } from "../events/types.js";
32
44
  import { getGlobalEventBus, withEventBusSignal } from "../events/bus.js";
33
45
 
@@ -45,7 +57,9 @@ export type InvocationErrorCode =
45
57
  | "OUTPUT_VALIDATION_ERROR"
46
58
  | "NO_OUTPUT"
47
59
  | "ABORTED"
48
- | "TIMEOUT";
60
+ | "TIMEOUT"
61
+ /** The capability of the call does not permit it (CAPABILITIES-2026-10). `reason` tells why. */
62
+ | "FORBIDDEN";
49
63
 
50
64
  /** An error of the invocation itself. An error that the handler throws passes through unchanged. */
51
65
  export class InvocationError extends Error {
@@ -53,11 +67,14 @@ export class InvocationError extends Error {
53
67
  readonly code: InvocationErrorCode;
54
68
  readonly path: ProcedurePath;
55
69
  readonly retryable: boolean = false;
70
+ /** The reason of a `FORBIDDEN` error: `effects-undeclared`, `effects-exceeded` or `clearance`. */
71
+ readonly reason: CapabilityRefusalReason | undefined;
56
72
 
57
- constructor(code: InvocationErrorCode, message: string, path: ProcedurePath) {
73
+ constructor(code: InvocationErrorCode, message: string, path: ProcedurePath, reason?: CapabilityRefusalReason) {
58
74
  super(message);
59
75
  this.code = code;
60
76
  this.path = path;
77
+ this.reason = reason;
61
78
  }
62
79
  }
63
80
 
@@ -108,6 +125,20 @@ export interface InvokeOptions {
108
125
  interceptors?: readonly Interceptor[] | undefined;
109
126
  /** The invocation whose handler makes this call. The context client sets it for a nested call. */
110
127
  parent?: Invocation | undefined;
128
+ /**
129
+ * The capability of the call: its clearance, permitted effects and deadline. The host makes it
130
+ * from its authentication. The nested calls get it, or a narrower one. Absent: no limits.
131
+ */
132
+ capability?: Capability | undefined;
133
+ /** The order of the clearance values. Narrowing two different clearances needs it. */
134
+ clearanceOrder?: ClearanceOrder | undefined;
135
+ /** The resources of an invocation for its capability (`ctx.resources`). */
136
+ resources?: ((invocation: Invocation) => ResourcesOf) | undefined;
137
+ /**
138
+ * The filter of the output that leaves the host (the call of the host, not its nested calls).
139
+ * It drops a refused stream item. A refused value gives `FORBIDDEN` (reason `clearance`).
140
+ */
141
+ admitItem?: ((item: unknown, invocation: Invocation) => boolean) | undefined;
111
142
  }
112
143
 
113
144
  // =============================================================================
@@ -130,12 +161,16 @@ export interface Invocation {
130
161
  readonly parent: Invocation | undefined;
131
162
  /** 0 for a call of a host, 1 for its nested calls, and so on. */
132
163
  readonly depth: number;
164
+ /** The capability of the invocation (frozen). */
165
+ readonly capability: Capability;
133
166
  }
134
167
 
135
168
  /** The changes that an interceptor gives to the next step: the handler and the nested calls get them. */
136
169
  export interface InvocationChanges {
137
170
  signal?: AbortSignal | undefined;
138
171
  metadata?: Record<string, unknown> | undefined;
172
+ /** A request for a narrower capability. The framework calculates the result: it is never wider. */
173
+ capability?: CapabilityRequest | undefined;
139
174
  }
140
175
 
141
176
  /** The rest of the chain: the next interceptors, then the validation and the handler. */
@@ -256,8 +291,15 @@ export async function invokeProcedure<TOutput = unknown>(
256
291
  signal: options.signal,
257
292
  parent: options.parent,
258
293
  depth: options.parent ? options.parent.depth + 1 : 0,
294
+ capability: options.capability ?? UNLIMITED_CAPABILITY,
259
295
  };
260
- const interceptors = options.interceptors ?? [];
296
+ // A deadline that the parent does not have (from a host, a request or a narrowed nested call)
297
+ // ends the invocation at that time, as the timeout interceptor does
298
+ const deadline = invocation.capability.deadline;
299
+ const ownDeadline = deadline !== undefined && options.parent?.capability.deadline !== deadline;
300
+ const interceptors = ownDeadline
301
+ ? [timeoutInterceptor({ ms: Math.max(0, deadline - Date.now()), appliesTo: () => true }), ...(options.interceptors ?? [])]
302
+ : (options.interceptors ?? []);
261
303
  if (interceptors.length === 0) return runInvocation<TOutput>(invocation, options);
262
304
 
263
305
  const dispatch = (index: number, current: Invocation, currentOptions: InvokeOptions): Promise<ProcedureOutput<unknown>> => {
@@ -269,8 +311,16 @@ export async function invokeProcedure<TOutput = unknown>(
269
311
  ...current,
270
312
  signal: changes.signal ?? current.signal,
271
313
  metadata: changes.metadata ?? current.metadata,
314
+ capability: changes.capability
315
+ ? narrowCapability(current.capability, changes.capability, currentOptions.clearanceOrder)
316
+ : current.capability,
272
317
  };
273
- return dispatch(index + 1, changed, { ...currentOptions, signal: changed.signal, metadata: changed.metadata });
318
+ return dispatch(index + 1, changed, {
319
+ ...currentOptions,
320
+ signal: changed.signal,
321
+ metadata: changed.metadata,
322
+ capability: changed.capability,
323
+ });
274
324
  });
275
325
  };
276
326
  return dispatch(0, invocation, options) as Promise<ProcedureOutput<TOutput>>;
@@ -282,6 +332,14 @@ async function runInvocation<TOutput>(invocation: Invocation, options: InvokeOpt
282
332
  const handler = procedure.handler!;
283
333
  if (options.signal?.aborted) throw abortError(path);
284
334
 
335
+ // The admission: before the input validation, so a refused call learns nothing from its input
336
+ const parent = invocation.parent;
337
+ if (!parent || examinesNestedCall(parent.procedure, parent.capability, invocation.capability)) {
338
+ const refusal = capabilityRefusal(procedure, invocation.capability);
339
+ if (refusal?.reason === "deadline") throw new InvocationError("TIMEOUT", refusal.message, path);
340
+ if (refusal) throw new InvocationError("FORBIDDEN", refusal.message, path, refusal.reason);
341
+ }
342
+
285
343
  let data: unknown = input;
286
344
  if (!options.inputValidated) {
287
345
  // A synchronous schema gives its result at once, so the handler starts before an await
@@ -301,11 +359,32 @@ async function runInvocation<TOutput>(invocation: Invocation, options: InvokeOpt
301
359
  }
302
360
 
303
361
  // The handler starts here, before the first await (the site's trace depends on it)
304
- const result: unknown = await handler(data, createProcedureContext(procedure, options, invocation));
362
+ const result: unknown = await handler(data, createProcedureContext(procedure, { ...options, capability: invocation.capability }, invocation));
363
+ // The output filter examines only what leaves the host
364
+ const admit = invocation.depth === 0 ? options.admitItem : undefined;
305
365
  if (isAsyncIterable(result)) {
306
- return { kind: "stream", items: validatedItems(procedure, result, options.signal) as AsyncGenerator<TOutput, void, undefined> };
366
+ const items = validatedItems(procedure, result, options.signal);
367
+ return {
368
+ kind: "stream",
369
+ items: (admit ? admittedItems(items, admit, invocation) : items) as AsyncGenerator<TOutput, void, undefined>,
370
+ };
371
+ }
372
+ const value = await validateOutput(procedure, result);
373
+ if (admit && !admit(value, invocation)) {
374
+ throw new InvocationError("FORBIDDEN", `The clearance of the call does not permit the output of ${pathToKey(path)}`, path, "clearance");
375
+ }
376
+ return { kind: "value", value: value as TOutput };
377
+ }
378
+
379
+ /** The items that the output filter of the host admits. */
380
+ async function* admittedItems(
381
+ items: AsyncGenerator<unknown, void, undefined>,
382
+ admit: (item: unknown, invocation: Invocation) => boolean,
383
+ invocation: Invocation,
384
+ ): AsyncGenerator<unknown, void, undefined> {
385
+ for await (const item of items) {
386
+ if (admit(item, invocation)) yield item;
307
387
  }
308
- return { kind: "value", value: (await validateOutput(procedure, result)) as TOutput };
309
388
  }
310
389
 
311
390
  /** This function finds the procedure at a path in the registry of the options, then runs it. */
@@ -379,6 +458,8 @@ export function createProcedureContext(
379
458
  path: procedure.path,
380
459
  client,
381
460
  registry: options.registry ?? PROCEDURE_REGISTRY,
461
+ capability: options.capability ?? UNLIMITED_CAPABILITY,
462
+ resources: invocation && options.resources ? options.resources(invocation) : (EMPTY_RESOURCES as ResourcesOf),
382
463
  };
383
464
  if (options.signal) context.signal = options.signal;
384
465
  if (options.repository) context.repository = options.repository;
@@ -391,23 +472,35 @@ export function createProcedureContext(
391
472
  return context;
392
473
  }
393
474
 
475
+ /** The resources of an invocation when the host gives none. */
476
+ const EMPTY_RESOURCES = Object.freeze({});
477
+
394
478
  /** The client of a procedure's context: it runs the nested calls from the registry. */
395
479
  function registryClient(caller: AnyProcedure, options: InvokeOptions): ProcedureClient {
396
- const start = <TOutput>(path: ProcedurePath, input: unknown): Promise<ProcedureOutput<TOutput>> => {
480
+ const start = <TOutput>(path: ProcedurePath, input: unknown, callOptions?: NestedCallOptions): Promise<ProcedureOutput<TOutput>> => {
397
481
  // The call policy of the host, with the immediate caller (BUGS-2026-07 H18, deep dive MCP-3).
398
482
  // It runs before the lookup, so a refused path tells nothing about the procedure.
399
483
  const refusal = nestedCallRefusal(caller, path, options);
400
484
  if (refusal) {
401
485
  return Promise.reject(new InvocationError(refusal.code, refusal.message, path));
402
486
  }
403
- return invokePath<TOutput>(path, input, options);
487
+ // A nested call gets the capability of its caller, or a narrower one that it asks for
488
+ const narrow = callOptions?.narrow;
489
+ if (!narrow) return invokePath<TOutput>(path, input, options);
490
+ let capability: Capability;
491
+ try {
492
+ capability = narrowCapability(options.capability, narrow, options.clearanceOrder);
493
+ } catch (error) {
494
+ return Promise.reject(error);
495
+ }
496
+ return invokePath<TOutput>(path, input, { ...options, capability });
404
497
  };
405
498
  return {
406
- async call<TInput, TOutput>(path: ProcedurePath, input: TInput): Promise<TOutput> {
407
- return outputValue(await start<TOutput>(path, input), path);
499
+ async call<TInput, TOutput>(path: ProcedurePath, input: TInput, callOptions?: NestedCallOptions): Promise<TOutput> {
500
+ return outputValue(await start<TOutput>(path, input, callOptions), path);
408
501
  },
409
- stream<TInput, TOutput>(path: ProcedurePath, input: TInput): AsyncIterable<TOutput> {
410
- return streamOutput(() => start<TOutput>(path, input));
502
+ stream<TInput, TOutput>(path: ProcedurePath, input: TInput, callOptions?: NestedCallOptions): AsyncIterable<TOutput> {
503
+ return streamOutput(() => start<TOutput>(path, input, callOptions));
411
504
  },
412
505
  };
413
506
  }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Procedure modules and explicit registries (architecture review, roadmap 3.1).
3
3
  *
4
- * Importing a package root registers its procedures in the global `PROCEDURE_REGISTRY`
5
- * (DECISIONS-2026-10, "Every package root registers its procedures"). That stays. A host that
6
- * wants its own registry uses the module of each package instead: the `./module` entry of a
7
- * package exports its procedures as data, and importing it registers nothing.
4
+ * Importing a package registers nothing (ARCHITECTURE-PROPOSALS-2026-10, P9). A host with its own
5
+ * registry gives it the module of each package: the `./module` entry exports the procedures as
6
+ * data. The global `PROCEDURE_REGISTRY` gets the procedures of a package from its `./register`
7
+ * entry (`import "@procedures/git/register"`).
8
8
  *
9
9
  * ```ts
10
10
  * import { createRegistry, Client, LocalTransport } from "@procedures/client";
@@ -18,16 +18,15 @@
18
18
  * rule of a server all use the registry of the invocation (`ctx.registry`).
19
19
  */
20
20
 
21
- import { ProcedureRegistry, RegistryError } from "./registry.js";
21
+ import { PROCEDURE_REGISTRY, ProcedureRegistry, RegistryError } from "./registry.js";
22
22
  import { coreProcedures } from "./core/index.js";
23
23
  import { metaProcedures as definitionProcedures } from "./define-procedure.js";
24
24
  import type { AnyProcedure, ProcedureModule, RegistrationOptions } from "./types.js";
25
25
 
26
26
  /**
27
- * The procedures that `@procedures/client` registers in the global registry when it is
28
- * imported: the control flow (`client.chain`, `client.map` and the others) and
29
- * `procedure.define`/`get`/`list`/`delete`. `createRegistry()` always registers this module, so an
30
- * explicit registry has what the global registry has.
27
+ * The procedures of `@procedures/client`: the control flow (`client.chain`, `client.map` and the
28
+ * others) and `procedure.define`/`get`/`list`/`delete`. `createRegistry()` always registers this
29
+ * module. `import "@procedures/client/register"` registers it in the global registry.
31
30
  */
32
31
  export const clientModule: ProcedureModule = {
33
32
  name: "@procedures/client",
@@ -105,3 +104,14 @@ export function registerModules(
105
104
  export function createRegistry(...modules: ModuleInput[]): ProcedureRegistry {
106
105
  return registerModules(new ProcedureRegistry(), [clientModule, ...modules]);
107
106
  }
107
+
108
+ /**
109
+ * This function registers the procedures of `clientModule` in a registry: the global registry by
110
+ * default. A procedure that the registry has already stays. The `./register` entry of the package
111
+ * starts this function.
112
+ */
113
+ export function registerClientProcedures(registry: ProcedureRegistry = PROCEDURE_REGISTRY): void {
114
+ for (const procedure of clientModule.procedures) {
115
+ if (!registry.has(procedure.path)) registry.register(procedure);
116
+ }
117
+ }
@@ -69,10 +69,101 @@ export interface ProcedureMetadata {
69
69
  */
70
70
  calls?: readonly string[];
71
71
 
72
+ /**
73
+ * The side effects of the procedure (CAPABILITIES-2026-10). A host with a restriction of
74
+ * effects refuses a procedure without this declaration.
75
+ */
76
+ effects?: Effects;
77
+
72
78
  /** Custom metadata fields */
73
79
  [key: string]: unknown;
74
80
  }
75
81
 
82
+ // =============================================================================
83
+ // Capabilities (CAPABILITIES-2026-10)
84
+ // =============================================================================
85
+
86
+ /**
87
+ * The side effects of a procedure. The resource names are strings that the application chooses.
88
+ * The standard names are `fs` (the files of the host), `network` (systems outside the host),
89
+ * `process` (it starts any program) and `registry` (the procedure registry).
90
+ */
91
+ export interface Effects {
92
+ /** The resources that the procedure reads. Absent: not known. */
93
+ readonly reads?: readonly string[];
94
+ /** The resources that the procedure changes. An empty list: it changes nothing. Absent: not known. */
95
+ readonly writes?: readonly string[];
96
+ /** True: a second call with the same input changes nothing more. The default is false. */
97
+ readonly idempotent?: boolean;
98
+ /**
99
+ * True: the procedure can delete or overwrite data that it did not make. The default is true for
100
+ * a procedure that writes, as in MCP. A procedure that writes only new data declares false.
101
+ */
102
+ readonly destructive?: boolean;
103
+ }
104
+
105
+ /** The effects that a capability permits. An absent field permits everything of its kind. */
106
+ export interface PermittedEffects {
107
+ /** The resources that a call can read: names, or `"any"`. */
108
+ readonly reads?: readonly string[] | "any";
109
+ /** The resources that a call can change: names, or `"any"`. An empty list: no change. */
110
+ readonly writes?: readonly string[] | "any";
111
+ /** False: a call cannot start a destructive procedure. */
112
+ readonly destructive?: boolean;
113
+ }
114
+
115
+ /**
116
+ * The types that an application gives to the framework, by declaration merging:
117
+ *
118
+ * ```ts
119
+ * declare module "@procedures/client" {
120
+ * interface Register {
121
+ * clearance: "public" | "shared" | "private";
122
+ * resources: { world: World };
123
+ * }
124
+ * }
125
+ * ```
126
+ */
127
+ export interface Register {}
128
+
129
+ /** The type of a clearance value: `Register["clearance"]`, or `unknown`. */
130
+ export type ClearanceOf = Register extends { clearance: infer C } ? C : unknown;
131
+
132
+ /** The type of `ctx.resources`: `Register["resources"]`, or a record of unknown values. */
133
+ export type ResourcesOf = Register extends { resources: infer R } ? R : Readonly<Record<string, unknown>>;
134
+
135
+ /**
136
+ * The permissions of one invocation. A nested call gets the same capability or a narrower one,
137
+ * and only the host makes a capability (from its authentication). The object is frozen.
138
+ */
139
+ export interface Capability<C = ClearanceOf> {
140
+ /** The data that the call can see. Absent: no limit. The application gives the order. */
141
+ readonly clearance?: C;
142
+ /** The effects that the call and its nested calls can have. Absent: all effects. */
143
+ readonly effects?: PermittedEffects;
144
+ /** The time (milliseconds since the epoch) at which the call and its nested calls stop. */
145
+ readonly deadline?: number;
146
+ }
147
+
148
+ /**
149
+ * A request for a narrower capability (a nested call, an interceptor). The framework calculates
150
+ * the result, so a request for more has no effect.
151
+ */
152
+ export interface CapabilityRequest<C = ClearanceOf> {
153
+ clearance?: C;
154
+ effects?: PermittedEffects;
155
+ /** The time from now, in milliseconds. The result is the earlier of the two deadlines. */
156
+ timeoutMs?: number;
157
+ }
158
+
159
+ /** The order of clearance values: a partial order with a meet (the highest common lower value). */
160
+ export interface ClearanceOrder<C = ClearanceOf> {
161
+ /** True when `a` is the same as or lower than `b`. */
162
+ leq(a: C, b: C): boolean;
163
+ /** The highest value that is the same as or lower than both. */
164
+ meet(a: C, b: C): C;
165
+ }
166
+
76
167
  // =============================================================================
77
168
  // Procedure Context
78
169
  // =============================================================================
@@ -172,6 +263,18 @@ export interface ProcedureContext {
172
263
  * `hydrateInput`, so a trace sees the events of its own program only.
173
264
  */
174
265
  onRef?: ((event: HydrationEvent) => void) | undefined;
266
+
267
+ /**
268
+ * The capability of this invocation (CAPABILITIES-2026-10): its clearance, permitted effects
269
+ * and deadline. The nested calls of `ctx.client` get it, or a narrower one. Frozen.
270
+ */
271
+ capability: Capability;
272
+
273
+ /**
274
+ * The resources that the host gives to this invocation, for its capability: a view of a
275
+ * database for a lower clearance, for example. Empty when the host gives none.
276
+ */
277
+ resources: ResourcesOf;
175
278
  }
176
279
 
177
280
  /**
@@ -195,7 +298,8 @@ export interface ProcedureClient {
195
298
  */
196
299
  call<TInput = unknown, TOutput = unknown>(
197
300
  path: ProcedurePath,
198
- input: TInput
301
+ input: TInput,
302
+ options?: NestedCallOptions
199
303
  ): Promise<TOutput>;
200
304
 
201
305
  /**
@@ -205,10 +309,17 @@ export interface ProcedureClient {
205
309
  */
206
310
  stream?<TInput = unknown, TOutput = unknown>(
207
311
  path: ProcedurePath,
208
- input: TInput
312
+ input: TInput,
313
+ options?: NestedCallOptions
209
314
  ): AsyncIterable<TOutput>;
210
315
  }
211
316
 
317
+ /** The options of one nested call. */
318
+ export interface NestedCallOptions {
319
+ /** A narrower capability for the nested call. A request for more has no effect. */
320
+ narrow?: CapabilityRequest | undefined;
321
+ }
322
+
212
323
  // =============================================================================
213
324
  // Procedure Handler
214
325
  // =============================================================================
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Importing this file registers the procedures of `@procedures/client` in the global registry:
3
+ * `import "@procedures/client/register"`. These are the control flow (`client.chain`, `client.map`
4
+ * and the others) and `procedure.define`/`get`/`list`/`delete`. The other entries of the package
5
+ * register nothing (ARCHITECTURE-PROPOSALS-2026-10, P9).
6
+ */
7
+
8
+ import { registerClientProcedures } from "./procedures/module.js";
9
+
10
+ registerClientProcedures();
@@ -12,10 +12,14 @@
12
12
  * procedures (a host for an outside caller sets it).
13
13
  * - `timeoutMs`: the time limit of each call from a caller (not of its nested calls).
14
14
  * - `token`, `browser`: the checks of the network transports (`client-server` applies them).
15
+ * - `capability`, `readOnly`, `clearanceOrder`, `resources`, `admitItem`: the capability of each
16
+ * call and what the host gives for it (CAPABILITIES-2026-10).
15
17
  */
16
18
 
17
- import type { ProcedurePath } from "../procedures/types.js";
18
- import type { Interceptor } from "../procedures/invoke.js";
19
+ import type { Capability, ClearanceOrder, ProcedurePath, ResourcesOf } from "../procedures/types.js";
20
+ import type { Interceptor, Invocation } from "../procedures/invoke.js";
21
+ import { narrowCapability } from "../procedures/capability.js";
22
+ import type { ServerRequest } from "./types.js";
19
23
  import { timeoutInterceptor } from "../procedures/interceptors.js";
20
24
  import type { BrowserGuardOptions } from "./browser-guard.js";
21
25
  import type { ProcedureRegistry } from "../procedures/registry.js";
@@ -67,6 +71,20 @@ export interface HostPolicyOptions {
67
71
  token?: string | undefined;
68
72
  /** The browser checks of the network transports (`allowedOrigins`, `allowedHosts`, `allowGet`). */
69
73
  browser?: BrowserGuardOptions | undefined;
74
+ /**
75
+ * The capability of each call from a caller: one value, or a function of the request. The
76
+ * function reads only what the transport authenticated (the token, a session), not a claim of
77
+ * the caller. Without it: no limits.
78
+ */
79
+ capability?: Capability | ((request: ServerRequest<unknown>) => Capability | undefined) | undefined;
80
+ /** True: the callers start only the procedures that change nothing (`effects.writes` is empty). */
81
+ readOnly?: boolean | undefined;
82
+ /** The order of the clearance values. */
83
+ clearanceOrder?: ClearanceOrder | undefined;
84
+ /** The resources of each invocation for its capability (`ctx.resources`). */
85
+ resources?: ((invocation: Invocation) => ResourcesOf) | undefined;
86
+ /** The filter of the output that leaves the host. */
87
+ admitItem?: ((item: unknown, invocation: Invocation) => boolean) | undefined;
70
88
  }
71
89
 
72
90
  /** What a host lets its callers do. */
@@ -76,6 +94,22 @@ export interface HostPolicy {
76
94
  readonly interceptors: readonly Interceptor[];
77
95
  readonly token: string | undefined;
78
96
  readonly browser: BrowserGuardOptions;
97
+ /** The capability of a call from a caller, or undefined for no limits. */
98
+ readonly capabilityOf: ((request?: ServerRequest<unknown>) => Capability | undefined) | undefined;
99
+ readonly clearanceOrder: ClearanceOrder | undefined;
100
+ readonly resources: ((invocation: Invocation) => ResourcesOf) | undefined;
101
+ readonly admitItem: ((item: unknown, invocation: Invocation) => boolean) | undefined;
102
+ }
103
+
104
+ /** The capability function of a policy: the capability of the options, narrowed by `readOnly`. */
105
+ function capabilityOfOptions(options: HostPolicyOptions): HostPolicy["capabilityOf"] {
106
+ const { capability, readOnly } = options;
107
+ if (capability === undefined && !readOnly) return undefined;
108
+ const readOnlyRequest = readOnly ? { effects: { writes: [] as readonly string[] } } : undefined;
109
+ return (request) => {
110
+ const base = typeof capability === "function" ? (request ? capability(request) : undefined) : capability;
111
+ return readOnlyRequest ? narrowCapability(base, readOnlyRequest, options.clearanceOrder) : base;
112
+ };
79
113
  }
80
114
 
81
115
  function exposeOf(options: HostPolicyOptions): ((path: ProcedurePath) => boolean) | undefined {
@@ -106,6 +140,10 @@ export function hostPolicy(options: HostPolicyOptions = {}): HostPolicy {
106
140
  interceptors,
107
141
  token: options.token,
108
142
  browser: options.browser ?? {},
143
+ capabilityOf: capabilityOfOptions(options),
144
+ clearanceOrder: options.clearanceOrder,
145
+ resources: options.resources,
146
+ admitItem: options.admitItem,
109
147
  };
110
148
  }
111
149
 
@@ -117,5 +155,9 @@ export function procedureServerOptions(policy: HostPolicy, registry?: ProcedureR
117
155
  ...(policy.expose ? { expose: policy.expose } : {}),
118
156
  strictCalls: policy.strictCalls,
119
157
  ...(policy.interceptors.length > 0 ? { interceptors: policy.interceptors } : {}),
158
+ ...(policy.capabilityOf ? { capability: policy.capabilityOf } : {}),
159
+ ...(policy.clearanceOrder ? { clearanceOrder: policy.clearanceOrder } : {}),
160
+ ...(policy.resources ? { resources: policy.resources } : {}),
161
+ ...(policy.admitItem ? { admitItem: policy.admitItem } : {}),
120
162
  };
121
163
  }