clay-server 3.3.2-beta.2 → 3.4.0-beta.1

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.
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="OpenCode">
2
+ <rect width="64" height="64" rx="16" fill="#111814"/>
3
+ <path d="m26 19-13 13 13 13M38 19l13 13-13 13" fill="none" stroke="#78e08f" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
4
+ </svg>
package/lib/sdk-bridge.js CHANGED
@@ -129,7 +129,7 @@ function createSDKBridge(opts) {
129
129
  var _cachedFreshAuthState = null;
130
130
  var _cachedFreshAuthAt = 0;
131
131
 
132
- function getKiroMcpServers(session) {
132
+ function getAcpMcpServers(session) {
133
133
  if (!session) return [];
134
134
  var bridgeArgs = [
135
135
  path.join(__dirname, "yoke", "mcp-bridge-server.js"),
@@ -1525,13 +1525,16 @@ function createSDKBridge(opts) {
1525
1525
  sandboxMode: codexConfig.sandbox,
1526
1526
  webSearchMode: codexConfig.webSearch,
1527
1527
  },
1528
+ ACP: {
1529
+ mcpServers: getAcpMcpServers(session),
1530
+ },
1528
1531
  KIRO: {
1529
1532
  engine: kiroConfig.engine,
1530
1533
  mode: kiroConfig.mode,
1531
1534
  // ACP accepts additional stdio MCP servers per session. The bridge is
1532
1535
  // bound to this Clay session so partner calls cannot leak across
1533
1536
  // concurrent Kiro sessions or users.
1534
- mcpServers: getKiroMcpServers(session),
1537
+ mcpServers: getAcpMcpServers(session),
1535
1538
  },
1536
1539
  },
1537
1540
  };
@@ -1787,6 +1790,19 @@ function createSDKBridge(opts) {
1787
1790
  } catch (e) {}
1788
1791
  if ((codexBin && fs.existsSync(codexBin)) || tryLookup(yoke.getVendorInfo("codex").binaryName)) result.push("codex");
1789
1792
 
1793
+ var acpVendorKeys = ["gemini", "opencode"];
1794
+ var acpProfiles = require("./yoke/acp-agent-profiles");
1795
+ for (var acpVendorIndex = 0; acpVendorIndex < acpVendorKeys.length; acpVendorIndex++) {
1796
+ var acpVendor = acpVendorKeys[acpVendorIndex];
1797
+ var acpInfo = yoke.getVendorInfo(acpVendor);
1798
+ if (!linuxUser || acpInfo.osUserIsolation) {
1799
+ var acpProfile = acpProfiles.getAcpAgentProfile(acpVendor);
1800
+ if (acpProfiles.findAcpAgentPath(acpProfile)) result.push(acpVendor);
1801
+ } else {
1802
+ console.log("[sdk-bridge] " + acpInfo.displayName + " hidden for OS-isolated user " + linuxUser + ": per-user spawning is not implemented");
1803
+ }
1804
+ }
1805
+
1790
1806
  // Kiro has no per-user ACP spawn path yet. Do not advertise the daemon's
1791
1807
  // binary to an OS-isolated user, even if that user also has Kiro installed.
1792
1808
  var kiroInfo = yoke.getVendorInfo("kiro");
@@ -0,0 +1,188 @@
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
+ var ACP_AGENT_PROFILES = {
100
+ gemini: {
101
+ vendor: "gemini",
102
+ displayName: "Gemini CLI",
103
+ binaryName: "gemini",
104
+ overrideName: "GEMINI_CLI_PATH",
105
+ args: ["--acp", "--approval-mode=default"],
106
+ defaultModels: ["auto"],
107
+ defaultModel: "auto",
108
+ // Gemini advertises loadSession, but releases through 0.46.0 could load
109
+ // the transcript without restoring the model's conversation memory.
110
+ // Keep resume gated until Clay's live contract test proves it reliable.
111
+ sessionResume: false,
112
+ permissionModeGuaranteed: true,
113
+ },
114
+ opencode: {
115
+ vendor: "opencode",
116
+ displayName: "OpenCode",
117
+ binaryName: "opencode",
118
+ overrideName: "OPENCODE_CLI_PATH",
119
+ args: ["acp"],
120
+ defaultModels: ["auto"],
121
+ defaultModel: "auto",
122
+ sessionResume: null,
123
+ fetchModels: fetchOpenCodeModels,
124
+ permissionModeGuaranteed: true,
125
+ prepare: function(ctx) {
126
+ var injected = ctx.initOpts && ctx.initOpts._openCodeAgentNames;
127
+ if (Array.isArray(injected)) {
128
+ ctx.driverState.agentNames = injected.slice();
129
+ return;
130
+ }
131
+ return fetchOpenCodeAgentNames(ctx.binaryPath, ctx.cwd, ctx.initOpts && ctx.initOpts.env).then(function(names) {
132
+ ctx.driverState.agentNames = names;
133
+ });
134
+ },
135
+ buildProcessOptions: function(ctx, base) {
136
+ var existing = (base.env && base.env.OPENCODE_CONFIG_CONTENT) || process.env.OPENCODE_CONFIG_CONTENT;
137
+ var config = {};
138
+ if (existing) config = JSON.parse(existing);
139
+ var agent = Object.assign({}, config.agent || {});
140
+ var names = ["build", "plan"].concat(ctx.driverState.agentNames || []);
141
+ for (var i = 0; i < names.length; i++) {
142
+ agent[names[i]] = Object.assign({}, agent[names[i]] || {}, { permission: "ask" });
143
+ }
144
+ base.env = Object.assign({}, base.env || {}, {
145
+ OPENCODE_CONFIG_CONTENT: JSON.stringify(Object.assign({}, config, { permission: "ask", agent: agent })),
146
+ });
147
+ return base;
148
+ },
149
+ validateProcessOptions: function(ctx) {
150
+ var injected = ctx.initOpts && ctx.initOpts._openCodeResolvedConfig;
151
+ var resolved = injected
152
+ ? Promise.resolve(injected)
153
+ : fetchOpenCodeResolvedConfig(ctx.binaryPath, ctx.cwd, ctx.processOptions.env);
154
+ return resolved.then(function(config) {
155
+ validateOpenCodeConfig(config);
156
+ });
157
+ },
158
+ ensureSafePermissionMode: function() {
159
+ // OpenCode ACP modes select agents, not approval policy. The process
160
+ // configuration above enforces ask at both global and effective-agent levels.
161
+ return Promise.resolve();
162
+ },
163
+ },
164
+ };
165
+
166
+ function getAcpAgentProfile(vendor) {
167
+ return ACP_AGENT_PROFILES[vendor] || null;
168
+ }
169
+
170
+ function getAcpAgentDriver(vendor) {
171
+ return getAcpAgentProfile(vendor);
172
+ }
173
+
174
+ function findAcpAgentPath(profile) {
175
+ if (!profile) return null;
176
+ return findOnPath(profile.binaryName, profile.overrideName);
177
+ }
178
+
179
+ module.exports = {
180
+ ACP_AGENT_PROFILES: ACP_AGENT_PROFILES,
181
+ getAcpAgentProfile: getAcpAgentProfile,
182
+ getAcpAgentDriver: getAcpAgentDriver,
183
+ findAcpAgentPath: findAcpAgentPath,
184
+ fetchOpenCodeModels: fetchOpenCodeModels,
185
+ fetchOpenCodeAgentNames: fetchOpenCodeAgentNames,
186
+ fetchOpenCodeResolvedConfig: fetchOpenCodeResolvedConfig,
187
+ validateOpenCodeConfig: validateOpenCodeConfig,
188
+ };
@@ -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
+ };
@@ -0,0 +1,179 @@
1
+ // ACP Event Normalizer
2
+ // --------------------
3
+ // Converts standard session/update payloads to stable YOKE events.
4
+
5
+ function toolNameForKind(kind, title) {
6
+ switch (kind) {
7
+ case "execute": return "Bash";
8
+ case "read": return "Read";
9
+ case "edit": return "Edit";
10
+ case "delete": return "Edit";
11
+ case "move": return "Edit";
12
+ case "search": return "Grep";
13
+ case "fetch": return "WebFetch";
14
+ case "think": return "Think";
15
+ default: return title || "Tool";
16
+ }
17
+ }
18
+
19
+ function createEventState(opts) {
20
+ opts = opts || {};
21
+ return {
22
+ vendor: opts.vendor || "acp",
23
+ blockCounter: 0,
24
+ textBlockOpen: false,
25
+ textBlockId: null,
26
+ thinkBlockOpen: false,
27
+ thinkBlockId: null,
28
+ toolBlocks: {},
29
+ toolMeta: {},
30
+ toolContent: {},
31
+ lastInputTokens: null,
32
+ contextWindow: opts.contextWindow || null,
33
+ configOptions: [],
34
+ };
35
+ }
36
+
37
+ function extractContent(content) {
38
+ if (!Array.isArray(content)) return "";
39
+ var parts = [];
40
+ for (var i = 0; i < content.length; i++) {
41
+ var item = content[i];
42
+ if (!item) continue;
43
+ if (item.type === "content" && item.content && typeof item.content.text === "string") {
44
+ parts.push(item.content.text);
45
+ } else if (item.type === "diff") {
46
+ parts.push((item.path ? "--- " + item.path + "\n" : "") + (item.newText || ""));
47
+ } else if (typeof item.text === "string") {
48
+ parts.push(item.text);
49
+ }
50
+ }
51
+ return parts.join("\n");
52
+ }
53
+
54
+ function finalToolContent(state, callId, update) {
55
+ if (callId && state.toolContent[callId]) return state.toolContent[callId];
56
+ var direct = extractContent(update.content);
57
+ if (direct) return direct;
58
+ if (typeof update.rawOutput === "string") return update.rawOutput;
59
+ if (update.rawOutput && typeof update.rawOutput.output === "string") return update.rawOutput.output;
60
+ return "";
61
+ }
62
+
63
+ function normalizeStatus(status) {
64
+ if (status === "in_progress" || status === "inProgress") return "in_progress";
65
+ if (status === "completed") return "completed";
66
+ return "pending";
67
+ }
68
+
69
+ function normalizeAcpUpdate(update, state) {
70
+ var events = [];
71
+ if (!update) return events;
72
+ var type = update.sessionUpdate;
73
+
74
+ if (type === "agent_message_chunk") {
75
+ var text = update.content && typeof update.content.text === "string" ? update.content.text : "";
76
+ if (!state.textBlockOpen) {
77
+ state.textBlockOpen = true;
78
+ state.textBlockId = "blk_" + (++state.blockCounter);
79
+ events.push({ yokeType: "text_start", blockId: state.textBlockId });
80
+ }
81
+ if (text) events.push({ yokeType: "text_delta", blockId: state.textBlockId, text: text });
82
+ return events;
83
+ }
84
+
85
+ if (type === "agent_thought_chunk") {
86
+ var thought = update.content && typeof update.content.text === "string" ? update.content.text : "";
87
+ if (!state.thinkBlockOpen) {
88
+ state.thinkBlockOpen = true;
89
+ state.thinkBlockId = "blk_" + (++state.blockCounter);
90
+ events.push({ yokeType: "thinking_start", blockId: state.thinkBlockId });
91
+ }
92
+ if (thought) events.push({ yokeType: "thinking_delta", blockId: state.thinkBlockId, text: thought });
93
+ return events;
94
+ }
95
+
96
+ if (type === "tool_call" || type === "tool_call_update") {
97
+ var callId = update.toolCallId;
98
+ var name = toolNameForKind(update.kind, update.title);
99
+ if (callId && update.kind) {
100
+ state.toolMeta[callId] = { kind: update.kind, title: update.title, rawInput: update.rawInput || {} };
101
+ }
102
+ if (callId && !state.toolBlocks[callId]) {
103
+ state.toolBlocks[callId] = "blk_" + (++state.blockCounter);
104
+ events.push({ yokeType: "tool_start", blockId: state.toolBlocks[callId], toolId: callId, toolName: name });
105
+ events.push({ yokeType: "tool_executing", blockId: state.toolBlocks[callId], toolId: callId, toolName: name, input: update.rawInput || {} });
106
+ }
107
+ var chunk = extractContent(update.content);
108
+ if (callId && chunk) state.toolContent[callId] = (state.toolContent[callId] || "") + chunk;
109
+ if (update.status === "completed" || update.status === "failed") {
110
+ events.push({
111
+ yokeType: "tool_result",
112
+ blockId: state.toolBlocks[callId],
113
+ toolId: callId,
114
+ content: finalToolContent(state, callId, update),
115
+ isError: update.status === "failed",
116
+ });
117
+ }
118
+ return events;
119
+ }
120
+
121
+ if (type === "plan") {
122
+ var entries = Array.isArray(update.entries) ? update.entries : [];
123
+ events.push({
124
+ yokeType: "plan_updated",
125
+ title: "Plan",
126
+ explanation: "",
127
+ plan: entries.map(function(entry) {
128
+ return { step: entry.content || "", status: normalizeStatus(entry.status) };
129
+ }),
130
+ });
131
+ return events;
132
+ }
133
+
134
+ if (type === "usage_update") {
135
+ if (typeof update.used === "number") state.lastInputTokens = update.used;
136
+ if (typeof update.size === "number") state.contextWindow = update.size;
137
+ return events;
138
+ }
139
+
140
+ if (type === "config_option_update") {
141
+ state.configOptions = Array.isArray(update.configOptions) ? update.configOptions : [];
142
+ return events;
143
+ }
144
+
145
+ events.push({
146
+ yokeType: "runtime_specific",
147
+ vendor: state.vendor,
148
+ eventType: "session/update:" + type,
149
+ raw: update,
150
+ });
151
+ return events;
152
+ }
153
+
154
+ function closeOpenBlocks(state) {
155
+ var events = [];
156
+ if (state.thinkBlockOpen) {
157
+ events.push({ yokeType: "thinking_stop", blockId: state.thinkBlockId });
158
+ state.thinkBlockOpen = false;
159
+ }
160
+ return events;
161
+ }
162
+
163
+ function resetTurnState(state) {
164
+ state.textBlockOpen = false;
165
+ state.textBlockId = null;
166
+ state.thinkBlockOpen = false;
167
+ state.thinkBlockId = null;
168
+ state.toolBlocks = {};
169
+ state.toolMeta = {};
170
+ state.toolContent = {};
171
+ }
172
+
173
+ module.exports = {
174
+ createEventState: createEventState,
175
+ normalizeAcpUpdate: normalizeAcpUpdate,
176
+ closeOpenBlocks: closeOpenBlocks,
177
+ resetTurnState: resetTurnState,
178
+ toolNameForKind: toolNameForKind,
179
+ };