@calliopelabs/cli 3.3.1 → 3.4.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/dist/attach.d.ts +26 -11
- package/dist/attach.d.ts.map +1 -1
- package/dist/attach.js +219 -98
- package/dist/attach.js.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +26 -36
- package/dist/policy.js.map +1 -1
- package/dist/runtime/permissions.d.ts +2 -0
- package/dist/runtime/permissions.d.ts.map +1 -1
- package/dist/runtime/permissions.js +33 -0
- package/dist/runtime/permissions.js.map +1 -1
- package/dist/runtime/turn.js +39 -14
- package/dist/runtime/turn.js.map +1 -1
- package/dist/sandbox/docker.d.ts.map +1 -1
- package/dist/sandbox/docker.js +7 -2
- package/dist/sandbox/docker.js.map +1 -1
- package/dist/ui/error-boundary.js +1 -1
- package/dist/ui/error-boundary.js.map +1 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +3 -0
- package/dist/ui/index.js.map +1 -1
- package/package.json +5 -1
package/dist/attach.d.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
/** Protocol versions offered in `initialize`; keep in step with calliope-vscode scripts/ahp-supported-versions.json. */
|
|
24
24
|
export declare const SUPPORTED_VERSIONS: string[];
|
|
25
|
+
export declare const ATTACH_TIMEOUT_MS = 60000;
|
|
25
26
|
export type FallbackReason = 'disabled' | 'unentitled' | 'auth' | 'version' | 'unreachable';
|
|
26
27
|
export declare const FALLBACK_EXIT = 3;
|
|
27
28
|
type Fail = {
|
|
@@ -33,10 +34,13 @@ export declare function isDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
|
33
34
|
/** A session's default chat channel: `ahp-chat://default/<base64url(session)>`. */
|
|
34
35
|
export declare function defaultChatUri(session: string): string;
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
37
|
+
* Classify authentication refusals before upgrading the transport. An authorized
|
|
38
|
+
* agent-host route answers a plain GET with 426 Upgrade Required.
|
|
38
39
|
*/
|
|
39
|
-
export declare function preflight(wsUrl: string, headers: Record<string, string>, fetchImpl?: typeof fetch
|
|
40
|
+
export declare function preflight(wsUrl: string, headers: Record<string, string>, fetchImpl?: typeof fetch, options?: {
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
}): Promise<Fail | undefined>;
|
|
40
44
|
/** Find (or start) the user's agent host on a JupyterHub; returns its WebSocket URL. */
|
|
41
45
|
export declare function resolveHubHost(opts: {
|
|
42
46
|
hubUrl: string;
|
|
@@ -59,19 +63,27 @@ interface Message {
|
|
|
59
63
|
message: string;
|
|
60
64
|
};
|
|
61
65
|
}
|
|
66
|
+
interface AttachSocket {
|
|
67
|
+
send(data: string): void;
|
|
68
|
+
close(): void;
|
|
69
|
+
terminate?(): void;
|
|
70
|
+
addEventListener(type: string, fn: (ev: any) => void): void;
|
|
71
|
+
}
|
|
62
72
|
/** The minimal JSON-RPC client attach needs, over any WebSocket-shaped object. */
|
|
63
73
|
export declare class AhpConnection {
|
|
64
74
|
private readonly ws;
|
|
65
75
|
private nextId;
|
|
66
76
|
private readonly pending;
|
|
77
|
+
private failure?;
|
|
78
|
+
private onClosed?;
|
|
67
79
|
readonly actions: Array<(action: any, channel: string, serverSeq?: number) => void>;
|
|
68
|
-
closed
|
|
69
|
-
constructor(ws:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
set closed(callback: ((detail: string) => void) | undefined);
|
|
81
|
+
constructor(ws: AttachSocket);
|
|
82
|
+
private stop;
|
|
83
|
+
call(method: string, params: Record<string, unknown>, options?: {
|
|
84
|
+
timeoutMs?: number;
|
|
85
|
+
signal?: AbortSignal;
|
|
86
|
+
}): Promise<Message>;
|
|
75
87
|
dispatch(channel: string, clientSeq: number, action: Record<string, unknown>): void;
|
|
76
88
|
close(): void;
|
|
77
89
|
}
|
|
@@ -144,6 +156,9 @@ export interface SessionSnapshot {
|
|
|
144
156
|
export declare function follow(conn: AhpConnection, session: string, io: FollowIo, subscribed?: Promise<ChatSnapshot | undefined>, sessionSubscribed?: Promise<SessionSnapshot | undefined>): {
|
|
145
157
|
done: Promise<string>;
|
|
146
158
|
};
|
|
147
|
-
export declare function runAttach(args: string[], env?: NodeJS.ProcessEnv
|
|
159
|
+
export declare function runAttach(args: string[], env?: NodeJS.ProcessEnv, options?: {
|
|
160
|
+
timeoutMs?: number;
|
|
161
|
+
createSocket?: (url: string, headers: Record<string, string>) => AttachSocket;
|
|
162
|
+
}): Promise<number>;
|
|
148
163
|
export {};
|
|
149
164
|
//# sourceMappingURL=attach.d.ts.map
|
package/dist/attach.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attach.d.ts","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;
|
|
1
|
+
{"version":3,"file":"attach.d.ts","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"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"}
|
package/dist/attach.js
CHANGED
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
* CALLIOPE_AHP=off is the kill switch.
|
|
22
22
|
*/
|
|
23
23
|
import * as readline from 'readline';
|
|
24
|
+
import WebSocket from 'ws';
|
|
24
25
|
/** Protocol versions offered in `initialize`; keep in step with calliope-vscode scripts/ahp-supported-versions.json. */
|
|
25
|
-
export const SUPPORTED_VERSIONS = ['1.0.0', '0.6.0'];
|
|
26
|
+
export const SUPPORTED_VERSIONS = ['1.0.0', '0.9.0', '0.6.0'];
|
|
27
|
+
export const ATTACH_TIMEOUT_MS = 60_000;
|
|
26
28
|
export const FALLBACK_EXIT = 3;
|
|
27
29
|
const ROOT = 'ahp-root://';
|
|
28
30
|
const UNSUPPORTED_PROTOCOL_VERSION = -32005;
|
|
@@ -35,13 +37,17 @@ export function defaultChatUri(session) {
|
|
|
35
37
|
return `ahp-chat://default/${Buffer.from(session).toString('base64url')}`;
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
+
* Classify authentication refusals before upgrading the transport. An authorized
|
|
41
|
+
* agent-host route answers a plain GET with 426 Upgrade Required.
|
|
40
42
|
*/
|
|
41
|
-
export async function preflight(wsUrl, headers, fetchImpl = fetch) {
|
|
43
|
+
export async function preflight(wsUrl, headers, fetchImpl = fetch, options = {}) {
|
|
42
44
|
const http = wsUrl.replace(/^ws/, 'http');
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? ATTACH_TIMEOUT_MS);
|
|
47
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
43
48
|
try {
|
|
44
|
-
const res = await fetchImpl(http, { headers, redirect: 'manual' });
|
|
49
|
+
const res = await fetchImpl(http, { headers, redirect: 'manual', signal });
|
|
50
|
+
void res.body?.cancel().catch(() => { });
|
|
45
51
|
if (res.status === 426 || res.ok) {
|
|
46
52
|
return undefined;
|
|
47
53
|
}
|
|
@@ -49,8 +55,12 @@ export async function preflight(wsUrl, headers, fetchImpl = fetch) {
|
|
|
49
55
|
const toLogin = res.status >= 300 && res.status < 400 && /\/(hub\/)?login|oauth/.test(res.headers.get('location') ?? '');
|
|
50
56
|
return fail(res.status === 401 || res.status === 403 || toLogin ? 'auth' : 'unreachable', `the host route answered HTTP ${res.status}`);
|
|
51
57
|
}
|
|
52
|
-
catch
|
|
53
|
-
return fail('unreachable',
|
|
58
|
+
catch {
|
|
59
|
+
return fail('unreachable', signal.aborted ? 'host preflight timed out' : 'host preflight failed');
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
controller.abort();
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
/** Find (or start) the user's agent host on a JupyterHub; returns its WebSocket URL. */
|
|
@@ -58,10 +68,30 @@ export async function resolveHubHost(opts) {
|
|
|
58
68
|
const { hubUrl, user, token, serverName = 'agenthost', fetchImpl = fetch, timeoutMs = 60_000 } = opts;
|
|
59
69
|
const api = new URL('hub/api/', hubUrl.endsWith('/') ? hubUrl : `${hubUrl}/`);
|
|
60
70
|
const headers = { Authorization: `token ${token}` };
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
73
|
+
const request = (url, init = {}) => fetchImpl(url, {
|
|
74
|
+
...init, signal: controller.signal, redirect: 'error', credentials: 'omit',
|
|
75
|
+
});
|
|
61
76
|
const deadline = Date.now() + timeoutMs;
|
|
62
77
|
try {
|
|
78
|
+
const tap = await request(new URL('/agent-host/status', hubUrl), { headers });
|
|
79
|
+
if (tap.status === 401)
|
|
80
|
+
return fail('auth', 'hub tap refused the credential');
|
|
81
|
+
if (tap.status !== 404) {
|
|
82
|
+
const body = await tap.json();
|
|
83
|
+
if (tap.ok && body?.enabled === true && body.endpoint === '/agent-host/') {
|
|
84
|
+
const ws = new URL('/agent-host/', hubUrl);
|
|
85
|
+
ws.protocol = ws.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
86
|
+
return { ok: true, url: ws.toString() };
|
|
87
|
+
}
|
|
88
|
+
const reason = body?.reason === 'disabled' || body?.reason === 'unentitled' || body?.reason === 'auth'
|
|
89
|
+
? body.reason : tap.status === 403 ? 'auth' : 'unreachable';
|
|
90
|
+
return fail(reason, `hub tap answered ${tap.status}`);
|
|
91
|
+
}
|
|
63
92
|
for (let spawned = false;;) {
|
|
64
|
-
|
|
93
|
+
controller.signal.throwIfAborted();
|
|
94
|
+
const res = await request(new URL(`users/${encodeURIComponent(user)}`, api), { headers });
|
|
65
95
|
if (!res.ok) {
|
|
66
96
|
return fail(res.status === 401 || res.status === 403 ? 'auth' : 'unreachable', `hub user lookup answered ${res.status}`);
|
|
67
97
|
}
|
|
@@ -69,30 +99,35 @@ export async function resolveHubHost(opts) {
|
|
|
69
99
|
const server = body.servers?.[serverName];
|
|
70
100
|
if (server?.ready && server.url) {
|
|
71
101
|
const ws = new URL(`${server.url}agent-host/`, hubUrl);
|
|
102
|
+
if (ws.origin !== new URL(hubUrl).origin)
|
|
103
|
+
return fail('unreachable', 'hub answered a foreign server URL');
|
|
72
104
|
ws.protocol = ws.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
73
105
|
return { ok: true, url: ws.toString() };
|
|
74
106
|
}
|
|
75
|
-
if (!spawned) {
|
|
76
|
-
const spawn = await
|
|
77
|
-
|
|
107
|
+
if (!spawned && !server?.pending) {
|
|
108
|
+
const spawn = await request(new URL(`users/${encodeURIComponent(user)}/servers/${encodeURIComponent(serverName)}`, api), {
|
|
109
|
+
method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ profile: serverName }),
|
|
110
|
+
});
|
|
111
|
+
if (spawn.status === 401)
|
|
78
112
|
return fail('auth', 'hub refused the token');
|
|
79
|
-
}
|
|
80
113
|
if (spawn.status === 403 || (spawn.status === 400 && !server)) {
|
|
81
114
|
return fail('unentitled', `hub refused to start ${serverName}: ${spawn.status}`);
|
|
82
115
|
}
|
|
83
|
-
if (!spawn.ok && spawn.status !== 400)
|
|
116
|
+
if (!spawn.ok && spawn.status !== 400)
|
|
84
117
|
return fail('unreachable', `hub could not start ${serverName}: ${spawn.status}`);
|
|
85
|
-
}
|
|
86
118
|
spawned = true;
|
|
87
119
|
}
|
|
88
|
-
if (Date.now() >= deadline)
|
|
120
|
+
if (Date.now() >= deadline)
|
|
89
121
|
return fail('unreachable', `${serverName} not ready within ${Math.round(timeoutMs / 1000)}s`);
|
|
90
|
-
|
|
91
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
122
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(1000, Math.max(0, deadline - Date.now()))));
|
|
92
123
|
}
|
|
93
124
|
}
|
|
94
125
|
catch (err) {
|
|
95
|
-
return fail('unreachable', `hub unreachable: ${err.message}`);
|
|
126
|
+
return fail('unreachable', controller.signal.aborted ? 'hub discovery timed out' : `hub unreachable: ${err.message}`);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
controller.abort();
|
|
96
131
|
}
|
|
97
132
|
}
|
|
98
133
|
/** The minimal JSON-RPC client attach needs, over any WebSocket-shaped object. */
|
|
@@ -100,15 +135,34 @@ export class AhpConnection {
|
|
|
100
135
|
ws;
|
|
101
136
|
nextId = 1;
|
|
102
137
|
pending = new Map();
|
|
138
|
+
failure;
|
|
139
|
+
onClosed;
|
|
103
140
|
actions = [];
|
|
104
|
-
closed
|
|
141
|
+
set closed(callback) {
|
|
142
|
+
this.onClosed = callback;
|
|
143
|
+
if (this.failure)
|
|
144
|
+
callback?.(this.failure);
|
|
145
|
+
}
|
|
105
146
|
constructor(ws) {
|
|
106
147
|
this.ws = ws;
|
|
107
148
|
ws.addEventListener('message', ev => {
|
|
108
|
-
|
|
149
|
+
if (this.failure)
|
|
150
|
+
return;
|
|
151
|
+
let msg;
|
|
152
|
+
try {
|
|
153
|
+
msg = JSON.parse(String(ev.data));
|
|
154
|
+
if (!msg || typeof msg !== 'object' || Array.isArray(msg))
|
|
155
|
+
throw new Error('invalid frame');
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
this.stop('invalid host message');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
109
161
|
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
110
|
-
this.pending.get(msg.id)
|
|
162
|
+
const pending = this.pending.get(msg.id);
|
|
111
163
|
this.pending.delete(msg.id);
|
|
164
|
+
pending.cleanup();
|
|
165
|
+
pending.resolve(msg);
|
|
112
166
|
return;
|
|
113
167
|
}
|
|
114
168
|
if (msg.method === 'action' && msg.params?.action) {
|
|
@@ -117,20 +171,51 @@ export class AhpConnection {
|
|
|
117
171
|
}
|
|
118
172
|
}
|
|
119
173
|
});
|
|
120
|
-
ws.addEventListener('close', ev => this.
|
|
174
|
+
ws.addEventListener('close', ev => this.stop(`connection closed (${ev.code ?? 'unknown'})`));
|
|
175
|
+
ws.addEventListener('error', () => this.stop('host transport failed'));
|
|
121
176
|
}
|
|
122
|
-
|
|
177
|
+
stop(detail) {
|
|
178
|
+
if (this.failure)
|
|
179
|
+
return;
|
|
180
|
+
this.failure = detail;
|
|
181
|
+
for (const pending of this.pending.values()) {
|
|
182
|
+
pending.cleanup();
|
|
183
|
+
pending.resolve({ error: { code: -32000, message: detail } });
|
|
184
|
+
}
|
|
185
|
+
this.pending.clear();
|
|
186
|
+
this.onClosed?.(detail);
|
|
187
|
+
try {
|
|
188
|
+
this.ws.terminate ? this.ws.terminate() : this.ws.close();
|
|
189
|
+
}
|
|
190
|
+
catch { /* Already closed. */ }
|
|
191
|
+
}
|
|
192
|
+
call(method, params, options = {}) {
|
|
193
|
+
if (this.failure)
|
|
194
|
+
return Promise.resolve({ error: { code: -32000, message: this.failure } });
|
|
195
|
+
if (options.signal?.aborted) {
|
|
196
|
+
this.stop('host setup timed out');
|
|
197
|
+
return Promise.resolve({ error: { code: -32000, message: this.failure } });
|
|
198
|
+
}
|
|
123
199
|
const id = this.nextId++;
|
|
124
200
|
return new Promise(resolve => {
|
|
125
|
-
this.
|
|
126
|
-
|
|
201
|
+
const aborted = () => this.stop('host setup timed out');
|
|
202
|
+
const timer = setTimeout(() => this.stop('host request timed out'), options.timeoutMs ?? ATTACH_TIMEOUT_MS);
|
|
203
|
+
const cleanup = () => { clearTimeout(timer); options.signal?.removeEventListener('abort', aborted); };
|
|
204
|
+
this.pending.set(id, { resolve, cleanup });
|
|
205
|
+
options.signal?.addEventListener('abort', aborted, { once: true });
|
|
206
|
+
try {
|
|
207
|
+
this.ws.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
this.stop('host request failed');
|
|
211
|
+
}
|
|
127
212
|
});
|
|
128
213
|
}
|
|
129
214
|
dispatch(channel, clientSeq, action) {
|
|
130
215
|
this.ws.send(JSON.stringify({ jsonrpc: '2.0', method: 'dispatchAction', params: { channel, clientSeq, action } }));
|
|
131
216
|
}
|
|
132
217
|
close() {
|
|
133
|
-
this.
|
|
218
|
+
this.stop('connection closed');
|
|
134
219
|
}
|
|
135
220
|
}
|
|
136
221
|
/** Classify the `initialize` answer; undefined when the host is usable. */
|
|
@@ -162,6 +247,7 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
|
|
|
162
247
|
const chat = defaultChatUri(session);
|
|
163
248
|
let seq = 0;
|
|
164
249
|
let finish;
|
|
250
|
+
let ended = false;
|
|
165
251
|
const done = new Promise(resolve => { finish = resolve; });
|
|
166
252
|
// Confirmations this client knows are open, by tool call, each able to
|
|
167
253
|
// withdraw its prompt. One prompt at a time: a snapshot can hold several, and
|
|
@@ -191,11 +277,14 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
|
|
|
191
277
|
};
|
|
192
278
|
// Following is over: no prompt outlives it, or its readline keeps the process alive.
|
|
193
279
|
const end = (reason) => {
|
|
280
|
+
if (ended)
|
|
281
|
+
return;
|
|
282
|
+
ended = true;
|
|
194
283
|
settleAll();
|
|
195
284
|
finish(reason);
|
|
196
285
|
};
|
|
197
286
|
const confirm = (target, turnId, toolCallId, confirmationTitle, toolInput, request) => {
|
|
198
|
-
if (open.has(toolCallId)) {
|
|
287
|
+
if (ended || open.has(toolCallId)) {
|
|
199
288
|
return;
|
|
200
289
|
}
|
|
201
290
|
const asked = new AbortController();
|
|
@@ -231,8 +320,8 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
|
|
|
231
320
|
io.write(action.content ?? '');
|
|
232
321
|
break;
|
|
233
322
|
case 'chat/toolCallReady':
|
|
234
|
-
if (action.
|
|
235
|
-
confirm(chat, action.turnId, action.toolCallId, action.confirmationTitle, action.toolInput);
|
|
323
|
+
if (!action.confirmed) {
|
|
324
|
+
confirm(chat, action.turnId, action.toolCallId, action.confirmationTitle ?? action.invocationMessage, action.toolInput);
|
|
236
325
|
}
|
|
237
326
|
break;
|
|
238
327
|
case 'chat/toolCallConfirmed':
|
|
@@ -244,6 +333,9 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
|
|
|
244
333
|
break;
|
|
245
334
|
case 'chat/turnCancelled':
|
|
246
335
|
settleTurn();
|
|
336
|
+
io.write('\n[turn cancelled]\n');
|
|
337
|
+
if (io.once)
|
|
338
|
+
end('cancelled');
|
|
247
339
|
break;
|
|
248
340
|
case 'chat/turnComplete':
|
|
249
341
|
settleTurn();
|
|
@@ -254,7 +346,7 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
|
|
|
254
346
|
break;
|
|
255
347
|
case 'chat/error':
|
|
256
348
|
settleTurn();
|
|
257
|
-
io.write(`\n[turn error] ${JSON.stringify(action.error ?? action).slice(0, 200)}\n`);
|
|
349
|
+
io.write(`\n[turn error] ${JSON.stringify(action.part?.error ?? action.error ?? action).slice(0, 200)}\n`);
|
|
258
350
|
if (io.once) {
|
|
259
351
|
end('error');
|
|
260
352
|
}
|
|
@@ -293,7 +385,7 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
|
|
|
293
385
|
const track = (channel, onAction, seed, snapshot) => {
|
|
294
386
|
let held = snapshot ? [] : undefined;
|
|
295
387
|
conn.actions.push((action, from, serverSeq) => {
|
|
296
|
-
if (from !== channel) {
|
|
388
|
+
if (ended || from !== channel) {
|
|
297
389
|
return;
|
|
298
390
|
}
|
|
299
391
|
if (held) {
|
|
@@ -303,6 +395,8 @@ export function follow(conn, session, io, subscribed, sessionSubscribed) {
|
|
|
303
395
|
onAction(action);
|
|
304
396
|
});
|
|
305
397
|
const join = (joined) => {
|
|
398
|
+
if (ended)
|
|
399
|
+
return;
|
|
306
400
|
seed(joined);
|
|
307
401
|
const early = held ?? [];
|
|
308
402
|
held = undefined;
|
|
@@ -364,7 +458,7 @@ function askYesNo(title, input, signal) {
|
|
|
364
458
|
rl.question(`\n${title}\n ${input}\nApprove? [y/N] `, answer => end(/^y(es)?$/i.test(answer.trim())));
|
|
365
459
|
});
|
|
366
460
|
}
|
|
367
|
-
export async function runAttach(args, env = process.env) {
|
|
461
|
+
export async function runAttach(args, env = process.env, options = {}) {
|
|
368
462
|
const opts = parseArgs(args);
|
|
369
463
|
const err = (s) => process.stderr.write(`${s}\n`);
|
|
370
464
|
const fallback = (f) => {
|
|
@@ -372,79 +466,106 @@ export async function runAttach(args, env = process.env) {
|
|
|
372
466
|
err('Falling back: run `calliope` to work locally.');
|
|
373
467
|
return FALLBACK_EXIT;
|
|
374
468
|
};
|
|
375
|
-
if (isDisabled(env))
|
|
469
|
+
if (isDisabled(env))
|
|
376
470
|
return fallback(fail('disabled', 'CALLIOPE_AHP is off'));
|
|
471
|
+
const timeoutMs = options.timeoutMs ?? ATTACH_TIMEOUT_MS;
|
|
472
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120_000) {
|
|
473
|
+
return fallback(fail('unreachable', 'invalid attach setup timeout'));
|
|
377
474
|
}
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
475
|
+
const controller = new AbortController();
|
|
476
|
+
const deadline = Date.now() + timeoutMs;
|
|
477
|
+
const remaining = () => Math.max(1, deadline - Date.now());
|
|
478
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
479
|
+
let ws;
|
|
480
|
+
let conn;
|
|
481
|
+
try {
|
|
482
|
+
const token = env[opts.tokenEnv];
|
|
483
|
+
const headers = token ? { Authorization: `token ${token}` } : {};
|
|
484
|
+
let url = opts.url;
|
|
485
|
+
if (!url && opts.hub && opts.user) {
|
|
486
|
+
if (!token)
|
|
487
|
+
return fallback(fail('auth', `set ${opts.tokenEnv} to a hub API token`));
|
|
488
|
+
const resolved = await resolveHubHost({ hubUrl: opts.hub, user: opts.user, token, timeoutMs: remaining() });
|
|
489
|
+
if (!resolved.ok)
|
|
490
|
+
return fallback(resolved);
|
|
491
|
+
url = resolved.url;
|
|
384
492
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return
|
|
493
|
+
if (!url) {
|
|
494
|
+
err('usage: calliope attach (--url <ws-url> | --hub <url> --user <name>) [session-uri] [--read-only]');
|
|
495
|
+
return 1;
|
|
388
496
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
497
|
+
const refused = await preflight(url, headers, fetch, { signal: controller.signal, timeoutMs: remaining() });
|
|
498
|
+
if (refused)
|
|
499
|
+
return fallback(refused);
|
|
500
|
+
controller.signal.throwIfAborted();
|
|
501
|
+
ws = options.createSocket ? options.createSocket(url, headers) : new WebSocket(url, { headers });
|
|
502
|
+
const socket = ws;
|
|
503
|
+
const opened = await new Promise(resolve => {
|
|
504
|
+
const finish = (ok) => { controller.signal.removeEventListener('abort', aborted); resolve(ok); };
|
|
505
|
+
const aborted = () => { try {
|
|
506
|
+
socket.terminate ? socket.terminate() : socket.close();
|
|
507
|
+
}
|
|
508
|
+
catch { /* Already closed. */ } finish(false); };
|
|
509
|
+
socket.addEventListener('open', () => finish(true));
|
|
510
|
+
socket.addEventListener('error', () => finish(false));
|
|
511
|
+
socket.addEventListener('close', () => finish(false));
|
|
512
|
+
controller.signal.addEventListener('abort', aborted, { once: true });
|
|
513
|
+
if (controller.signal.aborted)
|
|
514
|
+
aborted();
|
|
515
|
+
});
|
|
516
|
+
if (!opened)
|
|
517
|
+
return fallback(fail('unreachable', controller.signal.aborted ? 'host setup timed out' : 'the WebSocket upgrade failed'));
|
|
518
|
+
conn = new AhpConnection(socket);
|
|
519
|
+
const call = (method, params) => conn.call(method, params, { signal: controller.signal, timeoutMs: remaining() });
|
|
520
|
+
const init = await call('initialize', {
|
|
521
|
+
channel: ROOT, protocolVersions: SUPPORTED_VERSIONS, clientId: `calliope-cli-${process.pid}`,
|
|
522
|
+
clientInfo: { name: 'calliope-cli', version: '1' }, initialSubscriptions: [ROOT],
|
|
523
|
+
});
|
|
524
|
+
const bad = checkInitialize(init);
|
|
525
|
+
if (bad)
|
|
526
|
+
return fallback(bad);
|
|
527
|
+
if (!opts.session) {
|
|
528
|
+
const list = await call('listSessions', { channel: ROOT });
|
|
529
|
+
if (list.error)
|
|
530
|
+
return fallback(fail('unreachable', 'the host could not list sessions'));
|
|
531
|
+
const items = (list.result?.items ?? []);
|
|
532
|
+
process.stdout.write(items.length ? '' : 'No sessions on this host.\n');
|
|
533
|
+
for (const session of items)
|
|
534
|
+
process.stdout.write(`${session.resource} ${session.title ?? ''} ${session.modifiedAt ?? ''}\n`);
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
// Register follow before either subscription answers: frames may follow a
|
|
538
|
+
// snapshot in the same network read. Keep the setup deadline through both.
|
|
539
|
+
const chat = defaultChatUri(opts.session);
|
|
540
|
+
const sessionAnswer = call('subscribe', { channel: opts.session });
|
|
541
|
+
const chatAnswer = sessionAnswer.then(answer => answer.error ? answer : call('subscribe', { channel: chat }));
|
|
542
|
+
const { done } = follow(conn, opts.session, {
|
|
543
|
+
write: s => process.stdout.write(s), approve: opts.readOnly ? undefined : askYesNo, once: opts.once,
|
|
544
|
+
}, chatAnswer.then(answer => answer.result?.snapshot), sessionAnswer.then(answer => answer.result?.snapshot));
|
|
545
|
+
const answers = await Promise.all([sessionAnswer, chatAnswer]);
|
|
546
|
+
if (answers.some(answer => answer.error))
|
|
547
|
+
return fallback(fail('unreachable', 'the host could not subscribe to the session'));
|
|
548
|
+
clearTimeout(timer);
|
|
549
|
+
process.stderr.write(`attached to ${opts.session} (AHP ${init.result.protocolVersion}); Ctrl+C to detach\n`);
|
|
550
|
+
const ended = await done;
|
|
551
|
+
if (ended.startsWith('connection') || ended.startsWith('host') || ended.startsWith('invalid')) {
|
|
552
|
+
return fallback(fail('unreachable', 'the host connection was lost; the run remains on its host'));
|
|
553
|
+
}
|
|
554
|
+
return ended === 'turnComplete' ? 0 : 1;
|
|
406
555
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
channel: ROOT, protocolVersions: SUPPORTED_VERSIONS, clientId: `calliope-cli-${process.pid}`,
|
|
410
|
-
clientInfo: { name: 'calliope-cli', version: '1' }, initialSubscriptions: [ROOT],
|
|
411
|
-
});
|
|
412
|
-
const bad = checkInitialize(init);
|
|
413
|
-
if (bad) {
|
|
414
|
-
conn.close();
|
|
415
|
-
return fallback(bad);
|
|
556
|
+
catch {
|
|
557
|
+
return fallback(fail('unreachable', controller.signal.aborted ? 'host setup timed out' : 'host attachment failed'));
|
|
416
558
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
for (const s of items) {
|
|
427
|
-
process.stdout.write(`${s.resource} ${s.title ?? ''} ${s.modifiedAt ?? ''}\n`);
|
|
559
|
+
finally {
|
|
560
|
+
clearTimeout(timer);
|
|
561
|
+
controller.abort();
|
|
562
|
+
conn?.close();
|
|
563
|
+
if (!conn && ws) {
|
|
564
|
+
try {
|
|
565
|
+
ws.terminate ? ws.terminate() : ws.close();
|
|
566
|
+
}
|
|
567
|
+
catch { /* Already closed. */ }
|
|
428
568
|
}
|
|
429
|
-
conn.close();
|
|
430
|
-
return 0;
|
|
431
569
|
}
|
|
432
|
-
// The session snapshot lists the input waiting in every chat, a subagent's
|
|
433
|
-
// included; the chat snapshot holds the confirmations waiting on the default
|
|
434
|
-
// chat, which is subscribed once the session answers. follow() listens from
|
|
435
|
-
// the moment the first request goes out: an action can share a network read
|
|
436
|
-
// with an answer, and Node delivers it before an awaited call resumes.
|
|
437
|
-
const chat = defaultChatUri(opts.session);
|
|
438
|
-
const sessionAnswer = conn.call('subscribe', { channel: opts.session });
|
|
439
|
-
const chatAnswer = sessionAnswer.then(() => conn.call('subscribe', { channel: chat }));
|
|
440
|
-
const { done } = follow(conn, opts.session, {
|
|
441
|
-
write: s => process.stdout.write(s),
|
|
442
|
-
approve: opts.readOnly ? undefined : askYesNo,
|
|
443
|
-
once: opts.once,
|
|
444
|
-
}, chatAnswer.then(answer => answer.result?.snapshot), sessionAnswer.then(answer => answer.result?.snapshot));
|
|
445
|
-
process.stderr.write(`attached to ${opts.session} (AHP ${init.result.protocolVersion}); Ctrl+C to detach\n`);
|
|
446
|
-
const ended = await done;
|
|
447
|
-
conn.close();
|
|
448
|
-
return ended === 'turnComplete' ? 0 : 1;
|
|
449
570
|
}
|
|
450
571
|
//# sourceMappingURL=attach.js.map
|