@procedures/client 0.44.0 → 0.45.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.
- package/CHANGELOG.md +19 -0
- package/README.md +2 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.d.ts +4 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +3 -1
- package/dist/browser.js.map +1 -1
- package/dist/procedures/core/index.d.ts +50 -2
- package/dist/procedures/core/index.d.ts.map +1 -1
- package/dist/procedures/core/index.js +148 -3
- package/dist/procedures/core/index.js.map +1 -1
- package/dist/procedures/core/operands.d.ts +4 -2
- package/dist/procedures/core/operands.d.ts.map +1 -1
- package/dist/procedures/core/operands.js +4 -3
- package/dist/procedures/core/operands.js.map +1 -1
- package/dist/procedures/index.d.ts +2 -2
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +1 -1
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/program.d.ts +14 -1
- package/dist/procedures/program.d.ts.map +1 -1
- package/dist/procedures/program.js +8 -1
- package/dist/procedures/program.js.map +1 -1
- package/dist/server/face-rules.d.ts +62 -0
- package/dist/server/face-rules.d.ts.map +1 -0
- package/dist/server/face-rules.js +79 -0
- package/dist/server/face-rules.js.map +1 -0
- package/dist/server/procedure-server.d.ts +5 -1
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +13 -2
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/server.d.ts +3 -2
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +4 -3
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +8 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +2 -2
- package/src/browser.ts +9 -0
- package/src/procedures/core/index.ts +211 -2
- package/src/procedures/core/operands.ts +5 -4
- package/src/procedures/index.ts +5 -0
- package/src/procedures/program.ts +24 -2
- package/src/server/face-rules.ts +115 -0
- package/src/server/procedure-server.ts +12 -2
- package/src/server/server.ts +4 -3
- 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
|
/**
|
package/src/procedures/index.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/src/server/server.ts
CHANGED
|
@@ -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;
|
package/src/server/types.ts
CHANGED
|
@@ -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
|
/**
|