@aglit-ai/cli 0.0.0-stage → 0.1.2

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 CHANGED
@@ -1,3 +1,89 @@
1
- # Temporary Holding Version
1
+ # Aglit Computer Use
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Computer Use makes a signed-in desktop remotely controllable from the user's Aglit
4
+ account. Aglit Mini Computer remains the separate VM host application.
5
+
6
+ After the CLI and a compatible signed desktop release have been published:
7
+
8
+ ```sh
9
+ pnpm add -g @aglit-ai/cli
10
+ aglit
11
+ aglit start --headed
12
+ aglit start --headless
13
+ aglit status
14
+ aglit disable
15
+ aglit enable
16
+ aglit uninstall
17
+ ```
18
+
19
+ Run `aglit` in an interactive terminal to open the guided setup. `aglit init`
20
+ also runs this flow and installs the signed runtime if it is missing. Setup asks
21
+ for a computer name and whether to keep the app in the background or open its
22
+ Desktop window. **Headless is the default**, including login startup. Headed
23
+ mode is an explicit choice; it shows the same app and profile. `aglit start
24
+ --headless` hides its window without disabling its connection.
25
+
26
+ Setup opens Aglit's Google login in the browser,
27
+ detects existing Codex/Claude logins, and uses a configured authenticated provider
28
+ or the sole available provider. Multiple usable providers prompt for a choice.
29
+ Provider login detection is not an inference acceptance check. Original provider
30
+ accounts remain owned by the user; uninstall does not delete their original login.
31
+
32
+ ```sh
33
+ aglit init --name "Office Mac" --provider codex
34
+ aglit providers
35
+ aglit teams
36
+ aglit pipelines
37
+ aglit pipelines --open
38
+ aglit init --name "Office Mac" --provider auto --team TEAM_ID
39
+ aglit invite USER_EMAIL --team TEAM_ID
40
+ ```
41
+
42
+ The terminal shows the current account's team memberships and authorized
43
+ pipeline devices. `aglit pipelines --open` opens the existing encrypted pipeline
44
+ viewer for pipeline contents and controls. A device listed in the catalog is not
45
+ proof that its pipelines are connected. Team selection remains invitation
46
+ context; it does not share the newly registered computer.
47
+
48
+ For unattended setup, use `aglit init --json --name "Office Mac" --provider codex
49
+ --account YOUR_EMAIL --headless`. `--account` checks the actual signed-in
50
+ account before configuration or remote access is enabled; it never selects or
51
+ impersonates an account. JSON mode and redirected input skip interactive prompts.
52
+ Provide an explicit `--provider` when detection
53
+ cannot select a single provider. Initial account login and OS consent still need
54
+ the user. On macOS, Screen Recording and Accessibility must be granted before
55
+ `init` enables access. The native tray offers status, enable/disable, Console and
56
+ quit while the application shell stays hidden. Login-item registration starts
57
+ the same private profile on the next user login.
58
+
59
+ `--team` selects the existing backend team/invitation scope. It does not grant team
60
+ members access to a separately registered personal computer. The backend requires
61
+ current accepted membership/admin authority for its operations; a pending
62
+ invitation is not access. Shared computer enrollment needs an authoritative team
63
+ device binding, which this CLI does not create from a caller-supplied team ID.
64
+
65
+ `status` distinguishes the local enabled setting from a connected tunnel. It also
66
+ reports permissions and the selected provider. Neither status means video/input,
67
+ inference or end-to-end cryptographic acceptance has been verified.
68
+
69
+ `uninstall` purges by default. It disables this installation, revokes viewer
70
+ sessions, confirms deletion of its cloud registration, removes its login item,
71
+ signs out, waits for its process to exit and deletes its private runtime/profile.
72
+ This includes bundled plugins/helpers, copied provider state, keys, settings,
73
+ history and logs inside that installation. Mini Computer, VMs, shared browser
74
+ profiles, existing provider accounts and other applications are outside that
75
+ ownership. A failed cloud revocation preserves credentials for retry; local
76
+ access is disabled. Interrupted native-file cleanup can be resumed with the same
77
+ command. The package-manager-installed CLI wrapper itself can be removed with
78
+ `pnpm remove -g @aglit-ai/cli` after runtime uninstall.
79
+
80
+ The installer supports the existing Apple silicon macOS and Windows x64 release
81
+ formats. It verifies the platform pointer against the immutable version receipt,
82
+ asset size and SHA-256/SHA-512, contained archive paths/links, OS publisher admission,
83
+ the exact source commit, obfuscation marker and native CLI protocol version before
84
+ launch. Unsigned Windows artifacts and older desktop releases are rejected.
85
+ Computer Use starts with Anywhere Access disabled and a new per-install device
86
+ identity. It never adopts a managed guest's key or changes its registration.
87
+
88
+ See [architecture and acceptance](../../docs/computer-use.md). The source CLI can
89
+ be inspected without installation using `node packages/cli/bin/aglit.cjs --help`.
package/bin/aglit.cjs ADDED
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const os = require('node:os');
6
+ const { parseArgs } = require('node:util');
7
+ const installation = require('../src/installation.cjs');
8
+ const { createUi, teamsFromRoles } = require('../src/ui.cjs');
9
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
10
+ const help = `Aglit Computer Use
11
+
12
+ aglit Interactive setup
13
+ aglit init Install and register this computer
14
+ [--name "Computer name"] [--provider codex|claude|auto]
15
+ [--account EMAIL] [--headed | --headless] [--team TEAM_ID] [--invite EMAIL]
16
+ aglit install Install the signed Desktop runtime
17
+ aglit start [--headed | --headless]
18
+ aglit status [--json]
19
+ aglit enable | disable
20
+ aglit teams
21
+ aglit pipelines [--open]
22
+ aglit providers
23
+ aglit invite EMAIL --team TEAM_ID
24
+ aglit uninstall
25
+
26
+ Aglit runs in the background by default. --headed opens the Desktop window.
27
+ Use --json with install, init, start or status for non-interactive JSON output.
28
+ Uninstall revokes access and purges only this installation. Mini Computer,
29
+ VMs, shared browser profiles and original provider logins remain separate.
30
+ Team selection does not grant access to this personal computer.
31
+ `;
32
+
33
+ async function finishUninstall(receipt) {
34
+ const deadline = Date.now() + 30000;
35
+ while (Date.now() < deadline) {
36
+ await delay(500);
37
+ try { process.kill(receipt.pid, 0); } catch (error) {
38
+ if (error.code !== 'ESRCH') throw error;
39
+ installation.purge();
40
+ return { uninstalled: true, purged: true };
41
+ }
42
+ }
43
+ throw Error('Runtime has not exited; local files preserved. Retry aglit uninstall after it quits.');
44
+ }
45
+
46
+ async function main() {
47
+ const { values, positionals } = parseArgs({ options: {
48
+ name: { type: 'string' }, provider: { type: 'string' }, account: { type: 'string' },
49
+ team: { type: 'string' }, invite: { type: 'string' }, headed: { type: 'boolean' },
50
+ headless: { type: 'boolean' }, open: { type: 'boolean' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
51
+ }, allowPositionals: true, strict: true });
52
+ const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY && !values.json && !process.env.CI);
53
+ if (values.help || !positionals.length && !interactive) { process.stdout.write(help); return; }
54
+ const [command = 'init', email] = positionals;
55
+ const options = {
56
+ install: ['json'], init: ['name', 'provider', 'account', 'team', 'invite', 'headed', 'headless', 'json'],
57
+ start: ['headed', 'headless', 'json'], status: ['json'], enable: [], disable: [], teams: ['json'], pipelines: ['open', 'json'], providers: ['json'], invite: ['team'], uninstall: [],
58
+ };
59
+ if (!Object.hasOwn(options, command) || Object.keys(values).some(key => !options[command].includes(key)) || positionals.length > (command === 'invite' ? 2 : 1)) throw Error('Invalid command or options. Run aglit --help.');
60
+ if (values.headed && values.headless) throw Error('Choose either --headed or --headless');
61
+ if (values.team && !/^[A-Za-z0-9._:-]{1,128}$/.test(values.team)) throw Error('Invalid team ID');
62
+ if (values.provider && !['auto', 'codex', 'claude'].includes(values.provider)) throw Error('Provider must be auto, codex or claude');
63
+ const validEmail = value => value.length <= 254 && /^[^\s@*]+@[^\s@*]+\.[^\s@*]+$/.test(value);
64
+ const recipient = command === 'invite' ? email : values.invite;
65
+ if (recipient && !validEmail(recipient)) throw Error('An individual invitation email address is required');
66
+ if (values.account !== undefined && !validEmail(values.account)) throw Error('An individual account email address is required');
67
+ if ((command === 'invite' || values.invite) && (!recipient || !values.team)) throw Error('Invitations require an email address and --team TEAM_ID');
68
+ if (values.name !== undefined && (!values.name.trim() || values.name.length > 100 || /[\x00-\x1f\x7f-\x9f]/.test(values.name))) throw Error('Computer name must contain 1–100 printable characters');
69
+ const ui = await createUi(interactive);
70
+ const installed = () => fs.existsSync(path.join(installation.root, 'installation.json'));
71
+ if (command === 'install') {
72
+ const receipt = await ui.step('Download and verify Desktop', () => installation.install());
73
+ if (interactive) ui.info('Desktop installed. Run aglit init to register this computer.');
74
+ else ui.result(receipt);
75
+ ui.finish('Ready for setup'); return;
76
+ }
77
+ if (command === 'status' && !installed()) {
78
+ ui.result({ product: 'Aglit Computer Use', installed: false, running: false, enabled: false });
79
+ ui.finish('Run aglit to set up this computer'); return;
80
+ }
81
+ if (command === 'uninstall' && !installed()) {
82
+ if (fs.existsSync(installation.root)) installation.purgeIncomplete();
83
+ ui.result({ uninstalled: true, purged: true }); ui.finish('Uninstalled'); return;
84
+ }
85
+ if ((command === 'uninstall' || command === 'status') && installed()) {
86
+ const pending = installation.readUninstall();
87
+ if (pending) {
88
+ ui.result(command === 'uninstall' ? await finishUninstall(pending) : { installed: true, uninstalling: true, enabled: false });
89
+ ui.finish('Uninstall cleanup'); return;
90
+ }
91
+ }
92
+ let name = values.name;
93
+ let mode = values.headed ? 'headed' : 'headless';
94
+ if (command === 'init') {
95
+ if (!name && interactive && !installed()) name = await ui.name(os.hostname());
96
+ if (interactive && values.headed === undefined && values.headless === undefined) mode = await ui.mode();
97
+ if (!installed()) {
98
+ if (!interactive && !name) throw Error('Provide --name for unattended setup');
99
+ await ui.step('Download and verify Desktop', () => installation.install());
100
+ }
101
+ }
102
+ if (!installed()) throw Error('Run aglit init to install and register this computer first.');
103
+ installation.readInstallation();
104
+ const configPath = path.join(installation.profile, 'ipc/sdk-ipc.json');
105
+ const control = (action, fields = {}) => require('../src/control.cjs').control(configPath, action, fields);
106
+ let state;
107
+ try { state = await control('status'); } catch (error) {
108
+ if (command === 'status') { ui.result({ product: 'Aglit Computer Use', installed: true, running: false, enabled: null, error: error.message }); ui.finish('Runtime stopped'); process.exitCode = 1; return; }
109
+ state = await ui.step('Start Desktop in the background', async () => {
110
+ installation.start();
111
+ const deadline = Date.now() + 60000;
112
+ while (Date.now() < deadline) {
113
+ await delay(1000);
114
+ try { return await control('status'); } catch { /* Wait for this installation's authenticated native socket. */ }
115
+ }
116
+ throw Error('Computer Use did not become ready. See runtime.log in its installation directory.');
117
+ });
118
+ }
119
+ if (command === 'status') { ui.result(state); ui.finish('Computer status'); return; }
120
+ if (command === 'teams' || command === 'pipelines') {
121
+ const result = await ui.step(command === 'teams' ? 'Load your teams' : 'Load authorized pipeline devices', () => control(command));
122
+ if (interactive) ui[command](result); else ui.result(result);
123
+ if (command === 'pipelines' && values.open) await control('open-pipelines');
124
+ ui.finish(command === 'teams' ? 'Your teams' : 'Your pipelines'); return;
125
+ }
126
+ if (command === 'start' || command === 'init') {
127
+ const window = await control('window', { mode });
128
+ state = { ...state, ...window };
129
+ if (command === 'start') { ui.result(state); ui.finish(mode === 'headed' ? 'Desktop window opened' : 'Running in the background'); return; }
130
+ }
131
+ if (command === 'init') {
132
+ if (!state.email) {
133
+ state = await ui.step('Complete sign-in in your browser', async () => {
134
+ await control('login');
135
+ const deadline = Date.now() + 5 * 60000;
136
+ while (Date.now() < deadline) {
137
+ await delay(1500); const current = await control('status'); if (current.email) return current;
138
+ }
139
+ throw Error('Sign-in timed out. Run aglit init to resume.');
140
+ });
141
+ }
142
+ if (values.account && state.email?.toLowerCase() !== values.account.toLowerCase())
143
+ throw Error(`Signed in as ${state.email}; expected ${values.account}. Switch the browser sign-in account and rerun aglit init.`);
144
+ if (interactive && !values.account && !await ui.confirmAccount(state.email))
145
+ throw Object.assign(Error('Setup cancelled before enabling remote access.'), { cancelled: true });
146
+ ui.info(`Signed in as ${state.email}`);
147
+ let teamId = values.team;
148
+ if (interactive) {
149
+ const discovery = await ui.step('Load your teams and pipelines', () => Promise.allSettled([control('teams'), control('pipelines')]));
150
+ let teams = [];
151
+ if (discovery[0].status === 'fulfilled') { ui.teams(discovery[0].value); teams = teamsFromRoles(discovery[0].value); }
152
+ else ui.info('Teams are unavailable. You can retry with aglit teams.');
153
+ if (discovery[1].status === 'fulfilled') ui.pipelines(discovery[1].value);
154
+ else ui.info('Pipeline devices are unavailable. You can retry with aglit pipelines.');
155
+ if (!teamId && teams.length) teamId = await ui.team(teams);
156
+ }
157
+ name ||= state.name;
158
+ if (!name && interactive) name = await ui.name(os.hostname());
159
+ if (!name) throw Error('Provide --name for unattended setup');
160
+ const detected = await ui.step('Find signed-in AI providers', () => control('providers'));
161
+ const available = detected.providers.filter(provider => provider.authenticated).map(provider => provider.provider);
162
+ let provider = values.provider && values.provider !== 'auto' ? values.provider :
163
+ available.includes(detected.preferred) ? detected.preferred : available.length === 1 ? available[0] : null;
164
+ if (!provider && available.length && interactive) provider = await ui.provider(available, detected.preferred);
165
+ if (!provider || !available.includes(provider)) throw Error('Sign in to Codex or Claude on this computer, then rerun aglit init --provider codex|claude.');
166
+ await ui.step('Save computer settings', () => control('configure', { name, provider, ...(teamId ? { teamId } : {}) }));
167
+ const permissions = await ui.step('Check screen recording and input permissions', () => control('permissions'));
168
+ if (!interactive) ui.result(permissions);
169
+ if (permissions.permissions?.supported && (permissions.permissions.screenRecording !== 'granted' || permissions.permissions.accessibility !== 'granted'))
170
+ throw Error('Grant Screen Recording and Accessibility in System Settings, then rerun aglit init. Anywhere Access has not been enabled.');
171
+ state = await ui.step('Connect this computer to your account', async () => {
172
+ await control('enable');
173
+ const deadline = Date.now() + 30000;
174
+ do {
175
+ const current = await control('status');
176
+ if (current.connected) return current;
177
+ await delay(1000);
178
+ } while (Date.now() < deadline);
179
+ throw Error('Anywhere Access is enabled but its connection is not ready. Check aglit status before retrying.');
180
+ });
181
+ if (values.invite) ui.result(await control('invite', { teamId: values.team, email: values.invite }));
182
+ ui.result(state); ui.finish('Computer ready · https://console.aglit.ai/'); return;
183
+ }
184
+ if (command === 'invite') { ui.result(await control('invite', { teamId: values.team, email })); ui.finish('Invitation sent'); return; }
185
+ if (command === 'uninstall') {
186
+ const result = await control('uninstall');
187
+ if (!result?.revoked || !result?.quitting || !Number.isSafeInteger(result.pid) || result.pid <= 0) throw Error('Revocation was not confirmed; installation preserved');
188
+ ui.result(await ui.step('Remove this installation', () => finishUninstall(result)));
189
+ ui.finish('Uninstalled'); return;
190
+ }
191
+ ui.result(await control(command)); ui.finish('Done');
192
+ }
193
+
194
+ main().then(() => process.exit(process.exitCode || 0), error => {
195
+ if (!error.cancelled) process.stderr.write(`aglit: ${error.message}\n`);
196
+ process.exit(error.cancelled ? 130 : 1);
197
+ });
package/package.json CHANGED
@@ -1,6 +1,25 @@
1
1
  {
2
2
  "name": "@aglit-ai/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.2",
4
+ "description": "Install and manage Aglit Computer Use",
5
+ "license": "UNLICENSED",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "aglit": "bin/aglit.cjs"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=22"
17
+ },
18
+ "dependencies": {
19
+ "@clack/prompts": "1.8.1",
20
+ "yauzl": "3.4.0"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ }
25
+ }
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const net = require('node:net');
4
+ const crypto = require('node:crypto');
5
+
6
+ // One authenticated request per connection, with no automatic mutation replay.
7
+ // This is the existing native SDK handshake, not a new localhost HTTP API.
8
+ function control(configPath, action, fields = {}) {
9
+ const stat = fs.lstatSync(configPath);
10
+ if (!stat.isFile() || stat.isSymbolicLink() || fs.realpathSync(configPath) !== configPath || stat.nlink !== 1 || stat.size > 65536 ||
11
+ process.platform !== 'win32' && (stat.uid !== process.getuid() || stat.mode & 0o077)) throw Error('Private native control configuration required');
12
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
13
+ if (config.version !== 1 || typeof config.endpoint !== 'string' || typeof config.token !== 'string' ||
14
+ !config.token || config.token.length > 4096) throw Error('Native control configuration is incomplete');
15
+ if (process.platform === 'win32') {
16
+ if (!config.endpoint.startsWith('\\\\.\\pipe\\aglit-ai-sdk-')) throw Error('Unexpected native pipe');
17
+ } else {
18
+ const socket = fs.lstatSync(config.endpoint);
19
+ if (!socket.isSocket() || socket.uid !== process.getuid()) throw Error('Native socket owner differs');
20
+ }
21
+ return new Promise((resolve, reject) => {
22
+ const id = crypto.randomUUID();
23
+ const socket = net.createConnection(config.endpoint);
24
+ let buffer = '', authenticated = false, settled = false;
25
+ const timeout = setTimeout(() => finish(Error('Native command timed out; its outcome may be uncertain. Check aglit status before retrying.')), action === 'permissions' ? 5 * 60000 : 30000);
26
+ const finish = (error, result) => {
27
+ if (settled) return;
28
+ settled = true; clearTimeout(timeout); socket.destroy();
29
+ if (error) reject(error); else resolve(result);
30
+ };
31
+ socket.setEncoding('utf8');
32
+ socket.once('connect', () => socket.write(JSON.stringify({ type: 'handshake', token: config.token }) + '\n'));
33
+ socket.on('error', error => finish(error));
34
+ socket.once('close', () => { if (!settled) finish(Error('Native connection closed before command acknowledgement')); });
35
+ socket.on('data', chunk => {
36
+ buffer += chunk;
37
+ if (Buffer.byteLength(buffer) > 256 * 1024) return finish(Error('Native response too large'));
38
+ let newline;
39
+ while ((newline = buffer.indexOf('\n')) >= 0) {
40
+ let response;
41
+ try { response = JSON.parse(buffer.slice(0, newline)); } catch { return finish(Error('Invalid native response')); }
42
+ buffer = buffer.slice(newline + 1);
43
+ if (!authenticated) {
44
+ if (response.type !== 'handshake' || response.ok !== true || response.version !== 1) return finish(Error('Native authentication rejected'));
45
+ authenticated = true;
46
+ socket.write(JSON.stringify({ id, request: { method: 'POST', path: '/computer-use/control', body: { action, ...fields } } }) + '\n');
47
+ } else {
48
+ if (response.id !== id) return finish(Error('Native response identity differs'));
49
+ return response.ok === true ? finish(null, response.result) : finish(Error(response.error || 'Native command rejected'));
50
+ }
51
+ }
52
+ });
53
+ });
54
+ }
55
+ module.exports = { control };
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { pipeline } = require('node:stream/promises');
5
+
6
+ // Extract into a fresh private directory. Defer contained relative symlinks until
7
+ // all regular files have been written, so archive links cannot redirect writes.
8
+ async function extract(archive, root) {
9
+ const yauzl = require('yauzl');
10
+ const zip = await new Promise((resolve, reject) => yauzl.open(archive, { lazyEntries: true, strictFileNames: true }, (error, value) => error ? reject(error) : resolve(value)));
11
+ const links = [];
12
+ let entries = 0, bytes = 0;
13
+ try {
14
+ await new Promise((resolve, reject) => {
15
+ zip.on('error', reject);
16
+ zip.on('end', resolve);
17
+ zip.on('entry', entry => {
18
+ void (async () => {
19
+ const name = entry.fileName;
20
+ if (++entries > 200000 || name.length > 4096 || /[\x00-\x1f\\]/.test(name) || path.isAbsolute(name) || /^[A-Za-z]:/.test(name) || name.split('/').includes('..')) throw Error('Unsafe archive entry');
21
+ const destination = path.resolve(root, name);
22
+ if (!destination.startsWith(root + path.sep)) throw Error('Archive entry escapes installation');
23
+ bytes += entry.uncompressedSize;
24
+ if (!Number.isSafeInteger(bytes) || bytes > 16 * 1024 ** 3) throw Error('Expanded release exceeds installation limit');
25
+ const mode = entry.externalFileAttributes >>> 16;
26
+ if (name.endsWith('/')) { fs.mkdirSync(destination, { recursive: true, mode: 0o755 }); return; }
27
+ const stream = await new Promise((done, fail) => zip.openReadStream(entry, (error, value) => error ? fail(error) : done(value)));
28
+ if ((mode & 0o170000) === 0o120000) {
29
+ if (entry.uncompressedSize > 4096) throw Error('Archive link too large');
30
+ const chunks = [];
31
+ for await (const chunk of stream) chunks.push(chunk);
32
+ const target = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks));
33
+ const resolved = path.resolve(path.dirname(destination), target);
34
+ if (!target || /[\x00-\x1f\\]/.test(target) || path.isAbsolute(target) || /^[A-Za-z]:/.test(target) || !resolved.startsWith(root + path.sep)) throw Error('Archive link escapes installation');
35
+ links.push({ destination, target });
36
+ } else {
37
+ if ((mode & 0o170000) && (mode & 0o170000) !== 0o100000) throw Error('Unsupported archive entry');
38
+ fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o755 });
39
+ await pipeline(stream, fs.createWriteStream(destination, { flags: 'wx', mode: mode & 0o777 || 0o644 }));
40
+ }
41
+ })().then(() => zip.readEntry(), reject);
42
+ });
43
+ zip.readEntry();
44
+ });
45
+ for (const link of links) fs.mkdirSync(path.dirname(link.destination), { recursive: true, mode: 0o755 });
46
+ for (const link of links) fs.symlinkSync(link.target, link.destination);
47
+ for (const link of links) {
48
+ if (!fs.realpathSync(link.destination).startsWith(root + path.sep)) throw Error('Resolved archive link escapes installation');
49
+ }
50
+ } finally { zip.close(); }
51
+ }
52
+ module.exports = { extract };
@@ -0,0 +1,214 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const os = require('node:os');
5
+ const crypto = require('node:crypto');
6
+ const { spawn, execFileSync } = require('node:child_process');
7
+ const { pipeline } = require('node:stream/promises');
8
+ const { Readable, Transform } = require('node:stream');
9
+
10
+ const root = path.join(process.platform === 'darwin' ? path.join(os.homedir(), 'Library/Application Support') :
11
+ process.platform === 'win32' ? process.env.APPDATA || path.join(os.homedir(), 'AppData/Roaming') : path.join(os.homedir(), '.config'), 'Aglit Computer Use');
12
+ const profile = path.join(root, 'profile');
13
+ const receiptPath = path.join(root, 'installation.json');
14
+ const same = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
15
+ function canonical(file) {
16
+ if (!path.isAbsolute(file) || !same(fs.realpathSync(file), file) || fs.lstatSync(file).isSymbolicLink()) throw Error('Canonical installation path required');
17
+ }
18
+ function run(program, args, options = {}) {
19
+ return execFileSync(program, args, { windowsHide: true, timeout: 60000, maxBuffer: 1024 * 1024, encoding: 'utf8', ...options });
20
+ }
21
+ const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
22
+ function privateRoot(create = false) {
23
+ canonical(root);
24
+ if (process.platform !== 'win32') {
25
+ const stat = fs.statSync(root);
26
+ if (stat.uid !== process.getuid() || stat.mode & 0o077) throw Error('Private installation owner required');
27
+ return;
28
+ }
29
+ const script = String.raw`
30
+ $ErrorActionPreference='Stop'
31
+ $target=$env:AGLIT_PRIVATE_ROOT;$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User
32
+ $system=[Security.Principal.SecurityIdentifier]::new('S-1-5-18')
33
+ if($env:AGLIT_CREATE_PRIVATE_ROOT -eq '1'){
34
+ $acl=[Security.AccessControl.DirectorySecurity]::new();$acl.SetOwner($sid);$acl.SetAccessRuleProtection($true,$false)
35
+ foreach($identity in @($sid,$system)){$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($identity,'FullControl','ContainerInherit,ObjectInherit','None','Allow'))}
36
+ Set-Acl -LiteralPath $target -AclObject $acl
37
+ }
38
+ $acl=Get-Acl -LiteralPath $target
39
+ if(!$acl.AreAccessRulesProtected -or $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value -ne $sid.Value){throw 'Private installation owner required'}
40
+ foreach($rule in $acl.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier])){
41
+ if($rule.AccessControlType -ne 'Allow' -or $rule.IdentityReference.Value -notin @($sid.Value,$system.Value)){throw 'Private installation ACL required'}
42
+ }
43
+ `;
44
+ run(powershell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script], {
45
+ env: { ...process.env, AGLIT_PRIVATE_ROOT: root, AGLIT_CREATE_PRIVATE_ROOT: create ? '1' : '0' },
46
+ });
47
+ }
48
+ function readInstallation(allowPartialCleanup = false) {
49
+ privateRoot();
50
+ canonical(receiptPath);
51
+ if (fs.statSync(receiptPath).size > 65536) throw Error('Installation receipt too large');
52
+ const value = JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
53
+ if (value.kind !== 'aglit-computer-use-installation-v1' || value.root !== root ||
54
+ value.profile !== profile || typeof value.executable !== 'string' ||
55
+ !value.executable.startsWith(root + path.sep) || !/^[a-f0-9]{64}$/.test(value.sha256)) throw Error('Installation receipt differs');
56
+ if (!allowPartialCleanup) { canonical(value.executable); canonical(value.profile); }
57
+ return value;
58
+ }
59
+ async function download(url) {
60
+ for (let hop = 0; hop < 6; hop++) {
61
+ const target = new URL(url);
62
+ if (target.protocol !== 'https:' || !['github.com', 'release-assets.githubusercontent.com', 'objects.githubusercontent.com'].includes(target.hostname)) throw Error('Untrusted release URL');
63
+ const response = await fetch(target, { redirect: 'manual', signal: AbortSignal.timeout(30 * 60 * 1000) });
64
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
65
+ await response.body?.cancel();
66
+ url = new URL(response.headers.get('location'), target).href;
67
+ continue;
68
+ }
69
+ if (!response.ok || !response.body) throw Error(`Release download failed (${response.status})`);
70
+ return response;
71
+ }
72
+ throw Error('Too many release redirects');
73
+ }
74
+ async function json(url) {
75
+ const response = await download(url);
76
+ let size = 0;
77
+ const chunks = [];
78
+ for await (const chunk of response.body) {
79
+ size += chunk.length;
80
+ if (size > 65536) throw Error('Release metadata too large');
81
+ chunks.push(chunk);
82
+ }
83
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
84
+ }
85
+ function parseRelease(value, platform) {
86
+ const fields = ['kind', 'package', 'platform', 'version', 'sourceCommit', 'assets', 'windowsSigningPolicy'];
87
+ if (!value || Object.keys(value).some(key => !fields.includes(key)) || value.kind !== 'aglit-github-release-v1' ||
88
+ value.package !== 'desktop-app' || value.platform !== platform || !/^\d+\.\d+\.\d+$/.test(value.version) ||
89
+ !/^[a-f0-9]{40}$/.test(value.sourceCommit) || !Array.isArray(value.assets) || value.assets.length !== 3 ||
90
+ value.windowsSigningPolicy !== undefined && (platform !== 'win' || value.windowsSigningPolicy !== 'unsigned-user-accepted')) throw Error('Invalid desktop release receipt');
91
+ const stem = `Aglit.AI-${value.version}-${platform === 'mac' ? 'mac-arm64' : 'win-x64'}`;
92
+ const names = platform === 'mac' ? [stem + '.dmg', stem + '.zip', stem + '.zip.blockmap'] : [stem + '.exe', stem + '.exe.blockmap', stem + '.zip'];
93
+ for (let index = 0; index < 3; index++) {
94
+ const asset = value.assets[index];
95
+ if (!asset || Object.keys(asset).sort().join(',') !== 'name,sha256,sha512,size' || asset.name !== names[index] ||
96
+ !Number.isSafeInteger(asset.size) || asset.size <= 0 || asset.size > 2 * 1024 ** 3 ||
97
+ !/^[a-f0-9]{64}$/.test(asset.sha256) || !/^[A-Za-z0-9+/]{86}==$/.test(asset.sha512)) throw Error('Invalid release asset');
98
+ }
99
+ return value;
100
+ }
101
+ function verifyPublisher(executable) {
102
+ if (process.platform === 'darwin') {
103
+ const bundle = path.resolve(executable, '../../..');
104
+ run('/usr/bin/codesign', ['--verify', '--deep', '--strict', '--test-requirement', '=anchor apple generic and identifier "ai.aglit.desktop" and certificate leaf[subject.OU] = "8DVJD3J343"', bundle]);
105
+ run('/usr/sbin/spctl', ['--assess', '--type', 'execute', bundle]);
106
+ } else {
107
+ const script = '$s=Get-AuthenticodeSignature -LiteralPath $env:AGLIT_VERIFY_EXECUTABLE; if($s.Status -ne "Valid" -or $s.SignerCertificate.Subject -notmatch "(?:^|,\\s*)CN=Aglit AI(?:,|$)"){throw "Aglit publisher signature required"}';
108
+ run(powershell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script], { env: { ...process.env, AGLIT_VERIFY_EXECUTABLE: executable } });
109
+ }
110
+ }
111
+ async function install() {
112
+ if (fs.existsSync(path.join(root, 'uninstalling.json'))) throw Error('Finish aglit uninstall before reinstalling');
113
+ if (!(process.platform === 'darwin' && process.arch === 'arm64') && !(process.platform === 'win32' && process.arch === 'x64')) throw Error('Published Computer Use releases currently support Apple silicon Macs and Windows x64');
114
+ if (fs.existsSync(receiptPath)) return { installed: true, ...readInstallation() };
115
+ if (fs.existsSync(root)) throw Error(`Existing unowned installation directory preserved: ${root}`);
116
+ const platform = process.platform === 'darwin' ? 'mac' : 'win';
117
+ const base = 'https://github.com/Aglit-AI/desktop-app-release/releases/download/';
118
+ const desired = parseRelease(await json(base + `updates-${platform}/github-release-${platform}.json`), platform);
119
+ const release = parseRelease(await json(base + `v${desired.version}/github-release-${platform}.json`), platform);
120
+ if (JSON.stringify(desired) !== JSON.stringify(release)) throw Error('Release pointer differs from immutable release');
121
+ // The product's unsigned Windows policy is not silently inherited by the CLI.
122
+ if (release.windowsSigningPolicy) throw Error('This Windows release is unsigned; Computer Use installation requires a signed release');
123
+ canonical(path.dirname(root));
124
+ fs.mkdirSync(root, { mode: 0o700 });
125
+ privateRoot(true);
126
+ fs.writeFileSync(path.join(root, 'installation-attempt.json'), JSON.stringify({ kind: 'aglit-computer-use-attempt-v1', root }) + '\n', { flag: 'wx', mode: 0o600 });
127
+ const asset = release.assets.find(item => item.name.endsWith('.zip'));
128
+ const archive = path.join(root, 'runtime.zip');
129
+ let received = 0;
130
+ const sha256 = crypto.createHash('sha256'), sha512 = crypto.createHash('sha512');
131
+ const meter = new Transform({ transform(chunk, _encoding, done) {
132
+ received += chunk.length;
133
+ if (received > asset.size) return done(Error('Release exceeds declared size'));
134
+ sha256.update(chunk); sha512.update(chunk); done(null, chunk);
135
+ } });
136
+ const response = await download(base + `v${release.version}/${asset.name}`);
137
+ await pipeline(Readable.fromWeb(response.body), meter, fs.createWriteStream(archive, { flags: 'wx', mode: 0o600 }));
138
+ if (received !== asset.size || sha256.digest('hex') !== asset.sha256 || sha512.digest('base64') !== asset.sha512) throw Error('Release checksum differs; archive was not executed');
139
+ const runtime = path.join(root, 'runtime');
140
+ fs.mkdirSync(runtime, { mode: 0o700 });
141
+ await require('./extract.cjs').extract(archive, runtime);
142
+ const executable = process.platform === 'darwin' ? path.join(runtime, 'Aglit AI.app/Contents/MacOS/Aglit AI') : path.join(runtime, 'Aglit AI.exe');
143
+ canonical(executable);
144
+ verifyPublisher(executable);
145
+ const resources = process.platform === 'darwin' ? path.join(runtime, 'Aglit AI.app/Contents/Resources') : path.join(runtime, 'resources');
146
+ const buildInfoPath = path.join(resources, 'app.asar.unpacked/dist-electron/desktop-app/build-info.json');
147
+ canonical(buildInfoPath);
148
+ if (fs.statSync(buildInfoPath).size > 16 * 1024 * 1024) throw Error('Build metadata too large');
149
+ const buildInfo = JSON.parse(fs.readFileSync(buildInfoPath, 'utf8'));
150
+ if (buildInfo.gitCommit !== release.sourceCommit || buildInfo.dirty !== false ||
151
+ buildInfo.runtimeMinified !== true || buildInfo.computerUseControlVersion !== 1)
152
+ throw Error('A published obfuscated desktop release with Computer Use CLI support is required; this archive was not launched');
153
+ fs.mkdirSync(profile, { mode: 0o700 });
154
+ fs.writeFileSync(path.join(profile, 'system-settings.json'), JSON.stringify({ settings: { enableAnywhereAccess: false, remoteAccessDeviceId: crypto.randomUUID() } }) + '\n', { flag: 'wx', mode: 0o600 });
155
+ const receipt = { kind: 'aglit-computer-use-installation-v1', root, profile, executable, version: release.version, sourceCommit: release.sourceCommit, sha256: asset.sha256 };
156
+ fs.writeFileSync(receiptPath, JSON.stringify(receipt) + '\n', { flag: 'wx', mode: 0o600 });
157
+ fs.unlinkSync(archive);
158
+ return { installed: true, ...receipt };
159
+ }
160
+ function start() {
161
+ if (fs.existsSync(path.join(root, 'uninstalling.json'))) throw Error('Computer Use is uninstalling. Run aglit uninstall to finish cleanup.');
162
+ if (process.platform === 'win32') run(powershell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command',
163
+ '$p=[Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()); if($p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)){throw "Run aglit init from a non-elevated interactive user terminal"}']);
164
+ const installed = readInstallation();
165
+ verifyPublisher(installed.executable);
166
+ const log = fs.openSync(path.join(root, 'runtime.log'), 'a', 0o600);
167
+ const env = { ...process.env };
168
+ // Do not let an operator's guest/development environment adopt another identity.
169
+ for (const key of Object.keys(env)) if (key.startsWith('AGLIT_') || key.startsWith('VITE_') || ['NODE_ENV', 'ELECTRON_RUN_AS_NODE', 'SERVER_PORT', 'ELECTRON_BRIDGE_PORT'].includes(key)) delete env[key];
170
+ Object.assign(env, { AGLIT_USER_DATA_PATH: profile, AGLIT_ELECTRON_USER_DATA_PATH: profile });
171
+ const child = spawn(installed.executable, ['--computer-use'], { detached: true, windowsHide: true, stdio: ['ignore', log, log], env });
172
+ child.on('error', error => { process.stderr.write(`Computer Use failed to start: ${error.message}\n`); });
173
+ child.unref(); fs.closeSync(log);
174
+ }
175
+ function purge() {
176
+ readInstallation(true);
177
+ // Exact fixed per-user root only. Node removes contained symlinks themselves;
178
+ // it does not recursively follow them into Mini Computer or browser profiles.
179
+ if (path.resolve(root) !== root || path.basename(root) !== 'Aglit Computer Use') throw Error('Purge target differs');
180
+ canonical(root);
181
+ // Preserve recovery receipts until all potentially locked native payloads have
182
+ // been removed. A failed Windows deletion can then resume the same uninstall.
183
+ for (const name of fs.readdirSync(root)) {
184
+ if (name === 'installation.json' || name === 'uninstalling.json') continue;
185
+ const target = path.join(root, name);
186
+ if (path.dirname(target) !== root) throw Error('Cleanup target escapes installation');
187
+ fs.rmSync(target, { recursive: true, force: false, maxRetries: 3, retryDelay: 300 });
188
+ }
189
+ if (fs.existsSync(path.join(root, 'uninstalling.json'))) fs.unlinkSync(path.join(root, 'uninstalling.json'));
190
+ fs.unlinkSync(receiptPath);
191
+ fs.rmdirSync(root);
192
+ if (fs.existsSync(root)) throw Error('Computer Use cleanup is incomplete');
193
+ }
194
+ function purgeIncomplete() {
195
+ if (fs.existsSync(receiptPath)) throw Error('Installed Computer Use must revoke access before uninstall');
196
+ privateRoot();
197
+ const attempt = path.join(root, 'installation-attempt.json');
198
+ canonical(attempt);
199
+ if (fs.statSync(attempt).size > 4096) throw Error('Invalid installation attempt');
200
+ const receipt = JSON.parse(fs.readFileSync(attempt, 'utf8'));
201
+ if (receipt.kind !== 'aglit-computer-use-attempt-v1' || receipt.root !== root) throw Error('Unknown installation directory preserved');
202
+ fs.rmSync(root, { recursive: true, force: false });
203
+ }
204
+ function readUninstall() {
205
+ const file = path.join(root, 'uninstalling.json');
206
+ if (!fs.existsSync(file)) return null;
207
+ readInstallation(true); canonical(file);
208
+ if (fs.statSync(file).size > 4096) throw Error('Invalid uninstall receipt');
209
+ const receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
210
+ if (receipt.kind !== 'aglit-computer-use-uninstall-v1' || receipt.root !== root || receipt.revoked !== true ||
211
+ !Number.isSafeInteger(receipt.pid) || receipt.pid <= 0) throw Error('Invalid uninstall receipt');
212
+ return receipt;
213
+ }
214
+ module.exports = { root, profile, readInstallation, install, start, purge, purgeIncomplete, readUninstall };
package/src/ui.cjs ADDED
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const clean = value => String(value ?? '').replace(/[\x00-\x1f\x7f-\x9f]/g, '');
4
+ function teamsFromRoles(value) {
5
+ const teams = new Map();
6
+ for (const company of value?.companies || []) {
7
+ for (const team of [...(company.teams || []), ...(company.organizations || []).flatMap(organization => organization.teams || [])]) {
8
+ if (typeof team.teamId === 'string') teams.set(team.teamId, { id: team.teamId, roles: team.roles || [] });
9
+ }
10
+ }
11
+ for (const role of value?.roles || []) {
12
+ const id = role.teamId || (role.scopeType === 'team' ? role.scopeId : null);
13
+ if (typeof id === 'string' && !teams.has(id)) teams.set(id, { id, roles: [role.roleLabel || role.role].filter(Boolean) });
14
+ }
15
+ return [...teams.values()];
16
+ }
17
+ async function createUi(interactive) {
18
+ if (!interactive) return {
19
+ interactive: false,
20
+ step: (_label, action) => action(),
21
+ result: value => process.stdout.write(JSON.stringify(value, null, 2) + '\n'),
22
+ info: message => process.stderr.write(message + '\n'),
23
+ finish() {},
24
+ };
25
+ const p = await import('@clack/prompts');
26
+ p.intro('Aglit Computer Use');
27
+ async function answer(promise) {
28
+ const value = await promise;
29
+ if (p.isCancel(value)) {
30
+ p.cancel('Setup cancelled.');
31
+ throw Object.assign(Error('Setup cancelled'), { cancelled: true });
32
+ }
33
+ return value;
34
+ }
35
+ return {
36
+ interactive: true,
37
+ name: initialValue => answer(p.text({ message: 'Name this computer', placeholder: initialValue, defaultValue: initialValue,
38
+ validate: value => value && (!value.trim() || value.length > 100 || /[\x00-\x1f\x7f-\x9f]/.test(value)) ? 'Use 1–100 printable characters.' : undefined })),
39
+ mode: () => answer(p.select({ message: 'How should Aglit run?', initialValue: 'headless', options: [
40
+ { value: 'headless', label: 'Headless', hint: 'default · background' },
41
+ { value: 'headed', label: 'Open Desktop window', hint: 'headed' },
42
+ ] })),
43
+ provider: (providers, preferred) => answer(p.select({ message: 'Choose your AI provider', initialValue: preferred,
44
+ options: providers.map(value => ({ value, label: value === 'codex' ? 'Codex' : 'Claude' })) })),
45
+ team: teams => answer(p.select({ message: 'Team for invitations', initialValue: '', options: [
46
+ { value: '', label: 'Personal computer', hint: 'access stays with your account' },
47
+ ...teams.map(team => ({ value: team.id, label: clean(team.id), hint: team.roles.map(clean).join(', ') })),
48
+ ] })),
49
+ teams(value) {
50
+ const teams = teamsFromRoles(value);
51
+ p.note(teams.length ? teams.map(team => `${clean(team.id)} ${team.roles.map(clean).join(', ')}`).join('\n') : 'No team memberships.', 'Your teams');
52
+ return teams;
53
+ },
54
+ pipelines(value) {
55
+ p.note((value.devices.length ? value.devices.map(device => `${clean(device.name)}${device.platform ? ' · ' + clean(device.platform) : ''}`).join('\n') + '\n\n' : 'No authorized pipeline devices.\n\n') +
56
+ 'https://pipelines.aglit.ai/\nOpen the encrypted viewer with aglit pipelines --open.', 'Pipeline devices');
57
+ },
58
+ confirmAccount: email => answer(p.confirm({ message: `Register with ${clean(email)}?`, initialValue: true })),
59
+ async step(label, action) {
60
+ const spinner = p.spinner(); spinner.start(label);
61
+ try { const value = await action(); spinner.stop(label); return value; }
62
+ catch (error) { spinner.stop('Setup paused'); throw error; }
63
+ },
64
+ result(value) {
65
+ if (value.product === 'Aglit Computer Use') {
66
+ p.note([
67
+ value.name && `Computer ${clean(value.name)}`,
68
+ value.email && `Account ${clean(value.email)}`,
69
+ `Runtime ${value.running ? 'running' : value.installed ? 'stopped' : 'not installed'}`,
70
+ value.mode && `Window ${value.windowVisible ? 'open' : 'hidden'}`,
71
+ `Access ${value.connected ? 'connected' : value.enabled ? 'connecting' : 'disabled'}`,
72
+ value.provider && `Provider ${clean(value.provider)}`,
73
+ ].filter(Boolean).join('\n'), 'This computer');
74
+ } else p.log.info(JSON.stringify(value, null, 2));
75
+ },
76
+ info: message => p.log.info(clean(message)),
77
+ finish: message => p.outro(message),
78
+ };
79
+ }
80
+
81
+ module.exports = { createUi, teamsFromRoles };