bazilion 0.15.0 → 0.17.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/cli.js +1138 -687
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +8401 -4401
- package/dist/daemon.js.map +1 -1
- package/dist/migrations/0001_init.sql +109 -0
- package/dist/web/client/assets/ChatPane-BI3QVX2A.js +76 -0
- package/dist/web/client/assets/{ConfirmDialog-CGZzom9E.js → ConfirmDialog-Qam6Ut08.js} +1 -1
- package/dist/web/client/assets/{FieldRow-DYNU9gWb.js → FieldRow-swJ5qn2h.js} +1 -1
- package/dist/web/client/assets/ProductionTeamPolicyEditor-LGw1UuF-.js +7 -0
- package/dist/web/client/assets/ProfileCommunicationEditor-raoau_YB.js +1 -0
- package/dist/web/client/assets/{ResultSourceConversation-CGhKTSTD.js → ResultSourceConversation-BEwkhFog.js} +1 -1
- package/dist/web/client/assets/TeamTabs-BDdTLGIf.js +1 -0
- package/dist/web/client/assets/TemplatesTabs-BX5hhEJ7.js +1 -0
- package/dist/web/client/assets/{UnsavedChangesGuard-CFQcKsrf.js → UnsavedChangesGuard-CVJtmR1D.js} +1 -1
- package/dist/web/client/assets/{_id-D_eKokkd.js → _id-BQPOE3gj.js} +1 -1
- package/dist/web/client/assets/{_id-9nCIhbVd.js → _id-BkYrUP0R.js} +1 -1
- package/dist/web/client/assets/_id-CriixlZD.js +1 -0
- package/dist/web/client/assets/_id-DHqq2W34.js +1 -0
- package/dist/web/client/assets/{_id-BSu3AqKA.js → _id-DmEVZIlx.js} +1 -1
- package/dist/web/client/assets/activity-Bzr8R18n.js +1 -0
- package/dist/web/client/assets/{agents-NY3VrZgB.js → agents-13oafeOA.js} +1 -1
- package/dist/web/client/assets/{agents-CCmiZek6.js → agents-DF4NALmJ.js} +1 -1
- package/dist/web/client/assets/{approvals-CPDAPgrM.js → approvals-DsG0Ijls.js} +1 -1
- package/dist/web/client/assets/{attention-Qra_9_OI.js → attention-DG7K25mz.js} +1 -1
- package/dist/web/client/assets/chevron-right-DFqFHtLy.js +1 -0
- package/dist/web/client/assets/config-DAaDbFhS.js +3 -0
- package/dist/web/client/assets/createServerFn-Dzfjt7mA.js +3 -0
- package/dist/web/client/assets/git-review-presentation-CzHQulDk.js +1 -0
- package/dist/web/client/assets/{inbox-C5W-sm1q.js → inbox-C9e7QjsX.js} +1 -1
- package/dist/web/client/assets/index-Dsstm2hf.js +50 -0
- package/dist/web/client/assets/{learning-Dr-7_7J4.js → learning-23arTd21.js} +1 -1
- package/dist/web/client/assets/login-BIXnBsuj.js +1 -0
- package/dist/web/client/assets/{mcp-BueuwQiv.js → mcp-Cdx9qAl8.js} +1 -1
- package/dist/web/client/assets/{members-ZikuYNcy.js → members-fotOz4rR.js} +1 -1
- package/dist/web/client/assets/{memory-1JUJlPrn.js → memory-C7UznVot.js} +1 -1
- package/dist/web/client/assets/policy-CactLW1X.js +1 -0
- package/dist/web/client/assets/{results-mMr8Ar2u.js → results-DqEuYnQH.js} +1 -1
- package/dist/web/client/assets/review-CnBSYclb.js +1 -0
- package/dist/web/client/assets/routes-COxMuwVz.js +1 -0
- package/dist/web/client/assets/{services-kfQA9cpJ.js → services-xEANwZAu.js} +1 -1
- package/dist/web/client/assets/{skills-C6pcgxaC.js → skills-CamV6wYM.js} +1 -1
- package/dist/web/client/assets/styles-C6br2xxl.css +2 -0
- package/dist/web/client/assets/{teams-LB0KBXY9.js → teams-B12dMSjW.js} +1 -1
- package/dist/web/client/assets/{teams-C6_Noz5X.js → teams-CbH43zSG.js} +1 -1
- package/dist/web/client/assets/{telegram-A2jaI1QM.js → telegram-muIql4u-.js} +1 -1
- package/dist/web/client/assets/{tokens-BGlUpJcL.js → tokens-DGFmApdF.js} +1 -1
- package/dist/web/client/assets/{triggers-C54_feGg.js → triggers-D8n3sCBW.js} +1 -1
- package/dist/web/client/assets/welcome-CMHWRZhs.js +1 -0
- package/dist/web/client/assets/x-BGCHwZOH.js +1 -0
- package/dist/web/server/assets/{ChatPane-DJ30FdFG.js → ChatPane-DybH39WK.js} +524 -15
- package/dist/web/server/assets/{ResultSourceConversation-zYogeOWp.js → ResultSourceConversation-BmXTcEOA.js} +1 -1
- package/dist/web/server/assets/{TeamTabs-TCKjCvMA.js → TeamTabs-DQXh-3ij.js} +4 -0
- package/dist/web/server/assets/{_id-1LwoHhJg.js → _id-BN1BPEDI.js} +1 -1
- package/dist/web/server/assets/{_id-DFHB9JFQ.js → _id-BYKpT2Tu.js} +1 -1
- package/dist/web/server/assets/{_id-B_-CDHOi2.js → _id-BqWTB8Wl.js} +1 -1
- package/dist/web/server/assets/{_id-BvSWsPI5.js → _id-CPfILjMa.js} +1 -1
- package/dist/web/server/assets/{_id-1hTvnKLN2.js → _id-C_2er0_G.js} +3 -3
- package/dist/web/server/assets/{_id-B2EEyStS.js → _id-DDyA9CGX.js} +1 -1
- package/dist/web/server/assets/{_id-DxFLSIio.js → _id-DSPFJMzf.js} +1 -1
- package/dist/web/server/assets/_id-DtJn-yzu.js +779 -0
- package/dist/web/server/assets/{_id-DDNVQx3y.js → _id-Dw3gKOGv.js} +1 -1
- package/dist/web/server/assets/{_id-T_8btpbB.js → _id-XkxlX036.js} +1 -1
- package/dist/web/server/assets/{_tanstack-start-manifest_v-DP4ZUBw3.js → _tanstack-start-manifest_v-vOXRfKJD.js} +97 -84
- package/dist/web/server/assets/{activity-C88qrNqv.js → activity-BX1jZBPQ.js} +1 -1
- package/dist/web/server/assets/{activity-5fWKPDxj.js → activity-bozPMU2v.js} +2 -2
- package/dist/web/server/assets/{activity-D73w4mkZ.js → activity-pYQPT3QT.js} +1 -1
- package/dist/web/server/assets/{agents-DRSo7lRX.js → agents-BKBfW700.js} +1 -1
- package/dist/web/server/assets/{agents-DB5hCSAI.js → agents-CVCfxnE2.js} +1 -1
- package/dist/web/server/assets/{approvals-4y9bZF4r.js → approvals-C3ya6B6U.js} +1 -1
- package/dist/web/server/assets/{attention-BFx8ZkmH.js → attention-l28hGCHS.js} +1 -1
- package/dist/web/server/assets/{auth-_eaCwhyq.js → auth-AvDFNeOv.js} +1 -1
- package/dist/web/server/assets/{config-B7JIfv_7.js → config-BWKn4XII.js} +1 -1
- package/dist/web/server/assets/{daemon-client-B06W1Qa2.js → daemon-client-Cgx-YW4l.js} +35 -0
- package/dist/web/server/assets/git-review-BB3M9nYg.js +26 -0
- package/dist/web/server/assets/git-review-f_uta4Mx.js +179 -0
- package/dist/web/server/assets/git-review-presentation-TJiHJhen.js +81 -0
- package/dist/web/server/assets/{inbox-YRpDQcvp.js → inbox-BBZiOOQs.js} +1 -1
- package/dist/web/server/assets/{learning-UBZb6h6M.js → learning-CheVay1i.js} +1 -1
- package/dist/web/server/assets/{mcp-Dv6RXMaS.js → mcp-DCBDmNtQ.js} +1 -1
- package/dist/web/server/assets/{members-BFTKSlOe.js → members-C2He8mrU.js} +1 -1
- package/dist/web/server/assets/{members-CD3GiTfF.js → members-C5FQruRZ.js} +1 -1
- package/dist/web/server/assets/{members-cc5SeEaW.js → members-C_2xB9pr.js} +2 -2
- package/dist/web/server/assets/{memory-CVqnyUaM.js → memory-4tI-UbOf.js} +1 -1
- package/dist/web/server/assets/{memory-C5i_GxlY.js → memory-BTcmf0sD.js} +2 -2
- package/dist/web/server/assets/{memory-D0PQgZAn.js → memory-rmiFNj7T.js} +1 -1
- package/dist/web/server/assets/{policy-Dgjb0_gK.js → policy-BGILKGHz.js} +2 -2
- package/dist/web/server/assets/{policy-bLKwvnja.js → policy-C3D2Q1EA.js} +1 -1
- package/dist/web/server/assets/{policy-C-p8HIhA.js → policy-DS7LGe--.js} +1 -1
- package/dist/web/server/assets/{results-Cfev4KNt.js → results-C6H42tjq.js} +1 -1
- package/dist/web/server/assets/{results-Cj-Dlw3C.js → results-Cgx7SrFt.js} +3 -3
- package/dist/web/server/assets/{results-CwEQ5EUL.js → results-_6K_zXQS.js} +1 -1
- package/dist/web/server/assets/review-DHhiL-JQ.js +393 -0
- package/dist/web/server/assets/review-DSTMCwP4.js +14 -0
- package/dist/web/server/assets/{router-d3sTMpMX.js → router-ElARvZzI.js} +34 -27
- package/dist/web/server/assets/{routes-B765DVhj.js → routes-B6x3pEW8.js} +1 -1
- package/dist/web/server/assets/{routes-cW4MWU18.js → routes-BgMvLvS-.js} +1 -1
- package/dist/web/server/assets/{routes-MIZz6uMr.js → routes-DsUzzxk4.js} +2 -2
- package/dist/web/server/assets/{services-BCbdn3R9.js → services-yHZroxsi.js} +1 -1
- package/dist/web/server/assets/{skills-Bpoa8MIp.js → skills--bIKID1Q.js} +1 -1
- package/dist/web/server/assets/{teams-hWGjbpUA.js → teams--a956vWL.js} +1 -1
- package/dist/web/server/assets/{teams-CXzwqzcE.js → teams-BbRfuVXY.js} +1 -1
- package/dist/web/server/assets/{telegram-DhpvtGUc.js → telegram-4kMxRtVZ.js} +1 -1
- package/dist/web/server/assets/{tokens-Bjp19Vkn.js → tokens-BYwTN9JZ.js} +1 -1
- package/dist/web/server/assets/{triggers-DJlcvXzn.js → triggers-BM6mzLuH.js} +1 -1
- package/dist/web/server/server.js +56 -28
- package/dist/worker.js +2214 -1132
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
- package/dist/web/client/assets/ChatPane-55j13bBj.js +0 -74
- package/dist/web/client/assets/ProductionTeamPolicyEditor-DT5Sl8jd.js +0 -7
- package/dist/web/client/assets/ProfileCommunicationEditor-TyCU5vXX.js +0 -1
- package/dist/web/client/assets/TeamTabs-BZHyRnbd.js +0 -1
- package/dist/web/client/assets/TemplatesTabs-BHzkPMRC.js +0 -1
- package/dist/web/client/assets/_id-CGd21j3k.js +0 -1
- package/dist/web/client/assets/_id-SRMFlIgP.js +0 -1
- package/dist/web/client/assets/activity-Cn2-9PpZ.js +0 -1
- package/dist/web/client/assets/chevron-right-DXE4CWf2.js +0 -1
- package/dist/web/client/assets/config-D8EO0LPB.js +0 -3
- package/dist/web/client/assets/index-BUhhk_u9.js +0 -52
- package/dist/web/client/assets/login-Bu5UxbJh.js +0 -1
- package/dist/web/client/assets/policy-DbkhsFNA.js +0 -1
- package/dist/web/client/assets/routes-DJYB-MS9.js +0 -1
- package/dist/web/client/assets/styles-CcNf8jT_.css +0 -2
- package/dist/web/client/assets/welcome-DyUIswRi.js +0 -1
- package/dist/web/client/assets/x-BY2MPLr9.js +0 -1
- package/dist/web/server/assets/_id-CDBtO_jL.js +0 -381
package/dist/worker.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// ../daemon/src/runtime/worker/entry.ts
|
|
4
4
|
import { randomUUID as randomUUID20 } from "crypto";
|
|
5
|
-
import { Type as
|
|
5
|
+
import { Type as Type5 } from "typebox";
|
|
6
6
|
|
|
7
7
|
// ../daemon/src/core/agent/delete.ts
|
|
8
8
|
import { randomUUID } from "crypto";
|
|
@@ -1108,6 +1108,41 @@ function qmdBackend(root, options = {}) {
|
|
|
1108
1108
|
};
|
|
1109
1109
|
}
|
|
1110
1110
|
|
|
1111
|
+
// ../daemon/src/lib/coding-environment/receipt-reference.ts
|
|
1112
|
+
function codingLogReference(content) {
|
|
1113
|
+
const parsed = objectFrom(textContent(content));
|
|
1114
|
+
if (!parsed) return null;
|
|
1115
|
+
return referenceFrom(parsed) ?? referenceFrom(objectFrom(parsed.receipt));
|
|
1116
|
+
}
|
|
1117
|
+
function referenceFrom(value) {
|
|
1118
|
+
if (!value) return null;
|
|
1119
|
+
const commandId = value.id;
|
|
1120
|
+
const teamId = value.teamId;
|
|
1121
|
+
if (typeof commandId !== "string" || commandId.length === 0) return null;
|
|
1122
|
+
if (typeof teamId !== "string" || teamId.length === 0) return null;
|
|
1123
|
+
return { commandId, teamId };
|
|
1124
|
+
}
|
|
1125
|
+
function textContent(content) {
|
|
1126
|
+
if (typeof content === "string") return content;
|
|
1127
|
+
if (!Array.isArray(content)) return "";
|
|
1128
|
+
let out = "";
|
|
1129
|
+
for (const block of content) {
|
|
1130
|
+
if (block && block.type === "text" && typeof block.text === "string") out += block.text;
|
|
1131
|
+
}
|
|
1132
|
+
return out;
|
|
1133
|
+
}
|
|
1134
|
+
function objectFrom(value) {
|
|
1135
|
+
if (typeof value === "string") {
|
|
1136
|
+
try {
|
|
1137
|
+
return objectFrom(JSON.parse(value));
|
|
1138
|
+
} catch {
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
1143
|
+
return value;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1111
1146
|
// ../daemon/src/runtime/pi/events.ts
|
|
1112
1147
|
function translatePiEvent(e) {
|
|
1113
1148
|
switch (e.type) {
|
|
@@ -1128,8 +1163,8 @@ function translatePiEvent(e) {
|
|
|
1128
1163
|
const m = e.message;
|
|
1129
1164
|
if (m.role !== "assistant") return [];
|
|
1130
1165
|
const out = [];
|
|
1131
|
-
const
|
|
1132
|
-
if (
|
|
1166
|
+
const text2 = extractAssistantText(m);
|
|
1167
|
+
if (text2) out.push({ type: "assistant_message", text: text2 });
|
|
1133
1168
|
for (const block of m.content ?? []) {
|
|
1134
1169
|
if (block.type === "toolCall") {
|
|
1135
1170
|
out.push({
|
|
@@ -1147,6 +1182,22 @@ function translatePiEvent(e) {
|
|
|
1147
1182
|
}
|
|
1148
1183
|
return out;
|
|
1149
1184
|
}
|
|
1185
|
+
case "tool_execution_update": {
|
|
1186
|
+
if (e.toolName !== "coding_command") return [];
|
|
1187
|
+
const progress = e.partialResult?.details?.codingProgress;
|
|
1188
|
+
if (!progress || typeof progress.id !== "string" || typeof progress.commandId !== "string" || typeof progress.output !== "string" || typeof progress.elapsedMs !== "number")
|
|
1189
|
+
return [];
|
|
1190
|
+
return [
|
|
1191
|
+
{
|
|
1192
|
+
type: "coding_progress",
|
|
1193
|
+
id: progress.id,
|
|
1194
|
+
commandId: progress.commandId,
|
|
1195
|
+
output: progress.output,
|
|
1196
|
+
truncated: progress.truncated === true,
|
|
1197
|
+
elapsedMs: Math.max(0, Math.floor(progress.elapsedMs))
|
|
1198
|
+
}
|
|
1199
|
+
];
|
|
1200
|
+
}
|
|
1150
1201
|
case "tool_execution_end": {
|
|
1151
1202
|
if (e.toolName === "ask_user")
|
|
1152
1203
|
return [
|
|
@@ -1157,9 +1208,9 @@ function translatePiEvent(e) {
|
|
|
1157
1208
|
result: "Question outcome is available through the question card or authorized conversation history."
|
|
1158
1209
|
}
|
|
1159
1210
|
];
|
|
1160
|
-
const
|
|
1211
|
+
const text2 = extractToolResultText(e.result);
|
|
1161
1212
|
if (e.isError) {
|
|
1162
|
-
return [{ type: "tool_error", id: e.toolCallId, name: e.toolName, error:
|
|
1213
|
+
return [{ type: "tool_error", id: e.toolCallId, name: e.toolName, error: text2 }];
|
|
1163
1214
|
}
|
|
1164
1215
|
const images = extractToolResultImages(e.result);
|
|
1165
1216
|
return [
|
|
@@ -1167,7 +1218,7 @@ function translatePiEvent(e) {
|
|
|
1167
1218
|
type: "tool_result",
|
|
1168
1219
|
id: e.toolCallId,
|
|
1169
1220
|
name: e.toolName,
|
|
1170
|
-
result:
|
|
1221
|
+
result: text2,
|
|
1171
1222
|
resultReference: extractResultReference(e.result),
|
|
1172
1223
|
...images.length > 0 ? { images } : {}
|
|
1173
1224
|
}
|
|
@@ -1245,15 +1296,34 @@ function piMessagesToProviderView(messages, questionResultVisible) {
|
|
|
1245
1296
|
}
|
|
1246
1297
|
case "assistant": {
|
|
1247
1298
|
const am = m;
|
|
1248
|
-
const
|
|
1299
|
+
const text2 = extractAssistantText(am);
|
|
1249
1300
|
const toolCalls = extractAssistantToolCalls(am);
|
|
1250
|
-
const msg = { role: "assistant", content:
|
|
1301
|
+
const msg = { role: "assistant", content: text2 };
|
|
1251
1302
|
if (toolCalls.length > 0) msg.toolCalls = toolCalls;
|
|
1252
1303
|
out.push(msg);
|
|
1253
1304
|
break;
|
|
1254
1305
|
}
|
|
1255
1306
|
case "toolResult": {
|
|
1256
1307
|
const tr = m;
|
|
1308
|
+
if ([
|
|
1309
|
+
"repository_context",
|
|
1310
|
+
"coding_environment",
|
|
1311
|
+
"coding_command",
|
|
1312
|
+
"coding_receipt",
|
|
1313
|
+
"coding_log",
|
|
1314
|
+
// BAZ-042: a snapshot result names withheld paths, so history must not render it.
|
|
1315
|
+
"source_snapshot"
|
|
1316
|
+
].includes(tr.toolName ?? "")) {
|
|
1317
|
+
const codingLog = codingLogReference(tr.content);
|
|
1318
|
+
out.push({
|
|
1319
|
+
role: "tool",
|
|
1320
|
+
toolCallId: tr.toolCallId,
|
|
1321
|
+
toolName: tr.toolName,
|
|
1322
|
+
content: "Coding context or command evidence retained privately. Captured transport output follows its communication approval; ask the Agent for the relevant result.",
|
|
1323
|
+
...codingLog ? { codingLog } : {}
|
|
1324
|
+
});
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1257
1327
|
if (tr.toolName === "ask_user" && !questionResultVisible?.(m)) {
|
|
1258
1328
|
out.push({
|
|
1259
1329
|
role: "tool",
|
|
@@ -1287,6 +1357,107 @@ function extractResultReference(value) {
|
|
|
1287
1357
|
return typeof id === "string" && /^[a-f0-9-]{36}$/.test(id) ? { resultId: id } : void 0;
|
|
1288
1358
|
}
|
|
1289
1359
|
|
|
1360
|
+
// ../daemon/src/lib/coding-environment/diagnostics.ts
|
|
1361
|
+
var CODING_OUTPUT_BYTES = 64 * 1024;
|
|
1362
|
+
var CODING_RETAINED_BYTES = 2 * 1024 * 1024;
|
|
1363
|
+
var CODING_LIVE_BYTES = 8 * 1024;
|
|
1364
|
+
function diagnosticTail(value, limit) {
|
|
1365
|
+
const bytes = Buffer.from(value);
|
|
1366
|
+
if (bytes.length <= limit) return { text: value, truncated: false };
|
|
1367
|
+
let offset = bytes.length - limit;
|
|
1368
|
+
while (offset < bytes.length && ((bytes[offset] ?? 0) & 192) === 128) offset++;
|
|
1369
|
+
return { text: bytes.subarray(offset).toString("utf8"), truncated: true };
|
|
1370
|
+
}
|
|
1371
|
+
var CodingDiagnostics = class {
|
|
1372
|
+
decoder = new TextDecoder();
|
|
1373
|
+
secrets;
|
|
1374
|
+
lookbehind;
|
|
1375
|
+
retainBytes;
|
|
1376
|
+
pending = "";
|
|
1377
|
+
text = "";
|
|
1378
|
+
truncated = false;
|
|
1379
|
+
observedBytes = 0;
|
|
1380
|
+
redactions = 0;
|
|
1381
|
+
finished = false;
|
|
1382
|
+
constructor(secrets, retainBytes = CODING_OUTPUT_BYTES) {
|
|
1383
|
+
this.secrets = [...new Set(secrets.filter(Boolean))].sort((a, b) => b.length - a.length);
|
|
1384
|
+
this.lookbehind = Math.max(1, ...this.secrets.map((secret) => secret.length)) - 1;
|
|
1385
|
+
this.retainBytes = retainBytes;
|
|
1386
|
+
}
|
|
1387
|
+
append(bytes) {
|
|
1388
|
+
if (this.finished) throw new Error("Coding diagnostics already finalized");
|
|
1389
|
+
this.observedBytes += bytes.length;
|
|
1390
|
+
for (let offset = 0; offset < bytes.length; offset += 8192) {
|
|
1391
|
+
this.pending += this.decoder.decode(bytes.subarray(offset, offset + 8192), { stream: true });
|
|
1392
|
+
this.flush(false);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Redacted cumulative tail for a live update. Unlike `finish()` this does not seal
|
|
1397
|
+
* the diagnostics: more output may follow. It deliberately excludes the trailing
|
|
1398
|
+
* lookbehind bytes so a credential split across chunks cannot leak before the next
|
|
1399
|
+
* chunk resolves it.
|
|
1400
|
+
*/
|
|
1401
|
+
preview(limit = CODING_LIVE_BYTES) {
|
|
1402
|
+
return diagnosticTail(this.text, limit);
|
|
1403
|
+
}
|
|
1404
|
+
finish() {
|
|
1405
|
+
if (!this.finished) {
|
|
1406
|
+
this.pending += this.decoder.decode();
|
|
1407
|
+
this.flush(true);
|
|
1408
|
+
this.finished = true;
|
|
1409
|
+
}
|
|
1410
|
+
return {
|
|
1411
|
+
diagnostic: this.text,
|
|
1412
|
+
truncated: this.truncated,
|
|
1413
|
+
observedBytes: this.observedBytes,
|
|
1414
|
+
redacted: this.redactions > 0
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
flush(final) {
|
|
1418
|
+
let end = final ? this.pending.length : Math.max(0, this.pending.length - this.lookbehind);
|
|
1419
|
+
const last = this.pending.charCodeAt(end - 1);
|
|
1420
|
+
if (!final && last >= 55296 && last <= 56319) end--;
|
|
1421
|
+
let offset = 0;
|
|
1422
|
+
let safe = "";
|
|
1423
|
+
while (offset < end) {
|
|
1424
|
+
const secret = this.secrets.find((value) => this.pending.startsWith(value, offset));
|
|
1425
|
+
if (secret) {
|
|
1426
|
+
safe += "[redacted]";
|
|
1427
|
+
this.redactions++;
|
|
1428
|
+
offset += secret.length;
|
|
1429
|
+
} else {
|
|
1430
|
+
const code = this.pending.charCodeAt(offset);
|
|
1431
|
+
const char = this.pending[offset] ?? "";
|
|
1432
|
+
safe += code < 32 && code !== 9 && code !== 10 || code >= 127 && code <= 159 ? `\\u${code.toString(16).padStart(4, "0")}` : char;
|
|
1433
|
+
offset++;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
this.pending = this.pending.slice(offset);
|
|
1437
|
+
const tail = diagnosticTail(this.text + safe, this.retainBytes);
|
|
1438
|
+
this.text = tail.text;
|
|
1439
|
+
this.truncated ||= tail.truncated;
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
function codingSecrets(env) {
|
|
1443
|
+
const values = /* @__PURE__ */ new Set();
|
|
1444
|
+
function collect(value, depth = 0) {
|
|
1445
|
+
if (depth > 8) return;
|
|
1446
|
+
if (typeof value === "string" && value.length >= 4) values.add(value);
|
|
1447
|
+
else if (value && typeof value === "object")
|
|
1448
|
+
for (const entry of Object.values(value)) collect(entry, depth + 1);
|
|
1449
|
+
}
|
|
1450
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1451
|
+
if (!value || !/KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH/i.test(key)) continue;
|
|
1452
|
+
values.add(value);
|
|
1453
|
+
try {
|
|
1454
|
+
collect(JSON.parse(value));
|
|
1455
|
+
} catch {
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return [...values];
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1290
1461
|
// ../daemon/src/lib/result-source.ts
|
|
1291
1462
|
import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, openSync as openSync2, readSync, realpathSync as realpathSync2 } from "fs";
|
|
1292
1463
|
import { basename as basename2, join as join13 } from "path";
|
|
@@ -1302,16 +1473,16 @@ function readCanonicalSessionFile(directory, filename, sessionId, minimumEntryOf
|
|
|
1302
1473
|
if (!stat2.isFile() || stat2.size > 64 * 1024 * 1024)
|
|
1303
1474
|
throw new Error("Result source session is unavailable or too large");
|
|
1304
1475
|
const buffer = Buffer.alloc(stat2.size + 1);
|
|
1305
|
-
let
|
|
1306
|
-
while (
|
|
1307
|
-
const n = readSync(fd, buffer,
|
|
1476
|
+
let count2 = 0;
|
|
1477
|
+
while (count2 < buffer.length) {
|
|
1478
|
+
const n = readSync(fd, buffer, count2, buffer.length - count2, null);
|
|
1308
1479
|
if (n === 0) break;
|
|
1309
|
-
|
|
1480
|
+
count2 += n;
|
|
1310
1481
|
}
|
|
1311
|
-
if (
|
|
1482
|
+
if (count2 !== stat2.size) throw new Error("Result source session changed while reading");
|
|
1312
1483
|
const entries = [];
|
|
1313
1484
|
let position = 0;
|
|
1314
|
-
for (const line of buffer.subarray(0,
|
|
1485
|
+
for (const line of buffer.subarray(0, count2).toString("utf8").split("\n")) {
|
|
1315
1486
|
if (line.trim()) entries.push({ entry: JSON.parse(line), offset: position });
|
|
1316
1487
|
position += Buffer.byteLength(line) + 1;
|
|
1317
1488
|
}
|
|
@@ -1324,876 +1495,640 @@ function readCanonicalSessionFile(directory, filename, sessionId, minimumEntryOf
|
|
|
1324
1495
|
}
|
|
1325
1496
|
}
|
|
1326
1497
|
|
|
1327
|
-
// ../daemon/src/runtime/pi/
|
|
1328
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync7, statSync as statSync5, writeFileSync as writeFileSync9 } from "fs";
|
|
1329
|
-
import { basename as basename4, join as join20 } from "path";
|
|
1498
|
+
// ../daemon/src/runtime/pi/coding.ts
|
|
1330
1499
|
import {
|
|
1331
|
-
|
|
1332
|
-
} from "@earendil-works/pi-ai";
|
|
1333
|
-
import {
|
|
1334
|
-
buildSessionContext,
|
|
1335
|
-
createAgentSession,
|
|
1336
|
-
DefaultResourceLoader,
|
|
1337
|
-
SessionManager,
|
|
1338
|
-
SettingsManager
|
|
1500
|
+
createLocalBashOperations
|
|
1339
1501
|
} from "@earendil-works/pi-coding-agent";
|
|
1502
|
+
import { Type } from "typebox";
|
|
1340
1503
|
|
|
1341
|
-
// ../daemon/src/
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
var PROVIDER_ALIASES = {
|
|
1345
|
-
bedrock: "amazon-bedrock",
|
|
1346
|
-
"azure-openai": "azure-openai-responses"
|
|
1347
|
-
};
|
|
1348
|
-
var PROTECTED_PROVIDER_NAMES = [
|
|
1349
|
-
"anthropic",
|
|
1350
|
-
"openai",
|
|
1351
|
-
"openai-codex",
|
|
1352
|
-
"google",
|
|
1353
|
-
"google-vertex",
|
|
1354
|
-
"azure-openai",
|
|
1355
|
-
"bedrock",
|
|
1356
|
-
"mistral",
|
|
1357
|
-
"groq",
|
|
1358
|
-
"cerebras",
|
|
1359
|
-
"xai",
|
|
1360
|
-
"zai",
|
|
1361
|
-
"huggingface",
|
|
1362
|
-
"openrouter",
|
|
1363
|
-
"vercel-ai-gateway",
|
|
1364
|
-
"deepseek",
|
|
1365
|
-
"fireworks",
|
|
1366
|
-
"together",
|
|
1367
|
-
"baseten",
|
|
1368
|
-
"moonshotai",
|
|
1369
|
-
"moonshotai-cn",
|
|
1370
|
-
"kimi-coding",
|
|
1371
|
-
"minimax",
|
|
1372
|
-
"minimax-cn",
|
|
1373
|
-
"qwen-token-plan",
|
|
1374
|
-
"qwen-token-plan-cn",
|
|
1375
|
-
"qwen-token-plan-individual",
|
|
1376
|
-
"xiaomi",
|
|
1377
|
-
"xiaomi-token-plan-ams",
|
|
1378
|
-
"xiaomi-token-plan-cn",
|
|
1379
|
-
"xiaomi-token-plan-sgp",
|
|
1380
|
-
"ant-ling",
|
|
1381
|
-
"nvidia",
|
|
1382
|
-
"opencode",
|
|
1383
|
-
"opencode-go",
|
|
1384
|
-
"zai-coding-cn",
|
|
1385
|
-
"github-copilot",
|
|
1386
|
-
"cloudflare-ai-gateway",
|
|
1387
|
-
"cloudflare-workers-ai",
|
|
1388
|
-
"lmstudio",
|
|
1389
|
-
"ollama",
|
|
1390
|
-
"llamacpp"
|
|
1391
|
-
];
|
|
1392
|
-
var LOCAL_PROVIDERS = {
|
|
1393
|
-
lmstudio: {
|
|
1394
|
-
baseUrl: "http://127.0.0.1:1234/v1",
|
|
1395
|
-
apiKey: "lm-studio",
|
|
1396
|
-
authHeader: false
|
|
1397
|
-
},
|
|
1398
|
-
ollama: { baseUrl: "http://127.0.0.1:11434/v1", apiKey: "ollama", authHeader: false },
|
|
1399
|
-
llamacpp: { baseUrl: "http://127.0.0.1:8080/v1", apiKey: "no-key", authHeader: true }
|
|
1400
|
-
};
|
|
1401
|
-
var PROVIDER_API_KEY_ENV = {
|
|
1402
|
-
anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
|
1403
|
-
openai: ["OPENAI_API_KEY"],
|
|
1404
|
-
google: ["GEMINI_API_KEY"],
|
|
1405
|
-
"azure-openai": ["AZURE_OPENAI_API_KEY"],
|
|
1406
|
-
mistral: ["MISTRAL_API_KEY"],
|
|
1407
|
-
groq: ["GROQ_API_KEY"],
|
|
1408
|
-
cerebras: ["CEREBRAS_API_KEY"],
|
|
1409
|
-
xai: ["XAI_API_KEY"],
|
|
1410
|
-
zai: ["ZAI_API_KEY"],
|
|
1411
|
-
huggingface: ["HF_TOKEN"],
|
|
1412
|
-
openrouter: ["OPENROUTER_API_KEY"],
|
|
1413
|
-
"vercel-ai-gateway": ["AI_GATEWAY_API_KEY"],
|
|
1414
|
-
deepseek: ["DEEPSEEK_API_KEY"],
|
|
1415
|
-
fireworks: ["FIREWORKS_API_KEY"],
|
|
1416
|
-
together: ["TOGETHER_API_KEY"],
|
|
1417
|
-
baseten: ["BASETEN_API_KEY"],
|
|
1418
|
-
moonshotai: ["MOONSHOT_API_KEY"],
|
|
1419
|
-
"moonshotai-cn": ["MOONSHOT_CN_API_KEY"],
|
|
1420
|
-
"kimi-coding": ["KIMI_API_KEY"],
|
|
1421
|
-
minimax: ["MINIMAX_API_KEY"],
|
|
1422
|
-
"minimax-cn": ["MINIMAX_CN_API_KEY"],
|
|
1423
|
-
"qwen-token-plan": ["QWEN_TOKEN_PLAN_API_KEY"],
|
|
1424
|
-
"qwen-token-plan-cn": ["QWEN_TOKEN_PLAN_CN_API_KEY"],
|
|
1425
|
-
"qwen-token-plan-individual": ["QWEN_TOKEN_PLAN_API_KEY"],
|
|
1426
|
-
xiaomi: ["XIAOMI_API_KEY"],
|
|
1427
|
-
"xiaomi-token-plan-ams": ["XIAOMI_TOKEN_PLAN_AMS_API_KEY"],
|
|
1428
|
-
"xiaomi-token-plan-cn": ["XIAOMI_TOKEN_PLAN_CN_API_KEY"],
|
|
1429
|
-
"xiaomi-token-plan-sgp": ["XIAOMI_TOKEN_PLAN_SGP_API_KEY"],
|
|
1430
|
-
"ant-ling": ["ANT_LING_API_KEY"],
|
|
1431
|
-
nvidia: ["NVIDIA_API_KEY"],
|
|
1432
|
-
opencode: ["OPENCODE_API_KEY"],
|
|
1433
|
-
"opencode-go": ["OPENCODE_GO_API_KEY"],
|
|
1434
|
-
"zai-coding-cn": ["ZAI_CODING_CN_API_KEY"],
|
|
1435
|
-
"github-copilot": ["COPILOT_GITHUB_TOKEN"],
|
|
1436
|
-
"cloudflare-ai-gateway": ["CLOUDFLARE_API_KEY"],
|
|
1437
|
-
"cloudflare-workers-ai": ["CLOUDFLARE_API_KEY"],
|
|
1438
|
-
lmstudio: ["LMSTUDIO_API_KEY"],
|
|
1439
|
-
ollama: ["OLLAMA_API_KEY"],
|
|
1440
|
-
llamacpp: ["LLAMACPP_API_KEY"]
|
|
1441
|
-
};
|
|
1442
|
-
var PROVIDER_CREDENTIAL_ENV = {
|
|
1443
|
-
bedrock: [
|
|
1444
|
-
"AWS_PROFILE",
|
|
1445
|
-
"AWS_BEARER_TOKEN_BEDROCK",
|
|
1446
|
-
"AWS_ACCESS_KEY_ID",
|
|
1447
|
-
"AWS_SECRET_ACCESS_KEY",
|
|
1448
|
-
"AWS_SESSION_TOKEN",
|
|
1449
|
-
"AWS_REGION",
|
|
1450
|
-
"AWS_DEFAULT_REGION"
|
|
1451
|
-
],
|
|
1452
|
-
"google-vertex": [
|
|
1453
|
-
"GOOGLE_CLOUD_PROJECT",
|
|
1454
|
-
"GOOGLE_CLOUD_LOCATION",
|
|
1455
|
-
"GOOGLE_APPLICATION_CREDENTIALS"
|
|
1456
|
-
],
|
|
1457
|
-
"cloudflare-ai-gateway": ["CLOUDFLARE_API_KEY", "CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_GATEWAY_ID"],
|
|
1458
|
-
"cloudflare-workers-ai": ["CLOUDFLARE_API_KEY", "CLOUDFLARE_ACCOUNT_ID"]
|
|
1504
|
+
// ../daemon/src/core/coding-environment/config.ts
|
|
1505
|
+
var CodingEnvironmentValidationError = class extends Error {
|
|
1506
|
+
status = 400;
|
|
1459
1507
|
};
|
|
1460
|
-
function
|
|
1461
|
-
|
|
1508
|
+
function invalid(field) {
|
|
1509
|
+
throw new CodingEnvironmentValidationError(`Invalid coding environment ${field}`);
|
|
1510
|
+
}
|
|
1511
|
+
function object(value, keys2, field) {
|
|
1512
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalid(field);
|
|
1513
|
+
const record = value;
|
|
1514
|
+
if (Object.keys(record).some((key) => !keys2.includes(key))) invalid(field);
|
|
1515
|
+
return record;
|
|
1516
|
+
}
|
|
1517
|
+
function text(value, max, field) {
|
|
1518
|
+
if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value) > max || Array.from(value).some((char) => {
|
|
1519
|
+
const code = char.charCodeAt(0);
|
|
1520
|
+
return code === 127 || code < 32 && code !== 9 && code !== 10;
|
|
1521
|
+
}))
|
|
1522
|
+
invalid(field);
|
|
1523
|
+
return value;
|
|
1462
1524
|
}
|
|
1463
|
-
function
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1525
|
+
function codingRelativePath(value, field = "cwd") {
|
|
1526
|
+
const path = text(value, 4096, field);
|
|
1527
|
+
if (path === ".") return path;
|
|
1528
|
+
const parts = path.split("/");
|
|
1529
|
+
if (parts.length > 16 || path.includes("\\") || Array.from(path).some((char) => char.charCodeAt(0) < 32) || parts.some((part) => !part || part === "." || part === ".." || part === ".git"))
|
|
1530
|
+
invalid(field);
|
|
1531
|
+
return path;
|
|
1532
|
+
}
|
|
1533
|
+
function validateCodingCommand(input) {
|
|
1534
|
+
const value = object(input, ["command", "cwd", "purpose", "timeoutSeconds"], "command");
|
|
1535
|
+
const command = text(value.command, 4096, "command");
|
|
1536
|
+
if (command.includes("\0")) invalid("command");
|
|
1537
|
+
const cwd = codingRelativePath(value.cwd);
|
|
1538
|
+
if (!["runtime", "dependency", "prepare", "build", "test"].includes(String(value.purpose)))
|
|
1539
|
+
invalid("purpose");
|
|
1540
|
+
if (!Number.isInteger(value.timeoutSeconds) || Number(value.timeoutSeconds) < 1 || Number(value.timeoutSeconds) > 300)
|
|
1541
|
+
invalid("timeout");
|
|
1542
|
+
return {
|
|
1543
|
+
command,
|
|
1544
|
+
cwd,
|
|
1545
|
+
purpose: value.purpose,
|
|
1546
|
+
timeoutSeconds: value.timeoutSeconds
|
|
1547
|
+
};
|
|
1469
1548
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1549
|
+
|
|
1550
|
+
// ../daemon/src/runtime/coding-directory.ts
|
|
1551
|
+
import { realpathSync as realpathSync4 } from "fs";
|
|
1552
|
+
import { join as join15 } from "path";
|
|
1553
|
+
|
|
1554
|
+
// ../daemon/src/lib/repository-context/files.ts
|
|
1555
|
+
import { createHash as createHash6 } from "crypto";
|
|
1556
|
+
import {
|
|
1557
|
+
closeSync as closeSync3,
|
|
1558
|
+
constants as constants3,
|
|
1559
|
+
fstatSync as fstatSync3,
|
|
1560
|
+
lstatSync as lstatSync2,
|
|
1561
|
+
opendirSync,
|
|
1562
|
+
openSync as openSync3,
|
|
1563
|
+
readSync as readSync2,
|
|
1564
|
+
realpathSync as realpathSync3
|
|
1565
|
+
} from "fs";
|
|
1566
|
+
import { isAbsolute as isAbsolute2, join as join14 } from "path";
|
|
1567
|
+
var hash = (value) => createHash6("sha256").update(value).digest("hex");
|
|
1568
|
+
var ContextReadError = class extends Error {
|
|
1569
|
+
code;
|
|
1570
|
+
constructor(code) {
|
|
1571
|
+
super(code);
|
|
1572
|
+
this.code = code;
|
|
1482
1573
|
}
|
|
1483
|
-
}
|
|
1484
|
-
function
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1574
|
+
};
|
|
1575
|
+
function identity(stat2) {
|
|
1576
|
+
return `${stat2.dev}:${stat2.ino}`;
|
|
1577
|
+
}
|
|
1578
|
+
function stamp(stat2) {
|
|
1579
|
+
return `${identity(stat2)}:${stat2.size}:${stat2.mtimeMs}:${stat2.ctimeMs}`;
|
|
1580
|
+
}
|
|
1581
|
+
function relativeParts(target) {
|
|
1582
|
+
if (target.length > 4096 || isAbsolute2(target) || [...target].some(
|
|
1583
|
+
(char) => char === "\\" || char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127
|
|
1584
|
+
) || target.split("/").some((part) => part === ".." || part === ".git"))
|
|
1585
|
+
throw new ContextReadError("unsafe_target");
|
|
1586
|
+
const parts = target.split("/").filter((part) => part && part !== ".");
|
|
1587
|
+
if (parts.length > 17) throw new ContextReadError("depth_limit");
|
|
1588
|
+
return parts;
|
|
1589
|
+
}
|
|
1590
|
+
var ContextDirectory = class _ContextDirectory {
|
|
1591
|
+
fd;
|
|
1592
|
+
identity;
|
|
1593
|
+
originalStamp;
|
|
1594
|
+
children = [];
|
|
1595
|
+
listing;
|
|
1596
|
+
observedFiles = /* @__PURE__ */ new Map();
|
|
1597
|
+
registeredPath;
|
|
1598
|
+
label;
|
|
1599
|
+
parent;
|
|
1600
|
+
constructor(registeredPath, label = ".", parent) {
|
|
1601
|
+
this.registeredPath = registeredPath;
|
|
1602
|
+
this.label = label;
|
|
1603
|
+
this.parent = parent;
|
|
1604
|
+
if (process.platform !== "linux") throw new ContextReadError("safe_reads_unavailable");
|
|
1605
|
+
const path = parent ? join14(parent.path, registeredPath) : realpathSync3(registeredPath);
|
|
1606
|
+
this.fd = openSync3(path, constants3.O_RDONLY | constants3.O_DIRECTORY | constants3.O_NOFOLLOW);
|
|
1607
|
+
const stat2 = fstatSync3(this.fd);
|
|
1608
|
+
this.identity = hash(identity(stat2));
|
|
1609
|
+
this.originalStamp = identity(stat2);
|
|
1610
|
+
}
|
|
1611
|
+
get path() {
|
|
1612
|
+
return `/proc/self/fd/${this.fd}`;
|
|
1613
|
+
}
|
|
1614
|
+
directory(name) {
|
|
1615
|
+
const child = new _ContextDirectory(
|
|
1616
|
+
name,
|
|
1617
|
+
this.label === "." ? name : `${this.label}/${name}`,
|
|
1618
|
+
this
|
|
1619
|
+
);
|
|
1620
|
+
this.children.push(child);
|
|
1621
|
+
return child;
|
|
1489
1622
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
return "google-generative-ai";
|
|
1498
|
-
case "google-vertex":
|
|
1499
|
-
return "google-vertex";
|
|
1500
|
-
case "azure-openai":
|
|
1501
|
-
return "azure-openai-responses";
|
|
1502
|
-
case "bedrock":
|
|
1503
|
-
return "bedrock-converse-stream";
|
|
1504
|
-
case "openai-codex":
|
|
1505
|
-
return "openai-codex-responses";
|
|
1506
|
-
default:
|
|
1507
|
-
return "openai-completions";
|
|
1623
|
+
entry(name) {
|
|
1624
|
+
try {
|
|
1625
|
+
return lstatSync2(join14(this.path, name));
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
if (error.code === "ENOENT") return null;
|
|
1628
|
+
throw new ContextReadError("unreadable");
|
|
1629
|
+
}
|
|
1508
1630
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
}
|
|
1524
|
-
async function createBazilionPiRuntime(options) {
|
|
1525
|
-
const providerName = options.providerName;
|
|
1526
|
-
const canonicalName = piProviderName(providerName);
|
|
1527
|
-
const credentials = new InMemoryCredentialStore();
|
|
1528
|
-
const apiKey = options.apiKey ?? providerApiKey(providerName, options.env);
|
|
1529
|
-
const credentialEnv = providerCredentialEnv(providerName, options.env);
|
|
1530
|
-
if (apiKey || credentialEnv) {
|
|
1531
|
-
await credentials.modify(
|
|
1532
|
-
canonicalName,
|
|
1533
|
-
async () => canonicalName === "openai-codex" && apiKey ? {
|
|
1534
|
-
type: "oauth",
|
|
1535
|
-
access: apiKey,
|
|
1536
|
-
// Bazilion owns refresh outside Pi. This deliberately empty value
|
|
1537
|
-
// satisfies Pi's canonical OAuth shape but is never used because
|
|
1538
|
-
// the token is long-lived only inside this one-shot runtime and
|
|
1539
|
-
// mid-turn replacement is supplied through Agent.getApiKey.
|
|
1540
|
-
refresh: "",
|
|
1541
|
-
expires: Number.MAX_SAFE_INTEGER
|
|
1542
|
-
} : {
|
|
1543
|
-
type: "api_key",
|
|
1544
|
-
...apiKey ? { key: apiKey } : {},
|
|
1545
|
-
...credentialEnv ? { env: credentialEnv } : {}
|
|
1631
|
+
list() {
|
|
1632
|
+
const names = this.listNames();
|
|
1633
|
+
this.listing = JSON.stringify(names);
|
|
1634
|
+
return names;
|
|
1635
|
+
}
|
|
1636
|
+
listNames() {
|
|
1637
|
+
const handle = opendirSync(this.path);
|
|
1638
|
+
const names = [];
|
|
1639
|
+
try {
|
|
1640
|
+
let entry = handle.readSync();
|
|
1641
|
+
while (entry) {
|
|
1642
|
+
if (names.length >= 16384) throw new ContextReadError("directory_entry_limit");
|
|
1643
|
+
names.push(entry.name);
|
|
1644
|
+
entry = handle.readSync();
|
|
1546
1645
|
}
|
|
1547
|
-
|
|
1646
|
+
return names.sort();
|
|
1647
|
+
} finally {
|
|
1648
|
+
handle.closeSync();
|
|
1649
|
+
}
|
|
1548
1650
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1651
|
+
read(name, maxBytes) {
|
|
1652
|
+
const entry = this.entry(name);
|
|
1653
|
+
if (!entry) {
|
|
1654
|
+
this.observedFiles.set(name, null);
|
|
1655
|
+
return null;
|
|
1656
|
+
}
|
|
1657
|
+
if (!entry.isFile() || entry.isSymbolicLink()) throw new ContextReadError("unsafe_file");
|
|
1658
|
+
if (entry.size > maxBytes) throw new ContextReadError("byte_limit");
|
|
1659
|
+
let fd;
|
|
1660
|
+
try {
|
|
1661
|
+
fd = openSync3(
|
|
1662
|
+
join14(this.path, name),
|
|
1663
|
+
constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK
|
|
1664
|
+
);
|
|
1665
|
+
const before = fstatSync3(fd);
|
|
1666
|
+
if (!before.isFile() || stamp(entry) !== stamp(before))
|
|
1667
|
+
throw new ContextReadError("source_changed");
|
|
1668
|
+
const buffer = Buffer.alloc(Math.min(maxBytes + 1, before.size + 1));
|
|
1669
|
+
let size = 0;
|
|
1670
|
+
while (size < buffer.length) {
|
|
1671
|
+
const count2 = readSync2(fd, buffer, size, buffer.length - size, null);
|
|
1672
|
+
if (!count2) break;
|
|
1673
|
+
size += count2;
|
|
1674
|
+
}
|
|
1675
|
+
if (size > maxBytes || size !== before.size || stamp(fstatSync3(fd)) !== stamp(before)) {
|
|
1676
|
+
throw new ContextReadError("source_changed");
|
|
1677
|
+
}
|
|
1678
|
+
this.observedFiles.set(name, stamp(before));
|
|
1679
|
+
return buffer.subarray(0, size);
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
if (error instanceof ContextReadError) throw error;
|
|
1682
|
+
throw new ContextReadError("unreadable");
|
|
1683
|
+
} finally {
|
|
1684
|
+
if (fd !== void 0) closeSync3(fd);
|
|
1685
|
+
}
|
|
1564
1686
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1687
|
+
validate(skipGit = false, names) {
|
|
1688
|
+
const currentPath = this.parent ? join14(this.parent.path, this.registeredPath) : realpathSync3(this.registeredPath);
|
|
1689
|
+
const stat2 = lstatSync2(currentPath);
|
|
1690
|
+
if (!stat2.isDirectory() || identity(stat2) !== this.originalStamp)
|
|
1691
|
+
throw new ContextReadError("root_changed");
|
|
1692
|
+
if (this.listing !== void 0 && JSON.stringify(this.listNames()) !== this.listing) {
|
|
1693
|
+
throw new ContextReadError("source_changed");
|
|
1694
|
+
}
|
|
1695
|
+
for (const [name, previous] of this.observedFiles) {
|
|
1696
|
+
if (names && !names.has(name)) continue;
|
|
1697
|
+
const current = this.entry(name);
|
|
1698
|
+
if ((current ? stamp(current) : null) !== previous)
|
|
1699
|
+
throw new ContextReadError("source_changed");
|
|
1700
|
+
}
|
|
1701
|
+
for (const child of this.children) {
|
|
1702
|
+
if (!skipGit || !child.label.split("/").includes(".git")) child.validate(skipGit, names);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
close() {
|
|
1706
|
+
for (const child of this.children) child.close();
|
|
1707
|
+
closeSync3(this.fd);
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1586
1710
|
|
|
1587
|
-
// ../daemon/src/runtime/
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1711
|
+
// ../daemon/src/runtime/coding-directory.ts
|
|
1712
|
+
function resolveCodingDirectory(root, cwd) {
|
|
1713
|
+
const directory = new ContextDirectory(root);
|
|
1714
|
+
try {
|
|
1715
|
+
let current = directory;
|
|
1716
|
+
for (const part of relativeParts(cwd)) current = current.directory(part);
|
|
1717
|
+
const path = realpathSync4(join15(root, cwd));
|
|
1718
|
+
directory.validate();
|
|
1719
|
+
return { path, identity: current.identity };
|
|
1720
|
+
} finally {
|
|
1721
|
+
directory.close();
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1591
1724
|
|
|
1592
|
-
// ../daemon/src/runtime/
|
|
1593
|
-
import {
|
|
1594
|
-
import { join as join14 } from "path";
|
|
1725
|
+
// ../daemon/src/runtime/shell/approval.ts
|
|
1726
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
1595
1727
|
|
|
1596
|
-
// ../daemon/src/runtime/
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1728
|
+
// ../daemon/src/runtime/shell/security.ts
|
|
1729
|
+
var DEFAULT_BASH_SANDBOX_IMAGE = "debian:bookworm-slim";
|
|
1730
|
+
var SAFE_SHELL_ENV_KEYS = [
|
|
1731
|
+
"PATH",
|
|
1732
|
+
"HOME",
|
|
1733
|
+
"LANG",
|
|
1734
|
+
"LANGUAGE",
|
|
1735
|
+
"LC_ALL",
|
|
1736
|
+
"LC_CTYPE",
|
|
1737
|
+
"TERM",
|
|
1738
|
+
"SHELL",
|
|
1739
|
+
"TMPDIR",
|
|
1740
|
+
"TZ"
|
|
1741
|
+
];
|
|
1742
|
+
var ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1743
|
+
var RESERVED_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1744
|
+
var DOCKER_IMAGE_RE = /^[A-Za-z0-9][A-Za-z0-9._/:@-]*$/;
|
|
1745
|
+
var DOCKER_CLIENT_CONTROL_ENV = /* @__PURE__ */ new Set([
|
|
1746
|
+
"DOCKER_HOST",
|
|
1747
|
+
"DOCKER_CONTEXT",
|
|
1748
|
+
"DOCKER_CONFIG",
|
|
1749
|
+
"DOCKER_CERT_PATH",
|
|
1750
|
+
"DOCKER_TLS_VERIFY",
|
|
1751
|
+
"DOCKER_API_VERSION",
|
|
1752
|
+
"XDG_CONFIG_HOME",
|
|
1753
|
+
"XDG_RUNTIME_DIR"
|
|
1754
|
+
]);
|
|
1755
|
+
var SANDBOX_CONTROL_ENV = /* @__PURE__ */ new Set(["BASH_ENV"]);
|
|
1756
|
+
var SENSITIVE_HOME_PATH_RE = /(?:~(?:[A-Za-z0-9_-]+)?|\$(?:HOME|\{HOME\})|\/(?:home|Users)\/[^\s'"`;|&/]+|\/root)\/\.(?:ssh|aws|gnupg)(?:\/[^\s'"`;|&]*)?/gi;
|
|
1757
|
+
var SENSITIVE_FILE_RE = /(?:\bauth\.json\b|\bbazilion\.db\b|(?:^|[\s/'"=])\.env(?:\.[A-Za-z0-9_-]+)?(?=$|[\s/'"`;|&])|\/etc\/(?:shadow|sudoers)(?:\.d\/[^\s'"`;|&]+)?)/gim;
|
|
1758
|
+
var SENSITIVE_ENV_RE = /\b(?:OPENAI_CODEX_OAUTH|AWS_ACCESS_KEY_ID|AWS_PROFILE|GOOGLE_APPLICATION_CREDENTIALS|BAZILION_TOKEN|(?:[A-Z][A-Z0-9_]*_)?(?:API_KEY|ACCESS_TOKEN|AUTH_TOKEN|OAUTH_TOKEN|CLIENT_SECRET|SECRET_ACCESS_KEY|PRIVATE_KEY))\b/g;
|
|
1759
|
+
var REMOTE_PIPE_RE = /(?:^|[;&|(\n])\s*(?:command\s+)?(?:curl|wget)\b[^;&\n]*(?:\|\s*(?:(?:sudo|env)\s+)*(?:[^\s;&|()]+\/)?(?:sh|bash|dash|zsh|fish|node|python(?:3)?|perl|ruby)\b)/gim;
|
|
1760
|
+
var COMMAND_BOUNDARY_RE = /[;&|)\n]/;
|
|
1761
|
+
var ShellSecurityConfigError = class extends Error {
|
|
1762
|
+
constructor(message) {
|
|
1763
|
+
super(message);
|
|
1764
|
+
this.name = "ShellSecurityConfigError";
|
|
1613
1765
|
}
|
|
1614
|
-
|
|
1766
|
+
};
|
|
1767
|
+
function isValidEnvName(name) {
|
|
1768
|
+
return ENV_NAME_RE.test(name) && !RESERVED_OBJECT_KEYS.has(name);
|
|
1615
1769
|
}
|
|
1616
|
-
function
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1770
|
+
function addRisk(command, risks, seen, input) {
|
|
1771
|
+
const start = Math.max(0, input.start);
|
|
1772
|
+
const end = Math.min(command.length, Math.max(start, input.end));
|
|
1773
|
+
const key = `${input.code}:${start}:${end}`;
|
|
1774
|
+
if (seen.has(key)) return;
|
|
1775
|
+
seen.add(key);
|
|
1776
|
+
risks.push({
|
|
1777
|
+
code: input.code,
|
|
1778
|
+
severity: input.severity ?? "danger",
|
|
1779
|
+
message: input.message,
|
|
1780
|
+
matchedText: command.slice(start, end),
|
|
1781
|
+
span: { start, end }
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
function addRegexRisks(command, risks, seen, pattern, code, message) {
|
|
1785
|
+
pattern.lastIndex = 0;
|
|
1786
|
+
for (const match of command.matchAll(pattern)) {
|
|
1787
|
+
if (match.index === void 0) continue;
|
|
1788
|
+
let start = match.index;
|
|
1789
|
+
let matchedText = match[0];
|
|
1790
|
+
const leading = matchedText.match(/^[;&|(\n\s]+/);
|
|
1791
|
+
if (leading) {
|
|
1792
|
+
start += leading[0].length;
|
|
1793
|
+
matchedText = matchedText.slice(leading[0].length);
|
|
1794
|
+
}
|
|
1795
|
+
addRisk(command, risks, seen, {
|
|
1796
|
+
code,
|
|
1797
|
+
message,
|
|
1798
|
+
start,
|
|
1799
|
+
end: start + matchedText.length
|
|
1800
|
+
});
|
|
1627
1801
|
}
|
|
1628
1802
|
}
|
|
1629
|
-
function
|
|
1630
|
-
return
|
|
1803
|
+
function shellTokens(fragment) {
|
|
1804
|
+
return Array.from(
|
|
1805
|
+
fragment.matchAll(/"(?:\\.|[^"\\])*"|'[^']*'|[^\s]+/g),
|
|
1806
|
+
(match) => match[0].replace(/^(?:"|')|(?:"|')$/g, "")
|
|
1807
|
+
);
|
|
1631
1808
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
const
|
|
1639
|
-
const
|
|
1640
|
-
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1809
|
+
function isBroadFilesystemTarget(rawTarget) {
|
|
1810
|
+
const target = rawTarget.replace(/[;,]+$/, "");
|
|
1811
|
+
if (target.startsWith("/")) return true;
|
|
1812
|
+
return /^(?:\.|\.\/\*+|\*+|~(?:\/.*)?|\$(?:HOME|\{HOME\})(?:\/.*)?|\.\.(?:\/.*)?)$/.test(target);
|
|
1813
|
+
}
|
|
1814
|
+
function classifyBroadFilesystemOperations(command, risks, seen) {
|
|
1815
|
+
const commandRe = /(?:^|[;&|(\n])\s*(?:command\s+)?(rm|find|chmod|chown)\b[^;&|)\n]*/gim;
|
|
1816
|
+
for (const match of command.matchAll(commandRe)) {
|
|
1817
|
+
if (match.index === void 0 || !match[1]) continue;
|
|
1818
|
+
const nameOffset = match[0].toLowerCase().indexOf(match[1].toLowerCase());
|
|
1819
|
+
const start = match.index + nameOffset;
|
|
1820
|
+
const text2 = command.slice(start, match.index + match[0].length).trimEnd();
|
|
1821
|
+
const tokens = shellTokens(text2);
|
|
1822
|
+
const name = tokens[0]?.toLowerCase();
|
|
1823
|
+
const args = tokens.slice(1);
|
|
1824
|
+
let risky = false;
|
|
1825
|
+
if (name === "rm") {
|
|
1826
|
+
const recursive = args.some(
|
|
1827
|
+
(arg) => arg === "--recursive" || /^-[^-]+$/.test(arg) && /[rR]/.test(arg)
|
|
1828
|
+
);
|
|
1829
|
+
risky = recursive && args.some((arg) => !arg.startsWith("-") && isBroadFilesystemTarget(arg));
|
|
1830
|
+
} else if (name === "find") {
|
|
1831
|
+
const target = args.find((arg) => !arg.startsWith("-"));
|
|
1832
|
+
risky = args.includes("-delete") && target !== void 0 && isBroadFilesystemTarget(target);
|
|
1833
|
+
} else if (name === "chmod" || name === "chown") {
|
|
1834
|
+
const recursive = args.some(
|
|
1835
|
+
(arg) => arg === "--recursive" || /^-[^-]+$/.test(arg) && /R/.test(arg)
|
|
1836
|
+
);
|
|
1837
|
+
risky = recursive && args.some((arg) => !arg.startsWith("-") && isBroadFilesystemTarget(arg));
|
|
1653
1838
|
}
|
|
1839
|
+
if (!risky) continue;
|
|
1840
|
+
addRisk(command, risks, seen, {
|
|
1841
|
+
code: "broad-destructive-operation",
|
|
1842
|
+
message: "Recursively changes or deletes a broad filesystem target.",
|
|
1843
|
+
start,
|
|
1844
|
+
end: start + text2.length
|
|
1845
|
+
});
|
|
1654
1846
|
}
|
|
1655
|
-
return loaded;
|
|
1656
1847
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1848
|
+
function isLocalHostname(rawHostname) {
|
|
1849
|
+
const hostname = rawHostname.trim().replace(/^.*@/, "").replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
|
|
1850
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || hostname === "0.0.0.0" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
1851
|
+
}
|
|
1852
|
+
function hasExternalHttpDestination(segment) {
|
|
1853
|
+
const urls = Array.from(segment.matchAll(/https?:\/\/[^\s'"`]+/gi), (match) => match[0]);
|
|
1854
|
+
if (urls.length === 0) return true;
|
|
1855
|
+
return urls.some((rawUrl) => {
|
|
1661
1856
|
try {
|
|
1662
|
-
return
|
|
1663
|
-
} catch
|
|
1664
|
-
|
|
1665
|
-
throw new Error(`private Agent home is unsafe at ${file}`);
|
|
1857
|
+
return !isLocalHostname(new URL(rawUrl).hostname);
|
|
1858
|
+
} catch {
|
|
1859
|
+
return true;
|
|
1666
1860
|
}
|
|
1667
|
-
};
|
|
1668
|
-
return {
|
|
1669
|
-
"AGENTS.md": read("AGENTS.md"),
|
|
1670
|
-
"SOUL.md": read("SOUL.md"),
|
|
1671
|
-
"TOOLS.md": read("TOOLS.md"),
|
|
1672
|
-
"IDENTITY.md": read("IDENTITY.md"),
|
|
1673
|
-
"BOOTSTRAP.md": read("BOOTSTRAP.md")
|
|
1674
|
-
};
|
|
1861
|
+
});
|
|
1675
1862
|
}
|
|
1676
|
-
function
|
|
1677
|
-
const
|
|
1678
|
-
const
|
|
1679
|
-
const
|
|
1680
|
-
|
|
1681
|
-
const content = homeDocuments[file]?.trimEnd();
|
|
1682
|
-
if (!content) continue;
|
|
1683
|
-
contextBlocks.push(`## ${file}
|
|
1684
|
-
|
|
1685
|
-
${content}`);
|
|
1686
|
-
}
|
|
1687
|
-
if (contextBlocks.length > 0) {
|
|
1688
|
-
parts.push(`# Project Context
|
|
1689
|
-
|
|
1690
|
-
${contextBlocks.join("\n\n")}`);
|
|
1863
|
+
function remoteTransferHosts(segment) {
|
|
1864
|
+
const hosts = [];
|
|
1865
|
+
const uriRe = /(?:sftp|ssh|rsync):\/\/(?:[^@\s/]+@)?(\[[^\]]+\]|[^\s/:]+)(?::\d+)?/gi;
|
|
1866
|
+
for (const match of segment.matchAll(uriRe)) {
|
|
1867
|
+
if (match[1]) hosts.push(match[1]);
|
|
1691
1868
|
}
|
|
1692
|
-
const
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
[
|
|
1696
|
-
"# First-Run Ritual",
|
|
1697
|
-
"",
|
|
1698
|
-
"This is your first session. The document below is **conversational guidance**, not a checklist to execute in one shot. It describes a multi-turn Q&A you should have with the human, one question per turn.",
|
|
1699
|
-
"",
|
|
1700
|
-
"## Hard rules",
|
|
1701
|
-
"- Your first reply is ONLY a greeting + ONE question. No tool calls. Wait for the human to answer.",
|
|
1702
|
-
"- Each subsequent turn: at most one new question. Wait between turns.",
|
|
1703
|
-
"- Only after the ritual is complete (you have enough to write IDENTITY.md): call `home_write` once, then `bootstrap_done`.",
|
|
1704
|
-
"",
|
|
1705
|
-
"## BOOTSTRAP.md",
|
|
1706
|
-
"",
|
|
1707
|
-
bootstrap
|
|
1708
|
-
].join("\n")
|
|
1709
|
-
);
|
|
1869
|
+
const scpRe = /(?:^|\s)(?:[^@\s:]+@)?(\[[^\]]+\]|[A-Za-z0-9._-]+):[^\s]+/g;
|
|
1870
|
+
for (const match of segment.matchAll(scpRe)) {
|
|
1871
|
+
if (match[1]) hosts.push(match[1]);
|
|
1710
1872
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
const
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1873
|
+
return hosts;
|
|
1874
|
+
}
|
|
1875
|
+
function invocationSegment(command, match, commandName) {
|
|
1876
|
+
if (match.index === void 0) return null;
|
|
1877
|
+
const nameOffset = match[0].toLowerCase().indexOf(commandName.toLowerCase());
|
|
1878
|
+
if (nameOffset < 0) return null;
|
|
1879
|
+
const start = match.index + nameOffset;
|
|
1880
|
+
const boundaryOffset = command.slice(start).search(COMMAND_BOUNDARY_RE);
|
|
1881
|
+
const end = boundaryOffset < 0 ? command.length : start + boundaryOffset;
|
|
1882
|
+
return { start, text: command.slice(start, end).trimEnd() };
|
|
1883
|
+
}
|
|
1884
|
+
function classifyOutboundExfiltration(command, risks, seen) {
|
|
1885
|
+
const invocationRe = /(?:^|[;&|(\n])\s*(?:command\s+)?(curl|wget|scp|rsync|nc|netcat|sftp)\b/gim;
|
|
1886
|
+
for (const match of command.matchAll(invocationRe)) {
|
|
1887
|
+
const name = match[1]?.toLowerCase();
|
|
1888
|
+
if (!name) continue;
|
|
1889
|
+
const segment = invocationSegment(command, match, name);
|
|
1890
|
+
if (!segment) continue;
|
|
1891
|
+
let risky = false;
|
|
1892
|
+
if (name === "curl") {
|
|
1893
|
+
const uploads = /(?:^|\s)(?:-X\s*POST\b|-[dFT](?:\s|[^\s]))/.test(segment.text) || /(?:^|\s)(?:--request(?:=|\s+)POST\b|--data(?:-[a-z-]+)?(?:=|\s)|--form(?:-string)?(?:=|\s)|--upload-file(?:=|\s))/i.test(
|
|
1894
|
+
segment.text
|
|
1895
|
+
);
|
|
1896
|
+
risky = uploads && hasExternalHttpDestination(segment.text);
|
|
1897
|
+
} else if (name === "wget") {
|
|
1898
|
+
const uploads = /(?:^|\s)(?:--method(?:=|\s+)POST\b|--post-data(?:=|\s)|--post-file(?:=|\s)|--body-data(?:=|\s)|--body-file(?:=|\s))/i.test(
|
|
1899
|
+
segment.text
|
|
1900
|
+
);
|
|
1901
|
+
risky = uploads && hasExternalHttpDestination(segment.text);
|
|
1902
|
+
} else if (name === "scp" || name === "rsync") {
|
|
1903
|
+
const hosts = remoteTransferHosts(segment.text);
|
|
1904
|
+
risky = hosts.some((host) => !isLocalHostname(host)) || hosts.length === 0 && /\$/.test(segment.text);
|
|
1905
|
+
} else if (name === "sftp") {
|
|
1906
|
+
const hosts = remoteTransferHosts(segment.text);
|
|
1907
|
+
if (hosts.length > 0) {
|
|
1908
|
+
risky = hosts.some((host) => !isLocalHostname(host));
|
|
1909
|
+
} else {
|
|
1910
|
+
const destination = shellTokens(segment.text).at(-1);
|
|
1911
|
+
risky = destination !== void 0 && destination !== name && !isLocalHostname(destination);
|
|
1912
|
+
}
|
|
1913
|
+
} else {
|
|
1914
|
+
const tokens = shellTokens(segment.text);
|
|
1915
|
+
const host = tokens.find(
|
|
1916
|
+
(token, index) => index > 0 && !token.startsWith("-") && !/^\d+$/.test(token) && !["tcp", "udp"].includes(token.toLowerCase())
|
|
1917
|
+
);
|
|
1918
|
+
risky = host !== void 0 && !isLocalHostname(host);
|
|
1919
|
+
}
|
|
1920
|
+
if (!risky) continue;
|
|
1921
|
+
addRisk(command, risks, seen, {
|
|
1922
|
+
code: "outbound-exfiltration",
|
|
1923
|
+
message: "Uploads data or opens a transfer connection to a non-local destination.",
|
|
1924
|
+
start: segment.start,
|
|
1925
|
+
end: segment.start + segment.text.length
|
|
1738
1926
|
});
|
|
1739
|
-
const unavailableNote = unavailable.length > 0 ? `Attached but unavailable or invalid: ${unavailable.join(", ")}.` : "";
|
|
1740
|
-
parts.push(
|
|
1741
|
-
[
|
|
1742
|
-
"# Available Skills",
|
|
1743
|
-
"",
|
|
1744
|
-
"Use these attached skill instructions when they are relevant to the task.",
|
|
1745
|
-
"",
|
|
1746
|
-
...skillBlocks,
|
|
1747
|
-
...unavailableNote ? ["", unavailableNote] : []
|
|
1748
|
-
].join("\n")
|
|
1749
|
-
);
|
|
1750
1927
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1928
|
+
}
|
|
1929
|
+
function classifyPrivilegeAndCredentialAccess(command, risks, seen) {
|
|
1930
|
+
const patterns = [
|
|
1931
|
+
/(?:^|[;&|(\n])\s*(?:sudo|su|doas|pkexec)\b[^;&|)\n]*/gim,
|
|
1932
|
+
/(?:^|[;&|(\n])\s*security\s+(?:find-(?:generic|internet)-password|dump-keychain)\b[^;&|)\n]*/gim,
|
|
1933
|
+
/(?:^|[;&|(\n])\s*gcloud\s+auth\b[^;&|)\n]*/gim,
|
|
1934
|
+
/(?:^|[;&|(\n])\s*aws\s+(?:configure|sso\s+login)\b[^;&|)\n]*/gim,
|
|
1935
|
+
/(?:^|[;&|(\n])\s*git\s+credential(?:-[A-Za-z0-9_-]+|\s+(?:fill|approve|reject))?\b[^;&|)\n]*/gim,
|
|
1936
|
+
/(?:^|[;&|(\n])\s*(?:docker|npm|gh)\s+(?:login|auth\s+token)\b[^;&|)\n]*/gim,
|
|
1937
|
+
/(?:^|[;&|(\n])\s*(?:pass\s+(?:show|ls)|secret-tool\s+lookup|ssh-add\s+-[Ll])\b[^;&|)\n]*/gim,
|
|
1938
|
+
/(?:^|[;&|(\n])\s*(?:env|printenv|set|export\s+-p)\s*(?=$|[;&|)\n])/gim
|
|
1757
1939
|
];
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
);
|
|
1767
|
-
} else {
|
|
1768
|
-
parts.push(
|
|
1769
|
-
`# About the User
|
|
1770
|
-
|
|
1771
|
-
This team's USER.md is empty. As you learn STABLE facts about the human (preferences, role, working hours, how they like to be addressed), populate it via \`user_md_get\` then \`user_md_write\` (always get first \u2014 you need the etag). Reserve this for things you're confident are durable \u2014 project knowledge belongs in \`memory_write\`, personal notes about yourself in \`home_write\` on IDENTITY.md. **Do NOT send peer messages announcing USER.md changes \u2014 every agent in the team sees the new content in their system prompt on their next turn automatically.**`
|
|
1772
|
-
);
|
|
1773
|
-
}
|
|
1774
|
-
const reviewedLessons = boundReviewedLessons(agent.privateLessons);
|
|
1775
|
-
if (reviewedLessons.length > 0) {
|
|
1776
|
-
parts.push(
|
|
1777
|
-
`# Reviewed lessons
|
|
1778
|
-
|
|
1779
|
-
These private lessons were explicitly approved by the human for your behavior. Follow them when relevant; current user instructions and higher-priority policy still win.
|
|
1780
|
-
|
|
1781
|
-
${reviewedLessons.map((lesson) => `- ${lesson}`).join("\n")}`
|
|
1940
|
+
for (const pattern of patterns) {
|
|
1941
|
+
addRegexRisks(
|
|
1942
|
+
command,
|
|
1943
|
+
risks,
|
|
1944
|
+
seen,
|
|
1945
|
+
pattern,
|
|
1946
|
+
"privilege-or-credential-access",
|
|
1947
|
+
"Requests elevated privileges or accesses a credential store."
|
|
1782
1948
|
);
|
|
1783
1949
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1950
|
+
}
|
|
1951
|
+
function classifyBashCommand(command) {
|
|
1952
|
+
const risks = [];
|
|
1953
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1954
|
+
addRegexRisks(
|
|
1955
|
+
command,
|
|
1956
|
+
risks,
|
|
1957
|
+
seen,
|
|
1958
|
+
SENSITIVE_HOME_PATH_RE,
|
|
1959
|
+
"sensitive-path-read",
|
|
1960
|
+
"Accesses a path that commonly contains credentials."
|
|
1786
1961
|
);
|
|
1787
|
-
|
|
1962
|
+
addRegexRisks(
|
|
1963
|
+
command,
|
|
1964
|
+
risks,
|
|
1965
|
+
seen,
|
|
1966
|
+
SENSITIVE_FILE_RE,
|
|
1967
|
+
"sensitive-path-read",
|
|
1968
|
+
"Accesses a sensitive environment, authentication, or database file."
|
|
1969
|
+
);
|
|
1970
|
+
addRegexRisks(
|
|
1971
|
+
command,
|
|
1972
|
+
risks,
|
|
1973
|
+
seen,
|
|
1974
|
+
SENSITIVE_ENV_RE,
|
|
1975
|
+
"sensitive-path-read",
|
|
1976
|
+
"References an environment variable name commonly used for credentials."
|
|
1977
|
+
);
|
|
1978
|
+
classifyBroadFilesystemOperations(command, risks, seen);
|
|
1979
|
+
addRegexRisks(
|
|
1980
|
+
command,
|
|
1981
|
+
risks,
|
|
1982
|
+
seen,
|
|
1983
|
+
REMOTE_PIPE_RE,
|
|
1984
|
+
"remote-pipe-execution",
|
|
1985
|
+
"Pipes downloaded content directly into an interpreter."
|
|
1986
|
+
);
|
|
1987
|
+
classifyOutboundExfiltration(command, risks, seen);
|
|
1988
|
+
classifyPrivilegeAndCredentialAccess(command, risks, seen);
|
|
1989
|
+
return risks.sort((a, b) => a.span.start - b.span.start || a.code.localeCompare(b.code));
|
|
1788
1990
|
}
|
|
1789
|
-
function
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
if (remaining <= 0) break;
|
|
1797
|
-
selected.push(lesson.slice(0, remaining));
|
|
1798
|
-
used += Math.min(lesson.length, remaining);
|
|
1991
|
+
function parseEnvAllowlist(raw) {
|
|
1992
|
+
if (raw === void 0 || raw.trim() === "") return [];
|
|
1993
|
+
const values = raw.split(",").map((value) => value.trim());
|
|
1994
|
+
if (values.some((value) => value === "")) {
|
|
1995
|
+
throw new ShellSecurityConfigError(
|
|
1996
|
+
"BAZILION_BASH_SANDBOX_ENV_ALLOWLIST contains an empty environment variable name"
|
|
1997
|
+
);
|
|
1799
1998
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
"LANGUAGE",
|
|
1822
|
-
"LC_ALL",
|
|
1823
|
-
"LC_CTYPE",
|
|
1824
|
-
"TERM",
|
|
1825
|
-
"SHELL",
|
|
1826
|
-
"TMPDIR",
|
|
1827
|
-
"TZ"
|
|
1828
|
-
];
|
|
1829
|
-
var ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1830
|
-
var RESERVED_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1831
|
-
var DOCKER_IMAGE_RE = /^[A-Za-z0-9][A-Za-z0-9._/:@-]*$/;
|
|
1832
|
-
var DOCKER_CLIENT_CONTROL_ENV = /* @__PURE__ */ new Set([
|
|
1833
|
-
"DOCKER_HOST",
|
|
1834
|
-
"DOCKER_CONTEXT",
|
|
1835
|
-
"DOCKER_CONFIG",
|
|
1836
|
-
"DOCKER_CERT_PATH",
|
|
1837
|
-
"DOCKER_TLS_VERIFY",
|
|
1838
|
-
"DOCKER_API_VERSION",
|
|
1839
|
-
"XDG_CONFIG_HOME",
|
|
1840
|
-
"XDG_RUNTIME_DIR"
|
|
1841
|
-
]);
|
|
1842
|
-
var SANDBOX_CONTROL_ENV = /* @__PURE__ */ new Set(["BASH_ENV"]);
|
|
1843
|
-
var SENSITIVE_HOME_PATH_RE = /(?:~(?:[A-Za-z0-9_-]+)?|\$(?:HOME|\{HOME\})|\/(?:home|Users)\/[^\s'"`;|&/]+|\/root)\/\.(?:ssh|aws|gnupg)(?:\/[^\s'"`;|&]*)?/gi;
|
|
1844
|
-
var SENSITIVE_FILE_RE = /(?:\bauth\.json\b|\bbazilion\.db\b|(?:^|[\s/'"=])\.env(?:\.[A-Za-z0-9_-]+)?(?=$|[\s/'"`;|&])|\/etc\/(?:shadow|sudoers)(?:\.d\/[^\s'"`;|&]+)?)/gim;
|
|
1845
|
-
var SENSITIVE_ENV_RE = /\b(?:OPENAI_CODEX_OAUTH|AWS_ACCESS_KEY_ID|AWS_PROFILE|GOOGLE_APPLICATION_CREDENTIALS|BAZILION_TOKEN|(?:[A-Z][A-Z0-9_]*_)?(?:API_KEY|ACCESS_TOKEN|AUTH_TOKEN|OAUTH_TOKEN|CLIENT_SECRET|SECRET_ACCESS_KEY|PRIVATE_KEY))\b/g;
|
|
1846
|
-
var REMOTE_PIPE_RE = /(?:^|[;&|(\n])\s*(?:command\s+)?(?:curl|wget)\b[^;&\n]*(?:\|\s*(?:(?:sudo|env)\s+)*(?:[^\s;&|()]+\/)?(?:sh|bash|dash|zsh|fish|node|python(?:3)?|perl|ruby)\b)/gim;
|
|
1847
|
-
var COMMAND_BOUNDARY_RE = /[;&|)\n]/;
|
|
1848
|
-
var ShellSecurityConfigError = class extends Error {
|
|
1849
|
-
constructor(message) {
|
|
1850
|
-
super(message);
|
|
1851
|
-
this.name = "ShellSecurityConfigError";
|
|
1852
|
-
}
|
|
1853
|
-
};
|
|
1854
|
-
function isValidEnvName(name) {
|
|
1855
|
-
return ENV_NAME_RE.test(name) && !RESERVED_OBJECT_KEYS.has(name);
|
|
1856
|
-
}
|
|
1857
|
-
function addRisk(command, risks, seen, input) {
|
|
1858
|
-
const start = Math.max(0, input.start);
|
|
1859
|
-
const end = Math.min(command.length, Math.max(start, input.end));
|
|
1860
|
-
const key = `${input.code}:${start}:${end}`;
|
|
1861
|
-
if (seen.has(key)) return;
|
|
1862
|
-
seen.add(key);
|
|
1863
|
-
risks.push({
|
|
1864
|
-
code: input.code,
|
|
1865
|
-
severity: input.severity ?? "danger",
|
|
1866
|
-
message: input.message,
|
|
1867
|
-
matchedText: command.slice(start, end),
|
|
1868
|
-
span: { start, end }
|
|
1869
|
-
});
|
|
1870
|
-
}
|
|
1871
|
-
function addRegexRisks(command, risks, seen, pattern, code, message) {
|
|
1872
|
-
pattern.lastIndex = 0;
|
|
1873
|
-
for (const match of command.matchAll(pattern)) {
|
|
1874
|
-
if (match.index === void 0) continue;
|
|
1875
|
-
let start = match.index;
|
|
1876
|
-
let matchedText = match[0];
|
|
1877
|
-
const leading = matchedText.match(/^[;&|(\n\s]+/);
|
|
1878
|
-
if (leading) {
|
|
1879
|
-
start += leading[0].length;
|
|
1880
|
-
matchedText = matchedText.slice(leading[0].length);
|
|
1999
|
+
const unique = [];
|
|
2000
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2001
|
+
for (const value of values) {
|
|
2002
|
+
if (!isValidEnvName(value)) {
|
|
2003
|
+
throw new ShellSecurityConfigError(
|
|
2004
|
+
`BAZILION_BASH_SANDBOX_ENV_ALLOWLIST contains invalid environment variable name: ${value}`
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
if (DOCKER_CLIENT_CONTROL_ENV.has(value)) {
|
|
2008
|
+
throw new ShellSecurityConfigError(
|
|
2009
|
+
`BAZILION_BASH_SANDBOX_ENV_ALLOWLIST cannot include Docker client control variable: ${value}`
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
if (SANDBOX_CONTROL_ENV.has(value)) {
|
|
2013
|
+
throw new ShellSecurityConfigError(
|
|
2014
|
+
`BAZILION_BASH_SANDBOX_ENV_ALLOWLIST cannot include shell startup control variable: ${value}`
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
if (!seen.has(value)) {
|
|
2018
|
+
seen.add(value);
|
|
2019
|
+
unique.push(value);
|
|
1881
2020
|
}
|
|
1882
|
-
addRisk(command, risks, seen, {
|
|
1883
|
-
code,
|
|
1884
|
-
message,
|
|
1885
|
-
start,
|
|
1886
|
-
end: start + matchedText.length
|
|
1887
|
-
});
|
|
1888
2021
|
}
|
|
2022
|
+
return unique;
|
|
1889
2023
|
}
|
|
1890
|
-
function
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
2024
|
+
function resolveShellSecurityConfig(env = process.env) {
|
|
2025
|
+
const rawMode = env.BAZILION_BASH_SANDBOX?.trim() ?? "";
|
|
2026
|
+
if (rawMode !== "" && rawMode !== "off" && rawMode !== "docker") {
|
|
2027
|
+
throw new ShellSecurityConfigError(
|
|
2028
|
+
`BAZILION_BASH_SANDBOX must be "off" or "docker", received: ${rawMode}`
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
const rawApproval = env.BAZILION_BASH_APPROVAL?.trim() ?? "";
|
|
2032
|
+
if (rawApproval !== "" && rawApproval !== "off" && rawApproval !== "dangerous") {
|
|
2033
|
+
throw new ShellSecurityConfigError(
|
|
2034
|
+
`BAZILION_BASH_APPROVAL must be "off" or "dangerous", received: ${rawApproval}`
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
const rawImage = env.BAZILION_BASH_SANDBOX_IMAGE?.trim() ?? "";
|
|
2038
|
+
if (rawImage !== "" && !DOCKER_IMAGE_RE.test(rawImage)) {
|
|
2039
|
+
throw new ShellSecurityConfigError(
|
|
2040
|
+
`BAZILION_BASH_SANDBOX_IMAGE is not a valid Docker image reference: ${rawImage}`
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
return {
|
|
2044
|
+
sandboxMode: rawMode === "docker" ? "docker" : "off",
|
|
2045
|
+
approvalMode: rawApproval === "dangerous" ? "dangerous" : "off",
|
|
2046
|
+
sandboxImage: rawImage || DEFAULT_BASH_SANDBOX_IMAGE,
|
|
2047
|
+
envAllowlist: parseEnvAllowlist(env.BAZILION_BASH_SANDBOX_ENV_ALLOWLIST)
|
|
2048
|
+
};
|
|
1895
2049
|
}
|
|
1896
|
-
function
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2050
|
+
function assertEnvName(name) {
|
|
2051
|
+
if (!isValidEnvName(name)) {
|
|
2052
|
+
throw new ShellSecurityConfigError(`invalid shell environment variable name: ${name}`);
|
|
2053
|
+
}
|
|
2054
|
+
if (DOCKER_CLIENT_CONTROL_ENV.has(name) || SANDBOX_CONTROL_ENV.has(name)) {
|
|
2055
|
+
throw new ShellSecurityConfigError(`unsafe shell environment variable name: ${name}`);
|
|
2056
|
+
}
|
|
1900
2057
|
}
|
|
1901
|
-
function
|
|
1902
|
-
const
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
const text = command.slice(start, match.index + match[0].length).trimEnd();
|
|
1908
|
-
const tokens = shellTokens(text);
|
|
1909
|
-
const name = tokens[0]?.toLowerCase();
|
|
1910
|
-
const args = tokens.slice(1);
|
|
1911
|
-
let risky = false;
|
|
1912
|
-
if (name === "rm") {
|
|
1913
|
-
const recursive = args.some(
|
|
1914
|
-
(arg) => arg === "--recursive" || /^-[^-]+$/.test(arg) && /[rR]/.test(arg)
|
|
1915
|
-
);
|
|
1916
|
-
risky = recursive && args.some((arg) => !arg.startsWith("-") && isBroadFilesystemTarget(arg));
|
|
1917
|
-
} else if (name === "find") {
|
|
1918
|
-
const target = args.find((arg) => !arg.startsWith("-"));
|
|
1919
|
-
risky = args.includes("-delete") && target !== void 0 && isBroadFilesystemTarget(target);
|
|
1920
|
-
} else if (name === "chmod" || name === "chown") {
|
|
1921
|
-
const recursive = args.some(
|
|
1922
|
-
(arg) => arg === "--recursive" || /^-[^-]+$/.test(arg) && /R/.test(arg)
|
|
1923
|
-
);
|
|
1924
|
-
risky = recursive && args.some((arg) => !arg.startsWith("-") && isBroadFilesystemTarget(arg));
|
|
1925
|
-
}
|
|
1926
|
-
if (!risky) continue;
|
|
1927
|
-
addRisk(command, risks, seen, {
|
|
1928
|
-
code: "broad-destructive-operation",
|
|
1929
|
-
message: "Recursively changes or deletes a broad filesystem target.",
|
|
1930
|
-
start,
|
|
1931
|
-
end: start + text.length
|
|
1932
|
-
});
|
|
2058
|
+
function buildScrubbedShellEnv(env, allowedKeys = []) {
|
|
2059
|
+
const scrubbed = {};
|
|
2060
|
+
const keys2 = new Set(SAFE_SHELL_ENV_KEYS);
|
|
2061
|
+
for (const key of allowedKeys) {
|
|
2062
|
+
assertEnvName(key);
|
|
2063
|
+
keys2.add(key);
|
|
1933
2064
|
}
|
|
2065
|
+
for (const key of keys2) {
|
|
2066
|
+
const value = env[key];
|
|
2067
|
+
if (value !== void 0) scrubbed[key] = value;
|
|
2068
|
+
}
|
|
2069
|
+
return scrubbed;
|
|
1934
2070
|
}
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
2071
|
+
|
|
2072
|
+
// ../daemon/src/runtime/shell/approval.ts
|
|
2073
|
+
var BashApprovalDeniedError = class extends Error {
|
|
2074
|
+
risks;
|
|
2075
|
+
constructor(risks) {
|
|
2076
|
+
const codes = [...new Set(risks.map((risk) => risk.code))];
|
|
2077
|
+
super(`Dangerous bash command denied (${codes.join(", ")})`);
|
|
2078
|
+
this.name = "BashApprovalDeniedError";
|
|
2079
|
+
this.risks = risks;
|
|
2080
|
+
}
|
|
2081
|
+
};
|
|
2082
|
+
async function requireBashApproval(command, toolCallId, signal, host) {
|
|
2083
|
+
const risks = classifyBashCommand(command);
|
|
2084
|
+
if (!risks.length) return;
|
|
2085
|
+
const decision = host ? await host.requestApproval({ toolCallId, command, risks, signal }) : "denied";
|
|
2086
|
+
if (decision !== "approved") throw new BashApprovalDeniedError(risks);
|
|
1938
2087
|
}
|
|
1939
|
-
function
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
return
|
|
2088
|
+
function createApprovalGatedBashTool(base, host) {
|
|
2089
|
+
return defineTool({
|
|
2090
|
+
...base,
|
|
2091
|
+
description: `${base.description} Commands classified as dangerous require operator approval before execution.`,
|
|
2092
|
+
executionMode: "sequential",
|
|
2093
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
2094
|
+
await requireBashApproval(params.command, toolCallId, signal, host);
|
|
2095
|
+
return base.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
1947
2096
|
}
|
|
1948
2097
|
});
|
|
1949
2098
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2099
|
+
|
|
2100
|
+
// ../daemon/src/runtime/shell/docker.ts
|
|
2101
|
+
import { spawn } from "child_process";
|
|
2102
|
+
import { randomUUID as randomUUID18 } from "crypto";
|
|
2103
|
+
import { closeSync as closeSync4, constants as constants4, mkdtempSync as mkdtempSync2, openSync as openSync4, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
|
|
2104
|
+
import { access, lstat, realpath, stat } from "fs/promises";
|
|
2105
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2106
|
+
import { delimiter, isAbsolute as isAbsolute3, join as join16, relative as relative2, sep as sep3 } from "path";
|
|
2107
|
+
|
|
2108
|
+
// ../daemon/src/runtime/shell/coding.ts
|
|
2109
|
+
function validateDockerCodingSelection(input) {
|
|
2110
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
2111
|
+
throw new Error("Invalid Docker coding selection");
|
|
2112
|
+
const value = input;
|
|
2113
|
+
if (Object.keys(value).some((key) => !["revision", "cwd", "env"].includes(key)) || !Number.isSafeInteger(value.revision) || value.revision < 0)
|
|
2114
|
+
throw new Error("Invalid Docker coding revision");
|
|
2115
|
+
const cwd = codingRelativePath(value.cwd);
|
|
2116
|
+
if (!value.env || typeof value.env !== "object" || Array.isArray(value.env))
|
|
2117
|
+
throw new Error("Invalid Docker coding environment");
|
|
2118
|
+
const env = {};
|
|
2119
|
+
for (const [key, entry] of Object.entries(value.env)) {
|
|
2120
|
+
if (key === "CI" && (entry === "true" || entry === "false")) env.CI = entry;
|
|
2121
|
+
else if (key === "NO_COLOR" && (entry === "0" || entry === "1")) env.NO_COLOR = entry;
|
|
2122
|
+
else if (key === "TZ" && entry === "UTC") env.TZ = entry;
|
|
2123
|
+
else throw new Error("Invalid Docker coding environment");
|
|
1959
2124
|
}
|
|
1960
|
-
return
|
|
2125
|
+
return { revision: value.revision, cwd, env };
|
|
1961
2126
|
}
|
|
1962
|
-
function
|
|
1963
|
-
|
|
1964
|
-
const nameOffset = match[0].toLowerCase().indexOf(commandName.toLowerCase());
|
|
1965
|
-
if (nameOffset < 0) return null;
|
|
1966
|
-
const start = match.index + nameOffset;
|
|
1967
|
-
const boundaryOffset = command.slice(start).search(COMMAND_BOUNDARY_RE);
|
|
1968
|
-
const end = boundaryOffset < 0 ? command.length : start + boundaryOffset;
|
|
1969
|
-
return { start, text: command.slice(start, end).trimEnd() };
|
|
1970
|
-
}
|
|
1971
|
-
function classifyOutboundExfiltration(command, risks, seen) {
|
|
1972
|
-
const invocationRe = /(?:^|[;&|(\n])\s*(?:command\s+)?(curl|wget|scp|rsync|nc|netcat|sftp)\b/gim;
|
|
1973
|
-
for (const match of command.matchAll(invocationRe)) {
|
|
1974
|
-
const name = match[1]?.toLowerCase();
|
|
1975
|
-
if (!name) continue;
|
|
1976
|
-
const segment = invocationSegment(command, match, name);
|
|
1977
|
-
if (!segment) continue;
|
|
1978
|
-
let risky = false;
|
|
1979
|
-
if (name === "curl") {
|
|
1980
|
-
const uploads = /(?:^|\s)(?:-X\s*POST\b|-[dFT](?:\s|[^\s]))/.test(segment.text) || /(?:^|\s)(?:--request(?:=|\s+)POST\b|--data(?:-[a-z-]+)?(?:=|\s)|--form(?:-string)?(?:=|\s)|--upload-file(?:=|\s))/i.test(
|
|
1981
|
-
segment.text
|
|
1982
|
-
);
|
|
1983
|
-
risky = uploads && hasExternalHttpDestination(segment.text);
|
|
1984
|
-
} else if (name === "wget") {
|
|
1985
|
-
const uploads = /(?:^|\s)(?:--method(?:=|\s+)POST\b|--post-data(?:=|\s)|--post-file(?:=|\s)|--body-data(?:=|\s)|--body-file(?:=|\s))/i.test(
|
|
1986
|
-
segment.text
|
|
1987
|
-
);
|
|
1988
|
-
risky = uploads && hasExternalHttpDestination(segment.text);
|
|
1989
|
-
} else if (name === "scp" || name === "rsync") {
|
|
1990
|
-
const hosts = remoteTransferHosts(segment.text);
|
|
1991
|
-
risky = hosts.some((host) => !isLocalHostname(host)) || hosts.length === 0 && /\$/.test(segment.text);
|
|
1992
|
-
} else if (name === "sftp") {
|
|
1993
|
-
const hosts = remoteTransferHosts(segment.text);
|
|
1994
|
-
if (hosts.length > 0) {
|
|
1995
|
-
risky = hosts.some((host) => !isLocalHostname(host));
|
|
1996
|
-
} else {
|
|
1997
|
-
const destination = shellTokens(segment.text).at(-1);
|
|
1998
|
-
risky = destination !== void 0 && destination !== name && !isLocalHostname(destination);
|
|
1999
|
-
}
|
|
2000
|
-
} else {
|
|
2001
|
-
const tokens = shellTokens(segment.text);
|
|
2002
|
-
const host = tokens.find(
|
|
2003
|
-
(token, index) => index > 0 && !token.startsWith("-") && !/^\d+$/.test(token) && !["tcp", "udp"].includes(token.toLowerCase())
|
|
2004
|
-
);
|
|
2005
|
-
risky = host !== void 0 && !isLocalHostname(host);
|
|
2006
|
-
}
|
|
2007
|
-
if (!risky) continue;
|
|
2008
|
-
addRisk(command, risks, seen, {
|
|
2009
|
-
code: "outbound-exfiltration",
|
|
2010
|
-
message: "Uploads data or opens a transfer connection to a non-local destination.",
|
|
2011
|
-
start: segment.start,
|
|
2012
|
-
end: segment.start + segment.text.length
|
|
2013
|
-
});
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
function classifyPrivilegeAndCredentialAccess(command, risks, seen) {
|
|
2017
|
-
const patterns = [
|
|
2018
|
-
/(?:^|[;&|(\n])\s*(?:sudo|su|doas|pkexec)\b[^;&|)\n]*/gim,
|
|
2019
|
-
/(?:^|[;&|(\n])\s*security\s+(?:find-(?:generic|internet)-password|dump-keychain)\b[^;&|)\n]*/gim,
|
|
2020
|
-
/(?:^|[;&|(\n])\s*gcloud\s+auth\b[^;&|)\n]*/gim,
|
|
2021
|
-
/(?:^|[;&|(\n])\s*aws\s+(?:configure|sso\s+login)\b[^;&|)\n]*/gim,
|
|
2022
|
-
/(?:^|[;&|(\n])\s*git\s+credential(?:-[A-Za-z0-9_-]+|\s+(?:fill|approve|reject))?\b[^;&|)\n]*/gim,
|
|
2023
|
-
/(?:^|[;&|(\n])\s*(?:docker|npm|gh)\s+(?:login|auth\s+token)\b[^;&|)\n]*/gim,
|
|
2024
|
-
/(?:^|[;&|(\n])\s*(?:pass\s+(?:show|ls)|secret-tool\s+lookup|ssh-add\s+-[Ll])\b[^;&|)\n]*/gim,
|
|
2025
|
-
/(?:^|[;&|(\n])\s*(?:env|printenv|set|export\s+-p)\s*(?=$|[;&|)\n])/gim
|
|
2026
|
-
];
|
|
2027
|
-
for (const pattern of patterns) {
|
|
2028
|
-
addRegexRisks(
|
|
2029
|
-
command,
|
|
2030
|
-
risks,
|
|
2031
|
-
seen,
|
|
2032
|
-
pattern,
|
|
2033
|
-
"privilege-or-credential-access",
|
|
2034
|
-
"Requests elevated privileges or accesses a credential store."
|
|
2035
|
-
);
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
function classifyBashCommand(command) {
|
|
2039
|
-
const risks = [];
|
|
2040
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2041
|
-
addRegexRisks(
|
|
2042
|
-
command,
|
|
2043
|
-
risks,
|
|
2044
|
-
seen,
|
|
2045
|
-
SENSITIVE_HOME_PATH_RE,
|
|
2046
|
-
"sensitive-path-read",
|
|
2047
|
-
"Accesses a path that commonly contains credentials."
|
|
2048
|
-
);
|
|
2049
|
-
addRegexRisks(
|
|
2050
|
-
command,
|
|
2051
|
-
risks,
|
|
2052
|
-
seen,
|
|
2053
|
-
SENSITIVE_FILE_RE,
|
|
2054
|
-
"sensitive-path-read",
|
|
2055
|
-
"Accesses a sensitive environment, authentication, or database file."
|
|
2056
|
-
);
|
|
2057
|
-
addRegexRisks(
|
|
2058
|
-
command,
|
|
2059
|
-
risks,
|
|
2060
|
-
seen,
|
|
2061
|
-
SENSITIVE_ENV_RE,
|
|
2062
|
-
"sensitive-path-read",
|
|
2063
|
-
"References an environment variable name commonly used for credentials."
|
|
2064
|
-
);
|
|
2065
|
-
classifyBroadFilesystemOperations(command, risks, seen);
|
|
2066
|
-
addRegexRisks(
|
|
2067
|
-
command,
|
|
2068
|
-
risks,
|
|
2069
|
-
seen,
|
|
2070
|
-
REMOTE_PIPE_RE,
|
|
2071
|
-
"remote-pipe-execution",
|
|
2072
|
-
"Pipes downloaded content directly into an interpreter."
|
|
2073
|
-
);
|
|
2074
|
-
classifyOutboundExfiltration(command, risks, seen);
|
|
2075
|
-
classifyPrivilegeAndCredentialAccess(command, risks, seen);
|
|
2076
|
-
return risks.sort((a, b) => a.span.start - b.span.start || a.code.localeCompare(b.code));
|
|
2077
|
-
}
|
|
2078
|
-
function parseEnvAllowlist(raw) {
|
|
2079
|
-
if (raw === void 0 || raw.trim() === "") return [];
|
|
2080
|
-
const values = raw.split(",").map((value) => value.trim());
|
|
2081
|
-
if (values.some((value) => value === "")) {
|
|
2082
|
-
throw new ShellSecurityConfigError(
|
|
2083
|
-
"BAZILION_BASH_SANDBOX_ENV_ALLOWLIST contains an empty environment variable name"
|
|
2084
|
-
);
|
|
2085
|
-
}
|
|
2086
|
-
const unique = [];
|
|
2087
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2088
|
-
for (const value of values) {
|
|
2089
|
-
if (!isValidEnvName(value)) {
|
|
2090
|
-
throw new ShellSecurityConfigError(
|
|
2091
|
-
`BAZILION_BASH_SANDBOX_ENV_ALLOWLIST contains invalid environment variable name: ${value}`
|
|
2092
|
-
);
|
|
2093
|
-
}
|
|
2094
|
-
if (DOCKER_CLIENT_CONTROL_ENV.has(value)) {
|
|
2095
|
-
throw new ShellSecurityConfigError(
|
|
2096
|
-
`BAZILION_BASH_SANDBOX_ENV_ALLOWLIST cannot include Docker client control variable: ${value}`
|
|
2097
|
-
);
|
|
2098
|
-
}
|
|
2099
|
-
if (SANDBOX_CONTROL_ENV.has(value)) {
|
|
2100
|
-
throw new ShellSecurityConfigError(
|
|
2101
|
-
`BAZILION_BASH_SANDBOX_ENV_ALLOWLIST cannot include shell startup control variable: ${value}`
|
|
2102
|
-
);
|
|
2103
|
-
}
|
|
2104
|
-
if (!seen.has(value)) {
|
|
2105
|
-
seen.add(value);
|
|
2106
|
-
unique.push(value);
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
return unique;
|
|
2110
|
-
}
|
|
2111
|
-
function resolveShellSecurityConfig(env = process.env) {
|
|
2112
|
-
const rawMode = env.BAZILION_BASH_SANDBOX?.trim() ?? "";
|
|
2113
|
-
if (rawMode !== "" && rawMode !== "off" && rawMode !== "docker") {
|
|
2114
|
-
throw new ShellSecurityConfigError(
|
|
2115
|
-
`BAZILION_BASH_SANDBOX must be "off" or "docker", received: ${rawMode}`
|
|
2116
|
-
);
|
|
2117
|
-
}
|
|
2118
|
-
const rawApproval = env.BAZILION_BASH_APPROVAL?.trim() ?? "";
|
|
2119
|
-
if (rawApproval !== "" && rawApproval !== "off" && rawApproval !== "dangerous") {
|
|
2120
|
-
throw new ShellSecurityConfigError(
|
|
2121
|
-
`BAZILION_BASH_APPROVAL must be "off" or "dangerous", received: ${rawApproval}`
|
|
2122
|
-
);
|
|
2123
|
-
}
|
|
2124
|
-
const rawImage = env.BAZILION_BASH_SANDBOX_IMAGE?.trim() ?? "";
|
|
2125
|
-
if (rawImage !== "" && !DOCKER_IMAGE_RE.test(rawImage)) {
|
|
2126
|
-
throw new ShellSecurityConfigError(
|
|
2127
|
-
`BAZILION_BASH_SANDBOX_IMAGE is not a valid Docker image reference: ${rawImage}`
|
|
2128
|
-
);
|
|
2129
|
-
}
|
|
2130
|
-
return {
|
|
2131
|
-
sandboxMode: rawMode === "docker" ? "docker" : "off",
|
|
2132
|
-
approvalMode: rawApproval === "dangerous" ? "dangerous" : "off",
|
|
2133
|
-
sandboxImage: rawImage || DEFAULT_BASH_SANDBOX_IMAGE,
|
|
2134
|
-
envAllowlist: parseEnvAllowlist(env.BAZILION_BASH_SANDBOX_ENV_ALLOWLIST)
|
|
2135
|
-
};
|
|
2136
|
-
}
|
|
2137
|
-
function assertEnvName(name) {
|
|
2138
|
-
if (!isValidEnvName(name)) {
|
|
2139
|
-
throw new ShellSecurityConfigError(`invalid shell environment variable name: ${name}`);
|
|
2140
|
-
}
|
|
2141
|
-
if (DOCKER_CLIENT_CONTROL_ENV.has(name) || SANDBOX_CONTROL_ENV.has(name)) {
|
|
2142
|
-
throw new ShellSecurityConfigError(`unsafe shell environment variable name: ${name}`);
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
function buildScrubbedShellEnv(env, allowedKeys = []) {
|
|
2146
|
-
const scrubbed = {};
|
|
2147
|
-
const keys = new Set(SAFE_SHELL_ENV_KEYS);
|
|
2148
|
-
for (const key of allowedKeys) {
|
|
2149
|
-
assertEnvName(key);
|
|
2150
|
-
keys.add(key);
|
|
2151
|
-
}
|
|
2152
|
-
for (const key of keys) {
|
|
2153
|
-
const value = env[key];
|
|
2154
|
-
if (value !== void 0) scrubbed[key] = value;
|
|
2155
|
-
}
|
|
2156
|
-
return scrubbed;
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
// ../daemon/src/runtime/shell/approval.ts
|
|
2160
|
-
var BashApprovalDeniedError = class extends Error {
|
|
2161
|
-
risks;
|
|
2162
|
-
constructor(risks) {
|
|
2163
|
-
const codes = [...new Set(risks.map((risk) => risk.code))];
|
|
2164
|
-
super(`Dangerous bash command denied (${codes.join(", ")})`);
|
|
2165
|
-
this.name = "BashApprovalDeniedError";
|
|
2166
|
-
this.risks = risks;
|
|
2167
|
-
}
|
|
2168
|
-
};
|
|
2169
|
-
function createApprovalGatedBashTool(base, host) {
|
|
2170
|
-
return defineTool({
|
|
2171
|
-
...base,
|
|
2172
|
-
description: `${base.description} Commands classified as dangerous require operator approval before execution.`,
|
|
2173
|
-
executionMode: "sequential",
|
|
2174
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
2175
|
-
const risks = classifyBashCommand(params.command);
|
|
2176
|
-
if (risks.length > 0) {
|
|
2177
|
-
const decision = host ? await host.requestApproval({
|
|
2178
|
-
toolCallId,
|
|
2179
|
-
command: params.command,
|
|
2180
|
-
risks,
|
|
2181
|
-
signal
|
|
2182
|
-
}) : "denied";
|
|
2183
|
-
if (decision !== "approved") throw new BashApprovalDeniedError(risks);
|
|
2184
|
-
}
|
|
2185
|
-
return base.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
2186
|
-
}
|
|
2187
|
-
});
|
|
2127
|
+
function codingContainerCwd(cwd) {
|
|
2128
|
+
return cwd === "." ? "/workspace" : `/workspace/${codingRelativePath(cwd)}`;
|
|
2188
2129
|
}
|
|
2189
2130
|
|
|
2190
2131
|
// ../daemon/src/runtime/shell/docker.ts
|
|
2191
|
-
import { spawn } from "child_process";
|
|
2192
|
-
import { randomUUID as randomUUID18 } from "crypto";
|
|
2193
|
-
import { closeSync as closeSync4, constants as constants4, mkdtempSync as mkdtempSync2, openSync as openSync4, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
|
|
2194
|
-
import { access, lstat, realpath, stat } from "fs/promises";
|
|
2195
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
2196
|
-
import { delimiter, isAbsolute as isAbsolute3, join as join15, relative as relative3, sep as sep3 } from "path";
|
|
2197
2132
|
var CONTAINER_WORKDIR = "/workspace";
|
|
2198
2133
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
2199
2134
|
var MAX_STDERR_CHARS = 16 * 1024;
|
|
@@ -2201,6 +2136,7 @@ var CLEANUP_TIMEOUT_MS = 650;
|
|
|
2201
2136
|
var CLEANUP_RETRY_DELAY_MS = 100;
|
|
2202
2137
|
var CLEANUP_ATTEMPTS = 3;
|
|
2203
2138
|
var DOCKER_CONTEXT_TIMEOUT_MS = 2e3;
|
|
2139
|
+
var DOCKER_CREATE_TIMEOUT_MS = 5e3;
|
|
2204
2140
|
var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2205
2141
|
var CONTAINER_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]+$/;
|
|
2206
2142
|
var DOCKER_CLIENT_ENV_KEYS = [
|
|
@@ -2273,7 +2209,7 @@ function buildDockerRunSpec(input) {
|
|
|
2273
2209
|
"--entrypoint",
|
|
2274
2210
|
"/bin/bash",
|
|
2275
2211
|
"--workdir",
|
|
2276
|
-
|
|
2212
|
+
codingContainerCwd(input.codingCwd ?? "."),
|
|
2277
2213
|
// A bind mount is read/write unless `readonly` is present. No other mount is added.
|
|
2278
2214
|
"--mount",
|
|
2279
2215
|
`type=bind,source=${input.mountSource},target=${CONTAINER_WORKDIR},bind-recursive=disabled`
|
|
@@ -2319,7 +2255,7 @@ function buildDockerRunSpec(input) {
|
|
|
2319
2255
|
` } : {}
|
|
2320
2256
|
};
|
|
2321
2257
|
}
|
|
2322
|
-
function createPreparedDockerBashOperations(runtime) {
|
|
2258
|
+
function createPreparedDockerBashOperations(runtime, lifecycle) {
|
|
2323
2259
|
validatePreparedDockerRuntime(runtime);
|
|
2324
2260
|
return {
|
|
2325
2261
|
async exec(command, cwd, { onData, signal, timeout }) {
|
|
@@ -2336,9 +2272,10 @@ function createPreparedDockerBashOperations(runtime) {
|
|
|
2336
2272
|
await assertDockerExecutableIdentity(runtime.dockerPath, runtime.executableIdentity);
|
|
2337
2273
|
await assertLocalDockerSocket(runtime.endpoint);
|
|
2338
2274
|
const dockerClientEnv = { DOCKER_HOST: runtime.endpoint };
|
|
2275
|
+
if (runtime.coding) resolveCodingDirectory(workspace.source, runtime.coding.cwd);
|
|
2339
2276
|
const currentImageId = await inspectSandboxImage(
|
|
2340
2277
|
runtime.dockerPath,
|
|
2341
|
-
runtime.image,
|
|
2278
|
+
runtime.coding ? runtime.imageId : runtime.image,
|
|
2342
2279
|
dockerClientEnv
|
|
2343
2280
|
);
|
|
2344
2281
|
if (currentImageId !== runtime.imageId) {
|
|
@@ -2346,11 +2283,12 @@ function createPreparedDockerBashOperations(runtime) {
|
|
|
2346
2283
|
}
|
|
2347
2284
|
await assertDockerExecutableIdentity(runtime.dockerPath, runtime.executableIdentity);
|
|
2348
2285
|
if (signal?.aborted) throw new Error("aborted");
|
|
2349
|
-
const containerName = `bazilion-bash-${process.pid}-${randomUUID18().replaceAll("-", "")}`;
|
|
2286
|
+
const containerName = lifecycle?.name("bash") ?? `bazilion-bash-${process.pid}-${randomUUID18().replaceAll("-", "")}`;
|
|
2350
2287
|
const spec = buildDockerRunSpec({
|
|
2351
2288
|
image: runtime.image,
|
|
2352
2289
|
resolvedImage: runtime.imageId,
|
|
2353
|
-
env: { ...runtime.containerEnv },
|
|
2290
|
+
env: { ...runtime.containerEnv, ...runtime.coding?.env },
|
|
2291
|
+
codingCwd: runtime.coding?.cwd,
|
|
2354
2292
|
dockerPath: runtime.dockerPath,
|
|
2355
2293
|
hostEnv: dockerClientEnv,
|
|
2356
2294
|
command,
|
|
@@ -2360,10 +2298,94 @@ function createPreparedDockerBashOperations(runtime) {
|
|
|
2360
2298
|
uid: runtime.uid,
|
|
2361
2299
|
gid: runtime.gid
|
|
2362
2300
|
});
|
|
2363
|
-
|
|
2301
|
+
await lifecycle?.beforeCreate({
|
|
2302
|
+
dockerPath: runtime.dockerPath,
|
|
2303
|
+
executableIdentity: runtime.executableIdentity,
|
|
2304
|
+
endpoint: runtime.endpoint,
|
|
2305
|
+
containerName
|
|
2306
|
+
});
|
|
2307
|
+
try {
|
|
2308
|
+
return await executeRegisteredDockerRun(
|
|
2309
|
+
spec,
|
|
2310
|
+
{ onData, signal, timeout, timeoutMs },
|
|
2311
|
+
lifecycle
|
|
2312
|
+
);
|
|
2313
|
+
} finally {
|
|
2314
|
+
await lifecycle?.afterRemove(containerName);
|
|
2315
|
+
}
|
|
2364
2316
|
}
|
|
2365
2317
|
};
|
|
2366
2318
|
}
|
|
2319
|
+
function runProtectedContainerCreate(spec, options) {
|
|
2320
|
+
if (options?.signal?.aborted) return Promise.reject(new Error("aborted"));
|
|
2321
|
+
let envFileFd;
|
|
2322
|
+
try {
|
|
2323
|
+
if (spec.envFileContent !== void 0) {
|
|
2324
|
+
envFileFd = openAnonymousEnvFile(spec.envFileContent);
|
|
2325
|
+
}
|
|
2326
|
+
} catch (error) {
|
|
2327
|
+
return Promise.reject(
|
|
2328
|
+
new Error(
|
|
2329
|
+
`Docker sandbox could not prepare its preflight environment: ${error instanceof Error ? error.message : String(error)}`
|
|
2330
|
+
)
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
return new Promise((resolve6, reject) => {
|
|
2334
|
+
let child;
|
|
2335
|
+
try {
|
|
2336
|
+
child = spawn(spec.executable, ["container", "create", ...spec.args.slice(1)], {
|
|
2337
|
+
stdio: ["ignore", "ignore", "pipe", envFileFd ?? "ignore"],
|
|
2338
|
+
windowsHide: true,
|
|
2339
|
+
env: spec.processEnv
|
|
2340
|
+
});
|
|
2341
|
+
} catch (error) {
|
|
2342
|
+
if (envFileFd !== void 0) closeSync4(envFileFd);
|
|
2343
|
+
reject(error instanceof Error ? formatSpawnError(spec.executable, error) : error);
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
if (envFileFd !== void 0) closeSync4(envFileFd);
|
|
2347
|
+
let settled = false;
|
|
2348
|
+
let stderr = "";
|
|
2349
|
+
let timeoutHandle;
|
|
2350
|
+
let killSettleHandle;
|
|
2351
|
+
let termination;
|
|
2352
|
+
const finish = (error) => {
|
|
2353
|
+
if (settled) return;
|
|
2354
|
+
settled = true;
|
|
2355
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
2356
|
+
if (killSettleHandle) clearTimeout(killSettleHandle);
|
|
2357
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
2358
|
+
if (error) reject(error);
|
|
2359
|
+
else resolve6();
|
|
2360
|
+
};
|
|
2361
|
+
const commandTimeout = options?.timeoutMs !== void 0 && options.timeoutMs <= DOCKER_CREATE_TIMEOUT_MS;
|
|
2362
|
+
const timeoutError = () => commandTimeout ? new Error(`timeout:${options?.timeout}`) : new Error(
|
|
2363
|
+
`Docker sandbox could not create its protected preflight container within ${DOCKER_CREATE_TIMEOUT_MS}ms`
|
|
2364
|
+
);
|
|
2365
|
+
child.stderr?.on("data", (data) => {
|
|
2366
|
+
stderr = `${stderr}${data.toString("utf8")}`.slice(-MAX_STDERR_CHARS);
|
|
2367
|
+
});
|
|
2368
|
+
child.once("error", (error) => finish(formatSpawnError(spec.executable, error)));
|
|
2369
|
+
child.once("close", (code) => {
|
|
2370
|
+
if (termination) finish(termination);
|
|
2371
|
+
else if (code === 0) finish();
|
|
2372
|
+
else finish(formatDockerCreateError(spec, stderr, code));
|
|
2373
|
+
});
|
|
2374
|
+
const terminate = (error) => {
|
|
2375
|
+
if (settled || termination) return;
|
|
2376
|
+
termination = error;
|
|
2377
|
+
child.kill("SIGKILL");
|
|
2378
|
+
killSettleHandle = setTimeout(() => finish(error), CLEANUP_TIMEOUT_MS);
|
|
2379
|
+
};
|
|
2380
|
+
const onAbort = () => terminate(new Error("aborted"));
|
|
2381
|
+
timeoutHandle = setTimeout(
|
|
2382
|
+
() => terminate(timeoutError()),
|
|
2383
|
+
Math.min(options?.timeoutMs ?? DOCKER_CREATE_TIMEOUT_MS, DOCKER_CREATE_TIMEOUT_MS)
|
|
2384
|
+
);
|
|
2385
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2386
|
+
if (options?.signal?.aborted) onAbort();
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2367
2389
|
async function recheckProtectedMount(mount) {
|
|
2368
2390
|
const sourceInfo = await lstat(mount.source);
|
|
2369
2391
|
if (sourceInfo.isSymbolicLink() || !sourceInfo.isDirectory()) {
|
|
@@ -2380,6 +2402,22 @@ async function resolveRequiredRoot(root) {
|
|
|
2380
2402
|
if (!isAbsolute3(root)) throw new Error("Docker sandbox mount root must be absolute");
|
|
2381
2403
|
return resolveMountSource(root);
|
|
2382
2404
|
}
|
|
2405
|
+
async function resolveAbsoluteDockerPath(requested, hostEnv) {
|
|
2406
|
+
validateDockerPath(requested);
|
|
2407
|
+
const candidates = isAbsolute3(requested) ? [requested] : (hostEnv.PATH ?? "").split(delimiter).filter(Boolean).map((dir) => join16(dir, requested));
|
|
2408
|
+
for (const candidate of candidates) {
|
|
2409
|
+
try {
|
|
2410
|
+
await access(candidate, constants4.X_OK);
|
|
2411
|
+
const info = await stat(candidate);
|
|
2412
|
+
if (!info.isFile()) continue;
|
|
2413
|
+
const canonical = await realpath(candidate);
|
|
2414
|
+
if (!isAbsolute3(canonical)) continue;
|
|
2415
|
+
return canonical;
|
|
2416
|
+
} catch {
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
throw new Error(`Docker sandbox executable is unavailable: ${requested}`);
|
|
2420
|
+
}
|
|
2383
2421
|
async function inspectDockerExecutableIdentity(dockerPath) {
|
|
2384
2422
|
try {
|
|
2385
2423
|
await access(dockerPath, constants4.X_OK);
|
|
@@ -2423,6 +2461,7 @@ async function assertLocalDockerSocket(endpoint) {
|
|
|
2423
2461
|
}
|
|
2424
2462
|
}
|
|
2425
2463
|
function validatePreparedDockerRuntime(runtime) {
|
|
2464
|
+
if (runtime.coding) validateDockerCodingSelection(runtime.coding);
|
|
2426
2465
|
if (!isAbsolute3(runtime.dockerPath)) {
|
|
2427
2466
|
throw new Error("protected Docker executable must be absolute");
|
|
2428
2467
|
}
|
|
@@ -2440,17 +2479,17 @@ function validatePreparedDockerRuntime(runtime) {
|
|
|
2440
2479
|
throw new Error("protected Docker container environment does not match policy");
|
|
2441
2480
|
}
|
|
2442
2481
|
}
|
|
2443
|
-
function validateDockerExecutableIdentity(
|
|
2444
|
-
if (typeof
|
|
2482
|
+
function validateDockerExecutableIdentity(identity2) {
|
|
2483
|
+
if (typeof identity2 !== "object" || identity2 === null) {
|
|
2445
2484
|
throw new Error("protected Docker executable identity is invalid");
|
|
2446
2485
|
}
|
|
2447
2486
|
const fields = [
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2487
|
+
identity2.device,
|
|
2488
|
+
identity2.inode,
|
|
2489
|
+
identity2.mode,
|
|
2490
|
+
identity2.size,
|
|
2491
|
+
identity2.modifiedTimeNs,
|
|
2492
|
+
identity2.changedTimeNs
|
|
2454
2493
|
];
|
|
2455
2494
|
if (fields.some((field) => typeof field !== "string" || !/^\d+$/.test(field))) {
|
|
2456
2495
|
throw new Error("protected Docker executable identity is invalid");
|
|
@@ -2485,7 +2524,7 @@ function createDockerBashOperations(options) {
|
|
|
2485
2524
|
const dockerClientEnv = await resolveLocalDockerClientEnv(dockerPath, hostEnv);
|
|
2486
2525
|
const resolvedImage = await inspectSandboxImage(dockerPath, image, dockerClientEnv);
|
|
2487
2526
|
if (signal?.aborted) throw new Error("aborted");
|
|
2488
|
-
const containerName = `bazilion-bash-${process.pid}-${randomUUID18().replaceAll("-", "")}`;
|
|
2527
|
+
const containerName = options.lifecycle?.name("bash") ?? `bazilion-bash-${process.pid}-${randomUUID18().replaceAll("-", "")}`;
|
|
2489
2528
|
const spec = buildDockerRunSpec({
|
|
2490
2529
|
image,
|
|
2491
2530
|
env,
|
|
@@ -2499,10 +2538,44 @@ function createDockerBashOperations(options) {
|
|
|
2499
2538
|
readOnlyMounts,
|
|
2500
2539
|
hostEnv: dockerClientEnv
|
|
2501
2540
|
});
|
|
2502
|
-
|
|
2541
|
+
if (options.lifecycle) {
|
|
2542
|
+
const absolute = await resolveAbsoluteDockerPath(dockerPath, hostEnv);
|
|
2543
|
+
await options.lifecycle.beforeCreate({
|
|
2544
|
+
dockerPath: absolute,
|
|
2545
|
+
executableIdentity: await inspectDockerExecutableIdentity(absolute),
|
|
2546
|
+
endpoint: dockerClientEnv.DOCKER_HOST,
|
|
2547
|
+
containerName
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
try {
|
|
2551
|
+
return await executeRegisteredDockerRun(
|
|
2552
|
+
spec,
|
|
2553
|
+
{ onData, signal, timeout, timeoutMs },
|
|
2554
|
+
options.lifecycle
|
|
2555
|
+
);
|
|
2556
|
+
} finally {
|
|
2557
|
+
await options.lifecycle?.afterRemove(containerName);
|
|
2558
|
+
}
|
|
2503
2559
|
}
|
|
2504
2560
|
};
|
|
2505
2561
|
}
|
|
2562
|
+
async function executeRegisteredDockerRun(spec, options, lifecycle) {
|
|
2563
|
+
if (!lifecycle) return executeDockerRun(spec, options);
|
|
2564
|
+
const deadline = options.timeoutMs === void 0 ? void 0 : Date.now() + options.timeoutMs;
|
|
2565
|
+
await runProtectedContainerCreate(spec, options);
|
|
2566
|
+
await lifecycle.afterCreate(spec.containerName);
|
|
2567
|
+
if (options.signal?.aborted) throw new Error("aborted");
|
|
2568
|
+
const remaining = deadline === void 0 ? void 0 : deadline - Date.now();
|
|
2569
|
+
if (remaining !== void 0 && remaining <= 0) throw new Error(`timeout:${options.timeout}`);
|
|
2570
|
+
return executeDockerRun(
|
|
2571
|
+
{
|
|
2572
|
+
...spec,
|
|
2573
|
+
args: ["container", "start", "--attach", spec.containerName],
|
|
2574
|
+
envFileContent: void 0
|
|
2575
|
+
},
|
|
2576
|
+
{ ...options, timeoutMs: remaining }
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2506
2579
|
function executeDockerRun(spec, options) {
|
|
2507
2580
|
let envFileFd;
|
|
2508
2581
|
try {
|
|
@@ -2578,8 +2651,8 @@ function executeDockerRun(spec, options) {
|
|
|
2578
2651
|
});
|
|
2579
2652
|
}
|
|
2580
2653
|
function openAnonymousEnvFile(content) {
|
|
2581
|
-
const dir = mkdtempSync2(
|
|
2582
|
-
const path =
|
|
2654
|
+
const dir = mkdtempSync2(join16(tmpdir2(), "bazilion-docker-env-"));
|
|
2655
|
+
const path = join16(dir, "environment");
|
|
2583
2656
|
let fd;
|
|
2584
2657
|
try {
|
|
2585
2658
|
writeFileSync7(path, content, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
@@ -2702,227 +2775,1113 @@ async function inspectSandboxImage(dockerPath, image, processEnv) {
|
|
|
2702
2775
|
}
|
|
2703
2776
|
throw error;
|
|
2704
2777
|
}
|
|
2705
|
-
const separator = raw.indexOf(" ");
|
|
2706
|
-
if (separator < 0) {
|
|
2707
|
-
throw new Error(`Docker sandbox received an invalid inspection result for image "${image}"`);
|
|
2778
|
+
const separator = raw.indexOf(" ");
|
|
2779
|
+
if (separator < 0) {
|
|
2780
|
+
throw new Error(`Docker sandbox received an invalid inspection result for image "${image}"`);
|
|
2781
|
+
}
|
|
2782
|
+
let id;
|
|
2783
|
+
let volumes;
|
|
2784
|
+
try {
|
|
2785
|
+
id = JSON.parse(raw.slice(0, separator));
|
|
2786
|
+
volumes = JSON.parse(raw.slice(separator + 1));
|
|
2787
|
+
} catch {
|
|
2788
|
+
throw new Error(`Docker sandbox received an invalid inspection result for image "${image}"`);
|
|
2789
|
+
}
|
|
2790
|
+
if (typeof id !== "string" || !id.startsWith("sha256:")) {
|
|
2791
|
+
throw new Error(`Docker sandbox received an invalid image id for "${image}"`);
|
|
2792
|
+
}
|
|
2793
|
+
if (volumes !== null && typeof volumes === "object" && Object.keys(volumes).length > 0) {
|
|
2794
|
+
throw new Error(
|
|
2795
|
+
`Docker sandbox image "${image}" declares writable volumes (${Object.keys(volumes).sort().join(", ")}); use an image without VOLUME declarations`
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
if (volumes !== null && (typeof volumes !== "object" || Array.isArray(volumes))) {
|
|
2799
|
+
throw new Error(`Docker sandbox received invalid volume metadata for image "${image}"`);
|
|
2800
|
+
}
|
|
2801
|
+
return id;
|
|
2802
|
+
}
|
|
2803
|
+
function runDockerInspection(dockerPath, args, processEnv, subject) {
|
|
2804
|
+
return new Promise((resolve6, reject) => {
|
|
2805
|
+
const child = spawn(dockerPath, args, {
|
|
2806
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2807
|
+
windowsHide: true,
|
|
2808
|
+
env: processEnv
|
|
2809
|
+
});
|
|
2810
|
+
let settled = false;
|
|
2811
|
+
let stdout = "";
|
|
2812
|
+
let stderr = "";
|
|
2813
|
+
const finish = (error) => {
|
|
2814
|
+
if (settled) return;
|
|
2815
|
+
settled = true;
|
|
2816
|
+
clearTimeout(timeoutHandle);
|
|
2817
|
+
if (error) {
|
|
2818
|
+
reject(error);
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
resolve6(stdout.trim());
|
|
2822
|
+
};
|
|
2823
|
+
const timeoutHandle = setTimeout(() => {
|
|
2824
|
+
child.kill("SIGKILL");
|
|
2825
|
+
finish(
|
|
2826
|
+
new Error(
|
|
2827
|
+
`Docker sandbox could not inspect ${subject} within ${DOCKER_CONTEXT_TIMEOUT_MS}ms`
|
|
2828
|
+
)
|
|
2829
|
+
);
|
|
2830
|
+
}, DOCKER_CONTEXT_TIMEOUT_MS);
|
|
2831
|
+
child.stdout.on("data", (data) => {
|
|
2832
|
+
stdout = `${stdout}${data.toString("utf8")}`.slice(-4096);
|
|
2833
|
+
});
|
|
2834
|
+
child.stderr.on("data", (data) => {
|
|
2835
|
+
stderr = `${stderr}${data.toString("utf8")}`.slice(-4096);
|
|
2836
|
+
});
|
|
2837
|
+
child.once("error", (error) => finish(formatSpawnError(dockerPath, error)));
|
|
2838
|
+
child.once("close", (code) => {
|
|
2839
|
+
if (code === 0) finish();
|
|
2840
|
+
else
|
|
2841
|
+
finish(
|
|
2842
|
+
new Error(
|
|
2843
|
+
`Docker sandbox could not inspect ${subject}: ${stderr.trim() || `Docker exited with status ${code}`}`
|
|
2844
|
+
)
|
|
2845
|
+
);
|
|
2846
|
+
});
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
function buildDockerClientEnv(hostEnv) {
|
|
2850
|
+
const env = {};
|
|
2851
|
+
for (const key of DOCKER_CLIENT_ENV_KEYS) {
|
|
2852
|
+
const value = hostEnv[key];
|
|
2853
|
+
if (value !== void 0) env[key] = value;
|
|
2854
|
+
}
|
|
2855
|
+
return env;
|
|
2856
|
+
}
|
|
2857
|
+
function validateDockerPath(dockerPath) {
|
|
2858
|
+
if (!dockerPath.trim()) throw new Error("Docker sandbox executable path is required");
|
|
2859
|
+
if (dockerPath.includes("\0"))
|
|
2860
|
+
throw new Error("Docker sandbox executable path contains a NUL byte");
|
|
2861
|
+
}
|
|
2862
|
+
function validateImage(image) {
|
|
2863
|
+
if (!image.trim()) throw new Error("Docker sandbox image is required");
|
|
2864
|
+
if (image.startsWith("-")) {
|
|
2865
|
+
throw new Error(`Docker sandbox image cannot start with "-": ${JSON.stringify(image)}`);
|
|
2866
|
+
}
|
|
2867
|
+
if (image.includes("\0")) throw new Error("Docker sandbox image contains a NUL byte");
|
|
2868
|
+
}
|
|
2869
|
+
function validateContainerName(containerName) {
|
|
2870
|
+
if (!CONTAINER_NAME.test(containerName)) {
|
|
2871
|
+
throw new Error(`Docker sandbox container name is invalid: ${JSON.stringify(containerName)}`);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
function validateMountArgument(mountSource) {
|
|
2875
|
+
if (!mountSource) throw new Error("Docker sandbox mount path is required");
|
|
2876
|
+
if (mountSource.includes("\0") || mountSource.includes(",")) {
|
|
2877
|
+
throw new Error(
|
|
2878
|
+
`Docker sandbox mount path cannot be represented safely: ${JSON.stringify(mountSource)}`
|
|
2879
|
+
);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
function validateReadOnlyMounts(mounts, workspaceSource) {
|
|
2883
|
+
const targets = /* @__PURE__ */ new Set([CONTAINER_WORKDIR, "/tmp"]);
|
|
2884
|
+
for (const mount of mounts) {
|
|
2885
|
+
validateMountArgument(mount.source);
|
|
2886
|
+
if (mount.sourceRoot !== void 0 && (!isAbsolute3(mount.sourceRoot) || mount.sourceRoot.includes("\0") || mount.sourceRoot.includes(","))) {
|
|
2887
|
+
throw new Error(
|
|
2888
|
+
`Docker sandbox read-only mount root is invalid: ${JSON.stringify(mount.sourceRoot)}`
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
if (!mount.target.startsWith("/") || mount.target.includes("\0") || mount.target.includes(",") || mount.target.split("/").includes("..")) {
|
|
2892
|
+
throw new Error(
|
|
2893
|
+
`Docker sandbox read-only mount target is invalid: ${JSON.stringify(mount.target)}`
|
|
2894
|
+
);
|
|
2895
|
+
}
|
|
2896
|
+
if (mount.source === workspaceSource) {
|
|
2897
|
+
throw new Error("Docker sandbox cannot mount the writable workspace again as an input");
|
|
2898
|
+
}
|
|
2899
|
+
if (targets.has(mount.target)) {
|
|
2900
|
+
throw new Error(`Docker sandbox mount target is duplicated or reserved: ${mount.target}`);
|
|
2901
|
+
}
|
|
2902
|
+
targets.add(mount.target);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
function isWithin(root, candidate) {
|
|
2906
|
+
const rel = relative2(root, candidate);
|
|
2907
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute3(rel);
|
|
2908
|
+
}
|
|
2909
|
+
function validateHostIdentity(uid, gid) {
|
|
2910
|
+
if (!Number.isSafeInteger(uid) || uid < 0 || !Number.isSafeInteger(gid) || gid < 0) {
|
|
2911
|
+
throw new Error(`Docker sandbox received an invalid host uid/gid: ${uid}:${gid}`);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
function formatSpawnError(dockerPath, error) {
|
|
2915
|
+
const code = error.code;
|
|
2916
|
+
if (code === "ENOENT") {
|
|
2917
|
+
return new Error(`Docker sandbox unavailable: executable "${dockerPath}" was not found`);
|
|
2918
|
+
}
|
|
2919
|
+
if (code === "EACCES") {
|
|
2920
|
+
return new Error(`Docker sandbox unavailable: executable "${dockerPath}" is not executable`);
|
|
2921
|
+
}
|
|
2922
|
+
return new Error(`Docker sandbox could not start "${dockerPath}": ${error.message}`);
|
|
2923
|
+
}
|
|
2924
|
+
function formatDockerRunError(spec, stderr) {
|
|
2925
|
+
const detail3 = stderr.trim() || "Docker exited with status 125 without an error message";
|
|
2926
|
+
if (/no such image|unable to find image|manifest unknown|pull access denied/i.test(detail3)) {
|
|
2927
|
+
return new Error(`Docker sandbox image "${spec.image}" is unavailable: ${detail3}`);
|
|
2928
|
+
}
|
|
2929
|
+
if (/mounts denied|invalid mount config|bind source path does not exist|mount.*failed/i.test(detail3)) {
|
|
2930
|
+
return new Error(`Docker sandbox mount failed for "${spec.mountSource}": ${detail3}`);
|
|
2931
|
+
}
|
|
2932
|
+
if (/cannot connect to the docker daemon|is the docker daemon running/i.test(detail3)) {
|
|
2933
|
+
return new Error(`Docker sandbox unavailable: ${detail3}`);
|
|
2934
|
+
}
|
|
2935
|
+
return new Error(`Docker sandbox failed to start container "${spec.containerName}": ${detail3}`);
|
|
2936
|
+
}
|
|
2937
|
+
function formatDockerCreateError(spec, stderr, exitCode) {
|
|
2938
|
+
const detail3 = stderr.trim() || `Docker exited with ${exitCode === null ? "no status" : `status ${exitCode}`} without an error message`;
|
|
2939
|
+
if (/mounts denied|invalid mount config|bind source path does not exist|mount.*failed/i.test(detail3)) {
|
|
2940
|
+
return new Error(`Docker sandbox preflight mount failed for "${spec.mountSource}": ${detail3}`);
|
|
2941
|
+
}
|
|
2942
|
+
if (/cannot connect to the docker daemon|is the docker daemon running/i.test(detail3)) {
|
|
2943
|
+
return new Error(`Docker sandbox preflight could not reach the local Docker daemon: ${detail3}`);
|
|
2944
|
+
}
|
|
2945
|
+
return new Error(`Docker sandbox preflight could not create the required container: ${detail3}`);
|
|
2946
|
+
}
|
|
2947
|
+
async function removeContainer(dockerPath, containerName, processEnv) {
|
|
2948
|
+
const args = ["container", "rm", "--force", containerName];
|
|
2949
|
+
let last;
|
|
2950
|
+
for (let attempt = 0; attempt < CLEANUP_ATTEMPTS; attempt += 1) {
|
|
2951
|
+
last = await runCleanupCommand(dockerPath, args, processEnv);
|
|
2952
|
+
if (last.ok) return;
|
|
2953
|
+
if (attempt < CLEANUP_ATTEMPTS - 1) {
|
|
2954
|
+
await new Promise((resolve6) => setTimeout(resolve6, CLEANUP_RETRY_DELAY_MS));
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
if (last && !last.missing) {
|
|
2958
|
+
console.warn(
|
|
2959
|
+
`Docker sandbox could not confirm cleanup of container "${containerName}": ${last.error || "cleanup command failed"}`
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
function runCleanupCommand(dockerPath, args, processEnv) {
|
|
2964
|
+
return new Promise((resolve6) => {
|
|
2965
|
+
let done = false;
|
|
2966
|
+
let stderr = "";
|
|
2967
|
+
const child = spawn(dockerPath, args, {
|
|
2968
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
2969
|
+
windowsHide: true,
|
|
2970
|
+
env: processEnv
|
|
2971
|
+
});
|
|
2972
|
+
const finish = (ok, error = "") => {
|
|
2973
|
+
if (done) return;
|
|
2974
|
+
done = true;
|
|
2975
|
+
clearTimeout(timeoutHandle);
|
|
2976
|
+
const detail3 = error || stderr.trim();
|
|
2977
|
+
resolve6({
|
|
2978
|
+
ok,
|
|
2979
|
+
missing: /no such container/i.test(detail3),
|
|
2980
|
+
error: detail3
|
|
2981
|
+
});
|
|
2982
|
+
};
|
|
2983
|
+
const timeoutHandle = setTimeout(() => {
|
|
2984
|
+
child.kill("SIGKILL");
|
|
2985
|
+
finish(false, `cleanup timed out after ${CLEANUP_TIMEOUT_MS}ms`);
|
|
2986
|
+
}, CLEANUP_TIMEOUT_MS);
|
|
2987
|
+
child.stderr.on("data", (data) => {
|
|
2988
|
+
stderr = `${stderr}${data.toString("utf8")}`.slice(-2048);
|
|
2989
|
+
});
|
|
2990
|
+
child.once("error", (error) => finish(false, error.message));
|
|
2991
|
+
child.once("close", (code) => finish(code === 0));
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
// ../daemon/src/runtime/pi/coding.ts
|
|
2996
|
+
function codingTools(input) {
|
|
2997
|
+
const readSecrets = () => typeof input.secrets === "function" ? input.secrets() : input.secrets ?? [];
|
|
2998
|
+
const hostOperations = input.docker ? null : createLocalBashOperations();
|
|
2999
|
+
return [
|
|
3000
|
+
{
|
|
3001
|
+
name: "coding_environment",
|
|
3002
|
+
label: "Inspect coding environment",
|
|
3003
|
+
description: "Read the actual environment admitted to this turn for a Team-relative directory. No commands run. Discover prerequisites during the task; no Team checklist is required. Missing network/tooling is a specific blocker, never permission to change posture.",
|
|
3004
|
+
parameters: Type.Object(
|
|
3005
|
+
{ target: Type.String({ default: ".", maxLength: 4096 }) },
|
|
3006
|
+
{ additionalProperties: false }
|
|
3007
|
+
),
|
|
3008
|
+
executionMode: "sequential",
|
|
3009
|
+
async execute(_id, params) {
|
|
3010
|
+
const value = await input.host.invoke({
|
|
3011
|
+
action: "environment",
|
|
3012
|
+
target: params.target
|
|
3013
|
+
});
|
|
3014
|
+
return {
|
|
3015
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
3016
|
+
details: { codingEnvironment: value }
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
},
|
|
3020
|
+
{
|
|
3021
|
+
name: "coding_command",
|
|
3022
|
+
label: "Run coding command",
|
|
3023
|
+
description: "Execute an ad hoc finite runtime/dependency/preparation/build/test command in this turn\u2019s actual environment. Use discovered repository commands, not saved operator checks. Resolve prerequisites and use available offline artifacts within current authority. Share coding-receipt:<id> through send_message if a teammate needs evidence; never wait holding its workspace lease. Success is an observed exit, not proof about later code.",
|
|
3024
|
+
parameters: Type.Object(
|
|
3025
|
+
{
|
|
3026
|
+
command: Type.String({ minLength: 1, maxLength: 4096 }),
|
|
3027
|
+
cwd: Type.String({ maxLength: 4096 }),
|
|
3028
|
+
purpose: Type.Union(
|
|
3029
|
+
["runtime", "dependency", "prepare", "build", "test"].map(
|
|
3030
|
+
(value) => Type.Literal(value)
|
|
3031
|
+
)
|
|
3032
|
+
),
|
|
3033
|
+
timeoutSeconds: Type.Integer({ minimum: 1, maximum: 300 })
|
|
3034
|
+
},
|
|
3035
|
+
{ additionalProperties: false }
|
|
3036
|
+
),
|
|
3037
|
+
executionMode: "sequential",
|
|
3038
|
+
async execute(toolCallId, params, signal, onUpdate) {
|
|
3039
|
+
const command = validateCodingCommand(params);
|
|
3040
|
+
const context = await input.context(command.cwd);
|
|
3041
|
+
if (context.instructions.state !== "complete")
|
|
3042
|
+
throw new Error("Coding scope instructions are incomplete");
|
|
3043
|
+
const started = await input.host.invoke({
|
|
3044
|
+
action: "start",
|
|
3045
|
+
toolCallId,
|
|
3046
|
+
input: command
|
|
3047
|
+
});
|
|
3048
|
+
const output = new CodingDiagnostics(readSecrets());
|
|
3049
|
+
let lastProgressAt = 0;
|
|
3050
|
+
const emitProgress = (force) => {
|
|
3051
|
+
if (!onUpdate) return;
|
|
3052
|
+
const now = Date.now();
|
|
3053
|
+
if (!force && now - lastProgressAt < 400) return;
|
|
3054
|
+
lastProgressAt = now;
|
|
3055
|
+
const preview = output.preview(CODING_LIVE_BYTES);
|
|
3056
|
+
onUpdate({
|
|
3057
|
+
content: [{ type: "text", text: preview.text }],
|
|
3058
|
+
details: {
|
|
3059
|
+
codingProgress: {
|
|
3060
|
+
id: toolCallId,
|
|
3061
|
+
commandId: started.id,
|
|
3062
|
+
output: preview.text,
|
|
3063
|
+
truncated: preview.truncated,
|
|
3064
|
+
elapsedMs: Math.max(0, now - started.startedAt)
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
});
|
|
3068
|
+
};
|
|
3069
|
+
let outcome;
|
|
3070
|
+
try {
|
|
3071
|
+
if (input.approval)
|
|
3072
|
+
await requireBashApproval(command.command, toolCallId, signal, input.approvalHost);
|
|
3073
|
+
signal?.throwIfAborted();
|
|
3074
|
+
const cwd = resolveCodingDirectory(input.root, command.cwd);
|
|
3075
|
+
const operations = input.docker ? createPreparedDockerBashOperations(
|
|
3076
|
+
{
|
|
3077
|
+
...input.docker,
|
|
3078
|
+
coding: {
|
|
3079
|
+
revision: input.docker.coding?.revision ?? 0,
|
|
3080
|
+
cwd: command.cwd,
|
|
3081
|
+
env: input.docker.coding?.env ?? {}
|
|
3082
|
+
}
|
|
3083
|
+
},
|
|
3084
|
+
input.lifecycle
|
|
3085
|
+
) : hostOperations;
|
|
3086
|
+
const result = await operations.exec(
|
|
3087
|
+
command.command,
|
|
3088
|
+
input.docker ? input.root : cwd.path,
|
|
3089
|
+
{
|
|
3090
|
+
onData: (bytes) => {
|
|
3091
|
+
output.append(bytes);
|
|
3092
|
+
emitProgress(false);
|
|
3093
|
+
},
|
|
3094
|
+
signal,
|
|
3095
|
+
timeout: command.timeoutSeconds
|
|
3096
|
+
}
|
|
3097
|
+
);
|
|
3098
|
+
emitProgress(true);
|
|
3099
|
+
outcome = {
|
|
3100
|
+
state: result.exitCode === 0 ? "succeeded" : result.exitCode === null ? "interrupted" : "failed",
|
|
3101
|
+
exitCode: result.exitCode,
|
|
3102
|
+
...output.finish(),
|
|
3103
|
+
reason: result.exitCode === null ? "exit_unavailable" : null
|
|
3104
|
+
};
|
|
3105
|
+
} catch (error) {
|
|
3106
|
+
const message = error instanceof Error ? error.message : "Command execution unavailable";
|
|
3107
|
+
const state = signal?.aborted || message === "aborted" ? "cancelled" : message.startsWith("timeout:") ? "timed_out" : error instanceof BashApprovalDeniedError ? "blocked" : /cleanup|teardown/i.test(message) ? "interrupted" : "blocked";
|
|
3108
|
+
outcome = { state, exitCode: null, ...output.finish(), reason: message };
|
|
3109
|
+
}
|
|
3110
|
+
const receipt = await input.host.invoke({
|
|
3111
|
+
action: "finish",
|
|
3112
|
+
id: started.id,
|
|
3113
|
+
outcome
|
|
3114
|
+
});
|
|
3115
|
+
return {
|
|
3116
|
+
content: [{ type: "text", text: JSON.stringify(receipt) }],
|
|
3117
|
+
details: { codingReceipt: receipt }
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
},
|
|
3121
|
+
{
|
|
3122
|
+
name: "coding_receipt",
|
|
3123
|
+
label: "Read coding result",
|
|
3124
|
+
description: "Read a retained command result with current applicability. Your own receipts are private. For a teammate\u2019s receipt, provide the id of its policy-authorized message containing coding-receipt:<id>. Old results are not current verification.",
|
|
3125
|
+
parameters: Type.Object(
|
|
3126
|
+
{
|
|
3127
|
+
id: Type.String({ maxLength: 64 }),
|
|
3128
|
+
messageId: Type.Optional(Type.String({ maxLength: 64 }))
|
|
3129
|
+
},
|
|
3130
|
+
{ additionalProperties: false }
|
|
3131
|
+
),
|
|
3132
|
+
executionMode: "sequential",
|
|
3133
|
+
async execute(_id, params) {
|
|
3134
|
+
const value = await input.host.invoke({
|
|
3135
|
+
action: "read",
|
|
3136
|
+
...params
|
|
3137
|
+
});
|
|
3138
|
+
return {
|
|
3139
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
3140
|
+
details: { codingEvidence: value }
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
},
|
|
3144
|
+
{
|
|
3145
|
+
name: "coding_log",
|
|
3146
|
+
label: "Read retained command output",
|
|
3147
|
+
description: "Read a bounded page of retained diagnostic output for a coding receipt, or search it. Producing Agents may read their own logs; a teammate must pass the id of the policy-authorized message that carried coding-receipt:<id>. Output is bounded and may be truncated, expired, deleted or unavailable \u2014 report that state honestly and do not infer completeness from an exit code.",
|
|
3148
|
+
parameters: Type.Object(
|
|
3149
|
+
{
|
|
3150
|
+
id: Type.String({ maxLength: 64 }),
|
|
3151
|
+
messageId: Type.Optional(Type.String({ maxLength: 64 })),
|
|
3152
|
+
search: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
|
|
3153
|
+
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
3154
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 65536 }))
|
|
3155
|
+
},
|
|
3156
|
+
{ additionalProperties: false }
|
|
3157
|
+
),
|
|
3158
|
+
executionMode: "sequential",
|
|
3159
|
+
async execute(_id, params) {
|
|
3160
|
+
const values = params;
|
|
3161
|
+
if (values.search !== void 0) {
|
|
3162
|
+
const result = await input.host.invoke({
|
|
3163
|
+
action: "log-search",
|
|
3164
|
+
id: values.id,
|
|
3165
|
+
query: values.search,
|
|
3166
|
+
...values.messageId !== void 0 ? { messageId: values.messageId } : {}
|
|
3167
|
+
});
|
|
3168
|
+
return {
|
|
3169
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
3170
|
+
details: { codingLogSearch: result }
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
const value = await input.host.invoke({
|
|
3174
|
+
action: "log",
|
|
3175
|
+
id: values.id,
|
|
3176
|
+
...values.messageId !== void 0 ? { messageId: values.messageId } : {},
|
|
3177
|
+
...values.offset !== void 0 ? { offset: values.offset } : {},
|
|
3178
|
+
...values.limit !== void 0 ? { limit: values.limit } : {}
|
|
3179
|
+
});
|
|
3180
|
+
return {
|
|
3181
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
3182
|
+
details: { codingLog: value }
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
},
|
|
3186
|
+
{
|
|
3187
|
+
name: "source_snapshot",
|
|
3188
|
+
label: "Capture the current source state",
|
|
3189
|
+
description: "Capture a bounded snapshot of the repository source you are about to work from, and quote its reference in your summary. Call this once before you start editing: the reference is how a later change or verification is tied to the version it actually applies to. It is read-only and never changes the repository. The result may be incomplete \u2014 when it is, say so, because an incomplete snapshot cannot support a claim that code is unchanged. Untracked file content is never included unless you name each path.",
|
|
3190
|
+
parameters: Type.Object(
|
|
3191
|
+
{
|
|
3192
|
+
includeUntracked: Type.Optional(
|
|
3193
|
+
Type.Array(Type.String({ maxLength: 4096 }), { maxItems: 1e3 })
|
|
3194
|
+
)
|
|
3195
|
+
},
|
|
3196
|
+
{ additionalProperties: false }
|
|
3197
|
+
),
|
|
3198
|
+
executionMode: "sequential",
|
|
3199
|
+
async execute(toolCallId, params) {
|
|
3200
|
+
const values = params;
|
|
3201
|
+
const value = await input.host.invoke({
|
|
3202
|
+
action: "snapshot",
|
|
3203
|
+
toolCallId,
|
|
3204
|
+
...values.includeUntracked ? { includeUntracked: values.includeUntracked } : {}
|
|
3205
|
+
});
|
|
3206
|
+
return {
|
|
3207
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
3208
|
+
details: { sourceSnapshot: value }
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
];
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
// ../daemon/src/runtime/pi/repository-context.ts
|
|
3216
|
+
import { Type as Type2 } from "typebox";
|
|
3217
|
+
|
|
3218
|
+
// ../daemon/src/runtime/pi/repository-context-contract.ts
|
|
3219
|
+
var object2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3220
|
+
var keys = (value, required, optional = []) => required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
|
|
3221
|
+
var string = (value, limit = 4096) => typeof value === "string" && value.length <= limit;
|
|
3222
|
+
var digest = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
3223
|
+
var count = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
3224
|
+
var list3 = (value, limit, check) => Array.isArray(value) && value.length <= limit && value.every(check);
|
|
3225
|
+
var issue = (value) => object2(value) && keys(value, ["code", "message"], ["path"]) && string(value.code, 100) && string(value.message, 1024) && (value.path === void 0 || string(value.path));
|
|
3226
|
+
function assertRepositoryContext(value, teamId) {
|
|
3227
|
+
if (!object2(value) || !keys(value, [
|
|
3228
|
+
"version",
|
|
3229
|
+
"teamId",
|
|
3230
|
+
"rootIdentity",
|
|
3231
|
+
"target",
|
|
3232
|
+
"capturedAt",
|
|
3233
|
+
"fingerprint",
|
|
3234
|
+
"instructions",
|
|
3235
|
+
"commands",
|
|
3236
|
+
"git"
|
|
3237
|
+
]) || value.version !== 1 || !string(value.teamId, 256) || teamId !== void 0 && value.teamId !== teamId || value.rootIdentity !== null && !digest(value.rootIdentity) || !string(value.target) || !count(value.capturedAt) || !digest(value.fingerprint) || !object2(value.instructions) || !object2(value.commands) || !object2(value.git) || Buffer.byteLength(JSON.stringify(value)) > 256 * 1024)
|
|
3238
|
+
throw new Error("Invalid repository context report");
|
|
3239
|
+
const instructions = value.instructions;
|
|
3240
|
+
const commands = value.commands;
|
|
3241
|
+
const git = value.git;
|
|
3242
|
+
if (!keys(instructions, ["state", "issues", "files"]) || !keys(commands, ["state", "issues", "sources", "candidates"]) || !keys(git, [
|
|
3243
|
+
"state",
|
|
3244
|
+
"root",
|
|
3245
|
+
"branch",
|
|
3246
|
+
"head",
|
|
3247
|
+
"headState",
|
|
3248
|
+
"staged",
|
|
3249
|
+
"unstaged",
|
|
3250
|
+
"untracked",
|
|
3251
|
+
"conflicted",
|
|
3252
|
+
"issues"
|
|
3253
|
+
]) || !["complete", "incomplete"].includes(String(instructions.state)) || !list3(instructions.issues, 128, issue) || !list3(
|
|
3254
|
+
instructions.files,
|
|
3255
|
+
17,
|
|
3256
|
+
(file) => object2(file) && keys(file, ["path", "scope", "precedence", "sha256", "content"]) && string(file.path) && string(file.scope) && count(file.precedence) && digest(file.sha256) && string(file.content, 64 * 1024) && Buffer.byteLength(file.content) <= 64 * 1024
|
|
3257
|
+
) || !["complete", "incomplete"].includes(String(commands.state)) || !list3(commands.issues, 128, issue) || !list3(
|
|
3258
|
+
commands.sources,
|
|
3259
|
+
32,
|
|
3260
|
+
(source) => object2(source) && keys(source, ["path", "sha256"]) && string(source.path) && digest(source.sha256)
|
|
3261
|
+
) || !list3(
|
|
3262
|
+
commands.candidates,
|
|
3263
|
+
32,
|
|
3264
|
+
(candidate) => object2(candidate) && keys(candidate, [
|
|
3265
|
+
"source",
|
|
3266
|
+
"sha256",
|
|
3267
|
+
"location",
|
|
3268
|
+
"cwd",
|
|
3269
|
+
"command",
|
|
3270
|
+
"kind",
|
|
3271
|
+
"packageManager"
|
|
3272
|
+
]) && string(candidate.source) && digest(candidate.sha256) && string(candidate.location) && string(candidate.cwd) && string(candidate.command) && ["script", "excerpt"].includes(String(candidate.kind)) && [null, "npm", "pnpm", "yarn"].includes(candidate.packageManager)
|
|
3273
|
+
) || !["available", "unavailable", "not_repository"].includes(String(git.state)) || ![null, "branch", "detached", "unborn"].includes(git.headState) || ![git.root, git.branch, git.head].every((value2) => value2 === null || string(value2)) || ![git.staged, git.unstaged, git.untracked, git.conflicted].every(count) || !list3(git.issues, 128, issue))
|
|
3274
|
+
throw new Error("Invalid repository context report");
|
|
3275
|
+
const report = value;
|
|
3276
|
+
if (report.instructions.files.reduce((bytes, file) => bytes + Buffer.byteLength(file.content), 0) > 128 * 1024 || report.instructions.state === "complete" && (report.rootIdentity === null || report.instructions.issues.length > 0))
|
|
3277
|
+
throw new Error("Invalid repository instruction completeness");
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
// ../daemon/src/runtime/pi/repository-context.ts
|
|
3281
|
+
function repositoryContextIntegration(initial, host, cwd) {
|
|
3282
|
+
assertRepositoryContext(initial);
|
|
3283
|
+
if (initial.instructions.state !== "complete")
|
|
3284
|
+
throw new Error("Repository instructions incomplete");
|
|
3285
|
+
let current = initial;
|
|
3286
|
+
let refreshPrompt = () => {
|
|
3287
|
+
};
|
|
3288
|
+
const files = () => ({
|
|
3289
|
+
agentsFiles: [
|
|
3290
|
+
{
|
|
3291
|
+
path: "Repository instructions and provenance",
|
|
3292
|
+
content: [
|
|
3293
|
+
"# Repository context",
|
|
3294
|
+
`Captured target: ${current.target}; fingerprint: ${current.fingerprint}; captured at: ${new Date(current.capturedAt).toISOString()}.`,
|
|
3295
|
+
`Repository root for this runtime: ${cwd}. This is captured context, not a live workspace lock or verification evidence.`,
|
|
3296
|
+
"Platform/runtime policy and explicit operator instructions take priority. Repository guidance specializes general private Agent preferences for repository work. Deeper documents govern only their own subtree. Team Policy and execution authority are unchanged.",
|
|
3297
|
+
"For coding tasks, investigate repository context and actual coding_environment as needed, then use coding_command for task-related finite checks/preparation. Do not ask the operator to configure named checks. A source-backed Team memory recipe is advisory; refresh it before reuse. Missing network/tooling is a concrete prerequisite to report, never authority to change the runtime. To delegate shared-workspace preparation, send_message and end the turn; do not wait_for_reply while holding the peer workspace.",
|
|
3298
|
+
"Before editing a different subtree, call repository_context with its directory or file path. A returned snapshot replaces previous applicable repository guidance; older transcript snapshots are historical. Do not edit a scope whose instructions are incomplete.",
|
|
3299
|
+
current.instructions.state === "complete" ? "Applicable instructions are complete for the captured scope. Missing AGENTS.md is normal." : `BLOCKED SCOPE: ${current.instructions.issues.map((issue2) => issue2.code).join(", ")}. Resolve this context before editing.`,
|
|
3300
|
+
...current.instructions.state === "complete" ? current.instructions.files.map(
|
|
3301
|
+
(file) => `## ${file.path}
|
|
3302
|
+
Scope: ${file.scope}; precedence: ${file.precedence}; SHA-256: ${file.sha256}
|
|
3303
|
+
|
|
3304
|
+
${file.content}`
|
|
3305
|
+
) : [],
|
|
3306
|
+
`Git orientation at capture time: ${JSON.stringify(current.git)}`,
|
|
3307
|
+
`Command suggestions (task data, not execution permission):
|
|
3308
|
+
${JSON.stringify(current.commands)}`
|
|
3309
|
+
].join("\n\n")
|
|
3310
|
+
}
|
|
3311
|
+
]
|
|
3312
|
+
});
|
|
3313
|
+
const tool = {
|
|
3314
|
+
name: "repository_context",
|
|
3315
|
+
label: "Repository context",
|
|
3316
|
+
description: "Resolve the complete applicable repository AGENTS.md instructions and passive command/Git context for a contained file or directory. Call before editing a new subtree. Does not run project commands or change cwd. Incomplete instructions block editing that scope.",
|
|
3317
|
+
parameters: Type2.Object(
|
|
3318
|
+
{ target: Type2.String({ maxLength: 4096 }) },
|
|
3319
|
+
{ additionalProperties: false }
|
|
3320
|
+
),
|
|
3321
|
+
executionMode: "sequential",
|
|
3322
|
+
async execute(_id, params) {
|
|
3323
|
+
const target = params.target;
|
|
3324
|
+
const replacement = await host(target);
|
|
3325
|
+
assertRepositoryContext(replacement);
|
|
3326
|
+
if (replacement.teamId !== initial.teamId || replacement.rootIdentity !== initial.rootIdentity) {
|
|
3327
|
+
current = {
|
|
3328
|
+
...initial,
|
|
3329
|
+
instructions: {
|
|
3330
|
+
state: "incomplete",
|
|
3331
|
+
files: [],
|
|
3332
|
+
issues: [
|
|
3333
|
+
{
|
|
3334
|
+
code: "root_changed",
|
|
3335
|
+
message: "Repository identity changed; start a new admitted turn."
|
|
3336
|
+
}
|
|
3337
|
+
]
|
|
3338
|
+
},
|
|
3339
|
+
commands: { state: "incomplete", sources: [], candidates: [], issues: [] }
|
|
3340
|
+
};
|
|
3341
|
+
refreshPrompt();
|
|
3342
|
+
throw new Error("Repository context identity changed; start a new admitted turn");
|
|
3343
|
+
}
|
|
3344
|
+
current = replacement;
|
|
3345
|
+
refreshPrompt();
|
|
3346
|
+
return {
|
|
3347
|
+
content: [{ type: "text", text: JSON.stringify(replacement) }],
|
|
3348
|
+
details: { repositoryContext: replacement }
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
};
|
|
3352
|
+
return {
|
|
3353
|
+
files,
|
|
3354
|
+
tool,
|
|
3355
|
+
async resolve(target) {
|
|
3356
|
+
await tool.execute("coding-context", { target }, void 0, void 0, {});
|
|
3357
|
+
return current;
|
|
3358
|
+
},
|
|
3359
|
+
bind(session, allowedTools) {
|
|
3360
|
+
refreshPrompt = () => session.setActiveToolsByName(allowedTools);
|
|
3361
|
+
}
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
// ../daemon/src/runtime/pi/session.ts
|
|
3366
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync7, statSync as statSync5, writeFileSync as writeFileSync9 } from "fs";
|
|
3367
|
+
import { basename as basename4, join as join22 } from "path";
|
|
3368
|
+
import {
|
|
3369
|
+
createAssistantMessageEventStream
|
|
3370
|
+
} from "@earendil-works/pi-ai";
|
|
3371
|
+
import {
|
|
3372
|
+
buildSessionContext,
|
|
3373
|
+
createAgentSession,
|
|
3374
|
+
DefaultResourceLoader,
|
|
3375
|
+
SessionManager,
|
|
3376
|
+
SettingsManager
|
|
3377
|
+
} from "@earendil-works/pi-coding-agent";
|
|
3378
|
+
|
|
3379
|
+
// ../daemon/src/runtime/providers/pi-runtime.ts
|
|
3380
|
+
import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
|
3381
|
+
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
3382
|
+
var PROVIDER_ALIASES = {
|
|
3383
|
+
bedrock: "amazon-bedrock",
|
|
3384
|
+
"azure-openai": "azure-openai-responses"
|
|
3385
|
+
};
|
|
3386
|
+
var PROTECTED_PROVIDER_NAMES = [
|
|
3387
|
+
"anthropic",
|
|
3388
|
+
"openai",
|
|
3389
|
+
"openai-codex",
|
|
3390
|
+
"google",
|
|
3391
|
+
"google-vertex",
|
|
3392
|
+
"azure-openai",
|
|
3393
|
+
"bedrock",
|
|
3394
|
+
"mistral",
|
|
3395
|
+
"groq",
|
|
3396
|
+
"cerebras",
|
|
3397
|
+
"xai",
|
|
3398
|
+
"zai",
|
|
3399
|
+
"huggingface",
|
|
3400
|
+
"openrouter",
|
|
3401
|
+
"vercel-ai-gateway",
|
|
3402
|
+
"deepseek",
|
|
3403
|
+
"fireworks",
|
|
3404
|
+
"together",
|
|
3405
|
+
"baseten",
|
|
3406
|
+
"moonshotai",
|
|
3407
|
+
"moonshotai-cn",
|
|
3408
|
+
"kimi-coding",
|
|
3409
|
+
"minimax",
|
|
3410
|
+
"minimax-cn",
|
|
3411
|
+
"qwen-token-plan",
|
|
3412
|
+
"qwen-token-plan-cn",
|
|
3413
|
+
"qwen-token-plan-individual",
|
|
3414
|
+
"xiaomi",
|
|
3415
|
+
"xiaomi-token-plan-ams",
|
|
3416
|
+
"xiaomi-token-plan-cn",
|
|
3417
|
+
"xiaomi-token-plan-sgp",
|
|
3418
|
+
"ant-ling",
|
|
3419
|
+
"nvidia",
|
|
3420
|
+
"opencode",
|
|
3421
|
+
"opencode-go",
|
|
3422
|
+
"zai-coding-cn",
|
|
3423
|
+
"github-copilot",
|
|
3424
|
+
"cloudflare-ai-gateway",
|
|
3425
|
+
"cloudflare-workers-ai",
|
|
3426
|
+
"lmstudio",
|
|
3427
|
+
"ollama",
|
|
3428
|
+
"llamacpp"
|
|
3429
|
+
];
|
|
3430
|
+
var LOCAL_PROVIDERS = {
|
|
3431
|
+
lmstudio: {
|
|
3432
|
+
baseUrl: "http://127.0.0.1:1234/v1",
|
|
3433
|
+
apiKey: "lm-studio",
|
|
3434
|
+
authHeader: false
|
|
3435
|
+
},
|
|
3436
|
+
ollama: { baseUrl: "http://127.0.0.1:11434/v1", apiKey: "ollama", authHeader: false },
|
|
3437
|
+
llamacpp: { baseUrl: "http://127.0.0.1:8080/v1", apiKey: "no-key", authHeader: true }
|
|
3438
|
+
};
|
|
3439
|
+
var PROVIDER_API_KEY_ENV = {
|
|
3440
|
+
anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
|
3441
|
+
openai: ["OPENAI_API_KEY"],
|
|
3442
|
+
google: ["GEMINI_API_KEY"],
|
|
3443
|
+
"azure-openai": ["AZURE_OPENAI_API_KEY"],
|
|
3444
|
+
mistral: ["MISTRAL_API_KEY"],
|
|
3445
|
+
groq: ["GROQ_API_KEY"],
|
|
3446
|
+
cerebras: ["CEREBRAS_API_KEY"],
|
|
3447
|
+
xai: ["XAI_API_KEY"],
|
|
3448
|
+
zai: ["ZAI_API_KEY"],
|
|
3449
|
+
huggingface: ["HF_TOKEN"],
|
|
3450
|
+
openrouter: ["OPENROUTER_API_KEY"],
|
|
3451
|
+
"vercel-ai-gateway": ["AI_GATEWAY_API_KEY"],
|
|
3452
|
+
deepseek: ["DEEPSEEK_API_KEY"],
|
|
3453
|
+
fireworks: ["FIREWORKS_API_KEY"],
|
|
3454
|
+
together: ["TOGETHER_API_KEY"],
|
|
3455
|
+
baseten: ["BASETEN_API_KEY"],
|
|
3456
|
+
moonshotai: ["MOONSHOT_API_KEY"],
|
|
3457
|
+
"moonshotai-cn": ["MOONSHOT_CN_API_KEY"],
|
|
3458
|
+
"kimi-coding": ["KIMI_API_KEY"],
|
|
3459
|
+
minimax: ["MINIMAX_API_KEY"],
|
|
3460
|
+
"minimax-cn": ["MINIMAX_CN_API_KEY"],
|
|
3461
|
+
"qwen-token-plan": ["QWEN_TOKEN_PLAN_API_KEY"],
|
|
3462
|
+
"qwen-token-plan-cn": ["QWEN_TOKEN_PLAN_CN_API_KEY"],
|
|
3463
|
+
"qwen-token-plan-individual": ["QWEN_TOKEN_PLAN_API_KEY"],
|
|
3464
|
+
xiaomi: ["XIAOMI_API_KEY"],
|
|
3465
|
+
"xiaomi-token-plan-ams": ["XIAOMI_TOKEN_PLAN_AMS_API_KEY"],
|
|
3466
|
+
"xiaomi-token-plan-cn": ["XIAOMI_TOKEN_PLAN_CN_API_KEY"],
|
|
3467
|
+
"xiaomi-token-plan-sgp": ["XIAOMI_TOKEN_PLAN_SGP_API_KEY"],
|
|
3468
|
+
"ant-ling": ["ANT_LING_API_KEY"],
|
|
3469
|
+
nvidia: ["NVIDIA_API_KEY"],
|
|
3470
|
+
opencode: ["OPENCODE_API_KEY"],
|
|
3471
|
+
"opencode-go": ["OPENCODE_GO_API_KEY"],
|
|
3472
|
+
"zai-coding-cn": ["ZAI_CODING_CN_API_KEY"],
|
|
3473
|
+
"github-copilot": ["COPILOT_GITHUB_TOKEN"],
|
|
3474
|
+
"cloudflare-ai-gateway": ["CLOUDFLARE_API_KEY"],
|
|
3475
|
+
"cloudflare-workers-ai": ["CLOUDFLARE_API_KEY"],
|
|
3476
|
+
lmstudio: ["LMSTUDIO_API_KEY"],
|
|
3477
|
+
ollama: ["OLLAMA_API_KEY"],
|
|
3478
|
+
llamacpp: ["LLAMACPP_API_KEY"]
|
|
3479
|
+
};
|
|
3480
|
+
var PROVIDER_CREDENTIAL_ENV = {
|
|
3481
|
+
bedrock: [
|
|
3482
|
+
"AWS_PROFILE",
|
|
3483
|
+
"AWS_BEARER_TOKEN_BEDROCK",
|
|
3484
|
+
"AWS_ACCESS_KEY_ID",
|
|
3485
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
3486
|
+
"AWS_SESSION_TOKEN",
|
|
3487
|
+
"AWS_REGION",
|
|
3488
|
+
"AWS_DEFAULT_REGION"
|
|
3489
|
+
],
|
|
3490
|
+
"google-vertex": [
|
|
3491
|
+
"GOOGLE_CLOUD_PROJECT",
|
|
3492
|
+
"GOOGLE_CLOUD_LOCATION",
|
|
3493
|
+
"GOOGLE_APPLICATION_CREDENTIALS"
|
|
3494
|
+
],
|
|
3495
|
+
"cloudflare-ai-gateway": ["CLOUDFLARE_API_KEY", "CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_GATEWAY_ID"],
|
|
3496
|
+
"cloudflare-workers-ai": ["CLOUDFLARE_API_KEY", "CLOUDFLARE_ACCOUNT_ID"]
|
|
3497
|
+
};
|
|
3498
|
+
function piProviderName(providerName) {
|
|
3499
|
+
return PROVIDER_ALIASES[providerName] ?? providerName;
|
|
3500
|
+
}
|
|
3501
|
+
function providerApiKey(providerName, env) {
|
|
3502
|
+
for (const name of PROVIDER_API_KEY_ENV[providerName] ?? []) {
|
|
3503
|
+
const value = env[name];
|
|
3504
|
+
if (value) return value;
|
|
3505
|
+
}
|
|
3506
|
+
return LOCAL_PROVIDERS[providerName]?.apiKey;
|
|
3507
|
+
}
|
|
3508
|
+
function providerBaseUrl(providerName, env) {
|
|
3509
|
+
switch (providerName) {
|
|
3510
|
+
case "lmstudio":
|
|
3511
|
+
return env.LMSTUDIO_URL ?? LOCAL_PROVIDERS.lmstudio?.baseUrl;
|
|
3512
|
+
case "ollama":
|
|
3513
|
+
return env.OLLAMA_URL ?? LOCAL_PROVIDERS.ollama?.baseUrl;
|
|
3514
|
+
case "llamacpp":
|
|
3515
|
+
return env.LLAMACPP_URL ?? LOCAL_PROVIDERS.llamacpp?.baseUrl;
|
|
3516
|
+
case "vercel-ai-gateway":
|
|
3517
|
+
return env.AI_GATEWAY_BASE_URL;
|
|
3518
|
+
default:
|
|
3519
|
+
return void 0;
|
|
2708
3520
|
}
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
throw new Error(`Docker sandbox received an invalid inspection result for image "${image}"`);
|
|
3521
|
+
}
|
|
3522
|
+
function providerCredentialEnv(providerName, env) {
|
|
3523
|
+
const result = {};
|
|
3524
|
+
for (const name of PROVIDER_CREDENTIAL_ENV[providerName] ?? []) {
|
|
3525
|
+
const value = env[name];
|
|
3526
|
+
if (value !== void 0) result[name] = value;
|
|
2716
3527
|
}
|
|
2717
|
-
|
|
2718
|
-
|
|
3528
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
3529
|
+
}
|
|
3530
|
+
function fallbackApi(providerName) {
|
|
3531
|
+
switch (providerName) {
|
|
3532
|
+
case "anthropic":
|
|
3533
|
+
return "anthropic-messages";
|
|
3534
|
+
case "google":
|
|
3535
|
+
return "google-generative-ai";
|
|
3536
|
+
case "google-vertex":
|
|
3537
|
+
return "google-vertex";
|
|
3538
|
+
case "azure-openai":
|
|
3539
|
+
return "azure-openai-responses";
|
|
3540
|
+
case "bedrock":
|
|
3541
|
+
return "bedrock-converse-stream";
|
|
3542
|
+
case "openai-codex":
|
|
3543
|
+
return "openai-codex-responses";
|
|
3544
|
+
default:
|
|
3545
|
+
return "openai-completions";
|
|
2719
3546
|
}
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
3547
|
+
}
|
|
3548
|
+
var UnknownModelError = class extends Error {
|
|
3549
|
+
providerName;
|
|
3550
|
+
modelId;
|
|
3551
|
+
// Fields are declared explicitly, not as constructor parameter properties:
|
|
3552
|
+
// worker subprocesses run Node's strip-only TypeScript, which rejects them.
|
|
3553
|
+
constructor(providerName, modelId) {
|
|
3554
|
+
super(
|
|
3555
|
+
`Unknown model "${modelId}" for provider "${providerName}": it is not in that provider's catalog, and no endpoint is configured for it. Choose a catalogued model, or configure an endpoint for a custom one.`
|
|
2723
3556
|
);
|
|
3557
|
+
this.name = "UnknownModelError";
|
|
3558
|
+
this.providerName = providerName;
|
|
3559
|
+
this.modelId = modelId;
|
|
2724
3560
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
3561
|
+
};
|
|
3562
|
+
function fallbackPiModel(providerName, modelId, baseUrl) {
|
|
3563
|
+
return {
|
|
3564
|
+
id: modelId,
|
|
3565
|
+
name: modelId,
|
|
3566
|
+
api: fallbackApi(providerName),
|
|
3567
|
+
provider: piProviderName(providerName),
|
|
3568
|
+
baseUrl: baseUrl ?? "",
|
|
3569
|
+
reasoning: false,
|
|
3570
|
+
input: ["text"],
|
|
3571
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
3572
|
+
contextWindow: 32768,
|
|
3573
|
+
maxTokens: 4096
|
|
3574
|
+
};
|
|
2729
3575
|
}
|
|
2730
|
-
function
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
3576
|
+
async function createBazilionPiRuntime(options) {
|
|
3577
|
+
const providerName = options.providerName;
|
|
3578
|
+
const canonicalName = piProviderName(providerName);
|
|
3579
|
+
const credentials = new InMemoryCredentialStore();
|
|
3580
|
+
const apiKey = options.apiKey ?? providerApiKey(providerName, options.env);
|
|
3581
|
+
const credentialEnv = providerCredentialEnv(providerName, options.env);
|
|
3582
|
+
if (apiKey || credentialEnv) {
|
|
3583
|
+
await credentials.modify(
|
|
3584
|
+
canonicalName,
|
|
3585
|
+
async () => canonicalName === "openai-codex" && apiKey ? {
|
|
3586
|
+
type: "oauth",
|
|
3587
|
+
access: apiKey,
|
|
3588
|
+
// Bazilion owns refresh outside Pi. This deliberately empty value
|
|
3589
|
+
// satisfies Pi's canonical OAuth shape but is never used because
|
|
3590
|
+
// the token is long-lived only inside this one-shot runtime and
|
|
3591
|
+
// mid-turn replacement is supplied through Agent.getApiKey.
|
|
3592
|
+
refresh: "",
|
|
3593
|
+
expires: Number.MAX_SAFE_INTEGER
|
|
3594
|
+
} : {
|
|
3595
|
+
type: "api_key",
|
|
3596
|
+
...apiKey ? { key: apiKey } : {},
|
|
3597
|
+
...credentialEnv ? { env: credentialEnv } : {}
|
|
2747
3598
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
stderr = `${stderr}${data.toString("utf8")}`.slice(-4096);
|
|
2763
|
-
});
|
|
2764
|
-
child.once("error", (error) => finish(formatSpawnError(dockerPath, error)));
|
|
2765
|
-
child.once("close", (code) => {
|
|
2766
|
-
if (code === 0) finish();
|
|
2767
|
-
else
|
|
2768
|
-
finish(
|
|
2769
|
-
new Error(
|
|
2770
|
-
`Docker sandbox could not inspect ${subject}: ${stderr.trim() || `Docker exited with status ${code}`}`
|
|
2771
|
-
)
|
|
2772
|
-
);
|
|
3599
|
+
);
|
|
3600
|
+
}
|
|
3601
|
+
const runtime = await ModelRuntime.create({ credentials, modelsPath: null });
|
|
3602
|
+
const local = LOCAL_PROVIDERS[providerName];
|
|
3603
|
+
const baseUrl = options.baseUrl ?? providerBaseUrl(providerName, options.env);
|
|
3604
|
+
if (local) {
|
|
3605
|
+
const modelId = options.modelId ?? "model-name";
|
|
3606
|
+
const model = fallbackPiModel(providerName, modelId, baseUrl ?? local.baseUrl);
|
|
3607
|
+
runtime.registerProvider(canonicalName, {
|
|
3608
|
+
baseUrl: baseUrl ?? local.baseUrl,
|
|
3609
|
+
api: "openai-completions",
|
|
3610
|
+
authHeader: local.authHeader,
|
|
3611
|
+
apiKey: apiKey ?? local.apiKey,
|
|
3612
|
+
models: [model]
|
|
2773
3613
|
});
|
|
2774
|
-
})
|
|
3614
|
+
} else if (baseUrl) {
|
|
3615
|
+
runtime.registerProvider(canonicalName, { baseUrl });
|
|
3616
|
+
}
|
|
3617
|
+
return runtime;
|
|
2775
3618
|
}
|
|
2776
|
-
function
|
|
3619
|
+
async function createProtectedPiRuntime(options) {
|
|
2777
3620
|
const env = {};
|
|
2778
|
-
for (const
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
3621
|
+
for (const field of options.credentialEnv ?? []) env[field.name] = field.value;
|
|
3622
|
+
return createBazilionPiRuntime({
|
|
3623
|
+
providerName: options.providerName,
|
|
3624
|
+
modelId: options.modelId,
|
|
3625
|
+
env,
|
|
3626
|
+
apiKey: options.apiKey,
|
|
3627
|
+
baseUrl: options.baseUrl
|
|
3628
|
+
});
|
|
2783
3629
|
}
|
|
2784
|
-
function
|
|
2785
|
-
|
|
2786
|
-
if (
|
|
2787
|
-
|
|
3630
|
+
function resolvePiModel(runtime, providerName, modelId, baseUrl) {
|
|
3631
|
+
const known = runtime.getModel(piProviderName(providerName), modelId);
|
|
3632
|
+
if (known) return baseUrl ? { ...known, baseUrl } : known;
|
|
3633
|
+
if (!baseUrl) throw new UnknownModelError(providerName, modelId);
|
|
3634
|
+
return fallbackPiModel(providerName, modelId, baseUrl);
|
|
2788
3635
|
}
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
3636
|
+
|
|
3637
|
+
// ../daemon/src/runtime/auth/openai-codex.ts
|
|
3638
|
+
import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
|
|
3639
|
+
|
|
3640
|
+
// ../daemon/src/runtime/providers/pi-adapter.ts
|
|
3641
|
+
import {
|
|
3642
|
+
Type as Type3
|
|
3643
|
+
} from "@earendil-works/pi-ai";
|
|
3644
|
+
|
|
3645
|
+
// ../daemon/src/runtime/session/prompt.ts
|
|
3646
|
+
import { existsSync as existsSync10, readdirSync as readdirSync6 } from "fs";
|
|
3647
|
+
import { join as join17 } from "path";
|
|
3648
|
+
|
|
3649
|
+
// ../daemon/src/runtime/safe-files.ts
|
|
3650
|
+
import {
|
|
3651
|
+
closeSync as closeSync5,
|
|
3652
|
+
constants as constants5,
|
|
3653
|
+
fstatSync as fstatSync4,
|
|
3654
|
+
lstatSync as lstatSync3,
|
|
3655
|
+
mkdirSync as mkdirSync5,
|
|
3656
|
+
openSync as openSync5,
|
|
3657
|
+
readFileSync as readFileSync9,
|
|
3658
|
+
realpathSync as realpathSync5
|
|
3659
|
+
} from "fs";
|
|
3660
|
+
import { dirname as dirname3, isAbsolute as isAbsolute4, relative as relative3, resolve as resolve4 } from "path";
|
|
3661
|
+
function resolveRealDirectory(path) {
|
|
3662
|
+
const requested = resolve4(path);
|
|
3663
|
+
const entry = lstatSync3(requested);
|
|
3664
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
3665
|
+
throw new Error("expected a real directory");
|
|
2793
3666
|
}
|
|
2794
|
-
|
|
3667
|
+
return realpathSync5(requested);
|
|
2795
3668
|
}
|
|
2796
|
-
function
|
|
2797
|
-
|
|
2798
|
-
|
|
3669
|
+
function readRegularFileNoFollow(path) {
|
|
3670
|
+
const entry = lstatSync3(path);
|
|
3671
|
+
if (entry.isSymbolicLink()) throw new Error("symbolic links are not allowed");
|
|
3672
|
+
if (!entry.isFile()) throw new Error("target is not a regular file");
|
|
3673
|
+
let fd;
|
|
3674
|
+
try {
|
|
3675
|
+
fd = openSync5(path, constants5.O_RDONLY | noFollowFlag());
|
|
3676
|
+
if (!fstatSync4(fd).isFile()) throw new Error("target is not a regular file");
|
|
3677
|
+
return readFileSync9(fd, "utf8");
|
|
3678
|
+
} finally {
|
|
3679
|
+
if (fd !== void 0) closeSync5(fd);
|
|
2799
3680
|
}
|
|
2800
3681
|
}
|
|
2801
|
-
function
|
|
2802
|
-
|
|
2803
|
-
if (mountSource.includes("\0") || mountSource.includes(",")) {
|
|
2804
|
-
throw new Error(
|
|
2805
|
-
`Docker sandbox mount path cannot be represented safely: ${JSON.stringify(mountSource)}`
|
|
2806
|
-
);
|
|
2807
|
-
}
|
|
3682
|
+
function noFollowFlag() {
|
|
3683
|
+
return typeof constants5.O_NOFOLLOW === "number" ? constants5.O_NOFOLLOW : 0;
|
|
2808
3684
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
);
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
3685
|
+
|
|
3686
|
+
// ../daemon/src/runtime/session/prompt.ts
|
|
3687
|
+
var SANDBOX_SKILLS_DIR = "/skills";
|
|
3688
|
+
function loadPromptSkills(skillsDir, attachedNames) {
|
|
3689
|
+
if (!existsSync10(skillsDir) || attachedNames.length === 0) return [];
|
|
3690
|
+
const attached = new Set(attachedNames);
|
|
3691
|
+
const discovered = readdirSync6(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && attached.has(entry.name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
3692
|
+
const loaded = [];
|
|
3693
|
+
for (const entry of discovered) {
|
|
3694
|
+
const hostDir = join17(skillsDir, entry.name);
|
|
3695
|
+
try {
|
|
3696
|
+
const parsed = parseSkillContent(readRegularFileNoFollow(join17(hostDir, "SKILL.md")));
|
|
3697
|
+
const safeLabel = entry.name.replace(/[^A-Za-z0-9._-]/g, "_") || "skill";
|
|
3698
|
+
loaded.push({
|
|
3699
|
+
name: entry.name,
|
|
3700
|
+
description: parsed.frontmatter.description,
|
|
3701
|
+
body: parsed.body.trim(),
|
|
3702
|
+
hostDir,
|
|
3703
|
+
sandboxDir: `${SANDBOX_SKILLS_DIR}/${loaded.length}-${safeLabel}`
|
|
3704
|
+
});
|
|
3705
|
+
} catch {
|
|
2828
3706
|
}
|
|
2829
|
-
targets.add(mount.target);
|
|
2830
3707
|
}
|
|
3708
|
+
return loaded;
|
|
2831
3709
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
3710
|
+
var CONTEXT_FILE_ORDER = ["AGENTS.md", "SOUL.md", "TOOLS.md", "IDENTITY.md"];
|
|
3711
|
+
function loadProtectedHomeDocuments(agentDir) {
|
|
3712
|
+
const root = resolveRealDirectory(agentDir);
|
|
3713
|
+
const read = (file) => {
|
|
3714
|
+
try {
|
|
3715
|
+
return readRegularFileNoFollow(join17(root, file));
|
|
3716
|
+
} catch (error) {
|
|
3717
|
+
if (error.code === "ENOENT") return null;
|
|
3718
|
+
throw new Error(`private Agent home is unsafe at ${file}`);
|
|
3719
|
+
}
|
|
3720
|
+
};
|
|
3721
|
+
return {
|
|
3722
|
+
"AGENTS.md": read("AGENTS.md"),
|
|
3723
|
+
"SOUL.md": read("SOUL.md"),
|
|
3724
|
+
"TOOLS.md": read("TOOLS.md"),
|
|
3725
|
+
"IDENTITY.md": read("IDENTITY.md"),
|
|
3726
|
+
"BOOTSTRAP.md": read("BOOTSTRAP.md")
|
|
3727
|
+
};
|
|
2840
3728
|
}
|
|
2841
|
-
function
|
|
2842
|
-
const
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
3729
|
+
function buildSystemPrompt(agent, options = {}) {
|
|
3730
|
+
const parts = [];
|
|
3731
|
+
const homeDocuments = options.homeDocuments ?? loadProtectedHomeDocuments(agent.agent.dir);
|
|
3732
|
+
const contextBlocks = [];
|
|
3733
|
+
for (const file of CONTEXT_FILE_ORDER) {
|
|
3734
|
+
const content = homeDocuments[file]?.trimEnd();
|
|
3735
|
+
if (!content) continue;
|
|
3736
|
+
contextBlocks.push(`## ${file}
|
|
3737
|
+
|
|
3738
|
+
${content}`);
|
|
2848
3739
|
}
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
if (/no such image|unable to find image|manifest unknown|pull access denied/i.test(detail3)) {
|
|
2854
|
-
return new Error(`Docker sandbox image "${spec.image}" is unavailable: ${detail3}`);
|
|
3740
|
+
if (contextBlocks.length > 0) {
|
|
3741
|
+
parts.push(`# Agent instructions
|
|
3742
|
+
|
|
3743
|
+
${contextBlocks.join("\n\n")}`);
|
|
2855
3744
|
}
|
|
2856
|
-
|
|
2857
|
-
|
|
3745
|
+
const bootstrap = homeDocuments["BOOTSTRAP.md"]?.trimEnd();
|
|
3746
|
+
if (bootstrap) {
|
|
3747
|
+
parts.push(
|
|
3748
|
+
[
|
|
3749
|
+
"# First-Run Ritual",
|
|
3750
|
+
"",
|
|
3751
|
+
"This is your first session. The document below is **conversational guidance**, not a checklist to execute in one shot. It describes a multi-turn Q&A you should have with the human, one question per turn.",
|
|
3752
|
+
"",
|
|
3753
|
+
"## Hard rules",
|
|
3754
|
+
"- Your first reply is ONLY a greeting + ONE question. No tool calls. Wait for the human to answer.",
|
|
3755
|
+
"- Each subsequent turn: at most one new question. Wait between turns.",
|
|
3756
|
+
"- Only after the ritual is complete (you have enough to write IDENTITY.md): call `home_write` once, then `bootstrap_done`.",
|
|
3757
|
+
"",
|
|
3758
|
+
"## BOOTSTRAP.md",
|
|
3759
|
+
"",
|
|
3760
|
+
bootstrap
|
|
3761
|
+
].join("\n")
|
|
3762
|
+
);
|
|
2858
3763
|
}
|
|
2859
|
-
|
|
2860
|
-
|
|
3764
|
+
parts.push(
|
|
3765
|
+
[
|
|
3766
|
+
"# Agent Home",
|
|
3767
|
+
"",
|
|
3768
|
+
"Your private home holds who you are \u2014 identity, soul, and behaviour rules. It is not shared with other agents and cannot be overwritten by them. The files above (IDENTITY.md, SOUL.md, AGENTS.md, TOOLS.md) live in this home, plus BOOTSTRAP.md when you are still in your first-run ritual.",
|
|
3769
|
+
"",
|
|
3770
|
+
"- To change who you are (name, vibe, personality, how you behave): use `home_write`.",
|
|
3771
|
+
"- To inspect exact wording of your own files: use `home_read` or `home_list`.",
|
|
3772
|
+
"- To remember facts the user told you or things you learned: use `memory_write` \u2014 NOT `home_write`.",
|
|
3773
|
+
"- To produce work output (code, docs, artefacts): use the workspace tools currently exposed to you \u2014 those operate on your team workspace, not your private home."
|
|
3774
|
+
].join("\n")
|
|
3775
|
+
);
|
|
3776
|
+
if (agent.skills.length > 0) {
|
|
3777
|
+
const loaded = options.skills ?? [];
|
|
3778
|
+
const loadedNames = new Set(loaded.map((skill) => skill.name));
|
|
3779
|
+
const unavailable = agent.skills.filter((name) => !loadedNames.has(name));
|
|
3780
|
+
const skillBlocks = loaded.map((skill) => {
|
|
3781
|
+
const runtimeDir = options.sandboxMode === "docker" ? skill.sandboxDir : skill.hostDir;
|
|
3782
|
+
return [
|
|
3783
|
+
`## ${skill.name}`,
|
|
3784
|
+
"",
|
|
3785
|
+
skill.description,
|
|
3786
|
+
"",
|
|
3787
|
+
`Runtime directory: \`${runtimeDir}\`. Resolve any relative scripts or assets mentioned by this skill from that directory.`,
|
|
3788
|
+
"",
|
|
3789
|
+
skill.body
|
|
3790
|
+
].filter((line, index, lines) => line !== "" || lines[index - 1] !== "").join("\n").trimEnd();
|
|
3791
|
+
});
|
|
3792
|
+
const unavailableNote = unavailable.length > 0 ? `Attached but unavailable or invalid: ${unavailable.join(", ")}.` : "";
|
|
3793
|
+
parts.push(
|
|
3794
|
+
[
|
|
3795
|
+
"# Available Skills",
|
|
3796
|
+
"",
|
|
3797
|
+
"Use these attached skill instructions when they are relevant to the task.",
|
|
3798
|
+
"",
|
|
3799
|
+
...skillBlocks,
|
|
3800
|
+
...unavailableNote ? ["", unavailableNote] : []
|
|
3801
|
+
].join("\n")
|
|
3802
|
+
);
|
|
2861
3803
|
}
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
3804
|
+
const teamLines = [
|
|
3805
|
+
"# Team",
|
|
3806
|
+
"",
|
|
3807
|
+
`- ${agent.team.id} (${agent.team.name}): ${options.sandboxMode === "docker" ? "/workspace" : agent.team.path}`,
|
|
3808
|
+
"",
|
|
3809
|
+
"Your team is where work product lives \u2014 code, docs, artefacts, shared scratch. It may be shared with other agents in the same team. Use the workspace tools currently exposed to work there. Never use workspace tools to edit your identity/soul/behaviour files \u2014 those live in your private home and are reached via `home_write` / `home_read`."
|
|
3810
|
+
];
|
|
3811
|
+
parts.push(teamLines.join("\n"));
|
|
3812
|
+
if (agent.team.userMd.trim()) {
|
|
3813
|
+
parts.push(
|
|
3814
|
+
`# About the User
|
|
3815
|
+
|
|
3816
|
+
Shared context about the human you're working with in this team. Both you and the human curate it. To update: call \`user_md_get\` (returns current content + an etag), merge your change into the full text, then call \`user_md_write\` with the merged content and the etag. Use this for STABLE user-specific facts (preferences, role, working hours, how they like to be addressed) \u2014 and to CORRECT stale entries when the human tells you something different from what's recorded. For project knowledge use \`memory_write\` instead; for personal notes about yourself use \`home_write\` on IDENTITY.md. **Do NOT send peer messages announcing USER.md changes \u2014 every agent in the team sees the new content in their system prompt on their next turn automatically.**
|
|
3817
|
+
|
|
3818
|
+
${agent.team.userMd.trim()}`
|
|
3819
|
+
);
|
|
3820
|
+
} else {
|
|
3821
|
+
parts.push(
|
|
3822
|
+
`# About the User
|
|
3823
|
+
|
|
3824
|
+
This team's USER.md is empty. As you learn STABLE facts about the human (preferences, role, working hours, how they like to be addressed), populate it via \`user_md_get\` then \`user_md_write\` (always get first \u2014 you need the etag). Reserve this for things you're confident are durable \u2014 project knowledge belongs in \`memory_write\`, personal notes about yourself in \`home_write\` on IDENTITY.md. **Do NOT send peer messages announcing USER.md changes \u2014 every agent in the team sees the new content in their system prompt on their next turn automatically.**`
|
|
3825
|
+
);
|
|
2873
3826
|
}
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
3827
|
+
const reviewedLessons = boundReviewedLessons(agent.privateLessons);
|
|
3828
|
+
if (reviewedLessons.length > 0) {
|
|
3829
|
+
parts.push(
|
|
3830
|
+
`# Reviewed lessons
|
|
3831
|
+
|
|
3832
|
+
These private lessons were explicitly approved by the human for your behavior. Follow them when relevant; current user instructions and higher-priority policy still win.
|
|
3833
|
+
|
|
3834
|
+
${reviewedLessons.map((lesson) => `- ${lesson}`).join("\n")}`
|
|
2877
3835
|
);
|
|
2878
3836
|
}
|
|
3837
|
+
parts.push(
|
|
3838
|
+
"# Memory\n\nYou share a persistent memory backend with every other agent in this team. Use `memory_write` to remember things across sessions, and `memory_search` / `memory_read` / `memory_list` to recall them. This memory is for project knowledge \u2014 codebase notes, decisions, things the user told you about the work. For personal notes about yourself (preferences, persona quirks), use `home_write` on IDENTITY.md instead. Always check memory at the start of a session: another agent in the team may have already learned something useful. **Do NOT send peer messages announcing memory writes \u2014 every agent has access to the same store via `memory_search` and will find your note when they need it.**"
|
|
3839
|
+
);
|
|
3840
|
+
return parts.join("\n\n---\n\n");
|
|
2879
3841
|
}
|
|
2880
|
-
function
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
const
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
clearTimeout(timeoutHandle);
|
|
2893
|
-
const detail3 = error || stderr.trim();
|
|
2894
|
-
resolve6({
|
|
2895
|
-
ok,
|
|
2896
|
-
missing: /no such container/i.test(detail3),
|
|
2897
|
-
error: detail3
|
|
2898
|
-
});
|
|
2899
|
-
};
|
|
2900
|
-
const timeoutHandle = setTimeout(() => {
|
|
2901
|
-
child.kill("SIGKILL");
|
|
2902
|
-
finish(false, `cleanup timed out after ${CLEANUP_TIMEOUT_MS}ms`);
|
|
2903
|
-
}, CLEANUP_TIMEOUT_MS);
|
|
2904
|
-
child.stderr.on("data", (data) => {
|
|
2905
|
-
stderr = `${stderr}${data.toString("utf8")}`.slice(-2048);
|
|
2906
|
-
});
|
|
2907
|
-
child.once("error", (error) => finish(false, error.message));
|
|
2908
|
-
child.once("close", (code) => finish(code === 0));
|
|
2909
|
-
});
|
|
3842
|
+
function boundReviewedLessons(lessons, maxCharacters = 8e3) {
|
|
3843
|
+
const selected = [];
|
|
3844
|
+
let used = 0;
|
|
3845
|
+
for (const raw of lessons) {
|
|
3846
|
+
const lesson = raw.trim();
|
|
3847
|
+
if (!lesson) continue;
|
|
3848
|
+
const remaining = maxCharacters - used;
|
|
3849
|
+
if (remaining <= 0) break;
|
|
3850
|
+
selected.push(lesson.slice(0, remaining));
|
|
3851
|
+
used += Math.min(lesson.length, remaining);
|
|
3852
|
+
}
|
|
3853
|
+
return selected;
|
|
2910
3854
|
}
|
|
2911
3855
|
|
|
2912
3856
|
// ../daemon/src/runtime/shell/tooling.ts
|
|
3857
|
+
import { existsSync as existsSync11, lstatSync as lstatSync4, realpathSync as realpathSync6 } from "fs";
|
|
3858
|
+
import { dirname as dirname4, isAbsolute as isAbsolute5, join as join18, relative as relative4, sep as sep4 } from "path";
|
|
3859
|
+
import {
|
|
3860
|
+
createBashToolDefinition,
|
|
3861
|
+
createLocalBashOperations as createLocalBashOperations2,
|
|
3862
|
+
defineTool as defineTool2
|
|
3863
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2913
3864
|
var HOST_CODING_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
2914
3865
|
var HOST_FILE_TOOL_NAMES = ["read", "edit", "write", "grep", "find", "ls"];
|
|
2915
3866
|
var CONTAINER_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
2916
3867
|
var SANDBOX_INPUTS_DIR = "/inputs";
|
|
2917
3868
|
var SANDBOX_MEMORY_DIR = "/workspace/memory";
|
|
2918
|
-
function createProtectedSessionShellTools(cwd, runtime, approvalHost) {
|
|
3869
|
+
function createProtectedSessionShellTools(cwd, runtime, approvalHost, lifecycle) {
|
|
3870
|
+
const operations = createPreparedDockerBashOperations(runtime, lifecycle);
|
|
3871
|
+
const modelCwd = codingContainerCwd(runtime.coding?.cwd ?? ".");
|
|
2919
3872
|
const base = createBashToolDefinition(cwd, {
|
|
2920
|
-
operations:
|
|
3873
|
+
operations: {
|
|
3874
|
+
exec(command, requestedCwd, options) {
|
|
3875
|
+
if (requestedCwd !== modelCwd && requestedCwd !== cwd)
|
|
3876
|
+
throw new Error("Protected command working directory differs from the admitted scope");
|
|
3877
|
+
return operations.exec(command, cwd, options);
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
2921
3880
|
});
|
|
2922
3881
|
const dockerBash = defineTool2({
|
|
2923
3882
|
...base,
|
|
2924
3883
|
label: "bash (protected Docker sandbox)",
|
|
2925
|
-
description: `${base.description} Runs in an ephemeral, network-disabled Docker container with the Team workspace mounted read/write plus only the preflighted read-only memory, skill, and attachment mounts. Host files, host tools, and host credentials are unavailable.`
|
|
3884
|
+
description: `${base.description} Default working directory: ${codingContainerCwd(runtime.coding?.cwd ?? ".")}. Runs in an ephemeral, network-disabled Docker container with the Team workspace mounted read/write plus only the preflighted read-only memory, skill, and attachment mounts. Host files, host tools, and host credentials are unavailable.`
|
|
2926
3885
|
});
|
|
2927
3886
|
return {
|
|
2928
3887
|
config: {
|
|
@@ -2948,21 +3907,40 @@ function buildSandboxContainerEnv(env, allowlist) {
|
|
|
2948
3907
|
}
|
|
2949
3908
|
function createSessionShellTools(cwd, env, options = {}) {
|
|
2950
3909
|
const config = resolveShellSecurityConfig(env);
|
|
3910
|
+
if (options.preparedDocker) {
|
|
3911
|
+
if (config.sandboxMode !== "docker")
|
|
3912
|
+
throw new Error("Prepared coding environment cannot execute on the host");
|
|
3913
|
+
const runtime = options.preparedDocker;
|
|
3914
|
+
const base2 = createBashToolDefinition(cwd, {
|
|
3915
|
+
operations: createPreparedDockerBashOperations(runtime, options.dockerLifecycle)
|
|
3916
|
+
});
|
|
3917
|
+
const tool = defineTool2({
|
|
3918
|
+
...base2,
|
|
3919
|
+
label: "bash (prepared Docker sandbox)",
|
|
3920
|
+
description: `${base2.description} Default working directory: ${codingContainerCwd(runtime.coding?.cwd ?? ".")}. Fresh network-disabled Docker container with the Team workspace and preflighted read-only mounts.`
|
|
3921
|
+
});
|
|
3922
|
+
return {
|
|
3923
|
+
config: { ...config, sandboxImage: runtime.image, envAllowlist: [] },
|
|
3924
|
+
hostToolNames: [],
|
|
3925
|
+
customBash: config.approvalMode === "dangerous" ? createApprovalGatedBashTool(tool, options.approvalHost) : tool
|
|
3926
|
+
};
|
|
3927
|
+
}
|
|
2951
3928
|
if (config.sandboxMode === "off" && config.approvalMode === "off") {
|
|
2952
3929
|
return { config, hostToolNames: HOST_CODING_TOOL_NAMES };
|
|
2953
3930
|
}
|
|
2954
3931
|
if (config.sandboxMode === "off") {
|
|
2955
3932
|
const base2 = createBashToolDefinition(cwd, {
|
|
2956
|
-
operations:
|
|
3933
|
+
operations: createLocalBashOperations2()
|
|
2957
3934
|
});
|
|
2958
3935
|
const customBash2 = createApprovalGatedBashTool(base2, options.approvalHost);
|
|
2959
3936
|
return { config, hostToolNames: HOST_FILE_TOOL_NAMES, customBash: customBash2 };
|
|
2960
3937
|
}
|
|
2961
3938
|
const operations = createDockerBashOperations({
|
|
3939
|
+
lifecycle: options.dockerLifecycle,
|
|
2962
3940
|
image: config.sandboxImage,
|
|
2963
3941
|
env: buildSandboxContainerEnv(env, config.envAllowlist),
|
|
2964
3942
|
readOnlyMounts: [
|
|
2965
|
-
...existsSync11(
|
|
3943
|
+
...existsSync11(join18(cwd, "memory")) ? [safeReadOnlyMount(join18(cwd, "memory"), SANDBOX_MEMORY_DIR, cwd)] : [],
|
|
2966
3944
|
...options.inputsDir ? [safeReadOnlyMount(options.inputsDir, SANDBOX_INPUTS_DIR, dirname4(options.inputsDir))] : [],
|
|
2967
3945
|
...(options.skillMounts ?? []).map(
|
|
2968
3946
|
(mount) => safeReadOnlyMount(mount.source, mount.target, dirname4(mount.source))
|
|
@@ -2979,14 +3957,14 @@ function createSessionShellTools(cwd, env, options = {}) {
|
|
|
2979
3957
|
return { config, hostToolNames: [], customBash };
|
|
2980
3958
|
}
|
|
2981
3959
|
function safeReadOnlyMount(source, target, expectedRoot) {
|
|
2982
|
-
const info =
|
|
3960
|
+
const info = lstatSync4(source);
|
|
2983
3961
|
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
2984
3962
|
throw new Error(`Docker sandbox read-only mount must be a real directory: ${source}`);
|
|
2985
3963
|
}
|
|
2986
|
-
const sourceRoot =
|
|
2987
|
-
const canonicalSource =
|
|
3964
|
+
const sourceRoot = realpathSync6(expectedRoot);
|
|
3965
|
+
const canonicalSource = realpathSync6(source);
|
|
2988
3966
|
const rel = relative4(sourceRoot, canonicalSource);
|
|
2989
|
-
const contained = rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !
|
|
3967
|
+
const contained = rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute5(rel);
|
|
2990
3968
|
if (!contained) {
|
|
2991
3969
|
throw new Error(`Docker sandbox read-only mount escaped its configured root: ${source}`);
|
|
2992
3970
|
}
|
|
@@ -2995,18 +3973,18 @@ function safeReadOnlyMount(source, target, expectedRoot) {
|
|
|
2995
3973
|
|
|
2996
3974
|
// ../daemon/src/runtime/worker/runtime.ts
|
|
2997
3975
|
import {
|
|
2998
|
-
constants as
|
|
3976
|
+
constants as constants6,
|
|
2999
3977
|
existsSync as existsSync12,
|
|
3000
|
-
lstatSync as
|
|
3978
|
+
lstatSync as lstatSync5,
|
|
3001
3979
|
mkdirSync as mkdirSync6,
|
|
3002
3980
|
mkdtempSync as mkdtempSync3,
|
|
3003
|
-
realpathSync as
|
|
3981
|
+
realpathSync as realpathSync7,
|
|
3004
3982
|
rmSync as rmSync11,
|
|
3005
3983
|
statSync as statSync3
|
|
3006
3984
|
} from "fs";
|
|
3007
3985
|
import { access as access2 } from "fs/promises";
|
|
3008
3986
|
import { tmpdir as tmpdir3 } from "os";
|
|
3009
|
-
import { isAbsolute as
|
|
3987
|
+
import { isAbsolute as isAbsolute6, join as join19, relative as relative5, sep as sep5 } from "path";
|
|
3010
3988
|
var REDACTED_VALUE = "[REDACTED]";
|
|
3011
3989
|
var REASONING_LEVELS = /* @__PURE__ */ new Set([
|
|
3012
3990
|
"off",
|
|
@@ -3017,6 +3995,9 @@ var REASONING_LEVELS = /* @__PURE__ */ new Set([
|
|
|
3017
3995
|
"xhigh"
|
|
3018
3996
|
]);
|
|
3019
3997
|
var CONFIGURED_KEYS = /* @__PURE__ */ new Set([
|
|
3998
|
+
"containerNamespace",
|
|
3999
|
+
"configuredDocker",
|
|
4000
|
+
"repositoryContext",
|
|
3020
4001
|
"questionEnabled",
|
|
3021
4002
|
"conversation",
|
|
3022
4003
|
"kind",
|
|
@@ -3032,6 +4013,7 @@ var CONFIGURED_KEYS = /* @__PURE__ */ new Set([
|
|
|
3032
4013
|
"bashApprovalMode"
|
|
3033
4014
|
]);
|
|
3034
4015
|
var CONFIGURED_REQUIRED_KEYS = /* @__PURE__ */ new Set([
|
|
4016
|
+
"repositoryContext",
|
|
3035
4017
|
"conversation",
|
|
3036
4018
|
"kind",
|
|
3037
4019
|
"agent",
|
|
@@ -3042,6 +4024,8 @@ var CONFIGURED_REQUIRED_KEYS = /* @__PURE__ */ new Set([
|
|
|
3042
4024
|
"bashApprovalMode"
|
|
3043
4025
|
]);
|
|
3044
4026
|
var PROTECTED_KEYS = /* @__PURE__ */ new Set([
|
|
4027
|
+
"containerNamespace",
|
|
4028
|
+
"repositoryContext",
|
|
3045
4029
|
"questionEnabled",
|
|
3046
4030
|
"conversation",
|
|
3047
4031
|
"kind",
|
|
@@ -3058,7 +4042,9 @@ var PROTECTED_KEYS = /* @__PURE__ */ new Set([
|
|
|
3058
4042
|
"scratch"
|
|
3059
4043
|
]);
|
|
3060
4044
|
var PROTECTED_REQUIRED_KEYS = new Set(
|
|
3061
|
-
[...PROTECTED_KEYS].filter(
|
|
4045
|
+
[...PROTECTED_KEYS].filter(
|
|
4046
|
+
(key) => key !== "images" && key !== "questionEnabled" && key !== "containerNamespace"
|
|
4047
|
+
)
|
|
3062
4048
|
);
|
|
3063
4049
|
var REVIEW_KEYS = /* @__PURE__ */ new Set([
|
|
3064
4050
|
"kind",
|
|
@@ -3119,11 +4105,21 @@ function validateMinimalWorkerProcessEnv(env, scratch, platform = process.platfo
|
|
|
3119
4105
|
function parseWorkerInput(value) {
|
|
3120
4106
|
const input = objectRecord(value, "worker input");
|
|
3121
4107
|
const kind = input.kind;
|
|
4108
|
+
if (input.containerNamespace !== void 0 && (typeof input.containerNamespace !== "string" || !/^[a-f0-9-]{36}$/.test(input.containerNamespace)))
|
|
4109
|
+
throw new Error("Invalid container namespace");
|
|
3122
4110
|
if (input.questionEnabled !== void 0 && typeof input.questionEnabled !== "boolean")
|
|
3123
4111
|
throw new Error("Invalid worker question capability");
|
|
3124
4112
|
if (kind === "configured_operator_http") {
|
|
3125
4113
|
assertExactKeys(input, CONFIGURED_KEYS, "configured worker input", CONFIGURED_REQUIRED_KEYS);
|
|
3126
4114
|
if (!isResolvedAgent(input.agent)) throw new Error("worker: configured input requires an agent");
|
|
4115
|
+
if (input.configuredDocker !== void 0) {
|
|
4116
|
+
const prepared = objectRecord(input.configuredDocker, "configured Docker inputs");
|
|
4117
|
+
const keys2 = /* @__PURE__ */ new Set(["paths", "docker"]);
|
|
4118
|
+
assertExactKeys(prepared, keys2, "configured Docker inputs", keys2);
|
|
4119
|
+
assertProtectedWorkerPaths(prepared.paths, input.agent);
|
|
4120
|
+
assertProtectedDockerRuntime(prepared.docker, prepared.paths);
|
|
4121
|
+
}
|
|
4122
|
+
assertRepositoryContext(input.repositoryContext, input.agent.team.id);
|
|
3127
4123
|
requireString(input.message, "message");
|
|
3128
4124
|
if (!Array.isArray(input.enabledProviders) || !input.enabledProviders.every(isString)) {
|
|
3129
4125
|
throw new Error("worker: configured input requires enabledProviders");
|
|
@@ -3149,6 +4145,7 @@ function parseWorkerInput(value) {
|
|
|
3149
4145
|
if (kind === "protected") {
|
|
3150
4146
|
assertExactKeys(input, PROTECTED_KEYS, "protected worker input", PROTECTED_REQUIRED_KEYS);
|
|
3151
4147
|
if (!isResolvedAgent(input.agent)) throw new Error("worker: protected input requires an agent");
|
|
4148
|
+
assertRepositoryContext(input.repositoryContext, input.agent.team.id);
|
|
3152
4149
|
requireString(input.message, "message");
|
|
3153
4150
|
requireString(input.turnId, "turnId");
|
|
3154
4151
|
assertConversationTarget(input.conversation);
|
|
@@ -3281,8 +4278,8 @@ function protectedRuntimeSecrets(runtime) {
|
|
|
3281
4278
|
runtime.credentialFile?.content
|
|
3282
4279
|
].filter((value) => Boolean(value));
|
|
3283
4280
|
}
|
|
3284
|
-
function redactExactValues(
|
|
3285
|
-
let redacted =
|
|
4281
|
+
function redactExactValues(text2, values) {
|
|
4282
|
+
let redacted = text2;
|
|
3286
4283
|
for (const value of [...new Set(values)].filter(Boolean).sort((a, b) => b.length - a.length)) {
|
|
3287
4284
|
redacted = redacted.replaceAll(value, REDACTED_VALUE);
|
|
3288
4285
|
}
|
|
@@ -3391,7 +4388,8 @@ function assertProtectedDockerRuntime(value, paths) {
|
|
|
3391
4388
|
"gid",
|
|
3392
4389
|
"workspace",
|
|
3393
4390
|
"readOnlyMounts",
|
|
3394
|
-
"containerEnv"
|
|
4391
|
+
"containerEnv",
|
|
4392
|
+
"coding"
|
|
3395
4393
|
]),
|
|
3396
4394
|
"protected Docker runtime",
|
|
3397
4395
|
/* @__PURE__ */ new Set([
|
|
@@ -3431,7 +4429,7 @@ function assertProtectedDockerRuntime(value, paths) {
|
|
|
3431
4429
|
throw new Error(`worker: protected Docker executable identity has invalid ${name}`);
|
|
3432
4430
|
}
|
|
3433
4431
|
}
|
|
3434
|
-
if (typeof runtime.endpoint !== "string" || !runtime.endpoint.startsWith("unix://") || !
|
|
4432
|
+
if (typeof runtime.endpoint !== "string" || !runtime.endpoint.startsWith("unix://") || !isAbsolute6(runtime.endpoint.slice("unix://".length)) || containsUnsafeDockerArgument(runtime.endpoint)) {
|
|
3435
4433
|
throw new Error("worker: protected Docker endpoint must be a local Unix socket");
|
|
3436
4434
|
}
|
|
3437
4435
|
if (!nonEmptyString(runtime.image) || runtime.image.startsWith("-") || runtime.image.includes("\0") || runtime.image.includes("\n") || runtime.image.includes("\r")) {
|
|
@@ -3495,6 +4493,7 @@ function assertProtectedDockerRuntime(value, paths) {
|
|
|
3495
4493
|
runtime.readOnlyMounts,
|
|
3496
4494
|
paths
|
|
3497
4495
|
);
|
|
4496
|
+
if (runtime.coding !== void 0) validateDockerCodingSelection(runtime.coding);
|
|
3498
4497
|
const containerEnv = objectRecord(runtime.containerEnv, "protected Docker container environment");
|
|
3499
4498
|
const expectedContainerEnv = {
|
|
3500
4499
|
PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
@@ -3544,11 +4543,11 @@ function containsUnsafeDockerArgument(value) {
|
|
|
3544
4543
|
function requireCanonicalRealDirectory(value, name) {
|
|
3545
4544
|
if (typeof value !== "string") throw new Error(`worker: ${name} must be a path`);
|
|
3546
4545
|
try {
|
|
3547
|
-
const entry =
|
|
4546
|
+
const entry = lstatSync5(value);
|
|
3548
4547
|
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
3549
4548
|
throw new Error("unsafe entry");
|
|
3550
4549
|
}
|
|
3551
|
-
const canonical =
|
|
4550
|
+
const canonical = realpathSync7(value);
|
|
3552
4551
|
if (canonical !== value) throw new Error("non-canonical entry");
|
|
3553
4552
|
return canonical;
|
|
3554
4553
|
} catch {
|
|
@@ -3559,7 +4558,7 @@ function resolveCanonicalDirectory(value, name) {
|
|
|
3559
4558
|
try {
|
|
3560
4559
|
const entry = statSync3(value);
|
|
3561
4560
|
if (!entry.isDirectory()) throw new Error("not a directory");
|
|
3562
|
-
return
|
|
4561
|
+
return realpathSync7(value);
|
|
3563
4562
|
} catch {
|
|
3564
4563
|
throw new Error(`worker: ${name} is unavailable or unsafe`);
|
|
3565
4564
|
}
|
|
@@ -3592,16 +4591,16 @@ function assertMinimalWorkerScratch(value) {
|
|
|
3592
4591
|
}
|
|
3593
4592
|
}
|
|
3594
4593
|
function requireCanonicalWindowsDirectory(value, name) {
|
|
3595
|
-
if (!value || !
|
|
4594
|
+
if (!value || !isAbsolute6(value) || !existsSync12(value) || !statSync3(value).isDirectory()) {
|
|
3596
4595
|
throw new Error(`minimal worker bootstrap requires a valid ${name} directory`);
|
|
3597
4596
|
}
|
|
3598
|
-
return
|
|
4597
|
+
return realpathSync7(value);
|
|
3599
4598
|
}
|
|
3600
4599
|
function requireCanonicalWindowsFile(value, name) {
|
|
3601
|
-
if (!value || !
|
|
4600
|
+
if (!value || !isAbsolute6(value) || !existsSync12(value) || !statSync3(value).isFile()) {
|
|
3602
4601
|
throw new Error(`minimal worker bootstrap requires a valid ${name} executable`);
|
|
3603
4602
|
}
|
|
3604
|
-
return
|
|
4603
|
+
return realpathSync7(value);
|
|
3605
4604
|
}
|
|
3606
4605
|
function objectRecord(value, label) {
|
|
3607
4606
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -3636,13 +4635,13 @@ function isString(value) {
|
|
|
3636
4635
|
return typeof value === "string";
|
|
3637
4636
|
}
|
|
3638
4637
|
function requireAbsolutePath(value, name) {
|
|
3639
|
-
if (typeof value !== "string" || !
|
|
4638
|
+
if (typeof value !== "string" || !isAbsolute6(value) || value.includes("\0")) {
|
|
3640
4639
|
throw new Error(`worker: ${name} must be an absolute path`);
|
|
3641
4640
|
}
|
|
3642
4641
|
}
|
|
3643
4642
|
function lexicallyWithin(root, candidate) {
|
|
3644
4643
|
const rel = relative5(root, candidate);
|
|
3645
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep5}`) && !
|
|
4644
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep5}`) && !isAbsolute6(rel);
|
|
3646
4645
|
}
|
|
3647
4646
|
function assertConversationTarget(value) {
|
|
3648
4647
|
const target = objectRecord(value, "conversation target");
|
|
@@ -3687,18 +4686,18 @@ function askUserTool(ask) {
|
|
|
3687
4686
|
context.toolCallId,
|
|
3688
4687
|
args
|
|
3689
4688
|
);
|
|
3690
|
-
const
|
|
3691
|
-
return receipt ? { content: [{ type: "text", text }], questionReceipt: receipt } :
|
|
4689
|
+
const text2 = JSON.stringify(result);
|
|
4690
|
+
return receipt ? { content: [{ type: "text", text: text2 }], questionReceipt: receipt } : text2;
|
|
3692
4691
|
}
|
|
3693
4692
|
};
|
|
3694
4693
|
}
|
|
3695
4694
|
|
|
3696
4695
|
// ../daemon/src/runtime/pi/tools.ts
|
|
3697
|
-
import { Type as
|
|
4696
|
+
import { Type as Type4 } from "typebox";
|
|
3698
4697
|
|
|
3699
4698
|
// ../daemon/src/runtime/tools/bootstrap.ts
|
|
3700
4699
|
import { existsSync as existsSync13, rmSync as rmSync12 } from "fs";
|
|
3701
|
-
import { join as
|
|
4700
|
+
import { join as join20 } from "path";
|
|
3702
4701
|
function bootstrapTool(agentDir) {
|
|
3703
4702
|
return {
|
|
3704
4703
|
def: {
|
|
@@ -3707,7 +4706,7 @@ function bootstrapTool(agentDir) {
|
|
|
3707
4706
|
parameters: { type: "object", properties: {} }
|
|
3708
4707
|
},
|
|
3709
4708
|
async invoke() {
|
|
3710
|
-
const path =
|
|
4709
|
+
const path = join20(agentDir, "BOOTSTRAP.md");
|
|
3711
4710
|
if (existsSync13(path)) {
|
|
3712
4711
|
rmSync12(path);
|
|
3713
4712
|
return "BOOTSTRAP.md removed. Bootstrap is complete.";
|
|
@@ -3848,15 +4847,15 @@ function browserTools(host, agentId) {
|
|
|
3848
4847
|
|
|
3849
4848
|
// ../daemon/src/runtime/tools/deliver-file.ts
|
|
3850
4849
|
import {
|
|
3851
|
-
closeSync as
|
|
3852
|
-
constants as
|
|
3853
|
-
fstatSync as
|
|
3854
|
-
openSync as
|
|
3855
|
-
readSync as
|
|
3856
|
-
realpathSync as
|
|
4850
|
+
closeSync as closeSync6,
|
|
4851
|
+
constants as constants7,
|
|
4852
|
+
fstatSync as fstatSync5,
|
|
4853
|
+
openSync as openSync6,
|
|
4854
|
+
readSync as readSync3,
|
|
4855
|
+
realpathSync as realpathSync8,
|
|
3857
4856
|
statSync as statSync4
|
|
3858
4857
|
} from "fs";
|
|
3859
|
-
import { basename as basename3, extname, isAbsolute as
|
|
4858
|
+
import { basename as basename3, extname, isAbsolute as isAbsolute7, relative as relative6, resolve as resolve5, sep as sep6 } from "path";
|
|
3860
4859
|
var MAX_DELIVER_BYTES = 25 * 1024 * 1024;
|
|
3861
4860
|
var MIME = {
|
|
3862
4861
|
".pdf": "application/pdf",
|
|
@@ -3874,13 +4873,13 @@ var MIME = {
|
|
|
3874
4873
|
};
|
|
3875
4874
|
function isWithin2(root, candidate) {
|
|
3876
4875
|
const rel = relative6(root, candidate);
|
|
3877
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep6}`) && !
|
|
4876
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep6}`) && !isAbsolute7(rel);
|
|
3878
4877
|
}
|
|
3879
4878
|
function deliverFileTool(cwd, sink, sessionId = "") {
|
|
3880
4879
|
const workspacePath = resolve5(cwd);
|
|
3881
4880
|
let workspaceRealPath = null;
|
|
3882
4881
|
try {
|
|
3883
|
-
workspaceRealPath =
|
|
4882
|
+
workspaceRealPath = realpathSync8(workspacePath);
|
|
3884
4883
|
} catch {
|
|
3885
4884
|
}
|
|
3886
4885
|
return {
|
|
@@ -3902,7 +4901,7 @@ function deliverFileTool(cwd, sink, sessionId = "") {
|
|
|
3902
4901
|
async invoke(args, context) {
|
|
3903
4902
|
const p = String(args.path ?? "");
|
|
3904
4903
|
if (!p) throw new Error("deliver_file: path is required");
|
|
3905
|
-
if (
|
|
4904
|
+
if (isAbsolute7(p)) {
|
|
3906
4905
|
throw new Error(`deliver_file: path must stay within the workspace: ${p}`);
|
|
3907
4906
|
}
|
|
3908
4907
|
const abs = resolve5(workspacePath, p);
|
|
@@ -3915,7 +4914,7 @@ function deliverFileTool(cwd, sink, sessionId = "") {
|
|
|
3915
4914
|
let real;
|
|
3916
4915
|
let expected;
|
|
3917
4916
|
try {
|
|
3918
|
-
real =
|
|
4917
|
+
real = realpathSync8(abs);
|
|
3919
4918
|
expected = statSync4(real);
|
|
3920
4919
|
} catch {
|
|
3921
4920
|
throw new Error(`deliver_file: no such file: ${p}`);
|
|
@@ -3925,13 +4924,13 @@ function deliverFileTool(cwd, sink, sessionId = "") {
|
|
|
3925
4924
|
}
|
|
3926
4925
|
let fd;
|
|
3927
4926
|
try {
|
|
3928
|
-
fd =
|
|
4927
|
+
fd = openSync6(real, constants7.O_RDONLY | constants7.O_NOFOLLOW | constants7.O_NONBLOCK);
|
|
3929
4928
|
} catch {
|
|
3930
4929
|
throw new Error(`deliver_file: no such file: ${p}`);
|
|
3931
4930
|
}
|
|
3932
4931
|
let data;
|
|
3933
4932
|
try {
|
|
3934
|
-
const stat2 =
|
|
4933
|
+
const stat2 = fstatSync5(fd);
|
|
3935
4934
|
if (!stat2.isFile()) {
|
|
3936
4935
|
throw new Error(`deliver_file: not a regular file: ${p}`);
|
|
3937
4936
|
}
|
|
@@ -3940,7 +4939,7 @@ function deliverFileTool(cwd, sink, sessionId = "") {
|
|
|
3940
4939
|
}
|
|
3941
4940
|
let confirmedReal;
|
|
3942
4941
|
try {
|
|
3943
|
-
confirmedReal =
|
|
4942
|
+
confirmedReal = realpathSync8(abs);
|
|
3944
4943
|
} catch {
|
|
3945
4944
|
throw new Error(`deliver_file: path changed during validation: ${p}`);
|
|
3946
4945
|
}
|
|
@@ -3955,14 +4954,14 @@ function deliverFileTool(cwd, sink, sessionId = "") {
|
|
|
3955
4954
|
const buffer = Buffer.alloc(MAX_DELIVER_BYTES + 1);
|
|
3956
4955
|
let length = 0;
|
|
3957
4956
|
while (length < buffer.length) {
|
|
3958
|
-
const
|
|
3959
|
-
if (
|
|
3960
|
-
length +=
|
|
4957
|
+
const count2 = readSync3(fd, buffer, length, buffer.length - length, null);
|
|
4958
|
+
if (count2 === 0) break;
|
|
4959
|
+
length += count2;
|
|
3961
4960
|
}
|
|
3962
4961
|
if (length > MAX_DELIVER_BYTES) throw new Error("deliver_file: file exceeds 25 MiB");
|
|
3963
4962
|
data = buffer.subarray(0, length).toString("base64");
|
|
3964
4963
|
} finally {
|
|
3965
|
-
|
|
4964
|
+
closeSync6(fd);
|
|
3966
4965
|
}
|
|
3967
4966
|
const name = basename3(abs);
|
|
3968
4967
|
const mimeType = MIME[extname(abs).toLowerCase()] ?? "application/octet-stream";
|
|
@@ -3987,15 +4986,15 @@ function deliverFileTool(cwd, sink, sessionId = "") {
|
|
|
3987
4986
|
|
|
3988
4987
|
// ../daemon/src/runtime/tools/home.ts
|
|
3989
4988
|
import {
|
|
3990
|
-
closeSync as
|
|
3991
|
-
constants as
|
|
3992
|
-
fstatSync as
|
|
3993
|
-
lstatSync as
|
|
3994
|
-
openSync as
|
|
4989
|
+
closeSync as closeSync7,
|
|
4990
|
+
constants as constants8,
|
|
4991
|
+
fstatSync as fstatSync6,
|
|
4992
|
+
lstatSync as lstatSync6,
|
|
4993
|
+
openSync as openSync7,
|
|
3995
4994
|
readdirSync as readdirSync7,
|
|
3996
4995
|
writeFileSync as writeFileSync8
|
|
3997
4996
|
} from "fs";
|
|
3998
|
-
import { join as
|
|
4997
|
+
import { join as join21 } from "path";
|
|
3999
4998
|
var HOME_FILES_READABLE = [
|
|
4000
4999
|
"IDENTITY.md",
|
|
4001
5000
|
"SOUL.md",
|
|
@@ -4060,18 +5059,18 @@ function homeTools(agentDir) {
|
|
|
4060
5059
|
let fd;
|
|
4061
5060
|
try {
|
|
4062
5061
|
rejectExistingSymlinkOrNonFile(path);
|
|
4063
|
-
fd =
|
|
5062
|
+
fd = openSync7(
|
|
4064
5063
|
path,
|
|
4065
|
-
|
|
5064
|
+
constants8.O_WRONLY | constants8.O_CREAT | constants8.O_TRUNC | noFollowFlag(),
|
|
4066
5065
|
384
|
|
4067
5066
|
);
|
|
4068
|
-
if (!
|
|
5067
|
+
if (!fstatSync6(fd).isFile()) throw new Error("target is not a regular file");
|
|
4069
5068
|
writeFileSync8(fd, content, "utf8");
|
|
4070
5069
|
} catch (err) {
|
|
4071
5070
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4072
5071
|
throw new Error(`home_write: could not write ${file}: ${msg}`);
|
|
4073
5072
|
} finally {
|
|
4074
|
-
if (fd !== void 0)
|
|
5073
|
+
if (fd !== void 0) closeSync7(fd);
|
|
4075
5074
|
}
|
|
4076
5075
|
return `wrote ${file} (${Buffer.byteLength(content, "utf8")} bytes)`;
|
|
4077
5076
|
}
|
|
@@ -4087,7 +5086,7 @@ function homeTools(agentDir) {
|
|
|
4087
5086
|
for (const file of HOME_FILES_READABLE) {
|
|
4088
5087
|
const path = privateHomeFile(homeRoot, file);
|
|
4089
5088
|
try {
|
|
4090
|
-
const s =
|
|
5089
|
+
const s = lstatSync6(path);
|
|
4091
5090
|
if (s.isSymbolicLink() || !s.isFile()) continue;
|
|
4092
5091
|
entries.push(`${file} (${s.size}b)`);
|
|
4093
5092
|
} catch {
|
|
@@ -4116,10 +5115,10 @@ function resolvePrivateHomeRoot(agentDir) {
|
|
|
4116
5115
|
}
|
|
4117
5116
|
}
|
|
4118
5117
|
function privateHomeFile(homeRoot, file) {
|
|
4119
|
-
return
|
|
5118
|
+
return join21(homeRoot, file);
|
|
4120
5119
|
}
|
|
4121
5120
|
function rejectSymlinkOrNonFile(path) {
|
|
4122
|
-
const entry =
|
|
5121
|
+
const entry = lstatSync6(path);
|
|
4123
5122
|
if (entry.isSymbolicLink()) throw new Error("symbolic links are not allowed");
|
|
4124
5123
|
if (!entry.isFile()) throw new Error("target is not a regular file");
|
|
4125
5124
|
}
|
|
@@ -4253,9 +5252,9 @@ function messagingTools(host, fromAgentId) {
|
|
|
4253
5252
|
},
|
|
4254
5253
|
async invoke(args) {
|
|
4255
5254
|
const to = String(args.to ?? "");
|
|
4256
|
-
const
|
|
5255
|
+
const text2 = String(args.text ?? "");
|
|
4257
5256
|
if (!to) throw new Error('send_message: "to" is required');
|
|
4258
|
-
if (!
|
|
5257
|
+
if (!text2) throw new Error('send_message: "text" is required');
|
|
4259
5258
|
if (!await host.agentExists(to)) {
|
|
4260
5259
|
throw new Error(`send_message: agent not found: ${to}`);
|
|
4261
5260
|
}
|
|
@@ -4263,7 +5262,7 @@ function messagingTools(host, fromAgentId) {
|
|
|
4263
5262
|
const { messageId } = await host.sendMessage({
|
|
4264
5263
|
from: fromAgentId,
|
|
4265
5264
|
to,
|
|
4266
|
-
payload: JSON.stringify({ text }),
|
|
5265
|
+
payload: JSON.stringify({ text: text2 }),
|
|
4267
5266
|
replyTo
|
|
4268
5267
|
});
|
|
4269
5268
|
return `sent message ${messageId}`;
|
|
@@ -4323,7 +5322,7 @@ function messagingTools(host, fromAgentId) {
|
|
|
4323
5322
|
{
|
|
4324
5323
|
def: {
|
|
4325
5324
|
name: "wait_for_reply",
|
|
4326
|
-
description: "
|
|
5325
|
+
description: "Wait for a reply only when the peer does not need your current workspace. For shared-workspace preparation, send_message and end this turn; the inbox reply resumes work after cleanup releases ownership.",
|
|
4327
5326
|
parameters: {
|
|
4328
5327
|
type: "object",
|
|
4329
5328
|
properties: {
|
|
@@ -4439,25 +5438,25 @@ function normalizeWhitespace(value) {
|
|
|
4439
5438
|
function htmlToMarkdown(html) {
|
|
4440
5439
|
const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
4441
5440
|
const title = titleMatch ? normalizeWhitespace(stripTags(titleMatch[1] ?? "")) : void 0;
|
|
4442
|
-
let
|
|
4443
|
-
|
|
5441
|
+
let text2 = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<noscript[\s\S]*?<\/noscript>/gi, "").replace(/<nav[\s\S]*?<\/nav>/gi, "").replace(/<header[\s\S]*?<\/header>/gi, "").replace(/<footer[\s\S]*?<\/footer>/gi, "");
|
|
5442
|
+
text2 = text2.replace(/<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, body) => {
|
|
4444
5443
|
const label = normalizeWhitespace(stripTags(body));
|
|
4445
5444
|
return label ? `[${label}](${href})` : href;
|
|
4446
5445
|
});
|
|
4447
|
-
|
|
5446
|
+
text2 = text2.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, level, body) => {
|
|
4448
5447
|
const n = Math.max(1, Math.min(6, Number.parseInt(level, 10)));
|
|
4449
5448
|
return `
|
|
4450
5449
|
${"#".repeat(n)} ${normalizeWhitespace(stripTags(body))}
|
|
4451
5450
|
`;
|
|
4452
5451
|
});
|
|
4453
|
-
|
|
5452
|
+
text2 = text2.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, body) => {
|
|
4454
5453
|
const label = normalizeWhitespace(stripTags(body));
|
|
4455
5454
|
return label ? `
|
|
4456
5455
|
- ${label}` : "";
|
|
4457
5456
|
});
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
return { text: normalizeWhitespace(
|
|
5457
|
+
text2 = text2.replace(/<(br|hr)\s*\/?>/gi, "\n").replace(/<\/(p|div|section|article|tr|ul|ol|table|blockquote)>/gi, "\n");
|
|
5458
|
+
text2 = stripTags(text2);
|
|
5459
|
+
return { text: normalizeWhitespace(text2), title };
|
|
4461
5460
|
}
|
|
4462
5461
|
function markdownToPlain(md) {
|
|
4463
5462
|
let t = md;
|
|
@@ -4491,8 +5490,8 @@ function extractReadable(html, url, mode) {
|
|
|
4491
5490
|
if (!parsed?.content) return fallback();
|
|
4492
5491
|
const title = parsed.title || void 0;
|
|
4493
5492
|
if (mode === "text") {
|
|
4494
|
-
const
|
|
4495
|
-
return
|
|
5493
|
+
const text2 = normalizeWhitespace(parsed.textContent ?? "");
|
|
5494
|
+
return text2 ? { text: text2, title } : fallback();
|
|
4496
5495
|
}
|
|
4497
5496
|
const rendered = htmlToMarkdown(parsed.content);
|
|
4498
5497
|
return { text: rendered.text, title: title ?? rendered.title };
|
|
@@ -4803,9 +5802,9 @@ async function readBodyCapped(res, maxBytes) {
|
|
|
4803
5802
|
const decoder = new TextDecoder(charset, { fatal: false });
|
|
4804
5803
|
const reader = res.body?.getReader();
|
|
4805
5804
|
if (!reader) {
|
|
4806
|
-
const
|
|
4807
|
-
if (
|
|
4808
|
-
return { text, truncated: false };
|
|
5805
|
+
const text2 = await res.text();
|
|
5806
|
+
if (text2.length > maxBytes) return { text: text2.slice(0, maxBytes), truncated: true };
|
|
5807
|
+
return { text: text2, truncated: false };
|
|
4809
5808
|
}
|
|
4810
5809
|
const chunks = [];
|
|
4811
5810
|
let total = 0;
|
|
@@ -4862,8 +5861,8 @@ async function firecrawlScrape(url, mode, env, fetchFn, timeoutMs) {
|
|
|
4862
5861
|
if (!body.success || !body.data?.markdown) return null;
|
|
4863
5862
|
const md = body.data.markdown;
|
|
4864
5863
|
const title = body.data.metadata?.title;
|
|
4865
|
-
const
|
|
4866
|
-
return title ? { text, title } : { text };
|
|
5864
|
+
const text2 = mode === "text" ? markdownToPlain(md) : md;
|
|
5865
|
+
return title ? { text: text2, title } : { text: text2 };
|
|
4867
5866
|
} catch {
|
|
4868
5867
|
return null;
|
|
4869
5868
|
} finally {
|
|
@@ -5059,7 +6058,7 @@ function ourToolToPiTool(h) {
|
|
|
5059
6058
|
name: h.def.name,
|
|
5060
6059
|
label: h.def.name,
|
|
5061
6060
|
description: h.def.description,
|
|
5062
|
-
parameters:
|
|
6061
|
+
parameters: Type4.Unsafe(h.def.parameters),
|
|
5063
6062
|
async execute(toolCallId, params) {
|
|
5064
6063
|
const out = await h.invoke(params, { toolCallId });
|
|
5065
6064
|
return {
|
|
@@ -5156,8 +6155,10 @@ async function createBazilionSession(opts) {
|
|
|
5156
6155
|
const cwd = agent.team.path;
|
|
5157
6156
|
if (!existsSync14(cwd)) mkdirSync7(cwd, { recursive: true });
|
|
5158
6157
|
const promptSkills = restricted ? [] : loadPromptSkills(paths.skillsDir, agent.skills);
|
|
5159
|
-
const uploadsDir =
|
|
6158
|
+
const uploadsDir = join22(agent.agent.dir, "uploads");
|
|
5160
6159
|
const shellTools = restricted ? null : createSessionShellTools(cwd, env, {
|
|
6160
|
+
preparedDocker: opts.preparedDocker,
|
|
6161
|
+
dockerLifecycle: opts.dockerLifecycle,
|
|
5161
6162
|
...existsSync14(uploadsDir) ? { inputsDir: uploadsDir } : {},
|
|
5162
6163
|
skillMounts: promptSkills.map((skill) => ({
|
|
5163
6164
|
source: skill.hostDir,
|
|
@@ -5165,7 +6166,7 @@ async function createBazilionSession(opts) {
|
|
|
5165
6166
|
})),
|
|
5166
6167
|
approvalHost: bashApprovalHost
|
|
5167
6168
|
});
|
|
5168
|
-
const sessionDir = restricted?.sessionDir ??
|
|
6169
|
+
const sessionDir = restricted?.sessionDir ?? join22(paths.agentDir(agent.agent.id), "sessions");
|
|
5169
6170
|
mkdirSync7(sessionDir, { recursive: true });
|
|
5170
6171
|
const existing = restricted ? null : explicitConversationFile(sessionDir, opts.conversation);
|
|
5171
6172
|
const sessionManager = existing ? SessionManager.open(existing, sessionDir, cwd) : SessionManager.create(cwd, sessionDir);
|
|
@@ -5182,9 +6183,14 @@ async function createBazilionSession(opts) {
|
|
|
5182
6183
|
skills: promptSkills,
|
|
5183
6184
|
sandboxMode: shellTools?.config.sandboxMode ?? "off"
|
|
5184
6185
|
});
|
|
6186
|
+
const repository = !restricted && opts.repositoryContext && opts.repositoryContextHost ? repositoryContextIntegration(
|
|
6187
|
+
opts.repositoryContext,
|
|
6188
|
+
opts.repositoryContextHost,
|
|
6189
|
+
shellTools?.config.sandboxMode === "docker" ? "/workspace" : cwd
|
|
6190
|
+
) : void 0;
|
|
5185
6191
|
const resourceLoader = new DefaultResourceLoader({
|
|
5186
6192
|
cwd,
|
|
5187
|
-
agentDir:
|
|
6193
|
+
agentDir: join22(paths.home, "pi"),
|
|
5188
6194
|
settingsManager,
|
|
5189
6195
|
noExtensions: true,
|
|
5190
6196
|
noSkills: true,
|
|
@@ -5193,6 +6199,7 @@ async function createBazilionSession(opts) {
|
|
|
5193
6199
|
noContextFiles: true,
|
|
5194
6200
|
appendSystemPrompt: bazilionPrompt ? [bazilionPrompt] : void 0
|
|
5195
6201
|
});
|
|
6202
|
+
if (repository) resourceLoader.getAgentsFiles = repository.files;
|
|
5196
6203
|
await resourceLoader.reload();
|
|
5197
6204
|
const bazilionTools = restricted ? restricted.tools : createBazilionCustomTools({
|
|
5198
6205
|
agent,
|
|
@@ -5207,11 +6214,28 @@ async function createBazilionSession(opts) {
|
|
|
5207
6214
|
sessionId: sessionManager.getSessionId(),
|
|
5208
6215
|
env
|
|
5209
6216
|
});
|
|
6217
|
+
if (repository) bazilionTools.push(repository.tool);
|
|
6218
|
+
if (repository && opts.codingHost && shellTools && shellTools.config.sandboxMode === "docker" && !opts.preparedDocker)
|
|
6219
|
+
throw new Error("Coding commands require the admitted Docker runtime");
|
|
6220
|
+
const redactionSecrets = uniqueSecrets(codingSecrets(env));
|
|
6221
|
+
if (repository && opts.codingHost && shellTools)
|
|
6222
|
+
bazilionTools.push(
|
|
6223
|
+
...codingTools({
|
|
6224
|
+
host: opts.codingHost,
|
|
6225
|
+
root: cwd,
|
|
6226
|
+
context: repository.resolve,
|
|
6227
|
+
docker: opts.preparedDocker,
|
|
6228
|
+
lifecycle: opts.dockerLifecycle,
|
|
6229
|
+
approval: shellTools.config.approvalMode === "dangerous",
|
|
6230
|
+
approvalHost: bashApprovalHost,
|
|
6231
|
+
secrets: () => redactionSecrets
|
|
6232
|
+
})
|
|
6233
|
+
);
|
|
5210
6234
|
const customTools = shellTools?.customBash ? [...bazilionTools, shellTools.customBash] : bazilionTools;
|
|
5211
6235
|
const allowedTools = [...shellTools?.hostToolNames ?? [], ...customTools.map((t) => t.name)];
|
|
5212
6236
|
const { session } = await createAgentSession({
|
|
5213
6237
|
cwd,
|
|
5214
|
-
agentDir:
|
|
6238
|
+
agentDir: join22(paths.home, "pi"),
|
|
5215
6239
|
model,
|
|
5216
6240
|
thinkingLevel: toPiThinkingLevel(restricted?.reasoningLevel ?? agent.reasoningLevel),
|
|
5217
6241
|
tools: allowedTools,
|
|
@@ -5221,11 +6245,14 @@ async function createBazilionSession(opts) {
|
|
|
5221
6245
|
modelRuntime,
|
|
5222
6246
|
resourceLoader
|
|
5223
6247
|
});
|
|
6248
|
+
repository?.bind(session, allowedTools);
|
|
5224
6249
|
if (refreshApiKey) {
|
|
5225
6250
|
session.agent.getApiKey = async (requestedProvider) => {
|
|
5226
6251
|
if (requestedProvider !== piProviderName(providerName)) return void 0;
|
|
5227
6252
|
try {
|
|
5228
|
-
|
|
6253
|
+
const token = await refreshApiKey(providerName);
|
|
6254
|
+
if (token && !redactionSecrets.includes(token)) redactionSecrets.push(token);
|
|
6255
|
+
return token;
|
|
5229
6256
|
} catch {
|
|
5230
6257
|
return void 0;
|
|
5231
6258
|
}
|
|
@@ -5265,8 +6292,10 @@ async function createProtectedBazilionSession(opts) {
|
|
|
5265
6292
|
const shellTools = createProtectedSessionShellTools(
|
|
5266
6293
|
opts.paths.teamDir,
|
|
5267
6294
|
opts.docker,
|
|
5268
|
-
opts.bashApprovalHost
|
|
6295
|
+
opts.bashApprovalHost,
|
|
6296
|
+
opts.dockerLifecycle
|
|
5269
6297
|
);
|
|
6298
|
+
const modelCwd = codingContainerCwd(opts.docker.coding?.cwd ?? ".");
|
|
5270
6299
|
mkdirSync7(opts.paths.sessionDir, { recursive: true });
|
|
5271
6300
|
const existing = explicitConversationFile(opts.paths.sessionDir, opts.conversation);
|
|
5272
6301
|
const sessionManager = existing ? SessionManager.open(existing, opts.paths.sessionDir, opts.paths.teamDir) : SessionManager.create(opts.paths.teamDir, opts.paths.sessionDir);
|
|
@@ -5279,8 +6308,13 @@ async function createProtectedBazilionSession(opts) {
|
|
|
5279
6308
|
}),
|
|
5280
6309
|
protectedRuntimeSecrets(opts.runtime)
|
|
5281
6310
|
);
|
|
6311
|
+
const repository = opts.repositoryContext && opts.repositoryContextHost ? repositoryContextIntegration(
|
|
6312
|
+
redactJsonValue(opts.repositoryContext, protectedRuntimeSecrets(opts.runtime)),
|
|
6313
|
+
opts.repositoryContextHost,
|
|
6314
|
+
PROTECTED_MODEL_CWD
|
|
6315
|
+
) : void 0;
|
|
5282
6316
|
const resourceLoader = new DefaultResourceLoader({
|
|
5283
|
-
cwd:
|
|
6317
|
+
cwd: modelCwd,
|
|
5284
6318
|
agentDir: opts.scratch.piAgentDir,
|
|
5285
6319
|
settingsManager,
|
|
5286
6320
|
noExtensions: true,
|
|
@@ -5291,6 +6325,7 @@ async function createProtectedBazilionSession(opts) {
|
|
|
5291
6325
|
systemPrompt: PROTECTED_BASE_SYSTEM_PROMPT,
|
|
5292
6326
|
appendSystemPrompt: bazilionPrompt ? [bazilionPrompt] : void 0
|
|
5293
6327
|
});
|
|
6328
|
+
if (repository) resourceLoader.getAgentsFiles = repository.files;
|
|
5294
6329
|
await resourceLoader.reload();
|
|
5295
6330
|
const bazilionTools = createProtectedBazilionCustomTools({
|
|
5296
6331
|
agent: opts.agent,
|
|
@@ -5301,10 +6336,25 @@ async function createProtectedBazilionSession(opts) {
|
|
|
5301
6336
|
askUser: opts.askUser,
|
|
5302
6337
|
sessionId: sessionManager.getSessionId()
|
|
5303
6338
|
});
|
|
6339
|
+
if (repository) bazilionTools.push(repository.tool);
|
|
6340
|
+
const redactionSecrets = uniqueSecrets(protectedRuntimeSecrets(opts.runtime));
|
|
6341
|
+
if (repository && opts.codingHost)
|
|
6342
|
+
bazilionTools.push(
|
|
6343
|
+
...codingTools({
|
|
6344
|
+
host: opts.codingHost,
|
|
6345
|
+
root: opts.paths.teamDir,
|
|
6346
|
+
context: repository.resolve,
|
|
6347
|
+
docker: opts.docker,
|
|
6348
|
+
lifecycle: opts.dockerLifecycle,
|
|
6349
|
+
approval: true,
|
|
6350
|
+
approvalHost: opts.bashApprovalHost,
|
|
6351
|
+
secrets: () => redactionSecrets
|
|
6352
|
+
})
|
|
6353
|
+
);
|
|
5304
6354
|
if (!shellTools.customBash) throw new Error("protected Docker bash tool is unavailable");
|
|
5305
6355
|
const customTools = [...bazilionTools, shellTools.customBash];
|
|
5306
6356
|
const { session } = await createAgentSession({
|
|
5307
|
-
cwd:
|
|
6357
|
+
cwd: modelCwd,
|
|
5308
6358
|
agentDir: opts.scratch.piAgentDir,
|
|
5309
6359
|
model,
|
|
5310
6360
|
thinkingLevel: toPiThinkingLevel(opts.runtime.reasoningLevel),
|
|
@@ -5315,10 +6365,14 @@ async function createProtectedBazilionSession(opts) {
|
|
|
5315
6365
|
modelRuntime,
|
|
5316
6366
|
resourceLoader
|
|
5317
6367
|
});
|
|
6368
|
+
repository?.bind(
|
|
6369
|
+
session,
|
|
6370
|
+
customTools.map((tool) => tool.name)
|
|
6371
|
+
);
|
|
5318
6372
|
installProtectedCredentialBoundary(
|
|
5319
6373
|
session,
|
|
5320
6374
|
opts.runtime.providerName,
|
|
5321
|
-
|
|
6375
|
+
redactionSecrets,
|
|
5322
6376
|
opts.refreshApiKey
|
|
5323
6377
|
);
|
|
5324
6378
|
return sessionHandle(session);
|
|
@@ -5369,15 +6423,18 @@ async function createRestrictedReviewSession(opts) {
|
|
|
5369
6423
|
installProtectedCredentialBoundary(
|
|
5370
6424
|
session,
|
|
5371
6425
|
opts.runtime.providerName,
|
|
5372
|
-
protectedRuntimeSecrets(opts.runtime),
|
|
6426
|
+
uniqueSecrets(protectedRuntimeSecrets(opts.runtime)),
|
|
5373
6427
|
opts.refreshApiKey
|
|
5374
6428
|
);
|
|
5375
6429
|
return sessionHandle(session);
|
|
5376
6430
|
}
|
|
6431
|
+
function uniqueSecrets(values) {
|
|
6432
|
+
return [...new Set(values.filter(Boolean))];
|
|
6433
|
+
}
|
|
5377
6434
|
function materializeProtectedCredentialFile(runtime, scratch) {
|
|
5378
6435
|
const fields = [...runtime.credentialEnv ?? []];
|
|
5379
6436
|
if (runtime.credentialFile) {
|
|
5380
|
-
const path =
|
|
6437
|
+
const path = join22(scratch.tempDir, "provider-credential.json");
|
|
5381
6438
|
writeFileSync9(path, runtime.credentialFile.content, {
|
|
5382
6439
|
encoding: "utf8",
|
|
5383
6440
|
mode: 384,
|
|
@@ -5398,8 +6455,7 @@ function createInMemorySettingsManager() {
|
|
|
5398
6455
|
}
|
|
5399
6456
|
});
|
|
5400
6457
|
}
|
|
5401
|
-
function installProtectedCredentialBoundary(session, providerName,
|
|
5402
|
-
const activeTokens = [...new Set(initialSecrets.filter(Boolean))];
|
|
6458
|
+
function installProtectedCredentialBoundary(session, providerName, activeTokens, refreshApiKey) {
|
|
5403
6459
|
session.agent.state.messages = redactJsonValue(session.agent.state.messages, activeTokens);
|
|
5404
6460
|
const originalStream = session.agent.streamFunction;
|
|
5405
6461
|
session.agent.streamFunction = async (model, context, options) => {
|
|
@@ -5515,7 +6571,7 @@ function toPiThinkingLevel(level) {
|
|
|
5515
6571
|
function explicitConversationFile(directory, target) {
|
|
5516
6572
|
if (!target || !/^[0-9a-f-]{36}$/i.test(target.id) || target.filename !== `${target.id}.jsonl`)
|
|
5517
6573
|
throw new Error("Explicit conversation target is required");
|
|
5518
|
-
const file =
|
|
6574
|
+
const file = join22(directory, target.filename);
|
|
5519
6575
|
readCanonicalSessionFile(directory, target.filename, target.id);
|
|
5520
6576
|
return file;
|
|
5521
6577
|
}
|
|
@@ -5636,6 +6692,21 @@ function createIpcMessagingHost(call) {
|
|
|
5636
6692
|
approvalStatus: (agentId, approvalId) => call("approvalStatus", { agentId, approvalId })
|
|
5637
6693
|
};
|
|
5638
6694
|
}
|
|
6695
|
+
function createIpcDockerLifecycle(namespace, call) {
|
|
6696
|
+
if (!namespace) return void 0;
|
|
6697
|
+
return {
|
|
6698
|
+
name: (kind) => `bazilion-${namespace}-${kind}-${randomUUID20()}`,
|
|
6699
|
+
beforeCreate: async (identity2) => {
|
|
6700
|
+
await call("containerBeforeCreate", { containerName: identity2.containerName });
|
|
6701
|
+
},
|
|
6702
|
+
afterCreate: async (containerName) => {
|
|
6703
|
+
await call("containerAfterCreate", { containerName });
|
|
6704
|
+
},
|
|
6705
|
+
afterRemove: async (containerName) => {
|
|
6706
|
+
await call("containerAfterRemove", { containerName });
|
|
6707
|
+
}
|
|
6708
|
+
};
|
|
6709
|
+
}
|
|
5639
6710
|
function createIpcUserMdHost(call) {
|
|
5640
6711
|
return {
|
|
5641
6712
|
get: (teamId) => call("userMdGet", { teamId }),
|
|
@@ -5681,13 +6752,13 @@ function createReviewState(evidenceItems) {
|
|
|
5681
6752
|
name: "propose_lesson",
|
|
5682
6753
|
label: "propose_lesson",
|
|
5683
6754
|
description: "Submit zero to five evidence-backed lesson proposals in one final batch. This is the only available action.",
|
|
5684
|
-
parameters:
|
|
5685
|
-
proposals:
|
|
5686
|
-
|
|
5687
|
-
scope:
|
|
5688
|
-
text:
|
|
5689
|
-
evidenceEntryIds:
|
|
5690
|
-
|
|
6755
|
+
parameters: Type5.Object({
|
|
6756
|
+
proposals: Type5.Array(
|
|
6757
|
+
Type5.Object({
|
|
6758
|
+
scope: Type5.Union([Type5.Literal("private"), Type5.Literal("shared")]),
|
|
6759
|
+
text: Type5.String({ minLength: 1, maxLength: 500 }),
|
|
6760
|
+
evidenceEntryIds: Type5.Array(
|
|
6761
|
+
Type5.Object({ sessionId: Type5.String(), entryOrdinal: Type5.Integer({ minimum: 0 }) }),
|
|
5691
6762
|
{ minItems: 1 }
|
|
5692
6763
|
)
|
|
5693
6764
|
}),
|
|
@@ -5699,14 +6770,14 @@ function createReviewState(evidenceItems) {
|
|
|
5699
6770
|
const input = params;
|
|
5700
6771
|
const validated = [];
|
|
5701
6772
|
for (const candidate of input.proposals) {
|
|
5702
|
-
const
|
|
5703
|
-
if (!
|
|
6773
|
+
const text2 = candidate.text.trim();
|
|
6774
|
+
if (!text2 || text2.length > 500) throw new Error("proposal text must be 1-500 characters");
|
|
5704
6775
|
for (const evidence of candidate.evidenceEntryIds) {
|
|
5705
6776
|
if (!allowedEvidence.has(`${evidence.sessionId}:${evidence.entryOrdinal}`)) {
|
|
5706
6777
|
throw new Error("proposal cites evidence outside the supplied digest");
|
|
5707
6778
|
}
|
|
5708
6779
|
}
|
|
5709
|
-
validated.push({ ...candidate, text });
|
|
6780
|
+
validated.push({ ...candidate, text: text2 });
|
|
5710
6781
|
}
|
|
5711
6782
|
proposalBatchSubmitted = true;
|
|
5712
6783
|
proposals.push(...validated);
|
|
@@ -5729,6 +6800,8 @@ function createReviewState(evidenceItems) {
|
|
|
5729
6800
|
};
|
|
5730
6801
|
}
|
|
5731
6802
|
async function createSessionForInput(input, ipcCall) {
|
|
6803
|
+
if (input.kind !== "restricted_review" && !input.repositoryContext)
|
|
6804
|
+
throw new Error("Coding turn requires daemon-prepared repository context");
|
|
5732
6805
|
if (input.kind === "configured_operator_http") {
|
|
5733
6806
|
const paths = resolvePaths();
|
|
5734
6807
|
const memory2 = qmdBackend(`${input.agent.team.path}/memory`);
|
|
@@ -5750,7 +6823,12 @@ async function createSessionForInput(input, ipcCall) {
|
|
|
5750
6823
|
}) : void 0;
|
|
5751
6824
|
const handle2 = await createBazilionSession({
|
|
5752
6825
|
agent: input.agent,
|
|
6826
|
+
preparedDocker: input.configuredDocker?.docker,
|
|
6827
|
+
dockerLifecycle: createIpcDockerLifecycle(input.containerNamespace, ipcCall),
|
|
5753
6828
|
conversation: input.conversation,
|
|
6829
|
+
repositoryContext: input.repositoryContext,
|
|
6830
|
+
repositoryContextHost: (target) => ipcCall("repositoryContext", { target }),
|
|
6831
|
+
codingHost: { invoke: (request) => ipcCall("coding", request) },
|
|
5754
6832
|
paths,
|
|
5755
6833
|
env: process.env,
|
|
5756
6834
|
memory: memory2,
|
|
@@ -5804,10 +6882,14 @@ async function createSessionForInput(input, ipcCall) {
|
|
|
5804
6882
|
const handle = await createProtectedBazilionSession({
|
|
5805
6883
|
agent: input.agent,
|
|
5806
6884
|
conversation: input.conversation,
|
|
6885
|
+
repositoryContext: input.repositoryContext,
|
|
6886
|
+
repositoryContextHost: (target) => ipcCall("repositoryContext", { target }),
|
|
6887
|
+
codingHost: { invoke: (request) => ipcCall("coding", request) },
|
|
5807
6888
|
runtime: input.runtime,
|
|
5808
6889
|
paths: input.paths,
|
|
5809
6890
|
scratch: input.scratch,
|
|
5810
6891
|
docker: input.docker,
|
|
6892
|
+
dockerLifecycle: createIpcDockerLifecycle(input.containerNamespace, ipcCall),
|
|
5811
6893
|
memory,
|
|
5812
6894
|
messagingHost,
|
|
5813
6895
|
userMdHost,
|