@turns/dev 0.0.11 → 0.0.12
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/agent-anchor.js +6 -2
- package/dist/codex-update.js +93 -0
- package/dist/harness-auth.js +289 -0
- package/dist/queue-worker.js +77 -1
- package/package.json +1 -1
package/dist/agent-anchor.js
CHANGED
|
@@ -599,14 +599,18 @@ class CodexAppServerTransport extends LineTransport {
|
|
|
599
599
|
? await this.request('thread/start', {
|
|
600
600
|
...configuration,
|
|
601
601
|
...(this.anchor.model && this.anchor.model !== 'auto' ? { model: this.anchor.model } : {}),
|
|
602
|
-
...(this.anchor.thinkingLevel
|
|
602
|
+
...(this.anchor.thinkingLevel
|
|
603
|
+
? { config: { model_reasoning_effort: this.anchor.thinkingLevel } }
|
|
604
|
+
: {}),
|
|
603
605
|
...(this.anchor.codexHistoryMode === 'paginated' ? { historyMode: 'paginated' } : {}),
|
|
604
606
|
}, 90_000)
|
|
605
607
|
: await this.request('thread/resume', {
|
|
606
608
|
threadId: this.threadId,
|
|
607
609
|
...configuration,
|
|
608
610
|
...(this.anchor.model && this.anchor.model !== 'auto' ? { model: this.anchor.model } : {}),
|
|
609
|
-
...(this.anchor.thinkingLevel
|
|
611
|
+
...(this.anchor.thinkingLevel
|
|
612
|
+
? { config: { model_reasoning_effort: this.anchor.thinkingLevel } }
|
|
613
|
+
: {}),
|
|
610
614
|
}, 90_000);
|
|
611
615
|
const threadId = String(opened?.thread?.id || opened?.threadId || opened?.id || '');
|
|
612
616
|
if (!threadId)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Machine-local Codex CLI self-update, callable by the Turns daemon. */
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { resolveHarnessCommand } from "./harness-registry.js";
|
|
5
|
+
const VERSION_TIMEOUT_MS = 15_000;
|
|
6
|
+
const UPDATE_TIMEOUT_MS = 5 * 60_000;
|
|
7
|
+
const OUTPUT_LIMIT = 64 * 1024;
|
|
8
|
+
/** Run the resolved executable directly—never through a shell—so update
|
|
9
|
+
* requests cannot become arbitrary command execution. */
|
|
10
|
+
function runCommand(command, args, timeoutMs) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const child = spawn(command, args, {
|
|
13
|
+
cwd: os.homedir(),
|
|
14
|
+
env: process.env,
|
|
15
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
16
|
+
});
|
|
17
|
+
let output = '';
|
|
18
|
+
let settled = false;
|
|
19
|
+
const settle = (result) => {
|
|
20
|
+
if (settled)
|
|
21
|
+
return;
|
|
22
|
+
settled = true;
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
resolve(result);
|
|
25
|
+
};
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
try {
|
|
28
|
+
child.kill('SIGKILL');
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
settle({ output, exitCode: null, timedOut: true });
|
|
32
|
+
}, timeoutMs);
|
|
33
|
+
timer.unref?.();
|
|
34
|
+
const collect = (chunk) => {
|
|
35
|
+
if (output.length < OUTPUT_LIMIT)
|
|
36
|
+
output += String(chunk).slice(0, OUTPUT_LIMIT - output.length);
|
|
37
|
+
};
|
|
38
|
+
child.stdout.on('data', collect);
|
|
39
|
+
child.stderr.on('data', collect);
|
|
40
|
+
child.on('error', () => settle({ output, exitCode: null, failed: true }));
|
|
41
|
+
child.on('close', (exitCode) => settle({ output, exitCode }));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function parseCodexVersion(output) {
|
|
45
|
+
const text = String(output || '').trim();
|
|
46
|
+
const prefixed = text.match(/codex(?:-cli)?\s+([^\s]+)/iu);
|
|
47
|
+
return (prefixed?.[1] || text.split(/\s+/u).at(-1) || '').slice(0, 120);
|
|
48
|
+
}
|
|
49
|
+
function commandFailure(result, fallback) {
|
|
50
|
+
if (result.timedOut)
|
|
51
|
+
return new Error('The Codex update timed out.');
|
|
52
|
+
const lines = result.output
|
|
53
|
+
.replace(/\u001b\[[0-9;?]*[A-Za-z]/gu, '')
|
|
54
|
+
.split('\n')
|
|
55
|
+
.map((line) => line.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
return new Error(lines.at(-1) || fallback);
|
|
58
|
+
}
|
|
59
|
+
export function createCodexUpdateManager(dependencies = {}) {
|
|
60
|
+
const resolveCommand = dependencies.resolveCommand || (() => resolveHarnessCommand('codex'));
|
|
61
|
+
const run = dependencies.run || runCommand;
|
|
62
|
+
let active = null;
|
|
63
|
+
const perform = async () => {
|
|
64
|
+
const beforeRun = await run(resolveCommand(), ['--version'], VERSION_TIMEOUT_MS);
|
|
65
|
+
if (beforeRun.failed || beforeRun.timedOut || beforeRun.exitCode !== 0) {
|
|
66
|
+
throw commandFailure(beforeRun, 'Codex is not installed or its version could not be read.');
|
|
67
|
+
}
|
|
68
|
+
const previousVersion = parseCodexVersion(beforeRun.output);
|
|
69
|
+
const updateRun = await run(resolveCommand(), ['update'], UPDATE_TIMEOUT_MS);
|
|
70
|
+
if (updateRun.failed || updateRun.timedOut || updateRun.exitCode !== 0) {
|
|
71
|
+
throw commandFailure(updateRun, 'Codex could not update itself. Run `codex update` in a terminal on the machine.');
|
|
72
|
+
}
|
|
73
|
+
// Resolve again because the updater may replace a shim or install path.
|
|
74
|
+
const afterRun = await run(resolveCommand(), ['--version'], VERSION_TIMEOUT_MS);
|
|
75
|
+
if (afterRun.failed || afterRun.timedOut || afterRun.exitCode !== 0) {
|
|
76
|
+
throw commandFailure(afterRun, 'Codex updated, but its new version could not be read.');
|
|
77
|
+
}
|
|
78
|
+
const currentVersion = parseCodexVersion(afterRun.output);
|
|
79
|
+
return {
|
|
80
|
+
...(previousVersion ? { previousVersion } : {}),
|
|
81
|
+
...(currentVersion ? { currentVersion } : {}),
|
|
82
|
+
updated: Boolean(previousVersion && currentVersion && previousVersion !== currentVersion),
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
/** Concurrent clicks share one installer process instead of racing writes. */
|
|
87
|
+
update() {
|
|
88
|
+
if (!active)
|
|
89
|
+
active = perform().finally(() => (active = null));
|
|
90
|
+
return active;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness sign-in on this machine, steered from the browser.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code and Codex each hold their own account credentials, and when a
|
|
5
|
+
* grant expires every turn fails until someone runs the harness's login
|
|
6
|
+
* command in a terminal on this machine. The daemon is that terminal: it can
|
|
7
|
+
* run both CLIs' browserless sign-in flows and relay the two strings a user
|
|
8
|
+
* actually needs — the approval URL and, for Codex's device flow, the
|
|
9
|
+
* one-time code — while accepting the single string that may come back, the
|
|
10
|
+
* authorization code Claude's flow reads on stdin.
|
|
11
|
+
*
|
|
12
|
+
* Both flows are driven entirely by scraping the CLI's human-readable
|
|
13
|
+
* output, because neither CLI offers a machine-readable login mode. The
|
|
14
|
+
* scraping is deliberately loose — the first https URL is the approval
|
|
15
|
+
* link, a short hyphenated all-caps token is the device code — so cosmetic
|
|
16
|
+
* output changes in either CLI degrade to "no URL yet" rather than to a
|
|
17
|
+
* crash. Auth *status*, by contrast, is read from the commands both CLIs
|
|
18
|
+
* commit to: `claude auth status --json` and `codex login status`.
|
|
19
|
+
*
|
|
20
|
+
* One flow per harness runs at a time. Flow state lives in this process and
|
|
21
|
+
* dies with it; the credentials themselves live where the harness put them,
|
|
22
|
+
* so a daemon restart mid-flow only means starting the flow again.
|
|
23
|
+
*/
|
|
24
|
+
import { spawn } from 'node:child_process';
|
|
25
|
+
import os from 'node:os';
|
|
26
|
+
import { resolveHarnessCommand } from "./harness-registry.js";
|
|
27
|
+
/** Harnesses whose sign-in this module can run. Mirrors the shared
|
|
28
|
+
* HARNESS_AUTH_HARNESSES list; cli/ cannot import cloudflare/shared. */
|
|
29
|
+
export const HARNESS_AUTH_HARNESSES = ['claude', 'codex'];
|
|
30
|
+
const STATUS_TIMEOUT_MS = 30_000;
|
|
31
|
+
const STATUS_CACHE_MS = 10_000;
|
|
32
|
+
const FLOW_TIMEOUT_MS = 15 * 60_000;
|
|
33
|
+
const OUTPUT_TAIL_LIMIT = 4_096;
|
|
34
|
+
const ACTIVE_FLOW_STATUSES = new Set(['starting', 'awaiting_approval', 'awaiting_code']);
|
|
35
|
+
/** Terminal control sequences stripped before any output is interpreted. */
|
|
36
|
+
function stripAnsi(text) {
|
|
37
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: terminal escapes are control characters by definition
|
|
38
|
+
return String(text || '').replace(/\u001b\[[0-9;?]*[A-Za-z]/g, '');
|
|
39
|
+
}
|
|
40
|
+
/** The first https URL in a chunk of CLI output, if any. */
|
|
41
|
+
export function extractLoginUrl(text) {
|
|
42
|
+
const match = stripAnsi(text).match(/https:\/\/[^\s"')\]]+/u);
|
|
43
|
+
return match ? match[0] : '';
|
|
44
|
+
}
|
|
45
|
+
/** A short hyphenated all-caps token on its own line: Codex's device code. */
|
|
46
|
+
export function extractDeviceCode(text) {
|
|
47
|
+
for (const line of stripAnsi(text).split('\n')) {
|
|
48
|
+
const match = line.trim().match(/^([A-Z0-9]{3,8}-[A-Z0-9]{3,8})$/u);
|
|
49
|
+
if (match)
|
|
50
|
+
return match[1];
|
|
51
|
+
}
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
/** What `claude auth status --json` reported, from either exit status. The
|
|
55
|
+
* JSON object is cut out of the surrounding text rather than assumed to be
|
|
56
|
+
* the whole of it, because warnings can share the stream with the report. */
|
|
57
|
+
export function parseClaudeAuthStatus(output) {
|
|
58
|
+
try {
|
|
59
|
+
const text = String(output || '');
|
|
60
|
+
const status = JSON.parse(text.slice(text.indexOf('{'), text.lastIndexOf('}') + 1));
|
|
61
|
+
if (typeof status?.loggedIn !== 'boolean')
|
|
62
|
+
return { authenticated: null };
|
|
63
|
+
const account = [status.email, status.subscriptionType ? `(${status.subscriptionType})` : '']
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.join(' ');
|
|
66
|
+
return { authenticated: status.loggedIn, ...(account ? { account } : {}) };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { authenticated: null };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** What `codex login status` reported — on stderr, where codex prints it.
|
|
73
|
+
* The command has no JSON mode, so the two sentences it prints are the
|
|
74
|
+
* interface: "Logged in using …" and "Not logged in". Anything else is
|
|
75
|
+
* treated as unreadable rather than guessed. */
|
|
76
|
+
export function parseCodexLoginStatus(output) {
|
|
77
|
+
const text = stripAnsi(output);
|
|
78
|
+
const loggedIn = text.match(/Logged in(?: using (.+?))?\s*$/mu);
|
|
79
|
+
if (loggedIn)
|
|
80
|
+
return { authenticated: true, ...(loggedIn[1] ? { account: loggedIn[1].trim() } : {}) };
|
|
81
|
+
if (/Not logged in/u.test(text))
|
|
82
|
+
return { authenticated: false };
|
|
83
|
+
return { authenticated: null };
|
|
84
|
+
}
|
|
85
|
+
/** Both streams interleaved: codex reports status on stderr, claude on stdout. */
|
|
86
|
+
function runCommand(command, args, timeoutMs) {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
const child = spawn(command, args, {
|
|
89
|
+
cwd: os.homedir(),
|
|
90
|
+
env: process.env,
|
|
91
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
92
|
+
});
|
|
93
|
+
let output = '';
|
|
94
|
+
let settled = false;
|
|
95
|
+
const settle = (result) => {
|
|
96
|
+
if (settled)
|
|
97
|
+
return;
|
|
98
|
+
settled = true;
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
resolve(result);
|
|
101
|
+
};
|
|
102
|
+
const timer = setTimeout(() => {
|
|
103
|
+
try {
|
|
104
|
+
child.kill('SIGKILL');
|
|
105
|
+
}
|
|
106
|
+
catch { }
|
|
107
|
+
settle({ output, timedOut: true });
|
|
108
|
+
}, timeoutMs);
|
|
109
|
+
timer.unref?.();
|
|
110
|
+
const collect = (chunk) => {
|
|
111
|
+
if (output.length < 64 * 1024)
|
|
112
|
+
output += chunk;
|
|
113
|
+
};
|
|
114
|
+
child.stdout.on('data', collect);
|
|
115
|
+
child.stderr.on('data', collect);
|
|
116
|
+
child.on('error', () => settle({ output, failed: true }));
|
|
117
|
+
child.on('close', () => settle({ output }));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export function createHarnessAuthManager() {
|
|
121
|
+
/** One sign-in flow per harness, keyed by harness id. */
|
|
122
|
+
const flows = new Map();
|
|
123
|
+
/** Recent status answers, so a polling browser does not spawn a CLI per poll. */
|
|
124
|
+
const statusCache = new Map();
|
|
125
|
+
const flowSnapshot = (harness) => {
|
|
126
|
+
const flow = flows.get(harness);
|
|
127
|
+
if (!flow)
|
|
128
|
+
return { status: 'idle' };
|
|
129
|
+
return {
|
|
130
|
+
status: flow.status,
|
|
131
|
+
...(flow.url ? { url: flow.url } : {}),
|
|
132
|
+
...(flow.code ? { code: flow.code } : {}),
|
|
133
|
+
...(flow.needsCode ? { needsCode: true } : {}),
|
|
134
|
+
...(flow.error ? { error: flow.error } : {}),
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
const readStatus = async (harness) => {
|
|
138
|
+
const cached = statusCache.get(harness);
|
|
139
|
+
if (cached && Date.now() - cached.at < STATUS_CACHE_MS)
|
|
140
|
+
return cached.answer;
|
|
141
|
+
let answer = { authenticated: null };
|
|
142
|
+
try {
|
|
143
|
+
const command = resolveHarnessCommand(harness);
|
|
144
|
+
const args = harness === 'claude' ? ['auth', 'status', '--json'] : ['login', 'status'];
|
|
145
|
+
const run = await runCommand(command, args, STATUS_TIMEOUT_MS);
|
|
146
|
+
if (!run.failed && !run.timedOut) {
|
|
147
|
+
answer = harness === 'claude' ? parseClaudeAuthStatus(run.output) : parseCodexLoginStatus(run.output);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// An uninstalled harness reads as unknown, and the flow will say why.
|
|
152
|
+
}
|
|
153
|
+
statusCache.set(harness, { at: Date.now(), answer });
|
|
154
|
+
return answer;
|
|
155
|
+
};
|
|
156
|
+
const finishFlow = (harness, flow, status, error = '') => {
|
|
157
|
+
if (!flows.get(harness) || flows.get(harness) !== flow)
|
|
158
|
+
return;
|
|
159
|
+
flow.status = status;
|
|
160
|
+
flow.error = error || '';
|
|
161
|
+
clearTimeout(flow.timer);
|
|
162
|
+
// A finished flow changed (or failed to change) the credential store, so
|
|
163
|
+
// the next status question must ask the CLI rather than the cache.
|
|
164
|
+
statusCache.delete(harness);
|
|
165
|
+
};
|
|
166
|
+
const startFlow = (harness) => {
|
|
167
|
+
const existing = flows.get(harness);
|
|
168
|
+
if (existing && ACTIVE_FLOW_STATUSES.has(existing.status))
|
|
169
|
+
return;
|
|
170
|
+
const command = resolveHarnessCommand(harness);
|
|
171
|
+
const args = harness === 'claude' ? ['auth', 'login'] : ['login', '--device-auth'];
|
|
172
|
+
const flow = {
|
|
173
|
+
status: 'starting',
|
|
174
|
+
url: '',
|
|
175
|
+
code: '',
|
|
176
|
+
needsCode: false,
|
|
177
|
+
error: '',
|
|
178
|
+
tail: '',
|
|
179
|
+
child: null,
|
|
180
|
+
canceled: false,
|
|
181
|
+
timer: null,
|
|
182
|
+
};
|
|
183
|
+
flows.set(harness, flow);
|
|
184
|
+
const child = spawn(command, args, {
|
|
185
|
+
cwd: os.homedir(),
|
|
186
|
+
env: process.env,
|
|
187
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
188
|
+
});
|
|
189
|
+
flow.child = child;
|
|
190
|
+
flow.timer = setTimeout(() => {
|
|
191
|
+
flow.canceled = true;
|
|
192
|
+
try {
|
|
193
|
+
child.kill('SIGKILL');
|
|
194
|
+
}
|
|
195
|
+
catch { }
|
|
196
|
+
finishFlow(harness, flow, 'failed', 'The sign-in flow timed out.');
|
|
197
|
+
}, FLOW_TIMEOUT_MS);
|
|
198
|
+
flow.timer.unref?.();
|
|
199
|
+
const observe = (chunk) => {
|
|
200
|
+
const text = String(chunk);
|
|
201
|
+
flow.tail = (flow.tail + text).slice(-OUTPUT_TAIL_LIMIT);
|
|
202
|
+
if (!flow.url) {
|
|
203
|
+
const url = extractLoginUrl(text);
|
|
204
|
+
if (url) {
|
|
205
|
+
flow.url = url;
|
|
206
|
+
flow.needsCode = harness === 'claude';
|
|
207
|
+
flow.status = flow.needsCode ? 'awaiting_code' : 'awaiting_approval';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (harness === 'codex' && !flow.code) {
|
|
211
|
+
const code = extractDeviceCode(flow.tail);
|
|
212
|
+
if (code)
|
|
213
|
+
flow.code = code;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
child.stdout.on('data', observe);
|
|
217
|
+
child.stderr.on('data', observe);
|
|
218
|
+
child.on('error', (error) => finishFlow(harness, flow, 'failed', error?.message || 'The sign-in command failed.'));
|
|
219
|
+
child.on('close', (exitCode) => {
|
|
220
|
+
if (flow.status === 'failed')
|
|
221
|
+
return;
|
|
222
|
+
if (flow.canceled) {
|
|
223
|
+
flows.delete(harness);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (exitCode === 0)
|
|
227
|
+
finishFlow(harness, flow, 'succeeded');
|
|
228
|
+
else {
|
|
229
|
+
// The last output line is usually the CLI's own explanation. It has
|
|
230
|
+
// secrets stripped by the caller like every other error string.
|
|
231
|
+
const lines = stripAnsi(flow.tail).split('\n').map((line) => line.trim()).filter(Boolean);
|
|
232
|
+
finishFlow(harness, flow, 'failed', lines.at(-1) || 'The sign-in flow ended without succeeding.');
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
const submitCode = (harness, code) => {
|
|
237
|
+
const flow = flows.get(harness);
|
|
238
|
+
if (!flow || flow.status !== 'awaiting_code' || !flow.child?.stdin?.writable) {
|
|
239
|
+
throw new Error('No sign-in flow is waiting for a code.');
|
|
240
|
+
}
|
|
241
|
+
const line = String(code || '').trim();
|
|
242
|
+
if (!line)
|
|
243
|
+
throw new Error('The pasted code was empty.');
|
|
244
|
+
flow.child.stdin.write(`${line}\n`);
|
|
245
|
+
// The harness is now exchanging the code; only its exit says how it went.
|
|
246
|
+
flow.status = 'awaiting_approval';
|
|
247
|
+
flow.needsCode = false;
|
|
248
|
+
};
|
|
249
|
+
const cancelFlow = (harness) => {
|
|
250
|
+
const flow = flows.get(harness);
|
|
251
|
+
if (!flow)
|
|
252
|
+
return;
|
|
253
|
+
if (ACTIVE_FLOW_STATUSES.has(flow.status)) {
|
|
254
|
+
flow.canceled = true;
|
|
255
|
+
clearTimeout(flow.timer);
|
|
256
|
+
try {
|
|
257
|
+
flow.child?.kill('SIGTERM');
|
|
258
|
+
}
|
|
259
|
+
catch { }
|
|
260
|
+
}
|
|
261
|
+
flows.delete(harness);
|
|
262
|
+
};
|
|
263
|
+
/** Answer one action. Throws with a user-readable message on refusal. */
|
|
264
|
+
const handle = async ({ harness, action, code }) => {
|
|
265
|
+
if (!HARNESS_AUTH_HARNESSES.includes(harness)) {
|
|
266
|
+
throw new Error(`Sign-in is not supported for harness "${harness}".`);
|
|
267
|
+
}
|
|
268
|
+
if (action === 'login')
|
|
269
|
+
startFlow(harness);
|
|
270
|
+
else if (action === 'code')
|
|
271
|
+
submitCode(harness, code);
|
|
272
|
+
else if (action === 'cancel')
|
|
273
|
+
cancelFlow(harness);
|
|
274
|
+
else if (action !== 'status')
|
|
275
|
+
throw new Error(`Unknown harness auth action "${action}".`);
|
|
276
|
+
const flow = flowSnapshot(harness);
|
|
277
|
+
// While a flow is running its own status is the interesting part, and
|
|
278
|
+
// racing the CLI's status command against its login command helps no one.
|
|
279
|
+
const status = ACTIVE_FLOW_STATUSES.has(flow.status)
|
|
280
|
+
? (statusCache.get(harness)?.answer ?? { authenticated: null })
|
|
281
|
+
: await readStatus(harness);
|
|
282
|
+
return { authenticated: status.authenticated, ...(status.account ? { account: status.account } : {}), flow };
|
|
283
|
+
};
|
|
284
|
+
const close = () => {
|
|
285
|
+
for (const harness of [...flows.keys()])
|
|
286
|
+
cancelFlow(harness);
|
|
287
|
+
};
|
|
288
|
+
return { handle, close };
|
|
289
|
+
}
|
package/dist/queue-worker.js
CHANGED
|
@@ -5,11 +5,13 @@ import path from 'node:path';
|
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
7
|
import { anchorId, connectExistingAgentAnchors, launchAgentAnchor, waitForAgentTurn } from "./agent-anchor-client.js";
|
|
8
|
+
import { createCodexUpdateManager } from "./codex-update.js";
|
|
8
9
|
import { assertAllowedWorkingDirectory, workingDirectoryIsAllowed } from "./directory-policy.js";
|
|
9
10
|
import { browseMachinePath, streamMachineFileRange } from "./file-browsing.js";
|
|
10
11
|
import { applyFileEdit } from "./file-edits.js";
|
|
11
12
|
import { searchMachinePaths } from "./file-search.js";
|
|
12
13
|
import { readMachineGitBaseBlob, readMachineGitPatch, readMachineGitStatus } from "./git-status.js";
|
|
14
|
+
import { createHarnessAuthManager } from "./harness-auth.js";
|
|
13
15
|
import { nativeDescriptorAgentSessionId, nativeDescriptorWorkingDirectory } from "./local-session-sources.js";
|
|
14
16
|
import { compactRuntimeValue } from "./native-record-compaction.js";
|
|
15
17
|
import { createSecretRedactor } from "./secret-redaction.js";
|
|
@@ -742,6 +744,8 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
742
744
|
expectedText: claim.prompt.text,
|
|
743
745
|
createSession: creating,
|
|
744
746
|
permissionMode: String(claim.session?.permissionMode || 'bypassPermissions'),
|
|
747
|
+
model: String(claim.session?.model || ''),
|
|
748
|
+
thinkingLevel: String(claim.session?.modelEffort || ''),
|
|
745
749
|
codexHistoryMode: String(claim.session?.codexHistoryMode || ''),
|
|
746
750
|
timeoutMs: QUEUE_TURN_TIMEOUT_MS,
|
|
747
751
|
idleTimeoutMs: QUEUE_WARM_MS,
|
|
@@ -1206,6 +1210,74 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1206
1210
|
});
|
|
1207
1211
|
}
|
|
1208
1212
|
};
|
|
1213
|
+
/**
|
|
1214
|
+
* Answer one harness sign-in question or action for this machine.
|
|
1215
|
+
*
|
|
1216
|
+
* Like browsing, sign-in is a property of the machine rather than of a
|
|
1217
|
+
* session: the session only names which machine and harness the viewer
|
|
1218
|
+
* means. The answer's `flow.url` and `flow.code` deliberately skip the
|
|
1219
|
+
* redactor — they exist to be shown to the user, and the redactor would
|
|
1220
|
+
* mangle an OAuth URL's `state` and `code_challenge` parameters — while
|
|
1221
|
+
* error strings stay redacted like every other handler's.
|
|
1222
|
+
*/
|
|
1223
|
+
const harnessAuth = createHarnessAuthManager();
|
|
1224
|
+
const handleHarnessAuth = async (message, replyRpc) => {
|
|
1225
|
+
const sessionId = String(message.sessionId || '');
|
|
1226
|
+
const harness = String(message.harness || '');
|
|
1227
|
+
try {
|
|
1228
|
+
const answer = await harnessAuth.handle({
|
|
1229
|
+
harness,
|
|
1230
|
+
action: String(message.action || 'status'),
|
|
1231
|
+
code: String(message.code || ''),
|
|
1232
|
+
});
|
|
1233
|
+
if (answer.flow?.error)
|
|
1234
|
+
answer.flow.error = redactor.redactText(answer.flow.error);
|
|
1235
|
+
replyRpc.notify({
|
|
1236
|
+
type: 'session.harness.auth.result',
|
|
1237
|
+
requestId: message.requestId,
|
|
1238
|
+
sessionId,
|
|
1239
|
+
harness,
|
|
1240
|
+
ok: true,
|
|
1241
|
+
answer,
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
catch (error) {
|
|
1245
|
+
replyRpc.notify({
|
|
1246
|
+
type: 'session.harness.auth.result',
|
|
1247
|
+
requestId: message.requestId,
|
|
1248
|
+
sessionId,
|
|
1249
|
+
harness,
|
|
1250
|
+
ok: false,
|
|
1251
|
+
error: redactor.redactText(error?.message || String(error)),
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
/** Upgrade the machine's Codex installation. Only this fixed, resolved
|
|
1256
|
+
* command is exposed; the browser cannot supply a binary, arguments, or
|
|
1257
|
+
* shell input. Errors are redacted before crossing the machine boundary. */
|
|
1258
|
+
const codexUpdate = createCodexUpdateManager();
|
|
1259
|
+
const handleCodexUpdate = async (message, replyRpc) => {
|
|
1260
|
+
const sessionId = String(message.sessionId || '');
|
|
1261
|
+
try {
|
|
1262
|
+
const answer = await codexUpdate.update();
|
|
1263
|
+
replyRpc.notify({
|
|
1264
|
+
type: 'session.codex.update.result',
|
|
1265
|
+
requestId: message.requestId,
|
|
1266
|
+
sessionId,
|
|
1267
|
+
ok: true,
|
|
1268
|
+
answer,
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
catch (error) {
|
|
1272
|
+
replyRpc.notify({
|
|
1273
|
+
type: 'session.codex.update.result',
|
|
1274
|
+
requestId: message.requestId,
|
|
1275
|
+
sessionId,
|
|
1276
|
+
ok: false,
|
|
1277
|
+
error: redactor.redactText(error?.message || String(error)),
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1209
1281
|
/**
|
|
1210
1282
|
* Apply one queued browser edit, and report how it went.
|
|
1211
1283
|
*
|
|
@@ -1282,6 +1354,10 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1282
1354
|
void handleFileSearch(message, rpc);
|
|
1283
1355
|
if (message.type === 'session.git.read.request')
|
|
1284
1356
|
void handleGitRead(message, rpc);
|
|
1357
|
+
if (message.type === 'session.harness.auth.request')
|
|
1358
|
+
void handleHarnessAuth(message, rpc);
|
|
1359
|
+
if (message.type === 'session.codex.update.request')
|
|
1360
|
+
void handleCodexUpdate(message, rpc);
|
|
1285
1361
|
if (message.type === 'session.file.edit.request')
|
|
1286
1362
|
void handleFileEdit(message, rpc);
|
|
1287
1363
|
});
|
|
@@ -1296,7 +1372,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1296
1372
|
type: 'machine.register',
|
|
1297
1373
|
machineId: machine,
|
|
1298
1374
|
machineHostname: os.hostname(),
|
|
1299
|
-
capabilities: { fileSearch: true, git: true, fileRanges: true },
|
|
1375
|
+
capabilities: { fileSearch: true, git: true, fileRanges: true, harnessAuth: true, codexUpdate: true },
|
|
1300
1376
|
workerId,
|
|
1301
1377
|
workerInstanceId,
|
|
1302
1378
|
anchorSessionIds: [...anchors.keys()],
|