@chibitek/chibi 0.1.162 → 0.1.169

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 (104) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-D0_WX2x_.js → abnfDiagram-O67JEVCF-aQkRN0ee.js} +1 -1
  2. package/dist/assets/{arc-lzBXqTbJ.js → arc-BT7qPVAa.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-KQZbO2M3.js → architectureDiagram-NJMV4G6O-oMlARqKW.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-zb3pW64Y.js → blockDiagram-BEXU5L5S-CKUrb4js.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-CADaXczd.js → c4Diagram-YGBWAQC7-BkL5cRIA.js} +1 -1
  6. package/dist/assets/channel-BZDQCBTd.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-DJP27cEp.js → chunk-2Q5K7J3B-BMitkm2s.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-Cp9Z7Ybl.js → chunk-3FUC2YCW-DOhVN2ed.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-DjXxom_0.js → chunk-5VM5RSS4-BFkFo_ng.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-DpEJ08zA.js → chunk-GTNCS2PH-QbxYUf3Q.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-CQ4Kadr-.js → chunk-GWA4HPMP-BZChfTYw.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-CwXmVMdh.js → chunk-JWPE2WC7-BXpmYglA.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-Vl_kib_R.js → chunk-NETBCI7D-CIZTrz1i.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-BhAVbfJe.js → chunk-WEXAMYUT-FhqQ0sO0.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-s1a4nKQE.js → chunk-XXDRQBXY-5ThtmFbP.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-DeGQSNky.js → classDiagram-v2-NBCMYWYE-BzObC5Qw.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-D5sUHYFG.js → cose-bilkent-JH36ORCC-BASV9Okv.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-DmTvN9T2.js → cynefin-OW5HDTMX-B0zQYMFh.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-Cqtt1jCF.js → cynefinDiagram-VND7K2PF-DMEO1cHA.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-DRZKkSJg.js → dagre-6A5THRUB-BG04TSi_.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-1od4dKOU.js → diagram-22UHCM2B-CfpVNMhg.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-D3_yCQOJ.js → diagram-3UASUU5V-DG9L4I1L.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-D9UgsVGC.js → diagram-ATOU4E4O-Db7Bf63S.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-DMxsJ_X5.js → diagram-CDSNMT55-Cdzv1tJn.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-CK47Pp3D.js → diagram-MLGK6HIB-BBMeXGKE.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-D0axYDk-.js → diagram-MPIPVDR6-BAQ22zk3.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-hkbV8j2o.js → ebnfDiagram-ZINNZB2B-BDZgSFSF.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-DL9jncwy.js → elk-276RUBZZ-Bx4-nN8N.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-etb_6SR6.js → erDiagram-OPXOYQCR-D2Pn-cqm.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-4Dgg4I8f.js → flowDiagram-KWPJA3E3-hsbvL3Ow.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-DRBB4AaO.js → ganttDiagram-FUAMR5RP-DBUgYe4p.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-CzgKN-tE.js → gitGraphDiagram-X574FWY7-ChGeSINg.js} +1 -1
  33. package/dist/assets/{index-fHtcwzVK.js → index-ByXr4FRl.js} +195 -196
  34. package/dist/assets/index-COwCaN7p.css +1 -0
  35. package/dist/assets/{index-BT2giPxq.js → index-Ch93Ygtq.js} +1 -1
  36. package/dist/assets/{infoDiagram-VRGFBTTK-DwpjLCMe.js → infoDiagram-VRGFBTTK-DY2EeC-A.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-H-CWnU1-.js → ishikawaDiagram-OU5B5YK6-BznYuY04.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-CY-oDrpq.js → journeyDiagram-ZHPQQLJL-CX74ZLbW.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-DMGIKH3g.js → kanban-definition-PNTS6WVX-0DPavB-t.js} +1 -1
  40. package/dist/assets/{linear-BHhtTLK8.js → linear-BHeqbvM8.js} +1 -1
  41. package/dist/assets/{mermaid.core-BWK_Ui3n.js → mermaid.core-Dm-Ny6a0.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-Cc17hw2c.js → mindmap-definition-NLK3R4M7-BPrMElav.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-BqFjUBJK.js → pegDiagram-GJSIUBJH-CaD7wyyX.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-ikmgTCa3.js → pieDiagram-5QR66LMP-lbSp8R4K.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-CC3s5Dsc.js → quadrantDiagram-O4NWA36T-Bs3Sn2of.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-Dtwf869F.js → railroadDiagram-XR7U4H2S-C88ykXCV.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-C5p7AgwQ.js → requirementDiagram-PLB6GJNP-mgIQnORW.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-8NKN770X.js → sankeyDiagram-IPEJSGJF-K5esEw-m.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-DQucgrWO.js → sequenceDiagram-PO4LG4MO-D5v8PJqQ.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-Cxmcj9Nl.js → sizeCapture-INFHLROL-BHBBDTEI.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-DEdvHXnJ.js → stateDiagram-v2-GCMORJYK-CeRdNmKQ.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-BOGePqj1.js → swimlanes-2SLR337P-D8CbMCI8.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-CQHcTGnI.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-DfpRxJgE.js → timeline-definition-EJHVYXUP-CMG02UV1.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-BOmQFDT4.js → usecaseDiagram-POWQR4AR-Bm8h6ijK.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-84pfbvUi.js → vennDiagram-UO4OBE2U-Dk9tFjlM.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-Cbxnw8ip.js → wardleyDiagram-VNRHLVJA-61vrb2Ft.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-BLiz8vqY.js → xychartDiagram-PMCCYNJV-C7uO44ng.js} +1 -1
  59. package/dist/handbook/atomic-handbook.html +5 -0
  60. package/dist/index.html +2 -2
  61. package/dist-server/container-mcp.js +5 -0
  62. package/dist-server/drivers/agents-proxy.js +65 -41
  63. package/dist-server/index.js +4652 -4232
  64. package/dist-server/openmausbot.js +499 -282
  65. package/dist-server/pair-cli.js +499 -282
  66. package/dist-server/server/auto-approve.js +4 -1
  67. package/dist-server/server/brand.js +11 -0
  68. package/dist-server/server/config.js +13 -18
  69. package/dist-server/server/connection-credential.js +59 -0
  70. package/dist-server/server/container-computer.js +5 -26
  71. package/dist-server/server/credential-card-request.js +52 -0
  72. package/dist-server/server/dictation.js +6 -3
  73. package/dist-server/server/drivers/agents-call.js +3 -23
  74. package/dist-server/server/drivers/agents-catalog.js +2 -20
  75. package/dist-server/server/drivers/agents-credential.js +57 -0
  76. package/dist-server/server/drivers/builtIn.js +9 -1
  77. package/dist-server/server/drivers/claude-elicitation.js +76 -0
  78. package/dist-server/server/drivers/claude-stdin.js +47 -0
  79. package/dist-server/server/drivers/claude.js +23 -39
  80. package/dist-server/server/drivers/codex-elicitation.js +93 -0
  81. package/dist-server/server/drivers/codex.js +41 -107
  82. package/dist-server/server/engine-platform-fleet.js +32 -0
  83. package/dist-server/server/external-room.js +24 -0
  84. package/dist-server/server/index.js +101 -114
  85. package/dist-server/server/mcp-elicitation.js +120 -0
  86. package/dist-server/server/mcp-http.js +52 -2
  87. package/dist-server/server/mcp-probe.js +3 -0
  88. package/dist-server/server/peer-roster.js +43 -0
  89. package/dist-server/server/phone-secret.js +12 -4
  90. package/dist-server/server/routes/dictation.js +6 -1
  91. package/dist-server/server/system-prompt.js +1 -1
  92. package/dist-server/server/text-actions.js +25 -0
  93. package/dist-server/shared/credential-request.js +15 -6
  94. package/dist-server/shared/engine-platforms.js +19 -0
  95. package/dist-server/vps-container-mcp.js +5 -0
  96. package/package.json +1 -1
  97. package/dist/assets/channel-Br57lm8F.js +0 -1
  98. package/dist/assets/index-B2fJMh8N.css +0 -1
  99. package/dist/assets/swimlanesDiagram-TC7HE7FX-BSe9TZ6t.js +0 -8
  100. package/dist-server/connector-proxy.js +0 -205
  101. package/dist-server/server/composio.js +0 -1051
  102. package/dist-server/server/connector-proxy.js +0 -249
  103. package/dist-server/server/mcp-public-registry.js +0 -325
  104. package/dist-server/shared/connector-availability.js +0 -12
@@ -33,6 +33,8 @@ import { SPAWNED_PROXIES } from "../proxy-paths.js";
33
33
  import { rtkRewrite } from "../rtk-rewrite.js";
34
34
  import { volatileNote, volatileSectionsOf, withNote } from "./volatile-notes.js";
35
35
  import { extractMcpImages } from "../mcp-tool-images.js";
36
+ import { claudeUserMessage, diagnosticClaudeUserMessage } from "./claude-stdin.js";
37
+ import { createClaudeElicitations } from "./claude-elicitation.js";
36
38
  import { ASK_USER_QUESTION_TOOL, askQuestionSummary, parseAskQuestions, parseChoices, questionChoices, } from "../../shared/ask-question.js";
37
39
  const CONNECTOR_CACHE_MS = 5 * 60 * 1000;
38
40
  /** Parent-session credentials/routing that a named account must not inherit.
@@ -812,45 +814,6 @@ function decodeConfig(raw) {
812
814
  ...(disallowedTools !== undefined ? { disallowedTools } : {}),
813
815
  };
814
816
  }
815
- /** Claude's stream-json input accepts the same image source blocks as the
816
- * Anthropic Messages API. Keep the old string form for text-only turns so a
817
- * CLI update cannot disturb the overwhelmingly common path. */
818
- function claudeUserMessage(text, images) {
819
- if (!images?.length)
820
- return { type: "user", message: { role: "user", content: text } };
821
- const content = images.map((image) => ({
822
- type: "image",
823
- source: {
824
- type: "base64",
825
- media_type: image.mime,
826
- data: readFileSync(image.path).toString("base64"),
827
- },
828
- }));
829
- if (text)
830
- content.push({ type: "text", text });
831
- return { type: "user", message: { role: "user", content } };
832
- }
833
- /** Native traces are routinely attached to bug reports. Preserve the image
834
- * block's shape and size for debugging, but never persist its base64 bytes. */
835
- function diagnosticClaudeUserMessage(message) {
836
- if (!Array.isArray(message.message.content))
837
- return message;
838
- return {
839
- ...message,
840
- message: {
841
- ...message.message,
842
- content: message.message.content.map((block) => block.type === "image"
843
- ? {
844
- ...block,
845
- source: {
846
- ...block.source,
847
- data: `[image data: ${block.source.data.length} base64 chars]`,
848
- },
849
- }
850
- : block),
851
- },
852
- };
853
- }
854
817
  export const ClaudeDriver = {
855
818
  driverKind: DRIVER_KIND,
856
819
  metadata: { displayName: "Claude", supportsMultipleInstances: true },
@@ -919,6 +882,7 @@ export const ClaudeDriver = {
919
882
  const sumCost = (a, b) => a === null ? b : b === null ? a : a + b;
920
883
  const STEER_FOLLOWUP_MS = 1_500;
921
884
  const sessions = new Map();
885
+ const elicitations = createClaudeElicitations({ emit: (event) => emit(event), base: (threadId, turnId) => base(threadId, turnId) });
922
886
  // "Always allow this session", per thread: the actions the person allowed
923
887
  // for the native Claude session. Kept here, not on the broker, so a
924
888
  // relaunch with --resume (idle close, retry) still honors them; a fresh
@@ -960,6 +924,7 @@ export const ClaudeDriver = {
960
924
  if (!s || s.closing)
961
925
  return;
962
926
  s.closing = true;
927
+ elicitations.endThread(threadId);
963
928
  if (s.idleTimer)
964
929
  clearTimeout(s.idleTimer);
965
930
  // Broker ownership belongs to this session. Detach and close it now,
@@ -1831,6 +1796,22 @@ export const ClaudeDriver = {
1831
1796
  }
1832
1797
  break;
1833
1798
  }
1799
+ case "control_request":
1800
+ // an MCP server's confirmation question (server/drivers/claude-elicitation.ts)
1801
+ elicitations.handle(o, {
1802
+ threadId,
1803
+ turnId: currentTurnId(),
1804
+ closing: session.closing,
1805
+ write: (frame) => {
1806
+ try {
1807
+ if (session.child.stdin.writable && !session.child.stdin.destroyed)
1808
+ session.child.stdin.write(JSON.stringify(frame) + "\n");
1809
+ appendNative(threadId, { dir: "out", source: "claude.sdk.message", msg: frame });
1810
+ }
1811
+ catch { }
1812
+ },
1813
+ });
1814
+ break;
1834
1815
  case "user":
1835
1816
  for (const b of Array.isArray(o.message?.content) ? o.message.content : []) {
1836
1817
  if (b.type === "tool_result") {
@@ -2272,6 +2253,9 @@ export const ClaudeDriver = {
2272
2253
  steer,
2273
2254
  interruptTurn: async (threadId) => active.get(threadId)?.stop(),
2274
2255
  respondToRequest: async (threadId, requestId, decision) => {
2256
+ const confirmation = elicitations.respond(threadId, requestId, decision);
2257
+ if (confirmation)
2258
+ return confirmation;
2275
2259
  // fail-closed by construction: no broker, or an ask that already
2276
2260
  // timed out / settled, is `unavailable` — the caller denies
2277
2261
  const broker = sessions.get(threadId)?.broker ?? active.get(threadId)?.broker;
@@ -0,0 +1,93 @@
1
+ // MCP elicitation forms Codex's app-server forwards (mcpServer/elicitation/request)
2
+ // that Atomic can answer on a permission card: Codex's own app-access approvals,
3
+ // and a configured server's plain confirmation (server/mcp-elicitation.ts).
4
+ import { elicitationConfirm } from "../mcp-elicitation.js";
5
+ const plainRecord = (value) => value && typeof value === "object" && !Array.isArray(value)
6
+ ? value
7
+ : null;
8
+ export const containsControlCharacter = (value) => {
9
+ for (const character of value) {
10
+ const code = character.charCodeAt(0);
11
+ if (code <= 0x1f || code === 0x7f)
12
+ return true;
13
+ }
14
+ return false;
15
+ };
16
+ function boundedLabel(value) {
17
+ if (typeof value !== "string")
18
+ return null;
19
+ const label = value.trim();
20
+ return label && label.length <= 160 && !containsControlCharacter(label) ? label : null;
21
+ }
22
+ function ordinaryApprovalValue(value) {
23
+ const normalized = value.trim().toLowerCase();
24
+ if (/session|always|permanent|forever|persistent/.test(normalized))
25
+ return false;
26
+ return normalized === "once" || /^(?:accept|approve|allow)(?:ed|[-_]?once)?$/.test(normalized);
27
+ }
28
+ /** Recognize only schema-backed app-access approvals. Arbitrary MCP forms
29
+ * (credentials, free text, URLs, or required fields without a one-time enum)
30
+ * remain user input and are declined; Full access never fabricates them. */
31
+ export function mcpAppApprovalForm(params) {
32
+ const request = plainRecord(params);
33
+ if (!request || request.mode !== "form")
34
+ return null;
35
+ const metadata = plainRecord(request._meta);
36
+ const target = plainRecord(metadata?.target);
37
+ const toolParams = plainRecord(metadata?.tool_params);
38
+ const message = boundedLabel(request.message) ?? "App access requested";
39
+ const appName = [
40
+ metadata?.app_name,
41
+ metadata?.appName,
42
+ metadata?.app,
43
+ target?.app,
44
+ target?.name,
45
+ toolParams?.app_name,
46
+ toolParams?.app,
47
+ metadata?.connector_name,
48
+ metadata?.connectorName,
49
+ ].map(boundedLabel).find(Boolean) ?? message.match(/^Allow ChatGPT to use (.+?)\?$/i)?.[1]?.trim();
50
+ // The application identity is the second half of the discriminator. A
51
+ // required approval-looking enum by itself must not turn an arbitrary form
52
+ // into a permission prompt.
53
+ if (!appName)
54
+ return null;
55
+ const schema = plainRecord(request.requestedSchema);
56
+ const properties = plainRecord(schema?.properties);
57
+ const required = schema?.required;
58
+ if (!properties ||
59
+ !Array.isArray(required) ||
60
+ required.length === 0 ||
61
+ required.length > 8 ||
62
+ !required.every((key) => typeof key === "string" && key.length > 0 && key.length <= 100))
63
+ return null;
64
+ const content = {};
65
+ for (const key of required) {
66
+ const field = plainRecord(properties[key]);
67
+ if (!field)
68
+ return null;
69
+ const enumValues = Array.isArray(field.enum)
70
+ ? field.enum.filter((value) => typeof value === "string")
71
+ : [];
72
+ const oneOfValues = Array.isArray(field.oneOf)
73
+ ? field.oneOf
74
+ .map((option) => boundedLabel(plainRecord(option)?.const))
75
+ .filter((value) => Boolean(value))
76
+ : [];
77
+ const chosen = [...oneOfValues, ...enumValues].find(ordinaryApprovalValue);
78
+ if (!chosen)
79
+ return null;
80
+ content[key] = chosen;
81
+ }
82
+ const tool = boundedLabel(appName) ?? boundedLabel(request.serverName) ?? "app_access";
83
+ return { tool, summary: message, allowResult: { action: "accept", content } };
84
+ }
85
+ /** A configured server (by its mount name) asking the person to confirm a
86
+ * change: Addigy confirms every write this way. The card always needs the
87
+ * person, whatever the approval mode. Null for anything else. */
88
+ export function codexServerConfirm(params, customByMountName) {
89
+ const serverName = plainRecord(params)?.serverName;
90
+ if (typeof serverName !== "string" || !customByMountName.has(serverName))
91
+ return null;
92
+ return elicitationConfirm(params);
93
+ }
@@ -15,6 +15,8 @@ import { describeProbeFailure, describeSpawnFailure, killCliTree, probeCli, spaw
15
15
  import { setupLog } from "../setup-log.js";
16
16
  import { isHarnessOwnedMcpEnvName } from "../mcp-registry.js";
17
17
  import { toolAnnotations } from "../mcp-tool-hints.js";
18
+ import { elicitationAnswer, elicitationSummary, elicitationTool } from "../mcp-elicitation.js";
19
+ import { codexServerConfirm, containsControlCharacter, mcpAppApprovalForm } from "./codex-elicitation.js";
18
20
  import { newEventId, newId } from "../contracts.js";
19
21
  import { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
20
22
  import { codexLocalProviderArgs } from "./local-inject.js";
@@ -181,83 +183,6 @@ function additionalPermissionSummary(permissions, reason) {
181
183
  const plainRecord = (value) => value && typeof value === "object" && !Array.isArray(value)
182
184
  ? value
183
185
  : null;
184
- const containsControlCharacter = (value) => {
185
- for (const character of value) {
186
- const code = character.charCodeAt(0);
187
- if (code <= 0x1f || code === 0x7f)
188
- return true;
189
- }
190
- return false;
191
- };
192
- function boundedLabel(value) {
193
- if (typeof value !== "string")
194
- return null;
195
- const label = value.trim();
196
- return label && label.length <= 160 && !containsControlCharacter(label) ? label : null;
197
- }
198
- function ordinaryApprovalValue(value) {
199
- const normalized = value.trim().toLowerCase();
200
- if (/session|always|permanent|forever|persistent/.test(normalized))
201
- return false;
202
- return normalized === "once" || /^(?:accept|approve|allow)(?:ed|[-_]?once)?$/.test(normalized);
203
- }
204
- /** Recognize only schema-backed app-access approvals. Arbitrary MCP forms
205
- * (credentials, free text, URLs, or required fields without a one-time enum)
206
- * remain user input and are declined; Full access never fabricates them. */
207
- function mcpAppApprovalForm(params) {
208
- const request = plainRecord(params);
209
- if (!request || request.mode !== "form")
210
- return null;
211
- const metadata = plainRecord(request._meta);
212
- const target = plainRecord(metadata?.target);
213
- const toolParams = plainRecord(metadata?.tool_params);
214
- const message = boundedLabel(request.message) ?? "App access requested";
215
- const appName = [
216
- metadata?.app_name,
217
- metadata?.appName,
218
- metadata?.app,
219
- target?.app,
220
- target?.name,
221
- toolParams?.app_name,
222
- toolParams?.app,
223
- metadata?.connector_name,
224
- metadata?.connectorName,
225
- ].map(boundedLabel).find(Boolean) ?? message.match(/^Allow ChatGPT to use (.+?)\?$/i)?.[1]?.trim();
226
- // The application identity is the second half of the discriminator. A
227
- // required approval-looking enum by itself must not turn an arbitrary form
228
- // into a permission prompt.
229
- if (!appName)
230
- return null;
231
- const schema = plainRecord(request.requestedSchema);
232
- const properties = plainRecord(schema?.properties);
233
- const required = schema?.required;
234
- if (!properties ||
235
- !Array.isArray(required) ||
236
- required.length === 0 ||
237
- required.length > 8 ||
238
- !required.every((key) => typeof key === "string" && key.length > 0 && key.length <= 100))
239
- return null;
240
- const content = {};
241
- for (const key of required) {
242
- const field = plainRecord(properties[key]);
243
- if (!field)
244
- return null;
245
- const enumValues = Array.isArray(field.enum)
246
- ? field.enum.filter((value) => typeof value === "string")
247
- : [];
248
- const oneOfValues = Array.isArray(field.oneOf)
249
- ? field.oneOf
250
- .map((option) => boundedLabel(plainRecord(option)?.const))
251
- .filter((value) => Boolean(value))
252
- : [];
253
- const chosen = [...oneOfValues, ...enumValues].find(ordinaryApprovalValue);
254
- if (!chosen)
255
- return null;
256
- content[key] = chosen;
257
- }
258
- const tool = boundedLabel(appName) ?? boundedLabel(request.serverName) ?? "app_access";
259
- return { tool, summary: message, allowResult: { action: "accept", content } };
260
- }
261
186
  /** Codex persists these values on its native thread. Keep them explicit on
262
187
  * start, resume, and every turn so switching modes cannot leave a more
263
188
  * permissive sandbox/reviewer stuck to the next request. */
@@ -920,7 +845,9 @@ export const CodexDriver = {
920
845
  const isLegacyMcpPermission = method === "mcpServer/elicitation/request" &&
921
846
  params?._meta?.codex_approval_kind === "mcp_tool_call";
922
847
  const mcpAppApproval = isMcpElicitation ? mcpAppApprovalForm(params) : null;
923
- const isMcpPermission = isLegacyMcpPermission || mcpAppApproval !== null;
848
+ // one of the person's own servers confirming a change (Addigy's writes)
849
+ const serverConfirm = isMcpElicitation && !isLegacyMcpPermission && !mcpAppApproval ? codexServerConfirm(params, customByMountName) : null;
850
+ const isMcpPermission = isLegacyMcpPermission || mcpAppApproval !== null || serverConfirm !== null;
924
851
  const isQuestion = method === "item/tool/requestUserInput";
925
852
  const isAdditionalPermission = method === "item/permissions/requestApproval";
926
853
  const isPermission = legacy || isMcpPermission || isAdditionalPermission ||
@@ -976,25 +903,29 @@ export const CodexDriver = {
976
903
  const mcpToolRef = customServer && mcpTool
977
904
  ? { server: customServer, tool: mcpTool, ...(toolAnnotations(params?._meta?.annotations) ? { annotations: toolAnnotations(params._meta.annotations) } : {}) }
978
905
  : undefined;
979
- const tool = mcpAppApproval
980
- ? mcpAppApproval.tool
981
- : isLegacyMcpPermission
982
- ? (mcpTool ?? "mcp")
906
+ const tool = serverConfirm
907
+ ? elicitationTool(customByMountName.get(params.serverName))
908
+ : mcpAppApproval
909
+ ? mcpAppApproval.tool
910
+ : isLegacyMcpPermission
911
+ ? (mcpTool ?? "mcp")
912
+ : isAdditionalPermission
913
+ ? "permissions"
914
+ : method === "item/fileChange/requestApproval" || method === "applyPatchApproval"
915
+ ? "edit"
916
+ : isQuestion
917
+ ? "ask_user"
918
+ : "shell";
919
+ const permissionResult = (allow) => serverConfirm
920
+ ? elicitationAnswer(serverConfirm, allow)
921
+ : isMcpPermission
922
+ ? allow
923
+ ? (mcpAppApproval?.allowResult ?? { action: "accept", content: {} })
924
+ : { action: "decline" }
983
925
  : isAdditionalPermission
984
- ? "permissions"
985
- : method === "item/fileChange/requestApproval" || method === "applyPatchApproval"
986
- ? "edit"
987
- : isQuestion
988
- ? "ask_user"
989
- : "shell";
990
- const permissionResult = (allow) => isMcpPermission
991
- ? allow
992
- ? (mcpAppApproval?.allowResult ?? { action: "accept", content: {} })
993
- : { action: "decline" }
994
- : isAdditionalPermission
995
- ? { permissions: allow ? grantedPermissions(params.permissions) : {}, scope: "turn" }
996
- : { decision: allow ? (legacy ? "approved" : "accept") : legacy ? "denied" : "decline" };
997
- if (autoAcceptPermissions && isPermission) {
926
+ ? { permissions: allow ? grantedPermissions(params.permissions) : {}, scope: "turn" }
927
+ : { decision: allow ? (legacy ? "approved" : "accept") : legacy ? "denied" : "decline" };
928
+ if (autoAcceptPermissions && isPermission && !serverConfirm) {
998
929
  return send({
999
930
  jsonrpc: "2.0",
1000
931
  id: msg.id,
@@ -1004,15 +935,17 @@ export const CodexDriver = {
1004
935
  const requestId = newId();
1005
936
  const summary = isAdditionalPermission
1006
937
  ? additionalPermissionSummary(params.permissions, params.reason)
1007
- : isMcpPermission
1008
- ? (mcpAppApproval?.summary ?? (typeof params.message === "string" ? params.message : "MCP access requested"))
1009
- : typeof params.command === "string"
1010
- ? params.command
1011
- : Array.isArray(params.questions)
1012
- ? params.questions.map((q) => q.question ?? q.header).filter(Boolean).join(" · ")
1013
- : typeof params.reason === "string"
1014
- ? params.reason
1015
- : tool;
938
+ : serverConfirm
939
+ ? elicitationSummary(customByMountName.get(params.serverName), serverConfirm)
940
+ : isMcpPermission
941
+ ? (mcpAppApproval?.summary ?? (typeof params.message === "string" ? params.message : "MCP access requested"))
942
+ : typeof params.command === "string"
943
+ ? params.command
944
+ : Array.isArray(params.questions)
945
+ ? params.questions.map((q) => q.question ?? q.header).filter(Boolean).join(" · ")
946
+ : typeof params.reason === "string"
947
+ ? params.reason
948
+ : tool;
1016
949
  const choices = isQuestion
1017
950
  ? (params.questions?.[0]?.options ?? []).map((o) => o.label).slice(0, 5)
1018
951
  : undefined;
@@ -1031,7 +964,8 @@ export const CodexDriver = {
1031
964
  send({
1032
965
  jsonrpc: "2.0",
1033
966
  id: msg.id,
1034
- result: permissionResult(behavior === "allow"),
967
+ // a server's question nobody answered is "no answer", not a no
968
+ result: serverConfirm && source !== "user" ? { action: "cancel" } : permissionResult(behavior === "allow"),
1035
969
  });
1036
970
  }
1037
971
  emit({ ...base(threadId, turnId), type: "request.resolved", requestId, behavior, source });
@@ -1048,7 +982,7 @@ export const CodexDriver = {
1048
982
  summary,
1049
983
  choices,
1050
984
  approvalScope: controlsHost ? "local-computer" : undefined,
1051
- requiresExplicitApproval: isAdditionalPermission || undefined,
985
+ requiresExplicitApproval: isAdditionalPermission || serverConfirm !== null || undefined,
1052
986
  ...(mcpToolRef ? { mcpTool: mcpToolRef } : {}),
1053
987
  });
1054
988
  };
@@ -0,0 +1,32 @@
1
+ // Which engines the runtime fleet carries on this OS. The gate itself lives in
2
+ // shared/engine-platforms.ts (e.g. Qwen off on Windows for now); this module
3
+ // applies it to instance maps so server/config.ts stays a config loader.
4
+ import { engineOnPlatform } from "../shared/engine-platforms.js";
5
+ /** Apple Intelligence runs only on Apple silicon Macs; elsewhere the engine
6
+ * is left out of the fleet rather than listed as unavailable. */
7
+ export function onDeviceFleet(platform = process.platform, arch = process.arch) {
8
+ return platform === "darwin" && arch === "arm64"
9
+ ? { appleIntelligence: { driver: "appleIntelligence", displayName: "Apple Intelligence (on this Mac)" } }
10
+ : {};
11
+ }
12
+ /** An engine switched off on this OS gets no runtime instance: not in the
13
+ * picker, not installable, and a caller looking one up finds nothing to load.
14
+ * Only the runtime map drops it; a saved entry stays on disk and comes back
15
+ * when the engine is turned on again. Mutates and returns `map`. */
16
+ export function dropEnginesOffPlatform(map, platform) {
17
+ for (const [id, entry] of Object.entries(map)) {
18
+ if (!engineOnPlatform(entry.driver, platform))
19
+ delete map[id];
20
+ }
21
+ return map;
22
+ }
23
+ /** The runtime map leaves out engines switched off on this OS; a save must not
24
+ * erase them from config.json, so saved entries for them go back into `map`
25
+ * exactly as saved. Mutates and returns `map`. */
26
+ export function restoreEnginesOffPlatform(map, saved, platform) {
27
+ for (const [id, entry] of Object.entries(saved ?? {})) {
28
+ if (!Object.hasOwn(map, id) && !engineOnPlatform(entry.driver, platform))
29
+ map[id] = structuredClone(entry);
30
+ }
31
+ return map;
32
+ }
@@ -0,0 +1,24 @@
1
+ // Who an external (shared) room admits: people, each with their own
2
+ // orchestrator, and no other bot. A guest team's specialists stay home and
3
+ // work through the orchestrator its person brought (Erick's rule, 2026-10-27).
4
+ //
5
+ // Pure: no store, no server. The invite flow supplies the records.
6
+ import { isPrimaryBot } from "../shared/primary-bot.js";
7
+ /** How many people one external room holds, each bringing one orchestrator.
8
+ * One value so it can be raised later. */
9
+ export const EXTERNAL_ROOM_MAX_HUMANS = 4;
10
+ const key = (email) => email.trim().toLowerCase();
11
+ /** Why `email` cannot join, or undefined when it may. */
12
+ export function externalRoomHumanProblem(guests, email) {
13
+ if (guests.some(guest => key(guest.email) === key(email)))
14
+ return undefined;
15
+ if (guests.length >= EXTERNAL_ROOM_MAX_HUMANS)
16
+ return `An external room holds at most ${EXTERNAL_ROOM_MAX_HUMANS} people`;
17
+ }
18
+ /** Why `bot` cannot join, or undefined when it may: it must be the
19
+ * orchestrator one of the room's people brought. */
20
+ export function externalRoomBotProblem(guests, bot) {
21
+ const brought = guests.some(guest => guest.orchestratorId === bot.id);
22
+ if (!brought || !(bot.chiefOfStaff || isPrimaryBot(bot)))
23
+ return "An external room admits people and each person's own orchestrator only";
24
+ }