@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,90 @@
|
|
|
1
|
+
// A remote browser profile as a local CDP endpoint (company RFC 0008 §5). The
|
|
2
|
+
// caller's own CDP client connects to 127.0.0.1; each WebSocket it opens rides
|
|
3
|
+
// its own `product` stream to `browser-profile:<name>` on the machine, where the
|
|
4
|
+
// daemon admits it and Volter Browsers' door reviews every command. The
|
|
5
|
+
// machine's loopback cdpUrl and Volter Browsers' publication token never leave it.
|
|
6
|
+
//
|
|
7
|
+
// This is Volter Browsers' client, carried here until the Teams packages split.
|
|
8
|
+
import { createServer } from 'node:http';
|
|
9
|
+
import { randomBytes } from 'node:crypto';
|
|
10
|
+
import { WebSocketServer } from 'ws';
|
|
11
|
+
import { fromWebPage } from './client.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Serve `browser-profile:<profile>` locally. `/json/version` answers with a
|
|
15
|
+
* browser WebSocket URL on this listener, so `chromium.connectOverCDP(address)`
|
|
16
|
+
* and other CDP clients work unchanged. `channel` is a machine channel, or a
|
|
17
|
+
* function that opens one: with a function, a channel the server closed (a grant
|
|
18
|
+
* revoked or changed) is reopened for the next connection, which is admitted
|
|
19
|
+
* afresh. Returns { address, close }.
|
|
20
|
+
*/
|
|
21
|
+
export async function attachBrowserProfile(channel, { profile, listen = { host: '127.0.0.1', port: 0 } }) {
|
|
22
|
+
if (typeof profile !== 'string' || !profile) throw new Error('a browser profile name is required');
|
|
23
|
+
const target = `browser-profile:${profile}`;
|
|
24
|
+
const source = typeof channel === 'function' ? channel : null;
|
|
25
|
+
let current = source ? null : channel;
|
|
26
|
+
let opening = null;
|
|
27
|
+
const machine = async () => {
|
|
28
|
+
if (current && !current.closed) return current;
|
|
29
|
+
if (!source) throw new Error('the machine channel closed');
|
|
30
|
+
opening ??= source().then((opened) => { current = opened; return opened; }).finally(() => { opening = null; });
|
|
31
|
+
return opening;
|
|
32
|
+
};
|
|
33
|
+
// An unguessable path: another local user's page cannot open the socket by guessing it.
|
|
34
|
+
const path = `/devtools/browser/${randomBytes(12).toString('hex')}`;
|
|
35
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: 8 * 1024 * 1024 });
|
|
36
|
+
let base = '';
|
|
37
|
+
const http = createServer((req, res) => {
|
|
38
|
+
const route = new URL(req.url ?? '/', 'http://x').pathname.replace(/\/$/, '');
|
|
39
|
+
// Local programs only: a web page on this host (one a teammate drives included) never
|
|
40
|
+
// reads the socket path, and DNS rebinding never reaches it.
|
|
41
|
+
const host = String(req.headers.host ?? '');
|
|
42
|
+
if (fromWebPage(req) || !/^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/.test(host)) { res.writeHead(403); res.end(); return; }
|
|
43
|
+
if (route === '/json/version') {
|
|
44
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
45
|
+
res.end(JSON.stringify({ Browser: `Volter Browsers (${profile})`, 'Protocol-Version': '1.3', webSocketDebuggerUrl: `ws://${base}${path}` }));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (route === '/json' || route === '/json/list') {
|
|
49
|
+
// Pages are discovered over the browser socket (Target.getTargets); none are routed separately.
|
|
50
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
51
|
+
res.end('[]');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
res.writeHead(404); res.end();
|
|
55
|
+
});
|
|
56
|
+
http.on('upgrade', async (req, socket, head) => {
|
|
57
|
+
if (fromWebPage(req) || new URL(req.url ?? '/', 'http://x').pathname !== path) { socket.end('HTTP/1.1 404 Not Found\r\n\r\n'); return; }
|
|
58
|
+
let stream;
|
|
59
|
+
try { stream = await (await machine()).open('product', { target }); }
|
|
60
|
+
catch (error) { socket.end(`HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\n\r\n${String(error.message).slice(0, 200)}`); return; }
|
|
61
|
+
wss.handleUpgrade(req, socket, head, (client) => {
|
|
62
|
+
stream.on('data', (text) => { if (client.readyState === client.OPEN) client.send(String(text)); });
|
|
63
|
+
stream.on('close', (reason) => client.close(1000, String(reason ?? '').slice(0, 120)));
|
|
64
|
+
client.on('message', (data, binary) => { if (!binary && !stream.closed) stream.write(String(data)); });
|
|
65
|
+
client.on('close', () => stream.close());
|
|
66
|
+
client.on('error', () => stream.close());
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
await new Promise((resolve, reject) => { http.once('error', reject); http.listen(listen.port ?? 0, listen.host ?? '127.0.0.1', resolve); });
|
|
70
|
+
const a = http.address();
|
|
71
|
+
base = `${a.family === 'IPv6' ? `[${a.address}]` : a.address}:${a.port}`;
|
|
72
|
+
return {
|
|
73
|
+
address: `http://${base}`,
|
|
74
|
+
close: () => new Promise((resolve) => { for (const client of wss.clients) client.terminate(); if (source) current?.close?.(); http.close(() => resolve()); }),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The browser profiles a machine's Volter Browsers serves, as `view` on `browser-profile:*`
|
|
80
|
+
* sees them: each one's name, whether its browser is running, whether it may be shared with
|
|
81
|
+
* teammates, and the sites a running one is signed in to (domains only).
|
|
82
|
+
*/
|
|
83
|
+
export async function listBrowserProfiles(channel) {
|
|
84
|
+
const stream = await channel.open('product', { target: 'browser-profile:*', op: 'list' });
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
let answered = false;
|
|
87
|
+
stream.on('data', (text) => { answered = true; try { resolve(JSON.parse(String(text)).profiles); } catch (error) { reject(error); } });
|
|
88
|
+
stream.on('close', (reason) => { if (!answered) reject(new Error(`the machine answered no list${reason ? `: ${reason}` : ''}`)); });
|
|
89
|
+
});
|
|
90
|
+
}
|
package/client.mjs
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// The harness's side of a machine channel (opened with @volter/teams/client): operators
|
|
2
|
+
// get a `HarnessTransport` so the harness SDK runs unchanged over the channel; everyone
|
|
3
|
+
// gets terminal attachments, files, ports and describe as their grants allow. The
|
|
4
|
+
// frontend package loads when a runtime attachment is first opened.
|
|
5
|
+
import { connect as netConnect } from 'node:net';
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
7
|
+
import { EventEmitter } from 'node:events';
|
|
8
|
+
import { Channel } from '@volter/teams/channel';
|
|
9
|
+
import { openLocalMachine, openRemoteMachine, StreamRpc, fromWebPage } from '@volter/teams/client';
|
|
10
|
+
|
|
11
|
+
export { openLocalMachine, openRemoteMachine, StreamRpc, fromWebPage };
|
|
12
|
+
|
|
13
|
+
export async function execServerBridge(channel, harness, { host = '127.0.0.1' } = {}) {
|
|
14
|
+
// a channel, or a function opening one per connection (closed with it), so a daemon's restart strands nothing
|
|
15
|
+
const own = typeof channel === 'function';
|
|
16
|
+
const source = own ? channel : async () => channel;
|
|
17
|
+
const { WebSocketServer } = await import('ws');
|
|
18
|
+
// An unguessable path, and no request a web page makes: a page on this host (one a teammate
|
|
19
|
+
// drives through Volter Browsers included) must not reach the machine's exec server.
|
|
20
|
+
const route = `/${randomBytes(12).toString('hex')}`;
|
|
21
|
+
const server = new WebSocketServer({ host, port: 0, path: route, maxPayload: 64 * 1024 * 1024, verifyClient: ({ req }) => !fromWebPage(req) });
|
|
22
|
+
await new Promise((resolve, reject) => { server.once('listening', resolve); server.once('error', reject); });
|
|
23
|
+
server.on('connection', async (socket) => {
|
|
24
|
+
// the client speaks first (its `initialize`), before the stream below is open: hold its frames till then
|
|
25
|
+
let stream = null;
|
|
26
|
+
const early = [];
|
|
27
|
+
const send = (data) => stream.write(Buffer.from(`${data.toString('utf8').replace(/\n$/, '')}\n`, 'utf8').toString('base64'));
|
|
28
|
+
socket.on('message', (data) => { if (!stream) early.push(data); else if (!stream.closed) send(data); });
|
|
29
|
+
socket.on('close', () => { if (stream && !stream.closed) stream.close('client closed'); });
|
|
30
|
+
let ch = null;
|
|
31
|
+
try { ch = await source(); stream = await ch.open('exec-server', { harness }); }
|
|
32
|
+
catch (error) { if (own) ch?.close?.(); socket.close(1011, String(error.message ?? error).slice(0, 120)); return; }
|
|
33
|
+
if (own) stream.on('close', () => ch.close?.());
|
|
34
|
+
if (socket.readyState !== socket.OPEN) { stream.close('client closed'); return; }
|
|
35
|
+
let buffer = '';
|
|
36
|
+
stream.on('data', (chunk) => {
|
|
37
|
+
buffer += Buffer.from(chunk, 'base64').toString('utf8');
|
|
38
|
+
let nl;
|
|
39
|
+
while ((nl = buffer.indexOf('\n')) >= 0) {
|
|
40
|
+
const line = buffer.slice(0, nl);
|
|
41
|
+
buffer = buffer.slice(nl + 1);
|
|
42
|
+
if (line) socket.send(line);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
stream.on('close', (reason) => socket.close(1000, String(reason ?? '').slice(0, 120)));
|
|
46
|
+
for (const data of early.splice(0)) send(data);
|
|
47
|
+
});
|
|
48
|
+
const { port } = server.address();
|
|
49
|
+
return {
|
|
50
|
+
url: `ws://${host}:${port}${route}`,
|
|
51
|
+
close: () => new Promise((resolve) => { for (const client of server.clients) client.terminate(); server.close(() => resolve()); }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A `HarnessTransport` (harness SDK `ClientOptions.transport`) over the
|
|
57
|
+
* channel's `harness` stream. Only an operator's channel is admitted to it.
|
|
58
|
+
*/
|
|
59
|
+
export function harnessTransport(channel) {
|
|
60
|
+
let stream = null;
|
|
61
|
+
return {
|
|
62
|
+
get running() { return stream !== null && !stream.closed; },
|
|
63
|
+
async start(handlers) {
|
|
64
|
+
stream = await channel.open('harness');
|
|
65
|
+
let buffer = '';
|
|
66
|
+
stream.on('data', (chunk) => {
|
|
67
|
+
buffer += chunk;
|
|
68
|
+
let index;
|
|
69
|
+
while ((index = buffer.indexOf('\n')) >= 0) {
|
|
70
|
+
const line = buffer.slice(0, index);
|
|
71
|
+
buffer = buffer.slice(index + 1);
|
|
72
|
+
if (line) handlers.onLine(line);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
stream.on('close', (reason) => handlers.onExit(null, reason ?? null));
|
|
76
|
+
},
|
|
77
|
+
write(line) {
|
|
78
|
+
if (!stream || stream.closed) throw new Error('harness stream is not open');
|
|
79
|
+
stream.write(line.endsWith('\n') ? line : line + '\n');
|
|
80
|
+
},
|
|
81
|
+
async close() {
|
|
82
|
+
stream?.close();
|
|
83
|
+
stream = null;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The node's own doors (describe, grants, launch, files, ports, attention, titles) over the `node` stream. */
|
|
89
|
+
export async function nodeRpc(channel) {
|
|
90
|
+
const stream = await channel.open('node');
|
|
91
|
+
return new StreamRpc(stream);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Attach to a pane. Returns an EventEmitter: 'data' (string of terminal
|
|
96
|
+
* bytes), 'close'; `write(text)` sends input when mode is control;
|
|
97
|
+
* `resize(cols, rows)`; `close()`.
|
|
98
|
+
*/
|
|
99
|
+
export async function attachPane(channel, { pane, mode = 'observe', cols = 120, rows = 40 }) {
|
|
100
|
+
const stream = await channel.open('terminal', { pane, mode, cols, rows });
|
|
101
|
+
const emitter = new EventEmitter();
|
|
102
|
+
stream.on('data', (chunk) => {
|
|
103
|
+
// The terminal substrate speaks JSON frames: {type:'hello',cols,rows} | {type:'data',data} | {type:'error',message};
|
|
104
|
+
// the node adds {type:'notice',message} for input it refused (a suspended pane).
|
|
105
|
+
const text = Buffer.from(chunk, 'base64').toString('utf8');
|
|
106
|
+
let frame = null;
|
|
107
|
+
try { frame = JSON.parse(text); } catch { /* raw bytes */ }
|
|
108
|
+
if (frame && typeof frame.data === 'string') emitter.emit('data', frame.data);
|
|
109
|
+
else if (frame && frame.type === 'hello') emitter.emit('hello', frame);
|
|
110
|
+
else if (frame && frame.type === 'error') emitter.emit('error-message', frame.message);
|
|
111
|
+
else if (frame && frame.type === 'notice') emitter.emit('notice', frame.message);
|
|
112
|
+
else emitter.emit('data', text);
|
|
113
|
+
});
|
|
114
|
+
stream.on('close', (reason) => emitter.emit('close', reason));
|
|
115
|
+
emitter.write = (text) => stream.write(JSON.stringify({ input: text }));
|
|
116
|
+
emitter.resize = (c, r) => stream.write(JSON.stringify({ resize: { cols: c, rows: r } }));
|
|
117
|
+
emitter.close = () => stream.close();
|
|
118
|
+
return emitter;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Attach to a MANAGED runtime on the Machine (frontend.v2 over the
|
|
123
|
+
* channel). Returns a `FrontendClient` from `@volter/supercode-frontend`
|
|
124
|
+
* whose fetch rides the channel's `frontend` stream; the node resolves the
|
|
125
|
+
* runtime's receipt and mints this principal its own credential, so `lease()`,
|
|
126
|
+
* `takeControl()`, `sendInput()`, `startEvents()` behave exactly as on loopback
|
|
127
|
+
* and the lease coordinator sees this caller as its own RuntimeClientId.
|
|
128
|
+
*/
|
|
129
|
+
export async function attachRuntime(channel, { runtime, permissions = ['observe', 'interact', 'approve'] }) {
|
|
130
|
+
const { FrontendClient } = await import('@volter/supercode-frontend');
|
|
131
|
+
const stream = await channel.open('frontend', { runtime });
|
|
132
|
+
let next = 1;
|
|
133
|
+
const pending = new Map(); // id → { onHead, onChunk, onEnd }
|
|
134
|
+
stream.on('data', (chunk) => {
|
|
135
|
+
let message;
|
|
136
|
+
try { message = JSON.parse(chunk); } catch { return; }
|
|
137
|
+
const waiter = pending.get(message.id);
|
|
138
|
+
if (!waiter) return;
|
|
139
|
+
if (message.status !== undefined && !waiter.headed) { waiter.headed = true; waiter.onHead(message); }
|
|
140
|
+
if (message.chunk) waiter.onChunk(Buffer.from(message.chunk, 'base64'));
|
|
141
|
+
if (message.end) { pending.delete(message.id); waiter.onEnd(message.error ?? null); }
|
|
142
|
+
});
|
|
143
|
+
stream.on('close', (reason) => {
|
|
144
|
+
for (const waiter of pending.values()) waiter.onEnd(reason ?? 'stream closed');
|
|
145
|
+
pending.clear();
|
|
146
|
+
});
|
|
147
|
+
const fetchOverStream = (url, init = {}) => new Promise((resolve, reject) => {
|
|
148
|
+
const id = next++;
|
|
149
|
+
const path = url.startsWith('http') ? new URL(url).pathname + new URL(url).search : url;
|
|
150
|
+
let controller;
|
|
151
|
+
const body = new ReadableStream({ start(c) { controller = c; } });
|
|
152
|
+
const waiter = {
|
|
153
|
+
headed: false,
|
|
154
|
+
onHead: (head) => resolve(new Response(body, { status: head.status, headers: head.headers ?? {} })),
|
|
155
|
+
onChunk: (bytes) => { try { controller.enqueue(new Uint8Array(bytes)); } catch { /* consumer gone */ } },
|
|
156
|
+
onEnd: (error) => { try { if (error && !waiter.headed) reject(new Error(error)); controller.close(); } catch { /* already closed */ } },
|
|
157
|
+
};
|
|
158
|
+
pending.set(id, waiter);
|
|
159
|
+
init.signal?.addEventListener('abort', () => { pending.delete(id); stream.write(JSON.stringify({ id, abort: true })); try { controller.error(new Error('aborted')); } catch { /* closed */ } });
|
|
160
|
+
const bodyText = init.body == null ? null : typeof init.body === 'string' ? Buffer.from(init.body) : Buffer.from(init.body);
|
|
161
|
+
stream.write(JSON.stringify({ id, method: init.method ?? 'GET', path, headers: init.headers ?? {}, body: bodyText ? bodyText.toString('base64') : null }));
|
|
162
|
+
});
|
|
163
|
+
const client = new FrontendClient({ baseUrl: 'http://node', token: 'via-node', permissions, fetch: fetchOverStream });
|
|
164
|
+
client.detachChannel = () => stream.close();
|
|
165
|
+
return client;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Forward a Machine port to this box (`port:<pane>/<n>`): a local listener whose
|
|
170
|
+
* every accepted connection rides its own `port` stream to 127.0.0.1:<port> on the Machine.
|
|
171
|
+
* Returns { address, close }.
|
|
172
|
+
*/
|
|
173
|
+
export async function forwardPort(channel, { pane = null, port, listen = { host: '127.0.0.1', port: 0 } }) {
|
|
174
|
+
const { createServer } = await import('node:net');
|
|
175
|
+
const server = createServer(async (local) => {
|
|
176
|
+
let remote;
|
|
177
|
+
try {
|
|
178
|
+
remote = await channel.open('port', { pane, port });
|
|
179
|
+
} catch (error) {
|
|
180
|
+
local.destroy(error);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
remote.on('data', (chunk) => { if (!local.destroyed) local.write(Buffer.from(chunk, 'base64')); });
|
|
184
|
+
remote.on('close', () => local.end());
|
|
185
|
+
local.on('data', (bytes) => { if (!remote.closed) remote.write(bytes.toString('base64')); });
|
|
186
|
+
local.on('close', () => remote.close());
|
|
187
|
+
local.on('error', () => remote.close());
|
|
188
|
+
});
|
|
189
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(listen.port ?? 0, listen.host ?? '127.0.0.1', resolve); });
|
|
190
|
+
const a = server.address();
|
|
191
|
+
return { address: `${a.address}:${a.port}`, close: () => new Promise((r) => server.close(() => r())) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Watch a Machine's live state: 'snapshot' once, then 'delta' events with pane
|
|
196
|
+
* added/changed/gone and port changes; 'error' for a tick that failed; 'close'. The node
|
|
197
|
+
* shows a participant only the panes its grants cover.
|
|
198
|
+
*/
|
|
199
|
+
export async function watchMachine(channel, { intervalMs = 1000 } = {}) {
|
|
200
|
+
const stream = await channel.open('watch', { intervalMs });
|
|
201
|
+
const emitter = new EventEmitter();
|
|
202
|
+
stream.on('data', (chunk) => {
|
|
203
|
+
let message;
|
|
204
|
+
try { message = JSON.parse(chunk); } catch { return; }
|
|
205
|
+
if (message.type === 'snapshot') emitter.emit('snapshot', message);
|
|
206
|
+
else if (message.type === 'delta') emitter.emit('delta', message);
|
|
207
|
+
else if (message.type === 'error') emitter.emit('error-message', message.message);
|
|
208
|
+
});
|
|
209
|
+
stream.on('close', (reason) => emitter.emit('close', reason));
|
|
210
|
+
emitter.close = () => stream.close();
|
|
211
|
+
return emitter;
|
|
212
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ERROR_CODES } from '../contracts/index.mjs';
|
|
2
|
+
// A native action crosses a process boundary. Persist intent before crossing it;
|
|
3
|
+
// an unanswered claim or action is reconciled as unknown, never executed again.
|
|
4
|
+
export class EffectConsumer {
|
|
5
|
+
constructor(connector, adapter) { this.connector = connector; this.adapter = adapter; this.work = Promise.resolve(); }
|
|
6
|
+
poll() {
|
|
7
|
+
const work = this.work.then(() => this.pollSerial());
|
|
8
|
+
this.work = work.catch(() => {});
|
|
9
|
+
return work;
|
|
10
|
+
}
|
|
11
|
+
async pollSerial() {
|
|
12
|
+
const c = this.connector;
|
|
13
|
+
await this.adapter.recheck?.();
|
|
14
|
+
for (const [id, local] of Object.entries(c.state.effects)) {
|
|
15
|
+
if (local.result && !local.reported && local.incarnation_id === c.state.incarnation_id) {
|
|
16
|
+
try {
|
|
17
|
+
const path = `/machines/${c.machineId}/effects/${encodeURIComponent(id)}`;
|
|
18
|
+
const delivery = await c.effectRequest('GET', path);
|
|
19
|
+
await this.report(delivery.effect, path, local.result);
|
|
20
|
+
} catch (error) { c.lastError = error; }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
let after = null;
|
|
24
|
+
do {
|
|
25
|
+
const page = await c.effectRequest('GET', `/machines/${c.machineId}/effects?limit=100${after ? `&after=${encodeURIComponent(after)}` : ''}`);
|
|
26
|
+
for (const delivery of page.items ?? []) {
|
|
27
|
+
try { await this.consume(delivery); }
|
|
28
|
+
catch (error) { c.lastError = error; }
|
|
29
|
+
}
|
|
30
|
+
if (page.next_cursor === after && after !== null) throw new Error('effect pagination did not advance');
|
|
31
|
+
after = page.next_cursor ?? null;
|
|
32
|
+
} while (after && !c.abort.signal.aborted);
|
|
33
|
+
}
|
|
34
|
+
async consume(delivery) {
|
|
35
|
+
const c = this.connector, effect = delivery.effect, id = effect.id;
|
|
36
|
+
if (effect.incarnation_id !== c.state.incarnation_id || effect.machine_id !== c.machineId || effect.team_id !== c.teamId) return;
|
|
37
|
+
const path = `/machines/${c.machineId}/effects/${encodeURIComponent(id)}`;
|
|
38
|
+
let local = c.state.effects[id];
|
|
39
|
+
if (local?.result) return this.report(effect, path, local.result);
|
|
40
|
+
if (local || delivery.state === 'running') {
|
|
41
|
+
// A claim response may have been lost. Even when the server still says pending,
|
|
42
|
+
// the persisted attempt is not replayed: claim under a fresh key then report unknown.
|
|
43
|
+
if (delivery.state === 'pending') {
|
|
44
|
+
const claim = await c.effectRequest('POST', `${path}/claim`, { incarnation_id: c.state.incarnation_id }, { freshKey: true });
|
|
45
|
+
if (!claim.execute && claim.delivery.state !== 'running') return;
|
|
46
|
+
}
|
|
47
|
+
return this.finish(effect, path, { state: 'unknown', error: null });
|
|
48
|
+
}
|
|
49
|
+
if (effect.expires_at <= c.clock.nowMs() || c.abort.signal.aborted) return;
|
|
50
|
+
let prepared, preparationError;
|
|
51
|
+
try { prepared = await this.adapter.prepare(effect); } catch (error) { preparationError = error; }
|
|
52
|
+
c.state.effects[id] = { state: 'claiming', incarnation_id: c.state.incarnation_id }; c.save();
|
|
53
|
+
let claim;
|
|
54
|
+
try { claim = await c.effectRequest('POST', `${path}/claim`, { incarnation_id: c.state.incarnation_id }, { freshKey: true }); }
|
|
55
|
+
catch (error) { await prepared?.dispose?.(); throw error; }
|
|
56
|
+
if (!claim.execute) {
|
|
57
|
+
await prepared?.dispose?.();
|
|
58
|
+
if (claim.delivery.state === 'running') return this.finish(effect, path, { state: 'unknown', error: null });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (preparationError) return this.finish(effect, path, failed(preparationError));
|
|
62
|
+
c.state.effects[id] = { state: 'started', incarnation_id: c.state.incarnation_id }; c.save();
|
|
63
|
+
let result;
|
|
64
|
+
try {
|
|
65
|
+
if (c.abort.signal.aborted || effect.expires_at <= c.clock.nowMs()) {
|
|
66
|
+
result = failed(Object.assign(new Error('effect expired before native execution'), { code: 'forbidden' }));
|
|
67
|
+
} else {
|
|
68
|
+
const receipt = await prepared.execute();
|
|
69
|
+
result = { state: 'succeeded', receipt: { native_session_id: receipt?.native_session_id ?? null, runtime_id: receipt?.runtime_id ?? null } };
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// Only a locally proven refusal is a failure. Timeout, transport loss and an
|
|
73
|
+
// arbitrary native exception cannot prove the harness did not perform the action.
|
|
74
|
+
result = error.nativeOutcome === 'failed' ? failed(error) : { state: 'unknown', error: null };
|
|
75
|
+
} finally { await prepared?.dispose?.(); }
|
|
76
|
+
return this.finish(effect, path, result);
|
|
77
|
+
}
|
|
78
|
+
async finish(effect, path, result) {
|
|
79
|
+
const c = this.connector;
|
|
80
|
+
c.state.effects[effect.id] = { state: 'finished', incarnation_id: c.state.incarnation_id, result }; c.save();
|
|
81
|
+
return this.report(effect, path, result);
|
|
82
|
+
}
|
|
83
|
+
async report(effect, path, result) {
|
|
84
|
+
try { await this.connector.effectRequest('POST', `${path}/result`, result); }
|
|
85
|
+
catch (error) { if ([401,403,404,409].includes(error.status)) await this.adapter.discard?.(effect); throw error; }
|
|
86
|
+
if (result.state === 'succeeded') await this.adapter.activate?.(effect);
|
|
87
|
+
else await this.adapter.discard?.(effect);
|
|
88
|
+
this.connector.state.effects[effect.id].reported = true; this.connector.save();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function failed(error) {
|
|
92
|
+
const nativeCode = ({busy:'controller_busy',controller_required:'controller_busy',lease_expired:'conflict',unauthenticated:'authentication_required',unauthorized:'forbidden',unsupported_action:'unsupported',invalid_argument:'invalid_request'})[error.sdkCode];
|
|
93
|
+
const code = [nativeCode, error.sdkCode, error.data?.code, error.code].find(value => ERROR_CODES.includes(value)) ?? 'unsupported';
|
|
94
|
+
return { state: 'failed', error: { code, message: error.teamsSafeMessage ? error.message : 'native adapter could not safely complete the requested action', request_id: 'local', retryable: false, details: null } };
|
|
95
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CaptureReceipt, Coverage, HarnessId, NormalizedMessage, SyncRule, NativeEffect } from '../contracts/index.js';
|
|
2
|
+
|
|
3
|
+
import type { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
4
|
+
|
|
5
|
+
export interface ConnectorClock {
|
|
6
|
+
nowMs(): number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LoadedConnectorSession {
|
|
10
|
+
messages?: Array<NormalizedMessage | ({ value: NormalizedMessage } & Record<string, unknown>)>;
|
|
11
|
+
turns?: Array<NormalizedMessage | ({ value: NormalizedMessage } & Record<string, unknown>)>;
|
|
12
|
+
title?: string | null;
|
|
13
|
+
repository?: unknown;
|
|
14
|
+
recorded_activity_at?: number | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ConnectorCore {
|
|
18
|
+
translateSession?: SupercodeHarnessClient['translateSession'];
|
|
19
|
+
runtimeCapabilities?: SupercodeHarnessClient['runtimeCapabilities'];
|
|
20
|
+
attachManagedRuntime?: SupercodeHarnessClient['attachManagedRuntime'];
|
|
21
|
+
resumeManagedRuntime?: SupercodeHarnessClient['resumeManagedRuntime'];
|
|
22
|
+
materializeSession?: SupercodeHarnessClient['materializeSession'];
|
|
23
|
+
session(locator: unknown): { load(): Promise<LoadedConnectorSession>; follow?(options?: { signal?: AbortSignal }): AsyncIterable<unknown> };
|
|
24
|
+
discover?(query: { workspace?: string; harnesses?: HarnessId[]; homes?: unknown; limit?: number; cursor?: string }): Promise<{ sessions?: Array<{ locator: { harness?: HarnessId; session_id?: string } & Record<string, unknown> }>; entries?: Array<{ locator: { harness?: HarnessId; session_id?: string } & Record<string, unknown> }>; next_cursor?: string | null; nextCursor?: string | null }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TeamsConnectorOptions {
|
|
28
|
+
server: string;
|
|
29
|
+
teamId: string;
|
|
30
|
+
machineId: string;
|
|
31
|
+
sourceId: string;
|
|
32
|
+
credential: string;
|
|
33
|
+
stateDir: string;
|
|
34
|
+
core: ConnectorCore;
|
|
35
|
+
/** Optional embedding adapter; ambiguous exceptions are never retried. */
|
|
36
|
+
effectExecutor?: (effect: NativeEffect, context: {core: ConnectorCore; signal: AbortSignal}) => Promise<{native_session_id?: string | null; runtime_id?: string | null} | void>;
|
|
37
|
+
clock?: ConnectorClock;
|
|
38
|
+
fetch?: typeof globalThis.fetch;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CaptureOptions {
|
|
42
|
+
locator: unknown;
|
|
43
|
+
descriptor?: { title?: string | null; updated_at_ms?: number | null; parent_session_id?: string | null } | null;
|
|
44
|
+
harness: HarnessId;
|
|
45
|
+
nativeSessionId: string;
|
|
46
|
+
visibility?: 'team' | 'restricted';
|
|
47
|
+
ruleId: string;
|
|
48
|
+
ruleRevision: number;
|
|
49
|
+
coverage?: Coverage;
|
|
50
|
+
backfill?: 'none' | 'all' | { mode: 'none' | 'all' } | { mode: 'since'; since: number };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ConnectorConfigurationValue {
|
|
54
|
+
machine: unknown;
|
|
55
|
+
source: unknown;
|
|
56
|
+
rules: SyncRule[];
|
|
57
|
+
authorization_revision: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export declare class TeamsConnector {
|
|
61
|
+
constructor(options: TeamsConnectorOptions);
|
|
62
|
+
readonly lastError?: unknown;
|
|
63
|
+
configuration(): Promise<ConnectorConfigurationValue>;
|
|
64
|
+
start(options?: { bindings?: () => unknown | Promise<unknown>; roots?: () => Array<{ id: string; machine_id: string; name: string; harnesses: HarnessId[]; path?: string; workspace?: string; homes?: unknown }> | Promise<Array<{ id: string; machine_id: string; name: string; harnesses: HarnessId[]; path?: string; workspace?: string; homes?: unknown }>> }): Promise<ConnectorConfigurationValue>;
|
|
65
|
+
capture(options: CaptureOptions): Promise<CaptureReceipt | null>;
|
|
66
|
+
flush(): Promise<CaptureReceipt | null>;
|
|
67
|
+
pollEffects(): Promise<void>;
|
|
68
|
+
stop(): Promise<void>;
|
|
69
|
+
}
|