surf-cli 2.17.0 → 2.18.0
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.
- package/README.md +28 -4
- package/agents/gpt-pro.md +2 -2
- package/native/browser-session-store.cjs +30 -2
- package/native/chatgpt-client-selection.cjs +79 -35
- package/native/chatgpt-client-ui.cjs +313 -198
- package/native/chatgpt-client.cjs +7 -2
- package/native/cli.cjs +121 -4
- package/native/file-transfer.cjs +5 -1
- package/native/host-helpers.cjs +1 -1
- package/native/host.cjs +424 -7
- package/native/mcp-server.cjs +1 -1
- package/native/oracle-cli.cjs +2 -2
- package/native/socket-permissions.cjs +114 -0
- package/native/tool-scope.cjs +6 -2
- package/native/video-recorder.cjs +444 -0
- package/native/workflow-definition.cjs +5 -0
- package/package.json +3 -3
- package/scripts/install-native-host.cjs +53 -6
- package/skills/surf/SKILL.md +7 -3
|
@@ -4,6 +4,7 @@ const path = require("path");
|
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const { execFileSync, execSync } = require("child_process");
|
|
6
6
|
const { parseListenEndpoint } = require("../native/listener.cjs");
|
|
7
|
+
const { normalizeSocketConfig } = require("../native/socket-permissions.cjs");
|
|
7
8
|
const { getStateDir, loadHostIdentity, loadRegistry } = require("../native/remote-auth.cjs");
|
|
8
9
|
|
|
9
10
|
const HOST_NAME = "surf.browser.host";
|
|
@@ -166,7 +167,9 @@ function wslPathToWindowsPath(wslPath) {
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
function createWrapper(wrapperDir, nodePath, hostPath, target = process.platform, listen) {
|
|
170
|
+
function createWrapper(wrapperDir, nodePath, hostPath, target = process.platform, listen, socketMode, socketGroup) {
|
|
171
|
+
const socketConfig = normalizeSocketConfig(socketMode, socketGroup);
|
|
172
|
+
assertSocketAccessTargetSupported(socketConfig.mode, socketConfig.group, target);
|
|
170
173
|
fs.mkdirSync(wrapperDir, { recursive: true });
|
|
171
174
|
|
|
172
175
|
if (target === "wsl-windows") {
|
|
@@ -186,9 +189,13 @@ function createWrapper(wrapperDir, nodePath, hostPath, target = process.platform
|
|
|
186
189
|
|
|
187
190
|
const shPath = path.join(wrapperDir, "host-wrapper.sh");
|
|
188
191
|
const hostDir = path.dirname(hostPath);
|
|
192
|
+
const socketEnvironment = [
|
|
193
|
+
socketConfig.mode === undefined ? "" : `: "\${SURF_SOCKET_MODE:=${socketConfig.mode.toString(8)}}"\nexport SURF_SOCKET_MODE\n`,
|
|
194
|
+
socketConfig.group === undefined ? "" : `: "\${SURF_SOCKET_GROUP:=${socketConfig.group}}"\nexport SURF_SOCKET_GROUP\n`,
|
|
195
|
+
].join("");
|
|
189
196
|
const content = `#!/usr/bin/env bash
|
|
190
197
|
cd "${hostDir}"
|
|
191
|
-
${listen ? `: "\${SURF_LISTEN:=${listen}}"\nexport SURF_LISTEN\n` : ""}exec "${nodePath}" "${hostPath}" "$@"
|
|
198
|
+
${listen ? `: "\${SURF_LISTEN:=${listen}}"\nexport SURF_LISTEN\n` : ""}${socketEnvironment}exec "${nodePath}" "${hostPath}" "$@"
|
|
192
199
|
`;
|
|
193
200
|
fs.writeFileSync(shPath, content);
|
|
194
201
|
fs.chmodSync(shPath, "755");
|
|
@@ -201,6 +208,12 @@ function assertListenTargetSupported(listen, target) {
|
|
|
201
208
|
}
|
|
202
209
|
}
|
|
203
210
|
|
|
211
|
+
function assertSocketAccessTargetSupported(socketMode, socketGroup, target) {
|
|
212
|
+
if ((socketMode !== undefined || socketGroup !== undefined) && (target === "win32" || target === "wsl-windows")) {
|
|
213
|
+
throw new Error("--socket-mode and --socket-group are only supported for POSIX native-host wrappers");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
204
217
|
function readExistingManifest(manifestPath) {
|
|
205
218
|
if (!fs.existsSync(manifestPath)) return {};
|
|
206
219
|
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
@@ -276,7 +289,14 @@ function installWindowsRegistry(browser, extensionId, wrapperPath) {
|
|
|
276
289
|
|
|
277
290
|
function parseArgs() {
|
|
278
291
|
const args = process.argv.slice(2);
|
|
279
|
-
const result = {
|
|
292
|
+
const result = {
|
|
293
|
+
extensionId: null,
|
|
294
|
+
browsers: ["chrome"],
|
|
295
|
+
target: "auto",
|
|
296
|
+
listen: undefined,
|
|
297
|
+
socketMode: undefined,
|
|
298
|
+
socketGroup: undefined,
|
|
299
|
+
};
|
|
280
300
|
|
|
281
301
|
for (let i = 0; i < args.length; i++) {
|
|
282
302
|
const arg = args[i];
|
|
@@ -292,6 +312,12 @@ function parseArgs() {
|
|
|
292
312
|
} else if (arg === "--listen") {
|
|
293
313
|
result.listen = args[++i];
|
|
294
314
|
if (!result.listen || result.listen.startsWith("--")) throw new Error("--listen requires a Tailnet IP and port");
|
|
315
|
+
} else if (arg === "--socket-mode") {
|
|
316
|
+
result.socketMode = args[++i];
|
|
317
|
+
if (!result.socketMode || result.socketMode.startsWith("--")) throw new Error("--socket-mode requires 600 or 660");
|
|
318
|
+
} else if (arg === "--socket-group") {
|
|
319
|
+
result.socketGroup = args[++i];
|
|
320
|
+
if (!result.socketGroup || result.socketGroup.startsWith("--")) throw new Error("--socket-group requires a group name or gid");
|
|
295
321
|
} else if (arg === "--help" || arg === "-h") {
|
|
296
322
|
printHelp();
|
|
297
323
|
process.exit(0);
|
|
@@ -322,6 +348,12 @@ Options:
|
|
|
322
348
|
Persist an authenticated Tailnet-only listener endpoint.
|
|
323
349
|
Requires at least one surf remote authorize client first.
|
|
324
350
|
Supports Tailscale IPv4 or IPv6 addresses; POSIX wrappers only.
|
|
351
|
+
--socket-mode <600|660>
|
|
352
|
+
Persist the local Unix socket mode (default: 600).
|
|
353
|
+
Mode 660 requires --socket-group; POSIX wrappers only.
|
|
354
|
+
--socket-group <group-or-gid>
|
|
355
|
+
Persist the local Unix socket group for mode 660.
|
|
356
|
+
Use a dedicated group; this grants full Surf authority.
|
|
325
357
|
|
|
326
358
|
Examples:
|
|
327
359
|
node install-native-host.cjs abcdefghijklmnopabcdefghijklmnop
|
|
@@ -329,13 +361,14 @@ Examples:
|
|
|
329
361
|
node install-native-host.cjs abcdefghijklmnop --browser all
|
|
330
362
|
node install-native-host.cjs abcdefghijklmnop --target linux
|
|
331
363
|
node install-native-host.cjs abcdefghijklmnop --listen 100.64.1.2:4321
|
|
364
|
+
node install-native-host.cjs abcdefghijklmnop --socket-mode 660 --socket-group surf
|
|
332
365
|
`);
|
|
333
366
|
}
|
|
334
367
|
|
|
335
368
|
function main() {
|
|
336
369
|
let parsed;
|
|
337
370
|
try { parsed = parseArgs(); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); }
|
|
338
|
-
const { extensionId, browsers, target, listen } = parsed;
|
|
371
|
+
const { extensionId, browsers, target, listen, socketMode, socketGroup } = parsed;
|
|
339
372
|
|
|
340
373
|
if (!extensionId) {
|
|
341
374
|
console.error("Error: Extension ID required");
|
|
@@ -378,7 +411,12 @@ function main() {
|
|
|
378
411
|
}
|
|
379
412
|
|
|
380
413
|
const effectiveTarget = runningInWsl && target !== "linux" ? "wsl-windows" : process.platform;
|
|
381
|
-
|
|
414
|
+
let socketConfig;
|
|
415
|
+
try {
|
|
416
|
+
socketConfig = normalizeSocketConfig(socketMode, socketGroup);
|
|
417
|
+
assertListenTargetSupported(listen, effectiveTarget);
|
|
418
|
+
assertSocketAccessTargetSupported(socketMode, socketGroup, effectiveTarget);
|
|
419
|
+
} catch (error) { console.error(`Error: ${error.message}`); process.exit(1); }
|
|
382
420
|
|
|
383
421
|
const nodePath = findNode();
|
|
384
422
|
if (!nodePath) {
|
|
@@ -407,7 +445,15 @@ function main() {
|
|
|
407
445
|
console.log(`Wrapper dir: ${wrapperDir}`);
|
|
408
446
|
console.log("");
|
|
409
447
|
|
|
410
|
-
const wrapperPath = createWrapper(
|
|
448
|
+
const wrapperPath = createWrapper(
|
|
449
|
+
wrapperDir,
|
|
450
|
+
nodePath,
|
|
451
|
+
hostPath,
|
|
452
|
+
effectiveTarget,
|
|
453
|
+
listener,
|
|
454
|
+
socketConfig.mode,
|
|
455
|
+
socketConfig.group,
|
|
456
|
+
);
|
|
411
457
|
console.log(`Created wrapper: ${wrapperPath}`);
|
|
412
458
|
console.log("");
|
|
413
459
|
|
|
@@ -450,4 +496,5 @@ module.exports = {
|
|
|
450
496
|
createWrapper,
|
|
451
497
|
writeManifest,
|
|
452
498
|
assertListenTargetSupported,
|
|
499
|
+
assertSocketAccessTargetSupported,
|
|
453
500
|
};
|
package/skills/surf/SKILL.md
CHANGED
|
@@ -15,6 +15,8 @@ On macOS, Chrome reads the native messaging manifest at `~/Library/Application S
|
|
|
15
15
|
|
|
16
16
|
If a command reports `Socket connect failed`, run `surf doctor` first, then check the `Attempted socket:` line. Default sockets are `/tmp/surf.sock` on macOS/Linux/WSL2 and `//./pipe/surf` on Windows. If `SURF_SOCKET` is set, the browser-launched host and the shell running `surf` must use the same value.
|
|
17
17
|
|
|
18
|
+
For opt-in POSIX group sharing, install with `surf install <extension-id> --socket-mode 660 --socket-group <group>`. The default remains `0600`; mode `660` grants every member of that group full Surf authority, so use a dedicated narrow group. Re-run `surf install` without those flags to clear the wrapper settings. Remote Surf credentials remain the revocable per-client alternative.
|
|
19
|
+
|
|
18
20
|
## Remote Surf
|
|
19
21
|
|
|
20
22
|
Remote clients require a per-client credential; Tailnet reachability alone is not authorization. On the POSIX browser host, authorize the client before installing the listener:
|
|
@@ -112,11 +114,13 @@ surf oracle result <job-id> --wait --json
|
|
|
112
114
|
|
|
113
115
|
`status` reads persisted state without touching Chrome. `result` attempts to harvest the answer and returns the job object with `response` once its state is `captured`. A Ctrl-C during waiting exits with status 130 and prints `Recover with: surf oracle result <id>`. Once the job is `awaiting`, the persisted ChatGPT conversation URL is its durable key, so `surf oracle result <id>` can recover after CLI exit, native-host restart, or Chrome restart by reopening that conversation.
|
|
114
116
|
|
|
115
|
-
Treat Pro quota as scarce. Oracle never selects Pro effort implicitly; request it with `--effort pro`. ChatGPT model aliases include `
|
|
117
|
+
Treat Pro quota as scarce. Oracle never selects Pro effort implicitly; request it with `--effort pro`. ChatGPT model aliases include `gpt-6-astra`, `latest`, `gpt-5.6-sol`, and `gpt-5.5`; `latest` is an explicit floating choice, while `gpt-6-astra` must read back as model 6 before submission. Accepted `--effort` values are `instant`, `medium`, `high`, `xhigh`/`extra-high`, and `pro`. Use `--model gpt-6-astra --effort pro` for GPT-6 Astra with Pro effort. Requested model and effort selections are read back before submission, and an unverifiable selection fails with `model_verification_failed` instead of silently continuing. Capacity is one non-terminal oracle job. A `capacity` error includes the in-flight job ID; poll that job or wait for it to finish rather than submitting the same consult again.
|
|
118
|
+
|
|
119
|
+
ChatGPT can hide the model version at lower effort settings. Use `--model latest` if floating model selection is intended; do not retry an unverifiable `gpt-6-astra` request as `latest` without the user's approval.
|
|
116
120
|
|
|
117
|
-
When loaded as a Pi extension, Surf also registers a `surf-oracle` external-job provider when the runtime exposes that bridge. The provider maps `start`, `status`, `result`, and `reattach` to durable Surf Oracle jobs and returns pi-subagents' external-job contract shape: `providerJobId`, a contract state (`queued`, `running`, `completed`, `failed`), the conversation URL, the captured result text as `output`, and failure code and message. It honors `options.model`, `options.effort`, `options.file`, and `options.github` for starts and follow-ups, so `model: gpt-
|
|
121
|
+
When loaded as a Pi extension, Surf also registers a `surf-oracle` external-job provider when the runtime exposes that bridge. The provider maps `start`, `status`, `result`, and `reattach` to durable Surf Oracle jobs and returns pi-subagents' external-job contract shape: `providerJobId`, a contract state (`queued`, `running`, `completed`, `failed`), the conversation URL, the captured result text as `output`, and failure code and message. It honors `options.model`, `options.effort`, `options.file`, and `options.github` for starts and follow-ups, so `model: gpt-6-astra` plus `effort: pro` selects ChatGPT GPT-6 Astra with Pro effort through the browser, while `github: true` requires Chat mode and the connected GitHub tool. `reattach` only harvests an existing job by ID; it never submits the prompt again.
|
|
118
122
|
|
|
119
|
-
When Surf is installed as a Pi package, it exposes an optional `gpt-pro` package agent for `pi-subagents`. That profile uses `runner.type: external-job`, provider `surf-oracle`, `options.model: gpt-
|
|
123
|
+
When Surf is installed as a Pi package, it exposes an optional `gpt-pro` package agent for `pi-subagents`. That profile uses `runner.type: external-job`, provider `surf-oracle`, `options.model: gpt-6-astra`, and `options.effort: pro`. Surf remains useful without Pi or `pi-subagents`.
|
|
120
124
|
|
|
121
125
|
Context comes from repeatable `--files` globs. Use `--file <path>` for one additional local attachment; `--github` requires Chat mode and a connected GitHub tool. Surf fails closed when a glob matches nothing or a matched file is unreadable, binary, or invalid UTF-8. It also blocks gitignored files and basenames matching `.env*`, `*.pem`, `*.key`, `id_rsa*`, `id_ed25519*`, `*.p12`, `*.pfx`, `credentials*`, or `secrets*`. Use `--allow-sensitive` only after intentionally reviewing those files; it overrides the block rather than redacting content. Context up to 60,000 evidence characters is inserted inline, while larger context becomes one private text attachment. The assembly manifest records each path, byte count, SHA-256, inline or bundle disposition, and deny-list outcome.
|
|
122
126
|
|