pi-crew 0.1.38 → 0.1.40
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 +28 -0
- package/README.md +42 -3
- package/package.json +3 -3
- package/schema.json +2 -1
- package/src/agents/discover-agents.ts +1 -1
- package/src/config/config.ts +81 -2
- package/src/extension/async-notifier.ts +26 -4
- package/src/extension/import-index.ts +1 -1
- package/src/extension/notification-sink.ts +1 -1
- package/src/extension/register.ts +23 -7
- package/src/extension/registration/subagent-tools.ts +11 -6
- package/src/extension/result-watcher.ts +6 -1
- package/src/extension/team-tool/api.ts +65 -8
- package/src/extension/team-tool/doctor.ts +2 -2
- package/src/observability/metric-sink.ts +1 -1
- package/src/runtime/child-pi.ts +2 -1
- package/src/runtime/crew-agent-records.ts +5 -4
- package/src/runtime/diagnostic-export.ts +2 -16
- package/src/runtime/live-session-runtime.ts +12 -6
- package/src/runtime/manifest-cache.ts +10 -1
- package/src/runtime/sidechain-output.ts +2 -1
- package/src/runtime/subagent-manager.ts +6 -1
- package/src/runtime/task-runner/live-executor.ts +3 -0
- package/src/runtime/team-runner.ts +74 -3
- package/src/schema/config-schema.ts +1 -0
- package/src/state/artifact-store.ts +4 -2
- package/src/state/event-log.ts +2 -1
- package/src/state/jsonl-writer.ts +3 -1
- package/src/state/mailbox.ts +4 -3
- package/src/state/types.ts +12 -0
- package/src/teams/discover-teams.ts +1 -1
- package/src/utils/paths.ts +3 -2
- package/src/utils/redaction.ts +41 -0
- package/src/workflows/discover-workflows.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import { loadConfig } from "../../config/config.ts";
|
|
3
3
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
4
|
-
import { saveRunTasks,
|
|
4
|
+
import { loadRunManifestById, saveRunManifest, saveRunTasks, updateRunStatus } from "../../state/state-store.ts";
|
|
5
5
|
import { withRunLockSync } from "../../state/locks.ts";
|
|
6
6
|
import { canTransitionTaskStatus, isTeamTaskStatus } from "../../state/contracts.ts";
|
|
7
7
|
import { claimTask, releaseTaskClaim, transitionClaimedTaskStatus } from "../../state/task-claims.ts";
|
|
@@ -9,6 +9,7 @@ import { acknowledgeMailboxMessage, appendMailboxMessage, readDeliveryState, rea
|
|
|
9
9
|
import { appendEvent, readEvents, readEventsCursor } from "../../state/event-log.ts";
|
|
10
10
|
import { resolveCrewRuntime } from "../../runtime/runtime-resolver.ts";
|
|
11
11
|
import { probeLiveSessionRuntime } from "../../subagents/live/session-runtime.ts";
|
|
12
|
+
import { currentCrewRole, permissionForRole } from "../../runtime/role-permission.ts";
|
|
12
13
|
import { touchWorkerHeartbeat } from "../../runtime/worker-heartbeat.ts";
|
|
13
14
|
import { agentOutputPath, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
14
15
|
import { buildAgentDashboard, readAgentOutput } from "../../runtime/agent-observability.ts";
|
|
@@ -54,6 +55,13 @@ function snapshotHasRunId(snapshot: { values?: unknown }, runId: string): boolea
|
|
|
54
55
|
});
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
function canApprovePlan(): { allowed: boolean; reason?: string } {
|
|
59
|
+
const role = currentCrewRole();
|
|
60
|
+
if (!role) return { allowed: true };
|
|
61
|
+
if (permissionForRole(role) === "read_only") return { allowed: false, reason: `Role '${role}' is read-only and cannot approve or cancel plan gates.` };
|
|
62
|
+
return { allowed: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
58
66
|
const cfg = configRecord(params.config);
|
|
59
67
|
const operation = typeof cfg.operation === "string" ? cfg.operation : "read-manifest";
|
|
@@ -74,6 +82,47 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
74
82
|
if (operation === "read-manifest") {
|
|
75
83
|
return result(JSON.stringify(loaded.manifest, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
76
84
|
}
|
|
85
|
+
if (operation === "approve-plan") {
|
|
86
|
+
const permission = canApprovePlan();
|
|
87
|
+
if (!permission.allowed) return result(permission.reason ?? "Plan approval is not allowed in this context.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
88
|
+
try {
|
|
89
|
+
return withRunLockSync(loaded.manifest, () => {
|
|
90
|
+
const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded;
|
|
91
|
+
const approval = current.manifest.planApproval;
|
|
92
|
+
if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
93
|
+
const now = new Date().toISOString();
|
|
94
|
+
const manifest = { ...current.manifest, updatedAt: now, planApproval: { ...approval, status: "approved" as const, approvedAt: now, updatedAt: now } };
|
|
95
|
+
saveRunManifest(manifest);
|
|
96
|
+
appendEvent(manifest.eventsPath, { type: "plan.approved", runId: manifest.runId, taskId: approval.planTaskId, message: "Adaptive implementation plan approved; resume the run to execute mutating tasks.", metadata: { provenance: "api" } });
|
|
97
|
+
return result(JSON.stringify(manifest.planApproval, null, 2), { action: "api", status: "ok", runId: manifest.runId, artifactsRoot: manifest.artifactsRoot });
|
|
98
|
+
});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
101
|
+
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (operation === "cancel-plan") {
|
|
105
|
+
const permission = canApprovePlan();
|
|
106
|
+
if (!permission.allowed) return result(permission.reason ?? "Plan approval cancellation is not allowed in this context.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
107
|
+
try {
|
|
108
|
+
return withRunLockSync(loaded.manifest, () => {
|
|
109
|
+
const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded;
|
|
110
|
+
const approval = current.manifest.planApproval;
|
|
111
|
+
if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
112
|
+
const now = new Date().toISOString();
|
|
113
|
+
const tasks = current.tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled" as const, finishedAt: now, error: "Plan approval was cancelled." } : task);
|
|
114
|
+
let manifest: typeof current.manifest = { ...current.manifest, updatedAt: now, planApproval: { ...approval, status: "cancelled" as const, cancelledAt: now, updatedAt: now } };
|
|
115
|
+
saveRunManifest(manifest);
|
|
116
|
+
saveRunTasks(manifest, tasks);
|
|
117
|
+
appendEvent(manifest.eventsPath, { type: "plan.cancelled", runId: manifest.runId, taskId: approval.planTaskId, message: "Adaptive implementation plan was cancelled.", metadata: { provenance: "api" } });
|
|
118
|
+
manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
|
|
119
|
+
return result(JSON.stringify({ planApproval: manifest.planApproval, cancelledTasks: tasks.filter((task) => task.status === "cancelled").map((task) => task.id) }, null, 2), { action: "api", status: "ok", runId: manifest.runId, artifactsRoot: manifest.artifactsRoot });
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
77
126
|
if (operation === "list-tasks") {
|
|
78
127
|
return result(JSON.stringify(loaded.tasks, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
79
128
|
}
|
|
@@ -133,8 +182,11 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
133
182
|
const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
|
|
134
183
|
if (!agent) return result("API read-agent-transcript requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
135
184
|
const artifactTranscriptPath = safeContainedPath(loaded.manifest.artifactsRoot, agent.transcriptPath);
|
|
136
|
-
const
|
|
137
|
-
const
|
|
185
|
+
const fallbackPath = agentOutputPath(loaded.manifest, agent.taskId);
|
|
186
|
+
const artifactText = artifactTranscriptPath ? safeReadContainedFile(loaded.manifest.artifactsRoot, artifactTranscriptPath) ?? "" : "";
|
|
187
|
+
const fallbackText = artifactText ? "" : safeReadContainedFile(loaded.manifest.stateRoot, fallbackPath) ?? "";
|
|
188
|
+
const transcriptPath = artifactText ? artifactTranscriptPath : fallbackPath;
|
|
189
|
+
const text = artifactText || fallbackText;
|
|
138
190
|
return result(text || `(no transcript at ${transcriptPath})`, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
139
191
|
}
|
|
140
192
|
if (operation === "read-agent-output") {
|
|
@@ -191,11 +243,16 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
191
243
|
const task = loaded.tasks.find((item) => item.id === agent.taskId);
|
|
192
244
|
if (!task) return result(`API ${operation} agent '${agentId}' does not match a run task.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
193
245
|
if (operation === "resume-agent" && !prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
246
|
+
try {
|
|
247
|
+
const request = appendLiveAgentControlRequest(loaded.manifest, { taskId: task.id, agentId: agent.id, operation: operation === "resume-agent" ? "resume" : operation === "steer-agent" ? "steer" : "stop", message: operation === "resume-agent" ? prompt : message });
|
|
248
|
+
publishLiveControlRealtime(request);
|
|
249
|
+
ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
|
|
250
|
+
appendEvent(loaded.manifest.eventsPath, { type: "agent.control.queued", runId: loaded.manifest.runId, taskId: agent.taskId, message: `Queued ${request.operation} control request for live agent.`, data: { request, realtime: true } });
|
|
251
|
+
return result(JSON.stringify({ queued: true, request }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
252
|
+
} catch (queueError) {
|
|
253
|
+
const message = queueError instanceof Error ? queueError.message : String(queueError);
|
|
254
|
+
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
255
|
+
}
|
|
199
256
|
}
|
|
200
257
|
}
|
|
201
258
|
if (operation === "read-mailbox") {
|
|
@@ -107,8 +107,8 @@ export function buildTeamDoctorReport(input: TeamDoctorReportInput): TeamDoctorR
|
|
|
107
107
|
const userWritable = checkWritableDir(userCrewRoot());
|
|
108
108
|
const projectWritable = checkWritableDir(projectCrewRoot(input.cwd));
|
|
109
109
|
return [
|
|
110
|
-
{ label: "user state", ok: userWritable.ok, detail: userWritable.detail },
|
|
111
|
-
{ label: "project state", ok: projectWritable.ok, detail: projectWritable.detail },
|
|
110
|
+
{ label: "user state", ok: userWritable.ok || userWritable.detail.endsWith(": missing"), detail: userWritable.detail },
|
|
111
|
+
{ label: "project state", ok: projectWritable.ok || projectWritable.detail.endsWith(": missing"), detail: projectWritable.detail },
|
|
112
112
|
{ label: "project state root", ok: true, detail: path.join(projectCrewRoot(input.cwd), DEFAULT_PATHS.state.runsSubdir) },
|
|
113
113
|
{ label: "artifacts root", ok: true, detail: path.join(projectCrewRoot(input.cwd), DEFAULT_PATHS.state.artifactsSubdir) },
|
|
114
114
|
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { redactSecrets } from "../
|
|
3
|
+
import { redactSecrets } from "../utils/redaction.ts";
|
|
4
4
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
5
5
|
import type { MetricRegistry } from "./metric-registry.ts";
|
|
6
6
|
import type { MetricSnapshot } from "./metrics-primitives.ts";
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { getPiSpawnCommand } from "./pi-spawn.ts";
|
|
|
7
7
|
import { DEFAULT_CHILD_PI } from "../config/defaults.ts";
|
|
8
8
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
9
9
|
import { attachPostExitStdioGuard, trySignalChild } from "./post-exit-stdio-guard.ts";
|
|
10
|
+
import { redactJsonLine } from "../utils/redaction.ts";
|
|
10
11
|
|
|
11
12
|
const POST_EXIT_STDIO_GUARD_MS = DEFAULT_CHILD_PI.postExitStdioGuardMs;
|
|
12
13
|
const FINAL_DRAIN_MS = DEFAULT_CHILD_PI.finalDrainMs;
|
|
@@ -118,7 +119,7 @@ export function buildChildPiSpawnOptions(cwd: string, env: NodeJS.ProcessEnv): S
|
|
|
118
119
|
function appendTranscript(input: ChildPiRunInput, line: string): void {
|
|
119
120
|
if (!input.transcriptPath) return;
|
|
120
121
|
fs.mkdirSync(path.dirname(input.transcriptPath), { recursive: true });
|
|
121
|
-
fs.appendFileSync(input.transcriptPath, `${line}\n`, "utf-8");
|
|
122
|
+
fs.appendFileSync(input.transcriptPath, `${redactJsonLine(line)}\n`, "utf-8");
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
function compactString(value: string, maxChars = MAX_COMPACT_CONTENT_CHARS): string {
|
|
@@ -7,6 +7,7 @@ import type { CrewAgentProgress, CrewAgentRecord, CrewRuntimeKind } from "./crew
|
|
|
7
7
|
import { taskStatusToAgentStatus } from "./crew-agent-runtime.ts";
|
|
8
8
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
9
9
|
import { assertSafePathId, resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
10
|
+
import { redactSecretString, redactSecrets } from "../utils/redaction.ts";
|
|
10
11
|
|
|
11
12
|
export function agentsPath(manifest: TeamRunManifest): string {
|
|
12
13
|
return path.join(manifest.stateRoot, "agents.json");
|
|
@@ -71,7 +72,7 @@ export function readCrewAgents(manifest: TeamRunManifest): CrewAgentRecord[] {
|
|
|
71
72
|
|
|
72
73
|
export function saveCrewAgents(manifest: TeamRunManifest, records: CrewAgentRecord[]): void {
|
|
73
74
|
fs.mkdirSync(manifest.stateRoot, { recursive: true });
|
|
74
|
-
atomicWriteJson(agentsPath(manifest), records);
|
|
75
|
+
atomicWriteJson(agentsPath(manifest), redactSecrets(records));
|
|
75
76
|
for (const record of records) writeCrewAgentStatus(manifest, record);
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -84,7 +85,7 @@ export function upsertCrewAgent(manifest: TeamRunManifest, record: CrewAgentReco
|
|
|
84
85
|
|
|
85
86
|
export function writeCrewAgentStatus(manifest: TeamRunManifest, record: CrewAgentRecord): void {
|
|
86
87
|
ensureAgentStateDir(manifest, record.taskId);
|
|
87
|
-
atomicWriteJson(agentStatusPath(manifest, record.taskId), record);
|
|
88
|
+
atomicWriteJson(agentStatusPath(manifest, record.taskId), redactSecrets(record));
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
export function readCrewAgentStatus(manifest: TeamRunManifest, taskOrAgentId: string): CrewAgentRecord | undefined {
|
|
@@ -121,7 +122,7 @@ export function appendCrewAgentEvent(manifest: TeamRunManifest, taskId: string,
|
|
|
121
122
|
ensureAgentStateDir(manifest, taskId);
|
|
122
123
|
const filePath = agentStateFile(manifest, taskId, "events.jsonl");
|
|
123
124
|
const seq = nextAgentEventSeq(filePath);
|
|
124
|
-
fs.appendFileSync(filePath, `${JSON.stringify({ seq, time: new Date().toISOString(), event })}\n`, "utf-8");
|
|
125
|
+
fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets({ seq, time: new Date().toISOString(), event }))}\n`, "utf-8");
|
|
125
126
|
try {
|
|
126
127
|
const stat = fs.statSync(filePath);
|
|
127
128
|
agentEventSeqCache.set(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, seq });
|
|
@@ -172,7 +173,7 @@ export function readCrewAgentEventsCursor(manifest: TeamRunManifest, taskId: str
|
|
|
172
173
|
export function appendCrewAgentOutput(manifest: TeamRunManifest, taskId: string, text: string): void {
|
|
173
174
|
if (!text.trim()) return;
|
|
174
175
|
ensureAgentStateDir(manifest, taskId);
|
|
175
|
-
fs.appendFileSync(agentStateFile(manifest, taskId, "output.log"), `${text}\n`, "utf-8");
|
|
176
|
+
fs.appendFileSync(agentStateFile(manifest, taskId, "output.log"), `${redactSecretString(text)}\n`, "utf-8");
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
export function emptyCrewAgentProgress(): CrewAgentProgress {
|
|
@@ -9,6 +9,8 @@ import { loadRunManifestById } from "../state/state-store.ts";
|
|
|
9
9
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
10
10
|
import { summarizeHeartbeats, type HeartbeatSummary } from "../ui/heartbeat-aggregator.ts";
|
|
11
11
|
import type { RunUiSnapshot } from "../ui/snapshot-types.ts";
|
|
12
|
+
import { redactSecrets } from "../utils/redaction.ts";
|
|
13
|
+
export { redactSecrets } from "../utils/redaction.ts";
|
|
12
14
|
|
|
13
15
|
export interface DiagnosticReport {
|
|
14
16
|
schemaVersion?: number;
|
|
@@ -25,22 +27,6 @@ export interface DiagnosticReport {
|
|
|
25
27
|
|
|
26
28
|
const SECRET_KEY_PATTERN = /(token|key|password|secret|credential|auth)/i;
|
|
27
29
|
|
|
28
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
29
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function redactSecrets(value: unknown, keyName = ""): unknown {
|
|
33
|
-
if (SECRET_KEY_PATTERN.test(keyName)) return "***";
|
|
34
|
-
if (typeof value === "string") return value.replace(/((?:token|key|password|secret|credential|auth)[\w.-]*\s*[=:]\s*)[^\s,;]+/gi, "$1***");
|
|
35
|
-
if (Array.isArray(value)) return value.map((item) => redactSecrets(item));
|
|
36
|
-
if (isRecord(value)) {
|
|
37
|
-
const output: Record<string, unknown> = {};
|
|
38
|
-
for (const [key, entry] of Object.entries(value)) output[key] = redactSecrets(entry, key);
|
|
39
|
-
return output;
|
|
40
|
-
}
|
|
41
|
-
return value;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
30
|
function envRedacted(): Record<string, string> {
|
|
45
31
|
const output: Record<string, string> = {};
|
|
46
32
|
for (const [key, value] of Object.entries(process.env)) {
|
|
@@ -10,6 +10,7 @@ import { subscribeLiveControlRealtime } from "./live-control-realtime.ts";
|
|
|
10
10
|
import { eventToSidechainType, sidechainOutputPath, writeSidechainEntry } from "./sidechain-output.ts";
|
|
11
11
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
12
12
|
import { isLiveSessionRuntimeAvailable } from "./runtime-resolver.ts";
|
|
13
|
+
import { redactSecrets } from "../utils/redaction.ts";
|
|
13
14
|
|
|
14
15
|
export interface LiveSessionSpawnInput {
|
|
15
16
|
manifest: TeamRunManifest;
|
|
@@ -25,6 +26,7 @@ export interface LiveSessionSpawnInput {
|
|
|
25
26
|
parentContext?: string;
|
|
26
27
|
parentModel?: unknown;
|
|
27
28
|
modelRegistry?: unknown;
|
|
29
|
+
isCurrent?: () => boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface LiveSessionRunResult {
|
|
@@ -70,7 +72,7 @@ type LiveSessionLike = {
|
|
|
70
72
|
function appendTranscript(filePath: string | undefined, event: unknown): void {
|
|
71
73
|
if (!filePath) return;
|
|
72
74
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
73
|
-
fs.appendFileSync(filePath, `${JSON.stringify(event)}\n`, "utf-8");
|
|
75
|
+
fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets(event))}\n`, "utf-8");
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
@@ -173,6 +175,7 @@ export async function probeLiveSessionRuntime(): Promise<LiveSessionUnavailableR
|
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<LiveSessionRunResult> {
|
|
178
|
+
const isCurrent = input.isCurrent ?? (() => true);
|
|
176
179
|
if (process.env.PI_CREW_MOCK_LIVE_SESSION === "success") {
|
|
177
180
|
const agentId = `${input.manifest.runId}:${input.task.id}`;
|
|
178
181
|
const inherited = input.runtimeConfig?.inheritContext === true && input.parentContext ? ` with inherited context: ${input.parentContext}` : "";
|
|
@@ -183,9 +186,9 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
183
186
|
const sidechainPath = sidechainOutputPath(input.manifest.stateRoot, input.task.id);
|
|
184
187
|
writeSidechainEntry(sidechainPath, { agentId, type: "user", message: { role: "user", content: input.prompt }, cwd: input.task.cwd });
|
|
185
188
|
writeSidechainEntry(sidechainPath, { agentId, type: "message", message: event, cwd: input.task.cwd });
|
|
186
|
-
input.onEvent?.(event);
|
|
189
|
+
if (isCurrent()) input.onEvent?.(event);
|
|
187
190
|
const stdout = `Mock live-session success for ${input.agent.name}${inherited}`;
|
|
188
|
-
input.onOutput?.(stdout);
|
|
191
|
+
if (isCurrent()) input.onOutput?.(stdout);
|
|
189
192
|
updateLiveAgentStatus(agentId, "completed");
|
|
190
193
|
return { available: true, exitCode: 0, stdout, stderr: "", jsonEvents: 1 };
|
|
191
194
|
}
|
|
@@ -234,7 +237,7 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
234
237
|
const seenControlRequestIds = new Set<string>();
|
|
235
238
|
let controlBusy = false;
|
|
236
239
|
const pollControl = async () => {
|
|
237
|
-
if (controlBusy || !session) return;
|
|
240
|
+
if (!isCurrent() || controlBusy || !session) return;
|
|
238
241
|
controlBusy = true;
|
|
239
242
|
try {
|
|
240
243
|
controlCursor = await applyLiveAgentControlRequests({ manifest: input.manifest, taskId: input.task.id, agentId, session, cursor: controlCursor, seenRequestIds: seenControlRequestIds });
|
|
@@ -243,11 +246,13 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
243
246
|
}
|
|
244
247
|
};
|
|
245
248
|
unsubscribeControlRealtime = subscribeLiveControlRealtime((request) => {
|
|
246
|
-
if (request.runId !== input.manifest.runId || request.taskId !== input.task.id || !session) return;
|
|
249
|
+
if (!isCurrent() || request.runId !== input.manifest.runId || request.taskId !== input.task.id || !session) return;
|
|
247
250
|
void applyLiveAgentControlRequest({ request, taskId: input.task.id, agentId, session, seenRequestIds: seenControlRequestIds });
|
|
248
251
|
});
|
|
249
252
|
await pollControl();
|
|
250
|
-
controlTimer = setInterval(() => {
|
|
253
|
+
controlTimer = setInterval(() => {
|
|
254
|
+
if (isCurrent()) void pollControl();
|
|
255
|
+
}, 500);
|
|
251
256
|
let turnCount = 0;
|
|
252
257
|
let softLimitReached = false;
|
|
253
258
|
const maxTurns = input.runtimeConfig?.maxTurns;
|
|
@@ -256,6 +261,7 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
256
261
|
writeSidechainEntry(sidechainPath, { agentId, type: "user", message: { role: "user", content: input.prompt }, cwd: input.task.cwd });
|
|
257
262
|
if (typeof session.subscribe === "function") {
|
|
258
263
|
unsubscribe = session.subscribe((event) => {
|
|
264
|
+
if (!isCurrent()) return;
|
|
259
265
|
jsonEvents += 1;
|
|
260
266
|
appendTranscript(input.transcriptPath, event);
|
|
261
267
|
const sidechainType = eventToSidechainType(event);
|
|
@@ -58,13 +58,22 @@ function parseManifest(filePath: string): TeamRunManifest | undefined {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function sameFilesystemPath(left: string, right: string): boolean {
|
|
62
|
+
if (path.resolve(left) === path.resolve(right)) return true;
|
|
63
|
+
try {
|
|
64
|
+
return fs.realpathSync.native(left) === fs.realpathSync.native(right);
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
function validateManifestForRoot(root: string, runId: string, manifest: TeamRunManifest): boolean {
|
|
62
71
|
try {
|
|
63
72
|
if (!isSafePathId(runId)) return false;
|
|
64
73
|
const stateRoot = resolveContainedRelativePath(root, runId, "runId");
|
|
65
74
|
const crewRoot = path.dirname(path.dirname(root));
|
|
66
75
|
const artifactsRoot = resolveContainedRelativePath(path.join(crewRoot, DEFAULT_PATHS.state.artifactsSubdir), runId, "runId");
|
|
67
|
-
if (manifest.runId !== runId || manifest.stateRoot
|
|
76
|
+
if (manifest.runId !== runId || !sameFilesystemPath(manifest.stateRoot, stateRoot) || !sameFilesystemPath(manifest.tasksPath, path.join(stateRoot, DEFAULT_PATHS.state.tasksFile)) || !sameFilesystemPath(manifest.eventsPath, path.join(stateRoot, DEFAULT_PATHS.state.eventsFile)) || !sameFilesystemPath(manifest.artifactsRoot, artifactsRoot)) return false;
|
|
68
77
|
if (fs.existsSync(artifactsRoot)) {
|
|
69
78
|
if (fs.lstatSync(artifactsRoot).isSymbolicLink()) return false;
|
|
70
79
|
resolveRealContainedPath(path.dirname(artifactsRoot), path.basename(artifactsRoot));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { redactSecrets } from "../utils/redaction.ts";
|
|
3
4
|
|
|
4
5
|
export interface SidechainEntry {
|
|
5
6
|
isSidechain: true;
|
|
@@ -12,7 +13,7 @@ export interface SidechainEntry {
|
|
|
12
13
|
|
|
13
14
|
export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
|
|
14
15
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
15
|
-
fs.appendFileSync(filePath, `${JSON.stringify({ isSidechain: true, timestamp: new Date().toISOString(), ...entry })}\n`, "utf-8");
|
|
16
|
+
fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets({ isSidechain: true, timestamp: new Date().toISOString(), ...entry }))}\n`, "utf-8");
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function sidechainOutputPath(stateRoot: string, taskId: string): string {
|
|
@@ -6,6 +6,7 @@ import { DEFAULT_SUBAGENT } from "../config/defaults.ts";
|
|
|
6
6
|
import { projectCrewRoot } from "../utils/paths.ts";
|
|
7
7
|
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
8
8
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
9
|
+
import { redactSecrets } from "../utils/redaction.ts";
|
|
9
10
|
|
|
10
11
|
export type SubagentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "error" | "blocked" | "stopped";
|
|
11
12
|
|
|
@@ -17,6 +18,7 @@ export interface SubagentSpawnOptions {
|
|
|
17
18
|
background: boolean;
|
|
18
19
|
model?: string;
|
|
19
20
|
maxTurns?: number;
|
|
21
|
+
ownerSessionGeneration?: number;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export interface SubagentRecord {
|
|
@@ -33,6 +35,7 @@ export interface SubagentRecord {
|
|
|
33
35
|
resultConsumed?: boolean;
|
|
34
36
|
model?: string;
|
|
35
37
|
background: boolean;
|
|
38
|
+
ownerSessionGeneration?: number;
|
|
36
39
|
stuckNotified?: boolean;
|
|
37
40
|
blockedAt?: number;
|
|
38
41
|
promise?: Promise<void>;
|
|
@@ -66,7 +69,7 @@ export function savePersistedSubagentRecord(cwd: string, record: SubagentRecord)
|
|
|
66
69
|
try {
|
|
67
70
|
const filePath = persistedSubagentPath(cwd, record.id);
|
|
68
71
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
69
|
-
fs.writeFileSync(filePath, `${JSON.stringify(serializableRecord(record), null, 2)}\n`, "utf-8");
|
|
72
|
+
fs.writeFileSync(filePath, `${JSON.stringify(redactSecrets(serializableRecord(record)), null, 2)}\n`, "utf-8");
|
|
70
73
|
} catch (error) {
|
|
71
74
|
logInternalError("subagent-manager.save", error, `id=${record.id}`);
|
|
72
75
|
}
|
|
@@ -136,6 +139,7 @@ export class SubagentManager {
|
|
|
136
139
|
startedAt: Date.now(),
|
|
137
140
|
model: options.model,
|
|
138
141
|
background: options.background,
|
|
142
|
+
ownerSessionGeneration: options.ownerSessionGeneration,
|
|
139
143
|
};
|
|
140
144
|
this.records.set(record.id, record);
|
|
141
145
|
this.cwdByRecord.set(record.id, options.cwd);
|
|
@@ -373,6 +377,7 @@ export class SubagentManager {
|
|
|
373
377
|
id: current.id,
|
|
374
378
|
runId: current.runId,
|
|
375
379
|
durationMs: Math.max(0, Date.now() - current.blockedAt),
|
|
380
|
+
ownerSessionGeneration: current.ownerSessionGeneration,
|
|
376
381
|
});
|
|
377
382
|
savePersistedSubagentRecord(cwd, current);
|
|
378
383
|
};
|
|
@@ -24,6 +24,7 @@ export interface RunLiveTaskInput {
|
|
|
24
24
|
parentContext?: string;
|
|
25
25
|
parentModel?: unknown;
|
|
26
26
|
modelRegistry?: unknown;
|
|
27
|
+
isCurrent?: () => boolean;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export interface RunLiveTaskOutput {
|
|
@@ -65,6 +66,7 @@ export async function runLiveTask(input: RunLiveTaskInput): Promise<RunLiveTaskO
|
|
|
65
66
|
}
|
|
66
67
|
};
|
|
67
68
|
const attemptStartedAt = new Date();
|
|
69
|
+
const isCurrent = input.isCurrent ?? (() => input.signal?.aborted !== true);
|
|
68
70
|
const liveResult = await runLiveSessionTask({
|
|
69
71
|
manifest,
|
|
70
72
|
task,
|
|
@@ -77,6 +79,7 @@ export async function runLiveTask(input: RunLiveTaskInput): Promise<RunLiveTaskO
|
|
|
77
79
|
parentContext: input.parentContext,
|
|
78
80
|
parentModel: input.parentModel,
|
|
79
81
|
modelRegistry: input.modelRegistry,
|
|
82
|
+
isCurrent,
|
|
80
83
|
onOutput: (text) => appendCrewAgentOutput(manifest, task.id, text),
|
|
81
84
|
onEvent: (event) => {
|
|
82
85
|
appendCrewAgentEvent(manifest, task.id, event);
|
|
@@ -24,6 +24,7 @@ import type { MetricRegistry } from "../observability/metric-registry.ts";
|
|
|
24
24
|
import { childCorrelation, withCorrelation } from "../observability/correlation.ts";
|
|
25
25
|
import { resolveBatchConcurrency } from "./concurrency.ts";
|
|
26
26
|
import { mapConcurrent } from "./parallel-utils.ts";
|
|
27
|
+
import { permissionForRole } from "./role-permission.ts";
|
|
27
28
|
|
|
28
29
|
export interface ExecuteTeamRunInput {
|
|
29
30
|
manifest: TeamRunManifest;
|
|
@@ -398,6 +399,47 @@ function failedTaskFrom(result: { tasks: TeamTaskState[] }, taskId: string): Tea
|
|
|
398
399
|
return result.tasks.find((item) => item.id === taskId && item.status === "failed");
|
|
399
400
|
}
|
|
400
401
|
|
|
402
|
+
function requiresPlanApproval(workflow: WorkflowConfig, runtimeConfig: CrewRuntimeConfig | undefined): boolean {
|
|
403
|
+
return workflow.name === "implementation" && runtimeConfig?.requirePlanApproval === true;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function isPlanApprovalPending(manifest: TeamRunManifest): boolean {
|
|
407
|
+
return manifest.planApproval?.required === true && manifest.planApproval.status === "pending";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function isMutatingTask(task: TeamTaskState): boolean {
|
|
411
|
+
return permissionForRole(task.role) !== "read_only";
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function ensurePlanApprovalRequested(manifest: TeamRunManifest, tasks: TeamTaskState[]): TeamRunManifest {
|
|
415
|
+
if (manifest.planApproval) return manifest;
|
|
416
|
+
const assessTask = tasks.find((task) => task.stepId === "assess" && task.status === "completed");
|
|
417
|
+
const now = new Date().toISOString();
|
|
418
|
+
const updated: TeamRunManifest = {
|
|
419
|
+
...manifest,
|
|
420
|
+
updatedAt: now,
|
|
421
|
+
planApproval: {
|
|
422
|
+
required: true,
|
|
423
|
+
status: "pending",
|
|
424
|
+
requestedAt: now,
|
|
425
|
+
updatedAt: now,
|
|
426
|
+
planTaskId: assessTask?.id,
|
|
427
|
+
planArtifactPath: assessTask?.resultArtifact?.path,
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
saveRunManifest(updated);
|
|
431
|
+
appendEvent(updated.eventsPath, { type: "plan.approval_required", runId: updated.runId, taskId: assessTask?.id, message: "Adaptive implementation plan requires explicit approval before mutating tasks run.", data: { planArtifactPath: assessTask?.resultArtifact?.path } });
|
|
432
|
+
return updated;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function cancelPlanTasks(tasks: TeamTaskState[], reason: string): TeamTaskState[] {
|
|
436
|
+
return tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled", finishedAt: new Date().toISOString(), error: reason, graph: task.graph ? { ...task.graph, queue: "done" } : undefined } : task);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function hasPendingMutatingAdaptiveTask(tasks: TeamTaskState[]): boolean {
|
|
440
|
+
return tasks.some((task) => task.status === "queued" && task.adaptive && isMutatingTask(task));
|
|
441
|
+
}
|
|
442
|
+
|
|
401
443
|
export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
|
|
402
444
|
let workflow = input.workflow;
|
|
403
445
|
let manifest = updateRunStatus(input.manifest, "running", input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results.");
|
|
@@ -422,7 +464,18 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
422
464
|
manifest = updateRunStatus(manifest, "blocked", "Adaptive planner did not produce a valid subagent plan.");
|
|
423
465
|
return { manifest, tasks };
|
|
424
466
|
}
|
|
425
|
-
if (initialAdaptive.injected)
|
|
467
|
+
if (initialAdaptive.injected) {
|
|
468
|
+
manifest = requiresPlanApproval(workflow, input.runtimeConfig) ? ensurePlanApprovalRequested(manifest, tasks) : manifest;
|
|
469
|
+
queueIndex = buildTaskGraphIndex(tasks);
|
|
470
|
+
} else if (requiresPlanApproval(workflow, input.runtimeConfig) && hasPendingMutatingAdaptiveTask(tasks)) {
|
|
471
|
+
manifest = ensurePlanApprovalRequested(manifest, tasks);
|
|
472
|
+
}
|
|
473
|
+
if (manifest.planApproval?.status === "cancelled") {
|
|
474
|
+
tasks = cancelPlanTasks(tasks, "Plan approval was cancelled.");
|
|
475
|
+
await saveRunTasksAsync(manifest, tasks);
|
|
476
|
+
manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
|
|
477
|
+
return { manifest, tasks };
|
|
478
|
+
}
|
|
426
479
|
manifest = writeProgress(manifest, tasks, "team-runner");
|
|
427
480
|
await saveRunManifestAsync(manifest);
|
|
428
481
|
const runtimeKind = input.runtime?.kind ?? (input.executeWorkers ? "child-process" : "scaffold");
|
|
@@ -451,8 +504,16 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
451
504
|
if (concurrency.reason.includes(";unbounded:")) {
|
|
452
505
|
appendEvent(manifest.eventsPath, { type: "limits.unbounded", runId: manifest.runId, message: "Unbounded worker concurrency was explicitly enabled for this run.", data: { concurrencyReason: concurrency.reason, maxConcurrent: concurrency.maxConcurrent } });
|
|
453
506
|
}
|
|
454
|
-
const
|
|
507
|
+
const approvalPending = isPlanApprovalPending(manifest);
|
|
508
|
+
const candidateBatch = approvalPending ? getReadyTasks(tasks, tasks.length, queueIndex) : getReadyTasks(tasks, concurrency.selectedCount, queueIndex);
|
|
509
|
+
const readyBatch = approvalPending ? candidateBatch.filter((task) => !isMutatingTask(task)).slice(0, concurrency.selectedCount) : candidateBatch;
|
|
455
510
|
if (readyBatch.length === 0) {
|
|
511
|
+
if (approvalPending && candidateBatch.some(isMutatingTask)) {
|
|
512
|
+
await saveRunTasksAsync(manifest, tasks);
|
|
513
|
+
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
514
|
+
manifest = updateRunStatus(manifest, "blocked", "Plan approval required before mutating implementation tasks run.");
|
|
515
|
+
return { manifest, tasks };
|
|
516
|
+
}
|
|
456
517
|
tasks = markBlocked(tasks, "No ready queued task; dependency graph may be invalid.");
|
|
457
518
|
await saveRunTasksAsync(manifest, tasks);
|
|
458
519
|
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
@@ -460,7 +521,7 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
460
521
|
return { manifest, tasks };
|
|
461
522
|
}
|
|
462
523
|
|
|
463
|
-
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, message: `Starting ready batch with ${readyBatch.length} task(s).`, data: { taskIds: readyBatch.map((task) => task.id), readyCount: snapshot.ready.length, blockedCount: snapshot.blocked.length, runningCount: snapshot.running.length, doneCount: snapshot.done.length, selectedCount: readyBatch.length, maxConcurrent: concurrency.maxConcurrent, defaultConcurrency: concurrency.defaultConcurrency, concurrencyReason: concurrency.reason } });
|
|
524
|
+
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, message: `Starting ready batch with ${readyBatch.length} task(s).`, data: { taskIds: readyBatch.map((task) => task.id), readyCount: snapshot.ready.length, blockedCount: snapshot.blocked.length, runningCount: snapshot.running.length, doneCount: snapshot.done.length, selectedCount: readyBatch.length, maxConcurrent: concurrency.maxConcurrent, defaultConcurrency: concurrency.defaultConcurrency, concurrencyReason: approvalPending ? `${concurrency.reason};plan-approval-read-only` : concurrency.reason } });
|
|
464
525
|
const results = await mapConcurrent(
|
|
465
526
|
readyBatch,
|
|
466
527
|
concurrency.selectedCount,
|
|
@@ -520,7 +581,17 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
520
581
|
return { manifest, tasks };
|
|
521
582
|
}
|
|
522
583
|
if (injectedAfterBatch.injected) {
|
|
584
|
+
manifest = requiresPlanApproval(workflow, input.runtimeConfig) ? ensurePlanApprovalRequested(manifest, tasks) : manifest;
|
|
523
585
|
queueIndex = buildTaskGraphIndex(tasks);
|
|
586
|
+
} else if (requiresPlanApproval(workflow, input.runtimeConfig) && hasPendingMutatingAdaptiveTask(tasks)) {
|
|
587
|
+
manifest = ensurePlanApprovalRequested(manifest, tasks);
|
|
588
|
+
}
|
|
589
|
+
if (manifest.planApproval?.status === "cancelled") {
|
|
590
|
+
tasks = cancelPlanTasks(tasks, "Plan approval was cancelled.");
|
|
591
|
+
await saveRunTasksAsync(manifest, tasks);
|
|
592
|
+
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
593
|
+
manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
|
|
594
|
+
return { manifest, tasks };
|
|
524
595
|
}
|
|
525
596
|
await saveRunTasksAsync(manifest, tasks);
|
|
526
597
|
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
@@ -36,6 +36,7 @@ export const PiTeamsRuntimeConfigSchema = Type.Object({
|
|
|
36
36
|
inheritContext: Type.Optional(Type.Boolean()),
|
|
37
37
|
promptMode: Type.Optional(Type.Union([Type.Literal("replace"), Type.Literal("append")])),
|
|
38
38
|
groupJoin: Type.Optional(Type.Union([Type.Literal("off"), Type.Literal("group"), Type.Literal("smart")])),
|
|
39
|
+
requirePlanApproval: Type.Optional(Type.Boolean()),
|
|
39
40
|
}, { additionalProperties: false });
|
|
40
41
|
|
|
41
42
|
export const PiTeamsControlConfigSchema = Type.Object({
|
|
@@ -4,6 +4,7 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import type { ArtifactDescriptor } from "./types.ts";
|
|
5
5
|
import { atomicWriteFile } from "./atomic-write.ts";
|
|
6
6
|
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
7
|
+
import { redactSecretString } from "../utils/redaction.ts";
|
|
7
8
|
|
|
8
9
|
function hashContent(content: string): string {
|
|
9
10
|
return createHash("sha256").update(content).digest("hex");
|
|
@@ -108,7 +109,8 @@ export function writeArtifact(artifactsRoot: string, options: ArtifactWriteOptio
|
|
|
108
109
|
resolveRealContainedPath(path.dirname(artifactsRoot), path.basename(artifactsRoot));
|
|
109
110
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
110
111
|
resolveRealContainedPath(artifactsRoot, path.dirname(filePath));
|
|
111
|
-
|
|
112
|
+
const content = redactSecretString(options.content);
|
|
113
|
+
atomicWriteFile(filePath, content);
|
|
112
114
|
const stats = fs.statSync(filePath);
|
|
113
115
|
return {
|
|
114
116
|
kind: options.kind,
|
|
@@ -116,7 +118,7 @@ export function writeArtifact(artifactsRoot: string, options: ArtifactWriteOptio
|
|
|
116
118
|
createdAt: new Date().toISOString(),
|
|
117
119
|
producer: options.producer,
|
|
118
120
|
sizeBytes: stats.size,
|
|
119
|
-
contentHash: hashContent(
|
|
121
|
+
contentHash: hashContent(content),
|
|
120
122
|
retention: options.retention ?? "run",
|
|
121
123
|
};
|
|
122
124
|
}
|
package/src/state/event-log.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { DEFAULT_EVENT_LOG } from "../config/defaults.ts";
|
|
5
5
|
import { atomicWriteFile } from "./atomic-write.ts";
|
|
6
6
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
7
|
+
import { redactSecrets } from "../utils/redaction.ts";
|
|
7
8
|
|
|
8
9
|
export type TeamEventProvenance = "live_worker" | "test" | "healthcheck" | "replay" | "api" | "background" | "team_runner";
|
|
9
10
|
export type TeamWatcherAction = "act" | "observe" | "ignore";
|
|
@@ -135,7 +136,7 @@ export function appendEvent(eventsPath: string, event: AppendTeamEvent): TeamEve
|
|
|
135
136
|
} catch (error) {
|
|
136
137
|
logInternalError("event-log.size-check", error, `eventsPath=${eventsPath}`);
|
|
137
138
|
}
|
|
138
|
-
fs.appendFileSync(eventsPath, `${JSON.stringify(fullEvent)}\n`, "utf-8");
|
|
139
|
+
fs.appendFileSync(eventsPath, `${JSON.stringify(redactSecrets(fullEvent))}\n`, "utf-8");
|
|
139
140
|
const seq = fullEvent.metadata?.seq ?? 0;
|
|
140
141
|
try {
|
|
141
142
|
const stat = fs.statSync(eventsPath);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import { redactJsonLine } from "../utils/redaction.ts";
|
|
2
3
|
|
|
3
4
|
export interface DrainableSource {
|
|
4
5
|
pause(): void;
|
|
@@ -50,7 +51,8 @@ export function createJsonlWriter(filePath: string | undefined, source: Drainabl
|
|
|
50
51
|
return {
|
|
51
52
|
writeLine(line: string) {
|
|
52
53
|
if (!stream || closed || !line.trim()) return;
|
|
53
|
-
const
|
|
54
|
+
const safeLine = redactJsonLine(line);
|
|
55
|
+
const chunk = `${safeLine}\n`;
|
|
54
56
|
const chunkBytes = Buffer.byteLength(chunk, "utf-8");
|
|
55
57
|
if (bytesWritten + chunkBytes > maxBytes) return;
|
|
56
58
|
try {
|