@volter/supercode-teams 0.3.154 → 0.3.155

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
@@ -27,7 +27,10 @@ verified caller at its local origin. `harness.v1.sessions.close_remote` accepts
27
27
  that forwarded caller at the target's Teams door only, under pane control. It
28
28
  retains native live-work checks, then calls the existing pane-close door to
29
29
  recheck its grant, recorded starter and host entry. It adds no caller identity
30
- or origin-attestation protocol. A nonstarter needs explicit
30
+ or origin-attestation protocol. Native activity coverage on this path currently
31
+ includes Claude Code and Codex. Other recognized harness panes refuse without
32
+ `foreign: true` when their activity cannot be read; plain command panes are
33
+ handled separately. A nonstarter needs explicit
31
34
  `foreign: true` for that one pane. These are node-door methods, not additions to
32
35
  the native harness service. [Scope and qualification](../../docs/evidence/remote-pane-close-2026-10-08.md).
33
36
 
package/bin/workspace.mjs CHANGED
@@ -1160,6 +1160,11 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
1160
1160
  // on the machine credential and the daemon keeps the agent's reader context for it; a pane session's agent is the
1161
1161
  // one its launch recorded.
1162
1162
  connector.recordAgentSession=binding=>machine.bindAgentSession(binding,{delegator:context.machine_only?null:contextName});
1163
+ machine.agentDelegator=context.machine_only?null:contextName;
1164
+ machine.agentSessionIntent=session=>{
1165
+ const item=connector.state.agent_sessions?.[session];
1166
+ return item&&['pending','bound'].includes(item.state)?{agent:item.agent,org:item.org,context:item.context,assignments:item.assignments}:null;
1167
+ };
1163
1168
  machine.onLaunchAgent=(session,environment)=>connector.queueAgentSession(session,environment);
1164
1169
  connector.flushAgentIdentity=()=>machine.flushAgentIdentity({delegator:context.machine_only?null:contextName});
1165
1170
  connector.agentClosureReceipt=async id=>{
@@ -1411,6 +1416,7 @@ function humanOutput(value) {
1411
1416
  const data = value?.data ?? value;
1412
1417
  if (data === null) return `${context}Done.\n`;
1413
1418
  if (typeof data === 'string') return `${context}${data}\n`;
1419
+ if(typeof data?.session==='string'&&data.agent)return `${context}Session: ${data.session}\nAgent: ${data.agent.path??data.agent.name??data.agent.id} (${data.agent.id})\n${data.main?'Main session':'Concurrent session'}${data.source?`\nSource: ${data.source}`:''}\n`;
1414
1420
  const row = humanRow(data);
1415
1421
  return `${context}${Object.entries(row).map(([key, item]) => `${key}: ${item}`).join('\n')}\n`;
1416
1422
  }
@@ -1423,7 +1429,7 @@ function humanTranscript(descriptor, messages) {
1423
1429
  const lines = messages.map(message => { const value = message.value ?? message; const body = [text(value.content ?? value.text).trim(), calls(value)].filter(Boolean).join('\n'); return `\n${value.role ?? 'message'}:\n${body || '(no text)'}\n`; });
1424
1430
  return head + coverage + (lines.length ? lines.join('') : '\n(no messages in this window)\n');
1425
1431
  }
1426
- function humanRow(value) { if (!value || typeof value !== 'object') return { value }; const descriptor = value.session ?? value; return Object.fromEntries(Object.entries(descriptor).filter(([, item]) => item === null || ['string', 'number', 'boolean'].includes(typeof item)).slice(0, 8)); }
1432
+ function humanRow(value) { if (!value || typeof value !== 'object') return { value }; const descriptor = value.session&&typeof value.session==='object'?value.session:value; return Object.fromEntries(Object.entries(descriptor).filter(([, item]) => item === null || ['string', 'number', 'boolean'].includes(typeof item)).slice(0, 8)); }
1427
1433
  async function stream(iterator, json) { for await (const event of iterator) process.stdout.write(json ? `${JSON.stringify(event)}\n` : humanOutput(event)); }
1428
1434
  function unsupported(message) { throw Object.assign(new Error(message), { code: 'unsupported', exitCode: 5 }); }
1429
1435
  function notFound(message) { throw Object.assign(new Error(message), { code: 'not_found', exitCode: 4 }); }
@@ -1256,12 +1256,15 @@ export class TeamsConnector {
1256
1256
  for(const [session,item] of Object.entries(sessions)){
1257
1257
  if(item.state!=='pending'||item.next_at>now)continue;
1258
1258
  try{
1259
+ // Reading as the launch's agent needs its delegation, not a transcript already captured in Teams. Keep the
1260
+ // reader first, so a slow initial capture cannot strand the session's whoami/sessions commands.
1261
+ const binding=await this.recordAgentSession?.({session,agent:item.agent,org:item.org,context:item.context,assignments:item.assignments});
1262
+ if(binding)item.context=binding.context;
1259
1263
  if(!item.declared){
1260
1264
  await this.request('POST',`/machines/${this.machineId}/agent-sessions`,{session,agent:item.agent,main:item.main});item.declared=true;
1261
1265
  // its lineage exists now: a session held for want of it is sent again in this pass
1262
1266
  if(this.state.profile_holds?.[session])delete this.state.profile_holds[session];
1263
1267
  }
1264
- await this.recordAgentSession?.({session,agent:item.agent,org:item.org,context:item.context,assignments:item.assignments});
1265
1268
  item.state='bound';item.bound_at=now;delete item.error;
1266
1269
  }catch(error){
1267
1270
  item.attempts+=1;item.error=errorSummary(error,now);
package/machine.mjs CHANGED
@@ -365,6 +365,7 @@ export class TeamsMachine extends MachineCore {
365
365
  /** Typed lines by key → the answer their first attempt gives (see #serveTurn). */
366
366
  #turnKeys = new Map();
367
367
  #sessionBindings = new Map();
368
+ #agentReaders = new Map();
368
369
 
369
370
  /** Native process evidence, published by the connector; a missing transport never reports an end. */
370
371
  async sessionBindings(machineId,incarnationId) {
@@ -402,10 +403,10 @@ export class TeamsMachine extends MachineCore {
402
403
 
403
404
  /** An agent's delegated reader credential installed as a context of this machine's supercode home: what its
404
405
  * sessions' Teams verbs run under. */
405
- async #installAgentContext({origin,team,agent,credential}){
406
+ async #installAgentContext({origin,team,agent,credential},signal=AbortSignal.timeout(15000)){
406
407
  if(typeof origin!=='string'||typeof team!=='string'||typeof agent!=='string'||typeof credential?.token!=='string')throw Object.assign(new Error('agent context requires server, Org, identity and delegated credential'),{code:400});
407
408
  const client=new TeamsClient({server:origin,credential:credential.token});
408
- const me=await client.me({signal:AbortSignal.timeout(15000)});
409
+ const me=await client.me({signal});
409
410
  if(me.principal.id!==agent||me.principal.kind!=='agent'||!me.teams.some(item=>item.team.id===team))throw Object.assign(new Error('delegated credential does not belong to this agent and Org'),{code:403});
410
411
  const key=createHash('sha256').update(`${client.server}/${team}/${agent}`).digest('hex');
411
412
  const name=`agent-${key.slice(0,32)}`,ref=`cred_${key}`,contexts=new WorkspaceContexts(supercodeHome(this.env));
@@ -414,23 +415,49 @@ export class TeamsMachine extends MachineCore {
414
415
  return {context:name,agent,org:team,expires_at:credential.expires_at};
415
416
  }
416
417
 
418
+ /** A reader is reused only while it belongs to this agent and Org and has not expired. Concurrent sessions of
419
+ * one agent share a renewal. The connected person's authority is used only to delegate; no command runs as them. */
420
+ async #agentReader({agent,org,context},delegator){
421
+ const contexts=new WorkspaceContexts(supercodeHome(this.env)),document=contexts.read();
422
+ let selected=context?document.contexts[context]:null;
423
+ if(selected&&(selected.principal_id!==agent||(org&&selected.team_id!==org)))
424
+ throw Object.assign(new Error('agent reader context belongs to another agent or Org'),{code:403});
425
+ if(!selected){
426
+ const existing=Object.entries(document.contexts).find(([,item])=>item.principal_id===agent&&item.team_id===org);
427
+ if(existing)[context,selected]=existing;
428
+ }
429
+ org??=selected?.team_id;
430
+ const credential=selected?contexts.credential(selected.credential_ref):null;
431
+ if(credential?.token&&credential.server_id===selected.server_id&&credential.origin===selected.origin
432
+ &&Number.isSafeInteger(credential.expires_at)&&credential.expires_at>Date.now())return {context,org};
433
+ if(!delegator)return {context:null,org};
434
+ const key=`${org??''}/${agent}`;
435
+ if(this.#agentReaders.has(key))return this.#agentReaders.get(key);
436
+ const renewal=(async()=>{
437
+ const source=fromContext({supercodeHome:supercodeHome(this.env),name:delegator}),signal=AbortSignal.timeout(20000);
438
+ if(org&&source.context.team_id!==org)throw Object.assign(new Error('agent reader renewal belongs to another Org'),{code:403});
439
+ const team=source.client.team(source.context.team_id),identity=(await team.agents.get(agent,{signal})).data;
440
+ if(identity.state!=='active')throw Object.assign(new Error('only an active agent receives a reader context'),{code:403});
441
+ const reader=await team.agents.delegateReader(identity.id,{expires_in_ms:86400000},{signal,ifMatch:identity.revision});
442
+ return this.#installAgentContext({origin:source.context.origin,team:source.context.team_id,agent:identity.id,credential:reader.data},signal);
443
+ })();
444
+ this.#agentReaders.set(key,renewal);
445
+ try{return await renewal;}finally{this.#agentReaders.delete(key);}
446
+ }
447
+
417
448
  /**
418
449
  * A session this machine's connector declared its agent's (D138 step 5): the agent's reader context, as its launch
419
450
  * named it, is kept for the session (agent-session-contexts.json, read by `harness.v1.agents.session`), so its Teams
420
- * verbs run as that agent. A launch that named none gets one delegated here through `delegator`, the context of the
421
- * person this machine is connected as, who manages the agent; a machine connected on its machine credential alone
422
- * keeps the session without one.
451
+ * verbs run as that agent. Missing or expired readers are delegated through the connected person's context, which
452
+ * manages the agent. A machine connected on its machine credential alone keeps the session without one.
423
453
  */
424
454
  async bindAgentSession({session,agent,org,context=null,assignments=[]},{delegator=null}={}){
425
- if(!context&&delegator){
426
- const selected=fromContext({supercodeHome:supercodeHome(this.env),name:delegator}),signal=AbortSignal.timeout(20000);
427
- const team=selected.client.team(selected.context.team_id),identity=(await team.agents.get(agent,{signal})).data;
428
- if(identity.state!=='ended'){
429
- const reader=await team.agents.delegateReader(identity.id,{expires_in_ms:86400000},{signal,ifMatch:identity.revision});
430
- context=(await this.#installAgentContext({origin:selected.context.origin,team:selected.context.team_id,agent:identity.id,credential:reader.data})).context;
431
- }
432
- org??=selected.context.team_id;
433
- }
455
+ const previous=readJson(join(this.home,'agent-session-contexts.json'),{sessions:{}}).sessions[session];
456
+ if(previous&&(previous.agent!==agent||(org&&previous.org!==org)))
457
+ throw Object.assign(new Error('a recorded session cannot change agent or Org'),{code:403});
458
+ context??=previous?.context;
459
+ org??=previous?.org;
460
+ ({context,org}=await this.#agentReader({agent,org,context},delegator));
434
461
  const file=join(this.home,'agent-session-contexts.json'),record=readJson(file,{version:1,sessions:{}});
435
462
  record.sessions[session]={context,agent,org,assignments};
436
463
  const temporary=`${file}.${randomBytes(12).toString('hex')}.tmp`;
@@ -3390,7 +3417,10 @@ export class TeamsMachine extends MachineCore {
3390
3417
  case 'harness.v1.agents.session': {
3391
3418
  if(!operator)throw Object.assign(new Error('local agent session lookup requires operator authority'),{code:403});
3392
3419
  if(typeof params.session!=='string')throw Object.assign(new Error('name a native session'),{code:400});
3393
- return readJson(join(this.home,'agent-session-contexts.json'),{sessions:{}}).sessions[params.session]??null;
3420
+ const binding=readJson(join(this.home,'agent-session-contexts.json'),{sessions:{}}).sessions[params.session]
3421
+ ??this.agentSessionIntent?.(params.session);
3422
+ if(!binding)return null;
3423
+ return this.bindAgentSession({session:params.session,...binding},{delegator:this.agentDelegator??null});
3394
3424
  }
3395
3425
  case 'harness.v1.agents.closureReceipt': {
3396
3426
  if(!operator)throw Object.assign(new Error('native closure receipts require operator authority'),{code:403});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.154",
3
+ "version": "0.3.155",
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",