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.
Files changed (126) hide show
  1. package/dist/cli.js +1138 -687
  2. package/dist/cli.js.map +1 -1
  3. package/dist/daemon.js +8401 -4401
  4. package/dist/daemon.js.map +1 -1
  5. package/dist/migrations/0001_init.sql +109 -0
  6. package/dist/web/client/assets/ChatPane-BI3QVX2A.js +76 -0
  7. package/dist/web/client/assets/{ConfirmDialog-CGZzom9E.js → ConfirmDialog-Qam6Ut08.js} +1 -1
  8. package/dist/web/client/assets/{FieldRow-DYNU9gWb.js → FieldRow-swJ5qn2h.js} +1 -1
  9. package/dist/web/client/assets/ProductionTeamPolicyEditor-LGw1UuF-.js +7 -0
  10. package/dist/web/client/assets/ProfileCommunicationEditor-raoau_YB.js +1 -0
  11. package/dist/web/client/assets/{ResultSourceConversation-CGhKTSTD.js → ResultSourceConversation-BEwkhFog.js} +1 -1
  12. package/dist/web/client/assets/TeamTabs-BDdTLGIf.js +1 -0
  13. package/dist/web/client/assets/TemplatesTabs-BX5hhEJ7.js +1 -0
  14. package/dist/web/client/assets/{UnsavedChangesGuard-CFQcKsrf.js → UnsavedChangesGuard-CVJtmR1D.js} +1 -1
  15. package/dist/web/client/assets/{_id-D_eKokkd.js → _id-BQPOE3gj.js} +1 -1
  16. package/dist/web/client/assets/{_id-9nCIhbVd.js → _id-BkYrUP0R.js} +1 -1
  17. package/dist/web/client/assets/_id-CriixlZD.js +1 -0
  18. package/dist/web/client/assets/_id-DHqq2W34.js +1 -0
  19. package/dist/web/client/assets/{_id-BSu3AqKA.js → _id-DmEVZIlx.js} +1 -1
  20. package/dist/web/client/assets/activity-Bzr8R18n.js +1 -0
  21. package/dist/web/client/assets/{agents-NY3VrZgB.js → agents-13oafeOA.js} +1 -1
  22. package/dist/web/client/assets/{agents-CCmiZek6.js → agents-DF4NALmJ.js} +1 -1
  23. package/dist/web/client/assets/{approvals-CPDAPgrM.js → approvals-DsG0Ijls.js} +1 -1
  24. package/dist/web/client/assets/{attention-Qra_9_OI.js → attention-DG7K25mz.js} +1 -1
  25. package/dist/web/client/assets/chevron-right-DFqFHtLy.js +1 -0
  26. package/dist/web/client/assets/config-DAaDbFhS.js +3 -0
  27. package/dist/web/client/assets/createServerFn-Dzfjt7mA.js +3 -0
  28. package/dist/web/client/assets/git-review-presentation-CzHQulDk.js +1 -0
  29. package/dist/web/client/assets/{inbox-C5W-sm1q.js → inbox-C9e7QjsX.js} +1 -1
  30. package/dist/web/client/assets/index-Dsstm2hf.js +50 -0
  31. package/dist/web/client/assets/{learning-Dr-7_7J4.js → learning-23arTd21.js} +1 -1
  32. package/dist/web/client/assets/login-BIXnBsuj.js +1 -0
  33. package/dist/web/client/assets/{mcp-BueuwQiv.js → mcp-Cdx9qAl8.js} +1 -1
  34. package/dist/web/client/assets/{members-ZikuYNcy.js → members-fotOz4rR.js} +1 -1
  35. package/dist/web/client/assets/{memory-1JUJlPrn.js → memory-C7UznVot.js} +1 -1
  36. package/dist/web/client/assets/policy-CactLW1X.js +1 -0
  37. package/dist/web/client/assets/{results-mMr8Ar2u.js → results-DqEuYnQH.js} +1 -1
  38. package/dist/web/client/assets/review-CnBSYclb.js +1 -0
  39. package/dist/web/client/assets/routes-COxMuwVz.js +1 -0
  40. package/dist/web/client/assets/{services-kfQA9cpJ.js → services-xEANwZAu.js} +1 -1
  41. package/dist/web/client/assets/{skills-C6pcgxaC.js → skills-CamV6wYM.js} +1 -1
  42. package/dist/web/client/assets/styles-C6br2xxl.css +2 -0
  43. package/dist/web/client/assets/{teams-LB0KBXY9.js → teams-B12dMSjW.js} +1 -1
  44. package/dist/web/client/assets/{teams-C6_Noz5X.js → teams-CbH43zSG.js} +1 -1
  45. package/dist/web/client/assets/{telegram-A2jaI1QM.js → telegram-muIql4u-.js} +1 -1
  46. package/dist/web/client/assets/{tokens-BGlUpJcL.js → tokens-DGFmApdF.js} +1 -1
  47. package/dist/web/client/assets/{triggers-C54_feGg.js → triggers-D8n3sCBW.js} +1 -1
  48. package/dist/web/client/assets/welcome-CMHWRZhs.js +1 -0
  49. package/dist/web/client/assets/x-BGCHwZOH.js +1 -0
  50. package/dist/web/server/assets/{ChatPane-DJ30FdFG.js → ChatPane-DybH39WK.js} +524 -15
  51. package/dist/web/server/assets/{ResultSourceConversation-zYogeOWp.js → ResultSourceConversation-BmXTcEOA.js} +1 -1
  52. package/dist/web/server/assets/{TeamTabs-TCKjCvMA.js → TeamTabs-DQXh-3ij.js} +4 -0
  53. package/dist/web/server/assets/{_id-1LwoHhJg.js → _id-BN1BPEDI.js} +1 -1
  54. package/dist/web/server/assets/{_id-DFHB9JFQ.js → _id-BYKpT2Tu.js} +1 -1
  55. package/dist/web/server/assets/{_id-B_-CDHOi2.js → _id-BqWTB8Wl.js} +1 -1
  56. package/dist/web/server/assets/{_id-BvSWsPI5.js → _id-CPfILjMa.js} +1 -1
  57. package/dist/web/server/assets/{_id-1hTvnKLN2.js → _id-C_2er0_G.js} +3 -3
  58. package/dist/web/server/assets/{_id-B2EEyStS.js → _id-DDyA9CGX.js} +1 -1
  59. package/dist/web/server/assets/{_id-DxFLSIio.js → _id-DSPFJMzf.js} +1 -1
  60. package/dist/web/server/assets/_id-DtJn-yzu.js +779 -0
  61. package/dist/web/server/assets/{_id-DDNVQx3y.js → _id-Dw3gKOGv.js} +1 -1
  62. package/dist/web/server/assets/{_id-T_8btpbB.js → _id-XkxlX036.js} +1 -1
  63. package/dist/web/server/assets/{_tanstack-start-manifest_v-DP4ZUBw3.js → _tanstack-start-manifest_v-vOXRfKJD.js} +97 -84
  64. package/dist/web/server/assets/{activity-C88qrNqv.js → activity-BX1jZBPQ.js} +1 -1
  65. package/dist/web/server/assets/{activity-5fWKPDxj.js → activity-bozPMU2v.js} +2 -2
  66. package/dist/web/server/assets/{activity-D73w4mkZ.js → activity-pYQPT3QT.js} +1 -1
  67. package/dist/web/server/assets/{agents-DRSo7lRX.js → agents-BKBfW700.js} +1 -1
  68. package/dist/web/server/assets/{agents-DB5hCSAI.js → agents-CVCfxnE2.js} +1 -1
  69. package/dist/web/server/assets/{approvals-4y9bZF4r.js → approvals-C3ya6B6U.js} +1 -1
  70. package/dist/web/server/assets/{attention-BFx8ZkmH.js → attention-l28hGCHS.js} +1 -1
  71. package/dist/web/server/assets/{auth-_eaCwhyq.js → auth-AvDFNeOv.js} +1 -1
  72. package/dist/web/server/assets/{config-B7JIfv_7.js → config-BWKn4XII.js} +1 -1
  73. package/dist/web/server/assets/{daemon-client-B06W1Qa2.js → daemon-client-Cgx-YW4l.js} +35 -0
  74. package/dist/web/server/assets/git-review-BB3M9nYg.js +26 -0
  75. package/dist/web/server/assets/git-review-f_uta4Mx.js +179 -0
  76. package/dist/web/server/assets/git-review-presentation-TJiHJhen.js +81 -0
  77. package/dist/web/server/assets/{inbox-YRpDQcvp.js → inbox-BBZiOOQs.js} +1 -1
  78. package/dist/web/server/assets/{learning-UBZb6h6M.js → learning-CheVay1i.js} +1 -1
  79. package/dist/web/server/assets/{mcp-Dv6RXMaS.js → mcp-DCBDmNtQ.js} +1 -1
  80. package/dist/web/server/assets/{members-BFTKSlOe.js → members-C2He8mrU.js} +1 -1
  81. package/dist/web/server/assets/{members-CD3GiTfF.js → members-C5FQruRZ.js} +1 -1
  82. package/dist/web/server/assets/{members-cc5SeEaW.js → members-C_2xB9pr.js} +2 -2
  83. package/dist/web/server/assets/{memory-CVqnyUaM.js → memory-4tI-UbOf.js} +1 -1
  84. package/dist/web/server/assets/{memory-C5i_GxlY.js → memory-BTcmf0sD.js} +2 -2
  85. package/dist/web/server/assets/{memory-D0PQgZAn.js → memory-rmiFNj7T.js} +1 -1
  86. package/dist/web/server/assets/{policy-Dgjb0_gK.js → policy-BGILKGHz.js} +2 -2
  87. package/dist/web/server/assets/{policy-bLKwvnja.js → policy-C3D2Q1EA.js} +1 -1
  88. package/dist/web/server/assets/{policy-C-p8HIhA.js → policy-DS7LGe--.js} +1 -1
  89. package/dist/web/server/assets/{results-Cfev4KNt.js → results-C6H42tjq.js} +1 -1
  90. package/dist/web/server/assets/{results-Cj-Dlw3C.js → results-Cgx7SrFt.js} +3 -3
  91. package/dist/web/server/assets/{results-CwEQ5EUL.js → results-_6K_zXQS.js} +1 -1
  92. package/dist/web/server/assets/review-DHhiL-JQ.js +393 -0
  93. package/dist/web/server/assets/review-DSTMCwP4.js +14 -0
  94. package/dist/web/server/assets/{router-d3sTMpMX.js → router-ElARvZzI.js} +34 -27
  95. package/dist/web/server/assets/{routes-B765DVhj.js → routes-B6x3pEW8.js} +1 -1
  96. package/dist/web/server/assets/{routes-cW4MWU18.js → routes-BgMvLvS-.js} +1 -1
  97. package/dist/web/server/assets/{routes-MIZz6uMr.js → routes-DsUzzxk4.js} +2 -2
  98. package/dist/web/server/assets/{services-BCbdn3R9.js → services-yHZroxsi.js} +1 -1
  99. package/dist/web/server/assets/{skills-Bpoa8MIp.js → skills--bIKID1Q.js} +1 -1
  100. package/dist/web/server/assets/{teams-hWGjbpUA.js → teams--a956vWL.js} +1 -1
  101. package/dist/web/server/assets/{teams-CXzwqzcE.js → teams-BbRfuVXY.js} +1 -1
  102. package/dist/web/server/assets/{telegram-DhpvtGUc.js → telegram-4kMxRtVZ.js} +1 -1
  103. package/dist/web/server/assets/{tokens-Bjp19Vkn.js → tokens-BYwTN9JZ.js} +1 -1
  104. package/dist/web/server/assets/{triggers-DJlcvXzn.js → triggers-BM6mzLuH.js} +1 -1
  105. package/dist/web/server/server.js +56 -28
  106. package/dist/worker.js +2214 -1132
  107. package/dist/worker.js.map +1 -1
  108. package/package.json +3 -3
  109. package/dist/web/client/assets/ChatPane-55j13bBj.js +0 -74
  110. package/dist/web/client/assets/ProductionTeamPolicyEditor-DT5Sl8jd.js +0 -7
  111. package/dist/web/client/assets/ProfileCommunicationEditor-TyCU5vXX.js +0 -1
  112. package/dist/web/client/assets/TeamTabs-BZHyRnbd.js +0 -1
  113. package/dist/web/client/assets/TemplatesTabs-BHzkPMRC.js +0 -1
  114. package/dist/web/client/assets/_id-CGd21j3k.js +0 -1
  115. package/dist/web/client/assets/_id-SRMFlIgP.js +0 -1
  116. package/dist/web/client/assets/activity-Cn2-9PpZ.js +0 -1
  117. package/dist/web/client/assets/chevron-right-DXE4CWf2.js +0 -1
  118. package/dist/web/client/assets/config-D8EO0LPB.js +0 -3
  119. package/dist/web/client/assets/index-BUhhk_u9.js +0 -52
  120. package/dist/web/client/assets/login-Bu5UxbJh.js +0 -1
  121. package/dist/web/client/assets/policy-DbkhsFNA.js +0 -1
  122. package/dist/web/client/assets/routes-DJYB-MS9.js +0 -1
  123. package/dist/web/client/assets/styles-CcNf8jT_.css +0 -2
  124. package/dist/web/client/assets/welcome-DyUIswRi.js +0 -1
  125. package/dist/web/client/assets/x-BY2MPLr9.js +0 -1
  126. 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 Type3 } from "typebox";
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 text = extractAssistantText(m);
1132
- if (text) out.push({ type: "assistant_message", text });
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 text = extractToolResultText(e.result);
1211
+ const text2 = extractToolResultText(e.result);
1161
1212
  if (e.isError) {
1162
- return [{ type: "tool_error", id: e.toolCallId, name: e.toolName, error: text }];
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: text,
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 text = extractAssistantText(am);
1299
+ const text2 = extractAssistantText(am);
1249
1300
  const toolCalls = extractAssistantToolCalls(am);
1250
- const msg = { role: "assistant", content: text };
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 count = 0;
1306
- while (count < buffer.length) {
1307
- const n = readSync(fd, buffer, count, buffer.length - count, null);
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
- count += n;
1480
+ count2 += n;
1310
1481
  }
1311
- if (count !== stat2.size) throw new Error("Result source session changed while reading");
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, count).toString("utf8").split("\n")) {
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/session.ts
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
- createAssistantMessageEventStream
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/runtime/providers/pi-runtime.ts
1342
- import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
1343
- import { ModelRuntime } from "@earendil-works/pi-coding-agent";
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 piProviderName(providerName) {
1461
- return PROVIDER_ALIASES[providerName] ?? providerName;
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 providerApiKey(providerName, env) {
1464
- for (const name of PROVIDER_API_KEY_ENV[providerName] ?? []) {
1465
- const value = env[name];
1466
- if (value) return value;
1467
- }
1468
- return LOCAL_PROVIDERS[providerName]?.apiKey;
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
- function providerBaseUrl(providerName, env) {
1471
- switch (providerName) {
1472
- case "lmstudio":
1473
- return env.LMSTUDIO_URL ?? LOCAL_PROVIDERS.lmstudio?.baseUrl;
1474
- case "ollama":
1475
- return env.OLLAMA_URL ?? LOCAL_PROVIDERS.ollama?.baseUrl;
1476
- case "llamacpp":
1477
- return env.LLAMACPP_URL ?? LOCAL_PROVIDERS.llamacpp?.baseUrl;
1478
- case "vercel-ai-gateway":
1479
- return env.AI_GATEWAY_BASE_URL;
1480
- default:
1481
- return void 0;
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 providerCredentialEnv(providerName, env) {
1485
- const result = {};
1486
- for (const name of PROVIDER_CREDENTIAL_ENV[providerName] ?? []) {
1487
- const value = env[name];
1488
- if (value !== void 0) result[name] = value;
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
- return Object.keys(result).length > 0 ? result : void 0;
1491
- }
1492
- function fallbackApi(providerName) {
1493
- switch (providerName) {
1494
- case "anthropic":
1495
- return "anthropic-messages";
1496
- case "google":
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
- function fallbackPiModel(providerName, modelId, baseUrl) {
1511
- return {
1512
- id: modelId,
1513
- name: modelId,
1514
- api: fallbackApi(providerName),
1515
- provider: piProviderName(providerName),
1516
- baseUrl: baseUrl ?? "",
1517
- reasoning: false,
1518
- input: ["text"],
1519
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1520
- contextWindow: 32768,
1521
- maxTokens: 4096
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
- const runtime = await ModelRuntime.create({ credentials, modelsPath: null });
1550
- const local = LOCAL_PROVIDERS[providerName];
1551
- const baseUrl = options.baseUrl ?? providerBaseUrl(providerName, options.env);
1552
- if (local) {
1553
- const modelId = options.modelId ?? "model-name";
1554
- const model = fallbackPiModel(providerName, modelId, baseUrl ?? local.baseUrl);
1555
- runtime.registerProvider(canonicalName, {
1556
- baseUrl: baseUrl ?? local.baseUrl,
1557
- api: "openai-completions",
1558
- authHeader: local.authHeader,
1559
- apiKey: apiKey ?? local.apiKey,
1560
- models: [model]
1561
- });
1562
- } else if (baseUrl) {
1563
- runtime.registerProvider(canonicalName, { baseUrl });
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
- return runtime;
1566
- }
1567
- async function createProtectedPiRuntime(options) {
1568
- const env = {};
1569
- for (const field of options.credentialEnv ?? []) env[field.name] = field.value;
1570
- return createBazilionPiRuntime({
1571
- providerName: options.providerName,
1572
- modelId: options.modelId,
1573
- env,
1574
- apiKey: options.apiKey,
1575
- baseUrl: options.baseUrl
1576
- });
1577
- }
1578
- function resolvePiModel(runtime, providerName, modelId, baseUrl) {
1579
- const known = runtime.getModel(piProviderName(providerName), modelId);
1580
- if (known) return baseUrl ? { ...known, baseUrl } : known;
1581
- return fallbackPiModel(providerName, modelId, baseUrl);
1582
- }
1583
-
1584
- // ../daemon/src/runtime/auth/openai-codex.ts
1585
- import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
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/providers/pi-adapter.ts
1588
- import {
1589
- Type
1590
- } from "@earendil-works/pi-ai";
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/session/prompt.ts
1593
- import { existsSync as existsSync10, readdirSync as readdirSync6 } from "fs";
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/safe-files.ts
1597
- import {
1598
- closeSync as closeSync3,
1599
- constants as constants3,
1600
- fstatSync as fstatSync3,
1601
- lstatSync as lstatSync2,
1602
- mkdirSync as mkdirSync5,
1603
- openSync as openSync3,
1604
- readFileSync as readFileSync9,
1605
- realpathSync as realpathSync3
1606
- } from "fs";
1607
- import { dirname as dirname3, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve4 } from "path";
1608
- function resolveRealDirectory(path) {
1609
- const requested = resolve4(path);
1610
- const entry = lstatSync2(requested);
1611
- if (entry.isSymbolicLink() || !entry.isDirectory()) {
1612
- throw new Error("expected a real directory");
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
- return realpathSync3(requested);
1766
+ };
1767
+ function isValidEnvName(name) {
1768
+ return ENV_NAME_RE.test(name) && !RESERVED_OBJECT_KEYS.has(name);
1615
1769
  }
1616
- function readRegularFileNoFollow(path) {
1617
- const entry = lstatSync2(path);
1618
- if (entry.isSymbolicLink()) throw new Error("symbolic links are not allowed");
1619
- if (!entry.isFile()) throw new Error("target is not a regular file");
1620
- let fd;
1621
- try {
1622
- fd = openSync3(path, constants3.O_RDONLY | noFollowFlag());
1623
- if (!fstatSync3(fd).isFile()) throw new Error("target is not a regular file");
1624
- return readFileSync9(fd, "utf8");
1625
- } finally {
1626
- if (fd !== void 0) closeSync3(fd);
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 noFollowFlag() {
1630
- return typeof constants3.O_NOFOLLOW === "number" ? constants3.O_NOFOLLOW : 0;
1803
+ function shellTokens(fragment) {
1804
+ return Array.from(
1805
+ fragment.matchAll(/"(?:\\.|[^"\\])*"|'[^']*'|[^\s]+/g),
1806
+ (match) => match[0].replace(/^(?:"|')|(?:"|')$/g, "")
1807
+ );
1631
1808
  }
1632
-
1633
- // ../daemon/src/runtime/session/prompt.ts
1634
- var SANDBOX_SKILLS_DIR = "/skills";
1635
- function loadPromptSkills(skillsDir, attachedNames) {
1636
- if (!existsSync10(skillsDir) || attachedNames.length === 0) return [];
1637
- const attached = new Set(attachedNames);
1638
- const discovered = readdirSync6(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && attached.has(entry.name)).sort((a, b) => a.name.localeCompare(b.name));
1639
- const loaded = [];
1640
- for (const entry of discovered) {
1641
- const hostDir = join14(skillsDir, entry.name);
1642
- try {
1643
- const parsed = parseSkillContent(readRegularFileNoFollow(join14(hostDir, "SKILL.md")));
1644
- const safeLabel = entry.name.replace(/[^A-Za-z0-9._-]/g, "_") || "skill";
1645
- loaded.push({
1646
- name: entry.name,
1647
- description: parsed.frontmatter.description,
1648
- body: parsed.body.trim(),
1649
- hostDir,
1650
- sandboxDir: `${SANDBOX_SKILLS_DIR}/${loaded.length}-${safeLabel}`
1651
- });
1652
- } catch {
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
- var CONTEXT_FILE_ORDER = ["AGENTS.md", "SOUL.md", "TOOLS.md", "IDENTITY.md"];
1658
- function loadProtectedHomeDocuments(agentDir) {
1659
- const root = resolveRealDirectory(agentDir);
1660
- const read = (file) => {
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 readRegularFileNoFollow(join14(root, file));
1663
- } catch (error) {
1664
- if (error.code === "ENOENT") return null;
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 buildSystemPrompt(agent, options = {}) {
1677
- const parts = [];
1678
- const homeDocuments = options.homeDocuments ?? loadProtectedHomeDocuments(agent.agent.dir);
1679
- const contextBlocks = [];
1680
- for (const file of CONTEXT_FILE_ORDER) {
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 bootstrap = homeDocuments["BOOTSTRAP.md"]?.trimEnd();
1693
- if (bootstrap) {
1694
- parts.push(
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
- parts.push(
1712
- [
1713
- "# Agent Home",
1714
- "",
1715
- "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.",
1716
- "",
1717
- "- To change who you are (name, vibe, personality, how you behave): use `home_write`.",
1718
- "- To inspect exact wording of your own files: use `home_read` or `home_list`.",
1719
- "- To remember facts the user told you or things you learned: use `memory_write` \u2014 NOT `home_write`.",
1720
- "- 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."
1721
- ].join("\n")
1722
- );
1723
- if (agent.skills.length > 0) {
1724
- const loaded = options.skills ?? [];
1725
- const loadedNames = new Set(loaded.map((skill) => skill.name));
1726
- const unavailable = agent.skills.filter((name) => !loadedNames.has(name));
1727
- const skillBlocks = loaded.map((skill) => {
1728
- const runtimeDir = options.sandboxMode === "docker" ? skill.sandboxDir : skill.hostDir;
1729
- return [
1730
- `## ${skill.name}`,
1731
- "",
1732
- skill.description,
1733
- "",
1734
- `Runtime directory: \`${runtimeDir}\`. Resolve any relative scripts or assets mentioned by this skill from that directory.`,
1735
- "",
1736
- skill.body
1737
- ].filter((line, index, lines) => line !== "" || lines[index - 1] !== "").join("\n").trimEnd();
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
- const teamLines = [
1752
- "# Team",
1753
- "",
1754
- `- ${agent.team.id} (${agent.team.name}): ${options.sandboxMode === "docker" ? "/workspace" : agent.team.path}`,
1755
- "",
1756
- "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`."
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
- parts.push(teamLines.join("\n"));
1759
- if (agent.team.userMd.trim()) {
1760
- parts.push(
1761
- `# About the User
1762
-
1763
- 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.**
1764
-
1765
- ${agent.team.userMd.trim()}`
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
- parts.push(
1785
- "# 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.**"
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
- return parts.join("\n\n---\n\n");
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 boundReviewedLessons(lessons, maxCharacters = 8e3) {
1790
- const selected = [];
1791
- let used = 0;
1792
- for (const raw of lessons) {
1793
- const lesson = raw.trim();
1794
- if (!lesson) continue;
1795
- const remaining = maxCharacters - used;
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
- return selected;
1801
- }
1802
-
1803
- // ../daemon/src/runtime/shell/tooling.ts
1804
- import { existsSync as existsSync11, lstatSync as lstatSync3, realpathSync as realpathSync4 } from "fs";
1805
- import { dirname as dirname4, isAbsolute as isAbsolute4, join as join16, relative as relative4, sep as sep4 } from "path";
1806
- import {
1807
- createBashToolDefinition,
1808
- createLocalBashOperations,
1809
- defineTool as defineTool2
1810
- } from "@earendil-works/pi-coding-agent";
1811
-
1812
- // ../daemon/src/runtime/shell/approval.ts
1813
- import { defineTool } from "@earendil-works/pi-coding-agent";
1814
-
1815
- // ../daemon/src/runtime/shell/security.ts
1816
- var DEFAULT_BASH_SANDBOX_IMAGE = "debian:bookworm-slim";
1817
- var SAFE_SHELL_ENV_KEYS = [
1818
- "PATH",
1819
- "HOME",
1820
- "LANG",
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 shellTokens(fragment) {
1891
- return Array.from(
1892
- fragment.matchAll(/"(?:\\.|[^"\\])*"|'[^']*'|[^\s]+/g),
1893
- (match) => match[0].replace(/^(?:"|')|(?:"|')$/g, "")
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 isBroadFilesystemTarget(rawTarget) {
1897
- const target = rawTarget.replace(/[;,]+$/, "");
1898
- if (target.startsWith("/")) return true;
1899
- return /^(?:\.|\.\/\*+|\*+|~(?:\/.*)?|\$(?:HOME|\{HOME\})(?:\/.*)?|\.\.(?:\/.*)?)$/.test(target);
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 classifyBroadFilesystemOperations(command, risks, seen) {
1902
- const commandRe = /(?:^|[;&|(\n])\s*(?:command\s+)?(rm|find|chmod|chown)\b[^;&|)\n]*/gim;
1903
- for (const match of command.matchAll(commandRe)) {
1904
- if (match.index === void 0 || !match[1]) continue;
1905
- const nameOffset = match[0].toLowerCase().indexOf(match[1].toLowerCase());
1906
- const start = match.index + nameOffset;
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
- function isLocalHostname(rawHostname) {
1936
- const hostname = rawHostname.trim().replace(/^.*@/, "").replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
1937
- return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || hostname === "0.0.0.0" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
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 hasExternalHttpDestination(segment) {
1940
- const urls = Array.from(segment.matchAll(/https?:\/\/[^\s'"`]+/gi), (match) => match[0]);
1941
- if (urls.length === 0) return true;
1942
- return urls.some((rawUrl) => {
1943
- try {
1944
- return !isLocalHostname(new URL(rawUrl).hostname);
1945
- } catch {
1946
- return true;
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
- function remoteTransferHosts(segment) {
1951
- const hosts = [];
1952
- const uriRe = /(?:sftp|ssh|rsync):\/\/(?:[^@\s/]+@)?(\[[^\]]+\]|[^\s/:]+)(?::\d+)?/gi;
1953
- for (const match of segment.matchAll(uriRe)) {
1954
- if (match[1]) hosts.push(match[1]);
1955
- }
1956
- const scpRe = /(?:^|\s)(?:[^@\s:]+@)?(\[[^\]]+\]|[A-Za-z0-9._-]+):[^\s]+/g;
1957
- for (const match of segment.matchAll(scpRe)) {
1958
- if (match[1]) hosts.push(match[1]);
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 hosts;
2125
+ return { revision: value.revision, cwd, env };
1961
2126
  }
1962
- function invocationSegment(command, match, commandName) {
1963
- if (match.index === void 0) return null;
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
- CONTAINER_WORKDIR,
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
- return executeDockerRun(spec, { onData, signal, timeout, timeoutMs });
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(identity) {
2444
- if (typeof identity !== "object" || identity === null) {
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
- identity.device,
2449
- identity.inode,
2450
- identity.mode,
2451
- identity.size,
2452
- identity.modifiedTimeNs,
2453
- identity.changedTimeNs
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
- return executeDockerRun(spec, { onData, signal, timeout, timeoutMs });
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(join15(tmpdir2(), "bazilion-docker-env-"));
2582
- const path = join15(dir, "environment");
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
- let id;
2710
- let volumes;
2711
- try {
2712
- id = JSON.parse(raw.slice(0, separator));
2713
- volumes = JSON.parse(raw.slice(separator + 1));
2714
- } catch {
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
- if (typeof id !== "string" || !id.startsWith("sha256:")) {
2718
- throw new Error(`Docker sandbox received an invalid image id for "${image}"`);
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
- if (volumes !== null && typeof volumes === "object" && Object.keys(volumes).length > 0) {
2721
- throw new Error(
2722
- `Docker sandbox image "${image}" declares writable volumes (${Object.keys(volumes).sort().join(", ")}); use an image without VOLUME declarations`
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
- if (volumes !== null && (typeof volumes !== "object" || Array.isArray(volumes))) {
2726
- throw new Error(`Docker sandbox received invalid volume metadata for image "${image}"`);
2727
- }
2728
- return id;
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 runDockerInspection(dockerPath, args, processEnv, subject) {
2731
- return new Promise((resolve6, reject) => {
2732
- const child = spawn(dockerPath, args, {
2733
- stdio: ["ignore", "pipe", "pipe"],
2734
- windowsHide: true,
2735
- env: processEnv
2736
- });
2737
- let settled = false;
2738
- let stdout = "";
2739
- let stderr = "";
2740
- const finish = (error) => {
2741
- if (settled) return;
2742
- settled = true;
2743
- clearTimeout(timeoutHandle);
2744
- if (error) {
2745
- reject(error);
2746
- return;
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
- resolve6(stdout.trim());
2749
- };
2750
- const timeoutHandle = setTimeout(() => {
2751
- child.kill("SIGKILL");
2752
- finish(
2753
- new Error(
2754
- `Docker sandbox could not inspect ${subject} within ${DOCKER_CONTEXT_TIMEOUT_MS}ms`
2755
- )
2756
- );
2757
- }, DOCKER_CONTEXT_TIMEOUT_MS);
2758
- child.stdout.on("data", (data) => {
2759
- stdout = `${stdout}${data.toString("utf8")}`.slice(-4096);
2760
- });
2761
- child.stderr.on("data", (data) => {
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 buildDockerClientEnv(hostEnv) {
3619
+ async function createProtectedPiRuntime(options) {
2777
3620
  const env = {};
2778
- for (const key of DOCKER_CLIENT_ENV_KEYS) {
2779
- const value = hostEnv[key];
2780
- if (value !== void 0) env[key] = value;
2781
- }
2782
- return env;
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 validateDockerPath(dockerPath) {
2785
- if (!dockerPath.trim()) throw new Error("Docker sandbox executable path is required");
2786
- if (dockerPath.includes("\0"))
2787
- throw new Error("Docker sandbox executable path contains a NUL byte");
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
- function validateImage(image) {
2790
- if (!image.trim()) throw new Error("Docker sandbox image is required");
2791
- if (image.startsWith("-")) {
2792
- throw new Error(`Docker sandbox image cannot start with "-": ${JSON.stringify(image)}`);
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
- if (image.includes("\0")) throw new Error("Docker sandbox image contains a NUL byte");
3667
+ return realpathSync5(requested);
2795
3668
  }
2796
- function validateContainerName(containerName) {
2797
- if (!CONTAINER_NAME.test(containerName)) {
2798
- throw new Error(`Docker sandbox container name is invalid: ${JSON.stringify(containerName)}`);
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 validateMountArgument(mountSource) {
2802
- if (!mountSource) throw new Error("Docker sandbox mount path is required");
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
- function validateReadOnlyMounts(mounts, workspaceSource) {
2810
- const targets = /* @__PURE__ */ new Set([CONTAINER_WORKDIR, "/tmp"]);
2811
- for (const mount of mounts) {
2812
- validateMountArgument(mount.source);
2813
- if (mount.sourceRoot !== void 0 && (!isAbsolute3(mount.sourceRoot) || mount.sourceRoot.includes("\0") || mount.sourceRoot.includes(","))) {
2814
- throw new Error(
2815
- `Docker sandbox read-only mount root is invalid: ${JSON.stringify(mount.sourceRoot)}`
2816
- );
2817
- }
2818
- if (!mount.target.startsWith("/") || mount.target.includes("\0") || mount.target.includes(",") || mount.target.split("/").includes("..")) {
2819
- throw new Error(
2820
- `Docker sandbox read-only mount target is invalid: ${JSON.stringify(mount.target)}`
2821
- );
2822
- }
2823
- if (mount.source === workspaceSource) {
2824
- throw new Error("Docker sandbox cannot mount the writable workspace again as an input");
2825
- }
2826
- if (targets.has(mount.target)) {
2827
- throw new Error(`Docker sandbox mount target is duplicated or reserved: ${mount.target}`);
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
- function isWithin(root, candidate) {
2833
- const rel = relative3(root, candidate);
2834
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute3(rel);
2835
- }
2836
- function validateHostIdentity(uid, gid) {
2837
- if (!Number.isSafeInteger(uid) || uid < 0 || !Number.isSafeInteger(gid) || gid < 0) {
2838
- throw new Error(`Docker sandbox received an invalid host uid/gid: ${uid}:${gid}`);
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 formatSpawnError(dockerPath, error) {
2842
- const code = error.code;
2843
- if (code === "ENOENT") {
2844
- return new Error(`Docker sandbox unavailable: executable "${dockerPath}" was not found`);
2845
- }
2846
- if (code === "EACCES") {
2847
- return new Error(`Docker sandbox unavailable: executable "${dockerPath}" is not executable`);
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
- return new Error(`Docker sandbox could not start "${dockerPath}": ${error.message}`);
2850
- }
2851
- function formatDockerRunError(spec, stderr) {
2852
- const detail3 = stderr.trim() || "Docker exited with status 125 without an error message";
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
- if (/mounts denied|invalid mount config|bind source path does not exist|mount.*failed/i.test(detail3)) {
2857
- return new Error(`Docker sandbox mount failed for "${spec.mountSource}": ${detail3}`);
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
- if (/cannot connect to the docker daemon|is the docker daemon running/i.test(detail3)) {
2860
- return new Error(`Docker sandbox unavailable: ${detail3}`);
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
- return new Error(`Docker sandbox failed to start container "${spec.containerName}": ${detail3}`);
2863
- }
2864
- async function removeContainer(dockerPath, containerName, processEnv) {
2865
- const args = ["container", "rm", "--force", containerName];
2866
- let last;
2867
- for (let attempt = 0; attempt < CLEANUP_ATTEMPTS; attempt += 1) {
2868
- last = await runCleanupCommand(dockerPath, args, processEnv);
2869
- if (last.ok) return;
2870
- if (attempt < CLEANUP_ATTEMPTS - 1) {
2871
- await new Promise((resolve6) => setTimeout(resolve6, CLEANUP_RETRY_DELAY_MS));
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
- if (last && !last.missing) {
2875
- console.warn(
2876
- `Docker sandbox could not confirm cleanup of container "${containerName}": ${last.error || "cleanup command failed"}`
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 runCleanupCommand(dockerPath, args, processEnv) {
2881
- return new Promise((resolve6) => {
2882
- let done = false;
2883
- let stderr = "";
2884
- const child = spawn(dockerPath, args, {
2885
- stdio: ["ignore", "ignore", "pipe"],
2886
- windowsHide: true,
2887
- env: processEnv
2888
- });
2889
- const finish = (ok, error = "") => {
2890
- if (done) return;
2891
- done = true;
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: createPreparedDockerBashOperations(runtime)
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: createLocalBashOperations()
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(join16(cwd, "memory")) ? [safeReadOnlyMount(join16(cwd, "memory"), SANDBOX_MEMORY_DIR, cwd)] : [],
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 = lstatSync3(source);
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 = realpathSync4(expectedRoot);
2987
- const canonicalSource = realpathSync4(source);
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}`) && !isAbsolute4(rel);
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 constants5,
3976
+ constants as constants6,
2999
3977
  existsSync as existsSync12,
3000
- lstatSync as lstatSync4,
3978
+ lstatSync as lstatSync5,
3001
3979
  mkdirSync as mkdirSync6,
3002
3980
  mkdtempSync as mkdtempSync3,
3003
- realpathSync as realpathSync5,
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 isAbsolute5, join as join17, relative as relative5, sep as sep5 } from "path";
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((key) => key !== "images" && key !== "questionEnabled")
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(text, values) {
3285
- let redacted = text;
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://") || !isAbsolute5(runtime.endpoint.slice("unix://".length)) || containsUnsafeDockerArgument(runtime.endpoint)) {
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 = lstatSync4(value);
4546
+ const entry = lstatSync5(value);
3548
4547
  if (entry.isSymbolicLink() || !entry.isDirectory()) {
3549
4548
  throw new Error("unsafe entry");
3550
4549
  }
3551
- const canonical = realpathSync5(value);
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 realpathSync5(value);
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 || !isAbsolute5(value) || !existsSync12(value) || !statSync3(value).isDirectory()) {
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 realpathSync5(value);
4597
+ return realpathSync7(value);
3599
4598
  }
3600
4599
  function requireCanonicalWindowsFile(value, name) {
3601
- if (!value || !isAbsolute5(value) || !existsSync12(value) || !statSync3(value).isFile()) {
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 realpathSync5(value);
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" || !isAbsolute5(value) || value.includes("\0")) {
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}`) && !isAbsolute5(rel);
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 text = JSON.stringify(result);
3691
- return receipt ? { content: [{ type: "text", text }], questionReceipt: receipt } : text;
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 Type2 } from "typebox";
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 join18 } from "path";
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 = join18(agentDir, "BOOTSTRAP.md");
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 closeSync5,
3852
- constants as constants6,
3853
- fstatSync as fstatSync4,
3854
- openSync as openSync5,
3855
- readSync as readSync2,
3856
- realpathSync as realpathSync6,
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 isAbsolute6, relative as relative6, resolve as resolve5, sep as sep6 } from "path";
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}`) && !isAbsolute6(rel);
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 = realpathSync6(workspacePath);
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 (isAbsolute6(p)) {
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 = realpathSync6(abs);
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 = openSync5(real, constants6.O_RDONLY | constants6.O_NOFOLLOW | constants6.O_NONBLOCK);
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 = fstatSync4(fd);
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 = realpathSync6(abs);
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 count = readSync2(fd, buffer, length, buffer.length - length, null);
3959
- if (count === 0) break;
3960
- length += count;
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
- closeSync5(fd);
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 closeSync6,
3991
- constants as constants7,
3992
- fstatSync as fstatSync5,
3993
- lstatSync as lstatSync5,
3994
- openSync as openSync6,
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 join19 } from "path";
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 = openSync6(
5062
+ fd = openSync7(
4064
5063
  path,
4065
- constants7.O_WRONLY | constants7.O_CREAT | constants7.O_TRUNC | noFollowFlag(),
5064
+ constants8.O_WRONLY | constants8.O_CREAT | constants8.O_TRUNC | noFollowFlag(),
4066
5065
  384
4067
5066
  );
4068
- if (!fstatSync5(fd).isFile()) throw new Error("target is not a regular file");
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) closeSync6(fd);
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 = lstatSync5(path);
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 join19(homeRoot, file);
5118
+ return join21(homeRoot, file);
4120
5119
  }
4121
5120
  function rejectSymlinkOrNonFile(path) {
4122
- const entry = lstatSync5(path);
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 text = String(args.text ?? "");
5255
+ const text2 = String(args.text ?? "");
4257
5256
  if (!to) throw new Error('send_message: "to" is required');
4258
- if (!text) throw new Error('send_message: "text" is required');
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: "Block until a reply to a message you sent arrives, or until the timeout expires.",
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 text = 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, "");
4443
- text = text.replace(/<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, body) => {
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
- text = text.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, level, body) => {
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
- text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, body) => {
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
- text = text.replace(/<(br|hr)\s*\/?>/gi, "\n").replace(/<\/(p|div|section|article|tr|ul|ol|table|blockquote)>/gi, "\n");
4459
- text = stripTags(text);
4460
- return { text: normalizeWhitespace(text), title };
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 text = normalizeWhitespace(parsed.textContent ?? "");
4495
- return text ? { text, title } : fallback();
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 text = await res.text();
4807
- if (text.length > maxBytes) return { text: text.slice(0, maxBytes), truncated: true };
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 text = mode === "text" ? markdownToPlain(md) : md;
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: Type2.Unsafe(h.def.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 = join20(agent.agent.dir, "uploads");
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 ?? join20(paths.agentDir(agent.agent.id), "sessions");
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: join20(paths.home, "pi"),
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: join20(paths.home, "pi"),
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
- return await refreshApiKey(providerName);
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: PROTECTED_MODEL_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: PROTECTED_MODEL_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
- protectedRuntimeSecrets(opts.runtime),
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 = join20(scratch.tempDir, "provider-credential.json");
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, initialSecrets, refreshApiKey) {
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 = join20(directory, target.filename);
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: Type3.Object({
5685
- proposals: Type3.Array(
5686
- Type3.Object({
5687
- scope: Type3.Union([Type3.Literal("private"), Type3.Literal("shared")]),
5688
- text: Type3.String({ minLength: 1, maxLength: 500 }),
5689
- evidenceEntryIds: Type3.Array(
5690
- Type3.Object({ sessionId: Type3.String(), entryOrdinal: Type3.Integer({ minimum: 0 }) }),
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 text = candidate.text.trim();
5703
- if (!text || text.length > 500) throw new Error("proposal text must be 1-500 characters");
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,