surf-cli 2.16.1 → 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 +30 -6
- 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 +626 -197
- package/native/chatgpt-client.cjs +65 -6
- package/native/cli.cjs +121 -4
- package/native/file-transfer.cjs +10 -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 +68 -7
- package/native/oracle-host.cjs +46 -2
- package/native/oracle-jobs.cjs +5 -3
- 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/pi-extension/surf.ts +31 -2
- package/scripts/install-native-host.cjs +53 -6
- package/skills/surf/SKILL.md +11 -7
package/native/oracle-cli.cjs
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
const { openClientTransport } = require("./client-transport.cjs");
|
|
2
2
|
const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
|
|
3
3
|
const { assembleContext } = require("./oracle-context.cjs");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
4
6
|
|
|
5
7
|
const RESULT_TIMEOUT_SECONDS = 20;
|
|
6
8
|
const POLL_DELAYS_MS = [5000, 10000, 20000, 40000, 60000];
|
|
7
9
|
const ORACLE_ERROR_CODES = new Set([
|
|
8
10
|
"auth",
|
|
11
|
+
"attachment_chooser_interception",
|
|
12
|
+
"attachment_file_access",
|
|
13
|
+
"attachment_processing",
|
|
14
|
+
"attachment_selector_drift",
|
|
9
15
|
"capacity",
|
|
16
|
+
"chat_mode_selection_failed",
|
|
17
|
+
"chat_mode_selector_drift",
|
|
18
|
+
"chat_mode_unavailable",
|
|
10
19
|
"cloudflare",
|
|
11
20
|
"context_incomplete",
|
|
12
21
|
"dispatch_failed",
|
|
22
|
+
"github_tool_disconnected",
|
|
23
|
+
"github_tool_missing",
|
|
24
|
+
"github_tool_selection_failed",
|
|
25
|
+
"github_tool_selector_drift",
|
|
13
26
|
"harvest_failed",
|
|
27
|
+
"invalid_request",
|
|
14
28
|
"invalid_transition",
|
|
15
29
|
"model_verification_failed",
|
|
16
30
|
"not_found",
|
|
@@ -30,8 +44,10 @@ Commands:
|
|
|
30
44
|
|
|
31
45
|
Ask/follow options:
|
|
32
46
|
--files <glob> Add context files (repeatable)
|
|
33
|
-
--
|
|
34
|
-
--
|
|
47
|
+
--file <path> Attach one local file
|
|
48
|
+
--model <model> Select model: gpt-6-astra, latest, gpt-5.6-sol, gpt-5.5
|
|
49
|
+
--effort <effort> Select effort: instant, medium, high, xhigh, pro
|
|
50
|
+
--github Require the ChatGPT Chat tab and GitHub tool
|
|
35
51
|
--detach Return after dispatch
|
|
36
52
|
--allow-sensitive Allow deny-listed context files
|
|
37
53
|
|
|
@@ -56,11 +72,12 @@ function requireOptionValue(argv, index, name) {
|
|
|
56
72
|
|
|
57
73
|
function parseOptions(argv) {
|
|
58
74
|
const positional = [];
|
|
59
|
-
const options = { files: [] };
|
|
60
|
-
const valueOptions = new Set(["files", "model", "effort"]);
|
|
75
|
+
const options = { files: [], file: [] };
|
|
76
|
+
const valueOptions = new Set(["files", "file", "model", "effort"]);
|
|
61
77
|
const booleanOptions = new Set([
|
|
62
78
|
"allow-sensitive",
|
|
63
79
|
"detach",
|
|
80
|
+
"github",
|
|
64
81
|
"json",
|
|
65
82
|
"no-lock",
|
|
66
83
|
"wait",
|
|
@@ -75,7 +92,7 @@ function parseOptions(argv) {
|
|
|
75
92
|
const name = value.slice(2);
|
|
76
93
|
if (valueOptions.has(name)) {
|
|
77
94
|
const optionValue = requireOptionValue(argv, index, name);
|
|
78
|
-
if (name === "files") options.
|
|
95
|
+
if (name === "files" || name === "file") options[name].push(optionValue);
|
|
79
96
|
else options[name] = optionValue;
|
|
80
97
|
index += 1;
|
|
81
98
|
} else if (booleanOptions.has(name)) {
|
|
@@ -94,14 +111,16 @@ function assertAllowedOptions(command, options) {
|
|
|
94
111
|
"allow-sensitive",
|
|
95
112
|
"detach",
|
|
96
113
|
"effort",
|
|
114
|
+
"file",
|
|
97
115
|
"files",
|
|
116
|
+
"github",
|
|
98
117
|
"model",
|
|
99
118
|
]);
|
|
100
119
|
const allowed = command === "ask" || command === "follow"
|
|
101
120
|
? ask
|
|
102
121
|
: command === "result" ? new Set([...common, "wait"]) : common;
|
|
103
122
|
for (const [name, value] of Object.entries(options)) {
|
|
104
|
-
if (name === "files" && value.length === 0) continue;
|
|
123
|
+
if ((name === "files" || name === "file") && value.length === 0) continue;
|
|
105
124
|
if (value !== undefined && !allowed.has(name)) {
|
|
106
125
|
throw codedError("invalid_transition", `--${name} is not supported by oracle ${command}`);
|
|
107
126
|
}
|
|
@@ -131,8 +150,12 @@ function parseOracleCommand(argv) {
|
|
|
131
150
|
command,
|
|
132
151
|
prompt,
|
|
133
152
|
files: parsed.options.files,
|
|
153
|
+
...(parsed.options.file.length > 0
|
|
154
|
+
? { file: parsed.options.file.length === 1 ? parsed.options.file[0] : parsed.options.file }
|
|
155
|
+
: {}),
|
|
134
156
|
model: parsed.options.model,
|
|
135
157
|
effort: parsed.options.effort,
|
|
158
|
+
github: parsed.options.github === true,
|
|
136
159
|
detach: parsed.options.detach === true,
|
|
137
160
|
allowSensitive: parsed.options["allow-sensitive"] === true,
|
|
138
161
|
json,
|
|
@@ -151,8 +174,12 @@ function parseOracleCommand(argv) {
|
|
|
151
174
|
id,
|
|
152
175
|
prompt,
|
|
153
176
|
files: parsed.options.files,
|
|
177
|
+
...(parsed.options.file.length > 0
|
|
178
|
+
? { file: parsed.options.file.length === 1 ? parsed.options.file[0] : parsed.options.file }
|
|
179
|
+
: {}),
|
|
154
180
|
model: parsed.options.model,
|
|
155
181
|
effort: parsed.options.effort,
|
|
182
|
+
github: parsed.options.github === true,
|
|
156
183
|
detach: parsed.options.detach === true,
|
|
157
184
|
allowSensitive: parsed.options["allow-sensitive"] === true,
|
|
158
185
|
json,
|
|
@@ -191,12 +218,44 @@ function composeAskRequest(spec, context) {
|
|
|
191
218
|
prompt,
|
|
192
219
|
...(spec.model ? { model: spec.model } : {}),
|
|
193
220
|
...(spec.effort ? { effort: spec.effort } : {}),
|
|
221
|
+
...(spec.file ? { file: spec.file } : {}),
|
|
222
|
+
...(spec.github ? { github: true } : {}),
|
|
194
223
|
...(context ? { contextManifest: context.manifest } : {}),
|
|
195
224
|
...(context?.bundlePath ? { bundlePath: context.bundlePath } : {}),
|
|
196
225
|
...(spec.id ? { follow: spec.id } : {}),
|
|
197
226
|
};
|
|
198
227
|
}
|
|
199
228
|
|
|
229
|
+
async function resolveOracleAttachment(value, cwd = process.cwd()) {
|
|
230
|
+
const values = value === undefined || value === null
|
|
231
|
+
? []
|
|
232
|
+
: Array.isArray(value) ? value : [value];
|
|
233
|
+
if (values.length > 1) {
|
|
234
|
+
throw codedError(
|
|
235
|
+
"attachment_file_access",
|
|
236
|
+
"Oracle supports one explicit local attachment; provide a single --file path",
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (values.length === 0) return undefined;
|
|
240
|
+
const requested = values[0];
|
|
241
|
+
if (typeof requested !== "string" || !requested.trim()) {
|
|
242
|
+
throw codedError("attachment_file_access", "Oracle attachment file access failed: --file must be a path");
|
|
243
|
+
}
|
|
244
|
+
const resolved = path.resolve(cwd, requested);
|
|
245
|
+
try {
|
|
246
|
+
const stats = await fs.promises.stat(resolved);
|
|
247
|
+
if (!stats.isFile()) throw new Error("not a regular file");
|
|
248
|
+
await fs.promises.access(resolved, fs.constants.R_OK);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw codedError(
|
|
251
|
+
"attachment_file_access",
|
|
252
|
+
`Oracle attachment file access failed for ${resolved}: ${error?.message || error}`,
|
|
253
|
+
{ path: resolved },
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return resolved;
|
|
257
|
+
}
|
|
258
|
+
|
|
200
259
|
function unwrapResponse(response) {
|
|
201
260
|
if (response?.error) {
|
|
202
261
|
const message = response.error.message
|
|
@@ -389,6 +448,7 @@ async function handleOracleCli(argv, {
|
|
|
389
448
|
}
|
|
390
449
|
}
|
|
391
450
|
|
|
451
|
+
const attachment = await resolveOracleAttachment(spec.file, cwd);
|
|
392
452
|
const context = spec.files.length > 0
|
|
393
453
|
? await assembleContext({
|
|
394
454
|
files: spec.files,
|
|
@@ -396,7 +456,7 @@ async function handleOracleCli(argv, {
|
|
|
396
456
|
allowSensitive: spec.allowSensitive,
|
|
397
457
|
})
|
|
398
458
|
: null;
|
|
399
|
-
const request = composeAskRequest(spec, context);
|
|
459
|
+
const request = composeAskRequest({ ...spec, ...(attachment ? { file: attachment } : {}) }, context);
|
|
400
460
|
const dispatchInterrupt = () => {
|
|
401
461
|
stderr.write(
|
|
402
462
|
"Interrupted during dispatch. A job may already have been created. Run surf oracle status or surf oracle list to find it.\n",
|
|
@@ -430,5 +490,6 @@ module.exports = {
|
|
|
430
490
|
formatOracleOutput,
|
|
431
491
|
handleOracleCli,
|
|
432
492
|
parseOracleCommand,
|
|
493
|
+
resolveOracleAttachment,
|
|
433
494
|
shapeOracleError,
|
|
434
495
|
};
|
package/native/oracle-host.cjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
1
3
|
const chatgptClient = require("./chatgpt-client.cjs");
|
|
2
4
|
const oracleJobs = require("./oracle-jobs.cjs");
|
|
3
5
|
|
|
@@ -23,6 +25,39 @@ function withJobId(error, jobId, fallbackCode) {
|
|
|
23
25
|
return result;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
async function resolveOracleAttachments(args) {
|
|
29
|
+
const explicit = args?.file === undefined || args?.file === null
|
|
30
|
+
? []
|
|
31
|
+
: Array.isArray(args.file) ? args.file : [args.file];
|
|
32
|
+
if (explicit.length > 1) {
|
|
33
|
+
throw codedError(
|
|
34
|
+
"attachment_file_access",
|
|
35
|
+
"Oracle supports one explicit local attachment; provide a single --file path",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const requested = [...explicit, ...(args?.bundlePath ? [args.bundlePath] : [])];
|
|
39
|
+
const resolved = [];
|
|
40
|
+
for (const filePath of requested) {
|
|
41
|
+
if (typeof filePath !== "string" || !filePath.trim()) {
|
|
42
|
+
throw codedError("attachment_file_access", "Oracle attachment file access failed: file path is required");
|
|
43
|
+
}
|
|
44
|
+
const absolutePath = path.resolve(filePath);
|
|
45
|
+
try {
|
|
46
|
+
const stats = await fs.promises.stat(absolutePath);
|
|
47
|
+
if (!stats.isFile()) throw new Error("not a regular file");
|
|
48
|
+
await fs.promises.access(absolutePath, fs.constants.R_OK);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw codedError(
|
|
51
|
+
"attachment_file_access",
|
|
52
|
+
`Oracle attachment file access failed for ${absolutePath}: ${error?.message || error}`,
|
|
53
|
+
{ path: absolutePath },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
resolved.push(absolutePath);
|
|
57
|
+
}
|
|
58
|
+
return resolved;
|
|
59
|
+
}
|
|
60
|
+
|
|
26
61
|
function createOracleHost({ queueAiRequest, requestCallExtension, buildProviderUploadMessage, log }) {
|
|
27
62
|
const closeTab = (request, tabId) => requestCallExtension(
|
|
28
63
|
request,
|
|
@@ -65,6 +100,7 @@ function createOracleHost({ queueAiRequest, requestCallExtension, buildProviderU
|
|
|
65
100
|
async function ask(request, args) {
|
|
66
101
|
assertLocalOracleRequest(request);
|
|
67
102
|
const model = args.model ? chatgptClient.normalizeChatGPTModelChoice(args.model) : null;
|
|
103
|
+
const explicitAttachmentPaths = await resolveOracleAttachments({ ...args, bundlePath: undefined });
|
|
68
104
|
const created = oracleJobs.createJob({
|
|
69
105
|
prompt: args.prompt,
|
|
70
106
|
contextManifest: args.contextManifest,
|
|
@@ -72,11 +108,16 @@ function createOracleHost({ queueAiRequest, requestCallExtension, buildProviderU
|
|
|
72
108
|
effortRequested: args.effort ?? null,
|
|
73
109
|
follow: args.follow ?? null,
|
|
74
110
|
requestId: args.requestId ?? null,
|
|
111
|
+
attachmentPaths: explicitAttachmentPaths,
|
|
112
|
+
github: args.github === true,
|
|
75
113
|
});
|
|
76
114
|
if (created.requestDeduped) return oracleJobs.getJob(created.id);
|
|
77
115
|
let createdTabId = null;
|
|
78
116
|
|
|
79
117
|
try {
|
|
118
|
+
const attachmentPaths = args?.bundlePath
|
|
119
|
+
? [...explicitAttachmentPaths, ...(await resolveOracleAttachments({ bundlePath: args.bundlePath }))]
|
|
120
|
+
: explicitAttachmentPaths;
|
|
80
121
|
let parent = null;
|
|
81
122
|
if (args.follow) {
|
|
82
123
|
parent = oracleJobs.getJob(args.follow);
|
|
@@ -93,7 +134,10 @@ function createOracleHost({ queueAiRequest, requestCallExtension, buildProviderU
|
|
|
93
134
|
prompt: args.prompt,
|
|
94
135
|
model,
|
|
95
136
|
effort: args.effort,
|
|
96
|
-
file:
|
|
137
|
+
file: attachmentPaths.length > 0
|
|
138
|
+
? attachmentPaths.length === 1 ? attachmentPaths[0] : attachmentPaths
|
|
139
|
+
: undefined,
|
|
140
|
+
...(args.github === true ? { github: true } : {}),
|
|
97
141
|
startUrl: parent?.conversationUrl,
|
|
98
142
|
createTab: async () => {
|
|
99
143
|
const tabInfo = await browserOptions(request).createTab();
|
|
@@ -304,4 +348,4 @@ function createOracleHost({ queueAiRequest, requestCallExtension, buildProviderU
|
|
|
304
348
|
};
|
|
305
349
|
}
|
|
306
350
|
|
|
307
|
-
module.exports = { assertLocalOracleRequest, createOracleHost };
|
|
351
|
+
module.exports = { assertLocalOracleRequest, createOracleHost, resolveOracleAttachments };
|
package/native/oracle-jobs.cjs
CHANGED
|
@@ -47,13 +47,15 @@ function stableJson(value) {
|
|
|
47
47
|
return JSON.stringify(value) ?? "null";
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function requestFingerprint({ prompt, contextManifest, model, effortRequested, follow }) {
|
|
50
|
+
function requestFingerprint({ prompt, contextManifest, model, effortRequested, follow, attachmentPaths, github }) {
|
|
51
51
|
return promptDigest(stableJson({
|
|
52
52
|
promptDigest: promptDigest(prompt),
|
|
53
53
|
contextManifest: contextManifest ?? {},
|
|
54
54
|
model: model ?? null,
|
|
55
55
|
effortRequested: effortRequested ?? null,
|
|
56
56
|
follow: follow ?? null,
|
|
57
|
+
attachmentPaths: attachmentPaths ?? [],
|
|
58
|
+
github: github === true,
|
|
57
59
|
}));
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -92,13 +94,13 @@ function readJobs(root = getPrivateStateRoot()) {
|
|
|
92
94
|
.map((job) => hydrateJobMetadata(job, root));
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
function createJob({ prompt, contextManifest = {}, model = null, effortRequested = null, follow = null, requestId = null }) {
|
|
97
|
+
function createJob({ prompt, contextManifest = {}, model = null, effortRequested = null, follow = null, requestId = null, attachmentPaths = [], github = false }) {
|
|
96
98
|
const root = getPrivateStateRoot();
|
|
97
99
|
const base = oracleRoot(root);
|
|
98
100
|
ensurePrivateDir(base, root);
|
|
99
101
|
const safeRequestId = normalizedRequestId(requestId);
|
|
100
102
|
const fingerprint = safeRequestId
|
|
101
|
-
? requestFingerprint({ prompt, contextManifest, model, effortRequested, follow })
|
|
103
|
+
? requestFingerprint({ prompt, contextManifest, model, effortRequested, follow, attachmentPaths, github })
|
|
102
104
|
: null;
|
|
103
105
|
if (safeRequestId) {
|
|
104
106
|
const existing = readJobs(root).find((job) => job.requestId === safeRequestId);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const { execFileSync } = require("child_process");
|
|
3
|
+
|
|
4
|
+
const DEFAULT_SOCKET_MODE = 0o600;
|
|
5
|
+
const ALLOWED_SOCKET_MODES = new Set([0o600, 0o660]);
|
|
6
|
+
const MAX_GID = 0xfffffffe;
|
|
7
|
+
const SOCKET_GROUP_PATTERN = /^(?:\d+|[A-Za-z_][A-Za-z0-9_.-]*)$/;
|
|
8
|
+
|
|
9
|
+
function parseSocketMode(value) {
|
|
10
|
+
if (value === undefined) return DEFAULT_SOCKET_MODE;
|
|
11
|
+
if (typeof value === "number" && ALLOWED_SOCKET_MODES.has(value)) return value;
|
|
12
|
+
if (value === 600) return DEFAULT_SOCKET_MODE;
|
|
13
|
+
if (value === 660) return 0o660;
|
|
14
|
+
const text = String(value).trim();
|
|
15
|
+
if (!/^0?(?:600|660)$/.test(text)) {
|
|
16
|
+
throw new Error("SURF_SOCKET_MODE must be 600 or 660");
|
|
17
|
+
}
|
|
18
|
+
return Number.parseInt(text, 8);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validateSocketGroup(value) {
|
|
22
|
+
if (value === undefined) return undefined;
|
|
23
|
+
if (value === null || typeof value === "boolean") {
|
|
24
|
+
throw new Error("SURF_SOCKET_GROUP must be a numeric gid or a simple group name");
|
|
25
|
+
}
|
|
26
|
+
const group = String(value).trim();
|
|
27
|
+
if (!SOCKET_GROUP_PATTERN.test(group)) {
|
|
28
|
+
throw new Error("SURF_SOCKET_GROUP must be a numeric gid or a simple group name");
|
|
29
|
+
}
|
|
30
|
+
if (/^\d+$/.test(group)) {
|
|
31
|
+
const gid = Number(group);
|
|
32
|
+
if (!Number.isSafeInteger(gid) || gid < 0 || gid > MAX_GID) {
|
|
33
|
+
throw new Error("SURF_SOCKET_GROUP gid is out of range");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return group;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeSocketConfig(socketMode, socketGroup) {
|
|
40
|
+
const mode = socketMode === undefined ? undefined : parseSocketMode(socketMode);
|
|
41
|
+
const group = socketGroup === undefined ? undefined : validateSocketGroup(socketGroup);
|
|
42
|
+
if (mode === 0o660 && !group) {
|
|
43
|
+
throw new Error("SURF_SOCKET_MODE=660 requires SURF_SOCKET_GROUP");
|
|
44
|
+
}
|
|
45
|
+
return { mode, group };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveSocketGroup(value) {
|
|
49
|
+
const group = validateSocketGroup(value);
|
|
50
|
+
if (group === undefined) return undefined;
|
|
51
|
+
if (/^\d+$/.test(group)) return Number(group);
|
|
52
|
+
|
|
53
|
+
const command = process.platform === "darwin" ? "dscl" : "getent";
|
|
54
|
+
const args = process.platform === "darwin"
|
|
55
|
+
? [".", "-read", `/Groups/${group}`, "PrimaryGroupID"]
|
|
56
|
+
: ["group", group];
|
|
57
|
+
let output;
|
|
58
|
+
try {
|
|
59
|
+
output = execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw new Error(`could not resolve socket group ${group}: ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
const match = process.platform === "darwin"
|
|
64
|
+
? output.match(/\bPrimaryGroupID:\s*(\d+)\b/)
|
|
65
|
+
: output.trim().split(/\r?\n/, 1)[0]?.split(":")[2]?.match(/^\d+$/);
|
|
66
|
+
const gid = Number(match?.[1] || match?.[0]);
|
|
67
|
+
if (!Number.isSafeInteger(gid) || gid < 0 || gid > MAX_GID) {
|
|
68
|
+
throw new Error(`could not resolve socket group ${group}`);
|
|
69
|
+
}
|
|
70
|
+
return gid;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveSocketPermissions(socketMode, socketGroup) {
|
|
74
|
+
const config = normalizeSocketConfig(socketMode, socketGroup);
|
|
75
|
+
const mode = config.mode ?? DEFAULT_SOCKET_MODE;
|
|
76
|
+
const gid = resolveSocketGroup(config.group);
|
|
77
|
+
return { mode, group: config.group, gid };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function assertSocketPath(socketPath) {
|
|
81
|
+
let stat;
|
|
82
|
+
try {
|
|
83
|
+
stat = fs.lstatSync(socketPath);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error(`could not inspect local socket: ${error.message}`);
|
|
86
|
+
}
|
|
87
|
+
if (stat.isSymbolicLink()) throw new Error("refusing symbolic-link local socket");
|
|
88
|
+
if (!stat.isSocket()) throw new Error("local socket path is not a Unix socket");
|
|
89
|
+
return stat;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function applySocketPermissions(socketPath, permissions) {
|
|
93
|
+
const before = assertSocketPath(socketPath);
|
|
94
|
+
if (permissions.gid !== undefined) {
|
|
95
|
+
fs.chownSync(socketPath, before.uid, permissions.gid);
|
|
96
|
+
}
|
|
97
|
+
fs.chmodSync(socketPath, permissions.mode);
|
|
98
|
+
const after = assertSocketPath(socketPath);
|
|
99
|
+
if ((after.mode & 0o7777) !== permissions.mode) {
|
|
100
|
+
throw new Error(`local socket mode is not ${permissions.mode.toString(8)}`);
|
|
101
|
+
}
|
|
102
|
+
if (permissions.gid !== undefined && after.gid !== permissions.gid) {
|
|
103
|
+
throw new Error(`local socket group is not ${permissions.group}`);
|
|
104
|
+
}
|
|
105
|
+
return after;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
applySocketPermissions,
|
|
110
|
+
normalizeSocketConfig,
|
|
111
|
+
parseSocketMode,
|
|
112
|
+
resolveSocketPermissions,
|
|
113
|
+
validateSocketGroup,
|
|
114
|
+
};
|
package/native/tool-scope.cjs
CHANGED
|
@@ -21,7 +21,7 @@ const BROWSER_READ_TOOLS = new Set([
|
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
23
|
const BROWSER_WRITE_TOOLS = new Set([
|
|
24
|
-
"session.new", "session.ensure", "session.close", "session.rebind", "session.reopen",
|
|
24
|
+
"session.new", "session.ensure", "session.cleanup", "session.close", "session.rebind", "session.reopen",
|
|
25
25
|
"tab.new", "new_tab", "tabs_create",
|
|
26
26
|
"tab.move", "tab.switch", "switch_tab",
|
|
27
27
|
"tab.group", "tab.ungroup",
|
|
@@ -36,7 +36,7 @@ const BROWSER_WRITE_TARGETED_TOOLS = new Set([
|
|
|
36
36
|
]);
|
|
37
37
|
|
|
38
38
|
const TAB_TOOLS = new Set([
|
|
39
|
-
"ai", "computer", "batch", "record", "animate-audit", "perf-audit",
|
|
39
|
+
"ai", "computer", "batch", "record", "video.start", "animate-audit", "perf-audit",
|
|
40
40
|
"navigate", "go", "back", "forward", "reload", "tab.reload",
|
|
41
41
|
"screenshot", "snap", "resize",
|
|
42
42
|
"page.read", "read_page", "page.text", "get_page_text", "page.html", "page.save", "page.state",
|
|
@@ -87,10 +87,14 @@ function classifyTool(tool, args = {}) {
|
|
|
87
87
|
return { scope: "browser-write", targetUse: "default-tab" };
|
|
88
88
|
}
|
|
89
89
|
if (BROWSER_WRITE_TOOLS.has(tool)) return { scope: "browser-write", targetUse: "browser" };
|
|
90
|
+
if (tool === "video.stop" || tool === "video.status" || tool === "video.restart") {
|
|
91
|
+
return { scope: "host", targetUse: "host", resourceKeys: ["video-recorder"] };
|
|
92
|
+
}
|
|
90
93
|
if (TAB_TOOLS.has(tool)) {
|
|
91
94
|
const resourceKeys = [];
|
|
92
95
|
if (tool === "network.export" && typeof args.output === "string") resourceKeys.push(`file:${path.resolve(args.output)}`);
|
|
93
96
|
if (tool.startsWith("playbook.record.")) resourceKeys.push("playbook-recorder");
|
|
97
|
+
if (tool === "video.start") resourceKeys.push("video-recorder");
|
|
94
98
|
return { scope: "tab", targetUse: "default-tab", resourceKeys };
|
|
95
99
|
}
|
|
96
100
|
return { scope: "browser-write", targetUse: "browser", conservative: true };
|