clay-server 4.7.1-beta.5 → 4.8.0-beta.2

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 (55) hide show
  1. package/bin/login-browser-open.js +7 -0
  2. package/lib/bundled-skills/clay-agent-browser/SKILL.md +36 -0
  3. package/lib/clay-skill-install.js +33 -0
  4. package/lib/clay-skill-storage.js +49 -0
  5. package/lib/project-http.js +14 -5
  6. package/lib/project-shared-browser.js +256 -0
  7. package/lib/project-vendor-login.js +52 -18
  8. package/lib/project.js +38 -1
  9. package/lib/public/app.js +8 -0
  10. package/lib/public/css/filebrowser.css +2 -25
  11. package/lib/public/css/issues.css +1 -14
  12. package/lib/public/css/mcp-skills-workbench.css +51 -0
  13. package/lib/public/css/project-logs.css +0 -11
  14. package/lib/public/css/right-workbench.css +28 -0
  15. package/lib/public/css/scheduled-tasks.css +1 -1
  16. package/lib/public/css/shared-browser-skins.css +28 -0
  17. package/lib/public/css/shared-browser.css +91 -0
  18. package/lib/public/css/sticky-notes.css +0 -11
  19. package/lib/public/css/vendor-login-browser.css +63 -0
  20. package/lib/public/index.html +30 -22
  21. package/lib/public/modules/app-messages.js +7 -0
  22. package/lib/public/modules/app-notifications.js +1 -1
  23. package/lib/public/modules/extension-settings.js +110 -0
  24. package/lib/public/modules/mcp-skills-workbench.js +67 -0
  25. package/lib/public/modules/mcp-ui.js +9 -12
  26. package/lib/public/modules/notifications.js +2 -107
  27. package/lib/public/modules/shared-browser-activity.js +66 -0
  28. package/lib/public/modules/shared-browser-input.js +73 -0
  29. package/lib/public/modules/shared-browser-tabs.js +33 -0
  30. package/lib/public/modules/shared-browser-viewport-menu.js +76 -0
  31. package/lib/public/modules/shared-browser.js +214 -0
  32. package/lib/public/modules/sidebar-mobile.js +3 -3
  33. package/lib/public/modules/skills-actions.js +247 -0
  34. package/lib/public/modules/skills-detail.js +148 -0
  35. package/lib/public/modules/skills-state.js +10 -0
  36. package/lib/public/modules/skills.js +64 -459
  37. package/lib/public/modules/tool-palette-order.js +11 -13
  38. package/lib/public/modules/vendor-login-browser.js +243 -0
  39. package/lib/public/modules/vendor-login.js +23 -4
  40. package/lib/public/style.css +7 -0
  41. package/lib/sdk-bridge.js +14 -1
  42. package/lib/sessions.js +2 -0
  43. package/lib/shared-browser-activity-history.js +17 -0
  44. package/lib/shared-browser-policy.js +26 -0
  45. package/lib/shared-browser-runtime.js +82 -0
  46. package/lib/shared-browser-tools.js +21 -0
  47. package/lib/shared-browser-worker.js +160 -0
  48. package/lib/terminal.js +4 -0
  49. package/lib/vendor-login-browser-flow.js +46 -0
  50. package/lib/vendor-login-browser-passkeys.js +27 -0
  51. package/lib/vendor-login-browser-worker.js +149 -0
  52. package/lib/vendor-login-browser.js +162 -0
  53. package/lib/ws-schema.js +14 -0
  54. package/lib/yoke/skill-discovery.js +22 -2
  55. package/package.json +2 -1
@@ -0,0 +1,160 @@
1
+ // Isolated browser process. Its OS identity matches the owning Driver session.
2
+ var chromium = require("playwright").chromium;
3
+ var policy = require("./shared-browser-policy");
4
+ var browser = null;
5
+ var context = null;
6
+ var page = null;
7
+ var closing = false;
8
+ var viewing = false;
9
+ var epoch = 0;
10
+ var queue = Promise.resolve();
11
+ var pending = 0;
12
+ var timer = null;
13
+ var activeActivity = null;
14
+ var pointerSequence = 0;
15
+ var viewport = { width: 1280, height: 800 };
16
+ function emit(message) {
17
+ if (!closing && process.stdout.writableLength < 3 * 1024 * 1024) process.stdout.write(JSON.stringify(message) + "\n");
18
+ }
19
+ function state() {
20
+ return { url: page ? policy.visibleUrl(page.url()) : "", width: viewport.width, height: viewport.height };
21
+ }
22
+ async function screenshot() {
23
+ return (await page.screenshot({ type: "jpeg", quality: 70, caret: "initial", timeout: 5000 })).toString("base64");
24
+ }
25
+ async function capture() {
26
+ if (closing) return;
27
+ try {
28
+ if (viewing && page && !page.isClosed()) {
29
+ var current = page;
30
+ var data = await screenshot();
31
+ if (current === page) emit(Object.assign({ type: "frame", data: data }, state()));
32
+ }
33
+ } catch (e) {}
34
+ if (!closing) timer = setTimeout(capture, 250);
35
+ }
36
+ async function start() {
37
+ var channels = ["chrome", "msedge", null];
38
+ for (var i = 0; i < channels.length; i++) {
39
+ try { browser = await chromium.launch({ channel: channels[i] || undefined, headless: true, chromiumSandbox: true, timeout: 15000 }); break; }
40
+ catch (e) { if (closing) return; }
41
+ }
42
+ if (!browser) throw new Error("Install Chrome or run npx playwright install chromium on the Clay host. Linux also needs browser libraries and sandbox support.");
43
+ if (closing) { await browser.close(); return; }
44
+ var cdp = await browser.newBrowserCDPSession();
45
+ var version = await cdp.send("Browser.getVersion");
46
+ await cdp.detach();
47
+ context = await browser.newContext({ viewport: viewport, acceptDownloads: false, userAgent: version.userAgent.replace("HeadlessChrome/", "Chrome/") });
48
+ await context.addInitScript(require("./vendor-login-browser-passkeys").disableLoginPasskeys);
49
+ await context.route("**/*", function (route) {
50
+ var protocol = new URL(route.request().url()).protocol;
51
+ return ["http:", "https:"].includes(protocol) ? route.continue() : route.abort();
52
+ });
53
+ context.on("page", function (opened) {
54
+ page = opened;
55
+ opened.setDefaultTimeout(10000);
56
+ opened.on("dialog", function (dialog) { dialog.dismiss().catch(function () {}); });
57
+ opened.on("download", function (download) { download.cancel().catch(function () {}); });
58
+ opened.on("framenavigated", function (frame) { if (frame === opened.mainFrame() && page === opened) emit(Object.assign({ type: "state" }, state())); });
59
+ opened.on("close", function () { var pages = context.pages(); page = pages.length ? pages[pages.length - 1] : null; emit(Object.assign({ type: "state" }, state())); });
60
+ });
61
+ page = await context.newPage();
62
+ emit(Object.assign({ type: "ready" }, state()));
63
+ capture();
64
+ }
65
+ function reportActivity(phase) {
66
+ if (activeActivity) emit({ type: "activity", epoch: activeActivity.epoch, generation: activeActivity.generation,
67
+ activity: { id: activeActivity.id, text: activeActivity.text, phase: phase } });
68
+ }
69
+ function reportPointer(x, y, clicked) {
70
+ if (activeActivity) emit({ type: "pointer", epoch: activeActivity.epoch, generation: activeActivity.generation,
71
+ pointer: { x: x, y: y, width: viewport.width, height: viewport.height, clicked: !!clicked, sequence: ++pointerSequence } });
72
+ }
73
+ function activityText(message) {
74
+ var defaults = { inspect: "I'll take a look at the page to see what's there.", navigate: "I'll open this page so we can take a look.", back: "I'll go back to the previous page.", forward: "I'll move forward to the next page.", reload: "I'll refresh the page to see its latest state.", click: "I'll click this control and see what happens.", text: "I'll enter the text in this field.", select: "I'll choose this option in the form.", key: "I'll use the keyboard for this step.", wheel: "I'll scroll a little to see more of the page.", resize: "I'll change the screen size to check the layout." };
75
+ return message.activity.intent || defaults[message.action === "inspect" ? "inspect" : message.event.kind] || "I'll work through the next step in the browser.";
76
+ }
77
+ async function action(event) {
78
+ if (!page || page.isClosed()) page = await context.newPage();
79
+ event = policy.validateAction(event);
80
+ if (event.kind === "navigate") await page.goto(event.url, { waitUntil: "domcontentloaded", timeout: 20000 });
81
+ else if (event.kind === "back") await page.goBack({ waitUntil: "domcontentloaded", timeout: 20000 });
82
+ else if (event.kind === "forward") await page.goForward({ waitUntil: "domcontentloaded", timeout: 20000 });
83
+ else if (event.kind === "reload") await page.reload({ waitUntil: "domcontentloaded", timeout: 20000 });
84
+ else if (event.kind === "click" && event.selector) {
85
+ var target = page.locator(event.selector);
86
+ await target.click({ trial: true });
87
+ var box = await target.boundingBox();
88
+ if (box) {
89
+ var x = Math.max(0, Math.min(viewport.width - 1, box.x + box.width / 2));
90
+ var y = Math.max(0, Math.min(viewport.height - 1, box.y + box.height / 2));
91
+ await page.mouse.move(x, y);
92
+ reportPointer(x, y, true);
93
+ await target.click({ position: { x: x - box.x, y: y - box.y } });
94
+ } else await target.click();
95
+ }
96
+ else if (["move", "down", "up", "click"].includes(event.kind)) {
97
+ await page.mouse.move(Math.max(0, Math.min(viewport.width - 1, event.x)), Math.max(0, Math.min(viewport.height - 1, event.y)));
98
+ reportPointer(Math.max(0, Math.min(viewport.width - 1, event.x)), Math.max(0, Math.min(viewport.height - 1, event.y)), event.kind === "click" || event.kind === "down");
99
+ if (event.kind === "down") await page.mouse.down();
100
+ if (event.kind === "up") await page.mouse.up();
101
+ if (event.kind === "click") await page.mouse.click(event.x, event.y);
102
+ } else if (event.kind === "text") {
103
+ if (event.selector) await page.locator(event.selector).fill(event.text);
104
+ else await page.keyboard.insertText(event.text);
105
+ } else if (event.kind === "select") await page.locator(event.selector).selectOption(event.text);
106
+ else if (event.kind === "key") await page.keyboard.press(event.key);
107
+ else if (event.kind === "wheel") await page.mouse.wheel(0, Math.max(-2000, Math.min(2000, event.deltaY)));
108
+ else if (event.kind === "resize") {
109
+ viewport = { width: Math.round(event.width), height: Math.round(event.height) };
110
+ await page.setViewportSize(viewport);
111
+ }
112
+ return state();
113
+ }
114
+ async function command(message) {
115
+ if (message.epoch !== epoch) throw new Error("Browser control changed; this action was cancelled");
116
+ activeActivity = message.activity ? { id: message.id, epoch: message.epoch, generation: message.activity.generation, text: activityText(message) } : null;
117
+ if (message.event && ["navigate", "back", "forward", "reload", "resize"].includes(message.event.kind) && activeActivity) {
118
+ emit({ type: "pointer", epoch: message.epoch, generation: activeActivity.generation, pointer: null });
119
+ }
120
+ reportActivity("running");
121
+ if (message.action === "inspect") {
122
+ if (!page || page.isClosed()) throw new Error("Open a page first");
123
+ return Object.assign(state(), { snapshot: (await page.locator("body").ariaSnapshot()).slice(0, 24000), image: await screenshot() });
124
+ }
125
+ return action(message.event);
126
+ }
127
+ async function close() {
128
+ if (closing) return;
129
+ closing = true;
130
+ clearTimeout(timer);
131
+ if (browser) await browser.close().catch(function () {});
132
+ process.exit(0);
133
+ }
134
+ var lines = require("readline").createInterface({ input: process.stdin });
135
+ lines.on("line", function (line) {
136
+ if (line.length > 30000 || closing) return;
137
+ var msg;
138
+ try { msg = JSON.parse(line); } catch (e) { return; }
139
+ if (msg.type === "start") {
140
+ if (!browser) queue = queue.then(start).catch(function (e) { emit({ type: "error", error: e.message }); });
141
+ } else if (msg.type === "viewing") viewing = !!msg.value;
142
+ else if (msg.type === "control") {
143
+ epoch = msg.epoch;
144
+ queue = queue.then(async function () {
145
+ if (page) await page.mouse.up().catch(function () {});
146
+ emit({ type: "result", id: msg.id, value: { epoch: msg.epoch } });
147
+ });
148
+ } else if (msg.type === "command") {
149
+ if (pending >= 80) { emit({ type: "result", id: msg.id, error: "Browser is busy; retry shortly" }); return; }
150
+ pending++;
151
+ queue = queue.then(function () { return command(msg); }).then(function (value) {
152
+ reportActivity("complete"); activeActivity = null;
153
+ emit({ type: "result", id: msg.id, value: value });
154
+ emit(Object.assign({ type: "state" }, state()));
155
+ }, function (e) { reportActivity("failed"); activeActivity = null; emit({ type: "result", id: msg.id, error: e.message }); }).finally(function () { pending--; });
156
+ }
157
+ });
158
+ lines.on("close", close);
159
+ process.on("SIGTERM", close);
160
+ process.on("SIGINT", close);
package/lib/terminal.js CHANGED
@@ -25,6 +25,10 @@ function createTerminal(cwd, cols, rows, osUserInfo, opts) {
25
25
 
26
26
  // Build a minimal, isolated environment (no daemon env leakage)
27
27
  var termEnv = buildUserEnv(osUserInfo);
28
+ if (opts && opts.loginBrowserEnv) {
29
+ termEnv.BROWSER = opts.loginBrowserEnv.BROWSER;
30
+ termEnv.CLAY_LOGIN_URL_FILE = opts.loginBrowserEnv.CLAY_LOGIN_URL_FILE;
31
+ }
28
32
  var spawnOpts = {
29
33
  name: "xterm-256color",
30
34
  cols: cols || 80,
@@ -0,0 +1,46 @@
1
+ var createLoginBrowser = require("./vendor-login-browser").createLoginBrowser;
2
+
3
+ function attachBrowserLogin(ctx) {
4
+ function allowed(ws, flow) {
5
+ var userId = ws && ws._clayUser ? String(ws._clayUser.id) : null;
6
+ return userId === flow.ownerId && ctx.hasTerminalPermission(ws);
7
+ }
8
+ function prepare(ws, flow, identity) {
9
+ if (ctx.osUsers && !identity.osUserInfo) throw new Error("OS user identity is unavailable");
10
+ flow.ownerId = ws && ws._clayUser ? String(ws._clayUser.id) : null;
11
+ flow.browser = (ctx.createLoginBrowser || createLoginBrowser)({
12
+ ws: ws, vendor: flow.vendor, osUserInfo: identity.osUserInfo,
13
+ sendTo: ctx.sendTo,
14
+ canAccess: function (peer) { return allowed(peer, flow); },
15
+ });
16
+ flow.expiryTimer = setTimeout(function () { ctx.cancel(flow.key); }, 15 * 60 * 1000);
17
+ flow.expiryTimer.unref();
18
+ }
19
+ function handle(ws, msg, flows) {
20
+ var flow = flows[ctx.flowKey(ws, String(msg.vendor || "claude"))];
21
+ if (msg.type === "vendor_login_cancel" && msg.browserId && (!flow || !flow.browser)) return true;
22
+ if (typeof msg.type === "string" && msg.type.indexOf("term_") === 0) {
23
+ var vendors = Object.keys(flows);
24
+ for (var i = 0; i < vendors.length; i++) {
25
+ var candidate = flows[vendors[i]];
26
+ if (candidate.terminalId === msg.id && !allowed(ws, candidate)) return true;
27
+ }
28
+ }
29
+ if (msg.type === "vendor_login_browser_attach" || msg.type === "vendor_login_browser_input") {
30
+ if (!flow || !flow.browser || flow.browser.id !== msg.browserId || !allowed(ws, flow)) return true;
31
+ if (msg.type === "vendor_login_browser_attach") flow.browser.attach(ws);
32
+ else flow.browser.input(ws, msg.event);
33
+ return true;
34
+ }
35
+ if (flow && flow.browser && (msg.type === "vendor_login_start" || msg.type === "vendor_login_cancel")) {
36
+ if (!allowed(ws, flow) || (msg.type === "vendor_login_cancel" && msg.browserId !== flow.browser.id)) {
37
+ ctx.sendTo(ws, { type: "vendor_login_error", vendor: msg.vendor, error: "This sign-in belongs to another request. Reopen sign-in to continue." });
38
+ return true;
39
+ }
40
+ }
41
+ return false;
42
+ }
43
+ return { prepare: prepare, handle: handle };
44
+ }
45
+
46
+ module.exports = { attachBrowserLogin: attachBrowserLogin };
@@ -0,0 +1,27 @@
1
+ // Runs before page scripts in each remote sign-in page, frame, and popup.
2
+ function disableLoginPasskeys() {
3
+ var credentials = window.CredentialsContainer && window.CredentialsContainer.prototype;
4
+ if (credentials) {
5
+ ["get", "create"].forEach(function (method) {
6
+ var original = credentials[method];
7
+ if (typeof original !== "function") return;
8
+ Object.defineProperty(credentials, method, {
9
+ configurable: true,
10
+ writable: true,
11
+ value: function (options) {
12
+ if (options && "publicKey" in Object(options)) {
13
+ return Promise.reject(new DOMException(
14
+ "Passkeys are unavailable in this remote sign-in browser. Choose another sign-in method.",
15
+ "NotSupportedError"
16
+ ));
17
+ }
18
+ return original.apply(this, arguments);
19
+ },
20
+ });
21
+ });
22
+ }
23
+ // Feature detection should choose password/email instead of host-native authenticator UI.
24
+ Object.defineProperty(window, "PublicKeyCredential", { configurable: true, writable: true, value: undefined });
25
+ }
26
+
27
+ module.exports = { disableLoginPasskeys: disableLoginPasskeys };
@@ -0,0 +1,149 @@
1
+ // Runs as the same OS identity as the login CLI. Only the human supplies input.
2
+ var readline = require("readline");
3
+ var spawn = require("child_process").spawn;
4
+ var chromium = require("playwright").chromium;
5
+ var browser = null;
6
+ var page = null;
7
+ var installing = null;
8
+ var viewing = true;
9
+ var closing = false;
10
+ var captureTimer = null;
11
+ var inputQueue = Promise.resolve();
12
+ var queued = 0;
13
+ var viewport = { width: 1000, height: 720 };
14
+
15
+ function emit(message) {
16
+ if (!closing && process.stdout.writableLength < 1024 * 1024) process.stdout.write(JSON.stringify(message) + "\n");
17
+ }
18
+
19
+ function status(text, phase) { emit({ type: "status", text: text, phase: phase || "loading" }); }
20
+
21
+ async function launch() {
22
+ var channels = ["chrome", "msedge", null];
23
+ for (var i = 0; i < channels.length; i++) {
24
+ try {
25
+ return await chromium.launch({ channel: channels[i] || undefined, headless: true, chromiumSandbox: true, timeout: 20000 });
26
+ } catch (e) {
27
+ if (closing) throw e;
28
+ }
29
+ }
30
+ status("Preparing the sign-in browser. The first download may take a few minutes…");
31
+ await new Promise(function (resolve, reject) {
32
+ installing = spawn(process.execPath, [require("path").join(require.resolve("playwright/package.json"), "../cli.js"), "install", "chromium"], { stdio: "ignore", env: process.env });
33
+ var timer = setTimeout(function () { installing.kill(); reject(new Error("Install timed out")); }, 180000);
34
+ installing.once("error", function (err) { clearTimeout(timer); reject(err); });
35
+ installing.once("exit", function (code) {
36
+ clearTimeout(timer);
37
+ installing = null;
38
+ if (code === 0) resolve(); else reject(new Error("Browser installation failed"));
39
+ });
40
+ });
41
+ if (closing) throw new Error("Closed");
42
+ return chromium.launch({ headless: true, chromiumSandbox: true, timeout: 20000 });
43
+ }
44
+
45
+ async function capture() {
46
+ if (closing) return;
47
+ try {
48
+ if (page && !page.isClosed() && viewing && process.stdout.writableLength < 262144) {
49
+ var current = page;
50
+ var data = await current.screenshot({ type: "jpeg", quality: 65, caret: "initial", timeout: 3000 });
51
+ if (current === page) emit({ type: "frame", data: data.toString("base64"), origin: new URL(current.url()).origin,
52
+ width: current.viewportSize().width, height: current.viewportSize().height });
53
+ }
54
+ } catch (e) {}
55
+ if (!closing) captureTimer = setTimeout(capture, 250);
56
+ }
57
+
58
+ async function start(url) {
59
+ browser = await launch();
60
+ if (closing) { await browser.close(); return; }
61
+ var session = await browser.newBrowserCDPSession();
62
+ var version;
63
+ try { version = await session.send("Browser.getVersion"); }
64
+ finally { await session.detach(); }
65
+ // Preserve the native version/platform and replace only the headless product token.
66
+ var userAgent = version.userAgent.replace("HeadlessChrome/", "Chrome/");
67
+ var context = await browser.newContext({ viewport: viewport, acceptDownloads: false, userAgent: userAgent });
68
+ await context.addInitScript(require("./vendor-login-browser-passkeys").disableLoginPasskeys);
69
+ var callback = new URL(new URL(url).searchParams.get("redirect_uri"));
70
+ await context.route("**/*", function (route) {
71
+ var target = new URL(route.request().url());
72
+ if (target.protocol === "https:" || (target.origin === callback.origin && target.pathname === callback.pathname)) return route.continue();
73
+ return route.abort();
74
+ });
75
+ context.on("page", function (opened) {
76
+ page = opened;
77
+ opened.setViewportSize(viewport).catch(function () {});
78
+ opened.on("download", function (download) { download.cancel().catch(function () {}); });
79
+ opened.on("close", function () {
80
+ var pages = context.pages();
81
+ page = pages.length ? pages[pages.length - 1] : null;
82
+ });
83
+ });
84
+ page = await context.newPage();
85
+ capture();
86
+ status("Opening sign-in page…");
87
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 60000 });
88
+ status("Sign in to connect your account.", "ready");
89
+ }
90
+
91
+ async function input(event) {
92
+ if (!page || page.isClosed() || closing) return;
93
+ if (["move", "down", "up"].indexOf(event.kind) >= 0 && Number.isFinite(event.x) && Number.isFinite(event.y)) {
94
+ var pointerSize = page.viewportSize();
95
+ var x = Math.max(0, Math.min(pointerSize.width - 1, event.x));
96
+ var y = Math.max(0, Math.min(pointerSize.height - 1, event.y));
97
+ await page.mouse.move(x, y);
98
+ if (event.kind === "down") await page.mouse.down();
99
+ if (event.kind === "up") await page.mouse.up();
100
+ } else if (event.kind === "click" && Number.isFinite(event.x) && Number.isFinite(event.y)) {
101
+ var size = page.viewportSize();
102
+ await page.mouse.click(Math.max(0, Math.min(size.width - 1, event.x)), Math.max(0, Math.min(size.height - 1, event.y)));
103
+ } else if (event.kind === "wheel" && Number.isFinite(event.deltaY)) {
104
+ await page.mouse.wheel(0, Math.max(-1500, Math.min(1500, event.deltaY)));
105
+ } else if (event.kind === "text" && typeof event.text === "string" && event.text.length <= 4096) {
106
+ await page.keyboard.insertText(event.text);
107
+ } else if (event.kind === "key" && /^(Tab|Shift\+Tab|Enter|Backspace|Delete|Escape|ArrowLeft|ArrowRight|ArrowUp|ArrowDown|Home|End|ControlOrMeta\+A)$/.test(event.key)) {
108
+ await page.keyboard.press(event.key);
109
+ } else if (event.kind === "reload") {
110
+ await page.reload({ waitUntil: "domcontentloaded", timeout: 30000 });
111
+ } else if (event.kind === "resize" && Number.isFinite(event.width) && Number.isFinite(event.height)) {
112
+ viewport = { width: Math.round(Math.max(320, Math.min(1000, event.width))), height: Math.round(Math.max(360, Math.min(720, event.height))) };
113
+ await page.setViewportSize(viewport);
114
+ }
115
+ }
116
+
117
+ async function close() {
118
+ if (closing) return;
119
+ closing = true;
120
+ clearTimeout(captureTimer);
121
+ if (installing) installing.kill();
122
+ if (browser) await browser.close().catch(function () {});
123
+ process.exit(0);
124
+ }
125
+
126
+ var started = false;
127
+ var lines = readline.createInterface({ input: process.stdin });
128
+ lines.on("line", function (line) {
129
+ if (line.length > 20000 || closing) return;
130
+ var message;
131
+ try { message = JSON.parse(line); } catch (e) { return; }
132
+ if (message.type === "start" && !started) {
133
+ started = true;
134
+ start(message.url).catch(function () {
135
+ status("Unable to open the sign-in browser. The host may need browser libraries or sandbox support. Use terminal sign-in to continue.", "error");
136
+ });
137
+ } else if (message.type === "viewing") {
138
+ viewing = !!message.value;
139
+ } else if (message.type === "input" && message.event && queued < 100) {
140
+ if (message.event.kind === "move" && queued >= 10) return;
141
+ queued++;
142
+ inputQueue = inputQueue.then(function () { return input(message.event); }).catch(function () {
143
+ status("The page did not respond. Try reloading.", "error");
144
+ }).finally(function () { queued--; });
145
+ }
146
+ });
147
+ lines.on("close", close);
148
+ process.on("SIGTERM", close);
149
+ process.on("SIGINT", close);
@@ -0,0 +1,162 @@
1
+ // Private, human-controlled browser transport for a single vendor login.
2
+ var fs = require("fs");
3
+ var os = require("os");
4
+ var path = require("path");
5
+ var crypto = require("crypto");
6
+ var spawn = require("child_process").spawn;
7
+ var buildUserEnv = require("./build-user-env").buildUserEnv;
8
+ var wrapSpawnAsUser = require("./os-users").wrapSpawnAsUser;
9
+
10
+ function loginUrl(value, vendor) {
11
+ try {
12
+ var url = new URL(value);
13
+ var hosts = vendor === "codex" ? ["auth.openai.com"] : ["claude.ai", "claude.com", "console.anthropic.com", "platform.claude.com"];
14
+ if (url.protocol !== "https:" || url.username || url.password || hosts.indexOf(url.hostname) < 0) return null;
15
+ if (!url.searchParams.get("state") || !url.searchParams.get("code_challenge")) return null;
16
+ var callback = new URL(url.searchParams.get("redirect_uri"));
17
+ if (callback.protocol !== "http:" || ["localhost", "127.0.0.1", "[::1]"].indexOf(callback.hostname) < 0) return null;
18
+ return url.href;
19
+ } catch (e) { return null; }
20
+ }
21
+
22
+ function createLoginBrowser(options) {
23
+ var id = crypto.randomUUID();
24
+ var dir = fs.mkdtempSync(path.join(os.tmpdir(), "clay-login-"));
25
+ fs.chmodSync(dir, 0o700);
26
+ var info = options.osUserInfo;
27
+ if (info) fs.chownSync(dir, info.uid, info.gid);
28
+ var urlFile = path.join(dir, "url");
29
+ var opener = path.resolve(__dirname, "../bin/login-browser-open.js");
30
+ var wrapper = path.join(dir, process.platform === "win32" ? "open.cmd" : "open");
31
+ var quote = function (value) { return "'" + value.replace(/'/g, "'\\''") + "'"; };
32
+ var script = process.platform === "win32"
33
+ ? '@"' + process.execPath + '" "' + opener + '" %*\r\n'
34
+ : "#!/bin/sh\nexec " + quote(process.execPath) + " " + quote(opener) + ' "$@"\n';
35
+ fs.writeFileSync(wrapper, script, { mode: 0o700 });
36
+ if (info) fs.chownSync(wrapper, info.uid, info.gid);
37
+ var child = null;
38
+ var peer = options.ws;
39
+ var closed = false;
40
+ var started = false;
41
+ var status = "Waiting for the sign-in page…";
42
+ var statusPhase = "loading";
43
+ var tail = "";
44
+ var pollBusy = false;
45
+ var waitTimer = setTimeout(function () {
46
+ if (!started && !closed) {
47
+ status = "Still waiting for the CLI sign-in page. Use terminal sign-in if it needs attention.";
48
+ send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
49
+ }
50
+ }, 30000);
51
+ waitTimer.unref();
52
+
53
+ function send(message) {
54
+ if (closed || !peer || peer.readyState !== 1 || !options.canAccess(peer)) return;
55
+ if (message.type === "vendor_login_browser_frame" && peer.bufferedAmount > 1024 * 1024) return;
56
+ options.sendTo(peer, Object.assign({ vendor: options.vendor, browserId: id }, message));
57
+ }
58
+ function write(message) {
59
+ if (child && child.stdin.writable && child.stdin.writableLength < 65536) child.stdin.write(JSON.stringify(message) + "\n");
60
+ }
61
+ function start(url) {
62
+ if (closed || started) return;
63
+ started = true;
64
+ clearTimeout(waitTimer);
65
+ status = "Opening a private sign-in browser…";
66
+ send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
67
+ var env = buildUserEnv(info);
68
+ if (process.platform === "win32" && !info) {
69
+ ["SystemRoot", "WINDIR", "TEMP", "TMP", "LOCALAPPDATA", "APPDATA", "USERPROFILE", "COMSPEC", "PATHEXT"].forEach(function (key) {
70
+ if (process.env[key]) env[key] = process.env[key];
71
+ });
72
+ }
73
+ var opts = { cwd: info ? info.home : os.homedir(), env: env, stdio: ["pipe", "pipe", "ignore"] };
74
+ if (info) { opts.uid = info.uid; opts.gid = info.gid; }
75
+ var spec = wrapSpawnAsUser(process.execPath, [path.join(__dirname, "vendor-login-browser-worker.js")], opts);
76
+ child = spawn(spec.command, spec.args, spec.options);
77
+ var buffer = "";
78
+ child.stdout.setEncoding("utf8");
79
+ child.stdout.on("data", function (chunk) {
80
+ buffer += chunk;
81
+ if (buffer.length > 4 * 1024 * 1024) { child.kill(); return; }
82
+ var at;
83
+ while ((at = buffer.indexOf("\n")) >= 0) {
84
+ var line = buffer.slice(0, at);
85
+ buffer = buffer.slice(at + 1);
86
+ try {
87
+ var message = JSON.parse(line);
88
+ if (message.type === "status") {
89
+ status = message.text;
90
+ statusPhase = message.phase || "loading";
91
+ send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
92
+ } else if (message.type === "frame") {
93
+ send({ type: "vendor_login_browser_frame", data: message.data, origin: message.origin, width: message.width, height: message.height });
94
+ write({ type: "viewing", value: !!(peer && peer.readyState === 1 && options.canAccess(peer)) });
95
+ }
96
+ } catch (e) {}
97
+ }
98
+ });
99
+ child.on("error", function () {
100
+ statusPhase = "error";
101
+ status = "The sign-in browser could not start. Use terminal sign-in to continue.";
102
+ send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
103
+ });
104
+ child.on("exit", function () {
105
+ if (closed) return;
106
+ statusPhase = "error";
107
+ status = "The sign-in browser closed. You can retry or use terminal sign-in.";
108
+ send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
109
+ });
110
+ child.stdin.on("error", function () {});
111
+ write({ type: "start", url: url });
112
+ }
113
+ function acceptUrl(value) {
114
+ var url = loginUrl(value, options.vendor);
115
+ if (url) start(url);
116
+ }
117
+ var timer = setInterval(function () {
118
+ if (closed || started || pollBusy) return;
119
+ pollBusy = true;
120
+ fs.readFile(urlFile, "utf8", function (err, value) {
121
+ pollBusy = false;
122
+ if (!err && value.length <= 16384) acceptUrl(value);
123
+ });
124
+ }, 250);
125
+ timer.unref();
126
+ return {
127
+ id: id,
128
+ env: { BROWSER: wrapper, CLAY_LOGIN_URL_FILE: urlFile },
129
+ output: function (chunk) {
130
+ // Codex also prints the complete URL when the platform opener ignores BROWSER.
131
+ tail = (tail + chunk).slice(-32768);
132
+ var matches = tail.match(/https:\/\/[^\s\x1b<>"']+(?=[\s\x1b<>"'])/g) || [];
133
+ for (var i = 0; i < matches.length; i++) acceptUrl(matches[i]);
134
+ },
135
+ attach: function (ws) {
136
+ peer = ws;
137
+ send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
138
+ write({ type: "viewing", value: true });
139
+ },
140
+ input: function (ws, event) {
141
+ if (ws !== peer || !options.canAccess(ws) || !event || typeof event !== "object") return;
142
+ write({ type: "input", event: event });
143
+ },
144
+ close: function () {
145
+ if (closed) return;
146
+ closed = true;
147
+ clearInterval(timer);
148
+ clearTimeout(waitTimer);
149
+ if (child) {
150
+ child.stdin.end();
151
+ var killTimer = setTimeout(function () { child.kill("SIGKILL"); }, 5000);
152
+ killTimer.unref();
153
+ child.once("exit", function () { clearTimeout(killTimer); });
154
+ }
155
+ fs.rmSync(dir, { recursive: true, force: true });
156
+ tail = "";
157
+ peer = null;
158
+ },
159
+ };
160
+ }
161
+
162
+ module.exports = { createLoginBrowser: createLoginBrowser, loginUrl: loginUrl };
package/lib/ws-schema.js CHANGED
@@ -219,7 +219,21 @@ var schema = {
219
219
  // Auth
220
220
  // -----------------------------------------------------------------------
221
221
  "auth_required": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Authentication required (e.g. API key needed)" },
222
+ "shared_browser_state_request": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Session-owned shared browser state_request" },
223
+ "shared_browser_open": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Session-owned shared browser open" },
224
+ "shared_browser_view": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Session-owned shared browser view" },
225
+ "shared_browser_control": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Session-owned shared browser control" },
226
+ "shared_browser_close": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Close one exact browser tab" },
227
+ "shared_browser_end": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Session-owned shared browser end" },
228
+ "shared_browser_input": { direction: "c2s", handler: "lib/project-shared-browser.js", description: "Session-owned shared browser input" },
229
+ "shared_browser_state": { direction: "s2c", handler: "lib/public/modules/shared-browser.js", description: "Private shared browser state" },
230
+ "shared_browser_frame": { direction: "s2c", handler: "lib/public/modules/shared-browser.js", description: "Private shared browser frame" },
231
+ "shared_browser_error": { direction: "s2c", handler: "lib/public/modules/shared-browser.js", description: "Private shared browser error" },
222
232
  "vendor_login_start": { direction: "c2s", handler: "lib/project-vendor-login.js", description: "Start (or re-attach to) the per-vendor login terminal" },
233
+ "vendor_login_browser_attach": { direction: "c2s", handler: "lib/vendor-login-browser-flow.js", description: "Attach the sign-in owner to their private browser" },
234
+ "vendor_login_browser_input": { direction: "c2s", handler: "lib/vendor-login-browser-flow.js", description: "Send human input to the private sign-in browser" },
235
+ "vendor_login_browser_frame": { direction: "s2c", handler: "lib/public/modules/vendor-login-browser.js", description: "Private sign-in viewport; never recorded in conversation history" },
236
+ "vendor_login_browser_status": { direction: "s2c", handler: "lib/public/modules/vendor-login-browser.js", description: "Private sign-in browser status" },
223
237
  "vendor_login_cancel": { direction: "c2s", handler: "lib/project-vendor-login.js", description: "Dismiss the login flow and kill its terminal" },
224
238
  "vendor_login_state_request": { direction: "c2s", handler: "lib/project-vendor-login.js", description: "Ask for the project's active login flows" },
225
239
  "vendor_login_ready": { direction: "s2c", handler: "lib/public/modules/vendor-login.js", description: "Login terminal is available; open/re-attach the modal to it" },
@@ -1,11 +1,12 @@
1
1
  // Cross-vendor skill discovery
2
2
  // ----------------------------
3
- // Skills remain in their vendor-owned directories. This module only builds a
4
- // merged view so every YOKE adapter can expose the same installed skills.
3
+ // Ordinary skills stay in vendor-owned directories. Clay-specific skills use
4
+ // private roots and are attached only to Clay-owned adapter queries.
5
5
 
6
6
  var fs = require("fs");
7
7
  var os = require("os");
8
8
  var path = require("path");
9
+ var clayStorage = require("../clay-skill-storage");
9
10
 
10
11
  function defaultHomeDir() {
11
12
  try { return require("../config").REAL_HOME; } catch (e) { return os.homedir(); }
@@ -13,10 +14,12 @@ function defaultHomeDir() {
13
14
 
14
15
  function optionsForOsUser(userInfo, fsAsUser) {
15
16
  if (!userInfo || typeof userInfo.home !== "string" || !userInfo.home) return { disable: true };
17
+ var migrateAsUser = !fsAsUser || fsAsUser === require("../os-users").fsAsUser;
16
18
  fsAsUser = fsAsUser || require("../os-users").fsAsUser;
17
19
  return {
18
20
  homeDir: userInfo.home,
19
21
  scopedHome: true,
22
+ migrationIdentity: migrateAsUser ? userInfo : null,
20
23
  readDir: function (dir) { return fsAsUser("list", { dir: dir }, userInfo).map(function (entry) { return { name: entry.name, isDir: entry.isDir }; }); },
21
24
  readFile: function (file) { return fsAsUser("read", { file: file, readContent: true }, userInfo).content; },
22
25
  realpath: function (file) { return fsAsUser("realpath", { file: file }, userInfo).path; },
@@ -89,6 +92,7 @@ function getSkillRoots(cwd, opts) {
89
92
  var codexHome = env.CODEX_HOME ? path.resolve(homeDir, env.CODEX_HOME) : path.join(homeDir, ".codex");
90
93
  var claudeConfigDir = env.CLAUDE_CONFIG_DIR ? path.resolve(homeDir, env.CLAUDE_CONFIG_DIR) : path.join(homeDir, ".claude");
91
94
  var roots = [
95
+ { path: path.join(__dirname, "../bundled-skills"), source: "clay-builtin" },
92
96
  { path: path.join(homeDir, ".agents", "skills"), source: "agents-user" },
93
97
  { path: path.join(codexHome, "skills"), source: "codex-user" },
94
98
  { path: path.join(codexHome, "skills", ".system"), source: "codex-system-user" },
@@ -101,6 +105,8 @@ function getSkillRoots(cwd, opts) {
101
105
  roots.push({ path: path.join(cwd, ".codex", "skills", ".system"), source: "codex-system-project" });
102
106
  roots.push({ path: path.join(cwd, ".claude", "skills"), source: "claude-project" });
103
107
  }
108
+ roots.push({ path: clayStorage.privateRoot(homeDir), source: "clay-user" });
109
+ if (cwd) roots.push({ path: clayStorage.privateRoot(cwd), source: "clay-project" });
104
110
  return roots;
105
111
  }
106
112
 
@@ -108,6 +114,20 @@ function discoverSkills(cwd, opts) {
108
114
  if (opts && opts.disable) return [];
109
115
  var merged = {};
110
116
  var roots = getSkillRoots(cwd, opts);
117
+ var home = path.dirname(path.dirname(roots.find(function (root) { return root.source === "clay-user"; }).path));
118
+ var legacy = roots.filter(function (root) { return !/^clay-/.test(root.source); }).map(function (root) {
119
+ return { path: root.path, base: root.source === "project" || /-project$/.test(root.source) ? cwd : home };
120
+ });
121
+ // Run filesystem migration as the same OS owner used for skill discovery.
122
+ if (!opts || !opts.readDir || opts.migrationIdentity) {
123
+ try {
124
+ if (opts && opts.migrationIdentity) {
125
+ var identity = opts.migrationIdentity;
126
+ var job = require("../os-users").wrapSpawnAsUser(process.execPath, [require.resolve("../clay-skill-storage"), JSON.stringify(legacy)], { uid: identity.uid, gid: identity.gid, timeout: 10000 });
127
+ require("child_process").execFileSync(job.command, job.args, job.options);
128
+ } else clayStorage.migrate(legacy);
129
+ } catch (error) { console.error("[skills] Clay-only skill migration failed: " + error.message); }
130
+ }
111
131
  for (var i = 0; i < roots.length; i++) scanRoot(roots[i].path, roots[i].source, merged, opts);
112
132
  return Object.keys(merged).sort().map(function(name) { return merged[name]; });
113
133
  }