@bridge_gpt/mcp-server 0.2.16 → 0.2.19
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/CONDUCTOR.md +75 -0
- package/README.md +2 -2
- package/build/agent-capabilities/probe-context.js +13 -3
- package/build/agent-capabilities/probes.js +262 -11
- package/build/agent-capabilities/reporter.js +1 -0
- package/build/agents.generated.js +3 -3
- package/build/backend-warnings.js +44 -0
- package/build/claude-settings.js +129 -0
- package/build/commands.generated.js +7 -6
- package/build/conductor/bridge-api-client.js +198 -18
- package/build/conductor/claude-hook.js +22 -4
- package/build/conductor/cli.js +76 -25
- package/build/conductor/deny-enforcement-preflight.js +96 -0
- package/build/conductor/doctor.js +183 -2
- package/build/conductor/done-gate.js +5 -0
- package/build/conductor/epic-reconcile.js +71 -14
- package/build/conductor/epic-runtime.js +839 -67
- package/build/conductor/epic-state.js +524 -63
- package/build/conductor/errors.js +156 -3
- package/build/conductor/event-accessors.js +252 -0
- package/build/conductor/file-scope-guard.js +201 -0
- package/build/conductor/github-mergeability.js +85 -0
- package/build/conductor/local-merge.js +47 -1
- package/build/conductor/merge-identity.js +41 -0
- package/build/conductor/merge-ledger.js +19 -72
- package/build/conductor/plan.js +12 -2
- package/build/conductor/pr-ci-producer.js +17 -2
- package/build/conductor/pr-discovery.js +11 -1
- package/build/conductor/producer-ledger.js +1 -1
- package/build/conductor/store.js +161 -18
- package/build/conductor/supervisor-config.js +4 -39
- package/build/conductor/supervisor-escalation.js +10 -26
- package/build/conductor/supervisor-ledger.js +5 -12
- package/build/conductor/supervisor-merge.js +32 -5
- package/build/conductor/supervisor-message-relay.js +2 -5
- package/build/conductor/supervisor-notification.js +1 -1
- package/build/conductor/supervisor-runtime.js +12 -54
- package/build/conductor/supervisor-state.js +4 -18
- package/build/conductor/supervisor-types.js +2 -2
- package/build/conductor/taxonomy.js +12 -0
- package/build/conductor/tools.js +28 -6
- package/build/conductor/worker-ledger-cli.js +244 -0
- package/build/conductor-bin.js +1800 -5166
- package/build/conductor-claude-hook-bin.js +4 -2
- package/build/doctor.js +40 -0
- package/build/executor/cli.js +229 -0
- package/build/executor/credentials.js +65 -0
- package/build/executor/deps.js +117 -0
- package/build/executor/env.js +79 -0
- package/build/executor/heartbeat.js +59 -0
- package/build/executor/http-client.js +131 -0
- package/build/executor/index.js +10 -0
- package/build/executor/job-errors.js +55 -0
- package/build/executor/job-log-registry.js +110 -0
- package/build/executor/job-runner.js +688 -0
- package/build/executor/job-types.js +60 -0
- package/build/executor/merge-job.js +155 -0
- package/build/executor/observation.js +123 -0
- package/build/executor/permissions.js +79 -0
- package/build/executor/preflight.js +144 -0
- package/build/executor/process.js +81 -0
- package/build/executor/prompt-spec.js +235 -0
- package/build/executor/results.js +134 -0
- package/build/executor/resume-pre-spawn.js +179 -0
- package/build/executor/runner.js +98 -0
- package/build/executor/terminal-mutation.js +34 -0
- package/build/executor/test-clock.js +109 -0
- package/build/executor/types.js +18 -0
- package/build/executor/verdict-artifact.js +53 -0
- package/build/executor/viewer-tabs.js +78 -0
- package/build/executor/watch-cli.js +113 -0
- package/build/executor/worker-command.js +106 -0
- package/build/executor/worker-finalization.js +97 -0
- package/build/executor/worker-log.js +92 -0
- package/build/executor/worktree-gc.js +134 -0
- package/build/executor/worktree-inspection.js +86 -0
- package/build/executor/worktree.js +103 -0
- package/build/index.js +13950 -9669
- package/build/install-bridge.js +25 -8
- package/build/install-doctor.js +387 -0
- package/build/mcp-invoke.js +19 -3
- package/build/mcp-provisioning.js +31 -25
- package/build/mcp-registration-doctor.js +27 -7
- package/build/mcp-server-invocation.js +152 -0
- package/build/pipelines.generated.js +31 -6
- package/build/readme.generated.js +1 -1
- package/build/regression-check.js +53 -1
- package/build/review-tickets.js +175 -21
- package/build/sfcc/reads-site-preference.js +52 -19
- package/build/start-tickets-conductor.js +47 -99
- package/build/start-tickets-prereqs.js +185 -4
- package/build/start-tickets.js +218 -180
- package/build/version.generated.js +1 -1
- package/build/visual-diff-worker.js +313 -0
- package/build/visual-diff.js +632 -0
- package/build/worktree-core.js +202 -0
- package/package.json +10 -6
- package/pipelines/review-ticket.json +24 -2
- package/public/css/main.min.css +3311 -1
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +7924 -1
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +5 -2
|
@@ -74,16 +74,174 @@ export function buildConductorJiraUrl(baseUrl, apiPath, params = {}) {
|
|
|
74
74
|
}
|
|
75
75
|
return url.toString();
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
const CONDUCTOR_BRIDGE_API_ERROR_KINDS = [
|
|
78
|
+
"invalid-input",
|
|
79
|
+
"network",
|
|
80
|
+
"timeout",
|
|
81
|
+
"unauthorized",
|
|
82
|
+
"server",
|
|
83
|
+
"http",
|
|
84
|
+
];
|
|
85
|
+
/** Max length of the sanitized body preview embedded in error diagnostics. */
|
|
86
|
+
const CONDUCTOR_ERROR_PREVIEW_MAX = 200;
|
|
87
|
+
/**
|
|
88
|
+
* Redact obvious secret-shaped tokens from a diagnostic preview string so a
|
|
89
|
+
* backend message that happens to echo a token/header never reaches a log line.
|
|
90
|
+
*/
|
|
91
|
+
function redactErrorPreview(text) {
|
|
92
|
+
return text
|
|
93
|
+
.replace(/sk-[A-Za-z0-9_-]{8,}/g, "[REDACTED]")
|
|
94
|
+
.replace(/(Bearer|X-API-Key|api[_-]?key)\b\s*[:=]?\s*\S+/gi, "$1 [REDACTED]");
|
|
95
|
+
}
|
|
96
|
+
/** Collapse whitespace, redact secrets, and bound a string to the preview cap. */
|
|
97
|
+
function boundedErrorPreview(text) {
|
|
98
|
+
const redacted = redactErrorPreview(text).replace(/\s+/g, " ").trim();
|
|
99
|
+
return redacted.length > CONDUCTOR_ERROR_PREVIEW_MAX
|
|
100
|
+
? `${redacted.slice(0, CONDUCTOR_ERROR_PREVIEW_MAX)}…`
|
|
101
|
+
: redacted;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Extract sanitized diagnostics from a parsed backend error body. Recognizes the
|
|
105
|
+
* FastAPI/backend shapes `{ detail: { error_code, message } }`,
|
|
106
|
+
* `{ detail: "..." }`, and `{ error_code, message }`; any other shape yields a
|
|
107
|
+
* bounded stringified preview. Never throws and never returns raw/unbounded text.
|
|
108
|
+
*/
|
|
109
|
+
export function extractSanitizedErrorDiagnostics(body) {
|
|
110
|
+
if (typeof body === "string") {
|
|
111
|
+
const trimmed = body.trim();
|
|
112
|
+
return trimmed ? { bodyPreview: boundedErrorPreview(trimmed) } : {};
|
|
113
|
+
}
|
|
114
|
+
if (!body || typeof body !== "object") {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
const record = body;
|
|
118
|
+
const detail = record["detail"];
|
|
119
|
+
let errorCode;
|
|
120
|
+
let message;
|
|
121
|
+
if (detail && typeof detail === "object") {
|
|
122
|
+
const d = detail;
|
|
123
|
+
if (typeof d["error_code"] === "string")
|
|
124
|
+
errorCode = d["error_code"];
|
|
125
|
+
if (typeof d["message"] === "string")
|
|
126
|
+
message = d["message"];
|
|
127
|
+
}
|
|
128
|
+
else if (typeof detail === "string") {
|
|
129
|
+
message = detail;
|
|
130
|
+
}
|
|
131
|
+
if (!errorCode && typeof record["error_code"] === "string") {
|
|
132
|
+
errorCode = record["error_code"];
|
|
133
|
+
}
|
|
134
|
+
if (!message && typeof record["message"] === "string") {
|
|
135
|
+
message = record["message"];
|
|
136
|
+
}
|
|
137
|
+
const diagnostics = {};
|
|
138
|
+
if (errorCode)
|
|
139
|
+
diagnostics.errorCode = boundedErrorPreview(errorCode);
|
|
140
|
+
if (message)
|
|
141
|
+
diagnostics.bodyPreview = boundedErrorPreview(message);
|
|
142
|
+
return diagnostics;
|
|
143
|
+
}
|
|
144
|
+
/** Redact exact secret substrings (e.g. the API key from the request headers). */
|
|
145
|
+
function redactDiagnosticValues(diagnostics, secrets) {
|
|
146
|
+
const scrub = (text) => {
|
|
147
|
+
let out = text;
|
|
148
|
+
for (const secret of secrets) {
|
|
149
|
+
if (secret && secret.length >= 4)
|
|
150
|
+
out = out.split(secret).join("[REDACTED]");
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
};
|
|
154
|
+
const out = {};
|
|
155
|
+
if (diagnostics.errorCode)
|
|
156
|
+
out.errorCode = scrub(diagnostics.errorCode);
|
|
157
|
+
if (diagnostics.bodyPreview)
|
|
158
|
+
out.bodyPreview = scrub(diagnostics.bodyPreview);
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Best-effort read of a non-2xx response body into sanitized diagnostics. Never
|
|
163
|
+
* throws — a missing/invalid/non-JSON body simply yields `{}`. Any auth-header
|
|
164
|
+
* values in `headers` (e.g. `X-API-Key`) are additionally redacted from the
|
|
165
|
+
* preview so a body that echoes the credential can never surface it.
|
|
166
|
+
*/
|
|
167
|
+
async function readSanitizedErrorDiagnostics(resp, headers = {}) {
|
|
168
|
+
try {
|
|
169
|
+
const diagnostics = extractSanitizedErrorDiagnostics(await resp.json());
|
|
170
|
+
const secrets = Object.entries(headers)
|
|
171
|
+
.filter(([k]) => /key|authorization|token/i.test(k))
|
|
172
|
+
.map(([, v]) => v);
|
|
173
|
+
return redactDiagnosticValues(diagnostics, secrets);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Sanitized HTTP error for conductor Bridge API calls — never leaks secrets.
|
|
181
|
+
*
|
|
182
|
+
* Construction is backward-compatible in two forms:
|
|
183
|
+
* - `new ConductorBridgeApiError(kind, status?, diagnostics?)` — the canonical
|
|
184
|
+
* form; builds a message from the coarse kind plus optional HTTP status,
|
|
185
|
+
* backend `error_code`, and a bounded secret-redacted message preview.
|
|
186
|
+
* - `new ConductorBridgeApiError("some legacy message")` — a bare message
|
|
187
|
+
* string (not one of the known kinds) is preserved verbatim as the error
|
|
188
|
+
* message, with `kind` defaulting to `"http"` and no diagnostic fields set.
|
|
189
|
+
*/
|
|
78
190
|
export class ConductorBridgeApiError extends Error {
|
|
79
191
|
kind;
|
|
80
192
|
status;
|
|
81
|
-
|
|
82
|
-
|
|
193
|
+
errorCode;
|
|
194
|
+
bodyPreview;
|
|
195
|
+
constructor(kindOrMessage, status, diagnostics) {
|
|
196
|
+
const isKnownKind = CONDUCTOR_BRIDGE_API_ERROR_KINDS.includes(kindOrMessage);
|
|
197
|
+
const errorCode = diagnostics?.errorCode;
|
|
198
|
+
const bodyPreview = diagnostics?.bodyPreview;
|
|
199
|
+
if (isKnownKind) {
|
|
200
|
+
const parts = [
|
|
201
|
+
`Conductor Bridge API request failed (${kindOrMessage}${typeof status === "number" ? `, status ${status}` : ""})`,
|
|
202
|
+
];
|
|
203
|
+
if (errorCode)
|
|
204
|
+
parts.push(`code=${errorCode}`);
|
|
205
|
+
if (bodyPreview)
|
|
206
|
+
parts.push(bodyPreview);
|
|
207
|
+
super(parts.join(": "));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Legacy/backward-compatible bare-message construction.
|
|
211
|
+
super(kindOrMessage);
|
|
212
|
+
}
|
|
83
213
|
this.name = "ConductorBridgeApiError";
|
|
84
|
-
this.kind =
|
|
85
|
-
|
|
214
|
+
this.kind = (isKnownKind ? kindOrMessage : "http");
|
|
215
|
+
if (typeof status === "number")
|
|
216
|
+
this.status = status;
|
|
217
|
+
if (errorCode)
|
|
218
|
+
this.errorCode = errorCode;
|
|
219
|
+
if (bodyPreview)
|
|
220
|
+
this.bodyPreview = bodyPreview;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Build a bounded, secret-free diagnostic string from an unknown thrown error
|
|
225
|
+
* for tick warnings/logs. For a {@link ConductorBridgeApiError} it surfaces the
|
|
226
|
+
* coarse kind, HTTP status, backend error code, and sanitized message preview
|
|
227
|
+
* (never a stack, raw body, or secret). For any other Error it returns the
|
|
228
|
+
* constructor name; non-Errors fall back to `fallback`.
|
|
229
|
+
*/
|
|
230
|
+
export function safeDiagnosticMessage(err, fallback) {
|
|
231
|
+
if (err instanceof ConductorBridgeApiError) {
|
|
232
|
+
const parts = [`kind=${err.kind}`];
|
|
233
|
+
if (typeof err.status === "number")
|
|
234
|
+
parts.push(`status=${err.status}`);
|
|
235
|
+
if (err.errorCode)
|
|
236
|
+
parts.push(`code=${err.errorCode}`);
|
|
237
|
+
if (err.bodyPreview)
|
|
238
|
+
parts.push(err.bodyPreview);
|
|
239
|
+
return parts.join(" ");
|
|
240
|
+
}
|
|
241
|
+
if (err instanceof Error) {
|
|
242
|
+
return err.constructor.name;
|
|
86
243
|
}
|
|
244
|
+
return fallback;
|
|
87
245
|
}
|
|
88
246
|
/** GET auth headers. The API key travels ONLY in a header, never in the URL. */
|
|
89
247
|
function conductorGetHeaders(access) {
|
|
@@ -108,13 +266,14 @@ export async function fetchConductorJsonWithTimeout(url, headers, timeoutMs, fet
|
|
|
108
266
|
throw new ConductorBridgeApiError(controller.signal.aborted ? "timeout" : "network");
|
|
109
267
|
}
|
|
110
268
|
if (!resp.ok) {
|
|
269
|
+
const diagnostics = await readSanitizedErrorDiagnostics(resp, headers);
|
|
111
270
|
if (resp.status === 401 || resp.status === 403) {
|
|
112
|
-
throw new ConductorBridgeApiError("unauthorized", resp.status);
|
|
271
|
+
throw new ConductorBridgeApiError("unauthorized", resp.status, diagnostics);
|
|
113
272
|
}
|
|
114
273
|
if (resp.status >= 500) {
|
|
115
|
-
throw new ConductorBridgeApiError("server", resp.status);
|
|
274
|
+
throw new ConductorBridgeApiError("server", resp.status, diagnostics);
|
|
116
275
|
}
|
|
117
|
-
throw new ConductorBridgeApiError("http", resp.status);
|
|
276
|
+
throw new ConductorBridgeApiError("http", resp.status, diagnostics);
|
|
118
277
|
}
|
|
119
278
|
try {
|
|
120
279
|
return await resp.json();
|
|
@@ -252,13 +411,14 @@ async function fetchConductorJsonWithMethodAndTimeout(method, url, headers, body
|
|
|
252
411
|
throw new ConductorBridgeApiError(controller.signal.aborted ? "timeout" : "network");
|
|
253
412
|
}
|
|
254
413
|
if (!resp.ok) {
|
|
414
|
+
const diagnostics = await readSanitizedErrorDiagnostics(resp, headers);
|
|
255
415
|
if (resp.status === 401 || resp.status === 403) {
|
|
256
|
-
throw new ConductorBridgeApiError("unauthorized", resp.status);
|
|
416
|
+
throw new ConductorBridgeApiError("unauthorized", resp.status, diagnostics);
|
|
257
417
|
}
|
|
258
418
|
if (resp.status >= 500) {
|
|
259
|
-
throw new ConductorBridgeApiError("server", resp.status);
|
|
419
|
+
throw new ConductorBridgeApiError("server", resp.status, diagnostics);
|
|
260
420
|
}
|
|
261
|
-
throw new ConductorBridgeApiError("http", resp.status);
|
|
421
|
+
throw new ConductorBridgeApiError("http", resp.status, diagnostics);
|
|
262
422
|
}
|
|
263
423
|
try {
|
|
264
424
|
return await resp.json();
|
|
@@ -391,6 +551,8 @@ function requireNoSlashPathSegment(value) {
|
|
|
391
551
|
const EPIC_TICKET_STATUS_VALUES = [
|
|
392
552
|
"planned", "ready", "dispatched", "running", "blocked", "abandoned", "done", "ready_for_review",
|
|
393
553
|
"reviewing",
|
|
554
|
+
// BAPI-527: asynchronous post-merge parse state (see EpicTicketStatusValue).
|
|
555
|
+
"parse_pending",
|
|
394
556
|
];
|
|
395
557
|
function requireEpicTicketStatusValue(value) {
|
|
396
558
|
if (typeof value !== "string" || !EPIC_TICKET_STATUS_VALUES.includes(value)) {
|
|
@@ -508,6 +670,26 @@ export async function fetchActiveEpicRuns(access, fetchImpl = globalThis.fetch)
|
|
|
508
670
|
}
|
|
509
671
|
return [];
|
|
510
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* PATCH `/jira/epic-runs/runs/{identifier}` to transition an epic run's
|
|
675
|
+
* lifecycle status. Drives the same backend `update_epic_run` CAS path the
|
|
676
|
+
* manual close-out used: passing `expectedStatus` makes the transition a safe
|
|
677
|
+
* compare-and-swap (a stale status surfaces as a sanitized
|
|
678
|
+
* {@link ConductorBridgeApiError} — HTTP 400 VALIDATION — rather than clobbering
|
|
679
|
+
* a concurrent terminal transition). Prefer passing the concrete run UUID as
|
|
680
|
+
* `epicKey` when the runtime has it. Returns the normalized {@link EpicRunRecord}.
|
|
681
|
+
*/
|
|
682
|
+
export async function updateEpicRunStatus(access, request, fetchImpl = globalThis.fetch) {
|
|
683
|
+
requireNonEmptyString(request.epicKey);
|
|
684
|
+
const url = buildConductorJiraUrl(access.baseUrl, epicRunApiPath(request.epicKey));
|
|
685
|
+
const body = JSON.stringify({
|
|
686
|
+
repo_name: access.repoName,
|
|
687
|
+
status: request.status,
|
|
688
|
+
...(request.expectedStatus ? { expected_status: request.expectedStatus } : {}),
|
|
689
|
+
});
|
|
690
|
+
const parsed = await fetchConductorJsonPatchWithTimeout(url, conductorPostHeaders(access), body, CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
|
|
691
|
+
return parsed;
|
|
692
|
+
}
|
|
511
693
|
// ---------------------------------------------------------------------------
|
|
512
694
|
// Per-ticket CAS status advancement
|
|
513
695
|
// ---------------------------------------------------------------------------
|
|
@@ -746,14 +928,12 @@ export async function getEpicPlan(access, epicKey, planVersion, fetchImpl = glob
|
|
|
746
928
|
const parsed = await fetchConductorJsonWithTimeout(url, conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
|
|
747
929
|
return parsed;
|
|
748
930
|
}
|
|
749
|
-
// ---------------------------------------------------------------------------
|
|
750
|
-
// Parse pipeline helpers (BAPI-415)
|
|
751
|
-
// ---------------------------------------------------------------------------
|
|
752
931
|
/**
|
|
753
|
-
* GET `/jira/parse-status?repo_name=<repo>` and return the
|
|
754
|
-
* status.
|
|
755
|
-
*
|
|
756
|
-
* {@link ConductorBridgeApiError} on any transport/auth/server
|
|
932
|
+
* GET `/jira/parse-status?repo_name=<repo>` and return the durable parse-run
|
|
933
|
+
* status. See {@link ParseRepositoryStatusValue} for the full state set (the
|
|
934
|
+
* status surface distinguishes queued/running from terminal succeeded/failed).
|
|
935
|
+
* Throws a sanitized {@link ConductorBridgeApiError} on any transport/auth/server
|
|
936
|
+
* failure. URL + auth are unchanged from the pre-BAPI-527 two-state client.
|
|
757
937
|
*/
|
|
758
938
|
export async function fetchParseStatus(access, fetchImpl = globalThis.fetch) {
|
|
759
939
|
const url = buildConductorJiraUrl(access.baseUrl, "/parse-status", {
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is a dependency-light command-hook entrypoint registered into a spawned
|
|
5
5
|
* Claude worker's `.claude/settings.local.json` by `start-tickets`. Claude Code
|
|
6
|
-
* invokes it for lifecycle events (SessionStart,
|
|
7
|
-
*
|
|
6
|
+
* invokes it for lifecycle events (SessionStart, SessionEnd, Notification,
|
|
7
|
+
* optionally PreToolUse), passing the native hook JSON on stdin. `Stop` and
|
|
8
|
+
* `SubagentStop` are intentionally NOT registered/emitted: they are per-turn /
|
|
9
|
+
* per-subagent events, not session-terminal (BAPI-507 N-1); only `SessionEnd`
|
|
10
|
+
* is a true session end and maps to `run.stopped`.
|
|
8
11
|
* The writer:
|
|
9
12
|
*
|
|
10
13
|
* 1. reads + parses the native Claude hook payload from stdin,
|
|
@@ -50,15 +53,30 @@ export function resolveClaudeHookEventName(payload) {
|
|
|
50
53
|
/**
|
|
51
54
|
* Map a Claude lifecycle event name to a canonical conductor semantic type.
|
|
52
55
|
* Unknown events map to `null` (the caller then skips emission).
|
|
56
|
+
*
|
|
57
|
+
* BAPI-507 (N-1): `Stop` and `SubagentStop` are intentionally ignored (→ null).
|
|
58
|
+
* Claude Code fires `Stop` at the end of EACH assistant turn and `SubagentStop`
|
|
59
|
+
* at each Task-subagent completion — NOT only at session termination. Mapping
|
|
60
|
+
* them to `run.stopped` folded a ticket to `ready_for_review` from minutes into
|
|
61
|
+
* the session, while the worker was still implementing with no branch pushed and
|
|
62
|
+
* no PR. The only true session-terminal event is `SessionEnd`, which maps to
|
|
63
|
+
* `run.stopped`. (Verified against the Claude Code lifecycle-hook contract:
|
|
64
|
+
* `SessionEnd` fires once when the session ends; `Stop`/`SubagentStop` are
|
|
65
|
+
* per-turn/per-subagent.)
|
|
53
66
|
*/
|
|
54
67
|
export function mapClaudeHookEventToSemanticType(eventName) {
|
|
55
68
|
switch (eventName) {
|
|
56
69
|
case "SessionStart":
|
|
57
70
|
return "run.started";
|
|
58
|
-
case "
|
|
71
|
+
case "SessionEnd":
|
|
72
|
+
// The one true session-terminal event: the worker's session has ended.
|
|
59
73
|
return "run.stopped";
|
|
74
|
+
case "Stop":
|
|
75
|
+
// Per-turn boundary, NOT session end — deliberately ignored (BAPI-507 N-1).
|
|
76
|
+
return null;
|
|
60
77
|
case "SubagentStop":
|
|
61
|
-
|
|
78
|
+
// Per-subagent completion, NOT session end — deliberately ignored.
|
|
79
|
+
return null;
|
|
62
80
|
case "Notification":
|
|
63
81
|
return "agent.notification";
|
|
64
82
|
case "PreToolUse":
|
package/build/conductor/cli.js
CHANGED
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
* `process.exit` itself (the bin wrapper owns that).
|
|
15
15
|
*/
|
|
16
16
|
import { readFileSync, unlinkSync } from "node:fs";
|
|
17
|
-
import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
|
|
17
|
+
import { ConductorValidationError, ConductorEpicTickV1FrozenError, toConductorErrorEnvelope, } from "./errors.js";
|
|
18
18
|
import { emitConductorEvent, purgeConductorLedger, sendWorkerMessage, checkWorkerMessages, } from "./store.js";
|
|
19
|
+
import { isDuplicateConstraintError } from "./producer-ledger.js";
|
|
19
20
|
import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
|
|
20
21
|
import { installConductorGitHooks } from "./git-hooks.js";
|
|
22
|
+
import { runFileScopeGuardCli } from "./file-scope-guard.js";
|
|
21
23
|
import { runPostCommitHookProducer, runReferenceTransactionHookProducer } from "./git-producer.js";
|
|
22
24
|
import { buildConductorDoctorReport, formatConductorDoctorReport } from "./doctor.js";
|
|
23
25
|
import { resolveSupervisorConfig } from "./supervisor-config.js";
|
|
@@ -25,6 +27,21 @@ import { resolveSupervisorConfig } from "./supervisor-config.js";
|
|
|
25
27
|
// pulls in the SQLite store, and a static import here would force every conductor
|
|
26
28
|
// CLI invocation (and every test that mocks ./store.js for the other commands) to
|
|
27
29
|
// resolve the full supervisor/store graph eagerly.
|
|
30
|
+
/**
|
|
31
|
+
* Human-readable error line for a sanitized conductor error envelope. For the
|
|
32
|
+
* BAPI-526 LEDGER_NATIVE_MODULE_LOAD_FAILED envelope ONLY, append a compact,
|
|
33
|
+
* allowlisted `details:` suffix (module / node_version / node_modules_abi) so a
|
|
34
|
+
* CLI operator can see the ABI skew. Never prints arbitrary detail keys, paths,
|
|
35
|
+
* stacks, argv, or raw caught messages.
|
|
36
|
+
*/
|
|
37
|
+
export function formatConductorErrorLine(envelope) {
|
|
38
|
+
if (envelope.error === "LEDGER_NATIVE_MODULE_LOAD_FAILED" && envelope.details) {
|
|
39
|
+
const d = envelope.details;
|
|
40
|
+
return (`${envelope.message} ` +
|
|
41
|
+
`details: module=${d.module} node_version=${d.node_version} node_modules_abi=${d.node_modules_abi}`);
|
|
42
|
+
}
|
|
43
|
+
return envelope.message;
|
|
44
|
+
}
|
|
28
45
|
/** Human-readable usage text for the conductor CLI. */
|
|
29
46
|
export function getConductorUsage() {
|
|
30
47
|
return [
|
|
@@ -46,14 +63,14 @@ export function getConductorUsage() {
|
|
|
46
63
|
" git-hook post-commit Run the post-commit producer (invoked by the installed hook)",
|
|
47
64
|
" git-hook reference-transaction --phase <p> --stdin-file <f>",
|
|
48
65
|
" Run the reference-transaction producer (invoked by the hook)",
|
|
66
|
+
" file-scope-guard Warn-only: compare the branch diff against the declared",
|
|
67
|
+
" touched-file set (always exits 0; never blocks a PR)",
|
|
49
68
|
"",
|
|
50
69
|
"supervise options:",
|
|
51
70
|
" --run-id <id> Run/session identifier to supervise (required)",
|
|
52
71
|
" --wake-interval-ms <n> Deterministic event-poll cadence (clamped 30000..60000)",
|
|
53
72
|
" --global-timeout-ms <n> Total wall-clock ceiling for the run",
|
|
54
|
-
" --llm-budget-calls <n> Max LLM judgment calls per run before degraded-only mode",
|
|
55
73
|
" --escalation-cooldown-ms <n> Min gap between escalations for the same worker+reason",
|
|
56
|
-
" --no-llm Deterministic-only mode (never call the LLM judgment boundary)",
|
|
57
74
|
"",
|
|
58
75
|
"install-git-hooks notes:",
|
|
59
76
|
" Hooks are LOCAL, unversioned, opportunistic, and bypassable. Missing hooks are a",
|
|
@@ -102,6 +119,9 @@ export function getConductorUsage() {
|
|
|
102
119
|
"",
|
|
103
120
|
"doctor / purge options:",
|
|
104
121
|
" --json Print machine-readable JSON",
|
|
122
|
+
" --no-deny-probe (doctor only) Skip the deny-enforcement preflight — no headless",
|
|
123
|
+
" agent is spawned; the deny_enforcement section reports an",
|
|
124
|
+
" explicit skipped state (enforcement UNVERIFIED, never enforced)",
|
|
105
125
|
"",
|
|
106
126
|
"Examples:",
|
|
107
127
|
" conductor emit-event --type run.started --source git-hook --run-id BAPI-393 \\",
|
|
@@ -151,6 +171,7 @@ const VALID_COMMANDS = new Set([
|
|
|
151
171
|
"purge",
|
|
152
172
|
"install-git-hooks",
|
|
153
173
|
"git-hook",
|
|
174
|
+
"file-scope-guard",
|
|
154
175
|
]);
|
|
155
176
|
/**
|
|
156
177
|
* Parse the top-level conductor argv into a subcommand (without a CLI
|
|
@@ -215,6 +236,7 @@ function tokenizeFlags(argv, valueFlags, boolFlags) {
|
|
|
215
236
|
const EMIT_VALUE_FLAGS = new Set([
|
|
216
237
|
"--type",
|
|
217
238
|
"--source",
|
|
239
|
+
"--id",
|
|
218
240
|
"--subject",
|
|
219
241
|
"--run-id",
|
|
220
242
|
"--worker-id",
|
|
@@ -313,6 +335,10 @@ export function parseEmitEventArgs(argv, deps = {}) {
|
|
|
313
335
|
const input = {
|
|
314
336
|
source,
|
|
315
337
|
type: type,
|
|
338
|
+
// Optional caller-supplied deterministic id (BAPI-527): forwarded to the store
|
|
339
|
+
// so a duplicate collides on the `events.id` UNIQUE constraint, giving the
|
|
340
|
+
// worker gate path server-side dedup without an in-process ledger poll.
|
|
341
|
+
id: values.get("--id"),
|
|
316
342
|
subject: values.get("--subject"),
|
|
317
343
|
run_id: values.get("--run-id"),
|
|
318
344
|
worker_id: values.get("--worker-id"),
|
|
@@ -342,7 +368,22 @@ export async function runEmitEventCommand(argv, deps = {}) {
|
|
|
342
368
|
console.log(getConductorUsage());
|
|
343
369
|
return 0;
|
|
344
370
|
}
|
|
345
|
-
|
|
371
|
+
let result;
|
|
372
|
+
try {
|
|
373
|
+
result = await emitConductorEvent(parsed.input);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
// A forwarded deterministic `--id` (BAPI-527) that collides on the events.id
|
|
377
|
+
// UNIQUE constraint is a server-side dedup, not a failure: report it as a
|
|
378
|
+
// structured duplicate so the worker gate path can treat it as a no-op emit
|
|
379
|
+
// without ever polling the ledger in-process.
|
|
380
|
+
if (isDuplicateConstraintError(error)) {
|
|
381
|
+
const dup = { ok: false, reason: "duplicate" };
|
|
382
|
+
console.log(parsed.json ? JSON.stringify(dup) : JSON.stringify(dup, null, 2));
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
346
387
|
if (parsed.json) {
|
|
347
388
|
console.log(JSON.stringify(result));
|
|
348
389
|
}
|
|
@@ -514,21 +555,25 @@ export async function runCheckMessagesCommand(argv) {
|
|
|
514
555
|
return 0;
|
|
515
556
|
}
|
|
516
557
|
const DIAGNOSTIC_BOOL_FLAGS = new Set(["--json", "--help"]);
|
|
558
|
+
const DOCTOR_BOOL_FLAGS = new Set([...DIAGNOSTIC_BOOL_FLAGS, "--no-deny-probe"]);
|
|
517
559
|
/**
|
|
518
560
|
* Run the strictly read-only `doctor` command. Combines ledger health, git hook
|
|
519
561
|
* health, and epic-tick schedule enablement status. `--json` emits the full
|
|
520
562
|
* report with `epic_tick` alongside `git_hooks` at the top level.
|
|
563
|
+
* `--no-deny-probe` skips the deny-enforcement preflight (no headless agent
|
|
564
|
+
* spawn); the report then carries an explicit skipped state, never enforced.
|
|
521
565
|
*/
|
|
522
|
-
export async function runDoctorCommand(argv) {
|
|
523
|
-
const { bools } = tokenizeFlags(argv, new Set(),
|
|
566
|
+
export async function runDoctorCommand(argv, deps = {}) {
|
|
567
|
+
const { bools } = tokenizeFlags(argv, new Set(), DOCTOR_BOOL_FLAGS);
|
|
524
568
|
if (bools.has("--help")) {
|
|
525
569
|
console.log(getConductorUsage());
|
|
526
570
|
return 0;
|
|
527
571
|
}
|
|
528
572
|
// scheduleDeps omitted: buildConductorDoctorReport lazily loads schedule-run.
|
|
529
|
-
|
|
573
|
+
// A caller may inject deps (e.g. a fake deny inspector in tests) to stay hermetic.
|
|
574
|
+
const report = await buildConductorDoctorReport(bools.has("--no-deny-probe") ? { ...deps, skipDenyProbe: true } : deps);
|
|
530
575
|
if (bools.has("--json")) {
|
|
531
|
-
console.log(JSON.stringify({ ...report.ledger, git_hooks: report.git_hooks, epic_tick: report.epic_tick, mcp_profile: report.mcp_profile }));
|
|
576
|
+
console.log(JSON.stringify({ ...report.ledger, git_hooks: report.git_hooks, epic_tick: report.epic_tick, mcp_profile: report.mcp_profile, native_ledger: report.native_ledger, deny_enforcement: report.deny_enforcement }));
|
|
532
577
|
return 0;
|
|
533
578
|
}
|
|
534
579
|
console.log(formatConductorDoctorReport(report));
|
|
@@ -685,7 +730,17 @@ export function applyEpicTickPreToolUseDefault(env = process.env) {
|
|
|
685
730
|
* `conductor doctor` / `emit-event` invocation never eagerly resolves the
|
|
686
731
|
* epic/store graph.
|
|
687
732
|
*/
|
|
688
|
-
export async function runEpicTickCommand(
|
|
733
|
+
export async function runEpicTickCommand(_argv) {
|
|
734
|
+
// BAPI-534 (Epic Conductor v2 slice 0): the v1 epic-tick path is FROZEN. This
|
|
735
|
+
// fail-loud guard is the FIRST executable statement — before argument parsing,
|
|
736
|
+
// `applyEpicTickPreToolUseDefault()`, and any lazy `import("./epic-runtime.js")`
|
|
737
|
+
// — so no v1 tick (or its store/supervisor graph) can ever run. Interactive
|
|
738
|
+
// `/start-tickets` is unaffected. `runConductorCli` catches this named error
|
|
739
|
+
// and formats it consistently (EPIC_TICK_V1_FROZEN → exit 1).
|
|
740
|
+
throw new ConductorEpicTickV1FrozenError();
|
|
741
|
+
}
|
|
742
|
+
/** @deprecated v1 epic-tick body — retained unreachable behind the freeze guard. */
|
|
743
|
+
export async function runEpicTickCommandLegacy(argv) {
|
|
689
744
|
const parsed = parseEpicTickArgs(argv);
|
|
690
745
|
if (parsed.help) {
|
|
691
746
|
console.log(getConductorUsage());
|
|
@@ -761,7 +816,7 @@ export async function runApprovePlanCommand(argv) {
|
|
|
761
816
|
}
|
|
762
817
|
catch (error) {
|
|
763
818
|
const envelope = toConductorErrorEnvelope(error);
|
|
764
|
-
console.error(`Error: ${envelope
|
|
819
|
+
console.error(`Error: ${formatConductorErrorLine(envelope)}`);
|
|
765
820
|
return 1;
|
|
766
821
|
}
|
|
767
822
|
if (parsed.help) {
|
|
@@ -809,7 +864,7 @@ export async function runApprovePlanCommand(argv) {
|
|
|
809
864
|
console.log(JSON.stringify(envelope));
|
|
810
865
|
}
|
|
811
866
|
else {
|
|
812
|
-
console.error(`Error: ${envelope
|
|
867
|
+
console.error(`Error: ${formatConductorErrorLine(envelope)}`);
|
|
813
868
|
}
|
|
814
869
|
return envelope.status >= 500 ? 2 : 1;
|
|
815
870
|
}
|
|
@@ -847,7 +902,7 @@ export async function runEpicStatusCommand(argv) {
|
|
|
847
902
|
}
|
|
848
903
|
catch (error) {
|
|
849
904
|
const envelope = toConductorErrorEnvelope(error);
|
|
850
|
-
console.error(`Error: ${envelope
|
|
905
|
+
console.error(`Error: ${formatConductorErrorLine(envelope)}`);
|
|
851
906
|
return 1;
|
|
852
907
|
}
|
|
853
908
|
if (parsed.help) {
|
|
@@ -913,7 +968,7 @@ export async function runEpicStatusCommand(argv) {
|
|
|
913
968
|
console.log(JSON.stringify(envelope));
|
|
914
969
|
}
|
|
915
970
|
else {
|
|
916
|
-
console.error(`Error: ${envelope
|
|
971
|
+
console.error(`Error: ${formatConductorErrorLine(envelope)}`);
|
|
917
972
|
}
|
|
918
973
|
return envelope.status >= 500 ? 2 : 1;
|
|
919
974
|
}
|
|
@@ -925,10 +980,9 @@ const SUPERVISE_VALUE_FLAGS = new Set([
|
|
|
925
980
|
"--run-id",
|
|
926
981
|
"--wake-interval-ms",
|
|
927
982
|
"--global-timeout-ms",
|
|
928
|
-
"--llm-budget-calls",
|
|
929
983
|
"--escalation-cooldown-ms",
|
|
930
984
|
]);
|
|
931
|
-
const SUPERVISE_BOOL_FLAGS = new Set(["--
|
|
985
|
+
const SUPERVISE_BOOL_FLAGS = new Set(["--help"]);
|
|
932
986
|
/** Parse a required positive-integer flag, raising a sanitized error otherwise. */
|
|
933
987
|
function parsePositiveIntFlag(values, flag) {
|
|
934
988
|
const raw = values.get(flag);
|
|
@@ -946,9 +1000,8 @@ function parsePositiveIntFlag(values, flag) {
|
|
|
946
1000
|
/**
|
|
947
1001
|
* Parse `supervise` flags. `--run-id` is required and must be non-empty /
|
|
948
1002
|
* non-whitespace. Numeric overrides are validated as non-negative integers (the
|
|
949
|
-
* config resolver clamps them to safe bounds)
|
|
950
|
-
*
|
|
951
|
-
* {@link ConductorValidationError}.
|
|
1003
|
+
* config resolver clamps them to safe bounds). Malformed input raises a
|
|
1004
|
+
* sanitized {@link ConductorValidationError}.
|
|
952
1005
|
*/
|
|
953
1006
|
export function parseSuperviseArgs(argv) {
|
|
954
1007
|
const { values, bools } = tokenizeFlags(argv, SUPERVISE_VALUE_FLAGS, SUPERVISE_BOOL_FLAGS);
|
|
@@ -966,14 +1019,9 @@ export function parseSuperviseArgs(argv) {
|
|
|
966
1019
|
const globalTimeout = parsePositiveIntFlag(values, "--global-timeout-ms");
|
|
967
1020
|
if (globalTimeout !== undefined)
|
|
968
1021
|
overrides.global_timeout_ms = globalTimeout;
|
|
969
|
-
const llmCalls = parsePositiveIntFlag(values, "--llm-budget-calls");
|
|
970
|
-
if (llmCalls !== undefined)
|
|
971
|
-
overrides.llm_max_calls = llmCalls;
|
|
972
1022
|
const cooldown = parsePositiveIntFlag(values, "--escalation-cooldown-ms");
|
|
973
1023
|
if (cooldown !== undefined)
|
|
974
1024
|
overrides.escalation_cooldown_ms = cooldown;
|
|
975
|
-
if (bools.has("--no-llm"))
|
|
976
|
-
overrides.llm_enabled = false;
|
|
977
1025
|
return { runId: runIdRaw.trim(), overrides, help: false };
|
|
978
1026
|
}
|
|
979
1027
|
/**
|
|
@@ -990,7 +1038,7 @@ export async function runSuperviseCommand(argv) {
|
|
|
990
1038
|
}
|
|
991
1039
|
const config = resolveSupervisorConfig(parsed.overrides);
|
|
992
1040
|
console.log(`[supervisor] starting run=${parsed.runId} wake=${config.wake_interval_ms}ms ` +
|
|
993
|
-
`global_timeout=${config.global_timeout_ms}ms
|
|
1041
|
+
`global_timeout=${config.global_timeout_ms}ms`);
|
|
994
1042
|
const { runSupervisor } = await import("./supervisor-runtime.js");
|
|
995
1043
|
const result = await runSupervisor({ run_id: parsed.runId, config });
|
|
996
1044
|
return result.exit_code;
|
|
@@ -1056,6 +1104,9 @@ export async function runConductorCli(argv) {
|
|
|
1056
1104
|
return runInstallGitHooksCommand(parsed.argv);
|
|
1057
1105
|
case "git-hook":
|
|
1058
1106
|
return await runGitHookCommand(parsed.argv);
|
|
1107
|
+
case "file-scope-guard":
|
|
1108
|
+
// BAPI-507 (N-2): warn-only worker file-scope guard. Always exits 0.
|
|
1109
|
+
return runFileScopeGuardCli();
|
|
1059
1110
|
default:
|
|
1060
1111
|
console.error('Error: Unknown command. Run "conductor --help" for usage.');
|
|
1061
1112
|
return 1;
|
|
@@ -1063,7 +1114,7 @@ export async function runConductorCli(argv) {
|
|
|
1063
1114
|
}
|
|
1064
1115
|
catch (error) {
|
|
1065
1116
|
const envelope = toConductorErrorEnvelope(error);
|
|
1066
|
-
console.error(`Error: ${envelope
|
|
1117
|
+
console.error(`Error: ${formatConductorErrorLine(envelope)}`);
|
|
1067
1118
|
return envelope.status >= 500 ? 2 : 1;
|
|
1068
1119
|
}
|
|
1069
1120
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executor deny-enforcement preflight (TDD §7 / §11, R8).
|
|
3
|
+
*
|
|
4
|
+
* v2's permission model is "skip-permissions + a deterministic deny-layer": workers
|
|
5
|
+
* run `claude -p … --dangerously-skip-permissions` with a small stable deny set. But
|
|
6
|
+
* whether `permissions.deny` is actually enforced under `--dangerously-skip-permissions`
|
|
7
|
+
* is version-specific and must be PROBED, never assumed. This module exports the single
|
|
8
|
+
* reusable predicate the T3a executor's claim loop calls at startup/preflight before
|
|
9
|
+
* claiming any job: on a failed deny probe with no working fallback it returns
|
|
10
|
+
* `enforced: false`, and the executor must refuse to claim jobs (a fatal finding).
|
|
11
|
+
*
|
|
12
|
+
* This ticket (T9) defines the INTERFACE only; the claim loop that consumes it lands in
|
|
13
|
+
* T3a. The predicate is a thin wrapper over the shared `runDenyEnforcementCheck` — it
|
|
14
|
+
* never re-derives probe logic, never touches the wider probe registry, and never runs
|
|
15
|
+
* the advisory output-format / stream-json probes. It is exception-safe: any unexpected
|
|
16
|
+
* failure resolves to a fatal `enforced: false` result rather than throwing.
|
|
17
|
+
*/
|
|
18
|
+
import { resolveAgentSpec } from "../agent-registry.js";
|
|
19
|
+
import { createProbeContext } from "../agent-capabilities/probe-context.js";
|
|
20
|
+
import { createDefaultAgentCapabilitiesDeps } from "../agent-capabilities/default-deps.js";
|
|
21
|
+
import { runDenyEnforcementCheck } from "../agent-capabilities/probes.js";
|
|
22
|
+
/** Canonical refuse-to-claim directive included in every fatal/degraded-fatal result. */
|
|
23
|
+
const REFUSE_TO_CLAIM_WARNING = "The executor claim loop MUST refuse to claim jobs until settings permissions.deny " +
|
|
24
|
+
"or the PreToolUse fallback enforces the deny layer.";
|
|
25
|
+
/**
|
|
26
|
+
* Run the deny-enforcement preflight and map the shared deny-check outcome onto the
|
|
27
|
+
* standard inspection shape. Never throws — unexpected exceptions become a fatal
|
|
28
|
+
* `enforced: false` result. Always cleans up the probe context's temp dirs.
|
|
29
|
+
*/
|
|
30
|
+
export async function runDenyEnforcementPreflight(opts = {}) {
|
|
31
|
+
try {
|
|
32
|
+
const agent = resolveAgentSpec("claude");
|
|
33
|
+
if (!agent) {
|
|
34
|
+
return {
|
|
35
|
+
enforced: false,
|
|
36
|
+
layer: "none",
|
|
37
|
+
degraded: true,
|
|
38
|
+
warnings: [
|
|
39
|
+
"Could not resolve the 'claude' agent spec for the deny-enforcement preflight.",
|
|
40
|
+
REFUSE_TO_CLAIM_WARNING,
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const deps = opts.capabilityDeps ?? createDefaultAgentCapabilitiesDeps();
|
|
45
|
+
const { ctx, cleanup } = await createProbeContext(deps, agent, opts.timeoutMs);
|
|
46
|
+
try {
|
|
47
|
+
const { result, layer } = await runDenyEnforcementCheck(ctx, {
|
|
48
|
+
model: opts.model,
|
|
49
|
+
timeoutMs: opts.timeoutMs,
|
|
50
|
+
});
|
|
51
|
+
if (result.status === "pass" && layer === "settings-deny") {
|
|
52
|
+
return {
|
|
53
|
+
enforced: true,
|
|
54
|
+
layer: "settings-deny",
|
|
55
|
+
degraded: false,
|
|
56
|
+
warnings: [],
|
|
57
|
+
status: result.status,
|
|
58
|
+
detail: result.detail,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (result.status === "pass" && layer === "pretooluse-hook") {
|
|
62
|
+
return {
|
|
63
|
+
enforced: true,
|
|
64
|
+
layer: "pretooluse-hook",
|
|
65
|
+
degraded: true,
|
|
66
|
+
warnings: [
|
|
67
|
+
"settings permissions.deny was not enforced under --dangerously-skip-permissions; relying on PreToolUse fallback.",
|
|
68
|
+
],
|
|
69
|
+
status: result.status,
|
|
70
|
+
detail: result.detail,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Any other outcome (fail / hang / spawn-error) — deny is not enforced.
|
|
74
|
+
return {
|
|
75
|
+
enforced: false,
|
|
76
|
+
layer: "none",
|
|
77
|
+
degraded: true,
|
|
78
|
+
warnings: [`Deny-layer enforcement probe did not pass: ${result.detail}`, REFUSE_TO_CLAIM_WARNING],
|
|
79
|
+
status: result.status,
|
|
80
|
+
detail: result.detail,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await cleanup();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
89
|
+
return {
|
|
90
|
+
enforced: false,
|
|
91
|
+
layer: "none",
|
|
92
|
+
degraded: true,
|
|
93
|
+
warnings: [`Deny-layer enforcement preflight failed unexpectedly: ${msg}`, REFUSE_TO_CLAIM_WARNING],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|