@decentrys/agent 0.1.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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/browser/decentrys-agent.js +2051 -0
- package/dist/browser/decentrys-agent.mjs +2026 -0
- package/dist/client.d.ts +194 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +303 -0
- package/dist/client.js.map +1 -0
- package/dist/explain.d.ts +63 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +144 -0
- package/dist/explain.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger.d.ts +166 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +218 -0
- package/dist/ledger.js.map +1 -0
- package/dist/model.d.ts +256 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +74 -0
- package/dist/model.js.map +1 -0
- package/dist/policy.d.ts +137 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +845 -0
- package/dist/policy.js.map +1 -0
- package/package.json +64 -0
- package/src/client.test.ts +351 -0
- package/src/client.ts +392 -0
- package/src/explain.test.ts +148 -0
- package/src/explain.ts +204 -0
- package/src/index.ts +5 -0
- package/src/ledger.test.ts +192 -0
- package/src/ledger.ts +303 -0
- package/src/model.ts +324 -0
- package/src/policy.test.ts +762 -0
- package/src/policy.ts +951 -0
package/dist/explain.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Turning a decision into something a person can be held to.
|
|
4
|
+
*
|
|
5
|
+
* The question this file exists to answer is not "why was that blocked" — a
|
|
6
|
+
* block explains itself, loudly, at the moment it happens. It is **"why was
|
|
7
|
+
* that allowed"**, asked weeks later by someone looking at a drained account.
|
|
8
|
+
* A record that lists only what went wrong cannot answer it, so
|
|
9
|
+
* `explainAgentAction` renders every rule that was applied, the bound it
|
|
10
|
+
* carried and the value it saw, alongside the axes on which the policy set no
|
|
11
|
+
* bound at all. An allow with nothing behind it reads, correctly, as an allow
|
|
12
|
+
* with nothing behind it.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here recomputes anything. The explanation is a rendering of the
|
|
15
|
+
* decision record and cannot disagree with it — an explanation that is
|
|
16
|
+
* generated separately from the decision eventually explains a decision that
|
|
17
|
+
* was not made.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.explainAgentAction = explainAgentAction;
|
|
21
|
+
const protect_1 = require("@decentrys/protect");
|
|
22
|
+
const VERDICT_HEADLINE = {
|
|
23
|
+
allow: 'Permitted',
|
|
24
|
+
require_human_approval: 'Held for human approval',
|
|
25
|
+
deny: 'Denied',
|
|
26
|
+
};
|
|
27
|
+
/** What an operator would change, per rule, to permit the action. */
|
|
28
|
+
const REMEDIATION = {
|
|
29
|
+
AGENT_BINDING: 'Use the policy sealed for this agent, or bind this policy to it.',
|
|
30
|
+
ACTION_TYPE: 'Add this action type to `allowedActions`, or remove it from `deniedActions`.',
|
|
31
|
+
CHAIN: 'Add this chain to `allowedChains`.',
|
|
32
|
+
COUNTERPARTY: 'Add this counterparty to `allowedCounterparties`, or remove it from `blockedCounterparties`.',
|
|
33
|
+
CONTRACT: 'Add this contract to `allowedContracts`.',
|
|
34
|
+
TOKEN: 'Add this token to `allowedTokens`.',
|
|
35
|
+
APPROVAL_ALLOWANCE: 'Have the agent request a bounded allowance, or set `allowUnlimitedApprovals` — '
|
|
36
|
+
+ 'which makes any later compromise of the spender unbounded.',
|
|
37
|
+
VALUE_PER_ACTION: 'Raise `maxValuePerActionUsd`, or state the action\'s `valueUsd`.',
|
|
38
|
+
CUMULATIVE_SPEND: 'Wait for the window to roll over, raise the window\'s `maxValueUsd`, or state the '
|
|
39
|
+
+ 'action\'s `valueUsd`.',
|
|
40
|
+
RATE_LIMIT: 'Wait for the window to roll over, or raise the window\'s `maxActions`. A rate ceiling being '
|
|
41
|
+
+ 'hit repeatedly is usually the agent looping, not the ceiling being wrong.',
|
|
42
|
+
RISK_LEVEL: 'A person should review the evidence on the decision\'s assessment before this is permitted.',
|
|
43
|
+
ASSESSMENT_AVAILABILITY: 'Restore connectivity to Decentrys, or accept unscreened actions by setting '
|
|
44
|
+
+ '`failMode: \'open\'` — which signs during precisely the window an attacker would choose.',
|
|
45
|
+
HUMAN_APPROVAL_THRESHOLD: 'A person must approve this action. Raise the threshold only if the operator '
|
|
46
|
+
+ 'intends actions of this size to proceed unattended.',
|
|
47
|
+
};
|
|
48
|
+
function explainAgentAction(decision) {
|
|
49
|
+
const failing = decision.evaluations.filter((e) => e.outcome === 'fail');
|
|
50
|
+
const escalating = decision.evaluations.filter((e) => e.outcome === 'escalate');
|
|
51
|
+
const blocking = [...failing, ...escalating];
|
|
52
|
+
const headline = buildHeadline(decision, failing, escalating);
|
|
53
|
+
const because = blocking.map((e) => e.statement);
|
|
54
|
+
const unbounded = decision.evaluations
|
|
55
|
+
.filter((e) => e.outcome === 'not_configured')
|
|
56
|
+
.map((e) => e.statement);
|
|
57
|
+
// Deduplicated: two rules can share a remedy, and telling an operator the
|
|
58
|
+
// same thing twice trains them to skim the list.
|
|
59
|
+
const toProceed = [...new Set(blocking.map((e) => REMEDIATION[e.rule]))];
|
|
60
|
+
const explanation = {
|
|
61
|
+
decisionId: decision.decisionId,
|
|
62
|
+
agentId: decision.agentId,
|
|
63
|
+
verdict: decision.verdict,
|
|
64
|
+
headline,
|
|
65
|
+
because,
|
|
66
|
+
rulesApplied: decision.evaluations,
|
|
67
|
+
unbounded,
|
|
68
|
+
toProceed,
|
|
69
|
+
provenance: {
|
|
70
|
+
policyFingerprint: decision.policyFingerprint,
|
|
71
|
+
...(decision.policyVersion === undefined ? {} : { policyVersion: decision.policyVersion }),
|
|
72
|
+
actionDigest: decision.actionDigest,
|
|
73
|
+
modelVersion: decision.modelVersion,
|
|
74
|
+
...(decision.assessment === undefined ? {} : {
|
|
75
|
+
riskModelVersion: decision.assessment.modelVersion,
|
|
76
|
+
riskLevel: decision.assessment.riskLevel,
|
|
77
|
+
}),
|
|
78
|
+
decidedAt: decision.decidedAt,
|
|
79
|
+
},
|
|
80
|
+
text: '',
|
|
81
|
+
};
|
|
82
|
+
explanation.text = render(decision, explanation);
|
|
83
|
+
return explanation;
|
|
84
|
+
}
|
|
85
|
+
function buildHeadline(decision, failing, escalating) {
|
|
86
|
+
const prefix = VERDICT_HEADLINE[decision.verdict];
|
|
87
|
+
if (decision.verdict === 'allow') {
|
|
88
|
+
const checked = decision.evaluations.filter((e) => e.outcome === 'pass').length;
|
|
89
|
+
const unconfigured = decision.evaluations.filter((e) => e.outcome === 'not_configured').length;
|
|
90
|
+
return `${prefix}: ${checked} ${checked === 1 ? 'rule' : 'rules'} were applied and passed`
|
|
91
|
+
+ (unconfigured > 0
|
|
92
|
+
? `, and ${unconfigured} ${unconfigured === 1 ? 'axis was' : 'axes were'} left unbounded by this policy.`
|
|
93
|
+
: '.');
|
|
94
|
+
}
|
|
95
|
+
const driving = failing[0] ?? escalating[0];
|
|
96
|
+
return driving
|
|
97
|
+
? `${prefix}: ${driving.statement}`
|
|
98
|
+
: `${prefix}.`;
|
|
99
|
+
}
|
|
100
|
+
function render(decision, explanation) {
|
|
101
|
+
const lines = [
|
|
102
|
+
`${explanation.headline}`,
|
|
103
|
+
'',
|
|
104
|
+
`Agent: ${decision.agentId}`,
|
|
105
|
+
`Decision: ${decision.decisionId} at ${decision.decidedAt}`,
|
|
106
|
+
`Policy: ${decision.policyVersion ? `${decision.policyVersion} ` : ''}#${decision.policyFingerprint}`,
|
|
107
|
+
`Action: #${decision.actionDigest}`,
|
|
108
|
+
];
|
|
109
|
+
if (decision.assessment) {
|
|
110
|
+
lines.push(`Risk: ${decision.assessment.riskLevel} `
|
|
111
|
+
+ `(confidence ${decision.assessment.confidence}) — `
|
|
112
|
+
+ `${protect_1.RISK_LEVEL_MEANING[decision.assessment.riskLevel]}`);
|
|
113
|
+
}
|
|
114
|
+
else if (decision.assessmentUnavailable) {
|
|
115
|
+
lines.push(`Risk: not assessed — ${decision.assessmentUnavailable}`);
|
|
116
|
+
}
|
|
117
|
+
lines.push('', 'Rules applied:');
|
|
118
|
+
for (const rule of decision.evaluations) {
|
|
119
|
+
const bound = rule.limit === undefined ? '' : ` [limit ${rule.limit}]`;
|
|
120
|
+
const seen = rule.observed === undefined ? '' : ` [observed ${rule.observed}]`;
|
|
121
|
+
lines.push(` ${symbolFor(rule.outcome)} ${rule.rule}: ${rule.statement}${bound}${seen}`);
|
|
122
|
+
}
|
|
123
|
+
if (explanation.unbounded.length > 0) {
|
|
124
|
+
lines.push('', 'Left unbounded by this policy:');
|
|
125
|
+
for (const line of explanation.unbounded)
|
|
126
|
+
lines.push(` - ${line}`);
|
|
127
|
+
}
|
|
128
|
+
if (explanation.toProceed.length > 0) {
|
|
129
|
+
lines.push('', 'For this to proceed, the operator would have to:');
|
|
130
|
+
for (const line of explanation.toProceed)
|
|
131
|
+
lines.push(` - ${line}`);
|
|
132
|
+
}
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
|
135
|
+
function symbolFor(outcome) {
|
|
136
|
+
switch (outcome) {
|
|
137
|
+
case 'pass': return 'PASS ';
|
|
138
|
+
case 'fail': return 'FAIL ';
|
|
139
|
+
case 'escalate': return 'HOLD ';
|
|
140
|
+
case 'not_applicable': return 'n/a ';
|
|
141
|
+
case 'not_configured': return 'unset';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=explain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explain.js","sourceRoot":"","sources":["../src/explain.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./model"), exports);
|
|
18
|
+
__exportStar(require("./policy"), exports);
|
|
19
|
+
__exportStar(require("./ledger"), exports);
|
|
20
|
+
__exportStar(require("./explain"), exports);
|
|
21
|
+
__exportStar(require("./client"), exports);
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,0CAAwB;AACxB,2CAAyB;AACzB,2CAAyB;AACzB,4CAA0B;AAC1B,2CAAyB"}
|
package/dist/ledger.d.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cumulative spend and rate accounting.
|
|
3
|
+
*
|
|
4
|
+
* ## What this is, honestly
|
|
5
|
+
*
|
|
6
|
+
* **This ledger is in-process and in-memory.** It counts what one instance of
|
|
7
|
+
* one process admitted. It is not shared, not durable, and not correct across
|
|
8
|
+
* a restart or a horizontal scale-out: two replicas each enforcing a $10,000
|
|
9
|
+
* daily cap enforce $20,000 between them, and a restart resets the day to
|
|
10
|
+
* zero.
|
|
11
|
+
*
|
|
12
|
+
* That is stated rather than papered over, and the shape of the API is chosen
|
|
13
|
+
* so it can be fixed without a rewrite: `evaluatePolicy` depends only on the
|
|
14
|
+
* `AgentUsage` interface below, so a Redis- or Postgres-backed implementation
|
|
15
|
+
* of two methods replaces this class entirely. A distributed limit needs
|
|
16
|
+
* shared storage with atomic reservation, which is a later concern and a real
|
|
17
|
+
* one — do not deploy a fleet of agents behind this class and describe the cap
|
|
18
|
+
* as enforced.
|
|
19
|
+
*
|
|
20
|
+
* ## Why reservations, rather than counting on the way out
|
|
21
|
+
*
|
|
22
|
+
* An agent asks "may I", gets an allow, and then signs. If budget were only
|
|
23
|
+
* counted when the transaction is confirmed, an agent could ask a hundred
|
|
24
|
+
* times before the first answer was recorded and get a hundred allows against
|
|
25
|
+
* a cap of one. So an admitted decision holds its value immediately, and the
|
|
26
|
+
* hold is released when the caller says the action did not happen or when it
|
|
27
|
+
* expires unclaimed. The failure direction is a briefly over-tight budget,
|
|
28
|
+
* which is the right way for this to be wrong.
|
|
29
|
+
*/
|
|
30
|
+
import type { AgentOutcomeRecord } from './model';
|
|
31
|
+
/**
|
|
32
|
+
* What `evaluatePolicy` needs to know about the past.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately two read methods and nothing else. Anything satisfying this can
|
|
35
|
+
* be handed to the evaluator — including a shared store, which is how this
|
|
36
|
+
* becomes correct across processes.
|
|
37
|
+
*/
|
|
38
|
+
export interface AgentUsage {
|
|
39
|
+
/** Value held or committed by this agent at or after `sinceMs`. */
|
|
40
|
+
spentUsdSince(agentId: string, sinceMs: number): number;
|
|
41
|
+
/** Decisions this guard admitted for the agent at or after `sinceMs`. */
|
|
42
|
+
actionsSince(agentId: string, sinceMs: number): number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A usage source that can also hold budget.
|
|
46
|
+
*
|
|
47
|
+
* Kept separate from `AgentUsage` because the two are genuinely different
|
|
48
|
+
* capabilities: a read-only source (a warehouse query, a metrics store) can
|
|
49
|
+
* answer "how much has this agent spent today" without being able to take a
|
|
50
|
+
* hold. `AgentGuard` accepts either, and says which it got — a guard running
|
|
51
|
+
* on a read-only source enforces cumulative caps against *settled* history
|
|
52
|
+
* only, which cannot stop two concurrent actions that each fit alone.
|
|
53
|
+
*/
|
|
54
|
+
export interface ReservationLedger extends AgentUsage {
|
|
55
|
+
reserve(input: {
|
|
56
|
+
decisionId: string;
|
|
57
|
+
agentId: string;
|
|
58
|
+
valueUsd: number;
|
|
59
|
+
idempotencyKey?: string;
|
|
60
|
+
now?: Date;
|
|
61
|
+
}): Reservation;
|
|
62
|
+
confirm(decisionId: string, options?: {
|
|
63
|
+
txHash?: string;
|
|
64
|
+
now?: Date;
|
|
65
|
+
}): AgentOutcomeRecord | null;
|
|
66
|
+
release(decisionId: string, options?: {
|
|
67
|
+
note?: string;
|
|
68
|
+
now?: Date;
|
|
69
|
+
}): AgentOutcomeRecord | null;
|
|
70
|
+
}
|
|
71
|
+
export declare function isReservationLedger(usage: AgentUsage): usage is ReservationLedger;
|
|
72
|
+
export type ReservationState = 'held' | 'confirmed' | 'released' | 'expired';
|
|
73
|
+
export interface Reservation {
|
|
74
|
+
decisionId: string;
|
|
75
|
+
agentId: string;
|
|
76
|
+
valueUsd: number;
|
|
77
|
+
idempotencyKey?: string;
|
|
78
|
+
state: ReservationState;
|
|
79
|
+
createdAtMs: number;
|
|
80
|
+
expiresAtMs: number;
|
|
81
|
+
settledAtMs?: number;
|
|
82
|
+
txHash?: string;
|
|
83
|
+
note?: string;
|
|
84
|
+
}
|
|
85
|
+
export interface SpendLedgerOptions {
|
|
86
|
+
/**
|
|
87
|
+
* How long an unsettled hold survives. Default 5 minutes — long enough for a
|
|
88
|
+
* signature and a broadcast, short enough that an agent that crashed
|
|
89
|
+
* mid-action does not hold its budget until the window rolls over.
|
|
90
|
+
*/
|
|
91
|
+
reservationTtlMs?: number;
|
|
92
|
+
/**
|
|
93
|
+
* How long settled entries are kept. Must exceed the longest policy window,
|
|
94
|
+
* or a cumulative cap silently stops being cumulative. Default 8 days.
|
|
95
|
+
*/
|
|
96
|
+
retentionMs?: number;
|
|
97
|
+
/** A hard ceiling on entries, so a runaway agent cannot exhaust memory. */
|
|
98
|
+
maxEntries?: number;
|
|
99
|
+
/**
|
|
100
|
+
* The clock, injected for the same reason Protect injects `fetch`: expiry
|
|
101
|
+
* and retention are time-dependent behaviour, and behaviour that can only be
|
|
102
|
+
* exercised by waiting is behaviour that does not get tested.
|
|
103
|
+
*/
|
|
104
|
+
clock?: () => number;
|
|
105
|
+
}
|
|
106
|
+
export declare class SpendLedger implements ReservationLedger {
|
|
107
|
+
private readonly reservationTtlMs;
|
|
108
|
+
private readonly retentionMs;
|
|
109
|
+
private readonly maxEntries;
|
|
110
|
+
private readonly clock;
|
|
111
|
+
private readonly entries;
|
|
112
|
+
private readonly byDecision;
|
|
113
|
+
private readonly byIdempotencyKey;
|
|
114
|
+
constructor(options?: SpendLedgerOptions);
|
|
115
|
+
/**
|
|
116
|
+
* Hold budget for an admitted decision.
|
|
117
|
+
*
|
|
118
|
+
* A repeated `idempotencyKey` returns the existing hold instead of taking a
|
|
119
|
+
* second one. An agent retrying a call after a timeout is one action, and
|
|
120
|
+
* counting it twice would tighten its own cap against it for no reason.
|
|
121
|
+
*/
|
|
122
|
+
reserve(input: {
|
|
123
|
+
decisionId: string;
|
|
124
|
+
agentId: string;
|
|
125
|
+
valueUsd: number;
|
|
126
|
+
idempotencyKey?: string;
|
|
127
|
+
now?: Date;
|
|
128
|
+
}): Reservation;
|
|
129
|
+
/** The action happened. The hold becomes a permanent commitment. */
|
|
130
|
+
confirm(decisionId: string, options?: {
|
|
131
|
+
txHash?: string;
|
|
132
|
+
now?: Date;
|
|
133
|
+
}): AgentOutcomeRecord | null;
|
|
134
|
+
/** The action did not happen. The hold is returned to the budget. */
|
|
135
|
+
release(decisionId: string, options?: {
|
|
136
|
+
note?: string;
|
|
137
|
+
now?: Date;
|
|
138
|
+
}): AgentOutcomeRecord | null;
|
|
139
|
+
/**
|
|
140
|
+
* Value held or committed in the window.
|
|
141
|
+
*
|
|
142
|
+
* Released and expired holds do not count. They are actions that did not
|
|
143
|
+
* happen, and charging an agent for them would shrink a cap the operator
|
|
144
|
+
* set for real spending.
|
|
145
|
+
*/
|
|
146
|
+
spentUsdSince(agentId: string, sinceMs: number): number;
|
|
147
|
+
/**
|
|
148
|
+
* Decisions admitted in the window — regardless of how they settled.
|
|
149
|
+
*
|
|
150
|
+
* A rate ceiling counts what the guard admitted, not what landed on chain.
|
|
151
|
+
* An agent that spins through assess-and-abandon at machine speed is exactly
|
|
152
|
+
* the loop the ceiling exists to catch, and it would be invisible to a
|
|
153
|
+
* counter that only saw confirmed transactions.
|
|
154
|
+
*/
|
|
155
|
+
actionsSince(agentId: string, sinceMs: number): number;
|
|
156
|
+
reservation(decisionId: string): Reservation | undefined;
|
|
157
|
+
/** Snapshot for inspection. Copies, so a caller cannot edit the ledger. */
|
|
158
|
+
list(agentId?: string): Reservation[];
|
|
159
|
+
clear(): void;
|
|
160
|
+
private settle;
|
|
161
|
+
private prune;
|
|
162
|
+
private enforceCeiling;
|
|
163
|
+
/** Idempotency keys are the caller's, so they are namespaced per agent. */
|
|
164
|
+
private keyFor;
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=ledger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"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"}
|
package/dist/ledger.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cumulative spend and rate accounting.
|
|
4
|
+
*
|
|
5
|
+
* ## What this is, honestly
|
|
6
|
+
*
|
|
7
|
+
* **This ledger is in-process and in-memory.** It counts what one instance of
|
|
8
|
+
* one process admitted. It is not shared, not durable, and not correct across
|
|
9
|
+
* a restart or a horizontal scale-out: two replicas each enforcing a $10,000
|
|
10
|
+
* daily cap enforce $20,000 between them, and a restart resets the day to
|
|
11
|
+
* zero.
|
|
12
|
+
*
|
|
13
|
+
* That is stated rather than papered over, and the shape of the API is chosen
|
|
14
|
+
* so it can be fixed without a rewrite: `evaluatePolicy` depends only on the
|
|
15
|
+
* `AgentUsage` interface below, so a Redis- or Postgres-backed implementation
|
|
16
|
+
* of two methods replaces this class entirely. A distributed limit needs
|
|
17
|
+
* shared storage with atomic reservation, which is a later concern and a real
|
|
18
|
+
* one — do not deploy a fleet of agents behind this class and describe the cap
|
|
19
|
+
* as enforced.
|
|
20
|
+
*
|
|
21
|
+
* ## Why reservations, rather than counting on the way out
|
|
22
|
+
*
|
|
23
|
+
* An agent asks "may I", gets an allow, and then signs. If budget were only
|
|
24
|
+
* counted when the transaction is confirmed, an agent could ask a hundred
|
|
25
|
+
* times before the first answer was recorded and get a hundred allows against
|
|
26
|
+
* a cap of one. So an admitted decision holds its value immediately, and the
|
|
27
|
+
* hold is released when the caller says the action did not happen or when it
|
|
28
|
+
* expires unclaimed. The failure direction is a briefly over-tight budget,
|
|
29
|
+
* which is the right way for this to be wrong.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.SpendLedger = void 0;
|
|
33
|
+
exports.isReservationLedger = isReservationLedger;
|
|
34
|
+
function isReservationLedger(usage) {
|
|
35
|
+
const candidate = usage;
|
|
36
|
+
return typeof candidate.reserve === 'function'
|
|
37
|
+
&& typeof candidate.confirm === 'function'
|
|
38
|
+
&& typeof candidate.release === 'function';
|
|
39
|
+
}
|
|
40
|
+
const DEFAULT_RESERVATION_TTL_MS = 5 * 60_000;
|
|
41
|
+
const DEFAULT_RETENTION_MS = 8 * 24 * 60 * 60_000;
|
|
42
|
+
const DEFAULT_MAX_ENTRIES = 50_000;
|
|
43
|
+
class SpendLedger {
|
|
44
|
+
reservationTtlMs;
|
|
45
|
+
retentionMs;
|
|
46
|
+
maxEntries;
|
|
47
|
+
clock;
|
|
48
|
+
entries = [];
|
|
49
|
+
byDecision = new Map();
|
|
50
|
+
byIdempotencyKey = new Map();
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
this.reservationTtlMs = options.reservationTtlMs ?? DEFAULT_RESERVATION_TTL_MS;
|
|
53
|
+
this.retentionMs = options.retentionMs ?? DEFAULT_RETENTION_MS;
|
|
54
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
55
|
+
this.clock = options.clock ?? (() => Date.now());
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Hold budget for an admitted decision.
|
|
59
|
+
*
|
|
60
|
+
* A repeated `idempotencyKey` returns the existing hold instead of taking a
|
|
61
|
+
* second one. An agent retrying a call after a timeout is one action, and
|
|
62
|
+
* counting it twice would tighten its own cap against it for no reason.
|
|
63
|
+
*/
|
|
64
|
+
reserve(input) {
|
|
65
|
+
const nowMs = input.now ? input.now.getTime() : this.clock();
|
|
66
|
+
this.prune(nowMs);
|
|
67
|
+
if (input.idempotencyKey) {
|
|
68
|
+
const existing = this.byIdempotencyKey.get(this.keyFor(input.agentId, input.idempotencyKey));
|
|
69
|
+
if (existing && existing.state !== 'released' && existing.state !== 'expired')
|
|
70
|
+
return existing;
|
|
71
|
+
}
|
|
72
|
+
const reservation = {
|
|
73
|
+
decisionId: input.decisionId,
|
|
74
|
+
agentId: input.agentId,
|
|
75
|
+
// A negative value would be a credit against the agent's own cap, which
|
|
76
|
+
// is a way to spend more than the cap allows. Clamped, not trusted.
|
|
77
|
+
valueUsd: Number.isFinite(input.valueUsd) ? Math.max(0, input.valueUsd) : 0,
|
|
78
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
79
|
+
state: 'held',
|
|
80
|
+
createdAtMs: nowMs,
|
|
81
|
+
expiresAtMs: nowMs + this.reservationTtlMs,
|
|
82
|
+
};
|
|
83
|
+
this.entries.push(reservation);
|
|
84
|
+
this.byDecision.set(reservation.decisionId, reservation);
|
|
85
|
+
if (input.idempotencyKey) {
|
|
86
|
+
this.byIdempotencyKey.set(this.keyFor(input.agentId, input.idempotencyKey), reservation);
|
|
87
|
+
}
|
|
88
|
+
this.enforceCeiling();
|
|
89
|
+
return reservation;
|
|
90
|
+
}
|
|
91
|
+
/** The action happened. The hold becomes a permanent commitment. */
|
|
92
|
+
confirm(decisionId, options = {}) {
|
|
93
|
+
return this.settle(decisionId, 'confirmed', {
|
|
94
|
+
...(options.txHash === undefined ? {} : { txHash: options.txHash }),
|
|
95
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** The action did not happen. The hold is returned to the budget. */
|
|
99
|
+
release(decisionId, options = {}) {
|
|
100
|
+
return this.settle(decisionId, 'released', {
|
|
101
|
+
...(options.note === undefined ? {} : { note: options.note }),
|
|
102
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Value held or committed in the window.
|
|
107
|
+
*
|
|
108
|
+
* Released and expired holds do not count. They are actions that did not
|
|
109
|
+
* happen, and charging an agent for them would shrink a cap the operator
|
|
110
|
+
* set for real spending.
|
|
111
|
+
*/
|
|
112
|
+
spentUsdSince(agentId, sinceMs) {
|
|
113
|
+
this.prune(this.clock());
|
|
114
|
+
let total = 0;
|
|
115
|
+
for (const entry of this.entries) {
|
|
116
|
+
if (entry.agentId !== agentId)
|
|
117
|
+
continue;
|
|
118
|
+
if (entry.state === 'released' || entry.state === 'expired')
|
|
119
|
+
continue;
|
|
120
|
+
if (entry.createdAtMs < sinceMs)
|
|
121
|
+
continue;
|
|
122
|
+
total += entry.valueUsd;
|
|
123
|
+
}
|
|
124
|
+
return total;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Decisions admitted in the window — regardless of how they settled.
|
|
128
|
+
*
|
|
129
|
+
* A rate ceiling counts what the guard admitted, not what landed on chain.
|
|
130
|
+
* An agent that spins through assess-and-abandon at machine speed is exactly
|
|
131
|
+
* the loop the ceiling exists to catch, and it would be invisible to a
|
|
132
|
+
* counter that only saw confirmed transactions.
|
|
133
|
+
*/
|
|
134
|
+
actionsSince(agentId, sinceMs) {
|
|
135
|
+
this.prune(this.clock());
|
|
136
|
+
let count = 0;
|
|
137
|
+
for (const entry of this.entries) {
|
|
138
|
+
if (entry.agentId !== agentId)
|
|
139
|
+
continue;
|
|
140
|
+
if (entry.createdAtMs < sinceMs)
|
|
141
|
+
continue;
|
|
142
|
+
count += 1;
|
|
143
|
+
}
|
|
144
|
+
return count;
|
|
145
|
+
}
|
|
146
|
+
reservation(decisionId) {
|
|
147
|
+
return this.byDecision.get(decisionId);
|
|
148
|
+
}
|
|
149
|
+
/** Snapshot for inspection. Copies, so a caller cannot edit the ledger. */
|
|
150
|
+
list(agentId) {
|
|
151
|
+
return this.entries
|
|
152
|
+
.filter((entry) => !agentId || entry.agentId === agentId)
|
|
153
|
+
.map((entry) => ({ ...entry }));
|
|
154
|
+
}
|
|
155
|
+
clear() {
|
|
156
|
+
this.entries.length = 0;
|
|
157
|
+
this.byDecision.clear();
|
|
158
|
+
this.byIdempotencyKey.clear();
|
|
159
|
+
}
|
|
160
|
+
// -------------------------------------------------------------------------
|
|
161
|
+
settle(decisionId, state, options) {
|
|
162
|
+
const now = options.now ?? new Date(this.clock());
|
|
163
|
+
const entry = this.byDecision.get(decisionId);
|
|
164
|
+
if (!entry)
|
|
165
|
+
return null;
|
|
166
|
+
// An expired hold that later confirms is a real and dangerous case: the
|
|
167
|
+
// budget was already returned, so the spend happened outside the cap. It
|
|
168
|
+
// is recorded as confirmed so the total is right, and the note says the
|
|
169
|
+
// hold had lapsed, because that is a tuning signal for the TTL.
|
|
170
|
+
const lapsed = entry.state === 'expired';
|
|
171
|
+
entry.state = state;
|
|
172
|
+
entry.settledAtMs = now.getTime();
|
|
173
|
+
if (options.txHash !== undefined)
|
|
174
|
+
entry.txHash = options.txHash;
|
|
175
|
+
if (options.note !== undefined)
|
|
176
|
+
entry.note = options.note;
|
|
177
|
+
return {
|
|
178
|
+
decisionId,
|
|
179
|
+
state,
|
|
180
|
+
...(entry.txHash === undefined ? {} : { txHash: entry.txHash }),
|
|
181
|
+
...(lapsed
|
|
182
|
+
? { note: 'The hold had already expired when this was settled; the reservation TTL may be too short.' }
|
|
183
|
+
: entry.note === undefined ? {} : { note: entry.note }),
|
|
184
|
+
at: now.toISOString(),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
prune(nowMs) {
|
|
188
|
+
for (const entry of this.entries) {
|
|
189
|
+
if (entry.state === 'held' && entry.expiresAtMs <= nowMs)
|
|
190
|
+
entry.state = 'expired';
|
|
191
|
+
}
|
|
192
|
+
for (let i = this.entries.length - 1; i >= 0; i -= 1) {
|
|
193
|
+
const entry = this.entries[i];
|
|
194
|
+
if (nowMs - entry.createdAtMs <= this.retentionMs)
|
|
195
|
+
continue;
|
|
196
|
+
this.entries.splice(i, 1);
|
|
197
|
+
this.byDecision.delete(entry.decisionId);
|
|
198
|
+
if (entry.idempotencyKey)
|
|
199
|
+
this.byIdempotencyKey.delete(this.keyFor(entry.agentId, entry.idempotencyKey));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
enforceCeiling() {
|
|
203
|
+
while (this.entries.length > this.maxEntries) {
|
|
204
|
+
const entry = this.entries.shift();
|
|
205
|
+
if (!entry)
|
|
206
|
+
break;
|
|
207
|
+
this.byDecision.delete(entry.decisionId);
|
|
208
|
+
if (entry.idempotencyKey)
|
|
209
|
+
this.byIdempotencyKey.delete(this.keyFor(entry.agentId, entry.idempotencyKey));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** Idempotency keys are the caller's, so they are namespaced per agent. */
|
|
213
|
+
keyFor(agentId, idempotencyKey) {
|
|
214
|
+
return `${agentId}${idempotencyKey}`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
exports.SpendLedger = SpendLedger;
|
|
218
|
+
//# sourceMappingURL=ledger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger.js","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"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"}
|