@volter/supercode-teams 0.2.166 → 0.2.168
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/workspace/args.mjs +2 -2
- package/bin/workspace.mjs +69 -3
- package/connector/native-effects.mjs +3 -1
- package/package.json +1 -1
package/bin/workspace/args.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const BOOLEANS = new Set([
|
|
2
2
|
'json', 'help', 'live', 'saved', 'watch', 'paginate', 'token-stdin',
|
|
3
|
-
'invite-stdin', 'include-children', 'rotate', 'install', 'force',
|
|
3
|
+
'invite-stdin', 'enrollment-stdin', 'include-children', 'rotate', 'install', 'force',
|
|
4
4
|
'take-control', 'dry-run', 'volter', 'foreground', 'no-connect', 'record-only', 'workplace-only',
|
|
5
5
|
]);
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ const VALUES = new Set([
|
|
|
11
11
|
'out', 'ttl', 'type', 'name', 'visibility', 'backfill', 'format', 'to',
|
|
12
12
|
'grant', 'binding', 'caps', 'fidelity', 'cwd', 'root', 'workspace-from', 'since', 'until', 'session',
|
|
13
13
|
'on', 'timeout', 'plan', 'tmux-socket', 'volter-issuer', 'workplace', 'organization', 'key', 'room',
|
|
14
|
-
'conversation', 'rh2', 'scope', 'agent', 'body-file',
|
|
14
|
+
'conversation', 'rh2', 'scope', 'agent', 'body-file', 'enrollment',
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
17
|
export function parseArgs(argv) {
|
package/bin/workspace.mjs
CHANGED
|
@@ -47,6 +47,7 @@ export async function workspaceMain(argv = process.argv.slice(2), env = process.
|
|
|
47
47
|
if (verb === 'server') return serverCommand({ flags, sub, rest, home, output });
|
|
48
48
|
if (verb === 'login') return loginCommand({ flags, origin: sub, rest, home, output });
|
|
49
49
|
if (verb === 'auth') return authCommand({ flags, sub, rest, home, contexts, output, env, dependencies });
|
|
50
|
+
if (verb === 'connect' && flags.enrollment_stdin) adoptEnrollment(contexts, home, flags);
|
|
50
51
|
const explicitContext = flags.context ?? env.SUPERCODE_CONTEXT;
|
|
51
52
|
let selected = explicitContext ?? contexts.read().current;
|
|
52
53
|
const urlReference = verb === 'sessions' ? rest.find(value => /^https?:\/\//.test(value)) : null;
|
|
@@ -55,6 +56,8 @@ export async function workspaceMain(argv = process.argv.slice(2), env = process.
|
|
|
55
56
|
if (selected === 'local') return localCommand({ verb, sub, rest, flags, output });
|
|
56
57
|
const { client, context } = fromContext({ supercodeHome: home, name: selected, fetch: globalThis.fetch });
|
|
57
58
|
const team = client.team(context.team_id);
|
|
59
|
+
// A machine-only context (a box enrolled by its custodian) holds its machine credential and nothing a person has.
|
|
60
|
+
if (context.machine_only && !['connect', 'disconnect', 'status'].includes(verb)) usage(`context ${selected} is machine ${context.machine_name}'s: run teams ${verb} where a person is signed in`);
|
|
58
61
|
const command = { flags, rest, output, client, context, team, explicitContext, home, contextName: selected, env, dependencies };
|
|
59
62
|
if (teamsPrefix && ['list', 'create'].includes(verb)) return teamsCommand(verb, sub, command);
|
|
60
63
|
if (verb === 'apps' || verb === 'app-installations') return appsCommand(verb,sub,{...command,output:value=>output(flags.json?value:redactAppSecrets(value))});
|
|
@@ -588,6 +591,26 @@ async function sourcesCommand(sub, command) {
|
|
|
588
591
|
}
|
|
589
592
|
|
|
590
593
|
async function machinesCommand(sub, { flags, rest, output, client, context, team }) {
|
|
594
|
+
if (sub === 'remove') {
|
|
595
|
+
// The custodian revokes a machine: its credentials, links and grants end, and every later door is refused.
|
|
596
|
+
requireArgs(rest, 1, 1, 'teams machines remove MACHINE_ID'); const current = await team.machines.get(rest[0]);
|
|
597
|
+
await client.request('DELETE', teamPath(context, `/machines/${rest[0]}`), { ifMatch: current.data.revision });
|
|
598
|
+
return output({ version: 1, context: scope(context), data: { removed: rest[0] } });
|
|
599
|
+
}
|
|
600
|
+
if (sub === 'enroll') {
|
|
601
|
+
// The signed-in person enrolls another machine as its custodian. The file carries only that machine's own
|
|
602
|
+
// credential; the machine connects from it (teams connect --enrollment-stdin) and never holds a person's.
|
|
603
|
+
requireArgs(rest, 0, 0, 'teams machines enroll --name NAME --out PATH'); if (!flags.name) usage('--name is required');
|
|
604
|
+
if (!flags.out) usage('--out is required to deliver the machine credential'); const destination = reserveProtected(flags.out, { force: flags.force });
|
|
605
|
+
try {
|
|
606
|
+
const enrollment_key = crypto.randomUUID();
|
|
607
|
+
const response = await team.machines.create({ name: flags.name, enrollment_key });
|
|
608
|
+
let credential = response.data.credential;
|
|
609
|
+
if (!credential) credential = (await client.request('POST', teamPath(context, `/sources/${response.data.source.id}/credentials`), { body: { rotate: true } })).data;
|
|
610
|
+
destination.write({ version: 1, kind: 'machine-enrollment', origin: context.origin, server_id: context.server_id, team_id: context.team_id, team_name: context.team_name ?? null, enrollment_key, machine: response.data.machine, source: response.data.source, credential });
|
|
611
|
+
return output({ version: 1, context: scope(context), data: { machine: response.data.machine, source: response.data.source, written_to: resolve(flags.out) } });
|
|
612
|
+
} catch (error) { destination.abort(); throw error; }
|
|
613
|
+
}
|
|
591
614
|
if (['subscribe','unsubscribe','events'].includes(sub)) {
|
|
592
615
|
requireArgs(rest,sub==='events'?0:1,sub==='events'?0:1,`teams machines ${sub} ${sub==='events'?'':'ADDRESS'}`);
|
|
593
616
|
const channel=await openLocalMachine(paths(teamsHome()).socket);
|
|
@@ -647,11 +670,29 @@ async function machinesCommand(sub, { flags, rest, output, client, context, team
|
|
|
647
670
|
return output(await client.request('POST', machineBase, { body: { subject_id: subject, caps, targets, expires_at: Date.now() + ttl } }));
|
|
648
671
|
}
|
|
649
672
|
}
|
|
650
|
-
usage('teams machines list|roots|access|authorize-continuation|subscribe|unsubscribe|events');
|
|
673
|
+
usage('teams machines list|roots|access|enroll|remove|authorize-continuation|subscribe|unsubscribe|events');
|
|
651
674
|
}
|
|
652
675
|
|
|
653
676
|
async function syncCommand(sub, { flags, rest, output, client, context, team, home }) {
|
|
654
677
|
if (sub === 'list') return output(await team.syncRules.list(optional(flags, ['machine'])));
|
|
678
|
+
if (sub === 'add' && flags.enrollment) {
|
|
679
|
+
// A custodian's rule for a machine it enrolled: the root is one that machine's connector registered (the
|
|
680
|
+
// server keeps each root's id and folder name, never its path, which the machine resolves for itself), chosen
|
|
681
|
+
// by --root ID or by the folder name of --repo. A root not registered yet is refused; `connect --cwd` registers it.
|
|
682
|
+
requireArgs(rest, 0, 0, 'teams sync add --enrollment FILE (--root ROOT_ID | --repo PATH) --harness ID');
|
|
683
|
+
if (!flags.root && !flags.repo) usage('--root or --repo is required with --enrollment'); if (!flags.harness) usage('--harness is required with --enrollment');
|
|
684
|
+
const enrolled = JSON.parse(readFileSync(resolve(flags.enrollment), 'utf8'));
|
|
685
|
+
if (enrolled.kind !== 'machine-enrollment' || enrolled.team_id !== context.team_id) usage('--enrollment names no machine of this team');
|
|
686
|
+
const roots = (await team.machines.roots(enrolled.machine.id)).items ?? [];
|
|
687
|
+
const folder = flags.root ? null : basename(String(flags.repo).replace(/[\\/]+$/, ''));
|
|
688
|
+
if (!flags.root && (!folder || folder === '.' || folder === '..')) usage('--repo names the root by its folder; give the folder, or --root ROOT_ID');
|
|
689
|
+
const matches = flags.root ? roots.filter((r) => r.id === flags.root) : roots.filter((r) => r.name === folder);
|
|
690
|
+
if (!matches.length) unsupported(`the machine has not registered ${flags.root ?? flags.repo} yet; its connect --cwd registers it`);
|
|
691
|
+
if (matches.length > 1) usage(`the machine has ${matches.length} roots named ${matches[0].name}; name one with --root (${matches.map((r) => r.id).join(', ')})`);
|
|
692
|
+
const root = matches[0];
|
|
693
|
+
if (!root.harnesses?.includes(flags.harness)) unsupported(`that root reports no ${flags.harness} discovery adapter`);
|
|
694
|
+
return output(await team.syncRules.create({ machine_id: enrolled.machine.id, source_id: enrolled.source.id, selector: { root_id: root.id, harnesses: [flags.harness] }, visibility: flags.visibility ?? 'restricted', backfill: backfill(flags.backfill ?? 'none') }));
|
|
695
|
+
}
|
|
655
696
|
if (sub === 'add') {
|
|
656
697
|
requireArgs(rest, 0, 0, 'teams sync add --repo PATH'); if (!flags.repo) usage('--repo is required');
|
|
657
698
|
const enrollment = new WorkspaceContexts(home).connector(context.server_id, context.team_id).read();
|
|
@@ -683,16 +724,38 @@ async function stopRunningConnector(saved) {
|
|
|
683
724
|
for (let waited = 0; running.pid > 0 && alive() && waited < 15_000; waited += 250) await new Promise(resolve => setTimeout(resolve, 250));
|
|
684
725
|
}
|
|
685
726
|
|
|
727
|
+
/**
|
|
728
|
+
* The machine side of `teams machines enroll`: the enrollment on stdin becomes this home's machine-only context (its
|
|
729
|
+
* machine credential, never a person's) and its connector state, and the connect that follows starts from them.
|
|
730
|
+
*/
|
|
731
|
+
function adoptEnrollment(contexts, home, flags) {
|
|
732
|
+
let enrolled;
|
|
733
|
+
try { enrolled = JSON.parse(readFileSync(0, 'utf8')); } catch { usage('stdin is not JSON: pipe the file teams machines enroll --out wrote'); }
|
|
734
|
+
if (enrolled?.version !== 1 || enrolled.kind !== 'machine-enrollment' || !enrolled.credential?.token || !enrolled.machine?.id || !enrolled.source?.id || !enrolled.enrollment_key) usage('stdin is not a machine enrollment (teams machines enroll --out)');
|
|
735
|
+
const connector = contexts.connector(enrolled.server_id, enrolled.team_id), saved = connector.read();
|
|
736
|
+
if (saved?.machine && saved.machine.id !== enrolled.machine.id) usage(`this home is already connected as ${saved.machine.name ?? saved.machine.id}; disconnect it first`);
|
|
737
|
+
const name = flags.context ?? 'machine', existing = contexts.read().contexts[name];
|
|
738
|
+
if (existing && !(existing.machine_only && existing.server_id === enrolled.server_id && existing.team_id === enrolled.team_id)) usage(`context ${name} is not this machine's; name another with --context`);
|
|
739
|
+
const credentialId = `cred_${crypto.randomUUID().replaceAll('-', '')}`;
|
|
740
|
+
contexts.writeCredential(credentialId, { token: enrolled.credential.token, kind: 'machine', expires_at: enrolled.credential.expires_at ?? null, server_id: enrolled.server_id, origin: enrolled.origin });
|
|
741
|
+
contexts.upsert(name, { origin: enrolled.origin, server_id: enrolled.server_id, team_id: enrolled.team_id, team_name: enrolled.team_name ?? null, principal_id: null, credential_ref: credentialId, label: null, machine_only: true, machine_name: enrolled.machine.name });
|
|
742
|
+
if (!saved?.machine) connector.write({ version: 1, enrollment_key: enrolled.enrollment_key, phase: 'enrolled', machine: enrolled.machine, source: enrolled.source, credential: enrolled.credential });
|
|
743
|
+
if (!contexts.read().current) contexts.use(name);
|
|
744
|
+
flags.context = name;
|
|
745
|
+
}
|
|
746
|
+
|
|
686
747
|
async function connectorCommand(verb, { flags, rest, output, client, context, team, contextName }, home) {
|
|
687
748
|
requireArgs(rest, 0, 0, `teams ${verb}`); const state = new WorkspaceContexts(home).connector(context.server_id, context.team_id);
|
|
688
749
|
if (verb === 'status') {
|
|
689
|
-
|
|
750
|
+
// A machine has no account to name: its own door answering its credential is the answer.
|
|
751
|
+
let reachable = true; let account = null;
|
|
752
|
+
try { if (context.machine_only) { const enrolled = state.read(); await client.request('GET', teamPath(context, `/machines/${enrolled?.machine?.id}/configuration`)); } else account = await client.me(); } catch { reachable = false; }
|
|
690
753
|
const saved = state.read(); const running = saved?.process?.socket ? await activeSocket(saved.process.socket) : false;
|
|
691
754
|
// A connector deep in a large capture can miss the probe's second while its process lives on: it is busy, not stopped.
|
|
692
755
|
const busy = !running && Boolean(saved?.process) && lifecycleHeld(state.directory);
|
|
693
756
|
// What the connector could not sync, as it last wrote it to its state (it is written while it runs).
|
|
694
757
|
const sync = saved ? readSyncStatus(state.directory) ?? { not_syncing: [], last_error: null, queued_captures: 0 } : null;
|
|
695
|
-
const value = { version: 1, context: scope(context), data: { reachable, account, connector: saved ? { enrolled: true, running: running || busy, ...(busy ? { busy: true } : {}), machine: saved.machine, source: saved.source, sync } : { enrolled: false, running: false }, sources: reachable ? (await team.sources.list()).items : [] } };
|
|
758
|
+
const value = { version: 1, context: scope(context), data: { reachable, account, connector: saved ? { enrolled: true, running: running || busy, ...(busy ? { busy: true } : {}), machine: saved.machine, source: saved.source, sync } : { enrolled: false, running: false }, sources: reachable && !context.machine_only ? (await team.sources.list()).items : [] } };
|
|
696
759
|
if (flags.json) return output(value);
|
|
697
760
|
const lines = [`Team ${context.team_name ?? context.team_id} · ${new URL(context.origin).host}${reachable ? '' : ' · unreachable'}`];
|
|
698
761
|
lines.push(saved?.machine ? `Connector ${saved.machine.name ?? saved.machine.id} · ${running ? 'running' : busy ? 'running (busy: its control socket did not answer within 1 s)' : 'not running'}${saved.service?.kind ? ` (${saved.service.kind})` : ''}` : 'Connector not connected (supercode teams connect)');
|
|
@@ -846,6 +909,9 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
846
909
|
}
|
|
847
910
|
// Uninstalling a Windows task already stopped its connector, so only one still answering is asked to stop.
|
|
848
911
|
if (enrollment.process?.socket && enrollment.process?.token && await activeSocket(enrollment.process.socket)) await requestStop(enrollment.process.socket, enrollment.process.token);
|
|
912
|
+
// A machine its custodian enrolled stops here; revoking it is the custodian's (teams machines remove).
|
|
913
|
+
// It keeps its enrollment, so connect resumes it; the custodian's removal is what ends it.
|
|
914
|
+
if (context.machine_only) { state.update((current) => ({ ...current, process: null, service: null })); return output({ version: 1, context: scope(context), data: { disconnected: true, machine_id: enrollment.machine.id, revoked: false } }); }
|
|
849
915
|
const machine = await team.machines.get(enrollment.machine.id);
|
|
850
916
|
await client.request('DELETE', teamPath(context, `/machines/${enrollment.machine.id}`), { ifMatch: machine.data.revision });
|
|
851
917
|
state.remove();
|
|
@@ -18,7 +18,9 @@ export class NativeEffects {
|
|
|
18
18
|
if (existing && JSON.stringify(existing.params) === JSON.stringify(params)) { existing.seen = true; existing.ruleId = ruleId; return; }
|
|
19
19
|
const { capabilities } = await c.core.runtimeCapabilities(params);
|
|
20
20
|
if (!capabilities.attach_existing_process || !capabilities.stream_events) return;
|
|
21
|
-
|
|
21
|
+
// A live endpoint whose runtime is gone (its receipt's process ended) has nothing to bind: that is quiet.
|
|
22
|
+
let runtime;
|
|
23
|
+
try { runtime = await c.core.attachManagedRuntime(params); } catch { return; }
|
|
22
24
|
try {
|
|
23
25
|
const actions = ['attach_observe'];
|
|
24
26
|
if (capabilities.send_input) actions.push('interact');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.168",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
|
|
6
6
|
"license": "MIT",
|