@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/cli/hermes-kanban.d.ts +2 -0
  3. package/dist/cli/hermes-kanban.d.ts.map +1 -0
  4. package/dist/cli/hermes-kanban.js +86 -0
  5. package/dist/cli/hermes-kanban.js.map +1 -0
  6. package/dist/cli/main.d.ts.map +1 -1
  7. package/dist/cli/main.js +5 -0
  8. package/dist/cli/main.js.map +1 -1
  9. package/dist/cli/serve.d.ts +1 -0
  10. package/dist/cli/serve.d.ts.map +1 -1
  11. package/dist/cli/serve.js +8 -4
  12. package/dist/cli/serve.js.map +1 -1
  13. package/dist/frameworks/hermes-kanban.d.ts +131 -0
  14. package/dist/frameworks/hermes-kanban.d.ts.map +1 -0
  15. package/dist/frameworks/hermes-kanban.js +486 -0
  16. package/dist/frameworks/hermes-kanban.js.map +1 -0
  17. package/dist/frameworks/index.d.ts +1 -0
  18. package/dist/frameworks/index.d.ts.map +1 -1
  19. package/dist/frameworks/index.js +1 -0
  20. package/dist/frameworks/index.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/telemetry.js +1 -1
  24. package/dist/workflow/context.d.ts.map +1 -1
  25. package/dist/workflow/context.js +6 -3
  26. package/dist/workflow/context.js.map +1 -1
  27. package/dist/workflow/receipt.d.ts +22 -6
  28. package/dist/workflow/receipt.d.ts.map +1 -1
  29. package/dist/workflow/receipt.js +109 -22
  30. package/dist/workflow/receipt.js.map +1 -1
  31. package/dist/workflow/types.d.ts +7 -0
  32. package/dist/workflow/types.d.ts.map +1 -1
  33. package/package.json +6 -1
  34. package/src/frameworks/hermes-kanban.ts +622 -0
  35. package/src/frameworks/index.ts +1 -0
  36. package/src/workflow/context.ts +6 -3
  37. package/src/workflow/receipt.ts +134 -30
  38. package/src/workflow/types.ts +7 -0
@@ -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): string {
19
- return sha256Hex(canonicalJson(payload));
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
- }): ReceiptV2 {
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 publicKeyFingerprint = createHash('sha256')
41
- .update(args.workflow_id || args.user_id || 'agentguard-workflow')
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, signature: signReceiptPayload(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
- }): ReceiptV3 {
85
- const v2 = buildReceipt(args);
86
- const unsigned: Omit<ReceiptV3, 'signature'> = {
87
- ...v2,
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
- const { signature: _oldSignature, ...withoutSignature } = unsigned as Omit<ReceiptV3, 'signature'> & { signature?: string };
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
- const { signature: _signature, ...unsigned } = receipt;
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: _signature, ...unsigned } = receipt;
102
- return signReceiptPayload(unsigned) === receipt.signature;
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
+ }
@@ -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;