@axtary/proxy 0.0.1 → 0.2.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 +202 -0
- package/README.md +85 -2
- package/dist/index.d.ts +119 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +788 -20
- package/dist/index.js.map +1 -1
- package/package.json +6 -8
package/dist/index.js
CHANGED
|
@@ -1,13 +1,196 @@
|
|
|
1
|
-
import { APPROVAL_ARTIFACT_VERSION, DEFAULT_EXPIRES_IN_SECONDS, issueActionPass, parseNormalizedAction, } from "@axtary/actionpass";
|
|
2
|
-
import {
|
|
1
|
+
import { APPROVAL_ARTIFACT_VERSION, ActionProvenanceSchema, actionPassDpopTarget, BudgetAmountSchema, createDpopProof, DEFAULT_EXPIRES_IN_SECONDS, issueActionPass, issueActionPassV1, issueActionPassV2, parseNormalizedAction, } from "@axtary/actionpass";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { evaluatePolicy, PolicyConfigSchema, } from "@axtary/policy";
|
|
3
4
|
import { performance } from "node:perf_hooks";
|
|
4
5
|
export const PROXY_SCHEMA_VERSION = "axtary.proxy.v0";
|
|
5
6
|
export const PROXY_POLICY_VERSION = "2026-05-30";
|
|
6
7
|
export const DEFAULT_HANDLER_TIMEOUT_MS = 10_000;
|
|
8
|
+
export const LOCAL_AUTHORIZATION_DECISION_BUDGET_MS = 200;
|
|
9
|
+
export class InMemoryBudgetMeter {
|
|
10
|
+
committed = new Map();
|
|
11
|
+
reservations = new Map();
|
|
12
|
+
reserve(input) {
|
|
13
|
+
const cost = normalizeBudgetAmount(input.cost);
|
|
14
|
+
const limit = normalizeBudgetAmount(input.limit);
|
|
15
|
+
const committedBefore = this.committed.get(input.scope) ?? {};
|
|
16
|
+
const pendingBefore = this.pendingForScope(input.scope);
|
|
17
|
+
const dimensions = new Set([
|
|
18
|
+
...Object.keys(cost),
|
|
19
|
+
...Object.keys(limit),
|
|
20
|
+
...Object.keys(committedBefore),
|
|
21
|
+
...Object.keys(pendingBefore),
|
|
22
|
+
]);
|
|
23
|
+
const exceeded = [...dimensions].some((dimension) => {
|
|
24
|
+
const requested = cost[dimension] ?? 0;
|
|
25
|
+
if (requested === 0)
|
|
26
|
+
return false;
|
|
27
|
+
const ceiling = limit[dimension];
|
|
28
|
+
return (ceiling === undefined ||
|
|
29
|
+
(committedBefore[dimension] ?? 0) +
|
|
30
|
+
(pendingBefore[dimension] ?? 0) +
|
|
31
|
+
requested >
|
|
32
|
+
ceiling);
|
|
33
|
+
});
|
|
34
|
+
if (exceeded) {
|
|
35
|
+
return {
|
|
36
|
+
accepted: false,
|
|
37
|
+
event: {
|
|
38
|
+
reservationId: null,
|
|
39
|
+
scope: input.scope,
|
|
40
|
+
status: "denied",
|
|
41
|
+
expiresAt: null,
|
|
42
|
+
cost,
|
|
43
|
+
limit,
|
|
44
|
+
committedBefore,
|
|
45
|
+
committedAfter: committedBefore,
|
|
46
|
+
reservedAfter: pendingBefore,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const reservation = {
|
|
51
|
+
reservationId: `budget_${randomUUID()}`,
|
|
52
|
+
scope: input.scope,
|
|
53
|
+
cost,
|
|
54
|
+
limit,
|
|
55
|
+
status: "pending",
|
|
56
|
+
expiresAt: input.expiresAt,
|
|
57
|
+
};
|
|
58
|
+
this.reservations.set(reservation.reservationId, reservation);
|
|
59
|
+
return {
|
|
60
|
+
accepted: true,
|
|
61
|
+
reservation,
|
|
62
|
+
event: {
|
|
63
|
+
reservationId: reservation.reservationId,
|
|
64
|
+
scope: input.scope,
|
|
65
|
+
status: "pending",
|
|
66
|
+
expiresAt: input.expiresAt.toISOString(),
|
|
67
|
+
cost,
|
|
68
|
+
limit,
|
|
69
|
+
committedBefore,
|
|
70
|
+
committedAfter: committedBefore,
|
|
71
|
+
reservedAfter: addBudgetAmounts(pendingBefore, cost),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
commit(reservationId) {
|
|
76
|
+
const reservation = this.pendingReservation(reservationId);
|
|
77
|
+
const committedBefore = this.committed.get(reservation.scope) ?? {};
|
|
78
|
+
const committedAfter = addBudgetAmounts(committedBefore, reservation.cost);
|
|
79
|
+
reservation.status = "committed";
|
|
80
|
+
this.committed.set(reservation.scope, committedAfter);
|
|
81
|
+
return {
|
|
82
|
+
reservationId,
|
|
83
|
+
scope: reservation.scope,
|
|
84
|
+
status: "committed",
|
|
85
|
+
expiresAt: reservation.expiresAt.toISOString(),
|
|
86
|
+
cost: reservation.cost,
|
|
87
|
+
limit: reservation.limit,
|
|
88
|
+
committedBefore,
|
|
89
|
+
committedAfter,
|
|
90
|
+
reservedAfter: this.pendingForScope(reservation.scope),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
rollback(reservationId) {
|
|
94
|
+
const reservation = this.pendingReservation(reservationId);
|
|
95
|
+
const committed = this.committed.get(reservation.scope) ?? {};
|
|
96
|
+
reservation.status = "rolled_back";
|
|
97
|
+
return {
|
|
98
|
+
reservationId,
|
|
99
|
+
scope: reservation.scope,
|
|
100
|
+
status: "rolled_back",
|
|
101
|
+
expiresAt: reservation.expiresAt.toISOString(),
|
|
102
|
+
cost: reservation.cost,
|
|
103
|
+
limit: reservation.limit,
|
|
104
|
+
committedBefore: committed,
|
|
105
|
+
committedAfter: committed,
|
|
106
|
+
reservedAfter: this.pendingForScope(reservation.scope),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
hydrate(events, now = new Date()) {
|
|
110
|
+
for (const event of events) {
|
|
111
|
+
if (!event.reservationId || !event.expiresAt)
|
|
112
|
+
continue;
|
|
113
|
+
const existing = this.reservations.get(event.reservationId);
|
|
114
|
+
if (event.status === "pending") {
|
|
115
|
+
this.reservations.set(event.reservationId, {
|
|
116
|
+
reservationId: event.reservationId,
|
|
117
|
+
scope: event.scope,
|
|
118
|
+
cost: event.cost,
|
|
119
|
+
limit: event.limit,
|
|
120
|
+
status: new Date(event.expiresAt).getTime() <= now.getTime()
|
|
121
|
+
? "rolled_back"
|
|
122
|
+
: "pending",
|
|
123
|
+
expiresAt: new Date(event.expiresAt),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else if (event.status === "committed") {
|
|
127
|
+
if (!existing || existing.status !== "committed") {
|
|
128
|
+
this.committed.set(event.scope, addBudgetAmounts(this.committed.get(event.scope) ?? {}, event.cost));
|
|
129
|
+
}
|
|
130
|
+
if (existing)
|
|
131
|
+
existing.status = "committed";
|
|
132
|
+
}
|
|
133
|
+
else if (event.status === "rolled_back" && existing) {
|
|
134
|
+
existing.status = "rolled_back";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
pendingReservation(reservationId) {
|
|
139
|
+
const reservation = this.reservations.get(reservationId);
|
|
140
|
+
if (!reservation) {
|
|
141
|
+
throw new Error("budget_reservation_not_found");
|
|
142
|
+
}
|
|
143
|
+
if (reservation.status !== "pending") {
|
|
144
|
+
throw new Error("budget_reservation_already_settled");
|
|
145
|
+
}
|
|
146
|
+
return reservation;
|
|
147
|
+
}
|
|
148
|
+
pendingForScope(scope) {
|
|
149
|
+
let pending = {};
|
|
150
|
+
for (const reservation of this.reservations.values()) {
|
|
151
|
+
if (reservation.scope === scope && reservation.status === "pending") {
|
|
152
|
+
pending = addBudgetAmounts(pending, reservation.cost);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return pending;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export class LedgerBackedBudgetMeter {
|
|
159
|
+
ledger;
|
|
160
|
+
initialization = null;
|
|
161
|
+
meter = new InMemoryBudgetMeter();
|
|
162
|
+
constructor(ledger) {
|
|
163
|
+
this.ledger = ledger;
|
|
164
|
+
}
|
|
165
|
+
async reserve(input) {
|
|
166
|
+
await this.initialize(input.now);
|
|
167
|
+
return this.meter.reserve(input);
|
|
168
|
+
}
|
|
169
|
+
async commit(reservationId) {
|
|
170
|
+
await this.initialize(new Date());
|
|
171
|
+
return this.meter.commit(reservationId);
|
|
172
|
+
}
|
|
173
|
+
async rollback(reservationId) {
|
|
174
|
+
await this.initialize(new Date());
|
|
175
|
+
return this.meter.rollback(reservationId);
|
|
176
|
+
}
|
|
177
|
+
initialize(now) {
|
|
178
|
+
this.initialization ??= this.ledger.readRecords().then((records) => {
|
|
179
|
+
this.meter.hydrate(records.flatMap((record) => (record.budget ? [record.budget] : [])), now);
|
|
180
|
+
});
|
|
181
|
+
return this.initialization;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
7
184
|
export class AxtaryProxyRuntime {
|
|
8
185
|
config;
|
|
186
|
+
rateLimitEvents = new Map();
|
|
187
|
+
budgetMeter;
|
|
188
|
+
budgetConfig;
|
|
9
189
|
constructor(config) {
|
|
10
190
|
this.config = config;
|
|
191
|
+
this.budgetConfig = normalizeProxyBudgetConfig(config.budget);
|
|
192
|
+
this.budgetMeter =
|
|
193
|
+
config.budgetMeter ?? new LedgerBackedBudgetMeter(config.ledger);
|
|
11
194
|
}
|
|
12
195
|
async handle(actionInput) {
|
|
13
196
|
const startedAt = performance.now();
|
|
@@ -16,6 +199,15 @@ export class AxtaryProxyRuntime {
|
|
|
16
199
|
try {
|
|
17
200
|
const parseStartedAt = performance.now();
|
|
18
201
|
action = parseNormalizedAction(actionInput);
|
|
202
|
+
if (action.budget) {
|
|
203
|
+
throw new Error("budget_authority_fields_forbidden");
|
|
204
|
+
}
|
|
205
|
+
if (action.provenance) {
|
|
206
|
+
throw new Error("provenance_authority_fields_forbidden");
|
|
207
|
+
}
|
|
208
|
+
if (this.config.provenanceResolver) {
|
|
209
|
+
action = actionWithProvenance(action, await this.config.provenanceResolver(action));
|
|
210
|
+
}
|
|
19
211
|
timings.parseMs = elapsed(parseStartedAt);
|
|
20
212
|
}
|
|
21
213
|
catch (error) {
|
|
@@ -33,7 +225,9 @@ export class AxtaryProxyRuntime {
|
|
|
33
225
|
const resolvedPolicy = typeof this.config.policy === "function"
|
|
34
226
|
? await this.config.policy()
|
|
35
227
|
: this.config.policy;
|
|
36
|
-
|
|
228
|
+
const policy = PolicyConfigSchema.parse(resolvedPolicy ?? {});
|
|
229
|
+
const behaviorSignals = await this.behaviorSignals(action);
|
|
230
|
+
policyDecision = evaluatePolicy(action, policy, this.policyEvaluationContext(action, policy, behaviorSignals));
|
|
37
231
|
timings.policyMs = elapsed(policyStartedAt);
|
|
38
232
|
}
|
|
39
233
|
catch (error) {
|
|
@@ -42,7 +236,7 @@ export class AxtaryProxyRuntime {
|
|
|
42
236
|
const ledgerStartedAt = performance.now();
|
|
43
237
|
const ledger = await this.append(action, decision, null);
|
|
44
238
|
timings.ledgerMs = elapsed(ledgerStartedAt);
|
|
45
|
-
timings
|
|
239
|
+
completeDecisionTiming(timings, startedAt);
|
|
46
240
|
return {
|
|
47
241
|
schemaVersion: PROXY_SCHEMA_VERSION,
|
|
48
242
|
status: "blocked",
|
|
@@ -63,7 +257,7 @@ export class AxtaryProxyRuntime {
|
|
|
63
257
|
}));
|
|
64
258
|
timings.approvalMs = elapsed(approvalStartedAt);
|
|
65
259
|
if (approvalEvidence.approval || approvalEvidence.approvalArtifact) {
|
|
66
|
-
policyDecision = approveStepUp(policyDecision);
|
|
260
|
+
policyDecision = approveStepUp(policyDecision, approvalEvidence);
|
|
67
261
|
}
|
|
68
262
|
}
|
|
69
263
|
catch (error) {
|
|
@@ -72,7 +266,7 @@ export class AxtaryProxyRuntime {
|
|
|
72
266
|
const ledgerStartedAt = performance.now();
|
|
73
267
|
const ledger = await this.append(action, decision, null);
|
|
74
268
|
timings.ledgerMs = elapsed(ledgerStartedAt);
|
|
75
|
-
timings
|
|
269
|
+
completeDecisionTiming(timings, startedAt);
|
|
76
270
|
return {
|
|
77
271
|
schemaVersion: PROXY_SCHEMA_VERSION,
|
|
78
272
|
status: "blocked",
|
|
@@ -88,7 +282,7 @@ export class AxtaryProxyRuntime {
|
|
|
88
282
|
const ledgerStartedAt = performance.now();
|
|
89
283
|
const ledger = await this.append(action, policyDecision, null);
|
|
90
284
|
timings.ledgerMs = elapsed(ledgerStartedAt);
|
|
91
|
-
timings
|
|
285
|
+
completeDecisionTiming(timings, startedAt);
|
|
92
286
|
return {
|
|
93
287
|
schemaVersion: PROXY_SCHEMA_VERSION,
|
|
94
288
|
status: "blocked",
|
|
@@ -99,13 +293,14 @@ export class AxtaryProxyRuntime {
|
|
|
99
293
|
timings,
|
|
100
294
|
};
|
|
101
295
|
}
|
|
296
|
+
this.consumeRateLimit(action, policyDecision);
|
|
102
297
|
const handler = this.config.handlers?.[action.capability.tool];
|
|
103
298
|
if (!handler) {
|
|
104
299
|
const decision = proxyDeny([`proxy_handler_missing:${action.capability.tool}`], policyDecision);
|
|
105
300
|
const ledgerStartedAt = performance.now();
|
|
106
301
|
const ledger = await this.append(action, decision, null);
|
|
107
302
|
timings.ledgerMs = elapsed(ledgerStartedAt);
|
|
108
|
-
timings
|
|
303
|
+
completeDecisionTiming(timings, startedAt);
|
|
109
304
|
return {
|
|
110
305
|
schemaVersion: PROXY_SCHEMA_VERSION,
|
|
111
306
|
status: "blocked",
|
|
@@ -121,7 +316,26 @@ export class AxtaryProxyRuntime {
|
|
|
121
316
|
const ledgerStartedAt = performance.now();
|
|
122
317
|
const ledger = await this.append(action, decision, null);
|
|
123
318
|
timings.ledgerMs = elapsed(ledgerStartedAt);
|
|
124
|
-
timings
|
|
319
|
+
completeDecisionTiming(timings, startedAt);
|
|
320
|
+
return {
|
|
321
|
+
schemaVersion: PROXY_SCHEMA_VERSION,
|
|
322
|
+
status: "blocked",
|
|
323
|
+
action,
|
|
324
|
+
decision,
|
|
325
|
+
actionPass: null,
|
|
326
|
+
ledger,
|
|
327
|
+
timings,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const budgetStartedAt = performance.now();
|
|
331
|
+
const budgetReservation = await this.reserveBudget(action, policyDecision.constraints.expiresInSeconds);
|
|
332
|
+
timings.budgetMs = elapsed(budgetStartedAt);
|
|
333
|
+
if (budgetReservation && !budgetReservation.accepted) {
|
|
334
|
+
const decision = proxyDeny(["budget_exceeded"], policyDecision);
|
|
335
|
+
const ledgerStartedAt = performance.now();
|
|
336
|
+
const ledger = await this.append(action, decision, null, budgetReservation.event);
|
|
337
|
+
timings.ledgerMs = elapsed(ledgerStartedAt);
|
|
338
|
+
completeDecisionTiming(timings, startedAt);
|
|
125
339
|
return {
|
|
126
340
|
schemaVersion: PROXY_SCHEMA_VERSION,
|
|
127
341
|
status: "blocked",
|
|
@@ -132,11 +346,14 @@ export class AxtaryProxyRuntime {
|
|
|
132
346
|
timings,
|
|
133
347
|
};
|
|
134
348
|
}
|
|
349
|
+
if (budgetReservation?.accepted) {
|
|
350
|
+
action = actionWithBudget(action, budgetReservation.reservation);
|
|
351
|
+
}
|
|
135
352
|
let actionPass = null;
|
|
136
353
|
if (this.config.signingKey) {
|
|
354
|
+
const actionPassStartedAt = performance.now();
|
|
137
355
|
try {
|
|
138
|
-
const
|
|
139
|
-
actionPass = await issueActionPass({
|
|
356
|
+
const issueInput = {
|
|
140
357
|
action,
|
|
141
358
|
decision: policyDecision,
|
|
142
359
|
issuer: this.config.issuer,
|
|
@@ -147,15 +364,42 @@ export class AxtaryProxyRuntime {
|
|
|
147
364
|
now: this.now(),
|
|
148
365
|
approval: approvalEvidence.approval,
|
|
149
366
|
approvalArtifact: approvalEvidence.approvalArtifact,
|
|
150
|
-
}
|
|
367
|
+
};
|
|
368
|
+
if (this.config.holderPublicJwk && this.config.statusReference) {
|
|
369
|
+
const passId = `ap_${randomUUID()}`;
|
|
370
|
+
actionPass = await issueActionPassV2({
|
|
371
|
+
...issueInput,
|
|
372
|
+
passId,
|
|
373
|
+
holderPublicJwk: this.config.holderPublicJwk,
|
|
374
|
+
status: {
|
|
375
|
+
status_list: await this.config.statusReference(passId),
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
else if (this.config.holderPublicJwk) {
|
|
380
|
+
actionPass = await issueActionPassV1({
|
|
381
|
+
...issueInput,
|
|
382
|
+
holderPublicJwk: this.config.holderPublicJwk,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
actionPass = await issueActionPass(issueInput);
|
|
387
|
+
}
|
|
151
388
|
timings.actionPassMs = elapsed(actionPassStartedAt);
|
|
152
389
|
}
|
|
153
|
-
catch {
|
|
154
|
-
|
|
390
|
+
catch (error) {
|
|
391
|
+
timings.actionPassMs = elapsed(actionPassStartedAt);
|
|
392
|
+
let budgetEvent;
|
|
393
|
+
if (budgetReservation?.accepted) {
|
|
394
|
+
const rollbackStartedAt = performance.now();
|
|
395
|
+
budgetEvent = await this.budgetMeter.rollback(budgetReservation.reservation.reservationId);
|
|
396
|
+
timings.budgetMs += elapsed(rollbackStartedAt);
|
|
397
|
+
}
|
|
398
|
+
const decision = proxyDeny([`proxy_actionpass_issue_failed:${errorToReason(error)}`], policyDecision);
|
|
155
399
|
const ledgerStartedAt = performance.now();
|
|
156
|
-
const ledger = await this.append(action, decision, null);
|
|
400
|
+
const ledger = await this.append(action, decision, null, budgetEvent);
|
|
157
401
|
timings.ledgerMs = elapsed(ledgerStartedAt);
|
|
158
|
-
timings
|
|
402
|
+
completeDecisionTiming(timings, startedAt);
|
|
159
403
|
return {
|
|
160
404
|
schemaVersion: PROXY_SCHEMA_VERSION,
|
|
161
405
|
status: "blocked",
|
|
@@ -168,16 +412,50 @@ export class AxtaryProxyRuntime {
|
|
|
168
412
|
}
|
|
169
413
|
}
|
|
170
414
|
const ledgerStartedAt = performance.now();
|
|
171
|
-
|
|
415
|
+
let ledger;
|
|
416
|
+
try {
|
|
417
|
+
ledger = await this.append(action, policyDecision, actionPass?.claims.jti ?? null, budgetReservation?.accepted ? budgetReservation.event : undefined);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
if (budgetReservation?.accepted) {
|
|
421
|
+
await this.budgetMeter.rollback(budgetReservation.reservation.reservationId);
|
|
422
|
+
}
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
172
425
|
timings.ledgerMs = elapsed(ledgerStartedAt);
|
|
426
|
+
timings.decisionMs = elapsed(startedAt);
|
|
173
427
|
const handlerStartedAt = performance.now();
|
|
428
|
+
let budgetSettled = false;
|
|
429
|
+
let settledBudgetEvent;
|
|
174
430
|
try {
|
|
431
|
+
const dpopProof = actionPass?.claims.apv === "axtary.actionpass.v1" ||
|
|
432
|
+
actionPass?.claims.apv === "axtary.actionpass.v2"
|
|
433
|
+
? await this.createProof(action, actionPass)
|
|
434
|
+
: null;
|
|
175
435
|
const result = await withTimeout(handler({
|
|
176
436
|
action,
|
|
177
437
|
decision: policyDecision,
|
|
178
438
|
actionPass,
|
|
439
|
+
dpopProof,
|
|
179
440
|
ledgerRecord: ledger.record,
|
|
180
441
|
}), this.config.handlerTimeoutMs ?? DEFAULT_HANDLER_TIMEOUT_MS);
|
|
442
|
+
const budgetEvent = budgetReservation?.accepted
|
|
443
|
+
? await this.budgetMeter.commit(budgetReservation.reservation.reservationId)
|
|
444
|
+
: undefined;
|
|
445
|
+
budgetSettled = budgetReservation?.accepted ?? false;
|
|
446
|
+
settledBudgetEvent = budgetEvent;
|
|
447
|
+
const executionLedger = await this.appendExecutionOutcome({
|
|
448
|
+
action,
|
|
449
|
+
decision: policyDecision,
|
|
450
|
+
actionPassId: actionPass?.claims.jti ?? null,
|
|
451
|
+
correlationId: ledger.record.id,
|
|
452
|
+
traceId: ledger.record.traceId,
|
|
453
|
+
outcome: outcomeFromResult(action, result),
|
|
454
|
+
budget: budgetEvent,
|
|
455
|
+
});
|
|
456
|
+
// The action actually executed (allowed, or approved step-up), so it is
|
|
457
|
+
// now "known" — record it for future novelty/sequence evaluation.
|
|
458
|
+
await this.observeBehavior(action);
|
|
181
459
|
timings.handlerMs = elapsed(handlerStartedAt);
|
|
182
460
|
timings.totalMs = elapsed(startedAt);
|
|
183
461
|
return {
|
|
@@ -187,11 +465,25 @@ export class AxtaryProxyRuntime {
|
|
|
187
465
|
decision: policyDecision,
|
|
188
466
|
actionPass,
|
|
189
467
|
ledger,
|
|
468
|
+
executionLedger,
|
|
190
469
|
result,
|
|
191
470
|
timings,
|
|
192
471
|
};
|
|
193
472
|
}
|
|
194
473
|
catch (error) {
|
|
474
|
+
const reason = errorToReason(error);
|
|
475
|
+
const budgetEvent = budgetReservation?.accepted && !budgetSettled
|
|
476
|
+
? await this.budgetMeter.rollback(budgetReservation.reservation.reservationId)
|
|
477
|
+
: settledBudgetEvent;
|
|
478
|
+
const executionLedger = await this.appendExecutionOutcome({
|
|
479
|
+
action,
|
|
480
|
+
decision: policyDecision,
|
|
481
|
+
actionPassId: actionPass?.claims.jti ?? null,
|
|
482
|
+
correlationId: ledger.record.id,
|
|
483
|
+
traceId: ledger.record.traceId,
|
|
484
|
+
outcome: outcomeFromError(action, reason),
|
|
485
|
+
budget: budgetEvent,
|
|
486
|
+
});
|
|
195
487
|
timings.handlerMs = elapsed(handlerStartedAt);
|
|
196
488
|
timings.totalMs = elapsed(startedAt);
|
|
197
489
|
return {
|
|
@@ -201,24 +493,250 @@ export class AxtaryProxyRuntime {
|
|
|
201
493
|
decision: policyDecision,
|
|
202
494
|
actionPass,
|
|
203
495
|
ledger,
|
|
496
|
+
executionLedger,
|
|
204
497
|
error: {
|
|
205
|
-
reason
|
|
498
|
+
reason,
|
|
206
499
|
},
|
|
207
500
|
timings,
|
|
208
501
|
};
|
|
209
502
|
}
|
|
210
503
|
}
|
|
211
|
-
|
|
504
|
+
async authorize(actionInput) {
|
|
505
|
+
const startedAt = performance.now();
|
|
506
|
+
const timings = createTimings();
|
|
507
|
+
let action;
|
|
508
|
+
try {
|
|
509
|
+
const parseStartedAt = performance.now();
|
|
510
|
+
action = parseNormalizedAction(actionInput);
|
|
511
|
+
if (action.budget) {
|
|
512
|
+
throw new Error("budget_authority_fields_forbidden");
|
|
513
|
+
}
|
|
514
|
+
if (action.provenance) {
|
|
515
|
+
throw new Error("provenance_authority_fields_forbidden");
|
|
516
|
+
}
|
|
517
|
+
if (this.config.provenanceResolver) {
|
|
518
|
+
action = actionWithProvenance(action, await this.config.provenanceResolver(action));
|
|
519
|
+
}
|
|
520
|
+
timings.parseMs = elapsed(parseStartedAt);
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
return {
|
|
524
|
+
schemaVersion: PROXY_SCHEMA_VERSION,
|
|
525
|
+
status: "malformed",
|
|
526
|
+
error: {
|
|
527
|
+
reason: errorToReason(error),
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
const policyStartedAt = performance.now();
|
|
532
|
+
let policyDecision;
|
|
533
|
+
try {
|
|
534
|
+
const resolvedPolicy = typeof this.config.policy === "function"
|
|
535
|
+
? await this.config.policy()
|
|
536
|
+
: this.config.policy;
|
|
537
|
+
const policy = PolicyConfigSchema.parse(resolvedPolicy ?? {});
|
|
538
|
+
const behaviorSignals = await this.behaviorSignals(action);
|
|
539
|
+
policyDecision = evaluatePolicy(action, policy, this.policyEvaluationContext(action, policy, behaviorSignals));
|
|
540
|
+
timings.policyMs = elapsed(policyStartedAt);
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
timings.policyMs = elapsed(policyStartedAt);
|
|
544
|
+
policyDecision = proxyDeny([
|
|
545
|
+
`proxy_policy_resolution_failed:${errorToReason(error)}`,
|
|
546
|
+
]);
|
|
547
|
+
}
|
|
548
|
+
if (policyDecision.decision === "allow" &&
|
|
549
|
+
!this.config.signingKey &&
|
|
550
|
+
this.config.allowUnsignedExecution !== true) {
|
|
551
|
+
policyDecision = proxyDeny(["proxy_signing_key_required"], policyDecision);
|
|
552
|
+
}
|
|
553
|
+
const budgetStartedAt = performance.now();
|
|
554
|
+
const budgetReservation = policyDecision.decision === "allow"
|
|
555
|
+
? await this.reserveBudget(action, policyDecision.constraints.expiresInSeconds)
|
|
556
|
+
: null;
|
|
557
|
+
timings.budgetMs = elapsed(budgetStartedAt);
|
|
558
|
+
if (budgetReservation && !budgetReservation.accepted) {
|
|
559
|
+
policyDecision = proxyDeny(["budget_exceeded"], policyDecision);
|
|
560
|
+
}
|
|
561
|
+
if (budgetReservation?.accepted) {
|
|
562
|
+
action = actionWithBudget(action, budgetReservation.reservation);
|
|
563
|
+
}
|
|
564
|
+
let actionPass = null;
|
|
565
|
+
let budgetEvent = budgetReservation?.event;
|
|
566
|
+
if (policyDecision.decision === "allow" && this.config.signingKey) {
|
|
567
|
+
const actionPassStartedAt = performance.now();
|
|
568
|
+
try {
|
|
569
|
+
const issueInput = {
|
|
570
|
+
action,
|
|
571
|
+
decision: policyDecision,
|
|
572
|
+
issuer: this.config.issuer,
|
|
573
|
+
tenant: this.config.tenant,
|
|
574
|
+
signingKey: this.config.signingKey,
|
|
575
|
+
keyId: this.config.keyId,
|
|
576
|
+
algorithm: this.config.algorithm,
|
|
577
|
+
now: this.now(),
|
|
578
|
+
};
|
|
579
|
+
if (this.config.holderPublicJwk && this.config.statusReference) {
|
|
580
|
+
const passId = `ap_${randomUUID()}`;
|
|
581
|
+
actionPass = await issueActionPassV2({
|
|
582
|
+
...issueInput,
|
|
583
|
+
passId,
|
|
584
|
+
holderPublicJwk: this.config.holderPublicJwk,
|
|
585
|
+
status: {
|
|
586
|
+
status_list: await this.config.statusReference(passId),
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
else if (this.config.holderPublicJwk) {
|
|
591
|
+
actionPass = await issueActionPassV1({
|
|
592
|
+
...issueInput,
|
|
593
|
+
holderPublicJwk: this.config.holderPublicJwk,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
actionPass = await issueActionPass(issueInput);
|
|
598
|
+
}
|
|
599
|
+
timings.actionPassMs = elapsed(actionPassStartedAt);
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
timings.actionPassMs = elapsed(actionPassStartedAt);
|
|
603
|
+
if (budgetReservation?.accepted) {
|
|
604
|
+
const rollbackStartedAt = performance.now();
|
|
605
|
+
budgetEvent = await this.budgetMeter.rollback(budgetReservation.reservation.reservationId);
|
|
606
|
+
timings.budgetMs += elapsed(rollbackStartedAt);
|
|
607
|
+
}
|
|
608
|
+
policyDecision = proxyDeny([`proxy_actionpass_issue_failed:${errorToReason(error)}`], policyDecision);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (policyDecision.decision === "allow") {
|
|
612
|
+
this.consumeRateLimit(action, policyDecision);
|
|
613
|
+
if (budgetReservation?.accepted) {
|
|
614
|
+
const commitStartedAt = performance.now();
|
|
615
|
+
budgetEvent = await this.budgetMeter.commit(budgetReservation.reservation.reservationId);
|
|
616
|
+
timings.budgetMs += elapsed(commitStartedAt);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const ledgerStartedAt = performance.now();
|
|
620
|
+
const ledger = await this.append(action, policyDecision, actionPass?.claims.jti ?? null, budgetEvent);
|
|
621
|
+
timings.ledgerMs = elapsed(ledgerStartedAt);
|
|
622
|
+
completeDecisionTiming(timings, startedAt);
|
|
623
|
+
const status = policyDecision.decision === "allow"
|
|
624
|
+
? "authorized"
|
|
625
|
+
: policyDecision.decision === "step_up"
|
|
626
|
+
? "step_up"
|
|
627
|
+
: "blocked";
|
|
628
|
+
return {
|
|
629
|
+
schemaVersion: PROXY_SCHEMA_VERSION,
|
|
630
|
+
status,
|
|
631
|
+
action,
|
|
632
|
+
decision: policyDecision,
|
|
633
|
+
actionPass,
|
|
634
|
+
ledger,
|
|
635
|
+
timings,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
append(action, decision, actionPassId, budget) {
|
|
212
639
|
return this.config.ledger.appendDecision({
|
|
213
640
|
action,
|
|
214
641
|
decision,
|
|
215
642
|
actionPassId,
|
|
643
|
+
budget,
|
|
216
644
|
occurredAt: this.now(),
|
|
217
645
|
});
|
|
218
646
|
}
|
|
647
|
+
appendExecutionOutcome(input) {
|
|
648
|
+
return this.config.ledger.appendExecutionOutcome({
|
|
649
|
+
action: input.action,
|
|
650
|
+
decision: input.decision,
|
|
651
|
+
actionPassId: input.actionPassId,
|
|
652
|
+
occurredAt: this.now(),
|
|
653
|
+
traceId: input.traceId,
|
|
654
|
+
executionOutcome: input.outcome,
|
|
655
|
+
budget: input.budget,
|
|
656
|
+
correlationId: input.correlationId,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
219
659
|
now() {
|
|
220
660
|
return this.config.now?.() ?? new Date();
|
|
221
661
|
}
|
|
662
|
+
async reserveBudget(action, expiresInSeconds) {
|
|
663
|
+
const budget = this.budgetConfig;
|
|
664
|
+
if (!budget.enabled) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
const cost = addBudgetAmounts(budget.costs["*"], budget.costs[action.capability.tool] ?? {});
|
|
668
|
+
const scopeValue = budget.scope === "actor"
|
|
669
|
+
? action.actor.agentId
|
|
670
|
+
: budget.scope === "task"
|
|
671
|
+
? action.intent.taskId
|
|
672
|
+
: this.config.tenant;
|
|
673
|
+
const now = this.now();
|
|
674
|
+
return this.budgetMeter.reserve({
|
|
675
|
+
scope: `${budget.scope}:${scopeValue}`,
|
|
676
|
+
cost,
|
|
677
|
+
limit: budget.limits,
|
|
678
|
+
now,
|
|
679
|
+
expiresAt: new Date(now.getTime() + expiresInSeconds * 1000),
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
async createProof(action, actionPass) {
|
|
683
|
+
if (!this.config.holderSigningKey || !this.config.holderPublicJwk) {
|
|
684
|
+
throw new Error("proxy_dpop_holder_key_required");
|
|
685
|
+
}
|
|
686
|
+
const target = actionPassDpopTarget(action);
|
|
687
|
+
return (await createDpopProof({
|
|
688
|
+
actionPass: actionPass.token,
|
|
689
|
+
method: target.method,
|
|
690
|
+
uri: target.uri,
|
|
691
|
+
signingKey: this.config.holderSigningKey,
|
|
692
|
+
publicJwk: this.config.holderPublicJwk,
|
|
693
|
+
algorithm: this.config.holderAlgorithm,
|
|
694
|
+
nonce: this.config.dpopNonce,
|
|
695
|
+
now: this.now(),
|
|
696
|
+
})).proof;
|
|
697
|
+
}
|
|
698
|
+
policyEvaluationContext(action, policy, behaviorSignals) {
|
|
699
|
+
const now = this.now();
|
|
700
|
+
const rateLimitCounts = Object.fromEntries(policy.rules.flatMap((rule) => {
|
|
701
|
+
const limit = rule.obligations.rateLimit;
|
|
702
|
+
if (!limit) {
|
|
703
|
+
return [];
|
|
704
|
+
}
|
|
705
|
+
const key = rateLimitKey(rule.id, limit.scope, action);
|
|
706
|
+
const cutoff = now.getTime() - limit.windowSeconds * 1000;
|
|
707
|
+
const active = (this.rateLimitEvents.get(key) ?? []).filter((timestamp) => timestamp > cutoff);
|
|
708
|
+
this.rateLimitEvents.set(key, active);
|
|
709
|
+
return [[rule.id, active.length]];
|
|
710
|
+
}));
|
|
711
|
+
return { now, rateLimitCounts, behaviorSignals };
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Read novelty/sequence signals from the durable store for this action. Pure
|
|
715
|
+
* read — no mutation; the action only becomes "known" after it executes (see
|
|
716
|
+
* `observeBehavior`). Returns undefined when no store is configured, leaving
|
|
717
|
+
* the behavior guard inert.
|
|
718
|
+
*/
|
|
719
|
+
async behaviorSignals(action) {
|
|
720
|
+
if (!this.config.behaviorStore)
|
|
721
|
+
return undefined;
|
|
722
|
+
return this.config.behaviorStore.evaluate(action);
|
|
723
|
+
}
|
|
724
|
+
/** Record an executed action so its combo/transition becomes known. */
|
|
725
|
+
async observeBehavior(action) {
|
|
726
|
+
if (!this.config.behaviorStore)
|
|
727
|
+
return;
|
|
728
|
+
await this.config.behaviorStore.observe(action);
|
|
729
|
+
}
|
|
730
|
+
consumeRateLimit(action, decision) {
|
|
731
|
+
const limit = decision.obligations?.rateLimit;
|
|
732
|
+
if (!limit) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const key = rateLimitKey(limit.ruleId, limit.scope, action);
|
|
736
|
+
const events = this.rateLimitEvents.get(key) ?? [];
|
|
737
|
+
events.push(this.now().getTime());
|
|
738
|
+
this.rateLimitEvents.set(key, events);
|
|
739
|
+
}
|
|
222
740
|
}
|
|
223
741
|
export function createProxyRuntime(config) {
|
|
224
742
|
return new AxtaryProxyRuntime(config);
|
|
@@ -238,6 +756,8 @@ function proxyDeny(reasons, previousDecision) {
|
|
|
238
756
|
maxFilesChanged: previousDecision?.constraints.maxFilesChanged ?? 0,
|
|
239
757
|
blockedPaths: previousDecision?.constraints.blockedPaths ?? [],
|
|
240
758
|
},
|
|
759
|
+
rule: previousDecision?.rule,
|
|
760
|
+
obligations: previousDecision?.obligations,
|
|
241
761
|
};
|
|
242
762
|
}
|
|
243
763
|
function normalizeApprovalResult(result) {
|
|
@@ -259,27 +779,275 @@ function normalizeApprovalResult(result) {
|
|
|
259
779
|
approvalArtifact: result.approvalArtifact,
|
|
260
780
|
};
|
|
261
781
|
}
|
|
262
|
-
function approveStepUp(decision) {
|
|
782
|
+
function approveStepUp(decision, evidence) {
|
|
783
|
+
const requiredRoles = decision.obligations?.requiredApproverRoles ?? [];
|
|
784
|
+
const actualRoles = evidence.approval?.approverRoles ??
|
|
785
|
+
evidence.approvalArtifact?.approverRoles ??
|
|
786
|
+
[];
|
|
787
|
+
const missingRole = requiredRoles.find((role) => !actualRoles.includes(role));
|
|
788
|
+
if (missingRole) {
|
|
789
|
+
return proxyDeny([`required_approver_role_missing:${missingRole}`], decision);
|
|
790
|
+
}
|
|
263
791
|
return {
|
|
264
792
|
...decision,
|
|
265
793
|
decision: "allow",
|
|
266
794
|
reasons: [...decision.reasons, "step_up_approved"],
|
|
267
795
|
};
|
|
268
796
|
}
|
|
797
|
+
function rateLimitKey(ruleId, scope, action) {
|
|
798
|
+
const value = scope === "actor"
|
|
799
|
+
? action.actor.agentId
|
|
800
|
+
: scope === "tool"
|
|
801
|
+
? action.capability.tool
|
|
802
|
+
: scope === "resource"
|
|
803
|
+
? action.capability.resource
|
|
804
|
+
: action.actor.tenant ?? "tenant:unknown";
|
|
805
|
+
return `${ruleId}:${scope}:${value}`;
|
|
806
|
+
}
|
|
807
|
+
function normalizeProxyBudgetConfig(input) {
|
|
808
|
+
const config = {
|
|
809
|
+
enabled: input?.enabled ?? false,
|
|
810
|
+
scope: input?.scope ?? "tenant",
|
|
811
|
+
limits: normalizeBudgetAmount(input?.limits ?? {}),
|
|
812
|
+
costs: Object.fromEntries(Object.entries(input?.costs ?? {}).map(([tool, cost]) => [
|
|
813
|
+
tool,
|
|
814
|
+
normalizeBudgetAmount(cost),
|
|
815
|
+
])),
|
|
816
|
+
};
|
|
817
|
+
if (config.enabled && !config.costs["*"]) {
|
|
818
|
+
throw new Error("budget_wildcard_cost_required");
|
|
819
|
+
}
|
|
820
|
+
if (config.enabled) {
|
|
821
|
+
for (const cost of Object.values(config.costs)) {
|
|
822
|
+
for (const dimension of Object.keys(cost)) {
|
|
823
|
+
if (config.limits[dimension] === undefined) {
|
|
824
|
+
throw new Error(`budget_limit_missing:${dimension}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return config;
|
|
830
|
+
}
|
|
831
|
+
function normalizeBudgetAmount(input) {
|
|
832
|
+
const amount = BudgetAmountSchema.parse(input);
|
|
833
|
+
return Object.fromEntries(Object.entries(amount).sort(([left], [right]) => left.localeCompare(right)));
|
|
834
|
+
}
|
|
835
|
+
function addBudgetAmounts(left, right) {
|
|
836
|
+
const dimensions = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
837
|
+
return Object.fromEntries([...dimensions]
|
|
838
|
+
.sort((a, b) => a.localeCompare(b))
|
|
839
|
+
.map((dimension) => [
|
|
840
|
+
dimension,
|
|
841
|
+
(left[dimension] ?? 0) + (right[dimension] ?? 0),
|
|
842
|
+
])
|
|
843
|
+
.filter(([, value]) => value !== 0));
|
|
844
|
+
}
|
|
845
|
+
function actionWithBudget(action, reservation) {
|
|
846
|
+
const budget = {
|
|
847
|
+
reservationId: reservation.reservationId,
|
|
848
|
+
cost: reservation.cost,
|
|
849
|
+
limit: reservation.limit,
|
|
850
|
+
commitStatus: "pending",
|
|
851
|
+
};
|
|
852
|
+
return parseNormalizedAction({
|
|
853
|
+
...action,
|
|
854
|
+
budget,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
function actionWithProvenance(action, provenanceInput) {
|
|
858
|
+
return parseNormalizedAction({
|
|
859
|
+
...action,
|
|
860
|
+
provenance: ActionProvenanceSchema.parse(provenanceInput),
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
function outcomeFromResult(action, result) {
|
|
864
|
+
const resultObject = isJsonObject(result) ? result : {};
|
|
865
|
+
const provider = providerFromValue(resultObject.provider) ?? providerFromTool(action);
|
|
866
|
+
const resultId = getString(resultObject.id) ??
|
|
867
|
+
getString(resultObject.commitSha) ??
|
|
868
|
+
getString(resultObject.sha) ??
|
|
869
|
+
(typeof resultObject.number === "number" ? String(resultObject.number) : undefined);
|
|
870
|
+
const resultUrl = getUrlString(resultObject.url);
|
|
871
|
+
const provenance = actionProvenanceProof(action);
|
|
872
|
+
const resourceId = getString(resultObject.path) ??
|
|
873
|
+
getString(resultObject.issueKey) ??
|
|
874
|
+
getString(resultObject.channel) ??
|
|
875
|
+
getString(resultObject.bucket) ??
|
|
876
|
+
getString(resultObject.projectId) ??
|
|
877
|
+
getString(resultObject.fileId) ??
|
|
878
|
+
getString(resultObject.branch) ??
|
|
879
|
+
getString(provenance.toolName) ??
|
|
880
|
+
getString(resultObject.workspace);
|
|
881
|
+
return {
|
|
882
|
+
schemaVersion: "axtary.ledger_execution_outcome.v0",
|
|
883
|
+
status: "succeeded",
|
|
884
|
+
provider,
|
|
885
|
+
resultId,
|
|
886
|
+
resultUrl,
|
|
887
|
+
resourceId,
|
|
888
|
+
resourceLabel: getString(resultObject.title) ?? resourceId,
|
|
889
|
+
sideEffectProof: compactJson({
|
|
890
|
+
...provenance,
|
|
891
|
+
provider,
|
|
892
|
+
kind: getString(resultObject.kind),
|
|
893
|
+
id: resultId,
|
|
894
|
+
url: resultUrl,
|
|
895
|
+
status: getString(resultObject.status) ?? getString(resultObject.state),
|
|
896
|
+
resourceId,
|
|
897
|
+
actionPassId: getString(resultObject.actionPassId),
|
|
898
|
+
number: typeof resultObject.number === "number" ? resultObject.number : undefined,
|
|
899
|
+
branch: getString(resultObject.branch),
|
|
900
|
+
commitSha: getString(resultObject.commitSha),
|
|
901
|
+
channel: getString(resultObject.channel),
|
|
902
|
+
ts: getString(resultObject.ts),
|
|
903
|
+
issueKey: getString(resultObject.issueKey),
|
|
904
|
+
path: getString(resultObject.path),
|
|
905
|
+
acceptedPermissions: Array.isArray(resultObject.acceptedPermissions)
|
|
906
|
+
? resultObject.acceptedPermissions
|
|
907
|
+
: undefined,
|
|
908
|
+
rateLimit: isJsonObject(resultObject.rateLimit)
|
|
909
|
+
? resultObject.rateLimit
|
|
910
|
+
: undefined,
|
|
911
|
+
workspace: getString(resultObject.workspace),
|
|
912
|
+
fileId: getString(resultObject.fileId),
|
|
913
|
+
mimeType: getString(resultObject.mimeType),
|
|
914
|
+
scope: getString(resultObject.scope),
|
|
915
|
+
appAuthorized: typeof resultObject.appAuthorized === "boolean"
|
|
916
|
+
? resultObject.appAuthorized
|
|
917
|
+
: undefined,
|
|
918
|
+
database: getString(resultObject.database),
|
|
919
|
+
role: getString(resultObject.role),
|
|
920
|
+
transactionReadOnly: typeof resultObject.transactionReadOnly === "boolean"
|
|
921
|
+
? resultObject.transactionReadOnly
|
|
922
|
+
: undefined,
|
|
923
|
+
roleLeastPrivilege: typeof resultObject.roleLeastPrivilege === "boolean"
|
|
924
|
+
? resultObject.roleLeastPrivilege
|
|
925
|
+
: undefined,
|
|
926
|
+
statementHash: getString(resultObject.statementHash),
|
|
927
|
+
predicateHash: getString(resultObject.predicateHash),
|
|
928
|
+
tables: Array.isArray(resultObject.tables)
|
|
929
|
+
? resultObject.tables
|
|
930
|
+
: undefined,
|
|
931
|
+
rowCount: typeof resultObject.rowCount === "number"
|
|
932
|
+
? resultObject.rowCount
|
|
933
|
+
: undefined,
|
|
934
|
+
fields: Array.isArray(resultObject.fields)
|
|
935
|
+
? resultObject.fields
|
|
936
|
+
: undefined,
|
|
937
|
+
rls: Array.isArray(resultObject.rls) ? resultObject.rls : undefined,
|
|
938
|
+
root: getString(resultObject.root),
|
|
939
|
+
bytes: typeof resultObject.bytes === "number" ? resultObject.bytes : undefined,
|
|
940
|
+
truncated: typeof resultObject.truncated === "boolean"
|
|
941
|
+
? resultObject.truncated
|
|
942
|
+
: undefined,
|
|
943
|
+
resultCount: Array.isArray(resultObject.results)
|
|
944
|
+
? resultObject.results.length
|
|
945
|
+
: undefined,
|
|
946
|
+
objectCount: Array.isArray(resultObject.objects)
|
|
947
|
+
? resultObject.objects.length
|
|
948
|
+
: undefined,
|
|
949
|
+
}),
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function outcomeFromError(action, reason) {
|
|
953
|
+
return {
|
|
954
|
+
schemaVersion: "axtary.ledger_execution_outcome.v0",
|
|
955
|
+
status: "failed",
|
|
956
|
+
provider: providerFromTool(action),
|
|
957
|
+
failureReason: reason,
|
|
958
|
+
errorClass: reason.split(":", 1)[0] || "handler_failed",
|
|
959
|
+
sideEffectProof: actionProvenanceProof(action),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function actionProvenanceProof(action) {
|
|
963
|
+
const payload = action.capability.payload;
|
|
964
|
+
if (action.capability.tool === "mcp.tool.call") {
|
|
965
|
+
return compactJson({
|
|
966
|
+
serverIdentity: action.toolDefinition?.serverIdentity ?? getString(payload.serverIdentity),
|
|
967
|
+
toolName: getString(payload.toolName),
|
|
968
|
+
toolSchemaVersion: action.toolDefinition?.schemaVersion,
|
|
969
|
+
definitionHash: action.toolDefinition?.definitionHash,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
if (action.capability.tool.startsWith("docs.")) {
|
|
973
|
+
return compactJson({
|
|
974
|
+
workspace: getString(payload.workspace),
|
|
975
|
+
query: getString(payload.query),
|
|
976
|
+
path: getString(payload.path),
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
if (action.capability.tool === "drive.files.read") {
|
|
980
|
+
return compactJson({
|
|
981
|
+
fileId: getString(payload.fileId),
|
|
982
|
+
scope: "https://www.googleapis.com/auth/drive.file",
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
return {};
|
|
986
|
+
}
|
|
987
|
+
function providerFromTool(action) {
|
|
988
|
+
const prefix = action.capability.tool.split(".", 1)[0];
|
|
989
|
+
return providerFromValue(prefix);
|
|
990
|
+
}
|
|
991
|
+
function providerFromValue(value) {
|
|
992
|
+
const provider = typeof value === "string" && value.startsWith("fake-")
|
|
993
|
+
? value.slice("fake-".length)
|
|
994
|
+
: value;
|
|
995
|
+
switch (provider) {
|
|
996
|
+
case "github":
|
|
997
|
+
case "slack":
|
|
998
|
+
case "linear":
|
|
999
|
+
case "docs":
|
|
1000
|
+
case "drive":
|
|
1001
|
+
case "mcp":
|
|
1002
|
+
case "aws":
|
|
1003
|
+
case "gcp":
|
|
1004
|
+
case "jira":
|
|
1005
|
+
case "postgres":
|
|
1006
|
+
return provider;
|
|
1007
|
+
default:
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function compactJson(input) {
|
|
1012
|
+
return Object.fromEntries(Object.entries(input).filter((entry) => entry[1] !== undefined));
|
|
1013
|
+
}
|
|
1014
|
+
function getString(value) {
|
|
1015
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
1016
|
+
}
|
|
1017
|
+
function getUrlString(value) {
|
|
1018
|
+
const url = getString(value);
|
|
1019
|
+
if (!url)
|
|
1020
|
+
return undefined;
|
|
1021
|
+
try {
|
|
1022
|
+
return new URL(url).toString();
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
return undefined;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
function isJsonObject(value) {
|
|
1029
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1030
|
+
}
|
|
269
1031
|
function errorToReason(error) {
|
|
270
1032
|
return error instanceof Error ? error.message : "unknown_error";
|
|
271
1033
|
}
|
|
272
1034
|
function createTimings() {
|
|
273
1035
|
return {
|
|
274
1036
|
totalMs: 0,
|
|
1037
|
+
decisionMs: 0,
|
|
275
1038
|
parseMs: 0,
|
|
276
1039
|
policyMs: 0,
|
|
1040
|
+
budgetMs: 0,
|
|
277
1041
|
actionPassMs: 0,
|
|
278
1042
|
ledgerMs: 0,
|
|
279
1043
|
approvalMs: 0,
|
|
280
1044
|
handlerMs: 0,
|
|
281
1045
|
};
|
|
282
1046
|
}
|
|
1047
|
+
function completeDecisionTiming(timings, startedAt) {
|
|
1048
|
+
timings.decisionMs = elapsed(startedAt);
|
|
1049
|
+
timings.totalMs = timings.decisionMs;
|
|
1050
|
+
}
|
|
283
1051
|
function elapsed(startedAt) {
|
|
284
1052
|
return Math.round((performance.now() - startedAt) * 1000) / 1000;
|
|
285
1053
|
}
|