@enterpriseai/cli 3.15.8 → 3.15.10
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 +38 -10
- package/dist/commands/deploy.js +1 -1
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +18 -13
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +59 -12
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +12 -3
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/ai-surfaces.d.ts +242 -6
- package/dist/lib/ai-surfaces.d.ts.map +1 -1
- package/dist/lib/ai-surfaces.js +3103 -174
- package/dist/lib/ai-surfaces.js.map +1 -1
- package/dist/lib/gofer-installer.js +1 -1
- package/dist/lib/update-maintenance.d.ts +6 -0
- package/dist/lib/update-maintenance.d.ts.map +1 -1
- package/dist/lib/update-maintenance.js +17 -2
- package/dist/lib/update-maintenance.js.map +1 -1
- package/package.json +3 -3
- package/resources/gofer/.gofer-version +7 -1
- package/resources/gofer/bash-scripts/install-optional-tools.sh +357 -16
- package/resources/gofer/copilot-instructions/typescript.instructions.md +2 -2
- package/resources/gofer/powershell-scripts/install-optional-tools.ps1 +419 -11
package/dist/lib/ai-surfaces.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
-
import { mkdir, readFile, rename,
|
|
1
|
+
import { constants as fsConstants, closeSync, existsSync, fstatSync, lstatSync, opendirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync, } from 'node:fs';
|
|
2
|
+
import { mkdir, open, readFile, rename, rm } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { basename, dirname, join, resolve, win32 as win32Path } from 'node:path';
|
|
4
|
+
import { basename, delimiter, dirname, join, relative, resolve, win32 as win32Path, } from 'node:path';
|
|
5
5
|
import { spawn, spawnSync } from 'node:child_process';
|
|
6
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
6
7
|
export const EAI_FIRST_PROMPT = [
|
|
7
8
|
'Use the repository EAI skill to start this app.',
|
|
8
9
|
'Ask me for the business outcome first.',
|
|
@@ -10,7 +11,197 @@ export const EAI_FIRST_PROMPT = [
|
|
|
10
11
|
'Keep internal numbered delivery stages hidden.',
|
|
11
12
|
'Pause for my approval of the business specification, then continue unless a material business, security, cost, deployment, or destructive decision needs approval.',
|
|
12
13
|
].join(' ');
|
|
13
|
-
|
|
14
|
+
const AI_SURFACE_DEFINITIONS = [
|
|
15
|
+
{
|
|
16
|
+
id: 'vscode-copilot',
|
|
17
|
+
name: 'GitHub Copilot in VS Code',
|
|
18
|
+
provider: 'GitHub',
|
|
19
|
+
kind: 'editor',
|
|
20
|
+
installUrl: 'https://code.visualstudio.com/docs/copilot/setup',
|
|
21
|
+
launchSupport: 'project-and-prompt',
|
|
22
|
+
commands: ['code'],
|
|
23
|
+
macApplications: ['/Applications/Visual Studio Code.app'],
|
|
24
|
+
windowsProgramFilesApplications: ['Microsoft VS Code/Code.exe'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'copilot-cli',
|
|
28
|
+
name: 'GitHub Copilot CLI',
|
|
29
|
+
provider: 'GitHub',
|
|
30
|
+
kind: 'cli',
|
|
31
|
+
installUrl: 'https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/install-copilot-cli',
|
|
32
|
+
launchSupport: 'project-and-prompt',
|
|
33
|
+
commands: ['copilot'],
|
|
34
|
+
macExecutables: ['.local/bin/copilot'],
|
|
35
|
+
windowsExecutables: [
|
|
36
|
+
'AppData/Local/Microsoft/WinGet/Links/copilot.exe',
|
|
37
|
+
'AppData/Roaming/npm/copilot.cmd',
|
|
38
|
+
],
|
|
39
|
+
linuxExecutables: ['.local/bin/copilot'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'copilot-desktop',
|
|
43
|
+
name: 'GitHub Copilot app',
|
|
44
|
+
provider: 'GitHub',
|
|
45
|
+
kind: 'desktop',
|
|
46
|
+
installUrl: 'https://docs.github.com/en/copilot/get-started/quickstart-copilot-app',
|
|
47
|
+
launchSupport: 'manual-project',
|
|
48
|
+
commands: [],
|
|
49
|
+
macApplications: ['/Applications/GitHub Copilot.app'],
|
|
50
|
+
windowsApplications: ['AppData/Local/Programs/GitHub Copilot/github.exe'],
|
|
51
|
+
windowsProgramFilesApplications: ['GitHub Copilot/github.exe'],
|
|
52
|
+
linuxApplications: ['/usr/bin/github'],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'antigravity-desktop',
|
|
56
|
+
name: 'Google Antigravity 2.0',
|
|
57
|
+
provider: 'Google',
|
|
58
|
+
kind: 'desktop',
|
|
59
|
+
installUrl: 'https://antigravity.google/download',
|
|
60
|
+
launchSupport: 'manual-project',
|
|
61
|
+
commands: [],
|
|
62
|
+
macApplications: ['/Applications/Antigravity.app'],
|
|
63
|
+
windowsApplications: ['AppData/Local/Programs/antigravity/Antigravity.exe'],
|
|
64
|
+
linuxApplications: [
|
|
65
|
+
'Applications/Antigravity-arm64/antigravity',
|
|
66
|
+
'Applications/Antigravity-x64/antigravity',
|
|
67
|
+
'Downloads/Antigravity-arm64/antigravity',
|
|
68
|
+
'Downloads/Antigravity-x64/antigravity',
|
|
69
|
+
'/opt/Antigravity-arm64/antigravity',
|
|
70
|
+
'/opt/Antigravity-x64/antigravity',
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'antigravity-cli',
|
|
75
|
+
name: 'Antigravity CLI (agy)',
|
|
76
|
+
provider: 'Google',
|
|
77
|
+
kind: 'cli',
|
|
78
|
+
installUrl: 'https://antigravity.google/docs/cli/install/',
|
|
79
|
+
launchSupport: 'project-and-prompt',
|
|
80
|
+
commands: ['agy'],
|
|
81
|
+
macExecutables: ['.local/bin/agy'],
|
|
82
|
+
windowsExecutables: ['AppData/Local/agy/bin/agy.exe'],
|
|
83
|
+
linuxExecutables: ['.local/bin/agy'],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'claude-desktop',
|
|
87
|
+
name: 'Claude Desktop',
|
|
88
|
+
provider: 'Anthropic',
|
|
89
|
+
kind: 'desktop',
|
|
90
|
+
installUrl: 'https://claude.com/download',
|
|
91
|
+
launchSupport: 'project-and-prompt',
|
|
92
|
+
commands: [],
|
|
93
|
+
macApplications: ['/Applications/Claude.app'],
|
|
94
|
+
windowsApplications: [
|
|
95
|
+
'AppData/Local/AnthropicClaude/Claude.exe',
|
|
96
|
+
'AppData/Local/Programs/Claude/Claude.exe',
|
|
97
|
+
],
|
|
98
|
+
windowsAppxPackageNames: ['Claude'],
|
|
99
|
+
linuxApplications: ['/usr/bin/claude-desktop', '/usr/lib/claude-desktop/claude-desktop'],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'claude-cli',
|
|
103
|
+
name: 'Claude Code',
|
|
104
|
+
provider: 'Anthropic',
|
|
105
|
+
kind: 'cli',
|
|
106
|
+
installUrl: 'https://code.claude.com/docs/en/setup',
|
|
107
|
+
launchSupport: 'project-and-prompt',
|
|
108
|
+
commands: ['claude'],
|
|
109
|
+
macExecutables: ['.local/bin/claude'],
|
|
110
|
+
windowsExecutables: ['.local/bin/claude.exe'],
|
|
111
|
+
linuxExecutables: ['.local/bin/claude'],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'codex-desktop',
|
|
115
|
+
name: 'ChatGPT desktop (Codex)',
|
|
116
|
+
provider: 'OpenAI',
|
|
117
|
+
kind: 'desktop',
|
|
118
|
+
installUrl: 'https://learn.chatgpt.com/docs/app',
|
|
119
|
+
launchSupport: 'manual-project',
|
|
120
|
+
commands: [],
|
|
121
|
+
macApplications: ['/Applications/ChatGPT.app', '/Applications/Codex.app'],
|
|
122
|
+
windowsApplications: [
|
|
123
|
+
'AppData/Local/Programs/ChatGPT/ChatGPT.exe',
|
|
124
|
+
'AppData/Local/Programs/Codex/Codex.exe',
|
|
125
|
+
],
|
|
126
|
+
windowsAppxPackageNames: ['OpenAI.Codex'],
|
|
127
|
+
linuxApplications: ['/usr/bin/chatgpt', '/usr/lib/chatgpt/ChatGPT'],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'codex-cli',
|
|
131
|
+
name: 'Codex CLI',
|
|
132
|
+
provider: 'OpenAI',
|
|
133
|
+
kind: 'cli',
|
|
134
|
+
installUrl: 'https://learn.chatgpt.com/docs/codex/cli',
|
|
135
|
+
launchSupport: 'project-and-prompt',
|
|
136
|
+
commands: ['codex'],
|
|
137
|
+
macExecutables: ['.local/bin/codex'],
|
|
138
|
+
windowsExecutables: ['AppData/Local/Programs/OpenAI/Codex/bin/codex.exe'],
|
|
139
|
+
linuxExecutables: ['.local/bin/codex'],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'grok-bot',
|
|
143
|
+
name: 'Grok Bot',
|
|
144
|
+
provider: 'xAI',
|
|
145
|
+
kind: 'desktop',
|
|
146
|
+
installUrl: 'https://x.ai/bot',
|
|
147
|
+
launchSupport: 'launch-only',
|
|
148
|
+
commands: [],
|
|
149
|
+
macApplications: ['/Applications/Grok Bot.app'],
|
|
150
|
+
windowsApplications: ['AppData/Local/Programs/Grok Bot/Grok Bot.exe'],
|
|
151
|
+
linuxApplications: ['/opt/Grok Bot/grok-bot', '/usr/bin/grok-bot'],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'grok-cli',
|
|
155
|
+
name: 'Grok Build',
|
|
156
|
+
provider: 'xAI',
|
|
157
|
+
kind: 'cli',
|
|
158
|
+
installUrl: 'https://x.ai/build',
|
|
159
|
+
launchSupport: 'project-and-prompt',
|
|
160
|
+
commands: ['grok'],
|
|
161
|
+
macExecutables: ['.grok/bin/grok'],
|
|
162
|
+
windowsExecutables: ['.grok/bin/grok.exe'],
|
|
163
|
+
linuxExecutables: ['.grok/bin/grok'],
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
const V2_AI_SURFACE_IDS = [
|
|
167
|
+
'vscode-copilot',
|
|
168
|
+
'copilot-desktop',
|
|
169
|
+
'antigravity-desktop',
|
|
170
|
+
'claude-desktop',
|
|
171
|
+
'codex-desktop',
|
|
172
|
+
'grok-bot',
|
|
173
|
+
'copilot-cli',
|
|
174
|
+
'antigravity-cli',
|
|
175
|
+
'claude-cli',
|
|
176
|
+
'codex-cli',
|
|
177
|
+
'grok-cli',
|
|
178
|
+
];
|
|
179
|
+
const AI_SURFACE_DEFINITIONS_BY_ID = new Map(AI_SURFACE_DEFINITIONS.map((definition) => [definition.id, definition]));
|
|
180
|
+
if (AI_SURFACE_DEFINITIONS_BY_ID.size !== V2_AI_SURFACE_IDS.length
|
|
181
|
+
|| AI_SURFACE_DEFINITIONS.length !== V2_AI_SURFACE_IDS.length) {
|
|
182
|
+
throw new Error('EAI AI surface definitions must contain each canonical v2 surface exactly once.');
|
|
183
|
+
}
|
|
184
|
+
export const AI_SURFACES = Object.freeze(V2_AI_SURFACE_IDS.map((id) => {
|
|
185
|
+
const definition = AI_SURFACE_DEFINITIONS_BY_ID.get(id);
|
|
186
|
+
if (!definition)
|
|
187
|
+
throw new Error(`EAI AI surface definition is missing: ${id}`);
|
|
188
|
+
for (const values of [
|
|
189
|
+
definition.commands,
|
|
190
|
+
definition.macApplications,
|
|
191
|
+
definition.windowsApplications,
|
|
192
|
+
definition.windowsProgramFilesApplications,
|
|
193
|
+
definition.windowsAppxPackageNames,
|
|
194
|
+
definition.linuxApplications,
|
|
195
|
+
definition.macExecutables,
|
|
196
|
+
definition.windowsExecutables,
|
|
197
|
+
definition.linuxExecutables,
|
|
198
|
+
]) {
|
|
199
|
+
if (values)
|
|
200
|
+
Object.freeze(values);
|
|
201
|
+
}
|
|
202
|
+
return Object.freeze(definition);
|
|
203
|
+
}));
|
|
204
|
+
const LEGACY_AI_SURFACES = [
|
|
14
205
|
{
|
|
15
206
|
id: 'vscode-copilot',
|
|
16
207
|
name: 'GitHub Copilot in VS Code',
|
|
@@ -54,7 +245,10 @@ export const AI_SURFACES = [
|
|
|
54
245
|
launchSupport: 'project-and-prompt',
|
|
55
246
|
commands: [],
|
|
56
247
|
macApplications: ['/Applications/Claude.app'],
|
|
57
|
-
windowsApplications: [
|
|
248
|
+
windowsApplications: [
|
|
249
|
+
'AppData/Local/AnthropicClaude/Claude.exe',
|
|
250
|
+
'AppData/Local/Programs/Claude/Claude.exe',
|
|
251
|
+
],
|
|
58
252
|
},
|
|
59
253
|
{
|
|
60
254
|
id: 'claude-cli',
|
|
@@ -101,194 +295,2328 @@ export const AI_SURFACES = [
|
|
|
101
295
|
function preferencesPath(home = homedir()) {
|
|
102
296
|
return join(home, '.eai', 'preferences.json');
|
|
103
297
|
}
|
|
104
|
-
export async function readAiPreferences(home = homedir()) {
|
|
298
|
+
export async function readAiPreferences(home = homedir()) {
|
|
299
|
+
try {
|
|
300
|
+
const parsed = JSON.parse(await readFile(preferencesPath(home), 'utf8'));
|
|
301
|
+
const validSurface = AI_SURFACES.some((surface) => surface.id === parsed.lastAiSurface);
|
|
302
|
+
return { version: 1, ...(validSurface ? { lastAiSurface: parsed.lastAiSurface } : {}) };
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return { version: 1 };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
export async function rememberAiSurface(surfaceId, home = homedir()) {
|
|
309
|
+
if (!AI_SURFACE_DEFINITIONS_BY_ID.has(surfaceId)) {
|
|
310
|
+
throw new Error(`Unknown AI surface: ${String(surfaceId)}`);
|
|
311
|
+
}
|
|
312
|
+
const target = preferencesPath(home);
|
|
313
|
+
const preferencesDirectory = dirname(target);
|
|
314
|
+
try {
|
|
315
|
+
await mkdir(preferencesDirectory, { mode: 0o700 });
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
if (error.code !== 'EEXIST')
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
const directoryStatus = lstatSync(preferencesDirectory);
|
|
322
|
+
const currentUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
323
|
+
if (!directoryStatus.isDirectory()
|
|
324
|
+
|| directoryStatus.isSymbolicLink()
|
|
325
|
+
|| (process.platform !== 'win32'
|
|
326
|
+
&& ((currentUid !== null && directoryStatus.uid !== currentUid)
|
|
327
|
+
|| (directoryStatus.mode & 0o022) !== 0))) {
|
|
328
|
+
throw new Error('EAI refused to write preferences through an untrusted directory.');
|
|
329
|
+
}
|
|
330
|
+
const temporary = join(preferencesDirectory, `.preferences.${process.pid}.${randomBytes(16).toString('hex')}.tmp`);
|
|
331
|
+
let handle = null;
|
|
332
|
+
try {
|
|
333
|
+
handle = await open(temporary, 'wx', 0o600);
|
|
334
|
+
await handle.writeFile(`${JSON.stringify({ version: 1, lastAiSurface: surfaceId }, null, 2)}\n`, { encoding: 'utf8' });
|
|
335
|
+
await handle.sync();
|
|
336
|
+
await handle.close();
|
|
337
|
+
handle = null;
|
|
338
|
+
await rename(temporary, target);
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
await handle?.close().catch(() => undefined);
|
|
342
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function windowsEnvironmentValue(environment, name) {
|
|
346
|
+
const exact = environment[name];
|
|
347
|
+
if (exact)
|
|
348
|
+
return exact;
|
|
349
|
+
const key = Object.keys(environment).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
350
|
+
return key ? environment[key] : undefined;
|
|
351
|
+
}
|
|
352
|
+
const WINDOWS_CMD_META_CHARACTERS = /([()\][%!^"`<>&|;, *?])/g;
|
|
353
|
+
const WINDOWS_CMD = 'C:\\Windows\\System32\\cmd.exe';
|
|
354
|
+
function escapeWindowsCmdCommand(value) {
|
|
355
|
+
return value.replace(WINDOWS_CMD_META_CHARACTERS, '^$1');
|
|
356
|
+
}
|
|
357
|
+
function escapeWindowsCmdArgument(value) {
|
|
358
|
+
// Match CommandLineToArgvW quoting first, then protect every cmd.exe metacharacter.
|
|
359
|
+
// This is the same non-shell interpolation strategy used by established Windows
|
|
360
|
+
// process launchers: cmd.exe receives one already-escaped command string and Node
|
|
361
|
+
// is told not to quote that string again.
|
|
362
|
+
let escaped = value.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"');
|
|
363
|
+
escaped = escaped.replace(/(?=(\\+?)?)\1$/, '$1$1');
|
|
364
|
+
escaped = `"${escaped}"`;
|
|
365
|
+
return escaped.replace(WINDOWS_CMD_META_CHARACTERS, '^$1');
|
|
366
|
+
}
|
|
367
|
+
export function buildWindowsCommandInvocation(command, args, action, _environment = process.env) {
|
|
368
|
+
if ([command, ...args].some((value) => /[\0\r\n]/.test(value))) {
|
|
369
|
+
throw new Error('EAI refused to execute a Windows command containing control characters.');
|
|
370
|
+
}
|
|
371
|
+
const normalizedCommand = win32Path.normalize(command);
|
|
372
|
+
const escapedCommand = /\.(?:cmd|bat)$/i.test(normalizedCommand)
|
|
373
|
+
? escapeWindowsCmdCommand(normalizedCommand)
|
|
374
|
+
: escapeWindowsCmdArgument(normalizedCommand);
|
|
375
|
+
const shellCommand = [escapedCommand, ...args.map(escapeWindowsCmdArgument)].join(' ');
|
|
376
|
+
return {
|
|
377
|
+
command: WINDOWS_CMD,
|
|
378
|
+
args: ['/d', '/v:off', '/s', action, `"${shellCommand}"`],
|
|
379
|
+
windowsVerbatimArguments: true,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
export function buildCommandOutputInvocation(command, args, platform = process.platform, environment = process.env) {
|
|
383
|
+
if (platform !== 'win32' || !/\.(?:cmd|bat)$/i.test(command)) {
|
|
384
|
+
return { command, args: [...args] };
|
|
385
|
+
}
|
|
386
|
+
return buildWindowsCommandInvocation(command, args, '/c', environment);
|
|
387
|
+
}
|
|
388
|
+
function defaultCommandPath(command) {
|
|
389
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(command))
|
|
390
|
+
return null;
|
|
391
|
+
const pathValue = process.env.PATH ?? '';
|
|
392
|
+
const extensions = process.platform === 'win32'
|
|
393
|
+
? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
|
|
394
|
+
: [''];
|
|
395
|
+
const commandHasExtension = process.platform === 'win32' && /\.[A-Za-z0-9]+$/.test(command);
|
|
396
|
+
for (const directory of pathValue.split(delimiter)) {
|
|
397
|
+
if (!directory)
|
|
398
|
+
continue;
|
|
399
|
+
for (const extension of commandHasExtension ? [''] : extensions) {
|
|
400
|
+
const candidate = process.platform === 'win32'
|
|
401
|
+
? win32Path.resolve(directory, `${command}${extension}`)
|
|
402
|
+
: resolve(directory, command);
|
|
403
|
+
try {
|
|
404
|
+
const status = statSync(candidate);
|
|
405
|
+
if (!status.isFile())
|
|
406
|
+
continue;
|
|
407
|
+
if (process.platform !== 'win32' && (status.mode & 0o111) === 0)
|
|
408
|
+
continue;
|
|
409
|
+
return realpathSync(candidate);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// PATH entries are untrusted input. A missing or inaccessible entry is
|
|
413
|
+
// simply not a detected command.
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
function defaultFileStatus(path) {
|
|
420
|
+
try {
|
|
421
|
+
const status = lstatSync(path);
|
|
422
|
+
return {
|
|
423
|
+
isFile: status.isFile(),
|
|
424
|
+
executable: process.platform === 'win32' || (status.mode & 0o111) !== 0,
|
|
425
|
+
mode: status.mode,
|
|
426
|
+
size: status.size,
|
|
427
|
+
isSymbolicLink: status.isSymbolicLink(),
|
|
428
|
+
uid: status.uid,
|
|
429
|
+
gid: status.gid,
|
|
430
|
+
device: status.dev,
|
|
431
|
+
inode: status.ino,
|
|
432
|
+
modifiedTimeMs: status.mtimeMs,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function defaultFileHeader(path, length) {
|
|
440
|
+
if (!Number.isSafeInteger(length) || length <= 0 || length > 4096)
|
|
441
|
+
return null;
|
|
442
|
+
let descriptor = null;
|
|
443
|
+
try {
|
|
444
|
+
const flags = process.platform === 'win32'
|
|
445
|
+
? fsConstants.O_RDONLY
|
|
446
|
+
: fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW;
|
|
447
|
+
descriptor = openSync(path, flags);
|
|
448
|
+
const status = fstatSync(descriptor);
|
|
449
|
+
if (!status.isFile() || (process.platform !== 'win32' && (status.mode & 0o022) !== 0))
|
|
450
|
+
return null;
|
|
451
|
+
const bytes = Buffer.alloc(length);
|
|
452
|
+
const bytesRead = readSync(descriptor, bytes, 0, length, 0);
|
|
453
|
+
return bytes.subarray(0, bytesRead);
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
if (descriptor !== null)
|
|
460
|
+
closeSync(descriptor);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function defaultFileSha256(path) {
|
|
464
|
+
const buffer = Buffer.alloc(1024 * 1024);
|
|
465
|
+
const hash = createHash('sha256');
|
|
466
|
+
let descriptor = null;
|
|
467
|
+
try {
|
|
468
|
+
const flags = process.platform === 'win32'
|
|
469
|
+
? fsConstants.O_RDONLY
|
|
470
|
+
: fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW;
|
|
471
|
+
descriptor = openSync(path, flags);
|
|
472
|
+
const status = fstatSync(descriptor);
|
|
473
|
+
if (!status.isFile() || (process.platform !== 'win32' && (status.mode & 0o022) !== 0))
|
|
474
|
+
return null;
|
|
475
|
+
let position = 0;
|
|
476
|
+
while (true) {
|
|
477
|
+
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, position);
|
|
478
|
+
if (bytesRead === 0)
|
|
479
|
+
break;
|
|
480
|
+
position += bytesRead;
|
|
481
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
482
|
+
}
|
|
483
|
+
return hash.digest('hex');
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
finally {
|
|
489
|
+
if (descriptor !== null)
|
|
490
|
+
closeSync(descriptor);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const MAX_CATALOG_TREE_FILES = 10_000;
|
|
494
|
+
const MAX_CATALOG_TREE_BYTES = 2 * 1024 * 1024 * 1024;
|
|
495
|
+
const MAX_CATALOG_TREE_FILE_BYTES = 1024 * 1024 * 1024;
|
|
496
|
+
const MAX_CATALOG_TREE_DIRECTORIES = 10_000;
|
|
497
|
+
const MAX_CATALOG_TREE_DEPTH = 64;
|
|
498
|
+
function boundedDirectoryEntries(path, maximumEntries) {
|
|
499
|
+
if (!Number.isSafeInteger(maximumEntries) || maximumEntries < 0)
|
|
500
|
+
return null;
|
|
501
|
+
const entries = [];
|
|
502
|
+
let directory = null;
|
|
503
|
+
try {
|
|
504
|
+
directory = opendirSync(path);
|
|
505
|
+
while (true) {
|
|
506
|
+
const entry = directory.readSync();
|
|
507
|
+
if (entry === null)
|
|
508
|
+
break;
|
|
509
|
+
if (entries.length >= maximumEntries)
|
|
510
|
+
return null;
|
|
511
|
+
entries.push(entry);
|
|
512
|
+
}
|
|
513
|
+
return entries;
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
try {
|
|
520
|
+
directory?.closeSync();
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// Reading to EOF may already have closed the directory handle.
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function catalogTreeNodeIsTrusted(path, expectDirectory) {
|
|
528
|
+
try {
|
|
529
|
+
const status = lstatSync(path);
|
|
530
|
+
if (status.isSymbolicLink()
|
|
531
|
+
|| (expectDirectory ? !status.isDirectory() : !status.isFile()))
|
|
532
|
+
return false;
|
|
533
|
+
if (process.platform === 'linux') {
|
|
534
|
+
// The Microsoft Debian package uses root:root files that may be
|
|
535
|
+
// group-writable. Group 0 is privileged, so reject any other owner/group
|
|
536
|
+
// and every world-writable node while accepting that exact package mode.
|
|
537
|
+
if (status.uid !== 0 || status.gid !== 0 || (status.mode & 0o002) !== 0)
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function catalogFileSha256(path) {
|
|
547
|
+
const hash = createHash('sha256');
|
|
548
|
+
const buffer = Buffer.alloc(1024 * 1024);
|
|
549
|
+
let descriptor = null;
|
|
550
|
+
try {
|
|
551
|
+
if (!catalogTreeNodeIsTrusted(path, false))
|
|
552
|
+
return null;
|
|
553
|
+
const before = lstatSync(path);
|
|
554
|
+
const flags = process.platform === 'win32'
|
|
555
|
+
? fsConstants.O_RDONLY
|
|
556
|
+
: fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW;
|
|
557
|
+
descriptor = openSync(path, flags);
|
|
558
|
+
const opened = fstatSync(descriptor);
|
|
559
|
+
if (!opened.isFile()
|
|
560
|
+
|| opened.dev !== before.dev
|
|
561
|
+
|| opened.ino !== before.ino
|
|
562
|
+
|| opened.size !== before.size
|
|
563
|
+
|| (process.platform === 'linux'
|
|
564
|
+
&& (opened.uid !== 0 || opened.gid !== 0 || (opened.mode & 0o002) !== 0)))
|
|
565
|
+
return null;
|
|
566
|
+
let position = 0;
|
|
567
|
+
while (true) {
|
|
568
|
+
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, position);
|
|
569
|
+
if (bytesRead === 0)
|
|
570
|
+
break;
|
|
571
|
+
position += bytesRead;
|
|
572
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
573
|
+
}
|
|
574
|
+
const after = fstatSync(descriptor);
|
|
575
|
+
if (after.dev !== opened.dev
|
|
576
|
+
|| after.ino !== opened.ino
|
|
577
|
+
|| after.size !== opened.size
|
|
578
|
+
|| after.mtimeMs !== opened.mtimeMs)
|
|
579
|
+
return null;
|
|
580
|
+
return hash.digest('hex');
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
if (descriptor !== null)
|
|
587
|
+
closeSync(descriptor);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function defaultDirectoryTreeIdentity(path) {
|
|
591
|
+
if (process.platform !== 'win32' && process.platform !== 'linux')
|
|
592
|
+
return null;
|
|
593
|
+
let realRoot;
|
|
594
|
+
try {
|
|
595
|
+
if (!catalogTreeNodeIsTrusted(path, true))
|
|
596
|
+
return null;
|
|
597
|
+
realRoot = realpathSync(path);
|
|
598
|
+
if (realRoot !== resolve(path))
|
|
599
|
+
return null;
|
|
600
|
+
let ancestor = realRoot;
|
|
601
|
+
while (true) {
|
|
602
|
+
if (!catalogTreeNodeIsTrusted(ancestor, true))
|
|
603
|
+
return null;
|
|
604
|
+
const parent = dirname(ancestor);
|
|
605
|
+
if (parent === ancestor)
|
|
606
|
+
break;
|
|
607
|
+
ancestor = parent;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
const files = [];
|
|
614
|
+
const directories = [];
|
|
615
|
+
const pending = [{ path: realRoot, depth: 0 }];
|
|
616
|
+
let discoveredBytes = 0;
|
|
617
|
+
try {
|
|
618
|
+
while (pending.length > 0) {
|
|
619
|
+
const current = pending.pop();
|
|
620
|
+
if (!current
|
|
621
|
+
|| current.depth > MAX_CATALOG_TREE_DEPTH
|
|
622
|
+
|| directories.length >= MAX_CATALOG_TREE_DIRECTORIES
|
|
623
|
+
|| !catalogTreeNodeIsTrusted(current.path, true))
|
|
624
|
+
return null;
|
|
625
|
+
const directoryStatus = lstatSync(current.path);
|
|
626
|
+
const maximumEntries = (MAX_CATALOG_TREE_FILES - files.length)
|
|
627
|
+
+ (MAX_CATALOG_TREE_DIRECTORIES - directories.length - 1);
|
|
628
|
+
const entries = boundedDirectoryEntries(current.path, maximumEntries);
|
|
629
|
+
if (!entries)
|
|
630
|
+
return null;
|
|
631
|
+
const entryNames = entries.map((entry) => entry.name).sort((left, right) => (Buffer.from(left).compare(Buffer.from(right))));
|
|
632
|
+
directories.push({
|
|
633
|
+
path: current.path,
|
|
634
|
+
entries: entryNames,
|
|
635
|
+
device: directoryStatus.dev,
|
|
636
|
+
inode: directoryStatus.ino,
|
|
637
|
+
});
|
|
638
|
+
for (const entry of entries) {
|
|
639
|
+
const candidate = join(current.path, entry.name);
|
|
640
|
+
if (entry.isSymbolicLink())
|
|
641
|
+
return null;
|
|
642
|
+
if (entry.isDirectory()) {
|
|
643
|
+
pending.push({ path: candidate, depth: current.depth + 1 });
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
if (!entry.isFile() || !catalogTreeNodeIsTrusted(candidate, false))
|
|
647
|
+
return null;
|
|
648
|
+
const relativePath = relative(realRoot, candidate).split('\\').join('/');
|
|
649
|
+
if (!relativePath
|
|
650
|
+
|| relativePath.startsWith('../')
|
|
651
|
+
|| /[\0\r\n]/.test(relativePath))
|
|
652
|
+
return null;
|
|
653
|
+
const status = lstatSync(candidate);
|
|
654
|
+
if (!Number.isSafeInteger(status.size)
|
|
655
|
+
|| status.size < 0
|
|
656
|
+
|| status.size > MAX_CATALOG_TREE_FILE_BYTES
|
|
657
|
+
|| files.length >= MAX_CATALOG_TREE_FILES
|
|
658
|
+
|| discoveredBytes > MAX_CATALOG_TREE_BYTES - status.size)
|
|
659
|
+
return null;
|
|
660
|
+
discoveredBytes += status.size;
|
|
661
|
+
files.push({ path: candidate, relativePath, size: status.size });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
files.sort((left, right) => Buffer.from(left.relativePath).compare(Buffer.from(right.relativePath)));
|
|
669
|
+
const aggregate = createHash('sha256');
|
|
670
|
+
let totalBytes = 0;
|
|
671
|
+
for (const file of files) {
|
|
672
|
+
const sha256 = catalogFileSha256(file.path);
|
|
673
|
+
if (!sha256)
|
|
674
|
+
return null;
|
|
675
|
+
totalBytes += file.size;
|
|
676
|
+
if (!Number.isSafeInteger(totalBytes) || totalBytes > MAX_CATALOG_TREE_BYTES)
|
|
677
|
+
return null;
|
|
678
|
+
aggregate.update(file.relativePath);
|
|
679
|
+
aggregate.update('\0');
|
|
680
|
+
aggregate.update(String(file.size));
|
|
681
|
+
aggregate.update('\0');
|
|
682
|
+
aggregate.update(sha256);
|
|
683
|
+
aggregate.update('\n');
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
for (const directory of directories) {
|
|
687
|
+
if (!catalogTreeNodeIsTrusted(directory.path, true))
|
|
688
|
+
return null;
|
|
689
|
+
const status = lstatSync(directory.path);
|
|
690
|
+
if (status.dev !== directory.device || status.ino !== directory.inode)
|
|
691
|
+
return null;
|
|
692
|
+
const relisted = boundedDirectoryEntries(directory.path, directory.entries.length);
|
|
693
|
+
if (!relisted)
|
|
694
|
+
return null;
|
|
695
|
+
const entries = relisted.map((entry) => entry.name)
|
|
696
|
+
.sort((left, right) => Buffer.from(left).compare(Buffer.from(right)));
|
|
697
|
+
if (entries.length !== directory.entries.length
|
|
698
|
+
|| entries.some((entry, index) => entry !== directory.entries[index]))
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
realPath: realRoot,
|
|
707
|
+
fileCount: files.length,
|
|
708
|
+
totalBytes,
|
|
709
|
+
sha256: aggregate.digest('hex'),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
const POSIX_METADATA_HELPERS = new Set([
|
|
713
|
+
'/usr/bin/codesign',
|
|
714
|
+
'/usr/bin/dpkg',
|
|
715
|
+
'/usr/bin/dpkg-query',
|
|
716
|
+
'/usr/bin/plutil',
|
|
717
|
+
'/usr/bin/xdg-mime',
|
|
718
|
+
]);
|
|
719
|
+
function runPosixMetadataHelper(command, args) {
|
|
720
|
+
if (!POSIX_METADATA_HELPERS.has(command))
|
|
721
|
+
return null;
|
|
722
|
+
try {
|
|
723
|
+
const status = statSync(command);
|
|
724
|
+
if (!status.isFile() || status.uid !== 0 || (status.mode & 0o022) !== 0)
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
const result = spawnSync(command, [...args], {
|
|
731
|
+
encoding: 'utf8',
|
|
732
|
+
windowsHide: true,
|
|
733
|
+
timeout: 5000,
|
|
734
|
+
env: { PATH: '/usr/bin:/bin:/usr/sbin:/sbin', LANG: 'C', LC_ALL: 'C' },
|
|
735
|
+
});
|
|
736
|
+
if (result.status !== 0)
|
|
737
|
+
return null;
|
|
738
|
+
return { stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
739
|
+
}
|
|
740
|
+
function machOArchitectures(path) {
|
|
741
|
+
const headerBytes = defaultFileHeader(path, 4096);
|
|
742
|
+
if (!headerBytes || headerBytes.length < 8)
|
|
743
|
+
return null;
|
|
744
|
+
const header = Buffer.from(headerBytes.buffer, headerBytes.byteOffset, headerBytes.byteLength);
|
|
745
|
+
const architectures = [];
|
|
746
|
+
const addCpuType = (cpuType) => {
|
|
747
|
+
if (cpuType === 0x01000007)
|
|
748
|
+
architectures.push('x64');
|
|
749
|
+
else if (cpuType === 0x0100000c)
|
|
750
|
+
architectures.push('arm64');
|
|
751
|
+
else
|
|
752
|
+
return false;
|
|
753
|
+
return true;
|
|
754
|
+
};
|
|
755
|
+
const magic = header.readUInt32BE(0);
|
|
756
|
+
if (magic === 0xfeedfacf || magic === 0xfeedface) {
|
|
757
|
+
if (!addCpuType(header.readUInt32BE(4)))
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
else if (magic === 0xcffaedfe || magic === 0xcefaedfe) {
|
|
761
|
+
if (!addCpuType(header.readUInt32LE(4)))
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
else if (magic === 0xcafebabe
|
|
765
|
+
|| magic === 0xbebafeca
|
|
766
|
+
|| magic === 0xcafebabf
|
|
767
|
+
|| magic === 0xbfbafeca) {
|
|
768
|
+
const littleEndian = magic === 0xbebafeca || magic === 0xbfbafeca;
|
|
769
|
+
const recordSize = magic === 0xcafebabf || magic === 0xbfbafeca ? 32 : 20;
|
|
770
|
+
const readUInt32 = (offset) => littleEndian
|
|
771
|
+
? header.readUInt32LE(offset)
|
|
772
|
+
: header.readUInt32BE(offset);
|
|
773
|
+
const count = readUInt32(4);
|
|
774
|
+
if (count < 1 || count > 64 || 8 + count * recordSize > header.length)
|
|
775
|
+
return null;
|
|
776
|
+
for (let index = 0; index < count; index += 1) {
|
|
777
|
+
if (!addCpuType(readUInt32(8 + index * recordSize)))
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
return [...new Set(architectures)];
|
|
785
|
+
}
|
|
786
|
+
function defaultMacApplicationIdentity(path, expected) {
|
|
787
|
+
if (process.platform !== 'darwin')
|
|
788
|
+
return null;
|
|
789
|
+
if (!expected
|
|
790
|
+
|| !/^[A-Za-z0-9.-]+$/.test(expected.bundleIdentifier)
|
|
791
|
+
|| !/^[A-Z0-9]{10}$/.test(expected.teamIdentifier))
|
|
792
|
+
return null;
|
|
793
|
+
const applicationPath = trustedPosixDirectory(path);
|
|
794
|
+
if (!applicationPath?.endsWith('.app'))
|
|
795
|
+
return null;
|
|
796
|
+
const trustedDeveloperId = runPosixMetadataHelper('/usr/bin/codesign', [
|
|
797
|
+
'--verify',
|
|
798
|
+
'--deep',
|
|
799
|
+
'--strict',
|
|
800
|
+
'--test-requirement',
|
|
801
|
+
`=identifier "${expected.bundleIdentifier}" and anchor apple generic and certificate leaf[subject.OU] = "${expected.teamIdentifier}"`,
|
|
802
|
+
applicationPath,
|
|
803
|
+
]);
|
|
804
|
+
if (!trustedDeveloperId)
|
|
805
|
+
return null;
|
|
806
|
+
const infoPlist = join(applicationPath, 'Contents', 'Info.plist');
|
|
807
|
+
const plistJson = runPosixMetadataHelper('/usr/bin/plutil', [
|
|
808
|
+
'-convert',
|
|
809
|
+
'json',
|
|
810
|
+
'-o',
|
|
811
|
+
'-',
|
|
812
|
+
infoPlist,
|
|
813
|
+
])?.stdout;
|
|
814
|
+
let plist;
|
|
815
|
+
try {
|
|
816
|
+
plist = JSON.parse(plistJson ?? '');
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
const bundleIdentifier = typeof plist.CFBundleIdentifier === 'string'
|
|
822
|
+
? plist.CFBundleIdentifier
|
|
823
|
+
: null;
|
|
824
|
+
if (bundleIdentifier !== expected.bundleIdentifier)
|
|
825
|
+
return null;
|
|
826
|
+
const executableName = typeof plist.CFBundleExecutable === 'string'
|
|
827
|
+
? plist.CFBundleExecutable
|
|
828
|
+
: null;
|
|
829
|
+
if (!executableName || /[\\/\0\r\n]/.test(executableName))
|
|
830
|
+
return null;
|
|
831
|
+
let executablePath;
|
|
832
|
+
try {
|
|
833
|
+
executablePath = realpathSync(join(applicationPath, 'Contents', 'MacOS', executableName));
|
|
834
|
+
if (!executablePath.startsWith(`${applicationPath}/Contents/MacOS/`))
|
|
835
|
+
return null;
|
|
836
|
+
const status = statSync(executablePath);
|
|
837
|
+
if (!status.isFile() || (status.mode & 0o111) === 0 || (status.mode & 0o022) !== 0)
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
const architectures = machOArchitectures(executablePath);
|
|
844
|
+
if (!architectures)
|
|
845
|
+
return null;
|
|
846
|
+
const urlTypes = Array.isArray(plist.CFBundleURLTypes)
|
|
847
|
+
? plist.CFBundleURLTypes
|
|
848
|
+
: [];
|
|
849
|
+
const urlSchemes = urlTypes.flatMap((entry) => Array.isArray(entry.CFBundleURLSchemes)
|
|
850
|
+
? entry.CFBundleURLSchemes.filter((value) => typeof value === 'string')
|
|
851
|
+
: []);
|
|
852
|
+
return {
|
|
853
|
+
bundleIdentifier,
|
|
854
|
+
teamIdentifier: expected.teamIdentifier,
|
|
855
|
+
executablePath,
|
|
856
|
+
architectures,
|
|
857
|
+
urlSchemes,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function trustedPosixAncestors(path) {
|
|
861
|
+
if (process.platform === 'win32')
|
|
862
|
+
return false;
|
|
863
|
+
const currentUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
864
|
+
try {
|
|
865
|
+
let cursor = path;
|
|
866
|
+
while (true) {
|
|
867
|
+
const status = lstatSync(cursor);
|
|
868
|
+
if (status.isSymbolicLink()
|
|
869
|
+
|| (status.mode & 0o022) !== 0
|
|
870
|
+
|| (currentUid !== null && status.uid !== 0 && status.uid !== currentUid))
|
|
871
|
+
return false;
|
|
872
|
+
const parent = dirname(cursor);
|
|
873
|
+
if (parent === cursor)
|
|
874
|
+
break;
|
|
875
|
+
cursor = parent;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
function trustedPosixDirectory(path) {
|
|
884
|
+
if (process.platform === 'win32')
|
|
885
|
+
return null;
|
|
886
|
+
try {
|
|
887
|
+
const original = lstatSync(path);
|
|
888
|
+
if (!original.isDirectory() || original.isSymbolicLink())
|
|
889
|
+
return null;
|
|
890
|
+
const realPath = realpathSync(path);
|
|
891
|
+
return realPath === resolve(path) && trustedPosixAncestors(realPath) ? realPath : null;
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
function rootOwnedProtectedExecutable(path) {
|
|
898
|
+
if (process.platform === 'win32')
|
|
899
|
+
return false;
|
|
900
|
+
try {
|
|
901
|
+
const realPath = realpathSync(path);
|
|
902
|
+
if (realPath !== path)
|
|
903
|
+
return false;
|
|
904
|
+
let cursor = realPath;
|
|
905
|
+
while (true) {
|
|
906
|
+
const status = lstatSync(cursor);
|
|
907
|
+
if (status.isSymbolicLink() || status.uid !== 0 || (status.mode & 0o022) !== 0)
|
|
908
|
+
return false;
|
|
909
|
+
if (cursor === realPath && (!status.isFile() || (status.mode & 0o111) === 0))
|
|
910
|
+
return false;
|
|
911
|
+
const parent = dirname(cursor);
|
|
912
|
+
if (parent === cursor)
|
|
913
|
+
break;
|
|
914
|
+
cursor = parent;
|
|
915
|
+
}
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
catch {
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function trustedPosixFile(path) {
|
|
923
|
+
if (process.platform === 'win32')
|
|
924
|
+
return null;
|
|
925
|
+
let realPath;
|
|
926
|
+
try {
|
|
927
|
+
const original = lstatSync(path);
|
|
928
|
+
if (!original.isFile() || original.isSymbolicLink() || (original.mode & 0o111) === 0)
|
|
929
|
+
return null;
|
|
930
|
+
realPath = realpathSync(path);
|
|
931
|
+
if (realPath !== resolve(path) || !trustedPosixAncestors(realPath))
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
catch {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
const status = defaultFileStatus(realPath);
|
|
938
|
+
return status?.isFile && status.executable && !status.isSymbolicLink
|
|
939
|
+
? { realPath, status }
|
|
940
|
+
: null;
|
|
941
|
+
}
|
|
942
|
+
function defaultMacExecutableIdentity(path, expected) {
|
|
943
|
+
if (process.platform !== 'darwin'
|
|
944
|
+
|| !expected
|
|
945
|
+
|| !/^[A-Za-z0-9.-]+$/.test(expected.identifier)
|
|
946
|
+
|| !/^[A-Z0-9]{10}$/.test(expected.teamIdentifier))
|
|
947
|
+
return null;
|
|
948
|
+
const trustedFile = trustedPosixFile(path);
|
|
949
|
+
if (!trustedFile)
|
|
950
|
+
return null;
|
|
951
|
+
const verified = runPosixMetadataHelper('/usr/bin/codesign', [
|
|
952
|
+
'--verify',
|
|
953
|
+
'--strict',
|
|
954
|
+
'--test-requirement',
|
|
955
|
+
`=identifier "${expected.identifier}" and anchor apple generic and certificate leaf[subject.OU] = "${expected.teamIdentifier}"`,
|
|
956
|
+
trustedFile.realPath,
|
|
957
|
+
]);
|
|
958
|
+
const architectures = verified ? machOArchitectures(trustedFile.realPath) : null;
|
|
959
|
+
if (!architectures)
|
|
960
|
+
return null;
|
|
961
|
+
return {
|
|
962
|
+
identifier: expected.identifier,
|
|
963
|
+
teamIdentifier: expected.teamIdentifier,
|
|
964
|
+
architectures,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
const WINDOWS_POWERSHELL = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
|
|
968
|
+
const WINDOWS_REG = 'C:\\Windows\\System32\\reg.exe';
|
|
969
|
+
function runWindowsMetadataHelper(command, args, environment = {}, timeout = 5000) {
|
|
970
|
+
if (process.platform !== 'win32' || !existsSync(command))
|
|
971
|
+
return null;
|
|
972
|
+
const result = spawnSync(command, [...args], {
|
|
973
|
+
encoding: 'utf8',
|
|
974
|
+
windowsHide: true,
|
|
975
|
+
timeout,
|
|
976
|
+
env: {
|
|
977
|
+
SystemRoot: 'C:\\Windows',
|
|
978
|
+
WINDIR: 'C:\\Windows',
|
|
979
|
+
PATH: 'C:\\Windows\\System32;C:\\Windows',
|
|
980
|
+
...environment,
|
|
981
|
+
},
|
|
982
|
+
});
|
|
983
|
+
if (result.status !== 0)
|
|
984
|
+
return null;
|
|
985
|
+
return { stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
986
|
+
}
|
|
987
|
+
function defaultWindowsExecutableIdentity(path) {
|
|
988
|
+
if (process.platform !== 'win32')
|
|
989
|
+
return null;
|
|
990
|
+
const script = [
|
|
991
|
+
"$ErrorActionPreference='Stop'",
|
|
992
|
+
'$item = Get-Item -LiteralPath $env:EAI_SURFACE_EXECUTABLE',
|
|
993
|
+
'if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { exit 1 }',
|
|
994
|
+
'$ancestor = $item.Directory; while ($null -ne $ancestor) { if (($ancestor.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { exit 1 }; $ancestor = $ancestor.Parent }',
|
|
995
|
+
'$signature = Get-AuthenticodeSignature -LiteralPath $env:EAI_SURFACE_EXECUTABLE',
|
|
996
|
+
"if ($signature.Status -ne 'Valid' -or $null -eq $signature.SignerCertificate) { exit 1 }",
|
|
997
|
+
'$stream = [System.IO.File]::OpenRead($env:EAI_SURFACE_EXECUTABLE)',
|
|
998
|
+
'try { $reader = [System.IO.BinaryReader]::new($stream); if ($reader.ReadUInt16() -ne 0x5A4D) { exit 1 }; $stream.Position = 0x3C; $peOffset = $reader.ReadInt32(); $stream.Position = $peOffset; if ($reader.ReadUInt32() -ne 0x00004550) { exit 1 }; $machine = $reader.ReadUInt16() } finally { $stream.Dispose() }',
|
|
999
|
+
"$architecture = switch ($machine) { 0x8664 { 'x64' } 0xAA64 { 'arm64' } default { exit 1 } }",
|
|
1000
|
+
'[pscustomobject]@{ productName = [string]$item.VersionInfo.ProductName; companyName = [string]$item.VersionInfo.CompanyName; publisher = [string]$signature.SignerCertificate.Subject; architecture = $architecture } | ConvertTo-Json -Compress',
|
|
1001
|
+
].join('; ');
|
|
1002
|
+
const result = runWindowsMetadataHelper(WINDOWS_POWERSHELL, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], { EAI_SURFACE_EXECUTABLE: path });
|
|
1003
|
+
if (!result)
|
|
1004
|
+
return null;
|
|
1005
|
+
try {
|
|
1006
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
1007
|
+
if (typeof parsed.productName !== 'string'
|
|
1008
|
+
|| typeof parsed.companyName !== 'string'
|
|
1009
|
+
|| typeof parsed.publisher !== 'string'
|
|
1010
|
+
|| (parsed.architecture !== 'x64' && parsed.architecture !== 'arm64'))
|
|
1011
|
+
return null;
|
|
1012
|
+
return {
|
|
1013
|
+
productName: parsed.productName,
|
|
1014
|
+
companyName: parsed.companyName,
|
|
1015
|
+
publisher: parsed.publisher,
|
|
1016
|
+
architecture: parsed.architecture,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
catch {
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function defaultWindowsVsCodeInstallationAcl(path) {
|
|
1024
|
+
if (process.platform !== 'win32')
|
|
1025
|
+
return false;
|
|
1026
|
+
const script = [
|
|
1027
|
+
"$ErrorActionPreference='Stop'",
|
|
1028
|
+
"$trustedWriters=@('S-1-5-18','S-1-5-32-544','S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464')",
|
|
1029
|
+
"$trustedOwners=@('S-1-5-18','S-1-5-32-544','S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464')",
|
|
1030
|
+
'$writeMask=[int64]0x500D0156',
|
|
1031
|
+
'function Test-ProtectedNode($item,[bool]$requireProtected,[bool]$ignoreInheritOnly){ if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { exit 1 }; $acl=Get-Acl -LiteralPath $item.FullName; if (-not $acl.AreAccessRulesCanonical -or ($requireProtected -and -not $acl.AreAccessRulesProtected) -or $acl.Sddl -match "NO_ACCESS_CONTROL") { exit 1 }; $owner=$acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value; if ($trustedOwners -notcontains $owner) { exit 1 }; foreach($rule in $acl.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier])) { if ($rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or $trustedWriters -contains $rule.IdentityReference.Value) { continue }; if ($ignoreInheritOnly -and (($rule.PropagationFlags -band [System.Security.AccessControl.PropagationFlags]::InheritOnly) -ne 0)) { continue }; if (([int64]$rule.FileSystemRights -band $writeMask) -ne 0) { exit 1 } } }',
|
|
1032
|
+
'$programFiles=[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::ProgramFiles)',
|
|
1033
|
+
'$programFilesItem=Get-Item -Force -LiteralPath $programFiles',
|
|
1034
|
+
"$expected=[System.IO.Path]::GetFullPath((Join-Path $programFilesItem.FullName 'Microsoft VS Code')).TrimEnd([char]92)",
|
|
1035
|
+
'$root=Get-Item -Force -LiteralPath $env:EAI_VSCODE_INSTALLATION',
|
|
1036
|
+
'$rootPath=[System.IO.Path]::GetFullPath($root.FullName).TrimEnd([char]92)',
|
|
1037
|
+
'if (-not [string]::Equals($rootPath,$expected,[System.StringComparison]::OrdinalIgnoreCase)) { exit 1 }',
|
|
1038
|
+
'Test-ProtectedNode $programFilesItem $true $true',
|
|
1039
|
+
'Test-ProtectedNode $root $true $false',
|
|
1040
|
+
'$pending=[System.Collections.Generic.Stack[System.IO.DirectoryInfo]]::new(); $pending.Push($root); $count=0',
|
|
1041
|
+
'while($pending.Count -gt 0) { $directory=$pending.Pop(); foreach($item in Get-ChildItem -Force -LiteralPath $directory.FullName) { $count++; if ($count -gt 20000) { exit 1 }; Test-ProtectedNode $item $false $false; if ($item.PSIsContainer) { $pending.Push($item) } } }',
|
|
1042
|
+
"Write-Output 'ok'",
|
|
1043
|
+
].join('; ');
|
|
1044
|
+
const result = runWindowsMetadataHelper(WINDOWS_POWERSHELL, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], { EAI_VSCODE_INSTALLATION: path }, 20_000);
|
|
1045
|
+
return result?.stdout.trim() === 'ok';
|
|
1046
|
+
}
|
|
1047
|
+
function defaultWindowsAppxIdentity(packageName) {
|
|
1048
|
+
if (process.platform !== 'win32' || !/^[A-Za-z0-9.-]+$/.test(packageName))
|
|
1049
|
+
return null;
|
|
1050
|
+
const script = [
|
|
1051
|
+
"$ErrorActionPreference='Stop'",
|
|
1052
|
+
'$package = Get-AppxPackage -Name $env:EAI_SURFACE_PACKAGE | Sort-Object Version -Descending | Select-Object -First 1',
|
|
1053
|
+
'if ($null -eq $package) { exit 1 }',
|
|
1054
|
+
'$manifest = Get-AppxPackageManifest -Package $package.PackageFullName',
|
|
1055
|
+
'$applicationIds = @($manifest.Package.Applications.Application | ForEach-Object { [string]$_.Id })',
|
|
1056
|
+
'if ($applicationIds.Count -eq 0 -or @($applicationIds | Where-Object { $_ -notmatch "^[A-Za-z0-9._-]+$" }).Count -gt 0) { exit 1 }',
|
|
1057
|
+
'[pscustomobject]@{ packageName = [string]$package.Name; publisher = [string]$package.Publisher; publisherId = [string]$package.PublisherId; familyName = [string]$package.PackageFamilyName; applicationIds = [string[]]$applicationIds; architecture = [string]$package.Architecture; signatureKind = [string]$package.SignatureKind; status = [string]$package.Status } | ConvertTo-Json -Compress',
|
|
1058
|
+
].join('; ');
|
|
1059
|
+
const result = runWindowsMetadataHelper(WINDOWS_POWERSHELL, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], { EAI_SURFACE_PACKAGE: packageName });
|
|
1060
|
+
if (!result)
|
|
1061
|
+
return null;
|
|
1062
|
+
try {
|
|
1063
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
1064
|
+
const architecture = typeof parsed.architecture === 'string' ? parsed.architecture.toLowerCase() : '';
|
|
1065
|
+
const applicationIds = Array.isArray(parsed.applicationIds)
|
|
1066
|
+
? parsed.applicationIds.filter((value) => typeof value === 'string')
|
|
1067
|
+
: typeof parsed.applicationIds === 'string' ? [parsed.applicationIds] : [];
|
|
1068
|
+
if (typeof parsed.packageName !== 'string'
|
|
1069
|
+
|| typeof parsed.publisher !== 'string'
|
|
1070
|
+
|| typeof parsed.publisherId !== 'string'
|
|
1071
|
+
|| typeof parsed.familyName !== 'string'
|
|
1072
|
+
|| (architecture !== 'x64' && architecture !== 'arm64')
|
|
1073
|
+
|| (parsed.signatureKind !== 'Developer' && parsed.signatureKind !== 'Store')
|
|
1074
|
+
|| parsed.status !== 'Ok'
|
|
1075
|
+
|| applicationIds.length === 0)
|
|
1076
|
+
return null;
|
|
1077
|
+
return {
|
|
1078
|
+
packageName: parsed.packageName,
|
|
1079
|
+
publisher: parsed.publisher,
|
|
1080
|
+
publisherId: parsed.publisherId,
|
|
1081
|
+
familyName: parsed.familyName,
|
|
1082
|
+
applicationIds,
|
|
1083
|
+
architecture,
|
|
1084
|
+
signatureKind: parsed.signatureKind,
|
|
1085
|
+
status: 'Ok',
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function windowsRegistryExecutable(output) {
|
|
1093
|
+
const value = output
|
|
1094
|
+
.split(/\r?\n/)
|
|
1095
|
+
.map((line) => line.match(/\bREG_(?:EXPAND_)?SZ\s+(.+)$/i)?.[1]?.trim())
|
|
1096
|
+
.find((candidate) => Boolean(candidate));
|
|
1097
|
+
if (!value || /[\0\r\n]/.test(value))
|
|
1098
|
+
return null;
|
|
1099
|
+
const quoted = value.match(/^"([A-Za-z]:\\[^"<>|?*]+\.exe)"\s+(?:"%1"|%1)$/i);
|
|
1100
|
+
if (quoted)
|
|
1101
|
+
return win32Path.normalize(quoted[1]);
|
|
1102
|
+
const unquoted = value.match(/^([A-Za-z]:\\[^\s"<>|?*]+\.exe)\s+(?:"%1"|%1)$/i);
|
|
1103
|
+
return unquoted ? win32Path.normalize(unquoted[1]) : null;
|
|
1104
|
+
}
|
|
1105
|
+
function defaultWindowsUrlHandlerExecutable(scheme) {
|
|
1106
|
+
const result = runWindowsMetadataHelper(WINDOWS_REG, ['query', `HKCR\\${scheme}\\shell\\open\\command`, '/ve']);
|
|
1107
|
+
return result ? windowsRegistryExecutable(result.stdout) : null;
|
|
1108
|
+
}
|
|
1109
|
+
function nodeArchitectureFromLinux(value) {
|
|
1110
|
+
if (value === 'amd64' || value === 'x86_64')
|
|
1111
|
+
return 'x64';
|
|
1112
|
+
if (value === 'arm64' || value === 'aarch64')
|
|
1113
|
+
return 'arm64';
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
const COPILOT_LINUX_VERSION = '1.1.15';
|
|
1117
|
+
const COPILOT_LINUX_ORIGIN = `https://github.com/github/app/releases/tag/v${COPILOT_LINUX_VERSION}`;
|
|
1118
|
+
const VERIFIED_COPILOT_LINUX_DEBS = {
|
|
1119
|
+
arm64: {
|
|
1120
|
+
artifactSha256: '587445c4ad98917638204f6a66f15b1868b8795b317725cbed37fc635df07e10',
|
|
1121
|
+
executableSize: 969_511_224,
|
|
1122
|
+
executableSha256: 'accf69bea75cb15c2afbb88b9011fa98a0705d8196ace29c49fb802548b27f0c',
|
|
1123
|
+
packageManifestSize: 5_892,
|
|
1124
|
+
packageManifestSha256: '1b54b99c5bcfb24e17d4d3ba1e73ffc80c8985021ed185cb0ac857890c541236',
|
|
1125
|
+
},
|
|
1126
|
+
x64: {
|
|
1127
|
+
artifactSha256: 'cf884fd0b9e5418285e82318c4fe712d74955f66982bae6a5d5c43aa1f44233e',
|
|
1128
|
+
executableSize: 982_846_656,
|
|
1129
|
+
executableSha256: '6033d7d87d60e5e86f00cf79f87174392051e016e6fad439d0eb657e6a5fdc55',
|
|
1130
|
+
packageManifestSize: 5_892,
|
|
1131
|
+
packageManifestSha256: '12d527ed0e0d9dd3f73bc88f99f15b1e49765f2d3157ab0f159158685c9ad1ec',
|
|
1132
|
+
},
|
|
1133
|
+
};
|
|
1134
|
+
const VSCODE_CATALOG_VERSION = '1.136.1';
|
|
1135
|
+
const VSCODE_CATALOG_COMMIT = 'a44adf7f53e00964ab890f9f8758a334f1fc15bc';
|
|
1136
|
+
const VSCODE_CATALOG_COMMIT_DIRECTORY = VSCODE_CATALOG_COMMIT.slice(0, 10);
|
|
1137
|
+
const VSCODE_CATALOG_LAUNCH_ENVIRONMENT = Object.freeze({
|
|
1138
|
+
ELECTRON_RUN_AS_NODE: '1',
|
|
1139
|
+
VSCODE_DEV: '',
|
|
1140
|
+
VSCODE_IPC_HOOK_CLI: '',
|
|
1141
|
+
});
|
|
1142
|
+
const WINDOWS_VSCODE_INSTALLATION_ENTRIES = Object.freeze([
|
|
1143
|
+
VSCODE_CATALOG_COMMIT_DIRECTORY,
|
|
1144
|
+
'bin',
|
|
1145
|
+
'Code.exe',
|
|
1146
|
+
'Code.VisualElementsManifest.xml',
|
|
1147
|
+
'unins000.dat',
|
|
1148
|
+
'unins000.exe',
|
|
1149
|
+
'unins000.msg',
|
|
1150
|
+
]);
|
|
1151
|
+
const WINDOWS_VSCODE_UNINSTALLER_FILES = Object.freeze([
|
|
1152
|
+
'unins000.dat',
|
|
1153
|
+
'unins000.exe',
|
|
1154
|
+
'unins000.msg',
|
|
1155
|
+
]);
|
|
1156
|
+
const WINDOWS_VSCODE_BIN_ENTRIES = Object.freeze([
|
|
1157
|
+
'code',
|
|
1158
|
+
'code-tunnel.exe',
|
|
1159
|
+
'code.cmd',
|
|
1160
|
+
]);
|
|
1161
|
+
// Exact payloads provisioned by the EAI Setup ARM64 release harness. The
|
|
1162
|
+
// executable alone is insufficient for Electron: bind every application runtime
|
|
1163
|
+
// file, including sibling native libraries, so modified code cannot inherit a
|
|
1164
|
+
// valid executable signature. Update these values only from a newly verified
|
|
1165
|
+
// official asset.
|
|
1166
|
+
const VERIFIED_VSCODE_CATALOGS = {
|
|
1167
|
+
win32: {
|
|
1168
|
+
arm64: {
|
|
1169
|
+
artifactSha256: '57454d84d55f07b532fcf42295c57a5445054006be668ad7b1c19c9de9f68e31',
|
|
1170
|
+
executable: {
|
|
1171
|
+
size: 218_732_896,
|
|
1172
|
+
sha256: 'c8e8f54f217223f3d4adff4dbd1f529aa7386c3a1705dbe214ecf187b963423e',
|
|
1173
|
+
},
|
|
1174
|
+
applicationTree: {
|
|
1175
|
+
fileCount: 2_463,
|
|
1176
|
+
totalBytes: 782_690_305,
|
|
1177
|
+
sha256: '11009193bf07e51892a0ae9f6030188c7f8914e79ef4344e2f6a3a344807753f',
|
|
1178
|
+
},
|
|
1179
|
+
auxiliaryTree: {
|
|
1180
|
+
fileCount: 3,
|
|
1181
|
+
totalBytes: 25_558_542,
|
|
1182
|
+
sha256: '73fbdad4bf097af9408bdcbe75f4c5cc1741b88b9eedf9d946b338124457408b',
|
|
1183
|
+
},
|
|
1184
|
+
visualManifest: {
|
|
1185
|
+
size: 398,
|
|
1186
|
+
sha256: 'cff3bb59579080b4ac4e69fc8d936c35bbb41455f66f9188ed54f4e31882e68b',
|
|
1187
|
+
},
|
|
1188
|
+
packageJson: {
|
|
1189
|
+
size: 15_233,
|
|
1190
|
+
sha256: '18ce306138992d44d6c0537c386943b277621865d5161e1932e792b09ef766f0',
|
|
1191
|
+
},
|
|
1192
|
+
productJson: {
|
|
1193
|
+
size: 71_150,
|
|
1194
|
+
sha256: '4bdbecbf1cd1a700f4f738216bd0e6f08f561a52e1fd9ca915ea9ff9a6a7a7a6',
|
|
1195
|
+
},
|
|
1196
|
+
copilotPackageJson: {
|
|
1197
|
+
size: 200_790,
|
|
1198
|
+
sha256: '586aa5105751792bedcb7c3ef785d0c1cb1bac8c97812afd50f1349f0a6c1a09',
|
|
1199
|
+
},
|
|
1200
|
+
cliJs: {
|
|
1201
|
+
size: 291_407,
|
|
1202
|
+
sha256: '487137301c6d9ac59dc846a93c17e2f52ba98fdf1670c164dac24b4eb5acad61',
|
|
1203
|
+
},
|
|
1204
|
+
origin: `https://update.code.visualstudio.com/${VSCODE_CATALOG_VERSION}/win32-arm64/stable`,
|
|
1205
|
+
},
|
|
1206
|
+
},
|
|
1207
|
+
linux: {
|
|
1208
|
+
arm64: {
|
|
1209
|
+
artifactSha256: 'baa72f92d3feaa76d015202c57271475ea15809e635587279171a972cdd612a4',
|
|
1210
|
+
executable: {
|
|
1211
|
+
size: 212_536_608,
|
|
1212
|
+
sha256: '8872f37cb828d74498b0451ed923a141e5b144dfd9de9c8f41265bf90b0af98d',
|
|
1213
|
+
},
|
|
1214
|
+
applicationTree: {
|
|
1215
|
+
fileCount: 2_415,
|
|
1216
|
+
totalBytes: 1_035_373_322,
|
|
1217
|
+
sha256: '437e3d7f2338684372876807533f7d38c0e9b56bd8be9238b520dfc2a01ef8e6',
|
|
1218
|
+
},
|
|
1219
|
+
packageJson: {
|
|
1220
|
+
size: 15_266,
|
|
1221
|
+
sha256: 'ba6a64fd136466074831d91c20ec6d3dd110c127a84a8ac9b47cd4f075172ad9',
|
|
1222
|
+
},
|
|
1223
|
+
productJson: {
|
|
1224
|
+
size: 67_081,
|
|
1225
|
+
sha256: '0fb7ff5e7c78427f0601f864ec8adc8e163a384f8da1c49e9e99adbffaa7e547',
|
|
1226
|
+
},
|
|
1227
|
+
copilotPackageJson: {
|
|
1228
|
+
size: 200_790,
|
|
1229
|
+
sha256: '586aa5105751792bedcb7c3ef785d0c1cb1bac8c97812afd50f1349f0a6c1a09',
|
|
1230
|
+
},
|
|
1231
|
+
cliJs: {
|
|
1232
|
+
size: 291_436,
|
|
1233
|
+
sha256: 'c26b66d4031d946523eb9b951da9bc56aba16417e91d84534ce6367e815fe342',
|
|
1234
|
+
},
|
|
1235
|
+
packageVersion: '1.136.1-1788414014',
|
|
1236
|
+
origin: `https://update.code.visualstudio.com/${VSCODE_CATALOG_VERSION}/linux-deb-arm64/stable`,
|
|
1237
|
+
},
|
|
1238
|
+
},
|
|
1239
|
+
};
|
|
1240
|
+
function vsCodeCatalog(platform, architecture) {
|
|
1241
|
+
if (platform !== 'win32' && platform !== 'linux')
|
|
1242
|
+
return null;
|
|
1243
|
+
return VERIFIED_VSCODE_CATALOGS[platform]?.[architecture] ?? null;
|
|
1244
|
+
}
|
|
1245
|
+
function rootOwnedReadOnlyFileContent(path) {
|
|
1246
|
+
try {
|
|
1247
|
+
const realPath = realpathSync(path);
|
|
1248
|
+
if (realPath !== resolve(path))
|
|
1249
|
+
return null;
|
|
1250
|
+
let cursor = realPath;
|
|
1251
|
+
while (true) {
|
|
1252
|
+
const status = lstatSync(cursor);
|
|
1253
|
+
if (status.isSymbolicLink() || status.uid !== 0 || (status.mode & 0o022) !== 0)
|
|
1254
|
+
return null;
|
|
1255
|
+
const parent = dirname(cursor);
|
|
1256
|
+
if (parent === cursor)
|
|
1257
|
+
break;
|
|
1258
|
+
cursor = parent;
|
|
1259
|
+
}
|
|
1260
|
+
const status = lstatSync(realPath);
|
|
1261
|
+
if (!status.isFile())
|
|
1262
|
+
return null;
|
|
1263
|
+
return readFileSync(path, 'utf8');
|
|
1264
|
+
}
|
|
1265
|
+
catch {
|
|
1266
|
+
return null;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
function immutableCopilotDpkgIdentityMatches(packageName, architecture, version, ownedPath) {
|
|
1270
|
+
const expected = VERIFIED_COPILOT_LINUX_DEBS[architecture];
|
|
1271
|
+
if (packageName !== 'github'
|
|
1272
|
+
|| !expected
|
|
1273
|
+
|| version !== COPILOT_LINUX_VERSION
|
|
1274
|
+
|| ownedPath !== '/usr/bin/github')
|
|
1275
|
+
return null;
|
|
1276
|
+
const status = defaultFileStatus(ownedPath);
|
|
1277
|
+
const header = defaultFileHeader(ownedPath, 64);
|
|
1278
|
+
const packageManifest = '/var/lib/dpkg/info/github.md5sums';
|
|
1279
|
+
const packageManifestStatus = defaultFileStatus(packageManifest);
|
|
1280
|
+
return status?.isFile
|
|
1281
|
+
&& status.executable
|
|
1282
|
+
&& (status.mode & 0o022) === 0
|
|
1283
|
+
&& status.size === expected.executableSize
|
|
1284
|
+
&& header
|
|
1285
|
+
&& validElfHeader(header, architecture)
|
|
1286
|
+
&& defaultFileSha256(ownedPath) === expected.executableSha256
|
|
1287
|
+
&& rootOwnedReadOnlyFileContent(packageManifest) !== null
|
|
1288
|
+
&& packageManifestStatus?.size === expected.packageManifestSize
|
|
1289
|
+
&& defaultFileSha256(packageManifest) === expected.packageManifestSha256
|
|
1290
|
+
? expected
|
|
1291
|
+
: null;
|
|
1292
|
+
}
|
|
1293
|
+
function immutableVsCodeDpkgIdentityMatches(packageName, architecture, version, ownedPath) {
|
|
1294
|
+
const expected = vsCodeCatalog('linux', architecture);
|
|
1295
|
+
if (packageName !== 'code'
|
|
1296
|
+
|| !expected?.packageVersion
|
|
1297
|
+
|| version !== expected.packageVersion
|
|
1298
|
+
|| ownedPath !== '/usr/share/code/code')
|
|
1299
|
+
return null;
|
|
1300
|
+
const status = defaultFileStatus(ownedPath);
|
|
1301
|
+
const header = defaultFileHeader(ownedPath, 64);
|
|
1302
|
+
return status?.isFile
|
|
1303
|
+
&& status.executable
|
|
1304
|
+
&& status.uid === 0
|
|
1305
|
+
&& status.gid === 0
|
|
1306
|
+
&& (status.mode & 0o022) === 0
|
|
1307
|
+
&& status.size === expected.executable.size
|
|
1308
|
+
&& header
|
|
1309
|
+
&& validElfHeader(header, architecture)
|
|
1310
|
+
&& defaultFileSha256(ownedPath) === expected.executable.sha256
|
|
1311
|
+
? expected
|
|
1312
|
+
: null;
|
|
1313
|
+
}
|
|
1314
|
+
function defaultLinuxPackageIdentity(path) {
|
|
1315
|
+
if (process.platform !== 'linux')
|
|
1316
|
+
return null;
|
|
1317
|
+
let ownedPath;
|
|
1318
|
+
try {
|
|
1319
|
+
ownedPath = realpathSync(path);
|
|
1320
|
+
}
|
|
1321
|
+
catch {
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
const owner = runPosixMetadataHelper('/usr/bin/dpkg-query', ['-S', ownedPath])?.stdout.trim();
|
|
1325
|
+
const ownerMatch = owner?.match(/^([A-Za-z0-9+.-]+)(?::[A-Za-z0-9_-]+)?:\s+(.+)$/);
|
|
1326
|
+
if (ownerMatch && ownerMatch[2] === ownedPath) {
|
|
1327
|
+
const packageName = ownerMatch[1];
|
|
1328
|
+
const metadata = runPosixMetadataHelper('/usr/bin/dpkg-query', [
|
|
1329
|
+
'-W',
|
|
1330
|
+
'-f=${Package}\t${Architecture}\t${Version}\t${Status}',
|
|
1331
|
+
packageName,
|
|
1332
|
+
])?.stdout.trim();
|
|
1333
|
+
const match = metadata?.match(/^([A-Za-z0-9+.-]+)\t([A-Za-z0-9_-]+)\t(\S+)\tinstall ok installed$/);
|
|
1334
|
+
const architecture = match ? nodeArchitectureFromLinux(match[2]) : null;
|
|
1335
|
+
const verified = runPosixMetadataHelper('/usr/bin/dpkg', ['--verify', packageName]);
|
|
1336
|
+
const immutableCopilot = match && architecture
|
|
1337
|
+
? immutableCopilotDpkgIdentityMatches(packageName, architecture, match[3], ownedPath)
|
|
1338
|
+
: null;
|
|
1339
|
+
const immutableVsCode = match && architecture
|
|
1340
|
+
? immutableVsCodeDpkgIdentityMatches(packageName, architecture, match[3], ownedPath)
|
|
1341
|
+
: null;
|
|
1342
|
+
if (match?.[1] === packageName
|
|
1343
|
+
&& architecture
|
|
1344
|
+
&& verified
|
|
1345
|
+
&& verified.stdout.trim() === ''
|
|
1346
|
+
&& immutableCopilot) {
|
|
1347
|
+
return {
|
|
1348
|
+
packageName,
|
|
1349
|
+
architecture,
|
|
1350
|
+
version: match[3],
|
|
1351
|
+
manager: 'dpkg',
|
|
1352
|
+
ownedPath,
|
|
1353
|
+
origin: COPILOT_LINUX_ORIGIN,
|
|
1354
|
+
signatureVerified: false,
|
|
1355
|
+
catalogArtifactSha256: immutableCopilot.artifactSha256,
|
|
1356
|
+
executableSha256: immutableCopilot.executableSha256,
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
if (match?.[1] === packageName
|
|
1360
|
+
&& architecture
|
|
1361
|
+
&& verified
|
|
1362
|
+
&& verified.stdout.trim() === ''
|
|
1363
|
+
&& immutableVsCode) {
|
|
1364
|
+
return {
|
|
1365
|
+
packageName,
|
|
1366
|
+
architecture,
|
|
1367
|
+
version: match[3],
|
|
1368
|
+
manager: 'dpkg',
|
|
1369
|
+
ownedPath,
|
|
1370
|
+
origin: immutableVsCode.origin,
|
|
1371
|
+
signatureVerified: false,
|
|
1372
|
+
catalogArtifactSha256: immutableVsCode.artifactSha256,
|
|
1373
|
+
executableSha256: immutableVsCode.executable.sha256,
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
function defaultLinuxUrlSchemeDesktopId(scheme) {
|
|
1380
|
+
if (process.platform !== 'linux')
|
|
1381
|
+
return null;
|
|
1382
|
+
const value = runPosixMetadataHelper('/usr/bin/xdg-mime', ['query', 'default', `x-scheme-handler/${scheme}`])?.stdout.trim();
|
|
1383
|
+
return value && /^[A-Za-z0-9][A-Za-z0-9._-]*\.desktop$/.test(value) ? value : null;
|
|
1384
|
+
}
|
|
1385
|
+
export const systemSurfaceProbe = {
|
|
1386
|
+
commandPath: defaultCommandPath,
|
|
1387
|
+
fileExists: existsSync,
|
|
1388
|
+
fileContent(path) {
|
|
1389
|
+
try {
|
|
1390
|
+
return readFileSync(path, 'utf8');
|
|
1391
|
+
}
|
|
1392
|
+
catch {
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
1396
|
+
realPath(path) {
|
|
1397
|
+
try {
|
|
1398
|
+
return realpathSync(path);
|
|
1399
|
+
}
|
|
1400
|
+
catch {
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
},
|
|
1404
|
+
directoryEntries(path) {
|
|
1405
|
+
try {
|
|
1406
|
+
return readdirSync(path);
|
|
1407
|
+
}
|
|
1408
|
+
catch {
|
|
1409
|
+
return [];
|
|
1410
|
+
}
|
|
1411
|
+
},
|
|
1412
|
+
directoryTreeIdentity: defaultDirectoryTreeIdentity,
|
|
1413
|
+
fileStatus: defaultFileStatus,
|
|
1414
|
+
fileHeader: defaultFileHeader,
|
|
1415
|
+
fileSha256: defaultFileSha256,
|
|
1416
|
+
macApplicationIdentity: defaultMacApplicationIdentity,
|
|
1417
|
+
macExecutableIdentity: defaultMacExecutableIdentity,
|
|
1418
|
+
windowsExecutableIdentity: defaultWindowsExecutableIdentity,
|
|
1419
|
+
windowsVsCodeInstallationAcl: defaultWindowsVsCodeInstallationAcl,
|
|
1420
|
+
windowsAppxIdentity: defaultWindowsAppxIdentity,
|
|
1421
|
+
windowsUrlHandlerExecutable: defaultWindowsUrlHandlerExecutable,
|
|
1422
|
+
linuxPackageIdentity: defaultLinuxPackageIdentity,
|
|
1423
|
+
linuxUrlSchemeDesktopId: defaultLinuxUrlSchemeDesktopId,
|
|
1424
|
+
};
|
|
1425
|
+
function windowsUserPathCandidates(paths, home, environment) {
|
|
1426
|
+
const candidates = [];
|
|
1427
|
+
const localAppData = windowsEnvironmentValue(environment, 'LOCALAPPDATA');
|
|
1428
|
+
const appData = windowsEnvironmentValue(environment, 'APPDATA');
|
|
1429
|
+
for (const path of paths) {
|
|
1430
|
+
if (/^[A-Za-z]:[\\/]/.test(path)) {
|
|
1431
|
+
candidates.push(path);
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
const localSuffix = path.match(/^AppData[\\/]Local[\\/](.+)$/i)?.[1];
|
|
1435
|
+
const roamingSuffix = path.match(/^AppData[\\/]Roaming[\\/](.+)$/i)?.[1];
|
|
1436
|
+
if (localSuffix && localAppData)
|
|
1437
|
+
candidates.push(win32Path.join(localAppData, localSuffix));
|
|
1438
|
+
if (roamingSuffix && appData)
|
|
1439
|
+
candidates.push(win32Path.join(appData, roamingSuffix));
|
|
1440
|
+
candidates.push(win32Path.join(home, path));
|
|
1441
|
+
}
|
|
1442
|
+
return [...new Set(candidates)];
|
|
1443
|
+
}
|
|
1444
|
+
function candidateApplicationPaths(surface, platform, home, environment) {
|
|
1445
|
+
if (platform === 'darwin') {
|
|
1446
|
+
return (surface.macApplications ?? []).flatMap((path) => [
|
|
1447
|
+
path,
|
|
1448
|
+
path.startsWith('/Applications/')
|
|
1449
|
+
? join(home, 'Applications', path.slice('/Applications/'.length))
|
|
1450
|
+
: join(home, 'Applications', basename(path)),
|
|
1451
|
+
]);
|
|
1452
|
+
}
|
|
1453
|
+
if (platform === 'win32') {
|
|
1454
|
+
const candidates = windowsUserPathCandidates(surface.windowsApplications ?? [], home, environment);
|
|
1455
|
+
const programFilesRoots = [
|
|
1456
|
+
windowsEnvironmentValue(environment, 'ProgramW6432'),
|
|
1457
|
+
windowsEnvironmentValue(environment, 'ProgramFiles'),
|
|
1458
|
+
'C:\\Program Files',
|
|
1459
|
+
].filter((value) => Boolean(value));
|
|
1460
|
+
for (const root of [...new Set(programFilesRoots)]) {
|
|
1461
|
+
for (const path of surface.windowsProgramFilesApplications ?? []) {
|
|
1462
|
+
candidates.push(win32Path.join(root, path));
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return [...new Set(candidates)];
|
|
1466
|
+
}
|
|
1467
|
+
if (platform === 'linux') {
|
|
1468
|
+
return (surface.linuxApplications ?? []).map((path) => path.startsWith('/') ? path : join(home, path));
|
|
1469
|
+
}
|
|
1470
|
+
return [];
|
|
1471
|
+
}
|
|
1472
|
+
function candidateExecutablePaths(surface, platform, home, environment) {
|
|
1473
|
+
if (platform === 'darwin') {
|
|
1474
|
+
return (surface.macExecutables ?? []).map((path) => path.startsWith('/') ? path : join(home, path));
|
|
1475
|
+
}
|
|
1476
|
+
if (platform === 'win32') {
|
|
1477
|
+
return windowsUserPathCandidates(surface.windowsExecutables ?? [], home, environment);
|
|
1478
|
+
}
|
|
1479
|
+
if (platform === 'linux') {
|
|
1480
|
+
return (surface.linuxExecutables ?? []).map((path) => path.startsWith('/') ? path : join(home, path));
|
|
1481
|
+
}
|
|
1482
|
+
return [];
|
|
1483
|
+
}
|
|
1484
|
+
function resolvedRegularExecutable(executable, platform, probe) {
|
|
1485
|
+
const originalStatus = probe.fileStatus?.(executable);
|
|
1486
|
+
if (originalStatus?.isSymbolicLink)
|
|
1487
|
+
return null;
|
|
1488
|
+
const realExecutable = probe.realPath?.(executable);
|
|
1489
|
+
if (!realExecutable)
|
|
1490
|
+
return null;
|
|
1491
|
+
const status = probe.fileStatus?.(realExecutable);
|
|
1492
|
+
if (!status?.isFile || !status.executable)
|
|
1493
|
+
return null;
|
|
1494
|
+
if (status.isSymbolicLink || (platform !== 'win32' && (status.mode & 0o022) !== 0))
|
|
1495
|
+
return null;
|
|
1496
|
+
return realExecutable;
|
|
1497
|
+
}
|
|
1498
|
+
function launchFileBinding(path, probe, platform = process.platform) {
|
|
1499
|
+
const realPath = probe.realPath?.(path);
|
|
1500
|
+
const status = realPath ? probe.fileStatus?.(realPath) : null;
|
|
1501
|
+
const sha256 = realPath ? probe.fileSha256?.(realPath)?.toLowerCase() : null;
|
|
1502
|
+
if (!realPath
|
|
1503
|
+
|| !status?.isFile
|
|
1504
|
+
|| !status.executable
|
|
1505
|
+
|| status.isSymbolicLink
|
|
1506
|
+
|| (platform !== 'win32' && (status.mode & 0o022) !== 0)
|
|
1507
|
+
|| !sha256
|
|
1508
|
+
|| !/^[0-9a-f]{64}$/.test(sha256))
|
|
1509
|
+
return null;
|
|
1510
|
+
return {
|
|
1511
|
+
realPath,
|
|
1512
|
+
size: status.size,
|
|
1513
|
+
sha256,
|
|
1514
|
+
...(status.device === undefined ? {} : { device: status.device }),
|
|
1515
|
+
...(status.inode === undefined ? {} : { inode: status.inode }),
|
|
1516
|
+
...(status.modifiedTimeMs === undefined ? {} : { modifiedTimeMs: status.modifiedTimeMs }),
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
const EXPECTED_MAC_APPLICATION_IDENTITIES = {
|
|
1520
|
+
'vscode-copilot': { bundleIdentifier: 'com.microsoft.VSCode', teamIdentifier: 'UBF8T346G9' },
|
|
1521
|
+
'copilot-desktop': {
|
|
1522
|
+
bundleIdentifier: 'com.github.githubapp',
|
|
1523
|
+
teamIdentifier: 'VEKTX9H2N7',
|
|
1524
|
+
requiredScheme: 'ghapp',
|
|
1525
|
+
},
|
|
1526
|
+
'antigravity-desktop': { bundleIdentifier: 'com.google.antigravity', teamIdentifier: 'EQHXZ8M8AV' },
|
|
1527
|
+
'claude-desktop': {
|
|
1528
|
+
bundleIdentifier: 'com.anthropic.claudefordesktop',
|
|
1529
|
+
teamIdentifier: 'Q6L2SF6YDW',
|
|
1530
|
+
requiredScheme: 'claude',
|
|
1531
|
+
},
|
|
1532
|
+
'codex-desktop': { bundleIdentifier: 'com.openai.codex', teamIdentifier: '2DC432GLL2' },
|
|
1533
|
+
'grok-bot': { bundleIdentifier: 'com.anysphere.sand', teamIdentifier: 'DCNK4UB866' },
|
|
1534
|
+
};
|
|
1535
|
+
const EXPECTED_MAC_EXECUTABLE_IDENTITIES = {
|
|
1536
|
+
'copilot-cli': { identifier: 'copilot', teamIdentifier: 'VEKTX9H2N7' },
|
|
1537
|
+
'claude-cli': { identifier: 'com.anthropic.claude-code', teamIdentifier: 'Q6L2SF6YDW' },
|
|
1538
|
+
'codex-cli': { identifier: 'codex', teamIdentifier: '2DC432GLL2' },
|
|
1539
|
+
};
|
|
1540
|
+
function authenticatedCliTarget(surface, platform, architecture, home, environment, probe) {
|
|
1541
|
+
// Current reproducible signer evidence exists only for these three macOS
|
|
1542
|
+
// binaries. Windows and Linux CLIs remain intentionally undetected until an
|
|
1543
|
+
// exact Authenticode/package or immutable catalog identity is recorded.
|
|
1544
|
+
if (surface.kind !== 'cli' || platform !== 'darwin')
|
|
1545
|
+
return null;
|
|
1546
|
+
const expected = EXPECTED_MAC_EXECUTABLE_IDENTITIES[surface.id];
|
|
1547
|
+
if (!expected)
|
|
1548
|
+
return null;
|
|
1549
|
+
const candidates = [
|
|
1550
|
+
...surface.commands.map((command) => probe.commandPath(command)).filter((value) => Boolean(value)),
|
|
1551
|
+
...candidateExecutablePaths(surface, platform, home, environment),
|
|
1552
|
+
];
|
|
1553
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
1554
|
+
const executable = resolvedRegularExecutable(candidate, platform, probe);
|
|
1555
|
+
if (!executable)
|
|
1556
|
+
continue;
|
|
1557
|
+
const identity = probe.macExecutableIdentity?.(executable, expected);
|
|
1558
|
+
const binding = launchFileBinding(executable, probe, platform);
|
|
1559
|
+
if (!identity
|
|
1560
|
+
|| !binding
|
|
1561
|
+
|| identity.identifier !== expected.identifier
|
|
1562
|
+
|| identity.teamIdentifier !== expected.teamIdentifier
|
|
1563
|
+
|| !identity.architectures.includes(architecture))
|
|
1564
|
+
continue;
|
|
1565
|
+
return {
|
|
1566
|
+
executable: binding.realPath,
|
|
1567
|
+
launchArgsPrefix: [],
|
|
1568
|
+
launchEnvironment: {},
|
|
1569
|
+
// Signer identity alone does not prove a particular command-line
|
|
1570
|
+
// contract version, so advertise no optional flags until signed version
|
|
1571
|
+
// metadata is captured in the provenance catalog.
|
|
1572
|
+
capabilities: [],
|
|
1573
|
+
verification: {
|
|
1574
|
+
kind: 'mac-executable',
|
|
1575
|
+
...binding,
|
|
1576
|
+
architecture,
|
|
1577
|
+
identifier: identity.identifier,
|
|
1578
|
+
teamIdentifier: identity.teamIdentifier,
|
|
1579
|
+
},
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
function windowsApplicationIdentityMatches(surface, executable, architecture, probe) {
|
|
1585
|
+
const identity = probe.windowsExecutableIdentity?.(executable);
|
|
1586
|
+
return Boolean(identity && windowsApplicationIdentityValueMatches(surface, identity, architecture));
|
|
1587
|
+
}
|
|
1588
|
+
function windowsApplicationIdentityValueMatches(surface, identity, architecture) {
|
|
1589
|
+
if (!identity || identity.architecture !== architecture)
|
|
1590
|
+
return false;
|
|
1591
|
+
if (surface.id === 'vscode-copilot') {
|
|
1592
|
+
return identity.productName.trim() === 'Visual Studio Code'
|
|
1593
|
+
&& identity.publisher === 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US';
|
|
1594
|
+
}
|
|
1595
|
+
if (surface.id === 'claude-desktop') {
|
|
1596
|
+
return (identity.productName.trim() === 'Claude' || identity.productName.trim() === 'Claude Desktop')
|
|
1597
|
+
&& identity.publisher === 'CN=Anthropic PBC, O=Anthropic PBC, C=US';
|
|
1598
|
+
}
|
|
1599
|
+
if (surface.id === 'copilot-desktop') {
|
|
1600
|
+
return identity.productName.trim() === 'GitHub Copilot'
|
|
1601
|
+
&& identity.publisher === 'CN=GitHub, Inc., O=GitHub, Inc., C=US';
|
|
1602
|
+
}
|
|
1603
|
+
return false;
|
|
1604
|
+
}
|
|
1605
|
+
function linuxPackageIdentityMatches(executable, packageName, architecture, probe) {
|
|
1606
|
+
const identity = probe.linuxPackageIdentity?.(executable);
|
|
1607
|
+
const realExecutable = probe.realPath?.(executable);
|
|
1608
|
+
if (!identity
|
|
1609
|
+
|| !realExecutable
|
|
1610
|
+
|| identity.packageName !== packageName
|
|
1611
|
+
|| identity.architecture !== architecture
|
|
1612
|
+
|| identity.ownedPath !== realExecutable
|
|
1613
|
+
|| !/^\S+$/.test(identity.version))
|
|
1614
|
+
return false;
|
|
1615
|
+
if (packageName === 'github') {
|
|
1616
|
+
const expected = VERIFIED_COPILOT_LINUX_DEBS[architecture];
|
|
1617
|
+
return Boolean(expected
|
|
1618
|
+
&& identity.manager === 'dpkg'
|
|
1619
|
+
&& identity.version === COPILOT_LINUX_VERSION
|
|
1620
|
+
&& identity.origin === COPILOT_LINUX_ORIGIN
|
|
1621
|
+
&& identity.signatureVerified === false
|
|
1622
|
+
&& identity.catalogArtifactSha256 === expected.artifactSha256
|
|
1623
|
+
&& identity.executableSha256 === expected.executableSha256);
|
|
1624
|
+
}
|
|
1625
|
+
if (packageName === 'code') {
|
|
1626
|
+
const expected = vsCodeCatalog('linux', architecture);
|
|
1627
|
+
return Boolean(expected
|
|
1628
|
+
&& expected.packageVersion
|
|
1629
|
+
&& identity.manager === 'dpkg'
|
|
1630
|
+
&& identity.version === expected.packageVersion
|
|
1631
|
+
&& identity.origin === expected.origin
|
|
1632
|
+
&& identity.signatureVerified === false
|
|
1633
|
+
&& identity.catalogArtifactSha256 === expected.artifactSha256
|
|
1634
|
+
&& identity.executableSha256 === expected.executable.sha256);
|
|
1635
|
+
}
|
|
1636
|
+
// Repository configuration plus package-manager ownership does not bind the
|
|
1637
|
+
// installed payload to a signed repository index. These packages remain
|
|
1638
|
+
// fail-closed until an immutable signed-index/package/payload catalog exists.
|
|
1639
|
+
return false;
|
|
1640
|
+
}
|
|
1641
|
+
function applicationLaunchVerification(surface, executable, platform, architecture, probe) {
|
|
1642
|
+
if (platform === 'darwin') {
|
|
1643
|
+
const expected = EXPECTED_MAC_APPLICATION_IDENTITIES[surface.id];
|
|
1644
|
+
const identity = probe.macApplicationIdentity?.(executable, expected);
|
|
1645
|
+
const realApplication = probe.realPath?.(executable);
|
|
1646
|
+
const realExecutable = identity ? probe.realPath?.(identity.executablePath) : null;
|
|
1647
|
+
const executableBinding = realExecutable ? launchFileBinding(realExecutable, probe, platform) : null;
|
|
1648
|
+
if (!expected
|
|
1649
|
+
|| !identity
|
|
1650
|
+
|| !realApplication
|
|
1651
|
+
|| !realExecutable
|
|
1652
|
+
|| !executableBinding
|
|
1653
|
+
|| identity.bundleIdentifier !== expected.bundleIdentifier
|
|
1654
|
+
|| identity.teamIdentifier !== expected.teamIdentifier
|
|
1655
|
+
|| !identity.architectures.includes(architecture)
|
|
1656
|
+
|| !realExecutable.startsWith(`${realApplication}/Contents/MacOS/`)
|
|
1657
|
+
|| (expected.requiredScheme !== undefined
|
|
1658
|
+
&& !identity.urlSchemes.includes(expected.requiredScheme)))
|
|
1659
|
+
return null;
|
|
1660
|
+
return {
|
|
1661
|
+
kind: 'mac-application',
|
|
1662
|
+
realPath: realApplication,
|
|
1663
|
+
executablePath: realExecutable,
|
|
1664
|
+
executableSize: executableBinding.size,
|
|
1665
|
+
executableSha256: executableBinding.sha256,
|
|
1666
|
+
...(executableBinding.device === undefined ? {} : { executableDevice: executableBinding.device }),
|
|
1667
|
+
...(executableBinding.inode === undefined ? {} : { executableInode: executableBinding.inode }),
|
|
1668
|
+
...(executableBinding.modifiedTimeMs === undefined
|
|
1669
|
+
? {}
|
|
1670
|
+
: { executableModifiedTimeMs: executableBinding.modifiedTimeMs }),
|
|
1671
|
+
architecture,
|
|
1672
|
+
bundleIdentifier: identity.bundleIdentifier,
|
|
1673
|
+
teamIdentifier: identity.teamIdentifier,
|
|
1674
|
+
...(expected.requiredScheme ? { requiredScheme: expected.requiredScheme } : {}),
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
if (platform === 'win32') {
|
|
1678
|
+
const identity = probe.windowsExecutableIdentity?.(executable);
|
|
1679
|
+
const binding = launchFileBinding(executable, probe, platform);
|
|
1680
|
+
if (!identity
|
|
1681
|
+
|| !binding
|
|
1682
|
+
|| !windowsApplicationIdentityValueMatches(surface, identity, architecture))
|
|
1683
|
+
return null;
|
|
1684
|
+
return {
|
|
1685
|
+
kind: 'windows-executable',
|
|
1686
|
+
...binding,
|
|
1687
|
+
architecture: identity.architecture,
|
|
1688
|
+
productName: identity.productName,
|
|
1689
|
+
...(identity.companyName ? { companyName: identity.companyName } : {}),
|
|
1690
|
+
publisher: identity.publisher,
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
if (platform !== 'linux')
|
|
1694
|
+
return null;
|
|
1695
|
+
const identity = probe.linuxPackageIdentity?.(executable);
|
|
1696
|
+
const binding = launchFileBinding(executable, probe, platform);
|
|
1697
|
+
if (!identity
|
|
1698
|
+
|| !binding
|
|
1699
|
+
|| !identity.catalogArtifactSha256
|
|
1700
|
+
|| identity.executableSha256 !== binding.sha256
|
|
1701
|
+
|| surface.id !== 'copilot-desktop'
|
|
1702
|
+
|| !linuxPackageIdentityMatches(executable, 'github', architecture, probe))
|
|
1703
|
+
return null;
|
|
1704
|
+
return {
|
|
1705
|
+
kind: 'linux-package',
|
|
1706
|
+
...binding,
|
|
1707
|
+
architecture: identity.architecture,
|
|
1708
|
+
packageName: identity.packageName,
|
|
1709
|
+
version: identity.version,
|
|
1710
|
+
manager: identity.manager,
|
|
1711
|
+
origin: identity.origin,
|
|
1712
|
+
catalogArtifactSha256: identity.catalogArtifactSha256,
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
function hasExpectedApplicationIdentity(surface, executable, platform, architecture, probe) {
|
|
1716
|
+
return applicationLaunchVerification(surface, executable, platform, architecture, probe) !== null;
|
|
1717
|
+
}
|
|
1718
|
+
function sameCatalogPath(left, right, platform) {
|
|
1719
|
+
return platform === 'win32'
|
|
1720
|
+
? win32Path.normalize(left).toLowerCase() === win32Path.normalize(right).toLowerCase()
|
|
1721
|
+
: resolve(left) === resolve(right);
|
|
1722
|
+
}
|
|
1723
|
+
function catalogFileMatches(path, expected, platform, probe) {
|
|
1724
|
+
const realPath = probe.realPath?.(path);
|
|
1725
|
+
const status = realPath ? probe.fileStatus?.(realPath) : null;
|
|
1726
|
+
return Boolean(realPath
|
|
1727
|
+
&& sameCatalogPath(realPath, path, platform)
|
|
1728
|
+
&& status?.isFile
|
|
1729
|
+
&& !status.isSymbolicLink
|
|
1730
|
+
&& status.size === expected.size
|
|
1731
|
+
&& probe.fileSha256?.(realPath)?.toLowerCase() === expected.sha256);
|
|
1732
|
+
}
|
|
1733
|
+
function catalogFileBinding(path, expected, platform, probe) {
|
|
1734
|
+
if (!catalogFileMatches(path, expected, platform, probe))
|
|
1735
|
+
return null;
|
|
1736
|
+
const realPath = probe.realPath?.(path);
|
|
1737
|
+
const status = realPath ? probe.fileStatus?.(realPath) : null;
|
|
1738
|
+
if (!realPath || !status)
|
|
1739
|
+
return null;
|
|
1740
|
+
return {
|
|
1741
|
+
realPath,
|
|
1742
|
+
size: status.size,
|
|
1743
|
+
sha256: expected.sha256,
|
|
1744
|
+
...(status.device === undefined ? {} : { device: status.device }),
|
|
1745
|
+
...(status.inode === undefined ? {} : { inode: status.inode }),
|
|
1746
|
+
...(status.modifiedTimeMs === undefined ? {} : { modifiedTimeMs: status.modifiedTimeMs }),
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
function vscodeCatalogMetadataMatches(resourceRoot, expected, platform, probe) {
|
|
1750
|
+
const pathFor = (...segments) => platform === 'win32'
|
|
1751
|
+
? win32Path.join(resourceRoot, ...segments)
|
|
1752
|
+
: join(resourceRoot, ...segments);
|
|
1753
|
+
const packagePath = pathFor('package.json');
|
|
1754
|
+
const productPath = pathFor('product.json');
|
|
1755
|
+
const copilotPath = pathFor('extensions', 'copilot', 'package.json');
|
|
1756
|
+
const cliPath = pathFor('out', 'cli.js');
|
|
1757
|
+
if (!catalogFileMatches(packagePath, expected.packageJson, platform, probe)
|
|
1758
|
+
|| !catalogFileMatches(productPath, expected.productJson, platform, probe)
|
|
1759
|
+
|| !catalogFileMatches(copilotPath, expected.copilotPackageJson, platform, probe)
|
|
1760
|
+
|| !catalogFileMatches(cliPath, expected.cliJs, platform, probe))
|
|
1761
|
+
return false;
|
|
105
1762
|
try {
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
1763
|
+
const packageMetadata = JSON.parse(probe.fileContent?.(packagePath) ?? '');
|
|
1764
|
+
const productMetadata = JSON.parse(probe.fileContent?.(productPath) ?? '');
|
|
1765
|
+
const copilotMetadata = JSON.parse(probe.fileContent?.(copilotPath) ?? '');
|
|
1766
|
+
const engines = copilotMetadata.engines;
|
|
1767
|
+
return packageMetadata.name === 'Code'
|
|
1768
|
+
&& packageMetadata.version === VSCODE_CATALOG_VERSION
|
|
1769
|
+
&& packageMetadata.main === './out/main.js'
|
|
1770
|
+
&& productMetadata.nameLong === 'Visual Studio Code'
|
|
1771
|
+
&& productMetadata.applicationName === 'code'
|
|
1772
|
+
&& productMetadata.commit === VSCODE_CATALOG_COMMIT
|
|
1773
|
+
&& productMetadata.quality === 'stable'
|
|
1774
|
+
&& copilotMetadata.publisher === 'GitHub'
|
|
1775
|
+
&& copilotMetadata.name === 'copilot-chat'
|
|
1776
|
+
&& copilotMetadata.version === '0.64.1'
|
|
1777
|
+
&& engines?.vscode === `^${VSCODE_CATALOG_VERSION}`;
|
|
109
1778
|
}
|
|
110
1779
|
catch {
|
|
111
|
-
return
|
|
1780
|
+
return false;
|
|
112
1781
|
}
|
|
113
1782
|
}
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
1783
|
+
function vscodeCatalogTreeMatches(applicationRoot, expected, platform, probe) {
|
|
1784
|
+
const identity = probe.directoryTreeIdentity?.(applicationRoot);
|
|
1785
|
+
return identity
|
|
1786
|
+
&& sameCatalogPath(identity.realPath, applicationRoot, platform)
|
|
1787
|
+
&& identity.fileCount === expected.fileCount
|
|
1788
|
+
&& identity.totalBytes === expected.totalBytes
|
|
1789
|
+
&& identity.sha256 === expected.sha256
|
|
1790
|
+
? identity
|
|
1791
|
+
: null;
|
|
1792
|
+
}
|
|
1793
|
+
function exactStringRecord(actual, expected) {
|
|
1794
|
+
const actualKeys = Object.keys(actual).sort();
|
|
1795
|
+
const expectedKeys = Object.keys(expected).sort();
|
|
1796
|
+
return actualKeys.length === expectedKeys.length
|
|
1797
|
+
&& actualKeys.every((key, index) => (key === expectedKeys[index] && actual[key] === expected[key]));
|
|
1798
|
+
}
|
|
1799
|
+
function windowsDirectoryHasExactEntries(directory, expectedEntries, probe) {
|
|
1800
|
+
const entries = probe.directoryEntries?.(directory);
|
|
1801
|
+
if (!entries || new Set(entries.map((entry) => entry.toLowerCase())).size !== entries.length)
|
|
1802
|
+
return false;
|
|
1803
|
+
const actual = [...entries].map((entry) => entry.toLowerCase()).sort();
|
|
1804
|
+
const expected = expectedEntries.map((entry) => entry.toLowerCase()).sort();
|
|
1805
|
+
return actual.length === expected.length
|
|
1806
|
+
&& actual.every((entry, index) => entry === expected[index]);
|
|
1807
|
+
}
|
|
1808
|
+
function windowsVsCodeInstallationRootMatches(installationRoot, probe) {
|
|
1809
|
+
if (!windowsDirectoryHasExactEntries(installationRoot, WINDOWS_VSCODE_INSTALLATION_ENTRIES, probe) || !windowsDirectoryHasExactEntries(win32Path.join(installationRoot, 'bin'), WINDOWS_VSCODE_BIN_ENTRIES, probe))
|
|
1810
|
+
return false;
|
|
1811
|
+
return WINDOWS_VSCODE_UNINSTALLER_FILES.every((file) => {
|
|
1812
|
+
const path = win32Path.join(installationRoot, file);
|
|
1813
|
+
const realPath = probe.realPath?.(path);
|
|
1814
|
+
const status = realPath ? probe.fileStatus?.(realPath) : null;
|
|
1815
|
+
return Boolean(realPath
|
|
1816
|
+
&& sameCatalogPath(realPath, path, 'win32')
|
|
1817
|
+
&& status?.isFile
|
|
1818
|
+
&& !status.isSymbolicLink);
|
|
121
1819
|
});
|
|
122
|
-
await chmod(temporary, 0o600).catch(() => undefined);
|
|
123
|
-
await rename(temporary, target);
|
|
124
1820
|
}
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
1821
|
+
function catalogTreeShapeMatches(actual, realPath, expected, platform) {
|
|
1822
|
+
return Boolean(actual
|
|
1823
|
+
&& expected
|
|
1824
|
+
&& sameCatalogPath(actual.realPath, realPath, platform)
|
|
1825
|
+
&& actual.fileCount === expected.fileCount
|
|
1826
|
+
&& actual.totalBytes === expected.totalBytes
|
|
1827
|
+
&& actual.sha256 === expected.sha256);
|
|
1828
|
+
}
|
|
1829
|
+
function vscodeCatalogVerificationShapeMatches(surface, platform) {
|
|
1830
|
+
const verification = surface.verification;
|
|
1831
|
+
if (surface.id !== 'vscode-copilot'
|
|
1832
|
+
|| (platform !== 'win32' && platform !== 'linux')
|
|
1833
|
+
|| verification?.kind !== 'vscode-catalog'
|
|
1834
|
+
|| verification.platform !== platform
|
|
1835
|
+
|| surface.executable === null
|
|
1836
|
+
|| !sameCatalogPath(surface.executable, verification.realPath, platform)
|
|
1837
|
+
|| !equalStringArrays(surface.launchArgsPrefix, [verification.cliPath])
|
|
1838
|
+
|| !exactStringRecord(surface.launchEnvironment, VSCODE_CATALOG_LAUNCH_ENVIRONMENT)
|
|
1839
|
+
|| verification.version !== VSCODE_CATALOG_VERSION
|
|
1840
|
+
|| verification.commit !== VSCODE_CATALOG_COMMIT)
|
|
1841
|
+
return false;
|
|
1842
|
+
const expected = vsCodeCatalog(platform, verification.architecture);
|
|
1843
|
+
if (!expected
|
|
1844
|
+
|| verification.catalogArtifactSha256 !== expected.artifactSha256
|
|
1845
|
+
|| verification.size !== expected.executable.size
|
|
1846
|
+
|| verification.sha256 !== expected.executable.sha256)
|
|
1847
|
+
return false;
|
|
1848
|
+
if (platform === 'win32') {
|
|
1849
|
+
const installationRoot = win32Path.dirname(verification.realPath);
|
|
1850
|
+
const applicationRoot = win32Path.join(installationRoot, VSCODE_CATALOG_COMMIT_DIRECTORY);
|
|
1851
|
+
const resourceRoot = win32Path.join(applicationRoot, 'resources', 'app');
|
|
1852
|
+
const auxiliaryRoot = win32Path.join(installationRoot, 'bin');
|
|
1853
|
+
const visualManifestPath = win32Path.join(installationRoot, 'Code.VisualElementsManifest.xml');
|
|
1854
|
+
return win32Path.basename(verification.realPath).toLowerCase() === 'code.exe'
|
|
1855
|
+
&& sameCatalogPath(verification.installationRoot, installationRoot, 'win32')
|
|
1856
|
+
&& sameCatalogPath(verification.cliPath, win32Path.join(resourceRoot, 'out', 'cli.js'), 'win32')
|
|
1857
|
+
&& catalogTreeShapeMatches(verification.applicationTree, applicationRoot, expected.applicationTree, 'win32')
|
|
1858
|
+
&& catalogTreeShapeMatches(verification.auxiliaryTree, auxiliaryRoot, expected.auxiliaryTree, 'win32')
|
|
1859
|
+
&& Boolean(expected.visualManifest
|
|
1860
|
+
&& verification.visualManifest
|
|
1861
|
+
&& sameCatalogPath(verification.visualManifest.realPath, visualManifestPath, 'win32')
|
|
1862
|
+
&& verification.visualManifest.size === expected.visualManifest.size
|
|
1863
|
+
&& verification.visualManifest.sha256 === expected.visualManifest.sha256);
|
|
1864
|
+
}
|
|
1865
|
+
return sameCatalogPath(verification.realPath, '/usr/share/code/code', 'linux')
|
|
1866
|
+
&& sameCatalogPath(verification.installationRoot, '/usr/share/code', 'linux')
|
|
1867
|
+
&& sameCatalogPath(verification.cliPath, '/usr/share/code/resources/app/out/cli.js', 'linux')
|
|
1868
|
+
&& catalogTreeShapeMatches(verification.applicationTree, '/usr/share/code', expected.applicationTree, 'linux')
|
|
1869
|
+
&& verification.auxiliaryTree === undefined
|
|
1870
|
+
&& verification.visualManifest === undefined;
|
|
1871
|
+
}
|
|
1872
|
+
function windowsVsCodeTarget(surface, architecture, home, environment, probe) {
|
|
1873
|
+
const expected = vsCodeCatalog('win32', architecture);
|
|
1874
|
+
if (!expected || (architecture !== 'x64' && architecture !== 'arm64'))
|
|
129
1875
|
return null;
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
1876
|
+
for (const candidate of candidateApplicationPaths(surface, 'win32', home, environment)) {
|
|
1877
|
+
if (win32Path.basename(candidate).toLowerCase() !== 'code.exe'
|
|
1878
|
+
|| !probe.fileExists(candidate))
|
|
1879
|
+
continue;
|
|
1880
|
+
const candidateInstallationRoot = win32Path.dirname(candidate);
|
|
1881
|
+
if (!probe.windowsVsCodeInstallationAcl?.(candidateInstallationRoot))
|
|
1882
|
+
continue;
|
|
1883
|
+
const binding = launchFileBinding(candidate, probe, 'win32');
|
|
1884
|
+
const identity = probe.windowsExecutableIdentity?.(candidate);
|
|
1885
|
+
if (!binding
|
|
1886
|
+
|| !sameCatalogPath(binding.realPath, candidate, 'win32')
|
|
1887
|
+
|| binding.size !== expected.executable.size
|
|
1888
|
+
|| binding.sha256 !== expected.executable.sha256
|
|
1889
|
+
|| !identity
|
|
1890
|
+
|| !windowsApplicationIdentityValueMatches(surface, identity, architecture))
|
|
1891
|
+
continue;
|
|
1892
|
+
const installationRoot = win32Path.dirname(binding.realPath);
|
|
1893
|
+
const applicationRoot = win32Path.join(installationRoot, VSCODE_CATALOG_COMMIT_DIRECTORY);
|
|
1894
|
+
const resourceRoot = win32Path.join(applicationRoot, 'resources', 'app');
|
|
1895
|
+
const auxiliaryRoot = win32Path.join(installationRoot, 'bin');
|
|
1896
|
+
const visualManifestPath = win32Path.join(installationRoot, 'Code.VisualElementsManifest.xml');
|
|
1897
|
+
const applicationTree = vscodeCatalogTreeMatches(applicationRoot, expected.applicationTree, 'win32', probe);
|
|
1898
|
+
const auxiliaryTree = expected.auxiliaryTree
|
|
1899
|
+
? vscodeCatalogTreeMatches(auxiliaryRoot, expected.auxiliaryTree, 'win32', probe)
|
|
1900
|
+
: null;
|
|
1901
|
+
const visualManifest = expected.visualManifest
|
|
1902
|
+
? catalogFileBinding(visualManifestPath, expected.visualManifest, 'win32', probe)
|
|
1903
|
+
: null;
|
|
1904
|
+
if (!applicationTree
|
|
1905
|
+
|| !auxiliaryTree
|
|
1906
|
+
|| !visualManifest
|
|
1907
|
+
|| !windowsVsCodeInstallationRootMatches(installationRoot, probe)
|
|
1908
|
+
|| !vscodeCatalogMetadataMatches(resourceRoot, expected, 'win32', probe))
|
|
1909
|
+
continue;
|
|
1910
|
+
const cliPath = win32Path.join(resourceRoot, 'out', 'cli.js');
|
|
1911
|
+
return {
|
|
1912
|
+
executable: binding.realPath,
|
|
1913
|
+
launchArgsPrefix: [cliPath],
|
|
1914
|
+
launchEnvironment: { ...VSCODE_CATALOG_LAUNCH_ENVIRONMENT },
|
|
1915
|
+
capabilities: ['initial-prompt'],
|
|
1916
|
+
verification: {
|
|
1917
|
+
kind: 'vscode-catalog',
|
|
1918
|
+
...binding,
|
|
1919
|
+
platform: 'win32',
|
|
1920
|
+
architecture,
|
|
1921
|
+
version: VSCODE_CATALOG_VERSION,
|
|
1922
|
+
commit: VSCODE_CATALOG_COMMIT,
|
|
1923
|
+
installationRoot,
|
|
1924
|
+
cliPath,
|
|
1925
|
+
catalogArtifactSha256: expected.artifactSha256,
|
|
1926
|
+
applicationTree,
|
|
1927
|
+
auxiliaryTree,
|
|
1928
|
+
visualManifest,
|
|
1929
|
+
},
|
|
1930
|
+
};
|
|
133
1931
|
}
|
|
134
|
-
return
|
|
1932
|
+
return null;
|
|
135
1933
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
1934
|
+
function vscodeHasCopilotMetadata(_home, _platform, probe, builtInCandidates) {
|
|
1935
|
+
// Only metadata inside the already authenticated application/package may be
|
|
1936
|
+
// used. User-writable extension manifests are not publisher authentication.
|
|
1937
|
+
return builtInCandidates.some((candidate) => probe.fileExists(candidate));
|
|
1938
|
+
}
|
|
1939
|
+
function macVsCodeTarget(surface, architecture, home, environment, probe) {
|
|
1940
|
+
for (const application of candidateApplicationPaths(surface, 'darwin', home, environment)) {
|
|
1941
|
+
if (!probe.fileExists(application))
|
|
1942
|
+
continue;
|
|
1943
|
+
const verification = applicationLaunchVerification(surface, application, 'darwin', architecture, probe);
|
|
1944
|
+
if (!verification || verification.kind !== 'mac-application')
|
|
1945
|
+
continue;
|
|
1946
|
+
const realApplication = probe.realPath?.(application);
|
|
1947
|
+
if (!realApplication)
|
|
1948
|
+
continue;
|
|
1949
|
+
const command = join(realApplication, 'Contents', 'Resources', 'app', 'bin', 'code');
|
|
1950
|
+
const realCommand = resolvedRegularExecutable(command, 'darwin', probe);
|
|
1951
|
+
const commandBinding = realCommand ? launchFileBinding(realCommand, probe, 'darwin') : null;
|
|
1952
|
+
if (!realCommand
|
|
1953
|
+
|| !commandBinding
|
|
1954
|
+
|| !realCommand.startsWith(`${realApplication}/Contents/Resources/app/bin/`))
|
|
1955
|
+
continue;
|
|
1956
|
+
const builtInCopilot = join(realApplication, 'Contents', 'Resources', 'app', 'extensions', 'copilot');
|
|
1957
|
+
if (!vscodeHasCopilotMetadata(home, 'darwin', probe, [builtInCopilot]))
|
|
1958
|
+
continue;
|
|
1959
|
+
return {
|
|
1960
|
+
executable: realCommand,
|
|
1961
|
+
launchArgsPrefix: [],
|
|
1962
|
+
launchEnvironment: {},
|
|
1963
|
+
verification: {
|
|
1964
|
+
...verification,
|
|
1965
|
+
commandRealPath: commandBinding.realPath,
|
|
1966
|
+
commandSize: commandBinding.size,
|
|
1967
|
+
commandSha256: commandBinding.sha256,
|
|
1968
|
+
...(commandBinding.device === undefined ? {} : { commandDevice: commandBinding.device }),
|
|
1969
|
+
...(commandBinding.inode === undefined ? {} : { commandInode: commandBinding.inode }),
|
|
1970
|
+
...(commandBinding.modifiedTimeMs === undefined
|
|
1971
|
+
? {}
|
|
1972
|
+
: { commandModifiedTimeMs: commandBinding.modifiedTimeMs }),
|
|
1973
|
+
},
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
return null;
|
|
1977
|
+
}
|
|
1978
|
+
function linuxVsCodeTarget(surface, architecture, _home, probe) {
|
|
1979
|
+
const expected = vsCodeCatalog('linux', architecture);
|
|
1980
|
+
const executable = '/usr/share/code/code';
|
|
1981
|
+
if (!expected
|
|
1982
|
+
|| (architecture !== 'x64' && architecture !== 'arm64')
|
|
1983
|
+
|| !probe.fileExists(executable))
|
|
1984
|
+
return null;
|
|
1985
|
+
const binding = launchFileBinding(executable, probe, 'linux');
|
|
1986
|
+
const header = probe.fileHeader?.(executable, 64);
|
|
1987
|
+
if (!binding
|
|
1988
|
+
|| binding.size !== expected.executable.size
|
|
1989
|
+
|| binding.sha256 !== expected.executable.sha256
|
|
1990
|
+
|| !header
|
|
1991
|
+
|| !validElfHeader(header, architecture)
|
|
1992
|
+
|| !linuxPackageIdentityMatches(executable, 'code', architecture, probe))
|
|
1993
|
+
return null;
|
|
1994
|
+
const installationRoot = '/usr/share/code';
|
|
1995
|
+
const resourceRoot = join(installationRoot, 'resources', 'app');
|
|
1996
|
+
const applicationTree = vscodeCatalogTreeMatches(installationRoot, expected.applicationTree, 'linux', probe);
|
|
1997
|
+
if (!applicationTree
|
|
1998
|
+
|| !vscodeCatalogMetadataMatches(resourceRoot, expected, 'linux', probe))
|
|
1999
|
+
return null;
|
|
2000
|
+
const cliPath = join(resourceRoot, 'out', 'cli.js');
|
|
2001
|
+
return {
|
|
2002
|
+
executable: binding.realPath,
|
|
2003
|
+
launchArgsPrefix: [cliPath],
|
|
2004
|
+
launchEnvironment: { ...VSCODE_CATALOG_LAUNCH_ENVIRONMENT },
|
|
2005
|
+
capabilities: ['initial-prompt'],
|
|
2006
|
+
verification: {
|
|
2007
|
+
kind: 'vscode-catalog',
|
|
2008
|
+
...binding,
|
|
2009
|
+
platform: 'linux',
|
|
2010
|
+
architecture,
|
|
2011
|
+
version: VSCODE_CATALOG_VERSION,
|
|
2012
|
+
commit: VSCODE_CATALOG_COMMIT,
|
|
2013
|
+
installationRoot,
|
|
2014
|
+
cliPath,
|
|
2015
|
+
catalogArtifactSha256: expected.artifactSha256,
|
|
2016
|
+
applicationTree,
|
|
2017
|
+
},
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
function parseDesktopEntry(content) {
|
|
2021
|
+
const values = new Map();
|
|
2022
|
+
let inDesktopEntry = false;
|
|
2023
|
+
for (const rawLine of content.replace(/^\uFEFF/, '').split(/\r?\n/)) {
|
|
2024
|
+
const line = rawLine.trim();
|
|
2025
|
+
if (!line || line.startsWith('#'))
|
|
2026
|
+
continue;
|
|
2027
|
+
if (line.startsWith('[') && line.endsWith(']')) {
|
|
2028
|
+
inDesktopEntry = line === '[Desktop Entry]';
|
|
2029
|
+
continue;
|
|
142
2030
|
}
|
|
143
|
-
|
|
2031
|
+
if (!inDesktopEntry)
|
|
2032
|
+
continue;
|
|
2033
|
+
const separator = rawLine.indexOf('=');
|
|
2034
|
+
if (separator <= 0)
|
|
144
2035
|
return null;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
2036
|
+
const key = rawLine.slice(0, separator).trim();
|
|
2037
|
+
// Locale-qualified keys (for example `Name[en_AU]`) are valid desktop-entry
|
|
2038
|
+
// metadata. Keep them separate from the unqualified values used for launch
|
|
2039
|
+
// validation, while still rejecting duplicate or malformed keys.
|
|
2040
|
+
if (!/^[A-Za-z][A-Za-z0-9-]*(?:\[[A-Za-z0-9_.@-]+\])?$/.test(key) || values.has(key))
|
|
152
2041
|
return null;
|
|
2042
|
+
values.set(key, rawLine.slice(separator + 1).trim());
|
|
2043
|
+
}
|
|
2044
|
+
return values.size > 0 ? values : null;
|
|
2045
|
+
}
|
|
2046
|
+
function decodeDesktopValue(value) {
|
|
2047
|
+
let decoded = '';
|
|
2048
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
2049
|
+
const character = value[index];
|
|
2050
|
+
if (character !== '\\') {
|
|
2051
|
+
decoded += character;
|
|
2052
|
+
continue;
|
|
153
2053
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const result = spawnSync(command, [...args], {
|
|
157
|
-
encoding: 'utf8',
|
|
158
|
-
windowsHide: true,
|
|
159
|
-
timeout: 5000,
|
|
160
|
-
env: { ...process.env, ...environment },
|
|
161
|
-
});
|
|
162
|
-
if (result.status !== 0)
|
|
2054
|
+
const escaped = value[index + 1];
|
|
2055
|
+
if (escaped === undefined)
|
|
163
2056
|
return null;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
if (platform === 'win32') {
|
|
177
|
-
return (surface.windowsApplications ?? []).map((path) => /^[A-Za-z]:[\\/]/.test(path) ? path : win32Path.join(home, path));
|
|
2057
|
+
const replacement = {
|
|
2058
|
+
s: ' ',
|
|
2059
|
+
n: '\n',
|
|
2060
|
+
t: '\t',
|
|
2061
|
+
r: '\r',
|
|
2062
|
+
'\\': '\\',
|
|
2063
|
+
};
|
|
2064
|
+
if (!(escaped in replacement))
|
|
2065
|
+
return null;
|
|
2066
|
+
decoded += replacement[escaped];
|
|
2067
|
+
index += 1;
|
|
178
2068
|
}
|
|
179
|
-
return
|
|
2069
|
+
return decoded;
|
|
180
2070
|
}
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
2071
|
+
function parseDesktopExec(value) {
|
|
2072
|
+
if (!value || /[\0\r\n]/.test(value))
|
|
2073
|
+
return null;
|
|
2074
|
+
const tokens = [];
|
|
2075
|
+
let current = '';
|
|
2076
|
+
let quoted = false;
|
|
2077
|
+
let escaped = false;
|
|
2078
|
+
const flush = () => {
|
|
2079
|
+
if (current)
|
|
2080
|
+
tokens.push(current);
|
|
2081
|
+
current = '';
|
|
2082
|
+
};
|
|
2083
|
+
for (const character of value) {
|
|
2084
|
+
if (escaped) {
|
|
2085
|
+
current += character;
|
|
2086
|
+
escaped = false;
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
if (character === '\\') {
|
|
2090
|
+
escaped = true;
|
|
187
2091
|
continue;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
executables.add(found);
|
|
192
|
-
}
|
|
193
|
-
for (const candidate of candidateApplicationPaths(surface, 'win32', home)) {
|
|
194
|
-
if (/\.(?:cmd|bat)$/i.test(candidate))
|
|
195
|
-
shims.add(candidate);
|
|
196
|
-
else if (/\.(?:exe|com)$/i.test(candidate))
|
|
197
|
-
executables.add(candidate);
|
|
198
|
-
}
|
|
199
|
-
for (const executable of executables) {
|
|
200
|
-
shims.add(win32Path.join(win32Path.dirname(executable), 'bin', 'code.cmd'));
|
|
201
|
-
}
|
|
202
|
-
for (const shim of shims) {
|
|
203
|
-
if (!probe.fileExists(shim))
|
|
2092
|
+
}
|
|
2093
|
+
if (character === '"') {
|
|
2094
|
+
quoted = !quoted;
|
|
204
2095
|
continue;
|
|
205
|
-
|
|
206
|
-
if (
|
|
2096
|
+
}
|
|
2097
|
+
if (/\s/.test(character) && !quoted) {
|
|
2098
|
+
flush();
|
|
207
2099
|
continue;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
2100
|
+
}
|
|
2101
|
+
current += character;
|
|
2102
|
+
}
|
|
2103
|
+
if (escaped || quoted)
|
|
2104
|
+
return null;
|
|
2105
|
+
flush();
|
|
2106
|
+
if (tokens.length === 0)
|
|
2107
|
+
return null;
|
|
2108
|
+
const result = [];
|
|
2109
|
+
for (const token of tokens) {
|
|
2110
|
+
if (/^%[fFuUdDnNickvm]$/.test(token))
|
|
211
2111
|
continue;
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
2112
|
+
const literal = token.replace(/%%/g, '%');
|
|
2113
|
+
if (/%[A-Za-z%]/.test(literal))
|
|
2114
|
+
return null;
|
|
2115
|
+
result.push(literal);
|
|
2116
|
+
}
|
|
2117
|
+
return result.length > 0 ? result : null;
|
|
2118
|
+
}
|
|
2119
|
+
function linuxApplicationDirectories(home, environment) {
|
|
2120
|
+
const dataHome = environment.XDG_DATA_HOME && environment.XDG_DATA_HOME.startsWith('/')
|
|
2121
|
+
? environment.XDG_DATA_HOME
|
|
2122
|
+
: join(home, '.local', 'share');
|
|
2123
|
+
const dataDirectories = (environment.XDG_DATA_DIRS ?? '/usr/local/share:/usr/share')
|
|
2124
|
+
.split(':')
|
|
2125
|
+
.filter((candidate) => candidate.startsWith('/'));
|
|
2126
|
+
return [...new Set([dataHome, ...dataDirectories].map((directory) => join(directory, 'applications')))];
|
|
2127
|
+
}
|
|
2128
|
+
function desktopEntryPath(desktopId, home, environment, probe) {
|
|
2129
|
+
if (!validDesktopFileName(desktopId))
|
|
2130
|
+
return null;
|
|
2131
|
+
for (const directory of linuxApplicationDirectories(home, environment)) {
|
|
2132
|
+
const candidate = join(directory, desktopId);
|
|
2133
|
+
if (probe.fileExists(candidate) && probe.fileContent?.(candidate))
|
|
2134
|
+
return candidate;
|
|
2135
|
+
}
|
|
2136
|
+
return null;
|
|
2137
|
+
}
|
|
2138
|
+
function validDesktopFileName(fileName) {
|
|
2139
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*\.desktop$/.test(fileName)
|
|
2140
|
+
|| fileName === 'GitHub Copilot.desktop';
|
|
2141
|
+
}
|
|
2142
|
+
function desktopEntryHasScheme(entry, scheme) {
|
|
2143
|
+
const mimeTypes = entry.get('MimeType')?.split(';').filter(Boolean) ?? [];
|
|
2144
|
+
return mimeTypes.includes(`x-scheme-handler/${scheme}`);
|
|
2145
|
+
}
|
|
2146
|
+
function regularExecutable(path, probe) {
|
|
2147
|
+
const status = probe.fileStatus?.(path);
|
|
2148
|
+
return Boolean(status?.isFile && status.executable && (status.mode & 0o022) === 0);
|
|
2149
|
+
}
|
|
2150
|
+
function expectedElfMachine(architecture) {
|
|
2151
|
+
if (architecture === 'x64')
|
|
2152
|
+
return 62;
|
|
2153
|
+
if (architecture === 'arm64')
|
|
2154
|
+
return 183;
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
function validElfHeader(header, architecture) {
|
|
2158
|
+
const machine = expectedElfMachine(architecture);
|
|
2159
|
+
return machine !== null
|
|
2160
|
+
&& header.length >= 20
|
|
2161
|
+
&& header[0] === 0x7f
|
|
2162
|
+
&& header[1] === 0x45
|
|
2163
|
+
&& header[2] === 0x4c
|
|
2164
|
+
&& header[3] === 0x46
|
|
2165
|
+
&& header[4] === 2
|
|
2166
|
+
&& header[5] === 1
|
|
2167
|
+
&& (header[18] | (header[19] << 8)) === machine;
|
|
2168
|
+
}
|
|
2169
|
+
function validAppImageHeader(header, architecture) {
|
|
2170
|
+
const appImageType = header[10];
|
|
2171
|
+
return validElfHeader(header, architecture)
|
|
2172
|
+
&& header[8] === 0x41
|
|
2173
|
+
&& header[9] === 0x49
|
|
2174
|
+
&& (appImageType === 1 || appImageType === 2);
|
|
2175
|
+
}
|
|
2176
|
+
// The current Antigravity 2 Linux distribution is an unsigned tar archive.
|
|
2177
|
+
// Bind the extracted executable to the immutable official archive payload;
|
|
2178
|
+
// legacy apt 1.x packages are intentionally not accepted as Antigravity 2.
|
|
2179
|
+
const VERIFIED_ANTIGRAVITY_LINUX_EXECUTABLES = {
|
|
2180
|
+
arm64: {
|
|
2181
|
+
version: '2.12.2',
|
|
2182
|
+
size: 197_528_864,
|
|
2183
|
+
sha256: 'e706505fdd89003390c256b084ee44625e4ab0aacd068b8f62290af8b0a6f6ed',
|
|
2184
|
+
},
|
|
2185
|
+
x64: {
|
|
2186
|
+
version: '2.12.2',
|
|
2187
|
+
size: 206_036_184,
|
|
2188
|
+
sha256: 'b0d127772d2983a93771055a93b673d5fdd1726d6e47db8e269b204e665972d6',
|
|
2189
|
+
},
|
|
2190
|
+
};
|
|
2191
|
+
function verifiedLinuxAntigravityTarget(surface, architecture, home, environment, probe) {
|
|
2192
|
+
const expected = VERIFIED_ANTIGRAVITY_LINUX_EXECUTABLES[architecture];
|
|
2193
|
+
if (!expected)
|
|
2194
|
+
return null;
|
|
2195
|
+
for (const candidate of candidateApplicationPaths(surface, 'linux', home, environment)) {
|
|
2196
|
+
if (!probe.fileExists(candidate))
|
|
215
2197
|
continue;
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
2198
|
+
const realPath = probe.realPath?.(candidate);
|
|
2199
|
+
const status = realPath ? probe.fileStatus?.(realPath) : null;
|
|
2200
|
+
const header = realPath ? probe.fileHeader?.(realPath, 64) : null;
|
|
2201
|
+
const sha256 = realPath ? probe.fileSha256?.(realPath)?.toLowerCase() : null;
|
|
2202
|
+
if (!realPath
|
|
2203
|
+
|| !status?.isFile
|
|
2204
|
+
|| !status.executable
|
|
2205
|
+
|| (status.mode & 0o022) !== 0
|
|
2206
|
+
|| status.size !== expected.size
|
|
2207
|
+
|| !header
|
|
2208
|
+
|| !validElfHeader(header, architecture)
|
|
2209
|
+
|| sha256 !== expected.sha256)
|
|
220
2210
|
continue;
|
|
221
|
-
return {
|
|
2211
|
+
return {
|
|
2212
|
+
executable: realPath,
|
|
2213
|
+
launchArgsPrefix: [],
|
|
2214
|
+
launchEnvironment: {},
|
|
2215
|
+
verification: {
|
|
2216
|
+
kind: 'portable-elf',
|
|
2217
|
+
realPath,
|
|
2218
|
+
size: status.size,
|
|
2219
|
+
sha256,
|
|
2220
|
+
architecture,
|
|
2221
|
+
},
|
|
2222
|
+
};
|
|
222
2223
|
}
|
|
223
2224
|
return null;
|
|
224
2225
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
2226
|
+
// The vendor's current AppImages have empty AppImage signature sections, so
|
|
2227
|
+
// verify the complete assets against independently recorded SHA-256 digests.
|
|
2228
|
+
// Source: the xAI-linked Cursor updater's stable `sand` channel, 2026-09-06.
|
|
2229
|
+
const VERIFIED_GROK_BOT_APPIMAGES = {
|
|
2230
|
+
'0.43.0': {
|
|
2231
|
+
arm64: {
|
|
2232
|
+
size: 132_756_388,
|
|
2233
|
+
sha256: '798757a576dd4a2f3953344af45197dd4dcf1fba792da850f20733a033ea4236',
|
|
2234
|
+
},
|
|
2235
|
+
x64: {
|
|
2236
|
+
size: 132_012_959,
|
|
2237
|
+
sha256: 'd810e4e1f49da15bc178f54cfc5840c780fb8efe1341d4220898fc4f244ba75b',
|
|
2238
|
+
},
|
|
2239
|
+
},
|
|
2240
|
+
};
|
|
2241
|
+
const VERIFIED_COPILOT_APPIMAGES = {
|
|
2242
|
+
'1.1.15': {
|
|
2243
|
+
arm64: {
|
|
2244
|
+
size: 485_141_000,
|
|
2245
|
+
sha256: '0d02335a7accea8e2f2faf0a6ab76f2ed9f0ed217793f900ed36ae110342f33a',
|
|
2246
|
+
},
|
|
2247
|
+
x64: {
|
|
2248
|
+
size: 535_726_584,
|
|
2249
|
+
sha256: '2f41db0a46b3c1d75d307bb47109440f4aa5faaf7f1b95df9f679ccbc35e2809',
|
|
2250
|
+
},
|
|
2251
|
+
},
|
|
2252
|
+
};
|
|
2253
|
+
function verifiedAppImage(executable, entry, architecture, expectedName, catalog, probe) {
|
|
2254
|
+
const realExecutable = probe.realPath?.(executable);
|
|
2255
|
+
if (!realExecutable?.startsWith('/') || !/\.AppImage$/i.test(realExecutable))
|
|
2256
|
+
return null;
|
|
2257
|
+
if (!regularExecutable(realExecutable, probe))
|
|
2258
|
+
return null;
|
|
2259
|
+
const header = probe.fileHeader?.(realExecutable, 64);
|
|
2260
|
+
if (!header || !validAppImageHeader(header, architecture))
|
|
2261
|
+
return null;
|
|
2262
|
+
const name = decodeDesktopValue(entry.get('Name') ?? '');
|
|
2263
|
+
const version = entry.get('X-AppImage-Version');
|
|
2264
|
+
if (name !== expectedName
|
|
2265
|
+
|| entry.get('Type') !== 'Application'
|
|
2266
|
+
|| entry.get('Terminal') === 'true'
|
|
2267
|
+
|| (version !== undefined && !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))) {
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
2270
|
+
const status = probe.fileStatus?.(realExecutable);
|
|
2271
|
+
if (!status)
|
|
2272
|
+
return null;
|
|
2273
|
+
const release = Object.entries(catalog)
|
|
2274
|
+
.map(([releaseVersion, architectures]) => ({ releaseVersion, asset: architectures[architecture] }))
|
|
2275
|
+
.find((candidate) => candidate.asset?.size === status.size);
|
|
2276
|
+
if (!release?.asset || (version !== undefined && version !== release.releaseVersion))
|
|
2277
|
+
return null;
|
|
2278
|
+
const actualDigest = probe.fileSha256?.(realExecutable)?.toLowerCase();
|
|
2279
|
+
if (actualDigest !== release.asset.sha256)
|
|
2280
|
+
return null;
|
|
2281
|
+
const declaredDigest = entry.get('X-AppImage-SHA256');
|
|
2282
|
+
if (declaredDigest !== undefined) {
|
|
2283
|
+
if (!/^[0-9a-f]{64}$/i.test(declaredDigest))
|
|
2284
|
+
return null;
|
|
2285
|
+
if (actualDigest !== declaredDigest.toLowerCase())
|
|
2286
|
+
return null;
|
|
2287
|
+
}
|
|
2288
|
+
return { realPath: realExecutable, size: status.size, sha256: actualDigest };
|
|
2289
|
+
}
|
|
2290
|
+
function verifiedGrokBotAppImage(executable, entry, architecture, probe) {
|
|
2291
|
+
return verifiedAppImage(executable, entry, architecture, 'Grok Bot', VERIFIED_GROK_BOT_APPIMAGES, probe)?.realPath ?? null;
|
|
2292
|
+
}
|
|
2293
|
+
function registeredLinuxCopilotAppImageTarget(home, environment, architecture, probe) {
|
|
2294
|
+
for (const directory of linuxApplicationDirectories(home, environment)) {
|
|
2295
|
+
for (const fileName of probe.directoryEntries?.(directory) ?? []) {
|
|
2296
|
+
if (!validDesktopFileName(fileName))
|
|
2297
|
+
continue;
|
|
2298
|
+
const entry = parseDesktopEntry(probe.fileContent?.(join(directory, fileName)) ?? '');
|
|
2299
|
+
if (!entry || !desktopEntryHasScheme(entry, 'ghapp'))
|
|
2300
|
+
continue;
|
|
2301
|
+
const invocation = parseDesktopExec(entry.get('Exec') ?? '');
|
|
2302
|
+
if (!invocation || invocation.length !== 1 || !invocation[0].startsWith('/'))
|
|
2303
|
+
continue;
|
|
2304
|
+
const verified = verifiedAppImage(invocation[0], entry, architecture, 'GitHub Copilot', VERIFIED_COPILOT_APPIMAGES, probe);
|
|
2305
|
+
if (!verified)
|
|
2306
|
+
continue;
|
|
2307
|
+
return {
|
|
2308
|
+
executable: verified.realPath,
|
|
2309
|
+
launchArgsPrefix: [],
|
|
2310
|
+
launchEnvironment: {},
|
|
2311
|
+
deepLinkScheme: 'ghapp',
|
|
2312
|
+
verification: {
|
|
2313
|
+
kind: 'copilot-appimage',
|
|
2314
|
+
realPath: verified.realPath,
|
|
2315
|
+
size: verified.size,
|
|
2316
|
+
sha256: verified.sha256,
|
|
2317
|
+
architecture,
|
|
2318
|
+
},
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
return null;
|
|
2323
|
+
}
|
|
2324
|
+
function registeredLinuxGrokBotAppImageTarget(home, environment, architecture, probe) {
|
|
2325
|
+
for (const directory of linuxApplicationDirectories(home, environment)) {
|
|
2326
|
+
for (const fileName of probe.directoryEntries?.(directory) ?? []) {
|
|
2327
|
+
if (!validDesktopFileName(fileName))
|
|
2328
|
+
continue;
|
|
2329
|
+
const path = join(directory, fileName);
|
|
2330
|
+
const entry = parseDesktopEntry(probe.fileContent?.(path) ?? '');
|
|
2331
|
+
if (!entry)
|
|
2332
|
+
continue;
|
|
2333
|
+
const invocation = parseDesktopExec(entry.get('Exec') ?? '');
|
|
2334
|
+
if (!invocation || !invocation[0].startsWith('/'))
|
|
2335
|
+
continue;
|
|
2336
|
+
const launchArgsPrefix = invocation.slice(1);
|
|
2337
|
+
if (launchArgsPrefix.length > 1
|
|
2338
|
+
|| (launchArgsPrefix.length === 1 && launchArgsPrefix[0] !== '--ozone-platform-hint=auto')) {
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2341
|
+
const executable = verifiedGrokBotAppImage(invocation[0], entry, architecture, probe);
|
|
2342
|
+
if (!executable)
|
|
2343
|
+
continue;
|
|
2344
|
+
const status = probe.fileStatus?.(executable);
|
|
2345
|
+
const sha256 = probe.fileSha256?.(executable)?.toLowerCase();
|
|
2346
|
+
if (!status || !sha256)
|
|
2347
|
+
continue;
|
|
2348
|
+
return {
|
|
2349
|
+
executable,
|
|
2350
|
+
launchArgsPrefix,
|
|
2351
|
+
launchEnvironment: {},
|
|
2352
|
+
verification: {
|
|
2353
|
+
kind: 'grok-appimage',
|
|
2354
|
+
realPath: executable,
|
|
2355
|
+
size: status.size,
|
|
2356
|
+
sha256,
|
|
2357
|
+
architecture,
|
|
2358
|
+
},
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
return null;
|
|
2363
|
+
}
|
|
2364
|
+
function registeredUrlSchemeTarget(surface, scheme, platform, home, environment, architecture, probe) {
|
|
2365
|
+
if (platform === 'darwin') {
|
|
2366
|
+
const application = candidateApplicationPaths(surface, platform, home, environment).find((candidate) => probe.fileExists(candidate)
|
|
2367
|
+
&& hasExpectedApplicationIdentity(surface, candidate, platform, architecture, probe));
|
|
2368
|
+
const executable = application ? probe.realPath?.(application) : null;
|
|
2369
|
+
const verification = application
|
|
2370
|
+
? applicationLaunchVerification(surface, application, platform, architecture, probe)
|
|
2371
|
+
: null;
|
|
2372
|
+
return executable && verification
|
|
2373
|
+
? { executable, launchArgsPrefix: [], launchEnvironment: {}, deepLinkScheme: scheme, verification }
|
|
2374
|
+
: null;
|
|
2375
|
+
}
|
|
237
2376
|
if (platform === 'win32') {
|
|
238
|
-
const
|
|
239
|
-
|
|
2377
|
+
const executable = probe.windowsUrlHandlerExecutable?.(scheme);
|
|
2378
|
+
if (!executable || !probe.fileExists(executable))
|
|
2379
|
+
return null;
|
|
2380
|
+
if (!windowsApplicationIdentityMatches(surface, executable, architecture, probe))
|
|
2381
|
+
return null;
|
|
2382
|
+
const realExecutable = probe.realPath?.(executable);
|
|
2383
|
+
const verification = applicationLaunchVerification(surface, executable, platform, architecture, probe);
|
|
2384
|
+
return realExecutable && verification
|
|
2385
|
+
? { executable: realExecutable, launchArgsPrefix: [], launchEnvironment: {}, deepLinkScheme: scheme, verification }
|
|
2386
|
+
: null;
|
|
240
2387
|
}
|
|
241
2388
|
if (platform === 'linux') {
|
|
242
|
-
const
|
|
243
|
-
|
|
2389
|
+
const desktopId = probe.linuxUrlSchemeDesktopId?.(scheme) ?? '';
|
|
2390
|
+
const path = desktopEntryPath(desktopId, home, environment, probe);
|
|
2391
|
+
const entry = path ? parseDesktopEntry(probe.fileContent?.(path) ?? '') : null;
|
|
2392
|
+
if (!entry || entry.get('Type') !== 'Application' || !desktopEntryHasScheme(entry, scheme))
|
|
2393
|
+
return null;
|
|
2394
|
+
const invocation = parseDesktopExec(entry.get('Exec') ?? '');
|
|
2395
|
+
if (!invocation || invocation.length !== 1)
|
|
2396
|
+
return null;
|
|
2397
|
+
const resolvedExecutable = invocation[0].startsWith('/')
|
|
2398
|
+
? invocation[0]
|
|
2399
|
+
: probe.commandPath(invocation[0]);
|
|
2400
|
+
if (!resolvedExecutable || !probe.fileExists(resolvedExecutable))
|
|
2401
|
+
return null;
|
|
2402
|
+
const expectedExecutable = surface.id === 'claude-desktop' ? '/usr/bin/claude-desktop' : '/usr/bin/github';
|
|
2403
|
+
if (resolvedExecutable !== expectedExecutable)
|
|
2404
|
+
return null;
|
|
2405
|
+
if (!hasExpectedApplicationIdentity(surface, resolvedExecutable, platform, architecture, probe))
|
|
2406
|
+
return null;
|
|
2407
|
+
const realExecutable = probe.realPath?.(resolvedExecutable);
|
|
2408
|
+
const verification = applicationLaunchVerification(surface, resolvedExecutable, platform, architecture, probe);
|
|
2409
|
+
return realExecutable && verification
|
|
2410
|
+
? { executable: realExecutable, launchArgsPrefix: [], launchEnvironment: {}, deepLinkScheme: scheme, verification }
|
|
2411
|
+
: null;
|
|
2412
|
+
}
|
|
2413
|
+
return null;
|
|
2414
|
+
}
|
|
2415
|
+
const EXPECTED_WINDOWS_APPX_IDENTITIES = {
|
|
2416
|
+
'claude-desktop': {
|
|
2417
|
+
packageName: 'Claude',
|
|
2418
|
+
publisher: 'CN="Anthropic, PBC", O="Anthropic, PBC", L=San Francisco, S=California, C=US, SERIALNUMBER=4860621, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US',
|
|
2419
|
+
publisherId: 'pzs8sxrjxfjjc',
|
|
2420
|
+
familyName: 'Claude_pzs8sxrjxfjjc',
|
|
2421
|
+
applicationId: 'Claude',
|
|
2422
|
+
signatureKinds: ['Developer'],
|
|
2423
|
+
},
|
|
2424
|
+
'codex-desktop': {
|
|
2425
|
+
packageName: 'OpenAI.Codex',
|
|
2426
|
+
publisher: 'CN=50BDFD77-8903-4850-9FFE-6E8522F64D5B',
|
|
2427
|
+
publisherId: '2p2nqsd0c76g0',
|
|
2428
|
+
familyName: 'OpenAI.Codex_2p2nqsd0c76g0',
|
|
2429
|
+
applicationId: 'App',
|
|
2430
|
+
signatureKinds: ['Store'],
|
|
2431
|
+
},
|
|
2432
|
+
};
|
|
2433
|
+
function windowsAppxTarget(surface, architecture, probe) {
|
|
2434
|
+
const expected = EXPECTED_WINDOWS_APPX_IDENTITIES[surface.id];
|
|
2435
|
+
if (!expected || (architecture !== 'x64' && architecture !== 'arm64'))
|
|
2436
|
+
return null;
|
|
2437
|
+
for (const packageName of surface.windowsAppxPackageNames ?? []) {
|
|
2438
|
+
const identity = probe.windowsAppxIdentity?.(packageName);
|
|
2439
|
+
if (!identity
|
|
2440
|
+
|| identity.packageName !== expected.packageName
|
|
2441
|
+
|| !identity.publisher.trim()
|
|
2442
|
+
|| (expected.publisher !== undefined && identity.publisher !== expected.publisher)
|
|
2443
|
+
|| identity.publisherId !== expected.publisherId
|
|
2444
|
+
|| identity.familyName !== expected.familyName
|
|
2445
|
+
|| !identity.applicationIds.includes(expected.applicationId)
|
|
2446
|
+
|| identity.architecture !== architecture
|
|
2447
|
+
|| !expected.signatureKinds.includes(identity.signatureKind)
|
|
2448
|
+
|| identity.status !== 'Ok')
|
|
2449
|
+
continue;
|
|
2450
|
+
return {
|
|
2451
|
+
executable: `shell:AppsFolder\\${identity.familyName}!${expected.applicationId}`,
|
|
2452
|
+
launchArgsPrefix: [],
|
|
2453
|
+
launchEnvironment: {},
|
|
2454
|
+
verification: {
|
|
2455
|
+
kind: 'windows-appx',
|
|
2456
|
+
packageName: identity.packageName,
|
|
2457
|
+
publisher: identity.publisher,
|
|
2458
|
+
publisherId: identity.publisherId,
|
|
2459
|
+
familyName: identity.familyName,
|
|
2460
|
+
applicationId: expected.applicationId,
|
|
2461
|
+
architecture: identity.architecture,
|
|
2462
|
+
signatureKind: identity.signatureKind,
|
|
2463
|
+
},
|
|
2464
|
+
};
|
|
244
2465
|
}
|
|
245
2466
|
return null;
|
|
246
2467
|
}
|
|
247
|
-
function findSurfaceTarget(surface, platform, home, probe) {
|
|
248
|
-
if (
|
|
249
|
-
|
|
2468
|
+
function findSurfaceTarget(surface, platform, architecture, home, environment, probe) {
|
|
2469
|
+
if (surface.id === 'vscode-copilot') {
|
|
2470
|
+
if (platform === 'darwin')
|
|
2471
|
+
return macVsCodeTarget(surface, architecture, home, environment, probe);
|
|
2472
|
+
if (platform === 'win32')
|
|
2473
|
+
return windowsVsCodeTarget(surface, architecture, home, environment, probe);
|
|
2474
|
+
if (platform === 'linux')
|
|
2475
|
+
return linuxVsCodeTarget(surface, architecture, home, probe);
|
|
2476
|
+
return null;
|
|
250
2477
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
2478
|
+
if (platform === 'linux' && surface.id === 'antigravity-desktop') {
|
|
2479
|
+
return verifiedLinuxAntigravityTarget(surface, architecture, home, environment, probe);
|
|
2480
|
+
}
|
|
2481
|
+
if (platform === 'linux' && surface.id === 'copilot-desktop') {
|
|
2482
|
+
const appImage = registeredLinuxCopilotAppImageTarget(home, environment, architecture, probe);
|
|
2483
|
+
if (appImage)
|
|
2484
|
+
return appImage;
|
|
258
2485
|
}
|
|
2486
|
+
if (platform === 'linux' && surface.id === 'grok-bot') {
|
|
2487
|
+
const appImage = registeredLinuxGrokBotAppImageTarget(home, environment, architecture, probe);
|
|
2488
|
+
if (appImage)
|
|
2489
|
+
return appImage;
|
|
2490
|
+
}
|
|
2491
|
+
if (surface.kind === 'cli') {
|
|
2492
|
+
return authenticatedCliTarget(surface, platform, architecture, home, environment, probe);
|
|
2493
|
+
}
|
|
2494
|
+
let applicationTarget = null;
|
|
2495
|
+
for (const application of candidateApplicationPaths(surface, platform, home, environment)) {
|
|
2496
|
+
if (!probe.fileExists(application))
|
|
2497
|
+
continue;
|
|
2498
|
+
const verification = applicationLaunchVerification(surface, application, platform, architecture, probe);
|
|
2499
|
+
const realApplication = verification ? probe.realPath?.(application) : null;
|
|
2500
|
+
if (!verification || !realApplication)
|
|
2501
|
+
continue;
|
|
2502
|
+
applicationTarget = {
|
|
2503
|
+
executable: realApplication,
|
|
2504
|
+
launchArgsPrefix: [],
|
|
2505
|
+
launchEnvironment: {},
|
|
2506
|
+
verification,
|
|
2507
|
+
};
|
|
2508
|
+
break;
|
|
2509
|
+
}
|
|
2510
|
+
const appx = platform === 'win32' ? windowsAppxTarget(surface, architecture, probe) : null;
|
|
259
2511
|
if (surface.id === 'claude-desktop') {
|
|
260
|
-
const handler = registeredUrlSchemeTarget('claude', platform, probe);
|
|
2512
|
+
const handler = registeredUrlSchemeTarget(surface, 'claude', platform, home, environment, architecture, probe);
|
|
261
2513
|
if (handler)
|
|
262
2514
|
return handler;
|
|
263
2515
|
}
|
|
264
2516
|
if (surface.id === 'copilot-desktop') {
|
|
265
|
-
const handler = registeredUrlSchemeTarget('ghapp', platform, probe);
|
|
2517
|
+
const handler = registeredUrlSchemeTarget(surface, 'ghapp', platform, home, environment, architecture, probe);
|
|
266
2518
|
if (handler)
|
|
267
2519
|
return handler;
|
|
268
2520
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
2521
|
+
if (appx)
|
|
2522
|
+
return appx;
|
|
2523
|
+
return applicationTarget;
|
|
2524
|
+
}
|
|
2525
|
+
function resolvedLaunchSupport(surface, target, targets, platform) {
|
|
2526
|
+
if (!target)
|
|
2527
|
+
return surface.launchSupport;
|
|
2528
|
+
if (surface.id === 'vscode-copilot') {
|
|
2529
|
+
return target.capabilities?.includes('initial-prompt') ? 'project-and-prompt' : 'project-only';
|
|
2530
|
+
}
|
|
2531
|
+
if (surface.id === 'claude-desktop') {
|
|
2532
|
+
return target.deepLinkScheme === 'claude' ? 'project-and-prompt' : 'manual-project';
|
|
2533
|
+
}
|
|
2534
|
+
if (surface.id === 'copilot-cli'
|
|
2535
|
+
|| surface.id === 'antigravity-cli'
|
|
2536
|
+
|| surface.id === 'claude-cli'
|
|
2537
|
+
|| surface.id === 'codex-cli'
|
|
2538
|
+
|| surface.id === 'grok-cli') {
|
|
2539
|
+
return target.capabilities?.includes('initial-prompt') ? 'project-and-prompt' : 'project-only';
|
|
2540
|
+
}
|
|
2541
|
+
if (surface.id === 'copilot-desktop') {
|
|
2542
|
+
return targets.get('copilot-cli')?.capabilities?.includes('desktop-project-launch')
|
|
2543
|
+
? 'project-only'
|
|
2544
|
+
: 'manual-project';
|
|
2545
|
+
}
|
|
2546
|
+
if (surface.id === 'codex-desktop') {
|
|
2547
|
+
return platform === 'darwin'
|
|
2548
|
+
&& targets.get('codex-cli')?.capabilities?.includes('desktop-project-launch')
|
|
2549
|
+
? 'project-only'
|
|
2550
|
+
: 'manual-project';
|
|
2551
|
+
}
|
|
2552
|
+
return surface.launchSupport;
|
|
273
2553
|
}
|
|
274
2554
|
export async function detectAiSurfaces(options = {}) {
|
|
275
2555
|
const platform = options.platform ?? process.platform;
|
|
2556
|
+
if (options.platform !== undefined
|
|
2557
|
+
&& options.platform !== process.platform
|
|
2558
|
+
&& options.architecture === undefined) {
|
|
2559
|
+
throw new Error(`An explicit architecture is required when detecting ${options.platform} AI surfaces on ${process.platform}.`);
|
|
2560
|
+
}
|
|
2561
|
+
const architecture = options.architecture ?? process.arch;
|
|
276
2562
|
const home = options.home ?? homedir();
|
|
277
|
-
const
|
|
2563
|
+
const environment = options.environment ?? process.env;
|
|
2564
|
+
const baseProbe = options.probe ?? systemSurfaceProbe;
|
|
2565
|
+
const macIdentityCache = new Map();
|
|
2566
|
+
const probe = platform === 'darwin' && baseProbe.macApplicationIdentity
|
|
2567
|
+
? {
|
|
2568
|
+
...baseProbe,
|
|
2569
|
+
macApplicationIdentity(path, expected) {
|
|
2570
|
+
const cacheKey = JSON.stringify([
|
|
2571
|
+
path,
|
|
2572
|
+
expected?.bundleIdentifier ?? '',
|
|
2573
|
+
expected?.teamIdentifier ?? '',
|
|
2574
|
+
expected?.requiredScheme ?? '',
|
|
2575
|
+
]);
|
|
2576
|
+
if (!macIdentityCache.has(cacheKey)) {
|
|
2577
|
+
macIdentityCache.set(cacheKey, baseProbe.macApplicationIdentity?.(path, expected) ?? null);
|
|
2578
|
+
}
|
|
2579
|
+
return macIdentityCache.get(cacheKey) ?? null;
|
|
2580
|
+
},
|
|
2581
|
+
}
|
|
2582
|
+
: baseProbe;
|
|
278
2583
|
const preferences = options.preferredSurface === undefined ? await readAiPreferences(home) : { version: 1, lastAiSurface: options.preferredSurface ?? undefined };
|
|
279
|
-
const
|
|
280
|
-
surface,
|
|
281
|
-
|
|
2584
|
+
const targets = new Map(AI_SURFACES.map((surface) => {
|
|
2585
|
+
const target = findSurfaceTarget(surface, platform, architecture, home, environment, probe);
|
|
2586
|
+
return [surface.id, target?.verification ? target : null];
|
|
282
2587
|
}));
|
|
2588
|
+
const installed = AI_SURFACES.map((definition) => {
|
|
2589
|
+
const target = targets.get(definition.id) ?? null;
|
|
2590
|
+
const surface = {
|
|
2591
|
+
...definition,
|
|
2592
|
+
launchSupport: resolvedLaunchSupport(definition, target, targets, platform),
|
|
2593
|
+
};
|
|
2594
|
+
return { surface, target };
|
|
2595
|
+
});
|
|
283
2596
|
const availableIds = new Set(installed.filter((item) => item.target).map((item) => item.surface.id));
|
|
284
2597
|
const preferredSurface = preferences.lastAiSurface && availableIds.has(preferences.lastAiSurface)
|
|
285
2598
|
? preferences.lastAiSurface
|
|
286
2599
|
: null;
|
|
2600
|
+
const launchSupportPriority = {
|
|
2601
|
+
'project-and-prompt': 4,
|
|
2602
|
+
'project-only': 3,
|
|
2603
|
+
'manual-project': 2,
|
|
2604
|
+
'launch-only': 1,
|
|
2605
|
+
};
|
|
2606
|
+
const bestInstalled = installed
|
|
2607
|
+
.filter((item) => item.target)
|
|
2608
|
+
.reduce((best, item) => {
|
|
2609
|
+
if (!best)
|
|
2610
|
+
return item;
|
|
2611
|
+
return launchSupportPriority[item.surface.launchSupport] > launchSupportPriority[best.surface.launchSupport]
|
|
2612
|
+
? item
|
|
2613
|
+
: best;
|
|
2614
|
+
}, null);
|
|
287
2615
|
const recommendedSurface = preferredSurface
|
|
288
|
-
??
|
|
2616
|
+
?? bestInstalled?.surface.id
|
|
289
2617
|
?? 'vscode-copilot';
|
|
290
2618
|
return {
|
|
291
|
-
contractVersion: 'eai.ai-surfaces/
|
|
2619
|
+
contractVersion: 'eai.ai-surfaces/v2',
|
|
292
2620
|
platform,
|
|
293
2621
|
projectDirectory: platform === 'win32'
|
|
294
2622
|
? win32Path.resolve(options.projectDirectory ?? process.cwd())
|
|
@@ -301,6 +2629,9 @@ export async function detectAiSurfaces(options = {}) {
|
|
|
301
2629
|
executable: target?.executable ?? null,
|
|
302
2630
|
launchArgsPrefix: target?.launchArgsPrefix ?? [],
|
|
303
2631
|
launchEnvironment: target?.launchEnvironment ?? {},
|
|
2632
|
+
capabilities: target?.capabilities ?? [],
|
|
2633
|
+
...(target?.deepLinkScheme ? { deepLinkScheme: target.deepLinkScheme } : {}),
|
|
2634
|
+
...(target?.verification ? { verification: target.verification } : {}),
|
|
304
2635
|
recommended: surface.id === recommendedSurface,
|
|
305
2636
|
previouslyUsed: surface.id === preferredSurface,
|
|
306
2637
|
status: target ? 'ready' : 'not-installed',
|
|
@@ -310,6 +2641,173 @@ export async function detectAiSurfaces(options = {}) {
|
|
|
310
2641
|
})),
|
|
311
2642
|
};
|
|
312
2643
|
}
|
|
2644
|
+
function isLegacyAiSurfaceId(surfaceId) {
|
|
2645
|
+
return surfaceId !== null && LEGACY_AI_SURFACES.some((surface) => surface.id === surfaceId);
|
|
2646
|
+
}
|
|
2647
|
+
function equalStringArrays(left, right) {
|
|
2648
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
2649
|
+
}
|
|
2650
|
+
function expectedInventoryLaunchSupport(definition, detected, detectedById, platform) {
|
|
2651
|
+
if (!detected.installed)
|
|
2652
|
+
return definition.launchSupport;
|
|
2653
|
+
if (definition.id === 'vscode-copilot') {
|
|
2654
|
+
return detected.capabilities.includes('initial-prompt') ? 'project-and-prompt' : 'project-only';
|
|
2655
|
+
}
|
|
2656
|
+
if (definition.id === 'claude-desktop') {
|
|
2657
|
+
return detected.deepLinkScheme === 'claude' ? 'project-and-prompt' : 'manual-project';
|
|
2658
|
+
}
|
|
2659
|
+
if (definition.id === 'copilot-cli'
|
|
2660
|
+
|| definition.id === 'antigravity-cli'
|
|
2661
|
+
|| definition.id === 'claude-cli'
|
|
2662
|
+
|| definition.id === 'codex-cli'
|
|
2663
|
+
|| definition.id === 'grok-cli') {
|
|
2664
|
+
return detected.capabilities.includes('initial-prompt') ? 'project-and-prompt' : 'project-only';
|
|
2665
|
+
}
|
|
2666
|
+
if (definition.id === 'copilot-desktop') {
|
|
2667
|
+
const cli = detectedById.get('copilot-cli');
|
|
2668
|
+
return cli?.installed && cli.capabilities.includes('desktop-project-launch')
|
|
2669
|
+
? 'project-only'
|
|
2670
|
+
: 'manual-project';
|
|
2671
|
+
}
|
|
2672
|
+
if (definition.id === 'codex-desktop') {
|
|
2673
|
+
const cli = detectedById.get('codex-cli');
|
|
2674
|
+
return platform === 'darwin'
|
|
2675
|
+
&& cli?.installed
|
|
2676
|
+
&& cli.capabilities.includes('desktop-project-launch')
|
|
2677
|
+
? 'project-only'
|
|
2678
|
+
: 'manual-project';
|
|
2679
|
+
}
|
|
2680
|
+
return definition.launchSupport;
|
|
2681
|
+
}
|
|
2682
|
+
function authenticatedSurfaceCapabilities(surface, platform) {
|
|
2683
|
+
return vscodeCatalogVerificationShapeMatches(surface, platform)
|
|
2684
|
+
? ['initial-prompt']
|
|
2685
|
+
: [];
|
|
2686
|
+
}
|
|
2687
|
+
function canonicalV2InventoryOrThrow(inventory) {
|
|
2688
|
+
if (inventory.contractVersion !== 'eai.ai-surfaces/v2'
|
|
2689
|
+
|| inventory.surfaces.length !== V2_AI_SURFACE_IDS.length) {
|
|
2690
|
+
throw new Error('EAI refused to serialize a noncanonical v2 AI surface inventory.');
|
|
2691
|
+
}
|
|
2692
|
+
const detectedById = new Map();
|
|
2693
|
+
for (const [index, detected] of inventory.surfaces.entries()) {
|
|
2694
|
+
const expectedId = V2_AI_SURFACE_IDS[index];
|
|
2695
|
+
const definition = AI_SURFACES[index];
|
|
2696
|
+
if (!definition
|
|
2697
|
+
|| detected.id !== expectedId
|
|
2698
|
+
|| detectedById.has(detected.id)
|
|
2699
|
+
|| detected.name !== definition.name
|
|
2700
|
+
|| detected.provider !== definition.provider
|
|
2701
|
+
|| detected.kind !== definition.kind
|
|
2702
|
+
|| detected.installUrl !== definition.installUrl
|
|
2703
|
+
|| !equalStringArrays(detected.commands, definition.commands)) {
|
|
2704
|
+
throw new Error('EAI refused to serialize a tampered or reordered v2 AI surface inventory.');
|
|
2705
|
+
}
|
|
2706
|
+
detectedById.set(detected.id, detected);
|
|
2707
|
+
}
|
|
2708
|
+
const validReference = (surfaceId) => surfaceId === null
|
|
2709
|
+
|| detectedById.has(surfaceId);
|
|
2710
|
+
if (!validReference(inventory.preferredSurface)
|
|
2711
|
+
|| !validReference(inventory.recommendedSurface)
|
|
2712
|
+
|| (inventory.preferredSurface !== null
|
|
2713
|
+
&& !detectedById.get(inventory.preferredSurface)?.installed)) {
|
|
2714
|
+
throw new Error('EAI refused to serialize a v2 inventory with a dangling surface reference.');
|
|
2715
|
+
}
|
|
2716
|
+
for (const [index, detected] of inventory.surfaces.entries()) {
|
|
2717
|
+
const definition = AI_SURFACES[index];
|
|
2718
|
+
const authenticatedCapabilities = detected.installed
|
|
2719
|
+
? authenticatedSurfaceCapabilities(detected, inventory.platform)
|
|
2720
|
+
: [];
|
|
2721
|
+
if (!Array.isArray(detected.capabilities)
|
|
2722
|
+
|| new Set(detected.capabilities).size !== detected.capabilities.length
|
|
2723
|
+
|| !equalStringArrays(detected.capabilities, authenticatedCapabilities)) {
|
|
2724
|
+
throw new Error(`EAI refused to serialize invalid capabilities for ${detected.id}.`);
|
|
2725
|
+
}
|
|
2726
|
+
const installed = detected.installed === true;
|
|
2727
|
+
if (installed !== Boolean(detected.executable && detected.verification)
|
|
2728
|
+
|| detected.status !== (installed ? 'ready' : 'not-installed')
|
|
2729
|
+
|| (!installed && (detected.executable !== null
|
|
2730
|
+
|| detected.verification !== undefined
|
|
2731
|
+
|| detected.capabilities.length !== 0))
|
|
2732
|
+
|| detected.recommended !== (detected.id === inventory.recommendedSurface)
|
|
2733
|
+
|| detected.previouslyUsed !== (detected.id === inventory.preferredSurface)
|
|
2734
|
+
|| detected.nextAction !== (installed
|
|
2735
|
+
? `Start EAI in ${definition.name}`
|
|
2736
|
+
: `Get ${definition.name} from ${definition.provider}`)
|
|
2737
|
+
|| detected.launchSupport !== expectedInventoryLaunchSupport(definition, detected, detectedById, inventory.platform)) {
|
|
2738
|
+
throw new Error(`EAI refused to serialize inconsistent v2 state for ${detected.id}.`);
|
|
2739
|
+
}
|
|
2740
|
+
if (installed
|
|
2741
|
+
&& detected.id === 'vscode-copilot'
|
|
2742
|
+
&& ((inventory.platform === 'win32' || inventory.platform === 'linux')
|
|
2743
|
+
? (detected.verification?.kind !== 'vscode-catalog'
|
|
2744
|
+
|| !vscodeCatalogVerificationShapeMatches(detected, inventory.platform))
|
|
2745
|
+
: inventory.platform === 'darwin'
|
|
2746
|
+
? detected.verification?.kind !== 'mac-application'
|
|
2747
|
+
: true)) {
|
|
2748
|
+
throw new Error('EAI refused to serialize unauthenticated VS Code catalog evidence.');
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
return inventory.surfaces;
|
|
2752
|
+
}
|
|
2753
|
+
export function serializeAiSurfaceInventory(inventory, contractVersion) {
|
|
2754
|
+
if (contractVersion === 'v2') {
|
|
2755
|
+
const canonicalDetected = canonicalV2InventoryOrThrow(inventory);
|
|
2756
|
+
return {
|
|
2757
|
+
contractVersion: 'eai.ai-surfaces/v2',
|
|
2758
|
+
platform: inventory.platform,
|
|
2759
|
+
projectDirectory: inventory.projectDirectory,
|
|
2760
|
+
preferredSurface: inventory.preferredSurface,
|
|
2761
|
+
recommendedSurface: inventory.recommendedSurface,
|
|
2762
|
+
surfaces: AI_SURFACES.map((definition, index) => ({
|
|
2763
|
+
id: definition.id,
|
|
2764
|
+
name: definition.name,
|
|
2765
|
+
provider: definition.provider,
|
|
2766
|
+
kind: definition.kind,
|
|
2767
|
+
installUrl: definition.installUrl,
|
|
2768
|
+
launchSupport: canonicalDetected[index].launchSupport,
|
|
2769
|
+
capabilities: [...canonicalDetected[index].capabilities],
|
|
2770
|
+
installed: canonicalDetected[index].installed,
|
|
2771
|
+
recommended: canonicalDetected[index].recommended,
|
|
2772
|
+
previouslyUsed: canonicalDetected[index].previouslyUsed,
|
|
2773
|
+
status: canonicalDetected[index].status,
|
|
2774
|
+
nextAction: canonicalDetected[index].nextAction,
|
|
2775
|
+
})),
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
const detectedById = new Map(inventory.surfaces.map((surface) => [surface.id, surface]));
|
|
2779
|
+
const preferredSurface = isLegacyAiSurfaceId(inventory.preferredSurface)
|
|
2780
|
+
&& detectedById.get(inventory.preferredSurface)?.installed
|
|
2781
|
+
? inventory.preferredSurface
|
|
2782
|
+
: null;
|
|
2783
|
+
const recommendedSurface = preferredSurface
|
|
2784
|
+
?? LEGACY_AI_SURFACES.find((surface) => detectedById.get(surface.id)?.installed)?.id
|
|
2785
|
+
?? 'vscode-copilot';
|
|
2786
|
+
return {
|
|
2787
|
+
contractVersion: 'eai.ai-surfaces/v1',
|
|
2788
|
+
platform: inventory.platform,
|
|
2789
|
+
projectDirectory: inventory.projectDirectory,
|
|
2790
|
+
preferredSurface,
|
|
2791
|
+
recommendedSurface,
|
|
2792
|
+
surfaces: LEGACY_AI_SURFACES.map((definition) => {
|
|
2793
|
+
const detected = detectedById.get(definition.id);
|
|
2794
|
+
const installed = Boolean(detected?.installed);
|
|
2795
|
+
return {
|
|
2796
|
+
...definition,
|
|
2797
|
+
installed,
|
|
2798
|
+
executable: detected?.executable ?? null,
|
|
2799
|
+
launchArgsPrefix: detected ? [...detected.launchArgsPrefix] : [],
|
|
2800
|
+
launchEnvironment: detected ? { ...detected.launchEnvironment } : {},
|
|
2801
|
+
recommended: definition.id === recommendedSurface,
|
|
2802
|
+
previouslyUsed: definition.id === preferredSurface,
|
|
2803
|
+
status: installed ? 'ready' : 'not-installed',
|
|
2804
|
+
nextAction: installed
|
|
2805
|
+
? `Start EAI in ${definition.name}`
|
|
2806
|
+
: `Get ${definition.name} from ${definition.provider}`,
|
|
2807
|
+
};
|
|
2808
|
+
}),
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
313
2811
|
function surfaceOrThrow(inventory, surfaceId) {
|
|
314
2812
|
const surface = inventory.surfaces.find((candidate) => candidate.id === surfaceId);
|
|
315
2813
|
if (!surface)
|
|
@@ -321,29 +2819,78 @@ export function buildAiLaunchPlan(inventory, surfaceId) {
|
|
|
321
2819
|
if (!surface.installed || !surface.executable) {
|
|
322
2820
|
throw new Error(`${surface.name} is not installed. Open ${surface.installUrl} to get it.`);
|
|
323
2821
|
}
|
|
2822
|
+
if (!surface.verification) {
|
|
2823
|
+
throw new Error(`EAI refused to launch ${surface.name} without authenticated launch evidence.`);
|
|
2824
|
+
}
|
|
2825
|
+
if (!equalStringArrays(surface.capabilities, authenticatedSurfaceCapabilities(surface, inventory.platform))) {
|
|
2826
|
+
throw new Error(`EAI refused unproven optional launch capabilities for ${surface.name}.`);
|
|
2827
|
+
}
|
|
324
2828
|
const project = inventory.projectDirectory;
|
|
325
2829
|
const common = {
|
|
326
2830
|
surfaceId,
|
|
327
2831
|
surfaceName: surface.name,
|
|
328
2832
|
projectDirectory: project,
|
|
329
2833
|
cwd: project,
|
|
2834
|
+
verification: surface.verification,
|
|
330
2835
|
};
|
|
331
2836
|
switch (surfaceId) {
|
|
332
2837
|
case 'vscode-copilot':
|
|
333
|
-
|
|
2838
|
+
if (surface.verification.kind === 'vscode-catalog') {
|
|
2839
|
+
if (!vscodeCatalogVerificationShapeMatches(surface, inventory.platform)) {
|
|
2840
|
+
throw new Error('EAI refused unauthenticated VS Code catalog evidence.');
|
|
2841
|
+
}
|
|
2842
|
+
return {
|
|
2843
|
+
...common,
|
|
2844
|
+
mode: 'process',
|
|
2845
|
+
command: surface.verification.realPath,
|
|
2846
|
+
args: [surface.verification.cliPath, 'chat', '-m', 'agent', EAI_FIRST_PROMPT],
|
|
2847
|
+
environment: { ...VSCODE_CATALOG_LAUNCH_ENVIRONMENT },
|
|
2848
|
+
preparedPrompt: true,
|
|
2849
|
+
userMessage: 'VS Code will open this project and start an EAI Copilot chat.',
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
if (inventory.platform !== 'darwin' || surface.verification.kind !== 'mac-application') {
|
|
2853
|
+
throw new Error('EAI refused unauthenticated VS Code launch evidence.');
|
|
2854
|
+
}
|
|
2855
|
+
return {
|
|
2856
|
+
...common,
|
|
2857
|
+
mode: 'process',
|
|
2858
|
+
command: surface.executable,
|
|
2859
|
+
args: [project],
|
|
2860
|
+
environment: {},
|
|
2861
|
+
preparedPrompt: false,
|
|
2862
|
+
userMessage: 'VS Code will open this project. Open Copilot Chat and ask it to use the repository EAI skill.',
|
|
2863
|
+
};
|
|
334
2864
|
case 'copilot-cli':
|
|
335
|
-
return
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
2865
|
+
return surface.capabilities.includes('initial-prompt')
|
|
2866
|
+
? { ...common, mode: 'terminal', command: surface.executable, args: ['-C', project, '-i', EAI_FIRST_PROMPT], preparedPrompt: true, userMessage: 'A terminal will open an interactive EAI Copilot session.' }
|
|
2867
|
+
: { ...common, mode: 'terminal', command: surface.executable, args: [], preparedPrompt: false, userMessage: 'A terminal will open GitHub Copilot CLI in this project. Ask it to use the repository EAI skill.' };
|
|
2868
|
+
case 'copilot-desktop': {
|
|
2869
|
+
const copilotCli = inventory.surfaces.find((candidate) => candidate.id === 'copilot-cli');
|
|
2870
|
+
if (copilotCli?.installed
|
|
2871
|
+
&& copilotCli.executable
|
|
2872
|
+
&& copilotCli.capabilities.includes('desktop-project-launch')) {
|
|
2873
|
+
if (!copilotCli.verification)
|
|
2874
|
+
throw new Error('EAI refused to delegate to an unverified GitHub Copilot CLI.');
|
|
2875
|
+
return { ...common, verification: copilotCli.verification, mode: 'process', command: copilotCli.executable, args: ['app'], preparedPrompt: false, userMessage: 'GitHub Copilot will open a new desktop session in this project. Ask it to use the repository EAI skill.' };
|
|
2876
|
+
}
|
|
2877
|
+
return surface.deepLinkScheme === 'ghapp'
|
|
2878
|
+
? { ...common, mode: 'application', command: surface.executable, args: ['ghapp://recent'], preparedPrompt: false, userMessage: 'GitHub Copilot will open. Choose this project folder once, then ask it to use the repository EAI skill.', ...(surface.verification ? { verification: surface.verification } : {}) }
|
|
2879
|
+
: { ...common, mode: 'application', command: surface.executable, args: [], preparedPrompt: false, userMessage: 'GitHub Copilot will open. Choose this project folder once, then ask it to use the repository EAI skill.', ...(surface.verification ? { verification: surface.verification } : {}) };
|
|
2880
|
+
}
|
|
2881
|
+
case 'antigravity-desktop':
|
|
2882
|
+
return { ...common, mode: 'application', command: surface.executable, args: [], preparedPrompt: false, userMessage: 'Google Antigravity will open. Add this project folder, create or select its project, then ask it to use the repository EAI skill.', ...(surface.verification ? { verification: surface.verification } : {}) };
|
|
2883
|
+
case 'antigravity-cli':
|
|
2884
|
+
return surface.capabilities.includes('initial-prompt')
|
|
2885
|
+
? { ...common, mode: 'terminal', command: surface.executable, args: ['-i', EAI_FIRST_PROMPT], preparedPrompt: true, userMessage: 'A terminal will open an interactive Antigravity EAI session in this project.' }
|
|
2886
|
+
: { ...common, mode: 'terminal', command: surface.executable, args: [], preparedPrompt: false, userMessage: 'A terminal will open Antigravity CLI in this project. Ask it to use the repository EAI skill.' };
|
|
340
2887
|
case 'claude-desktop':
|
|
341
|
-
return surface.
|
|
2888
|
+
return surface.deepLinkScheme === 'claude'
|
|
342
2889
|
? {
|
|
343
2890
|
...common,
|
|
344
|
-
mode: '
|
|
345
|
-
command:
|
|
346
|
-
args: [],
|
|
2891
|
+
mode: 'application',
|
|
2892
|
+
command: surface.executable,
|
|
2893
|
+
args: [`claude://code/new?q=${encodeURIComponent(EAI_FIRST_PROMPT)}&folder=${encodeURIComponent(project)}`],
|
|
347
2894
|
preparedPrompt: true,
|
|
348
2895
|
userMessage: 'Claude Desktop will open a Code session for this project with the EAI starting prompt ready to review.',
|
|
349
2896
|
}
|
|
@@ -356,17 +2903,32 @@ export function buildAiLaunchPlan(inventory, surfaceId) {
|
|
|
356
2903
|
userMessage: 'Claude Desktop will open. Choose this project folder, start a Code session, then ask it to use the repository EAI skill.',
|
|
357
2904
|
};
|
|
358
2905
|
case 'claude-cli':
|
|
359
|
-
return
|
|
2906
|
+
return surface.capabilities.includes('initial-prompt')
|
|
2907
|
+
? { ...common, mode: 'terminal', command: surface.executable, args: [EAI_FIRST_PROMPT], preparedPrompt: true, userMessage: 'A terminal will open an interactive Claude EAI session.' }
|
|
2908
|
+
: { ...common, mode: 'terminal', command: surface.executable, args: [], preparedPrompt: false, userMessage: 'A terminal will open Claude Code in this project. Ask it to use the repository EAI skill.' };
|
|
360
2909
|
case 'codex-desktop': {
|
|
361
2910
|
const codexCli = inventory.surfaces.find((candidate) => candidate.id === 'codex-cli');
|
|
362
|
-
return
|
|
363
|
-
|
|
2911
|
+
return inventory.platform === 'darwin'
|
|
2912
|
+
&& codexCli?.installed
|
|
2913
|
+
&& codexCli.executable
|
|
2914
|
+
&& codexCli.capabilities.includes('desktop-project-launch')
|
|
2915
|
+
? (() => {
|
|
2916
|
+
if (!codexCli.verification)
|
|
2917
|
+
throw new Error('EAI refused to delegate to an unverified Codex CLI.');
|
|
2918
|
+
return { ...common, verification: codexCli.verification, mode: 'process', command: codexCli.executable, args: ['app', project], preparedPrompt: false, userMessage: 'ChatGPT Desktop will open this project in Codex. Ask it to use the repository EAI skill.' };
|
|
2919
|
+
})()
|
|
364
2920
|
: { ...common, mode: 'application', command: surface.executable, args: [], preparedPrompt: false, userMessage: 'ChatGPT Desktop will open. Choose this project folder, select Codex, then ask it to use the repository EAI skill.' };
|
|
365
2921
|
}
|
|
366
2922
|
case 'codex-cli':
|
|
367
|
-
return
|
|
2923
|
+
return surface.capabilities.includes('initial-prompt')
|
|
2924
|
+
? { ...common, mode: 'terminal', command: surface.executable, args: [EAI_FIRST_PROMPT], preparedPrompt: true, userMessage: 'A terminal will open an interactive Codex EAI session.' }
|
|
2925
|
+
: { ...common, mode: 'terminal', command: surface.executable, args: [], preparedPrompt: false, userMessage: 'A terminal will open Codex CLI in this project. Ask it to use the repository EAI skill.' };
|
|
2926
|
+
case 'grok-bot':
|
|
2927
|
+
return { ...common, mode: 'application', command: surface.executable, args: [...surface.launchArgsPrefix], preparedPrompt: false, userMessage: 'Grok Bot will open. It manages cloud Bots and does not automatically open this local project; use Grok Build for a local coding workspace.', ...(surface.verification ? { verification: surface.verification } : {}) };
|
|
368
2928
|
case 'grok-cli':
|
|
369
|
-
return
|
|
2929
|
+
return surface.capabilities.includes('initial-prompt')
|
|
2930
|
+
? { ...common, mode: 'terminal', command: surface.executable, args: ['--cwd', project, EAI_FIRST_PROMPT], preparedPrompt: true, userMessage: 'A terminal will open an interactive Grok Build EAI session for this project.' }
|
|
2931
|
+
: { ...common, mode: 'terminal', command: surface.executable, args: ['--cwd', project], preparedPrompt: false, userMessage: 'A terminal will open Grok Build in this project. Ask it to use the repository EAI skill.' };
|
|
370
2932
|
}
|
|
371
2933
|
}
|
|
372
2934
|
function shellQuote(value, platform) {
|
|
@@ -374,14 +2936,30 @@ function shellQuote(value, platform) {
|
|
|
374
2936
|
return `"${value.replace(/"/g, '""')}"`;
|
|
375
2937
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
376
2938
|
}
|
|
377
|
-
function
|
|
2939
|
+
function sanitizedVsCodeRuntimeEnvironment(platform, ambient = process.env) {
|
|
2940
|
+
const sanitized = {};
|
|
2941
|
+
for (const [key, value] of Object.entries(ambient)) {
|
|
2942
|
+
const normalized = key.toUpperCase();
|
|
2943
|
+
if (value === undefined
|
|
2944
|
+
|| normalized.startsWith('NODE_')
|
|
2945
|
+
|| normalized.startsWith('ELECTRON_')
|
|
2946
|
+
|| normalized.startsWith('VSCODE_')
|
|
2947
|
+
|| (platform === 'linux'
|
|
2948
|
+
&& (normalized.startsWith('LD_') || normalized.startsWith('DYLD_'))))
|
|
2949
|
+
continue;
|
|
2950
|
+
sanitized[key] = value;
|
|
2951
|
+
}
|
|
2952
|
+
return { ...sanitized, ...VSCODE_CATALOG_LAUNCH_ENVIRONMENT };
|
|
2953
|
+
}
|
|
2954
|
+
function spawnDetached(command, args, cwd, environment, windowsVerbatimArguments, windowsHide = true, replaceEnvironment = false) {
|
|
378
2955
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
379
2956
|
const child = spawn(command, [...args], {
|
|
380
2957
|
cwd,
|
|
381
2958
|
detached: true,
|
|
382
2959
|
stdio: 'ignore',
|
|
383
|
-
windowsHide
|
|
384
|
-
env: { ...process.env, ...environment },
|
|
2960
|
+
windowsHide,
|
|
2961
|
+
env: replaceEnvironment ? { ...environment } : { ...process.env, ...environment },
|
|
2962
|
+
...(windowsVerbatimArguments ? { windowsVerbatimArguments } : {}),
|
|
385
2963
|
});
|
|
386
2964
|
child.once('spawn', () => {
|
|
387
2965
|
child.unref();
|
|
@@ -390,55 +2968,406 @@ function spawnDetached(command, args, cwd, environment) {
|
|
|
390
2968
|
child.once('error', rejectPromise);
|
|
391
2969
|
});
|
|
392
2970
|
}
|
|
2971
|
+
async function spawnWindowsCommand(command, args, action, cwd, environment, windowsHide = true) {
|
|
2972
|
+
const runtimeEnvironment = { ...process.env, ...environment };
|
|
2973
|
+
const invocation = buildWindowsCommandInvocation(command, args, action, runtimeEnvironment);
|
|
2974
|
+
await spawnDetached(invocation.command, invocation.args, cwd, environment, invocation.windowsVerbatimArguments, windowsHide);
|
|
2975
|
+
}
|
|
393
2976
|
export async function openExternalUrl(url, platform = process.platform) {
|
|
394
2977
|
if (!/^https:\/\//.test(url)) {
|
|
395
2978
|
throw new Error('EAI refused to open a non-HTTPS provider location.');
|
|
396
2979
|
}
|
|
397
2980
|
if (platform === 'darwin')
|
|
398
|
-
await spawnDetached('open', [url]);
|
|
2981
|
+
await spawnDetached('/usr/bin/open', [url]);
|
|
399
2982
|
else if (platform === 'win32')
|
|
400
|
-
await spawnDetached('rundll32.exe', ['url.dll,FileProtocolHandler', url]);
|
|
2983
|
+
await spawnDetached('C:\\Windows\\System32\\rundll32.exe', ['url.dll,FileProtocolHandler', url]);
|
|
401
2984
|
else
|
|
402
|
-
await spawnDetached('xdg-open', [url]);
|
|
2985
|
+
await spawnDetached('/usr/bin/xdg-open', [url]);
|
|
2986
|
+
}
|
|
2987
|
+
function trustedSurfaceUrlIsSupported(url) {
|
|
2988
|
+
return /^(?:claude:\/\/code\/new(?:[?#]|$)|ghapp:\/\/recent(?:[?#]|$))/.test(url);
|
|
403
2989
|
}
|
|
404
2990
|
async function openTrustedSurfaceUrl(url, platform) {
|
|
405
|
-
if (
|
|
2991
|
+
if (!trustedSurfaceUrlIsSupported(url)) {
|
|
406
2992
|
throw new Error('EAI refused to open an unsupported AI workspace location.');
|
|
407
2993
|
}
|
|
408
2994
|
if (platform === 'darwin')
|
|
409
|
-
await spawnDetached('open', [url]);
|
|
2995
|
+
await spawnDetached('/usr/bin/open', [url]);
|
|
410
2996
|
else if (platform === 'win32')
|
|
411
|
-
await spawnDetached('rundll32.exe', ['url.dll,FileProtocolHandler', url]);
|
|
2997
|
+
await spawnDetached('C:\\Windows\\System32\\rundll32.exe', ['url.dll,FileProtocolHandler', url]);
|
|
412
2998
|
else
|
|
413
|
-
await spawnDetached('xdg-open', [url]);
|
|
2999
|
+
await spawnDetached('/usr/bin/xdg-open', [url]);
|
|
414
3000
|
}
|
|
415
|
-
export
|
|
3001
|
+
export function buildLinuxTerminalInvocation(plan, commandPath = defaultCommandPath) {
|
|
3002
|
+
const candidates = [
|
|
3003
|
+
{ name: 'xdg-terminal-exec', trustedRealPaths: ['/usr/bin/xdg-terminal-exec'], args: () => ['--', plan.command, ...plan.args] },
|
|
3004
|
+
{ name: 'kgx', trustedRealPaths: ['/usr/bin/kgx'], args: () => [`--working-directory=${plan.cwd}`, '--', plan.command, ...plan.args] },
|
|
3005
|
+
{ name: 'gnome-terminal', trustedRealPaths: ['/usr/bin/gnome-terminal'], args: () => [`--working-directory=${plan.cwd}`, '--', plan.command, ...plan.args] },
|
|
3006
|
+
{ name: 'konsole', trustedRealPaths: ['/usr/bin/konsole'], args: () => ['--workdir', plan.cwd, '-e', plan.command, ...plan.args] },
|
|
3007
|
+
{ name: 'kitty', trustedRealPaths: ['/usr/bin/kitty'], args: () => ['--directory', plan.cwd, '--', plan.command, ...plan.args] },
|
|
3008
|
+
{ name: 'wezterm', trustedRealPaths: ['/usr/bin/wezterm'], args: () => ['start', '--cwd', plan.cwd, '--', plan.command, ...plan.args] },
|
|
3009
|
+
{ name: 'alacritty', trustedRealPaths: ['/usr/bin/alacritty'], args: () => ['--working-directory', plan.cwd, '-e', plan.command, ...plan.args] },
|
|
3010
|
+
{ name: 'xfce4-terminal', trustedRealPaths: ['/usr/bin/xfce4-terminal'], args: () => ['--working-directory', plan.cwd, '--execute', plan.command, ...plan.args] },
|
|
3011
|
+
{ name: 'mate-terminal', trustedRealPaths: ['/usr/bin/mate-terminal'], args: () => [`--working-directory=${plan.cwd}`, '--', plan.command, ...plan.args] },
|
|
3012
|
+
{ name: 'foot', trustedRealPaths: ['/usr/bin/foot'], args: () => [`--working-directory=${plan.cwd}`, '--', plan.command, ...plan.args] },
|
|
3013
|
+
{
|
|
3014
|
+
name: 'x-terminal-emulator',
|
|
3015
|
+
trustedRealPaths: [
|
|
3016
|
+
'/usr/bin/gnome-terminal', '/usr/bin/konsole', '/usr/bin/xfce4-terminal',
|
|
3017
|
+
'/usr/bin/mate-terminal', '/usr/bin/xterm',
|
|
3018
|
+
],
|
|
3019
|
+
args: () => ['-e', plan.command, ...plan.args],
|
|
3020
|
+
},
|
|
3021
|
+
{ name: 'xterm', trustedRealPaths: ['/usr/bin/xterm'], args: () => ['-e', plan.command, ...plan.args] },
|
|
3022
|
+
];
|
|
3023
|
+
for (const candidate of candidates) {
|
|
3024
|
+
const executable = commandPath(candidate.name);
|
|
3025
|
+
if (executable
|
|
3026
|
+
&& candidate.trustedRealPaths.includes(executable)
|
|
3027
|
+
&& (commandPath !== defaultCommandPath || rootOwnedProtectedExecutable(executable))) {
|
|
3028
|
+
return { command: executable, args: candidate.args() };
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
throw new Error('EAI could not find a supported Linux terminal emulator. Install GNOME Console, GNOME Terminal, Konsole, Kitty, WezTerm, Alacritty, Xfce Terminal, MATE Terminal, foot, xterm, or xdg-terminal-exec and retry.');
|
|
3032
|
+
}
|
|
3033
|
+
function unchangedFileBinding(expected, path, platform, probe) {
|
|
3034
|
+
const actual = launchFileBinding(path, probe, platform);
|
|
3035
|
+
return Boolean(actual
|
|
3036
|
+
&& actual.realPath === expected.realPath
|
|
3037
|
+
&& actual.size === expected.size
|
|
3038
|
+
&& actual.sha256 === expected.sha256
|
|
3039
|
+
&& (expected.device === undefined || actual.device === expected.device)
|
|
3040
|
+
&& (expected.inode === undefined || actual.inode === expected.inode)
|
|
3041
|
+
&& (expected.modifiedTimeMs === undefined || actual.modifiedTimeMs === expected.modifiedTimeMs));
|
|
3042
|
+
}
|
|
3043
|
+
function unchangedCatalogFileBinding(expected, path, catalog, platform, probe) {
|
|
3044
|
+
const actual = catalogFileBinding(path, catalog, platform, probe);
|
|
3045
|
+
return Boolean(actual
|
|
3046
|
+
&& sameCatalogPath(actual.realPath, expected.realPath, platform)
|
|
3047
|
+
&& actual.size === expected.size
|
|
3048
|
+
&& actual.sha256 === expected.sha256
|
|
3049
|
+
&& (expected.device === undefined || actual.device === expected.device)
|
|
3050
|
+
&& (expected.inode === undefined || actual.inode === expected.inode)
|
|
3051
|
+
&& (expected.modifiedTimeMs === undefined || actual.modifiedTimeMs === expected.modifiedTimeMs));
|
|
3052
|
+
}
|
|
3053
|
+
function launchArtifactStillAuthenticated(plan, platform, probe) {
|
|
3054
|
+
const verification = plan.verification;
|
|
3055
|
+
if (!verification)
|
|
3056
|
+
return false;
|
|
3057
|
+
const surface = AI_SURFACE_DEFINITIONS_BY_ID.get(plan.surfaceId);
|
|
3058
|
+
if (!surface)
|
|
3059
|
+
return false;
|
|
3060
|
+
if (verification.kind === 'copilot-appimage'
|
|
3061
|
+
|| verification.kind === 'grok-appimage'
|
|
3062
|
+
|| verification.kind === 'portable-elf') {
|
|
3063
|
+
if (platform !== 'linux'
|
|
3064
|
+
|| !unchangedFileBinding(verification, plan.command, platform, probe))
|
|
3065
|
+
return false;
|
|
3066
|
+
const header = probe.fileHeader?.(verification.realPath, 64);
|
|
3067
|
+
const appImageCatalog = verification.kind === 'grok-appimage'
|
|
3068
|
+
? VERIFIED_GROK_BOT_APPIMAGES
|
|
3069
|
+
: verification.kind === 'copilot-appimage'
|
|
3070
|
+
? VERIFIED_COPILOT_APPIMAGES
|
|
3071
|
+
: null;
|
|
3072
|
+
const knownArtifact = appImageCatalog
|
|
3073
|
+
? Object.values(appImageCatalog).some((architectures) => {
|
|
3074
|
+
const artifact = architectures[verification.architecture];
|
|
3075
|
+
return artifact?.size === verification.size && artifact.sha256 === verification.sha256;
|
|
3076
|
+
})
|
|
3077
|
+
: (() => {
|
|
3078
|
+
const artifact = VERIFIED_ANTIGRAVITY_LINUX_EXECUTABLES[verification.architecture];
|
|
3079
|
+
return artifact?.size === verification.size && artifact.sha256 === verification.sha256;
|
|
3080
|
+
})();
|
|
3081
|
+
const expectedSurface = verification.kind === 'grok-appimage'
|
|
3082
|
+
? plan.surfaceId === 'grok-bot'
|
|
3083
|
+
: verification.kind === 'copilot-appimage'
|
|
3084
|
+
? plan.surfaceId === 'copilot-desktop'
|
|
3085
|
+
: plan.surfaceId === 'antigravity-desktop';
|
|
3086
|
+
return Boolean(knownArtifact
|
|
3087
|
+
&& expectedSurface
|
|
3088
|
+
&& header
|
|
3089
|
+
&& (appImageCatalog
|
|
3090
|
+
? validAppImageHeader(header, verification.architecture)
|
|
3091
|
+
: validElfHeader(header, verification.architecture)));
|
|
3092
|
+
}
|
|
3093
|
+
if (verification.kind === 'mac-executable') {
|
|
3094
|
+
if (platform !== 'darwin'
|
|
3095
|
+
|| !unchangedFileBinding(verification, plan.command, platform, probe))
|
|
3096
|
+
return false;
|
|
3097
|
+
const expectedSurfaceId = plan.surfaceId === 'copilot-desktop'
|
|
3098
|
+
? 'copilot-cli'
|
|
3099
|
+
: plan.surfaceId === 'codex-desktop'
|
|
3100
|
+
? 'codex-cli'
|
|
3101
|
+
: plan.surfaceId;
|
|
3102
|
+
const expected = EXPECTED_MAC_EXECUTABLE_IDENTITIES[expectedSurfaceId];
|
|
3103
|
+
const identity = probe.macExecutableIdentity?.(verification.realPath, expected);
|
|
3104
|
+
return Boolean(expected
|
|
3105
|
+
&& identity
|
|
3106
|
+
&& verification.identifier === expected.identifier
|
|
3107
|
+
&& verification.teamIdentifier === expected.teamIdentifier
|
|
3108
|
+
&& identity.identifier === expected.identifier
|
|
3109
|
+
&& identity.teamIdentifier === expected.teamIdentifier
|
|
3110
|
+
&& identity.architectures.includes(verification.architecture));
|
|
3111
|
+
}
|
|
3112
|
+
if (verification.kind === 'mac-application') {
|
|
3113
|
+
if (platform !== 'darwin')
|
|
3114
|
+
return false;
|
|
3115
|
+
const expected = EXPECTED_MAC_APPLICATION_IDENTITIES[plan.surfaceId];
|
|
3116
|
+
if (!expected
|
|
3117
|
+
|| verification.bundleIdentifier !== expected.bundleIdentifier
|
|
3118
|
+
|| verification.teamIdentifier !== expected.teamIdentifier
|
|
3119
|
+
|| verification.requiredScheme !== expected.requiredScheme
|
|
3120
|
+
|| probe.realPath?.(verification.realPath) !== verification.realPath)
|
|
3121
|
+
return false;
|
|
3122
|
+
const identity = probe.macApplicationIdentity?.(verification.realPath, expected);
|
|
3123
|
+
if (!identity
|
|
3124
|
+
|| identity.bundleIdentifier !== expected.bundleIdentifier
|
|
3125
|
+
|| identity.teamIdentifier !== expected.teamIdentifier
|
|
3126
|
+
|| identity.executablePath !== verification.executablePath
|
|
3127
|
+
|| !identity.architectures.includes(verification.architecture)
|
|
3128
|
+
|| (expected.requiredScheme !== undefined
|
|
3129
|
+
&& !identity.urlSchemes.includes(expected.requiredScheme)))
|
|
3130
|
+
return false;
|
|
3131
|
+
const executableBinding = {
|
|
3132
|
+
realPath: verification.executablePath,
|
|
3133
|
+
size: verification.executableSize,
|
|
3134
|
+
sha256: verification.executableSha256,
|
|
3135
|
+
...(verification.executableDevice === undefined ? {} : { device: verification.executableDevice }),
|
|
3136
|
+
...(verification.executableInode === undefined ? {} : { inode: verification.executableInode }),
|
|
3137
|
+
...(verification.executableModifiedTimeMs === undefined
|
|
3138
|
+
? {}
|
|
3139
|
+
: { modifiedTimeMs: verification.executableModifiedTimeMs }),
|
|
3140
|
+
};
|
|
3141
|
+
if (!unchangedFileBinding(executableBinding, verification.executablePath, platform, probe))
|
|
3142
|
+
return false;
|
|
3143
|
+
if (verification.commandRealPath !== undefined) {
|
|
3144
|
+
if (verification.commandSize === undefined || verification.commandSha256 === undefined)
|
|
3145
|
+
return false;
|
|
3146
|
+
const commandBinding = {
|
|
3147
|
+
realPath: verification.commandRealPath,
|
|
3148
|
+
size: verification.commandSize,
|
|
3149
|
+
sha256: verification.commandSha256,
|
|
3150
|
+
...(verification.commandDevice === undefined ? {} : { device: verification.commandDevice }),
|
|
3151
|
+
...(verification.commandInode === undefined ? {} : { inode: verification.commandInode }),
|
|
3152
|
+
...(verification.commandModifiedTimeMs === undefined
|
|
3153
|
+
? {}
|
|
3154
|
+
: { modifiedTimeMs: verification.commandModifiedTimeMs }),
|
|
3155
|
+
};
|
|
3156
|
+
return unchangedFileBinding(commandBinding, plan.command, platform, probe);
|
|
3157
|
+
}
|
|
3158
|
+
return probe.realPath?.(plan.command) === verification.realPath;
|
|
3159
|
+
}
|
|
3160
|
+
if (verification.kind === 'vscode-catalog') {
|
|
3161
|
+
if (plan.surfaceId !== 'vscode-copilot'
|
|
3162
|
+
|| plan.surfaceName !== surface.name
|
|
3163
|
+
|| plan.mode !== 'process'
|
|
3164
|
+
|| plan.cwd !== plan.projectDirectory
|
|
3165
|
+
|| plan.preparedPrompt !== true
|
|
3166
|
+
|| !sameCatalogPath(plan.command, verification.realPath, verification.platform)
|
|
3167
|
+
|| !exactStringRecord(plan.environment ?? {}, VSCODE_CATALOG_LAUNCH_ENVIRONMENT)
|
|
3168
|
+
|| !equalStringArrays(plan.args, [verification.cliPath, 'chat', '-m', 'agent', EAI_FIRST_PROMPT])
|
|
3169
|
+
|| platform !== verification.platform
|
|
3170
|
+
|| (verification.architecture !== 'x64' && verification.architecture !== 'arm64')
|
|
3171
|
+
|| verification.version !== VSCODE_CATALOG_VERSION
|
|
3172
|
+
|| verification.commit !== VSCODE_CATALOG_COMMIT)
|
|
3173
|
+
return false;
|
|
3174
|
+
const installationRoot = verification.platform === 'win32'
|
|
3175
|
+
? win32Path.dirname(verification.realPath)
|
|
3176
|
+
: '/usr/share/code';
|
|
3177
|
+
if (verification.platform === 'win32'
|
|
3178
|
+
&& !probe.windowsVsCodeInstallationAcl?.(installationRoot))
|
|
3179
|
+
return false;
|
|
3180
|
+
const expected = vsCodeCatalog(verification.platform, verification.architecture);
|
|
3181
|
+
if (!expected
|
|
3182
|
+
|| verification.catalogArtifactSha256 !== expected.artifactSha256
|
|
3183
|
+
|| verification.size !== expected.executable.size
|
|
3184
|
+
|| verification.sha256 !== expected.executable.sha256
|
|
3185
|
+
|| !unchangedFileBinding(verification, plan.command, platform, probe))
|
|
3186
|
+
return false;
|
|
3187
|
+
const applicationRoot = verification.platform === 'win32'
|
|
3188
|
+
? win32Path.join(installationRoot, VSCODE_CATALOG_COMMIT_DIRECTORY)
|
|
3189
|
+
: installationRoot;
|
|
3190
|
+
const resourceRoot = verification.platform === 'win32'
|
|
3191
|
+
? win32Path.join(applicationRoot, 'resources', 'app')
|
|
3192
|
+
: join(applicationRoot, 'resources', 'app');
|
|
3193
|
+
const expectedCliPath = verification.platform === 'win32'
|
|
3194
|
+
? win32Path.join(resourceRoot, 'out', 'cli.js')
|
|
3195
|
+
: join(resourceRoot, 'out', 'cli.js');
|
|
3196
|
+
if (!sameCatalogPath(verification.installationRoot, installationRoot, verification.platform)
|
|
3197
|
+
|| !sameCatalogPath(verification.cliPath, expectedCliPath, verification.platform)
|
|
3198
|
+
|| !catalogTreeShapeMatches(verification.applicationTree, applicationRoot, expected.applicationTree, verification.platform)
|
|
3199
|
+
|| !vscodeCatalogMetadataMatches(resourceRoot, expected, verification.platform, probe))
|
|
3200
|
+
return false;
|
|
3201
|
+
const applicationTree = vscodeCatalogTreeMatches(applicationRoot, expected.applicationTree, verification.platform, probe);
|
|
3202
|
+
if (!applicationTree
|
|
3203
|
+
|| !sameCatalogPath(applicationTree.realPath, verification.applicationTree.realPath, verification.platform)
|
|
3204
|
+
|| applicationTree.fileCount !== verification.applicationTree.fileCount
|
|
3205
|
+
|| applicationTree.totalBytes !== verification.applicationTree.totalBytes
|
|
3206
|
+
|| applicationTree.sha256 !== verification.applicationTree.sha256)
|
|
3207
|
+
return false;
|
|
3208
|
+
if (verification.platform === 'win32') {
|
|
3209
|
+
const auxiliaryRoot = win32Path.join(installationRoot, 'bin');
|
|
3210
|
+
const auxiliaryTree = expected.auxiliaryTree
|
|
3211
|
+
? vscodeCatalogTreeMatches(auxiliaryRoot, expected.auxiliaryTree, 'win32', probe)
|
|
3212
|
+
: null;
|
|
3213
|
+
const visualManifestPath = win32Path.join(installationRoot, 'Code.VisualElementsManifest.xml');
|
|
3214
|
+
if (!windowsVsCodeInstallationRootMatches(installationRoot, probe)
|
|
3215
|
+
|| !auxiliaryTree
|
|
3216
|
+
|| !catalogTreeShapeMatches(verification.auxiliaryTree, auxiliaryRoot, expected.auxiliaryTree, 'win32')
|
|
3217
|
+
|| auxiliaryTree.fileCount !== verification.auxiliaryTree?.fileCount
|
|
3218
|
+
|| auxiliaryTree.totalBytes !== verification.auxiliaryTree.totalBytes
|
|
3219
|
+
|| auxiliaryTree.sha256 !== verification.auxiliaryTree.sha256
|
|
3220
|
+
|| !expected.visualManifest
|
|
3221
|
+
|| !verification.visualManifest
|
|
3222
|
+
|| !unchangedCatalogFileBinding(verification.visualManifest, visualManifestPath, expected.visualManifest, 'win32', probe))
|
|
3223
|
+
return false;
|
|
3224
|
+
const identity = probe.windowsExecutableIdentity?.(verification.realPath);
|
|
3225
|
+
return Boolean(identity
|
|
3226
|
+
&& windowsApplicationIdentityValueMatches(surface, identity, verification.architecture));
|
|
3227
|
+
}
|
|
3228
|
+
if (verification.auxiliaryTree !== undefined || verification.visualManifest !== undefined)
|
|
3229
|
+
return false;
|
|
3230
|
+
const header = probe.fileHeader?.(verification.realPath, 64);
|
|
3231
|
+
return Boolean(header
|
|
3232
|
+
&& validElfHeader(header, verification.architecture)
|
|
3233
|
+
&& linuxPackageIdentityMatches(verification.realPath, 'code', verification.architecture, probe));
|
|
3234
|
+
}
|
|
3235
|
+
if (verification.kind === 'windows-executable') {
|
|
3236
|
+
if (platform !== 'win32'
|
|
3237
|
+
|| !unchangedFileBinding(verification, plan.command, platform, probe))
|
|
3238
|
+
return false;
|
|
3239
|
+
const identity = probe.windowsExecutableIdentity?.(verification.realPath);
|
|
3240
|
+
return Boolean(identity
|
|
3241
|
+
&& windowsApplicationIdentityValueMatches(surface, identity, verification.architecture)
|
|
3242
|
+
&& identity.productName === verification.productName
|
|
3243
|
+
&& identity.companyName === verification.companyName
|
|
3244
|
+
&& identity.publisher === verification.publisher);
|
|
3245
|
+
}
|
|
3246
|
+
if (verification.kind === 'windows-appx') {
|
|
3247
|
+
if (platform !== 'win32')
|
|
3248
|
+
return false;
|
|
3249
|
+
const expected = EXPECTED_WINDOWS_APPX_IDENTITIES[plan.surfaceId];
|
|
3250
|
+
const identity = probe.windowsAppxIdentity?.(verification.packageName);
|
|
3251
|
+
return Boolean(expected
|
|
3252
|
+
&& identity
|
|
3253
|
+
&& plan.command === `shell:AppsFolder\\${verification.familyName}!${verification.applicationId}`
|
|
3254
|
+
&& verification.packageName === expected.packageName
|
|
3255
|
+
&& verification.applicationId === expected.applicationId
|
|
3256
|
+
&& verification.publisherId === expected.publisherId
|
|
3257
|
+
&& verification.familyName === expected.familyName
|
|
3258
|
+
&& (expected.publisher === undefined || verification.publisher === expected.publisher)
|
|
3259
|
+
&& expected.signatureKinds.includes(verification.signatureKind)
|
|
3260
|
+
&& identity.packageName === verification.packageName
|
|
3261
|
+
&& identity.publisher === verification.publisher
|
|
3262
|
+
&& identity.publisherId === verification.publisherId
|
|
3263
|
+
&& identity.familyName === verification.familyName
|
|
3264
|
+
&& identity.applicationIds.includes(verification.applicationId)
|
|
3265
|
+
&& identity.architecture === verification.architecture
|
|
3266
|
+
&& identity.signatureKind === verification.signatureKind
|
|
3267
|
+
&& identity.status === 'Ok');
|
|
3268
|
+
}
|
|
3269
|
+
if (verification.kind === 'linux-package') {
|
|
3270
|
+
if (platform !== 'linux'
|
|
3271
|
+
|| plan.surfaceId !== 'copilot-desktop'
|
|
3272
|
+
|| !unchangedFileBinding(verification, plan.command, platform, probe))
|
|
3273
|
+
return false;
|
|
3274
|
+
const identity = probe.linuxPackageIdentity?.(verification.realPath);
|
|
3275
|
+
return Boolean(identity
|
|
3276
|
+
&& linuxPackageIdentityMatches(verification.realPath, 'github', verification.architecture, probe)
|
|
3277
|
+
&& identity.packageName === verification.packageName
|
|
3278
|
+
&& identity.version === verification.version
|
|
3279
|
+
&& identity.manager === verification.manager
|
|
3280
|
+
&& identity.origin === verification.origin
|
|
3281
|
+
&& identity.catalogArtifactSha256 === verification.catalogArtifactSha256
|
|
3282
|
+
&& identity.executableSha256 === verification.sha256);
|
|
3283
|
+
}
|
|
3284
|
+
return false;
|
|
3285
|
+
}
|
|
3286
|
+
export async function executeAiLaunchPlan(plan, platform = process.platform, options = {}) {
|
|
3287
|
+
const dispatch = { dispatched: true, confirmed: false };
|
|
3288
|
+
if (plan.mode === 'url' && !trustedSurfaceUrlIsSupported(plan.command)) {
|
|
3289
|
+
throw new Error('EAI refused to open an unsupported AI workspace location.');
|
|
3290
|
+
}
|
|
3291
|
+
const deepLinkArguments = plan.args.filter((argument) => /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(argument));
|
|
3292
|
+
if (deepLinkArguments.some((argument) => !trustedSurfaceUrlIsSupported(argument))
|
|
3293
|
+
|| (plan.surfaceId === 'claude-desktop'
|
|
3294
|
+
&& deepLinkArguments.some((argument) => !argument.startsWith('claude://code/new')))
|
|
3295
|
+
|| (plan.surfaceId === 'copilot-desktop'
|
|
3296
|
+
&& deepLinkArguments.some((argument) => !argument.startsWith('ghapp://recent')))) {
|
|
3297
|
+
throw new Error('EAI refused to open an unsupported AI workspace location.');
|
|
3298
|
+
}
|
|
3299
|
+
if (plan.mode === 'application') {
|
|
3300
|
+
const safeApplicationArguments = (() => {
|
|
3301
|
+
if (plan.surfaceId === 'claude-desktop') {
|
|
3302
|
+
return plan.args.length === 0
|
|
3303
|
+
|| (plan.args.length === 1 && /^claude:\/\/code\/new(?:[?#]|$)/.test(plan.args[0]));
|
|
3304
|
+
}
|
|
3305
|
+
if (plan.surfaceId === 'copilot-desktop') {
|
|
3306
|
+
return plan.args.length === 0
|
|
3307
|
+
|| (plan.args.length === 1 && /^ghapp:\/\/recent(?:[?#]|$)/.test(plan.args[0]));
|
|
3308
|
+
}
|
|
3309
|
+
if (plan.surfaceId === 'grok-bot') {
|
|
3310
|
+
return plan.args.length === 0
|
|
3311
|
+
|| (plan.args.length === 1 && plan.args[0] === '--ozone-platform-hint=auto');
|
|
3312
|
+
}
|
|
3313
|
+
return plan.args.length === 0;
|
|
3314
|
+
})();
|
|
3315
|
+
if (!safeApplicationArguments) {
|
|
3316
|
+
throw new Error('EAI refused unexpected application launch arguments.');
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
if (plan.surfaceId === 'vscode-copilot'
|
|
3320
|
+
&& ((platform === 'win32' || platform === 'linux')
|
|
3321
|
+
? plan.verification?.kind !== 'vscode-catalog'
|
|
3322
|
+
: platform === 'darwin'
|
|
3323
|
+
? plan.verification?.kind !== 'mac-application'
|
|
3324
|
+
: true)) {
|
|
3325
|
+
throw new Error('EAI refused to launch VS Code without platform-authenticated catalog evidence.');
|
|
3326
|
+
}
|
|
3327
|
+
const probe = options.probe ?? systemSurfaceProbe;
|
|
3328
|
+
if (!launchArtifactStillAuthenticated(plan, platform, probe)) {
|
|
3329
|
+
throw new Error(`EAI refused to launch ${plan.surfaceName} because its authenticated application changed after detection.`);
|
|
3330
|
+
}
|
|
416
3331
|
if (plan.mode === 'url') {
|
|
417
3332
|
await openTrustedSurfaceUrl(plan.command, platform);
|
|
418
|
-
return;
|
|
3333
|
+
return dispatch;
|
|
419
3334
|
}
|
|
420
3335
|
if (plan.mode === 'application') {
|
|
421
3336
|
if (platform === 'darwin')
|
|
422
|
-
await spawnDetached('open', ['-a', plan.command, ...plan.args]);
|
|
3337
|
+
await spawnDetached('/usr/bin/open', ['-a', plan.command, ...plan.args]);
|
|
3338
|
+
else if (platform === 'win32' && plan.command.startsWith('shell:AppsFolder\\')) {
|
|
3339
|
+
await spawnDetached('C:\\Windows\\explorer.exe', [plan.command]);
|
|
3340
|
+
}
|
|
423
3341
|
else
|
|
424
3342
|
await spawnDetached(plan.command, plan.args, plan.cwd);
|
|
425
|
-
return;
|
|
3343
|
+
return dispatch;
|
|
426
3344
|
}
|
|
427
3345
|
if (plan.mode === 'process') {
|
|
3346
|
+
if (platform === 'win32' && /\.(?:cmd|bat)$/i.test(plan.command)) {
|
|
3347
|
+
await spawnWindowsCommand(plan.command, plan.args, '/c', plan.cwd, plan.environment);
|
|
3348
|
+
return dispatch;
|
|
3349
|
+
}
|
|
3350
|
+
if (plan.verification?.kind === 'vscode-catalog') {
|
|
3351
|
+
await spawnDetached(plan.command, plan.args, plan.cwd, sanitizedVsCodeRuntimeEnvironment(plan.verification.platform), undefined, true, true);
|
|
3352
|
+
return dispatch;
|
|
3353
|
+
}
|
|
428
3354
|
await spawnDetached(plan.command, plan.args, plan.cwd, plan.environment);
|
|
429
|
-
return;
|
|
3355
|
+
return dispatch;
|
|
3356
|
+
}
|
|
3357
|
+
if (platform === 'win32') {
|
|
3358
|
+
await spawnWindowsCommand(plan.command, plan.args, '/k', plan.cwd, plan.environment, false);
|
|
3359
|
+
return dispatch;
|
|
430
3360
|
}
|
|
431
|
-
const commandLine = [plan.command, ...plan.args].map((value) => shellQuote(value, platform)).join(' ');
|
|
432
3361
|
if (platform === 'darwin') {
|
|
3362
|
+
const commandLine = [plan.command, ...plan.args].map((value) => shellQuote(value, platform)).join(' ');
|
|
433
3363
|
const escaped = `cd ${shellQuote(plan.cwd, platform)} && ${commandLine}`.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
434
|
-
await spawnDetached('osascript', ['-e', `tell application "Terminal" to do script "${escaped}"`]);
|
|
435
|
-
}
|
|
436
|
-
else if (platform === 'win32') {
|
|
437
|
-
await spawnDetached('cmd.exe', ['/k', `cd /d ${shellQuote(plan.cwd, platform)} && ${commandLine}`]);
|
|
3364
|
+
await spawnDetached('/usr/bin/osascript', ['-e', `tell application "Terminal" to do script "${escaped}"`]);
|
|
438
3365
|
}
|
|
439
3366
|
else {
|
|
440
|
-
|
|
3367
|
+
const terminal = buildLinuxTerminalInvocation(plan, options.commandPath);
|
|
3368
|
+
await spawnDetached(terminal.command, terminal.args, plan.cwd, plan.environment);
|
|
441
3369
|
}
|
|
3370
|
+
return dispatch;
|
|
442
3371
|
}
|
|
443
3372
|
export function getAiSurface(inventory, surfaceId) {
|
|
444
3373
|
return surfaceOrThrow(inventory, surfaceId);
|