@sweny-ai/core 0.1.172 → 0.1.173
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/agent-env.js +6 -1
- package/dist/budget.d.ts +8 -0
- package/dist/budget.js +11 -0
- package/dist/cli/final-output.d.ts +26 -6
- package/dist/cli/final-output.js +64 -15
- package/dist/cli/main.js +12 -4
- package/dist/cli/resume.js +10 -2
- package/dist/executor.js +42 -4
- package/dist/harness/sandbox-wrapper.js +9 -1
- package/dist/journal.d.ts +114 -17
- package/dist/journal.js +463 -36
- package/dist/schema.d.ts +97 -97
- package/package.json +1 -1
package/dist/journal.d.ts
CHANGED
|
@@ -2,10 +2,23 @@
|
|
|
2
2
|
* Run journal (#363): an append-only log of one workflow run, enough to resume
|
|
3
3
|
* a killed or failed run without re-running the nodes that already finished.
|
|
4
4
|
*
|
|
5
|
-
* File: `.sweny/runs/<run-id>/journal.ndjson
|
|
6
|
-
* record carries the format version (`v`), a sequence number (`seq`)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* File: `.sweny/runs/<run-id>/journal.ndjson` (mode 0600), one JSON record per
|
|
6
|
+
* line. Every record carries the format version (`v`), a sequence number (`seq`)
|
|
7
|
+
* and an authentication code (`h`, HMAC-SHA256 of the record without `h`, under
|
|
8
|
+
* a per-run key). Each append is written and fsync'd before the executor moves
|
|
9
|
+
* on, so a record on disk is a fact.
|
|
10
|
+
*
|
|
11
|
+
* Integrity: the key is 32 random bytes made when the run starts and kept
|
|
12
|
+
* OUTSIDE the workspace, in the user's sweny state dir
|
|
13
|
+
* (`$SWENY_STATE_DIR`, else `$XDG_STATE_HOME/sweny`, else
|
|
14
|
+
* `~/.local/state/sweny`), under `run-keys/` (dir 0700, file 0600). Anything
|
|
15
|
+
* that can write the workspace but not that dir (an agent confined to the
|
|
16
|
+
* workspace, a checked-out branch, an artifact) cannot mint a record resume
|
|
17
|
+
* accepts. Both sandboxes deny reading the key dir; an unsandboxed agent
|
|
18
|
+
* running as the same OS user can still read it, which is why resume also
|
|
19
|
+
* checks the record sequence (see {@link buildResumePlan}) and every replayed
|
|
20
|
+
* route against the workflow's real edges. A resume on another machine needs
|
|
21
|
+
* the same state dir.
|
|
9
22
|
*
|
|
10
23
|
* Records, in run order:
|
|
11
24
|
* run:start workflow / instruction / input / tool hashes, harness id, input (redacted)
|
|
@@ -17,8 +30,12 @@
|
|
|
17
30
|
* output:applied the write returned (key + the ids it produced)
|
|
18
31
|
* node:end final result of the visit and the write-stage counters
|
|
19
32
|
* route the edge taken after the visit (null = the run ended)
|
|
33
|
+
* usage spend of one agent attempt (cumulative; live while it runs, final when it returns)
|
|
20
34
|
* run:end success | failed | crashed
|
|
21
35
|
*
|
|
36
|
+
* Spend: a resume seeds the run budget with every attempt's journaled usage,
|
|
37
|
+
* so a crash never resets the whole-run ceiling.
|
|
38
|
+
*
|
|
22
39
|
* Recovery: a torn or corrupt tail (power loss mid-append) is truncated back to
|
|
23
40
|
* the last valid record. Corruption in the middle of the file is refused.
|
|
24
41
|
*
|
|
@@ -35,9 +52,11 @@
|
|
|
35
52
|
* (the process died between the API call and the receipt) is looked up on the
|
|
36
53
|
* provider by a marker embedded in the body before anything is re-sent.
|
|
37
54
|
*/
|
|
38
|
-
import type { Logger, NodeResult, Skill, Workflow } from "./types.js";
|
|
55
|
+
import type { Logger, NodeResult, NodeUsage, Skill, Workflow } from "./types.js";
|
|
39
56
|
import type { SafeOutputIntent, WriteStageState } from "./safe-outputs.js";
|
|
40
|
-
|
|
57
|
+
import { type Spend } from "./budget.js";
|
|
58
|
+
/** v2: records are authenticated with a per-run HMAC key (v1 used a public checksum and is refused). */
|
|
59
|
+
export declare const JOURNAL_SCHEMA_VERSION = 2;
|
|
41
60
|
export declare const JOURNAL_DIR: string;
|
|
42
61
|
export declare const JOURNAL_FILE = "journal.ndjson";
|
|
43
62
|
/** Journals kept on disk; the oldest beyond this are deleted when a new run starts. */
|
|
@@ -55,12 +74,39 @@ export declare function workflowHashOf(workflow: Workflow): string;
|
|
|
55
74
|
export declare function instructionHash(sources: Record<string, {
|
|
56
75
|
content: string;
|
|
57
76
|
}>): string;
|
|
58
|
-
/**
|
|
59
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Fingerprint of everything that decides what the run's tools do: per skill its
|
|
79
|
+
* id, name, description, category, instruction, config field names and env var
|
|
80
|
+
* NAMES, every tool's name, description, input schema, access class and a
|
|
81
|
+
* digest of its handler source, the MCP server's type, command, args, url,
|
|
82
|
+
* env var NAMES and header NAMES, and its tool aliases; plus the sweny version
|
|
83
|
+
* (which pins built-in skill code) and the harness. Secret values never enter
|
|
84
|
+
* it: MCP env and header values are dropped, and args are redacted first, so
|
|
85
|
+
* rotating a token neither changes the fingerprint nor leaves a hash of it.
|
|
86
|
+
*/
|
|
87
|
+
export declare function toolsHash(skills: Map<string, Skill>, swenyVersion?: string, harnessId?: string, secrets?: string[]): string;
|
|
88
|
+
/** sweny's per-user state dir: `$SWENY_STATE_DIR`, else `$XDG_STATE_HOME/sweny`, else `~/.local/state/sweny`. */
|
|
89
|
+
export declare function swenyStateDir(env?: Record<string, string | undefined>): string;
|
|
90
|
+
/** Where run keys live. Never inside the workspace. */
|
|
91
|
+
export declare function runKeyDir(env?: Record<string, string | undefined>): string;
|
|
92
|
+
/** The key file for one run in one workspace (the workspace path is hashed, not stored). */
|
|
93
|
+
export declare function runKeyFile(cwd: string, runId: string, keyDir?: string): string;
|
|
94
|
+
/** Thrown when a journal's key is missing or unreadable: its records cannot be checked. */
|
|
95
|
+
export declare class JournalKeyError extends Error {
|
|
96
|
+
constructor(file: string, why: string);
|
|
97
|
+
}
|
|
98
|
+
/** Read a run key. Throws {@link JournalKeyError}. */
|
|
99
|
+
export declare function loadRunKey(file: string): Buffer;
|
|
60
100
|
/** True for keys like `GITHUB_TOKEN`, `apiKey`, `client-secret`, `Authorization`. */
|
|
61
101
|
export declare function isSecretKey(key: string): boolean;
|
|
62
102
|
/** Values of secret-named env vars / config entries, longest first (8+ chars, so short words never match). */
|
|
63
103
|
export declare function collectSecretValues(...maps: Array<Record<string, string | undefined> | undefined>): string[];
|
|
104
|
+
/**
|
|
105
|
+
* The secret values a run can see: secret-named env vars, plus skill config
|
|
106
|
+
* fields (resolved from their env vars) whose config key looks secret. The
|
|
107
|
+
* same set the journal redacts, for anything else that prints node output.
|
|
108
|
+
*/
|
|
109
|
+
export declare function runSecretValues(skills: Iterable<Skill> | undefined, env?: Record<string, string | undefined>): string[];
|
|
64
110
|
/**
|
|
65
111
|
* Deep copy of `value` with secrets replaced by `[redacted]`: string values
|
|
66
112
|
* under secret-looking keys, known token shapes, and any occurrence of a value
|
|
@@ -121,10 +167,14 @@ interface OutputRecord extends JournalRecord {
|
|
|
121
167
|
output?: unknown;
|
|
122
168
|
recovered?: boolean;
|
|
123
169
|
}
|
|
124
|
-
/** Thrown when the journal was written by a newer sweny. */
|
|
170
|
+
/** Thrown when the journal was written by a newer sweny, or by an older one without record authentication. */
|
|
125
171
|
export declare class JournalVersionError extends Error {
|
|
126
172
|
constructor(version: unknown);
|
|
127
173
|
}
|
|
174
|
+
/** Thrown when the journal's records describe something the executor never writes: edited, spliced or forged. */
|
|
175
|
+
export declare class JournalIntegrityError extends Error {
|
|
176
|
+
constructor(message: string);
|
|
177
|
+
}
|
|
128
178
|
/** Thrown at resume when what the run depends on changed since it was journaled (and `force` is off). */
|
|
129
179
|
export declare class JournalMismatchError extends Error {
|
|
130
180
|
readonly what: string[];
|
|
@@ -139,16 +189,25 @@ export interface JournalRead {
|
|
|
139
189
|
records: JournalRecord[];
|
|
140
190
|
/** Bytes dropped from a torn / corrupt tail (0 when the file was clean). */
|
|
141
191
|
truncatedBytes: number;
|
|
142
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* Set when an invalid record sits before valid ones (not a torn tail), or when
|
|
194
|
+
* a whole record fails authentication (tampering, never a torn write). Not repairable.
|
|
195
|
+
*/
|
|
143
196
|
corruptAtLine?: number;
|
|
197
|
+
/** The line whose record failed authentication, when that is why the journal is refused. */
|
|
198
|
+
forgedAtLine?: number;
|
|
144
199
|
}
|
|
145
200
|
/**
|
|
146
|
-
* Read a journal. With `repair`, a torn
|
|
147
|
-
*
|
|
148
|
-
*
|
|
201
|
+
* Read a journal. With `repair`, a torn tail is truncated on disk back to the
|
|
202
|
+
* last valid record. Records are authenticated with the run's key (`key`, else
|
|
203
|
+
* loaded from the state dir for the run the path names). Throws
|
|
204
|
+
* {@link JournalVersionError} for another format and {@link JournalKeyError}
|
|
205
|
+
* when the key is missing.
|
|
149
206
|
*/
|
|
150
207
|
export declare function readJournal(file: string, opts?: {
|
|
151
208
|
repair?: boolean;
|
|
209
|
+
key?: Buffer;
|
|
210
|
+
keyDir?: string;
|
|
152
211
|
}): JournalRead;
|
|
153
212
|
export type VisitAction = "replay" | "write-stage" | "rerun";
|
|
154
213
|
export interface PlannedVisit {
|
|
@@ -182,8 +241,25 @@ export interface ResumePlan {
|
|
|
182
241
|
ends: Map<string, EndRecord>;
|
|
183
242
|
intents: Map<string, OutputRecord>;
|
|
184
243
|
receipts: Map<string, OutputRecord>;
|
|
244
|
+
/** Spend every earlier attempt journaled: the run budget starts here on resume. */
|
|
245
|
+
priorSpend: Spend;
|
|
185
246
|
}
|
|
186
|
-
/**
|
|
247
|
+
/**
|
|
248
|
+
* Check that the records are a sequence the executor can write: one run:start
|
|
249
|
+
* first; nothing but run:resume after run:end; a visit starts only where the
|
|
250
|
+
* last route pointed (or the entry), or re-starts an unfinished or failed
|
|
251
|
+
* visit after a resume; checkpoints, writes, usage and node:end only inside
|
|
252
|
+
* the open visit; one route per finished visit, from that visit's node.
|
|
253
|
+
* Throws {@link JournalIntegrityError}.
|
|
254
|
+
*/
|
|
255
|
+
export declare function checkRecordSequence(records: JournalRecord[]): void;
|
|
256
|
+
/**
|
|
257
|
+
* Check a journal against the workflow it is resumed with: every visited node
|
|
258
|
+
* exists, and every journaled route is a real edge that its `max_iterations`
|
|
259
|
+
* still allowed. Returns the first problem, or undefined.
|
|
260
|
+
*/
|
|
261
|
+
export declare function checkJournalAgainstWorkflow(records: JournalRecord[], workflow: Workflow): string | undefined;
|
|
262
|
+
/** Work out what a resume replays and where it picks up. Throws {@link JournalIntegrityError} for an impossible sequence. */
|
|
187
263
|
export declare function buildResumePlan(records: JournalRecord[]): ResumePlan;
|
|
188
264
|
/**
|
|
189
265
|
* Nodes a resume would run again that may repeat a write outside safe outputs:
|
|
@@ -233,7 +309,16 @@ export interface ExecutionJournal {
|
|
|
233
309
|
wrapWrites(node: string, iteration: number, skills: Map<string, Skill>): Map<string, Skill>;
|
|
234
310
|
nodeEnd(node: string, iteration: number, result: NodeResult, writeState: WriteStageState): void;
|
|
235
311
|
route(from: string, to: string | null): void;
|
|
312
|
+
/**
|
|
313
|
+
* Spend of one agent attempt, cumulative for the attempt: `final` when the
|
|
314
|
+
* attempt returned, otherwise a live report (the journal may throttle those).
|
|
315
|
+
*/
|
|
316
|
+
usage?(node: string, iteration: number, attempt: number, usage: NodeUsage, final: boolean): void;
|
|
317
|
+
/** Spend earlier attempts of this run already journaled (resume); the run budget starts from it. */
|
|
318
|
+
priorSpend?(): Spend | undefined;
|
|
236
319
|
}
|
|
320
|
+
/** Minimum gap between two live usage records of one attempt. Final reports are always written. */
|
|
321
|
+
export declare const USAGE_JOURNAL_INTERVAL_MS = 2000;
|
|
237
322
|
/** Test seam: called before each append; a throw simulates the process dying there. */
|
|
238
323
|
export interface JournalFaults {
|
|
239
324
|
beforeAppend?(record: JournalRecord): void;
|
|
@@ -251,6 +336,8 @@ export interface RunJournalOptions {
|
|
|
251
336
|
faults?: JournalFaults;
|
|
252
337
|
/** Journals kept on disk (default {@link JOURNAL_KEEP}). */
|
|
253
338
|
keep?: number;
|
|
339
|
+
/** Where run keys live (default {@link runKeyDir}). Never inside the workspace. */
|
|
340
|
+
keyDir?: string;
|
|
254
341
|
}
|
|
255
342
|
export interface ResumeJournalOptions extends RunJournalOptions {
|
|
256
343
|
read: JournalRead;
|
|
@@ -265,8 +352,8 @@ export declare function journalDir(cwd: string, runId: string): string;
|
|
|
265
352
|
export declare function listJournalRuns(cwd?: string): string[];
|
|
266
353
|
/** Exact run id, or a unique prefix. */
|
|
267
354
|
export declare function findJournalRun(ref: string, cwd?: string): string | undefined;
|
|
268
|
-
/** Delete the oldest journals beyond `keep`, never `except`. Never throws. */
|
|
269
|
-
export declare function pruneJournals(cwd: string, keep?: number, except?: string): number;
|
|
355
|
+
/** Delete the oldest journals (and their keys) beyond `keep`, never `except`. Never throws. */
|
|
356
|
+
export declare function pruneJournals(cwd: string, keep?: number, except?: string, keyDir?: string): number;
|
|
270
357
|
export declare class RunJournal implements ExecutionJournal {
|
|
271
358
|
readonly runId: string;
|
|
272
359
|
readonly dir: string;
|
|
@@ -279,8 +366,14 @@ export declare class RunJournal implements ExecutionJournal {
|
|
|
279
366
|
private seq;
|
|
280
367
|
private dead;
|
|
281
368
|
private began;
|
|
369
|
+
private closed;
|
|
282
370
|
private secrets;
|
|
283
371
|
private consumed;
|
|
372
|
+
/** Per-run HMAC key; lives only here and in the key file outside the workspace. */
|
|
373
|
+
private key;
|
|
374
|
+
private readonly keyFile;
|
|
375
|
+
/** Last live usage record per attempt: time and spend, for throttling. */
|
|
376
|
+
private lastUsage;
|
|
284
377
|
private constructor();
|
|
285
378
|
/** A journal for a new run. Nothing touches disk until the executor calls `begin`. */
|
|
286
379
|
static create(opts: RunJournalOptions): RunJournal;
|
|
@@ -289,6 +382,8 @@ export declare class RunJournal implements ExecutionJournal {
|
|
|
289
382
|
/** False once an append failed: the rest of the run is not journaled. */
|
|
290
383
|
get active(): boolean;
|
|
291
384
|
private takeLock;
|
|
385
|
+
/** The run dir is private (0700); its parents are created as usual. */
|
|
386
|
+
private makeDir;
|
|
292
387
|
private releaseLock;
|
|
293
388
|
private append;
|
|
294
389
|
private closeFd;
|
|
@@ -299,7 +394,9 @@ export declare class RunJournal implements ExecutionJournal {
|
|
|
299
394
|
checkpoint(node: string, iteration: number, cp: JournalCheckpoint): void;
|
|
300
395
|
nodeEnd(node: string, iteration: number, result: NodeResult, writeState: WriteStageState): void;
|
|
301
396
|
route(from: string, to: string | null): void;
|
|
302
|
-
|
|
397
|
+
usage(node: string, iteration: number, attempt: number, usage: NodeUsage, final: boolean): void;
|
|
398
|
+
priorSpend(): Spend | undefined;
|
|
399
|
+
/** Close the run: `run:end`, then release the lock. Safe to call twice (one run:end). */
|
|
303
400
|
end(status: "success" | "failed" | "crashed"): void;
|
|
304
401
|
wrapWrites(node: string, iteration: number, skills: Map<string, Skill>): Map<string, Skill>;
|
|
305
402
|
private wrapTool;
|