@orcareplay/core 0.1.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/dist/blobs.d.ts +21 -0
- package/dist/blobs.d.ts.map +1 -0
- package/dist/blobs.js +97 -0
- package/dist/blobs.js.map +1 -0
- package/dist/graph.d.ts +106 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +393 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/paths.d.ts +45 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +115 -0
- package/dist/paths.js.map +1 -0
- package/dist/reader.d.ts +45 -0
- package/dist/reader.d.ts.map +1 -0
- package/dist/reader.js +152 -0
- package/dist/reader.js.map +1 -0
- package/dist/redaction.d.ts +48 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +245 -0
- package/dist/redaction.js.map +1 -0
- package/dist/writer.d.ts +66 -0
- package/dist/writer.d.ts.map +1 -0
- package/dist/writer.js +182 -0
- package/dist/writer.js.map +1 -0
- package/package.json +18 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
/** Bump when a rule is added or changed, so old traces stay interpretable. */
|
|
3
|
+
// Bumped when a rule's *name* or pattern changes, because both reach the trace: the placeholder is
|
|
4
|
+
// `<secret:<kind>:<hash>>`, and a reader comparing two traces needs to know the policy differed
|
|
5
|
+
// rather than the content. v2 renamed `openai_key` to `sk_api_key`; v3 stopped the entropy sweep
|
|
6
|
+
// eating protocol identifiers (`id`, `tool_use_id`, `tool_call_id`).
|
|
7
|
+
export const REDACTION_POLICY_VERSION = 3;
|
|
8
|
+
/** Environment capture is allowlist-only (spec §5). Everything else is denied. */
|
|
9
|
+
export const DEFAULT_ENV_ALLOWLIST = [
|
|
10
|
+
'TERM',
|
|
11
|
+
'LANG',
|
|
12
|
+
'LC_ALL',
|
|
13
|
+
'PATH',
|
|
14
|
+
'HOME',
|
|
15
|
+
'SHELL',
|
|
16
|
+
'TZ',
|
|
17
|
+
'USER',
|
|
18
|
+
'PWD',
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Request headers whose value is never written, whatever it looks like (spec §5).
|
|
22
|
+
*
|
|
23
|
+
* The single list. There were three — here, in the recording proxy, and in the TLS interceptor —
|
|
24
|
+
* and they had drifted: `api-key` and `x-goog-api-key` were known to exactly one of them, so the
|
|
25
|
+
* same Azure or Google credential was stripped on the intercepted path and written on the recorded
|
|
26
|
+
* one. Nothing about a header set that has to stay identical in three places will keep it that
|
|
27
|
+
* way, so it is defined once and imported.
|
|
28
|
+
*/
|
|
29
|
+
export const AUTH_REQUEST_HEADERS = [
|
|
30
|
+
'authorization',
|
|
31
|
+
'x-api-key',
|
|
32
|
+
// Azure OpenAI sends the key under its own name, and Google under another.
|
|
33
|
+
'api-key',
|
|
34
|
+
'x-goog-api-key',
|
|
35
|
+
'cookie',
|
|
36
|
+
'proxy-authorization',
|
|
37
|
+
];
|
|
38
|
+
/** A response can hand out credentials too. `set-cookie` is a session, not metadata. */
|
|
39
|
+
export const AUTH_RESPONSE_HEADERS = ['set-cookie'];
|
|
40
|
+
/** Every header name whose value is never written, in either direction. */
|
|
41
|
+
export const AUTH_HEADERS = [...AUTH_REQUEST_HEADERS, ...AUTH_RESPONSE_HEADERS];
|
|
42
|
+
/**
|
|
43
|
+
* Order matters: a PEM block or a JWT contains base64 that the narrower rules would otherwise
|
|
44
|
+
* chew into pieces, leaving fragments of key material on disk.
|
|
45
|
+
*/
|
|
46
|
+
const RULES = [
|
|
47
|
+
{
|
|
48
|
+
kind: 'private_key',
|
|
49
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g,
|
|
50
|
+
},
|
|
51
|
+
{ kind: 'jwt', pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
|
|
52
|
+
// `sk-` is not an OpenAI prefix, it is the convention half the industry copied: OpenAI's
|
|
53
|
+
// `sk-proj-`, Anthropic's `sk-ant-`, OrcaRouter's `sk-orca-` and a dozen gateways all match here.
|
|
54
|
+
// The rule name reaches the trace, `redactions.json` and the placeholder, so calling every one of
|
|
55
|
+
// them an OpenAI key told a reader something false about where their credential came from.
|
|
56
|
+
{ kind: 'sk_api_key', pattern: /sk-[A-Za-z0-9_-]{16,}/g },
|
|
57
|
+
// Every GitHub credential prefix, `ghr_` (refresh) included: it outlives the access token it
|
|
58
|
+
// renews, so leaving it to the entropy sweep would miss the longest-lived secret of the set.
|
|
59
|
+
{ kind: 'github_token', pattern: /gh[posur]_[A-Za-z0-9]{20,}/g },
|
|
60
|
+
{ kind: 'aws_access_key_id', pattern: /AKIA[0-9A-Z]{16}/g },
|
|
61
|
+
{ kind: 'slack_token', pattern: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
62
|
+
{ kind: 'google_api_key', pattern: /AIza[0-9A-Za-z_-]{35}/g },
|
|
63
|
+
];
|
|
64
|
+
const TOKEN = /[A-Za-z0-9_-]{20,}/g;
|
|
65
|
+
const PLACEHOLDER = /<secret:[a-z_]+:[0-9a-f]{8}>/g;
|
|
66
|
+
const MIN_ENTROPY_LENGTH = 20;
|
|
67
|
+
const ENTROPY_BITS_PER_CHAR = 4.0;
|
|
68
|
+
/** Shannon entropy of the token's own character distribution, in bits per character. */
|
|
69
|
+
function entropy(token) {
|
|
70
|
+
const freq = new Map();
|
|
71
|
+
for (const ch of token)
|
|
72
|
+
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
73
|
+
let bits = 0;
|
|
74
|
+
for (const n of freq.values()) {
|
|
75
|
+
const p = n / token.length;
|
|
76
|
+
bits -= p * Math.log2(p);
|
|
77
|
+
}
|
|
78
|
+
return bits;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A long identifier clears 4 bits/char on length alone: `getUserAuthenticationTokenFromRequest`
|
|
82
|
+
* scores 4.08. Agent traces are mostly source code, so redacting identifiers would corrupt the
|
|
83
|
+
* payloads the trace exists to preserve. Requiring both digits and letters keeps every random
|
|
84
|
+
* token (base64url has a digit with p≈0.98 at 20 chars) and drops prose-shaped ones.
|
|
85
|
+
*/
|
|
86
|
+
function looksRandom(token) {
|
|
87
|
+
return /[0-9]/.test(token) && /[A-Za-z]/.test(token);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Protocol identifiers, which are not secrets and must survive a recording verbatim.
|
|
91
|
+
*
|
|
92
|
+
* A `tool_use` id is generated per call, means nothing outside its own conversation, and the API
|
|
93
|
+
* requires it to match `^[a-zA-Z0-9_-]+$`. It is also ~25 characters of mixed-case base62, so the
|
|
94
|
+
* entropy sweep took it every time — and a fork replays recorded assistant turns, the agent echoes
|
|
95
|
+
* those ids back, and the placeholder then arrives at the live API, which rejects the request:
|
|
96
|
+
*
|
|
97
|
+
* messages.1.content.2.tool_use.id: String should match pattern '^[a-zA-Z0-9_-]+$'
|
|
98
|
+
*
|
|
99
|
+
* So `orca compare`, the feature the tool is pitched on, could not work against any real recording.
|
|
100
|
+
* Found by forking one, which no fixture could have shown.
|
|
101
|
+
*
|
|
102
|
+
* This shields these values from the *entropy heuristic* only. The pattern rules run first and are
|
|
103
|
+
* untouched, so a real credential parked under a key called `id` is still redacted by shape — what
|
|
104
|
+
* is relaxed is the guess, not the detection. The value must already look like a protocol id
|
|
105
|
+
* (`^[A-Za-z0-9_-]*$`, the shape the API demands); anything else under that key is left to the
|
|
106
|
+
* ordinary sweep.
|
|
107
|
+
*
|
|
108
|
+
* The quotes are matched as `\\*"` because a response body is stored as a *string* inside the
|
|
109
|
+
* event's JSON, so by the time the scanner sees it the text reads `\\"id\\":\\"toolu_…\\"`. A
|
|
110
|
+
* pattern that only accepted bare quotes matched nothing where it mattered and passed its own test,
|
|
111
|
+
* which is how the first version of this shipped and still broke every fork.
|
|
112
|
+
*/
|
|
113
|
+
const PROTOCOL_ID_VALUE = /(\\*")(?:id|tool_use_id|tool_call_id)\1\s*:\s*\1[A-Za-z0-9_-]*\1/g;
|
|
114
|
+
/**
|
|
115
|
+
* The same problem with a different alphabet. An Anthropic `thinking` block carries a `signature`
|
|
116
|
+
* over its own contents, and the API rejects a turn whose signature does not verify — so a fork,
|
|
117
|
+
* which replays recorded assistant turns for the agent to echo back, died on
|
|
118
|
+
* `messages.1.content.0: Invalid \`signature\` in \`thinking\` block`. It is base64, not an
|
|
119
|
+
* identifier, hence a second shape.
|
|
120
|
+
*
|
|
121
|
+
* It is not a credential either: it authenticates thinking text the trace already holds in full, so
|
|
122
|
+
* keeping it costs no secrecy that was not already spent.
|
|
123
|
+
*/
|
|
124
|
+
const PROTOCOL_SIGNATURE_VALUE = /(\\*")signature\1\s*:\s*\1[A-Za-z0-9+/=_-]*\1/g;
|
|
125
|
+
/** Regions the entropy sweep must not touch: what it already replaced, and what is not a secret. */
|
|
126
|
+
function spansOf(value) {
|
|
127
|
+
const spans = [];
|
|
128
|
+
for (const m of value.matchAll(PLACEHOLDER))
|
|
129
|
+
spans.push([m.index, m.index + m[0].length]);
|
|
130
|
+
for (const m of value.matchAll(PROTOCOL_ID_VALUE))
|
|
131
|
+
spans.push([m.index, m.index + m[0].length]);
|
|
132
|
+
for (const m of value.matchAll(PROTOCOL_SIGNATURE_VALUE)) {
|
|
133
|
+
spans.push([m.index, m.index + m[0].length]);
|
|
134
|
+
}
|
|
135
|
+
return spans;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Write-path redactor (spec §5).
|
|
139
|
+
*
|
|
140
|
+
* The placeholder is `<secret:kind:hash8>` where hash8 is sha256(salt + secret) truncated, so the
|
|
141
|
+
* same secret always yields the same placeholder — replay still matches structurally — while the
|
|
142
|
+
* secret itself is unrecoverable. The salt is per run and never written down, which is what stops
|
|
143
|
+
* a short secret from being brute-forced out of a published trace.
|
|
144
|
+
*/
|
|
145
|
+
export class Redactor {
|
|
146
|
+
#salt;
|
|
147
|
+
#envAllowlist;
|
|
148
|
+
#records = new Map();
|
|
149
|
+
constructor(opts = {}) {
|
|
150
|
+
this.#salt = opts.salt ?? randomBytes(16).toString('hex');
|
|
151
|
+
this.#envAllowlist = new Set(opts.envAllowlist ?? DEFAULT_ENV_ALLOWLIST);
|
|
152
|
+
}
|
|
153
|
+
redactString(s, context) {
|
|
154
|
+
const hits = new Map();
|
|
155
|
+
const value = this.#scan(s, context, hits);
|
|
156
|
+
this.#merge(hits);
|
|
157
|
+
return { value, hits: [...hits.values()] };
|
|
158
|
+
}
|
|
159
|
+
redactHeaders(headers) {
|
|
160
|
+
const hits = new Map();
|
|
161
|
+
const value = {};
|
|
162
|
+
for (const [name, raw] of Object.entries(headers)) {
|
|
163
|
+
const lower = name.toLowerCase();
|
|
164
|
+
const context = `header:${lower}`;
|
|
165
|
+
value[name] = AUTH_HEADERS.includes(lower)
|
|
166
|
+
? this.#hit(`header_${lower.replace(/-/g, '_')}`, raw, context, hits)
|
|
167
|
+
: this.#scan(raw, context, hits);
|
|
168
|
+
}
|
|
169
|
+
this.#merge(hits);
|
|
170
|
+
return { value, hits: [...hits.values()] };
|
|
171
|
+
}
|
|
172
|
+
redactEnv(env) {
|
|
173
|
+
const hits = new Map();
|
|
174
|
+
const out = {};
|
|
175
|
+
for (const key of this.#envAllowlist) {
|
|
176
|
+
const raw = env[key];
|
|
177
|
+
if (raw === undefined)
|
|
178
|
+
continue;
|
|
179
|
+
out[key] = this.#scan(raw, `env:${key}`, hits);
|
|
180
|
+
}
|
|
181
|
+
this.#merge(hits);
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
/** Every removal so far, aggregated by rule and identifier. Never contains a value. */
|
|
185
|
+
records() {
|
|
186
|
+
return [...this.#records.values()].map((r) => ({ ...r }));
|
|
187
|
+
}
|
|
188
|
+
rulesFired() {
|
|
189
|
+
const out = {};
|
|
190
|
+
for (const r of this.#records.values())
|
|
191
|
+
out[r.rule] = (out[r.rule] ?? 0) + r.count;
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
#scan(input, context, hits) {
|
|
195
|
+
let value = input;
|
|
196
|
+
for (const rule of RULES) {
|
|
197
|
+
value = value.replace(rule.pattern, (m) => this.#hit(rule.kind, m, context, hits));
|
|
198
|
+
}
|
|
199
|
+
return this.#scanEntropy(value, context, hits);
|
|
200
|
+
}
|
|
201
|
+
#scanEntropy(value, context, hits) {
|
|
202
|
+
// Placeholders already written by the pattern rules must not be rescanned as tokens.
|
|
203
|
+
const spans = spansOf(value);
|
|
204
|
+
let out = '';
|
|
205
|
+
let cut = 0;
|
|
206
|
+
for (const m of value.matchAll(TOKEN)) {
|
|
207
|
+
const token = m[0];
|
|
208
|
+
const start = m.index;
|
|
209
|
+
if (spans.some(([a, b]) => start >= a && start < b))
|
|
210
|
+
continue;
|
|
211
|
+
if (token.length < MIN_ENTROPY_LENGTH || !looksRandom(token))
|
|
212
|
+
continue;
|
|
213
|
+
if (entropy(token) <= ENTROPY_BITS_PER_CHAR)
|
|
214
|
+
continue;
|
|
215
|
+
out += value.slice(cut, start) + this.#hit('high_entropy', token, context, hits);
|
|
216
|
+
cut = start + token.length;
|
|
217
|
+
}
|
|
218
|
+
return out + value.slice(cut);
|
|
219
|
+
}
|
|
220
|
+
#hit(kind, secret, context, hits) {
|
|
221
|
+
const hash8 = createHash('sha256').update(this.#salt).update(secret).digest('hex').slice(0, 8);
|
|
222
|
+
const placeholder = `<secret:${kind}:${hash8}>`;
|
|
223
|
+
const identifier = context ? `${context}:${hash8}` : hash8;
|
|
224
|
+
// `\0` as an escape, not a raw NUL byte: a literal NUL in the source makes git classify
|
|
225
|
+
// this file as binary, so every diff of the redactor — the one file that most needs reading in
|
|
226
|
+
// review — comes out as "Bin 7248 -> 7441 bytes" instead of lines.
|
|
227
|
+
const key = `${kind}\0${identifier}`;
|
|
228
|
+
const seen = hits.get(key);
|
|
229
|
+
if (seen)
|
|
230
|
+
seen.count += 1;
|
|
231
|
+
else
|
|
232
|
+
hits.set(key, { rule: kind, identifier, placeholder, count: 1 });
|
|
233
|
+
return placeholder;
|
|
234
|
+
}
|
|
235
|
+
#merge(hits) {
|
|
236
|
+
for (const [key, hit] of hits) {
|
|
237
|
+
const seen = this.#records.get(key);
|
|
238
|
+
if (seen)
|
|
239
|
+
seen.count += hit.count;
|
|
240
|
+
else
|
|
241
|
+
this.#records.set(key, { ...hit });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=redaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redaction.js","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"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"}
|
package/dist/writer.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Actor, AdapterInfo, EventType, Manifest, Payload, TraceEvent } from '@orcareplay/schema';
|
|
2
|
+
export interface TraceWriterInit {
|
|
3
|
+
/** Defaults to a fresh `run_<12 hex>`. */
|
|
4
|
+
runId?: string;
|
|
5
|
+
adapter: AdapterInfo;
|
|
6
|
+
argv: string[];
|
|
7
|
+
cwd: string;
|
|
8
|
+
orcaVersion: string;
|
|
9
|
+
envAllowlist?: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Fork provenance (spec §1). Set on a run produced by forking another. The manifest is what
|
|
12
|
+
* every out-of-process reader sees first — `orca gc` deciding whether a run may be deleted, a
|
|
13
|
+
* third-party tool, the Python SDK — so recording this only as an event leaves those readers
|
|
14
|
+
* looking at an orphan.
|
|
15
|
+
*/
|
|
16
|
+
parentRun?: string;
|
|
17
|
+
forkPoint?: number;
|
|
18
|
+
forkModel?: string;
|
|
19
|
+
}
|
|
20
|
+
/** The parts of an event a caller supplies; the writer stamps the rest. */
|
|
21
|
+
export interface EventInit {
|
|
22
|
+
type: EventType;
|
|
23
|
+
actor: Actor;
|
|
24
|
+
turn?: number;
|
|
25
|
+
causes?: number[];
|
|
26
|
+
attrs?: Record<string, unknown>;
|
|
27
|
+
payload?: Payload;
|
|
28
|
+
/**
|
|
29
|
+
* When the event actually happened, for anything captured out of band.
|
|
30
|
+
*
|
|
31
|
+
* Shell and MCP frames are drained from disk after the agent exits, so stamping them at write
|
|
32
|
+
* time puts every one of them at the end of the run — `ts` and `mono_us` describing the drain
|
|
33
|
+
* rather than the event, which spec §2.1 makes authoritative for duration. Supplying the real
|
|
34
|
+
* moment is what lets them interleave with the model turns they happened between.
|
|
35
|
+
*/
|
|
36
|
+
occurredAt?: Date;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Append-only writer for one run (spec §2).
|
|
40
|
+
*
|
|
41
|
+
* Every event takes the same path to disk — spill, redact, validate, append — because a redactor
|
|
42
|
+
* that can be bypassed by one caller is not a redactor. Serialized bytes are scrubbed rather than
|
|
43
|
+
* individual fields, so a secret hiding in a key, a nested string, or an attrs value is caught
|
|
44
|
+
* regardless of shape.
|
|
45
|
+
*/
|
|
46
|
+
export declare class TraceWriter {
|
|
47
|
+
#private;
|
|
48
|
+
private constructor();
|
|
49
|
+
static create(runsDir: string, init: TraceWriterInit): Promise<TraceWriter>;
|
|
50
|
+
/**
|
|
51
|
+
* Record the repository state the run started from (spec §1).
|
|
52
|
+
*
|
|
53
|
+
* A setter rather than a constructor argument because the honest answer needs the run directory
|
|
54
|
+
* to exist: the dirty check has to exclude orca's own files, or every run in a clean checkout
|
|
55
|
+
* reports dirty. So the caller reads it once the capture layer is up and hands it back, and it
|
|
56
|
+
* lands in the manifest at close alongside the counts.
|
|
57
|
+
*/
|
|
58
|
+
setGit(info: Manifest['git']): void;
|
|
59
|
+
get runDir(): string;
|
|
60
|
+
get runId(): string;
|
|
61
|
+
/** The seq the next event will be given, which is also the number written so far. */
|
|
62
|
+
get seq(): number;
|
|
63
|
+
append(init: EventInit): Promise<TraceEvent>;
|
|
64
|
+
close(exitCode?: number): Promise<Manifest>;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../src/writer.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,KAAK,EACL,WAAW,EACX,SAAS,EACT,QAAQ,EACR,OAAO,EACP,UAAU,EACX,MAAM,oBAAoB,CAAC;AAc5B,MAAM,WAAW,eAAe;IAC9B,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,2EAA2E;AAC3E,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,IAAI,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,qBAAa,WAAW;;IAgBtB,OAAO;WAgBM,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC;IAqCjF;;;;;;;OAOG;IACH,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,IAAI;IAInC,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,qFAAqF;IACrF,IAAI,GAAG,IAAI,MAAM,CAEhB;IAEK,MAAM,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAmB5C,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;CAuElD"}
|
package/dist/writer.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { arch, platform } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { INLINE_PAYLOAD_LIMIT, RUN_ID_PATTERN, SCHEMA_VERSION, assertEvent, assertManifest, } from '@orcareplay/schema';
|
|
6
|
+
import { BlobStore, sha256File } from './blobs.js';
|
|
7
|
+
import { REDACTION_POLICY_VERSION, Redactor } from './redaction.js';
|
|
8
|
+
const FILE_MODE = 0o600;
|
|
9
|
+
const DIR_MODE = 0o700;
|
|
10
|
+
/**
|
|
11
|
+
* Append-only writer for one run (spec §2).
|
|
12
|
+
*
|
|
13
|
+
* Every event takes the same path to disk — spill, redact, validate, append — because a redactor
|
|
14
|
+
* that can be bypassed by one caller is not a redactor. Serialized bytes are scrubbed rather than
|
|
15
|
+
* individual fields, so a secret hiding in a key, a nested string, or an attrs value is caught
|
|
16
|
+
* regardless of shape.
|
|
17
|
+
*/
|
|
18
|
+
export class TraceWriter {
|
|
19
|
+
#runDir;
|
|
20
|
+
#runId;
|
|
21
|
+
#eventsPath;
|
|
22
|
+
#blobs;
|
|
23
|
+
#redactor;
|
|
24
|
+
#handle;
|
|
25
|
+
#base;
|
|
26
|
+
#started = process.hrtime.bigint();
|
|
27
|
+
#startedAtMs = Date.now();
|
|
28
|
+
#seq = 0;
|
|
29
|
+
#turn = 0;
|
|
30
|
+
#git;
|
|
31
|
+
#tail = Promise.resolve();
|
|
32
|
+
#sealed;
|
|
33
|
+
constructor(runId, runDir, base, redactor, handle) {
|
|
34
|
+
this.#runId = runId;
|
|
35
|
+
this.#runDir = runDir;
|
|
36
|
+
this.#eventsPath = join(runDir, 'events.jsonl');
|
|
37
|
+
this.#blobs = new BlobStore(join(runDir, 'blobs'));
|
|
38
|
+
this.#redactor = redactor;
|
|
39
|
+
this.#handle = handle;
|
|
40
|
+
this.#base = base;
|
|
41
|
+
}
|
|
42
|
+
static async create(runsDir, init) {
|
|
43
|
+
const runId = init.runId ?? `run_${randomBytes(6).toString('hex')}`;
|
|
44
|
+
if (!RUN_ID_PATTERN.test(runId))
|
|
45
|
+
throw new Error(`not a valid run id: ${JSON.stringify(runId)}`);
|
|
46
|
+
const runDir = join(runsDir, runId);
|
|
47
|
+
await mkdir(runDir, { recursive: true, mode: DIR_MODE });
|
|
48
|
+
// One redactor for the whole run: the salt stays constant, so a secret seen in the environment
|
|
49
|
+
// and again in a payload gets the same placeholder, and every removal lands in redactions.json.
|
|
50
|
+
const redactor = new Redactor(init.envAllowlist === undefined ? {} : { envAllowlist: init.envAllowlist });
|
|
51
|
+
const base = {
|
|
52
|
+
schema_version: SCHEMA_VERSION,
|
|
53
|
+
run_id: runId,
|
|
54
|
+
created_at: new Date().toISOString(),
|
|
55
|
+
orca_version: init.orcaVersion,
|
|
56
|
+
adapter: init.adapter,
|
|
57
|
+
argv: init.argv,
|
|
58
|
+
cwd: init.cwd,
|
|
59
|
+
env_allowlisted: redactor.redactEnv(process.env),
|
|
60
|
+
platform: { os: platform(), arch: arch(), node: process.version },
|
|
61
|
+
// Spread conditionally: the schema forbids unknown keys and a plain recording must carry no
|
|
62
|
+
// provenance at all, rather than three nulls that readers would have to special-case.
|
|
63
|
+
...(init.parentRun === undefined ? {} : { parent_run: init.parentRun }),
|
|
64
|
+
...(init.forkPoint === undefined ? {} : { fork_point: init.forkPoint }),
|
|
65
|
+
...(init.forkModel === undefined ? {} : { fork_model: init.forkModel }),
|
|
66
|
+
};
|
|
67
|
+
assertManifest(base);
|
|
68
|
+
await writeFile(join(runDir, 'manifest.json'), `${JSON.stringify(base, null, 2)}\n`, {
|
|
69
|
+
mode: FILE_MODE,
|
|
70
|
+
});
|
|
71
|
+
const handle = await open(join(runDir, 'events.jsonl'), 'a', FILE_MODE);
|
|
72
|
+
return new TraceWriter(runId, runDir, base, redactor, handle);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Record the repository state the run started from (spec §1).
|
|
76
|
+
*
|
|
77
|
+
* A setter rather than a constructor argument because the honest answer needs the run directory
|
|
78
|
+
* to exist: the dirty check has to exclude orca's own files, or every run in a clean checkout
|
|
79
|
+
* reports dirty. So the caller reads it once the capture layer is up and hands it back, and it
|
|
80
|
+
* lands in the manifest at close alongside the counts.
|
|
81
|
+
*/
|
|
82
|
+
setGit(info) {
|
|
83
|
+
this.#git = info;
|
|
84
|
+
}
|
|
85
|
+
get runDir() {
|
|
86
|
+
return this.#runDir;
|
|
87
|
+
}
|
|
88
|
+
get runId() {
|
|
89
|
+
return this.#runId;
|
|
90
|
+
}
|
|
91
|
+
/** The seq the next event will be given, which is also the number written so far. */
|
|
92
|
+
get seq() {
|
|
93
|
+
return this.#seq;
|
|
94
|
+
}
|
|
95
|
+
async append(init) {
|
|
96
|
+
if (this.#sealed)
|
|
97
|
+
throw new Error(`run ${this.#runId} is closed`);
|
|
98
|
+
// Clocks are read at call time so they describe the event, not the queue drain; seq is
|
|
99
|
+
// assigned inside the queue so a rejected event leaves no hole in the dense order.
|
|
100
|
+
const when = init.occurredAt ?? new Date();
|
|
101
|
+
const ts = when.toISOString();
|
|
102
|
+
// Monotonic for events stamped now; derived from the wall clock for one that happened earlier,
|
|
103
|
+
// since there is no hrtime reading to recover. Clamped at zero rather than going negative for a
|
|
104
|
+
// frame whose clock ran slightly behind the run's start.
|
|
105
|
+
const mono_us = init.occurredAt
|
|
106
|
+
? Math.max(0, (when.getTime() - this.#startedAtMs) * 1000)
|
|
107
|
+
: Number((process.hrtime.bigint() - this.#started) / 1000n);
|
|
108
|
+
const turn = init.turn ?? this.#turn;
|
|
109
|
+
this.#turn = turn;
|
|
110
|
+
const written = this.#tail.then(() => this.#write(ts, mono_us, turn, init));
|
|
111
|
+
this.#tail = written.catch(() => undefined);
|
|
112
|
+
return written;
|
|
113
|
+
}
|
|
114
|
+
async close(exitCode) {
|
|
115
|
+
if (this.#sealed)
|
|
116
|
+
return this.#sealed;
|
|
117
|
+
await this.#tail;
|
|
118
|
+
await this.#handle.sync();
|
|
119
|
+
await this.#handle.close();
|
|
120
|
+
const blobs = await this.#blobs.count();
|
|
121
|
+
const manifest = {
|
|
122
|
+
...this.#base,
|
|
123
|
+
ended_at: new Date().toISOString(),
|
|
124
|
+
counts: { events: this.#seq, blobs },
|
|
125
|
+
redaction: {
|
|
126
|
+
policy_version: REDACTION_POLICY_VERSION,
|
|
127
|
+
rules_fired: this.#redactor.rulesFired(),
|
|
128
|
+
},
|
|
129
|
+
integrity: { events_sha256: await sha256File(this.#eventsPath), blob_count: blobs },
|
|
130
|
+
// Conditional for the same reason the fork keys are: the schema forbids unknown keys, and a
|
|
131
|
+
// run outside a repository should carry no git block rather than three empty fields.
|
|
132
|
+
...(this.#git === undefined || Object.keys(this.#git).length === 0 ? {} : { git: this.#git }),
|
|
133
|
+
};
|
|
134
|
+
if (exitCode !== undefined)
|
|
135
|
+
manifest.exit_code = exitCode;
|
|
136
|
+
assertManifest(manifest);
|
|
137
|
+
const redactions = {
|
|
138
|
+
policy_version: REDACTION_POLICY_VERSION,
|
|
139
|
+
records: this.#redactor.records(),
|
|
140
|
+
};
|
|
141
|
+
await writeFile(join(this.#runDir, 'redactions.json'), `${JSON.stringify(redactions, null, 2)}\n`, {
|
|
142
|
+
mode: FILE_MODE,
|
|
143
|
+
});
|
|
144
|
+
await writeFile(join(this.#runDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
145
|
+
mode: FILE_MODE,
|
|
146
|
+
});
|
|
147
|
+
this.#sealed = manifest;
|
|
148
|
+
return manifest;
|
|
149
|
+
}
|
|
150
|
+
async #write(ts, mono_us, turn, init) {
|
|
151
|
+
const seq = this.#seq;
|
|
152
|
+
const removed = [];
|
|
153
|
+
let payload = init.payload;
|
|
154
|
+
if (payload !== undefined) {
|
|
155
|
+
const json = JSON.stringify(payload);
|
|
156
|
+
if (Buffer.byteLength(json) > INLINE_PAYLOAD_LIMIT) {
|
|
157
|
+
const scrubbed = this.#redactor.redactString(json, `event:${seq}:payload`);
|
|
158
|
+
removed.push(...scrubbed.hits.map((h) => h.identifier));
|
|
159
|
+
payload = await this.#blobs.put(scrubbed.value, 'application/json');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const event = { seq, ts, mono_us, turn, type: init.type, actor: init.actor };
|
|
163
|
+
if (init.causes !== undefined)
|
|
164
|
+
event.causes = init.causes;
|
|
165
|
+
if (init.attrs !== undefined)
|
|
166
|
+
event.attrs = init.attrs;
|
|
167
|
+
if (payload !== undefined)
|
|
168
|
+
event.payload = payload;
|
|
169
|
+
// Placeholders contain no quote or backslash, so scrubbing the serialized line cannot make it
|
|
170
|
+
// unparseable — and if it ever did, the parse throws and the event is never written.
|
|
171
|
+
const scrubbed = this.#redactor.redactString(JSON.stringify(event), `event:${seq}`);
|
|
172
|
+
removed.push(...scrubbed.hits.map((h) => h.identifier));
|
|
173
|
+
const out = JSON.parse(scrubbed.value);
|
|
174
|
+
if (removed.length > 0)
|
|
175
|
+
out.redacted = [...new Set(removed)];
|
|
176
|
+
assertEvent(out);
|
|
177
|
+
await this.#handle.write(`${JSON.stringify(out)}\n`);
|
|
178
|
+
this.#seq = seq + 1;
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.js","sourceRoot":"","sources":["../src/writer.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orcareplay/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@orcareplay/plugin-api": "0.1.0",
|
|
16
|
+
"@orcareplay/schema": "0.1.0"
|
|
17
|
+
}
|
|
18
|
+
}
|