@volter/supercode-teams 0.2.178 → 0.3.0

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/machine.mjs CHANGED
@@ -29,8 +29,12 @@ import { EventEmitter } from 'node:events';
29
29
  import { appendFileSync, readFileSync, writeFileSync, writeSync, statSync, readdirSync, mkdirSync, renameSync, rmSync, existsSync, openSync, readSync, closeSync, accessSync, constants } from 'node:fs';
30
30
  import { join, resolve, dirname } from 'node:path';
31
31
  import { fileURLToPath } from 'node:url';
32
+ import { boardGuardArguments } from './board-process-guard.mjs';
32
33
  import { homedir, hostname } from 'node:os';
33
34
  import { Workspaces, inspectCheckout } from './workspaces.mjs';
35
+ import { WorkspaceContexts, fromContext } from '@volter/teams/context';
36
+ import { TeamsClient } from '@volter/teams/http-client';
37
+ import { supercodeHome } from '@volter/teams/home';
34
38
  import { readMailHistory } from './mail-history.mjs';
35
39
  import { randomBytes, createHash } from 'node:crypto';
36
40
  import { WebSocket } from 'ws';
@@ -39,7 +43,7 @@ import { TmuxTerminalHost, classifyAttentionSignal, composerHoldsDraft, composer
39
43
  // than failing the load: a POSIX daemon never needs it. Windows needs a supercode-terminal with PtyTerminalHost.
40
44
  import * as terminal from '@volter/supercode-terminal';
41
45
  import { findLiveReceipt, mintFrontendCredential } from '@volter/supercode-harness-sdk/live-runtime';
42
- import { lineSplitter } from './lines.mjs';
46
+ import { lineSplitter } from '@volter/teams/lines';
43
47
  import { NODE_PROTOCOL } from '@volter/teams/channel';
44
48
  import { holds, observesPanes } from '@volter/teams/admission';
45
49
  import { MachineCore, MACHINE_VERSION } from '@volter/teams/machine-core';
@@ -53,6 +57,7 @@ import { JobOwner, executionCapabilities, streamProgram, serveWorkspace } from '
53
57
  import { TerminalMailbox, retainedTerminalMail } from './terminal-mail.mjs';
54
58
  import { confirmNativeInitialInput } from './native-initial-input.mjs';
55
59
  import { canonicalCodexFolderTrust, codexProgram, codexStartupArguments, codexUpdatePrompt, codexVersion, settleCodexHookReview } from './codex-startup.mjs';
60
+ import { sessionNoticeTarget } from './session-notice-target.mjs';
56
61
 
57
62
  /** The preload every launched pane carries: it announces the ports a pane's node processes listen on. */
58
63
  const PORTHOLE_HOOK = fileURLToPath(new URL('./porthole-hook.cjs', import.meta.url));
@@ -155,6 +160,29 @@ function paneExit(pane) {
155
160
  export class TeamsMachine extends MachineCore {
156
161
  /** Typed lines by key → the answer their first attempt gives (see #serveTurn). */
157
162
  #turnKeys = new Map();
163
+ #sessionBindings = new Map();
164
+
165
+ /** Native process evidence, published by the connector; a missing transport never reports an end. */
166
+ async sessionBindings(machineId,incarnationId) {
167
+ const panes=await this.host.listSessions();
168
+ const live=panes.filter(pane=>!pane.dead&&pane.rootPid);
169
+ const reading=live.length?await this.#serveRequest('harness.v1.sessions.activity_under',{pids:live.map(pane=>pane.rootPid)}):{activities:[]};
170
+ const byPid=new Map((reading.activities??[]).map(item=>[item.pid,item.activity]));
171
+ const current=new Set(panes.filter(pane=>!pane.dead).map(pane=>pane.contextKey));
172
+ for(const pane of live){
173
+ const activity=byPid.get(pane.rootPid);if(!activity?.session_id)continue;
174
+ const key=`${pane.contextKey}:${activity.harness}:${activity.session_id}`;
175
+ this.#sessionBindings.set(key,{id:`pane_${createHash('sha256').update(`${machineId}:${incarnationId}:${key}`).digest('hex').slice(0,32)}`,
176
+ machine_id:machineId,incarnation_id:incarnationId,pane:pane.contextKey,harness:activity.harness,
177
+ native_session_id:activity.session_id,native_runtime_id:activity.session_id,state:activity.presence==='running'?'running':'unknown',
178
+ observed_at:Date.now(),native_capabilities:[],evidence:activity.evidence});
179
+ }
180
+ for(const binding of this.#sessionBindings.values()){
181
+ if(!current.has(binding.pane)){binding.state='stopped';binding.observed_at=Date.now();}
182
+ else if(!live.some(pane=>pane.contextKey===binding.pane&&byPid.get(pane.rootPid)?.session_id===binding.native_session_id)){binding.state='unknown';binding.observed_at=Date.now();}
183
+ }
184
+ return [...this.#sessionBindings.values()];
185
+ }
158
186
 
159
187
  /**
160
188
  * `link`: { server, teamId, machineId, credential, incarnationId, configuration } to hold the
@@ -475,22 +503,87 @@ export class TeamsMachine extends MachineCore {
475
503
 
476
504
  // ---- mail ----------------------------------------------------------------
477
505
 
478
- #serveMail(entry, stream, accept, refuse) {
506
+ #nativeMailReply(request){
507
+ return new Promise((resolve,reject)=>{
508
+ const child=spawn(this.supercodeBin,['message','remote-deliver'],{stdio:['pipe','pipe','ignore'],env:this.env});
509
+ let output='',done=false;
510
+ const finish=(error,result)=>{if(done)return;done=true;clearTimeout(timer);error?reject(error):resolve(result);};
511
+ const timer=setTimeout(()=>{child.kill('SIGTERM');finish(new Error('native mail read timed out'));},10000);
512
+ child.stdout.on('data',chunk=>{output+=chunk;if(output.length>4*1024*1024){child.kill('SIGTERM');finish(new Error('native mail reply exceeded its bound'));}});
513
+ child.once('error',error=>finish(error));
514
+ child.once('close',code=>{
515
+ if(code!==0)return finish(new Error(`native mail read exited ${code}`));
516
+ try{finish(null,JSON.parse(output));}catch{finish(new Error('native mail read returned invalid JSON'));}
517
+ });
518
+ child.stdin.on('error',error=>finish(error));child.stdin.end(JSON.stringify(request));
519
+ });
520
+ }
521
+
522
+ #noticeTarget(address){
523
+ return sessionNoticeTarget(address,{machine:hostname().split('.')[0].toLowerCase().replace(/[^a-z0-9_-]/g,'-'),
524
+ mail:request=>this.#nativeMailReply(request),
525
+ discover:params=>this.#serveRequest('harness.v1.sessions.discover',params,10000),
526
+ recordedConfig:locator=>this.#serveRequest('harness.v1.sessions.recorded_config',{locator},15000)});
527
+ }
528
+
529
+ async #resumeNoticeTarget(address){
530
+ const pending=this.noticeResumes??=new Map();
531
+ if(pending.has(address))return pending.get(address);
532
+ const resume=(async()=>{
533
+ const status=await this.#noticeTarget(address);
534
+ if(status.state==='running')return;
535
+ if(!status.resumable)throw Object.assign(new Error(status.reason??'native session cannot resume'),{noticeCode:'session_resume_unavailable'});
536
+ const code=await new Promise((resolve,reject)=>{
537
+ // Explicit native approval and sandbox flags suppress open's unattended defaults.
538
+ const child=spawn(this.supercodeBin,['open',address,'--detach','--',...status.resume.args],
539
+ {cwd:status.resume.cwd,env:{...this.env,SUPERCODE_POPUPS:'0'},stdio:'ignore'});
540
+ const timer=setTimeout(()=>{child.kill('SIGTERM');resolve(null);},15000);
541
+ child.once('error',error=>{clearTimeout(timer);reject(error);});child.once('close',code=>{clearTimeout(timer);resolve(code);});
542
+ });
543
+ for(const deadline=Date.now()+10000;;await new Promise(resolve=>setTimeout(resolve,200))){
544
+ const live=await this.#nativeMailReply({op:'list'});
545
+ if(live.code===0&&live.sessions?.some(row=>row.address===address))return;
546
+ if(Date.now()>=deadline)throw Object.assign(new Error(code===0?'native resume started but has no live delivery door':`native resume was not confirmed (exit ${code??'timeout'})`),{noticeCode:'session_resume_pending'});
547
+ }
548
+ })();
549
+ pending.set(address,resume);
550
+ try{return await resume;}finally{pending.delete(address);}
551
+ }
552
+
553
+ #serveMail(entry, stream, accept, refuse,requestOverride=null) {
479
554
  // One message into a session on this machine, delivered by `supercode message
480
555
  // remote-deliver` through the same doors a local send uses. The receiver is the
481
556
  // target: `mail:*` reaches every session here, `mail:<address>` one of them; a
482
557
  // receiver named rather than addressed needs `mail:*`.
483
558
  const { admitted, principal } = entry;
484
- const request = stream.params?.request;
559
+ const request = requestOverride??stream.params?.request;
485
560
  if (request?.op === 'turn') return this.#serveTurn(entry, request, stream, accept, refuse);
486
561
  if (request?.op === 'history') return this.#serveMailHistory(entry, request, stream, accept, refuse);
487
562
  const to = typeof request?.to === 'string' ? request.to : '*';
488
563
  const target = to.startsWith('sc:') ? `mail:${to}` : 'mail:*';
489
- if (!request || !['send', 'file', 'list'].includes(request.op)) return refuse('mail needs a send, file, list or turn request');
564
+ if (!request || !['send', 'file', 'list','status'].includes(request.op)) return refuse('mail needs a send, file, list, status or turn request');
490
565
  if (!holds(admitted, 'mail', target)) {
491
566
  this.log.append({ principal, cap: 'mail', target, outcome: 'refused' });
492
567
  return refuse(`no mail grant for ${target}`);
493
568
  }
569
+ if(request.op==='status'){
570
+ if(!to.startsWith('sc:'))return refuse('status requires one exact native address');
571
+ accept();
572
+ void this.#noticeTarget(to).then(({resume,...native})=>{
573
+ if(!stream.closed){stream.write(JSON.stringify({code:0,native}));stream.close('status');}
574
+ }).catch(()=>{if(!stream.closed){stream.write(JSON.stringify({code:5,native:{state:'unknown',reason:'native_liveness_unavailable',resumable:false}}));stream.close('status');}});
575
+ return;
576
+ }
577
+ if(request.resume_if_closed===true){
578
+ if(request.op!=='send'||!to.startsWith('sc:'))return refuse('thread resume requires an exact send target');
579
+ accept();
580
+ void this.#resumeNoticeTarget(to).then(()=>{
581
+ if(stream.closed)return;
582
+ if(!holds(entry.admitted,'mail',target)){stream.write(JSON.stringify({code:2,error:'access_revoked',text:'native mail grant expired before delivery'}));stream.close('refused');return;}
583
+ this.#serveMail(entry,stream,()=>{},refuseWith=>{if(!stream.closed){stream.write(JSON.stringify({code:2,text:refuseWith}));stream.close('refused');}},{...request,resume_if_closed:false});
584
+ }).catch(error=>{if(!stream.closed){stream.write(JSON.stringify({code:3,error:error.noticeCode??'session_resume_unavailable',text:error.message}));stream.close('refused');}});
585
+ return;
586
+ }
494
587
  accept();
495
588
  const child = spawn(this.supercodeBin, ['message', 'remote-deliver'], { stdio: ['pipe', 'pipe', 'pipe'], env: this.env });
496
589
  let out = '';
@@ -629,6 +722,8 @@ export class TeamsMachine extends MachineCore {
629
722
  // A long-lived tmux server can predate a consolidated installation.
630
723
  // New panes inherit this daemon's selected tools, not that stale PATH.
631
724
  env.PATH ??= this.env.PATH;
725
+ // Its CLI reads this machine's contexts and sockets, not whatever home that server started with.
726
+ if (this.env.SUPERCODE_HOME) env.SUPERCODE_HOME ??= this.env.SUPERCODE_HOME;
632
727
  const inherited = env.NODE_OPTIONS ?? this.env.NODE_OPTIONS ?? '';
633
728
  // Quoted: NODE_OPTIONS splits on spaces, and a home path may hold one (Windows user folders often do).
634
729
  env.NODE_OPTIONS = `${inherited} --require ${JSON.stringify(PORTHOLE_HOOK)}`.trim();
@@ -640,7 +735,8 @@ export class TeamsMachine extends MachineCore {
640
735
  // Native session hooks carry the launcher's identity across Codex's shared
641
736
  // app-server process boundary. Process ancestry alone cannot recover it.
642
737
  #nativeQuestionCommand(params, pane, by, nativeInput) {
643
- const args = params.arguments ?? [];
738
+ if (params.env?.SUPERCODE_BOARD_PROCESS_GUARD === '1' && process.platform === 'win32') throw new Error('board process guard is unavailable on Windows; no unguarded session was launched');
739
+ const args = boardGuardArguments(params.kind,params.arguments ?? [],{cwd:params.cwd,enabled:params.env?.SUPERCODE_BOARD_PROCESS_GUARD === '1'});
644
740
  const name = String(params.program ?? '').split(/[\\/]/).pop();
645
741
  if (process.platform === 'win32' || !((params.kind === 'codex' && name === 'codex') ||
646
742
  (params.kind === 'claude-code' && name === 'claude'))) return launchCommand(params.program, args);
@@ -1066,12 +1162,14 @@ export class TeamsMachine extends MachineCore {
1066
1162
  v: MACHINE_VERSION,
1067
1163
  id: this.id,
1068
1164
  hostname: hostname(),
1165
+ name: this.machineEvents?.machine ?? hostname(),
1069
1166
  socket: this.socketPath,
1070
1167
  server: this.link?.server ?? null,
1071
1168
  machineId: this.link?.machineId ?? null,
1072
1169
  node: { pid: process.pid, version: MACHINE_VERSION, protocol: NODE_PROTOCOL, floor: this.floor },
1073
1170
  egress: this.env.SUPERCODE_TEAMS_EGRESS ?? 'unrestricted',
1074
1171
  execution: executionCapabilities(this.env),
1172
+ boardProcessPolicy: process.platform === 'win32' ? null : { version: 1, harnesses: ['claude-code','codex'], boundary: 'native-PreToolUse' },
1075
1173
  runSpec: this.env.SUPERCODE_TEAMS_RUN_SPEC_SHA256 ?? null,
1076
1174
  at: Date.now(),
1077
1175
  };
@@ -1080,6 +1178,16 @@ export class TeamsMachine extends MachineCore {
1080
1178
 
1081
1179
  listPorts() { return listAnnouncedPorts(this.portsDir); }
1082
1180
 
1181
+ /** The attach clients on each pane's tmux session, by its label; null where panes are not tmux's (a ConPTY host). */
1182
+ #viewerCounts() {
1183
+ if (process.platform === 'win32') return null;
1184
+ const listed = spawnSync('tmux', [...(this.tmuxSocket ? ['-L', this.tmuxSocket] : []), 'list-clients', '-F', '#{session_name}'], { timeout: 3_000, encoding: 'utf8' });
1185
+ const counts = new Map();
1186
+ if (listed.status !== 0) return counts; // no server, or no clients: nothing shows any pane
1187
+ for (const name of listed.stdout.split('\n').filter(Boolean)) counts.set(name, (counts.get(name) ?? 0) + 1);
1188
+ return counts;
1189
+ }
1190
+
1083
1191
  /**
1084
1192
  * A terminal window on this machine's screen showing the pane, so the people at the machine see what runs on it:
1085
1193
  * in whichever capable terminal is installed (popup-terminals.mjs), running an attach client. The pane ending ends
@@ -1087,7 +1195,7 @@ export class TeamsMachine extends MachineCore {
1087
1195
  */
1088
1196
  #popupWindow(contextKey, session) {
1089
1197
  const terminal = popupTerminal(this.env);
1090
- if (!terminal) return;
1198
+ if (!terminal) return false;
1091
1199
  const attach = process.platform === 'win32'
1092
1200
  ? [process.execPath, fileURLToPath(new URL('./bin/fleet.mjs', import.meta.url)), 'attach', contextKey, '--mode', 'control']
1093
1201
  // Presented as `supercode open`'s own client is (crates/cli/src/open.rs `attach`): the terminal sets its own
@@ -1095,7 +1203,7 @@ export class TeamsMachine extends MachineCore {
1095
1203
  // screen override, so the window's own scrollback, wheel and selection work instead of the wheel arriving
1096
1204
  // in the pane as up/down arrows.
1097
1205
  : ['/usr/bin/env', 'TERM=screen-256color', spawnSync('which', ['tmux'], { encoding: 'utf8' }).stdout.trim() || 'tmux', ...(this.tmuxSocket ? ['-L', this.tmuxSocket] : []), '-T', 'RGB', 'attach-session', '-t', session.label ?? contextKey];
1098
- openPopup(terminal, attach, { ...this.env, TMUX: '' });
1206
+ return openPopup(terminal, attach, { ...this.env, TMUX: '' });
1099
1207
  }
1100
1208
 
1101
1209
  /**
@@ -1420,6 +1528,99 @@ export class TeamsMachine extends MachineCore {
1420
1528
  if (!this.machineEvents || params.team_id!==this.machineEvents.teamId) throw new Error('this connector is not serving that team');
1421
1529
  return method==='machine.events.events'?this.machineEvents.snapshot():this.machineEvents.subscribe(params.address,method.endsWith('.unsubscribe'));
1422
1530
  }
1531
+ case 'harness.v1.agents.bindSession': {
1532
+ if (!operator) throw Object.assign(new Error('binding an agent session requires operator authority'),{code:403});
1533
+ const {session,context}=params;
1534
+ if(typeof session!=='string'||!/^([a-z][a-z0-9-]*):(.+)$/.test(session)||typeof context!=='string')throw Object.assign(new Error('name a native session and its agent reader context'),{code:400});
1535
+ const selected=fromContext({supercodeHome:supercodeHome(this.env),name:context});
1536
+ const identity=await selected.client.team(selected.context.team_id).sessions.get(session).identity({signal:AbortSignal.timeout(15000)});
1537
+ if(!identity.data.agent||identity.data.agent.id!==selected.context.principal_id)throw Object.assign(new Error('session does not belong to the context agent'),{code:403});
1538
+ const file=join(this.home,'agent-session-contexts.json'),record=readJson(file,{version:1,sessions:{}});
1539
+ record.sessions[session]={context,agent:identity.data.agent.id,org:selected.context.team_id,assignments:params.assignments??[],intended:params.intended??null};
1540
+ const temporary=`${file}.${randomBytes(12).toString('hex')}.tmp`;
1541
+ writeFileSync(temporary,JSON.stringify(record)+'\n',{mode:0o600,flag:'wx'});renameSync(temporary,file);
1542
+ act('agents.bindSession',session);return record.sessions[session];
1543
+ }
1544
+ case 'harness.v1.agents.session': {
1545
+ if(!operator)throw Object.assign(new Error('local agent session lookup requires operator authority'),{code:403});
1546
+ if(typeof params.session!=='string')throw Object.assign(new Error('name a native session'),{code:400});
1547
+ return readJson(join(this.home,'agent-session-contexts.json'),{sessions:{}}).sessions[params.session]??null;
1548
+ }
1549
+ case 'harness.v1.agents.closureReceipt': {
1550
+ if(!operator)throw Object.assign(new Error('native closure receipts require operator authority'),{code:403});
1551
+ if(typeof params.closure!=='string'||!/^close_[0-9a-f]{32}$/.test(params.closure))throw Object.assign(new Error('name a retained closure obligation'),{code:400});
1552
+ const receipt=readJson(join(this.home,'agent-closure-receipts.json'),{closures:{}}).closures[params.closure];
1553
+ return {attempted:!!receipt,pending:!!this.agentClosuresRunning?.has(params.closure),result:receipt?.result??null};
1554
+ }
1555
+ case 'harness.v1.agents.closeSession': {
1556
+ if(!operator||!this.link)throw Object.assign(new Error('native agent closure requires the connected machine operator'),{code:403});
1557
+ if(typeof params.closure!=='string'||!/^close_[0-9a-f]{32}$/.test(params.closure))throw Object.assign(new Error('name a retained closure obligation'),{code:400});
1558
+ const {server,teamId,machineId,credential}=this.link;
1559
+ const response=await fetch(`${server}/api/v1/teams/${teamId}/machines/${machineId}/agent-closures/${params.closure}`,{headers:{Authorization:`Bearer ${credential}`},redirect:'manual',signal:AbortSignal.timeout(15000)});
1560
+ if(!response.ok)throw Object.assign(new Error(`agent closure authorization returned HTTP ${response.status}`),{code:403});
1561
+ const closure=(await response.json()).data;
1562
+ if(closure.state!=='claimed'||closure.machine_id!==machineId||closure.team_id!==teamId)throw Object.assign(new Error('the machine has no claimed closure on that session'),{code:403});
1563
+ const session=`${closure.harness}:${closure.native_id}`,binding=readJson(join(this.home,'agent-session-contexts.json'),{sessions:{}}).sessions[session];
1564
+ if(binding&&binding.agent!==closure.agent_id)throw Object.assign(new Error('native session identity changed'),{code:409});
1565
+ const file=join(this.home,'agent-closure-receipts.json'),receipts=readJson(file,{version:1,closures:{}});
1566
+ if(receipts.closures[closure.id])return receipts.closures[closure.id].result??{state:'unknown',reason:'native closure attempt has no retained receipt'};
1567
+ receipts.closures[closure.id]={attempted_at:Date.now()};writeJson(file,receipts);
1568
+ (this.agentClosuresRunning??=new Set()).add(closure.id);
1569
+ let result;
1570
+ try{
1571
+ const found=await this.#serveRequest('harness.v1.sessions.discover',{query:closure.native_id,harnesses:[closure.harness],limit:200});
1572
+ const descriptor=found.sessions?.find(row=>row.locator?.session_id===closure.native_id&&row.locator?.harness===closure.harness);
1573
+ if(descriptor?.live_endpoint?.startsWith('supercode-live://')){
1574
+ const runtime=await this.#serveRequest('harness.v1.runtimes.attach_existing',{harness:closure.harness,runtime_id:closure.native_id,cwd:descriptor.cwd,base_url:descriptor.live_endpoint});
1575
+ const ended=await this.#serveRequest('harness.v1.runtimes.close',{connection:runtime.connection});
1576
+ result=ended?.closed?{state:'closed',reason:'native runtime close receipt'}:{state:'unknown',reason:'runtime returned no close receipt'};
1577
+ }else{
1578
+ const machine=hostname().split('.')[0].toLowerCase().replace(/[^a-z0-9_-]/g,'-'),address=`sc:${machine}:${closure.harness}:${closure.native_id}`;
1579
+ const code=await new Promise((resolve,reject)=>{
1580
+ const child=spawn(this.supercodeBin,['close',address],{env:{...this.env,SUPERCODE_CALLER:address},stdio:'ignore'});
1581
+ const timer=setTimeout(()=>{child.kill('SIGTERM');resolve(null);},30000);
1582
+ child.once('error',error=>{clearTimeout(timer);reject(error);});child.once('close',status=>{clearTimeout(timer);resolve(status);});
1583
+ });
1584
+ result=code===0?{state:'closed',reason:'native session close door completed'}:code===null?{state:'unknown',reason:'native close did not complete within 30 seconds'}:{state:'failed',reason:`native close refused (exit ${code})`};
1585
+ }
1586
+ }catch(error){result={state:'failed',reason:String(error.message).slice(0,1024)};}
1587
+ // Other sessions may have finished while this native close was in flight.
1588
+ try {
1589
+ const retained=readJson(file,{version:1,closures:{}});
1590
+ retained.closures[closure.id]={...retained.closures[closure.id],result};writeJson(file,retained);
1591
+ act('agents.closeSession',session,result.state,result.reason);return result;
1592
+ }finally{this.agentClosuresRunning.delete(closure.id);}
1593
+ }
1594
+ case 'harness.v1.agents.context': {
1595
+ if (!operator) throw Object.assign(new Error('installing an agent reader context requires operator authority'), { code: 403 });
1596
+ const {origin,team,agent,credential}=params;
1597
+ 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});
1598
+ const client=new TeamsClient({server:origin,credential:credential.token});
1599
+ const me=await client.me({signal:AbortSignal.timeout(15000)});
1600
+ 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});
1601
+ const key=createHash('sha256').update(`${client.server}/${team}/${agent}`).digest('hex');
1602
+ const name=`agent-${key.slice(0,32)}`,ref=`cred_${key}`,contexts=new WorkspaceContexts(supercodeHome(this.env));
1603
+ contexts.writeCredential(ref,{token:credential.token,kind:'service',expires_at:credential.expires_at,server_id:me.server_id,origin:client.server});
1604
+ contexts.upsert(name,{origin:client.server,server_id:me.server_id,team_id:team,principal_id:agent,credential_ref:ref,label:agent});
1605
+ act('agents.context',agent);
1606
+ return {context:name,agent,org:team,expires_at:credential.expires_at};
1607
+ }
1608
+ case 'harness.v1.agents.mint': {
1609
+ if (!operator) throw Object.assign(new Error('minting local agent ids requires operator authority'), { code: 403 });
1610
+ if (typeof params.key !== 'string' || !params.key || params.key.length > 1024) throw Object.assign(new Error('agent mint requires a stable key'), { code: 400 });
1611
+ const file = join(this.home, 'agent-identities.json');
1612
+ const record = readJson(file, { version: 1, ids: {} });
1613
+ const key = createHash('sha256').update(params.key).digest('hex');
1614
+ if (!record.ids[key]) {
1615
+ record.ids[key] = { id: `agent_${randomBytes(16).toString('hex')}`, minted_at: Date.now() };
1616
+ const temporary = `${file}.${randomBytes(12).toString('hex')}.tmp`;
1617
+ writeFileSync(temporary, JSON.stringify(record)+'\n', { mode: 0o600, flag: 'wx' });
1618
+ renameSync(temporary,file);
1619
+ }
1620
+ act('agents.mint', key);
1621
+ return { ...record.ids[key], synced: false };
1622
+ }
1623
+ case 'harness.v1.machine.describe':
1423
1624
  case 'describe':
1424
1625
  return this.describe();
1425
1626
  case 'panes.present': {
@@ -1434,6 +1635,7 @@ export class TeamsMachine extends MachineCore {
1434
1635
  return { pane: session.contextKey, idleComposer: composer != null && !/\besc to (?:interrupt|cancel)\b/iu.test(screen) };
1435
1636
  }));
1436
1637
  }
1638
+ case 'harness.v1.panes.list':
1437
1639
  case 'panes.list': {
1438
1640
  // A participant lists the panes its grants let it observe; view on the whole
1439
1641
  // machine (or operator) lists them all.
@@ -1445,9 +1647,12 @@ export class TeamsMachine extends MachineCore {
1445
1647
  // hand here: a list that made a caller ask a second door "and is anything
1446
1648
  // wrong with these?" would be a list that knows and will not say.
1447
1649
  const rows = await this.attentionEvidence();
1650
+ // how many clients each pane has now (a popped window, a `supercode open`, a relay streaming it elsewhere): what a caller must
1651
+ // know before it opens another view of a pane on the same screen
1652
+ const viewers = this.#viewerCounts();
1448
1653
  // `title` is what a viewer should show: the override if one is set, else the tmux label.
1449
1654
  return rows.filter((row) => row.session.id && shown(row.session.contextKey)).map(({ session: s, attention, rank, notify, evidence }) => ({
1450
- pane: s.contextKey, dead: Boolean(s.dead), label: s.label, title: titles[`pane:${s.contextKey}`] ?? s.label, cwd: s.cwd, owned: s.owned, activeCommand: s.activeCommand, pid: s.rootPid ?? null, size: s.size, suspended: this.suspended.has(s.contextKey),
1655
+ pane: s.contextKey, dead: Boolean(s.dead), label: s.label, title: titles[`pane:${s.contextKey}`] ?? s.label, cwd: s.cwd, owned: s.owned, activeCommand: s.activeCommand, pid: s.rootPid ?? null, size: s.size, suspended: this.suspended.has(s.contextKey), viewers: viewers ? (viewers.get(s.label) ?? 0) : null,
1451
1656
  attention, rank, notify, evidence, input_cleanup: this.inputRepairs[s.contextKey] ? {pending:true,at:this.inputRepairs[s.contextKey].at,reason:this.inputRepairs[s.contextKey].reason} : null,
1452
1657
  }));
1453
1658
  }
@@ -1541,16 +1746,20 @@ export class TeamsMachine extends MachineCore {
1541
1746
  this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, by, ...executable, status, error: exit?.failed ? exit.how : undefined });
1542
1747
  return { key, pane: contextKey, reused: false, status, exit: exit?.how ?? null };
1543
1748
  }
1544
- if (params.popup !== false) this.#popupWindow(contextKey, session);
1749
+ // the window this machine's screen shows the pane in, when one opened: the caller is told, so it does not open a
1750
+ // second view of a pane that is already in front of the person at the machine
1751
+ const window = params.popup !== false && this.#popupWindow(contextKey, session);
1545
1752
  if (params.notify) this.#noticeWhenEnded({ key, pane: contextKey, notify: params.notify, label: params.name ?? params.kind ?? params.program });
1546
- return { key, pane: contextKey, reused: false, status: 'ready' };
1753
+ return { key, pane: contextKey, reused: false, status: 'ready', window };
1547
1754
  }
1755
+ case 'harness.v1.launches.get':
1548
1756
  case 'launches.get': {
1549
1757
  if (!operator) throw Object.assign(new Error('operators only'), { code: 403 });
1550
1758
  const found = this.launches.find(params.key);
1551
1759
  if (!found) return null;
1552
1760
  return this.#launchReceipt(found);
1553
1761
  }
1762
+ case 'harness.v1.launches.cancel':
1554
1763
  case 'launches.cancel': {
1555
1764
  if (!operator) throw Object.assign(new Error('operators only'), { code: 403 });
1556
1765
  if (typeof params.key !== 'string' || !params.key.trim()) throw Object.assign(new Error('launch cancellation requires a nonempty key'), { code: 400 });
@@ -1576,6 +1785,7 @@ export class TeamsMachine extends MachineCore {
1576
1785
  }
1577
1786
  return rows;
1578
1787
  }
1788
+ case 'harness.v1.panes.close':
1579
1789
  case 'panes.close': {
1580
1790
  need('control', `pane:${params.pane}`);
1581
1791
  for (const pending of this.pendingStartups.values()) if (pending.pane === params.pane) pending.abort.abort();
@@ -1799,13 +2009,13 @@ export class TeamsMachine extends MachineCore {
1799
2009
  act('title', params.target, 'ok', renamed);
1800
2010
  return { ok: true, target: params.target, title: params.title ?? null, window: renamed };
1801
2011
  }
1802
- case 'workspace.inspect': case 'workspace.register': case 'workspace.unregister': case 'workspace.list': case 'workspace.resolve': {
2012
+ case 'harness.v1.workspaces.inspect': case 'harness.v1.workspaces.register': case 'harness.v1.workspaces.unregister': case 'harness.v1.workspaces.list': case 'harness.v1.workspaces.resolve': {
1803
2013
  if (!operator) throw Object.assign(new Error('operators only'), { code: 403 });
1804
2014
  const registry = new Workspaces(this.home);
1805
- const result = method === 'workspace.inspect' ? inspectCheckout(params.path)
1806
- : method === 'workspace.register' ? registry.register(params.path, params.repository)
1807
- : method === 'workspace.unregister' ? registry.unregister(params.path)
1808
- : method === 'workspace.list' ? registry.list()
2015
+ const result = method === 'harness.v1.workspaces.inspect' ? inspectCheckout(params.path)
2016
+ : method === 'harness.v1.workspaces.register' ? registry.register(params.path, params.repository)
2017
+ : method === 'harness.v1.workspaces.unregister' ? registry.unregister(params.path)
2018
+ : method === 'harness.v1.workspaces.list' ? registry.list()
1809
2019
  : registry.resolve(params.repository, params.branch, { solo: params.solo, card: params.card, create: params.create });
1810
2020
  act('workspace', '*', 'ok', method);
1811
2021
  return result;
@@ -20,7 +20,8 @@ const continuing = seed.arguments.includes('resume') || seed.arguments.includes(
20
20
  const writableRoots = [];
21
21
  for (let index = 0; index < seed.arguments.length; index += 1) {
22
22
  const arg = seed.arguments[index];
23
- if (continuing && ['--dangerously-bypass-approvals-and-sandbox', '--yolo'].includes(arg)) {
23
+ if (arg === '--dangerously-bypass-hook-trust') { config.push(arg); }
24
+ else if (continuing && ['--dangerously-bypass-approvals-and-sandbox', '--yolo'].includes(arg)) {
24
25
  config.push('-c', 'approval_policy="never"', '-c', 'sandbox_mode="danger-full-access"');
25
26
  } else if (continuing && arg === '--full-auto') {
26
27
  config.push('-c', 'approval_policy="on-request"', '-c', 'sandbox_mode="workspace-write"');
@@ -41,7 +42,7 @@ for (let index = 0; index < seed.arguments.length; index += 1) {
41
42
  }
42
43
  }
43
44
  if(writableRoots.length)config.push('-c',`sandbox_workspace_write.writable_roots=${JSON.stringify(writableRoots)}`);
44
- const server = spawn(seed.program, ['app-server', '--listen', endpoint, ...config], { stdio: ['ignore', 'ignore', 'inherit'] });
45
+ const server = spawn(seed.program, [...config, 'app-server', '--listen', endpoint], { stdio: ['ignore', 'ignore', 'inherit'] });
45
46
  let tui;
46
47
  let ended = false;
47
48
  let serverError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.178",
3
+ "version": "0.3.0",
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",
@@ -10,6 +10,7 @@
10
10
  "./machine": "./machine.mjs",
11
11
  "./browser-attach": "./browser-attach.mjs",
12
12
  "./attention": "./attention.mjs",
13
+ "./event-client": "./event-client.mjs",
13
14
  "./bin": "./bin/teams.mjs",
14
15
  "./fleet": "./fleet.mjs",
15
16
  "./bin/fleet": "./bin/fleet.mjs",
@@ -62,7 +63,7 @@
62
63
  "@volter/supercode-harness-sdk": "^0.3.43",
63
64
  "@volter/supercode-terminal": "^0.2.54",
64
65
  "@volter/identity": "^0.4.1",
65
- "@volter/teams": "^0.1.58",
66
+ "@volter/teams": "0.2.0",
66
67
  "@xterm/addon-serialize": "^0.14.0",
67
68
  "@xterm/headless": "^6.0.0",
68
69
  "protobufjs": "8.8.0",
@@ -125,7 +125,7 @@ export const harnessRoutes = Object.freeze({ preAuth, authed, beforeIdempotency,
125
125
 
126
126
  async function canonicalSession(api,team,row){const seen=new Set();while(row?.canonical_id){if(seen.has(row.id))fail(409,'conflict','correlation cycle');seen.add(row.id);row=await api.store.one('SELECT * FROM sessions WHERE team_id=? AND id=?',[team,row.canonical_id]);}return row;}
127
127
  async function constituents(api,team,row){const root=await canonicalSession(api,team,row);return api.store.query('WITH RECURSIVE family(id) AS (SELECT ? UNION SELECT s.id FROM sessions s JOIN family f ON s.canonical_id=f.id WHERE s.team_id=?) SELECT s.* FROM sessions s JOIN family f ON f.id=s.id WHERE s.team_id=?',[root.id,team,team]);}
128
- async function canReadMerged(api,auth,row){for(const member of await constituents(api,auth.team_id,row))if(!canRead(auth,member,await grants(api,auth.team_id,member.id)))return false;return true;}
128
+ async function canReadMerged(api,auth,row){if(auth.principal_kind==='agent'){try{await api.store.authorizedSession({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref:row.id,now:api.clock.nowMs()});return true;}catch(error){if([401,403,404,410].includes(error.status))return false;throw error;}}for(const member of await constituents(api,auth.team_id,row))if(!canRead(auth,member,await grants(api,auth.team_id,member.id)))return false;return true;}
129
129
  const managerGuard=(auth,sessionId,now,{governance=false}={})=>({
130
130
  sql:`SELECT 1 ok FROM sessions s JOIN memberships m ON m.team_id=s.team_id AND m.principal_id=? WHERE s.team_id=? AND s.id=? AND s.deleted_at IS NULL AND m.active=1 AND m.role!='viewer' AND (?=1 AND m.role IN ('owner','admin') OR s.custodian_id=? OR EXISTS (SELECT 1 FROM session_grants g WHERE g.team_id=s.team_id AND g.session_id=s.id AND g.role='manager' AND (g.expires_at IS NULL OR g.expires_at>?) AND (g.subject_kind='principal' AND g.subject_id=? OR g.subject_kind='group' AND EXISTS (SELECT 1 FROM group_members gm WHERE gm.team_id=s.team_id AND gm.group_id=g.subject_id AND gm.principal_id=?))))`,
131
131
  params:[auth.principal_id,auth.team_id,sessionId,governance?1:0,auth.principal_id,now,auth.principal_id,auth.principal_id],
@@ -171,6 +171,7 @@ async function listSessions(api,res,auth,url,requestId,search){
171
171
  const rows=[...currentRows,...olderRows],currentById=new Map(currentRows.map(row=>[row.id,row]));
172
172
  const boundaryNow=api.clock.nowMs();
173
173
  if(!membership?.active||credential?.revoked_at!=null||(credential?.expires_at!=null&&credential.expires_at<=boundaryNow))fail(401,'authentication_required','authentication required');
174
+ if(cursorKey&&!cursor)fail(409,'resync_required','cursor expired, mismatched, or authorization changed');
174
175
  let tuples;
175
176
  if(cursor){
176
177
  if(cursor.kind!==(search?'search':'list')||cursor.expires_at<=boundaryNow||cursor.auth_epoch!==team.auth_epoch||canonicalJson(JSON.parse(cursor.query_json))!==canonicalJson(normalizedQuery))fail(409,'resync_required','cursor expired, mismatched, or authorization changed');
@@ -441,13 +442,17 @@ async function viewBindingRoute(api,req,res,auth,ref,requestId){
441
442
  const bindings=await list();
442
443
  // Native transport destinations are custody information, not an extra transcript-reader capability.
443
444
  if(session.custodian_id===auth.principal_id&&!auth.installation_id)for(const binding of bindings){
444
- const resolution=await sessionDeliveryResolution(api,auth.team_id,{sessionRef:session.id,...binding},true);
445
+ const resolution=await sessionDeliveryResolution(api,auth.team_id,{sessionRef:session.id,...binding},true,auth);
445
446
  binding.delivery=resolution.target&&resolution.target.harness!==session.harness
446
447
  ? {...resolution,state:'unavailable',reason:'harness_mismatch'} : resolution;
447
448
  }
448
449
  send(res,200,result(api,auth,{session_id:session.id,harness:session.harness,bindings}),requestId);return;
449
450
  }
450
- if(session.custodian_id!==auth.principal_id)fail(403,'forbidden',"only the session's owner binds it to a view");
451
+ // The owner binds, in person or through an app they installed and consented to `sessions.access` (the app acts for its
452
+ // installer); an app installed by anyone else binds nothing of theirs.
453
+ const installer=auth.installation_id&&auth.app_grants==null&&auth.app_actions?.includes('sessions.access')
454
+ ?(await api.store.one("SELECT installer_id FROM app_installations WHERE id=? AND team_id=? AND state='active'",[auth.installation_id,auth.team_id]))?.installer_id:null;
455
+ if(session.custodian_id!==auth.principal_id&&session.custodian_id!==installer)fail(403,'forbidden',"only the session's owner binds it to a view");
451
456
  const body=await parseJson(req);exact(body,['issuer','room','conversation']);
452
457
  if(typeof body.room!=='string'||!body.room||typeof body.conversation!=='string'||!body.conversation)fail(400,'invalid_request','room and conversation are required');
453
458
  if(!await installedIssuer(api,body.issuer,'delegation.sessions',auth.team_id))fail(400,'invalid_request','this issuer is not an installed session integration');
package/server/index.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  // The Node host: one process on a data directory (SQLite in a worker thread, the
2
2
  // bootstrap credential in a protected file beside it), serving HTTP and the
3
3
  // machine doors on one listener. The server itself is core.mjs's.
4
- import { identityOrganizationsFromEnv } from '@volter/teams/server/identity-orgs';
5
4
  import { createServer as createHttpServer } from 'node:http';
6
5
  import { mkdirSync, openSync, closeSync, writeFileSync, readFileSync, renameSync, chmodSync, unlinkSync, existsSync } from 'node:fs';
7
6
  import { join } from 'node:path';
@@ -21,7 +20,7 @@ export function createTeamsServer({dataDir,listen={host:'127.0.0.1',port:7331},p
21
20
  const loopback=['127.0.0.1','::1','localhost'].includes(listen.host??'127.0.0.1');
22
21
  publicOrigin=normalizePublicOrigin(publicOrigin);
23
22
  if(!loopback&&!publicOrigin?.startsWith('https:'))throw new TypeError('non-loopback Teams servers require an HTTPS publicOrigin');
24
- const api=createTeamsApi({clock,ids,publicOrigin,identityOrganizations:identityOrganizationsFromEnv(process.env),uploadLimitBytes});
23
+ const api=createTeamsApi({clock,ids,publicOrigin,uploadLimitBytes});
25
24
  // The harness's own routes on this server (harness-routes.mjs); the handler owns the neutral ones.
26
25
  api.attach({harnessRoutes});
27
26
  const events=api.events;
@@ -7,6 +7,7 @@ import { ingestOtlpBatch, correlateSessions } from './arc2-transactions.mjs';
7
7
  import { claimEffect, getEffect, listEffects, reportEffect } from './effects-transactions.mjs';
8
8
  import { applyCaptureMessages } from './capture-transactions.mjs';
9
9
  import { MIGRATIONS, MESSAGE_AT } from '@volter/teams/server/migrations';
10
+ import { AGENT_ACTIVE, AGENT_DESCENDANTS } from '@volter/teams/server/agent-authority';
10
11
  import { APP_LINEAGE_REVOKED, appGrantAllows } from '@volter/teams/server/auth';
11
12
 
12
13
  /** Bring a database to the newest schema. `version()`/`setVersion(n)` name where the host records it. */
@@ -27,7 +28,7 @@ export function createStoreHandlers({ all, get, run, tx }) {
27
28
  const {teamId,principalId,credentialId,ref,now,bound}=args;
28
29
  // `bound`: a workplace's session-view token ({issuer, room, conversation, expiresAt}), which reads only a
29
30
  // session its owner bound to that Room and Conversation; it holds no credential and is no member.
30
- let membership=null,appFence=null,grantSource=null;
31
+ let membership=null,appFence=null,grantSource=null,agentGrantSource=null,agentReader=false;
31
32
  if((args.delegationDepth??0)>16)domainError(401,'authentication_required','delegation lineage is too deep');
32
33
  if(bound){
33
34
  if(!get(`SELECT 1 FROM app_installations i JOIN apps a ON a.id=i.app_id JOIN memberships m ON m.team_id=i.team_id AND m.principal_id=i.principal_id JOIN memberships installer ON installer.team_id=i.team_id AND installer.principal_id=i.installer_id WHERE i.team_id=? AND a.issuer=? AND i.id=? AND i.created_at=? AND i.state='active' AND a.state='active' AND m.active=1 AND installer.active=1 AND installer.role IN ('owner','admin') AND EXISTS (SELECT 1 FROM json_each(i.actions_json) WHERE value='delegation.sessions')`,[teamId,bound.issuer,bound.installationId,bound.installedAt]))domainError(401,'authentication_required','integration was uninstalled');
@@ -45,6 +46,12 @@ export function createStoreHandlers({ all, get, run, tx }) {
45
46
  if(appFence!==null)grantSource=get('SELECT id,principal_id FROM credentials WHERE id=?',[app.subject_credential_id??app.parent_id]);
46
47
  }
47
48
  if(!credential||!membership)domainError(401,'authentication_required','authentication required');
49
+ const principal=get('SELECT kind FROM principals WHERE id=?',[principalId]);
50
+ if(principal?.kind==='agent'){
51
+ agentGrantSource=get('SELECT c.id,c.principal_id FROM agent_credentials d JOIN credentials c ON c.id=d.parent_credential_id WHERE d.credential_id=?',[credentialId]);
52
+ if(!get(AGENT_ACTIVE,[teamId,principalId])||get(APP_LINEAGE_REVOKED,[credentialId,now]))domainError(401,'authentication_required','agent delegation revoked');
53
+ agentReader=true;
54
+ }
48
55
  }
49
56
  if(typeof ref!=='string'||ref.length<8)domainError(404,'not_found','not found');
50
57
  const groups=all('SELECT group_id FROM group_members WHERE team_id=? AND principal_id=?',[teamId,principalId]).map(row=>row.group_id);
@@ -69,7 +76,13 @@ export function createStoreHandlers({ all, get, run, tx }) {
69
76
  if(!grantSource)return false;
70
77
  try{authorizedSession({teamId,principalId:grantSource.principal_id,credentialId:grantSource.id,ref:session.id,now,delegationDepth:(args.delegationDepth??0)+1,projectionDepth:1});}catch{return false;}
71
78
  }
72
- if(session.visibility==='team'||session.custodian_id===principalId)return true;
79
+ if(agentGrantSource){
80
+ try{authorizedSession({teamId,principalId:agentGrantSource.principal_id,credentialId:agentGrantSource.id,ref:session.id,now,delegationDepth:(args.delegationDepth??0)+1,projectionDepth:1});}catch{return false;}
81
+ }
82
+ if(agentReader&&get(`WITH RECURSIVE owners(id) AS (${AGENT_DESCENDANTS}), family(id) AS (SELECT s.session_id FROM agent_sessions s JOIN owners o ON s.agent_id=o.id WHERE s.team_id=? UNION SELECT s.id FROM sessions s JOIN family f ON s.canonical_id=f.id WHERE s.team_id=?) SELECT 1 FROM family WHERE id=?`,[teamId,principalId,teamId,teamId,session.id]))return true;
83
+ // An agent's standing delegation covers its own family and descendants. Team visibility
84
+ // alone does not open a parent's or sibling's transcript; those require an explicit grant.
85
+ if(!agentReader&&(session.visibility==='team'||session.custodian_id===principalId))return true;
73
86
  const grants=all('SELECT subject_kind,subject_id,expires_at FROM session_grants WHERE team_id=? AND session_id=?',[teamId,session.id]);
74
87
  return grants.some(grant=>(grant.subject_kind==='principal'&&grant.subject_id===principalId||grant.subject_kind==='group'&&groups.includes(grant.subject_id))&&(grant.expires_at==null||grant.expires_at>now));
75
88
  });
@@ -0,0 +1,39 @@
1
+ // A Room reply may resume its existing thread. Configuration is read through the harness's own door.
2
+ import { isAbsolute } from 'node:path';
3
+
4
+ export function codexNoticeResumeArguments(config){
5
+ if(config?.harness!=='codex'||typeof config.cwd!=='string'||!isAbsolute(config.cwd))return null;
6
+ if(!['untrusted','on-failure','on-request','never'].includes(config.approval_policy))return null;
7
+ const policy=config.sandbox_policy;
8
+ if(!policy||typeof policy!=='object'||Array.isArray(policy)||!['read-only','workspace-write','danger-full-access'].includes(policy.type))return null;
9
+ const keys=['type',...(policy.type==='workspace-write'?['writable_roots','network_access','exclude_tmpdir_env_var','exclude_slash_tmp']:[])];
10
+ if(Object.keys(policy).some(key=>!keys.includes(key)))return null;
11
+ if(policy.writable_roots!==undefined&&(!Array.isArray(policy.writable_roots)||!policy.writable_roots.every(root=>typeof root==='string'&&isAbsolute(root))))return null;
12
+ for(const key of ['network_access','exclude_tmpdir_env_var','exclude_slash_tmp'])if(policy[key]!==undefined&&typeof policy[key]!=='boolean')return null;
13
+ const args=['--ask-for-approval',config.approval_policy,'--sandbox',policy.type];
14
+ if(policy.type==='workspace-write'){
15
+ args.push('-c',`sandbox_workspace_write.writable_roots=${JSON.stringify(policy.writable_roots??[])}`);
16
+ for(const key of ['network_access','exclude_tmpdir_env_var','exclude_slash_tmp'])args.push('-c',`sandbox_workspace_write.${key}=${policy[key]??(key!=='network_access')}`);
17
+ }
18
+ if(typeof config.model==='string'&&config.model)args.push('--model',config.model);
19
+ return args;
20
+ }
21
+
22
+ export async function sessionNoticeTarget(address,{machine,mail,discover,recordedConfig}){
23
+ const match=/^sc:([^:]+):([^:]+):([^:\s]+)$/.exec(address??'');
24
+ if(!match||match[1]!==machine)return {state:'unavailable',reason:'native_target_not_local',resumable:false};
25
+ const [, ,harness,id]=match;
26
+ const live=await mail({op:'list'});
27
+ if(live.code!==0||!Array.isArray(live.sessions))throw new Error('native liveness door returned no session catalogue');
28
+ if(live.sessions.some(row=>row.address===address))return {state:'running',reason:null,resumable:false};
29
+ const page=await discover({query:id,harnesses:[harness],limit:200});
30
+ const row=page.sessions?.find(value=>value.locator?.session_id===id&&value.locator?.harness===harness);
31
+ if(!row)return {state:'unavailable',reason:'native_session_not_found',resumable:false};
32
+ if(harness!=='codex')return {state:'stopped',reason:'native_resume_not_supported',resumable:false};
33
+ let config;
34
+ try{config=await recordedConfig(row.locator);}catch{return {state:'stopped',reason:'native_resume_profile_unavailable',resumable:false};}
35
+ const args=codexNoticeResumeArguments(config);
36
+ if(config.session_id!==id)return {state:'stopped',reason:'native_resume_identity_unrecorded',resumable:false};
37
+ return {state:'stopped',reason:args?null:'native_resume_profile_unrecorded',resumable:!!args,
38
+ ...(args?{resume:{cwd:config.cwd,args}}:{})};
39
+ }
package/worker/index.mjs CHANGED
@@ -6,9 +6,6 @@
6
6
  // PUBLIC_ORIGIN the https origin the Worker is served at
7
7
  // INIT_TEAM the first team's name (used once, at bootstrap)
8
8
  // Identity and delegation issuers are registered apps installed by team admins through the public doors.
9
- // VOLTER_ORG_CLIENT_ID, VOLTER_ORG_CLIENT_SECRET, VOLTER_TEAM_ORGS
10
- // optional: this server's product credential at the organization's identity issuer, and the teams whose
11
- // organization there admits its members with no invitation (`team=slug,…`)
12
9
  // SUPERCODE_TEAMS_BOOTSTRAP_TOKEN secret: the owner's first credential, minted with
13
10
  // `supercode teams server token`
14
11
  import { DurableObject } from 'cloudflare:workers';
@@ -18,7 +15,6 @@ import { createHttpHandler, authenticate } from '@volter/teams/server/http';
18
15
  import { harnessRoutes } from '../server/harness-routes.mjs';
19
16
  import { authorizeMachineUpgrade, acceptMachineSocket, LINK_PROTOCOL, CHANNEL_PROTOCOL } from '@volter/teams/server/machine-link';
20
17
  import { setOtlpSchemaSources } from '../server/otlp/transport.mjs';
21
- import { identityOrganizationsFromEnv } from '@volter/teams/server/identity-orgs';
22
18
  import { setViewerSources } from '@volter/teams/server/viewer';
23
19
  import { serveFetch } from './fetch-adapter.mjs';
24
20
  import { webSocketDuplex } from './websocket-duplex.mjs';
@@ -80,7 +76,7 @@ export class TeamsServer extends DurableObject {
80
76
  console.log(`storage: database_bytes=${ctx.storage.sql.databaseSize}`);
81
77
  const db = durableSql(ctx.storage);
82
78
  migrate({ exec: db.exec, tx: db.tx, version: () => ctx.storage.kv.get('schema_version') ?? 0, setVersion: (n) => ctx.storage.kv.put('schema_version', n) });
83
- this.api = createTeamsApi({ publicOrigin: normalizePublicOrigin(env.PUBLIC_ORIGIN), appRootSecret: env.SUPERCODE_TEAMS_BOOTSTRAP_TOKEN || null, identityOrganizations: identityOrganizationsFromEnv(env) });
79
+ this.api = createTeamsApi({ publicOrigin: normalizePublicOrigin(env.PUBLIC_ORIGIN), appRootSecret: env.SUPERCODE_TEAMS_BOOTSTRAP_TOKEN || null });
84
80
  this.api.storageUsage = () => {
85
81
  const sql = ctx.storage.sql;
86
82
  let freeBytes = null;