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.
@@ -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 = { extensionId: null, browsers: ["chrome"], target: "auto", listen: undefined };
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
- try { assertListenTargetSupported(listen, effectiveTarget); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); }
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(wrapperDir, nodePath, hostPath, effectiveTarget, listener);
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
  };
@@ -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 `instant`, `thinking`, `pro`, `gpt-5.5`, and `gpt-5.6-sol`. Accepted `--effort` values are `light`, `standard`, `extended`, `heavy`, and `pro`. Use `--model gpt-5.6-sol --effort pro` for GPT-5.6 Sol 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.
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-5.6-sol` plus `effort: pro` selects ChatGPT GPT-5.6 Sol 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.
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-5.6-sol`, and `options.effort: pro`. Surf remains useful without Pi or `pi-subagents`.
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