@volter/supercode-teams 0.3.141 → 0.3.142
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/agent-session.mjs +38 -0
- package/bin/workspace.mjs +5 -17
- package/fleet.mjs +8 -3
- package/machine.mjs +29 -10
- package/package.json +2 -2
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Native session hints choose the nearest actual harness when terminal servers inherited both variables.
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { basename } from 'node:path';
|
|
4
|
+
import { openLocalMachine, nodeRpc } from '../../client.mjs';
|
|
5
|
+
import { teamsHome, paths } from '@volter/teams/home';
|
|
6
|
+
import { ownClaim } from '../../live-sessions.mjs';
|
|
4
7
|
export function agentSessionHint(env=process.env){
|
|
5
8
|
if(env.SUPERCODE_AGENT_SESSION)return env.SUPERCODE_AGENT_SESSION;
|
|
6
9
|
const claude=env.CLAUDE_CODE_SESSION_ID,codex=env.CODEX_THREAD_ID;
|
|
@@ -18,3 +21,38 @@ export function agentSessionHint(env=process.env){
|
|
|
18
21
|
}
|
|
19
22
|
throw new Error('native session hints are ambiguous; name --session SESSION');
|
|
20
23
|
}
|
|
24
|
+
|
|
25
|
+
/** The daemon's retained binding is authoritative even when a launch has an old or empty context. */
|
|
26
|
+
export async function agentSessionEnvironment(env = process.env, session = agentSessionHint(env)) {
|
|
27
|
+
if (!session) return env;
|
|
28
|
+
let channel;
|
|
29
|
+
try {
|
|
30
|
+
channel = await openLocalMachine(paths(teamsHome(env)).socket);
|
|
31
|
+
const rpc = await nodeRpc(channel);
|
|
32
|
+
const identity = await rpc.call('harness.v1.agents.session', { session, claim: ownClaim(env) });
|
|
33
|
+
if (!identity) return env;
|
|
34
|
+
if (env.SUPERCODE_AGENT_ID && env.SUPERCODE_AGENT_ID !== identity.agent)
|
|
35
|
+
throw Object.assign(new Error('session ownership does not match its launch identity'), { code: 'forbidden' });
|
|
36
|
+
if (env.SUPERCODE_AGENT_ORG && env.SUPERCODE_AGENT_ORG !== identity.org)
|
|
37
|
+
throw Object.assign(new Error('session Org does not match its launch identity'), { code: 'forbidden' });
|
|
38
|
+
return { ...env, SUPERCODE_AGENT_CONTEXT: identity.context ?? '', SUPERCODE_AGENT_ID: identity.agent,
|
|
39
|
+
SUPERCODE_AGENT_ORG: identity.org, SUPERCODE_AGENT_ASSIGNMENTS: JSON.stringify(identity.assignments) };
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// An offline/older daemon does not authorize a fallback to a person's context.
|
|
42
|
+
// An existing launch reader can still be checked by the server itself.
|
|
43
|
+
if (!['ENOENT', 'ECONNREFUSED', 'machine_offline', -32601].includes(error.code)) throw error;
|
|
44
|
+
return env;
|
|
45
|
+
} finally { channel?.close(); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Choosing a context never changes an agent into the connected person. */
|
|
49
|
+
export function agentContextName(contexts, env = process.env, explicit = null) {
|
|
50
|
+
if (!env.SUPERCODE_AGENT_ID) return explicit ?? env.SUPERCODE_CONTEXT ?? contexts.read().current;
|
|
51
|
+
const name = explicit ?? (env.SUPERCODE_AGENT_CONTEXT || null);
|
|
52
|
+
if (!name) throw Object.assign(new Error('agent delegation is unavailable; no custodian context fallback is permitted'), { code: 'authentication_required' });
|
|
53
|
+
const selected = contexts.read().contexts[name];
|
|
54
|
+
if (!selected || selected.principal_id !== env.SUPERCODE_AGENT_ID
|
|
55
|
+
|| (env.SUPERCODE_AGENT_ORG && selected.team_id !== env.SUPERCODE_AGENT_ORG))
|
|
56
|
+
throw Object.assign(new Error('selected context does not belong to this agent and Org'), { code: 'forbidden' });
|
|
57
|
+
return name;
|
|
58
|
+
}
|
package/bin/workspace.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StringDecoder } from 'node:string_decoder';
|
|
3
3
|
import { hostname } from 'node:os';
|
|
4
|
-
import { agentSessionHint } from './workspace/agent-session.mjs';
|
|
4
|
+
import { agentSessionHint, agentSessionEnvironment, agentContextName } from './workspace/agent-session.mjs';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import { existsSync, readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
7
7
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
@@ -60,20 +60,8 @@ export async function workspaceMain(argv = process.argv.slice(2), env = process.
|
|
|
60
60
|
if (verb === 'login') return loginCommand({ flags, origin: sub, rest, home, output });
|
|
61
61
|
if (verb === 'auth') return authCommand({ flags, sub, rest, home, contexts, output, env, dependencies });
|
|
62
62
|
if (verb === 'connect' && flags.enrollment_stdin) adoptEnrollment(contexts, home, flags);
|
|
63
|
-
if(['agent','sessions'].includes(verb))
|
|
64
|
-
|
|
65
|
-
if(session){
|
|
66
|
-
let channel;
|
|
67
|
-
try{
|
|
68
|
-
channel=await openLocalMachine(paths(teamsHome()).socket);const rpc=await nodeRpc(channel);
|
|
69
|
-
const identity=await rpc.call('harness.v1.agents.session',{session});
|
|
70
|
-
if(identity&&!flags.context&&!env.SUPERCODE_AGENT_CONTEXT)env={...env,SUPERCODE_AGENT_CONTEXT:identity.context,SUPERCODE_AGENT_ID:identity.agent,SUPERCODE_AGENT_ASSIGNMENTS:JSON.stringify(identity.assignments)};
|
|
71
|
-
}catch(error){
|
|
72
|
-
// Only absence of an older/offline daemon permits the ordinary plain-session context.
|
|
73
|
-
if(!['ENOENT','ECONNREFUSED','machine_offline',-32601].includes(error.code))throw error;
|
|
74
|
-
}finally{channel?.close();}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
63
|
+
if(env.SUPERCODE_AGENT_ID||['agent','sessions'].includes(verb))
|
|
64
|
+
env=await agentSessionEnvironment(env,flags.session??agentSessionHint(env));
|
|
77
65
|
// Every door called from an agent starts under its delegated context. A sibling product verb
|
|
78
66
|
// must not silently recover the custodian's ordinary login when an agent's reader is bounded.
|
|
79
67
|
const agentContext = env.SUPERCODE_AGENT_CONTEXT || null;
|
|
@@ -87,7 +75,7 @@ export async function workspaceMain(argv = process.argv.slice(2), env = process.
|
|
|
87
75
|
throw new Error('agent delegation is unavailable; no custodian context fallback is permitted');
|
|
88
76
|
}
|
|
89
77
|
const explicitContext = flags.context ?? agentContext ?? env.SUPERCODE_CONTEXT;
|
|
90
|
-
let selected =
|
|
78
|
+
let selected = agentContextName(contexts,env,flags.context??null);
|
|
91
79
|
const urlReference = verb === 'sessions' ? rest.find(value => /^https?:\/\//.test(value)) : null;
|
|
92
80
|
if (!explicitContext && urlReference) selected = contextForUrl(contexts.read(), urlReference) ?? selected;
|
|
93
81
|
if (!selected) usage('no workspace context selected');
|
|
@@ -871,7 +859,7 @@ async function machinesCommand(sub, { flags, rest, output, client, context, team
|
|
|
871
859
|
}
|
|
872
860
|
if (action === 'grant') {
|
|
873
861
|
for (const key of ['to', 'caps', 'ttl']) if (!flags[key]) usage(`--${key} is required`);
|
|
874
|
-
const subject = /^(user|service|group):(.+)$/.exec(flags.to)?.[2]; if (!subject) usage('--to must be user:ID, service:ID or group:ID');
|
|
862
|
+
const subject = /^(user|service|agent|group):(.+)$/.exec(flags.to)?.[2]; if (!subject) usage('--to must be user:ID, service:ID, agent:ID or group:ID');
|
|
875
863
|
const ttl = duration(flags.ttl);
|
|
876
864
|
if (flags.session || flags.binding) {
|
|
877
865
|
// One session binding's native live actions (§5).
|
package/fleet.mjs
CHANGED
|
@@ -8,9 +8,14 @@ import { harnessTransport, nodeRpc, openRemoteMachine } from './client.mjs';
|
|
|
8
8
|
import { openMachine } from './machine-door.mjs';
|
|
9
9
|
import { WorkspaceContexts, fromContext } from './context.mjs';
|
|
10
10
|
import { teamsHome } from '@volter/teams/home';
|
|
11
|
+
import { agentSessionEnvironment, agentContextName } from './bin/workspace/agent-session.mjs';
|
|
11
12
|
|
|
12
13
|
/** Open the channel to the machine `name` names in `context` (or this box's daemon when name is null). */
|
|
13
|
-
export function openOn(name = null, { context = null, env = process.env } = {}) {
|
|
14
|
+
export async function openOn(name = null, { context = null, env = process.env } = {}) {
|
|
15
|
+
if (name && env.SUPERCODE_AGENT_ID) {
|
|
16
|
+
env = await agentSessionEnvironment(env);
|
|
17
|
+
context = agentContextName(new WorkspaceContexts(env.SUPERCODE_HOME ?? resolve(teamsHome(env), '..')), env, context);
|
|
18
|
+
}
|
|
14
19
|
return openMachine({ machine: name, context, env });
|
|
15
20
|
}
|
|
16
21
|
|
|
@@ -44,6 +49,7 @@ export async function nodeRequest(channel, method, params = {}) {
|
|
|
44
49
|
* that refused or is offline carries its reason. Nothing is stored.
|
|
45
50
|
*/
|
|
46
51
|
export async function fleetDiscover({ context = null, machines = null, params = {}, env = process.env } = {}) {
|
|
52
|
+
if (env.SUPERCODE_AGENT_ID) env = await agentSessionEnvironment(env);
|
|
47
53
|
// One page from each machine, never its whole catalog (yuerans' was 5,620 sessions, 20 MB, 13.5 s): the caller's
|
|
48
54
|
// limit, or FLEET_PAGE; each machine answers it from its daemon's held index.
|
|
49
55
|
// `--limit 0` is every session, as it always was
|
|
@@ -51,7 +57,7 @@ export async function fleetDiscover({ context = null, machines = null, params =
|
|
|
51
57
|
const home = teamsHome(env);
|
|
52
58
|
const supercodeHome = env.SUPERCODE_HOME ?? resolve(home, '..');
|
|
53
59
|
const contexts = new WorkspaceContexts(supercodeHome);
|
|
54
|
-
const name =
|
|
60
|
+
const name = agentContextName(contexts, env, context);
|
|
55
61
|
if (!name || name === 'local') throw new Error('--fleet discovers the machines of a Teams context: `supercode teams login …`, then `supercode context use NAME` or --context NAME');
|
|
56
62
|
const { client, context: selected } = fromContext({ supercodeHome, name, fetch: globalThis.fetch });
|
|
57
63
|
const token = contexts.credential(selected.credential_ref).token;
|
|
@@ -105,4 +111,3 @@ export async function fleetDiscover({ context = null, machines = null, params =
|
|
|
105
111
|
|
|
106
112
|
/** Sessions `discover --fleet` asks each machine for when the caller names no limit. */
|
|
107
113
|
const FLEET_PAGE = 50;
|
|
108
|
-
|
package/machine.mjs
CHANGED
|
@@ -682,13 +682,15 @@ export class TeamsMachine extends MachineCore {
|
|
|
682
682
|
// one follow at a time: a change while one is in flight is followed again when it ends, never beside it (two in flight
|
|
683
683
|
// would leave the earlier subscription unsubscribed)
|
|
684
684
|
let pending = null, followed = '', backoffMs = 0, running = false, again = false;
|
|
685
|
+
this.stopLiveFollow = () => { clearTimeout(pending); pending = null; again = false; this.live.stop(); };
|
|
685
686
|
const follow = async () => {
|
|
686
687
|
pending = null;
|
|
688
|
+
if (this.stopping) return;
|
|
687
689
|
if (running) { again = true; return; }
|
|
688
690
|
running = true;
|
|
689
691
|
try { await followNow(); } finally {
|
|
690
692
|
running = false;
|
|
691
|
-
if (again) { again = false; if (!pending) { pending = setTimeout(follow, 0); pending.unref?.(); } }
|
|
693
|
+
if (again && !this.stopping) { again = false; if (!pending) { pending = setTimeout(follow, 0); pending.unref?.(); } }
|
|
692
694
|
}
|
|
693
695
|
};
|
|
694
696
|
const followNow = async () => {
|
|
@@ -699,6 +701,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
699
701
|
try {
|
|
700
702
|
if (locators.length) {
|
|
701
703
|
const opened = await this.#serveRequest('harness.v1.sessions.activity.subscribe', { locators }, 30_000);
|
|
704
|
+
if (this.stopping) return;
|
|
702
705
|
this.liveActivity = opened?.subscription ?? null;
|
|
703
706
|
this.live.codexActivity(opened?.initial ?? [], { replace: true });
|
|
704
707
|
} else { this.liveActivity = null; this.live.codexActivity([], { replace: true }); }
|
|
@@ -706,6 +709,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
706
709
|
this.live.codexFollowedNow();
|
|
707
710
|
if (previous) void this.#serveRequest('harness.v1.sessions.activity.unsubscribe', { subscription: previous }, 30_000).catch(() => {});
|
|
708
711
|
} catch (error) {
|
|
712
|
+
if (this.stopping) return;
|
|
709
713
|
backoffMs = Math.min(300_000, Math.max(2000, (backoffMs || 1000) * 2));
|
|
710
714
|
this.emit('records-log', `live sessions: the Codex sessions' activity could not be followed (${error.message}); tried again in ${Math.round(backoffMs / 1000)} s`);
|
|
711
715
|
pending = setTimeout(follow, backoffMs); pending.unref?.();
|
|
@@ -713,7 +717,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
713
717
|
};
|
|
714
718
|
// a change is followed after 2 s (changes come in bursts); the first follow after the index's first answer at once,
|
|
715
719
|
// as sends wait on it (the Codex sessions are not known until it is done)
|
|
716
|
-
this.live.on('codex-followed', () => { if (!pending) { pending = setTimeout(follow, this.live.codexKnown ? 2000 : 0); pending.unref?.(); } });
|
|
720
|
+
this.live.on('codex-followed', () => { if (!this.stopping && !pending) { pending = setTimeout(follow, this.live.codexKnown ? 2000 : 0); pending.unref?.(); } });
|
|
717
721
|
this.on('internal-notification', (message) => {
|
|
718
722
|
const params = message.params ?? {};
|
|
719
723
|
if (params.subscription && params.subscription === this.liveActivity) this.live.codexActivity(params.activities);
|
|
@@ -1158,6 +1162,8 @@ export class TeamsMachine extends MachineCore {
|
|
|
1158
1162
|
this.jobs?.stopAll();
|
|
1159
1163
|
void this.relayEndpoint?.then((endpoint) => endpoint?.close()).catch(() => {});
|
|
1160
1164
|
this.carrier?.stop();
|
|
1165
|
+
this.stopLiveFollow?.();
|
|
1166
|
+
this.mailStore?.stop();
|
|
1161
1167
|
clearInterval(this.hookSpoolTimer);
|
|
1162
1168
|
this.idleNotices?.stop();
|
|
1163
1169
|
this.agentUpkeep?.stop();
|
|
@@ -1353,17 +1359,30 @@ export class TeamsMachine extends MachineCore {
|
|
|
1353
1359
|
accept();
|
|
1354
1360
|
this.log.append({ principal, cap: 'harness', target: '*', outcome: 'opened' });
|
|
1355
1361
|
const lines = lineSplitter();
|
|
1362
|
+
let forwarding = Promise.resolve();
|
|
1356
1363
|
stream.on('data', (chunk) => {
|
|
1357
1364
|
for (const line of lines(chunk)) {
|
|
1358
1365
|
if (!line) continue;
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1366
|
+
forwarding = forwarding.then(async () => {
|
|
1367
|
+
if (stream.closed) return;
|
|
1368
|
+
if (entry.verify) {
|
|
1369
|
+
const fresh = await entry.verify();
|
|
1370
|
+
if (fresh?.class !== 'operator') {
|
|
1371
|
+
this.log.append({ principal, cap: 'harness', target: '*', outcome: 'refused', detail: 'no live operator grant' });
|
|
1372
|
+
return stream.close('no live operator grant');
|
|
1373
|
+
}
|
|
1374
|
+
entry.admitted = fresh;
|
|
1375
|
+
}
|
|
1376
|
+
if (stream.closed) return;
|
|
1377
|
+
let message;
|
|
1378
|
+
try { message = JSON.parse(line); } catch { return; }
|
|
1379
|
+
const asked = message.id !== undefined && message.id !== null ? message.id : null;
|
|
1380
|
+
if (asked !== null) message.id = joinId(index, message.id);
|
|
1381
|
+
// a serve that is not there to take it answers the request at once, on this stream only
|
|
1382
|
+
if (!this.#toServe(JSON.stringify(message) + '\n') && asked !== null && !stream.closed) {
|
|
1383
|
+
try { stream.write(`${JSON.stringify({ jsonrpc: '2.0', id: asked, error: { code: -32000, message: 'harness serve is not running; it is being started again' } })}\n`); } catch { /* the stream went */ }
|
|
1384
|
+
}
|
|
1385
|
+
}).catch(() => { if (!stream.closed) stream.close('machine authority could not be verified'); });
|
|
1367
1386
|
}
|
|
1368
1387
|
});
|
|
1369
1388
|
stream.on('close', () => { entry.harness = null; });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.142",
|
|
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",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@volter/supercode-health": "^0.1.0",
|
|
71
71
|
"@volter/supercode-terminal": "^0.2.54",
|
|
72
72
|
"@volter/supercode-trace": "^0.1.0",
|
|
73
|
-
"@volter/teams": "0.2.
|
|
73
|
+
"@volter/teams": "0.2.37",
|
|
74
74
|
"@xterm/addon-serialize": "^0.14.0",
|
|
75
75
|
"@xterm/headless": "^6.0.0",
|
|
76
76
|
"protobufjs": "8.8.0",
|