@volter/supercode-teams 0.2.20
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 +119 -0
- package/attention.mjs +191 -0
- package/bin/fleet.mjs +151 -0
- package/bin/teams.mjs +291 -0
- package/bin/workspace/args.mjs +66 -0
- package/bin/workspace/files.mjs +110 -0
- package/bin/workspace/lifecycle.mjs +113 -0
- package/bin/workspace/volter-device.mjs +42 -0
- package/bin/workspace/volter-login.mjs +185 -0
- package/bin/workspace.mjs +726 -0
- package/browser-attach.mjs +90 -0
- package/client.mjs +212 -0
- package/connector/effects.mjs +95 -0
- package/connector/index.d.ts +69 -0
- package/connector/index.mjs +174 -0
- package/connector/native-continuation.d.ts +8 -0
- package/connector/native-continuation.mjs +50 -0
- package/connector/native-effects.mjs +164 -0
- package/context.mjs +34 -0
- package/contracts/adapter-map.json +16 -0
- package/contracts/index.d.ts +299 -0
- package/contracts/index.mjs +37 -0
- package/execution.mjs +206 -0
- package/fleet.mjs +78 -0
- package/index.mjs +12 -0
- package/machine-door.mjs +36 -0
- package/machine.mjs +1398 -0
- package/package.json +80 -0
- package/porthole-hook.cjs +137 -0
- package/remote/job-exec.py +146 -0
- package/remote/wire.py +23 -0
- package/server/arc2-transactions.mjs +125 -0
- package/server/attachment-relay.mjs +149 -0
- package/server/catalog.mjs +7 -0
- package/server/continuations.mjs +56 -0
- package/server/correlation.d.ts +4 -0
- package/server/correlation.mjs +17 -0
- package/server/effects-transactions.mjs +119 -0
- package/server/effects.mjs +97 -0
- package/server/event-routes.mjs +208 -0
- package/server/exports.mjs +46 -0
- package/server/harness-routes.mjs +248 -0
- package/server/index.d.ts +31 -0
- package/server/index.mjs +52 -0
- package/server/ingest.mjs +24 -0
- package/server/node-upgrade.mjs +39 -0
- package/server/otlp/collector-config.d.ts +2 -0
- package/server/otlp/collector-config.mjs +26 -0
- package/server/otlp/decoders/index.d.ts +63 -0
- package/server/otlp/decoders/index.mjs +311 -0
- package/server/otlp/index.d.ts +4 -0
- package/server/otlp/index.mjs +4 -0
- package/server/otlp/projection.d.ts +5 -0
- package/server/otlp/projection.mjs +17 -0
- package/server/otlp/receiver.d.ts +3 -0
- package/server/otlp/receiver.mjs +36 -0
- package/server/otlp/schema/google/protobuf/any.proto +162 -0
- package/server/otlp/schema/google/rpc/status.proto +48 -0
- package/server/otlp/schema/manifest.json +22 -0
- package/server/otlp/schema/opentelemetry/proto/collector/logs/v1/logs_service.proto +77 -0
- package/server/otlp/schema/opentelemetry/proto/collector/metrics/v1/metrics_service.proto +77 -0
- package/server/otlp/schema/opentelemetry/proto/collector/trace/v1/trace_service.proto +77 -0
- package/server/otlp/schema/opentelemetry/proto/common/v1/common.proto +154 -0
- package/server/otlp/schema/opentelemetry/proto/logs/v1/logs.proto +226 -0
- package/server/otlp/schema/opentelemetry/proto/metrics/v1/metrics.proto +735 -0
- package/server/otlp/schema/opentelemetry/proto/resource/v1/resource.proto +45 -0
- package/server/otlp/schema/opentelemetry/proto/trace/v1/trace.proto +359 -0
- package/server/otlp/transport.d.ts +10 -0
- package/server/otlp/transport.mjs +424 -0
- package/server/otlp/wire-codec.mjs +153 -0
- package/server/store-core.mjs +139 -0
- package/server/store-worker.mjs +23 -0
- package/worker/fetch-adapter.mjs +49 -0
- package/worker/index.mjs +127 -0
- package/worker/websocket-duplex.mjs +29 -0
- package/worker/wrangler.toml +38 -0
- package/workspace-client.d.ts +75 -0
- package/workspace-client.mjs +171 -0
- package/workspace-move.mjs +38 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync, existsSync, chmodSync, realpathSync, statSync, openSync, closeSync, fsyncSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
4
|
+
import { canonicalJson } from '../contracts/index.mjs';
|
|
5
|
+
import { EffectConsumer } from './effects.mjs';
|
|
6
|
+
import { NativeEffects } from './native-effects.mjs';
|
|
7
|
+
|
|
8
|
+
const hash=value=>createHash('sha256').update(value).digest('hex');
|
|
9
|
+
const recordedAt=entry=>{if(Number.isSafeInteger(entry.recorded_at))return entry.recorded_at;if(Number.isSafeInteger(entry.timestamp))return entry.timestamp;const value=entry.metadata?.timestamp??entry.value?.metadata?.timestamp??entry.timestamp;if(typeof value!=='string')return null;const parsed=Date.parse(value);return Number.isSafeInteger(parsed)?parsed:null;};
|
|
10
|
+
const serverOrigin=value=>{const url=new URL(value),loopback=url.hostname==='localhost'||url.hostname==='::1'||url.hostname==='[::1]'||/^127(?:\.\d{1,3}){3}$/.test(url.hostname);if(!['http:','https:'].includes(url.protocol)||url.username||url.password||url.search||url.hash||(url.pathname!==''&&url.pathname!=='/')||(url.protocol==='http:'&&!loopback))throw new TypeError('server must be an HTTPS origin (HTTP is loopback-only)');return url.origin;};
|
|
11
|
+
function atomic(path, value) {
|
|
12
|
+
const temporary = `${path}.${process.pid}.${randomBytes(12).toString('hex')}.tmp`;
|
|
13
|
+
const fd = openSync(temporary, 'wx', 0o600);
|
|
14
|
+
try { writeFileSync(fd, JSON.stringify(value, null, 2) + '\n'); fsyncSync(fd); }
|
|
15
|
+
catch (error) { try { unlinkSync(temporary); } catch {} throw error; }
|
|
16
|
+
finally { closeSync(fd); }
|
|
17
|
+
renameSync(temporary, path);
|
|
18
|
+
chmodSync(path, 0o600);
|
|
19
|
+
// Node cannot open a directory descriptor on Windows. The file is flushed on
|
|
20
|
+
// every platform; server-side claimed state also prevents replay after restart.
|
|
21
|
+
if (process.platform !== 'win32') {
|
|
22
|
+
const directory = openSync(dirname(path), 'r');
|
|
23
|
+
try { fsyncSync(directory); } finally { closeSync(directory); }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class TeamsConnector {
|
|
27
|
+
constructor({server,teamId,machineId,sourceId,credential,stateDir,core,hostKeyId=null,effectExecutor=null,clock={nowMs:()=>Date.now()},fetch:fetchImpl=globalThis.fetch}){Object.assign(this,{server:serverOrigin(server),teamId,machineId,sourceId,credential,stateDir,core,hostKeyId,effectExecutor,clock,fetch:fetchImpl});mkdirSync(stateDir,{recursive:true,mode:0o700});chmodSync(stateDir,0o700);this.statePath=join(stateDir,'connector.json');this.state=existsSync(this.statePath)?JSON.parse(readFileSync(this.statePath,'utf8')):{version:1,sequence:0,origins:{},outbox:[]};this.state.effects??={};this.state.incarnation_id=`inc_${randomBytes(16).toString('hex')}`;this.abort=new AbortController();this.work=Promise.resolve();this.nativeEffects=new NativeEffects(this);this.effects=new EffectConsumer(this,effectExecutor?{prepare:async effect=>({execute:()=>effectExecutor(effect,{core:this.core,signal:this.abort.signal})})}:this.nativeEffects);this.save();}
|
|
28
|
+
save(){atomic(this.statePath,this.state);} async request(method,path,body){const json=body===undefined?undefined:JSON.stringify(body),mutation=['POST','PATCH','DELETE'].includes(method);const response=await this.fetch(this.server+`/api/v1/teams/${this.teamId}`+path,{method,redirect:'manual',headers:{Authorization:`Bearer ${this.credential}`,'Content-Type':'application/json',...(mutation?{'Idempotency-Key':hash(`${method}\n${path}\n${json??''}`)}:{})},body:json,signal:this.abort.signal});if(response.type==='opaqueredirect'||response.status>=300&&response.status<400)throw Object.assign(new Error('cross-origin redirects are refused'),{code:'invalid_request',status:400});const value=await response.json();if(!response.ok)throw Object.assign(new Error(value.error?.message??`HTTP ${response.status}`),value.error,{status:response.status});return value.data;}
|
|
29
|
+
async configuration(){const value=await this.request('GET',`/machines/${this.machineId}/configuration`);this.configurationValue=value;return value;}
|
|
30
|
+
async start({bindings=()=>[],roots=()=>[]}={}) {
|
|
31
|
+
await this.configuration();
|
|
32
|
+
const heartbeat = async () => {
|
|
33
|
+
this.state.sequence += 1;
|
|
34
|
+
await this.request('POST', `/machines/${this.machineId}/heartbeat`, {
|
|
35
|
+
incarnation_id: this.state.incarnation_id, sequence: this.state.sequence,
|
|
36
|
+
bindings: [...this.nativeEffects.published(), ...await bindings()],
|
|
37
|
+
roots: this.localRoots.map(({id,machine_id,name,harnesses})=>({id,machine_id,name,harnesses})),
|
|
38
|
+
...(this.hostKeyId ? { host_key_id: this.hostKeyId } : {}),
|
|
39
|
+
});
|
|
40
|
+
this.save();
|
|
41
|
+
};
|
|
42
|
+
const beat = async () => {
|
|
43
|
+
this.localRoots = await roots();
|
|
44
|
+
await heartbeat();
|
|
45
|
+
const configuration = await this.configuration();
|
|
46
|
+
await this.syncConfiguredSessions(configuration, this.localRoots);
|
|
47
|
+
await heartbeat();
|
|
48
|
+
await this.pollEffects();
|
|
49
|
+
};
|
|
50
|
+
await beat();
|
|
51
|
+
let running = false;
|
|
52
|
+
this.timer = setInterval(() => {
|
|
53
|
+
if (running || this.abort.signal.aborted) return;
|
|
54
|
+
running = true;
|
|
55
|
+
void beat().catch(error => { this.lastError = error; }).finally(() => { running = false; });
|
|
56
|
+
}, 15000);
|
|
57
|
+
return this.configurationValue;
|
|
58
|
+
}
|
|
59
|
+
async effectFetch(method, path, {body, contentType='application/json', raw=false, freshKey=false}={}) {
|
|
60
|
+
const response = await this.fetch(this.server + `/api/v1/teams/${this.teamId}` + path, {
|
|
61
|
+
method, redirect: 'manual', headers: {
|
|
62
|
+
Authorization: `Bearer ${this.credential}`, Accept: raw ? 'application/octet-stream' : 'application/json',
|
|
63
|
+
'Content-Type': contentType, 'X-Teams-Incarnation': this.state.incarnation_id,
|
|
64
|
+
...(['POST','DELETE'].includes(method) && contentType === 'application/json' ? {'Idempotency-Key': freshKey ? randomBytes(24).toString('hex') : hash(`${method}\n${path}\n${body??''}`)} : {}),
|
|
65
|
+
}, body, ...(body instanceof ReadableStream ? {duplex:'half'} : {}), signal: this.abort.signal,
|
|
66
|
+
});
|
|
67
|
+
if (response.type === 'opaqueredirect' || response.status >= 300 && response.status < 400) throw Object.assign(new Error('redirects are refused'), {code:'invalid_request',status:400});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
const value = await response.json();
|
|
70
|
+
throw Object.assign(new Error(value.error?.message ?? `HTTP ${response.status}`), value.error, {status:response.status});
|
|
71
|
+
}
|
|
72
|
+
if (raw) return response;
|
|
73
|
+
if (response.status === 204) return null;
|
|
74
|
+
const value = await response.json();
|
|
75
|
+
return value.data ?? value;
|
|
76
|
+
}
|
|
77
|
+
effectRequest(method, path, body, options={}) { return this.effectFetch(method,path,{...options,body:body===undefined?undefined:JSON.stringify(body)}); }
|
|
78
|
+
pollEffects() { return this.effects.poll(); }
|
|
79
|
+
async syncConfiguredSessions(configuration,roots){
|
|
80
|
+
if(typeof this.core.discover!=='function')return;
|
|
81
|
+
this.followers??=new Map();
|
|
82
|
+
this.nativeEffects.beginDiscovery();
|
|
83
|
+
const activeRules=new Map(configuration.rules.filter(item=>item.state==='active').map(rule=>[rule.id,rule]));
|
|
84
|
+
for(const [key,follower] of this.followers){const rule=activeRules.get(follower.ruleId);if(!rule||rule.revision!==follower.ruleRevision){follower.controller.abort();this.followers.delete(key);}}
|
|
85
|
+
for(const rule of activeRules.values()){
|
|
86
|
+
const root=roots.find(item=>item.id===rule.selector.root_id);
|
|
87
|
+
if(!root)continue;
|
|
88
|
+
const descriptors=[];
|
|
89
|
+
let cursor;
|
|
90
|
+
do {
|
|
91
|
+
const page=await this.core.discover({workspace:root.path??root.workspace,harnesses:rule.selector.harnesses,homes:root.homes,limit:2048,...(cursor?{cursor}:{})});
|
|
92
|
+
descriptors.push(...(page.sessions??page.entries??[]));
|
|
93
|
+
cursor=page.next_cursor??page.nextCursor??null;
|
|
94
|
+
} while(cursor);
|
|
95
|
+
for(const descriptor of descriptors){
|
|
96
|
+
const locator=descriptor.locator,nativeSessionId=locator?.session_id,harness=locator?.harness;
|
|
97
|
+
if(!locator||!nativeSessionId||!harness)continue;
|
|
98
|
+
const options={locator,descriptor,harness,nativeSessionId,ruleId:rule.id,ruleRevision:rule.revision,backfill:rule.backfill,rootPath:root.path??root.workspace};
|
|
99
|
+
await this.capture(options);
|
|
100
|
+
try { await this.nativeEffects.discover(descriptor, rule.id); } catch (error) { this.lastError = error; }
|
|
101
|
+
const key=`${rule.id}:${harness}:${nativeSessionId}`;
|
|
102
|
+
if(this.followers.has(key)||typeof this.core.session(locator).follow!=='function')continue;
|
|
103
|
+
const controller=new AbortController(),stop=()=>controller.abort(this.abort.signal.reason);this.abort.signal.addEventListener('abort',stop,{once:true});
|
|
104
|
+
const task=(async()=>{for await(const _event of this.core.session(locator).follow({signal:controller.signal})){if(controller.signal.aborted)break;await this.capture(options);}})().catch(error=>{if(!controller.signal.aborted)this.lastError=error;}).finally(()=>{this.abort.signal.removeEventListener('abort',stop);this.followers.delete(key);});
|
|
105
|
+
this.followers.set(key,{ruleId:rule.id,ruleRevision:rule.revision,controller,task});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
this.nativeEffects.endDiscovery();
|
|
109
|
+
}
|
|
110
|
+
capture(options) {
|
|
111
|
+
const work=this.work.then(()=>this.captureSerial(options));
|
|
112
|
+
this.work=work.catch(()=>{});
|
|
113
|
+
return work;
|
|
114
|
+
}
|
|
115
|
+
async captureSerial({locator,descriptor=null,harness,nativeSessionId,visibility='restricted',ruleId=null,ruleRevision=null,coverage={completeness:'complete',reasons:[],missing:[]},backfill='all',rootPath=null}) {
|
|
116
|
+
if(this.state.outbox.length)await this.flushSerial();
|
|
117
|
+
const configuration=await this.configuration(),rule=configuration.rules.find(item=>item.id===ruleId&&item.revision===ruleRevision&&item.state==='active');if(!rule)throw Object.assign(new Error('sync rule is absent, inactive, or stale'),{code:'forbidden'});visibility=rule.visibility;
|
|
118
|
+
const artifactSource=locator?.storage?.kind==='file'&&existsSync(locator.storage.path)?realpathSync(locator.storage.path):null;
|
|
119
|
+
const artifactBefore=artifactSource?statSync(artifactSource):null;
|
|
120
|
+
const loaded=await this.core.session(locator).load(),configuredBackfill=rule.backfill??backfill??'all',backfillPolicy=typeof configuredBackfill==='string'?{mode:configuredBackfill,since:null}:configuredBackfill;
|
|
121
|
+
const key=`${harness}:${nativeSessionId}`,previous=this.state.origins[key]??null;
|
|
122
|
+
let messages=(loaded.messages??loaded.turns??[]).map((entry,index)=>{const value=entry.value??Object.fromEntries(Object.entries(entry).filter(([key])=>!['id','native_key','recorded_at','timestamp'].includes(key)));return{native_key:entry.id??entry.native_key??String(index),recorded_at:recordedAt(entry),value};});
|
|
123
|
+
if(previous?.last_rule_revision===ruleRevision&&previous?.backfill_mode===backfillPolicy.mode&&previous?.observed_keys?.length===messages.length&&messages.every((message,index)=>message.native_key===previous.observed_keys[index]&&hash(canonicalJson(message.value))===previous.observed_digests?.[index]))return null;
|
|
124
|
+
let effectivePrevious=previous;
|
|
125
|
+
if(!previous&&backfillPolicy.mode==='none'){this.state.origins[key]={generation:1,sequence:0,digest:null,baseline:messages.map(x=>x.native_key),baseline_digests:messages.map(x=>hash(canonicalJson(x.value))),observed_keys:messages.map(x=>x.native_key),observed_digests:messages.map(x=>hash(canonicalJson(x.value))),last_rule_revision:ruleRevision,backfill_mode:backfillPolicy.mode};this.save();return null;}
|
|
126
|
+
if(!previous&&backfillPolicy.mode==='since'){
|
|
127
|
+
if(!Number.isSafeInteger(backfillPolicy.since))throw Object.assign(new Error('since backfill requires an integer timestamp'),{code:'invalid_request'});
|
|
128
|
+
const excluded=messages.filter(message=>message.recorded_at==null||message.recorded_at<backfillPolicy.since);
|
|
129
|
+
effectivePrevious={generation:1,sequence:0,digest:null,baseline:excluded.map(message=>message.native_key),baseline_digests:excluded.map(message=>hash(canonicalJson(message.value)))};
|
|
130
|
+
this.state.origins[key]=effectivePrevious;
|
|
131
|
+
}
|
|
132
|
+
if(previous?.observed_keys?.every(x=>/^\d+$/.test(x))){
|
|
133
|
+
const prefix=messages.slice(0,previous.observed_keys.length);
|
|
134
|
+
const rewritten=prefix.length!==previous.observed_keys.length||prefix.some((message,index)=>message.native_key!==previous.observed_keys[index]||hash(canonicalJson(message.value))!==previous.observed_digests?.[index]);
|
|
135
|
+
if(rewritten)throw Object.assign(new Error('index-key source rewrote its baseline; explicit backfill is required'),{code:'resync_required'});
|
|
136
|
+
}
|
|
137
|
+
const clearBaseline=backfillPolicy.mode==='all';
|
|
138
|
+
if(effectivePrevious?.baseline&&!clearBaseline)messages=messages.filter(x=>!effectivePrevious.baseline.includes(x.native_key));
|
|
139
|
+
if(!messages.length)return null;
|
|
140
|
+
const captureId=`cap_${randomBytes(16).toString('hex')}`,generation=effectivePrevious?.generation??1,sequence=(effectivePrevious?.sequence??0)+1;
|
|
141
|
+
const begin={native_session_id:nativeSessionId,harness,generation,sequence,capture_id:captureId,previous_digest:effectivePrevious?.digest??null,visibility,rule_id:ruleId,rule_revision:ruleRevision};
|
|
142
|
+
const parts=[];for(let index=0;index<messages.length;index+=500)parts.push({capture_id:captureId,index:parts.length,messages:messages.slice(index,index+500)});
|
|
143
|
+
const artifacts=[];
|
|
144
|
+
// Native stores normally live in harness homes outside the repository. The locator is
|
|
145
|
+
// accepted only when it came from this configured core discovery, and raw bytes are safe
|
|
146
|
+
// to publish only when the complete native history is in the permitted projection.
|
|
147
|
+
let nativeBundle=null;
|
|
148
|
+
// A session in a shared SQLite store (OpenCode, Goose) has no file of its own to stat; it is
|
|
149
|
+
// stable when the core reads back the same messages this capture publishes.
|
|
150
|
+
const sqliteSource=locator?.storage?.kind==='sqlite';
|
|
151
|
+
if((artifactSource||sqliteSource)&&backfillPolicy.mode==='all'&&!effectivePrevious?.baseline&&typeof this.core.translateSession==='function'){
|
|
152
|
+
if(artifactSource&&!artifactBefore.isFile())throw Object.assign(new Error('native artifact locator is not a regular file'),{code:'invalid_request'});
|
|
153
|
+
const translated=(await this.core.translateSession({locator,target_harness:harness})).artifact;
|
|
154
|
+
const changed=new Error('native session changed while it was being captured');
|
|
155
|
+
if(artifactSource){const after=statSync(artifactSource);if(artifactBefore.dev!==after.dev||artifactBefore.ino!==after.ino||artifactBefore.size!==after.size||artifactBefore.mtimeMs!==after.mtimeMs)throw Object.assign(changed,{code:'resync_required'});}
|
|
156
|
+
else{const digest=entries=>hash(canonicalJson((entries.messages??entries.turns??[]).map(entry=>entry.value??entry)));if(digest(await this.core.session(locator).load())!==digest(loaded))throw Object.assign(changed,{code:'resync_required'});}
|
|
157
|
+
const translatedFiles=translated.files?.length?translated.files:[{path:translated.suggested_filename??(artifactSource?basename(artifactSource):`${nativeSessionId}.json`),content:translated.content,role:'primary'}];
|
|
158
|
+
if(translatedFiles.filter(file=>file.role==='primary').length!==1)throw Object.assign(new Error('native codec bundle must contain exactly one primary file'),{code:'fidelity_unavailable'});
|
|
159
|
+
let total=0;const bundleFiles=[];
|
|
160
|
+
for(const file of translatedFiles){const bytes=Buffer.from(file.content,'utf8');total+=bytes.length;if(total>256*1024*1024)throw Object.assign(new Error('native artifact bundle exceeds 256 MiB'),{code:'request_too_large'});const uploadId=`upload_${randomBytes(16).toString('hex')}`,chunks=[];for(let at=0,index=0;at<bytes.length||index===0;at+=1024*1024,index++){const chunk=bytes.subarray(at,Math.min(bytes.length,at+1024*1024));chunks.push({upload_id:uploadId,index,sha256:hash(chunk),bytes_base64:chunk.toString('base64')});if(bytes.length===0)break;}artifacts.push({manifest:{upload_id:uploadId,relative_name:file.path,sha256:hash(bytes),byte_length:bytes.length,chunks:chunks.length,media_type:'application/octet-stream',role:file.role==='source_recovery'?'sidecar':'native'},chunks});bundleFiles.push({upload_id:uploadId,role:file.role});}
|
|
161
|
+
nativeBundle={id:bundleFiles.find(file=>file.role==='primary').upload_id,harness,fidelity:translated.fidelity,residue:translated.residue??[],files:bundleFiles};
|
|
162
|
+
}
|
|
163
|
+
const recordedTimes=messages.map(message=>message.recorded_at).filter(Number.isSafeInteger),commit={capture_id:captureId,parts:parts.length,digest:'',title:descriptor?.title??null,repository:null,reported_user:null,recorded_activity_at:recordedTimes.length?Math.max(...recordedTimes):null,parent_native_id:descriptor?.parent_session_id??null,coverage:effectivePrevious?.baseline&&!clearBaseline?{...coverage,completeness:'partial',reasons:[...new Set([...coverage.reasons,'prefix_not_captured'])]}:coverage,artifacts:artifacts.map(item=>item.manifest),...(nativeBundle?{native_bundle:nativeBundle}:{})};
|
|
164
|
+
const digestCommit={...commit};delete digestCommit.digest;commit.digest=hash(canonicalJson({begin,parts,commit:digestCommit}));
|
|
165
|
+
this.state.outbox.push({begin,parts,commit,artifacts,clearBaseline,backfillMode:backfillPolicy.mode,observed:{keys:(loaded.messages??loaded.turns??[]).map((entry,index)=>entry.id??entry.native_key??String(index)),digests:(loaded.messages??loaded.turns??[]).map(entry=>hash(canonicalJson(entry.value??Object.fromEntries(Object.entries(entry).filter(([key])=>!['id','native_key','recorded_at','timestamp'].includes(key))))))}});this.save();return this.flushSerial();
|
|
166
|
+
}
|
|
167
|
+
flush() {
|
|
168
|
+
const work=this.work.then(()=>this.flushSerial());
|
|
169
|
+
this.work=work.catch(()=>{});
|
|
170
|
+
return work;
|
|
171
|
+
}
|
|
172
|
+
async flushSerial(){let latest=null;while(this.state.outbox.length){const item=this.state.outbox[0];const begun=await this.request('POST',`/sources/${this.sourceId}/captures`,item.begin);let receipt;if(begun.state==='committed')receipt=begun.receipt;else{for(const part of item.parts)await this.request('PUT',`/sources/${this.sourceId}/captures/${item.begin.capture_id}/parts/${part.index}`,part);for(const artifact of item.artifacts??[])for(const chunk of artifact.chunks)await this.request('PUT',`/sources/${this.sourceId}/uploads/${chunk.upload_id}/chunks/${chunk.index}`,chunk);receipt=await this.request('POST',`/sources/${this.sourceId}/captures/${item.begin.capture_id}/commit`,item.commit);}const key=`${item.begin.harness}:${item.begin.native_session_id}`,prior=this.state.origins[key]??{},next={...prior,generation:item.begin.generation,sequence:item.begin.sequence,digest:item.commit.digest,observed_keys:item.observed?.keys??prior.observed_keys,observed_digests:item.observed?.digests??prior.observed_digests,last_rule_revision:item.begin.rule_revision,backfill_mode:item.backfillMode};if(item.clearBaseline){delete next.baseline;delete next.baseline_digests;}this.state.origins[key]=next;this.state.outbox.shift();this.save();latest=receipt;}return latest;}
|
|
173
|
+
stop(){if(this.timer)clearInterval(this.timer);this.abort.abort();for(const follower of this.followers?.values()??[])follower.controller.abort();return this.nativeEffects.close();}
|
|
174
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SupercodeHarnessClient, ManagedRuntime } from '@volter/supercode-harness-sdk';
|
|
2
|
+
import type { LocalLaunchTicket } from '../contracts/index.js';
|
|
3
|
+
|
|
4
|
+
export interface NativeContinuationAdapter {
|
|
5
|
+
supports(request: { harness: string; fidelity: string }): boolean;
|
|
6
|
+
launch(request: { ticket: LocalLaunchTicket; directory: string; fidelity?: string; signal?: AbortSignal }): Promise<{ native_session_id: string; runtime: ManagedRuntime }>;
|
|
7
|
+
}
|
|
8
|
+
export declare function createNativeContinuationAdapter(options: { core: SupercodeHarnessClient }): NativeContinuationAdapter;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// A continuation materializes a new native session (new ids), so value_lossless is the only
|
|
5
|
+
// fidelity it delivers.
|
|
6
|
+
export const CONTINUATION_HARNESSES = new Set(['claude-code', 'codex', 'pi', 'grok', 'opencode']);
|
|
7
|
+
export const CONTINUATION_FIDELITY = 'value_lossless';
|
|
8
|
+
|
|
9
|
+
export function createNativeContinuationAdapter({ core }) {
|
|
10
|
+
if (!core || typeof core.materializeSession !== 'function' || typeof core.resumeManagedRuntime !== 'function') {
|
|
11
|
+
throw new TypeError('native continuation requires a materialization-capable core client');
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
supports({ harness, fidelity }) {
|
|
15
|
+
return CONTINUATION_HARNESSES.has(harness) && fidelity === CONTINUATION_FIDELITY;
|
|
16
|
+
},
|
|
17
|
+
async launch({ ticket, directory, fidelity = ticket?.fidelity, signal }) {
|
|
18
|
+
if (!this.supports({ harness: ticket?.harness, fidelity }) || !fidelitySatisfies(ticket?.artifact?.fidelity, fidelity)) throw unsupported('native continuation format or fidelity is unsupported');
|
|
19
|
+
if (signal?.aborted) throw signal.reason ?? new Error('native continuation was aborted');
|
|
20
|
+
const files = ticket.artifact.files.map(file => {
|
|
21
|
+
const path = resolve(directory, file.relative_name);
|
|
22
|
+
if (!path.startsWith(`${resolve(directory)}/`)) throw unsupported('native continuation artifact escapes its verified directory');
|
|
23
|
+
return { path: file.relative_name, content: readFileSync(path, 'utf8'), role: file.role };
|
|
24
|
+
});
|
|
25
|
+
const primary = files.filter(file => file.role === 'primary');
|
|
26
|
+
if (primary.length !== 1) throw unsupported('native continuation requires exactly one primary artifact');
|
|
27
|
+
const materialized = await core.materializeSession({
|
|
28
|
+
artifact: {
|
|
29
|
+
source_harness: ticket.harness,
|
|
30
|
+
target_harness: ticket.harness,
|
|
31
|
+
session_id: null,
|
|
32
|
+
content: primary[0].content,
|
|
33
|
+
suggested_filename: primary[0].path,
|
|
34
|
+
files,
|
|
35
|
+
fidelity,
|
|
36
|
+
residue: ticket.artifact.residue ?? [],
|
|
37
|
+
},
|
|
38
|
+
cwd: ticket.destination.cwd,
|
|
39
|
+
}, { signal });
|
|
40
|
+
const locator = materialized?.locator;
|
|
41
|
+
if (!locator?.session_id || locator.harness !== ticket.harness) throw unsupported('core materialization returned no resumable native identity');
|
|
42
|
+
const runtime = await core.resumeManagedRuntime({ harness: locator.harness, runtime_id: locator.session_id, cwd: ticket.destination.cwd }, { signal });
|
|
43
|
+
if (!runtime?.handle?.runtime_id) { await runtime?.close?.().catch(() => {}); throw unsupported('native runtime resume returned no runtime identity'); }
|
|
44
|
+
return { native_session_id: locator.session_id, runtime };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function unsupported(message) { return Object.assign(new Error(message), { code: 'unsupported', nativeOutcome: 'failed' }); }
|
|
50
|
+
function fidelitySatisfies(actual, requested) { return ({ semantic: 0, value_lossless: 1, byte_lossless: 2 }[actual] ?? -1) >= ({ semantic: 0, value_lossless: 1, byte_lossless: 2 }[requested] ?? Infinity); }
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { join, resolve, relative, dirname, isAbsolute, sep } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const digest = value => createHash('sha256').update(value).digest('hex');
|
|
6
|
+
const refused = (message, code = 'unsupported') => Object.assign(new Error(message), { code, nativeOutcome: 'failed', teamsSafeMessage: true });
|
|
7
|
+
const within = (root, path) => { const name = relative(root, path); return name === '' || !isAbsolute(name) && name !== '..' && !name.startsWith(`..${sep}`); };
|
|
8
|
+
const LIMIT = 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
export class NativeEffects {
|
|
11
|
+
constructor(connector) { this.c = connector; this.bindings = new Map(); this.attachments = new Map(); this.ownedRuntimes = new Set(); }
|
|
12
|
+
async discover(descriptor, ruleId) {
|
|
13
|
+
const c = this.c, locator = descriptor.locator;
|
|
14
|
+
if (!descriptor.live_endpoint?.startsWith('supercode-live://') || !descriptor.cwd || !c.core.attachManagedRuntime || !c.core.runtimeCapabilities) return;
|
|
15
|
+
const id = `bind_${digest(`${c.machineId}:${c.state.incarnation_id}:${locator.harness}:${locator.session_id}`).slice(0,32)}`;
|
|
16
|
+
const params = { harness: locator.harness, runtime_id: locator.session_id, cwd: descriptor.cwd, base_url: descriptor.live_endpoint };
|
|
17
|
+
const existing = this.bindings.get(id);
|
|
18
|
+
if (existing && JSON.stringify(existing.params) === JSON.stringify(params)) { existing.seen = true; existing.ruleId = ruleId; return; }
|
|
19
|
+
const { capabilities } = await c.core.runtimeCapabilities(params);
|
|
20
|
+
if (!capabilities.attach_existing_process || !capabilities.stream_events) return;
|
|
21
|
+
const runtime = await c.core.attachManagedRuntime(params);
|
|
22
|
+
try {
|
|
23
|
+
const actions = ['attach_observe'];
|
|
24
|
+
if (capabilities.send_input) actions.push('interact');
|
|
25
|
+
if (capabilities.interrupt) actions.push('interrupt');
|
|
26
|
+
if (capabilities.respond_to_requests) actions.push('approve');
|
|
27
|
+
this.bindings.set(id, { params, ruleId, seen: true, descriptor: {
|
|
28
|
+
id, machine_id: c.machineId, incarnation_id: c.state.incarnation_id,
|
|
29
|
+
native_runtime_id: runtime.handle.runtime_id, native_session_id: locator.session_id,
|
|
30
|
+
harness: locator.harness, state: 'running', observed_at: c.clock.nowMs(), native_capabilities: actions,
|
|
31
|
+
} });
|
|
32
|
+
} finally { await runtime.close(); } // This backend's close detaches the observer only.
|
|
33
|
+
}
|
|
34
|
+
beginDiscovery() { for (const binding of this.bindings.values()) binding.seen = false; }
|
|
35
|
+
endDiscovery() { for (const [id, binding] of this.bindings) if (!binding.seen) this.bindings.delete(id); }
|
|
36
|
+
published() { return [...this.bindings.values()].map(binding => ({ ...binding.descriptor, observed_at: this.c.clock.nowMs() })); }
|
|
37
|
+
async prepare(effect) {
|
|
38
|
+
if (effect.kind === 'continue') return this.prepareContinuation(effect);
|
|
39
|
+
const binding = this.bindings.get(effect.binding_id);
|
|
40
|
+
if (!binding) throw refused('native binding is no longer available', 'missing_context');
|
|
41
|
+
const action = effect.kind === 'input' ? 'interact' : effect.kind === 'attach' ? 'attach_observe' : effect.kind;
|
|
42
|
+
if (!binding.descriptor.native_capabilities.includes(action)) throw refused('native binding does not support this action');
|
|
43
|
+
if (effect.kind === 'attach') {
|
|
44
|
+
if (effect.payload.mode === 'control' && !binding.descriptor.native_capabilities.includes('interact')) throw refused('native binding cannot accept input');
|
|
45
|
+
return { execute: async () => {
|
|
46
|
+
const runtime = await this.c.core.attachManagedRuntime(binding.params, { signal: this.c.abort.signal });
|
|
47
|
+
if (effect.payload.mode === 'control') {
|
|
48
|
+
if (typeof runtime.acquireControl !== 'function') { await runtime.close(); throw refused('native control lease acquisition is unsupported'); }
|
|
49
|
+
try { await runtime.acquireControl({signal:this.c.abort.signal}); }
|
|
50
|
+
catch (error) { await runtime.close(); throw Object.assign(refused('native control lease is unavailable'),{sdkCode:error.sdkCode,data:error.data,code:error.code}); }
|
|
51
|
+
}
|
|
52
|
+
if (runtime.handle.runtime_id !== binding.descriptor.native_runtime_id) { await runtime.close(); throw refused('native binding identity changed', 'conflict'); }
|
|
53
|
+
const entry = { runtime, effect, binding, active: false, queue: [], bytes: 0, requests: new Map(), sending: null, closed: false };
|
|
54
|
+
entry.listener = frame => {
|
|
55
|
+
if (entry.closed) return;
|
|
56
|
+
if (frame.type === 'request') entry.requests.set(JSON.stringify(frame.requestId), frame);
|
|
57
|
+
const bytes = Buffer.byteLength(JSON.stringify(frame)) + 1;
|
|
58
|
+
if (bytes > LIMIT || entry.bytes + bytes > LIMIT) { this.c.lastError = refused('native attachment buffer exceeded 1 MiB', 'resync_required'); void this.closeAttachment(entry); return; }
|
|
59
|
+
entry.queue.push(frame); entry.bytes += bytes;
|
|
60
|
+
entry.wake?.();
|
|
61
|
+
};
|
|
62
|
+
runtime.on('event', entry.listener);
|
|
63
|
+
this.attachments.set(effect.payload.attachment_id, entry);
|
|
64
|
+
return { native_session_id: binding.descriptor.native_session_id, runtime_id: runtime.handle.runtime_id };
|
|
65
|
+
} };
|
|
66
|
+
}
|
|
67
|
+
const entry = this.attachments.get(effect.payload.attachment_id);
|
|
68
|
+
if (!entry || entry.closed || !entry.active || entry.effect.binding_id !== effect.binding_id) throw refused('native attachment is unavailable', 'missing_context');
|
|
69
|
+
let request, option;
|
|
70
|
+
if (effect.kind === 'approve') {
|
|
71
|
+
request = [...entry.requests.values()].find(item => String(item.requestId) === effect.payload.request_id);
|
|
72
|
+
option = request?.options.find(item => item.kind === (effect.payload.decision === 'allow_once' ? 'allow_once' : 'reject_once'));
|
|
73
|
+
if (!option) throw refused('native permission request or exact decision is unavailable', 'missing_context');
|
|
74
|
+
}
|
|
75
|
+
return { execute: async () => {
|
|
76
|
+
if (effect.kind === 'input') await entry.runtime.sendInput(effect.payload.text, { signal: this.c.abort.signal });
|
|
77
|
+
else if (effect.kind === 'interrupt') await entry.runtime.interrupt({ signal: this.c.abort.signal });
|
|
78
|
+
else if (effect.kind === 'approve') { await entry.runtime.respond(request.requestId, option.response, { signal: this.c.abort.signal }); entry.requests.delete(JSON.stringify(request.requestId)); }
|
|
79
|
+
else throw refused('native termination is not supported by an observer connection');
|
|
80
|
+
return { native_session_id: binding.descriptor.native_session_id, runtime_id: entry.runtime.handle.runtime_id };
|
|
81
|
+
} };
|
|
82
|
+
}
|
|
83
|
+
async activate(effect) {
|
|
84
|
+
if (effect.kind !== 'attach') return;
|
|
85
|
+
const entry = this.attachments.get(effect.payload.attachment_id);
|
|
86
|
+
if (!entry || entry.closed) throw refused('native observer closed before activation', 'missing_context');
|
|
87
|
+
if (entry.active) return;
|
|
88
|
+
entry.active = true;
|
|
89
|
+
const stream = new ReadableStream({
|
|
90
|
+
pull: async controller => {
|
|
91
|
+
while (!entry.closed && !entry.queue.length) await new Promise(resolve => { entry.wake = resolve; });
|
|
92
|
+
entry.wake = null;
|
|
93
|
+
if (entry.closed) { controller.close(); return; }
|
|
94
|
+
const frame = entry.queue.shift(), line = `${JSON.stringify(frame)}\n`;
|
|
95
|
+
entry.bytes -= Buffer.byteLength(line);
|
|
96
|
+
controller.enqueue(Buffer.from(line));
|
|
97
|
+
if (frame.type === 'closed' || frame.type === 'error' && frame.terminal) controller.close();
|
|
98
|
+
},
|
|
99
|
+
cancel: () => { entry.wake?.(); },
|
|
100
|
+
}, {highWaterMark:0});
|
|
101
|
+
// One bounded streaming request per attachment, rather than one HTTP mutation
|
|
102
|
+
// for every token. A failed response closes this observer; frames are never replayed.
|
|
103
|
+
entry.sending = this.frames(entry, stream).catch(error => { this.c.lastError = error; }).finally(() => this.closeAttachment(entry));
|
|
104
|
+
}
|
|
105
|
+
async discard(effect) { if (effect.kind === 'attach') await this.closeAttachment(this.attachments.get(effect.payload.attachment_id)); }
|
|
106
|
+
frames(entry, body) { return this.c.effectFetch('POST', `/machines/${this.c.machineId}/attachments/${entry.effect.payload.attachment_id}/frames`, { body, contentType: 'application/x-ndjson' }); }
|
|
107
|
+
async recheck() {
|
|
108
|
+
for (const entry of this.attachments.values()) {
|
|
109
|
+
if (entry.closed) continue;
|
|
110
|
+
if (entry.effect.expires_at <= this.c.clock.nowMs() || !this.bindings.has(entry.effect.binding_id)) { await this.closeAttachment(entry); continue; }
|
|
111
|
+
if (entry.active) try { await this.frames(entry, ''); await entry.runtime.heartbeat?.({signal:this.c.abort.signal}); } catch (error) { this.c.lastError = error; await this.closeAttachment(entry); }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async closeAttachment(entry) {
|
|
115
|
+
if (!entry || entry.closed) return;
|
|
116
|
+
entry.closed = true; entry.queue = []; entry.bytes = 0; entry.wake?.();
|
|
117
|
+
entry.runtime.off('event', entry.listener);
|
|
118
|
+
this.attachments.delete(entry.effect.payload.attachment_id);
|
|
119
|
+
await entry.runtime.close().catch(error => { this.c.lastError = error; });
|
|
120
|
+
if (!this.c.abort.signal.aborted) await this.c.effectRequest('DELETE', `/machines/${this.c.machineId}/attachments/${entry.effect.payload.attachment_id}`).catch(error => { this.c.lastError = error; });
|
|
121
|
+
}
|
|
122
|
+
async prepareContinuation(effect) {
|
|
123
|
+
const c = this.c, path = `/machines/${c.machineId}/effects/${effect.id}`;
|
|
124
|
+
const ticket = await c.effectRequest('GET', `${path}/continuation`);
|
|
125
|
+
if (ticket.kind !== 'remote_launch' || ticket.destination?.machine_id !== c.machineId || ticket.continuation_id !== effect.payload.continuation_id || ticket.expires_at <= c.clock.nowMs()) throw refused('invalid remote continuation ticket', 'invalid_request');
|
|
126
|
+
const root = c.localRoots?.find(item => item.id === ticket.destination.root_id);
|
|
127
|
+
if (!root) throw refused('destination root is not registered', 'missing_context');
|
|
128
|
+
const rootPath = realpathSync(root.path ?? root.workspace), cwd = realpathSync(resolve(rootPath, ticket.destination.cwd));
|
|
129
|
+
if (!within(rootPath, cwd) || !statSync(cwd).isDirectory()) throw refused('destination cwd escapes the configured root', 'forbidden');
|
|
130
|
+
const { createNativeContinuationAdapter } = await import('./native-continuation.mjs');
|
|
131
|
+
const adapter = createNativeContinuationAdapter({ core: c.core });
|
|
132
|
+
if (!adapter.supports({ harness: ticket.harness, fidelity: ticket.fidelity })) throw refused('native continuation format or requested fidelity is unsupported');
|
|
133
|
+
if (!Array.isArray(ticket.artifact?.files) || ticket.artifact.files.filter(file => file.role === 'primary').length !== 1) throw refused('invalid native artifact bundle', 'invalid_request');
|
|
134
|
+
const stage = mkdtempSync(join(c.stateDir, 'continuation-'));
|
|
135
|
+
try {
|
|
136
|
+
let total = 0;
|
|
137
|
+
for (const file of ticket.artifact.files) {
|
|
138
|
+
const name = file.relative_name;
|
|
139
|
+
if (typeof name !== 'string' || !name || name.includes('\\') || isAbsolute(name) || name.split('/').some(part => !part || part === '.' || part === '..') || !/^upload_[A-Za-z0-9_-]+$/.test(file.upload_id)) throw refused('unsafe artifact file', 'invalid_request');
|
|
140
|
+
if (!Number.isSafeInteger(file.byte_length) || file.byte_length < 0 || (total += file.byte_length) > 256 * 1024 * 1024) throw refused('native artifact bundle exceeds its bound', 'request_too_large');
|
|
141
|
+
const response = await c.effectFetch('GET', `${path}/artifacts/${file.upload_id}`, { raw: true });
|
|
142
|
+
const parts = []; let size = 0;
|
|
143
|
+
for await (const part of response.body) { size += part.length; if (size > file.byte_length) throw refused('artifact length mismatch', 'conflict'); parts.push(part); }
|
|
144
|
+
const bytes = Buffer.concat(parts);
|
|
145
|
+
if (bytes.length !== file.byte_length || digest(bytes) !== file.sha256) throw refused('artifact checksum mismatch', 'conflict');
|
|
146
|
+
const target = join(stage, name); mkdirSync(dirname(target), { recursive: true, mode: 0o700 }); writeFileSync(target, bytes, { flag: 'wx', mode: 0o600 });
|
|
147
|
+
}
|
|
148
|
+
return { dispose: () => rmSync(stage, { recursive: true, force: true }), execute: async () => {
|
|
149
|
+
if (realpathSync(resolve(rootPath, ticket.destination.cwd)) !== cwd || realpathSync(root.path ?? root.workspace) !== rootPath) throw refused('destination root changed', 'conflict');
|
|
150
|
+
// Recheck the pinned ticket after download and immediately before native work.
|
|
151
|
+
const fresh = await c.effectRequest('GET', `${path}/continuation`);
|
|
152
|
+
if (JSON.stringify(fresh) !== JSON.stringify(ticket)) throw refused('continuation ticket changed', 'conflict');
|
|
153
|
+
const launched = await adapter.launch({ ticket: { ...ticket, kind: 'local_launch', destination: { cwd } }, directory: stage, signal: c.abort.signal });
|
|
154
|
+
this.ownedRuntimes.add(launched.runtime);
|
|
155
|
+
return { native_session_id: launched.native_session_id, runtime_id: launched.runtime.handle.runtime_id };
|
|
156
|
+
} };
|
|
157
|
+
} catch (error) { rmSync(stage, { recursive: true, force: true }); throw error; }
|
|
158
|
+
}
|
|
159
|
+
async close() {
|
|
160
|
+
await Promise.allSettled([...this.attachments.values()].map(entry => this.closeAttachment(entry)));
|
|
161
|
+
await Promise.allSettled([...this.ownedRuntimes].map(runtime => runtime.close()));
|
|
162
|
+
this.ownedRuntimes.clear();
|
|
163
|
+
}
|
|
164
|
+
}
|
package/context.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync, openSync, closeSync, unlinkSync, existsSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { TeamsClient } from './workspace-client.mjs';
|
|
5
|
+
const NAME=/^[a-z][a-z0-9_-]{0,63}$/;
|
|
6
|
+
const ID=/^[A-Za-z][A-Za-z0-9_-]{0,127}$/;
|
|
7
|
+
function normalizeOrigin(value){const url=new URL(value);if(url.username||url.password||url.pathname!=='/'||url.search||url.hash)throw new Error('server must be a bare HTTP(S) origin');if(url.protocol!=='https:'&&!(url.protocol==='http:'&&['127.0.0.1','::1','[::1]','localhost'].includes(url.hostname)))throw new Error('non-loopback servers require HTTPS');return url.origin;}
|
|
8
|
+
export function workspacePaths(supercodeHome){const root=join(supercodeHome,'teams','workspaces');return {root,contexts:join(root,'contexts.json'),credentials:join(root,'credentials'),connectors:join(root,'connectors')};}
|
|
9
|
+
function locked(path,fn){mkdirSync(dirname(path),{recursive:true,mode:0o700});chmodSync(dirname(path),0o700);const lock=path+'.lock';let fd;try{fd=openSync(lock,'wx',0o600);}catch(e){throw Object.assign(new Error(`${path} is locked by another writer`),{code:'EBUSY',cause:e});}try{return fn();}finally{closeSync(fd);unlinkSync(lock);}}
|
|
10
|
+
function read(path,fallback){return existsSync(path)?JSON.parse(readFileSync(path,'utf8')):fallback;}
|
|
11
|
+
function atomic(path,value){const tmp=`${path}.${process.pid}.${randomBytes(12).toString('hex')}.tmp`,fd=openSync(tmp,'wx',0o600);try{writeFileSync(fd,JSON.stringify(value,null,2)+'\n');}finally{closeSync(fd);}try{renameSync(tmp,path);}catch(error){try{unlinkSync(tmp);}catch{}throw error;}chmodSync(path,0o600);}
|
|
12
|
+
export class WorkspaceContexts { constructor(supercodeHome){this.paths=workspacePaths(supercodeHome);} read(){return read(this.paths.contexts,{version:1,current:null,contexts:{}});} create(name,value){if(!NAME.test(name)||name==='local')throw new Error('context name must match [a-z][a-z0-9_-]{0,63} and cannot be local');return locked(this.paths.contexts,()=>{const doc=this.read();if(doc.contexts[name])throw new Error(`context ${name} already exists`);doc.contexts[name]=value;atomic(this.paths.contexts,doc);return value;});} use(name){return locked(this.paths.contexts,()=>{const doc=this.read();if(name!==null&&name!=='local'&&!doc.contexts[name])throw new Error(`unknown context ${name}`);doc.current=name;atomic(this.paths.contexts,doc);});} remove(name){return locked(this.paths.contexts,()=>{const doc=this.read();delete doc.contexts[name];if(doc.current===name)doc.current=null;atomic(this.paths.contexts,doc);});} writeCredential(id,value){mkdirSync(this.paths.credentials,{recursive:true,mode:0o700});locked(join(this.paths.credentials,`${id}.json`),()=>atomic(join(this.paths.credentials,`${id}.json`),value));} credential(id){return read(join(this.paths.credentials,`${id}.json`),null);} }
|
|
13
|
+
WorkspaceContexts.prototype.upsert=function(name,value){if(!NAME.test(name)||name==='local')throw new Error('context name must match [a-z][a-z0-9_-]{0,63} and cannot be local');return locked(this.paths.contexts,()=>{const doc=this.read();doc.contexts[name]=value;atomic(this.paths.contexts,doc);return value;});};
|
|
14
|
+
WorkspaceContexts.prototype.createAuthenticated=async function(name,{origin,teamId,credentialRef,fetch}){
|
|
15
|
+
if(!ID.test(teamId)||!/^cred_[A-Za-z0-9_-]+$/.test(credentialRef))throw new Error('team and credential reference must be immutable IDs');
|
|
16
|
+
const normalized=normalizeOrigin(origin),credential=this.credential(credentialRef);
|
|
17
|
+
if(!credential?.token)throw Object.assign(new Error(`credential ${credentialRef} is missing`),{code:'authentication_required'});
|
|
18
|
+
if(credential.origin!==normalized)throw Object.assign(new Error('credential origin does not match requested server'),{code:'authentication_required'});
|
|
19
|
+
const client=new TeamsClient({server:normalized,credential:credential.token,fetch});
|
|
20
|
+
const me=await client.me();
|
|
21
|
+
if(credential.server_id&&credential.server_id!==me.server_id)throw Object.assign(new Error('credential server audience does not match'),{code:'authentication_required'});
|
|
22
|
+
const membership=me.teams.find(item=>item.team.id===teamId);
|
|
23
|
+
if(!membership)throw Object.assign(new Error('credential is not a member of the requested team'),{code:'forbidden'});
|
|
24
|
+
return this.create(name,{origin:normalized,server_id:me.server_id,team_id:teamId,principal_id:me.principal.id,credential_ref:credentialRef});
|
|
25
|
+
};
|
|
26
|
+
WorkspaceContexts.prototype.connector=function(serverId,teamId){
|
|
27
|
+
if(!ID.test(serverId)||!ID.test(teamId))throw new Error('server and team must be immutable IDs');
|
|
28
|
+
const directory=join(this.paths.connectors,serverId,teamId),path=join(directory,'enrollment.json');
|
|
29
|
+
return {directory,read:()=>read(path,null),write:value=>{mkdirSync(directory,{recursive:true,mode:0o700});locked(path,()=>atomic(path,value));},update:fn=>{mkdirSync(directory,{recursive:true,mode:0o700});return locked(path,()=>{const value=fn(read(path,null));if(value===null){if(existsSync(path))unlinkSync(path);}else atomic(path,value);return value;});},remove:()=>{if(existsSync(path))locked(path,()=>unlinkSync(path));}};
|
|
30
|
+
};
|
|
31
|
+
export function fromContext({supercodeHome,name,fetch}){const contexts=new WorkspaceContexts(supercodeHome),doc=contexts.read(),selected=name??doc.current;if(!selected||selected==='local')throw new Error('a remote workspace context is required');const context=doc.contexts[selected];if(!context)throw new Error(`unknown context ${selected}`);const credential=contexts.credential(context.credential_ref);if(!credential?.token)throw Object.assign(new Error(`credential ${context.credential_ref} is missing`),{code:'authentication_required'});if(credential.server_id!==context.server_id||credential.origin!==context.origin)throw Object.assign(new Error('credential audience does not match context'),{code:'authentication_required'});return {client:new TeamsClient({server:context.origin,credential:credential.token,fetch}),context};}
|
|
32
|
+
/** A context name from a team's name: `Volter` → `volter`. */
|
|
33
|
+
export function contextNameOf(team){const name=String(team).toLowerCase().replace(/[^a-z0-9_-]+/g,'-').replace(/^[^a-z]+/,'').slice(0,64);return name||'team';}
|
|
34
|
+
export async function loginContext({supercodeHome,name,origin,teamId,token,invite,identityToken=null,as,fetch}){if(token!=null&&(invite!=null||identityToken!=null))throw new Error('a token signs in alone');if(token==null&&invite==null&&identityToken==null)throw new Error('a token, an invitation or an identity is required');origin=normalizeOrigin(origin);let credential,serverId,principalId,resolvedTeam=teamId;if(invite){const response=await fetch(new URL('/api/v1/invitations/accept',origin),{method:'POST',redirect:'manual',headers:{'Content-Type':'application/json'},body:JSON.stringify({enrollment_code:invite,...(identityToken?{access_token:identityToken}:{})})});if(response.type==='opaqueredirect'||(response.status>=300&&response.status<400))throw new Error('cross-origin redirects are refused');if(!response.ok)throw new Error(`invitation failed: HTTP ${response.status}`);const value=await response.json();credential=value.credential;principalId=value.principal.id;resolvedTeam=value.team.id;token=credential.token;}else if(identityToken){const response=await fetch(new URL('/api/v1/login/volter',origin),{method:'POST',redirect:'manual',headers:{'Content-Type':'application/json'},body:JSON.stringify({access_token:identityToken,...(teamId?{team_id:teamId}:{})})});if(response.type==='opaqueredirect'||(response.status>=300&&response.status<400))throw new Error('cross-origin redirects are refused');if(!response.ok){const detail=await response.json().catch(()=>null);throw new Error(`sign-in failed: ${detail?.error?.message??`HTTP ${response.status}`}`);}const value=await response.json();credential=value.credential;principalId=value.principal.id;resolvedTeam=value.team.id;token=credential.token;}const client=new TeamsClient({server:origin,credential:token,fetch});const me=await client.me();serverId=me.server_id;const membership=me.teams.find(x=>x.team.id===resolvedTeam||x.team.name===resolvedTeam);if(!membership)throw new Error('credential is not a member of the requested team');name??=contextNameOf(membership.team.name);principalId??=me.principal.id;const store=new WorkspaceContexts(supercodeHome);const credentialId=`cred_${crypto.randomUUID().replaceAll('-','')}`;store.writeCredential(credentialId,{token,kind:credential?.kind??'user',expires_at:credential?.expires_at??null,server_id:serverId,origin});const saved=store.upsert(name,{origin,server_id:serverId,team_id:membership.team.id,team_name:membership.team.name,principal_id:principalId,credential_ref:credentialId,label:as??null});if(!store.read().current)store.use(name);return saved;}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"doors": {
|
|
4
|
+
"discover": "harness.v1.sessions.discover",
|
|
5
|
+
"load": "harness.v1.sessions.load",
|
|
6
|
+
"watch": "harness.v1.sessions.follow",
|
|
7
|
+
"unwatch": "harness.v1.sessions.unfollow",
|
|
8
|
+
"export": "harness.v1.sessions.export",
|
|
9
|
+
"runtime_start": "harness.v1.runtimes.start",
|
|
10
|
+
"runtime_resume": "harness.v1.runtimes.resume",
|
|
11
|
+
"runtime_input": "harness.v1.runtimes.send_input",
|
|
12
|
+
"runtime_interrupt": "harness.v1.runtimes.interrupt",
|
|
13
|
+
"runtime_close": "harness.v1.runtimes.close"
|
|
14
|
+
},
|
|
15
|
+
"unsupported": []
|
|
16
|
+
}
|