@verax-ai/body 0.1.0 → 0.1.2

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/server.d.ts CHANGED
@@ -1,5 +1,188 @@
1
1
  import { type Server } from "node:http";
2
2
  import type { BodyConfig } from "./config.ts";
3
3
  import { TOOL_NAMES } from "./wiring.ts";
4
+ export declare const TOOL_META: ({
5
+ name: string;
6
+ description: string;
7
+ inputSchema: {
8
+ type: string;
9
+ additionalProperties: boolean;
10
+ properties: {
11
+ id: {
12
+ type: string;
13
+ description: string;
14
+ };
15
+ body?: undefined;
16
+ source?: undefined;
17
+ validFromMs?: undefined;
18
+ validUntilMs?: undefined;
19
+ ref?: undefined;
20
+ to?: undefined;
21
+ text?: undefined;
22
+ _ref?: undefined;
23
+ amountMinor?: undefined;
24
+ currency?: undefined;
25
+ payee?: undefined;
26
+ reference?: undefined;
27
+ };
28
+ required: string[];
29
+ };
30
+ } | {
31
+ name: string;
32
+ description: string;
33
+ inputSchema: {
34
+ type: string;
35
+ additionalProperties: boolean;
36
+ properties: {
37
+ id: {
38
+ type: string;
39
+ description: string;
40
+ };
41
+ body: {
42
+ description: string;
43
+ };
44
+ source: {
45
+ type: string;
46
+ description: string;
47
+ };
48
+ validFromMs: {
49
+ type: string;
50
+ description: string;
51
+ };
52
+ validUntilMs: {
53
+ type: string;
54
+ description: string;
55
+ };
56
+ ref?: undefined;
57
+ to?: undefined;
58
+ text?: undefined;
59
+ _ref?: undefined;
60
+ amountMinor?: undefined;
61
+ currency?: undefined;
62
+ payee?: undefined;
63
+ reference?: undefined;
64
+ };
65
+ required: string[];
66
+ };
67
+ } | {
68
+ name: string;
69
+ description: string;
70
+ inputSchema: {
71
+ type: string;
72
+ additionalProperties: boolean;
73
+ properties: {
74
+ ref: {
75
+ type: string;
76
+ description: string;
77
+ };
78
+ id?: undefined;
79
+ body?: undefined;
80
+ source?: undefined;
81
+ validFromMs?: undefined;
82
+ validUntilMs?: undefined;
83
+ to?: undefined;
84
+ text?: undefined;
85
+ _ref?: undefined;
86
+ amountMinor?: undefined;
87
+ currency?: undefined;
88
+ payee?: undefined;
89
+ reference?: undefined;
90
+ };
91
+ required: string[];
92
+ };
93
+ } | {
94
+ name: string;
95
+ description: string;
96
+ inputSchema: {
97
+ type: string;
98
+ additionalProperties: boolean;
99
+ properties: {
100
+ id?: undefined;
101
+ body?: undefined;
102
+ source?: undefined;
103
+ validFromMs?: undefined;
104
+ validUntilMs?: undefined;
105
+ ref?: undefined;
106
+ to?: undefined;
107
+ text?: undefined;
108
+ _ref?: undefined;
109
+ amountMinor?: undefined;
110
+ currency?: undefined;
111
+ payee?: undefined;
112
+ reference?: undefined;
113
+ };
114
+ required?: undefined;
115
+ };
116
+ } | {
117
+ name: string;
118
+ description: string;
119
+ inputSchema: {
120
+ type: string;
121
+ additionalProperties: boolean;
122
+ properties: {
123
+ to: {
124
+ type: string;
125
+ description: string;
126
+ };
127
+ text: {
128
+ type: string;
129
+ description: string;
130
+ };
131
+ _ref: {
132
+ type: string;
133
+ description: string;
134
+ };
135
+ id?: undefined;
136
+ body?: undefined;
137
+ source?: undefined;
138
+ validFromMs?: undefined;
139
+ validUntilMs?: undefined;
140
+ ref?: undefined;
141
+ amountMinor?: undefined;
142
+ currency?: undefined;
143
+ payee?: undefined;
144
+ reference?: undefined;
145
+ };
146
+ required: string[];
147
+ };
148
+ } | {
149
+ name: string;
150
+ description: string;
151
+ inputSchema: {
152
+ type: string;
153
+ additionalProperties: boolean;
154
+ properties: {
155
+ amountMinor: {
156
+ type: string;
157
+ description: string;
158
+ };
159
+ currency: {
160
+ type: string;
161
+ description: string;
162
+ };
163
+ payee: {
164
+ type: string;
165
+ description: string;
166
+ };
167
+ reference: {
168
+ type: string;
169
+ description: string;
170
+ };
171
+ _ref: {
172
+ type: string;
173
+ description: string;
174
+ };
175
+ id?: undefined;
176
+ body?: undefined;
177
+ source?: undefined;
178
+ validFromMs?: undefined;
179
+ validUntilMs?: undefined;
180
+ ref?: undefined;
181
+ to?: undefined;
182
+ text?: undefined;
183
+ };
184
+ required: string[];
185
+ };
186
+ })[];
4
187
  export declare function listen(config: BodyConfig): Promise<Server>;
5
188
  export { TOOL_NAMES };
package/dist/server.js CHANGED
@@ -3,7 +3,7 @@ import { createServer } from "node:http";
3
3
  import { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js";
4
4
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
- import { approvalsLogFor, approvePending, explain, LedgerDenyUnrecorded, loadApprovalsFromDir, } from "@verax-ai/proxy";
6
+ import { approvalsLogFor, approvePending, createApprovalBudgetGuard, explain, LedgerDenyUnrecorded, loadApprovalsFromDir, } from "@verax-ai/proxy";
7
7
  import { createVerifier, readBearer, resourceMetadataUrl, wwwAuthenticate } from "./auth.js";
8
8
  import { bumpMetric, bumpUnauthenticated } from "./metrics.js";
9
9
  import { isRevokedJti } from "./revoke.js";
@@ -11,48 +11,101 @@ import { loadOrCreateSigners } from "./keys.js";
11
11
  import { matchingInputs } from "./inputs-read.js";
12
12
  import { readPolicySnapshots } from "./policy-store.js";
13
13
  import { readHeartbeat, readWitnessPulse } from "./health-extras.js";
14
+ import { agentsWindow } from "./agents.js";
14
15
  import { inventoryHealth, readInventoryFile } from "./inventory-file.js";
15
16
  import { createBodyServices, TOOL_NAMES } from "./wiring.js";
16
- const TOOL_META = [
17
+ // What a brain reads before it calls. Each description says what the tool is
18
+ // for, what it does and does not do, what the gate may answer, and what comes
19
+ // back; each parameter says its format and its bounds. The answers named here
20
+ // are the proxy's: `denied:<reason>:<ref>`, `deferred:approval-required:<ref>`
21
+ // and `allowed:<ref>` (packages/proxy/src/proxy.ts).
22
+ const ID_FORMAT = "1 to 128 characters of letters, digits, '.', '_' or '-', starting with a letter or digit; case-sensitive.";
23
+ const REF_FORMAT = "1 to 64 characters of letters, digits, '.', '_' or '-', starting with a letter or digit.";
24
+ const REF_PARAM = {
25
+ type: "string",
26
+ description: "Optional reference you choose for this call, " +
27
+ REF_FORMAT +
28
+ " Resend the same call with the same _ref after an operator approved it to receive allowed:<ref>; " +
29
+ "a _ref reused for a different call is refused with denied:ref-reuse.",
30
+ };
31
+ export const TOOL_META = [
17
32
  {
18
33
  name: "memory.get",
19
- description: "Read a memory item. Stale items return { stale: true } without the body.",
34
+ description: "Reads one memory item this tenant stored earlier with memory.put, by its id. " +
35
+ "Use it to recall a fact, a setting or a note before acting on it; nothing is written. " +
36
+ "Like every call it passes the policy gate and leaves a signed decision record; an id that belongs to another tenant is answered with a signed deny. " +
37
+ "Returns the stored item as JSON: {id, body, source, validFromMs, validUntilMs, versionHash}. " +
38
+ "Outside the validity window the body is withheld: {stale: true, id, validUntilMs} after it, {notYetValid: true, id, validFromMs} before it. " +
39
+ 'An unknown id answers {error: "not-found", id}.',
20
40
  inputSchema: {
21
41
  type: "object",
22
42
  additionalProperties: false,
23
- properties: { id: { type: "string" } },
43
+ properties: {
44
+ id: { type: "string", description: "The id given to memory.put: " + ID_FORMAT },
45
+ },
24
46
  required: ["id"],
25
47
  },
26
48
  },
27
49
  {
28
50
  name: "memory.put",
29
- description: "Write a memory item. source and validUntilMs are required.",
51
+ description: "Writes one memory item for this tenant, or replaces the item with the same id, in the body's state directory on this machine. " +
52
+ "Use it to keep a fact for a later memory.get together with where it came from and how long it holds, so a stale fact is not served later. " +
53
+ "The call passes the policy gate and is recorded; the record carries the item's versionHash, a SHA-256 over id, body and validity window. " +
54
+ "Returns {ok: true, id, versionHash}. " +
55
+ 'A missing source answers {error: "source-required"}, a missing validUntilMs {error: "validUntilMs-required"}, a malformed id {error: "id-invalid"}.',
30
56
  inputSchema: {
31
57
  type: "object",
32
58
  additionalProperties: false,
33
59
  properties: {
34
- id: { type: "string" },
35
- body: {},
36
- source: { type: "object" },
37
- validFromMs: { type: "number" },
38
- validUntilMs: { type: "number" },
60
+ id: {
61
+ type: "string",
62
+ description: "Identifier to store under and read back with memory.get: " +
63
+ ID_FORMAT +
64
+ " An existing item with this id is replaced.",
65
+ },
66
+ body: {
67
+ description: "The value to keep, as any JSON: object, array, string, number or boolean. Stored as given and returned as given by memory.get.",
68
+ },
69
+ source: {
70
+ type: "object",
71
+ description: 'Where the value came from, as a JSON object of your choosing, for example {"kind": "document", "ref": "invoice-2026-09.pdf"}. Required; stored with the item so a later reader can weigh it.',
72
+ },
73
+ validFromMs: {
74
+ type: "number",
75
+ description: "Optional. Unix time in milliseconds from which the item may be served; before it memory.get answers notYetValid. Omit to serve it at once.",
76
+ },
77
+ validUntilMs: {
78
+ type: "number",
79
+ description: "Required. Unix time in milliseconds after which memory.get answers stale and withholds the body. Pick the moment the fact should no longer be trusted.",
80
+ },
39
81
  },
40
82
  required: ["id", "body", "source", "validUntilMs"],
41
83
  },
42
84
  },
43
85
  {
44
86
  name: "audit.explain",
45
- description: "Explain a decision ref against the ledger.",
87
+ description: "Reads one decision back from the signed ledger by its ref and explains it. " +
88
+ "Use it to check what the body decided about an earlier call and whether the recorded effect matched, before repeating a call or reporting on it; read-only, and the lookup itself is recorded too. " +
89
+ "Returns JSON with record (the signed decision's claims: tool, verdict, policy hash, timestamps), effect (the reconciled effect row), finding (match, mismatch or missing), witnessClass, guarantee, warnings, trustRoot (which key verified the signatures), and for a held call pair with its defer and resolution records. " +
90
+ "A ref that does not exist, or belongs to another tenant, is answered with the same signed deny, so neither case reveals the other.",
46
91
  inputSchema: {
47
92
  type: "object",
48
93
  additionalProperties: false,
49
- properties: { ref: { type: "string" } },
94
+ properties: {
95
+ ref: {
96
+ type: "string",
97
+ description: "The decision reference: the ref returned by an earlier call, also the tail of a denied:… or deferred:… answer; " +
98
+ REF_FORMAT,
99
+ },
100
+ },
50
101
  required: ["ref"],
51
102
  },
52
103
  },
53
104
  {
54
105
  name: "message.read",
55
- description: "Read the local inbox fixture as JSON.",
106
+ description: "Reads this tenant's inbox, the messages placed for it in the body's state directory on this machine, and returns them as a JSON array in arrival order, oldest first. " +
107
+ "Use it to see what has arrived before deciding what to answer. " +
108
+ "Takes no arguments; read-only; the call is recorded like every other. An empty or absent inbox answers [].",
56
109
  inputSchema: {
57
110
  type: "object",
58
111
  additionalProperties: false,
@@ -61,34 +114,63 @@ const TOOL_META = [
61
114
  },
62
115
  {
63
116
  name: "message.send",
64
- description: "Queue a message on the local outbox. Does not open a network.",
117
+ description: "Queues one message in this tenant's outbox on this machine for the delivery step the operator runs; this call opens no network connection and nothing leaves the body from it. " +
118
+ "Use it to hand off a message, not to deliver one. " +
119
+ "Like every call it passes the policy gate and leaves a signed decision record. " +
120
+ "The gate reads the host after the last '@' in to and allows it only when it is on the policy's egress allow-list; otherwise the call is refused with denied:egress-blocked, or denied:egress-host-missing when no host can be read. " +
121
+ "A policy rule in approve mode holds the call for an operator instead and answers deferred:approval-required:<ref>. " +
122
+ "Returns {queued: true, ref}, where ref is the decision reference for audit.explain.",
65
123
  inputSchema: {
66
124
  type: "object",
67
125
  additionalProperties: false,
68
126
  properties: {
69
- to: { type: "string" },
70
- text: { type: "string" },
127
+ to: {
128
+ type: "string",
129
+ description: "Recipient address with a host after the last '@', for example ops@example.com. The host, lower-cased, is matched against the policy's egress list.",
130
+ },
131
+ text: { type: "string", description: "The message body as plain text. Stored as given in the outbox row." },
132
+ _ref: REF_PARAM,
71
133
  },
72
134
  required: ["to", "text"],
73
135
  },
74
136
  },
75
137
  {
76
138
  name: "spend",
77
- description: "Authorizes a payment; does not move money.",
139
+ description: "Asks the body to authorize a payment and records the decision; the body never moves money, so authorized: true is a signed permission for a later payment step, not a transfer. " +
140
+ "Use it before any payment so that amount, currency, payee and reference are checked against the policy: the one currency the policy names, a cap per call, a payee list and a daily limit. " +
141
+ "A call outside those bounds is refused with a signed deny naming the bound: denied:spend-cap, denied:spend-payee, denied:spend-currency or denied:spend-daily. " +
142
+ "A call within them is held for an operator on this machine and answers deferred:approval-required:<ref>; once that ref is approved (verax approve, or the panel), resending the same call with the same _ref answers allowed:<ref>, and the authorization is recorded as {authorized: true, ref, amountMinor, currency, payee, reference}. " +
143
+ "Without a spend rule in the policy every call answers denied:spend-not-wired.",
78
144
  inputSchema: {
79
145
  type: "object",
80
146
  additionalProperties: false,
81
147
  properties: {
82
- amountMinor: { type: "integer" },
83
- currency: { type: "string" },
84
- payee: { type: "string" },
85
- reference: { type: "string" },
148
+ amountMinor: {
149
+ type: "integer",
150
+ description: "Amount in the currency's minor unit as a positive integer: cents, kuruş or pence, so 1250 means 12.50. Compared against the policy's cap per call and daily limit.",
151
+ },
152
+ currency: {
153
+ type: "string",
154
+ description: "ISO 4217 code in upper case, for example USD, EUR or TRY. Must equal the currency the policy's spend rule names.",
155
+ },
156
+ payee: {
157
+ type: "string",
158
+ description: "Who is to be paid, spelled exactly as the policy's payee list spells it (a merchant or account name). A payee off the list is refused.",
159
+ },
160
+ reference: {
161
+ type: "string",
162
+ description: "Your own reference for this payment, such as an invoice or order id. Recorded with the authorization and used by verax reconcile to match the card statement.",
163
+ },
164
+ _ref: REF_PARAM,
86
165
  },
87
166
  required: ["amountMinor", "currency", "payee", "reference"],
88
167
  },
89
168
  },
90
169
  ];
91
170
  const MAX_BODY_BYTES = 1024 * 1024;
171
+ // Unbounded GET /api/ledger stringified ~690 MB at 200k rows and threw Invalid string length (HTTP 500).
172
+ const DEFAULT_LEDGER_LIMIT = 1000;
173
+ const MAX_LEDGER_LIMIT = 5000;
92
174
  const responseSlot = new AsyncLocalStorage();
93
175
  function contentLengthOverLimit(req) {
94
176
  const raw = req.headers["content-length"];
@@ -209,14 +291,26 @@ export async function listen(config) {
209
291
  send(res, 200, { ok: true });
210
292
  return;
211
293
  }
212
- const decisions = await services.ledger.decisions();
213
- const effects = await services.ledger.effects();
214
- const last = decisions[decisions.length - 1];
294
+ // The ledger counts its own lines as it writes them; asking it is free.
295
+ // Reading both files back to count them was one second per call on a
296
+ // 100k-decision ledger, five times a minute for as long as a panel was open.
297
+ // When the active piece cannot be read back, the fallback below counts
298
+ // the merged files; it cannot tell which rows sit in the active piece,
299
+ // so the piece fields stay out of that degraded answer.
300
+ let counted = services.ledger.counts();
301
+ if (!counted) {
302
+ const decisions = await services.ledger.decisions();
303
+ const effects = await services.ledger.effects();
304
+ const last = decisions[decisions.length - 1];
305
+ counted = {
306
+ decisions: decisions.length,
307
+ effects: effects.length,
308
+ lastDecisionMs: last ? last.claims.timestampMs : null,
309
+ };
310
+ }
215
311
  send(res, 200, {
216
312
  ok: true,
217
- decisions: decisions.length,
218
- effects: effects.length,
219
- lastDecisionMs: last ? last.claims.timestampMs : null,
313
+ ...counted,
220
314
  lock: services.ledger.lockStatus(),
221
315
  heartbeat: readHeartbeat(config.stateDir),
222
316
  witness: readWitnessPulse(config.stateDir),
@@ -244,7 +338,8 @@ export async function listen(config) {
244
338
  const apiInventory = url.pathname === "/api/inventory";
245
339
  const contest = req.method === "POST" && url.pathname.startsWith("/api/contest/");
246
340
  const apiApprove = req.method === "POST" && url.pathname === "/api/approve";
247
- if (url.pathname !== "/mcp" && !apiLedger && !apiInventory && !contest && !apiApprove) {
341
+ const apiAgents = req.method === "GET" && url.pathname === "/api/agents";
342
+ if (url.pathname !== "/mcp" && !apiLedger && !apiInventory && !contest && !apiApprove && !apiAgents) {
248
343
  send(res, 404, { error: "not-found" });
249
344
  return;
250
345
  }
@@ -303,9 +398,8 @@ export async function listen(config) {
303
398
  send(res, 409, { error: "stale", requestHash: waiting.requestHash });
304
399
  return;
305
400
  }
306
- const decisions = await services.ledger.decisions();
307
- const defer = decisions.find((d) => d.claims.ref === ref && d.claims.decision === "defer");
308
- if (!defer) {
401
+ const defer = services.ledger.lookupByRef(ref);
402
+ if (!defer || defer.decision !== "defer") {
309
403
  send(res, 404, { error: "unknown-ref" });
310
404
  return;
311
405
  }
@@ -313,6 +407,7 @@ export async function listen(config) {
313
407
  // which says nothing about the person holding the phone; the session's
314
408
  // own subject does.
315
409
  const approver = verified.principal.brain;
410
+ const approvals = approvalsLogFor(services.ledger);
316
411
  const outcome = await approvePending({
317
412
  ledger: services.ledger,
318
413
  recordSigner: loadOrCreateSigners(config.stateDir).recordSigner,
@@ -321,18 +416,26 @@ export async function listen(config) {
321
416
  ref,
322
417
  approverId: approver,
323
418
  via: "http",
324
- policyHash: defer.claims.policyHash,
325
- approvals: approvalsLogFor(services.ledger),
419
+ policyHash: defer.policyHash,
420
+ approvals,
421
+ budgetGuard: createApprovalBudgetGuard({
422
+ policy: services.policy,
423
+ approvals,
424
+ now: () => Date.now(),
425
+ }),
326
426
  });
327
427
  if (!outcome.ok) {
328
428
  const code = outcome.reason === "unknown-ref" || outcome.reason === "snapshot-missing" ? 404 : 409;
329
- send(res, code, { error: outcome.reason });
429
+ send(res, code, {
430
+ error: outcome.reason,
431
+ ...(outcome.allowRef ? { allowRef: outcome.allowRef } : {}),
432
+ });
330
433
  return;
331
434
  }
332
435
  send(res, 200, { allowRef: outcome.allowRef, approver });
333
436
  return;
334
437
  }
335
- if (apiLedger || apiInventory || contest) {
438
+ if (apiLedger || apiInventory || contest || apiAgents) {
336
439
  // The audit doors hand out the whole ledger: every tenant's decisions, the
337
440
  // inputs documents that name their principals, and the approval snapshots
338
441
  // that carry spend arguments. `verax:read` is a brain scope, so it cannot be
@@ -349,11 +452,47 @@ export async function listen(config) {
349
452
  send(res, 200, readInventoryFile(config.inventoryFile));
350
453
  return;
351
454
  }
455
+ if (apiAgents) {
456
+ // The last day unless the caller names a window; the roster's
457
+ // agents are on the list whether or not they acted in it.
458
+ const now = Date.now();
459
+ const fromRaw = url.searchParams.get("from");
460
+ const toRaw = url.searchParams.get("to");
461
+ const from = fromRaw === null ? now - 86_400_000 : Number(fromRaw);
462
+ const to = toRaw === null ? now : Number(toRaw);
463
+ if (!Number.isFinite(from) || !Number.isFinite(to)) {
464
+ send(res, 400, { error: "bad-window" });
465
+ return;
466
+ }
467
+ send(res, 200, await agentsWindow({
468
+ ledger: services.ledger,
469
+ stateDir: config.stateDir,
470
+ inventoryFile: config.inventoryFile,
471
+ fromMs: from,
472
+ toMs: to,
473
+ }));
474
+ return;
475
+ }
352
476
  if (apiLedger) {
353
477
  const from = Number(url.searchParams.get("from") ?? "0");
354
478
  const to = Number(url.searchParams.get("to") ?? String(Number.MAX_SAFE_INTEGER));
355
- const decisions = (await services.ledger.decisions()).filter((d) => d.claims.timestampMs >= from && d.claims.timestampMs < to);
356
- const effects = (await services.ledger.effects()).filter((e) => e.row.timestampMs >= from && e.row.timestampMs < to);
479
+ const limitRaw = url.searchParams.get("limit");
480
+ const parsedLimit = limitRaw === null ? undefined : Number(limitRaw);
481
+ if (!Number.isFinite(from) ||
482
+ !Number.isFinite(to) ||
483
+ (parsedLimit !== undefined && (!Number.isFinite(parsedLimit) || parsedLimit < 0))) {
484
+ send(res, 400, { error: "bad-window" });
485
+ return;
486
+ }
487
+ const limit = parsedLimit === undefined ? DEFAULT_LEDGER_LIMIT : Math.min(MAX_LEDGER_LIMIT, Math.floor(parsedLimit));
488
+ // The window is read from the end of the files, so a day costs a
489
+ // day whatever the ledger's age. With a limit, the newest rows of
490
+ // the window come back and `more` says the rest is there to ask for.
491
+ const { rows: decisions, more, piecesTouched } = await services.ledger.decisionsWindow(from, to, limit);
492
+ // Effects belong to the decisions returned. When the limit cut the
493
+ // window, the oldest decision returned is where their window starts.
494
+ const effectsFrom = more && decisions.length > 0 ? decisions[0].claims.timestampMs : from;
495
+ const effects = await services.ledger.effectsWindow(effectsFrom, to);
357
496
  const hashes = [...new Set(decisions.map((d) => d.claims.policyHash))];
358
497
  send(res, 200, {
359
498
  decisions,
@@ -362,6 +501,9 @@ export async function listen(config) {
362
501
  policies: readPolicySnapshots(config.stateDir, hashes),
363
502
  inputs: await matchingInputs(config.stateDir, decisions),
364
503
  approvals: loadApprovalsFromDir(config.stateDir),
504
+ more,
505
+ piecesTouched,
506
+ limit,
365
507
  });
366
508
  return;
367
509
  }
@@ -1,6 +1,8 @@
1
1
  import { type Principal, type ToolCall, type ToolResult } from "@verax-ai/proxy";
2
2
  /** True when `id` exists under a different tenant. Does not read legacy `memory/`. */
3
3
  export declare function memoryBelongsToOtherTenant(stateDir: string, id: string, selfKey: string): boolean;
4
+ /** True when this tenant already has `id` under its own memory path. */
5
+ export declare function memoryExistsForTenant(stateDir: string, id: string, principal: Principal): boolean;
4
6
  export declare function readMemoryMeta(stateDir: string, id: string, principal?: Principal): Promise<{
5
7
  versionHash: string;
6
8
  validFromMs: number;
@@ -51,6 +51,13 @@ export function memoryBelongsToOtherTenant(stateDir, id, selfKey) {
51
51
  }
52
52
  return false;
53
53
  }
54
+ /** True when this tenant already has `id` under its own memory path. */
55
+ export function memoryExistsForTenant(stateDir, id, principal) {
56
+ const path = resolveMemoryPath(stateDir, id, principal);
57
+ if (path === null)
58
+ return false;
59
+ return existsSync(path);
60
+ }
54
61
  export async function readMemoryMeta(stateDir, id, principal) {
55
62
  if (!principal)
56
63
  return null;
package/dist/wiring.d.ts CHANGED
@@ -1,9 +1,14 @@
1
- import { createProxy, FileLedger, type EffectSigner, type ExplainOpts, type Principal, type RecordSigner, type ToolCall, type ToolResult } from "@verax-ai/proxy";
1
+ import { createProxy, FileLedger, type EffectSigner, type ExplainOpts, type Policy, type Principal, type RecordSigner, type ToolCall, type ToolResult } from "@verax-ai/proxy";
2
2
  export type ToolFn = (call: ToolCall, principal: Principal, ref?: string) => Promise<ToolResult>;
3
3
  export declare const TOOL_NAMES: readonly ["memory.get", "memory.put", "audit.explain", "message.read", "message.send", "spend"];
4
+ export type ExtraTool = {
5
+ name: string;
6
+ fn: ToolFn;
7
+ };
4
8
  export type BodyServices = {
5
9
  proxy: ReturnType<typeof createProxy>;
6
10
  ledger: FileLedger;
11
+ policy: Policy;
7
12
  policyHash: string;
8
13
  policyDocument: unknown;
9
14
  listTools: () => readonly string[];
@@ -11,7 +16,9 @@ export type BodyServices = {
11
16
  };
12
17
  /**
13
18
  * Tool functions live in a Map that is not exported. The only way to
14
- * reach them at runtime is `proxy.call` -> `inner`.
19
+ * reach them at runtime is `proxy.call` -> `inner`. Extra tools (a
20
+ * downstream prefix) enter that Map at construction; they still pass
21
+ * the gate.
15
22
  */
16
23
  export declare function createBodyServices(opts: {
17
24
  stateDir: string;
@@ -20,4 +27,5 @@ export declare function createBodyServices(opts: {
20
27
  effectSigner: EffectSigner;
21
28
  now?: () => number;
22
29
  nonce?: () => string;
30
+ extraTools?: readonly ExtraTool[];
23
31
  }): BodyServices;