@agentshouse/kit 0.2.1-c15804a0fa28d → 0.2.1-c2c600f2d2b30
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/agents.js +42 -19
- package/dist/api.js +11 -1
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +1 -0
- package/dist/clis.js +21 -4
- package/dist/conversations.js +11 -7
- package/dist/copies.js +0 -2
- package/dist/end.js +8 -0
- package/dist/secret-input.js +4 -1
- package/dist/sign-in.js +2 -1
- package/dist/stream.js +29 -4
- package/dist/transcription.js +2 -0
- package/dist/update.js +2 -0
- package/package.json +1 -1
package/dist/agents.js
CHANGED
|
@@ -3,13 +3,16 @@ import { readFile, realpath } from 'node:fs/promises';
|
|
|
3
3
|
import { killTree, startAdapter } from './acp.js';
|
|
4
4
|
import { CLIS, KIT_VERSION, below, installAdapter, installCli, locate, run, versionOf } from './clis.js';
|
|
5
5
|
import { agentBase, kitHome } from './home.js';
|
|
6
|
-
const SIGNED_OUT = { signed_in: false, models: [] };
|
|
6
|
+
const SIGNED_OUT = { signed_in: false, models: [], modes: [] };
|
|
7
7
|
function select(options, category) {
|
|
8
8
|
const option = options.find((candidate) => candidate.category === category);
|
|
9
9
|
return option?.type === 'select' ? option : undefined;
|
|
10
10
|
}
|
|
11
|
+
function choices(options) {
|
|
12
|
+
return options.flatMap((entry) => ('group' in entry ? entry.options : [entry]));
|
|
13
|
+
}
|
|
11
14
|
function values(options) {
|
|
12
|
-
return
|
|
15
|
+
return choices(options).map((entry) => entry.value);
|
|
13
16
|
}
|
|
14
17
|
function causeOf(error) {
|
|
15
18
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -21,9 +24,11 @@ async function models(kind, adapter, known) {
|
|
|
21
24
|
const agent = adapter.connection.agent;
|
|
22
25
|
const opened = await agent.request('session/new', { cwd: kitHome(), mcpServers: [] });
|
|
23
26
|
let options = opened.configOptions ?? [];
|
|
27
|
+
const mode = select(options, 'mode');
|
|
28
|
+
const modes = mode === undefined ? [] : choices(mode.options).map(({ value, name }) => ({ mode: value, name }));
|
|
24
29
|
const model = select(options, 'model');
|
|
25
30
|
if (model === undefined)
|
|
26
|
-
return [];
|
|
31
|
+
return { models: [], modes };
|
|
27
32
|
const offered = [];
|
|
28
33
|
for (const value of values(model.options)) {
|
|
29
34
|
if (select(options, 'model')?.currentValue !== value) {
|
|
@@ -39,10 +44,18 @@ async function models(kind, adapter, known) {
|
|
|
39
44
|
const effort = select(options, 'thought_level');
|
|
40
45
|
offered.push({ model: value, efforts: effort === undefined ? [] : values(effort.options) });
|
|
41
46
|
}
|
|
42
|
-
return offered;
|
|
47
|
+
return { models: offered, modes };
|
|
48
|
+
}
|
|
49
|
+
async function signedIn(kind, cli, known) {
|
|
50
|
+
const probe = CLIS[kind].signedIn;
|
|
51
|
+
// A sign-in check that has not answered in half a minute counts as signed out rather than stalling the report.
|
|
52
|
+
if ('command' in probe)
|
|
53
|
+
return (await run(cli, probe.command, 30_000)).status === 0;
|
|
54
|
+
return (await offer(kind, cli, known)).signed_in;
|
|
43
55
|
}
|
|
44
56
|
async function offer(kind, cli, known) {
|
|
45
57
|
const probe = CLIS[kind].signedIn;
|
|
58
|
+
// A sign-in check that has not answered in half a minute counts as signed out rather than stalling the report.
|
|
46
59
|
if ('command' in probe && (await run(cli, probe.command, 30_000)).status !== 0)
|
|
47
60
|
return SIGNED_OUT;
|
|
48
61
|
let adapter;
|
|
@@ -53,11 +66,15 @@ async function offer(kind, cli, known) {
|
|
|
53
66
|
if (typeof method !== 'string' || method.length === 0)
|
|
54
67
|
return SIGNED_OUT;
|
|
55
68
|
}
|
|
56
|
-
return { signed_in: true,
|
|
69
|
+
return { signed_in: true, ...(await models(kind, adapter, known.models)) };
|
|
57
70
|
}
|
|
58
71
|
catch (error) {
|
|
59
72
|
process.stderr.write(`kit: ${kind} did not offer its models: ${causeOf(error)}\n`);
|
|
60
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
signed_in: adapter === undefined && 'initializeMeta' in probe ? known.signed_in : true,
|
|
75
|
+
models: known.models,
|
|
76
|
+
modes: known.modes,
|
|
77
|
+
};
|
|
61
78
|
}
|
|
62
79
|
finally {
|
|
63
80
|
if (adapter !== undefined)
|
|
@@ -72,6 +89,8 @@ function reported(kind, reading) {
|
|
|
72
89
|
failure: reading.failure,
|
|
73
90
|
signed_in: reading.signed_in,
|
|
74
91
|
models: reading.models,
|
|
92
|
+
modes: reading.modes,
|
|
93
|
+
full_access: CLIS[kind].fullAccess,
|
|
75
94
|
};
|
|
76
95
|
}
|
|
77
96
|
function tooOld(kind, release) {
|
|
@@ -79,10 +98,12 @@ function tooOld(kind, release) {
|
|
|
79
98
|
return `${cli.bin} ${release} is older than ${cli.minimum}, the oldest this Kit runs`;
|
|
80
99
|
}
|
|
81
100
|
async function sshHostKey() {
|
|
101
|
+
// Listing a local host key takes a moment, so ten seconds only bounds a stuck ssh-keygen.
|
|
82
102
|
const listed = await run('ssh-keygen', ['-l', '-E', 'sha256', '-f', '/etc/ssh/ssh_host_ed25519_key.pub'], 10_000);
|
|
83
103
|
return listed.status === 0 ? (/^\d+ (SHA256:\S+)/.exec(listed.output)?.[1] ?? null) : null;
|
|
84
104
|
}
|
|
85
105
|
async function operatingSystem() {
|
|
106
|
+
// sw_vers answers at once, so ten seconds only bounds a stuck call.
|
|
86
107
|
if (process.platform === 'darwin')
|
|
87
108
|
return `macOS ${(await run('sw_vers', ['-productVersion'], 10_000)).output.trim()}`;
|
|
88
109
|
const release = await readFile('/etc/os-release', 'utf8');
|
|
@@ -114,7 +135,7 @@ export class Agents {
|
|
|
114
135
|
});
|
|
115
136
|
this.reading = reading;
|
|
116
137
|
return reading.then(({ agents }) => {
|
|
117
|
-
Promise.all(agents.map((kind) => this.
|
|
138
|
+
Promise.all(agents.map((kind) => this.load(kind)))
|
|
118
139
|
.then(() => this.report())
|
|
119
140
|
.catch(logged);
|
|
120
141
|
});
|
|
@@ -130,7 +151,7 @@ export class Agents {
|
|
|
130
151
|
const known = this.readings.get(kind);
|
|
131
152
|
const kept = known?.release === null || known?.minimum !== undefined ? SIGNED_OUT : (known ?? SIGNED_OUT);
|
|
132
153
|
if (known?.release !== release || known.minimum !== undefined) {
|
|
133
|
-
this.update(kind, { release, failure: null, signed_in: kept.signed_in, models: kept.models });
|
|
154
|
+
this.update(kind, { release, failure: null, signed_in: kept.signed_in, models: kept.models, modes: kept.modes });
|
|
134
155
|
}
|
|
135
156
|
const read = this.nextRead(kind);
|
|
136
157
|
offer(kind, cli, kept)
|
|
@@ -147,10 +168,12 @@ export class Agents {
|
|
|
147
168
|
return this.queue(kind, () => this.ensure(kind));
|
|
148
169
|
}
|
|
149
170
|
async reread(kind) {
|
|
150
|
-
await this.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
171
|
+
await this.examine(kind, this.readings.get(kind) ?? SIGNED_OUT, offer);
|
|
172
|
+
await this.report();
|
|
173
|
+
}
|
|
174
|
+
async recheck(kind) {
|
|
175
|
+
const known = this.readings.get(kind) ?? SIGNED_OUT;
|
|
176
|
+
await this.examine(kind, known, async (kind, cli) => (await signedIn(kind, cli, known)) ? { ...known, signed_in: true } : SIGNED_OUT);
|
|
154
177
|
await this.report();
|
|
155
178
|
}
|
|
156
179
|
report() {
|
|
@@ -207,18 +230,18 @@ export class Agents {
|
|
|
207
230
|
return { release, minimum, failure: null, ...SIGNED_OUT };
|
|
208
231
|
return { cli, release };
|
|
209
232
|
}
|
|
210
|
-
async examine(kind, known) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
233
|
+
async examine(kind, known, read) {
|
|
234
|
+
const current = this.nextRead(kind);
|
|
235
|
+
const resolved = await this.queue(kind, () => this.resolve(kind));
|
|
236
|
+
const reading = 'cli' in resolved ? { release: resolved.release, failure: null, ...(await read(kind, resolved.cli, known)) } : resolved;
|
|
237
|
+
if (this.reads.get(kind) === current)
|
|
238
|
+
this.readings.set(kind, reading);
|
|
215
239
|
}
|
|
216
240
|
async load(kind) {
|
|
217
241
|
const known = this.readings.get(kind);
|
|
218
242
|
if (known !== undefined && known.failure === null)
|
|
219
243
|
return;
|
|
220
|
-
this.
|
|
221
|
-
this.readings.set(kind, await this.examine(kind, SIGNED_OUT));
|
|
244
|
+
await this.examine(kind, SIGNED_OUT, offer);
|
|
222
245
|
}
|
|
223
246
|
async send() {
|
|
224
247
|
const agents = this.desired.agents.flatMap((kind) => {
|
package/dist/api.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { end, SIGN_IN_AGAIN } from './end.js';
|
|
1
2
|
import { readEnrolment } from './home.js';
|
|
2
3
|
import { relayed } from './relay.js';
|
|
3
4
|
export class HouseRefusal extends Error {
|
|
@@ -10,6 +11,7 @@ export class HouseRefusal extends Error {
|
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
13
|
export function retryDelay(attempt) {
|
|
14
|
+
// Full jitter, doubling from one second to a 30-second cap, spreads the Kits one outage dropped and retries within half a minute of House's return.
|
|
13
15
|
return Math.random() * Math.min(30_000, 1000 * 2 ** attempt);
|
|
14
16
|
}
|
|
15
17
|
export async function post(house, path, body, credential, signal) {
|
|
@@ -29,7 +31,14 @@ export async function post(house, path, body, credential, signal) {
|
|
|
29
31
|
}
|
|
30
32
|
async function enrolledPost(path, body, signal) {
|
|
31
33
|
const { house, credential } = (await readEnrolment());
|
|
32
|
-
|
|
34
|
+
try {
|
|
35
|
+
return await post(house, path, body, credential, signal);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof HouseRefusal && error.status === 401)
|
|
39
|
+
end(SIGN_IN_AGAIN);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
33
42
|
}
|
|
34
43
|
export const house = {
|
|
35
44
|
post: enrolledPost,
|
|
@@ -42,6 +51,7 @@ export const house = {
|
|
|
42
51
|
if (error instanceof HouseRefusal && error.status < 500)
|
|
43
52
|
throw error;
|
|
44
53
|
process.stderr.write(`kit: ${path} did not reach House: ${error.message}\n`);
|
|
54
|
+
// A delivery House did not take is sent again on the jittered curve for as long as it is still current.
|
|
45
55
|
await new Promise((resolve) => setTimeout(resolve, retryDelay(attempt)));
|
|
46
56
|
if (!current())
|
|
47
57
|
throw error;
|
package/dist/bootstrap.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
|
-
"version": "0.2.1-
|
|
3
|
+
"version": "0.2.1-c2c600f2d2b30",
|
|
4
4
|
"hosts": {
|
|
5
5
|
"macos": {
|
|
6
6
|
"asset": {
|
|
7
7
|
"name": "connect-linux.sh",
|
|
8
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-
|
|
8
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c2c600f2d2b30/connect-linux.sh"
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
11
|
"linux": {
|
|
12
12
|
"asset": {
|
|
13
13
|
"name": "connect-linux.sh",
|
|
14
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-
|
|
14
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c2c600f2d2b30/connect-linux.sh"
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"windows": {
|
|
18
18
|
"asset": {
|
|
19
19
|
"name": "connect-windows.ps1",
|
|
20
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-
|
|
20
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c2c600f2d2b30/connect-windows.ps1"
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
}
|
package/dist/bridge.js
CHANGED
|
@@ -105,6 +105,7 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
105
105
|
const after = resendAfter(forwarded, attempt);
|
|
106
106
|
if (after === null)
|
|
107
107
|
return forwarded;
|
|
108
|
+
// A write House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
|
|
108
109
|
await delay(after, undefined, { signal: closed.signal });
|
|
109
110
|
}
|
|
110
111
|
};
|
package/dist/clis.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import { userInfo } from 'node:os';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir, userInfo } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { kitHome } from './home.js';
|
|
7
7
|
const RUNNING_JOB = new Set(['running', 'paused']);
|
|
8
|
+
async function allowHouseInCodex() {
|
|
9
|
+
const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
|
|
10
|
+
await mkdir(rules, { recursive: true });
|
|
11
|
+
await writeFile(join(rules, 'house.rules'), 'prefix_rule(pattern = ["house"], decision = "allow")\n');
|
|
12
|
+
}
|
|
13
|
+
async function nothing() { }
|
|
8
14
|
function threadStatus({ method, params }) {
|
|
9
15
|
if (method !== 'session/update' || params?.update?.sessionUpdate !== 'session_info_update')
|
|
10
16
|
return undefined;
|
|
@@ -36,6 +42,9 @@ export const CLIS = {
|
|
|
36
42
|
install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
|
|
37
43
|
adapter: { package: '@agentclientprotocol/codex-acp', bin: 'codex-acp', executable: 'CODEX_PATH', env: {} },
|
|
38
44
|
args: [],
|
|
45
|
+
fullAccess: 'agent-full-access',
|
|
46
|
+
sessionMeta: {},
|
|
47
|
+
allowHouse: allowHouseInCodex,
|
|
39
48
|
signedIn: { command: ['login', 'status'] },
|
|
40
49
|
login: { args: ['login', '--device-auth'], code: 'show' },
|
|
41
50
|
queues: false,
|
|
@@ -52,9 +61,12 @@ export const CLIS = {
|
|
|
52
61
|
package: '@agentclientprotocol/claude-agent-acp',
|
|
53
62
|
bin: 'claude-agent-acp',
|
|
54
63
|
executable: 'CLAUDE_CODE_EXECUTABLE',
|
|
55
|
-
env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp' },
|
|
64
|
+
env: { CLAUDE_CODE_ENTRYPOINT: 'claude-agent-acp', IS_SANDBOX: '1' },
|
|
56
65
|
},
|
|
57
66
|
args: [],
|
|
67
|
+
fullAccess: 'bypassPermissions',
|
|
68
|
+
sessionMeta: { claudeCode: { options: { allowedTools: ['Bash(house:*)'] } } },
|
|
69
|
+
allowHouse: nothing,
|
|
58
70
|
signedIn: { command: ['auth', 'status'] },
|
|
59
71
|
login: { args: ['auth', 'login', '--claudeai'], code: 'collect', rejected: 'Invalid code' },
|
|
60
72
|
queues: false,
|
|
@@ -70,7 +82,10 @@ export const CLIS = {
|
|
|
70
82
|
minimum: '1.0.46',
|
|
71
83
|
install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
|
|
72
84
|
adapter: null,
|
|
73
|
-
args: ['agent', '--no-leader', 'stdio'],
|
|
85
|
+
args: ['agent', '--always-approve', '--no-leader', 'stdio'],
|
|
86
|
+
fullAccess: null,
|
|
87
|
+
sessionMeta: {},
|
|
88
|
+
allowHouse: nothing,
|
|
74
89
|
signedIn: { initializeMeta: 'defaultAuthMethodId' },
|
|
75
90
|
login: { args: ['login', '--device-auth'], code: 'show' },
|
|
76
91
|
queues: true,
|
|
@@ -90,7 +105,9 @@ const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.me
|
|
|
90
105
|
export const KIT_VERSION = KIT_PACKAGE.version;
|
|
91
106
|
const PROXY = /^https?_proxy$/i;
|
|
92
107
|
const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
|
|
108
|
+
// An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
|
|
93
109
|
const INSTALL_MS = 15 * 60_000;
|
|
110
|
+
// A login shell or a version read is ended after half a minute, so a profile that waits for input cannot stall the report.
|
|
94
111
|
const READ_MS = 30_000;
|
|
95
112
|
const CAUSE_CHARACTERS = 4000;
|
|
96
113
|
function prefix(kind) {
|
package/dist/conversations.js
CHANGED
|
@@ -285,13 +285,15 @@ export class Conversations {
|
|
|
285
285
|
conversation.opening = bridge;
|
|
286
286
|
try {
|
|
287
287
|
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
288
|
+
await CLIS[route.kind].allowHouse();
|
|
288
289
|
const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
|
|
289
290
|
void adapter.exited.then(() => bridge.close());
|
|
290
291
|
const agent = adapter.connection.agent;
|
|
291
292
|
const cwd = route.working_directory;
|
|
293
|
+
const _meta = CLIS[route.kind].sessionMeta;
|
|
292
294
|
const opened = sessionId === null
|
|
293
|
-
? await agent.request('session/new', { cwd, mcpServers: [] })
|
|
294
|
-
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [] })), sessionId };
|
|
295
|
+
? await agent.request('session/new', { cwd, mcpServers: [], _meta })
|
|
296
|
+
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
|
|
295
297
|
const running = {
|
|
296
298
|
adapter,
|
|
297
299
|
bridge,
|
|
@@ -321,6 +323,7 @@ export class Conversations {
|
|
|
321
323
|
async launchSettings(running, route, offered) {
|
|
322
324
|
let options = offered;
|
|
323
325
|
const settings = [
|
|
326
|
+
['mode', route.mode ?? CLIS[route.kind].fullAccess],
|
|
324
327
|
['model', route.model],
|
|
325
328
|
['thought_level', route.effort],
|
|
326
329
|
];
|
|
@@ -389,10 +392,8 @@ export class Conversations {
|
|
|
389
392
|
turn.admitted = true;
|
|
390
393
|
for (const note of turn.notes.splice(0))
|
|
391
394
|
this.kit.send(note);
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
this.draft(conversation, turn);
|
|
395
|
-
}
|
|
395
|
+
turn.unsentFrom = 0;
|
|
396
|
+
this.draft(conversation, turn);
|
|
396
397
|
if (turn.entries !== null)
|
|
397
398
|
this.plan(conversation, turn);
|
|
398
399
|
}
|
|
@@ -442,8 +443,11 @@ export class Conversations {
|
|
|
442
443
|
this.attach(conversation.turn ?? this.begin(conversation), sent);
|
|
443
444
|
}
|
|
444
445
|
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.answered(conversation, sent, null), async (error) => {
|
|
445
|
-
if (!running.adapter.connection.signal.aborted)
|
|
446
|
+
if (!running.adapter.connection.signal.aborted) {
|
|
446
447
|
this.answered(conversation, sent, causeOf(error));
|
|
448
|
+
if (conversation.open === 0 && conversation.running === running)
|
|
449
|
+
stop(running.bridge);
|
|
450
|
+
}
|
|
447
451
|
else if (sent.turn !== null) {
|
|
448
452
|
this.end(conversation, sent.turn, { failed: running.killed ? KILLED : await running.adapter.exited });
|
|
449
453
|
}
|
package/dist/copies.js
CHANGED
|
@@ -11,7 +11,6 @@ import { changed } from './translate.js';
|
|
|
11
11
|
import { relayed } from './relay.js';
|
|
12
12
|
const BRANCH = 'main';
|
|
13
13
|
const PROVENANCE = '_provenance';
|
|
14
|
-
const REPLICA_RETRY_MS = 30_000;
|
|
15
14
|
const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-kit@localhost' };
|
|
16
15
|
function causeOf(error) {
|
|
17
16
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -439,7 +438,6 @@ export class LocalCopies {
|
|
|
439
438
|
return this.received(copy);
|
|
440
439
|
if (code !== 'replica_behind')
|
|
441
440
|
throw error;
|
|
442
|
-
setTimeout(() => this.deliver(copy.key), REPLICA_RETRY_MS).unref();
|
|
443
441
|
return copy;
|
|
444
442
|
}
|
|
445
443
|
const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
|
package/dist/end.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { killDescendants } from './acp.js';
|
|
2
|
+
export const REPLACED = 'another Kit started for this Environment';
|
|
3
|
+
export const SIGN_IN_AGAIN = 'sign in again';
|
|
4
|
+
export function end(reason) {
|
|
5
|
+
process.stderr.write(`kit: ${reason}\n`);
|
|
6
|
+
killDescendants();
|
|
7
|
+
process.exit(0);
|
|
8
|
+
}
|
package/dist/secret-input.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { retryDelay } from './api.js';
|
|
1
|
+
import { HouseRefusal, retryDelay } from './api.js';
|
|
2
2
|
export async function holdSecretInput(house, subject, steps, waiting) {
|
|
3
3
|
for (let failures = 0; waiting();) {
|
|
4
4
|
try {
|
|
@@ -10,7 +10,10 @@ export async function holdSecretInput(house, subject, steps, waiting) {
|
|
|
10
10
|
failures = 0;
|
|
11
11
|
}
|
|
12
12
|
catch (error) {
|
|
13
|
+
if (error instanceof HouseRefusal && error.status < 500)
|
|
14
|
+
return null;
|
|
13
15
|
process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
16
|
+
// A wait that did not reach House is held again on the jittered curve for as long as its asker still waits.
|
|
14
17
|
await new Promise((resolve) => setTimeout(resolve, retryDelay(failures++)));
|
|
15
18
|
}
|
|
16
19
|
}
|
package/dist/sign-in.js
CHANGED
|
@@ -31,8 +31,9 @@ export class SignIns {
|
|
|
31
31
|
this.running.add(input.input_id);
|
|
32
32
|
this.changed();
|
|
33
33
|
this.login(input)
|
|
34
|
-
.then(() => this.agents.
|
|
34
|
+
.then(() => this.agents.recheck(input.cli))
|
|
35
35
|
.then(() => this.house.deliver(`/kit/inputs/${input.input_id}/ack`, {}))
|
|
36
|
+
.then(() => this.agents.reread(input.cli))
|
|
36
37
|
.catch(logged)
|
|
37
38
|
.finally(() => {
|
|
38
39
|
this.running.delete(input.input_id);
|
package/dist/stream.js
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { subscribe } from 'node:diagnostics_channel';
|
|
2
|
+
import { HouseRefusal, retryDelay } from './api.js';
|
|
3
|
+
import { end, REPLACED, SIGN_IN_AGAIN } from './end.js';
|
|
2
4
|
import { readEnrolment } from './home.js';
|
|
3
5
|
import { relayed } from './relay.js';
|
|
6
|
+
const PATH = '/kit/stream';
|
|
4
7
|
const SUBPROTOCOL = 'house.kit.stream.1';
|
|
8
|
+
const REFUSAL_HEADER = 'x-house-refusal';
|
|
5
9
|
function streamUrl(house) {
|
|
6
|
-
const url = new URL(
|
|
10
|
+
const url = new URL(PATH, house);
|
|
7
11
|
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
8
12
|
return url;
|
|
9
13
|
}
|
|
14
|
+
function header(headers, name) {
|
|
15
|
+
for (let index = 0; index < headers.length; index += 2) {
|
|
16
|
+
if (headers[index].toString('latin1').toLowerCase() === name)
|
|
17
|
+
return headers[index + 1].toString('latin1');
|
|
18
|
+
}
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
let refused = null;
|
|
22
|
+
subscribe('undici:request:headers', (message) => {
|
|
23
|
+
const { request, response } = message;
|
|
24
|
+
if (!request.path.endsWith(PATH))
|
|
25
|
+
return;
|
|
26
|
+
refused = new HouseRefusal(PATH, response.statusCode, header(response.headers, REFUSAL_HEADER) || response.statusText);
|
|
27
|
+
});
|
|
10
28
|
export function holdStream(handlers) {
|
|
11
29
|
let socket = null;
|
|
12
30
|
let failures = 0;
|
|
13
31
|
const connect = async () => {
|
|
14
32
|
const { house, credential } = (await readEnrolment());
|
|
33
|
+
refused = null;
|
|
15
34
|
const opening = new WebSocket(relayed(streamUrl(house)), {
|
|
16
35
|
protocols: [SUBPROTOCOL],
|
|
17
36
|
headers: { authorization: `Bearer ${credential}` },
|
|
@@ -31,8 +50,14 @@ export function holdStream(handlers) {
|
|
|
31
50
|
if (socket === opening)
|
|
32
51
|
socket = null;
|
|
33
52
|
process.stderr.write(`kit: control stream closed ${event.code} ${event.reason}\n`);
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
if (event.reason === 'replaced')
|
|
54
|
+
end(REPLACED);
|
|
55
|
+
if (event.reason === 'credential_rejected' || refused?.status === 401)
|
|
56
|
+
end(SIGN_IN_AGAIN);
|
|
57
|
+
if (refused !== null && refused.status >= 400 && refused.status < 500)
|
|
58
|
+
end(refused.message);
|
|
59
|
+
// A closed socket reopens in one to three seconds, spread so Kits one restart closed return apart; a failed opening backs off on the jittered curve.
|
|
60
|
+
setTimeout(() => void connect(), opened ? 1000 + Math.random() * 2000 : retryDelay(failures++));
|
|
36
61
|
};
|
|
37
62
|
};
|
|
38
63
|
void connect();
|
package/dist/transcription.js
CHANGED
|
@@ -36,6 +36,7 @@ function installed(artifact) {
|
|
|
36
36
|
return join(directory(), artifact.sha256);
|
|
37
37
|
}
|
|
38
38
|
async function executed(command, args, signal) {
|
|
39
|
+
// A download or a transcription runs as long as its size needs; the conversation's interrupt or end aborts it instead.
|
|
39
40
|
const ran = await run(command, args, 0, false, signal);
|
|
40
41
|
if (ran.status !== 0)
|
|
41
42
|
throw new Error(failureOf(ran, basename(command)));
|
|
@@ -68,6 +69,7 @@ async function install(artifacts, signal) {
|
|
|
68
69
|
try {
|
|
69
70
|
for (const artifact of missing) {
|
|
70
71
|
const part = `${installed(artifact)}.part`;
|
|
72
|
+
// Three retries carry a large download past a dropped connection before the transcription fails.
|
|
71
73
|
await executed('curl', ['-fsSL', '--retry', '3', '-o', part, artifact.url], signal);
|
|
72
74
|
if ((await sha256(part)) !== artifact.sha256)
|
|
73
75
|
throw new Error(`${artifact.url} does not match its sha256`);
|
package/dist/update.js
CHANGED
|
@@ -40,9 +40,11 @@ async function finished(inputId) {
|
|
|
40
40
|
const ack = await recorded();
|
|
41
41
|
if (ack !== null)
|
|
42
42
|
return ack;
|
|
43
|
+
// systemctl answers at once, so ten seconds only bounds a stuck service manager.
|
|
43
44
|
if ((await run('systemctl', ['is-active', '--quiet', unit(inputId)], 10_000)).status !== 0) {
|
|
44
45
|
return (await recorded()) ?? { refused: 'the Kit bootstrap did not finish' };
|
|
45
46
|
}
|
|
47
|
+
// The bootstrap runs for minutes, so checking each second sees its outcome within a second at little cost.
|
|
46
48
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
47
49
|
}
|
|
48
50
|
}
|