pi-crew 0.1.39 → 0.1.41
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 +34 -0
- package/README.md +50 -4
- package/docs/usage.md +11 -0
- package/package.json +1 -1
- package/schema.json +4 -1
- package/src/agents/discover-agents.ts +1 -1
- package/src/config/config.ts +87 -2
- package/src/extension/async-notifier.ts +26 -4
- 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 +38 -8
- package/src/extension/team-tool/api.ts +61 -2
- package/src/extension/team-tool/doctor.ts +31 -0
- package/src/extension/team-tool/status.ts +23 -3
- package/src/observability/metric-sink.ts +1 -1
- package/src/runtime/agent-control.ts +4 -5
- package/src/runtime/attention-events.ts +23 -0
- package/src/runtime/child-pi.ts +2 -1
- package/src/runtime/completion-guard.ts +99 -0
- package/src/runtime/crew-agent-records.ts +5 -4
- package/src/runtime/crew-agent-runtime.ts +2 -2
- package/src/runtime/diagnostic-export.ts +2 -16
- package/src/runtime/group-join.ts +22 -4
- package/src/runtime/live-session-runtime.ts +12 -6
- 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/task-runner.ts +25 -2
- package/src/runtime/team-runner.ts +131 -6
- package/src/schema/config-schema.ts +3 -0
- package/src/schema/team-tool-schema.ts +12 -3
- 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 +15 -4
- package/src/state/types.ts +25 -0
- package/src/teams/discover-teams.ts +1 -1
- package/src/ui/dashboard-panes/progress-pane.ts +3 -0
- package/src/ui/run-snapshot-cache.ts +29 -1
- package/src/ui/snapshot-types.ts +8 -0
- package/src/utils/fs-watch.ts +3 -3
- package/src/utils/redaction.ts +41 -0
- package/src/workflows/discover-workflows.ts +1 -1
|
@@ -22,9 +22,16 @@ interface ResultWatcherDependencies {
|
|
|
22
22
|
export interface ResultWatcherOptions extends ResultWatcherDependencies {
|
|
23
23
|
eventName?: string;
|
|
24
24
|
completionTtlMs?: number;
|
|
25
|
+
isCurrent?: () => boolean;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const RESULT_WATCHER_RESTART_MS = 3000;
|
|
29
|
+
const RESULT_WATCHER_POLL_MS = 1000;
|
|
30
|
+
|
|
31
|
+
function shouldFallBackToPolling(error: unknown): boolean {
|
|
32
|
+
const code = error && typeof error === "object" ? (error as { code?: unknown }).code : undefined;
|
|
33
|
+
return code === "EMFILE" || code === "ENOSPC" || code === "EPERM";
|
|
34
|
+
}
|
|
28
35
|
|
|
29
36
|
function readJson(filePath: string): unknown | undefined {
|
|
30
37
|
try {
|
|
@@ -40,18 +47,23 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
|
|
|
40
47
|
const eventName = options.eventName ?? "pi-crew:run-result";
|
|
41
48
|
const completionTtlMs = options.completionTtlMs ?? 5 * 60_000;
|
|
42
49
|
const watch = options.watch ?? watchWithErrorHandler;
|
|
50
|
+
const isCurrent = options.isCurrent ?? (() => true);
|
|
43
51
|
const seen = getGlobalSeenMap("pi-crew.result-watcher");
|
|
44
52
|
let watcher: fs.FSWatcher | null | undefined;
|
|
45
53
|
let restartTimer: ReturnType<typeof setTimeout> | undefined;
|
|
54
|
+
let pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
46
55
|
const coalescer = createFileCoalescer((file) => {
|
|
56
|
+
if (!isCurrent()) return;
|
|
47
57
|
const filePath = path.join(resultsDir, file);
|
|
48
58
|
if (!file.endsWith(".json") || !fs.existsSync(filePath)) return;
|
|
49
59
|
const payload = readJson(filePath);
|
|
50
|
-
if (payload
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
if (payload === undefined) {
|
|
61
|
+
coalescer.schedule(file, RESULT_WATCHER_POLL_MS);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const key = buildCompletionKey(payload as Record<string, unknown>, `file:${file}`);
|
|
65
|
+
if (!markSeenWithTtl(seen, key, Date.now(), completionTtlMs)) {
|
|
66
|
+
events.emit(eventName, payload);
|
|
55
67
|
}
|
|
56
68
|
try {
|
|
57
69
|
fs.unlinkSync(filePath);
|
|
@@ -59,11 +71,27 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
|
|
|
59
71
|
logInternalError("result-watcher.unlink", error, `filePath=${filePath}`);
|
|
60
72
|
}
|
|
61
73
|
}, 50);
|
|
62
|
-
const
|
|
74
|
+
const poll = () => {
|
|
75
|
+
if (!isCurrent() || !fs.existsSync(resultsDir)) return;
|
|
76
|
+
for (const file of fs.readdirSync(resultsDir).filter((entry) => entry.endsWith(".json"))) coalescer.schedule(file, 0);
|
|
77
|
+
};
|
|
78
|
+
const startPolling = () => {
|
|
79
|
+
if (pollTimer) return;
|
|
80
|
+
pollTimer = setInterval(poll, RESULT_WATCHER_POLL_MS);
|
|
81
|
+
pollTimer.unref?.();
|
|
82
|
+
poll();
|
|
83
|
+
};
|
|
84
|
+
const stopPolling = () => {
|
|
85
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
86
|
+
pollTimer = undefined;
|
|
87
|
+
};
|
|
88
|
+
const scheduleRestart = (error?: unknown) => {
|
|
89
|
+
if (shouldFallBackToPolling(error)) startPolling();
|
|
63
90
|
if (restartTimer) clearTimeout(restartTimer);
|
|
64
91
|
restartTimer = setTimeout(() => {
|
|
65
92
|
restartTimer = undefined;
|
|
66
93
|
try {
|
|
94
|
+
if (!isCurrent()) return;
|
|
67
95
|
fs.mkdirSync(resultsDir, { recursive: true });
|
|
68
96
|
handle.start();
|
|
69
97
|
} catch (error) {
|
|
@@ -74,23 +102,25 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
|
|
|
74
102
|
};
|
|
75
103
|
const handle: ResultWatcherHandle = {
|
|
76
104
|
start() {
|
|
105
|
+
if (!isCurrent()) return;
|
|
77
106
|
fs.mkdirSync(resultsDir, { recursive: true });
|
|
78
107
|
if (watcher) closeWatcher(watcher);
|
|
79
108
|
watcher = watch(resultsDir, (event, fileName) => {
|
|
80
109
|
if (event !== "rename" || !fileName) return;
|
|
81
110
|
coalescer.schedule(fileName.toString());
|
|
82
111
|
}, scheduleRestart);
|
|
112
|
+
if (watcher) stopPolling();
|
|
83
113
|
watcher?.unref?.();
|
|
84
114
|
},
|
|
85
115
|
prime() {
|
|
86
|
-
|
|
87
|
-
for (const file of fs.readdirSync(resultsDir).filter((entry) => entry.endsWith(".json"))) coalescer.schedule(file, 0);
|
|
116
|
+
poll();
|
|
88
117
|
},
|
|
89
118
|
stop() {
|
|
90
119
|
if (restartTimer) clearTimeout(restartTimer);
|
|
91
120
|
restartTimer = undefined;
|
|
92
121
|
closeWatcher(watcher);
|
|
93
122
|
watcher = undefined;
|
|
123
|
+
stopPolling();
|
|
94
124
|
coalescer.clear();
|
|
95
125
|
},
|
|
96
126
|
};
|
|
@@ -1,14 +1,15 @@
|
|
|
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";
|
|
8
|
-
import { acknowledgeMailboxMessage, appendMailboxMessage, readDeliveryState, readMailbox, validateMailbox, type MailboxDirection } from "../../state/mailbox.ts";
|
|
8
|
+
import { acknowledgeMailboxMessage, appendMailboxMessage, readDeliveryState, readMailbox, readMailboxMessage, validateMailbox, type MailboxDirection } from "../../state/mailbox.ts";
|
|
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
|
}
|
|
@@ -249,8 +298,18 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
249
298
|
if (!messageId) return result("API ack-message requires config.messageId.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
250
299
|
try {
|
|
251
300
|
return withRunLockSync(loaded.manifest, () => {
|
|
301
|
+
const message = readMailboxMessage(loaded.manifest, messageId);
|
|
252
302
|
const delivery = acknowledgeMailboxMessage(loaded.manifest, messageId);
|
|
253
303
|
appendEvent(loaded.manifest.eventsPath, { type: "mailbox.acknowledged", runId: loaded.manifest.runId, data: { messageId } });
|
|
304
|
+
if (message?.data?.kind === "group_join" && typeof message.data.requestId === "string") {
|
|
305
|
+
appendEvent(loaded.manifest.eventsPath, {
|
|
306
|
+
type: "agent.group_join.acknowledged",
|
|
307
|
+
runId: loaded.manifest.runId,
|
|
308
|
+
message: "Group join delivery acknowledged via mailbox ack.",
|
|
309
|
+
data: { requestId: message.data.requestId, messageId, batchId: message.data.batchId, partial: message.data.partial, acknowledgedAt: delivery.updatedAt, acknowledgedBy: "leader" },
|
|
310
|
+
metadata: { provenance: "api" },
|
|
311
|
+
});
|
|
312
|
+
}
|
|
254
313
|
ctx.events?.emit?.("crew.mailbox.acknowledged", { runId: loaded.manifest.runId, messageId, delivery });
|
|
255
314
|
return result(JSON.stringify(delivery, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
256
315
|
});
|
|
@@ -10,6 +10,7 @@ import { DEFAULT_PATHS } from "../../config/defaults.ts";
|
|
|
10
10
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
11
11
|
import { getPiSpawnCommand } from "../../runtime/pi-spawn.ts";
|
|
12
12
|
import { validateResources } from "../validate-resources.ts";
|
|
13
|
+
import { TeamToolParams } from "../../schema/team-tool-schema.ts";
|
|
13
14
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
14
15
|
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
15
16
|
|
|
@@ -59,6 +60,24 @@ function checkWritableDir(dir: string): { ok: boolean; detail: string } {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
function auditJsonSchema(schema: unknown): string[] {
|
|
64
|
+
const issues: string[] = [];
|
|
65
|
+
const walk = (node: unknown): void => {
|
|
66
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
|
67
|
+
const record = node as Record<string, unknown>;
|
|
68
|
+
if (Array.isArray(record.type)) issues.push("schema node uses array-valued type");
|
|
69
|
+
if (record.description && !record.type && !record.anyOf && !record.oneOf && !record.allOf && !record.properties) issues.push(`description-only schema node: ${record.description}`);
|
|
70
|
+
if (record.type === "array" && !record.items) issues.push("array schema missing items");
|
|
71
|
+
if (record.type && (record.anyOf || record.oneOf)) issues.push("schema node combines type with union keyword");
|
|
72
|
+
for (const value of Object.values(record)) {
|
|
73
|
+
if (Array.isArray(value)) for (const item of value) walk(item);
|
|
74
|
+
else walk(value);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
walk(schema);
|
|
78
|
+
return issues;
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
function makeLine(check: DoctorCheck): string {
|
|
63
82
|
return `- ${check.ok ? "OK" : "FAIL"} ${check.label}: ${check.detail}`;
|
|
64
83
|
}
|
|
@@ -130,6 +149,18 @@ export function buildTeamDoctorReport(input: TeamDoctorReportInput): TeamDoctorR
|
|
|
130
149
|
ok: input.validationErrors === 0,
|
|
131
150
|
detail: `${input.validationErrors} errors, ${input.validationWarnings} warnings`,
|
|
132
151
|
}]),
|
|
152
|
+
section("Schema", () => {
|
|
153
|
+
const schemaIssues = auditJsonSchema(TeamToolParams);
|
|
154
|
+
return [{ label: "strict-provider schema", ok: schemaIssues.length === 0, detail: schemaIssues.length ? schemaIssues.slice(0, 3).join("; ") : "team tool schema compatible" }];
|
|
155
|
+
}),
|
|
156
|
+
section("Async/result delivery", () => [
|
|
157
|
+
{ label: "result watcher", ok: true, detail: "fs.watch with polling fallback for EMFILE/ENOSPC/EPERM" },
|
|
158
|
+
{ label: "async notifier", ok: true, detail: "session-stale guarded completion notifications enabled" },
|
|
159
|
+
]),
|
|
160
|
+
section("Worktrees", () => [
|
|
161
|
+
{ label: "leader repository", ok: true, detail: input.cwd },
|
|
162
|
+
{ label: "cleanup policy", ok: true, detail: "dirty worktrees preserved unless force is set" },
|
|
163
|
+
]),
|
|
133
164
|
];
|
|
134
165
|
if (input.smokeChildPi) {
|
|
135
166
|
sections.push([`Child check`, `- ${input.smokeChildPi.ok ? "OK" : "FAIL"} child Pi smoke: ${input.smokeChildPi.detail}`]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { loadConfig } from "../../config/config.ts";
|
|
2
2
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
3
3
|
import { appendEvent, readEvents } from "../../state/event-log.ts";
|
|
4
|
+
import { readDeliveryState, readMailbox } from "../../state/mailbox.ts";
|
|
4
5
|
import { loadRunManifestById, updateRunStatus } from "../../state/state-store.ts";
|
|
5
6
|
import { aggregateUsage, formatUsage } from "../../state/usage.ts";
|
|
6
7
|
import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from "../../runtime/agent-control.ts";
|
|
@@ -27,15 +28,32 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
27
28
|
}
|
|
28
29
|
const counts = new Map<string, number>();
|
|
29
30
|
for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
|
|
30
|
-
const
|
|
31
|
+
const allEvents = readEvents(manifest.eventsPath);
|
|
32
|
+
const events = allEvents.slice(-8);
|
|
33
|
+
const attentionByTask = new Map(allEvents.filter((event) => event.type === "task.attention" && event.taskId).map((event) => [event.taskId!, event]));
|
|
31
34
|
const controlConfig = resolveCrewControlConfig(loadConfig(ctx.cwd).config);
|
|
32
35
|
const crewAgents = readCrewAgents(manifest).map((agent) => applyAttentionState(manifest, agent, controlConfig));
|
|
33
36
|
const artifactLines = manifest.artifacts.slice(-10).map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}`);
|
|
37
|
+
const deliveryState = readDeliveryState(manifest);
|
|
38
|
+
const ackTimeoutMs = loadConfig(ctx.cwd).config.runtime?.groupJoinAckTimeoutMs;
|
|
39
|
+
const groupJoinLines = readMailbox(manifest, "outbox")
|
|
40
|
+
.filter((message) => message.data?.kind === "group_join")
|
|
41
|
+
.slice(-5)
|
|
42
|
+
.map((message) => {
|
|
43
|
+
const ack = deliveryState.messages[message.id] === "acknowledged" ? "acknowledged" : "pending";
|
|
44
|
+
const ageMs = Date.now() - new Date(message.createdAt).getTime();
|
|
45
|
+
const requestId = String(message.data?.requestId ?? "unknown");
|
|
46
|
+
const timedOut = ack === "pending" && ackTimeoutMs !== undefined && Number.isFinite(ageMs) && ageMs > ackTimeoutMs;
|
|
47
|
+
if (timedOut && !allEvents.some((event) => event.type === "agent.group_join.ack_timeout" && event.data?.requestId === requestId)) {
|
|
48
|
+
appendEvent(manifest.eventsPath, { type: "agent.group_join.ack_timeout", runId: manifest.runId, message: "Group join delivery ack timed out; mailbox delivery remains the fallback.", data: { requestId, messageId: message.id, batchId: message.data?.batchId, partial: message.data?.partial, ageMs, ackTimeoutMs } });
|
|
49
|
+
}
|
|
50
|
+
return `- ${String(message.data?.partial) === "true" ? "partial" : "completed"} request=${requestId} message=${message.id} ack=${timedOut ? "timeout" : ack}`;
|
|
51
|
+
});
|
|
34
52
|
const totalUsage = aggregateUsage(tasks);
|
|
35
53
|
const activeAgents = crewAgents.filter((agent) => agent.status === "running");
|
|
36
54
|
const completedAgents = crewAgents.filter((agent) => agent.status !== "running");
|
|
37
55
|
const waitingTasks = tasks.filter((task) => task.status === "queued");
|
|
38
|
-
const agentLine = (agent: typeof crewAgents[number]): string => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.model ? ` model=${agent.model}` : ""}${agent.usage ? ` usage=${formatUsage(agent.usage)}` : ""}${agent.progress?.activityState
|
|
56
|
+
const agentLine = (agent: typeof crewAgents[number]): string => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.model ? ` model=${agent.model}` : ""}${agent.usage ? ` usage=${formatUsage(agent.usage)}` : ""}${agent.progress?.activityState ? ` activityState=${agent.progress.activityState}` : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${!agent.usage && agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.outputPath ? ` output=${agent.outputPath}` : ""}${agent.transcriptPath ? ` transcript=${agent.transcriptPath}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`;
|
|
39
57
|
const lines = [
|
|
40
58
|
`Run: ${manifest.runId}`,
|
|
41
59
|
`Team: ${manifest.team}`,
|
|
@@ -51,7 +69,7 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
51
69
|
"Task graph:",
|
|
52
70
|
...formatTaskGraphLines(tasks),
|
|
53
71
|
"Tasks:",
|
|
54
|
-
...(tasks.length ? tasks.map((task) => `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.modelRouting ? ` modelRouting=${task.modelRouting.requested ? `${task.modelRouting.requested}->` : ""}${task.modelRouting.resolved}${task.modelRouting.usedAttempt ? ` attempt=${task.modelRouting.usedAttempt + 1}` : ""}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`) : ["- (none)"]),
|
|
72
|
+
...(tasks.length ? tasks.map((task) => `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.modelRouting ? ` modelRouting=${task.modelRouting.requested ? `${task.modelRouting.requested}->` : ""}${task.modelRouting.resolved}${task.modelRouting.usedAttempt ? ` attempt=${task.modelRouting.usedAttempt + 1}` : ""}` : ""}${task.agentProgress?.activityState ? ` activityState=${task.agentProgress.activityState}` : ""}${attentionByTask.get(task.id)?.data?.reason ? ` attention=${String(attentionByTask.get(task.id)?.data?.reason)}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.resultArtifact ? ` result=${task.resultArtifact.path}` : ""}${task.transcriptArtifact ? ` transcript=${task.transcriptArtifact.path}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`) : ["- (none)"]),
|
|
55
73
|
`Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
|
|
56
74
|
"Active agents:",
|
|
57
75
|
...(activeAgents.length ? activeAgents.map(agentLine) : ["- (none)"]),
|
|
@@ -62,6 +80,8 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
62
80
|
"Policy decisions:",
|
|
63
81
|
...(manifest.policyDecisions?.length ? manifest.policyDecisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`) : ["- (none)"]),
|
|
64
82
|
`Total usage: ${formatUsage(totalUsage)}`,
|
|
83
|
+
"Group joins:",
|
|
84
|
+
...(groupJoinLines.length ? groupJoinLines : ["- (none)"]),
|
|
65
85
|
"",
|
|
66
86
|
"Recent artifacts:",
|
|
67
87
|
...(artifactLines.length ? artifactLines : ["- (none)"]),
|
|
@@ -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";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PiTeamsConfig } from "../config/config.ts";
|
|
2
2
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { appendTaskAttentionEvent } from "./attention-events.ts";
|
|
4
4
|
import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
|
|
5
5
|
import { upsertCrewAgent } from "./crew-agent-records.ts";
|
|
6
6
|
|
|
@@ -53,12 +53,11 @@ export function applyAttentionState(manifest: TeamRunManifest, agent: CrewAgentR
|
|
|
53
53
|
},
|
|
54
54
|
};
|
|
55
55
|
upsertCrewAgent(manifest, updated);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
runId: manifest.runId,
|
|
56
|
+
appendTaskAttentionEvent({
|
|
57
|
+
manifest,
|
|
59
58
|
taskId: agent.taskId,
|
|
60
59
|
message: `${agent.agent} needs attention (no observed activity for ${Math.floor(age / 1000)}s).`,
|
|
61
|
-
data: {
|
|
60
|
+
data: { activityState: "needs_attention", reason: "idle", elapsedMs: age, taskId: agent.taskId, agentName: agent.agent, suggestedAction: "Check worker status, wait, steer, or cancel if needed." },
|
|
62
61
|
});
|
|
63
62
|
return updated;
|
|
64
63
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { appendEvent, readEvents } from "../state/event-log.ts";
|
|
2
|
+
import type { CrewAttentionEventData, TeamRunManifest } from "../state/types.ts";
|
|
3
|
+
|
|
4
|
+
export interface AppendTaskAttentionInput {
|
|
5
|
+
manifest: TeamRunManifest;
|
|
6
|
+
taskId?: string;
|
|
7
|
+
message: string;
|
|
8
|
+
data: CrewAttentionEventData;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function appendTaskAttentionEvent(input: AppendTaskAttentionInput): boolean {
|
|
12
|
+
const recent = readEvents(input.manifest.eventsPath).slice(-100);
|
|
13
|
+
const duplicate = recent.some((event) => event.type === "task.attention" && event.taskId === input.taskId && event.data?.reason === input.data.reason && event.data?.activityState === input.data.activityState);
|
|
14
|
+
if (duplicate) return false;
|
|
15
|
+
appendEvent(input.manifest.eventsPath, {
|
|
16
|
+
type: "task.attention",
|
|
17
|
+
runId: input.manifest.runId,
|
|
18
|
+
taskId: input.taskId,
|
|
19
|
+
message: input.message,
|
|
20
|
+
data: { ...input.data },
|
|
21
|
+
});
|
|
22
|
+
return true;
|
|
23
|
+
}
|
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 {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
export interface CompletionMutationGuardInput {
|
|
4
|
+
role: string;
|
|
5
|
+
taskText?: string;
|
|
6
|
+
transcriptPath?: string;
|
|
7
|
+
stdout?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CompletionMutationGuardResult {
|
|
11
|
+
expectedMutation: boolean;
|
|
12
|
+
observedMutation: boolean;
|
|
13
|
+
reason?: "no_mutation_observed";
|
|
14
|
+
observedTools: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const MUTATING_ROLES = new Set(["executor", "test-engineer"]);
|
|
18
|
+
const MUTATING_TOOLS = new Set(["edit", "write", "multi_edit", "apply_patch"]);
|
|
19
|
+
const READ_ONLY_COMMANDS = /^(pwd|ls|dir|cat|type|sed|grep|rg|find|git\s+(status|diff|log|show|branch|remote|rev-parse|ls-files)|npm\s+(test|run\s+(typecheck|check|lint|test|ci))|node\s+--test)\b/i;
|
|
20
|
+
const MUTATING_COMMANDS = /\b(rm\s+-|del\s+|erase\s+|mv\s+|move\s+|cp\s+|copy\s+|mkdir\b|touch\b|git\s+(add|commit|push|reset|clean|checkout|switch|merge|rebase|stash)|npm\s+(install|i|uninstall|publish|version)|pnpm\s+(add|install|remove)|yarn\s+(add|install|remove)|python\b.*>|node\b.*>|echo\b.*>|Set-Content|Out-File)\b/i;
|
|
21
|
+
const READ_ONLY_HINTS = /\b(read-only|no edits?|do not edit|không sửa|khong sua|chỉ đọc|chi doc|plan only|chỉ lập plan|review only|audit only)\b/i;
|
|
22
|
+
|
|
23
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
24
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function commandText(value: unknown): string {
|
|
28
|
+
const record = asRecord(value);
|
|
29
|
+
if (!record) return typeof value === "string" ? value : "";
|
|
30
|
+
for (const key of ["command", "cmd", "script", "input"]) {
|
|
31
|
+
const raw = record[key];
|
|
32
|
+
if (typeof raw === "string") return raw;
|
|
33
|
+
}
|
|
34
|
+
return JSON.stringify(record);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isMutatingTool(tool: string, args: unknown): boolean {
|
|
38
|
+
const normalized = tool.toLowerCase();
|
|
39
|
+
if (MUTATING_TOOLS.has(normalized)) return true;
|
|
40
|
+
if (normalized === "bash" || normalized === "shell" || normalized === "powershell") {
|
|
41
|
+
const command = commandText(args).trim();
|
|
42
|
+
if (!command || READ_ONLY_COMMANDS.test(command)) return false;
|
|
43
|
+
return MUTATING_COMMANDS.test(command);
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function collectToolCallsFromEvent(event: unknown): Array<{ tool: string; args?: unknown }> {
|
|
49
|
+
const record = asRecord(event);
|
|
50
|
+
if (!record) return [];
|
|
51
|
+
const calls: Array<{ tool: string; args?: unknown }> = [];
|
|
52
|
+
const directTool = record.toolName ?? record.name ?? record.tool;
|
|
53
|
+
if (typeof directTool === "string" && (record.type === "tool_execution_start" || record.type === "toolCall" || record.type === "tool_call")) {
|
|
54
|
+
calls.push({ tool: directTool, args: record.args ?? record.input });
|
|
55
|
+
}
|
|
56
|
+
const content = Array.isArray(record.content) ? record.content : asRecord(record.message)?.content;
|
|
57
|
+
if (Array.isArray(content)) {
|
|
58
|
+
for (const part of content) {
|
|
59
|
+
const item = asRecord(part);
|
|
60
|
+
if (!item) continue;
|
|
61
|
+
const tool = item.name ?? item.toolName ?? item.tool;
|
|
62
|
+
if (typeof tool === "string" && (item.type === "toolCall" || item.type === "tool_call" || item.type === "tool_execution_start")) calls.push({ tool, args: item.input ?? item.args });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return calls;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function transcriptText(input: CompletionMutationGuardInput): string {
|
|
69
|
+
if (input.transcriptPath && fs.existsSync(input.transcriptPath)) return fs.readFileSync(input.transcriptPath, "utf-8");
|
|
70
|
+
return input.stdout ?? "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function expectsImplementationMutation(input: Pick<CompletionMutationGuardInput, "role" | "taskText">): boolean {
|
|
74
|
+
if (!MUTATING_ROLES.has(input.role)) return false;
|
|
75
|
+
return !READ_ONLY_HINTS.test(input.taskText ?? "");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function evaluateCompletionMutationGuard(input: CompletionMutationGuardInput): CompletionMutationGuardResult {
|
|
79
|
+
const expectedMutation = expectsImplementationMutation(input);
|
|
80
|
+
const observedTools: string[] = [];
|
|
81
|
+
let observedMutation = false;
|
|
82
|
+
const text = transcriptText(input);
|
|
83
|
+
for (const line of text.split("\n")) {
|
|
84
|
+
const trimmed = line.trim();
|
|
85
|
+
if (!trimmed) continue;
|
|
86
|
+
let event: unknown;
|
|
87
|
+
try { event = JSON.parse(trimmed); } catch { continue; }
|
|
88
|
+
for (const call of collectToolCallsFromEvent(event)) {
|
|
89
|
+
observedTools.push(call.tool);
|
|
90
|
+
if (isMutatingTool(call.tool, call.args)) observedMutation = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
expectedMutation,
|
|
95
|
+
observedMutation,
|
|
96
|
+
observedTools,
|
|
97
|
+
...(expectedMutation && !observedMutation ? { reason: "no_mutation_observed" as const } : {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -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 {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TeamTaskStatus } from "../state/contracts.ts";
|
|
2
|
-
import type { ModelRoutingState, UsageState } from "../state/types.ts";
|
|
2
|
+
import type { CrewActivityState, ModelRoutingState, UsageState } from "../state/types.ts";
|
|
3
3
|
|
|
4
4
|
export type CrewRuntimeKind = "scaffold" | "child-process" | "live-session";
|
|
5
5
|
export type CrewAgentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped";
|
|
@@ -21,7 +21,7 @@ export interface CrewAgentProgress {
|
|
|
21
21
|
turns?: number;
|
|
22
22
|
durationMs?: number;
|
|
23
23
|
lastActivityAt?: string;
|
|
24
|
-
activityState?:
|
|
24
|
+
activityState?: CrewActivityState;
|
|
25
25
|
failedTool?: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -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)) {
|