@claudexor/control-api 3.1.2 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authorization.d.ts +3 -0
- package/dist/authorization.d.ts.map +1 -0
- package/dist/authorization.js +15 -0
- package/dist/authorization.js.map +1 -0
- package/dist/binary-response.d.ts +5 -0
- package/dist/binary-response.d.ts.map +1 -0
- package/dist/binary-response.js +31 -0
- package/dist/binary-response.js.map +1 -0
- package/dist/catalog-query.d.ts +310 -0
- package/dist/catalog-query.d.ts.map +1 -0
- package/dist/catalog-query.js +36 -0
- package/dist/catalog-query.js.map +1 -0
- package/dist/daemon-server.d.ts +28 -17
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +115 -223
- package/dist/daemon-server.js.map +1 -1
- package/dist/decision-accept-risk.d.ts +36 -0
- package/dist/decision-accept-risk.d.ts.map +1 -0
- package/dist/decision-accept-risk.js +79 -0
- package/dist/decision-accept-risk.js.map +1 -0
- package/dist/decision-rerun.d.ts +2 -1
- package/dist/decision-rerun.d.ts.map +1 -1
- package/dist/decision-rerun.js +72 -48
- package/dist/decision-rerun.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/maintenance-routes.d.ts +2 -2
- package/dist/maintenance-routes.d.ts.map +1 -1
- package/dist/maintenance-routes.js +11 -10
- package/dist/maintenance-routes.js.map +1 -1
- package/dist/operation-catalog.d.ts +9 -9
- package/dist/operation-catalog.d.ts.map +1 -1
- package/dist/operation-catalog.js +22 -79
- package/dist/operation-catalog.js.map +1 -1
- package/dist/operation-draft.d.ts +3 -0
- package/dist/operation-draft.d.ts.map +1 -0
- package/dist/operation-draft.js +2 -0
- package/dist/operation-draft.js.map +1 -0
- package/dist/operation-parameters.d.ts.map +1 -1
- package/dist/operation-summaries.d.ts +3 -0
- package/dist/operation-summaries.d.ts.map +1 -0
- package/dist/operation-summaries.js +75 -0
- package/dist/operation-summaries.js.map +1 -0
- package/dist/primary-output.d.ts +3 -3
- package/dist/primary-output.d.ts.map +1 -1
- package/dist/primary-output.js +70 -19
- package/dist/primary-output.js.map +1 -1
- package/dist/problem-response.d.ts.map +1 -1
- package/dist/problem-response.js +14 -9
- package/dist/problem-response.js.map +1 -1
- package/dist/project-routes.d.ts +21 -13
- package/dist/project-routes.d.ts.map +1 -1
- package/dist/project-routes.js +80 -34
- package/dist/project-routes.js.map +1 -1
- package/dist/remote-operation-descriptors.d.ts +36 -0
- package/dist/remote-operation-descriptors.d.ts.map +1 -0
- package/dist/remote-operation-descriptors.js +35 -0
- package/dist/remote-operation-descriptors.js.map +1 -0
- package/dist/resource-routes.d.ts +4 -3
- package/dist/resource-routes.d.ts.map +1 -1
- package/dist/resource-routes.js +63 -39
- package/dist/resource-routes.js.map +1 -1
- package/dist/route-stages.d.ts +15 -0
- package/dist/route-stages.d.ts.map +1 -0
- package/dist/route-stages.js +24 -0
- package/dist/route-stages.js.map +1 -0
- package/dist/run-facts-projection.d.ts +35 -0
- package/dist/run-facts-projection.d.ts.map +1 -0
- package/dist/run-facts-projection.js +133 -0
- package/dist/run-facts-projection.js.map +1 -0
- package/dist/run-list-fingerprint.d.ts +7 -0
- package/dist/run-list-fingerprint.d.ts.map +1 -0
- package/dist/run-list-fingerprint.js +68 -0
- package/dist/run-list-fingerprint.js.map +1 -0
- package/dist/run-record.d.ts +4 -0
- package/dist/run-record.d.ts.map +1 -1
- package/dist/run-record.js.map +1 -1
- package/dist/run-retry-routes.d.ts +3 -2
- package/dist/run-retry-routes.d.ts.map +1 -1
- package/dist/run-retry-routes.js +103 -47
- package/dist/run-retry-routes.js.map +1 -1
- package/dist/run-start.d.ts +13 -1
- package/dist/run-start.d.ts.map +1 -1
- package/dist/run-start.js +85 -18
- package/dist/run-start.js.map +1 -1
- package/dist/run-timeline.d.ts.map +1 -1
- package/dist/run-timeline.js +16 -9
- package/dist/run-timeline.js.map +1 -1
- package/dist/thread-lifecycle-routes.d.ts +1 -1
- package/dist/thread-lifecycle-routes.d.ts.map +1 -1
- package/dist/thread-lifecycle-routes.js +3 -4
- package/dist/thread-lifecycle-routes.js.map +1 -1
- package/dist/thread-mutation.d.ts +6 -0
- package/dist/thread-mutation.d.ts.map +1 -1
- package/dist/thread-mutation.js +23 -0
- package/dist/thread-mutation.js.map +1 -1
- package/dist/thread-projection.d.ts.map +1 -1
- package/dist/thread-projection.js +9 -0
- package/dist/thread-projection.js.map +1 -1
- package/dist/thread-recovery.d.ts +54 -0
- package/dist/thread-recovery.d.ts.map +1 -0
- package/dist/thread-recovery.js +71 -0
- package/dist/thread-recovery.js.map +1 -0
- package/dist/thread-turn-routes.d.ts +22 -8
- package/dist/thread-turn-routes.d.ts.map +1 -1
- package/dist/thread-turn-routes.js +73 -49
- package/dist/thread-turn-routes.js.map +1 -1
- package/package.json +6 -6
package/dist/daemon-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync,
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { basename, extname, join } from "node:path";
|
|
5
5
|
import { deriveApplyEligibility, revertInPlaceFromAnchor, validateApplyGate, verifyAndDeliver, } from "@claudexor/delivery";
|
|
@@ -12,8 +12,8 @@ import { budgetValuationFromEvents, cashEstimatedFromLedgerEvent, normalizeLegac
|
|
|
12
12
|
import { controlWebEvidence, eventPayload, evidenceLevel, latestPlanProgress, readRunEvents, readRunEventsWithIntegrity, timelineEvents, } from "./run-timeline.js";
|
|
13
13
|
import { projectTurnRunCards } from "./thread-projection.js";
|
|
14
14
|
import { projectSession, projectThread, projectTurn } from "./thread-projection.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { handleThreadTurnCreate, handleThreadTurnRetry, } from "./thread-turn-routes.js";
|
|
16
|
+
import { chainIdleRunMutation, chainThreadMutation } from "./thread-mutation.js";
|
|
17
17
|
import { handleThreadLifecycleRoutes, } from "./thread-lifecycle-routes.js";
|
|
18
18
|
import * as runStart from "./run-start.js";
|
|
19
19
|
import { cancelDelegationFamily } from "./delegation-control.js";
|
|
@@ -21,22 +21,30 @@ import { directDelegatedChildrenFromRecords, delegatedDescendantsFromRecords, pa
|
|
|
21
21
|
import { handleRunRetryRoute } from "./run-retry-routes.js";
|
|
22
22
|
import { handleRunApplyRoutes, runIdempotentDelivery, } from "./run-apply-routes.js";
|
|
23
23
|
import { rerunWithFeedback } from "./decision-rerun.js";
|
|
24
|
+
import { acceptRiskDecision } from "./decision-accept-risk.js";
|
|
24
25
|
export { normalizeRunStartRequest } from "./run-start.js";
|
|
25
26
|
import { candidatesFor } from "./candidates.js";
|
|
26
27
|
import { handleProjectRoute } from "./project-routes.js";
|
|
28
|
+
import { writeBinaryResponse } from "./binary-response.js";
|
|
29
|
+
export { inlineContentDisposition } from "./binary-response.js";
|
|
27
30
|
import { handleRecoveryRoute } from "./recovery-routes.js";
|
|
28
31
|
import { handleJournalEventRoute } from "./journal-event-routes.js";
|
|
29
32
|
import { handleMaintenanceRoute } from "./maintenance-routes.js";
|
|
30
33
|
import { handleResourceRoute } from "./resource-routes.js";
|
|
31
34
|
import { handleArtifactServeRoute, listArtifacts, resolveProjectRoot, } from "./artifact-serve-routes.js";
|
|
32
35
|
import { requiredGateSpecsFromTaskArtifact } from "./task-contract-gates.js";
|
|
33
|
-
import {
|
|
36
|
+
import { bearerCredential } from "./authorization.js";
|
|
37
|
+
import { assertOnlyQueryParams, singleQuery } from "./query.js";
|
|
38
|
+
import { parseCredentialProfilesSnapshotQuery, parseHarnessListQuery, parseRunApplicabilityQuery, } from "./catalog-query.js";
|
|
34
39
|
import { lruGet, lruSet, parseRunListQuery, selectRunListPage, } from "./run-list.js";
|
|
40
|
+
import { summaryFingerprint } from "./run-list-fingerprint.js";
|
|
41
|
+
export { resetRunListFingerprintProbeCountForTests, runListFingerprintProbeCountForTests, } from "./run-list-fingerprint.js";
|
|
35
42
|
import { controlProblemError, normalizeRequestValidationError, revertRefusedProblem, thrownProblemCode, } from "./problem-response.js";
|
|
36
43
|
import { handleSecurityRoute } from "./security-routes.js";
|
|
37
|
-
import {
|
|
44
|
+
import { effectiveTerminalFacts, expectedRunFacts, projectRunFactsForDetail, } from "./run-facts-projection.js";
|
|
45
|
+
import { PlanQuestionsArtifact, CouncilProjection, derivePlanReadiness, ControlAuthReadinessRefreshRequest, ControlAuthReadinessRefreshResponse, ControlProblem, AccessProfile, AgentCapabilityCatalog, ControlHarnessListResponse, ControlHarnessModelsResponse, ControlSetupJob, ControlSetupJobCreateRequest, ControlSetupJobEvent, ControlSetupJobSnapshot, isTerminalControlSetupJobState, ControlSetupJobListFilter, ControlSetupJobListResponse, ControlRunStartInfo, ControlRunControlRequest, ControlRunControlResponse, ControlReviewerPanelEntry, ControlRunDetail, ControlRunListResponse, ControlRunSummary, ControlRunResult, ControlBudgetSnapshot, PaidBudget, ControlSettingsSnapshot, ControlSettingsUpdateRequest, ControlQuotaResponse, ControlCredentialProfileCreateRequest, ControlCredentialProfileCreateResponse, ControlCredentialProfileUpdateRequest, ControlCredentialProfileUpdateResponse, ControlCredentialProfileDeleteResponse, ControlInteractionAnswerRequest, ControlInteractionAnswerResponse, ControlRunDecisionRequest, ControlRunDecisionResponse, ControlRunApplicabilityResponse, ControlThreadCreateRequest, ControlThreadTurnRequest, ControlThreadUpdateRequest, ControlThreadDetail, ControlThreadListResponse, DecisionRecord, Id, ModeKind, RoutingGoal, ReviewFinding, RunDeliveryState, RunFailure, RunTelemetry, StructuredOutputConformance, TaskContract, ProtectedPathApproval, isTerminalLifecycle, needsDecision, needsOperatorAttention, outcomeBanner, outcomeFactsFromFailure, TestCommandInvocation, WorkProduct, } from "@claudexor/schema";
|
|
38
46
|
import { resolveControlProtocol } from "./operation-catalog.js";
|
|
39
|
-
import { assertNoInlineSecretValues, containsSecretLikeToken, errorCode, noProjectRepoRoot, nowIso, redactSecrets, sha256, } from "@claudexor/util";
|
|
47
|
+
import { assertNoInlineSecretValues, containsSecretLikeToken, errorCode, noProjectRepoRoot, nowIso, redactSecrets, safeProblemContext, safeProblemRequiredActions, sha256, } from "@claudexor/util";
|
|
40
48
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
41
49
|
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
42
50
|
const NO_PROJECT_ROOT = noProjectRepoRoot();
|
|
@@ -63,6 +71,8 @@ function stringArrayProperty(error, key) {
|
|
|
63
71
|
if (!error || typeof error !== "object" || !(key in error))
|
|
64
72
|
return [];
|
|
65
73
|
const value = error[key];
|
|
74
|
+
if (key === "requiredActions")
|
|
75
|
+
return safeProblemRequiredActions(value);
|
|
66
76
|
return Array.isArray(value)
|
|
67
77
|
? value
|
|
68
78
|
.filter((item) => typeof item === "string" && item.length > 0)
|
|
@@ -100,7 +110,7 @@ function problemBody(error, fallbackCode, fallbackRetryable, fallbackMessage) {
|
|
|
100
110
|
requiredActions: stringArrayProperty(error, "requiredActions"),
|
|
101
111
|
evidenceRefs: stringArrayProperty(error, "evidenceRefs"),
|
|
102
112
|
context: error && typeof error === "object" && "context" in error
|
|
103
|
-
? (error.context
|
|
113
|
+
? safeProblemContext(error.context)
|
|
104
114
|
: {},
|
|
105
115
|
});
|
|
106
116
|
}
|
|
@@ -256,15 +266,12 @@ export class DaemonControlApiServer {
|
|
|
256
266
|
if (!hostIsLoopback(req.headers.host) ||
|
|
257
267
|
!originIsLoopback(req.headers.origin))
|
|
258
268
|
return false;
|
|
259
|
-
|
|
260
|
-
return this.tokenMatches(m?.[1]?.trim());
|
|
269
|
+
return this.tokenMatches(bearerCredential(req.headers.authorization));
|
|
261
270
|
}
|
|
262
|
-
requestError(res, err) {
|
|
263
|
-
// QA-053:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const normalized = normalizeRequestValidationError(err);
|
|
267
|
-
this.problem(res, finiteHttpStatus(normalized, 400), normalized, "invalid_request", false, "bad request");
|
|
271
|
+
requestError(res, err, fallbackStatus = 400) {
|
|
272
|
+
// QA-053: normalize raw Zod issues only at the client-request boundary.
|
|
273
|
+
const normalized = fallbackStatus === 400 ? normalizeRequestValidationError(err) : err;
|
|
274
|
+
this.problem(res, finiteHttpStatus(normalized, fallbackStatus), normalized, fallbackStatus === 400 ? "invalid_request" : "internal_error", false, fallbackStatus === 400 ? "bad request" : "service failed");
|
|
268
275
|
}
|
|
269
276
|
json(res, status, body, contentType = "application/json") {
|
|
270
277
|
if (status >= 400 && contentType === "application/json") {
|
|
@@ -379,21 +386,22 @@ export class DaemonControlApiServer {
|
|
|
379
386
|
services: this.opts.services,
|
|
380
387
|
readBody: (request) => this.readBody(request),
|
|
381
388
|
json: (response, status, body) => this.json(response, status, body),
|
|
382
|
-
requestError: (response, error) => this.requestError(response, error),
|
|
389
|
+
requestError: (response, error, fallback) => this.requestError(response, error, fallback),
|
|
383
390
|
}, method, path, req, res))
|
|
384
391
|
return;
|
|
385
392
|
if (await handleMaintenanceRoute({
|
|
386
393
|
services: this.opts.services,
|
|
387
394
|
readBody: (request) => this.readBody(request),
|
|
388
395
|
json: (response, status, body) => this.json(response, status, body),
|
|
389
|
-
requestError: (response, error) => this.requestError(response, error),
|
|
396
|
+
requestError: (response, error, fallback) => this.requestError(response, error, fallback),
|
|
390
397
|
}, method, path, req, res))
|
|
391
398
|
return;
|
|
392
399
|
if (await handleProjectRoute({
|
|
393
400
|
services: this.opts.services,
|
|
394
401
|
readBody: (request) => this.readBody(request),
|
|
395
402
|
json: (response, status, body) => this.json(response, status, body),
|
|
396
|
-
requestError: (response, error) => this.requestError(response, error),
|
|
403
|
+
requestError: (response, error, fallback) => this.requestError(response, error, fallback),
|
|
404
|
+
binary: writeBinaryResponse,
|
|
397
405
|
}, method, path, req, res))
|
|
398
406
|
return;
|
|
399
407
|
if (method === "POST" && path === "/runs") {
|
|
@@ -415,8 +423,9 @@ export class DaemonControlApiServer {
|
|
|
415
423
|
services: this.opts.services,
|
|
416
424
|
findRun: (id) => this.findRun(id),
|
|
417
425
|
waitForRunStart: (id) => this.waitForRunStart(id),
|
|
426
|
+
serializeThreadMutation: (threadId, work) => chainThreadMutation(this.threadTurnChains, threadId, work),
|
|
418
427
|
json: (response, status, body) => this.json(response, status, body),
|
|
419
|
-
requestError: (response, error) => this.requestError(response, error),
|
|
428
|
+
requestError: (response, error, fallbackStatus) => this.requestError(response, error, fallbackStatus),
|
|
420
429
|
}, method, path, req, res))
|
|
421
430
|
return;
|
|
422
431
|
const runDetailMatch = /^\/runs\/([^/]+)$/.exec(path);
|
|
@@ -487,13 +496,7 @@ export class DaemonControlApiServer {
|
|
|
487
496
|
const idempotencyKey = runStart.requiredIdempotencyKey(req);
|
|
488
497
|
let repoRoot = null;
|
|
489
498
|
if (parsed.scope.kind === "project") {
|
|
490
|
-
repoRoot = parsed.scope.root
|
|
491
|
-
const absoluteRepoError = runStart.validateAbsoluteRepoRoot(repoRoot);
|
|
492
|
-
if (absoluteRepoError)
|
|
493
|
-
throw Object.assign(new Error(absoluteRepoError), { status: 400 });
|
|
494
|
-
if (!existsSync(repoRoot) || !lstatSync(repoRoot).isDirectory()) {
|
|
495
|
-
throw Object.assign(new Error(`project root does not exist or is not a directory: ${repoRoot}`), { status: 400 });
|
|
496
|
-
}
|
|
499
|
+
repoRoot = runStart.normalizeExistingProjectRoot(parsed.scope.root);
|
|
497
500
|
}
|
|
498
501
|
const thread = await svc({
|
|
499
502
|
title: parsed.title,
|
|
@@ -631,70 +634,17 @@ export class DaemonControlApiServer {
|
|
|
631
634
|
return this.requestError(res, err);
|
|
632
635
|
}
|
|
633
636
|
if (body.action === "accept_risk" || body.action === "override_needs_human") {
|
|
634
|
-
const decisionBody = body;
|
|
635
|
-
const decisionAction = body.action;
|
|
636
637
|
try {
|
|
637
|
-
return await
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
? "run reported it needs more input; a risk override cannot supply the missing input — re-run with the input (rerun_with_feedback)"
|
|
648
|
-
: "run reported the work is incomplete; a risk override cannot finish it — re-run until it completes (rerun_with_feedback)",
|
|
649
|
-
code: "work_state_needs_input",
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
// The override unblocks a NEEDS-DECISION run (review blocked /
|
|
653
|
-
// checks failed, D8); recording one elsewhere would claim an apply
|
|
654
|
-
// permission that does not exist.
|
|
655
|
-
if (!this.runNeedsDecision(rec)) {
|
|
656
|
-
return this.json(res, 409, {
|
|
657
|
-
error: rec.state === "succeeded"
|
|
658
|
-
? "run does not need a decision; apply it directly if its review is clean (no risk override needed)"
|
|
659
|
-
: `run is ${rec.state}; risk overrides only unblock needs-decision runs (use rerun_with_feedback instead)`,
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
const patch = readPatch(rec);
|
|
663
|
-
if (patch === null)
|
|
664
|
-
return this.json(res, 409, {
|
|
665
|
-
error: "no patch artifact; there is nothing to unblock for apply",
|
|
666
|
-
});
|
|
667
|
-
const decision = this.recordOperatorDecision(rec, {
|
|
668
|
-
action: decisionAction,
|
|
669
|
-
findingIds: decisionBody.findingIds,
|
|
670
|
-
acceptedRisks: decisionBody.acceptedRisks,
|
|
671
|
-
patchSha256: sha256(patch),
|
|
672
|
-
decidedAt: nowIso(),
|
|
673
|
-
}, {
|
|
674
|
-
key: decisionKey,
|
|
675
|
-
client: "control-api",
|
|
676
|
-
request: { runId: rec.runId ?? rec.id, body: decisionBody },
|
|
677
|
-
});
|
|
678
|
-
try {
|
|
679
|
-
writeOperatorDecisionProjection(rec, decision);
|
|
680
|
-
appendRunAuditEvent(rec, "control.applied", {
|
|
681
|
-
decision: decisionAction,
|
|
682
|
-
finding_ids: decisionBody.findingIds,
|
|
683
|
-
accepted_risks: decisionBody.acceptedRisks,
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
catch {
|
|
687
|
-
// Journal authority remains queryable and replayable by Idempotency-Key.
|
|
688
|
-
}
|
|
689
|
-
return this.json(res, 200, ControlRunDecisionResponse.parse({
|
|
690
|
-
accepted: true,
|
|
691
|
-
status: "applied",
|
|
692
|
-
// Honest ACK (QA-032B): nothing is mutated at decision time. The
|
|
693
|
-
// risk is accepted for this exact patch; Apply is now available
|
|
694
|
-
// and a fresh final check runs just-in-time before any mutation.
|
|
695
|
-
message: `${decisionAction} recorded for this exact patch; Apply is now available and will run a fresh final check before changing the project`,
|
|
696
|
-
}));
|
|
697
|
-
});
|
|
638
|
+
return await acceptRiskDecision({
|
|
639
|
+
services: this.opts.services,
|
|
640
|
+
chainMutation: (record, work) => this.chainRunMutation(record, work),
|
|
641
|
+
workStateVeto: (record) => this.runWorkStateVeto(record),
|
|
642
|
+
needsDecision: (record) => this.runNeedsDecision(record),
|
|
643
|
+
readPatch,
|
|
644
|
+
writeProjection: writeOperatorDecisionProjection,
|
|
645
|
+
appendAudit: (record, payload) => appendRunAuditEvent(record, "control.applied", payload),
|
|
646
|
+
json: (response, status, responseBody) => this.json(response, status, responseBody),
|
|
647
|
+
}, rec, body, decisionKey, res);
|
|
698
648
|
}
|
|
699
649
|
catch (error) {
|
|
700
650
|
return this.requestError(res, error);
|
|
@@ -812,28 +762,23 @@ export class DaemonControlApiServer {
|
|
|
812
762
|
return this.requestError(res, error);
|
|
813
763
|
}
|
|
814
764
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
765
|
+
try {
|
|
766
|
+
return await rerunWithFeedback({
|
|
767
|
+
daemon: this.opts.daemon,
|
|
768
|
+
services: this.opts.services,
|
|
769
|
+
serializeThreadMutation: (threadId, work) => chainThreadMutation(this.threadTurnChains, threadId, work),
|
|
770
|
+
waitForRunStart: (id) => this.waitForRunStart(id),
|
|
771
|
+
appendAudit: (record, payload) => appendRunAuditEvent(record, "control.applied", payload),
|
|
772
|
+
json: (response, status, responseBody) => this.json(response, status, responseBody),
|
|
773
|
+
}, rec, body, decisionKey, res);
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
return this.requestError(res, error, 500);
|
|
777
|
+
}
|
|
822
778
|
}
|
|
823
779
|
if (method === "GET" && path === "/harnesses") {
|
|
824
780
|
try {
|
|
825
|
-
|
|
826
|
-
const fresh = optionalBooleanQuery(url, "fresh");
|
|
827
|
-
const includeFakes = optionalBooleanQuery(url, "all");
|
|
828
|
-
const harnessIds = url.searchParams
|
|
829
|
-
.getAll("harness")
|
|
830
|
-
.map((id) => id.trim())
|
|
831
|
-
.filter(Boolean);
|
|
832
|
-
return this.service(res, "harnesses", {
|
|
833
|
-
...(fresh === undefined ? {} : { fresh }),
|
|
834
|
-
...(includeFakes === undefined ? {} : { includeFakes }),
|
|
835
|
-
...(harnessIds.length === 0 ? {} : { harnessIds }),
|
|
836
|
-
}, ControlHarnessListResponse);
|
|
781
|
+
return this.service(res, "harnesses", parseHarnessListQuery(url), ControlHarnessListResponse);
|
|
837
782
|
}
|
|
838
783
|
catch (err) {
|
|
839
784
|
return this.requestError(res, err);
|
|
@@ -841,6 +786,14 @@ export class DaemonControlApiServer {
|
|
|
841
786
|
}
|
|
842
787
|
if (method === "GET" && path === "/agent-capabilities")
|
|
843
788
|
return this.service(res, "agentCapabilities", undefined, AgentCapabilityCatalog);
|
|
789
|
+
if (method === "GET" && path === "/run-applicability") {
|
|
790
|
+
try {
|
|
791
|
+
return this.service(res, "runApplicability", parseRunApplicabilityQuery(url), ControlRunApplicabilityResponse);
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
return this.requestError(res, error);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
844
797
|
const harnessModelsMatch = /^\/harnesses\/([^/]+)\/models$/.exec(path);
|
|
845
798
|
if (method === "GET" && harnessModelsMatch) {
|
|
846
799
|
try {
|
|
@@ -974,8 +927,15 @@ export class DaemonControlApiServer {
|
|
|
974
927
|
return this.service(res, "quota", undefined, ControlQuotaResponse);
|
|
975
928
|
if (method === "POST" && path === "/quota")
|
|
976
929
|
return this.service(res, "refreshQuota", undefined, ControlQuotaResponse);
|
|
977
|
-
if (method === "GET" && path === "/credential-profiles")
|
|
978
|
-
|
|
930
|
+
if (method === "GET" && path === "/credential-profiles") {
|
|
931
|
+
try {
|
|
932
|
+
const query = parseCredentialProfilesSnapshotQuery(url);
|
|
933
|
+
return this.service(res, "credentialProfiles", query.input, query.schema);
|
|
934
|
+
}
|
|
935
|
+
catch (error) {
|
|
936
|
+
return this.requestError(res, error);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
979
939
|
if (method === "POST" && path === "/credential-profiles") {
|
|
980
940
|
let body;
|
|
981
941
|
try {
|
|
@@ -1410,23 +1370,8 @@ export class DaemonControlApiServer {
|
|
|
1410
1370
|
const state = facts.work_state?.state;
|
|
1411
1371
|
return state === "needs_input" || state === "incomplete" ? state : null;
|
|
1412
1372
|
}
|
|
1413
|
-
recordOperatorDecision(rec, decision, idempotency) {
|
|
1414
|
-
const record = this.opts.services?.recordOperatorDecision;
|
|
1415
|
-
if (!record) {
|
|
1416
|
-
throw Object.assign(new Error("operator decisions are not supported by this engine build"), {
|
|
1417
|
-
status: 501,
|
|
1418
|
-
});
|
|
1419
|
-
}
|
|
1420
|
-
return record(rec.runId ?? rec.id, rec.params, decision, idempotency);
|
|
1421
|
-
}
|
|
1422
1373
|
chainRunMutation(rec, work) {
|
|
1423
|
-
|
|
1424
|
-
if (!threadId)
|
|
1425
|
-
return work();
|
|
1426
|
-
return chainThreadMutation(this.threadTurnRouteCtx(), threadId, async () => {
|
|
1427
|
-
await assertThreadIdle(rec, () => this.opts.daemon.list());
|
|
1428
|
-
return work();
|
|
1429
|
-
});
|
|
1374
|
+
return chainIdleRunMutation(this.threadTurnChains, this.opts.daemon, rec, work);
|
|
1430
1375
|
}
|
|
1431
1376
|
threadTurnRouteCtx() {
|
|
1432
1377
|
const services = this.opts.services ?? {};
|
|
@@ -1437,10 +1382,12 @@ export class DaemonControlApiServer {
|
|
|
1437
1382
|
resolveRunArtifactPath: async (runId, rel) => this.resolveRunArtifactPath(runId, rel),
|
|
1438
1383
|
normalizeStart: runStart.normalizeRunStart,
|
|
1439
1384
|
preflightRunRequirements: services.preflightRunRequirements,
|
|
1385
|
+
preflightThreadRunRequirements: services.preflightThreadRunRequirements,
|
|
1440
1386
|
isTerminalState: (state) => TERMINAL_STATES.has(state),
|
|
1441
1387
|
daemon: this.opts.daemon,
|
|
1442
1388
|
threadDetail: services.threadDetail,
|
|
1443
1389
|
createThreadTurn: services.createThreadTurn,
|
|
1390
|
+
findThreadTurnByIdempotency: services.findThreadTurnByIdempotency,
|
|
1444
1391
|
setTurnEnqueueError: services.setTurnEnqueueError,
|
|
1445
1392
|
threadTurnChains: this.threadTurnChains,
|
|
1446
1393
|
};
|
|
@@ -1654,83 +1601,6 @@ function appendRunAuditEvent(rec, type, payload) {
|
|
|
1654
1601
|
/* audit append must not change control behavior */
|
|
1655
1602
|
}
|
|
1656
1603
|
}
|
|
1657
|
-
/**
|
|
1658
|
-
* QA-052 observability: how many artifact-path probes the run-list fingerprinter
|
|
1659
|
-
* has performed (each probe is a synchronous filesystem stat-family lookup). The
|
|
1660
|
-
* single-parse test pattern (eventsParseCount) proves warm-list work is BOUNDED
|
|
1661
|
-
* — by page size (only sliced records are fingerprinted) and, for terminal runs,
|
|
1662
|
-
* by the one-probe short-circuit below (1 path instead of 12).
|
|
1663
|
-
*/
|
|
1664
|
-
let fingerprintProbeCount = 0;
|
|
1665
|
-
export function runListFingerprintProbeCountForTests() {
|
|
1666
|
-
return fingerprintProbeCount;
|
|
1667
|
-
}
|
|
1668
|
-
export function resetRunListFingerprintProbeCountForTests() {
|
|
1669
|
-
fingerprintProbeCount = 0;
|
|
1670
|
-
}
|
|
1671
|
-
function summaryFingerprint(rec) {
|
|
1672
|
-
const mtime = (rel) => {
|
|
1673
|
-
fingerprintProbeCount++;
|
|
1674
|
-
if (!rec.runDir)
|
|
1675
|
-
return 0;
|
|
1676
|
-
const path = safeArtifactPath(rec.runDir, rel);
|
|
1677
|
-
if (!path)
|
|
1678
|
-
return 0;
|
|
1679
|
-
try {
|
|
1680
|
-
return statSync(path).mtimeMs;
|
|
1681
|
-
}
|
|
1682
|
-
catch {
|
|
1683
|
-
return 0;
|
|
1684
|
-
}
|
|
1685
|
-
};
|
|
1686
|
-
const identity = [rec.state, paramsFingerprint(rec), rec.finishedAt ?? "", rec.error ?? ""];
|
|
1687
|
-
// QA-052 terminal short-circuit: a TERMINAL run's every projected artifact
|
|
1688
|
-
// (events.jsonl, arbitration, telemetry, failure, the final/* outputs,
|
|
1689
|
-
// work_product) is written before the daemon marks the run terminal and never
|
|
1690
|
-
// changes afterward. The SOLE post-terminal mutation is the delivery/apply
|
|
1691
|
-
// overlay (final/delivery_state.yaml), which the apply/revert route is the one
|
|
1692
|
-
// writer of [B7]. So a warm re-list of a terminal run fingerprints ONE path
|
|
1693
|
-
// instead of twelve — the cheap probe still detects an apply/revert, and every
|
|
1694
|
-
// other axis is frozen. Active (queued/running) runs keep the full
|
|
1695
|
-
// artifact-mtime fingerprint because their events.jsonl and final/* land while
|
|
1696
|
-
// the run is still live.
|
|
1697
|
-
if (TERMINAL_STATES.has(rec.state)) {
|
|
1698
|
-
return [...identity, mtime("final/delivery_state.yaml")].join("|");
|
|
1699
|
-
}
|
|
1700
|
-
return [
|
|
1701
|
-
...identity,
|
|
1702
|
-
mtime("events.jsonl"),
|
|
1703
|
-
mtime("arbitration/decision.yaml"),
|
|
1704
|
-
mtime("final/telemetry.yaml"),
|
|
1705
|
-
mtime("final/failure.yaml"),
|
|
1706
|
-
mtime("final/summary.md"),
|
|
1707
|
-
// Primary outputs feed outputReadyState; a summary cached before the
|
|
1708
|
-
// answer/plan/report/patch landed must invalidate when it does.
|
|
1709
|
-
mtime("final/answer.md"),
|
|
1710
|
-
mtime("final/plan.md"),
|
|
1711
|
-
mtime("final/explore.md"),
|
|
1712
|
-
mtime("final/report.md"),
|
|
1713
|
-
mtime("final/patch.diff"),
|
|
1714
|
-
// The honest outcome (result kind / diffstat / adopted) is projected from
|
|
1715
|
-
// work_product.yaml; a summary cached before it landed must invalidate.
|
|
1716
|
-
mtime("final/work_product.yaml"),
|
|
1717
|
-
// The MUTABLE delivery state (applied / reverted / not_applied) is written
|
|
1718
|
-
// to delivery_state.yaml by markRunApplyState, and controlRunResult prefers
|
|
1719
|
-
// that overlay over work_product.yaml's immutable apply_state. It must be in
|
|
1720
|
-
// the fingerprint set or a summary cached before an apply/revert keeps
|
|
1721
|
-
// reporting the stale applyState (the dedicated apply route writes only this
|
|
1722
|
-
// file, so no other fingerprinted mtime would move). [B7]
|
|
1723
|
-
mtime("final/delivery_state.yaml"),
|
|
1724
|
-
].join("|");
|
|
1725
|
-
}
|
|
1726
|
-
function paramsFingerprint(rec) {
|
|
1727
|
-
try {
|
|
1728
|
-
return sha256(JSON.stringify(rec.params ?? null));
|
|
1729
|
-
}
|
|
1730
|
-
catch {
|
|
1731
|
-
return "unserializable-params";
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
1604
|
/** v0.9 strategy flags projected back so surfaces can tell a race from a repair loop. */
|
|
1735
1605
|
function strategyFromParams(p) {
|
|
1736
1606
|
if (p["untilClean"] === true)
|
|
@@ -1852,6 +1722,13 @@ function runOutcomeFacts(rec, decision, failure,
|
|
|
1852
1722
|
* folded into a DECISION-LESS terminal so a needs_input/incomplete ask
|
|
1853
1723
|
* carries its veto axis just like an arbitrated agent run. */
|
|
1854
1724
|
workState = null) {
|
|
1725
|
+
const legacy = legacyRunOutcomeFacts(rec, decision, failure, workState);
|
|
1726
|
+
if (!legacy)
|
|
1727
|
+
return null;
|
|
1728
|
+
return effectiveTerminalFacts(rec.runDir, legacy, decision, expectedRunFacts(rec)).outcomeFacts;
|
|
1729
|
+
}
|
|
1730
|
+
/** Legacy-only terminal projection retained for active/pre-RunFacts records. */
|
|
1731
|
+
function legacyRunOutcomeFacts(rec, decision, failure, workState = null) {
|
|
1855
1732
|
if (!isTerminalLifecycle(rec.state))
|
|
1856
1733
|
return null;
|
|
1857
1734
|
// Arbitrated runs already carry work_state on decision.facts (INV-116).
|
|
@@ -1901,6 +1778,15 @@ function summarizeRun(rec, eventsSnapshot) {
|
|
|
1901
1778
|
const externalContextPolicy = telemetry?.external_context_policy ?? task?.external_context.policy;
|
|
1902
1779
|
const webEvidence = controlWebEvidence(telemetry, task);
|
|
1903
1780
|
const decision = safeReadStructuredArtifact(rec, "arbitration/decision.yaml", DecisionRecord);
|
|
1781
|
+
const failure = readFailure(rec);
|
|
1782
|
+
const workState = telemetry?.final_attempt_id
|
|
1783
|
+
? (telemetry.attempts.find((a) => a.attempt_id === telemetry.final_attempt_id)?.outcome
|
|
1784
|
+
.work_state ?? null)
|
|
1785
|
+
: null;
|
|
1786
|
+
const legacyOutcome = legacyRunOutcomeFacts(rec, decision, failure, workState);
|
|
1787
|
+
const terminalFacts = legacyOutcome
|
|
1788
|
+
? effectiveTerminalFacts(rec.runDir, legacyOutcome, decision, expectedRunFacts(rec))
|
|
1789
|
+
: { runFacts: null, outcomeFacts: null, decision };
|
|
1904
1790
|
const budget = budgetSnapshot(rec, decision, eventsSnapshot);
|
|
1905
1791
|
const parsedReviewerPanel = Array.isArray(p["reviewerPanel"])
|
|
1906
1792
|
? ControlReviewerPanelEntry.array().safeParse(p["reviewerPanel"])
|
|
@@ -1931,7 +1817,7 @@ function summarizeRun(rec, eventsSnapshot) {
|
|
|
1931
1817
|
state: rec.state,
|
|
1932
1818
|
runDir: rec.runDir,
|
|
1933
1819
|
error: rec.error,
|
|
1934
|
-
failure
|
|
1820
|
+
failure,
|
|
1935
1821
|
project: projectMetadata(rec),
|
|
1936
1822
|
mode: parsedMode.success ? parsedMode.data : undefined,
|
|
1937
1823
|
strategy: strategyFromParams(p),
|
|
@@ -1958,7 +1844,6 @@ function summarizeRun(rec, eventsSnapshot) {
|
|
|
1958
1844
|
// The single engine validator's receipt, projected verbatim — surfaces
|
|
1959
1845
|
// never re-validate the answer (null = no structured-output contract).
|
|
1960
1846
|
outputConformance: outputConformance?.status ?? null,
|
|
1961
|
-
// Route receipt projected verbatim (INV-061 disclosure); never re-derived.
|
|
1962
1847
|
authRoute: telemetry?.auth_route
|
|
1963
1848
|
? {
|
|
1964
1849
|
requested: telemetry.auth_route.requested,
|
|
@@ -1967,6 +1852,7 @@ function summarizeRun(rec, eventsSnapshot) {
|
|
|
1967
1852
|
reason: telemetry.auth_route.reason,
|
|
1968
1853
|
harnessId: telemetry.auth_route.harness_id,
|
|
1969
1854
|
attemptId: telemetry.auth_route.attempt_id,
|
|
1855
|
+
profileId: telemetry.auth_route.profile_id,
|
|
1970
1856
|
modelMismatch: telemetry.auth_route.model_mismatch,
|
|
1971
1857
|
}
|
|
1972
1858
|
: null,
|
|
@@ -1979,13 +1865,10 @@ function summarizeRun(rec, eventsSnapshot) {
|
|
|
1979
1865
|
webEvidence,
|
|
1980
1866
|
requestRequirements: telemetry?.request_requirements ?? [],
|
|
1981
1867
|
toolPermissionPolicy: task?.tool_permission_policy,
|
|
1982
|
-
outputReadyState: outputReadyState(rec, parsedMode.success ? parsedMode.data : null,
|
|
1868
|
+
outputReadyState: outputReadyState(rec, parsedMode.success ? parsedMode.data : null, failure, terminalFacts.runFacts),
|
|
1983
1869
|
toolWarningsTotal: telemetry?.tool_warnings_total ?? 0,
|
|
1984
1870
|
result: controlRunResult(rec),
|
|
1985
|
-
outcomeFacts:
|
|
1986
|
-
? (telemetry.attempts.find((a) => a.attempt_id === telemetry.final_attempt_id)?.outcome
|
|
1987
|
-
.work_state ?? null)
|
|
1988
|
-
: null),
|
|
1871
|
+
outcomeFacts: terminalFacts.outcomeFacts,
|
|
1989
1872
|
route: controlRoute(telemetry, p),
|
|
1990
1873
|
// QA-034: routing rationale projected verbatim from the engine telemetry —
|
|
1991
1874
|
// surfaces never reconstruct the order from prose.
|
|
@@ -2043,23 +1926,29 @@ function detailFor(rec, pendingInteractions = [], cursor, operator = null, child
|
|
|
2043
1926
|
? readRunEventsWithIntegrity(rec)
|
|
2044
1927
|
: { events: [], integrity: undefined };
|
|
2045
1928
|
const summary = summarizeRun(rec, events);
|
|
2046
|
-
//
|
|
2047
|
-
// "something to apply" banner signal (a convergence patch
|
|
2048
|
-
// with no meta.result_kind, so result.kind alone would
|
|
2049
|
-
|
|
1929
|
+
// applyEligibility is non-null exactly when the run has an applyable patch —
|
|
1930
|
+
// the authoritative "something to apply" banner signal (a convergence patch
|
|
1931
|
+
// may carry kind:patch with no meta.result_kind, so result.kind alone would
|
|
1932
|
+
// miss it).
|
|
1933
|
+
const { runFacts, outcomeFacts, applyEligibility, requiredActions } = projectRunFactsForDetail(rec.runDir, summary.outcomeFacts ?? null, operator !== null, () => applyEligibilityFor(rec, operator), expectedRunFacts(rec));
|
|
2050
1934
|
const planProjection = planProjectionFor(rec, summary.mode);
|
|
2051
1935
|
return ControlRunDetail.parse({
|
|
2052
|
-
summary: {
|
|
1936
|
+
summary: {
|
|
1937
|
+
...summary,
|
|
1938
|
+
outcomeFacts,
|
|
1939
|
+
waitingOnUser: pendingInteractions.length > 0,
|
|
1940
|
+
},
|
|
2053
1941
|
children,
|
|
1942
|
+
runFacts,
|
|
2054
1943
|
// Server-owned outcome headline (D18), from the single projection owner —
|
|
2055
1944
|
// surfaces render it verbatim above model prose, never re-derive it.
|
|
2056
|
-
outcomeBanner: outcomeBanner(
|
|
1945
|
+
outcomeBanner: outcomeBanner(outcomeFacts, {
|
|
2057
1946
|
applyState: summary.result.applyState,
|
|
2058
1947
|
hasApplyableChange: applyEligibility !== null,
|
|
2059
1948
|
}),
|
|
2060
1949
|
lastSeq,
|
|
2061
1950
|
artifacts: rec.runDir ? listArtifacts(rec.runDir) : [],
|
|
2062
|
-
primaryOutput: primaryOutput(rec, summary.mode, failure),
|
|
1951
|
+
primaryOutput: primaryOutput(rec, summary.mode, failure, runFacts),
|
|
2063
1952
|
timeline: timelineEvents(rec, events, integrity),
|
|
2064
1953
|
budget: budgetSnapshot(rec, decision, events, integrity),
|
|
2065
1954
|
finalSummary: boundedArtifactText(rec, "final/summary.md"),
|
|
@@ -2083,8 +1972,9 @@ function detailFor(rec, pendingInteractions = [], cursor, operator = null, child
|
|
|
2083
1972
|
// Minimal typed required-actions (GH #29) for a succeeded-but-blocked run,
|
|
2084
1973
|
// from the single status-projection owner: review-blocked / checks-failed /
|
|
2085
1974
|
// needs-decision / work_state needs_input, keyed to the same validated
|
|
2086
|
-
// operator decision the needs-decision + apply gates consult
|
|
2087
|
-
|
|
1975
|
+
// operator decision the needs-decision + apply gates consult — the
|
|
1976
|
+
// canonical terminal actions plus legitimate post-terminal overlays.
|
|
1977
|
+
requiredActions,
|
|
2088
1978
|
});
|
|
2089
1979
|
}
|
|
2090
1980
|
function readTextArtifact(rec, relPath, redact = true) {
|
|
@@ -2272,9 +2162,10 @@ function applyGateError(rec, patch, targetRepoRoot, operatorDecision, finalVerif
|
|
|
2272
2162
|
}
|
|
2273
2163
|
function applyGateInputFor(rec, patch, targetRepoRoot, operatorDecision) {
|
|
2274
2164
|
const decision = safeReadStructuredArtifact(rec, "arbitration/decision.yaml", DecisionRecord);
|
|
2165
|
+
const terminal = effectiveTerminalFacts(rec.runDir, decision?.facts ?? null, decision, expectedRunFacts(rec));
|
|
2275
2166
|
return {
|
|
2276
2167
|
state: rec.state,
|
|
2277
|
-
decision,
|
|
2168
|
+
decision: terminal.decision,
|
|
2278
2169
|
workProduct: safeReadStructuredArtifact(rec, "final/work_product.yaml", WorkProduct),
|
|
2279
2170
|
patch,
|
|
2280
2171
|
originalRepoRoot: runRepoRoot(rec),
|
|
@@ -2287,9 +2178,10 @@ function applyGateInputFor(rec, patch, targetRepoRoot, operatorDecision) {
|
|
|
2287
2178
|
// an already-applied / reverted run gets a terminal eligibility disposition
|
|
2288
2179
|
// instead of a stale "rerun a fresh check" (QA-021).
|
|
2289
2180
|
applyState: controlRunResult(rec).applyState,
|
|
2290
|
-
// D-16 work_state veto (INV-116): thread the model-attested work outcome
|
|
2291
|
-
// the
|
|
2292
|
-
|
|
2181
|
+
// D-16 work_state veto (INV-116): thread the model-attested work outcome
|
|
2182
|
+
// (the canonical terminal facts) so the gate refuses a needs_input/
|
|
2183
|
+
// incomplete winner even with a clean legacy review.
|
|
2184
|
+
workState: terminal.outcomeFacts?.work_state ?? null,
|
|
2293
2185
|
};
|
|
2294
2186
|
}
|
|
2295
2187
|
/**
|