@verax-ai/proxy 0.1.1 → 0.1.3
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/approvals.d.ts +30 -1
- package/dist/approvals.js +64 -3
- package/dist/atomic-write.d.ts +1 -0
- package/dist/atomic-write.js +65 -0
- package/dist/in-flight-log.d.ts +12 -0
- package/dist/in-flight-log.js +65 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/inputs.d.ts +7 -1
- package/dist/inputs.js +35 -18
- package/dist/jsonl-tail.d.ts +26 -0
- package/dist/jsonl-tail.js +218 -0
- package/dist/ledger-manifest.d.ts +88 -0
- package/dist/ledger-manifest.js +225 -0
- package/dist/ledger.d.ts +88 -9
- package/dist/ledger.js +541 -86
- package/dist/policy.js +9 -1
- package/dist/proxy.js +266 -166
- package/dist/serial-queue.d.ts +5 -0
- package/dist/serial-queue.js +9 -0
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
package/dist/policy.js
CHANGED
|
@@ -197,7 +197,15 @@ export function loadPolicy(json) {
|
|
|
197
197
|
if (id === null)
|
|
198
198
|
return null;
|
|
199
199
|
const found = document.rules.find((r) => r.id === id);
|
|
200
|
-
return found
|
|
200
|
+
return found
|
|
201
|
+
? {
|
|
202
|
+
id: found.id,
|
|
203
|
+
text: found.text,
|
|
204
|
+
...(found.spend?.dailyMaxMinor !== undefined
|
|
205
|
+
? { spend: { dailyMaxMinor: found.spend.dailyMaxMinor } }
|
|
206
|
+
: {}),
|
|
207
|
+
}
|
|
208
|
+
: null;
|
|
201
209
|
},
|
|
202
210
|
evaluate(call, principal, ctx) {
|
|
203
211
|
if (call.name === "pay") {
|
package/dist/proxy.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { signDecisionRecord } from "@cedulon/core";
|
|
4
|
-
import { approvePending, approvalsLogFor, drainApprovalCommands } from "./approvals.js";
|
|
4
|
+
import { approvePending, approvalsLogFor, createApprovalBudgetGuard, drainApprovalCommands, spentTodayMinorOf, } from "./approvals.js";
|
|
5
|
+
import { SerialQueue } from "./serial-queue.js";
|
|
5
6
|
import { diskProbe } from "./disk.js";
|
|
7
|
+
import { markEnded, markStarted, startedWithoutEnd } from "./in-flight-log.js";
|
|
6
8
|
import { effectDescriptor, sha256Canonical } from "./hash.js";
|
|
7
9
|
import { inputsLogFor } from "./inputs.js";
|
|
8
10
|
import { countedWork, hasPrimaryEffect, lookupDecisionByRef, lookupReauthByHash, lookupResolvedBy, noteResolution, noteTenantRef, } from "./ledger.js";
|
|
@@ -129,6 +131,13 @@ function toolCallOf(call) {
|
|
|
129
131
|
export function createProxy(deps) {
|
|
130
132
|
const inputsLog = deps.inputsLog ?? inputsLogFor(deps.ledger);
|
|
131
133
|
const approvals = approvalsLogFor(deps.ledger);
|
|
134
|
+
const admission = new SerialQueue();
|
|
135
|
+
const inFlight = new Map();
|
|
136
|
+
const budgetGuard = createApprovalBudgetGuard({
|
|
137
|
+
policy: deps.policy,
|
|
138
|
+
approvals,
|
|
139
|
+
now: deps.now,
|
|
140
|
+
});
|
|
132
141
|
deps.ledger.effectSigner = deps.effectSigner;
|
|
133
142
|
const ledgerDir = deps.ledger.dir;
|
|
134
143
|
const stateDir = typeof ledgerDir === "string" ? ledgerDir : null;
|
|
@@ -192,30 +201,6 @@ export function createProxy(deps) {
|
|
|
192
201
|
return "daily-limited";
|
|
193
202
|
return null;
|
|
194
203
|
}
|
|
195
|
-
function utcDayStart(ms) {
|
|
196
|
-
const d = new Date(ms);
|
|
197
|
-
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
198
|
-
}
|
|
199
|
-
function spentTodayMinorOf(rows, nowMs, currency) {
|
|
200
|
-
const start = utcDayStart(nowMs);
|
|
201
|
-
const end = start + 86_400_000;
|
|
202
|
-
let sum = 0;
|
|
203
|
-
for (const row of rows) {
|
|
204
|
-
if (row.subject !== "spend")
|
|
205
|
-
continue;
|
|
206
|
-
if (row.status !== "pending" && row.status !== "approved")
|
|
207
|
-
continue;
|
|
208
|
-
const created = row.createdAtMs ?? row.expiresAtMs - deps.policy.approvalTtlMs;
|
|
209
|
-
if (created < start || created >= end)
|
|
210
|
-
continue;
|
|
211
|
-
if (row.args.currency !== currency)
|
|
212
|
-
continue;
|
|
213
|
-
const amt = row.args.amountMinor;
|
|
214
|
-
if (typeof amt === "number")
|
|
215
|
-
sum += amt;
|
|
216
|
-
}
|
|
217
|
-
return sum;
|
|
218
|
-
}
|
|
219
204
|
async function spendReauth(requestHash, inputs, timestampMs, subject) {
|
|
220
205
|
const existingReauth = await lookupReauthByHash(deps.ledger, requestHash);
|
|
221
206
|
if (existingReauth)
|
|
@@ -309,6 +294,43 @@ export function createProxy(deps) {
|
|
|
309
294
|
reasonCode: null,
|
|
310
295
|
};
|
|
311
296
|
}
|
|
297
|
+
function launchInner(dispatchedCall, principal, allowRef, flightKey) {
|
|
298
|
+
// Written before the tool runs, removed after the effect is recorded. A
|
|
299
|
+
// mark that survives the process is what tells a restarted body that this
|
|
300
|
+
// ref was already started (in-flight-log.ts).
|
|
301
|
+
if (stateDir !== null)
|
|
302
|
+
markStarted(stateDir, flightKey, dispatchedCall.name);
|
|
303
|
+
const work = runInner(dispatchedCall, principal, allowRef);
|
|
304
|
+
inFlight.set(flightKey, work);
|
|
305
|
+
return { kind: "run", work, key: flightKey, replayRef: allowRef };
|
|
306
|
+
}
|
|
307
|
+
async function retryPolicyDeny(dispatchedCall, principal, requestHash, inputs, timestampMs, subject, scopedRef) {
|
|
308
|
+
let spendCtx;
|
|
309
|
+
if (dispatchedCall.name === "spend") {
|
|
310
|
+
const approvalRows = await approvals.listAll();
|
|
311
|
+
spendCtx = {
|
|
312
|
+
spentTodayMinor: (currency) => spentTodayMinorOf(approvalRows, timestampMs, currency, deps.policy.approvalTtlMs),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const verdict = deps.policy.evaluate(dispatchedCall, principal, spendCtx);
|
|
316
|
+
if (verdict.decision !== "deny")
|
|
317
|
+
return null;
|
|
318
|
+
const denyRef = deps.nonce();
|
|
319
|
+
await writeRecord({
|
|
320
|
+
decision: "deny",
|
|
321
|
+
reasonCode: verdict.reasonCode,
|
|
322
|
+
ref: denyRef,
|
|
323
|
+
requestHash,
|
|
324
|
+
inputs: {
|
|
325
|
+
...inputs,
|
|
326
|
+
approver: { id: "verax-proxy", via: "proxy", resolves: scopedRef },
|
|
327
|
+
},
|
|
328
|
+
effectHash: null,
|
|
329
|
+
subject,
|
|
330
|
+
timestampMs,
|
|
331
|
+
});
|
|
332
|
+
return denied(verdict.reasonCode, denyRef);
|
|
333
|
+
}
|
|
312
334
|
return {
|
|
313
335
|
approvals,
|
|
314
336
|
inputsLog,
|
|
@@ -330,6 +352,7 @@ export function createProxy(deps) {
|
|
|
330
352
|
policyHash: defer.policyHash,
|
|
331
353
|
approvals,
|
|
332
354
|
inputsLog,
|
|
355
|
+
budgetGuard,
|
|
333
356
|
});
|
|
334
357
|
});
|
|
335
358
|
}
|
|
@@ -366,168 +389,245 @@ export function createProxy(deps) {
|
|
|
366
389
|
return denied("ledger-disk-low", ref);
|
|
367
390
|
}
|
|
368
391
|
const given = readRef(call.arguments);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (existing
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
decision: "deny",
|
|
393
|
-
reasonCode: "ref-reuse",
|
|
394
|
-
ref,
|
|
395
|
-
requestHash,
|
|
396
|
-
inputs: resolved.inputs,
|
|
397
|
-
effectHash: null,
|
|
398
|
-
subject: call.name,
|
|
399
|
-
timestampMs,
|
|
400
|
-
});
|
|
401
|
-
return denied("ref-reuse", ref);
|
|
402
|
-
}
|
|
403
|
-
if (existing.decision === "defer") {
|
|
404
|
-
const snap = await approvals.get(scopedRef);
|
|
405
|
-
const boundHit = lookupResolvedBy(deps.ledger, scopedRef);
|
|
406
|
-
if (boundHit?.kind === "expired") {
|
|
407
|
-
if (snap?.status === "pending")
|
|
408
|
-
await approvals.updateStatus(scopedRef, "expired");
|
|
409
|
-
return denied("expired", boundHit.ref);
|
|
410
|
-
}
|
|
411
|
-
if (snap && timestampMs > snap.expiresAtMs) {
|
|
412
|
-
const expireRef = deps.nonce();
|
|
392
|
+
const plan = await admission.enqueue(async () => {
|
|
393
|
+
if (given === "invalid") {
|
|
394
|
+
const ref = deps.nonce();
|
|
395
|
+
await writeRecord({
|
|
396
|
+
decision: "deny",
|
|
397
|
+
reasonCode: "ref-invalid",
|
|
398
|
+
ref,
|
|
399
|
+
requestHash,
|
|
400
|
+
inputs: resolved.inputs,
|
|
401
|
+
effectHash: null,
|
|
402
|
+
subject: call.name,
|
|
403
|
+
timestampMs,
|
|
404
|
+
});
|
|
405
|
+
return { kind: "done", result: denied("ref-invalid", ref) };
|
|
406
|
+
}
|
|
407
|
+
if (typeof given === "string") {
|
|
408
|
+
// The ledger, the approvals snapshot and the resolution index all key on the
|
|
409
|
+
// scoped ref. The brain only ever sees and resends the raw `_ref` it chose.
|
|
410
|
+
const scopedRef = scopedClaimsRef(principal, given);
|
|
411
|
+
const existing = await lookupDecisionByRef(deps.ledger, given, tenantKey(principal));
|
|
412
|
+
if (existing) {
|
|
413
|
+
if (existing.requestHash !== requestHash) {
|
|
414
|
+
const ref = deps.nonce();
|
|
413
415
|
await writeRecord({
|
|
414
416
|
decision: "deny",
|
|
415
|
-
reasonCode: "
|
|
416
|
-
ref
|
|
417
|
+
reasonCode: "ref-reuse",
|
|
418
|
+
ref,
|
|
417
419
|
requestHash,
|
|
418
|
-
inputs:
|
|
419
|
-
...resolved.inputs,
|
|
420
|
-
approver: { id: "verax-proxy", via: "proxy", resolves: scopedRef },
|
|
421
|
-
},
|
|
420
|
+
inputs: resolved.inputs,
|
|
422
421
|
effectHash: null,
|
|
423
422
|
subject: call.name,
|
|
424
423
|
timestampMs,
|
|
425
424
|
});
|
|
426
|
-
|
|
427
|
-
return denied("expired", expireRef);
|
|
425
|
+
return { kind: "done", result: denied("ref-reuse", ref) };
|
|
428
426
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
427
|
+
if (existing.decision === "defer") {
|
|
428
|
+
const snap = await approvals.get(scopedRef);
|
|
429
|
+
const boundHit = lookupResolvedBy(deps.ledger, scopedRef);
|
|
430
|
+
if (boundHit?.kind === "expired") {
|
|
431
|
+
if (snap?.status === "pending")
|
|
432
|
+
await approvals.updateStatus(scopedRef, "expired");
|
|
433
|
+
return { kind: "done", result: denied("expired", boundHit.ref) };
|
|
435
434
|
}
|
|
436
|
-
if (
|
|
437
|
-
|
|
435
|
+
if (snap && timestampMs > snap.expiresAtMs) {
|
|
436
|
+
const expireRef = deps.nonce();
|
|
437
|
+
await writeRecord({
|
|
438
|
+
decision: "deny",
|
|
439
|
+
reasonCode: "expired",
|
|
440
|
+
ref: expireRef,
|
|
441
|
+
requestHash,
|
|
442
|
+
inputs: {
|
|
443
|
+
...resolved.inputs,
|
|
444
|
+
approver: { id: "verax-proxy", via: "proxy", resolves: scopedRef },
|
|
445
|
+
},
|
|
446
|
+
effectHash: null,
|
|
447
|
+
subject: call.name,
|
|
448
|
+
timestampMs,
|
|
449
|
+
});
|
|
450
|
+
await approvals.updateStatus(scopedRef, "expired");
|
|
451
|
+
return { kind: "done", result: denied("expired", expireRef) };
|
|
438
452
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (allow.
|
|
442
|
-
|
|
453
|
+
const allowRef = snap?.allowRef ?? (boundHit?.kind === "allow" ? boundHit.ref : undefined);
|
|
454
|
+
const allow = allowRef ? await lookupDecisionByRef(deps.ledger, allowRef) : null;
|
|
455
|
+
if (allow?.ref && allow.decision === "allow" && allow.reasonCode === "approved-by-operator") {
|
|
456
|
+
const bound = await inputsLog.get(allow.ref);
|
|
457
|
+
if (bound?.approver?.resolves && bound.approver.resolves !== scopedRef) {
|
|
458
|
+
return { kind: "done", result: deferred(given) };
|
|
459
|
+
}
|
|
460
|
+
if (!snap?.allowRef) {
|
|
461
|
+
await approvals.updateStatus(scopedRef, "approved", { allowRef: allow.ref });
|
|
462
|
+
}
|
|
463
|
+
if (await hasPrimaryEffect(deps.ledger, allow.ref)) {
|
|
464
|
+
return { kind: "done", result: allowedReplay(allow.ref) };
|
|
465
|
+
}
|
|
466
|
+
if (allow.subject === "spend") {
|
|
467
|
+
return {
|
|
468
|
+
kind: "done",
|
|
469
|
+
result: await spendReauth(requestHash, resolved.inputs, timestampMs, call.name),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
const policyDeny = await retryPolicyDeny(dispatched, principal, requestHash, resolved.inputs, timestampMs, call.name, scopedRef);
|
|
473
|
+
if (policyDeny)
|
|
474
|
+
return { kind: "done", result: policyDeny };
|
|
475
|
+
const flying = inFlight.get(scopedRef);
|
|
476
|
+
if (flying)
|
|
477
|
+
return { kind: "wait", work: flying, replayRef: allow.ref };
|
|
478
|
+
// An approved allow means "the operator said yes", not "it ran",
|
|
479
|
+
// so the first retry after an approval is the first run. Only a
|
|
480
|
+
// mark left on disk says a run was already started and never
|
|
481
|
+
// finished — that one cannot be run again.
|
|
482
|
+
if (stateDir !== null && startedWithoutEnd(stateDir, scopedRef)) {
|
|
483
|
+
const unknownRef = deps.nonce();
|
|
484
|
+
await writeRecord({
|
|
485
|
+
decision: "deny",
|
|
486
|
+
reasonCode: "outcome-unknown",
|
|
487
|
+
ref: unknownRef,
|
|
488
|
+
requestHash,
|
|
489
|
+
inputs: {
|
|
490
|
+
...resolved.inputs,
|
|
491
|
+
approver: { id: "verax-proxy", via: "proxy", resolves: scopedRef },
|
|
492
|
+
},
|
|
493
|
+
effectHash: null,
|
|
494
|
+
subject: call.name,
|
|
495
|
+
timestampMs,
|
|
496
|
+
});
|
|
497
|
+
return { kind: "done", result: denied("outcome-unknown", unknownRef) };
|
|
498
|
+
}
|
|
499
|
+
return launchInner(dispatched, principal, allow.ref, scopedRef);
|
|
443
500
|
}
|
|
444
|
-
return
|
|
501
|
+
return { kind: "done", result: deferred(given) };
|
|
445
502
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (existing.decision === "deny") {
|
|
449
|
-
return denied(existing.reasonCode, given, call);
|
|
450
|
-
}
|
|
451
|
-
if (existing.decision === "allow" && existing.ref) {
|
|
452
|
-
if (await hasPrimaryEffect(deps.ledger, existing.ref)) {
|
|
453
|
-
return allowedReplay(existing.ref);
|
|
503
|
+
if (existing.decision === "deny") {
|
|
504
|
+
return { kind: "done", result: denied(existing.reasonCode, given, call) };
|
|
454
505
|
}
|
|
455
|
-
if (existing.
|
|
456
|
-
|
|
506
|
+
if (existing.decision === "allow" && existing.ref) {
|
|
507
|
+
if (await hasPrimaryEffect(deps.ledger, existing.ref)) {
|
|
508
|
+
return { kind: "done", result: allowedReplay(existing.ref) };
|
|
509
|
+
}
|
|
510
|
+
if (existing.subject === "spend") {
|
|
511
|
+
return {
|
|
512
|
+
kind: "done",
|
|
513
|
+
result: await spendReauth(requestHash, resolved.inputs, timestampMs, call.name),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const policyDeny = await retryPolicyDeny(dispatched, principal, requestHash, resolved.inputs, timestampMs, call.name, scopedRef);
|
|
517
|
+
if (policyDeny)
|
|
518
|
+
return { kind: "done", result: policyDeny };
|
|
519
|
+
const flying = inFlight.get(scopedRef);
|
|
520
|
+
if (flying)
|
|
521
|
+
return { kind: "wait", work: flying, replayRef: existing.ref };
|
|
522
|
+
const denyRef = deps.nonce();
|
|
523
|
+
await writeRecord({
|
|
524
|
+
decision: "deny",
|
|
525
|
+
reasonCode: "outcome-unknown",
|
|
526
|
+
ref: denyRef,
|
|
527
|
+
requestHash,
|
|
528
|
+
inputs: {
|
|
529
|
+
...resolved.inputs,
|
|
530
|
+
approver: { id: "verax-proxy", via: "proxy", resolves: scopedRef },
|
|
531
|
+
},
|
|
532
|
+
effectHash: null,
|
|
533
|
+
subject: call.name,
|
|
534
|
+
timestampMs,
|
|
535
|
+
});
|
|
536
|
+
return { kind: "done", result: denied("outcome-unknown", denyRef) };
|
|
457
537
|
}
|
|
458
|
-
return runInner(dispatched, principal, existing.ref);
|
|
459
538
|
}
|
|
460
539
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
reasonCode,
|
|
491
|
-
ref,
|
|
492
|
-
requestHash,
|
|
493
|
-
inputs: resolved.inputs,
|
|
494
|
-
effectHash: allow ? sha256Canonical(effectDescriptor(dispatched.name, dispatched.arguments)) : null,
|
|
495
|
-
subject: call.name,
|
|
496
|
-
timestampMs,
|
|
497
|
-
});
|
|
498
|
-
if (typeof given === "string") {
|
|
499
|
-
noteTenantRef(deps.ledger, tenantKey(principal), given);
|
|
500
|
-
}
|
|
501
|
-
if (decision === "defer") {
|
|
502
|
-
const rule = deps.policy.rule(verdict.rule);
|
|
503
|
-
const amount = dispatched.name === "spend" ? dispatched.arguments.amountMinor : dispatched.arguments.amount;
|
|
504
|
-
const payee = dispatched.arguments.payee;
|
|
505
|
-
const currency = dispatched.arguments.currency;
|
|
506
|
-
await approvals.append({
|
|
540
|
+
let spendCtx;
|
|
541
|
+
if (dispatched.name === "spend") {
|
|
542
|
+
const approvalRows = await approvals.listAll();
|
|
543
|
+
spendCtx = {
|
|
544
|
+
spentTodayMinor: (currency) => spentTodayMinorOf(approvalRows, timestampMs, currency, deps.policy.approvalTtlMs),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
const verdict = deps.policy.evaluate(dispatched, principal, spendCtx);
|
|
548
|
+
let reasonCode = resolved.reasonCode ?? verdict.reasonCode;
|
|
549
|
+
let decision = resolved.reasonCode ? "deny" : verdict.decision;
|
|
550
|
+
const bound = rateBound(timestampMs, decision === "allow" || decision === "defer");
|
|
551
|
+
if (bound) {
|
|
552
|
+
decision = "deny";
|
|
553
|
+
reasonCode = bound;
|
|
554
|
+
}
|
|
555
|
+
if (!bound &&
|
|
556
|
+
decision === "allow" &&
|
|
557
|
+
deps.checkTenantMismatch &&
|
|
558
|
+
(await deps.checkTenantMismatch(dispatched, principal))) {
|
|
559
|
+
decision = "deny";
|
|
560
|
+
reasonCode = "tenant-mismatch";
|
|
561
|
+
}
|
|
562
|
+
const ref = typeof given === "string" ? scopedClaimsRef(principal, given) : deps.nonce();
|
|
563
|
+
// What the brain is told: its own `_ref`, so a retry carries the same key back.
|
|
564
|
+
const shown = typeof given === "string" ? given : ref;
|
|
565
|
+
const allow = decision === "allow";
|
|
566
|
+
await writeRecord({
|
|
567
|
+
decision,
|
|
568
|
+
reasonCode,
|
|
507
569
|
ref,
|
|
508
570
|
requestHash,
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
inputsSummary: {
|
|
514
|
-
count: resolved.inputs.inputs.length,
|
|
515
|
-
ids: resolved.inputs.inputs.map((i) => i.id),
|
|
516
|
-
},
|
|
517
|
-
...(amount !== undefined ? { amount } : {}),
|
|
518
|
-
...(payee !== undefined ? { payee } : {}),
|
|
519
|
-
...(currency !== undefined ? { currency } : {}),
|
|
520
|
-
createdAtMs: timestampMs,
|
|
521
|
-
expiresAtMs: timestampMs + deps.policy.approvalTtlMs,
|
|
522
|
-
status: "pending",
|
|
523
|
-
brain: principal.brain,
|
|
571
|
+
inputs: resolved.inputs,
|
|
572
|
+
effectHash: allow ? sha256Canonical(effectDescriptor(dispatched.name, dispatched.arguments)) : null,
|
|
573
|
+
subject: call.name,
|
|
574
|
+
timestampMs,
|
|
524
575
|
});
|
|
525
|
-
|
|
576
|
+
if (typeof given === "string") {
|
|
577
|
+
noteTenantRef(deps.ledger, tenantKey(principal), given);
|
|
578
|
+
}
|
|
579
|
+
if (decision === "defer") {
|
|
580
|
+
const rule = deps.policy.rule(verdict.rule);
|
|
581
|
+
const amount = dispatched.name === "spend" ? dispatched.arguments.amountMinor : dispatched.arguments.amount;
|
|
582
|
+
const payee = dispatched.arguments.payee;
|
|
583
|
+
const currency = dispatched.arguments.currency;
|
|
584
|
+
await approvals.append({
|
|
585
|
+
ref,
|
|
586
|
+
requestHash,
|
|
587
|
+
subject: dispatched.name,
|
|
588
|
+
args: dispatched.arguments,
|
|
589
|
+
ruleId: verdict.rule,
|
|
590
|
+
ruleText: rule?.text ?? null,
|
|
591
|
+
inputsSummary: {
|
|
592
|
+
count: resolved.inputs.inputs.length,
|
|
593
|
+
ids: resolved.inputs.inputs.map((i) => i.id),
|
|
594
|
+
},
|
|
595
|
+
...(amount !== undefined ? { amount } : {}),
|
|
596
|
+
...(payee !== undefined ? { payee } : {}),
|
|
597
|
+
...(currency !== undefined ? { currency } : {}),
|
|
598
|
+
createdAtMs: timestampMs,
|
|
599
|
+
expiresAtMs: timestampMs + deps.policy.approvalTtlMs,
|
|
600
|
+
status: "pending",
|
|
601
|
+
brain: principal.brain,
|
|
602
|
+
});
|
|
603
|
+
return { kind: "done", result: deferred(shown) };
|
|
604
|
+
}
|
|
605
|
+
if (!allow) {
|
|
606
|
+
return { kind: "done", result: denied(reasonCode, shown, call) };
|
|
607
|
+
}
|
|
608
|
+
return launchInner(dispatched, principal, ref, ref);
|
|
609
|
+
});
|
|
610
|
+
if (plan.kind === "done")
|
|
611
|
+
return plan.result;
|
|
612
|
+
if (plan.kind === "wait") {
|
|
613
|
+
try {
|
|
614
|
+
await plan.work;
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
// The first call recorded the throw on its effect row.
|
|
618
|
+
}
|
|
619
|
+
return allowedReplay(plan.replayRef);
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
return await plan.work;
|
|
526
623
|
}
|
|
527
|
-
|
|
528
|
-
|
|
624
|
+
finally {
|
|
625
|
+
inFlight.delete(plan.key);
|
|
626
|
+
// runInner has written its effect row by now — on the throw path too,
|
|
627
|
+
// as `<name>:threw`. The outcome is on the ledger, so the mark goes.
|
|
628
|
+
if (stateDir !== null)
|
|
629
|
+
markEnded(stateDir, plan.key);
|
|
529
630
|
}
|
|
530
|
-
return runInner(dispatched, principal, ref);
|
|
531
631
|
},
|
|
532
632
|
};
|
|
533
633
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** One async tail so a later turn cannot start until the earlier one settles. */
|
|
2
|
+
export class SerialQueue {
|
|
3
|
+
tail = Promise.resolve();
|
|
4
|
+
enqueue(fn) {
|
|
5
|
+
const run = this.tail.then(fn, fn);
|
|
6
|
+
this.tail = run.then(() => undefined, () => undefined);
|
|
7
|
+
return run;
|
|
8
|
+
}
|
|
9
|
+
}
|
package/dist/types.d.ts
CHANGED