pi-crew 0.9.66 → 0.9.68
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 +109 -0
- package/README.md +2 -3
- package/agents/executor.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/dist/index.mjs +4451 -4092
- package/package.json +12 -8
- package/scripts/analyze-run.mjs +1333 -0
- package/scripts/resource-sampler.mjs +482 -0
- package/skills/real-test-pi-crew/SKILL.md +10 -9
- package/src/agents/agent-config.ts +35 -14
- package/src/config/role-tools.ts +2 -2
- package/src/extension/knowledge-injection.ts +17 -0
- package/src/extension/pi-api.ts +0 -16
- package/src/extension/team-tool/api/agent-control.ts +358 -0
- package/src/extension/team-tool/api/handler-context.ts +57 -0
- package/src/extension/team-tool/api/heartbeat.ts +75 -0
- package/src/extension/team-tool/api/mailbox.ts +242 -0
- package/src/extension/team-tool/api/plan-approval.ts +190 -0
- package/src/extension/team-tool/api/read.ts +443 -0
- package/src/extension/team-tool/api/task-claims.ts +207 -0
- package/src/extension/team-tool/api.ts +56 -1301
- package/src/extension/team-tool/cancel.ts +15 -4
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +4 -4
- package/src/extension/team-tool/lifecycle-actions.ts +5 -5
- package/src/extension/team-tool/parallel-dispatch.ts +2 -2
- package/src/extension/team-tool/respond.ts +11 -3
- package/src/extension/team-tool/run-intent.ts +357 -0
- package/src/extension/team-tool/run.ts +8 -285
- package/src/extension/team-tool/status.ts +56 -21
- package/src/extension/team-tool.ts +10 -8
- package/src/prompt/scratchpad-lifecycle.ts +80 -5
- package/src/runtime/child-pi/child-pi-spawn.ts +13 -7
- package/src/runtime/child-pi/child-pi.ts +22 -144
- package/src/runtime/child-pi/mock-fixtures.ts +171 -0
- package/src/runtime/crew-agent-records.ts +18 -1
- package/src/runtime/scratchpad/README.md +11 -0
- package/src/runtime/scratchpad/guest.ts +157 -7
- package/src/runtime/scratchpad/protocol.ts +13 -1
- package/src/runtime/scratchpad/transform.ts +206 -12
- package/src/runtime/supervisor-contact.ts +0 -16
- package/src/runtime/task-runner/child-executor.ts +1 -0
- package/src/runtime/task-runner/state-helpers.ts +9 -1
- package/src/runtime/team-runner.ts +39 -2
- package/src/state/contracts.ts +3 -0
- package/src/state/event-log/event-log.ts +19 -27
- package/src/state/gitignore-manager.ts +61 -7
- package/src/utils/glob-match.ts +29 -0
- package/types/dwf.d.ts +1 -1
- package/src/runtime/scratchpad/snapshot-hmac.ts +0 -161
- package/src/types/new-api-types.ts +0 -35
|
@@ -357,6 +357,12 @@ function persistSequence(eventsPath: string, seq: number): void {
|
|
|
357
357
|
// makes assignment atomic (JS is single-threaded); persistSequence() keeps the
|
|
358
358
|
// sidecar durable for crash recovery across restarts.
|
|
359
359
|
const seqCounters = new Map<string, number>();
|
|
360
|
+
// H6 (2026-08-10): FIFO cap — mirrors appendCounters (APPEND_COUNTER_MAX_ENTRIES = 256)
|
|
361
|
+
// and agentEventSeqCache (cap at :434). Without this, a long-lived parent pi process
|
|
362
|
+
// that observes many runs (dev sessions with hundreds of background runs over a week)
|
|
363
|
+
// accumulates one entry per distinct eventsPath forever. Eviction is safe: the next
|
|
364
|
+
// append re-seeds from the `.seq` sidecar via reserveSequenceUnderLock.
|
|
365
|
+
const SEQ_COUNTERS_MAX_ENTRIES = 256;
|
|
360
366
|
|
|
361
367
|
/** Atomically reserve the next sequence number for `eventsPath`.
|
|
362
368
|
*
|
|
@@ -384,7 +390,18 @@ function reserveSequence(eventsPath: string): number {
|
|
|
384
390
|
* (e.g. baseMetadata.seq) so a later auto-assigned seq never collides with it. */
|
|
385
391
|
function advanceSequenceCounter(eventsPath: string, seq: number): void {
|
|
386
392
|
const last = seqCounters.get(eventsPath);
|
|
387
|
-
if (last === undefined || seq > last)
|
|
393
|
+
if (last === undefined || seq > last) {
|
|
394
|
+
seqCounters.set(eventsPath, seq);
|
|
395
|
+
enforceSeqCountersCap();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** H6: FIFO eviction when the seqCounters map exceeds its bounded size. */
|
|
400
|
+
function enforceSeqCountersCap(): void {
|
|
401
|
+
if (seqCounters.size > SEQ_COUNTERS_MAX_ENTRIES) {
|
|
402
|
+
const oldest = seqCounters.keys().next().value;
|
|
403
|
+
if (oldest !== undefined) seqCounters.delete(oldest);
|
|
404
|
+
}
|
|
388
405
|
}
|
|
389
406
|
|
|
390
407
|
/** C-01: Reserve sequence INSIDE the cross-process lock. Reads the authoritative
|
|
@@ -401,6 +418,7 @@ function reserveSequenceUnderLock(eventsPath: string): number {
|
|
|
401
418
|
const last = Math.max(stored, inProcess);
|
|
402
419
|
const next = last + 1;
|
|
403
420
|
seqCounters.set(eventsPath, next);
|
|
421
|
+
enforceSeqCountersCap();
|
|
404
422
|
return next;
|
|
405
423
|
}
|
|
406
424
|
|
|
@@ -418,32 +436,6 @@ export function computeEventFingerprint(event: Pick<TeamEvent, "type" | "runId"
|
|
|
418
436
|
.slice(0, 16);
|
|
419
437
|
}
|
|
420
438
|
|
|
421
|
-
/**
|
|
422
|
-
* Check for sequence gaps between the sidecar file and the events file.
|
|
423
|
-
* This detects situations where the sidecar records a sequence number that has
|
|
424
|
-
* no corresponding event in the file (e.g., due to a crash between
|
|
425
|
-
* persistSequence and appendFile in older code, or other corruption).
|
|
426
|
-
*
|
|
427
|
-
* Returns an array of gap info: for each gap found, { missing: n } indicates
|
|
428
|
-
* sequence n is recorded in sidecar but has no corresponding event.
|
|
429
|
-
* An empty array means no gaps were found.
|
|
430
|
-
*/
|
|
431
|
-
export function checkSequenceGaps(eventsPath: string): { missing: number }[] {
|
|
432
|
-
if (!fs.existsSync(eventsPath)) return [];
|
|
433
|
-
const gaps: { missing: number }[] = [];
|
|
434
|
-
const storedSeq = readStoredSequence(eventsPath);
|
|
435
|
-
if (storedSeq === undefined) return [];
|
|
436
|
-
const maxInFile = scanSequence(eventsPath);
|
|
437
|
-
// If sidecar is ahead of file, report the missing sequences
|
|
438
|
-
// (sidecar stores the NEXT sequence to use, so storedSeq is the last written)
|
|
439
|
-
if (storedSeq > maxInFile) {
|
|
440
|
-
for (let i = maxInFile + 1; i <= storedSeq; i++) {
|
|
441
|
-
gaps.push({ missing: i });
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
return gaps;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
439
|
/**
|
|
448
440
|
* @deprecated Prefer `appendEventAsync()` in async contexts. The sync lock uses
|
|
449
441
|
* `sleepSync` which blocks the Node.js event loop, preventing AbortSignal handlers
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Manage .gitignore entries for the
|
|
2
|
+
* Manage .gitignore entries for the crew state directory.
|
|
3
|
+
*
|
|
4
|
+
* Supports BOTH layouts that `projectCrewRoot` (src/utils/paths.ts) can
|
|
5
|
+
* resolve to:
|
|
6
|
+
* - `.crew/` (default, created when neither .crew/ nor .pi/ exists)
|
|
7
|
+
* - `.pi/teams/` (legacy layout, reused when .pi/ already exists)
|
|
8
|
+
*
|
|
9
|
+
* Before this fix (2026-08-10, improvement-plan G12), only `.crew/`
|
|
10
|
+
* entries were written — so projects on the `.pi/teams/` layout silently
|
|
11
|
+
* committed their run state, artifacts, and logs to git. The layout is
|
|
12
|
+
* detected from the repo root (parent dir of the .gitignore path).
|
|
13
|
+
*
|
|
3
14
|
* Only adds entries if not already present; preserves existing content.
|
|
4
15
|
*/
|
|
5
16
|
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
6
18
|
import { atomicWriteFile } from "./atomic-write.ts";
|
|
7
19
|
|
|
8
20
|
/**
|
|
9
|
-
* Entries to add to .gitignore for
|
|
21
|
+
* Entries to add to .gitignore for the `.crew/` layout.
|
|
10
22
|
*
|
|
11
23
|
* - `/.crew/` and `/.crew` ignore the core state directory.
|
|
12
|
-
* - Exceptions allow optional commit of artifacts/ and graphs
|
|
24
|
+
* - Exceptions allow optional commit of artifacts/ and graphs/.
|
|
13
25
|
*/
|
|
14
26
|
const CREW_GITIGNORE_ENTRIES = [
|
|
15
27
|
"/.crew/",
|
|
@@ -21,12 +33,54 @@ const CREW_GITIGNORE_ENTRIES = [
|
|
|
21
33
|
];
|
|
22
34
|
|
|
23
35
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
36
|
+
* Entries to add to .gitignore for the `.pi/teams/` legacy layout.
|
|
37
|
+
*
|
|
38
|
+
* `.pi/` is shared with other pi-crew-unrelated pi tooling, so we ignore
|
|
39
|
+
* ONLY the specific pi-crew state subdirs rather than the whole `.pi/teams/`
|
|
40
|
+
* tree. `artifacts/` and `graphs/` stay committable (mirroring the `.crew/`
|
|
41
|
+
* exception policy).
|
|
42
|
+
*/
|
|
43
|
+
const PI_TEAMS_GITIGNORE_ENTRIES = [
|
|
44
|
+
"/.pi/teams/state/",
|
|
45
|
+
"/.pi/teams/cache/",
|
|
46
|
+
"/.pi/teams/worktrees/",
|
|
47
|
+
"/.pi/teams/imports/",
|
|
48
|
+
"/.pi/teams/audit/",
|
|
49
|
+
"!/.pi/teams/artifacts/",
|
|
50
|
+
"!/.pi/teams/graphs/",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve which gitignore entry set applies. The repo root is the parent
|
|
55
|
+
* directory of the `.gitignore` path. If `.crew/` exists there, use the
|
|
56
|
+
* `.crew/` entries; else if `.pi/` exists, use the `.pi/teams/` entries;
|
|
57
|
+
* else fall through to the default `.crew/` set (forward-looking for fresh
|
|
58
|
+
* projects — the crew-init step will create `.crew/`).
|
|
59
|
+
*/
|
|
60
|
+
function resolveEntries(gitignorePath: string): string[] {
|
|
61
|
+
const repoRoot = path.dirname(gitignorePath);
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(path.join(repoRoot, ".crew"))) return CREW_GITIGNORE_ENTRIES;
|
|
64
|
+
if (fs.existsSync(path.join(repoRoot, ".pi"))) return PI_TEAMS_GITIGNORE_ENTRIES;
|
|
65
|
+
} catch {
|
|
66
|
+
// Filesystem error — fall through to default. The crew-init step
|
|
67
|
+
// creates .crew/ anyway, so the default entries are correct.
|
|
68
|
+
}
|
|
69
|
+
return CREW_GITIGNORE_ENTRIES;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Update .gitignore with the appropriate crew entries. Creates the file if it
|
|
74
|
+
* doesn't exist. Preserves all existing content.
|
|
75
|
+
*
|
|
76
|
+
* Layout detection: if `.pi/` exists at the repo root and `.crew/` does not,
|
|
77
|
+
* the `.pi/teams/` entries are written instead (mirrors `projectCrewRoot`).
|
|
26
78
|
*/
|
|
27
79
|
export async function updateGitignore(gitignorePath: string): Promise<void> {
|
|
80
|
+
const entries = resolveEntries(gitignorePath);
|
|
81
|
+
|
|
28
82
|
if (!fs.existsSync(gitignorePath)) {
|
|
29
|
-
atomicWriteFile(gitignorePath,
|
|
83
|
+
atomicWriteFile(gitignorePath, entries.join("\n") + "\n");
|
|
30
84
|
return;
|
|
31
85
|
}
|
|
32
86
|
|
|
@@ -34,7 +88,7 @@ export async function updateGitignore(gitignorePath: string): Promise<void> {
|
|
|
34
88
|
const existingLines = new Set(current.split("\n").map((line) => line.trim()));
|
|
35
89
|
|
|
36
90
|
let appended = "";
|
|
37
|
-
for (const entry of
|
|
91
|
+
for (const entry of entries) {
|
|
38
92
|
if (!existingLines.has(entry)) {
|
|
39
93
|
appended += `\n${entry}`;
|
|
40
94
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob matcher with ReDoS protection.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `src/extension/team-tool/api.ts` so the security-critical
|
|
5
|
+
* ReDoS guard (pattern length cap + path-safe wildcards) is testable in
|
|
6
|
+
* isolation without importing the heavy `team-tool/api` transitive graph
|
|
7
|
+
* (which pulls in `pi-coding-agent` peer deps unavailable in unit tests).
|
|
8
|
+
*
|
|
9
|
+
* Semantics (intentional, path-safe):
|
|
10
|
+
* - `*` matches a run of non-separator chars (does NOT cross `/`)
|
|
11
|
+
* - `?` matches a single non-separator char
|
|
12
|
+
* - all other regex metacharacters are escaped (literal match)
|
|
13
|
+
* - patterns longer than 200 chars are rejected outright (ReDoS backstop)
|
|
14
|
+
*
|
|
15
|
+
* @see test/unit/glob-match-redos.test.ts — the regression suite for the
|
|
16
|
+
* catastrophic-backtracking fix (MEDIUM #4).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Prevent ReDoS: reject excessively long patterns.
|
|
20
|
+
const MAX_GLOB_PATTERN_LEN = 200;
|
|
21
|
+
|
|
22
|
+
export function globMatch(value: string, pattern: string): boolean {
|
|
23
|
+
if (pattern.length > MAX_GLOB_PATTERN_LEN) return false;
|
|
24
|
+
const regex = pattern
|
|
25
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape regex special chars
|
|
26
|
+
.replace(/\*/g, "[^/]*") // * matches non-slash characters only
|
|
27
|
+
.replace(/\?/g, "[^/]"); // ? matches single non-slash
|
|
28
|
+
return new RegExp(`^${regex}$`).test(value);
|
|
29
|
+
}
|
package/types/dwf.d.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* import type { WorkflowCtx } from "pi-crew/workflow";
|
|
25
25
|
* ```
|
|
26
26
|
*
|
|
27
|
-
* These interfaces mirror the runtime types in `src/runtime/dynamic-workflow-context.ts`.
|
|
27
|
+
* These interfaces mirror the runtime types in `src/runtime/goal-workflow/dynamic-workflow-context.ts`.
|
|
28
28
|
* They are authoring-only (no runtime values); the real implementations live in the runner.
|
|
29
29
|
*
|
|
30
30
|
* ## Resume & Checkpoint (round-18 P2-3)
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Snapshot HMAC helper — opt-in integrity for scratchpad snapshots.
|
|
3
|
-
*
|
|
4
|
-
* Closes the E.2 gap declared in docs/improvement-plan-2026-08-09.md and
|
|
5
|
-
* docs/runtime/scratchpad/README.md:120 ("v8.deserialize of restore content
|
|
6
|
-
* is unauthenticated (no HMAC)"). The threat model — a same-uid attacker
|
|
7
|
-
* plants a crafted V8 blob at the snapshot path to run deserialize gadgets
|
|
8
|
-
* in the guest — does not cross the existing same-uid boundary, but HMAC
|
|
9
|
-
* hardening is required before snapshots ever land in a shared/networked
|
|
10
|
-
* store.
|
|
11
|
-
*
|
|
12
|
-
* Migration window (see docs/decisions/2026-08-10-scratchpad-snapshot-hmac.md):
|
|
13
|
-
*
|
|
14
|
-
* Phase 1 (this module): HMAC sign-on-write + verify-on-read are OPT-IN
|
|
15
|
-
* via PI_CREW_SNAPSHOT_HMAC_KEY. When the key is unset, behaviour is
|
|
16
|
-
* unchanged (snapshots remain unsigned). When the key is set, writes
|
|
17
|
-
* attach a signature and reads verify it; an unsigned/failed snapshot
|
|
18
|
-
* is ACCEPTED with a warning so existing snapshots remain readable.
|
|
19
|
-
* Phase 2 (after one release): unsigned snapshots are REJECTED when the
|
|
20
|
-
* key is set (configurable via PI_CREW_SNAPSHOT_HMAC_STRICT=1).
|
|
21
|
-
* Phase 3 (after snapshots move to a shared store): the key becomes
|
|
22
|
-
* required and unsigned snapshots are always rejected.
|
|
23
|
-
*
|
|
24
|
-
* Wire-up into the actual write/read paths is intentionally deferred to a
|
|
25
|
-
* follow-up that audits the snapshot envelope format (V8 base64 vs raw
|
|
26
|
-
* bytes) and the writeArtifact redaction interaction. See ADR for the plan.
|
|
27
|
-
*/
|
|
28
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Env var holding the HMAC secret. When unset, HMAC is disabled (Phase 0
|
|
32
|
-
* behaviour — snapshots unsigned). When set, sign-on-write and
|
|
33
|
-
* verify-on-read are enabled.
|
|
34
|
-
*/
|
|
35
|
-
export const SNAPSHOT_HMAC_KEY_ENV = "PI_CREW_SNAPSHOT_HMAC_KEY";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Opt-in strict mode (Phase 2): when the key is set AND this is "1",
|
|
39
|
-
* unsigned or signature-mismatched snapshots are REJECTED on read instead
|
|
40
|
-
* of accepted with a warning.
|
|
41
|
-
*/
|
|
42
|
-
export const SNAPSHOT_HMAC_STRICT_ENV = "PI_CREW_SNAPSHOT_HMAC_STRICT";
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Header prefix for an inline signature. When snapshots are signed, the
|
|
46
|
-
* signature is prepended to the blob as `PI_CREW_SIG=<hex>\n` so a single
|
|
47
|
-
* read yields both the signature and the payload. (Sidecar files would
|
|
48
|
-
* require writeArtifact coordination that the envelope format does not
|
|
49
|
-
* currently support.)
|
|
50
|
-
*/
|
|
51
|
-
export const SNAPSHOT_SIG_PREFIX = "PI_CREW_SIG=";
|
|
52
|
-
|
|
53
|
-
export function getSnapshotHmacKey(env: NodeJS.ProcessEnv = process.env): Buffer | undefined {
|
|
54
|
-
const raw = env[SNAPSHOT_HMAC_KEY_ENV];
|
|
55
|
-
if (!raw) return undefined;
|
|
56
|
-
// Accept hex-encoded keys directly; otherwise encode the string as utf8.
|
|
57
|
-
// A key shorter than 32 bytes is rejected to prevent trivial brute-force.
|
|
58
|
-
const buf = /^[0-9a-fA-F]+$/.test(raw) && raw.length % 2 === 0 ? Buffer.from(raw, "hex") : Buffer.from(raw, "utf8");
|
|
59
|
-
if (buf.length < 32) {
|
|
60
|
-
throw new Error(
|
|
61
|
-
`${SNAPSHOT_HMAC_KEY_ENV} must be at least 32 bytes (got ${buf.length}); use a longer key or generate one with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
return buf;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function isSnapshotHmacStrict(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
68
|
-
return env[SNAPSHOT_HMAC_STRICT_ENV] === "1" || env[SNAPSHOT_HMAC_STRICT_ENV] === "true";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Compute the HMAC-SHA256 signature of `content` under `key`. Returns a
|
|
73
|
-
* lowercase hex string.
|
|
74
|
-
*/
|
|
75
|
-
export function signSnapshot(content: Buffer | string, key: Buffer): string {
|
|
76
|
-
return createHmac("sha256", key).update(content).digest("hex");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Constant-time signature comparison. Both signatures must be lowercase
|
|
81
|
-
* hex of the same length; anything else returns false without throwing.
|
|
82
|
-
*/
|
|
83
|
-
export function snapshotSignatureMatches(content: Buffer | string, signature: string, key: Buffer): boolean {
|
|
84
|
-
const expected = signSnapshot(content, key);
|
|
85
|
-
const a = Buffer.from(expected);
|
|
86
|
-
const b = Buffer.from(signature);
|
|
87
|
-
if (a.length !== b.length) return false;
|
|
88
|
-
return timingSafeEqual(a, b);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Outcome of {@link verifySnapshotPayload}. Callers decide how to react
|
|
93
|
-
* based on the strict-mode flag.
|
|
94
|
-
*/
|
|
95
|
-
export type SnapshotVerifyOutcome =
|
|
96
|
-
| { kind: "unsigned"; strict: boolean }
|
|
97
|
-
| { kind: "verified" }
|
|
98
|
-
| { kind: "mismatch"; strict: boolean }
|
|
99
|
-
| { kind: "hmac-disabled" };
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Verify a snapshot payload that may or may not carry an inline signature.
|
|
103
|
-
*
|
|
104
|
-
* @param payload The raw bytes read from disk (possibly including the
|
|
105
|
-
* `PI_CREW_SIG=<hex>\n` prefix).
|
|
106
|
-
* @param key The HMAC key, or `undefined` when HMAC is disabled.
|
|
107
|
-
* @param strict When true, unsigned/mismatched payloads are reported as
|
|
108
|
-
* rejectable. When false (the Phase 1 default), the caller is expected
|
|
109
|
-
* to accept the payload with a warning.
|
|
110
|
-
*/
|
|
111
|
-
export function verifySnapshotPayload(payload: Buffer, key: Buffer | undefined, strict: boolean): SnapshotVerifyOutcome {
|
|
112
|
-
if (!key) return { kind: "hmac-disabled" };
|
|
113
|
-
const prefixStr = SNAPSHOT_SIG_PREFIX;
|
|
114
|
-
if (payload.length < prefixStr.length + 1 || payload.subarray(0, prefixStr.length).toString("utf8") !== prefixStr) {
|
|
115
|
-
return { kind: "unsigned", strict };
|
|
116
|
-
}
|
|
117
|
-
const newlineIdx = payload.indexOf(0x0a, prefixStr.length);
|
|
118
|
-
if (newlineIdx < 0) return { kind: "unsigned", strict };
|
|
119
|
-
const sigHex = payload.subarray(prefixStr.length, newlineIdx).toString("utf8");
|
|
120
|
-
const body = payload.subarray(newlineIdx + 1);
|
|
121
|
-
return snapshotSignatureMatches(body, sigHex, key) ? { kind: "verified" } : { kind: "mismatch", strict };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Strip the inline signature prefix and return the bare payload. Returns
|
|
126
|
-
* the original buffer when no prefix is present. Used by read paths that
|
|
127
|
-
* have already called {@link verifySnapshotPayload} and decided to accept.
|
|
128
|
-
*/
|
|
129
|
-
export function stripSnapshotSignature(payload: Buffer): Buffer {
|
|
130
|
-
if (payload.length < SNAPSHOT_SIG_PREFIX.length) return payload;
|
|
131
|
-
if (payload.subarray(0, SNAPSHOT_SIG_PREFIX.length).toString("utf8") !== SNAPSHOT_SIG_PREFIX) return payload;
|
|
132
|
-
const newlineIdx = payload.indexOf(0x0a, SNAPSHOT_SIG_PREFIX.length);
|
|
133
|
-
if (newlineIdx < 0) return payload;
|
|
134
|
-
return payload.subarray(newlineIdx + 1);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Attach an inline signature to a payload. Returns a new buffer shaped as
|
|
139
|
-
* `PI_CREW_SIG=<hex>\n<payload>`. Used by write paths that have HMAC
|
|
140
|
-
* enabled.
|
|
141
|
-
*/
|
|
142
|
-
export function attachSnapshotSignature(payload: Buffer, key: Buffer): Buffer {
|
|
143
|
-
const sig = signSnapshot(payload, key);
|
|
144
|
-
return Buffer.concat([Buffer.from(`${SNAPSHOT_SIG_PREFIX}${sig}\n`, "utf8"), payload]);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Should the caller reject the snapshot based on the verify outcome?
|
|
149
|
-
* Centralises the strict-vs-migration decision so callers stay simple.
|
|
150
|
-
*/
|
|
151
|
-
export function shouldRejectSnapshot(outcome: SnapshotVerifyOutcome): boolean {
|
|
152
|
-
switch (outcome.kind) {
|
|
153
|
-
case "hmac-disabled":
|
|
154
|
-
return false;
|
|
155
|
-
case "verified":
|
|
156
|
-
return false;
|
|
157
|
-
case "unsigned":
|
|
158
|
-
case "mismatch":
|
|
159
|
-
return outcome.strict;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type imports from pi v0.77.0
|
|
3
|
-
*/
|
|
4
|
-
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
|
|
6
|
-
export type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
|
|
8
|
-
// Note: AgentEvent is not exported by pi-coding-agent v0.77.0
|
|
9
|
-
// Using AgentEndEvent and AgentStartEvent instead
|
|
10
|
-
|
|
11
|
-
// Type guards for pi-crew usage
|
|
12
|
-
/** @internal */
|
|
13
|
-
function isToolEvent(event: AgentSessionEvent): boolean {
|
|
14
|
-
return event.type === "tool_execution_start" || event.type === "tool_execution_update" || event.type === "tool_execution_end";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** @internal */
|
|
18
|
-
function isAgentLifecycleEvent(event: AgentSessionEvent): boolean {
|
|
19
|
-
return event.type === "agent_start" || event.type === "agent_end";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** @internal */
|
|
23
|
-
function isCompactionEvent(event: AgentSessionEvent): boolean {
|
|
24
|
-
return event.type === "compaction_start" || event.type === "compaction_end";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** @internal */
|
|
28
|
-
function isRetryEvent(event: AgentSessionEvent): boolean {
|
|
29
|
-
return event.type === "auto_retry_start" || event.type === "auto_retry_end";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** @internal */
|
|
33
|
-
function isQueueEvent(event: AgentSessionEvent): boolean {
|
|
34
|
-
return event.type === "queue_update";
|
|
35
|
-
}
|