@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/README.md +2 -1
- package/bin/fleet.mjs +4 -1
- package/bin/teams.mjs +5 -5
- package/bin/workspace/agent-session.mjs +20 -0
- package/bin/workspace/args.mjs +3 -3
- package/bin/workspace.mjs +170 -4
- package/board-process-guard.mjs +52 -0
- package/client.mjs +3 -29
- package/connector/agent-closures.mjs +28 -0
- package/connector/index.d.ts +1 -0
- package/connector/index.mjs +76 -5
- package/context.mjs +1 -34
- package/event-client.mjs +1 -0
- package/machine-door.mjs +1 -36
- package/machine.mjs +225 -15
- package/native-codex-pane.mjs +3 -2
- package/package.json +3 -2
- package/server/harness-routes.mjs +8 -3
- package/server/index.mjs +1 -2
- package/server/store-core.mjs +15 -2
- package/session-notice-target.mjs +39 -0
- package/worker/index.mjs +1 -5
- package/workspace-client.d.ts +4 -0
- package/workspace-client.mjs +2 -185
- package/lines.mjs +0 -19
package/workspace-client.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface AppDelegation { actions:string[]; actor:string; expires_in_ms?:
|
|
|
54
54
|
export interface AppToken { grants?:AppResourceGrant[]|null; token:string; kind:'service'; expires_at:number; actions:string[]; subject:string; act:string[] }
|
|
55
55
|
export interface AppCodeExchange { version:1; installation:AppInstallation; credential:AppToken; webhook_secret:string }
|
|
56
56
|
export interface TeamClient {
|
|
57
|
+
identity: import('@volter/teams/http-client').TeamClient['identity'];
|
|
58
|
+
events:{catalogue(options?:RequestOptions):Promise<Result<{kinds:string[]}>>;createSource(body:{name:string},options?:RequestOptions):Promise<Result<{id:string;name:string}>>;publish(source:string,body:{id:string;kind:string;resource:{kind:'task'|'agent';id:string};revision:number;data:Record<string,unknown>;actor?:{session?:string|null}},options?:RequestOptions):Promise<Result<OntologyEvent>>;follow(query?:{source?:string;kinds?:string[]},options?:StreamOptions):AsyncIterable<PublishedEventFrame>};
|
|
57
59
|
apps:{deregister(id:string,options?:RequestOptions):Promise<null>;catalogue(options?:RequestOptions):Promise<Result<{actions:string[];event_kinds:string[]}>>;list(options?:RequestOptions):Promise<Page<TeamsApp>>;get(id:string,options?:RequestOptions):Promise<Result<TeamsApp>>;register(body:RegisterTeamsApp,options?:RequestOptions):Promise<Result<TeamsApp & {secret?:string}>>;rotateSecret(id:string,options?:RequestOptions):Promise<Result<{id:string;secret?:string}>>;install(id:string,body:{redirect_url:string;state:string;actions?:string[]},options?:RequestOptions):Promise<Result<{installation:AppInstallation;code?:string;state:string;redirect_url:string;expires_at:number}>>};
|
|
58
60
|
appInstallations:{list(options?:RequestOptions):Promise<Page<AppInstallation>>;get(id:string,options?:RequestOptions):Promise<Result<AppInstallation>>;uninstall(id:string,options?:RequestOptions):Promise<null>;delegate(id:string,body:AppDelegation,options?:RequestOptions):Promise<Result<AppToken|null>>};
|
|
59
61
|
sessions:{list(query?:ListQuery,options?:RequestOptions):Promise<Page<Descriptor>>;search(query:SearchQuery,options?:RequestOptions):Promise<SearchPage>;watch(query?:ListQuery,options?:StreamOptions):AsyncIterable<Event>;get(id:string):SessionClient};
|
|
@@ -78,6 +80,8 @@ export interface TeamClient {
|
|
|
78
80
|
stream(attachment:Attachment,options?:Pick<RequestOptions,'signal'>):AsyncIterable<AttachmentFrame>;
|
|
79
81
|
};
|
|
80
82
|
}
|
|
83
|
+
export interface OntologyEvent { id:string|null;org:string;at:number|null;actor:{principal:string|null;session:string|null;lineage:string[]}|null;kind:string;resource:{kind:string;id:string};data:Record<string,unknown> }
|
|
84
|
+
export interface PublishedEventFrame { type:'snapshot_begin'|'snapshot_item'|'snapshot_end'|'event'|'checkpoint'|'access_revoked'|'resync_required';cursor?:string;sequence?:number;data?:OntologyEvent;terminal?:boolean;reason?:string }
|
|
81
85
|
export interface SessionClient {
|
|
82
86
|
artifact(uploadId:string,options?:{signal?:AbortSignal;range?:string}):Promise<Response>;
|
|
83
87
|
descriptor(options?:RequestOptions):Promise<Result<Descriptor>>;
|
package/workspace-client.mjs
CHANGED
|
@@ -1,185 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export class TeamsClient {
|
|
4
|
-
constructor({server,credential,fetch:fetchImpl=(...args)=>globalThis.fetch(...args)}){this.server=trimOrigin(server);this.credential=credential;this.fetch=fetchImpl;if(typeof fetchImpl!=='function')throw new TypeError('fetch is required');}
|
|
5
|
-
async request(method,path,{body,signal,idempotencyKey,ifMatch}={}) {
|
|
6
|
-
const key=idempotencyKey??(method==='GET'?null:randomKey());let delay=250,last;
|
|
7
|
-
for(let attempt=0;attempt<5;attempt++) { try {
|
|
8
|
-
const response=await this.fetch(this.server+path,{method,signal,redirect:'manual',headers:{Accept:'application/json',Authorization:`Bearer ${this.credential}`,...(body!==undefined?{'Content-Type':'application/json'}:{}),...(key?{'Idempotency-Key':key}:{}),...(ifMatch!==undefined?{'If-Match':String(ifMatch)}:{})},body:body===undefined?undefined:JSON.stringify(body)});
|
|
9
|
-
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,retryable:false});
|
|
10
|
-
const value=response.status===204?null:await response.json();if(!response.ok)throw Object.assign(new Error(value?.error?.message??`HTTP ${response.status}`),value?.error,{status:response.status});return value;
|
|
11
|
-
} catch(error) { last=error;if(method!=='GET'||signal?.aborted||!(!error.status||error.retryable)||attempt===4)throw error;await retryWait(delay,signal);delay=Math.min(delay*2,5000); } }
|
|
12
|
-
throw last;
|
|
13
|
-
}
|
|
14
|
-
me(options){return this.request('GET','/api/v1/me',options);} capabilities(options){return this.request('GET','/api/v1/capabilities',options);} team(id){return new TeamClient(this,id);}
|
|
15
|
-
redeemAppCode(body,options){return this.request('POST','/api/v1/apps/token',{...options,body});}
|
|
16
|
-
teams(queryValue={},options){return this.request('GET','/api/v1/teams'+query(queryValue),options);}
|
|
17
|
-
createTeam(body,options){return this.request('POST','/api/v1/teams',{...options,body});}
|
|
18
|
-
}
|
|
19
|
-
const segment = value => {
|
|
20
|
-
if (typeof value !== 'string' || !value || value.length > 256) throw new TypeError('a nonempty resource ID is required');
|
|
21
|
-
return encodeURIComponent(value);
|
|
22
|
-
};
|
|
23
|
-
class TeamClient {
|
|
24
|
-
constructor(root, id) {
|
|
25
|
-
this.root = root; this.id = id;
|
|
26
|
-
const base = `/api/v1/teams/${segment(id)}`;
|
|
27
|
-
const get = (path, options) => root.request('GET', base + path, options);
|
|
28
|
-
const post = (path, body, options) => root.request('POST', base + path, { ...options, body });
|
|
29
|
-
const patch = (path, body, options) => root.request('PATCH', base + path, { ...options, body });
|
|
30
|
-
const remove = (path, options) => root.request('DELETE', base + path, options);
|
|
31
|
-
const resource = (path, id) => `${path}/${segment(id)}`;
|
|
32
|
-
this.apps = {
|
|
33
|
-
catalogue: o => get('/apps/catalogue', o),
|
|
34
|
-
list: o => get('/apps', o), get: (id, o) => get(resource('/apps', id), o),
|
|
35
|
-
register: (body, o) => post('/apps', body, o),
|
|
36
|
-
deregister: (id, o) => remove(resource('/apps', id), o),
|
|
37
|
-
rotateSecret: (id, o) => post(resource('/apps', id) + '/secret', {}, o),
|
|
38
|
-
install: (id, body, o) => post(resource('/apps', id) + '/installations', body, o),
|
|
39
|
-
};
|
|
40
|
-
this.appInstallations = {
|
|
41
|
-
list: o => get('/app-installations', o), get: (id, o) => get(resource('/app-installations', id), o),
|
|
42
|
-
uninstall: (id, o) => remove(resource('/app-installations', id), o),
|
|
43
|
-
delegate: (id, body, o) => post(resource('/app-installations', id) + '/delegations', body, o),
|
|
44
|
-
};
|
|
45
|
-
this.sessions = {
|
|
46
|
-
list: (q = {}, o) => get('/sessions' + query(q), o),
|
|
47
|
-
search: (q, o) => get('/sessions/search' + query(q), o),
|
|
48
|
-
watch: (q = {}, o = {}) => events(root, base + '/sessions/events' + query(q), o),
|
|
49
|
-
get: id => new SessionClient(root, base, id),
|
|
50
|
-
};
|
|
51
|
-
this.members = {
|
|
52
|
-
list: (q = {}, o) => get('/members' + query(q), o),
|
|
53
|
-
update: (id, body, o) => patch(resource('/members', id), body, o),
|
|
54
|
-
remove: (id, o) => remove(resource('/members', id), o),
|
|
55
|
-
};
|
|
56
|
-
this.groups = {
|
|
57
|
-
list: (q = {}, o) => get('/groups' + query(q), o),
|
|
58
|
-
create: (body, o) => post('/groups', body, o),
|
|
59
|
-
remove: (id, o) => remove(resource('/groups', id), o),
|
|
60
|
-
addMember: (id, member, o) => root.request('PUT', base + resource('/groups', id) + resource('/members', member), o),
|
|
61
|
-
removeMember: (id, member, o) => remove(resource('/groups', id) + resource('/members', member), o),
|
|
62
|
-
};
|
|
63
|
-
this.machines = {
|
|
64
|
-
list: (q = {}, o) => get('/machines' + query(q), o),
|
|
65
|
-
get: (id, o) => get(resource('/machines', id), o),
|
|
66
|
-
create: (body, o) => post('/machines', body, o),
|
|
67
|
-
roots: (id, q = {}, o) => get(resource('/machines', id) + '/roots' + query(q), o),
|
|
68
|
-
remove: (id, o) => remove(resource('/machines', id), o),
|
|
69
|
-
liveGrants: machineId => {
|
|
70
|
-
const path = resource('/machines', machineId) + '/grants';
|
|
71
|
-
return {
|
|
72
|
-
list: (o) => get(path, o), get: (id, o) => get(resource(path, id), o),
|
|
73
|
-
create: (body, o) => post(path, body, o), remove: (id, o) => remove(resource(path, id), o),
|
|
74
|
-
};
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
this.sources = {
|
|
78
|
-
list: (q = {}, o) => get('/sources' + query(q), o),
|
|
79
|
-
get: (id, o) => get(resource('/sources', id), o),
|
|
80
|
-
create: (body, o) => post('/sources', body, o),
|
|
81
|
-
update: (id, body, o) => patch(resource('/sources', id), body, o),
|
|
82
|
-
credential: (id, body, o) => post(resource('/sources', id) + '/credentials', body, o),
|
|
83
|
-
configuration: (id, format = 'otel-collector', o) => get(resource('/sources', id) + '/config' + query({ format }), o),
|
|
84
|
-
};
|
|
85
|
-
this.syncRules = {
|
|
86
|
-
list: (q = {}, o) => get('/sync-rules' + query(q), o), get: (id, o) => get(resource('/sync-rules', id), o),
|
|
87
|
-
create: (body, o) => post('/sync-rules', body, o), update: (id, body, o) => patch(resource('/sync-rules', id), body, o),
|
|
88
|
-
remove: (id, o) => remove(resource('/sync-rules', id), o),
|
|
89
|
-
};
|
|
90
|
-
this.credentials = {
|
|
91
|
-
create: (body, o) => post('/credentials', body, o), remove: (id, o) => remove(resource('/credentials', id), o),
|
|
92
|
-
};
|
|
93
|
-
this.invitations = { create: (body, o) => post('/invitations', body, o) };
|
|
94
|
-
this.operations = { get: (id, o) => get(resource('/operations', id), o) };
|
|
95
|
-
this.continuations = {
|
|
96
|
-
get: (id, o) => get(resource('/continuations', id), o),
|
|
97
|
-
authorizeDestination: (id, body, o) => post(resource('/continuations', id) + '/authorize-destination', body, o),
|
|
98
|
-
launch: (id, o) => post(resource('/continuations', id) + '/launch', undefined, o),
|
|
99
|
-
completeLocal: (id, body, o) => post(resource('/continuations', id) + '/complete-local', body, o),
|
|
100
|
-
};
|
|
101
|
-
this.attachments = {
|
|
102
|
-
actions: (id, body, o) => post(resource('/attachments', id) + '/actions', body, o),
|
|
103
|
-
detach: (id, o) => remove(resource('/attachments', id), o),
|
|
104
|
-
stream: (attachment, o = {}) => attachmentEvents(root, base, attachment, o),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
class SessionClient {
|
|
109
|
-
constructor(root, base, id) { this.root = root; this.path = `${base}/sessions/${segment(id)}`; }
|
|
110
|
-
descriptor = o => this.root.request('GET', this.path, o);
|
|
111
|
-
read = (q = {}, o) => this.root.request('GET', this.path + '/messages' + query(q), o);
|
|
112
|
-
follow = (q = {}, o = {}) => events(this.root, this.path + '/events' + query(q), o);
|
|
113
|
-
access = o => this.root.request('GET', this.path + '/access', o);
|
|
114
|
-
grant = (body, o) => this.root.request('POST', this.path + '/access', { ...o, body });
|
|
115
|
-
revoke = (id, o) => this.root.request('DELETE', this.path + '/access/' + segment(id), o);
|
|
116
|
-
export = (body, o) => this.root.request('POST', this.path + '/exports', { ...o, body });
|
|
117
|
-
prepare = (body, o) => this.root.request('POST', this.path + '/continuations', { ...o, body });
|
|
118
|
-
attach = (body, o) => this.root.request('POST', this.path + '/attachments', { ...o, body });
|
|
119
|
-
artifact = async (id, { signal, range } = {}) => {
|
|
120
|
-
if (range !== undefined && !/^bytes=(?:[0-9]+-[0-9]*|-[0-9]+)$/.test(range)) throw new TypeError('range must be one byte range');
|
|
121
|
-
const response = await this.root.fetch(this.root.server + this.path + '/artifacts/' + segment(id), {
|
|
122
|
-
signal, redirect: 'manual', headers: { Authorization: `Bearer ${this.root.credential}`, ...(range ? { Range: range } : {}) },
|
|
123
|
-
});
|
|
124
|
-
rejectRedirect(response);
|
|
125
|
-
if (!response.ok) throw await sseError(response);
|
|
126
|
-
return response;
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
function rejectRedirect(response) {
|
|
130
|
-
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, retryable: false });
|
|
131
|
-
}
|
|
132
|
-
async function* attachmentEvents(root, base, attachment, { signal } = {}) {
|
|
133
|
-
const expected = root.server + base + '/attachments/' + segment(attachment.id) + '/stream';
|
|
134
|
-
if (attachment.stream_format !== 'observed_runtime_event' || attachment.stream_url !== expected || typeof attachment.credential !== 'string' || !attachment.credential) throw new TypeError('attachment stream target or credential is invalid');
|
|
135
|
-
let response;
|
|
136
|
-
try {
|
|
137
|
-
response = await root.fetch(expected, { signal, redirect: 'manual', headers: { Accept: 'text/event-stream', Authorization: `Bearer ${attachment.credential}` } });
|
|
138
|
-
rejectRedirect(response);
|
|
139
|
-
if (!response.ok) throw await sseError(response);
|
|
140
|
-
let buffer = '';
|
|
141
|
-
for await (const chunk of response.body.pipeThrough(new TextDecoderStream())) {
|
|
142
|
-
buffer += chunk;
|
|
143
|
-
let match;
|
|
144
|
-
while ((match = /\r?\n\r?\n/.exec(buffer))) {
|
|
145
|
-
const block = buffer.slice(0, match.index); buffer = buffer.slice(match.index + match[0].length);
|
|
146
|
-
if (new TextEncoder().encode(block).length > 1024 * 1024 + 4096) throw Object.assign(new Error('attachment frame exceeds 1 MiB'), { code: 'resync_required', retryable: false });
|
|
147
|
-
const lines = block.split(/\r?\n/), kind = lines.find(line => line.startsWith('event:'))?.slice(6).trim();
|
|
148
|
-
const data = lines.filter(line => line.startsWith('data:')).map(line => line.slice(5).replace(/^ /, '')).join('\n');
|
|
149
|
-
if (!data) continue;
|
|
150
|
-
if (kind === 'access_revoked' || kind === 'resync_required') throw Object.assign(new Error(`attachment stream ${kind}`), { code: kind, retryable: false });
|
|
151
|
-
if (kind !== 'runtime') throw Object.assign(new Error('unexpected attachment stream event'), { code: 'invalid_request', retryable: false });
|
|
152
|
-
const frame = JSON.parse(data);
|
|
153
|
-
yield frame;
|
|
154
|
-
if (frame.type === 'closed' || frame.type === 'error' && frame.terminal) return;
|
|
155
|
-
}
|
|
156
|
-
if (new TextEncoder().encode(buffer).length > 1024 * 1024 + 4096) throw Object.assign(new Error('attachment frame exceeds 1 MiB'), { code: 'resync_required', retryable: false });
|
|
157
|
-
}
|
|
158
|
-
if (!signal?.aborted) throw Object.assign(new Error('attachment stream ended; create a new attachment to reconnect'), { code: 'resync_required', retryable: false });
|
|
159
|
-
} catch (error) { if (!signal?.aborted) throw error; }
|
|
160
|
-
}
|
|
161
|
-
const query=q=>{const p=new URLSearchParams();for(const [k,v]of Object.entries(q))if(v!==undefined)p.set(k,String(v));return p.size?'?'+p:'';};
|
|
162
|
-
const retryWait=(delay,signal)=>new Promise((resolve,reject)=>{const done=()=>{signal?.removeEventListener('abort',abort);resolve();},timer=setTimeout(done,delay),abort=()=>{clearTimeout(timer);signal?.removeEventListener('abort',abort);reject(signal.reason);};signal?.addEventListener('abort',abort,{once:true});});
|
|
163
|
-
async function sseError(response){let value=null;try{value=await response.json();}catch{}return Object.assign(new Error(value?.error?.message??`SSE HTTP ${response.status}`),value?.error,{status:response.status,retryable:value?.error?.retryable??(response.status===429||response.status>=500)});}
|
|
164
|
-
async function* events(root,path,{signal,after}={}){
|
|
165
|
-
let cursor=after??null,lastYielded=null,lastError=null;
|
|
166
|
-
for(let attempt=0;attempt<5&&!signal?.aborted;attempt++){
|
|
167
|
-
const suffix=cursor?path+(path.includes('?')?'&':'?')+'after='+encodeURIComponent(cursor):path;
|
|
168
|
-
try{
|
|
169
|
-
const response=await root.fetch(root.server+suffix,{signal,redirect:'manual',headers:{Accept:'text/event-stream',Authorization:`Bearer ${root.credential}`,...(cursor?{'Last-Event-ID':cursor}:{})}});
|
|
170
|
-
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,retryable:false});
|
|
171
|
-
if(!response.ok)throw await sseError(response);
|
|
172
|
-
let buffer='';
|
|
173
|
-
for await(const chunk of response.body.pipeThrough(new TextDecoderStream())){
|
|
174
|
-
buffer+=chunk;let at;
|
|
175
|
-
while((at=buffer.indexOf('\n\n'))>=0){
|
|
176
|
-
const block=buffer.slice(0,at);buffer=buffer.slice(at+2);const id=block.split('\n').find(line=>line.startsWith('id: '))?.slice(4),data=block.split('\n').filter(line=>line.startsWith('data: ')).map(line=>line.slice(6)).join('\n');if(!data)continue;
|
|
177
|
-
const event=JSON.parse(data),terminal=event?.terminal||event?.type==='access_revoked'||event?.type==='resync_required',nextCursor=id??cursor;cursor=nextCursor;if(!terminal&&nextCursor&&nextCursor===lastYielded)continue;if(nextCursor)lastYielded=nextCursor;yield event;if(terminal)return;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
lastError=Object.assign(new Error('SSE stream ended before a terminal event'),{retryable:true});
|
|
181
|
-
}catch(error){if(signal?.aborted)return;if(error.retryable===false||error.status&&error.status!==429&&error.status<500)throw error;lastError=error;}
|
|
182
|
-
if(attempt<4)await retryWait(Math.min(250*2**attempt,5000),signal).catch(()=>{});
|
|
183
|
-
}
|
|
184
|
-
if(!signal?.aborted&&lastError)throw lastError;
|
|
185
|
-
}
|
|
1
|
+
// The Teams HTTP door has one implementation in the neutral Teams package.
|
|
2
|
+
export { TeamsClient } from "@volter/teams/http-client";
|
package/lines.mjs
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/** Splits a stream's chunks into its complete lines, in time linear in its length: each chunk is scanned once and a
|
|
2
|
-
* line joined once, when its newline arrives. Appending to one buffer and searching it from the start again on
|
|
3
|
-
* every chunk is quadratic in a long line, and one answer (a large session's `sessions.load`, a whole machine's
|
|
4
|
-
* `sessions.discover`) is a line of tens of megabytes. */
|
|
5
|
-
export function lineSplitter() {
|
|
6
|
-
let pending = [];
|
|
7
|
-
return (chunk) => {
|
|
8
|
-
const text = String(chunk), lines = [];
|
|
9
|
-
let from = 0, at;
|
|
10
|
-
while ((at = text.indexOf('\n', from)) >= 0) {
|
|
11
|
-
pending.push(text.slice(from, at));
|
|
12
|
-
lines.push(pending.join(''));
|
|
13
|
-
pending = [];
|
|
14
|
-
from = at + 1;
|
|
15
|
-
}
|
|
16
|
-
if (from < text.length) pending.push(text.slice(from));
|
|
17
|
-
return lines;
|
|
18
|
-
};
|
|
19
|
-
}
|