@aglit-ai/cli 0.1.10 → 0.1.17064

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/bin/aglit.cjs CHANGED
@@ -33,6 +33,8 @@ const help = `Aglit Computer Use
33
33
  aglit pipelines [--open]
34
34
  aglit providers
35
35
  aglit models list | choose
36
+ aglit models login copilot [--reauth] [--json]
37
+ aglit models login-status copilot --attempt ATTEMPT_ID [--json]
36
38
  aglit models publish | unpublish --connection CONNECTION_ID --model MODEL_ID
37
39
  aglit models register --device DEVICE_ID --fingerprint SHA256 [--name NAME]
38
40
  aglit models prefer CONNECTION_ID|auto
@@ -66,7 +68,7 @@ async function finishUninstall(receipt) {
66
68
  async function main() {
67
69
  const delegated = installation.development ? null : updates.delegate(__filename); if (delegated !== null) { process.exitCode = delegated; return; }
68
70
  const { values, positionals } = parseArgs({ options: {
69
- connections: { type: 'string' },
71
+ connections: { type: 'string' }, attempt: { type: 'string' }, reauth: {type:'boolean'},
70
72
  harness: { type: 'boolean' },
71
73
  device: { type: 'string' }, fingerprint: { type: 'string' }, challenge: { type: 'string' }, code: { type: 'string' }, connection: { type: 'string' }, model: { type: 'string' }, name: { type: 'string' }, provider: { type: 'string' }, account: { type: 'string' },
72
74
  team: { type: 'string' }, workspace: { type: 'string' }, check: { type: 'boolean' }, channel: { type: 'string' }, open: { type: 'boolean' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
@@ -75,7 +77,7 @@ async function main() {
75
77
  if (values.help || !positionals.length && !interactive) { process.stdout.write(help); return; }
76
78
  const [command = 'init', email, connectionId] = positionals;
77
79
  const options = {
78
- models: ['device','fingerprint','name','json','challenge','code','connection','connections','model','harness'],
80
+ models: ['device','fingerprint','name','json','challenge','code','connection','connections','model','harness','attempt','reauth'],
79
81
  install: ['json', 'channel'], init: ['name', 'provider', 'account', 'json', 'channel'],
80
82
  start: ['json', 'channel'], ui: ['json'], status: ['json'], update: ['check', 'channel', 'json'], updates: ['json'], enable: [], disable: [], rename: ['name', 'json'], console: ['json'], teams: ['json', 'open'], team: ['name', 'team', 'workspace', 'json'], pipelines: ['open', 'json'], providers: ['json'], invite: ['team'], uninstall: [],
81
83
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglit-ai/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.17064",
4
4
  "description": "Install and manage Aglit Computer Use",
5
5
  "license": "UNLICENSED",
6
6
  "type": "commonjs",
package/src/control.cjs CHANGED
@@ -22,7 +22,7 @@ function control(configPath, action, fields = {}) {
22
22
  const id = crypto.randomUUID();
23
23
  const socket = net.createConnection(config.endpoint);
24
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 : ['enable','models-test'].includes(action) ? 120000 : 30000);
25
+ const timeout = setTimeout(() => finish(Error('Native command timed out; its outcome may be uncertain. Check aglit status before retrying.')), action === 'permissions' ? 5 * 60000 : ['enable','models-test','models-login'].includes(action) ? 120000 : 30000);
26
26
  const finish = (error, result) => {
27
27
  if (settled) return;
28
28
  settled = true; clearTimeout(timeout); socket.destroy();
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+ /** UI-free selection policy lives in runtime-api; the CLI prompts only on its
3
+ * explicit choice-required result, then persists the choice through native control. */
4
+ async function listModelConnections(control) {
5
+ try {
6
+ const view = await control('models-list');
7
+ if (!view || !Array.isArray(view.connections) || !view.selection || typeof view.selection.status !== 'string') throw Error('Invalid discovery response');
8
+ return view;
9
+ } catch {
10
+ return { connections: [], selection: { status: 'discovery-unavailable', connection_id: null },
11
+ warnings: ['Model endpoint discovery is unavailable. Existing provider settings are unchanged; retry aglit models list later.'] };
12
+ }
13
+ }
14
+ async function chooseModelConnection(control, { provider, question } = {}) {
15
+ let view = await listModelConnections(control);
16
+ if (view.selection.status === 'discovery-unavailable') return { view, connectionId: null, unavailable: true };
17
+ // Init/choose is an explicit connection action. Passive models-list never
18
+ // prompts; reconnect only the saved/chosen endpoint, never every known host.
19
+ if (['preferred-unavailable','setup-required'].includes(view.selection.status)) {
20
+ const remote = view.connections.filter(value => value.kind === 'remote');
21
+ let target = view.preferred_connection_id ? remote.find(value => value.id === view.preferred_connection_id) : remote.length === 1 ? remote[0] : undefined;
22
+ let chosen = false;
23
+ if (!target && !view.preferred_connection_id && remote.length > 1) {
24
+ if (!question) throw Error('More than one model connection is available. Run aglit models choose.');
25
+ const answer = (await question(remote.map((value,index)=>`${index+1}. ${value.name}`).join('\n')+'\nModel connection: ')).trim();
26
+ if (!/^[1-9]\d*$/.test(answer) || !remote[Number(answer)-1]) throw Error('Choose a listed model connection');
27
+ target = remote[Number(answer)-1]; chosen = true;
28
+ }
29
+ if (target) {
30
+ view = await modelCommand(control,'register',undefined,{device:target.device_id,fingerprint:target.identity_fingerprint,name:target.name},question);
31
+ if (chosen) view = await control('models-prefer',{connection_id:target.id});
32
+ }
33
+ }
34
+ let selection = view.selection;
35
+ if (provider && provider !== 'auto' && provider !== 'aglit') {
36
+ const candidates = view.connections.filter(value => value.available && value.models.some(model => model.provider === provider));
37
+ if (candidates.length === 1) {
38
+ view = await control('models-prefer', { connection_id: candidates[0].id }); selection = view.selection;
39
+ } else if (!candidates.length) throw Error('No connected account supplies that provider');
40
+ else { view = { ...view, connections: candidates }; selection = { status: 'choice-required', connection_id: null }; }
41
+ }
42
+ if (selection.status === 'choice-required') {
43
+ if (!question) throw Error('More than one model connection is available. Run aglit models choose or aglit models prefer CONNECTION_ID.');
44
+ const choices = view.connections.filter(value => value.available && value.models.length);
45
+ const answer = await question(choices.map((value,index)=>`${index+1}. ${value.name}`).join('\n')+'\nModel connection: ');
46
+ if (!/^[1-9]\d*$/.test(answer.trim()) || !choices[Number(answer.trim())-1]) throw Error('Choose a listed model connection');
47
+ view = await control('models-prefer', { connection_id: choices[Number(answer.trim())-1].id }); selection = view.selection;
48
+ }
49
+ if (selection.status === 'preferred-unavailable') throw Error('Your saved model connection is unavailable. Reconnect it or explicitly choose another with aglit models prefer CONNECTION_ID.');
50
+ if (selection.status !== 'selected') throw Error('No model connection is ready. Connect a local provider or register an Aglit model endpoint.');
51
+ return { view, connectionId: selection.connection_id };
52
+ }
53
+ function validateModelCommand(command, id, values) {
54
+ const allowed = {list:['json'],login:['json','reauth'],'login-status':['attempt','json'],test:['connection','model','harness'],choose:[],pool:['name','connections'],publish:['connection','model'],unpublish:['connection','model'],unserve:[],register:['device','fingerprint','name','challenge','code'],prefer:[],remove:[]};
55
+ if (!Object.hasOwn(allowed,command) || Object.keys(values).some(key=>!allowed[command].includes(key)) ||
56
+ (!['prefer','remove','pool','login','login-status'].includes(command) && id) || (['prefer','remove','pool','login','login-status'].includes(command) && !id)) throw Error('Invalid model command or options');
57
+ if(['login','login-status'].includes(command)&&(id!=='copilot'||command==='login-status'&&!/^[a-f0-9-]{36}$/.test(values.attempt??'')))throw Error('Use aglit models login copilot or aglit models login-status copilot --attempt ATTEMPT_ID');
58
+ if(values.reauth!==undefined&&values.reauth!==true)throw Error('Use --reauth only to explicitly replace the Copilot login');
59
+ if (command === 'pool' && (!/^[A-Za-z0-9._-]{1,80}$/.test(id) || !values.name?.trim() || values.name.length > 100 ||
60
+ typeof values.connections !== 'string' || values.connections.length > 6400 || !values.connections.split(',').every(value => /^local:[A-Za-z0-9._:-]{1,180}$/.test(value) && !value.startsWith('local:pool:'))))
61
+ throw Error('Use aglit models pool POOL_ID --name NAME --connections local:ACCOUNT_A,local:ACCOUNT_B');
62
+ if (command === 'register' && (!/^[A-Za-z0-9._:-]{1,180}$/.test(values.device || '') || !/^[a-f0-9]{64}$/.test(values.fingerprint || '')))
63
+ throw Error('Use --device DEVICE_ID and --fingerprint with the endpoint SHA-256 fingerprint');
64
+ if (Boolean(values.challenge) !== Boolean(values.code) || values.code && !/^[0-9]{4,12}$/.test(values.code) ||
65
+ values.challenge && (!/^[A-Za-z0-9._:-]{1,256}$/.test(values.challenge))) throw Error('Supply both --challenge and a numeric --code');
66
+ if (id && !/^[A-Za-z0-9._:-]{1,200}$/.test(id)) throw Error('Invalid model connection ID');
67
+ if (['publish','unpublish'].includes(command) && (!/^local:[A-Za-z0-9._:-]{1,180}$/.test(values.connection||'') || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/.test(values.model||''))) throw Error('Choose a local --connection and --model');
68
+ if (command === 'remove' && !id.startsWith('remote:')) throw Error('A registered remote connection ID is required');
69
+ }
70
+ async function modelCommand(control, command, id, values, question) {
71
+ validateModelCommand(command,id,values);
72
+ if (command === 'list') return listModelConnections(control);
73
+ if(command==='login'||command==='login-status')return control(command==='login'?'models-login':'models-login-status',{provider_id:id,...(command==='login-status'?{attempt_id:values.attempt}:values.reauth?{reauth:'true'}:{})});
74
+ if (command === 'test') {
75
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/.test(values.model || '') || values.connection && !/^[A-Za-z0-9._:-]{1,200}$/.test(values.connection)) throw Error('Use aglit models test --model MODEL_ID [--connection CONNECTION_ID]');
76
+ return control('models-test', { model_id: values.model, ...(values.connection ? { connection_id: values.connection } : {}), ...(values.harness ? { harness: 'true' } : {}) });
77
+ }
78
+ if (command === 'pool') return control('models-pool-save', { pool_id: 'local:pool:' + id, name: values.name,
79
+ connection_ids: JSON.stringify(values.connections.split(',')) });
80
+ if (command === 'register') {
81
+ if (!values.device || !values.fingerprint || id) throw Error('Use aglit models register --device DEVICE_ID --fingerprint SHA256 [--name NAME]');
82
+ if (Boolean(values.challenge) !== Boolean(values.code) || values.code && !/^[0-9]{4,12}$/.test(values.code)) throw Error('Supply both --challenge and a numeric --code');
83
+ const fields = {device_id:values.device,identity_fingerprint:values.fingerprint,name:values.name||values.device, ...(values.challenge ? {challenge_id:values.challenge,code:values.code} : {})};
84
+ let result = await control('models-register',fields);
85
+ let consoleUrl;
86
+ if (typeof result.console_url === 'string') {
87
+ try { const url = new URL(result.console_url);
88
+ if ((url.protocol === 'https:' || process.env.NODE_ENV === 'development' && url.protocol === 'http:' && url.hostname === '127.0.0.1') &&
89
+ !url.username && !url.password && !url.search && !url.hash && url.pathname === '/employees/' + encodeURIComponent(values.device)) consoleUrl = url.href;
90
+ } catch { /* Never present an unvalidated authentication destination. */ }
91
+ }
92
+ const instructions = `A security-code dialog appears on ${fields.name}. Check that device.` +
93
+ (consoleUrl ? ` Open its console on that device if needed: ${consoleUrl}` : '');
94
+ if (result.authentication_required && result.challenge_id && question) {
95
+ const code = (await question(instructions + '\nRemote Access authentication code: ')).trim();
96
+ if (!/^[0-9]{4,12}$/.test(code)) throw Error('A numeric Remote Access code is required');
97
+ result = await control('models-register',{...fields,challenge_id:result.challenge_id,code});
98
+ }
99
+ if (result.authentication_required) throw Error(instructions + ' Complete registration with --challenge CHALLENGE_ID --code CODE' +
100
+ (result.challenge_id ? ' (challenge '+result.challenge_id+')' : ''));
101
+ return result;
102
+ }
103
+ if (command === 'prefer' || command === 'remove') {
104
+ if (!id || !/^[A-Za-z0-9._:-]{1,200}$/.test(id)) throw Error('A model connection ID is required');
105
+ return control(command==='prefer'?'models-prefer':'models-remove',{connection_id:command==='prefer'&&id==='auto'?'':id});
106
+ }
107
+ if (command === 'publish' || command === 'unpublish') return control('models-publish',{connection_id:values.connection,model_id:values.model,published:String(command==='publish')});
108
+ if (command === 'unserve') return control('models-unpublish-all');
109
+ if (command === 'choose') return (await chooseModelConnection(control,{question})).view;
110
+ throw Error('Use aglit models list|register|prefer|remove|publish|unpublish|unserve|choose');
111
+ }
112
+ module.exports={chooseModelConnection,listModelConnections,modelCommand,validateModelCommand};
@@ -0,0 +1,77 @@
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 { execFile } = require('node:child_process');
7
+ const { promisify } = require('node:util');
8
+ const run = promisify(execFile);
9
+ const need = (value, message) => { if (!value) throw Error(message); };
10
+
11
+ // Read a single payload from a verified npm tarball. Native tar writes only to
12
+ // stdout: package paths, links and lifecycle scripts never write into the guest.
13
+ async function packageFile(manifest, file, maximum, signal = AbortSignal.timeout(120000)) {
14
+ const url = new URL(manifest.dist.tarball);
15
+ need(url.protocol === 'https:' && url.hostname === 'registry.npmjs.org' && !url.port && !url.username && !url.password &&
16
+ /^sha512-[A-Za-z0-9+/]{86}==$/.test(manifest.dist.integrity), 'Untrusted runtime npm package');
17
+ need(['runtime/desktop-runtime.json', 'runtime/part.bin'].includes(file), 'Unknown runtime payload');
18
+ const directory = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'aglit-runtime-'));
19
+ const archive = path.join(directory, 'package.tgz');
20
+ try {
21
+ const response = await fetch(url, {redirect: 'error', signal: AbortSignal.any([signal, AbortSignal.timeout(120000)])});
22
+ need(response.ok && response.body, 'Runtime npm download unavailable');
23
+ const fd = fs.openSync(archive, 'wx', 0o600), digest = crypto.createHash('sha512');
24
+ let size = 0;
25
+ try {
26
+ for await (const chunk of response.body) {
27
+ size += chunk.length; need(size <= maximum + 2 * 1024 ** 2, 'Runtime npm package exceeds limit');
28
+ digest.update(chunk);
29
+ let offset = 0;
30
+ while (offset < chunk.length) offset += fs.writeSync(fd, chunk, offset, chunk.length - offset);
31
+ }
32
+ need('sha512-' + digest.digest('base64') === manifest.dist.integrity, 'Runtime npm integrity differs');
33
+ } finally { fs.closeSync(fd); }
34
+ const tar = process.platform === 'win32' ? path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/tar.exe') : '/usr/bin/tar';
35
+ const {stdout} = await run(tar, ['-xOf', archive, 'package/' + file], {encoding: 'buffer', maxBuffer: maximum + 1, timeout: 30000, windowsHide: true});
36
+ need(stdout.length > 0 && stdout.length <= maximum, 'Runtime payload exceeds limit');
37
+ return stdout;
38
+ } finally { fs.rmSync(directory, {recursive: true, force: true}); }
39
+ }
40
+
41
+ // The same verified part stream feeds normal downloads and native baseline
42
+ // admission, whose existing descriptor custody stays owned by its native worker.
43
+ async function* archiveParts({descriptor, parts, signal}) {
44
+ need(parts.length === descriptor.chunks.length && parts.length > 0 && parts.length <= 32, 'Runtime part graph differs');
45
+ const sha256 = crypto.createHash('sha256'), sha512 = crypto.createHash('sha512');
46
+ let received = 0;
47
+ for (const [index, part] of descriptor.chunks.entries()) {
48
+ need(parts[index].name === part.package && parts[index].version === part.version, 'Runtime part identity differs');
49
+ const bytes = await packageFile(parts[index], part.file, part.size, signal);
50
+ need(bytes.length === part.size && crypto.createHash('sha256').update(bytes).digest('hex') === part.sha256 &&
51
+ crypto.createHash('sha512').update(bytes).digest('base64') === part.sha512, 'Runtime part checksum differs');
52
+ received += bytes.length; sha256.update(bytes); sha512.update(bytes);
53
+ yield bytes;
54
+ }
55
+ need(received === descriptor.archive.size && sha256.digest('hex') === descriptor.archive.sha256 &&
56
+ sha512.digest('base64') === descriptor.archive.sha512, 'Runtime archive checksum differs');
57
+ }
58
+
59
+ async function downloadArchive({descriptor, parts, destination, onProgress = () => {}}) {
60
+ const fd = fs.openSync(destination, 'wx', 0o600), owned = fs.fstatSync(fd);
61
+ let received = 0, verified = false;
62
+ try {
63
+ for await (const bytes of archiveParts({descriptor, parts})) {
64
+ let offset = 0;
65
+ while (offset < bytes.length) offset += fs.writeSync(fd, bytes, offset, bytes.length - offset);
66
+ received += bytes.length; onProgress(received);
67
+ }
68
+ fs.fsyncSync(fd); verified = true;
69
+ } finally {
70
+ fs.closeSync(fd);
71
+ if (!verified) {
72
+ const current = fs.lstatSync(destination);
73
+ if (current.dev === owned.dev && current.ino === owned.ino && current.isFile() && !current.isSymbolicLink()) fs.unlinkSync(destination);
74
+ }
75
+ }
76
+ }
77
+ module.exports = {packageFile, archiveParts, downloadArchive};