profclaw 2.3.0 → 2.4.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 +15 -0
- package/README.md +5 -2
- package/config/pricing.yml +26 -2
- package/dist/agents/command-sandbox.d.ts +93 -0
- package/dist/agents/command-sandbox.js +235 -0
- package/dist/agents/executor-runner.d.ts +135 -0
- package/dist/agents/executor-runner.js +303 -0
- package/dist/agents/executor.d.ts +2 -0
- package/dist/agents/executor.js +35 -2
- package/dist/agents/output-truncator.d.ts +59 -0
- package/dist/agents/output-truncator.js +128 -0
- package/dist/agents/receipt/receipt-cli.d.ts +43 -0
- package/dist/agents/receipt/receipt-cli.js +82 -0
- package/dist/agents/receipt/receipt-types.d.ts +136 -0
- package/dist/agents/receipt/receipt-types.js +19 -0
- package/dist/agents/receipt/receipt-verify.d.ts +19 -0
- package/dist/agents/receipt/receipt-verify.js +176 -0
- package/dist/agents/receipt/receipt-view.d.ts +29 -0
- package/dist/agents/receipt/receipt-view.js +227 -0
- package/dist/agents/receipt/receipt-writer.d.ts +47 -0
- package/dist/agents/receipt/receipt-writer.js +186 -0
- package/dist/agents/result-store.d.ts +6 -0
- package/dist/agents/result-store.js +43 -2
- package/dist/agents/session-diff.d.ts +6 -0
- package/dist/agents/session-diff.js +10 -19
- package/dist/agents/tool-selector.d.ts +123 -0
- package/dist/agents/tool-selector.js +321 -0
- package/dist/agents/verified-run-git.d.ts +49 -0
- package/dist/agents/verified-run-git.js +82 -0
- package/dist/agents/verified-run.d.ts +149 -0
- package/dist/agents/verified-run.js +402 -0
- package/dist/agents/verifier.d.ts +64 -0
- package/dist/agents/verifier.js +186 -0
- package/dist/agents/worktree-tools.d.ts +47 -0
- package/dist/agents/worktree-tools.js +246 -0
- package/dist/chat/agentic-executor.js +14 -6
- package/dist/chat/execution/sandbox.d.ts +8 -0
- package/dist/chat/execution/sandbox.js +25 -1
- package/dist/chat/execution/tools/tts-speak.d.ts +2 -2
- package/dist/chat/meta-tool-handlers.d.ts +18 -0
- package/dist/chat/meta-tool-handlers.js +51 -0
- package/dist/chat/system-prompts.js +6 -1
- package/dist/chat/tool-handler.d.ts +2 -0
- package/dist/chat/tool-handler.js +47 -3
- package/dist/cli/commands/receipt.d.ts +7 -0
- package/dist/cli/commands/receipt.js +90 -0
- package/dist/cli/commands/run.d.ts +28 -0
- package/dist/cli/commands/run.js +204 -0
- package/dist/cli/index.js +4 -0
- package/dist/costs/token-tracker.d.ts +10 -1
- package/dist/costs/token-tracker.js +37 -5
- package/dist/providers/ai-sdk.js +33 -37
- package/dist/providers/core/models.js +59 -17
- package/dist/providers/core/types.d.ts +4 -0
- package/dist/providers/prompt-cache.d.ts +68 -0
- package/dist/providers/prompt-cache.js +165 -0
- package/dist/providers/smart-router.js +11 -3
- package/dist/routes/chat.js +22 -5
- package/dist/server.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers that connect receipts to the CLI: key handling and writer setup.
|
|
3
|
+
*/
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { generateKeyPairSync } from 'node:crypto';
|
|
6
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { ReceiptWriter, defaultReceiptPath, loadSignerFromFile } from './receipt-writer.js';
|
|
10
|
+
/** Create the writer for a run. Signing is off unless a key path is given. */
|
|
11
|
+
export function createRunReceipt(args) {
|
|
12
|
+
const env = args.env ?? process.env;
|
|
13
|
+
const keyPath = args.signKeyPath ?? (env['PROFCLAW_RECEIPT_KEY']?.trim() || undefined);
|
|
14
|
+
return new ReceiptWriter({
|
|
15
|
+
path: defaultReceiptPath(args.projectRoot, args.runId, env),
|
|
16
|
+
signer: keyPath ? loadSignerFromFile(keyPath) : undefined,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/** Version of the running profclaw package, or 'unknown'. */
|
|
20
|
+
export function toolVersion() {
|
|
21
|
+
try {
|
|
22
|
+
const pkg = createRequire(import.meta.url)('../../../package.json');
|
|
23
|
+
return typeof pkg.version === 'string' ? pkg.version : 'unknown';
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return 'unknown';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Write an Ed25519 keypair into dir. The private key file is mode 0600. */
|
|
30
|
+
export function generateKeyPairFiles(dir) {
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
33
|
+
const privateKeyPath = join(dir, 'receipt-key.pem');
|
|
34
|
+
const publicKeyPath = join(dir, 'receipt-key.pub.pem');
|
|
35
|
+
writeFileSync(privateKeyPath, privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600, flag: 'wx' });
|
|
36
|
+
writeFileSync(publicKeyPath, publicKey.export({ type: 'spki', format: 'pem' }), { mode: 0o644, flag: 'wx' });
|
|
37
|
+
return { privateKeyPath, publicKeyPath };
|
|
38
|
+
}
|
|
39
|
+
function argsSummaryChars(env) {
|
|
40
|
+
const n = Number(env['PROFCLAW_RECEIPT_ARGS_CHARS']);
|
|
41
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 300;
|
|
42
|
+
}
|
|
43
|
+
/** Short one-line description of tool arguments. Long values are cut so files are not copied into the receipt. */
|
|
44
|
+
export function summarizeArgs(args, maxChars = 300) {
|
|
45
|
+
const perValue = Math.max(16, Math.floor(maxChars / 3));
|
|
46
|
+
const parts = Object.entries(args).map(([key, value]) => {
|
|
47
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value) ?? 'undefined';
|
|
48
|
+
const short = text.length > perValue ? `${text.slice(0, perValue)}...(${text.length} chars)` : text;
|
|
49
|
+
return `${key}=${JSON.stringify(short)}`;
|
|
50
|
+
});
|
|
51
|
+
const joined = parts.join(' ');
|
|
52
|
+
return joined.length > maxChars ? `${joined.slice(0, maxChars)}...` : joined;
|
|
53
|
+
}
|
|
54
|
+
/** Everything `profclaw run` needs to record a receipt; a no-op setup when disabled. */
|
|
55
|
+
export function setupRunReceipt(args) {
|
|
56
|
+
const runId = randomUUID().slice(0, 8);
|
|
57
|
+
if (!args.enabled)
|
|
58
|
+
return { runId };
|
|
59
|
+
const env = args.env ?? process.env;
|
|
60
|
+
const receipt = createRunReceipt({ projectRoot: args.projectRoot, runId, signKeyPath: args.signKeyPath, env });
|
|
61
|
+
const cap = argsSummaryChars(env);
|
|
62
|
+
return {
|
|
63
|
+
runId,
|
|
64
|
+
receipt,
|
|
65
|
+
onToolCall: (e) => {
|
|
66
|
+
try {
|
|
67
|
+
receipt.append('tool_call', {
|
|
68
|
+
attempt: e.attempt,
|
|
69
|
+
name: e.name,
|
|
70
|
+
argsSummary: summarizeArgs(e.args, cap),
|
|
71
|
+
redacted: false,
|
|
72
|
+
durationMs: e.durationMs,
|
|
73
|
+
ok: e.ok,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Receipt problems must never break the agent; a missing event shows up as a bad chain or gap.
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=receipt-cli.js.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run receipt types (spec: docs/specs/run-receipt-v0.md).
|
|
3
|
+
* Shared by the writer, verifier and viewer.
|
|
4
|
+
*/
|
|
5
|
+
export declare const RECEIPT_VERSION = "0";
|
|
6
|
+
/** `prev` of the first event: 64 zeros. */
|
|
7
|
+
export declare const GENESIS_HASH: string;
|
|
8
|
+
export interface RunStartData {
|
|
9
|
+
goal: string;
|
|
10
|
+
verifyCommand: string;
|
|
11
|
+
repoHead: string | null;
|
|
12
|
+
branch: string;
|
|
13
|
+
tool: {
|
|
14
|
+
name: string;
|
|
15
|
+
version: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface AttemptStartData {
|
|
19
|
+
attempt: number;
|
|
20
|
+
model?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ToolCallData {
|
|
23
|
+
attempt: number;
|
|
24
|
+
name: string;
|
|
25
|
+
argsSummary: string;
|
|
26
|
+
redacted: boolean;
|
|
27
|
+
durationMs: number;
|
|
28
|
+
ok: boolean;
|
|
29
|
+
}
|
|
30
|
+
export type FileChangeKind = 'created' | 'modified' | 'deleted';
|
|
31
|
+
export interface FileChangeData {
|
|
32
|
+
path: string;
|
|
33
|
+
status: FileChangeKind;
|
|
34
|
+
beforeSha256: string | null;
|
|
35
|
+
afterSha256: string | null;
|
|
36
|
+
diffSha256: string;
|
|
37
|
+
diff?: string;
|
|
38
|
+
diffTruncated?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface VerifierResultData {
|
|
41
|
+
attempt: number;
|
|
42
|
+
command: string;
|
|
43
|
+
exitCode: number | null;
|
|
44
|
+
passed: boolean;
|
|
45
|
+
output: string;
|
|
46
|
+
failureScore: number;
|
|
47
|
+
durationMs?: number;
|
|
48
|
+
timedOut?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface UsageData {
|
|
51
|
+
attempt: number;
|
|
52
|
+
inputTokens: number;
|
|
53
|
+
outputTokens: number;
|
|
54
|
+
cacheReadTokens: number;
|
|
55
|
+
cacheWriteTokens: number;
|
|
56
|
+
totalTokens: number;
|
|
57
|
+
costUsd: number;
|
|
58
|
+
model?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface RollbackData {
|
|
61
|
+
attempt: number;
|
|
62
|
+
reason: string;
|
|
63
|
+
}
|
|
64
|
+
export type ReceiptOutcome = 'verified' | 'improved' | 'unchanged' | 'worse' | 'agent_error';
|
|
65
|
+
export interface AttemptEndData {
|
|
66
|
+
attempt: number;
|
|
67
|
+
outcome: ReceiptOutcome;
|
|
68
|
+
rolledBack: boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface RunEndData {
|
|
71
|
+
stopReason: string;
|
|
72
|
+
verified: boolean;
|
|
73
|
+
totals: {
|
|
74
|
+
attempts: number;
|
|
75
|
+
totalTokens: number;
|
|
76
|
+
costUsd: number;
|
|
77
|
+
filesChanged: number;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export interface ReceiptDataMap {
|
|
81
|
+
run_start: RunStartData;
|
|
82
|
+
attempt_start: AttemptStartData;
|
|
83
|
+
tool_call: ToolCallData;
|
|
84
|
+
file_change: FileChangeData;
|
|
85
|
+
verifier_result: VerifierResultData;
|
|
86
|
+
usage: UsageData;
|
|
87
|
+
rollback: RollbackData;
|
|
88
|
+
attempt_end: AttemptEndData;
|
|
89
|
+
run_end: RunEndData;
|
|
90
|
+
}
|
|
91
|
+
export type ReceiptEventType = keyof ReceiptDataMap;
|
|
92
|
+
export declare const RECEIPT_EVENT_TYPES: readonly ReceiptEventType[];
|
|
93
|
+
export interface ReceiptSignature {
|
|
94
|
+
alg: 'ed25519';
|
|
95
|
+
/** base64 SPKI DER */
|
|
96
|
+
publicKey: string;
|
|
97
|
+
/** base64 Ed25519 signature over the UTF-8 bytes of the run_end hash */
|
|
98
|
+
sig: string;
|
|
99
|
+
}
|
|
100
|
+
interface EventBase {
|
|
101
|
+
v: typeof RECEIPT_VERSION;
|
|
102
|
+
seq: number;
|
|
103
|
+
ts: string;
|
|
104
|
+
prev: string;
|
|
105
|
+
hash: string;
|
|
106
|
+
}
|
|
107
|
+
export type ReceiptEvent = {
|
|
108
|
+
[K in ReceiptEventType]: EventBase & {
|
|
109
|
+
type: K;
|
|
110
|
+
data: ReceiptDataMap[K];
|
|
111
|
+
signature?: ReceiptSignature;
|
|
112
|
+
};
|
|
113
|
+
}[ReceiptEventType];
|
|
114
|
+
/** Anything that can record receipt events. runVerifiedGoal only needs this. */
|
|
115
|
+
export interface ReceiptSink {
|
|
116
|
+
append<K extends ReceiptEventType>(type: K, data: ReceiptDataMap[K]): ReceiptEvent;
|
|
117
|
+
}
|
|
118
|
+
export type VerifyFailureReason = 'unreadable' | 'empty' | 'invalid_json' | 'invalid_event' | 'seq_mismatch' | 'prev_mismatch' | 'hash_mismatch' | 'bad_first_event' | 'event_after_run_end' | 'truncated' | 'bad_signature' | 'unsigned' | 'untrusted_key';
|
|
119
|
+
export interface SignatureStatus {
|
|
120
|
+
present: boolean;
|
|
121
|
+
valid?: boolean;
|
|
122
|
+
publicKey?: string;
|
|
123
|
+
}
|
|
124
|
+
export interface VerifyResult {
|
|
125
|
+
ok: boolean;
|
|
126
|
+
eventCount: number;
|
|
127
|
+
/** seq of the first bad event, or the seq where an event was expected (truncation) */
|
|
128
|
+
firstBadSeq?: number;
|
|
129
|
+
reason?: VerifyFailureReason;
|
|
130
|
+
message: string;
|
|
131
|
+
signature: SignatureStatus;
|
|
132
|
+
/** Hash of the last event read, when the chain was readable that far */
|
|
133
|
+
lastHash?: string;
|
|
134
|
+
}
|
|
135
|
+
export {};
|
|
136
|
+
//# sourceMappingURL=receipt-types.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run receipt types (spec: docs/specs/run-receipt-v0.md).
|
|
3
|
+
* Shared by the writer, verifier and viewer.
|
|
4
|
+
*/
|
|
5
|
+
export const RECEIPT_VERSION = '0';
|
|
6
|
+
/** `prev` of the first event: 64 zeros. */
|
|
7
|
+
export const GENESIS_HASH = '0'.repeat(64);
|
|
8
|
+
export const RECEIPT_EVENT_TYPES = [
|
|
9
|
+
'run_start',
|
|
10
|
+
'attempt_start',
|
|
11
|
+
'tool_call',
|
|
12
|
+
'file_change',
|
|
13
|
+
'verifier_result',
|
|
14
|
+
'usage',
|
|
15
|
+
'rollback',
|
|
16
|
+
'attempt_end',
|
|
17
|
+
'run_end',
|
|
18
|
+
];
|
|
19
|
+
//# sourceMappingURL=receipt-types.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Receipt verifier: chain integrity, completeness and optional signature.
|
|
3
|
+
* Reports the first bad seq and why. Spec: docs/specs/run-receipt-v0.md
|
|
4
|
+
*/
|
|
5
|
+
import type { ReceiptEvent, VerifyResult } from './receipt-types.js';
|
|
6
|
+
export interface VerifyOptions {
|
|
7
|
+
/** Trusted public key (PEM, or base64 SPKI DER). If set, the receipt must be signed by it. */
|
|
8
|
+
expectedPublicKey?: string;
|
|
9
|
+
/** Fail an unsigned receipt. Implied by expectedPublicKey. */
|
|
10
|
+
requireSignature?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Normalise a PEM or base64 DER public key to base64 SPKI DER. */
|
|
13
|
+
export declare function normalizePublicKey(input: string): string;
|
|
14
|
+
/** Verify receipt text (JSON Lines). */
|
|
15
|
+
export declare function verifyReceiptText(text: string, options?: VerifyOptions): VerifyResult;
|
|
16
|
+
export declare function verifyReceiptFile(path: string, options?: VerifyOptions): VerifyResult;
|
|
17
|
+
/** Parse events without verifying. For viewers; callers should verify separately. */
|
|
18
|
+
export declare function parseReceiptText(text: string): ReceiptEvent[];
|
|
19
|
+
//# sourceMappingURL=receipt-verify.d.ts.map
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Receipt verifier: chain integrity, completeness and optional signature.
|
|
3
|
+
* Reports the first bad seq and why. Spec: docs/specs/run-receipt-v0.md
|
|
4
|
+
*/
|
|
5
|
+
import { createPublicKey, verify as edVerify } from 'node:crypto';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { computeEventHash } from './receipt-writer.js';
|
|
8
|
+
import { GENESIS_HASH, RECEIPT_EVENT_TYPES, RECEIPT_VERSION } from './receipt-types.js';
|
|
9
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
10
|
+
function isRecord(v) {
|
|
11
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
12
|
+
}
|
|
13
|
+
/** Normalise a PEM or base64 DER public key to base64 SPKI DER. */
|
|
14
|
+
export function normalizePublicKey(input) {
|
|
15
|
+
const trimmed = input.trim();
|
|
16
|
+
const key = trimmed.includes('-----BEGIN')
|
|
17
|
+
? createPublicKey(trimmed)
|
|
18
|
+
: createPublicKey({ key: Buffer.from(trimmed, 'base64'), format: 'der', type: 'spki' });
|
|
19
|
+
return key.export({ type: 'spki', format: 'der' }).toString('base64');
|
|
20
|
+
}
|
|
21
|
+
function fail(events, seq, reason, message, extra = {}) {
|
|
22
|
+
return { ok: false, eventCount: events, firstBadSeq: seq, reason, message, signature: { present: false }, ...extra };
|
|
23
|
+
}
|
|
24
|
+
/** Structural check of one parsed line. Returns a problem description or null. */
|
|
25
|
+
function shapeProblem(obj) {
|
|
26
|
+
if (obj['v'] !== RECEIPT_VERSION)
|
|
27
|
+
return `unsupported version ${JSON.stringify(obj['v'])}`;
|
|
28
|
+
if (typeof obj['seq'] !== 'number' || !Number.isInteger(obj['seq']))
|
|
29
|
+
return 'seq is not an integer';
|
|
30
|
+
if (typeof obj['ts'] !== 'string')
|
|
31
|
+
return 'ts is not a string';
|
|
32
|
+
if (typeof obj['type'] !== 'string')
|
|
33
|
+
return 'type is not a string';
|
|
34
|
+
if (!isRecord(obj['data']))
|
|
35
|
+
return 'data is not an object';
|
|
36
|
+
if (typeof obj['prev'] !== 'string' || !HEX64.test(obj['prev']))
|
|
37
|
+
return 'prev is not a sha256 hex string';
|
|
38
|
+
if (typeof obj['hash'] !== 'string' || !HEX64.test(obj['hash']))
|
|
39
|
+
return 'hash is not a sha256 hex string';
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/** Verify receipt text (JSON Lines). */
|
|
43
|
+
export function verifyReceiptText(text, options = {}) {
|
|
44
|
+
const lines = text.split('\n');
|
|
45
|
+
if (lines.length > 0 && lines[lines.length - 1] === '')
|
|
46
|
+
lines.pop();
|
|
47
|
+
if (lines.length === 0)
|
|
48
|
+
return fail(0, 0, 'empty', 'Receipt is empty: expected a run_start event at seq 0');
|
|
49
|
+
let prev = GENESIS_HASH;
|
|
50
|
+
let runEnd;
|
|
51
|
+
let count = 0;
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(lines[i]);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return fail(count, i, 'invalid_json', `Line ${i + 1} (seq ${i}) is not valid JSON`);
|
|
59
|
+
}
|
|
60
|
+
if (!isRecord(parsed))
|
|
61
|
+
return fail(count, i, 'invalid_event', `Line ${i + 1} (seq ${i}) is not a JSON object`);
|
|
62
|
+
const problem = shapeProblem(parsed);
|
|
63
|
+
if (problem)
|
|
64
|
+
return fail(count, i, 'invalid_event', `Event at seq ${i}: ${problem}`);
|
|
65
|
+
if (runEnd) {
|
|
66
|
+
return fail(count, i, 'event_after_run_end', `Event at seq ${i} follows run_end`);
|
|
67
|
+
}
|
|
68
|
+
if (parsed['seq'] !== i) {
|
|
69
|
+
return fail(count, i, 'seq_mismatch', `Expected seq ${i} but found seq ${String(parsed['seq'])}: events were reordered, or one was deleted or inserted`);
|
|
70
|
+
}
|
|
71
|
+
if (parsed['prev'] !== prev) {
|
|
72
|
+
return fail(count, i, 'prev_mismatch', `seq ${i}: prev does not match the hash of seq ${i - 1}: the previous event was changed or removed`);
|
|
73
|
+
}
|
|
74
|
+
const expected = computeEventHash(parsed);
|
|
75
|
+
if (parsed['hash'] !== expected) {
|
|
76
|
+
return fail(count, i, 'hash_mismatch', `seq ${i}: hash does not match the event content: the event was edited`);
|
|
77
|
+
}
|
|
78
|
+
if (i === 0 && parsed['type'] !== 'run_start') {
|
|
79
|
+
return fail(count, 0, 'bad_first_event', `seq 0 must be run_start, found ${String(parsed['type'])}`);
|
|
80
|
+
}
|
|
81
|
+
prev = parsed['hash'];
|
|
82
|
+
count++;
|
|
83
|
+
if (parsed['type'] === 'run_end')
|
|
84
|
+
runEnd = parsed;
|
|
85
|
+
}
|
|
86
|
+
if (!runEnd) {
|
|
87
|
+
return fail(count, count, 'truncated', `No run_end event after seq ${count - 1}: the receipt is truncated or the run did not finish`, {
|
|
88
|
+
lastHash: prev,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return checkSignature(runEnd, count, prev, options);
|
|
92
|
+
}
|
|
93
|
+
function checkSignature(runEnd, count, lastHash, options) {
|
|
94
|
+
const seq = count - 1;
|
|
95
|
+
const sig = runEnd['signature'];
|
|
96
|
+
const wantSigned = options.requireSignature === true || options.expectedPublicKey !== undefined;
|
|
97
|
+
if (sig === undefined) {
|
|
98
|
+
if (wantSigned) {
|
|
99
|
+
return fail(count, seq, 'unsigned', 'Receipt has no signature but one is required', { lastHash });
|
|
100
|
+
}
|
|
101
|
+
return { ok: true, eventCount: count, message: `Chain intact, ${count} events, run complete (unsigned)`, signature: { present: false }, lastHash };
|
|
102
|
+
}
|
|
103
|
+
if (!isRecord(sig) || sig['alg'] !== 'ed25519' || typeof sig['publicKey'] !== 'string' || typeof sig['sig'] !== 'string') {
|
|
104
|
+
return fail(count, seq, 'bad_signature', 'run_end signature block is malformed', { lastHash, signature: { present: true, valid: false } });
|
|
105
|
+
}
|
|
106
|
+
const publicKey = sig['publicKey'];
|
|
107
|
+
let valid = false;
|
|
108
|
+
try {
|
|
109
|
+
const key = createPublicKey({ key: Buffer.from(publicKey, 'base64'), format: 'der', type: 'spki' });
|
|
110
|
+
valid = edVerify(null, Buffer.from(lastHash, 'utf-8'), key, Buffer.from(sig['sig'], 'base64'));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
valid = false;
|
|
114
|
+
}
|
|
115
|
+
if (!valid) {
|
|
116
|
+
return fail(count, seq, 'bad_signature', 'Signature does not verify against the last hash', {
|
|
117
|
+
lastHash,
|
|
118
|
+
signature: { present: true, valid: false, publicKey },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (options.expectedPublicKey !== undefined) {
|
|
122
|
+
let trusted;
|
|
123
|
+
try {
|
|
124
|
+
trusted = normalizePublicKey(options.expectedPublicKey);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return fail(count, seq, 'untrusted_key', 'The expected public key could not be parsed', { lastHash, signature: { present: true, valid: true, publicKey } });
|
|
128
|
+
}
|
|
129
|
+
if (trusted !== publicKey) {
|
|
130
|
+
return fail(count, seq, 'untrusted_key', 'Signature is valid but the signing key is not the expected key', {
|
|
131
|
+
lastHash,
|
|
132
|
+
signature: { present: true, valid: true, publicKey },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
eventCount: count,
|
|
139
|
+
message: `Chain intact, ${count} events, run complete, signature valid`,
|
|
140
|
+
signature: { present: true, valid: true, publicKey },
|
|
141
|
+
lastHash,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function verifyReceiptFile(path, options = {}) {
|
|
145
|
+
let text;
|
|
146
|
+
try {
|
|
147
|
+
text = readFileSync(path, 'utf-8');
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return fail(0, 0, 'unreadable', `Cannot read ${path}: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
151
|
+
}
|
|
152
|
+
return verifyReceiptText(text, options);
|
|
153
|
+
}
|
|
154
|
+
/** Parse events without verifying. For viewers; callers should verify separately. */
|
|
155
|
+
export function parseReceiptText(text) {
|
|
156
|
+
const events = [];
|
|
157
|
+
for (const line of text.split('\n')) {
|
|
158
|
+
if (line.trim() === '')
|
|
159
|
+
continue;
|
|
160
|
+
let parsed;
|
|
161
|
+
try {
|
|
162
|
+
parsed = JSON.parse(line);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (isRecord(parsed) && shapeProblem(parsed) === null && isKnownType(parsed['type'])) {
|
|
168
|
+
events.push(parsed);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return events;
|
|
172
|
+
}
|
|
173
|
+
function isKnownType(t) {
|
|
174
|
+
return typeof t === 'string' && RECEIPT_EVENT_TYPES.includes(t);
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=receipt-verify.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Receipt viewer: a terminal summary and a self-contained single-file HTML page
|
|
3
|
+
* (inline CSS and JS, no external assets). Every piece of receipt data is
|
|
4
|
+
* HTML-escaped, and a CSP meta tag blocks anything not inline.
|
|
5
|
+
*/
|
|
6
|
+
import type { AttemptEndData, FileChangeData, ReceiptEvent, RollbackData, RunEndData, RunStartData, ToolCallData, UsageData, VerifierResultData, VerifyResult } from './receipt-types.js';
|
|
7
|
+
export interface AttemptView {
|
|
8
|
+
attempt: number;
|
|
9
|
+
model?: string;
|
|
10
|
+
toolCalls: ToolCallData[];
|
|
11
|
+
verifier?: VerifierResultData;
|
|
12
|
+
usage?: UsageData;
|
|
13
|
+
rollbacks: RollbackData[];
|
|
14
|
+
end?: AttemptEndData;
|
|
15
|
+
}
|
|
16
|
+
export interface ReceiptModel {
|
|
17
|
+
start?: RunStartData;
|
|
18
|
+
baseline?: VerifierResultData;
|
|
19
|
+
attempts: AttemptView[];
|
|
20
|
+
files: FileChangeData[];
|
|
21
|
+
end?: RunEndData;
|
|
22
|
+
startedAt?: string;
|
|
23
|
+
endedAt?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function buildReceiptModel(events: ReceiptEvent[]): ReceiptModel;
|
|
26
|
+
export declare function renderTerminalSummary(events: ReceiptEvent[], verify?: VerifyResult): string;
|
|
27
|
+
export declare function escapeHtml(input: string): string;
|
|
28
|
+
export declare function renderHtml(events: ReceiptEvent[], verify?: VerifyResult): string;
|
|
29
|
+
//# sourceMappingURL=receipt-view.d.ts.map
|