@krapht/ork 1.0.0-alpha.17 → 1.0.0-alpha.19

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 (48) hide show
  1. package/dist/{__build_info__-C1Nm0hlJ.mjs → __build_info__-dPf4cEGL.mjs} +3 -134
  2. package/dist/{attach-EP374oMP.mjs → attach-BQl8nQz2.mjs} +5 -5
  3. package/dist/{attach-RQx__RzH.mjs → attach-DhZQ87Nz.mjs} +49 -224
  4. package/dist/bin.mjs +204 -154
  5. package/dist/{brief-Bs3cMQU0.mjs → brief-BQbeoPMc.mjs} +19 -9
  6. package/dist/{builtin-files-yYaNTQRU.mjs → builtin-files-CWL3JEo-.mjs} +1 -1
  7. package/dist/{command-tree-BkSYWmYJ.mjs → command-tree-B6uvMJq1.mjs} +1 -1
  8. package/dist/{completion-DIXWlnAl.mjs → completion-DKPuD_Rw.mjs} +47 -14
  9. package/dist/{context-Dq-VM_sq.mjs → context-Bw6R_EYq.mjs} +3 -3
  10. package/dist/detached-BQlbmqR0.mjs +38 -0
  11. package/dist/{find-k489laBa.mjs → find-B30-rd-u.mjs} +1 -1
  12. package/dist/flags-ByYBx-Xb.mjs +170 -0
  13. package/dist/{follow-ups-DD8NTPe6.mjs → follow-ups-Cxca1Jgg.mjs} +24 -18
  14. package/dist/{format-DQc9A_ei.mjs → format-C-nEZv-P.mjs} +27 -11
  15. package/dist/{help-groups-hB6kY34d.mjs → help-groups-BpRgBSTh.mjs} +13 -7
  16. package/dist/{in-a-run-D_9GlAlS.mjs → in-a-run-CZFzoT3R.mjs} +2 -2
  17. package/dist/{install-PVlV2QPq.mjs → install-CsqK1avz.mjs} +2 -183
  18. package/dist/{limits-CHqBO7wb.mjs → limits-CXh052II.mjs} +1 -1
  19. package/dist/{mcp-D38Ajb0b.mjs → mcp-B4wME9C4.mjs} +6 -6
  20. package/dist/{once-DuUzpzL2.mjs → once-BKn3pOtg.mjs} +3 -3
  21. package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
  22. package/dist/{pause-Bx3ePGrn.mjs → pause-B1grR1cl.mjs} +3 -3
  23. package/dist/{print-n2qetUHc.mjs → print-Bd8MWVJk.mjs} +1 -1
  24. package/dist/{project-records-Cune1l6I.mjs → project-records-BOEVs69i.mjs} +1 -1
  25. package/dist/{parse-BzWpMN-a.mjs → refusals-DQQX3AIb.mjs} +10 -1
  26. package/dist/{relay-D7RN4m68.mjs → relay-CeNiq3GT.mjs} +1 -1
  27. package/dist/{run-OOWQIKMJ.mjs → run-C0C54UiU.mjs} +116 -137
  28. package/dist/{run-BfxdsQJk.mjs → run-D6yU2rLf.mjs} +1 -1
  29. package/dist/{send-CKth_vgn.mjs → send-DqgF58hb.mjs} +2 -2
  30. package/dist/{src-DLsFsmOD.mjs → src-Ciy7i3se.mjs} +882 -71
  31. package/dist/{src-BHod38UQ.mjs → src-D2qpd-G7.mjs} +394 -247
  32. package/dist/{src-DmmrX2_C.mjs → src-DP02RP9O.mjs} +227 -180
  33. package/dist/{src-DOjTY2ET.mjs → src-n-u4TGev.mjs} +186 -43
  34. package/dist/{store-DACH768X.mjs → store-2KXzMCEN.mjs} +1 -1
  35. package/dist/ui/assets/app-C5vO1M-l.css +1 -0
  36. package/dist/ui/assets/{app-CN3acQ-1.js → app-ICEKpy_C.js} +21 -20
  37. package/dist/ui/assets/{index-B2uGxA4W.js → index-BGaix5wh.js} +2 -2
  38. package/dist/ui/index.html +1 -1
  39. package/dist/{ui-CJ-BX12n.mjs → ui-CAvygxTb.mjs} +53 -24
  40. package/dist/{usage-DxZ6JGd4.mjs → usage-BFBco5OS.mjs} +7 -7
  41. package/dist/{validate-BS0nCccW.mjs → validate-UrmC5AG1.mjs} +5 -5
  42. package/dist/{watch-Dp7IB4FR.mjs → watch-DTxkgB8R.mjs} +5 -5
  43. package/dist/{workflows-Djo1edlK.mjs → workflows-8GV-SeRm.mjs} +28 -4
  44. package/dist/{worktrees-Drof_Nyx.mjs → worktrees-CUdUvZlO.mjs} +9 -6
  45. package/package.json +1 -1
  46. package/dist/flags-B4ZZWspN.mjs +0 -125
  47. package/dist/post-2CaJz_mV.mjs +0 -46
  48. package/dist/ui/assets/app-9Q-62Ywz.css +0 -1
@@ -1,8 +1,8 @@
1
- import { $n as FEEDBACK_TARGETS, Bn as countedTokens, It as lowerChild, Lt as programFound, Qn as FEEDBACK_SEVERITIES, Rt as startAt, Ti as AGENT_DRIVERS, Xn as endFor, Yn as END_KINDS, Zn as FEEDBACK_KINDS, _r as MASK, dn as FOLLOW_UP_KINDS, qn as outcomeEnd, vt as parseReport, yr as maskSecrets } from "./src-DLsFsmOD.mjs";
1
+ import { Bt as programFound, Er as MASK, Or as maskSecrets, Pi as AGENT_DRIVERS, Un as countedTokens, Vt as startAt, cr as FEEDBACK_KINDS, ir as endFor, lr as FEEDBACK_SEVERITIES, mn as FOLLOW_UP_KINDS, rr as END_KINDS, tr as outcomeEnd, ur as FEEDBACK_TARGETS, xt as parseReport, zt as lowerChild } from "./src-Ciy7i3se.mjs";
2
2
  import { n as holdGuard, r as orphanGuard } from "./orphan-guard-BFKg8VSd.mjs";
3
3
  import path from "node:path";
4
4
  import { z } from "zod";
5
- import { existsSync, linkSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
+ import { chmodSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
7
7
  import { homedir, tmpdir } from "node:os";
8
8
  import { createInterface } from "node:readline";
@@ -1608,183 +1608,6 @@ const claudeUsage = {
1608
1608
  dollars: true,
1609
1609
  combine: addUsage
1610
1610
  };
1611
- // ../runtime/src/adapters/claude-code.ts
1612
- /**
1613
- * Per-request timeout for Ork's MCP server, in ms. The CLI otherwise gives an
1614
- * HTTP server's request 60 seconds and drops a call silent for 5 minutes, and
1615
- * `delegate` waits for whole child sessions (the per-server `timeout` raises
1616
- * both; `MCP_TOOL_TIMEOUT` still caps it).
1617
- */
1618
- const ORK_TOOL_TIMEOUT_MS = 864e5;
1619
- /** Claude Code's switch for the one-hour prompt cache. */
1620
- const PROMPT_CACHE_1H = "ENABLE_PROMPT_CACHING_1H";
1621
- /** Claude Code's switch that turns its auto-memory off. */
1622
- const NO_AUTO_MEMORY = "CLAUDE_CODE_DISABLE_AUTO_MEMORY";
1623
- /**
1624
- * The CLI flag that moves the per-machine sections (working directory, memory
1625
- * path, git status) out of the system prompt.
1626
- */
1627
- const EXCLUDE_DYNAMIC_SECTIONS = "--exclude-dynamic-system-prompt-sections";
1628
- /** How long a hook holds a call before it defers it instead. */
1629
- const HOLD_WINDOW_MS = 54e4;
1630
- /** The variables a session's prompt cache asks of Claude Code. */
1631
- const cacheEnv = (spec) => spec.promptCache === "1h" ? { [PROMPT_CACHE_1H]: "1" } : {};
1632
- /** Whether the session loads no MCP server but Ork's. */
1633
- const strictMcpOf = (spec) => spec.tools.strictMcp === true || spec.machineClaude !== true;
1634
- /** The variables that keep the machine's auto-memory out of a session. */
1635
- const memoryEnv = (spec) => spec.machineClaude === true ? {} : { [NO_AUTO_MEMORY]: "1" };
1636
- /** Claude Code's default timeout for a Bash call, in milliseconds. */
1637
- const BASH_TIMEOUT = "BASH_DEFAULT_TIMEOUT_MS";
1638
- /**
1639
- * Ten minutes, Claude Code's own most for a Bash call. At its default of two, a
1640
- * longer command (a typecheck of the workspace) moves to the background, and
1641
- * the session then polls it with `sleep`, `until` or `Monitor`, which ask.
1642
- */
1643
- const BASH_TIMEOUT_MS = 6e5;
1644
- /** The variables that let a session's Bash calls run to the end in front. */
1645
- const bashEnv = () => ({ [BASH_TIMEOUT]: String(BASH_TIMEOUT_MS) });
1646
- /** Everything a session adds to its driver's environment. */
1647
- const driverEnv = (spec) => ({
1648
- ...bashEnv(),
1649
- ...cacheEnv(spec),
1650
- ...memoryEnv(spec)
1651
- });
1652
- /** A later message goes in at the next step, even mid-turn. */
1653
- const userLine = (text, later = false) => `${JSON.stringify({
1654
- type: "user",
1655
- message: {
1656
- role: "user",
1657
- content: text
1658
- },
1659
- parent_tool_use_id: null,
1660
- ...later ? { priority: "next" } : {}
1661
- })}\n`;
1662
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1663
- /**
1664
- * The node's `--settings` with Ork's `PreToolUse` HTTP hook added: before every
1665
- * tool call, pre-approved ones included, the session asks whether its node may
1666
- * go on. Its `PostToolUse` hook after a Bash call frees the call's slot.
1667
- */
1668
- const withHoldHook = (settings, holdUrl, token) => {
1669
- const hooks = isRecord(settings?.["hooks"]) ? settings["hooks"] : {};
1670
- const listed = (event) => {
1671
- const own = hooks[event];
1672
- return Array.isArray(own) ? [...own] : [];
1673
- };
1674
- const hook = {
1675
- type: "http",
1676
- url: holdUrl,
1677
- headers: { Authorization: `Bearer ${token}` },
1678
- timeout: 600
1679
- };
1680
- return {
1681
- ...settings,
1682
- hooks: {
1683
- ...hooks,
1684
- PreToolUse: [...listed("PreToolUse"), {
1685
- matcher: "*",
1686
- hooks: [hook]
1687
- }],
1688
- PostToolUse: [...listed("PostToolUse"), {
1689
- matcher: "Bash",
1690
- hooks: [hook]
1691
- }]
1692
- }
1693
- };
1694
- };
1695
- /** The hook's answer: nothing to say for "go", the deferral otherwise. */
1696
- const hookOutput = (answer) => answer === "go" ? {} : { hookSpecificOutput: {
1697
- hookEventName: "PreToolUse",
1698
- permissionDecision: "defer",
1699
- permissionDecisionReason: "Ork paused this node; it goes on from this call when it continues."
1700
- } };
1701
- const claudeHold = {
1702
- answer: hookOutput,
1703
- windowMs: HOLD_WINDOW_MS
1704
- };
1705
- /**
1706
- * The `claude -p` arguments for a session spec. The brief goes on stdin;
1707
- * `allowed` are the Ork tools that need no permission prompt.
1708
- */
1709
- const buildCliArgs = (spec, mcp, allowed) => {
1710
- const mcpConfig = { mcpServers: {
1711
- ...spec.mcpServers,
1712
- ["ork"]: {
1713
- type: "http",
1714
- url: mcp.url,
1715
- headers: { Authorization: `Bearer ${mcp.token}` },
1716
- timeout: ORK_TOOL_TIMEOUT_MS
1717
- }
1718
- } };
1719
- const args = [
1720
- "-p",
1721
- "--input-format",
1722
- "stream-json",
1723
- "--output-format",
1724
- "stream-json",
1725
- "--verbose",
1726
- "--setting-sources",
1727
- spec.settingSources.join(","),
1728
- "--append-system-prompt",
1729
- spec.systemAppend,
1730
- EXCLUDE_DYNAMIC_SECTIONS,
1731
- "--permission-mode",
1732
- spec.permissionMode,
1733
- "--json-schema",
1734
- JSON.stringify(spec.reportSchema),
1735
- "--mcp-config",
1736
- JSON.stringify(mcpConfig),
1737
- "--permission-prompt-tool",
1738
- toolId("permission"),
1739
- "--allowedTools",
1740
- [...spec.tools.allow, ...allowed].join(",")
1741
- ];
1742
- if (spec.tools.only !== void 0) args.push("--tools", spec.tools.only.join(","));
1743
- if (strictMcpOf(spec)) args.push("--strict-mcp-config");
1744
- if (spec.agents !== void 0) args.push("--agents", JSON.stringify(spec.agents));
1745
- if (spec.tools.deny.length > 0) args.push("--disallowedTools", spec.tools.deny.join(","));
1746
- if (spec.model !== void 0) args.push("--model", spec.model);
1747
- if (spec.effort !== void 0) args.push("--effort", spec.effort);
1748
- if (spec.maxTurns !== void 0) args.push("--max-turns", String(spec.maxTurns));
1749
- if (spec.budgetUsd !== void 0) args.push("--max-budget-usd", String(spec.budgetUsd));
1750
- if (spec.resume !== void 0) args.push("--resume", spec.resume);
1751
- if (spec.resume !== void 0 && spec.fork === true) args.push("--fork-session");
1752
- for (const dir of spec.plugins ?? []) args.push("--plugin-dir", dir);
1753
- const settings = mcp.holdUrl === void 0 ? spec.settings : withHoldHook(spec.settings, mcp.holdUrl, mcp.token);
1754
- if (settings !== void 0) args.push("--settings", JSON.stringify(settings));
1755
- return args;
1756
- };
1757
- /** `claude`, as the session runner's strategies. */
1758
- const claudeCodeAdapter = {
1759
- driver: "claude-code",
1760
- command: {
1761
- bin: "claude",
1762
- args: (spec, leg) => buildCliArgs(spec, leg.endpoint, leg.allowed),
1763
- input: userLine
1764
- },
1765
- decode: decodeClaudeLine,
1766
- legDecoder: claudeDecoder,
1767
- mcp: {
1768
- kind: "flag",
1769
- wire: () => Promise.resolve({
1770
- args: [],
1771
- env: {},
1772
- release: () => Promise.resolve()
1773
- })
1774
- },
1775
- permissions: {
1776
- kind: "callback",
1777
- denyByDefault: false
1778
- },
1779
- usage: claudeUsage,
1780
- context: {
1781
- instructions: ["CLAUDE.md", "AGENTS.md"],
1782
- skills: ".claude/skills",
1783
- agents: ".claude/agents",
1784
- env: driverEnv
1785
- },
1786
- hold: claudeHold
1787
- };
1788
1611
  // ../runtime/src/lease-ledger.ts
1789
1612
  /**
1790
1613
  * @file The ledger of Command Code leases (RFC-014, `lease.ts`): one record per
@@ -1911,6 +1734,280 @@ const cleanStaleLeases = async (options) => {
1911
1734
  }
1912
1735
  return sweep;
1913
1736
  };
1737
+ // ../runtime/src/secret-file.ts
1738
+ /**
1739
+ * @file A session's private folder for files that hold a secret, such as a
1740
+ * config carrying Ork's bearer token (RFC-014): a 0700 folder outside the
1741
+ * checkout, under the system's temporary folder, named after the process that
1742
+ * made it, with each file written at mode 0600. The session removes it when
1743
+ * it ends; a folder whose process is gone (a crash) is removed when the next
1744
+ * one is made. Nothing here logs what it writes.
1745
+ */
1746
+ /** Where sessions' private folders live unless a caller names another place. */
1747
+ const secretRoot = () => path.join(tmpdir(), "ork-sessions");
1748
+ /** Removes the folders under `root` whose process is gone; returns them. */
1749
+ const sweepSecretFolders = (root) => {
1750
+ let names;
1751
+ try {
1752
+ names = readdirSync(root);
1753
+ } catch {
1754
+ return [];
1755
+ }
1756
+ return names.flatMap((name) => {
1757
+ const pid = Number(name.split("-")[0]);
1758
+ if (!Number.isInteger(pid) || alive(pid)) return [];
1759
+ const dir = path.join(root, name);
1760
+ rmSync(dir, {
1761
+ recursive: true,
1762
+ force: true
1763
+ });
1764
+ return [dir];
1765
+ });
1766
+ };
1767
+ /**
1768
+ * Refuses a root that isn't this user's own folder: in a shared temporary
1769
+ * folder another user could have made it first, or put a link there, to read or
1770
+ * swap what a session loads. One of ours that is open to others is closed.
1771
+ */
1772
+ const holdPrivate = (root) => {
1773
+ const held = lstatSync(root);
1774
+ const uid = process.getuid?.();
1775
+ if (!held.isDirectory() || uid !== void 0 && held.uid !== uid) throw new Error(`${root} is not a folder of this user's; remove it, or set TMPDIR to a private folder`);
1776
+ if ((held.mode & 63) !== 0) chmodSync(root, 448);
1777
+ };
1778
+ /** A new private folder under `root`, after sweeping the stale ones. */
1779
+ const makeSecretFolder = (root = secretRoot()) => {
1780
+ mkdirSync(root, {
1781
+ recursive: true,
1782
+ mode: 448
1783
+ });
1784
+ holdPrivate(root);
1785
+ sweepSecretFolders(root);
1786
+ const dir = mkdtempSync(path.join(root, `${String(process.pid)}-`));
1787
+ return {
1788
+ dir,
1789
+ write: (name, content) => {
1790
+ const file = path.join(dir, name);
1791
+ writeFileSync(file, content, { mode: 384 });
1792
+ return file;
1793
+ },
1794
+ remove: () => {
1795
+ rmSync(dir, {
1796
+ recursive: true,
1797
+ force: true
1798
+ });
1799
+ }
1800
+ };
1801
+ };
1802
+ // ../runtime/src/adapters/claude-code-flags.ts
1803
+ /**
1804
+ * Per-request timeout for Ork's MCP server, in ms. The CLI otherwise gives an
1805
+ * HTTP server's request 60 seconds and drops a call silent for 5 minutes, and
1806
+ * `delegate` waits for whole child sessions (the per-server `timeout` raises
1807
+ * both; `MCP_TOOL_TIMEOUT` still caps it).
1808
+ */
1809
+ const ORK_TOOL_TIMEOUT_MS = 864e5;
1810
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1811
+ /**
1812
+ * The node's `--settings` with Ork's `PreToolUse` HTTP hook added: before every
1813
+ * tool call, pre-approved ones included, the session asks whether its node may
1814
+ * go on. Its `PostToolUse` hook after a Bash call frees the call's slot.
1815
+ */
1816
+ const withHoldHook = (settings, holdUrl, token) => {
1817
+ const hooks = isRecord(settings?.["hooks"]) ? settings["hooks"] : {};
1818
+ const listed = (event) => {
1819
+ const own = hooks[event];
1820
+ return Array.isArray(own) ? [...own] : [];
1821
+ };
1822
+ const hook = {
1823
+ type: "http",
1824
+ url: holdUrl,
1825
+ headers: { Authorization: `Bearer ${token}` },
1826
+ timeout: 600
1827
+ };
1828
+ return {
1829
+ ...settings,
1830
+ hooks: {
1831
+ ...hooks,
1832
+ PreToolUse: [...listed("PreToolUse"), {
1833
+ matcher: "*",
1834
+ hooks: [hook]
1835
+ }],
1836
+ PostToolUse: [...listed("PostToolUse"), {
1837
+ matcher: "Bash",
1838
+ hooks: [hook]
1839
+ }]
1840
+ }
1841
+ };
1842
+ };
1843
+ /** The session's `--mcp-config`: its servers, and Ork's with the token. */
1844
+ const mcpConfigOf = (spec, mcp) => ({ mcpServers: {
1845
+ ...spec.mcpServers,
1846
+ ["ork"]: {
1847
+ type: "http",
1848
+ url: mcp.url,
1849
+ headers: { Authorization: `Bearer ${mcp.token}` },
1850
+ timeout: ORK_TOOL_TIMEOUT_MS
1851
+ }
1852
+ } });
1853
+ /** The session's `--settings`, with the hold hook (and its token) when held. */
1854
+ const settingsOf = (spec, mcp) => mcp.holdUrl === void 0 ? spec.settings : withHoldHook(spec.settings, mcp.holdUrl, mcp.token);
1855
+ /** The flags whose values carry Ork's token. */
1856
+ const SECRET_FLAGS = /* @__PURE__ */ new Set(["--mcp-config", "--settings"]);
1857
+ /**
1858
+ * `args` without the flags that carry Ork's token: a process's arguments can be
1859
+ * read by other users of the machine, so the session passes them as files.
1860
+ */
1861
+ const withoutSecretFlags = (args) => args.filter((arg, at) => !SECRET_FLAGS.has(arg) && !SECRET_FLAGS.has(args[at - 1] ?? ""));
1862
+ /**
1863
+ * Writes the session's MCP config and settings into a private folder (0700,
1864
+ * each file 0600) and passes their paths; the folder goes when the session
1865
+ * ends.
1866
+ */
1867
+ const secretFlags = {
1868
+ kind: "flag",
1869
+ wire: (spec, endpoint) => {
1870
+ const folder = makeSecretFolder();
1871
+ try {
1872
+ const config = JSON.stringify(mcpConfigOf(spec, endpoint));
1873
+ const args = ["--mcp-config", folder.write("mcp.json", config)];
1874
+ const settings = settingsOf(spec, endpoint);
1875
+ if (settings !== void 0) args.push("--settings", folder.write("settings.json", JSON.stringify(settings)));
1876
+ return Promise.resolve({
1877
+ args,
1878
+ env: {},
1879
+ release: () => {
1880
+ folder.remove();
1881
+ return Promise.resolve();
1882
+ }
1883
+ });
1884
+ } catch (error) {
1885
+ folder.remove();
1886
+ throw error;
1887
+ }
1888
+ }
1889
+ };
1890
+ // ../runtime/src/adapters/claude-code.ts
1891
+ /** Claude Code's switch for the one-hour prompt cache. */
1892
+ const PROMPT_CACHE_1H = "ENABLE_PROMPT_CACHING_1H";
1893
+ /** Claude Code's switch that turns its auto-memory off. */
1894
+ const NO_AUTO_MEMORY = "CLAUDE_CODE_DISABLE_AUTO_MEMORY";
1895
+ /**
1896
+ * The CLI flag that moves the per-machine sections (working directory, memory
1897
+ * path, git status) out of the system prompt.
1898
+ */
1899
+ const EXCLUDE_DYNAMIC_SECTIONS = "--exclude-dynamic-system-prompt-sections";
1900
+ /** How long a hook holds a call before it defers it instead. */
1901
+ const HOLD_WINDOW_MS = 54e4;
1902
+ /** The variables a session's prompt cache asks of Claude Code. */
1903
+ const cacheEnv = (spec) => spec.promptCache === "1h" ? { [PROMPT_CACHE_1H]: "1" } : {};
1904
+ /** Whether the session loads no MCP server but Ork's. */
1905
+ const strictMcpOf = (spec) => spec.tools.strictMcp === true || spec.machineClaude !== true;
1906
+ /** The variables that keep the machine's auto-memory out of a session. */
1907
+ const memoryEnv = (spec) => spec.machineClaude === true ? {} : { [NO_AUTO_MEMORY]: "1" };
1908
+ /** Claude Code's default timeout for a Bash call, in milliseconds. */
1909
+ const BASH_TIMEOUT = "BASH_DEFAULT_TIMEOUT_MS";
1910
+ /**
1911
+ * Ten minutes, Claude Code's own most for a Bash call. At its default of two, a
1912
+ * longer command (a typecheck of the workspace) moves to the background, and
1913
+ * the session then polls it with `sleep`, `until` or `Monitor`, which ask.
1914
+ */
1915
+ const BASH_TIMEOUT_MS = 6e5;
1916
+ /** The variables that let a session's Bash calls run to the end in front. */
1917
+ const bashEnv = () => ({ [BASH_TIMEOUT]: String(BASH_TIMEOUT_MS) });
1918
+ /** Everything a session adds to its driver's environment. */
1919
+ const driverEnv = (spec) => ({
1920
+ ...bashEnv(),
1921
+ ...cacheEnv(spec),
1922
+ ...memoryEnv(spec)
1923
+ });
1924
+ /** A later message goes in at the next step, even mid-turn. */
1925
+ const userLine = (text, later = false) => `${JSON.stringify({
1926
+ type: "user",
1927
+ message: {
1928
+ role: "user",
1929
+ content: text
1930
+ },
1931
+ parent_tool_use_id: null,
1932
+ ...later ? { priority: "next" } : {}
1933
+ })}\n`;
1934
+ /** The hook's answer: nothing to say for "go", the deferral otherwise. */
1935
+ const hookOutput = (answer) => answer === "go" ? {} : { hookSpecificOutput: {
1936
+ hookEventName: "PreToolUse",
1937
+ permissionDecision: "defer",
1938
+ permissionDecisionReason: "Ork paused this node; it goes on from this call when it continues."
1939
+ } };
1940
+ const claudeHold = {
1941
+ answer: hookOutput,
1942
+ windowMs: HOLD_WINDOW_MS
1943
+ };
1944
+ /**
1945
+ * The `claude -p` arguments for a session spec. The brief goes on stdin;
1946
+ * `allowed` are the Ork tools that need no permission prompt.
1947
+ */
1948
+ const buildCliArgs = (spec, mcp, allowed) => {
1949
+ const args = [
1950
+ "-p",
1951
+ "--input-format",
1952
+ "stream-json",
1953
+ "--output-format",
1954
+ "stream-json",
1955
+ "--verbose",
1956
+ "--setting-sources",
1957
+ spec.settingSources.join(","),
1958
+ "--append-system-prompt",
1959
+ spec.systemAppend,
1960
+ EXCLUDE_DYNAMIC_SECTIONS,
1961
+ "--permission-mode",
1962
+ spec.permissionMode,
1963
+ "--json-schema",
1964
+ JSON.stringify(spec.reportSchema),
1965
+ "--mcp-config",
1966
+ JSON.stringify(mcpConfigOf(spec, mcp)),
1967
+ "--permission-prompt-tool",
1968
+ toolId("permission"),
1969
+ "--allowedTools",
1970
+ [...spec.tools.allow, ...allowed].join(",")
1971
+ ];
1972
+ if (spec.tools.only !== void 0) args.push("--tools", spec.tools.only.join(","));
1973
+ if (strictMcpOf(spec)) args.push("--strict-mcp-config");
1974
+ if (spec.agents !== void 0) args.push("--agents", JSON.stringify(spec.agents));
1975
+ if (spec.tools.deny.length > 0) args.push("--disallowedTools", spec.tools.deny.join(","));
1976
+ if (spec.model !== void 0) args.push("--model", spec.model);
1977
+ if (spec.effort !== void 0) args.push("--effort", spec.effort);
1978
+ if (spec.maxTurns !== void 0) args.push("--max-turns", String(spec.maxTurns));
1979
+ if (spec.budgetUsd !== void 0) args.push("--max-budget-usd", String(spec.budgetUsd));
1980
+ if (spec.resume !== void 0) args.push("--resume", spec.resume);
1981
+ if (spec.resume !== void 0 && spec.fork === true) args.push("--fork-session");
1982
+ for (const dir of spec.plugins ?? []) args.push("--plugin-dir", dir);
1983
+ const settings = settingsOf(spec, mcp);
1984
+ if (settings !== void 0) args.push("--settings", JSON.stringify(settings));
1985
+ return args;
1986
+ };
1987
+ /** `claude`, as the session runner's strategies. */
1988
+ const claudeCodeAdapter = {
1989
+ driver: "claude-code",
1990
+ command: {
1991
+ bin: "claude",
1992
+ args: (spec, leg) => withoutSecretFlags(buildCliArgs(spec, leg.endpoint, leg.allowed)),
1993
+ input: userLine
1994
+ },
1995
+ decode: decodeClaudeLine,
1996
+ legDecoder: claudeDecoder,
1997
+ mcp: secretFlags,
1998
+ permissions: {
1999
+ kind: "callback",
2000
+ denyByDefault: false
2001
+ },
2002
+ usage: claudeUsage,
2003
+ context: {
2004
+ instructions: ["CLAUDE.md", "AGENTS.md"],
2005
+ skills: ".claude/skills",
2006
+ agents: ".claude/agents",
2007
+ env: driverEnv
2008
+ },
2009
+ hold: claudeHold
2010
+ };
1914
2011
  // ../runtime/src/lease.ts
1915
2012
  /**
1916
2013
  * @file The `lease` kind of `McpWiring` (RFC-014): Command Code reads MCP
@@ -2250,9 +2347,12 @@ const commandCodeUsage = {
2250
2347
  * session outside the checkout: its `beforeToolCall` hook blocks every call
2251
2348
  * the node did not allow. Ork's own tools (`mcp__ork__*`) always go through.
2252
2349
  * Command Code's tool loader passes, and reads inside the worktree need no
2253
- * rule, as with Claude Code; one reaching outside it does. The rules are the
2254
- * node's, in Claude Code's words (`Bash(git:*)`, `Read`, `mcp__github`); a
2255
- * specifier Ork does not read never allows a call and always denies one.
2350
+ * rule, as with Claude Code; one reaching outside it (by its real path, links
2351
+ * followed) does. The rules are the node's, in Claude Code's words
2352
+ * (`Bash(git:*)`, `Read`, `mcp__github`); a specifier Ork does not read never
2353
+ * allows a call and always denies one. A shell line is checked command by
2354
+ * command: each must be allowed, and one with a substitution Ork can't read
2355
+ * is allowed only by a plain `Bash` rule.
2256
2356
  */
2257
2357
  /** The rules of a session: an `acceptEdits` node may edit without a rule. */
2258
2358
  const modRules = (spec) => ({
@@ -2266,19 +2366,38 @@ const modRules = (spec) => ({
2266
2366
  * export taking the mod API), so it carries its own matcher.
2267
2367
  */
2268
2368
  const modSource = (rules) => `// Ork's rules for one session; written by Ork, removed after it.
2269
- import { relative, resolve } from "node:path";
2369
+ import { realpathSync } from "node:fs";
2370
+ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
2270
2371
  const RULES = ${JSON.stringify(rules)};
2271
2372
  // Command Code's own: loading a deferred tool's schema, Ork's report included.
2272
2373
  const META = new Set(["search_tools", "tool_search"]);
2273
2374
  // Reads a session makes in its worktree without a rule, as Claude Code does.
2274
2375
  const READS = new Set(["read_file", "read_multiple_files", "read_directory", "glob", "grep"]);
2376
+ // A read's paths: every value of a path-like key, and any other string that
2377
+ // looks like a path out (absolute, from the home folder, or climbing with ..).
2378
+ const looksOut = text => isAbsolute(text) || text.startsWith("~") || text.split("/").includes("..");
2275
2379
  const pathsOf = input => Object.entries(input ?? {})
2276
- .filter(([key]) => /path|file|dir/i.test(key))
2277
- .flatMap(([, value]) => (Array.isArray(value) ? value : [value]))
2278
- .filter(value => typeof value === "string");
2380
+ .flatMap(([key, value]) => (Array.isArray(value) ? value : [value]).map(each => [key, each]))
2381
+ .filter(([key, value]) => typeof value === "string" && (/path|file|dir/i.test(key) || looksOut(value)))
2382
+ .map(([, value]) => value);
2383
+ // Where a path really is: the nearest folder of it that exists, with its links
2384
+ // followed, and the rest of the path on top.
2385
+ const real = at => {
2386
+ const rest = [];
2387
+ for (let here = at; ; here = dirname(here)) {
2388
+ try {
2389
+ return resolve(realpathSync(here), ...rest.reverse());
2390
+ } catch {
2391
+ if (dirname(here) === here) return at;
2392
+ rest.push(basename(here));
2393
+ }
2394
+ }
2395
+ };
2396
+ const ROOT = real(RULES.cwd);
2279
2397
  const inside = input => pathsOf(input).every(each => {
2280
- const from = relative(RULES.cwd, resolve(RULES.cwd, each));
2281
- return from === "" || (!from.startsWith("..") && !from.startsWith("/"));
2398
+ if (each.startsWith("~")) return false;
2399
+ const from = relative(ROOT, real(resolve(RULES.cwd, each)));
2400
+ return from === "" || (from !== ".." && !from.startsWith("../") && !isAbsolute(from));
2282
2401
  });
2283
2402
  const TOOLS = ${JSON.stringify(COMMAND_CODE_TOOLS)};
2284
2403
  // A rule's word, as Command Code reads it, covers these of Ork's tool names.
@@ -2290,6 +2409,80 @@ const split = rule => {
2290
2409
  ? { token: rule.trim() }
2291
2410
  : { token: rule.slice(0, at).trim(), spec: rule.slice(at + 1, -1) };
2292
2411
  };
2412
+ // The simple commands of a shell line, split at ; & | && || and line breaks
2413
+ // outside quotes. null when Ork can't read it: a command or process
2414
+ // substitution, an unclosed quote, or nothing at all.
2415
+ const TICK = String.fromCharCode(96);
2416
+ const BACKSLASH = String.fromCharCode(92);
2417
+ const shellParts = line => {
2418
+ const parts = [];
2419
+ let part = "";
2420
+ let quote = null;
2421
+ for (let i = 0; i < line.length; i += 1) {
2422
+ const ch = line[i];
2423
+ const next = line[i + 1];
2424
+ if (quote === "'") {
2425
+ part += ch;
2426
+ if (ch === "'") quote = null;
2427
+ continue;
2428
+ }
2429
+ if (ch === BACKSLASH) {
2430
+ part += ch + (next ?? "");
2431
+ i += 1;
2432
+ continue;
2433
+ }
2434
+ if (ch === TICK || (ch === "$" && next === "(")) return null;
2435
+ if (quote === '"') {
2436
+ part += ch;
2437
+ if (ch === '"') quote = null;
2438
+ continue;
2439
+ }
2440
+ if ((ch === "<" || ch === ">") && next === "(") return null;
2441
+ if (ch === "'" || ch === '"') {
2442
+ quote = ch;
2443
+ part += ch;
2444
+ continue;
2445
+ }
2446
+ // Writing a file is an edit, so output goes only to /dev/null or to
2447
+ // another stream (2>&1); any other target makes the line one Ork can't read.
2448
+ if (ch === ">") {
2449
+ let j = i + 1;
2450
+ if (line[j] === ">" || line[j] === "|") j += 1;
2451
+ if (line[j] === "&") {
2452
+ if (!/[0-9-]/.test(line[j + 1] ?? "")) return null;
2453
+ part += line.slice(i, j + 2);
2454
+ i = j + 1;
2455
+ continue;
2456
+ }
2457
+ while (line[j] === " " || line[j] === "\\t") j += 1;
2458
+ let k = j;
2459
+ while (k < line.length && !" \\t;&|<>\\n\\r".includes(line[k])) k += 1;
2460
+ if (line.slice(j, k) !== "/dev/null") return null;
2461
+ part += line.slice(i, k);
2462
+ i = k - 1;
2463
+ continue;
2464
+ }
2465
+ // 2>&1 and &> are redirections, not a command's end.
2466
+ const redirect = ch === "&" && (next === ">" || line[i - 1] === ">" || line[i - 1] === "<");
2467
+ if (!redirect && (ch === ";" || ch === "&" || ch === "|" || ch === "\\n" || ch === "\\r")) {
2468
+ parts.push(part);
2469
+ part = "";
2470
+ continue;
2471
+ }
2472
+ part += ch;
2473
+ }
2474
+ if (quote !== null) return null;
2475
+ parts.push(part);
2476
+ const commands = parts.map(norm).filter(each => each !== "");
2477
+ return commands.length === 0 ? null : commands;
2478
+ };
2479
+ // The calls a rule is checked against: a shell line's simple commands, each
2480
+ // alone; one Ork can't read has no command, so only a plain rule covers it.
2481
+ const callsOf = (name, input) => {
2482
+ if ((TOOLS[name] ?? name) !== "Bash" || typeof input?.command !== "string") return [input];
2483
+ const parts = shellParts(input.command);
2484
+ return parts === null ? [{}] : parts.map(command => ({ ...input, command }));
2485
+ };
2293
2486
  const shellMatches = (command, pattern) => {
2294
2487
  let p = norm(pattern);
2295
2488
  const c = norm(command);
@@ -2316,14 +2509,21 @@ const matches = (rule, name, input) => {
2316
2509
  if (ork === "Bash" && typeof input?.command === "string") return shellMatches(input.command, spec);
2317
2510
  return null;
2318
2511
  };
2512
+ // A deny covers a shell line when it covers any of its commands; an allow,
2513
+ // only when every command is covered by one rule or another.
2514
+ const anyDenied = (name, calls) =>
2515
+ calls.some(call => RULES.deny.some(rule => matches(rule, name, call) !== false));
2516
+ const allCovered = (rules, name, calls) =>
2517
+ calls.every(call => rules.some(rule => matches(rule, name, call) === true));
2319
2518
  export const decide = (name, input) => {
2320
- if (RULES.deny.some(rule => matches(rule, name, input) !== false)) return "denied for this node";
2519
+ const calls = callsOf(name, input);
2520
+ if (anyDenied(name, calls)) return "denied for this node";
2321
2521
  if (name.startsWith("mcp__ork__") || META.has(name)) return undefined;
2322
- if (RULES.only !== undefined && !RULES.only.some(rule => matches(rule, name, input) === true)) {
2522
+ if (RULES.only !== undefined && !allCovered(RULES.only, name, calls)) {
2323
2523
  return "not one of this node's tools";
2324
2524
  }
2325
2525
  if (READS.has(name) && inside(input)) return undefined;
2326
- if (RULES.allow.some(rule => matches(rule, name, input) === true)) return undefined;
2526
+ if (allCovered(RULES.allow, name, calls)) return undefined;
2327
2527
  return "not allowed for this node";
2328
2528
  };
2329
2529
  export default cc => {
@@ -2436,59 +2636,6 @@ const commandCodeAdapter = {
2436
2636
  env: () => ({})
2437
2637
  }
2438
2638
  };
2439
- // ../runtime/src/secret-file.ts
2440
- /**
2441
- * @file A session's private folder for files that hold a secret, such as a
2442
- * config carrying Ork's bearer token (RFC-014): a 0700 folder outside the
2443
- * checkout, under the system's temporary folder, named after the process that
2444
- * made it, with each file written at mode 0600. The session removes it when
2445
- * it ends; a folder whose process is gone (a crash) is removed when the next
2446
- * one is made. Nothing here logs what it writes.
2447
- */
2448
- /** Where sessions' private folders live unless a caller names another place. */
2449
- const secretRoot = () => path.join(tmpdir(), "ork-sessions");
2450
- /** Removes the folders under `root` whose process is gone; returns them. */
2451
- const sweepSecretFolders = (root) => {
2452
- let names;
2453
- try {
2454
- names = readdirSync(root);
2455
- } catch {
2456
- return [];
2457
- }
2458
- return names.flatMap((name) => {
2459
- const pid = Number(name.split("-")[0]);
2460
- if (!Number.isInteger(pid) || alive(pid)) return [];
2461
- const dir = path.join(root, name);
2462
- rmSync(dir, {
2463
- recursive: true,
2464
- force: true
2465
- });
2466
- return [dir];
2467
- });
2468
- };
2469
- /** A new private folder under `root`, after sweeping the stale ones. */
2470
- const makeSecretFolder = (root = secretRoot()) => {
2471
- mkdirSync(root, {
2472
- recursive: true,
2473
- mode: 448
2474
- });
2475
- sweepSecretFolders(root);
2476
- const dir = mkdtempSync(path.join(root, `${String(process.pid)}-`));
2477
- return {
2478
- dir,
2479
- write: (name, content) => {
2480
- const file = path.join(dir, name);
2481
- writeFileSync(file, content, { mode: 384 });
2482
- return file;
2483
- },
2484
- remove: () => {
2485
- rmSync(dir, {
2486
- recursive: true,
2487
- force: true
2488
- });
2489
- }
2490
- };
2491
- };
2492
2639
  /** Claude Code tool names, as OpenCode's permission actions. */
2493
2640
  const ACTIONS = {
2494
2641
  Read: "read",
@@ -2579,11 +2726,11 @@ const permissionRules = (spec) => {
2579
2726
  ...spec.permissionMode === "acceptEdits" ? [worktree("edit")] : [],
2580
2727
  ...rulesOf(tools.allow, "allow", cwd)
2581
2728
  ].filter((rule) => inOnly(rule)),
2582
- {
2583
- action: `ork_*`,
2729
+ ...ORK_TOOLS.map((tool) => ({
2730
+ action: mcpAction("ork", tool.name),
2584
2731
  resource: "*",
2585
2732
  effect: "allow"
2586
- },
2733
+ })),
2587
2734
  {
2588
2735
  action: "read",
2589
2736
  resource: "*.env",