@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
@@ -12,6 +12,7 @@
12
12
  // own it is the harness that answers each tool permission (`god-mode`).
13
13
  // Questions never come through here: a bot's question always reaches a human.
14
14
  import { supportsApprovalMode } from "../shared/approval-mode.js";
15
+ import { isElicitationTool } from "./mcp-elicitation.js";
15
16
  /** A failed delivery is a runtime error, not another permission decision.
16
17
  * An expired ask must never become a fresh Allow/Deny card. */
17
18
  export async function deliverFullAccessApproval(adapter, threadId, requestId, turnId, isCurrent = () => false) {
@@ -64,7 +65,9 @@ export function autoVerdict(mode, tool, context) {
64
65
  // A question is for a person, whatever channel it arrived on — and
65
66
  // whatever the mode: even Full has no answer to give, only an approval
66
67
  // that would run the tool with none.
67
- if (ASKS_A_PERSON.has(tool.replace(/^mcp__[^_]+__/, "").toLowerCase())) {
68
+ // An MCP server asked the person to confirm a change (elicitation): no
69
+ // mode answers for them, Full included (server/mcp-elicitation.ts).
70
+ if (isElicitationTool(tool) || ASKS_A_PERSON.has(tool.replace(/^mcp__[^_]+__/, "").toLowerCase())) {
68
71
  return { approve: null, source: "no-grant" };
69
72
  }
70
73
  // Full's promise is literal: even a sandbox widening is approved. Entering
@@ -29,6 +29,15 @@ export const brandSchema = z
29
29
  .max(MAX_LOGO_CHARS, `logo must be under ${MAX_LOGO_CHARS} characters (inline a small SVG or link an https:// image)`)
30
30
  .refine((v) => v.startsWith("data:image/") || v.startsWith("https://"), "logo must be a data:image/... URI or an https:// URL")
31
31
  .optional(),
32
+ /** The company whose people use this deployment: the setup wizard greets
33
+ * people from it with its logo. Its own name and logo, so a client's
34
+ * deployment can carry the client's. */
35
+ companyName: z.string().trim().min(1).max(80, "companyName must be 80 characters or fewer").optional(),
36
+ companyLogo: z
37
+ .string()
38
+ .max(MAX_LOGO_CHARS, `companyLogo must be under ${MAX_LOGO_CHARS} characters`)
39
+ .refine((v) => v.startsWith("data:image/") || v.startsWith("https://") || v.startsWith("/"), "companyLogo must be a data:image/... URI, an https:// URL or a path on this server")
40
+ .optional(),
32
41
  /** Where "get help" points for this deployment. */
33
42
  supportUrl: z.string().url().startsWith("https://", "supportUrl must be an https:// URL").optional(),
34
43
  /** Tab and home-screen icon: an inline data:image/... URI (PNG or SVG, small) or an https:// URL. */
@@ -49,6 +58,8 @@ export const DEFAULT_BRAND = {
49
58
  accentLight: "#004F71",
50
59
  accentDark: "#00B398",
51
60
  logo: "/app-icon.svg",
61
+ companyName: "Chibitek",
62
+ companyLogo: "/chibitek-logo.png",
52
63
  };
53
64
  export function brandFile() {
54
65
  return process.env.OMB_BRAND_FILE || join(DATA_DIR, "brand.json");
@@ -19,6 +19,7 @@ import { isRemoteMcpServer, parseStoredMcpServer } from "./mcp-registry.js";
19
19
  import { hydrateMcpServers, mcpLaunchBlocker, mcpLaunchBlockerReason, mcpServersForDisk } from "./mcp-secrets.js";
20
20
  import { secureStdioLaunch } from "../electron/mcp-secrets.mjs";
21
21
  import { parseJson, schemaIssue } from "./schema.js";
22
+ import { dropEnginesOffPlatform, onDeviceFleet, restoreEnginesOffPlatform } from "./engine-platform-fleet.js";
22
23
  const optionalText = z.string().optional();
23
24
  const SSH_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
24
25
  const LEGACY_BROWSER_PROFILE_ID = /^[A-Za-z0-9_-]{1,40}$/;
@@ -904,8 +905,9 @@ export function loadConfig() {
904
905
  try {
905
906
  cfg = parseStoredConfig(parseJson(readFileSync(join(DATA_DIR, "config.json"), "utf8")));
906
907
  }
907
- catch {
908
- /* first run — env fallbacks below */
908
+ catch (error) {
909
+ if (error.code !== "ENOENT")
910
+ throw new Error("Saved Atomic settings could not be read; config.json has been left untouched.", { cause: error });
909
911
  }
910
912
  // MCP server secrets the desktop keeps in its encrypted store: config.json
911
913
  // holds placeholders, filled here for this process only (server/mcp-secrets.ts)
@@ -1118,12 +1120,11 @@ export function saveConfig(patch, options = {}) {
1118
1120
  const p = join(DATA_DIR, "config.json");
1119
1121
  let disk = {};
1120
1122
  try {
1121
- const parsed = jsonObjectSchema.safeParse(parseJson(readFileSync(p, "utf8")));
1122
- if (parsed.success)
1123
- disk = configWithoutRemovedIntegration(parsed.data).document;
1123
+ disk = configWithoutRemovedIntegration(jsonObjectSchema.parse(parseJson(readFileSync(p, "utf8")))).document;
1124
1124
  }
1125
- catch {
1126
- /* first write */
1125
+ catch (error) {
1126
+ if (error.code !== "ENOENT")
1127
+ throw new Error("Cannot save over unreadable Atomic settings; config.json has been left untouched.", { cause: error });
1127
1128
  }
1128
1129
  const checkedPatch = appConfigSchema.partial().extend({ threads: threadsPatchSchema.optional(), newBots: newBotsPatchSchema.optional() }).parse(patch);
1129
1130
  const before = configSaveListeners.size ? structuredClone(disk) : disk;
@@ -1317,8 +1318,8 @@ export function withInstanceCli(cfg, instanceId, cli) {
1317
1318
  return { ok: true, config: next };
1318
1319
  }
1319
1320
  /** Materialize defaults without freezing injected workspace settings or secrets. */
1320
- export function persistableInstanceConfigs(cfg) {
1321
- const map = instanceConfigs(cfg);
1321
+ export function persistableInstanceConfigs(cfg, platform = process.platform) {
1322
+ const map = instanceConfigs(cfg, undefined, undefined, platform);
1322
1323
  for (const [id, entry] of Object.entries(map)) {
1323
1324
  const environment = cfg.instances?.[id]?.environment;
1324
1325
  if (environment)
@@ -1337,7 +1338,7 @@ export function persistableInstanceConfigs(cfg) {
1337
1338
  else
1338
1339
  delete entry.config;
1339
1340
  }
1340
- return map;
1341
+ return restoreEnginesOffPlatform(map, cfg.instances, platform);
1341
1342
  }
1342
1343
  /** The credential env instanceConfigs() injects for one driver at runtime.
1343
1344
  * Each secret goes only to the driver that actually reads it: the API-key
@@ -1388,13 +1389,6 @@ const ATOMIC_FLEET = {
1388
1389
  "atomic-engine": { driver: "customAcp", displayName: "Atomic Engine", config: { cli: "atomic acp" } },
1389
1390
  ollamaCloud: { driver: "ollamaCloud", displayName: "Ollama Cloud" },
1390
1391
  };
1391
- /** Apple Intelligence runs only on Apple silicon Macs; elsewhere the engine
1392
- * is left out of the fleet rather than listed as unavailable. */
1393
- export function onDeviceFleet(platform = process.platform, arch = process.arch) {
1394
- return platform === "darwin" && arch === "arm64"
1395
- ? { appleIntelligence: { driver: "appleIntelligence", displayName: "Apple Intelligence (on this Mac)" } }
1396
- : {};
1397
- }
1398
1392
  function plainObject(value) {
1399
1393
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1400
1394
  }
@@ -1427,7 +1421,7 @@ function separateClaudeLogin(instanceId, entry) {
1427
1421
  const configDir = plainObject(entry.config)?.configDir;
1428
1422
  return typeof configDir === "string" && configDir.trim() !== "";
1429
1423
  }
1430
- export function instanceConfigs(cfg, onDevice = onDeviceFleet(), accountKeys = instanceKeys()) {
1424
+ export function instanceConfigs(cfg, onDevice = onDeviceFleet(), accountKeys = instanceKeys(), platform = process.platform) {
1431
1425
  // The default `grok` instance rides the `grokAgent` driver, not the API-key
1432
1426
  // one: like claude and codex it needs no credential from us, just the CLI
1433
1427
  // installed and logged in (it shows up unavailable otherwise). The API-key
@@ -1499,6 +1493,7 @@ export function instanceConfigs(cfg, onDevice = onDeviceFleet(), accountKeys = i
1499
1493
  if (Object.hasOwn(map, id))
1500
1494
  map[id] = withFleetDefaults(map[id], fallback);
1501
1495
  }
1496
+ dropEnginesOffPlatform(map, platform);
1502
1497
  for (const [id, sourceEntry] of Object.entries(map)) {
1503
1498
  // instanceConfigs() builds a transient runtime map. Never mutate the
1504
1499
  // caller's persisted entries while injecting workspace defaults: doing so
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import { MAX_MCP_SERVERS, isRemoteMcpServer, parseStoredMcpServer } from "./mcp-registry.js";
3
+ const schema = z.object({
4
+ name: z.string().regex(/^[a-z][a-z0-9_-]{0,31}$/),
5
+ url: z.string().max(2048),
6
+ header: z.string().regex(/^[A-Za-z][A-Za-z0-9-]{0,127}$/),
7
+ prefix: z.literal("Bearer ").optional(),
8
+ }).strict();
9
+ const forbiddenHeaders = /^(host|content-.*|accept|connection|transfer-encoding|proxy-.*|mcp-.*|origin|referer|cookie|set-cookie)$/i;
10
+ const fail = (message, status = 400) => { throw Object.assign(new Error(message), { status }); };
11
+ /** Non-secret, reviewed destination only. No arbitrary config paths or URLs carrying secrets. */
12
+ export function connectionCredential(raw) {
13
+ const parsed = schema.safeParse(raw);
14
+ if (!parsed.success)
15
+ return fail("Provide a connection name, HTTPS URL and credential header, without the secret.");
16
+ const connection = parsed.data;
17
+ let url;
18
+ try {
19
+ url = new URL(connection.url);
20
+ }
21
+ catch {
22
+ return fail("Use the service's full HTTPS MCP address.");
23
+ }
24
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
25
+ return fail("Use an HTTPS MCP address without credentials, query parameters or fragments.");
26
+ }
27
+ if (forbiddenHeaders.test(connection.header))
28
+ return fail("Choose the service's credential header, such as x-api-key or Authorization.");
29
+ connection.url = url.href;
30
+ // Reuse the catalog's reserved-name and server validation.
31
+ const server = parseStoredMcpServer(connection.name, { type: "http", url: connection.url });
32
+ if (!server.ok)
33
+ return fail("This connection name or address is not supported.");
34
+ return connection;
35
+ }
36
+ /** Keep other headers and settings. Never redirect an existing connection. */
37
+ export function connectionCredentialServer(connection, current, value) {
38
+ const raw = current[connection.name];
39
+ const parsed = parseStoredMcpServer(connection.name, raw ?? {
40
+ type: "http", url: connection.url, enabled: true, autoAllowReadOnly: false,
41
+ });
42
+ if (!parsed.ok || !isRemoteMcpServer(parsed.server) || parsed.server.url !== connection.url) {
43
+ return fail("This connection changed or uses another address. Request a new secure card.", 409);
44
+ }
45
+ if (raw === undefined && Object.keys(current).length >= MAX_MCP_SERVERS)
46
+ return fail("The MCP server limit has been reached.");
47
+ const server = parsed.server;
48
+ if (value === undefined)
49
+ return server;
50
+ if (!value || Buffer.byteLength(value) > 4096 || (/[\r\n]/.test(value) || value.includes(String.fromCharCode(0))))
51
+ return fail("Enter a credential of 1 to 4096 bytes on one line.");
52
+ // HTTP field names are case-insensitive: replace just the chosen field.
53
+ const headers = Object.fromEntries(Object.entries(server.headers).filter(([key]) => key.toLowerCase() !== connection.header.toLowerCase()));
54
+ headers[connection.header] = (connection.prefix ?? "") + value;
55
+ return { ...server, headers };
56
+ }
57
+ export function sameConnectionCredential(a, b) {
58
+ return a?.name === b?.name && a?.url === b?.url && a?.header === b?.header && a?.prefix === b?.prefix;
59
+ }
@@ -323,28 +323,10 @@ export function localVmRecreatableOnDemand(status) {
323
323
  && status.container === "missing"
324
324
  && status.create_supported;
325
325
  }
326
- /** Whether a turn may replace this stopped Local VM itself.
327
- *
328
- * A stopped desktop is never resumed (see `statusProblem`), but a stopped
329
- * container that Atomic made, on the current image, with its safety limits
330
- * and its durable workspace mount, can simply be removed and run again: the
331
- * person's files live in the host workspace folder, so only the desktop
332
- * session resets. Docker or Podman stops containers whenever the runtime or
333
- * the Mac restarts, so without this every such restart sent the person to
334
- * App Settings to delete and recreate the Local VM by hand. Anything foreign,
335
- * stale or unsafe still stays the person's call.
336
- */
337
- export function localVmReplaceableWhenStopped(status) {
338
- return Boolean(status.runtime
339
- && status.daemonUp
340
- && status.image
341
- && status.container === "stopped"
342
- && status.create_supported
343
- && status.managed
344
- && status.imageMatches
345
- && status.network !== "unsafe"
346
- && status.security !== "unsafe"
347
- && status.persistence === "durable");
326
+ /** A stopped VM may contain installed applications and files outside the mount.
327
+ * Replacement requires an explicit removal; startup never discards its disk. */
328
+ export function localVmReplaceableWhenStopped(_status) {
329
+ return false;
348
330
  }
349
331
  /** Whether Auto may attach this Local VM without a person choosing it: the
350
332
  * desktop is ready, or its image is prepared and the container can simply be
@@ -375,11 +357,8 @@ function statusProblem(status) {
375
357
  return "The existing Local VM is missing safety limits; recreate it";
376
358
  if (status.persistence === "unsafe")
377
359
  return "The existing Local VM is missing its durable workspace; recreate it";
378
- if (status.container === "stopped" && localVmReplaceableWhenStopped(status)) {
379
- return "The Local VM stopped; a fresh one starts the next time a bot uses it, and your files are kept";
380
- }
381
360
  if (status.container === "stopped")
382
- return "This desktop image cannot safely resume; recreate the Local VM";
361
+ return "The Local VM is stopped and its disk is kept. This image cannot safely resume. Export needed files before explicitly removing and recreating it.";
383
362
  if (status.desktop_error)
384
363
  return `The Local VM desktop failed to start: ${status.desktop_error}`;
385
364
  if (!status.desktopReady)
@@ -0,0 +1,52 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { CREDENTIAL_TARGETS, credentialIsConfigured, isCredentialTargetId, isReusableCredentialRequest } from "../shared/credential-request.js";
3
+ import { connectionCredential, connectionCredentialServer, sameConnectionCredential } from "./connection-credential.js";
4
+ const answer = (status, body) => ({ status, body });
5
+ const handoff = (label) => `Securely provide the ${label} from Atomic on your phone or computer. It is never added to chat.`;
6
+ export function requestCredentialCard(body, from, fromThreadId, room, cfg, store, storage, refusalFor) {
7
+ if (!isCredentialTargetId(body.credentialId)) {
8
+ return answer(400, { error: "unsupported credential id" });
9
+ }
10
+ const credentialId = body.credentialId;
11
+ const target = CREDENTIAL_TARGETS[credentialId];
12
+ const connection = credentialId === "connectionSecret" ? connectionCredential(body.connection) : undefined;
13
+ if (connection) {
14
+ if (storage() !== "os")
15
+ return answer(503, { error: "Secure connection entry requires this computer's encrypted credential store." });
16
+ if (from.mcpServers && !from.mcpServers.includes(connection.name))
17
+ return answer(403, { error: "Enable this connection for this bot before requesting its credential." });
18
+ const server = connectionCredentialServer(connection, cfg.mcpServers ?? {});
19
+ const refusal = refusalFor(connection.name, server);
20
+ if (refusal)
21
+ return answer(403, { error: refusal });
22
+ }
23
+ if (credentialIsConfigured(cfg, credentialId)) {
24
+ return answer(200, { alreadyConfigured: true, label: target.label });
25
+ }
26
+ const existing = store.activePath(fromThreadId).find((message) => isReusableCredentialRequest(message, credentialId, from.id, room) && sameConnectionCredential(message.secret?.connection, connection));
27
+ if (existing) {
28
+ if (!existing.text?.trim()) {
29
+ store.patchMessage(fromThreadId, existing.id, {
30
+ text: handoff(target.label),
31
+ });
32
+ }
33
+ return answer(200, { messageId: existing.id, label: target.label });
34
+ }
35
+ const reason = typeof body.reason === "string" ? body.reason.trim().slice(0, 240) : "";
36
+ const message = store.appendMessage(fromThreadId, {
37
+ role: "bot",
38
+ kind: "secret",
39
+ text: handoff(target.label),
40
+ ...(room ? { from: { botId: from.id, name: from.name, color: from.color } } : {}),
41
+ secret: {
42
+ target: credentialId,
43
+ ...(connection ? { connection } : {}),
44
+ label: connection ? `${connection.name} credential` : target.label,
45
+ description: `${reason ? `${target.description} ${reason}` : target.description} ${connection ? `Save for ${connection.url}, field ${connection.header}. ` : ""}${from.name} can use it but never read it back.`,
46
+ placeholder: target.placeholder,
47
+ helpUrl: target.helpUrl,
48
+ requestKey: randomUUID(),
49
+ },
50
+ });
51
+ return answer(201, { messageId: message.id, label: target.label });
52
+ }
@@ -86,13 +86,16 @@ export function checkCleanup(input, output) {
86
86
  }
87
87
  return { ok: true, text: cleaned };
88
88
  }
89
- async function ask(provider, prompt, timeoutMs) {
89
+ export async function ask(provider, prompt, timeoutMs) {
90
90
  if (!provider.generateText)
91
91
  return null;
92
92
  const expiry = new AbortController();
93
- const timer = setTimeout(() => expiry.abort(), timeoutMs);
93
+ let timer;
94
+ const timeout = new Promise(resolve => {
95
+ timer = setTimeout(() => { expiry.abort(); resolve(null); }, timeoutMs);
96
+ });
94
97
  try {
95
- return await provider.generateText(prompt, { signal: expiry.signal });
98
+ return await Promise.race([provider.generateText(prompt, { signal: expiry.signal }), timeout]);
96
99
  }
97
100
  catch {
98
101
  return null;
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // Nothing here reads the environment or holds state of its own. The caller
8
8
  // passes a ToolCallContext, and the per-turn counters live on it.
9
- import { CREDENTIAL_TARGETS, isCredentialTargetId } from "../../shared/credential-request.js";
9
+ import { requestCredential } from "./agents-credential.js";
10
10
  import { normalizeCronSchedule } from "../../shared/routine-schedule.js";
11
11
  import { peerRecallSpeaker } from "../peer-provenance.js";
12
12
  import { peerName } from "../peer-roster.js";
@@ -835,28 +835,8 @@ export async function callTool(name, args, context) {
835
835
  const message = typeof r.message === "string" ? r.message : `Updated room ${roomId}.`;
836
836
  return { text: message };
837
837
  }
838
- if (name === "request_credential") {
839
- const credentialId = args.credential_id;
840
- if (!isCredentialTargetId(credentialId)) {
841
- return { text: "request_credential needs a supported credential_id.", isError: true };
842
- }
843
- const reason = typeof args.reason === "string" ? args.reason.trim().slice(0, 240) : "";
844
- const r = await api("/api/internal/request-credential", {
845
- method: "POST",
846
- body: JSON.stringify({
847
- fromBotId: BOT_ID,
848
- fromThreadId: THREAD_ID,
849
- credentialId,
850
- ...(reason ? { reason } : {}),
851
- }),
852
- });
853
- if (r.alreadyConfigured) {
854
- return { text: `${r.label ?? CREDENTIAL_TARGETS[credentialId].label} is already configured. Continue the task.` };
855
- }
856
- return {
857
- text: `A secure ${r.label ?? CREDENTIAL_TARGETS[credentialId].label} request is ready. The desktop app and a freshly QR-paired mobile app show its secure entry card; older mobile pairings explain how to pair again or finish on the computer. End this turn; Atomic will resume the task after the user saves or declines. Never ask them to paste the key into chat.`,
858
- };
859
- }
838
+ if (name === "request_credential")
839
+ return requestCredential(args, BOT_ID, THREAD_ID, api);
860
840
  if (name === "list_routines") {
861
841
  const query = new URLSearchParams({ fromBotId: BOT_ID, fromThreadId: THREAD_ID });
862
842
  const r = await api(`/api/internal/routines?${query.toString()}`);
@@ -10,7 +10,7 @@
10
10
  // turn. It is pinned byte for byte, and its size budgeted, by
11
11
  // agents-catalog-wire.test.ts: change a description or a schema on purpose,
12
12
  // then update the goldens there.
13
- import { CREDENTIAL_TARGETS } from "../../shared/credential-request.js";
13
+ import { credentialTool } from "./agents-credential.js";
14
14
  import { PRIVATE_BOT_HIDDEN_TOOLS, agentToolAnnotations } from "../agent-tool-policy.js";
15
15
  /** The profile a spawned proxy was given. Everything is off unless the
16
16
  * harness says "1". */
@@ -450,25 +450,7 @@ const toolDefinitions = (externalRuntime) => [
450
450
  required: ["room_id", "action"],
451
451
  },
452
452
  },
453
- {
454
- name: "request_credential",
455
- description: "Ask the user for a supported API key through Atomic's secure credential flow. The desktop app and a freshly QR-paired mobile app show a secure entry card; older mobile pairings show how to pair again or finish on the computer. Never claim a secure field opened unless this request succeeds, and never ask the user to paste a secret into chat. The secret is saved by the desktop app and is never returned to you. After calling this tool, end the turn; Atomic resumes the task after the user saves or declines.",
456
- inputSchema: {
457
- type: "object",
458
- properties: {
459
- credential_id: {
460
- type: "string",
461
- enum: Object.keys(CREDENTIAL_TARGETS),
462
- description: "The credential the current task requires.",
463
- },
464
- reason: {
465
- type: "string",
466
- description: "Optional short, non-sensitive explanation of why the task needs it.",
467
- },
468
- },
469
- required: ["credential_id"],
470
- },
471
- },
453
+ credentialTool,
472
454
  {
473
455
  name: "marketplace_search",
474
456
  description: "Find an app or service the person wants connected in Atomic's Marketplace: official servers Atomic connects, and official apps through their Claude account. Then call open_marketplace. Never offer to build a connection or a community server.",
@@ -0,0 +1,57 @@
1
+ import { CREDENTIAL_TARGETS, isCredentialTargetId } from "../../shared/credential-request.js";
2
+ export async function requestCredential(args, BOT_ID, THREAD_ID, api) {
3
+ const credentialId = args.credential_id;
4
+ if (!isCredentialTargetId(credentialId)) {
5
+ return { text: "request_credential needs a supported credential_id.", isError: true };
6
+ }
7
+ const reason = typeof args.reason === "string" ? args.reason.trim().slice(0, 240) : "";
8
+ const r = await api("/api/internal/request-credential", {
9
+ method: "POST",
10
+ body: JSON.stringify({
11
+ fromBotId: BOT_ID,
12
+ fromThreadId: THREAD_ID,
13
+ credentialId,
14
+ ...(credentialId === "connectionSecret" ? { connection: args.connection } : {}),
15
+ ...(reason ? { reason } : {}),
16
+ }),
17
+ });
18
+ if (r.error)
19
+ return { text: String(r.error), isError: true };
20
+ if (r.alreadyConfigured) {
21
+ return { text: `${r.label ?? CREDENTIAL_TARGETS[credentialId].label} is already configured. Continue the task.` };
22
+ }
23
+ return {
24
+ text: `A secure ${r.label ?? CREDENTIAL_TARGETS[credentialId].label} request is ready. The desktop app and a freshly QR-paired mobile app show its secure entry card; older mobile pairings explain how to pair again or finish on the computer. End this turn; Atomic will resume the task after the user saves or declines. Never ask them to paste the key into chat.`,
25
+ };
26
+ }
27
+ export const credentialTool = {
28
+ name: "request_credential",
29
+ description: "Ask for a password, API key or token through Atomic's secure credential flow. Use connectionSecret with connection details for any HTTPS MCP service, or a built-in credential_id for a provider. The desktop app and a freshly QR-paired mobile app show a secure entry card; older mobile pairings show how to pair again or finish on the computer. Never claim a secure field opened unless this request succeeds, and never ask the user to paste a secret into chat. The secret is saved by the desktop app and is never returned to you. After calling this tool, end the turn; Atomic resumes the task after the user saves or declines.",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ credential_id: {
34
+ type: "string",
35
+ enum: Object.keys(CREDENTIAL_TARGETS),
36
+ description: "The credential the current task requires.",
37
+ },
38
+ connection: {
39
+ type: "object",
40
+ description: "Required for connectionSecret. The person reviews this HTTPS MCP destination before entering a password, API key or token. Never include the secret here.",
41
+ properties: {
42
+ name: { type: "string", description: "MCP server name." },
43
+ url: { type: "string", description: "Official HTTPS MCP URL, without credentials or query parameters." },
44
+ header: { type: "string", description: "Credential header name, for example x-api-key or Authorization." },
45
+ prefix: { type: "string", enum: ["Bearer "], description: "Only when the service requires a Bearer token." },
46
+ },
47
+ required: ["name", "url", "header"],
48
+ additionalProperties: false,
49
+ },
50
+ reason: {
51
+ type: "string",
52
+ description: "Optional short, non-sensitive explanation of why the task needs it.",
53
+ },
54
+ },
55
+ required: ["credential_id"],
56
+ },
57
+ };
@@ -1,3 +1,4 @@
1
+ import { engineOnPlatform } from "../../shared/engine-platforms.js";
1
2
  import { AntigravityDriver } from "./antigravity.js";
2
3
  import { BoxAgentDriver } from "./boxagent.js";
3
4
  import { ClaudeDriver } from "./claude.js";
@@ -20,7 +21,8 @@ import { MistralDriver } from "./mistral.js";
20
21
  import { MinimaxDriver } from "./minimax.js";
21
22
  import { OllamaCloudDriver } from "./ollama-cloud.js";
22
23
  import { AppleIntelligenceDriver } from "./apple-intelligence.js";
23
- export const BUILT_IN_DRIVERS = [
24
+ /** Every built-in driver, on every OS. Most code wants BUILT_IN_DRIVERS. */
25
+ export const ALL_BUILT_IN_DRIVERS = [
24
26
  GrokDriver,
25
27
  GrokAgentDriver,
26
28
  GeminiAgentDriver,
@@ -44,3 +46,9 @@ export const BUILT_IN_DRIVERS = [
44
46
  OllamaCloudDriver,
45
47
  AppleIntelligenceDriver,
46
48
  ];
49
+ /** The drivers registered on `platform`: all of them minus any engine switched
50
+ * off there (shared/engine-platforms.ts, e.g. Qwen on Windows for now). */
51
+ export function builtInDriversFor(platform) {
52
+ return ALL_BUILT_IN_DRIVERS.filter((driver) => engineOnPlatform(driver.driverKind, platform));
53
+ }
54
+ export const BUILT_IN_DRIVERS = builtInDriversFor(process.platform);
@@ -0,0 +1,76 @@
1
+ // MCP elicitation for Claude bots. Claude Code (2.1.270, stream-json input)
2
+ // forwards a configured server's `elicitation/create` to its host as
3
+ //
4
+ // {"type":"control_request","request_id":"…","request":{"subtype":"elicitation",
5
+ // "mcp_server_name":"addigy","message":"Delete policy X?","requested_schema":{…}}}
6
+ //
7
+ // and waits for a control_response on stdin. Addigy's hosted server confirms
8
+ // every write this way. A plain confirmation becomes a permission card that
9
+ // always needs the person (its tool is an elicitation tool: no approval
10
+ // mode, remembered grant or reviewer answers it). Anything else is answered
11
+ // `cancel` and never filled in (server/mcp-elicitation.ts).
12
+ import { newId } from "../contracts.js";
13
+ import { elicitationAnswer, elicitationConfirm, elicitationSummary, elicitationTool } from "../mcp-elicitation.js";
14
+ import { claudeControlResponse } from "./claude-stdin.js";
15
+ /** How long a confirmation card waits before it goes back as `cancel`. */
16
+ export const ELICITATION_TIMEOUT_MS = 15 * 60_000;
17
+ export function createClaudeElicitations(deps) {
18
+ const open = new Map();
19
+ return {
20
+ /** A control_request frame from the CLI's stdout. True when it was an
21
+ * elicitation (answered now or once the person decides). */
22
+ handle(frame, ctx) {
23
+ const o = frame;
24
+ if (!o || o.request?.subtype !== "elicitation")
25
+ return false;
26
+ if (typeof o.request_id !== "string")
27
+ return true;
28
+ const controlId = o.request_id;
29
+ const reply = (result) => ctx.write(claudeControlResponse(controlId, result));
30
+ const confirm = elicitationConfirm(o.request);
31
+ if (!confirm || ctx.closing) {
32
+ reply({ action: "cancel" });
33
+ return true;
34
+ }
35
+ const server = typeof o.request.mcp_server_name === "string" ? o.request.mcp_server_name : undefined;
36
+ const requestId = newId();
37
+ const finish = (allowed, source) => {
38
+ if (!open.delete(requestId))
39
+ return;
40
+ clearTimeout(timer);
41
+ reply(allowed === null ? { action: "cancel" } : elicitationAnswer(confirm, allowed));
42
+ deps.emit({ ...deps.base(ctx.threadId, ctx.turnId), type: "request.resolved", requestId, behavior: allowed ? "allow" : "deny", source });
43
+ };
44
+ const timer = setTimeout(() => finish(null, "timeout"), deps.timeoutMs ?? ELICITATION_TIMEOUT_MS);
45
+ timer.unref?.();
46
+ open.set(requestId, { threadId: ctx.threadId, finish });
47
+ deps.emit({
48
+ ...deps.base(ctx.threadId, ctx.turnId),
49
+ type: "request.opened",
50
+ requestId,
51
+ requestType: "permission",
52
+ tool: elicitationTool(server),
53
+ summary: elicitationSummary(server, confirm),
54
+ requiresExplicitApproval: true,
55
+ });
56
+ return true;
57
+ },
58
+ /** The person's answer to a card, or null when the card is not one of these. */
59
+ respond(threadId, requestId, decision) {
60
+ const entry = open.get(requestId);
61
+ if (!entry)
62
+ return null;
63
+ if (entry.threadId !== threadId)
64
+ return "unavailable";
65
+ const allowed = decision.behavior === "allow";
66
+ entry.finish(allowed, "user");
67
+ return allowed ? "allowed-once" : "rejected";
68
+ },
69
+ /** The session ended: every open question goes back as "no answer". */
70
+ endThread(threadId) {
71
+ for (const entry of Array.from(open.values()))
72
+ if (entry.threadId === threadId)
73
+ entry.finish(null, "system");
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,47 @@
1
+ // The stream-json frames Atomic writes to the Claude CLI's stdin: a user
2
+ // turn, its diagnostic copy for native traces, and the answer to a control
3
+ // request the CLI forwarded (an MCP server's elicitation).
4
+ import { readFileSync } from "node:fs";
5
+ /** Claude's stream-json input accepts the same image source blocks as the
6
+ * Anthropic Messages API. Keep the old string form for text-only turns so a
7
+ * CLI update cannot disturb the overwhelmingly common path. */
8
+ export function claudeUserMessage(text, images) {
9
+ if (!images?.length)
10
+ return { type: "user", message: { role: "user", content: text } };
11
+ const content = images.map((image) => ({
12
+ type: "image",
13
+ source: {
14
+ type: "base64",
15
+ media_type: image.mime,
16
+ data: readFileSync(image.path).toString("base64"),
17
+ },
18
+ }));
19
+ if (text)
20
+ content.push({ type: "text", text });
21
+ return { type: "user", message: { role: "user", content } };
22
+ }
23
+ /** Native traces are routinely attached to bug reports. Preserve the image
24
+ * block's shape and size for debugging, but never persist its base64 bytes. */
25
+ export function diagnosticClaudeUserMessage(message) {
26
+ if (!Array.isArray(message.message.content))
27
+ return message;
28
+ return {
29
+ ...message,
30
+ message: {
31
+ ...message.message,
32
+ content: message.message.content.map((block) => block.type === "image"
33
+ ? {
34
+ ...block,
35
+ source: {
36
+ ...block.source,
37
+ data: `[image data: ${block.source.data.length} base64 chars]`,
38
+ },
39
+ }
40
+ : block),
41
+ },
42
+ };
43
+ }
44
+ /** The reply to one control_request the CLI sent on stdout. */
45
+ export function claudeControlResponse(requestId, response) {
46
+ return { type: "control_response", response: { subtype: "success", request_id: requestId, response } };
47
+ }