@procedures/client 0.44.0 → 0.46.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 (48) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +2 -0
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/browser.d.ts +4 -2
  5. package/dist/browser.d.ts.map +1 -1
  6. package/dist/browser.js +3 -1
  7. package/dist/browser.js.map +1 -1
  8. package/dist/procedures/core/index.d.ts +50 -2
  9. package/dist/procedures/core/index.d.ts.map +1 -1
  10. package/dist/procedures/core/index.js +148 -3
  11. package/dist/procedures/core/index.js.map +1 -1
  12. package/dist/procedures/core/operands.d.ts +4 -2
  13. package/dist/procedures/core/operands.d.ts.map +1 -1
  14. package/dist/procedures/core/operands.js +4 -3
  15. package/dist/procedures/core/operands.js.map +1 -1
  16. package/dist/procedures/index.d.ts +2 -2
  17. package/dist/procedures/index.d.ts.map +1 -1
  18. package/dist/procedures/index.js +1 -1
  19. package/dist/procedures/index.js.map +1 -1
  20. package/dist/procedures/program.d.ts +14 -1
  21. package/dist/procedures/program.d.ts.map +1 -1
  22. package/dist/procedures/program.js +8 -1
  23. package/dist/procedures/program.js.map +1 -1
  24. package/dist/server/face-rules.d.ts +62 -0
  25. package/dist/server/face-rules.d.ts.map +1 -0
  26. package/dist/server/face-rules.js +79 -0
  27. package/dist/server/face-rules.js.map +1 -0
  28. package/dist/server/procedure-server.d.ts +5 -1
  29. package/dist/server/procedure-server.d.ts.map +1 -1
  30. package/dist/server/procedure-server.js +13 -2
  31. package/dist/server/procedure-server.js.map +1 -1
  32. package/dist/server/server.d.ts +3 -2
  33. package/dist/server/server.d.ts.map +1 -1
  34. package/dist/server/server.js +4 -3
  35. package/dist/server/server.js.map +1 -1
  36. package/dist/server/types.d.ts +8 -1
  37. package/dist/server/types.d.ts.map +1 -1
  38. package/dist/server/types.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/browser.ts +9 -0
  41. package/src/procedures/core/index.ts +211 -2
  42. package/src/procedures/core/operands.ts +5 -4
  43. package/src/procedures/index.ts +5 -0
  44. package/src/procedures/program.ts +24 -2
  45. package/src/server/face-rules.ts +115 -0
  46. package/src/server/procedure-server.ts +12 -2
  47. package/src/server/server.ts +4 -3
  48. package/src/server/types.ts +9 -1
@@ -15,7 +15,7 @@ import {
15
15
  type AnyProcedureRef,
16
16
  type RefScope,
17
17
  } from "../ref.js";
18
- import { hydrateInput, runControl, runOperand } from "../program.js";
18
+ import { hydrateInput, runControl, runOperand, type RunControlOptions } from "../program.js";
19
19
 
20
20
  /**
21
21
  * The operands of a control-flow procedure arrive raw: the procedure runs them itself. Its logic
@@ -37,10 +37,11 @@ function executorOf(ctx: ProcedureContext): <TIn, TOut>(path: ProcedurePath, inp
37
37
 
38
38
  /**
39
39
  * This function runs a control-flow procedure on the kernel: the runner of its kind runs the
40
- * operands of the input, in the scope of the caller.
40
+ * operands of the input, in the scope of the caller. The observer in `options` gets the result of
41
+ * each step of a chain (`client.trace`).
41
42
  */
42
- export function runControlFlow(name: string, input: unknown, ctx: ProcedureContext): Promise<unknown> {
43
- return runControl(name, input, { executor: executorOf(ctx), rawInput: rawRule(ctx), onRef: ctx.onRef, scope: scopeOf(input) });
43
+ export function runControlFlow(name: string, input: unknown, ctx: ProcedureContext, options: Pick<RunControlOptions, "onStep"> = {}): Promise<unknown> {
44
+ return runControl(name, input, { executor: executorOf(ctx), rawInput: rawRule(ctx), onRef: ctx.onRef, scope: scopeOf(input), onStep: options.onStep });
44
45
  }
45
46
 
46
47
  /**
@@ -323,6 +323,8 @@ export type {
323
323
  RunDocumentOptions,
324
324
  RunProgramOptions,
325
325
  RunOperandOptions,
326
+ RunControlOptions,
327
+ StepObserver,
326
328
  } from "./program.js";
327
329
 
328
330
  // The envelope form of a program (coeval D6, D9 and D10, roadmap item M7)
@@ -337,6 +339,9 @@ export {
337
339
  allCoreProcedures,
338
340
  allCoreModules,
339
341
  chainProcedure,
342
+ traceProcedure,
343
+ type TraceRow,
344
+ type TraceReason,
340
345
  parallelProcedure,
341
346
  conditionalProcedure,
342
347
  andProcedure,
@@ -995,6 +995,8 @@ export function programMachine(host: ProgramHost): Machine {
995
995
  setOwn(names, LAST, result.value);
996
996
  passed = result.value;
997
997
  results.push(result.value);
998
+ // The observer of this run (`RunControlOptions.onStep`) gets the result of each step
999
+ STEP_OBSERVERS.get(input.doc)?.(index, result.value);
998
1000
  }
999
1001
  return ok({ results, final: results[results.length - 1] });
1000
1002
  }
@@ -1377,15 +1379,35 @@ export async function runOperand(value: unknown, options: RunOperandOptions): Pr
1377
1379
  return runDocument(doc, { ...options, self: isAnyProcedureRef(value) ? frame : null });
1378
1380
  }
1379
1381
 
1382
+ /**
1383
+ * The observer of the steps of one `client.chain` run. It gets the index and the result of each
1384
+ * step that ends without an error, in the order of the steps.
1385
+ */
1386
+ export type StepObserver = (index: number, value: unknown) => void;
1387
+
1388
+ /** The options of `runControl()`. */
1389
+ export interface RunControlOptions extends RunDocumentOptions {
1390
+ /**
1391
+ * The observer of the steps of `client.chain` (`client.trace` uses it). Only the steps of this
1392
+ * chain go to it: a nested chain does not tell it. The other procedures ignore it.
1393
+ */
1394
+ readonly onStep?: StepObserver | undefined;
1395
+ }
1396
+
1397
+ /** The step observers, by the document of their `client.chain` run (`runControl()`). */
1398
+ const STEP_OBSERVERS = new WeakMap<object, StepObserver>();
1399
+
1380
1400
  /**
1381
1401
  * This function runs a control-flow procedure on the kernel: its input becomes a document of its
1382
1402
  * kind, and the runner of the kind runs the operands. The input is raw: its scope (`scopeOf()`)
1383
1403
  * gives the names of the caller.
1384
1404
  */
1385
- export async function runControl(name: string, input: unknown, options: RunDocumentOptions): Promise<unknown> {
1405
+ export async function runControl(name: string, input: unknown, options: RunControlOptions): Promise<unknown> {
1386
1406
  const value = desugarControlValue(name, input, { rawInput: options.rawInput, scoped: options.scope !== undefined });
1387
1407
  if (value === undefined) throw new TypeError(`client.${name}: the input does not have the shape of the procedure`);
1388
- return runDocument(controlDocument(name, value, input, options.scope !== undefined), options);
1408
+ const doc = controlDocument(name, value, input, options.scope !== undefined);
1409
+ if (options.onStep) STEP_OBSERVERS.set(doc, options.onStep);
1410
+ return runDocument(doc, options);
1389
1411
  }
1390
1412
 
1391
1413
  /** The document of the run of a control-flow procedure: its own entry names the runner of the procedure. */
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The rules of one face of a host (CLI-43, AG1-B): a narrowing and an expose rule for each call of
3
+ * the face.
4
+ *
5
+ * A host can serve one server through more than one face, for example a full face and a read-only
6
+ * face for a browser. `faceRules()` reads the rules of one face one time, and `apply()` adds them to
7
+ * each request of that face:
8
+ *
9
+ * - The narrowing becomes one more source of the narrowing key (`procedure-narrowing`, CLI-30). Thus
10
+ * the server applies the meet of all the sources: the capability of the policy, the request hook
11
+ * and the key of the caller. A narrowing can only narrow.
12
+ * - The expose rule goes to `ServerRequest.expose`. The server meets it with its own rule, for the
13
+ * call of the face and for each nested call. An expose rule can only narrow.
14
+ *
15
+ * The MCP face of `@procedures/mcp-server` and the HTTP and WebSocket faces of `@procedures/peer`
16
+ * use this module. It imports no Node code.
17
+ */
18
+
19
+ import { NARROWING_METADATA_KEY, addNarrowing, narrowWithin, narrowingOf } from "../procedures/narrowing.js";
20
+ import type { Capability, CapabilityRequest, ProcedurePath } from "../procedures/types.js";
21
+ import { exposePatterns, surfaceRule, type HostPolicy, type ToolSurface } from "./host-policy.js";
22
+ import type { ServerRequest } from "./types.js";
23
+
24
+ /** The rules of one face, as options of the face. */
25
+ export interface FaceRulesOptions {
26
+ /**
27
+ * A narrower capability for each call of the face, for example the read-only mode
28
+ * `{ effects: { writes: [] } }`. A narrowing that is not valid stops `faceRules()` with an error.
29
+ */
30
+ readonly narrowing?: CapabilityRequest<unknown> | readonly CapabilityRequest<unknown>[] | undefined;
31
+ /**
32
+ * The procedures that the callers of the face reach: the patterns of `hostPolicy({ expose })`, a
33
+ * tool surface or a function. The server meets this rule with the rule of the host.
34
+ */
35
+ readonly expose?: readonly string[] | ToolSurface | ((path: ProcedurePath) => boolean) | undefined;
36
+ }
37
+
38
+ /** The rules of one face, read one time. */
39
+ export interface FaceRules {
40
+ /** The sources of the narrowing as the decoder read them, frozen. The list is empty without a narrowing. */
41
+ readonly narrowings: readonly CapabilityRequest<unknown>[];
42
+ /** The expose rule of the face, or undefined without one. */
43
+ readonly expose: ((path: ProcedurePath) => boolean) | undefined;
44
+ /**
45
+ * The capability of the policy, narrowed by the narrowing of the face, without the time. A face
46
+ * lists its procedures with it: the time limits each call, not the list.
47
+ */
48
+ readonly capability: Capability | undefined;
49
+ /** This function gives the request with the rules of the face. It does not change the request that it gets. */
50
+ apply<T>(request: ServerRequest<T>): ServerRequest<T>;
51
+ }
52
+
53
+ /** This function gives the meet of two expose rules: a path passes when it passes each rule. */
54
+ export function meetExpose(
55
+ first: ((path: ProcedurePath) => boolean) | undefined,
56
+ second: ((path: ProcedurePath) => boolean) | undefined,
57
+ ): ((path: ProcedurePath) => boolean) | undefined {
58
+ if (!first) return second;
59
+ if (!second) return first;
60
+ return (path) => first(path) && second(path);
61
+ }
62
+
63
+ /** This function gives the rule of the expose option of a face. */
64
+ function exposeRule(expose: NonNullable<FaceRulesOptions["expose"]>): (path: ProcedurePath) => boolean {
65
+ if (typeof expose === "function") return expose;
66
+ return Array.isArray(expose) ? exposePatterns(expose as readonly string[]) : surfaceRule(expose as ToolSurface);
67
+ }
68
+
69
+ /**
70
+ * This function reads the rules of one face with the decoder and the meet of the wire (CLI-30).
71
+ * It throws for a part of the narrowing that is not valid, because each call of the face then gets
72
+ * a refusal. The argument `face` names the face in the message of the error, for example `MCP`.
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const rules = faceRules({ narrowing: { effects: { writes: [] } }, expose: ["graphapi.*"] }, policy, "read");
77
+ * const handler = { ...inner, handle: (request) => inner.handle(rules.apply(request)) };
78
+ * ```
79
+ */
80
+ export function faceRules(
81
+ options: FaceRulesOptions,
82
+ policy: Pick<HostPolicy, "capabilityOf" | "clearanceOrder">,
83
+ face?: string,
84
+ ): FaceRules {
85
+ let narrowings: readonly CapabilityRequest<unknown>[] = [];
86
+ let capability = policy.capabilityOf?.();
87
+ if (options.narrowing !== undefined) {
88
+ const wire = narrowingOf(options.narrowing);
89
+ // The sources as the decoder read them, frozen: a change of the options or of the metadata of a call does not change them
90
+ narrowings = Object.freeze(wire.narrowings.map((source) => Object.freeze({ ...source })));
91
+ const untimed = narrowings.map((source): CapabilityRequest<unknown> => ({
92
+ ...(source.clearance !== undefined ? { clearance: source.clearance } : {}),
93
+ ...(source.effects !== undefined ? { effects: source.effects } : {}),
94
+ }));
95
+ const listed = narrowWithin(capability, untimed, policy.clearanceOrder);
96
+ const refusal = [...wire.notes, ...listed.notes].find((note) => note.kind === "refused");
97
+ if (refusal) throw new Error(`The narrowing of ${face === undefined ? "the face" : `the ${face} face`} is not valid: ${refusal.message}`);
98
+ capability = listed.capability;
99
+ }
100
+ const expose = options.expose !== undefined ? exposeRule(options.expose) : undefined;
101
+ const sources = narrowings;
102
+ return {
103
+ narrowings,
104
+ expose,
105
+ capability,
106
+ apply: (request) => {
107
+ if (sources.length === 0 && !expose) return request;
108
+ return {
109
+ ...request,
110
+ ...(sources.length > 0 ? { metadata: { ...request.metadata, [NARROWING_METADATA_KEY]: addNarrowing(request.metadata[NARROWING_METADATA_KEY], sources) } } : {}),
111
+ ...(expose ? { expose: meetExpose(request.expose, expose) } : {}),
112
+ };
113
+ },
114
+ };
115
+ }
@@ -40,6 +40,7 @@ import type { ServerDebugReport } from "./types.js";
40
40
  import type { KeyedLimiter, LimiterStore } from "@mark1russell7/service/limiter";
41
41
  import { callLimiter, limitedCall, rateLimitedResponse, type CallLimit, type CallLimiter } from "./limits.js";
42
42
  import { writeLog } from "../logger.js";
43
+ import { meetExpose } from "./face-rules.js";
43
44
 
44
45
  // =============================================================================
45
46
  // Types
@@ -818,8 +819,16 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
818
819
  /**
819
820
  * The handlers that `register()` added come first (exact or pattern matches). Then the server
820
821
  * looks up the procedure of the method.
822
+ *
823
+ * The expose rule of the face of the request (`request.expose`, AG1-B) comes before both. A path
824
+ * outside it has no handler, also a handler of `register()` and an `alwaysOn` pin. A rule that is
825
+ * not a function admits no path, so a face that sets a wrong value does not open the server.
821
826
  */
822
- protected override findHandler(method: Method): ServerHandler | null {
827
+ protected override findHandler(method: Method, request?: ServerRequest<unknown>): ServerHandler | null {
828
+ if (request?.expose !== undefined) {
829
+ const rule: unknown = request.expose;
830
+ if (typeof rule !== "function" || rule(methodToPath(method)) !== true) return null;
831
+ }
823
832
  const handler = super.findHandler(method);
824
833
  if (handler) return handler;
825
834
  const procedure = this.resolveProcedure(method);
@@ -1115,7 +1124,8 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
1115
1124
  ...(request.elicit ? { elicit: request.elicit } : {}),
1116
1125
  ...(request.progress ? { progress: request.progress } : {}),
1117
1126
  repository,
1118
- expose: this.expose,
1127
+ // The rule of the face of the request meets the rule of the server, also for each nested call (AG1-B)
1128
+ expose: request.expose !== undefined ? meetExpose(this.expose, request.expose) : this.expose,
1119
1129
  strictCalls: this.strictCalls,
1120
1130
  interceptors: this.interceptors,
1121
1131
  capability,
@@ -309,7 +309,7 @@ export class Server {
309
309
 
310
310
  try {
311
311
  // Find handler
312
- const handler = this.findHandler(request.method);
312
+ const handler = this.findHandler(request.method, request);
313
313
  if (!handler) {
314
314
  throw new HandlerNotFoundError(request.method);
315
315
  }
@@ -351,9 +351,10 @@ export class Server {
351
351
 
352
352
  /**
353
353
  * Find handler for a method. A subclass can add handlers that it finds at request time
354
- * (`ProcedureServer` looks up its registry).
354
+ * (`ProcedureServer` looks up its registry). The request is absent for a lookup outside a call.
355
+ * `ProcedureServer` reads the expose rule of its face from it (AG1-B).
355
356
  */
356
- protected findHandler(method: Method): ServerHandler | null {
357
+ protected findHandler(method: Method, _request?: ServerRequest<unknown>): ServerHandler | null {
357
358
  for (const entry of this.handlers) {
358
359
  if (this.matchesMethod(entry.matcher, method)) {
359
360
  return entry.handler;
@@ -8,7 +8,7 @@
8
8
  import type { Elicit } from "../procedures/elicit.js";
9
9
  import type { ReportProgress } from "../procedures/progress.js";
10
10
  import type { Metadata, Status, Method } from "../client/types.js";
11
- import type { CapabilityRequest } from "../procedures/types.js";
11
+ import type { CapabilityRequest, ProcedurePath } from "../procedures/types.js";
12
12
  import type { ExposedError } from "./error-policy.js";
13
13
  import type { Logger } from "../logger.js";
14
14
  import { methodName } from "./method.js";
@@ -76,6 +76,14 @@ export interface ServerRequest<TReq = unknown> {
76
76
  * capability does not become wider.
77
77
  */
78
78
  narrowing?: CapabilityRequest | undefined;
79
+
80
+ /**
81
+ * The expose rule of the face that took the request (AG1-B), for example a read-only face of a
82
+ * browser. The face sets it, not the caller: it is a function, so JSON cannot carry it.
83
+ * `ProcedureServer` meets it with its own expose rule, for the call and for each nested call.
84
+ * Thus it can only narrow. A path outside it gets `NOT_FOUND`, as a path outside the rule of the server.
85
+ */
86
+ expose?: ((path: ProcedurePath) => boolean) | undefined;
79
87
  }
80
88
 
81
89
  /**