clay-server 3.3.2-beta.3 → 3.4.0-beta.10

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 (77) hide show
  1. package/lib/daemon-projects.js +3 -3
  2. package/lib/daemon.js +1 -1
  3. package/lib/project-connection.js +20 -5
  4. package/lib/project-debate.js +4 -0
  5. package/lib/project-file-watch.js +77 -16
  6. package/lib/project-mate-interaction.js +21 -1
  7. package/lib/project-memory.js +3 -0
  8. package/lib/project-shell-command.js +160 -0
  9. package/lib/project-user-message.js +10 -0
  10. package/lib/project.js +19 -4
  11. package/lib/public/antigravity-avatar.png +0 -0
  12. package/lib/public/app.js +28 -0
  13. package/lib/public/copilot-avatar.svg +5 -0
  14. package/lib/public/css/command-palette.css +22 -2
  15. package/lib/public/css/icon-strip.css +29 -13
  16. package/lib/public/css/input.css +79 -0
  17. package/lib/public/css/mates.css +7 -0
  18. package/lib/public/css/menus.css +48 -14
  19. package/lib/public/css/messages.css +9 -0
  20. package/lib/public/css/pane.css +4 -0
  21. package/lib/public/css/pwa-mobile.css +17 -0
  22. package/lib/public/css/title-bar.css +19 -0
  23. package/lib/public/grok-avatar.svg +4 -0
  24. package/lib/public/index.html +43 -4
  25. package/lib/public/junie-avatar.svg +11 -0
  26. package/lib/public/kimi-avatar.svg +4 -0
  27. package/lib/public/modules/app-header.js +4 -0
  28. package/lib/public/modules/app-messages.js +28 -5
  29. package/lib/public/modules/app-panels.js +37 -5
  30. package/lib/public/modules/app-projects.js +3 -1
  31. package/lib/public/modules/app-rendering.js +22 -1
  32. package/lib/public/modules/input.js +58 -28
  33. package/lib/public/modules/mate-sidebar.js +17 -3
  34. package/lib/public/modules/notifications.js +7 -1
  35. package/lib/public/modules/pane-bridge.js +11 -0
  36. package/lib/public/modules/pane-links.js +23 -0
  37. package/lib/public/modules/project-switcher.js +7 -6
  38. package/lib/public/modules/shell-command.js +148 -0
  39. package/lib/public/modules/sidebar-mates.js +7 -1
  40. package/lib/public/modules/sidebar-mobile.js +2 -1
  41. package/lib/public/modules/sidebar-projects.js +34 -43
  42. package/lib/public/modules/sidebar-sessions.js +6 -6
  43. package/lib/public/modules/split-pair-ui.js +3 -2
  44. package/lib/public/modules/tools.js +12 -1
  45. package/lib/public/modules/vendor-priority.js +14 -0
  46. package/lib/public/modules/vendor-selection.js +20 -0
  47. package/lib/public/modules/worktree-location.js +17 -0
  48. package/lib/public/opencode-avatar.svg +4 -0
  49. package/lib/public/qwen-avatar.svg +4 -0
  50. package/lib/public/style.css +3 -2
  51. package/lib/sdk-bridge.js +59 -19
  52. package/lib/sdk-message-processor.js +10 -1
  53. package/lib/session-notes-mcp-server.js +7 -1
  54. package/lib/worktree.js +9 -4
  55. package/lib/ws-schema.js +3 -0
  56. package/lib/yoke/acp-agent-profiles.js +238 -0
  57. package/lib/yoke/acp-driver-runtime.js +50 -0
  58. package/lib/yoke/acp-event-normalizer.js +179 -0
  59. package/lib/yoke/acp-process-manager.js +264 -0
  60. package/lib/yoke/acp-query-handle.js +487 -0
  61. package/lib/yoke/adapters/acp.js +317 -0
  62. package/lib/yoke/adapters/antigravity.js +417 -0
  63. package/lib/yoke/adapters/codex.js +98 -3
  64. package/lib/yoke/adapters/copilot.js +7 -0
  65. package/lib/yoke/adapters/grok.js +7 -0
  66. package/lib/yoke/adapters/junie.js +7 -0
  67. package/lib/yoke/adapters/kimi.js +7 -0
  68. package/lib/yoke/adapters/kiro.js +4 -4
  69. package/lib/yoke/adapters/opencode.js +7 -0
  70. package/lib/yoke/adapters/qwen.js +7 -0
  71. package/lib/yoke/codex-app-server.js +25 -8
  72. package/lib/yoke/index.js +81 -13
  73. package/lib/yoke/instructions.js +3 -0
  74. package/lib/yoke/interface.js +2 -0
  75. package/lib/yoke/kiro-acp-server.js +30 -276
  76. package/lib/yoke/vendor-registry.js +77 -0
  77. package/package.json +1 -1
package/lib/sdk-bridge.js CHANGED
@@ -110,6 +110,7 @@ function createSDKBridge(opts) {
110
110
  var adapters = opts.adapters || {};
111
111
  var mateDisplayName = opts.mateDisplayName || "";
112
112
  var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
113
+ var osUsers = !!opts.osUsers;
113
114
  var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
114
115
  // mcpServers may be either a static object or a getter function. The
115
116
  // getter form lets callers gate individual servers at call time (e.g.
@@ -129,7 +130,7 @@ function createSDKBridge(opts) {
129
130
  var _cachedFreshAuthState = null;
130
131
  var _cachedFreshAuthAt = 0;
131
132
 
132
- function getKiroMcpServers(session) {
133
+ function getAcpMcpServers(session) {
133
134
  if (!session) return [];
134
135
  var bridgeArgs = [
135
136
  path.join(__dirname, "yoke", "mcp-bridge-server.js"),
@@ -323,6 +324,14 @@ function createSDKBridge(opts) {
323
324
  var mergeSkills = skills.mergeSkills;
324
325
 
325
326
  // --- Message processing (extracted to sdk-message-processor.js) ---
327
+ function getSessionLinuxUser(session) {
328
+ if (!osUsers || !session) return null;
329
+ if (session.lastLinuxUser) return session.lastLinuxUser;
330
+ if (!session.ownerId) return null;
331
+ var owner = usersModule.findUserById(session.ownerId);
332
+ return owner && owner.linuxUser ? owner.linuxUser : null;
333
+ }
334
+
326
335
  // Auto-generate a session title via YOKE adapter.generateTitle().
327
336
  // Triggered by sdk-message-processor after AUTO_TITLE_TURN_THRESHOLD turns.
328
337
  function autoGenerateTitle(session) {
@@ -345,7 +354,10 @@ function createSDKBridge(opts) {
345
354
  }
346
355
  console.log("[auto-title] Calling adapter.generateTitle with " + userMessages.length + " messages for session " + session.localId);
347
356
 
348
- sessionAdapter.generateTitle(userMessages, { cwd: cwd }).then(function(title) {
357
+ sessionAdapter.generateTitle(userMessages, {
358
+ cwd: cwd,
359
+ linuxUser: getSessionLinuxUser(session) || undefined,
360
+ }).then(function(title) {
349
361
  if (!title || title.length < 2) return;
350
362
  title = title.substring(0, 100);
351
363
  if (!session.titleManuallySet) {
@@ -1131,7 +1143,9 @@ function createSDKBridge(opts) {
1131
1143
  async function rollbackConversation(session, numTurns) {
1132
1144
  var sessionAdapter = getAdapterForSession(session);
1133
1145
  if (sessionAdapter && typeof sessionAdapter.rollbackThread === "function") {
1134
- await sessionAdapter.rollbackThread(session.cliSessionId, numTurns);
1146
+ await sessionAdapter.rollbackThread(session.cliSessionId, numTurns, {
1147
+ linuxUser: getSessionLinuxUser(session) || undefined,
1148
+ });
1135
1149
  }
1136
1150
  // Claude: conversation rollback is handled by rewindFiles + local history trim
1137
1151
  }
@@ -1143,7 +1157,11 @@ function createSDKBridge(opts) {
1143
1157
 
1144
1158
  async function forkSessionUnified(session, uuid) {
1145
1159
  var sessionAdapter = getAdapterForSession(session);
1146
- var result = await sessionAdapter.forkSession(session.cliSessionId, { upToMessageId: uuid, dir: cwd });
1160
+ var result = await sessionAdapter.forkSession(session.cliSessionId, {
1161
+ upToMessageId: uuid,
1162
+ dir: cwd,
1163
+ linuxUser: getSessionLinuxUser(session) || undefined,
1164
+ });
1147
1165
  if (!result || !result.sessionId) throw new Error("Fork returned no session id");
1148
1166
 
1149
1167
  // Adapters with rollbackThread (e.g. Codex) use local history copy
@@ -1471,6 +1489,7 @@ function createSDKBridge(opts) {
1471
1489
 
1472
1490
  var queryOpts = {
1473
1491
  cwd: cwd,
1492
+ linuxUser: linuxUser || undefined,
1474
1493
  // appendSystemPrompt, NOT systemPrompt: a plain-string systemPrompt
1475
1494
  // REPLACES the entire Claude Code system prompt (sdk.d.ts:2096), which
1476
1495
  // would strip the driver session down to the two-sentence pair preset.
@@ -1525,13 +1544,20 @@ function createSDKBridge(opts) {
1525
1544
  sandboxMode: codexConfig.sandbox,
1526
1545
  webSearchMode: codexConfig.webSearch,
1527
1546
  },
1547
+ ACP: {
1548
+ mcpServers: getAcpMcpServers(session),
1549
+ },
1550
+ ANTIGRAVITY: {
1551
+ dangerouslySkipPermissions: dangerouslySkipPermissions
1552
+ || session.permissionMode === "bypassPermissions",
1553
+ },
1528
1554
  KIRO: {
1529
1555
  engine: kiroConfig.engine,
1530
1556
  mode: kiroConfig.mode,
1531
1557
  // ACP accepts additional stdio MCP servers per session. The bridge is
1532
1558
  // bound to this Clay session so partner calls cannot leak across
1533
1559
  // concurrent Kiro sessions or users.
1534
- mcpServers: getKiroMcpServers(session),
1560
+ mcpServers: getAcpMcpServers(session),
1535
1561
  },
1536
1562
  },
1537
1563
  };
@@ -1787,6 +1813,23 @@ function createSDKBridge(opts) {
1787
1813
  } catch (e) {}
1788
1814
  if ((codexBin && fs.existsSync(codexBin)) || tryLookup(yoke.getVendorInfo("codex").binaryName)) result.push("codex");
1789
1815
 
1816
+ var subprocessVendorKeys = ["antigravity", "opencode", "kimi", "grok", "copilot", "qwen", "junie"];
1817
+ var acpProfiles = require("./yoke/acp-agent-profiles");
1818
+ for (var subprocessVendorIndex = 0; subprocessVendorIndex < subprocessVendorKeys.length; subprocessVendorIndex++) {
1819
+ var subprocessVendor = subprocessVendorKeys[subprocessVendorIndex];
1820
+ var subprocessInfo = yoke.getVendorInfo(subprocessVendor);
1821
+ if (!linuxUser || subprocessInfo.osUserIsolation) {
1822
+ if (subprocessVendor === "antigravity") {
1823
+ if (tryLookup(subprocessInfo.binaryName)) result.push(subprocessVendor);
1824
+ } else {
1825
+ var acpProfile = acpProfiles.getAcpAgentProfile(subprocessVendor);
1826
+ if (acpProfiles.findAcpAgentPath(acpProfile)) result.push(subprocessVendor);
1827
+ }
1828
+ } else {
1829
+ console.log("[sdk-bridge] " + subprocessInfo.displayName + " hidden for OS-isolated user " + linuxUser + ": per-user spawning is not implemented");
1830
+ }
1831
+ }
1832
+
1790
1833
  // Kiro has no per-user ACP spawn path yet. Do not advertise the daemon's
1791
1834
  // binary to an OS-isolated user, even if that user also has Kiro installed.
1792
1835
  var kiroInfo = yoke.getVendorInfo("kiro");
@@ -1816,6 +1859,13 @@ function createSDKBridge(opts) {
1816
1859
  var defaultVendor = adapter ? adapter.vendor : "claude";
1817
1860
  sm.defaultVendor = defaultVendor;
1818
1861
 
1862
+ // Installation detection is independent of adapter initialization. Publish
1863
+ // it before warmup so a new project does not render the initial empty state
1864
+ // as if every CLI were missing while the default adapter starts up.
1865
+ sm.installedVendors = detectInstalledVendors(linuxUser);
1866
+ sm.availableVendors = getAvailableVendors(linuxUser);
1867
+ sendModelInfoForVendor(defaultVendor, sm.currentModel || "");
1868
+
1819
1869
  // Initialize default adapter first (provides skills, slash commands, etc.)
1820
1870
  if (adapter) {
1821
1871
  try {
@@ -1867,19 +1917,8 @@ function createSDKBridge(opts) {
1867
1917
  // actually issues a query with it.
1868
1918
  sm.modelsByVendor = sm.modelsByVendor || {};
1869
1919
 
1870
- // Detect installed vendors per-user (binary existence check)
1871
- sm.installedVendors = detectInstalledVendors(linuxUser);
1872
- sm.availableVendors = getAvailableVendors(linuxUser);
1873
-
1874
- // Send initial state to client
1875
- send({
1876
- type: "model_info",
1877
- model: sm.currentModel || "",
1878
- models: getModelsForVendor(defaultVendor),
1879
- vendor: defaultVendor,
1880
- availableVendors: sm.availableVendors,
1881
- installedVendors: sm.installedVendors,
1882
- });
1920
+ // Send the fully initialized state to the client.
1921
+ sendModelInfoForVendor(defaultVendor, sm.currentModel || "");
1883
1922
  }
1884
1923
 
1885
1924
  async function setModel(session, model) {
@@ -1993,7 +2032,7 @@ function createSDKBridge(opts) {
1993
2032
  // Creates a mention session that can be reused across multiple mentions
1994
2033
  // within a conversation flow (session continuity).
1995
2034
  async function createMentionSession(opts) {
1996
- // opts: { vendor, claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
2035
+ // opts: { vendor, linuxUser, claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
1997
2036
  var abortController = new AbortController();
1998
2037
 
1999
2038
  // Current response callbacks (swapped on each pushMessage)
@@ -2013,6 +2052,7 @@ function createSDKBridge(opts) {
2013
2052
  try {
2014
2053
  handle = await mentionAdapter.createQuery({
2015
2054
  cwd: cwd,
2055
+ linuxUser: opts.linuxUser || undefined,
2016
2056
  systemPrompt: opts.claudeMd,
2017
2057
  model: opts.model || undefined,
2018
2058
  toolServers: opts.includeMcpServers ? (mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined) : undefined,
@@ -792,7 +792,16 @@ function attachMessageProcessor(ctx) {
792
792
  // processQueryStream can finish the turn when the iterator closes.
793
793
  var adapterErrorText = parsed.text || parsed.message || parsed.error || "Agent runtime error";
794
794
  session._lastAdapterError = adapterErrorText;
795
- sendAndRecord(session, { type: "error", text: adapterErrorText });
795
+ var isSessionWriterConflict = /thread-store conflict|already has an active writer/i.test(adapterErrorText);
796
+ if (isSessionWriterConflict) {
797
+ sendAndRecord(session, {
798
+ type: "session_writer_conflict",
799
+ vendor: session.vendor || "codex",
800
+ text: "This Codex session is already open in another Clay or Codex process. Stop the other server or close the other session, then try again.",
801
+ });
802
+ } else {
803
+ sendAndRecord(session, { type: "error", text: adapterErrorText });
804
+ }
796
805
 
797
806
  } else if (parsed.yokeType === "model_refusal") {
798
807
  // Model declined the request. "fallback" => the CLI retried on another
@@ -2,7 +2,13 @@
2
2
 
3
3
  var buildShape = require("./session-spawn-mcp-server").buildShape;
4
4
 
5
- var MEMORY_CONTRACT = "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. Good notes capture an unresolved commitment, durable product decision, user preference, constraint, or handoff that will materially change future work. Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. Updates are visible too: update only when durable state materially changes, and remove a note created by your session when it stops being useful instead of turning it into a completion log. Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
5
+ var MEMORY_CONTRACT =
6
+ "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. " +
7
+ "Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. " +
8
+ "Good notes capture an unresolved commitment, durable product decision, user preference, constraint, or handoff that will materially change future work. " +
9
+ "Important exception for deferred defects: while doing code or technical work, actively create a sticky note when you discover a concrete defect, regression risk, security issue, or data-loss risk that is outside the current session goal and will remain unfixed when the turn ends. Do not wait for the user to ask. Include the observable evidence, affected component, likely impact, and a clear next action. Check active notes first when a duplicate is plausible. Do not create defect notes for speculation, general cleanup ideas, or problems you fixed in the current session. " +
10
+ "Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. " +
11
+ "Updates are visible too: update only when durable state materially changes, and remove a note created by your session when it stops being useful instead of turning it into a completion log. Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
6
12
 
7
13
  function getToolDefs(handlers) {
8
14
  return [
package/lib/worktree.js CHANGED
@@ -43,9 +43,14 @@ function isWorktree(projectPath) {
43
43
  }
44
44
  }
45
45
 
46
+ function isPathInside(parentPath, candidatePath) {
47
+ var relative = path.relative(path.resolve(parentPath), path.resolve(candidatePath));
48
+ return relative !== "" && relative !== ".." && relative.indexOf(".." + path.sep) !== 0 && !path.isAbsolute(relative);
49
+ }
50
+
46
51
  // Scan worktrees for a given project path
47
- // Returns array of { path, branch, bare, detached, accessible }
48
- // accessible = true if worktree path is inside parentPath
52
+ // Returns array of { path, branch, bare, detached, external }
53
+ // external = true when Git registered the worktree outside the main project folder
49
54
  function scanWorktrees(projectPath) {
50
55
  var resolvedParent = path.resolve(projectPath);
51
56
  try {
@@ -63,7 +68,7 @@ function scanWorktrees(projectPath) {
63
68
  if (wt.bare) continue;
64
69
  var resolvedWt = path.resolve(wt.path);
65
70
  if (resolvedWt === resolvedParent) continue;
66
- wt.accessible = resolvedWt.indexOf(resolvedParent + path.sep) === 0;
71
+ wt.external = !isPathInside(resolvedParent, resolvedWt);
67
72
  wt.dirName = path.basename(wt.path);
68
73
  results.push(wt);
69
74
  }
@@ -131,4 +136,4 @@ function removeWorktree(projectPath, worktreeDirName) {
131
136
  }
132
137
  }
133
138
 
134
- module.exports = { scanWorktrees: scanWorktrees, createWorktree: createWorktree, removeWorktree: removeWorktree, isWorktree: isWorktree };
139
+ module.exports = { scanWorktrees: scanWorktrees, createWorktree: createWorktree, removeWorktree: removeWorktree, isWorktree: isWorktree, isPathInside: isPathInside };
package/lib/ws-schema.js CHANGED
@@ -145,6 +145,7 @@ var schema = {
145
145
  "kill_process": { direction: "c2s", handler: "lib/project-sessions.js", description: "Kill a system process by PID" },
146
146
  "process_killed": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Process was successfully killed" },
147
147
  "process_conflict": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Conflict: another process is using the session" },
148
+ "session_writer_conflict": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Codex session is already open in another process" },
148
149
 
149
150
  // -----------------------------------------------------------------------
150
151
  // Context / usage
@@ -353,6 +354,8 @@ var schema = {
353
354
  "term_closed": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Terminal session was closed" },
354
355
  "term_list": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full list of open terminals" },
355
356
  "term_error": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Terminal error (e.g. access denied)" },
357
+ "shell_command": { direction: "c2s", handler: "lib/project-shell-command.js", description: "Run a one-shot shell command for agent context" },
358
+ "shell_command_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "One-shot shell command output and exit status" },
356
359
 
357
360
  // -----------------------------------------------------------------------
358
361
  // Sticky notes
@@ -0,0 +1,238 @@
1
+ // ACP Agent Drivers
2
+ // -----------------
3
+ // Vendor-specific facts and optional hooks for agents that implement ACP.
4
+ // Static profiles are sufficient for standard agents; richer runtimes can add
5
+ // hooks without forcing YOKE down to ACP's least-common-denominator feature set.
6
+
7
+ var fs = require("fs");
8
+ var execFile = require("child_process").execFile;
9
+ var execFileSync = require("child_process").execFileSync;
10
+
11
+ function findOnPath(binaryName, overrideName) {
12
+ var overridePath = overrideName && process.env[overrideName];
13
+ if (overridePath && fs.existsSync(overridePath)) return overridePath;
14
+ try {
15
+ var command = process.platform === "win32" ? "where" : "which";
16
+ var out = execFileSync(command, [binaryName], {
17
+ timeout: 3000,
18
+ encoding: "utf8",
19
+ stdio: ["pipe", "pipe", "pipe"],
20
+ });
21
+ return out.trim().split(/\r?\n/)[0] || null;
22
+ } catch (e) {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function fetchOpenCodeModels(binaryPath, cwd) {
28
+ return new Promise(function(resolve) {
29
+ execFile(binaryPath, ["models"], {
30
+ cwd: cwd || process.cwd(),
31
+ timeout: 20000,
32
+ maxBuffer: 4 * 1024 * 1024,
33
+ }, function(err, stdout) {
34
+ if (err || !stdout) { resolve([]); return; }
35
+ var seen = {};
36
+ var models = [];
37
+ var lines = String(stdout).split(/\r?\n/);
38
+ for (var i = 0; i < lines.length; i++) {
39
+ var value = lines[i].trim();
40
+ if (!value || seen[value]) continue;
41
+ seen[value] = true;
42
+ models.push(value);
43
+ }
44
+ resolve(models);
45
+ });
46
+ });
47
+ }
48
+
49
+ function fetchOpenCodeResolvedConfig(binaryPath, cwd, env) {
50
+ return new Promise(function(resolve, reject) {
51
+ execFile(binaryPath, ["debug", "config"], {
52
+ cwd: cwd || process.cwd(),
53
+ env: Object.assign({}, process.env, env || {}),
54
+ timeout: 30000,
55
+ maxBuffer: 4 * 1024 * 1024,
56
+ }, function(err, stdout) {
57
+ if (err) { reject(err); return; }
58
+ try {
59
+ resolve(JSON.parse(String(stdout || "{}")));
60
+ } catch (e) {
61
+ reject(new Error("OpenCode returned invalid resolved configuration"));
62
+ }
63
+ });
64
+ });
65
+ }
66
+
67
+ function fetchOpenCodeAgentNames(binaryPath, cwd, env) {
68
+ return fetchOpenCodeResolvedConfig(binaryPath, cwd, env).then(function(config) {
69
+ return Object.keys(config.agent || {});
70
+ });
71
+ }
72
+
73
+ function isSafeOpenCodePermission(value) {
74
+ if (value === "ask" || value === "deny") return true;
75
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
76
+ var keys = Object.keys(value);
77
+ if (!keys.length) return false;
78
+ for (var i = 0; i < keys.length; i++) {
79
+ if (!isSafeOpenCodePermission(value[keys[i]])) return false;
80
+ }
81
+ return true;
82
+ }
83
+
84
+ function validateOpenCodeConfig(config) {
85
+ if (!config || config.permission !== "ask") {
86
+ throw new Error("OpenCode resolved configuration does not preserve global ask permissions");
87
+ }
88
+ var agents = config.agent || {};
89
+ var names = Object.keys(agents);
90
+ for (var i = 0; i < names.length; i++) {
91
+ var agent = agents[names[i]] || {};
92
+ if (agent.permission === undefined) continue;
93
+ if (!isSafeOpenCodePermission(agent.permission)) {
94
+ throw new Error("OpenCode agent has unsafe resolved permissions: " + names[i]);
95
+ }
96
+ }
97
+ }
98
+
99
+ function trustDefaultWhenModeIsNotExposed(ctx, next) {
100
+ var options = ctx.state && ctx.state.configOptions;
101
+ for (var i = 0; i < (options || []).length; i++) {
102
+ if (options[i].category === "mode" || options[i].id === "mode") return next();
103
+ }
104
+ if (ctx.state && ctx.state.modes) return next();
105
+ return Promise.resolve();
106
+ }
107
+
108
+ var ACP_AGENT_PROFILES = {
109
+ opencode: {
110
+ vendor: "opencode",
111
+ displayName: "OpenCode",
112
+ binaryName: "opencode",
113
+ overrideName: "OPENCODE_CLI_PATH",
114
+ args: ["acp"],
115
+ defaultModels: ["auto"],
116
+ defaultModel: "auto",
117
+ sessionResume: null,
118
+ fetchModels: fetchOpenCodeModels,
119
+ permissionModeGuaranteed: true,
120
+ prepare: function(ctx) {
121
+ var injected = ctx.initOpts && ctx.initOpts._openCodeAgentNames;
122
+ if (Array.isArray(injected)) {
123
+ ctx.driverState.agentNames = injected.slice();
124
+ return;
125
+ }
126
+ return fetchOpenCodeAgentNames(ctx.binaryPath, ctx.cwd, ctx.initOpts && ctx.initOpts.env).then(function(names) {
127
+ ctx.driverState.agentNames = names;
128
+ });
129
+ },
130
+ buildProcessOptions: function(ctx, base) {
131
+ var existing = (base.env && base.env.OPENCODE_CONFIG_CONTENT) || process.env.OPENCODE_CONFIG_CONTENT;
132
+ var config = {};
133
+ if (existing) config = JSON.parse(existing);
134
+ var agent = Object.assign({}, config.agent || {});
135
+ var names = ["build", "plan"].concat(ctx.driverState.agentNames || []);
136
+ for (var i = 0; i < names.length; i++) {
137
+ agent[names[i]] = Object.assign({}, agent[names[i]] || {}, { permission: "ask" });
138
+ }
139
+ base.env = Object.assign({}, base.env || {}, {
140
+ OPENCODE_CONFIG_CONTENT: JSON.stringify(Object.assign({}, config, { permission: "ask", agent: agent })),
141
+ });
142
+ return base;
143
+ },
144
+ validateProcessOptions: function(ctx) {
145
+ var injected = ctx.initOpts && ctx.initOpts._openCodeResolvedConfig;
146
+ var resolved = injected
147
+ ? Promise.resolve(injected)
148
+ : fetchOpenCodeResolvedConfig(ctx.binaryPath, ctx.cwd, ctx.processOptions.env);
149
+ return resolved.then(function(config) {
150
+ validateOpenCodeConfig(config);
151
+ });
152
+ },
153
+ ensureSafePermissionMode: function() {
154
+ // OpenCode ACP modes select agents, not approval policy. The process
155
+ // configuration above enforces ask at both global and effective-agent levels.
156
+ return Promise.resolve();
157
+ },
158
+ },
159
+ kimi: {
160
+ vendor: "kimi",
161
+ displayName: "Kimi Code",
162
+ binaryName: "kimi",
163
+ overrideName: "KIMI_CLI_PATH",
164
+ args: ["acp"],
165
+ defaultModels: ["auto"],
166
+ defaultModel: "auto",
167
+ sessionResume: null,
168
+ ensureSafePermissionMode: trustDefaultWhenModeIsNotExposed,
169
+ },
170
+ grok: {
171
+ vendor: "grok",
172
+ displayName: "Grok Build",
173
+ binaryName: "grok",
174
+ overrideName: "GROK_CLI_PATH",
175
+ args: ["--no-auto-update", "--permission-mode", "ask", "agent", "stdio"],
176
+ defaultModels: ["auto"],
177
+ defaultModel: "auto",
178
+ sessionResume: null,
179
+ permissionModeGuaranteed: true,
180
+ },
181
+ copilot: {
182
+ vendor: "copilot",
183
+ displayName: "GitHub Copilot CLI",
184
+ binaryName: "copilot",
185
+ overrideName: "COPILOT_CLI_PATH",
186
+ args: ["--acp"],
187
+ defaultModels: ["auto"],
188
+ defaultModel: "auto",
189
+ sessionResume: null,
190
+ ensureSafePermissionMode: trustDefaultWhenModeIsNotExposed,
191
+ },
192
+ qwen: {
193
+ vendor: "qwen",
194
+ displayName: "Qwen Code",
195
+ binaryName: "qwen",
196
+ overrideName: "QWEN_CLI_PATH",
197
+ args: ["--acp", "--approval-mode", "default"],
198
+ defaultModels: ["auto"],
199
+ defaultModel: "auto",
200
+ sessionResume: null,
201
+ permissionModeGuaranteed: true,
202
+ },
203
+ junie: {
204
+ vendor: "junie",
205
+ displayName: "Junie CLI",
206
+ binaryName: "junie",
207
+ overrideName: "JUNIE_CLI_PATH",
208
+ args: ["--acp", "true"],
209
+ defaultModels: ["auto"],
210
+ defaultModel: "auto",
211
+ sessionResume: null,
212
+ ensureSafePermissionMode: trustDefaultWhenModeIsNotExposed,
213
+ },
214
+ };
215
+
216
+ function getAcpAgentProfile(vendor) {
217
+ return ACP_AGENT_PROFILES[vendor] || null;
218
+ }
219
+
220
+ function getAcpAgentDriver(vendor) {
221
+ return getAcpAgentProfile(vendor);
222
+ }
223
+
224
+ function findAcpAgentPath(profile) {
225
+ if (!profile) return null;
226
+ return findOnPath(profile.binaryName, profile.overrideName);
227
+ }
228
+
229
+ module.exports = {
230
+ ACP_AGENT_PROFILES: ACP_AGENT_PROFILES,
231
+ getAcpAgentProfile: getAcpAgentProfile,
232
+ getAcpAgentDriver: getAcpAgentDriver,
233
+ findAcpAgentPath: findAcpAgentPath,
234
+ fetchOpenCodeModels: fetchOpenCodeModels,
235
+ fetchOpenCodeAgentNames: fetchOpenCodeAgentNames,
236
+ fetchOpenCodeResolvedConfig: fetchOpenCodeResolvedConfig,
237
+ validateOpenCodeConfig: validateOpenCodeConfig,
238
+ };
@@ -0,0 +1,50 @@
1
+ // ACP Vendor Driver Runtime
2
+ // -------------------------
3
+ // ACP supplies defaults. Trusted vendor drivers may extend or replace them so
4
+ // the shared protocol never becomes the ceiling of the richer YOKE contract.
5
+
6
+ function hasHook(driver, name) {
7
+ return !!(driver && typeof driver[name] === "function");
8
+ }
9
+
10
+ function call(driver, name, context, fallback) {
11
+ if (hasHook(driver, name)) return driver[name](context, fallback);
12
+ return fallback ? fallback() : undefined;
13
+ }
14
+
15
+ function callAsync(driver, name, context, fallback) {
16
+ try {
17
+ return Promise.resolve(call(driver, name, context, fallback));
18
+ } catch (e) {
19
+ return Promise.reject(e);
20
+ }
21
+ }
22
+
23
+ function mergeCapabilities(driver, context, base) {
24
+ var defaults = Object.assign({}, base);
25
+ if (!hasHook(driver, "extendCapabilities")) return defaults;
26
+ var extended = driver.extendCapabilities(context, Object.assign({}, defaults));
27
+ return Object.assign({}, defaults, extended || {});
28
+ }
29
+
30
+ function buildParams(driver, hookName, context, base) {
31
+ var defaults = Object.assign({}, base);
32
+ if (!hasHook(driver, hookName)) return defaults;
33
+ var result = driver[hookName](context, Object.assign({}, defaults));
34
+ return result === undefined || result === null ? defaults : result;
35
+ }
36
+
37
+ function normalizeEvents(driver, context, fallback) {
38
+ var result = call(driver, "normalizeUpdate", context, fallback);
39
+ if (result === undefined || result === null) return [];
40
+ return Array.isArray(result) ? result : [result];
41
+ }
42
+
43
+ module.exports = {
44
+ hasHook: hasHook,
45
+ call: call,
46
+ callAsync: callAsync,
47
+ mergeCapabilities: mergeCapabilities,
48
+ buildParams: buildParams,
49
+ normalizeEvents: normalizeEvents,
50
+ };