@agentguard-run/spend 0.14.0 → 0.15.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/CHANGELOG.md +11 -0
- package/dist/cli/serve.d.ts +1 -0
- package/dist/cli/serve.d.ts.map +1 -1
- package/dist/cli/serve.js +8 -4
- package/dist/cli/serve.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/operating-pack.d.ts +88 -0
- package/dist/operating-pack.d.ts.map +1 -0
- package/dist/operating-pack.js +329 -0
- package/dist/operating-pack.js.map +1 -0
- package/dist/telemetry.js +1 -1
- package/dist/workflow/context.d.ts.map +1 -1
- package/dist/workflow/context.js +6 -3
- package/dist/workflow/context.js.map +1 -1
- package/dist/workflow/receipt.d.ts +22 -6
- package/dist/workflow/receipt.d.ts.map +1 -1
- package/dist/workflow/receipt.js +109 -22
- package/dist/workflow/receipt.js.map +1 -1
- package/dist/workflow/types.d.ts +7 -0
- package/dist/workflow/types.d.ts.map +1 -1
- package/package.json +8 -2
- package/src/operating-pack.ts +397 -0
- package/src/workflow/context.ts +6 -3
- package/src/workflow/receipt.ts +134 -30
- package/src/workflow/types.ts +7 -0
package/src/workflow/context.ts
CHANGED
|
@@ -127,7 +127,7 @@ export class WorkflowContext {
|
|
|
127
127
|
const cost = this.extractOutcomeCost(result);
|
|
128
128
|
const parentReceiptId = this._last_receipt_id;
|
|
129
129
|
const chainHash = this._receipts.length ? await computeChainHash(this._receipts[this._receipts.length - 1]!) : null;
|
|
130
|
-
const receipt = buildReceipt({
|
|
130
|
+
const receipt = await buildReceipt({
|
|
131
131
|
workflow_id: this.workflow_id,
|
|
132
132
|
outcome_name: name,
|
|
133
133
|
user_id: this.user_id,
|
|
@@ -137,6 +137,7 @@ export class WorkflowContext {
|
|
|
137
137
|
workflow_checkpoint_idx: null,
|
|
138
138
|
workflow_total_spend_usd_to_date: this._total_spend_usd + cost,
|
|
139
139
|
reviewer_cascade: extractReviewerCascade(result),
|
|
140
|
+
signingKeys: this.config.signingKeys,
|
|
140
141
|
});
|
|
141
142
|
|
|
142
143
|
this._receipts.push(receipt);
|
|
@@ -196,7 +197,7 @@ export class WorkflowContext {
|
|
|
196
197
|
|
|
197
198
|
private async writeCheckpoint(label?: string): Promise<CheckpointReceipt> {
|
|
198
199
|
const chainHash = this._receipts.length ? await computeChainHash(this._receipts[this._receipts.length - 1]!) : null;
|
|
199
|
-
const checkpoint = buildReceipt({
|
|
200
|
+
const checkpoint = await buildReceipt({
|
|
200
201
|
workflow_id: this.workflow_id,
|
|
201
202
|
outcome_name: 'CHECKPOINT',
|
|
202
203
|
user_id: this.user_id,
|
|
@@ -208,6 +209,7 @@ export class WorkflowContext {
|
|
|
208
209
|
is_checkpoint: true,
|
|
209
210
|
checkpoint_label: label ?? null,
|
|
210
211
|
receipt_type: 'checkpoint',
|
|
212
|
+
signingKeys: this.config.signingKeys,
|
|
211
213
|
}) as CheckpointReceipt;
|
|
212
214
|
this._receipts.push(checkpoint);
|
|
213
215
|
this._last_checkpoint_id = checkpoint.receipt_id;
|
|
@@ -219,7 +221,7 @@ export class WorkflowContext {
|
|
|
219
221
|
|
|
220
222
|
private async writeFinalReceipt(type: 'cancelled' | 'completed' | 'cap_hit', reason: string): Promise<void> {
|
|
221
223
|
const chainHash = this._receipts.length ? await computeChainHash(this._receipts[this._receipts.length - 1]!) : null;
|
|
222
|
-
const receipt = buildReceipt({
|
|
224
|
+
const receipt = await buildReceipt({
|
|
223
225
|
workflow_id: this.workflow_id,
|
|
224
226
|
outcome_name: type === 'completed' ? 'WORKFLOW_COMPLETED' : 'WORKFLOW_STOPPED',
|
|
225
227
|
user_id: this.user_id,
|
|
@@ -230,6 +232,7 @@ export class WorkflowContext {
|
|
|
230
232
|
workflow_total_spend_usd_to_date: this._total_spend_usd,
|
|
231
233
|
receipt_type: type === 'cancelled' ? 'cancel' : type,
|
|
232
234
|
cancelled_reason: reason,
|
|
235
|
+
signingKeys: this.config.signingKeys,
|
|
233
236
|
});
|
|
234
237
|
this._receipts.push(receipt);
|
|
235
238
|
this._last_receipt_id = receipt.receipt_id;
|
package/src/workflow/receipt.ts
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
|
-
import { createHash, randomUUID } from 'crypto';
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, randomUUID, sign as cryptoSign, verify as cryptoVerify, type KeyObject } from 'crypto';
|
|
2
2
|
import { canonicalJson, sha256Hex } from '../decision-log';
|
|
3
3
|
import type { ProvenanceBlock } from '../receipts/schema';
|
|
4
|
-
import type { ReceiptV2, ReceiptV3, ReviewerCascadeEvidence } from './types';
|
|
4
|
+
import type { ReceiptV2, ReceiptV3, ReviewerCascadeEvidence, WorkflowConfig } from './types';
|
|
5
|
+
|
|
6
|
+
const ED25519_PKCS8_SEED_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
|
|
7
|
+
const ED25519_SPKI_PUBLIC_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
8
|
+
|
|
9
|
+
type WorkflowSigningKeys = NonNullable<WorkflowConfig['signingKeys']>;
|
|
10
|
+
|
|
11
|
+
interface ResolvedSigningKey {
|
|
12
|
+
privateKey: KeyObject;
|
|
13
|
+
publicKeyRaw: Buffer;
|
|
14
|
+
publicKeyHex: string;
|
|
15
|
+
publicKeyFingerprint: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WorkflowReceiptVerifyOptions {
|
|
19
|
+
publicKeyHex?: string;
|
|
20
|
+
allowedSignerPublicKeysHex?: string[];
|
|
21
|
+
}
|
|
5
22
|
|
|
6
23
|
export function canonicalJSONStringify(value: unknown): string {
|
|
7
24
|
return canonicalJson(value);
|
|
@@ -15,11 +32,21 @@ export function workflowReceiptId(prefix = 'ag_r'): string {
|
|
|
15
32
|
return `${prefix}_${randomUUID().replace(/-/g, '').slice(0, 16)}`;
|
|
16
33
|
}
|
|
17
34
|
|
|
18
|
-
export function signReceiptPayload(payload: unknown):
|
|
19
|
-
|
|
35
|
+
export async function signReceiptPayload(payload: unknown, signingKeys?: WorkflowSigningKeys): Promise<{
|
|
36
|
+
signature: string;
|
|
37
|
+
public_key_fingerprint: string;
|
|
38
|
+
public_key_hex: string;
|
|
39
|
+
}> {
|
|
40
|
+
const resolved = resolveWorkflowSigningKey(signingKeys);
|
|
41
|
+
const canonical = canonicalJson(payload);
|
|
42
|
+
return {
|
|
43
|
+
signature: cryptoSign(null, Buffer.from(canonical, 'utf8'), resolved.privateKey).toString('hex'),
|
|
44
|
+
public_key_fingerprint: resolved.publicKeyFingerprint,
|
|
45
|
+
public_key_hex: resolved.publicKeyHex,
|
|
46
|
+
};
|
|
20
47
|
}
|
|
21
48
|
|
|
22
|
-
export function buildReceipt(args: {
|
|
49
|
+
export async function buildReceipt(args: {
|
|
23
50
|
workflow_id: string | null;
|
|
24
51
|
outcome_name: string;
|
|
25
52
|
user_id: string;
|
|
@@ -33,22 +60,20 @@ export function buildReceipt(args: {
|
|
|
33
60
|
reviewer_cascade?: ReviewerCascadeEvidence | null;
|
|
34
61
|
receipt_type?: ReceiptV2['receipt_type'];
|
|
35
62
|
cancelled_reason?: string | null;
|
|
36
|
-
|
|
63
|
+
signingKeys?: WorkflowSigningKeys;
|
|
64
|
+
receipt_id?: string;
|
|
65
|
+
signed_at?: string;
|
|
66
|
+
}): Promise<ReceiptV2> {
|
|
37
67
|
if (!Number.isFinite(args.cost_usd) || args.cost_usd < 0) {
|
|
38
68
|
throw new Error('Workflow receipt cost must be a finite non-negative number');
|
|
39
69
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
.digest('hex')
|
|
43
|
-
.slice(0, 16);
|
|
44
|
-
const unsigned: Omit<ReceiptV2, 'signature'> = {
|
|
45
|
-
receipt_id: workflowReceiptId(args.is_checkpoint ? 'ag_cp' : 'ag_r'),
|
|
70
|
+
const unsigned: Omit<ReceiptV2, 'signature' | 'public_key_fingerprint' | 'public_key_hex'> = {
|
|
71
|
+
receipt_id: args.receipt_id ?? workflowReceiptId(args.is_checkpoint ? 'ag_cp' : 'ag_r'),
|
|
46
72
|
schema_version: 2,
|
|
47
73
|
outcome_name: args.outcome_name,
|
|
48
74
|
user_id: args.user_id,
|
|
49
75
|
cost_usd: roundUsd(args.cost_usd),
|
|
50
|
-
signed_at: new Date().toISOString(),
|
|
51
|
-
public_key_fingerprint: publicKeyFingerprint,
|
|
76
|
+
signed_at: args.signed_at ?? new Date().toISOString(),
|
|
52
77
|
workflow_id: args.workflow_id,
|
|
53
78
|
parent_receipt_id: args.parent_receipt_id,
|
|
54
79
|
chain_validation_hash: args.chain_validation_hash,
|
|
@@ -61,12 +86,10 @@ export function buildReceipt(args: {
|
|
|
61
86
|
receipt_type: args.receipt_type ?? (args.is_checkpoint ? 'checkpoint' : 'outcome'),
|
|
62
87
|
cancelled_reason: args.cancelled_reason ?? null,
|
|
63
88
|
};
|
|
64
|
-
return { ...unsigned,
|
|
89
|
+
return { ...unsigned, ...(await signReceiptPayload(unsigned, args.signingKeys)) };
|
|
65
90
|
}
|
|
66
91
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
export function buildReceiptV3(args: {
|
|
92
|
+
export async function buildReceiptV3(args: {
|
|
70
93
|
workflow_id: string | null;
|
|
71
94
|
outcome_name: string;
|
|
72
95
|
user_id: string;
|
|
@@ -81,27 +104,108 @@ export function buildReceiptV3(args: {
|
|
|
81
104
|
reviewer_cascade?: ReviewerCascadeEvidence | null;
|
|
82
105
|
receipt_type?: ReceiptV2['receipt_type'];
|
|
83
106
|
cancelled_reason?: string | null;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
signingKeys?: WorkflowSigningKeys;
|
|
108
|
+
receipt_id?: string;
|
|
109
|
+
signed_at?: string;
|
|
110
|
+
}): Promise<ReceiptV3> {
|
|
111
|
+
const v2 = await buildReceipt(args);
|
|
112
|
+
const unsigned: Omit<ReceiptV3, 'signature' | 'public_key_fingerprint' | 'public_key_hex'> = {
|
|
113
|
+
...stripReceiptSignature(v2),
|
|
88
114
|
schema_version: 3,
|
|
89
115
|
provenance: args.provenance,
|
|
90
116
|
};
|
|
91
|
-
|
|
92
|
-
return { ...withoutSignature, signature: signReceiptPayload(withoutSignature) };
|
|
117
|
+
return { ...unsigned, ...(await signReceiptPayload(unsigned, args.signingKeys)) };
|
|
93
118
|
}
|
|
94
119
|
|
|
95
|
-
export function verifyAnyReceipt(receipt: ReceiptV2 | ReceiptV3): boolean {
|
|
96
|
-
|
|
97
|
-
return signReceiptPayload(unsigned) === receipt.signature;
|
|
120
|
+
export function verifyAnyReceipt(receipt: ReceiptV2 | ReceiptV3, options: WorkflowReceiptVerifyOptions = {}): boolean {
|
|
121
|
+
return verifyReceipt(receipt, options);
|
|
98
122
|
}
|
|
99
123
|
|
|
100
|
-
export function verifyReceipt(receipt: ReceiptV2): boolean {
|
|
101
|
-
const { signature:
|
|
102
|
-
|
|
124
|
+
export function verifyReceipt(receipt: ReceiptV2 | ReceiptV3, options: WorkflowReceiptVerifyOptions = {}): boolean {
|
|
125
|
+
const { signature, public_key_hex: embeddedPublicKeyHex, public_key_fingerprint: fingerprint, ...unsigned } = receipt as ReceiptV2 & { public_key_hex?: string };
|
|
126
|
+
if (!signature || !/^[0-9a-fA-F]{128}$/.test(String(signature))) return false;
|
|
127
|
+
const publicKeyHex = String(options.publicKeyHex || embeddedPublicKeyHex || '').toLowerCase();
|
|
128
|
+
if (!/^[0-9a-f]{64}$/.test(publicKeyHex)) return false;
|
|
129
|
+
const allowed = normalizeAllowedSignerKeys(options.allowedSignerPublicKeysHex);
|
|
130
|
+
if (allowed && !allowed.has(publicKeyHex)) return false;
|
|
131
|
+
const publicKeyRaw = Buffer.from(publicKeyHex, 'hex');
|
|
132
|
+
if (fingerprint && computePublicKeyFingerprint(publicKeyRaw) !== String(fingerprint)) return false;
|
|
133
|
+
const publicKey = createPublicKey({ key: Buffer.concat([ED25519_SPKI_PUBLIC_PREFIX, publicKeyRaw]), format: 'der', type: 'spki' });
|
|
134
|
+
try {
|
|
135
|
+
return cryptoVerify(null, Buffer.from(canonicalJson(unsigned), 'utf8'), publicKey, Buffer.from(signature, 'hex'));
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
103
139
|
}
|
|
104
140
|
|
|
105
141
|
export function roundUsd(value: number): number {
|
|
106
142
|
return Math.round(value * 1_000_000) / 1_000_000;
|
|
107
143
|
}
|
|
144
|
+
|
|
145
|
+
function stripReceiptSignature(receipt: ReceiptV2): Omit<ReceiptV2, 'signature' | 'public_key_fingerprint' | 'public_key_hex'> {
|
|
146
|
+
const { signature: _signature, public_key_fingerprint: _fingerprint, public_key_hex: _publicKeyHex, ...unsigned } = receipt;
|
|
147
|
+
return unsigned;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveWorkflowSigningKey(signingKeys?: WorkflowSigningKeys): ResolvedSigningKey {
|
|
151
|
+
if (signingKeys) return keyFromSeed(signingKeys.privateKey, signingKeys.publicKey);
|
|
152
|
+
const raw = process.env.AGENTGUARD_SIGNING_KEY_ED25519_PRIVATE;
|
|
153
|
+
if (!raw) throw new Error('Workflow receipt signing key is required');
|
|
154
|
+
return keyFromEnvironment(raw);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function keyFromSeed(seed: Uint8Array, expectedPublicKey?: Uint8Array): ResolvedSigningKey {
|
|
158
|
+
if (seed.length !== 32) throw new Error(`Ed25519 private seed must be 32 bytes, got ${seed.length} bytes`);
|
|
159
|
+
const privateKey = createPrivateKey({ key: Buffer.concat([ED25519_PKCS8_SEED_PREFIX, Buffer.from(seed)]), format: 'der', type: 'pkcs8' });
|
|
160
|
+
const publicKeyRaw = rawPublicKeyFromPrivate(privateKey);
|
|
161
|
+
if (expectedPublicKey && Buffer.compare(publicKeyRaw, Buffer.from(expectedPublicKey)) !== 0) {
|
|
162
|
+
throw new Error('Workflow receipt public key does not match private seed');
|
|
163
|
+
}
|
|
164
|
+
return resolved(privateKey, publicKeyRaw);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function keyFromEnvironment(raw: string): ResolvedSigningKey {
|
|
168
|
+
const value = raw.trim();
|
|
169
|
+
let privateKey: KeyObject;
|
|
170
|
+
if (value.startsWith('-----BEGIN')) {
|
|
171
|
+
privateKey = createPrivateKey(value);
|
|
172
|
+
} else if (value.startsWith('{')) {
|
|
173
|
+
privateKey = createPrivateKey({ key: JSON.parse(value), format: 'jwk' });
|
|
174
|
+
} else {
|
|
175
|
+
const clean = value.replace(/^0x/, '');
|
|
176
|
+
let bytes: Buffer;
|
|
177
|
+
if (/^[0-9a-fA-F]+$/.test(clean) && clean.length % 2 === 0) bytes = Buffer.from(clean, 'hex');
|
|
178
|
+
else bytes = Buffer.from(value, 'base64');
|
|
179
|
+
privateKey = bytes.length === 32
|
|
180
|
+
? createPrivateKey({ key: Buffer.concat([ED25519_PKCS8_SEED_PREFIX, bytes]), format: 'der', type: 'pkcs8' })
|
|
181
|
+
: createPrivateKey({ key: bytes, format: 'der', type: 'pkcs8' });
|
|
182
|
+
}
|
|
183
|
+
return resolved(privateKey, rawPublicKeyFromPrivate(privateKey));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function rawPublicKeyFromPrivate(privateKey: KeyObject): Buffer {
|
|
187
|
+
const der = createPublicKey(privateKey).export({ format: 'der', type: 'spki' });
|
|
188
|
+
return Buffer.from(der).subarray(-32);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resolved(privateKey: KeyObject, publicKeyRaw: Buffer): ResolvedSigningKey {
|
|
192
|
+
return {
|
|
193
|
+
privateKey,
|
|
194
|
+
publicKeyRaw,
|
|
195
|
+
publicKeyHex: publicKeyRaw.toString('hex'),
|
|
196
|
+
publicKeyFingerprint: computePublicKeyFingerprint(publicKeyRaw),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function computePublicKeyFingerprint(publicKeyRaw: Buffer): string {
|
|
201
|
+
return createHash('sha256').update(publicKeyRaw).digest('hex').slice(0, 16);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeAllowedSignerKeys(explicit?: string[]): Set<string> | null {
|
|
205
|
+
const values = [...(explicit ?? [])];
|
|
206
|
+
const envAllowed = process.env.AGENTGUARD_ALLOWED_SIGNER_PUBKEYS_HEX;
|
|
207
|
+
if (envAllowed) values.push(...envAllowed.split(','));
|
|
208
|
+
if (values.length === 0) return null;
|
|
209
|
+
const clean = values.map((value) => String(value).trim().toLowerCase()).filter((value) => /^[0-9a-f]{64}$/.test(value));
|
|
210
|
+
return new Set(clean);
|
|
211
|
+
}
|
package/src/workflow/types.ts
CHANGED
|
@@ -20,6 +20,12 @@ export interface WorkflowConfig {
|
|
|
20
20
|
user_id?: string;
|
|
21
21
|
api_base_url?: string;
|
|
22
22
|
license_key?: string;
|
|
23
|
+
signingKeys?: {
|
|
24
|
+
/** 32-byte Ed25519 secret seed. */
|
|
25
|
+
privateKey: Uint8Array;
|
|
26
|
+
/** 32-byte Ed25519 public key. */
|
|
27
|
+
publicKey: Uint8Array;
|
|
28
|
+
};
|
|
23
29
|
post_json?: (url: string, body: unknown, headers: Record<string, string>) => Promise<unknown>;
|
|
24
30
|
get_json?: (url: string, headers: Record<string, string>) => Promise<unknown>;
|
|
25
31
|
}
|
|
@@ -44,6 +50,7 @@ export interface ReceiptV2 {
|
|
|
44
50
|
signed_at: string;
|
|
45
51
|
signature: string;
|
|
46
52
|
public_key_fingerprint: string;
|
|
53
|
+
public_key_hex: string;
|
|
47
54
|
workflow_id: string | null;
|
|
48
55
|
parent_receipt_id: string | null;
|
|
49
56
|
chain_validation_hash: string | null;
|