@tea-agent/loop-agent 0.23.1 → 0.24.1
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 +61 -1
- package/README.md +1 -1
- package/bin/agent-worker.js +0 -0
- package/dist/commands/init.js +2 -2
- package/dist/executors/shell-executor.js +20 -7
- package/dist/shared/operator/capabilities.js +475 -2
- package/dist/task/dag-source-paths.js +50 -0
- package/dist/task/frontend-project-capability.js +316 -0
- package/dist/task/task-demand-routing.js +432 -0
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/artifact-card.js +23 -0
- package/dist/worker/console/chat/chat-event-store.js +495 -0
- package/dist/worker/console/chat/chat-ui-policy.js +25 -0
- package/dist/worker/console/chat/compaction-errors.js +64 -0
- package/dist/worker/console/chat/composer-draft-store.js +45 -0
- package/dist/worker/console/chat/context-panel.js +54 -0
- package/dist/worker/console/chat/contract-apply-receipt-store.js +174 -0
- package/dist/worker/console/chat/explore-tools.js +299 -0
- package/dist/worker/console/chat/human-gate-card.js +37 -0
- package/dist/worker/console/chat/interview-adapter.js +149 -0
- package/dist/worker/console/chat/operation-card.js +23 -0
- package/dist/worker/console/chat/pi-console-config.js +158 -0
- package/dist/worker/console/chat/pi-runtime.js +581 -43
- package/dist/worker/console/chat/repo-browser.js +140 -0
- package/dist/worker/console/chat/repo-walk.js +116 -0
- package/dist/worker/console/chat/resource-loader.js +18 -17
- package/dist/worker/console/chat/routes.js +1359 -65
- package/dist/worker/console/chat/runtime-context.js +24 -0
- package/dist/worker/console/chat/runtime-selection.js +37 -0
- package/dist/worker/console/chat/session-store.js +210 -11
- package/dist/worker/console/chat/shortcuts.js +15 -0
- package/dist/worker/console/chat/tool-adapter.js +81 -194
- package/dist/worker/console/chat/tools.js +72 -48
- package/dist/worker/console/chat/usage.js +37 -0
- package/dist/worker/console/chat/workspace-landing.js +56 -0
- package/dist/worker/console/dag-confirmation.js +42 -8
- package/dist/worker/console/human-gate-token.js +130 -0
- package/dist/worker/console/interview/grill-me.js +28 -5
- package/dist/worker/console/mutation-gate-receipt-store.js +184 -0
- package/dist/worker/console/operation-runner.js +6 -2
- package/dist/worker/console/operation-sse.js +26 -0
- package/dist/worker/console/operator-actions.js +420 -7
- package/dist/worker/console/server.js +14 -2
- package/dist/worker/console/static/assets/index-Cv68Ge5n.js +27 -0
- package/dist/worker/console/static/assets/index-Dp_2sKGk.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observe/routes.js +8 -4
- package/dist/workflows/dag/backend-test-intake-context.js +10 -12
- package/dist/workflows/dag/backend-test-markdown-workflow.js +232 -47
- package/dist/workflows/dag/backend-test-result-contract.js +233 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +23 -17
- package/dist/workflows/dag/frontend-lint-baseline.js +4 -4
- package/dist/workflows/dag/frontend-project-capability.js +1 -316
- package/dist/workflows/dag/frontend-worktree-diff.js +4 -1
- package/dist/workflows/dag/init-hybrid.js +46 -43
- package/dist/workflows/dag/rerun-plan.js +6 -10
- package/dist/workflows/dag/task-demand-routing.js +1 -383
- package/docs/README.md +1 -1
- package/docs/architecture/README.md +5 -5
- package/docs/architecture/evolution.md +4 -4
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/frontend-implementation-contract.schema.json +1 -1
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/frontend-implementation/SKILL.md +7 -0
- package/skills/frontend-implementation/references/node-contracts.md +1 -1
- package/skills/frontend-verification/SKILL.md +4 -3
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/worker/console/static/assets/index-DVl7Jxt5.js +0 -25
- package/dist/worker/console/static/assets/index-lVcIr9Ju.css +0 -1
|
@@ -16,6 +16,13 @@ import { toCanonicalDraftForCli, } from "./draft-store.js";
|
|
|
16
16
|
import { normalizeConsoleWorkflowKind } from "./workflow-kinds.js";
|
|
17
17
|
import { deriveTaskIdentityFromPrd, nextTaskIdRevision, } from "./prd-identity.js";
|
|
18
18
|
import { resolveSiblingAgentWorkerBin } from "./sibling-controller.js";
|
|
19
|
+
import { projectOperationEventSummary, projectOperationForChat, } from "./chat/chat-event-store.js";
|
|
20
|
+
import { MutationGateReceiptStore } from "./mutation-gate-receipt-store.js";
|
|
21
|
+
import { issueHumanGateToken, mutationGatePayloadHash, verifyHumanGateToken, } from "./human-gate-token.js";
|
|
22
|
+
const MUTATION_GATE_ACTIONS = new Set([
|
|
23
|
+
"standaloneTaskRerun",
|
|
24
|
+
"workerTaskRetry",
|
|
25
|
+
]);
|
|
19
26
|
const MUTATION_OR_LONG = new Set([
|
|
20
27
|
"newTask",
|
|
21
28
|
"importPrd",
|
|
@@ -192,6 +199,83 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
192
199
|
body: operatorSucceeded("operator capabilities", doc),
|
|
193
200
|
};
|
|
194
201
|
}
|
|
202
|
+
case "operationGet": {
|
|
203
|
+
const operationId = str(p.operationId);
|
|
204
|
+
if (!operationId)
|
|
205
|
+
return invalid(action, "operationId is required");
|
|
206
|
+
const operation = await ctx.operations.get(operationId);
|
|
207
|
+
if (!operation) {
|
|
208
|
+
return {
|
|
209
|
+
kind: "error",
|
|
210
|
+
status: 404,
|
|
211
|
+
body: operatorFailed({
|
|
212
|
+
command: action,
|
|
213
|
+
outcome: "not-found",
|
|
214
|
+
code: "NOT_FOUND",
|
|
215
|
+
message: `operation not found: ${operationId}`,
|
|
216
|
+
}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
kind: "sync",
|
|
221
|
+
status: 200,
|
|
222
|
+
body: operatorSucceeded(action, projectOperationForChat(operation)),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
case "operationList": {
|
|
226
|
+
const rawLimit = typeof p.limit === "number" ? p.limit : Number(p.limit ?? 20);
|
|
227
|
+
if (!Number.isFinite(rawLimit))
|
|
228
|
+
return invalid(action, "limit must be a number");
|
|
229
|
+
const limit = Math.max(1, Math.min(50, Math.floor(rawLimit)));
|
|
230
|
+
const state = str(p.state);
|
|
231
|
+
const actionFilter = str(p.action);
|
|
232
|
+
const operations = (await ctx.operations.list())
|
|
233
|
+
.filter((operation) => !state || operation.state === state)
|
|
234
|
+
.filter((operation) => !actionFilter || operation.action === actionFilter)
|
|
235
|
+
.slice(-limit)
|
|
236
|
+
.reverse()
|
|
237
|
+
.map(projectOperationForChat);
|
|
238
|
+
return {
|
|
239
|
+
kind: "sync",
|
|
240
|
+
status: 200,
|
|
241
|
+
body: operatorSucceeded(action, { operations, limit }),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
case "operationEventSummary": {
|
|
245
|
+
const operationId = str(p.operationId);
|
|
246
|
+
if (!operationId)
|
|
247
|
+
return invalid(action, "operationId is required");
|
|
248
|
+
const operation = await ctx.operations.get(operationId);
|
|
249
|
+
if (!operation) {
|
|
250
|
+
return {
|
|
251
|
+
kind: "error",
|
|
252
|
+
status: 404,
|
|
253
|
+
body: operatorFailed({
|
|
254
|
+
command: action,
|
|
255
|
+
outcome: "not-found",
|
|
256
|
+
code: "NOT_FOUND",
|
|
257
|
+
message: `operation not found: ${operationId}`,
|
|
258
|
+
}),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const rawLimit = typeof p.limit === "number" ? p.limit : Number(p.limit ?? 20);
|
|
262
|
+
if (!Number.isFinite(rawLimit))
|
|
263
|
+
return invalid(action, "limit must be a number");
|
|
264
|
+
const limit = Math.max(1, Math.min(100, Math.floor(rawLimit)));
|
|
265
|
+
const events = ctx.events
|
|
266
|
+
.snapshot(operationId)
|
|
267
|
+
.slice(-limit)
|
|
268
|
+
.map(projectOperationEventSummary);
|
|
269
|
+
return {
|
|
270
|
+
kind: "sync",
|
|
271
|
+
status: 200,
|
|
272
|
+
body: operatorSucceeded(action, {
|
|
273
|
+
operation: projectOperationForChat(operation),
|
|
274
|
+
events,
|
|
275
|
+
limit,
|
|
276
|
+
}),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
195
279
|
case "inspect": {
|
|
196
280
|
const body = await runReadCli(ctx, ["inspect", "--json"], "inspect");
|
|
197
281
|
return { kind: "sync", status: body.ok ? 200 : 400, body };
|
|
@@ -479,6 +563,16 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
479
563
|
case "prepareDagConfirmation": {
|
|
480
564
|
const dagHandle = str(p.dagHandle) ?? "staged-dag.json";
|
|
481
565
|
const taskId = str(p.taskId);
|
|
566
|
+
const operatorAction = str(p.operatorAction);
|
|
567
|
+
const operatorActionArgs = p.operatorActionArgs;
|
|
568
|
+
if (operatorActionArgs !== undefined && (!Array.isArray(operatorActionArgs) || operatorActionArgs.some((value) => typeof value !== "string"))) {
|
|
569
|
+
return invalid(action, "operatorActionArgs must be an array of strings");
|
|
570
|
+
}
|
|
571
|
+
if (operatorAction) {
|
|
572
|
+
const target = buildOperatorCapabilitiesDocument().actions.find((entry) => entry.action === operatorAction && entry.description.startsWith("Official CLI action coverage") && entry.humanConfirmation === "required");
|
|
573
|
+
if (!target)
|
|
574
|
+
return invalid(action, "operatorAction must name a required official action");
|
|
575
|
+
}
|
|
482
576
|
if (!taskId)
|
|
483
577
|
return invalid(action, "taskId is required");
|
|
484
578
|
let dagText = str(p.dagText) ?? str(p.dagJson);
|
|
@@ -616,6 +710,7 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
616
710
|
taskId,
|
|
617
711
|
dagHandle,
|
|
618
712
|
validationOk: true,
|
|
713
|
+
...(operatorAction ? { operatorAction, operatorActionArgs: operatorActionArgs ?? [] } : {}),
|
|
619
714
|
},
|
|
620
715
|
});
|
|
621
716
|
return {
|
|
@@ -630,7 +725,7 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
630
725
|
}
|
|
631
726
|
case "confirmDagConfirmation": {
|
|
632
727
|
const confirmationId = str(p.confirmationId);
|
|
633
|
-
const
|
|
728
|
+
const humanGateToken = p.humanGateToken;
|
|
634
729
|
if (!confirmationId) {
|
|
635
730
|
return invalid(action, "confirmationId is required");
|
|
636
731
|
}
|
|
@@ -669,7 +764,7 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
669
764
|
const result = await ctx.confirmations.confirm({
|
|
670
765
|
confirmationId,
|
|
671
766
|
operatorSessionId: ctx.operatorSessionId,
|
|
672
|
-
|
|
767
|
+
humanGateToken,
|
|
673
768
|
current: {
|
|
674
769
|
dagSha256: hashDagBytes(dagBytes),
|
|
675
770
|
sourceBindingSha256: conf.sourceBindingSha256,
|
|
@@ -992,6 +1087,13 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
992
1087
|
if (!runId || !reason || !clientRequestId) {
|
|
993
1088
|
return invalid(action, "runId, reason, and clientRequestId are required");
|
|
994
1089
|
}
|
|
1090
|
+
const gated = await consumeMutationGateReceipt(ctx, action, p);
|
|
1091
|
+
if (gated.kind === "error")
|
|
1092
|
+
return gated;
|
|
1093
|
+
if (gated.kind === "idempotent")
|
|
1094
|
+
return gated.result;
|
|
1095
|
+
const profile = str(p.profile);
|
|
1096
|
+
const taskId = str(p.taskId);
|
|
995
1097
|
const cliArgs = [
|
|
996
1098
|
"dag",
|
|
997
1099
|
"rerun-task",
|
|
@@ -1003,19 +1105,19 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1003
1105
|
clientRequestId,
|
|
1004
1106
|
"--json",
|
|
1005
1107
|
];
|
|
1006
|
-
const profile = str(p.profile);
|
|
1007
|
-
const taskId = str(p.taskId);
|
|
1008
1108
|
if (profile)
|
|
1009
1109
|
cliArgs.push("--profile", profile);
|
|
1010
1110
|
if (taskId)
|
|
1011
1111
|
cliArgs.push("--task-id", taskId);
|
|
1012
|
-
|
|
1112
|
+
const accepted = await acceptOperation(ctx, {
|
|
1013
1113
|
action,
|
|
1014
1114
|
actionParams: p,
|
|
1015
1115
|
clientRequestId,
|
|
1016
1116
|
taskId,
|
|
1017
1117
|
cliArgs,
|
|
1018
1118
|
});
|
|
1119
|
+
await finalizeMutationGateReceipt(ctx, gated.receiptId, accepted);
|
|
1120
|
+
return accepted;
|
|
1019
1121
|
}
|
|
1020
1122
|
case "workerTaskRetry": {
|
|
1021
1123
|
const taskId = str(p.taskId);
|
|
@@ -1025,6 +1127,11 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1025
1127
|
if (!taskId || !reason || !clientRequestId) {
|
|
1026
1128
|
return invalid(action, "taskId, reason, and clientRequestId are required");
|
|
1027
1129
|
}
|
|
1130
|
+
const gated = await consumeMutationGateReceipt(ctx, action, p);
|
|
1131
|
+
if (gated.kind === "error")
|
|
1132
|
+
return gated;
|
|
1133
|
+
if (gated.kind === "idempotent")
|
|
1134
|
+
return gated.result;
|
|
1028
1135
|
const args = [
|
|
1029
1136
|
"task",
|
|
1030
1137
|
"retry",
|
|
@@ -1036,7 +1143,7 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1036
1143
|
];
|
|
1037
1144
|
if (featureId)
|
|
1038
1145
|
args.push("--feature-id", featureId);
|
|
1039
|
-
|
|
1146
|
+
const accepted = await acceptOperation(ctx, {
|
|
1040
1147
|
action,
|
|
1041
1148
|
actionParams: p,
|
|
1042
1149
|
clientRequestId,
|
|
@@ -1044,8 +1151,149 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1044
1151
|
cliArgs: args,
|
|
1045
1152
|
externalCommand: resolveSiblingAgentWorkerBin(),
|
|
1046
1153
|
});
|
|
1154
|
+
await finalizeMutationGateReceipt(ctx, gated.receiptId, accepted);
|
|
1155
|
+
return accepted;
|
|
1156
|
+
}
|
|
1157
|
+
case "prepareMutationGate": {
|
|
1158
|
+
const targetAction = str(p.action);
|
|
1159
|
+
const actionParams = p.actionParams && typeof p.actionParams === "object" && !Array.isArray(p.actionParams)
|
|
1160
|
+
? p.actionParams
|
|
1161
|
+
: undefined;
|
|
1162
|
+
if (!targetAction || !actionParams) {
|
|
1163
|
+
return invalid(action, "action and actionParams are required");
|
|
1164
|
+
}
|
|
1165
|
+
if (!MUTATION_GATE_ACTIONS.has(targetAction)) {
|
|
1166
|
+
return invalid(action, "action must be standaloneTaskRerun or workerTaskRetry");
|
|
1167
|
+
}
|
|
1168
|
+
const secret = ctx.humanGateSecret;
|
|
1169
|
+
if (!secret) {
|
|
1170
|
+
return {
|
|
1171
|
+
kind: "error",
|
|
1172
|
+
status: 503,
|
|
1173
|
+
body: operatorFailed({
|
|
1174
|
+
command: action,
|
|
1175
|
+
outcome: "blocked",
|
|
1176
|
+
code: "INTERNAL_ERROR",
|
|
1177
|
+
message: "humanGateSecret is not configured on action context",
|
|
1178
|
+
}),
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
const paramsHash = mutationGatePayloadHash({
|
|
1182
|
+
action: targetAction,
|
|
1183
|
+
operatorSessionId: ctx.operatorSessionId,
|
|
1184
|
+
actionParams,
|
|
1185
|
+
});
|
|
1186
|
+
const expiresAt = new Date(Date.now() + 30 * 60_000).toISOString();
|
|
1187
|
+
const receiptId = `mut_${crypto.randomUUID()}`;
|
|
1188
|
+
const humanGateToken = issueHumanGateToken({ confirmationId: receiptId, payloadHash: paramsHash, expiresAt }, secret);
|
|
1189
|
+
const store = new MutationGateReceiptStore(ctx.appData);
|
|
1190
|
+
const receipt = await store.prepare({
|
|
1191
|
+
receiptId,
|
|
1192
|
+
operatorSessionId: ctx.operatorSessionId,
|
|
1193
|
+
action: targetAction,
|
|
1194
|
+
paramsHash,
|
|
1195
|
+
actionParams,
|
|
1196
|
+
expiresAt,
|
|
1197
|
+
humanGateToken,
|
|
1198
|
+
});
|
|
1199
|
+
return {
|
|
1200
|
+
kind: "sync",
|
|
1201
|
+
status: 200,
|
|
1202
|
+
body: operatorSucceeded("prepareMutationGate", {
|
|
1203
|
+
receiptId: receipt.receiptId,
|
|
1204
|
+
action: receipt.action,
|
|
1205
|
+
expiresAt: receipt.expiresAt,
|
|
1206
|
+
paramsHash: receipt.paramsHash,
|
|
1207
|
+
humanGateToken: receipt.humanGateToken,
|
|
1208
|
+
state: receipt.state,
|
|
1209
|
+
}),
|
|
1210
|
+
};
|
|
1047
1211
|
}
|
|
1048
|
-
default:
|
|
1212
|
+
default: {
|
|
1213
|
+
// M5 official-command coverage uses one generic tail for newly registered
|
|
1214
|
+
// CLI actions. It remains inside dispatchOperatorAction (the sole dispatch
|
|
1215
|
+
// surface) and only accepts argv arrays declared by the capability schema.
|
|
1216
|
+
const capability = buildOperatorCapabilitiesDocument().actions.find((entry) => entry.action === action && entry.description.startsWith("Official CLI action coverage"));
|
|
1217
|
+
if (capability) {
|
|
1218
|
+
const rawArgs = p.args;
|
|
1219
|
+
if (rawArgs !== undefined && (!Array.isArray(rawArgs) || rawArgs.some((value) => typeof value !== "string"))) {
|
|
1220
|
+
return invalid(action, "args must be an array of strings");
|
|
1221
|
+
}
|
|
1222
|
+
if (capability.humanConfirmation === "required") {
|
|
1223
|
+
const confirmationId = str(p.confirmationId);
|
|
1224
|
+
if (!confirmationId)
|
|
1225
|
+
return invalid(action, "confirmationId is required");
|
|
1226
|
+
const conf = await ctx.confirmations.get(confirmationId);
|
|
1227
|
+
if (!conf) {
|
|
1228
|
+
return {
|
|
1229
|
+
kind: "error",
|
|
1230
|
+
status: 404,
|
|
1231
|
+
body: operatorFailed({ command: action, outcome: "not-found", code: "NOT_FOUND", message: `confirmation not found: ${confirmationId}` }),
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
const confirmedArgs = conf.reviewPacket?.operatorActionArgs;
|
|
1235
|
+
const requestedArgs = rawArgs ?? [];
|
|
1236
|
+
if (conf.operatorSessionId !== ctx.operatorSessionId ||
|
|
1237
|
+
conf.reviewPacket?.operatorAction !== action ||
|
|
1238
|
+
!Array.isArray(confirmedArgs) ||
|
|
1239
|
+
JSON.stringify(confirmedArgs) !== JSON.stringify(requestedArgs)) {
|
|
1240
|
+
return {
|
|
1241
|
+
kind: "error",
|
|
1242
|
+
status: 403,
|
|
1243
|
+
body: operatorFailed({ command: action, outcome: "blocked", code: "HUMAN_CONFIRMATION_REQUIRED", message: "confirmation receipt is not bound to this action, arguments, and operator session" }),
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
let receiptBytes;
|
|
1247
|
+
try {
|
|
1248
|
+
receiptBytes = await readFile(conf.dagBytesPath, "utf8");
|
|
1249
|
+
}
|
|
1250
|
+
catch (error) {
|
|
1251
|
+
return {
|
|
1252
|
+
kind: "error",
|
|
1253
|
+
status: 409,
|
|
1254
|
+
body: operatorFailed({ command: action, outcome: "conflict", code: "CONFIRMATION_STALE", message: `cannot re-read confirmation receipt bytes: ${error instanceof Error ? error.message : String(error)}` }),
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
const consumed = await ctx.confirmations.consume({
|
|
1258
|
+
confirmationId,
|
|
1259
|
+
operatorSessionId: ctx.operatorSessionId,
|
|
1260
|
+
current: {
|
|
1261
|
+
dagSha256: hashDagBytes(receiptBytes),
|
|
1262
|
+
sourceBindingSha256: conf.sourceBindingSha256,
|
|
1263
|
+
taskContractBinding: conf.taskContractBinding,
|
|
1264
|
+
controllerFingerprint: ctx.client.getIdentity?.()?.packageFingerprint?.value ?? conf.controllerFingerprint,
|
|
1265
|
+
validationResultSha256: conf.validationResultSha256,
|
|
1266
|
+
},
|
|
1267
|
+
});
|
|
1268
|
+
if (!consumed.ok) {
|
|
1269
|
+
return {
|
|
1270
|
+
kind: "error",
|
|
1271
|
+
status: confirmationErrorStatus(consumed.code),
|
|
1272
|
+
body: operatorFailed({ command: action, outcome: "rejected", code: consumed.code, message: consumed.message }),
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
const prefix = capability.cli.replace(/^(loop-agent|agent-worker)\s+/, "").split(/\s+/);
|
|
1277
|
+
const args = [...prefix, ...(rawArgs ?? [])];
|
|
1278
|
+
if (capability.kind === "read") {
|
|
1279
|
+
if (!args.includes("--json"))
|
|
1280
|
+
args.push("--json");
|
|
1281
|
+
const body = await runReadCli(ctx, args, prefix.join(" "));
|
|
1282
|
+
return { kind: "sync", status: body.ok ? 200 : 400, body };
|
|
1283
|
+
}
|
|
1284
|
+
const clientRequestId = str(req.clientRequestId);
|
|
1285
|
+
if (!clientRequestId)
|
|
1286
|
+
return invalid(action, "clientRequestId is required");
|
|
1287
|
+
return acceptOperation(ctx, {
|
|
1288
|
+
action,
|
|
1289
|
+
actionParams: p,
|
|
1290
|
+
clientRequestId,
|
|
1291
|
+
cliArgs: args,
|
|
1292
|
+
...(capability.cli.startsWith("agent-worker ")
|
|
1293
|
+
? { externalCommand: resolveSiblingAgentWorkerBin() }
|
|
1294
|
+
: {}),
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1049
1297
|
return {
|
|
1050
1298
|
kind: "error",
|
|
1051
1299
|
status: 400,
|
|
@@ -1056,7 +1304,172 @@ export async function dispatchOperatorAction(ctx, req) {
|
|
|
1056
1304
|
message: `unknown or unsupported action: ${action}`,
|
|
1057
1305
|
}),
|
|
1058
1306
|
};
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
async function consumeMutationGateReceipt(ctx, action, p) {
|
|
1311
|
+
const confirmationId = str(p.confirmationId);
|
|
1312
|
+
const humanGateToken = p.humanGateToken;
|
|
1313
|
+
if (!confirmationId) {
|
|
1314
|
+
return {
|
|
1315
|
+
kind: "error",
|
|
1316
|
+
status: 400,
|
|
1317
|
+
body: operatorFailed({
|
|
1318
|
+
command: action,
|
|
1319
|
+
outcome: "blocked",
|
|
1320
|
+
code: "HUMAN_CONFIRMATION_REQUIRED",
|
|
1321
|
+
message: "confirmationId is required — call prepareMutationGate then confirm via the browser Human Gate",
|
|
1322
|
+
}),
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
const secret = ctx.humanGateSecret;
|
|
1326
|
+
if (!secret) {
|
|
1327
|
+
return {
|
|
1328
|
+
kind: "error",
|
|
1329
|
+
status: 503,
|
|
1330
|
+
body: operatorFailed({
|
|
1331
|
+
command: action,
|
|
1332
|
+
outcome: "blocked",
|
|
1333
|
+
code: "INTERNAL_ERROR",
|
|
1334
|
+
message: "humanGateSecret is not configured on action context",
|
|
1335
|
+
}),
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
const store = new MutationGateReceiptStore(ctx.appData);
|
|
1339
|
+
const receipt = await store.get(confirmationId);
|
|
1340
|
+
if (!receipt || receipt.operatorSessionId !== ctx.operatorSessionId) {
|
|
1341
|
+
return {
|
|
1342
|
+
kind: "error",
|
|
1343
|
+
status: 404,
|
|
1344
|
+
body: operatorFailed({
|
|
1345
|
+
command: action,
|
|
1346
|
+
outcome: "not-found",
|
|
1347
|
+
code: "NOT_FOUND",
|
|
1348
|
+
message: `mutation gate receipt not found: ${confirmationId}`,
|
|
1349
|
+
}),
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
if (receipt.action !== action) {
|
|
1353
|
+
return {
|
|
1354
|
+
kind: "error",
|
|
1355
|
+
status: 403,
|
|
1356
|
+
body: operatorFailed({
|
|
1357
|
+
command: action,
|
|
1358
|
+
outcome: "blocked",
|
|
1359
|
+
code: "HUMAN_CONFIRMATION_REQUIRED",
|
|
1360
|
+
message: "mutation gate receipt is bound to a different action",
|
|
1361
|
+
}),
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
const { confirmationId: _cid, humanGateToken: _tok, ...boundParams } = p;
|
|
1365
|
+
const expectedHash = mutationGatePayloadHash({
|
|
1366
|
+
action,
|
|
1367
|
+
operatorSessionId: ctx.operatorSessionId,
|
|
1368
|
+
actionParams: receipt.actionParams,
|
|
1369
|
+
});
|
|
1370
|
+
if (expectedHash !== receipt.paramsHash) {
|
|
1371
|
+
return {
|
|
1372
|
+
kind: "error",
|
|
1373
|
+
status: 403,
|
|
1374
|
+
body: operatorFailed({
|
|
1375
|
+
command: action,
|
|
1376
|
+
outcome: "blocked",
|
|
1377
|
+
code: "HUMAN_CONFIRMATION_REQUIRED",
|
|
1378
|
+
message: "mutation gate receipt params hash mismatch",
|
|
1379
|
+
}),
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
// Bound params from the receipt must match the dispatch params (minus gate fields).
|
|
1383
|
+
const presentedParams = {};
|
|
1384
|
+
for (const key of Object.keys(receipt.actionParams)) {
|
|
1385
|
+
presentedParams[key] = boundParams[key];
|
|
1386
|
+
}
|
|
1387
|
+
const presentedHash = mutationGatePayloadHash({
|
|
1388
|
+
action,
|
|
1389
|
+
operatorSessionId: ctx.operatorSessionId,
|
|
1390
|
+
actionParams: presentedParams,
|
|
1391
|
+
});
|
|
1392
|
+
if (presentedHash !== receipt.paramsHash) {
|
|
1393
|
+
return {
|
|
1394
|
+
kind: "error",
|
|
1395
|
+
status: 403,
|
|
1396
|
+
body: operatorFailed({
|
|
1397
|
+
command: action,
|
|
1398
|
+
outcome: "blocked",
|
|
1399
|
+
code: "HUMAN_CONFIRMATION_REQUIRED",
|
|
1400
|
+
message: "dispatch params do not match the prepared mutation gate receipt",
|
|
1401
|
+
}),
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
const check = verifyHumanGateToken(humanGateToken, { confirmationId: receipt.receiptId, payloadHash: receipt.paramsHash }, secret);
|
|
1405
|
+
if (!check.ok) {
|
|
1406
|
+
return {
|
|
1407
|
+
kind: "error",
|
|
1408
|
+
status: 403,
|
|
1409
|
+
body: operatorFailed({
|
|
1410
|
+
command: action,
|
|
1411
|
+
outcome: "blocked",
|
|
1412
|
+
code: "HUMAN_CONFIRMATION_REQUIRED",
|
|
1413
|
+
message: check.message,
|
|
1414
|
+
}),
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
const reserved = await store.tryReserve(confirmationId);
|
|
1418
|
+
if (!reserved.ok) {
|
|
1419
|
+
return {
|
|
1420
|
+
kind: "error",
|
|
1421
|
+
status: reserved.code === "NOT_FOUND" ? 404 : reserved.code === "EXPIRED" ? 410 : 409,
|
|
1422
|
+
body: operatorFailed({
|
|
1423
|
+
command: action,
|
|
1424
|
+
outcome: "rejected",
|
|
1425
|
+
code: reserved.code === "EXPIRED"
|
|
1426
|
+
? "CONFIRMATION_EXPIRED"
|
|
1427
|
+
: "HUMAN_CONFIRMATION_REQUIRED",
|
|
1428
|
+
message: reserved.message,
|
|
1429
|
+
}),
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
if (!reserved.reserved) {
|
|
1433
|
+
const operationId = reserved.receipt.operationId;
|
|
1434
|
+
if (operationId) {
|
|
1435
|
+
const existing = await ctx.operations.get(operationId);
|
|
1436
|
+
if (existing) {
|
|
1437
|
+
return {
|
|
1438
|
+
kind: "idempotent",
|
|
1439
|
+
result: {
|
|
1440
|
+
kind: "accepted",
|
|
1441
|
+
status: 202,
|
|
1442
|
+
body: {
|
|
1443
|
+
operationId: existing.operationId,
|
|
1444
|
+
state: existing.state,
|
|
1445
|
+
clientRequestId: existing.clientRequestId,
|
|
1446
|
+
action: existing.action,
|
|
1447
|
+
},
|
|
1448
|
+
},
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return {
|
|
1453
|
+
kind: "error",
|
|
1454
|
+
status: 409,
|
|
1455
|
+
body: operatorFailed({
|
|
1456
|
+
command: action,
|
|
1457
|
+
outcome: "conflict",
|
|
1458
|
+
code: "CONFIRMATION_CONSUMED",
|
|
1459
|
+
message: `mutation gate receipt already ${reserved.reason}`,
|
|
1460
|
+
details: { operationId },
|
|
1461
|
+
}),
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
return { kind: "ready", receiptId: confirmationId };
|
|
1465
|
+
}
|
|
1466
|
+
async function finalizeMutationGateReceipt(ctx, receiptId, accepted) {
|
|
1467
|
+
const store = new MutationGateReceiptStore(ctx.appData);
|
|
1468
|
+
if (accepted.kind === "accepted") {
|
|
1469
|
+
await store.markDispatched(receiptId, accepted.body.operationId);
|
|
1470
|
+
return;
|
|
1059
1471
|
}
|
|
1472
|
+
await store.releaseToPrepared(receiptId);
|
|
1060
1473
|
}
|
|
1061
1474
|
async function taskExistsOnDisk(repoRoot, taskId) {
|
|
1062
1475
|
try {
|
|
@@ -11,6 +11,7 @@ import { AssessmentStore } from "./interview/assessment.js";
|
|
|
11
11
|
import { InterviewSessionStore } from "./interview/session.js";
|
|
12
12
|
import { isLoopbackHost } from "./loopback.js";
|
|
13
13
|
import { ChatSessionStore } from "./chat/session-store.js";
|
|
14
|
+
import { createChatEventStore, createChatOperationLinker, } from "./chat/chat-event-store.js";
|
|
14
15
|
import { ConsolePiRuntime } from "./chat/pi-runtime.js";
|
|
15
16
|
import { handleChatRequest } from "./chat/routes.js";
|
|
16
17
|
import { createOperationEventStore, } from "./operation-sse.js";
|
|
@@ -56,7 +57,7 @@ export async function createConsoleServer(options) {
|
|
|
56
57
|
persistenceDir: path.join(appData.operations, "events"),
|
|
57
58
|
});
|
|
58
59
|
const drafts = new DraftStore(appData);
|
|
59
|
-
const confirmations = new DagConfirmationStore(appData);
|
|
60
|
+
const confirmations = new DagConfirmationStore(appData, bootToken.confirmationToken);
|
|
60
61
|
const interviews = new InterviewSessionStore(appData);
|
|
61
62
|
const assessments = new AssessmentStore(appData);
|
|
62
63
|
const readiness = await probePiReadiness({
|
|
@@ -90,6 +91,7 @@ export async function createConsoleServer(options) {
|
|
|
90
91
|
events,
|
|
91
92
|
drafts,
|
|
92
93
|
confirmations,
|
|
94
|
+
humanGateSecret: bootToken.confirmationToken,
|
|
93
95
|
interviews,
|
|
94
96
|
assessments,
|
|
95
97
|
readiness: readinessCache,
|
|
@@ -106,6 +108,12 @@ export async function createConsoleServer(options) {
|
|
|
106
108
|
// custom-tool execute() calls dispatch into the operator action layer (Gate 3).
|
|
107
109
|
// Chat session dir is dedicated (app-data/chat-sessions), NOT user default.
|
|
108
110
|
const chatStore = new ChatSessionStore(appData);
|
|
111
|
+
const chatEvents = createChatEventStore({ appData });
|
|
112
|
+
const chatOperationLinker = createChatOperationLinker({
|
|
113
|
+
chatEvents,
|
|
114
|
+
operations,
|
|
115
|
+
operationEvents: events,
|
|
116
|
+
});
|
|
109
117
|
const skillsDir = resolveLoopAgentSkillsDir(repoRoot);
|
|
110
118
|
const chatRuntime = new ConsolePiRuntime({
|
|
111
119
|
cwd: repoRoot,
|
|
@@ -120,6 +128,8 @@ export async function createConsoleServer(options) {
|
|
|
120
128
|
const chatRouteDeps = {
|
|
121
129
|
runtime: chatRuntime,
|
|
122
130
|
store: chatStore,
|
|
131
|
+
events: chatEvents,
|
|
132
|
+
operationLinker: chatOperationLinker,
|
|
123
133
|
appData,
|
|
124
134
|
bootToken,
|
|
125
135
|
getConsoleOrigin,
|
|
@@ -202,7 +212,8 @@ export async function createConsoleServer(options) {
|
|
|
202
212
|
if (pathname.startsWith("/api/operator/v1/") ||
|
|
203
213
|
pathname.startsWith("/api/session/")) {
|
|
204
214
|
// Phase 4: General Operator Chat routes (mounted under chat/).
|
|
205
|
-
if (pathname.startsWith("/api/operator/v1/chat/")
|
|
215
|
+
if (pathname.startsWith("/api/operator/v1/chat/") ||
|
|
216
|
+
pathname.startsWith("/api/operator/v1/pi/")) {
|
|
206
217
|
await handleChatRequest(req, res, chatRouteDeps, pathname);
|
|
207
218
|
return;
|
|
208
219
|
}
|
|
@@ -259,6 +270,7 @@ export async function createConsoleServer(options) {
|
|
|
259
270
|
// leaks across Console restarts (regression: close() previously
|
|
260
271
|
// only closed the socket, leaving ConsolePiRuntime sessions live).
|
|
261
272
|
try {
|
|
273
|
+
chatOperationLinker.dispose();
|
|
262
274
|
chatRuntime.disposeAll();
|
|
263
275
|
}
|
|
264
276
|
catch (error) {
|