@zhin.js/harness 0.0.0 → 1.0.1
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/package.json +3 -3
- package/src/authority.ts +0 -194
- package/src/budget.ts +0 -181
- package/src/contracts.ts +0 -762
- package/src/guard.ts +0 -305
- package/src/harness.ts +0 -975
- package/src/index.ts +0 -8
- package/src/journal.ts +0 -833
- package/src/mailbox.ts +0 -467
- package/src/policies/admission.ts +0 -164
- package/src/policies/groups.ts +0 -137
- package/src/policies/index.ts +0 -5
- package/src/policies/live.ts +0 -68
- package/src/policies/markers.ts +0 -58
- package/src/policies/merge.ts +0 -121
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/harness",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "ADR 0048 Agent Harness: actors, mail, engagements, attenuated authority, budgets and the guard kernel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"lib",
|
|
16
|
-
"
|
|
16
|
+
"README.md"
|
|
17
17
|
],
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/node": "^26.1.2",
|
|
20
20
|
"typescript": "^6.0.3",
|
|
21
|
-
"@zhin.js/database": "1.1.
|
|
21
|
+
"@zhin.js/database": "1.1.7"
|
|
22
22
|
},
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=22.19.0"
|
package/src/authority.ts
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ADR 0048 §3.4 invariant 1 — authority is only ever attenuated.
|
|
3
|
-
*
|
|
4
|
-
* Effective authority of a delegatee = delegator authority ∩ the narrowing
|
|
5
|
-
* declared at hand-off ∩ the delegatee agent's ceiling. Every function here is
|
|
6
|
-
* pure: inputs are never mutated and every returned object is frozen.
|
|
7
|
-
*/
|
|
8
|
-
import {
|
|
9
|
-
ALL,
|
|
10
|
-
TRUST_LEVELS,
|
|
11
|
-
intersectScopes,
|
|
12
|
-
scopeHas,
|
|
13
|
-
scopeWithin,
|
|
14
|
-
type ActorId,
|
|
15
|
-
type AgentCeiling,
|
|
16
|
-
type AgentName,
|
|
17
|
-
type Authority,
|
|
18
|
-
type AuthorityNarrowing,
|
|
19
|
-
type EgressScope,
|
|
20
|
-
type NameScope,
|
|
21
|
-
type Principal,
|
|
22
|
-
type TrustLevel,
|
|
23
|
-
} from './contracts.js';
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Trust
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
function trustRank(level: TrustLevel): number {
|
|
30
|
-
const rank = TRUST_LEVELS.indexOf(level);
|
|
31
|
-
if (rank < 0) throw new TypeError(`Unknown trust level: ${String(level)}`);
|
|
32
|
-
return rank;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isTrustLevel(value: unknown): value is TrustLevel {
|
|
36
|
-
return typeof value === 'string' && (TRUST_LEVELS as readonly string[]).includes(value);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Negative when `a` is lower than `b`, 0 when equal, positive when higher. Throws on unknown levels. */
|
|
40
|
-
export function compareTrust(a: TrustLevel, b: TrustLevel): number {
|
|
41
|
-
return trustRank(a) - trustRank(b);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** The lowest of the given levels. Throws when called with none. */
|
|
45
|
-
export function minTrust(...levels: readonly TrustLevel[]): TrustLevel {
|
|
46
|
-
if (levels.length === 0) throw new TypeError('minTrust requires at least one level');
|
|
47
|
-
let lowest = levels[0];
|
|
48
|
-
for (const level of levels) {
|
|
49
|
-
if (compareTrust(level, lowest) < 0) lowest = level;
|
|
50
|
-
}
|
|
51
|
-
trustRank(lowest);
|
|
52
|
-
return lowest;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
// Construction
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
function freezeScope(scope: NameScope): NameScope {
|
|
60
|
-
return intersectScopes(scope, ALL);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function freezePrincipal(principal: Principal): Principal {
|
|
64
|
-
return Object.freeze({
|
|
65
|
-
kind: principal.kind,
|
|
66
|
-
id: principal.id,
|
|
67
|
-
trust: principal.trust,
|
|
68
|
-
...(principal.venue ? { venue: Object.freeze({ kind: principal.venue.kind, id: principal.venue.id }) } : {}),
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function freezeEgress(network: boolean, hosts: NameScope): EgressScope {
|
|
73
|
-
return Object.freeze({ network, hosts: freezeScope(hosts) });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function dedupe(ids: readonly string[]): readonly string[] {
|
|
77
|
-
return Object.freeze([...new Set(ids)]);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Authority of an engagement's root mail: what the principal may do through
|
|
82
|
-
* this agent. Approvers are the human principal first, then configured ids.
|
|
83
|
-
*/
|
|
84
|
-
export function rootAuthority(
|
|
85
|
-
principal: Principal,
|
|
86
|
-
ceiling: AgentCeiling,
|
|
87
|
-
options: { readonly approvers?: readonly string[] } = {},
|
|
88
|
-
): Authority {
|
|
89
|
-
const approvers = [...(principal.kind === 'human' ? [principal.id] : []), ...(options.approvers ?? [])];
|
|
90
|
-
return Object.freeze({
|
|
91
|
-
principal: freezePrincipal(principal),
|
|
92
|
-
trust: minTrust(principal.trust, ceiling.maxTrust),
|
|
93
|
-
tools: freezeScope(ceiling.tools),
|
|
94
|
-
contacts: freezeScope(ceiling.contacts),
|
|
95
|
-
egress: freezeEgress(ceiling.egress.network, ceiling.egress.hosts),
|
|
96
|
-
approvers: dedupe(approvers),
|
|
97
|
-
chain: Object.freeze([]) as readonly ActorId[],
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Derives the authority handed to `target` by actor `via`. Never widens: a
|
|
103
|
-
* narrowing asking for more than the parent just gets the intersection.
|
|
104
|
-
* Principal and approvers are carried over unchanged.
|
|
105
|
-
*/
|
|
106
|
-
export function attenuate(
|
|
107
|
-
parent: Authority,
|
|
108
|
-
narrowing: AuthorityNarrowing,
|
|
109
|
-
target: AgentCeiling,
|
|
110
|
-
via: ActorId,
|
|
111
|
-
): Authority {
|
|
112
|
-
const tools = intersectScopes(intersectScopes(parent.tools, narrowing.tools ?? ALL), target.tools);
|
|
113
|
-
const contacts = intersectScopes(intersectScopes(parent.contacts, narrowing.contacts ?? ALL), target.contacts);
|
|
114
|
-
const trust = minTrust(parent.trust, narrowing.trust ?? parent.trust, target.maxTrust);
|
|
115
|
-
const network = parent.egress.network && (narrowing.egress?.network ?? true) && target.egress.network;
|
|
116
|
-
const hosts = intersectScopes(
|
|
117
|
-
intersectScopes(parent.egress.hosts, narrowing.egress?.hosts ?? ALL),
|
|
118
|
-
target.egress.hosts,
|
|
119
|
-
);
|
|
120
|
-
return Object.freeze({
|
|
121
|
-
principal: freezePrincipal(parent.principal),
|
|
122
|
-
trust,
|
|
123
|
-
tools,
|
|
124
|
-
contacts,
|
|
125
|
-
egress: freezeEgress(network, hosts),
|
|
126
|
-
approvers: Object.freeze([...parent.approvers]),
|
|
127
|
-
chain: Object.freeze([...parent.chain, via]),
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
// Verification (called by the guard kernel)
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
|
|
135
|
-
function sameJson(a: unknown, b: unknown): boolean {
|
|
136
|
-
if (a === b) return true;
|
|
137
|
-
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
|
|
138
|
-
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
139
|
-
const aKeys = Object.keys(a).filter((key) => (a as Record<string, unknown>)[key] !== undefined);
|
|
140
|
-
const bKeys = Object.keys(b).filter((key) => (b as Record<string, unknown>)[key] !== undefined);
|
|
141
|
-
if (aKeys.length !== bKeys.length) return false;
|
|
142
|
-
return aKeys.every(
|
|
143
|
-
(key) =>
|
|
144
|
-
Object.prototype.hasOwnProperty.call(b, key) &&
|
|
145
|
-
sameJson((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function sameList(a: readonly string[], b: readonly string[]): boolean {
|
|
150
|
-
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* `true` iff `child` is a single, valid attenuation step from `parent`:
|
|
155
|
-
* same principal and approvers, no wider trust / tools / contacts / egress,
|
|
156
|
-
* and a chain that extends the parent's by exactly one actor.
|
|
157
|
-
*/
|
|
158
|
-
export function isAttenuationOf(child: Authority, parent: Authority): boolean {
|
|
159
|
-
if (!isTrustLevel(child.trust) || !isTrustLevel(parent.trust)) return false;
|
|
160
|
-
if (!isTrustLevel(child.principal.trust)) return false;
|
|
161
|
-
if (!sameJson(child.principal, parent.principal)) return false;
|
|
162
|
-
if (!sameList(child.approvers, parent.approvers)) return false;
|
|
163
|
-
if (compareTrust(child.trust, parent.trust) > 0) return false;
|
|
164
|
-
if (compareTrust(child.trust, child.principal.trust) > 0) return false;
|
|
165
|
-
if (!scopeWithin(child.tools, parent.tools)) return false;
|
|
166
|
-
if (!scopeWithin(child.contacts, parent.contacts)) return false;
|
|
167
|
-
if (child.egress.network && !parent.egress.network) return false;
|
|
168
|
-
if (!scopeWithin(child.egress.hosts, parent.egress.hosts)) return false;
|
|
169
|
-
if (child.chain.length !== parent.chain.length + 1) return false;
|
|
170
|
-
return parent.chain.every((actor, index) => child.chain[index] === actor);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
// Queries
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
|
|
177
|
-
/** Agent name of an actor id: the part before `#`. */
|
|
178
|
-
export function agentOf(actor: ActorId): AgentName {
|
|
179
|
-
const hash = actor.indexOf('#');
|
|
180
|
-
return hash < 0 ? actor : actor.slice(0, hash);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function canCall(authority: Authority, tool: string): boolean {
|
|
184
|
-
return scopeHas(authority.tools, tool);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** Takes an agent name; use `agentOf(actor)` for actor ids. */
|
|
188
|
-
export function canContact(authority: Authority, agentName: AgentName): boolean {
|
|
189
|
-
return scopeHas(authority.contacts, agentName);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function allowsHost(authority: Authority, host: string): boolean {
|
|
193
|
-
return authority.egress.network && scopeHas(authority.egress.hosts, host);
|
|
194
|
-
}
|
package/src/budget.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-memory conserved budget ledger (ADR 0048 §3.4 invariant 2).
|
|
3
|
-
*
|
|
4
|
-
* Semantics, per budget key:
|
|
5
|
-
* - consumed(g) = g.ownSpent + Σ rolledUp(closed child), where
|
|
6
|
-
* rolledUp(c) = min(consumed(c), granted(c)).
|
|
7
|
-
* Overspend beyond a grant stays visible on that grant's own
|
|
8
|
-
* snapshot but never inflates the parent's accounting, so
|
|
9
|
-
* a subtree can never consume more than it was granted
|
|
10
|
-
* from above (conservation).
|
|
11
|
-
* - reserved(g) = Σ granted(c) over OPEN children c.
|
|
12
|
-
* - remaining(g) = granted(g) - consumed(g) - reserved(g) (may go negative
|
|
13
|
-
* after an overspending charge).
|
|
14
|
-
* - Snapshot: `spent` = consumed(g) (own + closed children), `reservedForChildren`
|
|
15
|
-
* = reserved(g), `remaining` as above.
|
|
16
|
-
*
|
|
17
|
-
* grant(): requested keys above the parent's remaining fail whole with
|
|
18
|
-
* `budget_exceeded` (no partial grant, no clamping). Omitted keys default to
|
|
19
|
-
* floor(parent remaining / 2), never below 0. The child's granted amount is
|
|
20
|
-
* reserved on the parent immediately.
|
|
21
|
-
*
|
|
22
|
-
* charge(): always records the usage on an open grant; reports
|
|
23
|
-
* `budget_exceeded` (with the exhausted keys) when any remaining < 0.
|
|
24
|
-
*
|
|
25
|
-
* close(): closes open descendants first (deepest first), then the grant.
|
|
26
|
-
* The parent's reservation is released and the child's consumed amount is
|
|
27
|
-
* rolled up, i.e. unspent returns to the parent. Idempotent.
|
|
28
|
-
*/
|
|
29
|
-
import {
|
|
30
|
-
BUDGET_KEYS,
|
|
31
|
-
type BudgetAmounts,
|
|
32
|
-
type BudgetKey,
|
|
33
|
-
type BudgetLedger,
|
|
34
|
-
type BudgetResult,
|
|
35
|
-
type BudgetSnapshot,
|
|
36
|
-
type GrantId,
|
|
37
|
-
} from './contracts.js';
|
|
38
|
-
|
|
39
|
-
type Mutable = { -readonly [K in BudgetKey]: number };
|
|
40
|
-
|
|
41
|
-
interface Node {
|
|
42
|
-
readonly id: GrantId;
|
|
43
|
-
readonly parent?: Node;
|
|
44
|
-
readonly granted: BudgetAmounts;
|
|
45
|
-
readonly ownSpent: Mutable;
|
|
46
|
-
readonly rolledUp: Mutable;
|
|
47
|
-
readonly children: Node[];
|
|
48
|
-
closed: boolean;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
type FailCode = 'budget_exceeded' | 'grant_closed' | 'unknown_grant' | 'invalid_amount';
|
|
52
|
-
|
|
53
|
-
const zero = (): Mutable => ({ modelCalls: 0, tokens: 0, wallMs: 0, sideEffects: 0 });
|
|
54
|
-
const valid = (n: unknown): n is number => typeof n === 'number' && Number.isFinite(n) && n >= 0;
|
|
55
|
-
|
|
56
|
-
export interface InMemoryBudgetLedgerOptions {
|
|
57
|
-
readonly idFactory?: () => GrantId;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export class InMemoryBudgetLedger implements BudgetLedger {
|
|
61
|
-
private readonly nodes = new Map<GrantId, Node>();
|
|
62
|
-
private readonly idFactory: () => GrantId;
|
|
63
|
-
|
|
64
|
-
constructor(options: InMemoryBudgetLedgerOptions = {}) {
|
|
65
|
-
let n = 0;
|
|
66
|
-
this.idFactory = options.idFactory ?? (() => `g${++n}`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
openRoot(amounts: BudgetAmounts): GrantId {
|
|
70
|
-
for (const k of BUDGET_KEYS) {
|
|
71
|
-
if (!valid(amounts?.[k])) throw new TypeError(`invalid budget amount for ${k}: ${String(amounts?.[k])}`);
|
|
72
|
-
}
|
|
73
|
-
return this.create(undefined, { ...amounts }).id;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
grant(parentId: GrantId, request: Partial<BudgetAmounts>): BudgetResult<GrantId> {
|
|
77
|
-
const parent = this.nodes.get(parentId);
|
|
78
|
-
if (!parent) return fail('unknown_grant', `unknown grant ${parentId}`);
|
|
79
|
-
if (parent.closed) return fail('grant_closed', `grant ${parentId} is closed`);
|
|
80
|
-
const bad = invalidKey(request);
|
|
81
|
-
if (bad) return fail('invalid_amount', `invalid amount for ${bad}`);
|
|
82
|
-
const rem = this.remaining(parent);
|
|
83
|
-
const exhausted = BUDGET_KEYS.filter((k) => request[k] !== undefined && (request[k] as number) > rem[k]);
|
|
84
|
-
if (exhausted.length > 0) {
|
|
85
|
-
return fail('budget_exceeded', `requested more than parent remaining for: ${exhausted.join(', ')}`, exhausted);
|
|
86
|
-
}
|
|
87
|
-
const amounts = zero();
|
|
88
|
-
for (const k of BUDGET_KEYS) amounts[k] = request[k] ?? Math.max(0, Math.floor(rem[k] / 2));
|
|
89
|
-
return { ok: true, value: this.create(parent, amounts).id };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
charge(grantId: GrantId, usage: Partial<BudgetAmounts>): BudgetResult<BudgetSnapshot> {
|
|
93
|
-
const node = this.nodes.get(grantId);
|
|
94
|
-
if (!node) return fail('unknown_grant', `unknown grant ${grantId}`);
|
|
95
|
-
const bad = invalidKey(usage);
|
|
96
|
-
if (bad) return fail('invalid_amount', `invalid amount for ${bad}`);
|
|
97
|
-
if (node.closed) return fail('grant_closed', `grant ${grantId} is closed`);
|
|
98
|
-
for (const k of BUDGET_KEYS) node.ownSpent[k] += usage[k] ?? 0;
|
|
99
|
-
const rem = this.remaining(node);
|
|
100
|
-
const exhausted = BUDGET_KEYS.filter((k) => rem[k] < 0);
|
|
101
|
-
if (exhausted.length > 0) {
|
|
102
|
-
return fail('budget_exceeded', `budget exceeded for: ${exhausted.join(', ')}`, exhausted);
|
|
103
|
-
}
|
|
104
|
-
return { ok: true, value: this.snap(node) };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
close(grantId: GrantId): BudgetResult<BudgetSnapshot> {
|
|
108
|
-
const node = this.nodes.get(grantId);
|
|
109
|
-
if (!node) return fail('unknown_grant', `unknown grant ${grantId}`);
|
|
110
|
-
this.closeNode(node);
|
|
111
|
-
return { ok: true, value: this.snap(node) };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
snapshot(grantId: GrantId): BudgetSnapshot | undefined {
|
|
115
|
-
const node = this.nodes.get(grantId);
|
|
116
|
-
return node ? this.snap(node) : undefined;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private create(parent: Node | undefined, granted: BudgetAmounts): Node {
|
|
120
|
-
const id = this.idFactory();
|
|
121
|
-
const node: Node = { id, parent, granted, ownSpent: zero(), rolledUp: zero(), children: [], closed: false };
|
|
122
|
-
this.nodes.set(id, node);
|
|
123
|
-
parent?.children.push(node);
|
|
124
|
-
return node;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
private closeNode(node: Node): void {
|
|
128
|
-
if (node.closed) return;
|
|
129
|
-
for (const child of node.children) this.closeNode(child);
|
|
130
|
-
node.closed = true;
|
|
131
|
-
if (node.parent) {
|
|
132
|
-
const consumed = this.consumed(node);
|
|
133
|
-
for (const k of BUDGET_KEYS) node.parent.rolledUp[k] += Math.min(consumed[k], node.granted[k]);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private consumed(node: Node): Mutable {
|
|
138
|
-
const out = zero();
|
|
139
|
-
for (const k of BUDGET_KEYS) out[k] = node.ownSpent[k] + node.rolledUp[k];
|
|
140
|
-
return out;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
private reserved(node: Node): Mutable {
|
|
144
|
-
const out = zero();
|
|
145
|
-
for (const c of node.children) if (!c.closed) for (const k of BUDGET_KEYS) out[k] += c.granted[k];
|
|
146
|
-
return out;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
private remaining(node: Node): Mutable {
|
|
150
|
-
const c = this.consumed(node);
|
|
151
|
-
const r = this.reserved(node);
|
|
152
|
-
const out = zero();
|
|
153
|
-
for (const k of BUDGET_KEYS) out[k] = node.granted[k] - c[k] - r[k];
|
|
154
|
-
return out;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
private snap(node: Node): BudgetSnapshot {
|
|
158
|
-
return Object.freeze({
|
|
159
|
-
grantId: node.id,
|
|
160
|
-
...(node.parent ? { parentId: node.parent.id } : {}),
|
|
161
|
-
granted: Object.freeze({ ...node.granted }),
|
|
162
|
-
spent: Object.freeze(this.consumed(node)),
|
|
163
|
-
reservedForChildren: Object.freeze(this.reserved(node)),
|
|
164
|
-
remaining: Object.freeze(this.remaining(node)),
|
|
165
|
-
closed: node.closed,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function invalidKey(amounts: Partial<BudgetAmounts>): BudgetKey | undefined {
|
|
171
|
-
if (amounts === null || typeof amounts !== 'object') return BUDGET_KEYS[0];
|
|
172
|
-
return BUDGET_KEYS.find((k) => amounts[k] !== undefined && !valid(amounts[k]));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function fail(
|
|
176
|
-
code: FailCode,
|
|
177
|
-
message: string,
|
|
178
|
-
exhausted?: readonly BudgetKey[],
|
|
179
|
-
): { readonly ok: false; readonly code: FailCode; readonly exhausted?: readonly BudgetKey[]; readonly message: string } {
|
|
180
|
-
return exhausted ? { ok: false, code, exhausted, message } : { ok: false, code, message };
|
|
181
|
-
}
|