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