bazilion 0.14.2 → 0.16.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 (122) hide show
  1. package/dist/cli.js +1752 -825
  2. package/dist/cli.js.map +1 -1
  3. package/dist/daemon.js +16624 -9803
  4. package/dist/daemon.js.map +1 -1
  5. package/dist/migrations/0001_init.sql +218 -3
  6. package/dist/web/client/assets/ChatPane-CQZzxOir.js +76 -0
  7. package/dist/web/client/assets/{ConfirmDialog-_bmG-wVG.js → ConfirmDialog-MicTTwdy.js} +1 -1
  8. package/dist/web/client/assets/{FieldRow-Do1cBkRc.js → FieldRow-DJ2Gx4Du.js} +1 -1
  9. package/dist/web/client/assets/{ProductionTeamPolicyEditor-XP-QJul4.js → ProductionTeamPolicyEditor-DqUnN_ce.js} +1 -1
  10. package/dist/web/client/assets/{ProfileCommunicationEditor-CwdFPdpk.js → ProfileCommunicationEditor-DArXub0b.js} +1 -1
  11. package/dist/web/client/assets/ResultCard-Cps_lXVN.js +1 -0
  12. package/dist/web/client/assets/ResultSourceConversation-CtzJ8QA-.js +1 -0
  13. package/dist/web/client/assets/TeamTabs-DbIk5vvT.js +1 -0
  14. package/dist/web/client/assets/{TemplatesTabs-D4WHvs6b.js → TemplatesTabs-CatJIjGi.js} +1 -1
  15. package/dist/web/client/assets/{UnsavedChangesGuard-B1aLqiK5.js → UnsavedChangesGuard-BCsLw-mU.js} +1 -1
  16. package/dist/web/client/assets/_id-B5Ov7cdO.js +1 -0
  17. package/dist/web/client/assets/{_id-BH3v_vI_.js → _id-BEDmIXVf.js} +1 -1
  18. package/dist/web/client/assets/_id-BFM_tn_s.js +1 -0
  19. package/dist/web/client/assets/_id-CrnEVcw8.js +1 -0
  20. package/dist/web/client/assets/{_id-3w4PFfma.js → _id-DxopIW-y.js} +1 -1
  21. package/dist/web/client/assets/{activity-A_HrG1Tv.js → activity-CrG8Bg60.js} +1 -1
  22. package/dist/web/client/assets/{agents-C8OVKq3N.js → agents-CMPOFcs-.js} +1 -1
  23. package/dist/web/client/assets/{agents-BeeFCt2c.js → agents-O4soKZND.js} +1 -1
  24. package/dist/web/client/assets/{approvals-BIdZ9BSn.js → approvals-CwDu_lrA.js} +1 -1
  25. package/dist/web/client/assets/{attention-B9bUrxiU.js → attention-BY8MMuM_.js} +1 -1
  26. package/dist/web/client/assets/chevron-right-Cf6vKeLq.js +1 -0
  27. package/dist/web/client/assets/{config-By86S7k-.js → config-Cn4iBJFz.js} +1 -1
  28. package/dist/web/client/assets/{inbox-DlXQCJLe.js → inbox-CWXql0K3.js} +1 -1
  29. package/dist/web/client/assets/{index-CLdmsmiN.js → index-Bq2zrG2_.js} +2 -2
  30. package/dist/web/client/assets/{learning-BqZIw2nA.js → learning-DAm4dyyc.js} +1 -1
  31. package/dist/web/client/assets/login-BsABI8Ux.js +1 -0
  32. package/dist/web/client/assets/{mcp-DKSY2yVa.js → mcp-loLB0wuh.js} +1 -1
  33. package/dist/web/client/assets/{members-9GuOiq6J.js → members-BJ0ikkTl.js} +1 -1
  34. package/dist/web/client/assets/{memory-DiGMI6FC.js → memory-Drn9Gv53.js} +1 -1
  35. package/dist/web/client/assets/{policy-TBGQffw5.js → policy-pQadZyk0.js} +1 -1
  36. package/dist/web/client/assets/results-9exU6f7m.js +1 -0
  37. package/dist/web/client/assets/routes-jw1PuqiR.js +1 -0
  38. package/dist/web/client/assets/{services-1Z398CES.js → services-BLso1BhA.js} +1 -1
  39. package/dist/web/client/assets/{skills-BP2gciFt.js → skills-BodqX3OI.js} +1 -1
  40. package/dist/web/client/assets/styles-d1yjBpud.css +2 -0
  41. package/dist/web/client/assets/{teams-DBIivGT7.js → teams-ByQedXZi.js} +1 -1
  42. package/dist/web/client/assets/{teams-tNoMDJ_M.js → teams-SYsF1bOR.js} +1 -1
  43. package/dist/web/client/assets/telegram-DC9PLFbB.js +1 -0
  44. package/dist/web/client/assets/{tokens-eDWCGQQ2.js → tokens-DGy8Ijpo.js} +1 -1
  45. package/dist/web/client/assets/{triggers-DZME3IcK.js → triggers-BXpVmHDG.js} +1 -1
  46. package/dist/web/client/assets/{welcome-B49sh_uf.js → welcome-BKsnsCxJ.js} +1 -1
  47. package/dist/web/client/assets/wire-constants-CBD-lTaa.js +1 -0
  48. package/dist/web/client/assets/{x-CHc7_-TH.js → x-BK4xNCzS.js} +1 -1
  49. package/dist/web/server/assets/ChatPane-CS5oI7ef.js +2666 -0
  50. package/dist/web/server/assets/ResultCard-B3zD9N0t.js +143 -0
  51. package/dist/web/server/assets/ResultSourceConversation-BpVWBRYq.js +64 -0
  52. package/dist/web/server/assets/{TeamTabs-Co3n2exj.js → TeamTabs-TCKjCvMA.js} +4 -0
  53. package/dist/web/server/assets/{_id-sNu5aXZt.js → _id-5pgKoid8.js} +2 -1
  54. package/dist/web/server/assets/_id-BDEpe6k0.js +6 -0
  55. package/dist/web/server/assets/{_id-DxCnEFpm.js → _id-BG2nkGxa.js} +3 -4
  56. package/dist/web/server/assets/{_id-BqWTB8Wl.js → _id-B_-CDHOi2.js} +1 -1
  57. package/dist/web/server/assets/{_id-BeKhPKw3.js → _id-BnqodC1c2.js} +7 -3
  58. package/dist/web/server/assets/{_id-DvlDkk04.js → _id-CeyHEsCZ.js} +1 -1
  59. package/dist/web/server/assets/{_id-BYKpT2Tu.js → _id-DFHB9JFQ.js} +1 -1
  60. package/dist/web/server/assets/{_id-DYU6uIBx.js → _id-DOnInzLb.js} +1 -1
  61. package/dist/web/server/assets/{_id-HMy7jM3P.js → _id-DrB9uR1b.js} +1 -1
  62. package/dist/web/server/assets/{_id-BAA9KJBU.js → _id-DtIYuJJX.js} +1 -1
  63. package/dist/web/server/assets/_id-DwZXhLlL.js +779 -0
  64. package/dist/web/server/assets/_id-wyjrf0JG.js +17 -0
  65. package/dist/web/server/assets/{_tanstack-start-manifest_v-Duhf0z7H.js → _tanstack-start-manifest_v-D7gKv6m6.js} +97 -70
  66. package/dist/web/server/assets/{activity-9VP6QDTH.js → activity-0VUkStOu.js} +1 -1
  67. package/dist/web/server/assets/{activity-BWv1goK6.js → activity-5fWKPDxj.js} +2 -2
  68. package/dist/web/server/assets/{activity-LjpblbDL.js → activity-C88qrNqv.js} +1 -1
  69. package/dist/web/server/assets/{agents-DjyQk94u.js → agents-C86K6HvS.js} +1 -1
  70. package/dist/web/server/assets/{agents-BJAv919x.js → agents-CSXLaM2W.js} +1 -1
  71. package/dist/web/server/assets/{approvals-BYi84gwl.js → approvals-C3MysI6l.js} +1 -1
  72. package/dist/web/server/assets/{approvals-7JOoBchn.js → approvals-DTdrltyk.js} +1 -1
  73. package/dist/web/server/assets/{approvals-Bz123liE.js → approvals-DXoN-T57.js} +15 -1
  74. package/dist/web/server/assets/{attention-UhqPK_Ro.js → attention-DLeLCKah.js} +1 -1
  75. package/dist/web/server/assets/{auth-7BHTH-KS.js → auth-CDZp9aEk.js} +1 -1
  76. package/dist/web/server/assets/{config-BMiajTZP.js → config-BVTHvljR.js} +1 -1
  77. package/dist/web/server/assets/{daemon-client-CI5NHwNC.js → daemon-client-xWyYKAcx.js} +59 -0
  78. package/dist/web/server/assets/{inbox-Dv55rGYq.js → inbox-PQTrjtOw.js} +1 -1
  79. package/dist/web/server/assets/{learning-C2upa1CE.js → learning-K0cmDsZR.js} +1 -1
  80. package/dist/web/server/assets/{login-BNj3U6ok.js → login-B_-9L8TU.js} +7 -2
  81. package/dist/web/server/assets/{login-Be8eP4Jp.js → login-rDox7PRD.js} +5 -2
  82. package/dist/web/server/assets/{mcp-BvnvlVFd.js → mcp-Rtraochg.js} +1 -1
  83. package/dist/web/server/assets/{members-DiYtr-zm.js → members-CD3GiTfF.js} +1 -1
  84. package/dist/web/server/assets/{members-DHYTVgwb.js → members-cc5SeEaW.js} +2 -2
  85. package/dist/web/server/assets/{members-BP3UTqu1.js → members-grAKFWgv.js} +1 -1
  86. package/dist/web/server/assets/{memory-CkXUgf5s.js → memory-C5i_GxlY.js} +2 -2
  87. package/dist/web/server/assets/{memory-DTFEmfF3.js → memory-CVqnyUaM.js} +1 -1
  88. package/dist/web/server/assets/{memory-BqV-Afy1.js → memory-egFpBD1g.js} +1 -1
  89. package/dist/web/server/assets/{policy-Bvvav8fh.js → policy-Dgjb0_gK.js} +2 -2
  90. package/dist/web/server/assets/{policy-BddK4xom.js → policy-bLKwvnja.js} +1 -1
  91. package/dist/web/server/assets/{policy-18fdTFO4.js → policy-ooSBgiDS.js} +1 -1
  92. package/dist/web/server/assets/results-CfNsFncJ.js +173 -0
  93. package/dist/web/server/assets/results-DXdYtHaq.js +12 -0
  94. package/dist/web/server/assets/results-N-kP4iFT.js +12 -0
  95. package/dist/web/server/assets/{router-CY_pCgP1.js → router-B6g_s7W3.js} +60 -43
  96. package/dist/web/server/assets/{routes-COtkyZG1.js → routes-BqSaZvlu.js} +3 -3
  97. package/dist/web/server/assets/{routes-BgHisAxH.js → routes-Dsh8F_7i.js} +2 -2
  98. package/dist/web/server/assets/{routes-DAU2g7Uz.js → routes-pcRy_Sf8.js} +1 -1
  99. package/dist/web/server/assets/{services-DozuJtzX.js → services-DlZA8fRL.js} +1 -1
  100. package/dist/web/server/assets/{skills-DfGSJTCB.js → skills-i2duvgW-.js} +1 -1
  101. package/dist/web/server/assets/{teams-be05ci17.js → teams-6RYIj1Xm.js} +1 -1
  102. package/dist/web/server/assets/{teams-DPnAX4wD.js → teams-B1AI3NfN.js} +1 -1
  103. package/dist/web/server/assets/{telegram-1AWL6lFw.js → telegram-C4q0YTVn.js} +1 -1
  104. package/dist/web/server/assets/{telegram-Dj4Wwhd6.js → telegram-CoNeOgqZ.js} +465 -1
  105. package/dist/web/server/assets/{telegram-DbOFpTFm.js → telegram-Zd1rIZxg.js} +1 -1
  106. package/dist/web/server/assets/{tokens-DjM8GsgS.js → tokens-RFquA2Bx.js} +1 -1
  107. package/dist/web/server/assets/{triggers-Nezfw-Bd.js → triggers-DVD-iE-N.js} +1 -1
  108. package/dist/web/server/server.js +31 -27
  109. package/dist/worker.js +1765 -710
  110. package/dist/worker.js.map +1 -1
  111. package/package.json +3 -3
  112. package/dist/web/client/assets/TeamTabs-DIRiKcqx.js +0 -1
  113. package/dist/web/client/assets/_id-Cx7o4Zmf.js +0 -1
  114. package/dist/web/client/assets/_id-DIiVVxBl.js +0 -1
  115. package/dist/web/client/assets/chevron-right-VWrk_dTX.js +0 -1
  116. package/dist/web/client/assets/login-CCMPXiny.js +0 -1
  117. package/dist/web/client/assets/routes-g3yUOhCb.js +0 -1
  118. package/dist/web/client/assets/styles-B5vcgXln.css +0 -2
  119. package/dist/web/client/assets/telegram-CJG_DFdI.js +0 -1
  120. package/dist/web/client/assets/wire-constants-zZ7Wjb9C.js +0 -74
  121. package/dist/web/server/assets/ChatPane-CgSe7tsy.js +0 -1429
  122. package/dist/web/server/assets/_id-ndxFHx0t.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";
@@ -1128,15 +1128,15 @@ function translatePiEvent(e) {
1128
1128
  const m = e.message;
1129
1129
  if (m.role !== "assistant") return [];
1130
1130
  const out = [];
1131
- const text = extractAssistantText(m);
1132
- if (text) out.push({ type: "assistant_message", text });
1131
+ const text2 = extractAssistantText(m);
1132
+ if (text2) out.push({ type: "assistant_message", text: text2 });
1133
1133
  for (const block of m.content ?? []) {
1134
1134
  if (block.type === "toolCall") {
1135
1135
  out.push({
1136
1136
  type: "tool_call",
1137
1137
  id: block.id,
1138
1138
  name: block.name,
1139
- arguments: JSON.stringify(block.arguments ?? {})
1139
+ arguments: block.name === "ask_user" ? "{}" : JSON.stringify(block.arguments ?? {})
1140
1140
  });
1141
1141
  }
1142
1142
  }
@@ -1148,9 +1148,18 @@ function translatePiEvent(e) {
1148
1148
  return out;
1149
1149
  }
1150
1150
  case "tool_execution_end": {
1151
- const text = extractToolResultText(e.result);
1151
+ if (e.toolName === "ask_user")
1152
+ return [
1153
+ {
1154
+ type: "tool_result",
1155
+ id: e.toolCallId,
1156
+ name: e.toolName,
1157
+ result: "Question outcome is available through the question card or authorized conversation history."
1158
+ }
1159
+ ];
1160
+ const text2 = extractToolResultText(e.result);
1152
1161
  if (e.isError) {
1153
- return [{ type: "tool_error", id: e.toolCallId, name: e.toolName, error: text }];
1162
+ return [{ type: "tool_error", id: e.toolCallId, name: e.toolName, error: text2 }];
1154
1163
  }
1155
1164
  const images = extractToolResultImages(e.result);
1156
1165
  return [
@@ -1158,7 +1167,8 @@ function translatePiEvent(e) {
1158
1167
  type: "tool_result",
1159
1168
  id: e.toolCallId,
1160
1169
  name: e.toolName,
1161
- result: text,
1170
+ result: text2,
1171
+ resultReference: extractResultReference(e.result),
1162
1172
  ...images.length > 0 ? { images } : {}
1163
1173
  }
1164
1174
  ];
@@ -1178,7 +1188,11 @@ function extractAssistantToolCalls(m) {
1178
1188
  const out = [];
1179
1189
  for (const block of m.content ?? []) {
1180
1190
  if (block.type === "toolCall") {
1181
- out.push({ id: block.id, name: block.name, arguments: JSON.stringify(block.arguments ?? {}) });
1191
+ out.push({
1192
+ id: block.id,
1193
+ name: block.name,
1194
+ arguments: block.name === "ask_user" ? "{}" : JSON.stringify(block.arguments ?? {})
1195
+ });
1182
1196
  }
1183
1197
  }
1184
1198
  return out;
@@ -1215,7 +1229,7 @@ function stringifyContent(content) {
1215
1229
  }
1216
1230
  return out;
1217
1231
  }
1218
- function piMessagesToProviderView(messages) {
1232
+ function piMessagesToProviderView(messages, questionResultVisible) {
1219
1233
  const out = [];
1220
1234
  for (const m of messages) {
1221
1235
  switch (m.role) {
@@ -1231,21 +1245,42 @@ function piMessagesToProviderView(messages) {
1231
1245
  }
1232
1246
  case "assistant": {
1233
1247
  const am = m;
1234
- const text = extractAssistantText(am);
1248
+ const text2 = extractAssistantText(am);
1235
1249
  const toolCalls = extractAssistantToolCalls(am);
1236
- const msg = { role: "assistant", content: text };
1250
+ const msg = { role: "assistant", content: text2 };
1237
1251
  if (toolCalls.length > 0) msg.toolCalls = toolCalls;
1238
1252
  out.push(msg);
1239
1253
  break;
1240
1254
  }
1241
1255
  case "toolResult": {
1242
1256
  const tr = m;
1257
+ if (["repository_context", "coding_environment", "coding_command", "coding_receipt"].includes(
1258
+ tr.toolName ?? ""
1259
+ )) {
1260
+ out.push({
1261
+ role: "tool",
1262
+ toolCallId: tr.toolCallId,
1263
+ toolName: tr.toolName,
1264
+ content: "Coding context or command evidence retained privately. Captured transport output follows its communication approval; ask the Agent for the relevant result."
1265
+ });
1266
+ break;
1267
+ }
1268
+ if (tr.toolName === "ask_user" && !questionResultVisible?.(m)) {
1269
+ out.push({
1270
+ role: "tool",
1271
+ toolCallId: tr.toolCallId,
1272
+ toolName: "ask_user",
1273
+ content: "Question content has not been released for this view."
1274
+ });
1275
+ break;
1276
+ }
1243
1277
  const images = extractToolResultImages({ content: tr.content });
1244
1278
  out.push({
1245
1279
  role: "tool",
1246
1280
  content: stringifyContent(tr.content),
1247
1281
  toolCallId: tr.toolCallId,
1248
1282
  toolName: tr.toolName,
1283
+ result: extractResultReference(m),
1249
1284
  ...images.length > 0 ? { images } : {}
1250
1285
  });
1251
1286
  break;
@@ -1256,491 +1291,338 @@ function piMessagesToProviderView(messages) {
1256
1291
  }
1257
1292
  return out;
1258
1293
  }
1294
+ function extractResultReference(value) {
1295
+ if (!value || typeof value !== "object") return void 0;
1296
+ const details = value.details;
1297
+ const id = details?.result?.resultId;
1298
+ return typeof id === "string" && /^[a-f0-9-]{36}$/.test(id) ? { resultId: id } : void 0;
1299
+ }
1259
1300
 
1260
- // ../daemon/src/runtime/pi/session.ts
1261
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, readdirSync as readdirSync8, statSync as statSync5, writeFileSync as writeFileSync9 } from "fs";
1262
- import { basename as basename3, join as join19 } from "path";
1263
- import {
1264
- createAssistantMessageEventStream
1265
- } from "@earendil-works/pi-ai";
1301
+ // ../daemon/src/lib/result-source.ts
1302
+ import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, openSync as openSync2, readSync, realpathSync as realpathSync2 } from "fs";
1303
+ import { basename as basename2, join as join13 } from "path";
1304
+ function readCanonicalSessionFile(directory, filename, sessionId, minimumEntryOffset = 0) {
1305
+ if (basename2(filename) !== filename) throw new Error("Invalid session filename");
1306
+ if (realpathSync2(directory) !== directory) throw new Error("Result session escaped its owner");
1307
+ const fd = openSync2(
1308
+ join13(directory, filename),
1309
+ constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK
1310
+ );
1311
+ try {
1312
+ const stat2 = fstatSync2(fd);
1313
+ if (!stat2.isFile() || stat2.size > 64 * 1024 * 1024)
1314
+ throw new Error("Result source session is unavailable or too large");
1315
+ const buffer = Buffer.alloc(stat2.size + 1);
1316
+ let count2 = 0;
1317
+ while (count2 < buffer.length) {
1318
+ const n = readSync(fd, buffer, count2, buffer.length - count2, null);
1319
+ if (n === 0) break;
1320
+ count2 += n;
1321
+ }
1322
+ if (count2 !== stat2.size) throw new Error("Result source session changed while reading");
1323
+ const entries = [];
1324
+ let position = 0;
1325
+ for (const line of buffer.subarray(0, count2).toString("utf8").split("\n")) {
1326
+ if (line.trim()) entries.push({ entry: JSON.parse(line), offset: position });
1327
+ position += Buffer.byteLength(line) + 1;
1328
+ }
1329
+ const header = entries[0]?.entry;
1330
+ if (header?.type !== "session" || header.id !== sessionId)
1331
+ throw new Error("Result source session does not match the active Agent session");
1332
+ return entries.filter((row, index) => index === 0 || row.offset >= minimumEntryOffset).map((row) => row.entry);
1333
+ } finally {
1334
+ closeSync2(fd);
1335
+ }
1336
+ }
1337
+
1338
+ // ../daemon/src/runtime/pi/coding.ts
1266
1339
  import {
1267
- createAgentSession,
1268
- DefaultResourceLoader,
1269
- SessionManager,
1270
- SettingsManager
1340
+ createLocalBashOperations
1271
1341
  } from "@earendil-works/pi-coding-agent";
1342
+ import { Type } from "typebox";
1272
1343
 
1273
- // ../daemon/src/runtime/providers/pi-runtime.ts
1274
- import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
1275
- import { ModelRuntime } from "@earendil-works/pi-coding-agent";
1276
- var PROVIDER_ALIASES = {
1277
- bedrock: "amazon-bedrock",
1278
- "azure-openai": "azure-openai-responses"
1279
- };
1280
- var PROTECTED_PROVIDER_NAMES = [
1281
- "anthropic",
1282
- "openai",
1283
- "openai-codex",
1284
- "google",
1285
- "google-vertex",
1286
- "azure-openai",
1287
- "bedrock",
1288
- "mistral",
1289
- "groq",
1290
- "cerebras",
1291
- "xai",
1292
- "zai",
1293
- "huggingface",
1294
- "openrouter",
1295
- "vercel-ai-gateway",
1296
- "deepseek",
1297
- "fireworks",
1298
- "together",
1299
- "baseten",
1300
- "moonshotai",
1301
- "moonshotai-cn",
1302
- "kimi-coding",
1303
- "minimax",
1304
- "minimax-cn",
1305
- "qwen-token-plan",
1306
- "qwen-token-plan-cn",
1307
- "qwen-token-plan-individual",
1308
- "xiaomi",
1309
- "xiaomi-token-plan-ams",
1310
- "xiaomi-token-plan-cn",
1311
- "xiaomi-token-plan-sgp",
1312
- "ant-ling",
1313
- "nvidia",
1314
- "opencode",
1315
- "opencode-go",
1316
- "zai-coding-cn",
1317
- "github-copilot",
1318
- "cloudflare-ai-gateway",
1319
- "cloudflare-workers-ai",
1320
- "lmstudio",
1321
- "ollama",
1322
- "llamacpp"
1323
- ];
1324
- var LOCAL_PROVIDERS = {
1325
- lmstudio: {
1326
- baseUrl: "http://127.0.0.1:1234/v1",
1327
- apiKey: "lm-studio",
1328
- authHeader: false
1329
- },
1330
- ollama: { baseUrl: "http://127.0.0.1:11434/v1", apiKey: "ollama", authHeader: false },
1331
- llamacpp: { baseUrl: "http://127.0.0.1:8080/v1", apiKey: "no-key", authHeader: true }
1332
- };
1333
- var PROVIDER_API_KEY_ENV = {
1334
- anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
1335
- openai: ["OPENAI_API_KEY"],
1336
- google: ["GEMINI_API_KEY"],
1337
- "azure-openai": ["AZURE_OPENAI_API_KEY"],
1338
- mistral: ["MISTRAL_API_KEY"],
1339
- groq: ["GROQ_API_KEY"],
1340
- cerebras: ["CEREBRAS_API_KEY"],
1341
- xai: ["XAI_API_KEY"],
1342
- zai: ["ZAI_API_KEY"],
1343
- huggingface: ["HF_TOKEN"],
1344
- openrouter: ["OPENROUTER_API_KEY"],
1345
- "vercel-ai-gateway": ["AI_GATEWAY_API_KEY"],
1346
- deepseek: ["DEEPSEEK_API_KEY"],
1347
- fireworks: ["FIREWORKS_API_KEY"],
1348
- together: ["TOGETHER_API_KEY"],
1349
- baseten: ["BASETEN_API_KEY"],
1350
- moonshotai: ["MOONSHOT_API_KEY"],
1351
- "moonshotai-cn": ["MOONSHOT_CN_API_KEY"],
1352
- "kimi-coding": ["KIMI_API_KEY"],
1353
- minimax: ["MINIMAX_API_KEY"],
1354
- "minimax-cn": ["MINIMAX_CN_API_KEY"],
1355
- "qwen-token-plan": ["QWEN_TOKEN_PLAN_API_KEY"],
1356
- "qwen-token-plan-cn": ["QWEN_TOKEN_PLAN_CN_API_KEY"],
1357
- "qwen-token-plan-individual": ["QWEN_TOKEN_PLAN_API_KEY"],
1358
- xiaomi: ["XIAOMI_API_KEY"],
1359
- "xiaomi-token-plan-ams": ["XIAOMI_TOKEN_PLAN_AMS_API_KEY"],
1360
- "xiaomi-token-plan-cn": ["XIAOMI_TOKEN_PLAN_CN_API_KEY"],
1361
- "xiaomi-token-plan-sgp": ["XIAOMI_TOKEN_PLAN_SGP_API_KEY"],
1362
- "ant-ling": ["ANT_LING_API_KEY"],
1363
- nvidia: ["NVIDIA_API_KEY"],
1364
- opencode: ["OPENCODE_API_KEY"],
1365
- "opencode-go": ["OPENCODE_GO_API_KEY"],
1366
- "zai-coding-cn": ["ZAI_CODING_CN_API_KEY"],
1367
- "github-copilot": ["COPILOT_GITHUB_TOKEN"],
1368
- "cloudflare-ai-gateway": ["CLOUDFLARE_API_KEY"],
1369
- "cloudflare-workers-ai": ["CLOUDFLARE_API_KEY"],
1370
- lmstudio: ["LMSTUDIO_API_KEY"],
1371
- ollama: ["OLLAMA_API_KEY"],
1372
- llamacpp: ["LLAMACPP_API_KEY"]
1373
- };
1374
- var PROVIDER_CREDENTIAL_ENV = {
1375
- bedrock: [
1376
- "AWS_PROFILE",
1377
- "AWS_BEARER_TOKEN_BEDROCK",
1378
- "AWS_ACCESS_KEY_ID",
1379
- "AWS_SECRET_ACCESS_KEY",
1380
- "AWS_SESSION_TOKEN",
1381
- "AWS_REGION",
1382
- "AWS_DEFAULT_REGION"
1383
- ],
1384
- "google-vertex": [
1385
- "GOOGLE_CLOUD_PROJECT",
1386
- "GOOGLE_CLOUD_LOCATION",
1387
- "GOOGLE_APPLICATION_CREDENTIALS"
1388
- ],
1389
- "cloudflare-ai-gateway": ["CLOUDFLARE_API_KEY", "CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_GATEWAY_ID"],
1390
- "cloudflare-workers-ai": ["CLOUDFLARE_API_KEY", "CLOUDFLARE_ACCOUNT_ID"]
1344
+ // ../daemon/src/core/coding-environment/config.ts
1345
+ var CodingEnvironmentValidationError = class extends Error {
1346
+ status = 400;
1391
1347
  };
1392
- function piProviderName(providerName) {
1393
- return PROVIDER_ALIASES[providerName] ?? providerName;
1394
- }
1395
- function providerApiKey(providerName, env) {
1396
- for (const name of PROVIDER_API_KEY_ENV[providerName] ?? []) {
1397
- const value = env[name];
1398
- if (value) return value;
1399
- }
1400
- return LOCAL_PROVIDERS[providerName]?.apiKey;
1401
- }
1402
- function providerBaseUrl(providerName, env) {
1403
- switch (providerName) {
1404
- case "lmstudio":
1405
- return env.LMSTUDIO_URL ?? LOCAL_PROVIDERS.lmstudio?.baseUrl;
1406
- case "ollama":
1407
- return env.OLLAMA_URL ?? LOCAL_PROVIDERS.ollama?.baseUrl;
1408
- case "llamacpp":
1409
- return env.LLAMACPP_URL ?? LOCAL_PROVIDERS.llamacpp?.baseUrl;
1410
- case "vercel-ai-gateway":
1411
- return env.AI_GATEWAY_BASE_URL;
1412
- default:
1413
- return void 0;
1414
- }
1415
- }
1416
- function providerCredentialEnv(providerName, env) {
1417
- const result = {};
1418
- for (const name of PROVIDER_CREDENTIAL_ENV[providerName] ?? []) {
1419
- const value = env[name];
1420
- if (value !== void 0) result[name] = value;
1421
- }
1422
- return Object.keys(result).length > 0 ? result : void 0;
1423
- }
1424
- function fallbackApi(providerName) {
1425
- switch (providerName) {
1426
- case "anthropic":
1427
- return "anthropic-messages";
1428
- case "google":
1429
- return "google-generative-ai";
1430
- case "google-vertex":
1431
- return "google-vertex";
1432
- case "azure-openai":
1433
- return "azure-openai-responses";
1434
- case "bedrock":
1435
- return "bedrock-converse-stream";
1436
- case "openai-codex":
1437
- return "openai-codex-responses";
1438
- default:
1439
- return "openai-completions";
1440
- }
1348
+ function invalid(field) {
1349
+ throw new CodingEnvironmentValidationError(`Invalid coding environment ${field}`);
1350
+ }
1351
+ function object(value, keys2, field) {
1352
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalid(field);
1353
+ const record = value;
1354
+ if (Object.keys(record).some((key) => !keys2.includes(key))) invalid(field);
1355
+ return record;
1356
+ }
1357
+ function text(value, max, field) {
1358
+ if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value) > max || Array.from(value).some((char) => {
1359
+ const code = char.charCodeAt(0);
1360
+ return code === 127 || code < 32 && code !== 9 && code !== 10;
1361
+ }))
1362
+ invalid(field);
1363
+ return value;
1441
1364
  }
1442
- function fallbackPiModel(providerName, modelId, baseUrl) {
1365
+ function codingRelativePath(value, field = "cwd") {
1366
+ const path = text(value, 4096, field);
1367
+ if (path === ".") return path;
1368
+ const parts = path.split("/");
1369
+ if (parts.length > 16 || path.includes("\\") || Array.from(path).some((char) => char.charCodeAt(0) < 32) || parts.some((part) => !part || part === "." || part === ".." || part === ".git"))
1370
+ invalid(field);
1371
+ return path;
1372
+ }
1373
+ function validateCodingCommand(input) {
1374
+ const value = object(input, ["command", "cwd", "purpose", "timeoutSeconds"], "command");
1375
+ const command = text(value.command, 4096, "command");
1376
+ if (command.includes("\0")) invalid("command");
1377
+ const cwd = codingRelativePath(value.cwd);
1378
+ if (!["runtime", "dependency", "prepare", "build", "test"].includes(String(value.purpose)))
1379
+ invalid("purpose");
1380
+ if (!Number.isInteger(value.timeoutSeconds) || Number(value.timeoutSeconds) < 1 || Number(value.timeoutSeconds) > 300)
1381
+ invalid("timeout");
1443
1382
  return {
1444
- id: modelId,
1445
- name: modelId,
1446
- api: fallbackApi(providerName),
1447
- provider: piProviderName(providerName),
1448
- baseUrl: baseUrl ?? "",
1449
- reasoning: false,
1450
- input: ["text"],
1451
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1452
- contextWindow: 32768,
1453
- maxTokens: 4096
1383
+ command,
1384
+ cwd,
1385
+ purpose: value.purpose,
1386
+ timeoutSeconds: value.timeoutSeconds
1454
1387
  };
1455
1388
  }
1456
- async function createBazilionPiRuntime(options) {
1457
- const providerName = options.providerName;
1458
- const canonicalName = piProviderName(providerName);
1459
- const credentials = new InMemoryCredentialStore();
1460
- const apiKey = options.apiKey ?? providerApiKey(providerName, options.env);
1461
- const credentialEnv = providerCredentialEnv(providerName, options.env);
1462
- if (apiKey || credentialEnv) {
1463
- await credentials.modify(
1464
- canonicalName,
1465
- async () => canonicalName === "openai-codex" && apiKey ? {
1466
- type: "oauth",
1467
- access: apiKey,
1468
- // Bazilion owns refresh outside Pi. This deliberately empty value
1469
- // satisfies Pi's canonical OAuth shape but is never used because
1470
- // the token is long-lived only inside this one-shot runtime and
1471
- // mid-turn replacement is supplied through Agent.getApiKey.
1472
- refresh: "",
1473
- expires: Number.MAX_SAFE_INTEGER
1474
- } : {
1475
- type: "api_key",
1476
- ...apiKey ? { key: apiKey } : {},
1477
- ...credentialEnv ? { env: credentialEnv } : {}
1389
+
1390
+ // ../daemon/src/lib/coding-environment/diagnostics.ts
1391
+ var CODING_OUTPUT_BYTES = 64 * 1024;
1392
+ function diagnosticTail(value, limit) {
1393
+ const bytes = Buffer.from(value);
1394
+ if (bytes.length <= limit) return { text: value, truncated: false };
1395
+ let offset = bytes.length - limit;
1396
+ while (offset < bytes.length && ((bytes[offset] ?? 0) & 192) === 128) offset++;
1397
+ return { text: bytes.subarray(offset).toString("utf8"), truncated: true };
1398
+ }
1399
+ var CodingDiagnostics = class {
1400
+ decoder = new TextDecoder();
1401
+ secrets;
1402
+ lookbehind;
1403
+ pending = "";
1404
+ text = "";
1405
+ truncated = false;
1406
+ finished = false;
1407
+ constructor(secrets) {
1408
+ this.secrets = [...new Set(secrets.filter(Boolean))].sort((a, b) => b.length - a.length);
1409
+ this.lookbehind = Math.max(1, ...this.secrets.map((secret) => secret.length)) - 1;
1410
+ }
1411
+ append(bytes) {
1412
+ if (this.finished) throw new Error("Coding diagnostics already finalized");
1413
+ for (let offset = 0; offset < bytes.length; offset += 8192) {
1414
+ this.pending += this.decoder.decode(bytes.subarray(offset, offset + 8192), { stream: true });
1415
+ this.flush(false);
1416
+ }
1417
+ }
1418
+ finish() {
1419
+ if (!this.finished) {
1420
+ this.pending += this.decoder.decode();
1421
+ this.flush(true);
1422
+ this.finished = true;
1423
+ }
1424
+ return { diagnostic: this.text, truncated: this.truncated };
1425
+ }
1426
+ flush(final) {
1427
+ let end = final ? this.pending.length : Math.max(0, this.pending.length - this.lookbehind);
1428
+ const last = this.pending.charCodeAt(end - 1);
1429
+ if (!final && last >= 55296 && last <= 56319) end--;
1430
+ let offset = 0;
1431
+ let safe = "";
1432
+ while (offset < end) {
1433
+ const secret = this.secrets.find((value) => this.pending.startsWith(value, offset));
1434
+ if (secret) {
1435
+ safe += "[redacted]";
1436
+ offset += secret.length;
1437
+ } else {
1438
+ const code = this.pending.charCodeAt(offset);
1439
+ const char = this.pending[offset] ?? "";
1440
+ safe += code < 32 && code !== 9 && code !== 10 || code >= 127 && code <= 159 ? `\\u${code.toString(16).padStart(4, "0")}` : char;
1441
+ offset++;
1478
1442
  }
1443
+ }
1444
+ this.pending = this.pending.slice(offset);
1445
+ const tail = diagnosticTail(this.text + safe, CODING_OUTPUT_BYTES);
1446
+ this.text = tail.text;
1447
+ this.truncated ||= tail.truncated;
1448
+ }
1449
+ };
1450
+
1451
+ // ../daemon/src/runtime/coding-directory.ts
1452
+ import { realpathSync as realpathSync4 } from "fs";
1453
+ import { join as join15 } from "path";
1454
+
1455
+ // ../daemon/src/lib/repository-context/files.ts
1456
+ import { createHash as createHash6 } from "crypto";
1457
+ import {
1458
+ closeSync as closeSync3,
1459
+ constants as constants3,
1460
+ fstatSync as fstatSync3,
1461
+ lstatSync as lstatSync2,
1462
+ opendirSync,
1463
+ openSync as openSync3,
1464
+ readSync as readSync2,
1465
+ realpathSync as realpathSync3
1466
+ } from "fs";
1467
+ import { isAbsolute as isAbsolute2, join as join14 } from "path";
1468
+ var hash = (value) => createHash6("sha256").update(value).digest("hex");
1469
+ var ContextReadError = class extends Error {
1470
+ code;
1471
+ constructor(code) {
1472
+ super(code);
1473
+ this.code = code;
1474
+ }
1475
+ };
1476
+ function identity(stat2) {
1477
+ return `${stat2.dev}:${stat2.ino}`;
1478
+ }
1479
+ function stamp(stat2) {
1480
+ return `${identity(stat2)}:${stat2.size}:${stat2.mtimeMs}:${stat2.ctimeMs}`;
1481
+ }
1482
+ function relativeParts(target) {
1483
+ if (target.length > 4096 || isAbsolute2(target) || [...target].some(
1484
+ (char) => char === "\\" || char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127
1485
+ ) || target.split("/").some((part) => part === ".." || part === ".git"))
1486
+ throw new ContextReadError("unsafe_target");
1487
+ const parts = target.split("/").filter((part) => part && part !== ".");
1488
+ if (parts.length > 17) throw new ContextReadError("depth_limit");
1489
+ return parts;
1490
+ }
1491
+ var ContextDirectory = class _ContextDirectory {
1492
+ fd;
1493
+ identity;
1494
+ originalStamp;
1495
+ children = [];
1496
+ listing;
1497
+ observedFiles = /* @__PURE__ */ new Map();
1498
+ registeredPath;
1499
+ label;
1500
+ parent;
1501
+ constructor(registeredPath, label = ".", parent) {
1502
+ this.registeredPath = registeredPath;
1503
+ this.label = label;
1504
+ this.parent = parent;
1505
+ if (process.platform !== "linux") throw new ContextReadError("safe_reads_unavailable");
1506
+ const path = parent ? join14(parent.path, registeredPath) : realpathSync3(registeredPath);
1507
+ this.fd = openSync3(path, constants3.O_RDONLY | constants3.O_DIRECTORY | constants3.O_NOFOLLOW);
1508
+ const stat2 = fstatSync3(this.fd);
1509
+ this.identity = hash(identity(stat2));
1510
+ this.originalStamp = identity(stat2);
1511
+ }
1512
+ get path() {
1513
+ return `/proc/self/fd/${this.fd}`;
1514
+ }
1515
+ directory(name) {
1516
+ const child = new _ContextDirectory(
1517
+ name,
1518
+ this.label === "." ? name : `${this.label}/${name}`,
1519
+ this
1479
1520
  );
1521
+ this.children.push(child);
1522
+ return child;
1480
1523
  }
1481
- const runtime = await ModelRuntime.create({ credentials, modelsPath: null });
1482
- const local = LOCAL_PROVIDERS[providerName];
1483
- const baseUrl = options.baseUrl ?? providerBaseUrl(providerName, options.env);
1484
- if (local) {
1485
- const modelId = options.modelId ?? "model-name";
1486
- const model = fallbackPiModel(providerName, modelId, baseUrl ?? local.baseUrl);
1487
- runtime.registerProvider(canonicalName, {
1488
- baseUrl: baseUrl ?? local.baseUrl,
1489
- api: "openai-completions",
1490
- authHeader: local.authHeader,
1491
- apiKey: apiKey ?? local.apiKey,
1492
- models: [model]
1493
- });
1494
- } else if (baseUrl) {
1495
- runtime.registerProvider(canonicalName, { baseUrl });
1496
- }
1497
- return runtime;
1498
- }
1499
- async function createProtectedPiRuntime(options) {
1500
- const env = {};
1501
- for (const field of options.credentialEnv ?? []) env[field.name] = field.value;
1502
- return createBazilionPiRuntime({
1503
- providerName: options.providerName,
1504
- modelId: options.modelId,
1505
- env,
1506
- apiKey: options.apiKey,
1507
- baseUrl: options.baseUrl
1508
- });
1509
- }
1510
- function resolvePiModel(runtime, providerName, modelId, baseUrl) {
1511
- const known = runtime.getModel(piProviderName(providerName), modelId);
1512
- if (known) return baseUrl ? { ...known, baseUrl } : known;
1513
- return fallbackPiModel(providerName, modelId, baseUrl);
1514
- }
1515
-
1516
- // ../daemon/src/runtime/auth/openai-codex.ts
1517
- import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
1518
-
1519
- // ../daemon/src/runtime/providers/pi-adapter.ts
1520
- import {
1521
- Type
1522
- } from "@earendil-works/pi-ai";
1523
-
1524
- // ../daemon/src/runtime/session/prompt.ts
1525
- import { existsSync as existsSync10, readdirSync as readdirSync6 } from "fs";
1526
- import { join as join13 } from "path";
1527
-
1528
- // ../daemon/src/runtime/safe-files.ts
1529
- import {
1530
- closeSync as closeSync2,
1531
- constants as constants2,
1532
- fstatSync as fstatSync2,
1533
- lstatSync as lstatSync2,
1534
- mkdirSync as mkdirSync5,
1535
- openSync as openSync2,
1536
- readFileSync as readFileSync9,
1537
- realpathSync as realpathSync2
1538
- } from "fs";
1539
- import { dirname as dirname3, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve4 } from "path";
1540
- function resolveRealDirectory(path) {
1541
- const requested = resolve4(path);
1542
- const entry = lstatSync2(requested);
1543
- if (entry.isSymbolicLink() || !entry.isDirectory()) {
1544
- throw new Error("expected a real directory");
1524
+ entry(name) {
1525
+ try {
1526
+ return lstatSync2(join14(this.path, name));
1527
+ } catch (error) {
1528
+ if (error.code === "ENOENT") return null;
1529
+ throw new ContextReadError("unreadable");
1530
+ }
1545
1531
  }
1546
- return realpathSync2(requested);
1547
- }
1548
- function readRegularFileNoFollow(path) {
1549
- const entry = lstatSync2(path);
1550
- if (entry.isSymbolicLink()) throw new Error("symbolic links are not allowed");
1551
- if (!entry.isFile()) throw new Error("target is not a regular file");
1552
- let fd;
1553
- try {
1554
- fd = openSync2(path, constants2.O_RDONLY | noFollowFlag());
1555
- if (!fstatSync2(fd).isFile()) throw new Error("target is not a regular file");
1556
- return readFileSync9(fd, "utf8");
1557
- } finally {
1558
- if (fd !== void 0) closeSync2(fd);
1532
+ list() {
1533
+ const names = this.listNames();
1534
+ this.listing = JSON.stringify(names);
1535
+ return names;
1559
1536
  }
1560
- }
1561
- function noFollowFlag() {
1562
- return typeof constants2.O_NOFOLLOW === "number" ? constants2.O_NOFOLLOW : 0;
1563
- }
1564
-
1565
- // ../daemon/src/runtime/session/prompt.ts
1566
- var SANDBOX_SKILLS_DIR = "/skills";
1567
- function loadPromptSkills(skillsDir, attachedNames) {
1568
- if (!existsSync10(skillsDir) || attachedNames.length === 0) return [];
1569
- const attached = new Set(attachedNames);
1570
- const discovered = readdirSync6(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && attached.has(entry.name)).sort((a, b) => a.name.localeCompare(b.name));
1571
- const loaded = [];
1572
- for (const entry of discovered) {
1573
- const hostDir = join13(skillsDir, entry.name);
1537
+ listNames() {
1538
+ const handle = opendirSync(this.path);
1539
+ const names = [];
1574
1540
  try {
1575
- const parsed = parseSkillContent(readRegularFileNoFollow(join13(hostDir, "SKILL.md")));
1576
- const safeLabel = entry.name.replace(/[^A-Za-z0-9._-]/g, "_") || "skill";
1577
- loaded.push({
1578
- name: entry.name,
1579
- description: parsed.frontmatter.description,
1580
- body: parsed.body.trim(),
1581
- hostDir,
1582
- sandboxDir: `${SANDBOX_SKILLS_DIR}/${loaded.length}-${safeLabel}`
1583
- });
1584
- } catch {
1541
+ let entry = handle.readSync();
1542
+ while (entry) {
1543
+ if (names.length >= 16384) throw new ContextReadError("directory_entry_limit");
1544
+ names.push(entry.name);
1545
+ entry = handle.readSync();
1546
+ }
1547
+ return names.sort();
1548
+ } finally {
1549
+ handle.closeSync();
1585
1550
  }
1586
1551
  }
1587
- return loaded;
1588
- }
1589
- var CONTEXT_FILE_ORDER = ["AGENTS.md", "SOUL.md", "TOOLS.md", "IDENTITY.md"];
1590
- function loadProtectedHomeDocuments(agentDir) {
1591
- const root = resolveRealDirectory(agentDir);
1592
- const read = (file) => {
1552
+ read(name, maxBytes) {
1553
+ const entry = this.entry(name);
1554
+ if (!entry) {
1555
+ this.observedFiles.set(name, null);
1556
+ return null;
1557
+ }
1558
+ if (!entry.isFile() || entry.isSymbolicLink()) throw new ContextReadError("unsafe_file");
1559
+ if (entry.size > maxBytes) throw new ContextReadError("byte_limit");
1560
+ let fd;
1593
1561
  try {
1594
- return readRegularFileNoFollow(join13(root, file));
1562
+ fd = openSync3(
1563
+ join14(this.path, name),
1564
+ constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK
1565
+ );
1566
+ const before = fstatSync3(fd);
1567
+ if (!before.isFile() || stamp(entry) !== stamp(before))
1568
+ throw new ContextReadError("source_changed");
1569
+ const buffer = Buffer.alloc(Math.min(maxBytes + 1, before.size + 1));
1570
+ let size = 0;
1571
+ while (size < buffer.length) {
1572
+ const count2 = readSync2(fd, buffer, size, buffer.length - size, null);
1573
+ if (!count2) break;
1574
+ size += count2;
1575
+ }
1576
+ if (size > maxBytes || size !== before.size || stamp(fstatSync3(fd)) !== stamp(before)) {
1577
+ throw new ContextReadError("source_changed");
1578
+ }
1579
+ this.observedFiles.set(name, stamp(before));
1580
+ return buffer.subarray(0, size);
1595
1581
  } catch (error) {
1596
- if (error.code === "ENOENT") return null;
1597
- throw new Error(`private Agent home is unsafe at ${file}`);
1582
+ if (error instanceof ContextReadError) throw error;
1583
+ throw new ContextReadError("unreadable");
1584
+ } finally {
1585
+ if (fd !== void 0) closeSync3(fd);
1598
1586
  }
1599
- };
1600
- return {
1601
- "AGENTS.md": read("AGENTS.md"),
1602
- "SOUL.md": read("SOUL.md"),
1603
- "TOOLS.md": read("TOOLS.md"),
1604
- "IDENTITY.md": read("IDENTITY.md"),
1605
- "BOOTSTRAP.md": read("BOOTSTRAP.md")
1606
- };
1607
- }
1608
- function buildSystemPrompt(agent, options = {}) {
1609
- const parts = [];
1610
- const homeDocuments = options.homeDocuments ?? loadProtectedHomeDocuments(agent.agent.dir);
1611
- const contextBlocks = [];
1612
- for (const file of CONTEXT_FILE_ORDER) {
1613
- const content = homeDocuments[file]?.trimEnd();
1614
- if (!content) continue;
1615
- contextBlocks.push(`## ${file}
1616
-
1617
- ${content}`);
1618
- }
1619
- if (contextBlocks.length > 0) {
1620
- parts.push(`# Project Context
1621
-
1622
- ${contextBlocks.join("\n\n")}`);
1623
- }
1624
- const bootstrap = homeDocuments["BOOTSTRAP.md"]?.trimEnd();
1625
- if (bootstrap) {
1626
- parts.push(
1627
- [
1628
- "# First-Run Ritual",
1629
- "",
1630
- "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.",
1631
- "",
1632
- "## Hard rules",
1633
- "- Your first reply is ONLY a greeting + ONE question. No tool calls. Wait for the human to answer.",
1634
- "- Each subsequent turn: at most one new question. Wait between turns.",
1635
- "- Only after the ritual is complete (you have enough to write IDENTITY.md): call `home_write` once, then `bootstrap_done`.",
1636
- "",
1637
- "## BOOTSTRAP.md",
1638
- "",
1639
- bootstrap
1640
- ].join("\n")
1641
- );
1642
1587
  }
1643
- parts.push(
1644
- [
1645
- "# Agent Home",
1646
- "",
1647
- "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.",
1648
- "",
1649
- "- To change who you are (name, vibe, personality, how you behave): use `home_write`.",
1650
- "- To inspect exact wording of your own files: use `home_read` or `home_list`.",
1651
- "- To remember facts the user told you or things you learned: use `memory_write` \u2014 NOT `home_write`.",
1652
- "- 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."
1653
- ].join("\n")
1654
- );
1655
- if (agent.skills.length > 0) {
1656
- const loaded = options.skills ?? [];
1657
- const loadedNames = new Set(loaded.map((skill) => skill.name));
1658
- const unavailable = agent.skills.filter((name) => !loadedNames.has(name));
1659
- const skillBlocks = loaded.map((skill) => {
1660
- const runtimeDir = options.sandboxMode === "docker" ? skill.sandboxDir : skill.hostDir;
1661
- return [
1662
- `## ${skill.name}`,
1663
- "",
1664
- skill.description,
1665
- "",
1666
- `Runtime directory: \`${runtimeDir}\`. Resolve any relative scripts or assets mentioned by this skill from that directory.`,
1667
- "",
1668
- skill.body
1669
- ].filter((line, index, lines) => line !== "" || lines[index - 1] !== "").join("\n").trimEnd();
1670
- });
1671
- const unavailableNote = unavailable.length > 0 ? `Attached but unavailable or invalid: ${unavailable.join(", ")}.` : "";
1672
- parts.push(
1673
- [
1674
- "# Available Skills",
1675
- "",
1676
- "Use these attached skill instructions when they are relevant to the task.",
1677
- "",
1678
- ...skillBlocks,
1679
- ...unavailableNote ? ["", unavailableNote] : []
1680
- ].join("\n")
1681
- );
1588
+ validate(skipGit = false, names) {
1589
+ const currentPath = this.parent ? join14(this.parent.path, this.registeredPath) : realpathSync3(this.registeredPath);
1590
+ const stat2 = lstatSync2(currentPath);
1591
+ if (!stat2.isDirectory() || identity(stat2) !== this.originalStamp)
1592
+ throw new ContextReadError("root_changed");
1593
+ if (this.listing !== void 0 && JSON.stringify(this.listNames()) !== this.listing) {
1594
+ throw new ContextReadError("source_changed");
1595
+ }
1596
+ for (const [name, previous] of this.observedFiles) {
1597
+ if (names && !names.has(name)) continue;
1598
+ const current = this.entry(name);
1599
+ if ((current ? stamp(current) : null) !== previous)
1600
+ throw new ContextReadError("source_changed");
1601
+ }
1602
+ for (const child of this.children) {
1603
+ if (!skipGit || !child.label.split("/").includes(".git")) child.validate(skipGit, names);
1604
+ }
1682
1605
  }
1683
- const teamLines = [
1684
- "# Team",
1685
- "",
1686
- `- ${agent.team.id} (${agent.team.name}): ${options.sandboxMode === "docker" ? "/workspace" : agent.team.path}`,
1687
- "",
1688
- "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`."
1689
- ];
1690
- parts.push(teamLines.join("\n"));
1691
- if (agent.team.userMd.trim()) {
1692
- parts.push(
1693
- `# About the User
1694
-
1695
- 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.**
1696
-
1697
- ${agent.team.userMd.trim()}`
1698
- );
1699
- } else {
1700
- parts.push(
1701
- `# About the User
1702
-
1703
- 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.**`
1704
- );
1606
+ close() {
1607
+ for (const child of this.children) child.close();
1608
+ closeSync3(this.fd);
1705
1609
  }
1706
- const reviewedLessons = boundReviewedLessons(agent.privateLessons);
1707
- if (reviewedLessons.length > 0) {
1708
- parts.push(
1709
- `# Reviewed lessons
1710
-
1711
- 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.
1610
+ };
1712
1611
 
1713
- ${reviewedLessons.map((lesson) => `- ${lesson}`).join("\n")}`
1714
- );
1715
- }
1716
- parts.push(
1717
- "# 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.**"
1718
- );
1719
- return parts.join("\n\n---\n\n");
1720
- }
1721
- function boundReviewedLessons(lessons, maxCharacters = 8e3) {
1722
- const selected = [];
1723
- let used = 0;
1724
- for (const raw of lessons) {
1725
- const lesson = raw.trim();
1726
- if (!lesson) continue;
1727
- const remaining = maxCharacters - used;
1728
- if (remaining <= 0) break;
1729
- selected.push(lesson.slice(0, remaining));
1730
- used += Math.min(lesson.length, remaining);
1612
+ // ../daemon/src/runtime/coding-directory.ts
1613
+ function resolveCodingDirectory(root, cwd) {
1614
+ const directory = new ContextDirectory(root);
1615
+ try {
1616
+ let current = directory;
1617
+ for (const part of relativeParts(cwd)) current = current.directory(part);
1618
+ const path = realpathSync4(join15(root, cwd));
1619
+ directory.validate();
1620
+ return { path, identity: current.identity };
1621
+ } finally {
1622
+ directory.close();
1731
1623
  }
1732
- return selected;
1733
1624
  }
1734
1625
 
1735
- // ../daemon/src/runtime/shell/tooling.ts
1736
- import { existsSync as existsSync11, lstatSync as lstatSync3, realpathSync as realpathSync3 } from "fs";
1737
- import { dirname as dirname4, isAbsolute as isAbsolute4, join as join15, relative as relative4, sep as sep4 } from "path";
1738
- import {
1739
- createBashToolDefinition,
1740
- createLocalBashOperations,
1741
- defineTool as defineTool2
1742
- } from "@earendil-works/pi-coding-agent";
1743
-
1744
1626
  // ../daemon/src/runtime/shell/approval.ts
1745
1627
  import { defineTool } from "@earendil-works/pi-coding-agent";
1746
1628
 
@@ -1836,8 +1718,8 @@ function classifyBroadFilesystemOperations(command, risks, seen) {
1836
1718
  if (match.index === void 0 || !match[1]) continue;
1837
1719
  const nameOffset = match[0].toLowerCase().indexOf(match[1].toLowerCase());
1838
1720
  const start = match.index + nameOffset;
1839
- const text = command.slice(start, match.index + match[0].length).trimEnd();
1840
- const tokens = shellTokens(text);
1721
+ const text2 = command.slice(start, match.index + match[0].length).trimEnd();
1722
+ const tokens = shellTokens(text2);
1841
1723
  const name = tokens[0]?.toLowerCase();
1842
1724
  const args = tokens.slice(1);
1843
1725
  let risky = false;
@@ -1860,7 +1742,7 @@ function classifyBroadFilesystemOperations(command, risks, seen) {
1860
1742
  code: "broad-destructive-operation",
1861
1743
  message: "Recursively changes or deletes a broad filesystem target.",
1862
1744
  start,
1863
- end: start + text.length
1745
+ end: start + text2.length
1864
1746
  });
1865
1747
  }
1866
1748
  }
@@ -2076,12 +1958,12 @@ function assertEnvName(name) {
2076
1958
  }
2077
1959
  function buildScrubbedShellEnv(env, allowedKeys = []) {
2078
1960
  const scrubbed = {};
2079
- const keys = new Set(SAFE_SHELL_ENV_KEYS);
1961
+ const keys2 = new Set(SAFE_SHELL_ENV_KEYS);
2080
1962
  for (const key of allowedKeys) {
2081
1963
  assertEnvName(key);
2082
- keys.add(key);
1964
+ keys2.add(key);
2083
1965
  }
2084
- for (const key of keys) {
1966
+ for (const key of keys2) {
2085
1967
  const value = env[key];
2086
1968
  if (value !== void 0) scrubbed[key] = value;
2087
1969
  }
@@ -2098,22 +1980,19 @@ var BashApprovalDeniedError = class extends Error {
2098
1980
  this.risks = risks;
2099
1981
  }
2100
1982
  };
1983
+ async function requireBashApproval(command, toolCallId, signal, host) {
1984
+ const risks = classifyBashCommand(command);
1985
+ if (!risks.length) return;
1986
+ const decision = host ? await host.requestApproval({ toolCallId, command, risks, signal }) : "denied";
1987
+ if (decision !== "approved") throw new BashApprovalDeniedError(risks);
1988
+ }
2101
1989
  function createApprovalGatedBashTool(base, host) {
2102
1990
  return defineTool({
2103
1991
  ...base,
2104
1992
  description: `${base.description} Commands classified as dangerous require operator approval before execution.`,
2105
1993
  executionMode: "sequential",
2106
1994
  async execute(toolCallId, params, signal, onUpdate, ctx) {
2107
- const risks = classifyBashCommand(params.command);
2108
- if (risks.length > 0) {
2109
- const decision = host ? await host.requestApproval({
2110
- toolCallId,
2111
- command: params.command,
2112
- risks,
2113
- signal
2114
- }) : "denied";
2115
- if (decision !== "approved") throw new BashApprovalDeniedError(risks);
2116
- }
1995
+ await requireBashApproval(params.command, toolCallId, signal, host);
2117
1996
  return base.execute(toolCallId, params, signal, onUpdate, ctx);
2118
1997
  }
2119
1998
  });
@@ -2122,10 +2001,35 @@ function createApprovalGatedBashTool(base, host) {
2122
2001
  // ../daemon/src/runtime/shell/docker.ts
2123
2002
  import { spawn } from "child_process";
2124
2003
  import { randomUUID as randomUUID18 } from "crypto";
2125
- import { closeSync as closeSync3, constants as constants3, mkdtempSync as mkdtempSync2, openSync as openSync3, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
2004
+ import { closeSync as closeSync4, constants as constants4, mkdtempSync as mkdtempSync2, openSync as openSync4, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
2126
2005
  import { access, lstat, realpath, stat } from "fs/promises";
2127
2006
  import { tmpdir as tmpdir2 } from "os";
2128
- import { delimiter, isAbsolute as isAbsolute3, join as join14, relative as relative3, sep as sep3 } from "path";
2007
+ import { delimiter, isAbsolute as isAbsolute3, join as join16, relative as relative2, sep as sep3 } from "path";
2008
+
2009
+ // ../daemon/src/runtime/shell/coding.ts
2010
+ function validateDockerCodingSelection(input) {
2011
+ if (!input || typeof input !== "object" || Array.isArray(input))
2012
+ throw new Error("Invalid Docker coding selection");
2013
+ const value = input;
2014
+ if (Object.keys(value).some((key) => !["revision", "cwd", "env"].includes(key)) || !Number.isSafeInteger(value.revision) || value.revision < 0)
2015
+ throw new Error("Invalid Docker coding revision");
2016
+ const cwd = codingRelativePath(value.cwd);
2017
+ if (!value.env || typeof value.env !== "object" || Array.isArray(value.env))
2018
+ throw new Error("Invalid Docker coding environment");
2019
+ const env = {};
2020
+ for (const [key, entry] of Object.entries(value.env)) {
2021
+ if (key === "CI" && (entry === "true" || entry === "false")) env.CI = entry;
2022
+ else if (key === "NO_COLOR" && (entry === "0" || entry === "1")) env.NO_COLOR = entry;
2023
+ else if (key === "TZ" && entry === "UTC") env.TZ = entry;
2024
+ else throw new Error("Invalid Docker coding environment");
2025
+ }
2026
+ return { revision: value.revision, cwd, env };
2027
+ }
2028
+ function codingContainerCwd(cwd) {
2029
+ return cwd === "." ? "/workspace" : `/workspace/${codingRelativePath(cwd)}`;
2030
+ }
2031
+
2032
+ // ../daemon/src/runtime/shell/docker.ts
2129
2033
  var CONTAINER_WORKDIR = "/workspace";
2130
2034
  var MAX_TIMEOUT_MS = 2147483647;
2131
2035
  var MAX_STDERR_CHARS = 16 * 1024;
@@ -2133,6 +2037,7 @@ var CLEANUP_TIMEOUT_MS = 650;
2133
2037
  var CLEANUP_RETRY_DELAY_MS = 100;
2134
2038
  var CLEANUP_ATTEMPTS = 3;
2135
2039
  var DOCKER_CONTEXT_TIMEOUT_MS = 2e3;
2040
+ var DOCKER_CREATE_TIMEOUT_MS = 5e3;
2136
2041
  var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
2137
2042
  var CONTAINER_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]+$/;
2138
2043
  var DOCKER_CLIENT_ENV_KEYS = [
@@ -2205,7 +2110,7 @@ function buildDockerRunSpec(input) {
2205
2110
  "--entrypoint",
2206
2111
  "/bin/bash",
2207
2112
  "--workdir",
2208
- CONTAINER_WORKDIR,
2113
+ codingContainerCwd(input.codingCwd ?? "."),
2209
2114
  // A bind mount is read/write unless `readonly` is present. No other mount is added.
2210
2115
  "--mount",
2211
2116
  `type=bind,source=${input.mountSource},target=${CONTAINER_WORKDIR},bind-recursive=disabled`
@@ -2251,7 +2156,7 @@ function buildDockerRunSpec(input) {
2251
2156
  ` } : {}
2252
2157
  };
2253
2158
  }
2254
- function createPreparedDockerBashOperations(runtime) {
2159
+ function createPreparedDockerBashOperations(runtime, lifecycle) {
2255
2160
  validatePreparedDockerRuntime(runtime);
2256
2161
  return {
2257
2162
  async exec(command, cwd, { onData, signal, timeout }) {
@@ -2268,9 +2173,10 @@ function createPreparedDockerBashOperations(runtime) {
2268
2173
  await assertDockerExecutableIdentity(runtime.dockerPath, runtime.executableIdentity);
2269
2174
  await assertLocalDockerSocket(runtime.endpoint);
2270
2175
  const dockerClientEnv = { DOCKER_HOST: runtime.endpoint };
2176
+ if (runtime.coding) resolveCodingDirectory(workspace.source, runtime.coding.cwd);
2271
2177
  const currentImageId = await inspectSandboxImage(
2272
2178
  runtime.dockerPath,
2273
- runtime.image,
2179
+ runtime.coding ? runtime.imageId : runtime.image,
2274
2180
  dockerClientEnv
2275
2181
  );
2276
2182
  if (currentImageId !== runtime.imageId) {
@@ -2278,11 +2184,12 @@ function createPreparedDockerBashOperations(runtime) {
2278
2184
  }
2279
2185
  await assertDockerExecutableIdentity(runtime.dockerPath, runtime.executableIdentity);
2280
2186
  if (signal?.aborted) throw new Error("aborted");
2281
- const containerName = `bazilion-bash-${process.pid}-${randomUUID18().replaceAll("-", "")}`;
2187
+ const containerName = lifecycle?.name("bash") ?? `bazilion-bash-${process.pid}-${randomUUID18().replaceAll("-", "")}`;
2282
2188
  const spec = buildDockerRunSpec({
2283
2189
  image: runtime.image,
2284
2190
  resolvedImage: runtime.imageId,
2285
- env: { ...runtime.containerEnv },
2191
+ env: { ...runtime.containerEnv, ...runtime.coding?.env },
2192
+ codingCwd: runtime.coding?.cwd,
2286
2193
  dockerPath: runtime.dockerPath,
2287
2194
  hostEnv: dockerClientEnv,
2288
2195
  command,
@@ -2292,29 +2199,129 @@ function createPreparedDockerBashOperations(runtime) {
2292
2199
  uid: runtime.uid,
2293
2200
  gid: runtime.gid
2294
2201
  });
2295
- return executeDockerRun(spec, { onData, signal, timeout, timeoutMs });
2202
+ await lifecycle?.beforeCreate({
2203
+ dockerPath: runtime.dockerPath,
2204
+ executableIdentity: runtime.executableIdentity,
2205
+ endpoint: runtime.endpoint,
2206
+ containerName
2207
+ });
2208
+ try {
2209
+ return await executeRegisteredDockerRun(
2210
+ spec,
2211
+ { onData, signal, timeout, timeoutMs },
2212
+ lifecycle
2213
+ );
2214
+ } finally {
2215
+ await lifecycle?.afterRemove(containerName);
2216
+ }
2296
2217
  }
2297
2218
  };
2298
2219
  }
2299
- async function recheckProtectedMount(mount) {
2300
- const sourceInfo = await lstat(mount.source);
2301
- if (sourceInfo.isSymbolicLink() || !sourceInfo.isDirectory()) {
2302
- throw new Error("Docker sandbox mount no longer matches preflight");
2303
- }
2304
- const sourceRoot = await resolveRequiredRoot(mount.sourceRoot);
2305
- const source = await resolveMountSource(mount.source);
2306
- if (sourceRoot !== mount.sourceRoot || source !== mount.source || !isWithin(sourceRoot, source)) {
2307
- throw new Error("Docker sandbox mount no longer matches preflight");
2308
- }
2220
+ function runProtectedContainerCreate(spec, options) {
2221
+ if (options?.signal?.aborted) return Promise.reject(new Error("aborted"));
2222
+ let envFileFd;
2223
+ try {
2224
+ if (spec.envFileContent !== void 0) {
2225
+ envFileFd = openAnonymousEnvFile(spec.envFileContent);
2226
+ }
2227
+ } catch (error) {
2228
+ return Promise.reject(
2229
+ new Error(
2230
+ `Docker sandbox could not prepare its preflight environment: ${error instanceof Error ? error.message : String(error)}`
2231
+ )
2232
+ );
2233
+ }
2234
+ return new Promise((resolve6, reject) => {
2235
+ let child;
2236
+ try {
2237
+ child = spawn(spec.executable, ["container", "create", ...spec.args.slice(1)], {
2238
+ stdio: ["ignore", "ignore", "pipe", envFileFd ?? "ignore"],
2239
+ windowsHide: true,
2240
+ env: spec.processEnv
2241
+ });
2242
+ } catch (error) {
2243
+ if (envFileFd !== void 0) closeSync4(envFileFd);
2244
+ reject(error instanceof Error ? formatSpawnError(spec.executable, error) : error);
2245
+ return;
2246
+ }
2247
+ if (envFileFd !== void 0) closeSync4(envFileFd);
2248
+ let settled = false;
2249
+ let stderr = "";
2250
+ let timeoutHandle;
2251
+ let killSettleHandle;
2252
+ let termination;
2253
+ const finish = (error) => {
2254
+ if (settled) return;
2255
+ settled = true;
2256
+ if (timeoutHandle) clearTimeout(timeoutHandle);
2257
+ if (killSettleHandle) clearTimeout(killSettleHandle);
2258
+ options?.signal?.removeEventListener("abort", onAbort);
2259
+ if (error) reject(error);
2260
+ else resolve6();
2261
+ };
2262
+ const commandTimeout = options?.timeoutMs !== void 0 && options.timeoutMs <= DOCKER_CREATE_TIMEOUT_MS;
2263
+ const timeoutError = () => commandTimeout ? new Error(`timeout:${options?.timeout}`) : new Error(
2264
+ `Docker sandbox could not create its protected preflight container within ${DOCKER_CREATE_TIMEOUT_MS}ms`
2265
+ );
2266
+ child.stderr?.on("data", (data) => {
2267
+ stderr = `${stderr}${data.toString("utf8")}`.slice(-MAX_STDERR_CHARS);
2268
+ });
2269
+ child.once("error", (error) => finish(formatSpawnError(spec.executable, error)));
2270
+ child.once("close", (code) => {
2271
+ if (termination) finish(termination);
2272
+ else if (code === 0) finish();
2273
+ else finish(formatDockerCreateError(spec, stderr, code));
2274
+ });
2275
+ const terminate = (error) => {
2276
+ if (settled || termination) return;
2277
+ termination = error;
2278
+ child.kill("SIGKILL");
2279
+ killSettleHandle = setTimeout(() => finish(error), CLEANUP_TIMEOUT_MS);
2280
+ };
2281
+ const onAbort = () => terminate(new Error("aborted"));
2282
+ timeoutHandle = setTimeout(
2283
+ () => terminate(timeoutError()),
2284
+ Math.min(options?.timeoutMs ?? DOCKER_CREATE_TIMEOUT_MS, DOCKER_CREATE_TIMEOUT_MS)
2285
+ );
2286
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
2287
+ if (options?.signal?.aborted) onAbort();
2288
+ });
2289
+ }
2290
+ async function recheckProtectedMount(mount) {
2291
+ const sourceInfo = await lstat(mount.source);
2292
+ if (sourceInfo.isSymbolicLink() || !sourceInfo.isDirectory()) {
2293
+ throw new Error("Docker sandbox mount no longer matches preflight");
2294
+ }
2295
+ const sourceRoot = await resolveRequiredRoot(mount.sourceRoot);
2296
+ const source = await resolveMountSource(mount.source);
2297
+ if (sourceRoot !== mount.sourceRoot || source !== mount.source || !isWithin(sourceRoot, source)) {
2298
+ throw new Error("Docker sandbox mount no longer matches preflight");
2299
+ }
2309
2300
  return { ...mount, source, sourceRoot };
2310
2301
  }
2311
2302
  async function resolveRequiredRoot(root) {
2312
2303
  if (!isAbsolute3(root)) throw new Error("Docker sandbox mount root must be absolute");
2313
2304
  return resolveMountSource(root);
2314
2305
  }
2306
+ async function resolveAbsoluteDockerPath(requested, hostEnv) {
2307
+ validateDockerPath(requested);
2308
+ const candidates = isAbsolute3(requested) ? [requested] : (hostEnv.PATH ?? "").split(delimiter).filter(Boolean).map((dir) => join16(dir, requested));
2309
+ for (const candidate of candidates) {
2310
+ try {
2311
+ await access(candidate, constants4.X_OK);
2312
+ const info = await stat(candidate);
2313
+ if (!info.isFile()) continue;
2314
+ const canonical = await realpath(candidate);
2315
+ if (!isAbsolute3(canonical)) continue;
2316
+ return canonical;
2317
+ } catch {
2318
+ }
2319
+ }
2320
+ throw new Error(`Docker sandbox executable is unavailable: ${requested}`);
2321
+ }
2315
2322
  async function inspectDockerExecutableIdentity(dockerPath) {
2316
2323
  try {
2317
- await access(dockerPath, constants3.X_OK);
2324
+ await access(dockerPath, constants4.X_OK);
2318
2325
  const info = await stat(dockerPath, { bigint: true });
2319
2326
  if (!info.isFile()) throw new Error("not a regular file");
2320
2327
  return {
@@ -2349,12 +2356,13 @@ async function assertLocalDockerSocket(endpoint) {
2349
2356
  try {
2350
2357
  const info = await stat(socketPath);
2351
2358
  if (!info.isSocket()) throw new Error("not a Unix socket");
2352
- await access(socketPath, constants3.R_OK | constants3.W_OK);
2359
+ await access(socketPath, constants4.R_OK | constants4.W_OK);
2353
2360
  } catch {
2354
2361
  throw new Error("Docker sandbox local Unix socket is unavailable");
2355
2362
  }
2356
2363
  }
2357
2364
  function validatePreparedDockerRuntime(runtime) {
2365
+ if (runtime.coding) validateDockerCodingSelection(runtime.coding);
2358
2366
  if (!isAbsolute3(runtime.dockerPath)) {
2359
2367
  throw new Error("protected Docker executable must be absolute");
2360
2368
  }
@@ -2372,17 +2380,17 @@ function validatePreparedDockerRuntime(runtime) {
2372
2380
  throw new Error("protected Docker container environment does not match policy");
2373
2381
  }
2374
2382
  }
2375
- function validateDockerExecutableIdentity(identity) {
2376
- if (typeof identity !== "object" || identity === null) {
2383
+ function validateDockerExecutableIdentity(identity2) {
2384
+ if (typeof identity2 !== "object" || identity2 === null) {
2377
2385
  throw new Error("protected Docker executable identity is invalid");
2378
2386
  }
2379
2387
  const fields = [
2380
- identity.device,
2381
- identity.inode,
2382
- identity.mode,
2383
- identity.size,
2384
- identity.modifiedTimeNs,
2385
- identity.changedTimeNs
2388
+ identity2.device,
2389
+ identity2.inode,
2390
+ identity2.mode,
2391
+ identity2.size,
2392
+ identity2.modifiedTimeNs,
2393
+ identity2.changedTimeNs
2386
2394
  ];
2387
2395
  if (fields.some((field) => typeof field !== "string" || !/^\d+$/.test(field))) {
2388
2396
  throw new Error("protected Docker executable identity is invalid");
@@ -2417,7 +2425,7 @@ function createDockerBashOperations(options) {
2417
2425
  const dockerClientEnv = await resolveLocalDockerClientEnv(dockerPath, hostEnv);
2418
2426
  const resolvedImage = await inspectSandboxImage(dockerPath, image, dockerClientEnv);
2419
2427
  if (signal?.aborted) throw new Error("aborted");
2420
- const containerName = `bazilion-bash-${process.pid}-${randomUUID18().replaceAll("-", "")}`;
2428
+ const containerName = options.lifecycle?.name("bash") ?? `bazilion-bash-${process.pid}-${randomUUID18().replaceAll("-", "")}`;
2421
2429
  const spec = buildDockerRunSpec({
2422
2430
  image,
2423
2431
  env,
@@ -2431,10 +2439,44 @@ function createDockerBashOperations(options) {
2431
2439
  readOnlyMounts,
2432
2440
  hostEnv: dockerClientEnv
2433
2441
  });
2434
- return executeDockerRun(spec, { onData, signal, timeout, timeoutMs });
2442
+ if (options.lifecycle) {
2443
+ const absolute = await resolveAbsoluteDockerPath(dockerPath, hostEnv);
2444
+ await options.lifecycle.beforeCreate({
2445
+ dockerPath: absolute,
2446
+ executableIdentity: await inspectDockerExecutableIdentity(absolute),
2447
+ endpoint: dockerClientEnv.DOCKER_HOST,
2448
+ containerName
2449
+ });
2450
+ }
2451
+ try {
2452
+ return await executeRegisteredDockerRun(
2453
+ spec,
2454
+ { onData, signal, timeout, timeoutMs },
2455
+ options.lifecycle
2456
+ );
2457
+ } finally {
2458
+ await options.lifecycle?.afterRemove(containerName);
2459
+ }
2435
2460
  }
2436
2461
  };
2437
2462
  }
2463
+ async function executeRegisteredDockerRun(spec, options, lifecycle) {
2464
+ if (!lifecycle) return executeDockerRun(spec, options);
2465
+ const deadline = options.timeoutMs === void 0 ? void 0 : Date.now() + options.timeoutMs;
2466
+ await runProtectedContainerCreate(spec, options);
2467
+ await lifecycle.afterCreate(spec.containerName);
2468
+ if (options.signal?.aborted) throw new Error("aborted");
2469
+ const remaining = deadline === void 0 ? void 0 : deadline - Date.now();
2470
+ if (remaining !== void 0 && remaining <= 0) throw new Error(`timeout:${options.timeout}`);
2471
+ return executeDockerRun(
2472
+ {
2473
+ ...spec,
2474
+ args: ["container", "start", "--attach", spec.containerName],
2475
+ envFileContent: void 0
2476
+ },
2477
+ { ...options, timeoutMs: remaining }
2478
+ );
2479
+ }
2438
2480
  function executeDockerRun(spec, options) {
2439
2481
  let envFileFd;
2440
2482
  try {
@@ -2454,7 +2496,7 @@ function executeDockerRun(spec, options) {
2454
2496
  windowsHide: true,
2455
2497
  env: spec.processEnv
2456
2498
  });
2457
- if (envFileFd !== void 0) closeSync3(envFileFd);
2499
+ if (envFileFd !== void 0) closeSync4(envFileFd);
2458
2500
  let settled = false;
2459
2501
  let termination;
2460
2502
  let timeoutHandle;
@@ -2510,16 +2552,16 @@ function executeDockerRun(spec, options) {
2510
2552
  });
2511
2553
  }
2512
2554
  function openAnonymousEnvFile(content) {
2513
- const dir = mkdtempSync2(join14(tmpdir2(), "bazilion-docker-env-"));
2514
- const path = join14(dir, "environment");
2555
+ const dir = mkdtempSync2(join16(tmpdir2(), "bazilion-docker-env-"));
2556
+ const path = join16(dir, "environment");
2515
2557
  let fd;
2516
2558
  try {
2517
2559
  writeFileSync7(path, content, { encoding: "utf8", flag: "wx", mode: 384 });
2518
- fd = openSync3(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
2560
+ fd = openSync4(path, constants4.O_RDONLY | constants4.O_NOFOLLOW);
2519
2561
  rmSync10(dir, { recursive: true, force: true });
2520
2562
  return fd;
2521
2563
  } catch (error) {
2522
- if (fd !== void 0) closeSync3(fd);
2564
+ if (fd !== void 0) closeSync4(fd);
2523
2565
  rmSync10(dir, { recursive: true, force: true });
2524
2566
  throw error;
2525
2567
  }
@@ -2537,7 +2579,7 @@ function resolveTimeoutMs(timeout) {
2537
2579
  }
2538
2580
  async function resolveMountSource(cwd) {
2539
2581
  try {
2540
- await access(cwd, constants3.F_OK);
2582
+ await access(cwd, constants4.F_OK);
2541
2583
  const info = await stat(cwd);
2542
2584
  if (!info.isDirectory()) {
2543
2585
  throw new Error(`Docker sandbox mount is not a directory: ${cwd}`);
@@ -2762,7 +2804,7 @@ function validateReadOnlyMounts(mounts, workspaceSource) {
2762
2804
  }
2763
2805
  }
2764
2806
  function isWithin(root, candidate) {
2765
- const rel = relative3(root, candidate);
2807
+ const rel = relative2(root, candidate);
2766
2808
  return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute3(rel);
2767
2809
  }
2768
2810
  function validateHostIdentity(uid, gid) {
@@ -2770,91 +2812,865 @@ function validateHostIdentity(uid, gid) {
2770
2812
  throw new Error(`Docker sandbox received an invalid host uid/gid: ${uid}:${gid}`);
2771
2813
  }
2772
2814
  }
2773
- function formatSpawnError(dockerPath, error) {
2774
- const code = error.code;
2775
- if (code === "ENOENT") {
2776
- return new Error(`Docker sandbox unavailable: executable "${dockerPath}" was not found`);
2777
- }
2778
- if (code === "EACCES") {
2779
- return new Error(`Docker sandbox unavailable: executable "${dockerPath}" is not executable`);
2815
+ function formatSpawnError(dockerPath, error) {
2816
+ const code = error.code;
2817
+ if (code === "ENOENT") {
2818
+ return new Error(`Docker sandbox unavailable: executable "${dockerPath}" was not found`);
2819
+ }
2820
+ if (code === "EACCES") {
2821
+ return new Error(`Docker sandbox unavailable: executable "${dockerPath}" is not executable`);
2822
+ }
2823
+ return new Error(`Docker sandbox could not start "${dockerPath}": ${error.message}`);
2824
+ }
2825
+ function formatDockerRunError(spec, stderr) {
2826
+ const detail3 = stderr.trim() || "Docker exited with status 125 without an error message";
2827
+ if (/no such image|unable to find image|manifest unknown|pull access denied/i.test(detail3)) {
2828
+ return new Error(`Docker sandbox image "${spec.image}" is unavailable: ${detail3}`);
2829
+ }
2830
+ if (/mounts denied|invalid mount config|bind source path does not exist|mount.*failed/i.test(detail3)) {
2831
+ return new Error(`Docker sandbox mount failed for "${spec.mountSource}": ${detail3}`);
2832
+ }
2833
+ if (/cannot connect to the docker daemon|is the docker daemon running/i.test(detail3)) {
2834
+ return new Error(`Docker sandbox unavailable: ${detail3}`);
2835
+ }
2836
+ return new Error(`Docker sandbox failed to start container "${spec.containerName}": ${detail3}`);
2837
+ }
2838
+ function formatDockerCreateError(spec, stderr, exitCode) {
2839
+ const detail3 = stderr.trim() || `Docker exited with ${exitCode === null ? "no status" : `status ${exitCode}`} without an error message`;
2840
+ if (/mounts denied|invalid mount config|bind source path does not exist|mount.*failed/i.test(detail3)) {
2841
+ return new Error(`Docker sandbox preflight mount failed for "${spec.mountSource}": ${detail3}`);
2842
+ }
2843
+ if (/cannot connect to the docker daemon|is the docker daemon running/i.test(detail3)) {
2844
+ return new Error(`Docker sandbox preflight could not reach the local Docker daemon: ${detail3}`);
2845
+ }
2846
+ return new Error(`Docker sandbox preflight could not create the required container: ${detail3}`);
2847
+ }
2848
+ async function removeContainer(dockerPath, containerName, processEnv) {
2849
+ const args = ["container", "rm", "--force", containerName];
2850
+ let last;
2851
+ for (let attempt = 0; attempt < CLEANUP_ATTEMPTS; attempt += 1) {
2852
+ last = await runCleanupCommand(dockerPath, args, processEnv);
2853
+ if (last.ok) return;
2854
+ if (attempt < CLEANUP_ATTEMPTS - 1) {
2855
+ await new Promise((resolve6) => setTimeout(resolve6, CLEANUP_RETRY_DELAY_MS));
2856
+ }
2857
+ }
2858
+ if (last && !last.missing) {
2859
+ console.warn(
2860
+ `Docker sandbox could not confirm cleanup of container "${containerName}": ${last.error || "cleanup command failed"}`
2861
+ );
2862
+ }
2863
+ }
2864
+ function runCleanupCommand(dockerPath, args, processEnv) {
2865
+ return new Promise((resolve6) => {
2866
+ let done = false;
2867
+ let stderr = "";
2868
+ const child = spawn(dockerPath, args, {
2869
+ stdio: ["ignore", "ignore", "pipe"],
2870
+ windowsHide: true,
2871
+ env: processEnv
2872
+ });
2873
+ const finish = (ok, error = "") => {
2874
+ if (done) return;
2875
+ done = true;
2876
+ clearTimeout(timeoutHandle);
2877
+ const detail3 = error || stderr.trim();
2878
+ resolve6({
2879
+ ok,
2880
+ missing: /no such container/i.test(detail3),
2881
+ error: detail3
2882
+ });
2883
+ };
2884
+ const timeoutHandle = setTimeout(() => {
2885
+ child.kill("SIGKILL");
2886
+ finish(false, `cleanup timed out after ${CLEANUP_TIMEOUT_MS}ms`);
2887
+ }, CLEANUP_TIMEOUT_MS);
2888
+ child.stderr.on("data", (data) => {
2889
+ stderr = `${stderr}${data.toString("utf8")}`.slice(-2048);
2890
+ });
2891
+ child.once("error", (error) => finish(false, error.message));
2892
+ child.once("close", (code) => finish(code === 0));
2893
+ });
2894
+ }
2895
+
2896
+ // ../daemon/src/runtime/pi/coding.ts
2897
+ function codingTools(input) {
2898
+ const hostOperations = input.docker ? null : createLocalBashOperations();
2899
+ return [
2900
+ {
2901
+ name: "coding_environment",
2902
+ label: "Inspect coding environment",
2903
+ 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.",
2904
+ parameters: Type.Object(
2905
+ { target: Type.String({ default: ".", maxLength: 4096 }) },
2906
+ { additionalProperties: false }
2907
+ ),
2908
+ executionMode: "sequential",
2909
+ async execute(_id, params) {
2910
+ const value = await input.host.invoke({
2911
+ action: "environment",
2912
+ target: params.target
2913
+ });
2914
+ return {
2915
+ content: [{ type: "text", text: JSON.stringify(value) }],
2916
+ details: { codingEnvironment: value }
2917
+ };
2918
+ }
2919
+ },
2920
+ {
2921
+ name: "coding_command",
2922
+ label: "Run coding command",
2923
+ 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.",
2924
+ parameters: Type.Object(
2925
+ {
2926
+ command: Type.String({ minLength: 1, maxLength: 4096 }),
2927
+ cwd: Type.String({ maxLength: 4096 }),
2928
+ purpose: Type.Union(
2929
+ ["runtime", "dependency", "prepare", "build", "test"].map(
2930
+ (value) => Type.Literal(value)
2931
+ )
2932
+ ),
2933
+ timeoutSeconds: Type.Integer({ minimum: 1, maximum: 300 })
2934
+ },
2935
+ { additionalProperties: false }
2936
+ ),
2937
+ executionMode: "sequential",
2938
+ async execute(toolCallId, params, signal) {
2939
+ const command = validateCodingCommand(params);
2940
+ const context = await input.context(command.cwd);
2941
+ if (context.instructions.state !== "complete")
2942
+ throw new Error("Coding scope instructions are incomplete");
2943
+ const started = await input.host.invoke({
2944
+ action: "start",
2945
+ toolCallId,
2946
+ input: command
2947
+ });
2948
+ const output = new CodingDiagnostics(input.secrets ?? []);
2949
+ let outcome;
2950
+ try {
2951
+ if (input.approval)
2952
+ await requireBashApproval(command.command, toolCallId, signal, input.approvalHost);
2953
+ signal?.throwIfAborted();
2954
+ const cwd = resolveCodingDirectory(input.root, command.cwd);
2955
+ const operations = input.docker ? createPreparedDockerBashOperations(
2956
+ {
2957
+ ...input.docker,
2958
+ coding: {
2959
+ revision: input.docker.coding?.revision ?? 0,
2960
+ cwd: command.cwd,
2961
+ env: input.docker.coding?.env ?? {}
2962
+ }
2963
+ },
2964
+ input.lifecycle
2965
+ ) : hostOperations;
2966
+ const result = await operations.exec(
2967
+ command.command,
2968
+ input.docker ? input.root : cwd.path,
2969
+ { onData: (bytes) => output.append(bytes), signal, timeout: command.timeoutSeconds }
2970
+ );
2971
+ outcome = {
2972
+ state: result.exitCode === 0 ? "succeeded" : result.exitCode === null ? "interrupted" : "failed",
2973
+ exitCode: result.exitCode,
2974
+ ...output.finish(),
2975
+ reason: result.exitCode === null ? "exit_unavailable" : null
2976
+ };
2977
+ } catch (error) {
2978
+ const message = error instanceof Error ? error.message : "Command execution unavailable";
2979
+ const state = signal?.aborted || message === "aborted" ? "cancelled" : message.startsWith("timeout:") ? "timed_out" : error instanceof BashApprovalDeniedError ? "blocked" : /cleanup|teardown/i.test(message) ? "interrupted" : "blocked";
2980
+ outcome = { state, exitCode: null, ...output.finish(), reason: message };
2981
+ }
2982
+ const receipt = await input.host.invoke({
2983
+ action: "finish",
2984
+ id: started.id,
2985
+ outcome
2986
+ });
2987
+ return {
2988
+ content: [{ type: "text", text: JSON.stringify(receipt) }],
2989
+ details: { codingReceipt: receipt }
2990
+ };
2991
+ }
2992
+ },
2993
+ {
2994
+ name: "coding_receipt",
2995
+ label: "Read coding result",
2996
+ 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.",
2997
+ parameters: Type.Object(
2998
+ {
2999
+ id: Type.String({ maxLength: 64 }),
3000
+ messageId: Type.Optional(Type.String({ maxLength: 64 }))
3001
+ },
3002
+ { additionalProperties: false }
3003
+ ),
3004
+ executionMode: "sequential",
3005
+ async execute(_id, params) {
3006
+ const value = await input.host.invoke({
3007
+ action: "read",
3008
+ ...params
3009
+ });
3010
+ return {
3011
+ content: [{ type: "text", text: JSON.stringify(value) }],
3012
+ details: { codingEvidence: value }
3013
+ };
3014
+ }
3015
+ }
3016
+ ];
3017
+ }
3018
+
3019
+ // ../daemon/src/runtime/pi/repository-context.ts
3020
+ import { Type as Type2 } from "typebox";
3021
+
3022
+ // ../daemon/src/runtime/pi/repository-context-contract.ts
3023
+ var object2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3024
+ var keys = (value, required, optional = []) => required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
3025
+ var string = (value, limit = 4096) => typeof value === "string" && value.length <= limit;
3026
+ var digest = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
3027
+ var count = (value) => Number.isSafeInteger(value) && value >= 0;
3028
+ var list3 = (value, limit, check) => Array.isArray(value) && value.length <= limit && value.every(check);
3029
+ 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));
3030
+ function assertRepositoryContext(value, teamId) {
3031
+ if (!object2(value) || !keys(value, [
3032
+ "version",
3033
+ "teamId",
3034
+ "rootIdentity",
3035
+ "target",
3036
+ "capturedAt",
3037
+ "fingerprint",
3038
+ "instructions",
3039
+ "commands",
3040
+ "git"
3041
+ ]) || 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)
3042
+ throw new Error("Invalid repository context report");
3043
+ const instructions = value.instructions;
3044
+ const commands = value.commands;
3045
+ const git = value.git;
3046
+ if (!keys(instructions, ["state", "issues", "files"]) || !keys(commands, ["state", "issues", "sources", "candidates"]) || !keys(git, [
3047
+ "state",
3048
+ "root",
3049
+ "branch",
3050
+ "head",
3051
+ "headState",
3052
+ "staged",
3053
+ "unstaged",
3054
+ "untracked",
3055
+ "conflicted",
3056
+ "issues"
3057
+ ]) || !["complete", "incomplete"].includes(String(instructions.state)) || !list3(instructions.issues, 128, issue) || !list3(
3058
+ instructions.files,
3059
+ 17,
3060
+ (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
3061
+ ) || !["complete", "incomplete"].includes(String(commands.state)) || !list3(commands.issues, 128, issue) || !list3(
3062
+ commands.sources,
3063
+ 32,
3064
+ (source) => object2(source) && keys(source, ["path", "sha256"]) && string(source.path) && digest(source.sha256)
3065
+ ) || !list3(
3066
+ commands.candidates,
3067
+ 32,
3068
+ (candidate) => object2(candidate) && keys(candidate, [
3069
+ "source",
3070
+ "sha256",
3071
+ "location",
3072
+ "cwd",
3073
+ "command",
3074
+ "kind",
3075
+ "packageManager"
3076
+ ]) && 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)
3077
+ ) || !["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))
3078
+ throw new Error("Invalid repository context report");
3079
+ const report = value;
3080
+ 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))
3081
+ throw new Error("Invalid repository instruction completeness");
3082
+ }
3083
+
3084
+ // ../daemon/src/runtime/pi/repository-context.ts
3085
+ function repositoryContextIntegration(initial, host, cwd) {
3086
+ assertRepositoryContext(initial);
3087
+ if (initial.instructions.state !== "complete")
3088
+ throw new Error("Repository instructions incomplete");
3089
+ let current = initial;
3090
+ let refreshPrompt = () => {
3091
+ };
3092
+ const files = () => ({
3093
+ agentsFiles: [
3094
+ {
3095
+ path: "Repository instructions and provenance",
3096
+ content: [
3097
+ "# Repository context",
3098
+ `Captured target: ${current.target}; fingerprint: ${current.fingerprint}; captured at: ${new Date(current.capturedAt).toISOString()}.`,
3099
+ `Repository root for this runtime: ${cwd}. This is captured context, not a live workspace lock or verification evidence.`,
3100
+ "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.",
3101
+ "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.",
3102
+ "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.",
3103
+ 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.`,
3104
+ ...current.instructions.state === "complete" ? current.instructions.files.map(
3105
+ (file) => `## ${file.path}
3106
+ Scope: ${file.scope}; precedence: ${file.precedence}; SHA-256: ${file.sha256}
3107
+
3108
+ ${file.content}`
3109
+ ) : [],
3110
+ `Git orientation at capture time: ${JSON.stringify(current.git)}`,
3111
+ `Command suggestions (task data, not execution permission):
3112
+ ${JSON.stringify(current.commands)}`
3113
+ ].join("\n\n")
3114
+ }
3115
+ ]
3116
+ });
3117
+ const tool = {
3118
+ name: "repository_context",
3119
+ label: "Repository context",
3120
+ 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.",
3121
+ parameters: Type2.Object(
3122
+ { target: Type2.String({ maxLength: 4096 }) },
3123
+ { additionalProperties: false }
3124
+ ),
3125
+ executionMode: "sequential",
3126
+ async execute(_id, params) {
3127
+ const target = params.target;
3128
+ const replacement = await host(target);
3129
+ assertRepositoryContext(replacement);
3130
+ if (replacement.teamId !== initial.teamId || replacement.rootIdentity !== initial.rootIdentity) {
3131
+ current = {
3132
+ ...initial,
3133
+ instructions: {
3134
+ state: "incomplete",
3135
+ files: [],
3136
+ issues: [
3137
+ {
3138
+ code: "root_changed",
3139
+ message: "Repository identity changed; start a new admitted turn."
3140
+ }
3141
+ ]
3142
+ },
3143
+ commands: { state: "incomplete", sources: [], candidates: [], issues: [] }
3144
+ };
3145
+ refreshPrompt();
3146
+ throw new Error("Repository context identity changed; start a new admitted turn");
3147
+ }
3148
+ current = replacement;
3149
+ refreshPrompt();
3150
+ return {
3151
+ content: [{ type: "text", text: JSON.stringify(replacement) }],
3152
+ details: { repositoryContext: replacement }
3153
+ };
3154
+ }
3155
+ };
3156
+ return {
3157
+ files,
3158
+ tool,
3159
+ async resolve(target) {
3160
+ await tool.execute("coding-context", { target }, void 0, void 0, {});
3161
+ return current;
3162
+ },
3163
+ bind(session, allowedTools) {
3164
+ refreshPrompt = () => session.setActiveToolsByName(allowedTools);
3165
+ }
3166
+ };
3167
+ }
3168
+
3169
+ // ../daemon/src/runtime/pi/session.ts
3170
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7, statSync as statSync5, writeFileSync as writeFileSync9 } from "fs";
3171
+ import { basename as basename4, join as join22 } from "path";
3172
+ import {
3173
+ createAssistantMessageEventStream
3174
+ } from "@earendil-works/pi-ai";
3175
+ import {
3176
+ buildSessionContext,
3177
+ createAgentSession,
3178
+ DefaultResourceLoader,
3179
+ SessionManager,
3180
+ SettingsManager
3181
+ } from "@earendil-works/pi-coding-agent";
3182
+
3183
+ // ../daemon/src/runtime/providers/pi-runtime.ts
3184
+ import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
3185
+ import { ModelRuntime } from "@earendil-works/pi-coding-agent";
3186
+ var PROVIDER_ALIASES = {
3187
+ bedrock: "amazon-bedrock",
3188
+ "azure-openai": "azure-openai-responses"
3189
+ };
3190
+ var PROTECTED_PROVIDER_NAMES = [
3191
+ "anthropic",
3192
+ "openai",
3193
+ "openai-codex",
3194
+ "google",
3195
+ "google-vertex",
3196
+ "azure-openai",
3197
+ "bedrock",
3198
+ "mistral",
3199
+ "groq",
3200
+ "cerebras",
3201
+ "xai",
3202
+ "zai",
3203
+ "huggingface",
3204
+ "openrouter",
3205
+ "vercel-ai-gateway",
3206
+ "deepseek",
3207
+ "fireworks",
3208
+ "together",
3209
+ "baseten",
3210
+ "moonshotai",
3211
+ "moonshotai-cn",
3212
+ "kimi-coding",
3213
+ "minimax",
3214
+ "minimax-cn",
3215
+ "qwen-token-plan",
3216
+ "qwen-token-plan-cn",
3217
+ "qwen-token-plan-individual",
3218
+ "xiaomi",
3219
+ "xiaomi-token-plan-ams",
3220
+ "xiaomi-token-plan-cn",
3221
+ "xiaomi-token-plan-sgp",
3222
+ "ant-ling",
3223
+ "nvidia",
3224
+ "opencode",
3225
+ "opencode-go",
3226
+ "zai-coding-cn",
3227
+ "github-copilot",
3228
+ "cloudflare-ai-gateway",
3229
+ "cloudflare-workers-ai",
3230
+ "lmstudio",
3231
+ "ollama",
3232
+ "llamacpp"
3233
+ ];
3234
+ var LOCAL_PROVIDERS = {
3235
+ lmstudio: {
3236
+ baseUrl: "http://127.0.0.1:1234/v1",
3237
+ apiKey: "lm-studio",
3238
+ authHeader: false
3239
+ },
3240
+ ollama: { baseUrl: "http://127.0.0.1:11434/v1", apiKey: "ollama", authHeader: false },
3241
+ llamacpp: { baseUrl: "http://127.0.0.1:8080/v1", apiKey: "no-key", authHeader: true }
3242
+ };
3243
+ var PROVIDER_API_KEY_ENV = {
3244
+ anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
3245
+ openai: ["OPENAI_API_KEY"],
3246
+ google: ["GEMINI_API_KEY"],
3247
+ "azure-openai": ["AZURE_OPENAI_API_KEY"],
3248
+ mistral: ["MISTRAL_API_KEY"],
3249
+ groq: ["GROQ_API_KEY"],
3250
+ cerebras: ["CEREBRAS_API_KEY"],
3251
+ xai: ["XAI_API_KEY"],
3252
+ zai: ["ZAI_API_KEY"],
3253
+ huggingface: ["HF_TOKEN"],
3254
+ openrouter: ["OPENROUTER_API_KEY"],
3255
+ "vercel-ai-gateway": ["AI_GATEWAY_API_KEY"],
3256
+ deepseek: ["DEEPSEEK_API_KEY"],
3257
+ fireworks: ["FIREWORKS_API_KEY"],
3258
+ together: ["TOGETHER_API_KEY"],
3259
+ baseten: ["BASETEN_API_KEY"],
3260
+ moonshotai: ["MOONSHOT_API_KEY"],
3261
+ "moonshotai-cn": ["MOONSHOT_CN_API_KEY"],
3262
+ "kimi-coding": ["KIMI_API_KEY"],
3263
+ minimax: ["MINIMAX_API_KEY"],
3264
+ "minimax-cn": ["MINIMAX_CN_API_KEY"],
3265
+ "qwen-token-plan": ["QWEN_TOKEN_PLAN_API_KEY"],
3266
+ "qwen-token-plan-cn": ["QWEN_TOKEN_PLAN_CN_API_KEY"],
3267
+ "qwen-token-plan-individual": ["QWEN_TOKEN_PLAN_API_KEY"],
3268
+ xiaomi: ["XIAOMI_API_KEY"],
3269
+ "xiaomi-token-plan-ams": ["XIAOMI_TOKEN_PLAN_AMS_API_KEY"],
3270
+ "xiaomi-token-plan-cn": ["XIAOMI_TOKEN_PLAN_CN_API_KEY"],
3271
+ "xiaomi-token-plan-sgp": ["XIAOMI_TOKEN_PLAN_SGP_API_KEY"],
3272
+ "ant-ling": ["ANT_LING_API_KEY"],
3273
+ nvidia: ["NVIDIA_API_KEY"],
3274
+ opencode: ["OPENCODE_API_KEY"],
3275
+ "opencode-go": ["OPENCODE_GO_API_KEY"],
3276
+ "zai-coding-cn": ["ZAI_CODING_CN_API_KEY"],
3277
+ "github-copilot": ["COPILOT_GITHUB_TOKEN"],
3278
+ "cloudflare-ai-gateway": ["CLOUDFLARE_API_KEY"],
3279
+ "cloudflare-workers-ai": ["CLOUDFLARE_API_KEY"],
3280
+ lmstudio: ["LMSTUDIO_API_KEY"],
3281
+ ollama: ["OLLAMA_API_KEY"],
3282
+ llamacpp: ["LLAMACPP_API_KEY"]
3283
+ };
3284
+ var PROVIDER_CREDENTIAL_ENV = {
3285
+ bedrock: [
3286
+ "AWS_PROFILE",
3287
+ "AWS_BEARER_TOKEN_BEDROCK",
3288
+ "AWS_ACCESS_KEY_ID",
3289
+ "AWS_SECRET_ACCESS_KEY",
3290
+ "AWS_SESSION_TOKEN",
3291
+ "AWS_REGION",
3292
+ "AWS_DEFAULT_REGION"
3293
+ ],
3294
+ "google-vertex": [
3295
+ "GOOGLE_CLOUD_PROJECT",
3296
+ "GOOGLE_CLOUD_LOCATION",
3297
+ "GOOGLE_APPLICATION_CREDENTIALS"
3298
+ ],
3299
+ "cloudflare-ai-gateway": ["CLOUDFLARE_API_KEY", "CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_GATEWAY_ID"],
3300
+ "cloudflare-workers-ai": ["CLOUDFLARE_API_KEY", "CLOUDFLARE_ACCOUNT_ID"]
3301
+ };
3302
+ function piProviderName(providerName) {
3303
+ return PROVIDER_ALIASES[providerName] ?? providerName;
3304
+ }
3305
+ function providerApiKey(providerName, env) {
3306
+ for (const name of PROVIDER_API_KEY_ENV[providerName] ?? []) {
3307
+ const value = env[name];
3308
+ if (value) return value;
3309
+ }
3310
+ return LOCAL_PROVIDERS[providerName]?.apiKey;
3311
+ }
3312
+ function providerBaseUrl(providerName, env) {
3313
+ switch (providerName) {
3314
+ case "lmstudio":
3315
+ return env.LMSTUDIO_URL ?? LOCAL_PROVIDERS.lmstudio?.baseUrl;
3316
+ case "ollama":
3317
+ return env.OLLAMA_URL ?? LOCAL_PROVIDERS.ollama?.baseUrl;
3318
+ case "llamacpp":
3319
+ return env.LLAMACPP_URL ?? LOCAL_PROVIDERS.llamacpp?.baseUrl;
3320
+ case "vercel-ai-gateway":
3321
+ return env.AI_GATEWAY_BASE_URL;
3322
+ default:
3323
+ return void 0;
3324
+ }
3325
+ }
3326
+ function providerCredentialEnv(providerName, env) {
3327
+ const result = {};
3328
+ for (const name of PROVIDER_CREDENTIAL_ENV[providerName] ?? []) {
3329
+ const value = env[name];
3330
+ if (value !== void 0) result[name] = value;
3331
+ }
3332
+ return Object.keys(result).length > 0 ? result : void 0;
3333
+ }
3334
+ function fallbackApi(providerName) {
3335
+ switch (providerName) {
3336
+ case "anthropic":
3337
+ return "anthropic-messages";
3338
+ case "google":
3339
+ return "google-generative-ai";
3340
+ case "google-vertex":
3341
+ return "google-vertex";
3342
+ case "azure-openai":
3343
+ return "azure-openai-responses";
3344
+ case "bedrock":
3345
+ return "bedrock-converse-stream";
3346
+ case "openai-codex":
3347
+ return "openai-codex-responses";
3348
+ default:
3349
+ return "openai-completions";
3350
+ }
3351
+ }
3352
+ function fallbackPiModel(providerName, modelId, baseUrl) {
3353
+ return {
3354
+ id: modelId,
3355
+ name: modelId,
3356
+ api: fallbackApi(providerName),
3357
+ provider: piProviderName(providerName),
3358
+ baseUrl: baseUrl ?? "",
3359
+ reasoning: false,
3360
+ input: ["text"],
3361
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
3362
+ contextWindow: 32768,
3363
+ maxTokens: 4096
3364
+ };
3365
+ }
3366
+ async function createBazilionPiRuntime(options) {
3367
+ const providerName = options.providerName;
3368
+ const canonicalName = piProviderName(providerName);
3369
+ const credentials = new InMemoryCredentialStore();
3370
+ const apiKey = options.apiKey ?? providerApiKey(providerName, options.env);
3371
+ const credentialEnv = providerCredentialEnv(providerName, options.env);
3372
+ if (apiKey || credentialEnv) {
3373
+ await credentials.modify(
3374
+ canonicalName,
3375
+ async () => canonicalName === "openai-codex" && apiKey ? {
3376
+ type: "oauth",
3377
+ access: apiKey,
3378
+ // Bazilion owns refresh outside Pi. This deliberately empty value
3379
+ // satisfies Pi's canonical OAuth shape but is never used because
3380
+ // the token is long-lived only inside this one-shot runtime and
3381
+ // mid-turn replacement is supplied through Agent.getApiKey.
3382
+ refresh: "",
3383
+ expires: Number.MAX_SAFE_INTEGER
3384
+ } : {
3385
+ type: "api_key",
3386
+ ...apiKey ? { key: apiKey } : {},
3387
+ ...credentialEnv ? { env: credentialEnv } : {}
3388
+ }
3389
+ );
3390
+ }
3391
+ const runtime = await ModelRuntime.create({ credentials, modelsPath: null });
3392
+ const local = LOCAL_PROVIDERS[providerName];
3393
+ const baseUrl = options.baseUrl ?? providerBaseUrl(providerName, options.env);
3394
+ if (local) {
3395
+ const modelId = options.modelId ?? "model-name";
3396
+ const model = fallbackPiModel(providerName, modelId, baseUrl ?? local.baseUrl);
3397
+ runtime.registerProvider(canonicalName, {
3398
+ baseUrl: baseUrl ?? local.baseUrl,
3399
+ api: "openai-completions",
3400
+ authHeader: local.authHeader,
3401
+ apiKey: apiKey ?? local.apiKey,
3402
+ models: [model]
3403
+ });
3404
+ } else if (baseUrl) {
3405
+ runtime.registerProvider(canonicalName, { baseUrl });
3406
+ }
3407
+ return runtime;
3408
+ }
3409
+ async function createProtectedPiRuntime(options) {
3410
+ const env = {};
3411
+ for (const field of options.credentialEnv ?? []) env[field.name] = field.value;
3412
+ return createBazilionPiRuntime({
3413
+ providerName: options.providerName,
3414
+ modelId: options.modelId,
3415
+ env,
3416
+ apiKey: options.apiKey,
3417
+ baseUrl: options.baseUrl
3418
+ });
3419
+ }
3420
+ function resolvePiModel(runtime, providerName, modelId, baseUrl) {
3421
+ const known = runtime.getModel(piProviderName(providerName), modelId);
3422
+ if (known) return baseUrl ? { ...known, baseUrl } : known;
3423
+ return fallbackPiModel(providerName, modelId, baseUrl);
3424
+ }
3425
+
3426
+ // ../daemon/src/runtime/auth/openai-codex.ts
3427
+ import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
3428
+
3429
+ // ../daemon/src/runtime/providers/pi-adapter.ts
3430
+ import {
3431
+ Type as Type3
3432
+ } from "@earendil-works/pi-ai";
3433
+
3434
+ // ../daemon/src/runtime/session/prompt.ts
3435
+ import { existsSync as existsSync10, readdirSync as readdirSync6 } from "fs";
3436
+ import { join as join17 } from "path";
3437
+
3438
+ // ../daemon/src/runtime/safe-files.ts
3439
+ import {
3440
+ closeSync as closeSync5,
3441
+ constants as constants5,
3442
+ fstatSync as fstatSync4,
3443
+ lstatSync as lstatSync3,
3444
+ mkdirSync as mkdirSync5,
3445
+ openSync as openSync5,
3446
+ readFileSync as readFileSync9,
3447
+ realpathSync as realpathSync5
3448
+ } from "fs";
3449
+ import { dirname as dirname3, isAbsolute as isAbsolute4, relative as relative3, resolve as resolve4 } from "path";
3450
+ function resolveRealDirectory(path) {
3451
+ const requested = resolve4(path);
3452
+ const entry = lstatSync3(requested);
3453
+ if (entry.isSymbolicLink() || !entry.isDirectory()) {
3454
+ throw new Error("expected a real directory");
3455
+ }
3456
+ return realpathSync5(requested);
3457
+ }
3458
+ function readRegularFileNoFollow(path) {
3459
+ const entry = lstatSync3(path);
3460
+ if (entry.isSymbolicLink()) throw new Error("symbolic links are not allowed");
3461
+ if (!entry.isFile()) throw new Error("target is not a regular file");
3462
+ let fd;
3463
+ try {
3464
+ fd = openSync5(path, constants5.O_RDONLY | noFollowFlag());
3465
+ if (!fstatSync4(fd).isFile()) throw new Error("target is not a regular file");
3466
+ return readFileSync9(fd, "utf8");
3467
+ } finally {
3468
+ if (fd !== void 0) closeSync5(fd);
3469
+ }
3470
+ }
3471
+ function noFollowFlag() {
3472
+ return typeof constants5.O_NOFOLLOW === "number" ? constants5.O_NOFOLLOW : 0;
3473
+ }
3474
+
3475
+ // ../daemon/src/runtime/session/prompt.ts
3476
+ var SANDBOX_SKILLS_DIR = "/skills";
3477
+ function loadPromptSkills(skillsDir, attachedNames) {
3478
+ if (!existsSync10(skillsDir) || attachedNames.length === 0) return [];
3479
+ const attached = new Set(attachedNames);
3480
+ const discovered = readdirSync6(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && attached.has(entry.name)).sort((a, b) => a.name.localeCompare(b.name));
3481
+ const loaded = [];
3482
+ for (const entry of discovered) {
3483
+ const hostDir = join17(skillsDir, entry.name);
3484
+ try {
3485
+ const parsed = parseSkillContent(readRegularFileNoFollow(join17(hostDir, "SKILL.md")));
3486
+ const safeLabel = entry.name.replace(/[^A-Za-z0-9._-]/g, "_") || "skill";
3487
+ loaded.push({
3488
+ name: entry.name,
3489
+ description: parsed.frontmatter.description,
3490
+ body: parsed.body.trim(),
3491
+ hostDir,
3492
+ sandboxDir: `${SANDBOX_SKILLS_DIR}/${loaded.length}-${safeLabel}`
3493
+ });
3494
+ } catch {
3495
+ }
3496
+ }
3497
+ return loaded;
3498
+ }
3499
+ var CONTEXT_FILE_ORDER = ["AGENTS.md", "SOUL.md", "TOOLS.md", "IDENTITY.md"];
3500
+ function loadProtectedHomeDocuments(agentDir) {
3501
+ const root = resolveRealDirectory(agentDir);
3502
+ const read = (file) => {
3503
+ try {
3504
+ return readRegularFileNoFollow(join17(root, file));
3505
+ } catch (error) {
3506
+ if (error.code === "ENOENT") return null;
3507
+ throw new Error(`private Agent home is unsafe at ${file}`);
3508
+ }
3509
+ };
3510
+ return {
3511
+ "AGENTS.md": read("AGENTS.md"),
3512
+ "SOUL.md": read("SOUL.md"),
3513
+ "TOOLS.md": read("TOOLS.md"),
3514
+ "IDENTITY.md": read("IDENTITY.md"),
3515
+ "BOOTSTRAP.md": read("BOOTSTRAP.md")
3516
+ };
3517
+ }
3518
+ function buildSystemPrompt(agent, options = {}) {
3519
+ const parts = [];
3520
+ const homeDocuments = options.homeDocuments ?? loadProtectedHomeDocuments(agent.agent.dir);
3521
+ const contextBlocks = [];
3522
+ for (const file of CONTEXT_FILE_ORDER) {
3523
+ const content = homeDocuments[file]?.trimEnd();
3524
+ if (!content) continue;
3525
+ contextBlocks.push(`## ${file}
3526
+
3527
+ ${content}`);
2780
3528
  }
2781
- return new Error(`Docker sandbox could not start "${dockerPath}": ${error.message}`);
2782
- }
2783
- function formatDockerRunError(spec, stderr) {
2784
- const detail3 = stderr.trim() || "Docker exited with status 125 without an error message";
2785
- if (/no such image|unable to find image|manifest unknown|pull access denied/i.test(detail3)) {
2786
- return new Error(`Docker sandbox image "${spec.image}" is unavailable: ${detail3}`);
3529
+ if (contextBlocks.length > 0) {
3530
+ parts.push(`# Agent instructions
3531
+
3532
+ ${contextBlocks.join("\n\n")}`);
2787
3533
  }
2788
- if (/mounts denied|invalid mount config|bind source path does not exist|mount.*failed/i.test(detail3)) {
2789
- return new Error(`Docker sandbox mount failed for "${spec.mountSource}": ${detail3}`);
3534
+ const bootstrap = homeDocuments["BOOTSTRAP.md"]?.trimEnd();
3535
+ if (bootstrap) {
3536
+ parts.push(
3537
+ [
3538
+ "# First-Run Ritual",
3539
+ "",
3540
+ "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.",
3541
+ "",
3542
+ "## Hard rules",
3543
+ "- Your first reply is ONLY a greeting + ONE question. No tool calls. Wait for the human to answer.",
3544
+ "- Each subsequent turn: at most one new question. Wait between turns.",
3545
+ "- Only after the ritual is complete (you have enough to write IDENTITY.md): call `home_write` once, then `bootstrap_done`.",
3546
+ "",
3547
+ "## BOOTSTRAP.md",
3548
+ "",
3549
+ bootstrap
3550
+ ].join("\n")
3551
+ );
2790
3552
  }
2791
- if (/cannot connect to the docker daemon|is the docker daemon running/i.test(detail3)) {
2792
- return new Error(`Docker sandbox unavailable: ${detail3}`);
3553
+ parts.push(
3554
+ [
3555
+ "# Agent Home",
3556
+ "",
3557
+ "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.",
3558
+ "",
3559
+ "- To change who you are (name, vibe, personality, how you behave): use `home_write`.",
3560
+ "- To inspect exact wording of your own files: use `home_read` or `home_list`.",
3561
+ "- To remember facts the user told you or things you learned: use `memory_write` \u2014 NOT `home_write`.",
3562
+ "- 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."
3563
+ ].join("\n")
3564
+ );
3565
+ if (agent.skills.length > 0) {
3566
+ const loaded = options.skills ?? [];
3567
+ const loadedNames = new Set(loaded.map((skill) => skill.name));
3568
+ const unavailable = agent.skills.filter((name) => !loadedNames.has(name));
3569
+ const skillBlocks = loaded.map((skill) => {
3570
+ const runtimeDir = options.sandboxMode === "docker" ? skill.sandboxDir : skill.hostDir;
3571
+ return [
3572
+ `## ${skill.name}`,
3573
+ "",
3574
+ skill.description,
3575
+ "",
3576
+ `Runtime directory: \`${runtimeDir}\`. Resolve any relative scripts or assets mentioned by this skill from that directory.`,
3577
+ "",
3578
+ skill.body
3579
+ ].filter((line, index, lines) => line !== "" || lines[index - 1] !== "").join("\n").trimEnd();
3580
+ });
3581
+ const unavailableNote = unavailable.length > 0 ? `Attached but unavailable or invalid: ${unavailable.join(", ")}.` : "";
3582
+ parts.push(
3583
+ [
3584
+ "# Available Skills",
3585
+ "",
3586
+ "Use these attached skill instructions when they are relevant to the task.",
3587
+ "",
3588
+ ...skillBlocks,
3589
+ ...unavailableNote ? ["", unavailableNote] : []
3590
+ ].join("\n")
3591
+ );
2793
3592
  }
2794
- return new Error(`Docker sandbox failed to start container "${spec.containerName}": ${detail3}`);
2795
- }
2796
- async function removeContainer(dockerPath, containerName, processEnv) {
2797
- const args = ["container", "rm", "--force", containerName];
2798
- let last;
2799
- for (let attempt = 0; attempt < CLEANUP_ATTEMPTS; attempt += 1) {
2800
- last = await runCleanupCommand(dockerPath, args, processEnv);
2801
- if (last.ok) return;
2802
- if (attempt < CLEANUP_ATTEMPTS - 1) {
2803
- await new Promise((resolve6) => setTimeout(resolve6, CLEANUP_RETRY_DELAY_MS));
2804
- }
3593
+ const teamLines = [
3594
+ "# Team",
3595
+ "",
3596
+ `- ${agent.team.id} (${agent.team.name}): ${options.sandboxMode === "docker" ? "/workspace" : agent.team.path}`,
3597
+ "",
3598
+ "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`."
3599
+ ];
3600
+ parts.push(teamLines.join("\n"));
3601
+ if (agent.team.userMd.trim()) {
3602
+ parts.push(
3603
+ `# About the User
3604
+
3605
+ 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.**
3606
+
3607
+ ${agent.team.userMd.trim()}`
3608
+ );
3609
+ } else {
3610
+ parts.push(
3611
+ `# About the User
3612
+
3613
+ 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.**`
3614
+ );
2805
3615
  }
2806
- if (last && !last.missing) {
2807
- console.warn(
2808
- `Docker sandbox could not confirm cleanup of container "${containerName}": ${last.error || "cleanup command failed"}`
3616
+ const reviewedLessons = boundReviewedLessons(agent.privateLessons);
3617
+ if (reviewedLessons.length > 0) {
3618
+ parts.push(
3619
+ `# Reviewed lessons
3620
+
3621
+ 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.
3622
+
3623
+ ${reviewedLessons.map((lesson) => `- ${lesson}`).join("\n")}`
2809
3624
  );
2810
3625
  }
3626
+ parts.push(
3627
+ "# 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.**"
3628
+ );
3629
+ return parts.join("\n\n---\n\n");
2811
3630
  }
2812
- function runCleanupCommand(dockerPath, args, processEnv) {
2813
- return new Promise((resolve6) => {
2814
- let done = false;
2815
- let stderr = "";
2816
- const child = spawn(dockerPath, args, {
2817
- stdio: ["ignore", "ignore", "pipe"],
2818
- windowsHide: true,
2819
- env: processEnv
2820
- });
2821
- const finish = (ok, error = "") => {
2822
- if (done) return;
2823
- done = true;
2824
- clearTimeout(timeoutHandle);
2825
- const detail3 = error || stderr.trim();
2826
- resolve6({
2827
- ok,
2828
- missing: /no such container/i.test(detail3),
2829
- error: detail3
2830
- });
2831
- };
2832
- const timeoutHandle = setTimeout(() => {
2833
- child.kill("SIGKILL");
2834
- finish(false, `cleanup timed out after ${CLEANUP_TIMEOUT_MS}ms`);
2835
- }, CLEANUP_TIMEOUT_MS);
2836
- child.stderr.on("data", (data) => {
2837
- stderr = `${stderr}${data.toString("utf8")}`.slice(-2048);
2838
- });
2839
- child.once("error", (error) => finish(false, error.message));
2840
- child.once("close", (code) => finish(code === 0));
2841
- });
3631
+ function boundReviewedLessons(lessons, maxCharacters = 8e3) {
3632
+ const selected = [];
3633
+ let used = 0;
3634
+ for (const raw of lessons) {
3635
+ const lesson = raw.trim();
3636
+ if (!lesson) continue;
3637
+ const remaining = maxCharacters - used;
3638
+ if (remaining <= 0) break;
3639
+ selected.push(lesson.slice(0, remaining));
3640
+ used += Math.min(lesson.length, remaining);
3641
+ }
3642
+ return selected;
2842
3643
  }
2843
3644
 
2844
3645
  // ../daemon/src/runtime/shell/tooling.ts
3646
+ import { existsSync as existsSync11, lstatSync as lstatSync4, realpathSync as realpathSync6 } from "fs";
3647
+ import { dirname as dirname4, isAbsolute as isAbsolute5, join as join18, relative as relative4, sep as sep4 } from "path";
3648
+ import {
3649
+ createBashToolDefinition,
3650
+ createLocalBashOperations as createLocalBashOperations2,
3651
+ defineTool as defineTool2
3652
+ } from "@earendil-works/pi-coding-agent";
2845
3653
  var HOST_CODING_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
2846
3654
  var HOST_FILE_TOOL_NAMES = ["read", "edit", "write", "grep", "find", "ls"];
2847
3655
  var CONTAINER_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
2848
3656
  var SANDBOX_INPUTS_DIR = "/inputs";
2849
3657
  var SANDBOX_MEMORY_DIR = "/workspace/memory";
2850
- function createProtectedSessionShellTools(cwd, runtime, approvalHost) {
3658
+ function createProtectedSessionShellTools(cwd, runtime, approvalHost, lifecycle) {
3659
+ const operations = createPreparedDockerBashOperations(runtime, lifecycle);
3660
+ const modelCwd = codingContainerCwd(runtime.coding?.cwd ?? ".");
2851
3661
  const base = createBashToolDefinition(cwd, {
2852
- operations: createPreparedDockerBashOperations(runtime)
3662
+ operations: {
3663
+ exec(command, requestedCwd, options) {
3664
+ if (requestedCwd !== modelCwd && requestedCwd !== cwd)
3665
+ throw new Error("Protected command working directory differs from the admitted scope");
3666
+ return operations.exec(command, cwd, options);
3667
+ }
3668
+ }
2853
3669
  });
2854
3670
  const dockerBash = defineTool2({
2855
3671
  ...base,
2856
3672
  label: "bash (protected Docker sandbox)",
2857
- 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.`
3673
+ 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.`
2858
3674
  });
2859
3675
  return {
2860
3676
  config: {
@@ -2880,21 +3696,40 @@ function buildSandboxContainerEnv(env, allowlist) {
2880
3696
  }
2881
3697
  function createSessionShellTools(cwd, env, options = {}) {
2882
3698
  const config = resolveShellSecurityConfig(env);
3699
+ if (options.preparedDocker) {
3700
+ if (config.sandboxMode !== "docker")
3701
+ throw new Error("Prepared coding environment cannot execute on the host");
3702
+ const runtime = options.preparedDocker;
3703
+ const base2 = createBashToolDefinition(cwd, {
3704
+ operations: createPreparedDockerBashOperations(runtime, options.dockerLifecycle)
3705
+ });
3706
+ const tool = defineTool2({
3707
+ ...base2,
3708
+ label: "bash (prepared Docker sandbox)",
3709
+ description: `${base2.description} Default working directory: ${codingContainerCwd(runtime.coding?.cwd ?? ".")}. Fresh network-disabled Docker container with the Team workspace and preflighted read-only mounts.`
3710
+ });
3711
+ return {
3712
+ config: { ...config, sandboxImage: runtime.image, envAllowlist: [] },
3713
+ hostToolNames: [],
3714
+ customBash: config.approvalMode === "dangerous" ? createApprovalGatedBashTool(tool, options.approvalHost) : tool
3715
+ };
3716
+ }
2883
3717
  if (config.sandboxMode === "off" && config.approvalMode === "off") {
2884
3718
  return { config, hostToolNames: HOST_CODING_TOOL_NAMES };
2885
3719
  }
2886
3720
  if (config.sandboxMode === "off") {
2887
3721
  const base2 = createBashToolDefinition(cwd, {
2888
- operations: createLocalBashOperations()
3722
+ operations: createLocalBashOperations2()
2889
3723
  });
2890
3724
  const customBash2 = createApprovalGatedBashTool(base2, options.approvalHost);
2891
3725
  return { config, hostToolNames: HOST_FILE_TOOL_NAMES, customBash: customBash2 };
2892
3726
  }
2893
3727
  const operations = createDockerBashOperations({
3728
+ lifecycle: options.dockerLifecycle,
2894
3729
  image: config.sandboxImage,
2895
3730
  env: buildSandboxContainerEnv(env, config.envAllowlist),
2896
3731
  readOnlyMounts: [
2897
- ...existsSync11(join15(cwd, "memory")) ? [safeReadOnlyMount(join15(cwd, "memory"), SANDBOX_MEMORY_DIR, cwd)] : [],
3732
+ ...existsSync11(join18(cwd, "memory")) ? [safeReadOnlyMount(join18(cwd, "memory"), SANDBOX_MEMORY_DIR, cwd)] : [],
2898
3733
  ...options.inputsDir ? [safeReadOnlyMount(options.inputsDir, SANDBOX_INPUTS_DIR, dirname4(options.inputsDir))] : [],
2899
3734
  ...(options.skillMounts ?? []).map(
2900
3735
  (mount) => safeReadOnlyMount(mount.source, mount.target, dirname4(mount.source))
@@ -2911,14 +3746,14 @@ function createSessionShellTools(cwd, env, options = {}) {
2911
3746
  return { config, hostToolNames: [], customBash };
2912
3747
  }
2913
3748
  function safeReadOnlyMount(source, target, expectedRoot) {
2914
- const info = lstatSync3(source);
3749
+ const info = lstatSync4(source);
2915
3750
  if (info.isSymbolicLink() || !info.isDirectory()) {
2916
3751
  throw new Error(`Docker sandbox read-only mount must be a real directory: ${source}`);
2917
3752
  }
2918
- const sourceRoot = realpathSync3(expectedRoot);
2919
- const canonicalSource = realpathSync3(source);
3753
+ const sourceRoot = realpathSync6(expectedRoot);
3754
+ const canonicalSource = realpathSync6(source);
2920
3755
  const rel = relative4(sourceRoot, canonicalSource);
2921
- const contained = rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute4(rel);
3756
+ const contained = rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute5(rel);
2922
3757
  if (!contained) {
2923
3758
  throw new Error(`Docker sandbox read-only mount escaped its configured root: ${source}`);
2924
3759
  }
@@ -2927,18 +3762,18 @@ function safeReadOnlyMount(source, target, expectedRoot) {
2927
3762
 
2928
3763
  // ../daemon/src/runtime/worker/runtime.ts
2929
3764
  import {
2930
- constants as constants4,
3765
+ constants as constants6,
2931
3766
  existsSync as existsSync12,
2932
- lstatSync as lstatSync4,
3767
+ lstatSync as lstatSync5,
2933
3768
  mkdirSync as mkdirSync6,
2934
3769
  mkdtempSync as mkdtempSync3,
2935
- realpathSync as realpathSync4,
3770
+ realpathSync as realpathSync7,
2936
3771
  rmSync as rmSync11,
2937
3772
  statSync as statSync3
2938
3773
  } from "fs";
2939
3774
  import { access as access2 } from "fs/promises";
2940
3775
  import { tmpdir as tmpdir3 } from "os";
2941
- import { isAbsolute as isAbsolute5, join as join16, relative as relative5, sep as sep5 } from "path";
3776
+ import { isAbsolute as isAbsolute6, join as join19, relative as relative5, sep as sep5 } from "path";
2942
3777
  var REDACTED_VALUE = "[REDACTED]";
2943
3778
  var REASONING_LEVELS = /* @__PURE__ */ new Set([
2944
3779
  "off",
@@ -2949,6 +3784,11 @@ var REASONING_LEVELS = /* @__PURE__ */ new Set([
2949
3784
  "xhigh"
2950
3785
  ]);
2951
3786
  var CONFIGURED_KEYS = /* @__PURE__ */ new Set([
3787
+ "containerNamespace",
3788
+ "configuredDocker",
3789
+ "repositoryContext",
3790
+ "questionEnabled",
3791
+ "conversation",
2952
3792
  "kind",
2953
3793
  "agent",
2954
3794
  "message",
@@ -2962,6 +3802,8 @@ var CONFIGURED_KEYS = /* @__PURE__ */ new Set([
2962
3802
  "bashApprovalMode"
2963
3803
  ]);
2964
3804
  var CONFIGURED_REQUIRED_KEYS = /* @__PURE__ */ new Set([
3805
+ "repositoryContext",
3806
+ "conversation",
2965
3807
  "kind",
2966
3808
  "agent",
2967
3809
  "message",
@@ -2971,6 +3813,10 @@ var CONFIGURED_REQUIRED_KEYS = /* @__PURE__ */ new Set([
2971
3813
  "bashApprovalMode"
2972
3814
  ]);
2973
3815
  var PROTECTED_KEYS = /* @__PURE__ */ new Set([
3816
+ "containerNamespace",
3817
+ "repositoryContext",
3818
+ "questionEnabled",
3819
+ "conversation",
2974
3820
  "kind",
2975
3821
  "agent",
2976
3822
  "message",
@@ -2984,7 +3830,11 @@ var PROTECTED_KEYS = /* @__PURE__ */ new Set([
2984
3830
  "apiKeyRefreshEnabled",
2985
3831
  "scratch"
2986
3832
  ]);
2987
- var PROTECTED_REQUIRED_KEYS = new Set([...PROTECTED_KEYS].filter((key) => key !== "images"));
3833
+ var PROTECTED_REQUIRED_KEYS = new Set(
3834
+ [...PROTECTED_KEYS].filter(
3835
+ (key) => key !== "images" && key !== "questionEnabled" && key !== "containerNamespace"
3836
+ )
3837
+ );
2988
3838
  var REVIEW_KEYS = /* @__PURE__ */ new Set([
2989
3839
  "kind",
2990
3840
  "agentId",
@@ -3044,14 +3894,27 @@ function validateMinimalWorkerProcessEnv(env, scratch, platform = process.platfo
3044
3894
  function parseWorkerInput(value) {
3045
3895
  const input = objectRecord(value, "worker input");
3046
3896
  const kind = input.kind;
3897
+ if (input.containerNamespace !== void 0 && (typeof input.containerNamespace !== "string" || !/^[a-f0-9-]{36}$/.test(input.containerNamespace)))
3898
+ throw new Error("Invalid container namespace");
3899
+ if (input.questionEnabled !== void 0 && typeof input.questionEnabled !== "boolean")
3900
+ throw new Error("Invalid worker question capability");
3047
3901
  if (kind === "configured_operator_http") {
3048
3902
  assertExactKeys(input, CONFIGURED_KEYS, "configured worker input", CONFIGURED_REQUIRED_KEYS);
3049
3903
  if (!isResolvedAgent(input.agent)) throw new Error("worker: configured input requires an agent");
3904
+ if (input.configuredDocker !== void 0) {
3905
+ const prepared = objectRecord(input.configuredDocker, "configured Docker inputs");
3906
+ const keys2 = /* @__PURE__ */ new Set(["paths", "docker"]);
3907
+ assertExactKeys(prepared, keys2, "configured Docker inputs", keys2);
3908
+ assertProtectedWorkerPaths(prepared.paths, input.agent);
3909
+ assertProtectedDockerRuntime(prepared.docker, prepared.paths);
3910
+ }
3911
+ assertRepositoryContext(input.repositoryContext, input.agent.team.id);
3050
3912
  requireString(input.message, "message");
3051
3913
  if (!Array.isArray(input.enabledProviders) || !input.enabledProviders.every(isString)) {
3052
3914
  throw new Error("worker: configured input requires enabledProviders");
3053
3915
  }
3054
3916
  requireString(input.turnId, "turnId");
3917
+ assertConversationTarget(input.conversation);
3055
3918
  if (input.bashApprovalMode !== "interactive" && input.bashApprovalMode !== "auto_deny") {
3056
3919
  throw new Error("worker: configured input requires a valid bashApprovalMode");
3057
3920
  }
@@ -3071,8 +3934,10 @@ function parseWorkerInput(value) {
3071
3934
  if (kind === "protected") {
3072
3935
  assertExactKeys(input, PROTECTED_KEYS, "protected worker input", PROTECTED_REQUIRED_KEYS);
3073
3936
  if (!isResolvedAgent(input.agent)) throw new Error("worker: protected input requires an agent");
3937
+ assertRepositoryContext(input.repositoryContext, input.agent.team.id);
3074
3938
  requireString(input.message, "message");
3075
3939
  requireString(input.turnId, "turnId");
3940
+ assertConversationTarget(input.conversation);
3076
3941
  if (input.bashApprovalMode !== "auto_deny") {
3077
3942
  throw new Error("worker: protected input requires auto-deny shell approval");
3078
3943
  }
@@ -3202,8 +4067,8 @@ function protectedRuntimeSecrets(runtime) {
3202
4067
  runtime.credentialFile?.content
3203
4068
  ].filter((value) => Boolean(value));
3204
4069
  }
3205
- function redactExactValues(text, values) {
3206
- let redacted = text;
4070
+ function redactExactValues(text2, values) {
4071
+ let redacted = text2;
3207
4072
  for (const value of [...new Set(values)].filter(Boolean).sort((a, b) => b.length - a.length)) {
3208
4073
  redacted = redacted.replaceAll(value, REDACTED_VALUE);
3209
4074
  }
@@ -3312,7 +4177,8 @@ function assertProtectedDockerRuntime(value, paths) {
3312
4177
  "gid",
3313
4178
  "workspace",
3314
4179
  "readOnlyMounts",
3315
- "containerEnv"
4180
+ "containerEnv",
4181
+ "coding"
3316
4182
  ]),
3317
4183
  "protected Docker runtime",
3318
4184
  /* @__PURE__ */ new Set([
@@ -3352,7 +4218,7 @@ function assertProtectedDockerRuntime(value, paths) {
3352
4218
  throw new Error(`worker: protected Docker executable identity has invalid ${name}`);
3353
4219
  }
3354
4220
  }
3355
- if (typeof runtime.endpoint !== "string" || !runtime.endpoint.startsWith("unix://") || !isAbsolute5(runtime.endpoint.slice("unix://".length)) || containsUnsafeDockerArgument(runtime.endpoint)) {
4221
+ if (typeof runtime.endpoint !== "string" || !runtime.endpoint.startsWith("unix://") || !isAbsolute6(runtime.endpoint.slice("unix://".length)) || containsUnsafeDockerArgument(runtime.endpoint)) {
3356
4222
  throw new Error("worker: protected Docker endpoint must be a local Unix socket");
3357
4223
  }
3358
4224
  if (!nonEmptyString(runtime.image) || runtime.image.startsWith("-") || runtime.image.includes("\0") || runtime.image.includes("\n") || runtime.image.includes("\r")) {
@@ -3416,6 +4282,7 @@ function assertProtectedDockerRuntime(value, paths) {
3416
4282
  runtime.readOnlyMounts,
3417
4283
  paths
3418
4284
  );
4285
+ if (runtime.coding !== void 0) validateDockerCodingSelection(runtime.coding);
3419
4286
  const containerEnv = objectRecord(runtime.containerEnv, "protected Docker container environment");
3420
4287
  const expectedContainerEnv = {
3421
4288
  PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
@@ -3465,11 +4332,11 @@ function containsUnsafeDockerArgument(value) {
3465
4332
  function requireCanonicalRealDirectory(value, name) {
3466
4333
  if (typeof value !== "string") throw new Error(`worker: ${name} must be a path`);
3467
4334
  try {
3468
- const entry = lstatSync4(value);
4335
+ const entry = lstatSync5(value);
3469
4336
  if (entry.isSymbolicLink() || !entry.isDirectory()) {
3470
4337
  throw new Error("unsafe entry");
3471
4338
  }
3472
- const canonical = realpathSync4(value);
4339
+ const canonical = realpathSync7(value);
3473
4340
  if (canonical !== value) throw new Error("non-canonical entry");
3474
4341
  return canonical;
3475
4342
  } catch {
@@ -3480,7 +4347,7 @@ function resolveCanonicalDirectory(value, name) {
3480
4347
  try {
3481
4348
  const entry = statSync3(value);
3482
4349
  if (!entry.isDirectory()) throw new Error("not a directory");
3483
- return realpathSync4(value);
4350
+ return realpathSync7(value);
3484
4351
  } catch {
3485
4352
  throw new Error(`worker: ${name} is unavailable or unsafe`);
3486
4353
  }
@@ -3513,16 +4380,16 @@ function assertMinimalWorkerScratch(value) {
3513
4380
  }
3514
4381
  }
3515
4382
  function requireCanonicalWindowsDirectory(value, name) {
3516
- if (!value || !isAbsolute5(value) || !existsSync12(value) || !statSync3(value).isDirectory()) {
4383
+ if (!value || !isAbsolute6(value) || !existsSync12(value) || !statSync3(value).isDirectory()) {
3517
4384
  throw new Error(`minimal worker bootstrap requires a valid ${name} directory`);
3518
4385
  }
3519
- return realpathSync4(value);
4386
+ return realpathSync7(value);
3520
4387
  }
3521
4388
  function requireCanonicalWindowsFile(value, name) {
3522
- if (!value || !isAbsolute5(value) || !existsSync12(value) || !statSync3(value).isFile()) {
4389
+ if (!value || !isAbsolute6(value) || !existsSync12(value) || !statSync3(value).isFile()) {
3523
4390
  throw new Error(`minimal worker bootstrap requires a valid ${name} executable`);
3524
4391
  }
3525
- return realpathSync4(value);
4392
+ return realpathSync7(value);
3526
4393
  }
3527
4394
  function objectRecord(value, label) {
3528
4395
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -3557,21 +4424,69 @@ function isString(value) {
3557
4424
  return typeof value === "string";
3558
4425
  }
3559
4426
  function requireAbsolutePath(value, name) {
3560
- if (typeof value !== "string" || !isAbsolute5(value) || value.includes("\0")) {
4427
+ if (typeof value !== "string" || !isAbsolute6(value) || value.includes("\0")) {
3561
4428
  throw new Error(`worker: ${name} must be an absolute path`);
3562
4429
  }
3563
4430
  }
3564
4431
  function lexicallyWithin(root, candidate) {
3565
4432
  const rel = relative5(root, candidate);
3566
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep5}`) && !isAbsolute5(rel);
4433
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep5}`) && !isAbsolute6(rel);
4434
+ }
4435
+ function assertConversationTarget(value) {
4436
+ const target = objectRecord(value, "conversation target");
4437
+ assertExactKeys(target, /* @__PURE__ */ new Set(["id", "filename"]), "conversation target");
4438
+ if (typeof target.id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(target.id) || target.filename !== `${target.id}.jsonl`)
4439
+ throw new Error("Invalid worker conversation target");
4440
+ }
4441
+
4442
+ // ../daemon/src/runtime/tools/ask-user.ts
4443
+ function askUserTool(ask) {
4444
+ return {
4445
+ def: {
4446
+ name: "ask_user",
4447
+ description: "Ask the operator one clarification question and wait for an explicit choice, free-text answer, Skip, or a typed no-answer outcome. An answer is information only: it never grants shell or communication permission. Do not ask for credentials. Do not invent a response when no answer is returned.",
4448
+ parameters: {
4449
+ type: "object",
4450
+ additionalProperties: false,
4451
+ required: ["prompt", "choices"],
4452
+ properties: {
4453
+ prompt: { type: "string", minLength: 1, maxLength: 4096 },
4454
+ choices: {
4455
+ type: "array",
4456
+ minItems: 2,
4457
+ maxItems: 4,
4458
+ items: {
4459
+ type: "object",
4460
+ additionalProperties: false,
4461
+ required: ["label"],
4462
+ properties: {
4463
+ label: { type: "string", minLength: 1, maxLength: 120 },
4464
+ description: { type: "string", minLength: 1, maxLength: 500 }
4465
+ }
4466
+ }
4467
+ },
4468
+ recommendedIndex: { type: "integer", minimum: 0, maximum: 3 }
4469
+ }
4470
+ }
4471
+ },
4472
+ async invoke(args, context) {
4473
+ if (!context?.toolCallId) throw new Error("Question requires its actual tool call identity");
4474
+ const { receipt, ...result } = await ask(
4475
+ context.toolCallId,
4476
+ args
4477
+ );
4478
+ const text2 = JSON.stringify(result);
4479
+ return receipt ? { content: [{ type: "text", text: text2 }], questionReceipt: receipt } : text2;
4480
+ }
4481
+ };
3567
4482
  }
3568
4483
 
3569
4484
  // ../daemon/src/runtime/pi/tools.ts
3570
- import { Type as Type2 } from "typebox";
4485
+ import { Type as Type4 } from "typebox";
3571
4486
 
3572
4487
  // ../daemon/src/runtime/tools/bootstrap.ts
3573
4488
  import { existsSync as existsSync13, rmSync as rmSync12 } from "fs";
3574
- import { join as join17 } from "path";
4489
+ import { join as join20 } from "path";
3575
4490
  function bootstrapTool(agentDir) {
3576
4491
  return {
3577
4492
  def: {
@@ -3580,7 +4495,7 @@ function bootstrapTool(agentDir) {
3580
4495
  parameters: { type: "object", properties: {} }
3581
4496
  },
3582
4497
  async invoke() {
3583
- const path = join17(agentDir, "BOOTSTRAP.md");
4498
+ const path = join20(agentDir, "BOOTSTRAP.md");
3584
4499
  if (existsSync13(path)) {
3585
4500
  rmSync12(path);
3586
4501
  return "BOOTSTRAP.md removed. Bootstrap is complete.";
@@ -3721,15 +4636,15 @@ function browserTools(host, agentId) {
3721
4636
 
3722
4637
  // ../daemon/src/runtime/tools/deliver-file.ts
3723
4638
  import {
3724
- closeSync as closeSync4,
3725
- constants as constants5,
3726
- fstatSync as fstatSync3,
3727
- openSync as openSync4,
3728
- readFileSync as readFileSync10,
3729
- realpathSync as realpathSync5,
4639
+ closeSync as closeSync6,
4640
+ constants as constants7,
4641
+ fstatSync as fstatSync5,
4642
+ openSync as openSync6,
4643
+ readSync as readSync3,
4644
+ realpathSync as realpathSync8,
3730
4645
  statSync as statSync4
3731
4646
  } from "fs";
3732
- import { basename as basename2, extname, isAbsolute as isAbsolute6, relative as relative6, resolve as resolve5, sep as sep6 } from "path";
4647
+ import { basename as basename3, extname, isAbsolute as isAbsolute7, relative as relative6, resolve as resolve5, sep as sep6 } from "path";
3733
4648
  var MAX_DELIVER_BYTES = 25 * 1024 * 1024;
3734
4649
  var MIME = {
3735
4650
  ".pdf": "application/pdf",
@@ -3747,19 +4662,19 @@ var MIME = {
3747
4662
  };
3748
4663
  function isWithin2(root, candidate) {
3749
4664
  const rel = relative6(root, candidate);
3750
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep6}`) && !isAbsolute6(rel);
4665
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep6}`) && !isAbsolute7(rel);
3751
4666
  }
3752
- function deliverFileTool(cwd, sink) {
4667
+ function deliverFileTool(cwd, sink, sessionId = "") {
3753
4668
  const workspacePath = resolve5(cwd);
3754
4669
  let workspaceRealPath = null;
3755
4670
  try {
3756
- workspaceRealPath = realpathSync5(workspacePath);
4671
+ workspaceRealPath = realpathSync8(workspacePath);
3757
4672
  } catch {
3758
4673
  }
3759
4674
  return {
3760
4675
  def: {
3761
4676
  name: "deliver_file",
3762
- description: "Send a file from your workspace to the user so they can download it (web), receive it as a document (Telegram), or save it (CLI). Use this to deliver reports, exports, or any artifact you produced. Max 25 MB.",
4677
+ description: "Send a file from your workspace to the user so they can download it (web), receive it as a document (Telegram), or save it (CLI). Use this to deliver reports, exports, or any artifact you produced. Max 25 MiB. Saved delivery remains subject to Team Policy.",
3763
4678
  parameters: {
3764
4679
  type: "object",
3765
4680
  properties: {
@@ -3772,10 +4687,10 @@ function deliverFileTool(cwd, sink) {
3772
4687
  additionalProperties: false
3773
4688
  }
3774
4689
  },
3775
- async invoke(args) {
4690
+ async invoke(args, context) {
3776
4691
  const p = String(args.path ?? "");
3777
4692
  if (!p) throw new Error("deliver_file: path is required");
3778
- if (isAbsolute6(p)) {
4693
+ if (isAbsolute7(p)) {
3779
4694
  throw new Error(`deliver_file: path must stay within the workspace: ${p}`);
3780
4695
  }
3781
4696
  const abs = resolve5(workspacePath, p);
@@ -3788,7 +4703,7 @@ function deliverFileTool(cwd, sink) {
3788
4703
  let real;
3789
4704
  let expected;
3790
4705
  try {
3791
- real = realpathSync5(abs);
4706
+ real = realpathSync8(abs);
3792
4707
  expected = statSync4(real);
3793
4708
  } catch {
3794
4709
  throw new Error(`deliver_file: no such file: ${p}`);
@@ -3798,13 +4713,13 @@ function deliverFileTool(cwd, sink) {
3798
4713
  }
3799
4714
  let fd;
3800
4715
  try {
3801
- fd = openSync4(real, constants5.O_RDONLY | constants5.O_NOFOLLOW | constants5.O_NONBLOCK);
4716
+ fd = openSync6(real, constants7.O_RDONLY | constants7.O_NOFOLLOW | constants7.O_NONBLOCK);
3802
4717
  } catch {
3803
4718
  throw new Error(`deliver_file: no such file: ${p}`);
3804
4719
  }
3805
4720
  let data;
3806
4721
  try {
3807
- const stat2 = fstatSync3(fd);
4722
+ const stat2 = fstatSync5(fd);
3808
4723
  if (!stat2.isFile()) {
3809
4724
  throw new Error(`deliver_file: not a regular file: ${p}`);
3810
4725
  }
@@ -3813,7 +4728,7 @@ function deliverFileTool(cwd, sink) {
3813
4728
  }
3814
4729
  let confirmedReal;
3815
4730
  try {
3816
- confirmedReal = realpathSync5(abs);
4731
+ confirmedReal = realpathSync8(abs);
3817
4732
  } catch {
3818
4733
  throw new Error(`deliver_file: path changed during validation: ${p}`);
3819
4734
  }
@@ -3822,32 +4737,53 @@ function deliverFileTool(cwd, sink) {
3822
4737
  }
3823
4738
  if (stat2.size > MAX_DELIVER_BYTES) {
3824
4739
  throw new Error(
3825
- `deliver_file: "${basename2(abs)}" is too large (${(stat2.size / 1024 / 1024).toFixed(1)} MB > 25 MB)`
4740
+ `deliver_file: "${basename3(abs)}" is too large (${(stat2.size / 1024 / 1024).toFixed(1)} MB > 25 MB)`
3826
4741
  );
3827
4742
  }
3828
- data = readFileSync10(fd).toString("base64");
4743
+ const buffer = Buffer.alloc(MAX_DELIVER_BYTES + 1);
4744
+ let length = 0;
4745
+ while (length < buffer.length) {
4746
+ const count2 = readSync3(fd, buffer, length, buffer.length - length, null);
4747
+ if (count2 === 0) break;
4748
+ length += count2;
4749
+ }
4750
+ if (length > MAX_DELIVER_BYTES) throw new Error("deliver_file: file exceeds 25 MiB");
4751
+ data = buffer.subarray(0, length).toString("base64");
3829
4752
  } finally {
3830
- closeSync4(fd);
4753
+ closeSync6(fd);
3831
4754
  }
3832
- const name = basename2(abs);
4755
+ const name = basename3(abs);
3833
4756
  const mimeType = MIME[extname(abs).toLowerCase()] ?? "application/octet-stream";
3834
- sink({ name, mimeType, data });
3835
- return `Delivered "${name}" (${mimeType}) to the user.`;
4757
+ if (!context?.toolCallId || !sessionId)
4758
+ throw new Error("deliver_file: missing source operation");
4759
+ const result = await sink(
4760
+ { name, mimeType, data },
4761
+ { sessionId, toolCallId: context.toolCallId }
4762
+ );
4763
+ return {
4764
+ content: [
4765
+ {
4766
+ type: "text",
4767
+ text: `Saved "${name}" (${mimeType}) as result ${result.resultId}. Delivery is subject to Team Policy.`
4768
+ }
4769
+ ],
4770
+ result
4771
+ };
3836
4772
  }
3837
4773
  };
3838
4774
  }
3839
4775
 
3840
4776
  // ../daemon/src/runtime/tools/home.ts
3841
4777
  import {
3842
- closeSync as closeSync5,
3843
- constants as constants6,
3844
- fstatSync as fstatSync4,
3845
- lstatSync as lstatSync5,
3846
- openSync as openSync5,
4778
+ closeSync as closeSync7,
4779
+ constants as constants8,
4780
+ fstatSync as fstatSync6,
4781
+ lstatSync as lstatSync6,
4782
+ openSync as openSync7,
3847
4783
  readdirSync as readdirSync7,
3848
4784
  writeFileSync as writeFileSync8
3849
4785
  } from "fs";
3850
- import { join as join18 } from "path";
4786
+ import { join as join21 } from "path";
3851
4787
  var HOME_FILES_READABLE = [
3852
4788
  "IDENTITY.md",
3853
4789
  "SOUL.md",
@@ -3912,18 +4848,18 @@ function homeTools(agentDir) {
3912
4848
  let fd;
3913
4849
  try {
3914
4850
  rejectExistingSymlinkOrNonFile(path);
3915
- fd = openSync5(
4851
+ fd = openSync7(
3916
4852
  path,
3917
- constants6.O_WRONLY | constants6.O_CREAT | constants6.O_TRUNC | noFollowFlag(),
4853
+ constants8.O_WRONLY | constants8.O_CREAT | constants8.O_TRUNC | noFollowFlag(),
3918
4854
  384
3919
4855
  );
3920
- if (!fstatSync4(fd).isFile()) throw new Error("target is not a regular file");
4856
+ if (!fstatSync6(fd).isFile()) throw new Error("target is not a regular file");
3921
4857
  writeFileSync8(fd, content, "utf8");
3922
4858
  } catch (err) {
3923
4859
  const msg = err instanceof Error ? err.message : String(err);
3924
4860
  throw new Error(`home_write: could not write ${file}: ${msg}`);
3925
4861
  } finally {
3926
- if (fd !== void 0) closeSync5(fd);
4862
+ if (fd !== void 0) closeSync7(fd);
3927
4863
  }
3928
4864
  return `wrote ${file} (${Buffer.byteLength(content, "utf8")} bytes)`;
3929
4865
  }
@@ -3939,7 +4875,7 @@ function homeTools(agentDir) {
3939
4875
  for (const file of HOME_FILES_READABLE) {
3940
4876
  const path = privateHomeFile(homeRoot, file);
3941
4877
  try {
3942
- const s = lstatSync5(path);
4878
+ const s = lstatSync6(path);
3943
4879
  if (s.isSymbolicLink() || !s.isFile()) continue;
3944
4880
  entries.push(`${file} (${s.size}b)`);
3945
4881
  } catch {
@@ -3968,10 +4904,10 @@ function resolvePrivateHomeRoot(agentDir) {
3968
4904
  }
3969
4905
  }
3970
4906
  function privateHomeFile(homeRoot, file) {
3971
- return join18(homeRoot, file);
4907
+ return join21(homeRoot, file);
3972
4908
  }
3973
4909
  function rejectSymlinkOrNonFile(path) {
3974
- const entry = lstatSync5(path);
4910
+ const entry = lstatSync6(path);
3975
4911
  if (entry.isSymbolicLink()) throw new Error("symbolic links are not allowed");
3976
4912
  if (!entry.isFile()) throw new Error("target is not a regular file");
3977
4913
  }
@@ -4105,9 +5041,9 @@ function messagingTools(host, fromAgentId) {
4105
5041
  },
4106
5042
  async invoke(args) {
4107
5043
  const to = String(args.to ?? "");
4108
- const text = String(args.text ?? "");
5044
+ const text2 = String(args.text ?? "");
4109
5045
  if (!to) throw new Error('send_message: "to" is required');
4110
- if (!text) throw new Error('send_message: "text" is required');
5046
+ if (!text2) throw new Error('send_message: "text" is required');
4111
5047
  if (!await host.agentExists(to)) {
4112
5048
  throw new Error(`send_message: agent not found: ${to}`);
4113
5049
  }
@@ -4115,7 +5051,7 @@ function messagingTools(host, fromAgentId) {
4115
5051
  const { messageId } = await host.sendMessage({
4116
5052
  from: fromAgentId,
4117
5053
  to,
4118
- payload: JSON.stringify({ text }),
5054
+ payload: JSON.stringify({ text: text2 }),
4119
5055
  replyTo
4120
5056
  });
4121
5057
  return `sent message ${messageId}`;
@@ -4175,7 +5111,7 @@ function messagingTools(host, fromAgentId) {
4175
5111
  {
4176
5112
  def: {
4177
5113
  name: "wait_for_reply",
4178
- description: "Block until a reply to a message you sent arrives, or until the timeout expires.",
5114
+ 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.",
4179
5115
  parameters: {
4180
5116
  type: "object",
4181
5117
  properties: {
@@ -4291,25 +5227,25 @@ function normalizeWhitespace(value) {
4291
5227
  function htmlToMarkdown(html) {
4292
5228
  const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
4293
5229
  const title = titleMatch ? normalizeWhitespace(stripTags(titleMatch[1] ?? "")) : void 0;
4294
- 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, "");
4295
- text = text.replace(/<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, body) => {
5230
+ 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, "");
5231
+ text2 = text2.replace(/<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, body) => {
4296
5232
  const label = normalizeWhitespace(stripTags(body));
4297
5233
  return label ? `[${label}](${href})` : href;
4298
5234
  });
4299
- text = text.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, level, body) => {
5235
+ text2 = text2.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, level, body) => {
4300
5236
  const n = Math.max(1, Math.min(6, Number.parseInt(level, 10)));
4301
5237
  return `
4302
5238
  ${"#".repeat(n)} ${normalizeWhitespace(stripTags(body))}
4303
5239
  `;
4304
5240
  });
4305
- text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, body) => {
5241
+ text2 = text2.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, body) => {
4306
5242
  const label = normalizeWhitespace(stripTags(body));
4307
5243
  return label ? `
4308
5244
  - ${label}` : "";
4309
5245
  });
4310
- text = text.replace(/<(br|hr)\s*\/?>/gi, "\n").replace(/<\/(p|div|section|article|tr|ul|ol|table|blockquote)>/gi, "\n");
4311
- text = stripTags(text);
4312
- return { text: normalizeWhitespace(text), title };
5246
+ text2 = text2.replace(/<(br|hr)\s*\/?>/gi, "\n").replace(/<\/(p|div|section|article|tr|ul|ol|table|blockquote)>/gi, "\n");
5247
+ text2 = stripTags(text2);
5248
+ return { text: normalizeWhitespace(text2), title };
4313
5249
  }
4314
5250
  function markdownToPlain(md) {
4315
5251
  let t = md;
@@ -4343,8 +5279,8 @@ function extractReadable(html, url, mode) {
4343
5279
  if (!parsed?.content) return fallback();
4344
5280
  const title = parsed.title || void 0;
4345
5281
  if (mode === "text") {
4346
- const text = normalizeWhitespace(parsed.textContent ?? "");
4347
- return text ? { text, title } : fallback();
5282
+ const text2 = normalizeWhitespace(parsed.textContent ?? "");
5283
+ return text2 ? { text: text2, title } : fallback();
4348
5284
  }
4349
5285
  const rendered = htmlToMarkdown(parsed.content);
4350
5286
  return { text: rendered.text, title: title ?? rendered.title };
@@ -4655,9 +5591,9 @@ async function readBodyCapped(res, maxBytes) {
4655
5591
  const decoder = new TextDecoder(charset, { fatal: false });
4656
5592
  const reader = res.body?.getReader();
4657
5593
  if (!reader) {
4658
- const text = await res.text();
4659
- if (text.length > maxBytes) return { text: text.slice(0, maxBytes), truncated: true };
4660
- return { text, truncated: false };
5594
+ const text2 = await res.text();
5595
+ if (text2.length > maxBytes) return { text: text2.slice(0, maxBytes), truncated: true };
5596
+ return { text: text2, truncated: false };
4661
5597
  }
4662
5598
  const chunks = [];
4663
5599
  let total = 0;
@@ -4714,8 +5650,8 @@ async function firecrawlScrape(url, mode, env, fetchFn, timeoutMs) {
4714
5650
  if (!body.success || !body.data?.markdown) return null;
4715
5651
  const md = body.data.markdown;
4716
5652
  const title = body.data.metadata?.title;
4717
- const text = mode === "text" ? markdownToPlain(md) : md;
4718
- return title ? { text, title } : { text };
5653
+ const text2 = mode === "text" ? markdownToPlain(md) : md;
5654
+ return title ? { text: text2, title } : { text: text2 };
4719
5655
  } catch {
4720
5656
  return null;
4721
5657
  } finally {
@@ -4911,19 +5847,19 @@ function ourToolToPiTool(h) {
4911
5847
  name: h.def.name,
4912
5848
  label: h.def.name,
4913
5849
  description: h.def.description,
4914
- parameters: Type2.Unsafe(h.def.parameters),
4915
- async execute(_toolCallId, params) {
4916
- const out = await h.invoke(params);
5850
+ parameters: Type4.Unsafe(h.def.parameters),
5851
+ async execute(toolCallId, params) {
5852
+ const out = await h.invoke(params, { toolCallId });
4917
5853
  return {
4918
5854
  content: toPiContent(out),
4919
- details: {}
5855
+ details: typeof out === "object" && !Array.isArray(out) ? "result" in out ? { result: out.result } : { questionReceipt: out.questionReceipt } : {}
4920
5856
  };
4921
5857
  }
4922
5858
  };
4923
5859
  }
4924
5860
  function toPiContent(out) {
4925
5861
  if (typeof out === "string") return [{ type: "text", text: out }];
4926
- return out.map(
5862
+ return (Array.isArray(out) ? out : out.content).map(
4927
5863
  (p) => p.type === "text" ? { type: "text", text: p.text } : { type: "image", data: p.data, mimeType: p.mimeType }
4928
5864
  );
4929
5865
  }
@@ -4947,8 +5883,9 @@ function createBazilionCustomTools(opts) {
4947
5883
  handlers.push(...mcpProxyTools(opts.mcpHost, opts.mcpTools));
4948
5884
  }
4949
5885
  if (opts.fileSink) {
4950
- handlers.push(deliverFileTool(opts.agent.team.path, opts.fileSink));
5886
+ handlers.push(deliverFileTool(opts.agent.team.path, opts.fileSink, opts.sessionId));
4951
5887
  }
5888
+ if (opts.askUser) handlers.push(askUserTool(opts.askUser));
4952
5889
  return handlers.map(ourToolToPiTool);
4953
5890
  }
4954
5891
  function createProtectedBazilionCustomTools(opts) {
@@ -4959,8 +5896,9 @@ function createProtectedBazilionCustomTools(opts) {
4959
5896
  protectedWebFetchTool(),
4960
5897
  ...messagingTools(opts.messagingHost, opts.agent.agent.id),
4961
5898
  ...userMdTools(opts.userMdHost, opts.agent.team.id),
4962
- deliverFileTool(opts.agent.team.path, opts.fileSink)
5899
+ deliverFileTool(opts.agent.team.path, opts.fileSink, opts.sessionId)
4963
5900
  ];
5901
+ if (opts.askUser) handlers.push(askUserTool(opts.askUser));
4964
5902
  return handlers.map(ourToolToPiTool);
4965
5903
  }
4966
5904
 
@@ -4984,6 +5922,7 @@ async function createBazilionSession(opts) {
4984
5922
  mcpHost,
4985
5923
  mcpTools,
4986
5924
  fileSink,
5925
+ askUser,
4987
5926
  bashApprovalHost,
4988
5927
  refreshApiKey,
4989
5928
  restricted
@@ -5005,8 +5944,10 @@ async function createBazilionSession(opts) {
5005
5944
  const cwd = agent.team.path;
5006
5945
  if (!existsSync14(cwd)) mkdirSync7(cwd, { recursive: true });
5007
5946
  const promptSkills = restricted ? [] : loadPromptSkills(paths.skillsDir, agent.skills);
5008
- const uploadsDir = join19(agent.agent.dir, "uploads");
5947
+ const uploadsDir = join22(agent.agent.dir, "uploads");
5009
5948
  const shellTools = restricted ? null : createSessionShellTools(cwd, env, {
5949
+ preparedDocker: opts.preparedDocker,
5950
+ dockerLifecycle: opts.dockerLifecycle,
5010
5951
  ...existsSync14(uploadsDir) ? { inputsDir: uploadsDir } : {},
5011
5952
  skillMounts: promptSkills.map((skill) => ({
5012
5953
  source: skill.hostDir,
@@ -5014,9 +5955,9 @@ async function createBazilionSession(opts) {
5014
5955
  })),
5015
5956
  approvalHost: bashApprovalHost
5016
5957
  });
5017
- const sessionDir = restricted?.sessionDir ?? join19(paths.agentDir(agent.agent.id), "sessions");
5958
+ const sessionDir = restricted?.sessionDir ?? join22(paths.agentDir(agent.agent.id), "sessions");
5018
5959
  mkdirSync7(sessionDir, { recursive: true });
5019
- const existing = restricted ? null : findMostRecent(sessionDir);
5960
+ const existing = restricted ? null : explicitConversationFile(sessionDir, opts.conversation);
5020
5961
  const sessionManager = existing ? SessionManager.open(existing, sessionDir, cwd) : SessionManager.create(cwd, sessionDir);
5021
5962
  const settingsManager = SettingsManager.inMemory({
5022
5963
  compaction: { enabled: false },
@@ -5031,9 +5972,14 @@ async function createBazilionSession(opts) {
5031
5972
  skills: promptSkills,
5032
5973
  sandboxMode: shellTools?.config.sandboxMode ?? "off"
5033
5974
  });
5975
+ const repository = !restricted && opts.repositoryContext && opts.repositoryContextHost ? repositoryContextIntegration(
5976
+ opts.repositoryContext,
5977
+ opts.repositoryContextHost,
5978
+ shellTools?.config.sandboxMode === "docker" ? "/workspace" : cwd
5979
+ ) : void 0;
5034
5980
  const resourceLoader = new DefaultResourceLoader({
5035
5981
  cwd,
5036
- agentDir: join19(paths.home, "pi"),
5982
+ agentDir: join22(paths.home, "pi"),
5037
5983
  settingsManager,
5038
5984
  noExtensions: true,
5039
5985
  noSkills: true,
@@ -5042,6 +5988,7 @@ async function createBazilionSession(opts) {
5042
5988
  noContextFiles: true,
5043
5989
  appendSystemPrompt: bazilionPrompt ? [bazilionPrompt] : void 0
5044
5990
  });
5991
+ if (repository) resourceLoader.getAgentsFiles = repository.files;
5045
5992
  await resourceLoader.reload();
5046
5993
  const bazilionTools = restricted ? restricted.tools : createBazilionCustomTools({
5047
5994
  agent,
@@ -5052,13 +5999,31 @@ async function createBazilionSession(opts) {
5052
5999
  mcpHost,
5053
6000
  mcpTools,
5054
6001
  fileSink,
6002
+ askUser,
6003
+ sessionId: sessionManager.getSessionId(),
5055
6004
  env
5056
6005
  });
6006
+ if (repository) bazilionTools.push(repository.tool);
6007
+ if (repository && opts.codingHost && shellTools && shellTools.config.sandboxMode === "docker" && !opts.preparedDocker)
6008
+ throw new Error("Coding commands require the admitted Docker runtime");
6009
+ if (repository && opts.codingHost && shellTools)
6010
+ bazilionTools.push(
6011
+ ...codingTools({
6012
+ host: opts.codingHost,
6013
+ root: cwd,
6014
+ context: repository.resolve,
6015
+ docker: opts.preparedDocker,
6016
+ lifecycle: opts.dockerLifecycle,
6017
+ approval: shellTools.config.approvalMode === "dangerous",
6018
+ approvalHost: bashApprovalHost,
6019
+ secrets: Object.entries(env).filter(([key]) => /KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH/i.test(key)).map(([, value]) => value ?? "")
6020
+ })
6021
+ );
5057
6022
  const customTools = shellTools?.customBash ? [...bazilionTools, shellTools.customBash] : bazilionTools;
5058
6023
  const allowedTools = [...shellTools?.hostToolNames ?? [], ...customTools.map((t) => t.name)];
5059
6024
  const { session } = await createAgentSession({
5060
6025
  cwd,
5061
- agentDir: join19(paths.home, "pi"),
6026
+ agentDir: join22(paths.home, "pi"),
5062
6027
  model,
5063
6028
  thinkingLevel: toPiThinkingLevel(restricted?.reasoningLevel ?? agent.reasoningLevel),
5064
6029
  tools: allowedTools,
@@ -5068,6 +6033,7 @@ async function createBazilionSession(opts) {
5068
6033
  modelRuntime,
5069
6034
  resourceLoader
5070
6035
  });
6036
+ repository?.bind(session, allowedTools);
5071
6037
  if (refreshApiKey) {
5072
6038
  session.agent.getApiKey = async (requestedProvider) => {
5073
6039
  if (requestedProvider !== piProviderName(providerName)) return void 0;
@@ -5112,10 +6078,12 @@ async function createProtectedBazilionSession(opts) {
5112
6078
  const shellTools = createProtectedSessionShellTools(
5113
6079
  opts.paths.teamDir,
5114
6080
  opts.docker,
5115
- opts.bashApprovalHost
6081
+ opts.bashApprovalHost,
6082
+ opts.dockerLifecycle
5116
6083
  );
6084
+ const modelCwd = codingContainerCwd(opts.docker.coding?.cwd ?? ".");
5117
6085
  mkdirSync7(opts.paths.sessionDir, { recursive: true });
5118
- const existing = findMostRecent(opts.paths.sessionDir);
6086
+ const existing = explicitConversationFile(opts.paths.sessionDir, opts.conversation);
5119
6087
  const sessionManager = existing ? SessionManager.open(existing, opts.paths.sessionDir, opts.paths.teamDir) : SessionManager.create(opts.paths.teamDir, opts.paths.sessionDir);
5120
6088
  const settingsManager = createInMemorySettingsManager();
5121
6089
  const bazilionPrompt = redactJsonValue(
@@ -5126,8 +6094,13 @@ async function createProtectedBazilionSession(opts) {
5126
6094
  }),
5127
6095
  protectedRuntimeSecrets(opts.runtime)
5128
6096
  );
6097
+ const repository = opts.repositoryContext && opts.repositoryContextHost ? repositoryContextIntegration(
6098
+ redactJsonValue(opts.repositoryContext, protectedRuntimeSecrets(opts.runtime)),
6099
+ opts.repositoryContextHost,
6100
+ PROTECTED_MODEL_CWD
6101
+ ) : void 0;
5129
6102
  const resourceLoader = new DefaultResourceLoader({
5130
- cwd: PROTECTED_MODEL_CWD,
6103
+ cwd: modelCwd,
5131
6104
  agentDir: opts.scratch.piAgentDir,
5132
6105
  settingsManager,
5133
6106
  noExtensions: true,
@@ -5138,18 +6111,35 @@ async function createProtectedBazilionSession(opts) {
5138
6111
  systemPrompt: PROTECTED_BASE_SYSTEM_PROMPT,
5139
6112
  appendSystemPrompt: bazilionPrompt ? [bazilionPrompt] : void 0
5140
6113
  });
6114
+ if (repository) resourceLoader.getAgentsFiles = repository.files;
5141
6115
  await resourceLoader.reload();
5142
6116
  const bazilionTools = createProtectedBazilionCustomTools({
5143
6117
  agent: opts.agent,
5144
6118
  memory: opts.memory,
5145
6119
  messagingHost: opts.messagingHost,
5146
6120
  userMdHost: opts.userMdHost,
5147
- fileSink: opts.fileSink
6121
+ fileSink: opts.fileSink,
6122
+ askUser: opts.askUser,
6123
+ sessionId: sessionManager.getSessionId()
5148
6124
  });
6125
+ if (repository) bazilionTools.push(repository.tool);
6126
+ if (repository && opts.codingHost)
6127
+ bazilionTools.push(
6128
+ ...codingTools({
6129
+ host: opts.codingHost,
6130
+ root: opts.paths.teamDir,
6131
+ context: repository.resolve,
6132
+ docker: opts.docker,
6133
+ lifecycle: opts.dockerLifecycle,
6134
+ approval: true,
6135
+ approvalHost: opts.bashApprovalHost,
6136
+ secrets: protectedRuntimeSecrets(opts.runtime)
6137
+ })
6138
+ );
5149
6139
  if (!shellTools.customBash) throw new Error("protected Docker bash tool is unavailable");
5150
6140
  const customTools = [...bazilionTools, shellTools.customBash];
5151
6141
  const { session } = await createAgentSession({
5152
- cwd: PROTECTED_MODEL_CWD,
6142
+ cwd: modelCwd,
5153
6143
  agentDir: opts.scratch.piAgentDir,
5154
6144
  model,
5155
6145
  thinkingLevel: toPiThinkingLevel(opts.runtime.reasoningLevel),
@@ -5160,6 +6150,10 @@ async function createProtectedBazilionSession(opts) {
5160
6150
  modelRuntime,
5161
6151
  resourceLoader
5162
6152
  });
6153
+ repository?.bind(
6154
+ session,
6155
+ customTools.map((tool) => tool.name)
6156
+ );
5163
6157
  installProtectedCredentialBoundary(
5164
6158
  session,
5165
6159
  opts.runtime.providerName,
@@ -5222,7 +6216,7 @@ async function createRestrictedReviewSession(opts) {
5222
6216
  function materializeProtectedCredentialFile(runtime, scratch) {
5223
6217
  const fields = [...runtime.credentialEnv ?? []];
5224
6218
  if (runtime.credentialFile) {
5225
- const path = join19(scratch.tempDir, "provider-credential.json");
6219
+ const path = join22(scratch.tempDir, "provider-credential.json");
5226
6220
  writeFileSync9(path, runtime.credentialFile.content, {
5227
6221
  encoding: "utf8",
5228
6222
  mode: 384,
@@ -5357,19 +6351,12 @@ function toPiThinkingLevel(level) {
5357
6351
  return "medium";
5358
6352
  }
5359
6353
  }
5360
- function findMostRecent(sessionDir) {
5361
- if (!existsSync14(sessionDir)) return null;
5362
- let newest = null;
5363
- for (const entry of readdirSync8(sessionDir)) {
5364
- if (!entry.endsWith(".jsonl")) continue;
5365
- const path = join19(sessionDir, entry);
5366
- try {
5367
- const s = statSync5(path);
5368
- if (!newest || s.mtimeMs > newest.mtimeMs) newest = { path, mtimeMs: s.mtimeMs };
5369
- } catch {
5370
- }
5371
- }
5372
- return newest?.path ?? null;
6354
+ function explicitConversationFile(directory, target) {
6355
+ if (!target || !/^[0-9a-f-]{36}$/i.test(target.id) || target.filename !== `${target.id}.jsonl`)
6356
+ throw new Error("Explicit conversation target is required");
6357
+ const file = join22(directory, target.filename);
6358
+ readCanonicalSessionFile(directory, target.filename, target.id);
6359
+ return file;
5373
6360
  }
5374
6361
 
5375
6362
  // ../daemon/src/runtime/worker/api-key-refresh.ts
@@ -5488,6 +6475,21 @@ function createIpcMessagingHost(call) {
5488
6475
  approvalStatus: (agentId, approvalId) => call("approvalStatus", { agentId, approvalId })
5489
6476
  };
5490
6477
  }
6478
+ function createIpcDockerLifecycle(namespace, call) {
6479
+ if (!namespace) return void 0;
6480
+ return {
6481
+ name: (kind) => `bazilion-${namespace}-${kind}-${randomUUID20()}`,
6482
+ beforeCreate: async (identity2) => {
6483
+ await call("containerBeforeCreate", { containerName: identity2.containerName });
6484
+ },
6485
+ afterCreate: async (containerName) => {
6486
+ await call("containerAfterCreate", { containerName });
6487
+ },
6488
+ afterRemove: async (containerName) => {
6489
+ await call("containerAfterRemove", { containerName });
6490
+ }
6491
+ };
6492
+ }
5491
6493
  function createIpcUserMdHost(call) {
5492
6494
  return {
5493
6495
  get: (teamId) => call("userMdGet", { teamId }),
@@ -5533,13 +6535,13 @@ function createReviewState(evidenceItems) {
5533
6535
  name: "propose_lesson",
5534
6536
  label: "propose_lesson",
5535
6537
  description: "Submit zero to five evidence-backed lesson proposals in one final batch. This is the only available action.",
5536
- parameters: Type3.Object({
5537
- proposals: Type3.Array(
5538
- Type3.Object({
5539
- scope: Type3.Union([Type3.Literal("private"), Type3.Literal("shared")]),
5540
- text: Type3.String({ minLength: 1, maxLength: 500 }),
5541
- evidenceEntryIds: Type3.Array(
5542
- Type3.Object({ sessionId: Type3.String(), entryOrdinal: Type3.Integer({ minimum: 0 }) }),
6538
+ parameters: Type5.Object({
6539
+ proposals: Type5.Array(
6540
+ Type5.Object({
6541
+ scope: Type5.Union([Type5.Literal("private"), Type5.Literal("shared")]),
6542
+ text: Type5.String({ minLength: 1, maxLength: 500 }),
6543
+ evidenceEntryIds: Type5.Array(
6544
+ Type5.Object({ sessionId: Type5.String(), entryOrdinal: Type5.Integer({ minimum: 0 }) }),
5543
6545
  { minItems: 1 }
5544
6546
  )
5545
6547
  }),
@@ -5551,14 +6553,14 @@ function createReviewState(evidenceItems) {
5551
6553
  const input = params;
5552
6554
  const validated = [];
5553
6555
  for (const candidate of input.proposals) {
5554
- const text = candidate.text.trim();
5555
- if (!text || text.length > 500) throw new Error("proposal text must be 1-500 characters");
6556
+ const text2 = candidate.text.trim();
6557
+ if (!text2 || text2.length > 500) throw new Error("proposal text must be 1-500 characters");
5556
6558
  for (const evidence of candidate.evidenceEntryIds) {
5557
6559
  if (!allowedEvidence.has(`${evidence.sessionId}:${evidence.entryOrdinal}`)) {
5558
6560
  throw new Error("proposal cites evidence outside the supplied digest");
5559
6561
  }
5560
6562
  }
5561
- validated.push({ ...candidate, text });
6563
+ validated.push({ ...candidate, text: text2 });
5562
6564
  }
5563
6565
  proposalBatchSubmitted = true;
5564
6566
  proposals.push(...validated);
@@ -5581,6 +6583,8 @@ function createReviewState(evidenceItems) {
5581
6583
  };
5582
6584
  }
5583
6585
  async function createSessionForInput(input, ipcCall) {
6586
+ if (input.kind !== "restricted_review" && !input.repositoryContext)
6587
+ throw new Error("Coding turn requires daemon-prepared repository context");
5584
6588
  if (input.kind === "configured_operator_http") {
5585
6589
  const paths = resolvePaths();
5586
6590
  const memory2 = qmdBackend(`${input.agent.team.path}/memory`);
@@ -5602,6 +6606,12 @@ async function createSessionForInput(input, ipcCall) {
5602
6606
  }) : void 0;
5603
6607
  const handle2 = await createBazilionSession({
5604
6608
  agent: input.agent,
6609
+ preparedDocker: input.configuredDocker?.docker,
6610
+ dockerLifecycle: createIpcDockerLifecycle(input.containerNamespace, ipcCall),
6611
+ conversation: input.conversation,
6612
+ repositoryContext: input.repositoryContext,
6613
+ repositoryContextHost: (target) => ipcCall("repositoryContext", { target }),
6614
+ codingHost: { invoke: (request) => ipcCall("coding", request) },
5605
6615
  paths,
5606
6616
  env: process.env,
5607
6617
  memory: memory2,
@@ -5614,7 +6624,15 @@ async function createSessionForInput(input, ipcCall) {
5614
6624
  mcpHost,
5615
6625
  mcpTools: input.mcpTools,
5616
6626
  bashApprovalHost: bashApprovalHost2,
5617
- fileSink: (file) => emit({ kind: "event", event: { type: "file", ...file } })
6627
+ askUser: input.questionEnabled ? (toolCallId, question) => ipcCall("askUser", { toolCallId, question }) : void 0,
6628
+ fileSink: async (file, source) => {
6629
+ const result = await ipcCall(
6630
+ "publishResult",
6631
+ { ...file, ...source }
6632
+ );
6633
+ emit({ kind: "event", event: { type: "file", ...file, result } });
6634
+ return result;
6635
+ }
5618
6636
  });
5619
6637
  return { handle: handle2 };
5620
6638
  }
@@ -5646,16 +6664,29 @@ async function createSessionForInput(input, ipcCall) {
5646
6664
  });
5647
6665
  const handle = await createProtectedBazilionSession({
5648
6666
  agent: input.agent,
6667
+ conversation: input.conversation,
6668
+ repositoryContext: input.repositoryContext,
6669
+ repositoryContextHost: (target) => ipcCall("repositoryContext", { target }),
6670
+ codingHost: { invoke: (request) => ipcCall("coding", request) },
5649
6671
  runtime: input.runtime,
5650
6672
  paths: input.paths,
5651
6673
  scratch: input.scratch,
5652
6674
  docker: input.docker,
6675
+ dockerLifecycle: createIpcDockerLifecycle(input.containerNamespace, ipcCall),
5653
6676
  memory,
5654
6677
  messagingHost,
5655
6678
  userMdHost,
5656
6679
  bashApprovalHost,
6680
+ askUser: input.questionEnabled ? (toolCallId, question) => ipcCall("askUser", { toolCallId, question }) : void 0,
5657
6681
  refreshApiKey,
5658
- fileSink: (file) => emit({ kind: "event", event: { type: "file", ...file } })
6682
+ fileSink: async (file, source) => {
6683
+ const result = await ipcCall("publishResult", {
6684
+ ...file,
6685
+ ...source
6686
+ });
6687
+ emit({ kind: "event", event: { type: "file", ...file, result } });
6688
+ return result;
6689
+ }
5659
6690
  });
5660
6691
  return { handle };
5661
6692
  }
@@ -5684,7 +6715,30 @@ async function main() {
5684
6715
  const ipcCall = createWorkerIpcCall();
5685
6716
  const { handle, reviewState } = await createSessionForInput(input, ipcCall);
5686
6717
  session = handle.session;
6718
+ const consumptionAcks = /* @__PURE__ */ new Set();
5687
6719
  const unsubscribe = session.subscribe((piEvent) => {
6720
+ if (input.kind !== "restricted_review" && input.questionEnabled && piEvent.type === "message_end" && piEvent.message.role === "toolResult" && piEvent.message.toolName === "ask_user" && !piEvent.message.isError) {
6721
+ const message = piEvent.message;
6722
+ const content = message.content[0];
6723
+ if (content?.type === "text") {
6724
+ try {
6725
+ const result = JSON.parse(content.text);
6726
+ if (typeof result.questionId === "string") {
6727
+ const ack = new Promise((resolve6) => setImmediate(resolve6)).then(
6728
+ () => ipcCall("questionConsumed", {
6729
+ questionId: result.questionId,
6730
+ toolCallId: message.toolCallId
6731
+ })
6732
+ ).catch(() => {
6733
+ }).finally(() => {
6734
+ consumptionAcks.delete(ack);
6735
+ });
6736
+ consumptionAcks.add(ack);
6737
+ }
6738
+ } catch {
6739
+ }
6740
+ }
6741
+ }
5688
6742
  if (reviewState && piEvent.type === "tool_execution_end" && piEvent.toolName === "propose_lesson" && piEvent.isError) {
5689
6743
  reviewState.recordValidationFailure();
5690
6744
  if (reviewState.validationFailures() >= 2) void session?.abort();
@@ -5704,6 +6758,7 @@ async function main() {
5704
6758
  promptImages.length > 0 ? { images: promptImages } : void 0
5705
6759
  );
5706
6760
  await session.agent.waitForIdle();
6761
+ await Promise.all(consumptionAcks);
5707
6762
  if (reviewState) {
5708
6763
  if (reviewState.validationFailures() >= 2) {
5709
6764
  throw new Error("review output remained invalid after one schema-repair attempt");