@chibitek/chibi 0.1.169 → 0.1.171

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 (99) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-aQkRN0ee.js → abnfDiagram-O67JEVCF-DrPBKmVh.js} +1 -1
  2. package/dist/assets/{arc-BT7qPVAa.js → arc-BEIChVSB.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-oMlARqKW.js → architectureDiagram-NJMV4G6O-Da60nTzs.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-CKUrb4js.js → blockDiagram-BEXU5L5S-_NelwIQj.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-BkL5cRIA.js → c4Diagram-YGBWAQC7-DUdkQ3dy.js} +1 -1
  6. package/dist/assets/channel-DEJSSlVQ.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-BMitkm2s.js → chunk-2Q5K7J3B-BQgYAptM.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-DOhVN2ed.js → chunk-3FUC2YCW-XXOXwcoW.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-BFkFo_ng.js → chunk-5VM5RSS4-CUyMXCSt.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-QbxYUf3Q.js → chunk-GTNCS2PH-BwctbINm.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-BZChfTYw.js → chunk-GWA4HPMP-DQ0bZK59.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-BXpmYglA.js → chunk-JWPE2WC7-BADCk9AK.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-CIZTrz1i.js → chunk-NETBCI7D-CoJxabSR.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-FhqQ0sO0.js → chunk-WEXAMYUT-fupByVJn.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-5ThtmFbP.js → chunk-XXDRQBXY-BavbGHGi.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-BzObC5Qw.js → classDiagram-v2-NBCMYWYE-B5wfUNKJ.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-BASV9Okv.js → cose-bilkent-JH36ORCC-DlYFmKDe.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-B0zQYMFh.js → cynefin-OW5HDTMX-Y0U-21MH.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-DMEO1cHA.js → cynefinDiagram-VND7K2PF-D1Fh1sU0.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-BG04TSi_.js → dagre-6A5THRUB-BNU-Trog.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-CfpVNMhg.js → diagram-22UHCM2B-Bjie4nDQ.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-DG9L4I1L.js → diagram-3UASUU5V-Qnbrq1T5.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-Db7Bf63S.js → diagram-ATOU4E4O-CNJasPCN.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-Cdzv1tJn.js → diagram-CDSNMT55-BAzGZBKX.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-BBMeXGKE.js → diagram-MLGK6HIB-I8ACRaw-.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-BAQ22zk3.js → diagram-MPIPVDR6-nLlrAEPz.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-BDZgSFSF.js → ebnfDiagram-ZINNZB2B-DF9BG3pH.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-Bx4-nN8N.js → elk-276RUBZZ-C1AJYKMV.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-D2Pn-cqm.js → erDiagram-OPXOYQCR-MNNcRrPy.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-hsbvL3Ow.js → flowDiagram-KWPJA3E3-C7mfBfgt.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-DBUgYe4p.js → ganttDiagram-FUAMR5RP-a9zNyDbb.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-ChGeSINg.js → gitGraphDiagram-X574FWY7-OL8NhUsO.js} +1 -1
  33. package/dist/assets/{index-ByXr4FRl.js → index-DojGBgbN.js} +167 -164
  34. package/dist/assets/{index-Ch93Ygtq.js → index-xypOVpVT.js} +1 -1
  35. package/dist/assets/{infoDiagram-VRGFBTTK-DY2EeC-A.js → infoDiagram-VRGFBTTK-CF-3pzUZ.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BznYuY04.js → ishikawaDiagram-OU5B5YK6-B2HqD-sv.js} +1 -1
  37. package/dist/assets/{journeyDiagram-ZHPQQLJL-CX74ZLbW.js → journeyDiagram-ZHPQQLJL-84gzOTb9.js} +1 -1
  38. package/dist/assets/{kanban-definition-PNTS6WVX-0DPavB-t.js → kanban-definition-PNTS6WVX-D7Ee23QC.js} +1 -1
  39. package/dist/assets/{linear-BHeqbvM8.js → linear-CdAXsmfc.js} +1 -1
  40. package/dist/assets/{mermaid.core-Dm-Ny6a0.js → mermaid.core-CE_M1v1e.js} +5 -5
  41. package/dist/assets/{mindmap-definition-NLK3R4M7-BPrMElav.js → mindmap-definition-NLK3R4M7-KSTIET9y.js} +1 -1
  42. package/dist/assets/{pegDiagram-GJSIUBJH-CaD7wyyX.js → pegDiagram-GJSIUBJH-C7ogvddb.js} +1 -1
  43. package/dist/assets/{pieDiagram-5QR66LMP-lbSp8R4K.js → pieDiagram-5QR66LMP-Bp1CgCrM.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-O4NWA36T-Bs3Sn2of.js → quadrantDiagram-O4NWA36T-Bw7aN9Lh.js} +1 -1
  45. package/dist/assets/{railroadDiagram-XR7U4H2S-C88ykXCV.js → railroadDiagram-XR7U4H2S-Dij4U4NY.js} +1 -1
  46. package/dist/assets/{requirementDiagram-PLB6GJNP-mgIQnORW.js → requirementDiagram-PLB6GJNP-DOxTQvCl.js} +1 -1
  47. package/dist/assets/{sankeyDiagram-IPEJSGJF-K5esEw-m.js → sankeyDiagram-IPEJSGJF-DPCUmNbV.js} +1 -1
  48. package/dist/assets/{sequenceDiagram-PO4LG4MO-D5v8PJqQ.js → sequenceDiagram-PO4LG4MO-DPjC8zGe.js} +1 -1
  49. package/dist/assets/{sizeCapture-INFHLROL-BHBBDTEI.js → sizeCapture-INFHLROL-WlmsNlh-.js} +1 -1
  50. package/dist/assets/{stateDiagram-v2-GCMORJYK-CeRdNmKQ.js → stateDiagram-v2-GCMORJYK-BwMPiapI.js} +1 -1
  51. package/dist/assets/{swimlanes-2SLR337P-D8CbMCI8.js → swimlanes-2SLR337P-Cyja7ZPb.js} +1 -1
  52. package/dist/assets/swimlanesDiagram-TC7HE7FX-Dh25zxzq.js +8 -0
  53. package/dist/assets/{timeline-definition-EJHVYXUP-CMG02UV1.js → timeline-definition-EJHVYXUP-BehkSauz.js} +1 -1
  54. package/dist/assets/{usecaseDiagram-POWQR4AR-Bm8h6ijK.js → usecaseDiagram-POWQR4AR-koUjcmpW.js} +1 -1
  55. package/dist/assets/{vennDiagram-UO4OBE2U-Dk9tFjlM.js → vennDiagram-UO4OBE2U-Ch8TXIpy.js} +1 -1
  56. package/dist/assets/{wardleyDiagram-VNRHLVJA-61vrb2Ft.js → wardleyDiagram-VNRHLVJA-Dm5kC4c2.js} +1 -1
  57. package/dist/assets/{xychartDiagram-PMCCYNJV-C7uO44ng.js → xychartDiagram-PMCCYNJV-YA-1zyQz.js} +1 -1
  58. package/dist/handbook/atomic-handbook.html +1 -1
  59. package/dist/index.html +1 -1
  60. package/dist-server/companion/src/routes.js +28 -1
  61. package/dist-server/container-mcp.js +1 -0
  62. package/dist-server/hooks/plan-hook.js +22 -0
  63. package/dist-server/index.js +38538 -37905
  64. package/dist-server/local-computer.js +1 -0
  65. package/dist-server/openmausbot.js +389 -303
  66. package/dist-server/pair-cli.js +389 -303
  67. package/dist-server/proxy-paths.js +1 -0
  68. package/dist-server/server/atomic-engine-release.js +13 -13
  69. package/dist-server/server/bot-work-modes.js +212 -0
  70. package/dist-server/server/config.js +6 -5
  71. package/dist-server/server/drivers/acp/core.js +6 -4
  72. package/dist-server/server/drivers/acp/custom.js +51 -37
  73. package/dist-server/server/drivers/builtIn.js +2 -1
  74. package/dist-server/server/drivers/chat-mcp-tools.js +1 -0
  75. package/dist-server/server/drivers/claude.js +28 -25
  76. package/dist-server/server/drivers/codex-approval-presets.js +34 -0
  77. package/dist-server/server/drivers/codex.js +11 -35
  78. package/dist-server/server/drivers/openai-chat.js +4 -3
  79. package/dist-server/server/hooks/plan-hook.js +17 -0
  80. package/dist-server/server/index.js +92 -175
  81. package/dist-server/server/mcp-interactive-probe.js +60 -0
  82. package/dist-server/server/mcp-remote-bridge.js +13 -6
  83. package/dist-server/server/message-file.js +28 -11
  84. package/dist-server/server/plan-policy.js +14 -0
  85. package/dist-server/server/proxy-paths.js +1 -0
  86. package/dist-server/server/remote-desktop.js +142 -0
  87. package/dist-server/server/routes/message-file.js +115 -0
  88. package/dist-server/server/shared-deliverables.js +109 -0
  89. package/dist-server/server/skill-learn.js +4 -0
  90. package/dist-server/server/store.js +4 -4
  91. package/dist-server/server/subscription-bots.js +3 -2
  92. package/dist-server/shared/approval-mode.js +3 -1
  93. package/dist-server/shared/engine-choices.js +1 -1
  94. package/dist-server/shared/provider-accounts.js +5 -5
  95. package/dist-server/shared/skill-command.js +16 -0
  96. package/dist-server/vps-container-mcp.js +1 -0
  97. package/package.json +1 -1
  98. package/dist/assets/channel-BZDQCBTd.js +0 -1
  99. package/dist/assets/swimlanesDiagram-TC7HE7FX-CQHcTGnI.js +0 -8
@@ -343,16 +343,25 @@ function mimeFor(path) {
343
343
  default: return "application/octet-stream";
344
344
  }
345
345
  }
346
- /**
347
- * Open a message-linked file under one of the supplied roots. The returned
348
- * handle, not the pathname, is what the caller streams. We canonicalise both
349
- * before and after opening and compare the handle with the post-open path;
350
- * this rejects symlink escapes and directory swaps instead of checking a
351
- * path and then opening a potentially different file.
352
- */
353
- export async function openMessageFile(href, roots) {
346
+ export async function openMessageFile(href, roots, access = {}) {
354
347
  const requested = referencedPath(href);
355
- const canonicalRoots = (await Promise.all(roots.map(async (root) => {
348
+ const canonicalize = async (paths) => Promise.all(paths.map(async (path) => {
349
+ try {
350
+ return await realpath(path);
351
+ }
352
+ catch {
353
+ return resolve(path);
354
+ }
355
+ }));
356
+ const blocked = [...(access.blockedRoots ?? []), ...await canonicalize(access.blockedRoots ?? [])];
357
+ const sharedBlocked = [...(access.sharedBlockedRoots ?? []), ...await canonicalize(access.sharedBlockedRoots ?? [])];
358
+ const isBlocked = (paths, candidate) => paths.some((root) => containedBy(resolve(root), candidate));
359
+ // Relative links keep their original conversation-relative meaning.
360
+ const shared = isAbsolute(requested) ? access.sharedRoots ?? [] : [];
361
+ const boundaries = await canonicalize(access.sharedRootBoundaries ?? []);
362
+ const canonicalShared = (await canonicalize(shared)).filter(root => !boundaries.some(boundary => containedBy(root, boundary)));
363
+ const canonicalOrdinary = await canonicalize(roots);
364
+ const canonicalRoots = (await Promise.all([...roots, ...canonicalShared].map(async (root) => {
356
365
  try {
357
366
  const canonical = await realpath(root);
358
367
  return (await stat(canonical)).isDirectory() ? canonical : null;
@@ -375,7 +384,15 @@ export async function openMessageFile(href, roots) {
375
384
  catch {
376
385
  continue;
377
386
  }
378
- const root = canonicalRoots.find((allowed) => containedBy(allowed, canonicalBefore));
387
+ const permitted = (canonical) => {
388
+ if (isBlocked(blocked, candidate) || isBlocked(blocked, canonical))
389
+ return false;
390
+ if (canonicalOrdinary.some((root) => containedBy(root, canonical)))
391
+ return true;
392
+ return canonicalShared.some((root) => containedBy(root, canonical))
393
+ && !isBlocked(sharedBlocked, candidate) && !isBlocked(sharedBlocked, canonical);
394
+ };
395
+ const root = permitted(canonicalBefore) && canonicalRoots.find((allowed) => containedBy(allowed, canonicalBefore));
379
396
  if (!root) {
380
397
  sawOutsideRoot = true;
381
398
  continue;
@@ -391,7 +408,7 @@ export async function openMessageFile(href, roots) {
391
408
  throw statusError(413, `file exceeds ${MESSAGE_FILE_MAX_BYTES} bytes`);
392
409
  }
393
410
  const canonicalAfter = await realpath(candidate);
394
- if (!containedBy(root, canonicalAfter))
411
+ if (!containedBy(root, canonicalAfter) || !permitted(canonicalAfter))
395
412
  throw statusError(403, "the linked file is outside this conversation's workspace");
396
413
  const after = await stat(canonicalAfter);
397
414
  if (opened.dev !== after.dev || opened.ino !== after.ino) {
@@ -0,0 +1,14 @@
1
+ // Atomic Engineering, 2026-10-31. Harness-owned Plan tool policy.
2
+ // Unknown tools, MCP annotations and shell command strings are never trusted.
3
+ export const PLAN_READ_TOOLS = ["Read", "Glob", "Grep"];
4
+ export const PLAN_REFUSAL = "Plan mode blocked this action. Review the plan and approve it in Atomic before making changes.";
5
+ export function planToolAllowed(tool) {
6
+ return typeof tool === "string" && PLAN_READ_TOOLS.includes(tool);
7
+ }
8
+ export function claudePlanHook(node, helper) {
9
+ const quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
10
+ const command = process.platform === "win32"
11
+ ? '"%OMB_PLAN_NODE%" "%OMB_PLAN_HOOK%" || exit /b 2'
12
+ : `${quote(node)} ${quote(helper)} || exit 2`;
13
+ return [{ matcher: "", hooks: [{ type: "command", command, timeout: 5 }] }];
14
+ }
@@ -44,6 +44,7 @@ export const SPAWNED_PROXIES = {
44
44
  hook: resolveProxy("hooks/omb-hook"),
45
45
  // the RTK add-on's Claude PreToolUse hook (bypassPermissions turns only)
46
46
  rtkHook: resolveProxy("hooks/rtk-hook"),
47
+ planHook: resolveProxy("hooks/plan-hook"),
47
48
  // Loaded by the external `pi` process via `-e`, not by this server — but
48
49
  // resolved through the same single source of truth so the packaged layout
49
50
  // check can assert it ships.
@@ -0,0 +1,142 @@
1
+ // This Mac from the phone: a picture of the whole screen, and taps, keys and
2
+ // typing sent back through the same Cua Driver the bots already use. No VNC
3
+ // server and no second sign-in. The gates are the companion's per-device
4
+ // remote desktop capability and the person's Take control, which pauses the
5
+ // bot so a tap never races its next action.
6
+ //
7
+ // ponytail: one still frame per request (about 1 per second on a Retina Mac).
8
+ // A streamed video path is the upgrade if watching motion ever matters.
9
+ import { execFile } from "node:child_process";
10
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { promisify } from "node:util";
14
+ import { z } from "zod";
15
+ import { json, readBody } from "./harness/http.js";
16
+ import { readCuaConnection, readCuaUnavailableReason } from "./local-computer.js";
17
+ const run = promisify(execFile);
18
+ const coordinate = z.number().finite().min(0).max(100_000);
19
+ const key = z.string().regex(/^[a-z0-9]{1,12}$/i);
20
+ const DESKTOP = { kind: "desktop", display_id: "primary" };
21
+ export const remoteInputSchema = z.discriminatedUnion("type", [
22
+ z.object({
23
+ type: z.literal("click"),
24
+ x: coordinate,
25
+ y: coordinate,
26
+ button: z.enum(["left", "right"]).default("left"),
27
+ count: z.number().int().min(1).max(2).default(1),
28
+ }),
29
+ z.object({ type: z.literal("type"), text: z.string().min(1).max(2_000) }),
30
+ z.object({
31
+ type: z.literal("key"),
32
+ key,
33
+ modifiers: z.array(z.enum(["cmd", "shift", "option", "ctrl"])).max(4).default([]),
34
+ }),
35
+ z.object({
36
+ type: z.literal("scroll"),
37
+ x: coordinate,
38
+ y: coordinate,
39
+ direction: z.enum(["up", "down", "left", "right"]),
40
+ amount: z.number().int().min(1).max(20).default(3),
41
+ }),
42
+ ]);
43
+ export function hostCuaCall(connection) {
44
+ const socket = connection.args[connection.args.indexOf("--socket") + 1];
45
+ if (!connection.args.includes("--socket") || !socket)
46
+ throw new Error("Cua Driver has no socket for this computer");
47
+ return async (tool, args, screenshotOut) => {
48
+ const { stdout } = await run(connection.command, ["call", tool, JSON.stringify(args), "--socket", socket, ...(screenshotOut ? ["--screenshot-out-file", screenshotOut] : [])], { env: { ...process.env, ...connection.env }, timeout: 15_000, maxBuffer: 4 * 1024 * 1024 });
49
+ return JSON.parse(stdout);
50
+ };
51
+ }
52
+ /** The whole primary display. `width`/`height` are the coordinate space
53
+ * clicks use; the image itself is a smaller JPEG to keep it phone-sized. */
54
+ export async function remoteFrame(call, shrink = sipsJpeg) {
55
+ const dir = await mkdtemp(join(tmpdir(), "atomic-remote-"));
56
+ try {
57
+ const png = join(dir, "screen.png");
58
+ const state = z.object({ screenshot_width: z.number().int().positive(), screenshot_height: z.number().int().positive() })
59
+ .parse(await call("get_desktop_state", {}, png));
60
+ const jpeg = join(dir, "screen.jpg");
61
+ await shrink(png, jpeg);
62
+ return {
63
+ image: `data:image/jpeg;base64,${(await readFile(jpeg)).toString("base64")}`,
64
+ width: state.screenshot_width,
65
+ height: state.screenshot_height,
66
+ };
67
+ }
68
+ finally {
69
+ // The picture is the person's screen: never leave it in a temp folder.
70
+ await rm(dir, { recursive: true, force: true });
71
+ }
72
+ }
73
+ async function sipsJpeg(input, output) {
74
+ await run("/usr/bin/sips", ["-s", "format", "jpeg", "-s", "formatOptions", "60", "-Z", "1600", input, "--out", output], { timeout: 15_000 });
75
+ }
76
+ /** Keys and typing go to the frontmost app, the one the person last tapped. */
77
+ async function frontmostPid(call) {
78
+ const { apps } = z.object({ apps: z.array(z.object({ pid: z.number().int().positive(), active: z.boolean().optional() }).passthrough()) })
79
+ .parse(await call("list_apps", {}));
80
+ const active = apps.find((app) => app.active);
81
+ if (!active)
82
+ throw Object.assign(new Error("No app is in front on this computer; tap a window first"), { status: 409 });
83
+ return active.pid;
84
+ }
85
+ export async function remoteInput(call, input) {
86
+ switch (input.type) {
87
+ case "click":
88
+ await call("click", { target: DESKTOP, x: input.x, y: input.y, button: input.button, count: input.count });
89
+ return;
90
+ case "scroll":
91
+ await call("scroll", { target: DESKTOP, x: input.x, y: input.y, direction: input.direction, amount: input.amount });
92
+ return;
93
+ case "type":
94
+ await call("type_text", { pid: await frontmostPid(call), text: input.text });
95
+ return;
96
+ case "key":
97
+ await call("press_key", { pid: await frontmostPid(call), key: input.key, modifiers: input.modifiers });
98
+ return;
99
+ }
100
+ }
101
+ /** `POST /api/bots/:id/computer/remote-frame|remote-input`. The companion
102
+ * admits these only for a device with remote desktop access; input also needs
103
+ * the person to hold Take control, which pauses the bot. */
104
+ export async function remoteDesktopRoute(path, method, req, res, deps) {
105
+ const m = path.match(/^\/api\/bots\/([\w-]+)\/computer\/remote-(frame|input)$/);
106
+ if (!m || method !== "POST")
107
+ return false;
108
+ const reply = (status, body) => (json(res, status, body), true);
109
+ const bot = deps.store.bot(m[1]);
110
+ if (!bot)
111
+ return reply(404, { error: "no such bot" });
112
+ if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
113
+ return reply(415, { error: "content-type must be application/json" });
114
+ }
115
+ if ((deps.platform ?? process.platform) !== "darwin" || await deps.surface(bot, bot.threadId) !== "local") {
116
+ return reply(409, { error: "Phone remote control works for This Mac for now. Choose This Mac for this bot, or open the cloud desktop." });
117
+ }
118
+ const call = (deps.cua ?? hostCall)();
119
+ if (!call)
120
+ return reply(409, { error: readCuaUnavailableReason() ?? "CUA Driver is not ready on this computer" });
121
+ res.setHeader("cache-control", "private, no-store");
122
+ try {
123
+ if (m[2] === "frame")
124
+ return reply(200, await remoteFrame(call));
125
+ if (!deps.controlHeld(bot.id))
126
+ return reply(409, { error: "Take control before using this computer" });
127
+ const input = remoteInputSchema.safeParse(await readBody(req));
128
+ if (!input.success)
129
+ return reply(400, { error: "invalid remote input" });
130
+ await remoteInput(call, input.data);
131
+ return reply(200, { ok: true });
132
+ }
133
+ catch (error) {
134
+ if (error.status !== 409)
135
+ throw error;
136
+ return reply(409, { error: error.message });
137
+ }
138
+ }
139
+ function hostCall() {
140
+ const connection = readCuaConnection();
141
+ return connection ? hostCuaCall(connection) : null;
142
+ }
@@ -0,0 +1,115 @@
1
+ import { messageAttachmentName, messageFileDisposition, messageFileDownloadName, messageFileRoots, messageImageTargetAt, messageReferencesFile, openMessageFile } from "../message-file.js";
2
+ import { PASS } from "./table.js";
3
+ export function createMessageFileRoutes({ store, attachments: ATTACHMENTS_DIR, workspaceDir, access }) {
4
+ return async ({ req, res, url, path, method, auth, json, readBody }) => {
5
+ const m = path.match(/^\/api\/threads\/([\w-]+)\/messages\/([\w-]+)\/file$/);
6
+ const streamsMessageImage = Boolean(m && method === "GET" && url.searchParams.get("preview") === "1");
7
+ if (m && (method === "POST" || streamsMessageImage)) {
8
+ if (method === "POST" && !String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
9
+ return json(res, 415, { error: "content-type must be application/json" });
10
+ }
11
+ const threadId = m[1];
12
+ const directBot = store.botByThread(threadId);
13
+ const group = directBot ? undefined : store.groupByThread(threadId);
14
+ if (!directBot && !group)
15
+ return json(res, 404, { error: "no such conversation" });
16
+ const message = store.messagesFor(threadId).find((candidate) => candidate.id === m[2]);
17
+ if (!message)
18
+ return json(res, 404, { error: "no such message" });
19
+ if (message.kind !== "text") {
20
+ return json(res, 403, { error: "that message does not share this file" });
21
+ }
22
+ const body = method === "POST" ? await readBody(req) : null;
23
+ const rawReference = streamsMessageImage ? url.searchParams.get("ref") : null;
24
+ if (streamsMessageImage && (!rawReference || !/^\d+$/.test(rawReference))) {
25
+ return json(res, 400, { error: "ref must identify a rendered image" });
26
+ }
27
+ const href = method === "POST"
28
+ ? (typeof body.path === "string" ? body.path : "")
29
+ : messageImageTargetAt(message.text ?? "", Number(rawReference));
30
+ if (!href)
31
+ return json(res, 400, { error: "path is required" });
32
+ // Generated image paths are durable capabilities on this exact message.
33
+ // They do not grant access to arbitrary workspace files or Markdown refs.
34
+ const generatedImage = method === "POST" && message.role === "bot" &&
35
+ message.attachments?.some((attachment) => attachment.kind === "image" && attachment.path === href) === true;
36
+ let roots;
37
+ let downloadName;
38
+ if (generatedImage) {
39
+ roots = [ATTACHMENTS_DIR];
40
+ }
41
+ else if (message.role === "user") {
42
+ downloadName = messageAttachmentName(message.text ?? "", href) ?? undefined;
43
+ if (!downloadName) {
44
+ return json(res, 403, { error: "that message does not share this file" });
45
+ }
46
+ roots = [ATTACHMENTS_DIR];
47
+ }
48
+ else {
49
+ if (!messageReferencesFile(message.text ?? "", href)) {
50
+ return json(res, 403, { error: "that bot message does not link to this file" });
51
+ }
52
+ const senderId = directBot?.id ?? message.from?.botId;
53
+ // The persisted bot-role message is the author record. Membership is
54
+ // intentionally not consulted: removing a bot must not break files it
55
+ // already shared in channel history.
56
+ if (!senderId) {
57
+ return json(res, 403, { error: "the file's bot author could not be verified" });
58
+ }
59
+ let pinnedCwd;
60
+ let configuredCwd;
61
+ if (directBot) {
62
+ pinnedCwd = store.taskByThread(directBot.id, threadId)?.cwd;
63
+ configuredCwd = directBot.cwd;
64
+ }
65
+ else if (group) {
66
+ const task = store.groupTaskByThread(group.id, threadId);
67
+ pinnedCwd = task ? task.pinnedCwd : group.threadId === threadId ? group.pinnedCwd : undefined;
68
+ configuredCwd = group.cwd;
69
+ }
70
+ roots = messageFileRoots({
71
+ senderWorkspace: workspaceDir(senderId),
72
+ attachments: ATTACHMENTS_DIR,
73
+ pinnedCwd,
74
+ configuredCwd,
75
+ });
76
+ }
77
+ const file = await openMessageFile(href, roots, message.role === "bot" && !generatedImage
78
+ ? access(directBot?.id ?? message.from?.botId, auth) : {});
79
+ if ((streamsMessageImage || generatedImage) && !file.mime.startsWith("image/")) {
80
+ await file.handle.close();
81
+ return json(res, 415, { error: "only images can be previewed here" });
82
+ }
83
+ res.writeHead(200, {
84
+ "content-type": file.mime,
85
+ "content-length": String(file.bytes),
86
+ ...(streamsMessageImage
87
+ ? { "content-disposition": "inline" }
88
+ : { "content-disposition": messageFileDisposition(messageFileDownloadName(downloadName, file.name)) }),
89
+ "cache-control": streamsMessageImage ? "private, max-age=3600" : "private, no-store",
90
+ "cdn-cache-control": "no-store",
91
+ "cloudflare-cdn-cache-control": "no-store",
92
+ pragma: "no-cache",
93
+ vary: "Authorization",
94
+ "x-content-type-options": "nosniff",
95
+ ...(streamsMessageImage
96
+ ? {
97
+ "cross-origin-resource-policy": "same-origin",
98
+ "referrer-policy": "no-referrer",
99
+ }
100
+ : {}),
101
+ });
102
+ if (file.bytes === 0) {
103
+ await file.handle.close();
104
+ res.end();
105
+ return;
106
+ }
107
+ const stream = file.handle.createReadStream({ start: 0, end: file.bytes - 1, autoClose: true });
108
+ stream.on("error", () => res.destroy());
109
+ res.on("close", () => stream.destroy());
110
+ stream.pipe(res);
111
+ return;
112
+ }
113
+ return PASS;
114
+ };
115
+ }
@@ -0,0 +1,109 @@
1
+ // Shared file-link settings. This widens message downloads only, never bot tools.
2
+ import { readFileSync } from "node:fs";
3
+ import { realpath, stat } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { z } from "zod";
7
+ import { writeFileAtomic } from "./atomic.js";
8
+ import { isRestricted } from "./bot-visibility.js";
9
+ import { PASS } from "./routes/table.js";
10
+ const rootsSchema = z.array(z.string().trim().min(1).max(4096).refine(path => !/[\0\r\n]/.test(path))).max(32);
11
+ const inside = (root, path) => {
12
+ const suffix = relative(root, path);
13
+ return suffix === "" || (suffix !== ".." && !suffix.startsWith(`..${sep}`) && !isAbsolute(suffix));
14
+ };
15
+ export function sharedDeliverableAccess(settings, options) {
16
+ const { dataDir, pawRoot, store, senderId, canSeeBot, canSeeGroup } = options;
17
+ const botRoots = (id, cwd) => [
18
+ join(dataDir, "workspaces", id), join(dataDir, "task-workspaces", id), join(dataDir, "bots", id),
19
+ ...(cwd ? [cwd] : []), ...store.tasks(id).flatMap(task => task.cwd ? [task.cwd] : []),
20
+ ];
21
+ const personal = [join(pawRoot, "me", "personal"),
22
+ join(settings.home, "workspace", "PAW", "me", "personal")];
23
+ const protectedBots = store.bots.filter(bot => bot.personal === true || isRestricted(bot.visibility) || !canSeeBot(bot.id));
24
+ const protectedBotIds = new Set(protectedBots.map(bot => bot.id));
25
+ const protectedRooms = store.groups.filter(group => isRestricted(group.audienceFloor) ||
26
+ !canSeeGroup(group.id) || group.memberIds.some(id => protectedBotIds.has(id)));
27
+ const roomRoots = protectedRooms.flatMap(group => [group.cwd, group.pinnedCwd,
28
+ ...store.groupTasks(group.id).map(task => task.pinnedCwd),
29
+ ].filter((path) => typeof path === "string" && path.length > 0));
30
+ return {
31
+ sharedRoots: settings.current(),
32
+ sharedRootBoundaries: [join(settings.home, "workspace")],
33
+ sharedBlockedRoots: [dataDir, ...personal, ...protectedBots.flatMap(bot => botRoots(bot.id, bot.cwd)), ...roomRoots],
34
+ blockedRoots: [
35
+ ...(store.bot(senderId ?? "")?.personal === true ? [] : personal),
36
+ ...protectedBots.filter(bot => bot.id !== senderId).flatMap(bot => botRoots(bot.id, bot.cwd)),
37
+ ],
38
+ };
39
+ }
40
+ /** Email workspace members, assistants and service loopback are not the machine owner. */
41
+ export function sharedDeliverablesOwner(auth) {
42
+ return auth.scopes.includes("admin") && (auth.kind === "loopback" ? auth.trust !== "service"
43
+ : auth.kind === "session" && !auth.session.email && !auth.session.userId);
44
+ }
45
+ export class SharedDeliverables {
46
+ file;
47
+ roots;
48
+ home;
49
+ constructor(dataDir, home = homedir()) {
50
+ this.home = home;
51
+ this.file = join(dataDir, "shared-deliverables.json");
52
+ try {
53
+ this.roots = rootsSchema.parse(JSON.parse(readFileSync(this.file, "utf8")));
54
+ }
55
+ catch (error) {
56
+ this.roots = error.code === "ENOENT"
57
+ ? [join(home, "workspace", "Reports"), join(home, "workspace", "Atomic")] : [];
58
+ }
59
+ }
60
+ current() { return [...this.roots]; }
61
+ async save(input, blockedRoots) {
62
+ const roots = rootsSchema.parse(input);
63
+ const canonicalBlocked = await Promise.all(blockedRoots.map(async (path) => {
64
+ try {
65
+ return await realpath(path);
66
+ }
67
+ catch {
68
+ return resolve(path);
69
+ }
70
+ }));
71
+ const workspace = await realpath(join(this.home, "workspace")).catch(() => resolve(this.home, "workspace"));
72
+ const next = [];
73
+ for (const raw of roots) {
74
+ const path = raw.startsWith("~/") ? join(this.home, raw.slice(2)) : raw;
75
+ if (!isAbsolute(path))
76
+ throw new Error("Use an absolute folder path.");
77
+ const canonical = await realpath(path);
78
+ if (!(await stat(canonical)).isDirectory())
79
+ throw new Error("Choose a folder.");
80
+ if (inside(canonical, workspace) || canonicalBlocked.some(root => inside(root, canonical))) {
81
+ throw new Error("Choose a deliverable folder, not the workspace or a private folder.");
82
+ }
83
+ next.push(canonical);
84
+ }
85
+ const unique = [...new Set(next)];
86
+ writeFileAtomic(this.file, JSON.stringify(unique, null, 2) + "\n", { mode: 0o600 });
87
+ this.roots = unique;
88
+ }
89
+ }
90
+ export function createSharedDeliverablesRoutes(settings, blockedRoots) {
91
+ return async ({ path, method, auth, req, res, json, readBody }) => {
92
+ if (path !== "/api/shared-deliverables")
93
+ return PASS;
94
+ if (!sharedDeliverablesOwner(auth))
95
+ return json(res, 403, { error: "Only the computer owner can manage shared deliverable folders." });
96
+ if (method === "GET")
97
+ return json(res, 200, { roots: settings.current() });
98
+ if (method !== "PUT")
99
+ return json(res, 405, { error: "Use GET or PUT." });
100
+ try {
101
+ const body = await readBody(req);
102
+ await settings.save(body.roots, blockedRoots());
103
+ return json(res, 200, { roots: settings.current() });
104
+ }
105
+ catch {
106
+ return json(res, 400, { error: "Choose existing deliverable folders. The whole workspace and private folders cannot be shared." });
107
+ }
108
+ };
109
+ }
@@ -5,6 +5,10 @@
5
5
  // There is no separate distillation engine. This module only builds the
6
6
  // prompt and recognises the slash command, so it works on every engine
7
7
  // that mounts the agents tools.
8
+ import { expandSkillCommand } from "../shared/skill-command.js";
9
+ export function expandSkillTurnText(text, authoring) {
10
+ return expandSkillCommand(authoring ? expandLearnTurnText(text) : text);
11
+ }
8
12
  import { SAVE_RUN_AS_SKILL_LINE } from "../shared/learn-request.js";
9
13
  export const LEARN_COMMAND = "/learn";
10
14
  export const LEARN_SOURCE_PREFIX = "learn:";
@@ -2618,16 +2618,16 @@ export class Store {
2618
2618
  return task;
2619
2619
  }
2620
2620
  /** One durable write: never leave only part of a bot's threads updated. */
2621
- setAllThreadApprovalMode(botId, mode) {
2621
+ setAllThreadApprovalMode(botId, mode, preservePlan = false) {
2622
2622
  const bot = this.bot(botId);
2623
2623
  if (!bot)
2624
2624
  return null;
2625
2625
  const patch = { approvalMode: mode, autoApprove: false, alwaysAllow: [] };
2626
- const tasks = (bot.tasks ?? []).map(task => ({ ...task, ...patch }));
2627
- const next = { ...bot, ...patch, approvalGrant: undefined, tasks };
2626
+ const tasks = (bot.tasks ?? []).map(task => preservePlan && approvalModeFor(this.projectBotForTask(botId, task.threadId)) === "plan" ? { ...task } : { ...task, ...patch });
2627
+ const next = { ...bot, ...(preservePlan && approvalModeFor(bot) === "plan" ? {} : patch), approvalGrant: undefined, tasks };
2628
2628
  this.saveBots(this.bots.map(candidate => candidate === bot ? next : candidate));
2629
2629
  bot.tasks?.forEach((task, index) => Object.assign(task, tasks[index]));
2630
- Object.assign(bot, patch, { approvalGrant: undefined });
2630
+ Object.assign(bot, { ...next, tasks: bot.tasks });
2631
2631
  this.emit({ type: "bot", botId });
2632
2632
  return bot;
2633
2633
  }
@@ -127,9 +127,10 @@ export function hostDefaultModel(provider, candidate) {
127
127
  * the first in `ranked` (the graded models, best value first within each
128
128
  * class) of that class, else the class below, and so on, else the class
129
129
  * above; with nothing graded, the account's own default when it is the
130
- * provider's, else its first. Undefined when it offers none. */
130
+ * provider's, else its first. Undefined when it offers none it can use. */
131
131
  export function bestProviderModel(input) {
132
- const models = providerModels(input.provider, input.candidate);
132
+ const usable = input.usable ?? (() => true);
133
+ const models = providerModels(input.provider, input.candidate).filter(usable);
133
134
  if (!models.length)
134
135
  return undefined;
135
136
  const order = new Map((input.ranked ?? []).map((id, index) => [id.toLowerCase(), index]));
@@ -2,13 +2,15 @@
2
2
  * permission mode, passed through: OpenMausBot never decides a permission
3
3
  * itself (Full access aside, which answers residual prompts because that is
4
4
  * what the person granted). The order is the order the selector shows. */
5
- export const APPROVAL_MODES = ["ask", "edits", "auto", "full", "custom"];
5
+ export const APPROVAL_MODES = ["ask", "edits", "auto", "full", "custom", "plan"];
6
6
  /** Only providers with an implemented permission mapping may expose a level.
7
7
  * `edits` (auto-accept edits) exists where the engine has such a mode:
8
8
  * Claude and Grok `acceptEdits`, Antigravity `auto_edit`, Qwen `auto-edit`,
9
9
  * Gemini `auto_edit`, Mistral Vibe `accept-edits`. Codex's Ask already runs `workspace-write`, so an edits
10
10
  * level would change nothing there. */
11
11
  export function supportsApprovalMode(driverKind, mode) {
12
+ if (mode === "plan")
13
+ return ["atomicEngine", "codex", "claudeAgent", "grok", "openai-compat", "minimax", "mistral", "ollamaCloud"].includes(driverKind ?? "");
12
14
  if (mode === "custom")
13
15
  return driverKind === "codex";
14
16
  if (mode === "edits")
@@ -127,7 +127,7 @@ export function engineBrand(engine) {
127
127
  const name = engine.managed?.organizationName?.trim() || (generic(engine.displayName) ? "OpenAI-compatible endpoint" : engine.displayName.trim());
128
128
  return { key: `endpoint:${name.toLowerCase()}`, name, group: "flatPlans" };
129
129
  }
130
- if (engine.driverKind === "customAcp") {
130
+ if (["customAcp", "atomicEngine"].includes(engine.driverKind)) {
131
131
  if (engine.instanceId === ATOMIC_ENGINE_ID || /^atomic engine$/i.test(engine.displayName.trim()))
132
132
  return { key: "atomic-engine", name: "Atomic Engine", group: "atomic" };
133
133
  const name = generic(engine.displayName) ? "Custom agent" : engine.displayName.trim();
@@ -8,10 +8,10 @@ export const ACCOUNT_PROVIDERS = [
8
8
  // Kimi Code reads its whole data root (login included) from KIMI_CODE_HOME.
9
9
  { id: "kimi", kind: "signIn", label: "Kimi Code", billing: "subscription", driver: "kimiAgent", homeEnv: "KIMI_CODE_HOME", loginArgs: ["login"], signupUrl: "https://www.kimi.com/code" },
10
10
  // Droid resolves <FACTORY_HOME_OVERRIDE>/.factory, so the folder is a home, not the data root.
11
- { id: "droid", kind: "signIn", label: "Factory Droid", billing: "subscription", driver: "droidAgent", homeEnv: "FACTORY_HOME_OVERRIDE", loginArgs: [], signupUrl: "https://factory.ai/pricing" },
11
+ { id: "droid", kind: "signIn", label: "Factory Droid", billing: "subscription", driver: "droidAgent", homeEnv: "FACTORY_HOME_OVERRIDE", loginArgs: [], signupUrl: "https://factory.com/pricing" },
12
12
  {
13
13
  id: "anthropic", kind: "apiKey", label: "Anthropic API", billing: "payPerUse", driver: "claudeAgent", envVar: "ANTHROPIC_API_KEY", icon: "anthropic",
14
- config: {}, keyTest: { provider: "anthropic" }, keyUrl: "https://console.anthropic.com/settings/keys",
14
+ config: {}, keyTest: { provider: "anthropic" }, keyUrl: "https://platform.claude.com/settings/keys",
15
15
  },
16
16
  {
17
17
  id: "openai", kind: "apiKey", label: "OpenAI API", billing: "payPerUse", driver: "openai-compat", envVar: "OPENAI_API_KEY", icon: "openai",
@@ -28,14 +28,14 @@ export const ACCOUNT_PROVIDERS = [
28
28
  // spending a request: there is no key test.
29
29
  id: "perplexity", kind: "apiKey", label: "Perplexity API", billing: "payPerUse", driver: "openai-compat", envVar: "PERPLEXITY_API_KEY", icon: "perplexity",
30
30
  config: { url: "https://api.perplexity.ai", apiKeyEnv: "PERPLEXITY_API_KEY", model: "sonar-pro", provider: "" },
31
- keyUrl: "https://www.perplexity.ai/account/api/keys",
31
+ keyUrl: "https://console.perplexity.ai/project/keys",
32
32
  },
33
33
  {
34
34
  // Alibaba's flat-price Qwen Coding Plan: one plan key serves Qwen, GLM, Kimi and MiniMax models.
35
35
  id: "qwenCodingPlan", kind: "apiKey", label: "Qwen Coding Plan", billing: "subscription", driver: "openai-compat", envVar: "QWEN_CODING_PLAN_API_KEY", icon: "qwen",
36
36
  config: { url: "https://coding-intl.dashscope.aliyuncs.com/v1", apiKeyEnv: "QWEN_CODING_PLAN_API_KEY", model: "qwen3.7-plus", provider: "" },
37
37
  keyTest: { provider: "openaiCompat", url: "https://coding-intl.dashscope.aliyuncs.com/v1" },
38
- keyUrl: "https://modelstudio.console.alibabacloud.com/", signupUrl: "https://www.alibabacloud.com/help/en/model-studio/coding-plan",
38
+ keyUrl: "https://modelstudio.console.alibabacloud.com/ap-southeast-1/subscription/coding-plan", signupUrl: "https://www.alibabacloud.com/help/en/model-studio/coding-plan",
39
39
  },
40
40
  {
41
41
  // MiniMax's flat-price M Plan (formerly the Coding Plan): an `sk-cp-` plan
@@ -61,7 +61,7 @@ export const ACCOUNT_PROVIDERS = [
61
61
  {
62
62
  // OpenCode Go is a flat monthly plan used through an OpenCode key.
63
63
  id: "opencodeGo", kind: "apiKey", label: "OpenCode Go", billing: "subscription", driver: "opencodeGo", envVar: "OPENCODE_API_KEY",
64
- config: {}, keyUrl: "https://opencode.ai/docs/providers/", signupUrl: "https://opencode.ai/go",
64
+ config: {}, keyUrl: "https://opencode.ai/go", signupUrl: "https://opencode.ai/go",
65
65
  },
66
66
  ];
67
67
  export function accountProvider(id) {
@@ -0,0 +1,16 @@
1
+ /** Skill commands remain ordinary user turns: they grant no extra permissions. */
2
+ export function expandSkillCommand(text) {
3
+ const match = /^\/(skills|skill)(?:\s+([\s\S]*))?$/i.exec(text.trim());
4
+ if (!match)
5
+ return text;
6
+ const request = match[2]?.trim() ?? "";
7
+ if (match[1].toLowerCase() === "skills" || !request) {
8
+ return "List the skills actually available to you in this conversation, with their exact names and a short description. " +
9
+ "Use the available skill catalog or discovery tools; do not invent skills or execute one. " +
10
+ (request ? `Filter the list for this request: ${request}\n` : "") +
11
+ "Explain that /skill <name> <task> invokes a skill and /learn saves a reusable procedure.";
12
+ }
13
+ return "Use the named available skill for the following request. Read its instructions before acting. " +
14
+ "If the skill is missing or the name is ambiguous, explain that and offer available matches; do not invent it. " +
15
+ "Existing permissions and approval requirements still apply. If no task is supplied, use the current task when clear, otherwise ask what to apply it to.\n\n" + request;
16
+ }
@@ -17174,6 +17174,7 @@ var SPAWNED_PROXIES = {
17174
17174
  hook: resolveProxy("hooks/omb-hook"),
17175
17175
  // the RTK add-on's Claude PreToolUse hook (bypassPermissions turns only)
17176
17176
  rtkHook: resolveProxy("hooks/rtk-hook"),
17177
+ planHook: resolveProxy("hooks/plan-hook"),
17177
17178
  // Loaded by the external `pi` process via `-e`, not by this server — but
17178
17179
  // resolved through the same single source of truth so the packaged layout
17179
17180
  // check can assert it ships.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chibitek/chibi",
3
- "version": "0.1.169",
3
+ "version": "0.1.171",
4
4
  "description": "Atomic by Chibitek: run your AI team's server anywhere and pair your devices to it",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./mermaid.core-Dm-Ny6a0.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
@@ -1,8 +0,0 @@
1
- import{c as r,s as e}from"./flowDiagram-KWPJA3E3-hsbvL3Ow.js";import{_ as a}from"./mermaid.core-Dm-Ny6a0.js";import"./chunk-5VM5RSS4-BFkFo_ng.js";import"./chunk-XXDRQBXY-5ThtmFbP.js";import"./chunk-WEXAMYUT-FhqQ0sO0.js";import"./chunk-GWA4HPMP-BZChfTYw.js";import"./channel-BZDQCBTd.js";import"./index-ByXr4FRl.js";var o=a(t=>`${e(t)}
2
- .swimlane.cluster:not([data-color-id]) rect {
3
- stroke: ${t.clusterBorder} !important;
4
- }
5
- [data-look="neo"].cluster rect {
6
- filter: none;
7
- }
8
- `,"getStyles"),s=o,u=r({styles:s});export{u as diagram};