@agentguard-run/spend 0.13.3 → 0.14.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/CHANGELOG.md +11 -0
- package/dist/cli/hermes-kanban.d.ts +2 -0
- package/dist/cli/hermes-kanban.d.ts.map +1 -0
- package/dist/cli/hermes-kanban.js +86 -0
- package/dist/cli/hermes-kanban.js.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +5 -0
- package/dist/cli/main.js.map +1 -1
- 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/frameworks/hermes-kanban.d.ts +131 -0
- package/dist/frameworks/hermes-kanban.d.ts.map +1 -0
- package/dist/frameworks/hermes-kanban.js +486 -0
- package/dist/frameworks/hermes-kanban.js.map +1 -0
- package/dist/frameworks/index.d.ts +1 -0
- package/dist/frameworks/index.d.ts.map +1 -1
- package/dist/frameworks/index.js +1 -0
- package/dist/frameworks/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- 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 +6 -1
- package/src/frameworks/hermes-kanban.ts +622 -0
- package/src/frameworks/index.ts +1 -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/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;
|