@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/LICENSE +192 -0
- package/README.md +60 -0
- package/dist/agents.d.ts +33 -0
- package/dist/agents.js +59 -0
- package/dist/approve-cli.js +27 -1
- package/dist/cli.js +5 -4
- package/dist/desktop.d.ts +23 -0
- package/dist/desktop.js +82 -30
- package/dist/doctor.js +87 -30
- package/dist/downstream.d.ts +28 -0
- package/dist/downstream.js +183 -0
- package/dist/inputs-read.d.ts +7 -0
- package/dist/inputs-read.js +47 -40
- package/dist/no-bypass-scan.js +7 -0
- package/dist/server.d.ts +183 -0
- package/dist/server.js +179 -37
- package/dist/tools/memory.d.ts +2 -0
- package/dist/tools/memory.js +7 -0
- package/dist/wiring.d.ts +10 -2
- package/dist/wiring.js +31 -4
- package/dist/witness.js +15 -13
- package/package.json +12 -1
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
|
-
|
|
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: "
|
|
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: {
|
|
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: "
|
|
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: {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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: "
|
|
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: {
|
|
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: "
|
|
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: "
|
|
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: {
|
|
70
|
-
|
|
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: "
|
|
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: {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
307
|
-
|
|
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.
|
|
325
|
-
approvals
|
|
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, {
|
|
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
|
|
356
|
-
const
|
|
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
|
}
|
package/dist/tools/memory.d.ts
CHANGED
|
@@ -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;
|
package/dist/tools/memory.js
CHANGED
|
@@ -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;
|