@agentshouse/kit 0.2.1-c441d29eb63f4 → 0.2.1-c4613862fad84

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/update.js CHANGED
@@ -1,138 +1,127 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { createHash } from 'node:crypto';
3
- import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
- import { homedir } from 'node:os';
2
+ import { mkdir } from 'node:fs/promises';
5
3
  import { join } from 'node:path';
6
- import { run } from './clis.js';
7
- import { kitHome, workspaceRoot } from './home.js';
8
- import { service, throughSudo } from './placement.js';
9
- function directory() {
10
- return join(kitHome(), 'update');
4
+ import { downloadServed, servedRelease } from './building.js';
5
+ import { KIT_RELEASE } from './clis.js';
6
+ import { restart, updateLine } from './end.js';
7
+ import { kitHome, readEnrolment } from './home.js';
8
+ import { placedPrefix } from './placement.js';
9
+ import { childEnvironment, relayOrigin } from './relay.js';
10
+ const INSTALLER_BYTES = 1_000_000;
11
+ // A release's Node.js, Kit package and adapters are about a hundred megabytes, so an hour carries them at thirty kilobytes a second, and a placement still running then has hung.
12
+ const PLACEMENT_MS = 60 * 60_000;
13
+ // A refused Kit is offline until its placement succeeds, so it tries again within five minutes of its Building or this computer recovering, while a placement that fails only after its hundred-megabyte download runs at most twelve times an hour.
14
+ export const PLACEMENT_RETRY_MS = 5 * 60_000;
15
+ const REFUSED = 'kit_bootstrap_refused: ';
16
+ const OUTPUT_CHARACTERS = 4000;
17
+ function causeOf(error) {
18
+ if (error instanceof TypeError && error.cause instanceof Error)
19
+ return `the Building did not answer: ${error.cause.message}`;
20
+ return error instanceof Error ? error.message : String(error);
11
21
  }
12
- function pending() {
13
- return join(directory(), 'pending.json');
14
- }
15
- function outcome() {
16
- return join(directory(), 'outcome');
17
- }
18
- function unit(callId) {
19
- return `house-kit-update-${callId}`;
20
- }
21
- function manager() {
22
- return service() === 'user' ? ['--user'] : [];
23
- }
24
- function elevated() {
25
- return throughSudo() ? ['sudo', '-n'] : [];
26
- }
27
- function account() {
28
- return throughSudo() ? [`--uid=${process.getuid()}`, `--gid=${process.getgid()}`] : [];
29
- }
30
- function placement() {
31
- return service() === 'user' ? ['--workspace', workspaceRoot()] : ['--linux'];
32
- }
33
- function record() {
34
- return `outcome=$1; shift; bash "$0" "$@"; printf %s "$?" > "$outcome"; ${[...elevated(), 'systemctl', ...manager(), 'start', 'house-kit.service'].join(' ')}`;
35
- }
36
- async function pendingCall() {
37
- try {
38
- return JSON.parse(await readFile(pending(), 'utf8')).input_id;
39
- }
40
- catch {
41
- return null;
42
- }
43
- }
44
- async function recorded() {
45
- const status = await readFile(outcome(), 'utf8').catch(() => null);
46
- if (status === null)
47
- return null;
48
- return status.trim() === '0' ? {} : { refused: `the Kit bootstrap exited with ${status.trim()}` };
49
- }
50
- async function forget(callId) {
51
- if ((await pendingCall()) !== callId)
52
- return;
53
- await rm(pending(), { force: true });
54
- await rm(outcome(), { force: true });
55
- }
56
- async function finished(callId) {
57
- for (;;) {
58
- const result = await recorded();
59
- if (result !== null)
60
- return result;
61
- // systemctl answers at once, so ten seconds only bounds a stuck service manager.
62
- if ((await run('systemctl', [...manager(), 'is-active', '--quiet', unit(callId)], 10_000)).status !== 0) {
63
- return (await recorded()) ?? { refused: 'the Kit bootstrap did not finish' };
64
- }
65
- // The bootstrap runs for minutes, so checking each second sees its outcome within a second at little cost.
66
- await new Promise((resolve) => setTimeout(resolve, 1000));
67
- }
68
- }
69
- function rerun(script, callId, flags) {
22
+ function placing(installer, house, prefix, signal) {
70
23
  return new Promise((resolve) => {
71
- const [command, ...args] = [
72
- ...elevated(),
73
- 'systemd-run',
74
- ...manager(),
75
- ...account(),
76
- '--wait',
77
- '--collect',
78
- '--quiet',
79
- `--unit=${unit(callId)}`,
80
- `--setenv=HOME=${homedir()}`,
81
- `--setenv=HOUSE_KIT_HOME=${kitHome()}`,
82
- '/bin/sh',
83
- '-c',
84
- record(),
85
- script,
86
- outcome(),
87
- ...placement(),
88
- ...flags,
89
- ];
90
- const child = spawn(command, args, { stdio: ['ignore', 'ignore', 'inherit'] });
91
- child.on('error', () => resolve({ refused: 'no service manager runs this Kit process' }));
92
- child.on('close', () => resolve(null));
24
+ const relay = relayOrigin();
25
+ const child = spawn('bash', [installer, '--house', house, '--place', prefix], {
26
+ stdio: ['ignore', 'pipe', 'pipe'],
27
+ env: { ...childEnvironment(), ...(relay === undefined ? {} : { HOUSE_KIT_RELAY: relay }) },
28
+ detached: true,
29
+ });
30
+ const stop = () => {
31
+ try {
32
+ process.kill(-child.pid, 'SIGKILL');
33
+ }
34
+ catch { }
35
+ };
36
+ signal.addEventListener('abort', stop, { once: true });
37
+ if (signal.aborted)
38
+ stop();
39
+ let output = '';
40
+ for (const stream of [child.stdout, child.stderr]) {
41
+ stream.setEncoding('utf8').on('data', (chunk) => {
42
+ output = (output + chunk).slice(-OUTPUT_CHARACTERS);
43
+ });
44
+ }
45
+ const ended = (failure) => {
46
+ signal.removeEventListener('abort', stop);
47
+ resolve(failure);
48
+ };
49
+ child.on('error', (error) => ended(error.message));
50
+ child.on('close', (status) => {
51
+ if (status === 0)
52
+ return ended(null);
53
+ if (signal.aborted)
54
+ return ended(`the placement did not end within ${PLACEMENT_MS / 1000} seconds`);
55
+ const lines = output.split('\n').filter((line) => line.trim() !== '');
56
+ const refused = lines.findLast((line) => line.startsWith(REFUSED));
57
+ ended(refused?.slice(REFUSED.length) ?? lines.at(-1) ?? `the placement ended with status ${status ?? 'a signal'}`);
58
+ });
93
59
  });
94
60
  }
95
61
  export class KitUpdate {
96
62
  calls;
97
63
  queue = Promise.resolve();
64
+ placed = null;
98
65
  constructor(calls) {
99
66
  this.calls = calls;
100
67
  }
101
- async resumed() {
102
- const callId = await pendingCall();
103
- if (callId === null)
104
- return;
105
- await this.enqueue(callId, () => finished(callId));
68
+ take(call) {
69
+ void this.calls.carry(call.input_id, async () => ({ result: await this.queued(() => this.update(call.release)) }));
106
70
  }
107
- start(call) {
108
- void this.enqueue(call.input_id, () => this.carry(call));
71
+ async takeRefused(release) {
72
+ for (;;) {
73
+ const failure = await this.queued(async () => (this.placed === null ? this.place(release, 'served') : null));
74
+ if (failure === null)
75
+ restart(`Kit ${this.placed} is placed; its supervisor starts it`);
76
+ process.stderr.write(`kit: ${updateLine(`Kit ${KIT_RELEASE} stays offline and tries again in ${PLACEMENT_RETRY_MS / 60_000} minutes`)}\n`);
77
+ await new Promise((resolve) => setTimeout(resolve, PLACEMENT_RETRY_MS));
78
+ }
109
79
  }
110
- enqueue(callId, work) {
111
- const before = this.queue;
112
- const carried = this.calls.carry(callId, async () => {
113
- await before;
114
- return { result: await work(), returned: () => forget(callId) };
115
- });
116
- this.queue = before.then(() => carried);
117
- return carried;
80
+ placedRelease() {
81
+ return this.placed;
82
+ }
83
+ queued(work) {
84
+ const taken = this.queue.then(work);
85
+ this.queue = taken.catch(() => undefined);
86
+ return taken;
118
87
  }
119
- async carry(call) {
120
- if ((await pendingCall()) === call.input_id)
121
- return finished(call.input_id);
122
- const answer = await fetch(call.bootstrap);
123
- if (!answer.ok)
124
- return { refused: `the Kit bootstrap could not be downloaded: ${answer.status}` };
125
- const script = Buffer.from(await answer.arrayBuffer());
126
- if (createHash('sha256').update(script).digest('hex') !== call.sha256) {
127
- return { refused: 'the Kit bootstrap does not match its sha256' };
88
+ async update(release) {
89
+ if (release === (this.placed ?? KIT_RELEASE))
90
+ return {};
91
+ if (this.placed !== null)
92
+ return { refused: `Kit ${this.placed} is placed and starts first, once no turn is open` };
93
+ const failure = await this.place(release, 'named');
94
+ if (failure !== null)
95
+ return { refused: failure };
96
+ process.stderr.write(`kit: Kit ${release} is placed; it starts once no turn is open\n`);
97
+ return {};
98
+ }
99
+ async place(release, which) {
100
+ const signal = AbortSignal.timeout(PLACEMENT_MS);
101
+ let version = release;
102
+ let failure;
103
+ try {
104
+ const prefix = placedPrefix();
105
+ const served = await servedRelease(signal);
106
+ if (which === 'named' && served.version !== release)
107
+ throw new Error(`the Building serves Kit ${served.version}, not Kit ${release}`);
108
+ version = served.version;
109
+ if (version === KIT_RELEASE)
110
+ throw new Error(`the Building serves Kit ${KIT_RELEASE}, which this Kit runs`);
111
+ await mkdir(join(kitHome(), 'update'), { recursive: true, mode: 0o700 });
112
+ const installer = join(kitHome(), 'update', served.installer);
113
+ await downloadServed(served.installer, served.sha256, installer, signal, INSTALLER_BYTES);
114
+ failure = await placing(installer, (await readEnrolment()).house, prefix, signal);
128
115
  }
129
- await mkdir(directory(), { recursive: true, mode: 0o700 });
130
- const path = join(directory(), 'connect-linux.sh');
131
- await writeFile(path, script, { mode: 0o700 });
132
- await rm(outcome(), { force: true });
133
- await writeFile(pending(), `${JSON.stringify({ input_id: call.input_id })}\n`, { mode: 0o600 });
134
- return (await rerun(path, call.input_id, call.update_clis ? ['--update-clis'] : [])) ?? (await recorded()) ?? {
135
- refused: 'the Kit bootstrap did not finish',
136
- };
116
+ catch (error) {
117
+ failure = signal.aborted ? `the placement did not end within ${PLACEMENT_MS / 1000} seconds` : causeOf(error);
118
+ }
119
+ if (failure !== null) {
120
+ const refused = `Kit ${version} could not be placed: ${failure}`;
121
+ process.stderr.write(`kit: ${refused}\n`);
122
+ return refused;
123
+ }
124
+ this.placed = version;
125
+ return null;
137
126
  }
138
127
  }
package/dist/wire.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { newerThanHouse, updateRequired } from './end.js';
2
2
  import { machine } from './placement.js';
3
3
  import { refusalOf } from './refusals.js';
4
- export const KIT_WIRE = 3;
4
+ export const KIT_WIRE = 4;
5
5
  export const KIT_WIRE_HEADER = 'x-house-kit-wire';
6
- function newer(text) {
6
+ let take = null;
7
+ let taken = false;
8
+ function unservedBy(text) {
7
9
  let body;
8
10
  try {
9
11
  body = JSON.parse(text);
@@ -11,12 +13,28 @@ function newer(text) {
11
13
  catch {
12
14
  return null;
13
15
  }
14
- return body?.error?.code === 'kit_newer_than_house' ? body.error : null;
16
+ const error = body?.error;
17
+ if (error?.code !== 'kit_update_required' && error?.code !== 'kit_newer_than_house')
18
+ return null;
19
+ return typeof error.release === 'string' ? error : null;
20
+ }
21
+ export function takeUnservedRelease(taking) {
22
+ take = taking;
23
+ }
24
+ export function unserved() {
25
+ return taken;
15
26
  }
16
27
  export function endUnserved(text) {
28
+ const refused = unservedBy(text);
29
+ if (refused !== null && take !== null) {
30
+ if (!taken) {
31
+ taken = true;
32
+ take(refused.release);
33
+ }
34
+ return;
35
+ }
17
36
  if (refusalOf(text)?.code === 'kit_update_required')
18
37
  updateRequired();
19
- const refused = newer(text);
20
- if (refused !== null)
38
+ if (refused?.code === 'kit_newer_than_house')
21
39
  newerThanHouse(refused.release, refused.commands[machine()]);
22
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.2.1-c441d29eb63f4",
3
+ "version": "0.2.1-c4613862fad84",
4
4
  "description": "Kit connects an Environment to agents.house.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -12,28 +12,27 @@
12
12
  "kit": "dist/kit-main.js",
13
13
  "house": "dist/house-main.js"
14
14
  },
15
- "exports": {
16
- "./bootstrap": "./dist/bootstrap.json"
17
- },
18
15
  "files": [
19
16
  "dist"
20
17
  ],
21
18
  "scripts": {
22
- "build": "tsc -p tsconfig.build.json && node scripts/bootstrap-manifest.mjs",
19
+ "bootstrap-manifest": "node scripts/bootstrap-manifest.mjs",
20
+ "build": "tsc -p tsconfig.build.json",
23
21
  "engine": "mkdir -p tmp/engine && docker run --rm -v \"$PWD:/kit\" -w /kit alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 sh scripts/engine.sh tmp/engine",
24
22
  "follow-skills": "node scripts/follow-skills.mjs",
23
+ "release-files": "node scripts/release-files.mjs",
25
24
  "test": "tsc -p tsconfig.json && vitest run"
26
25
  },
27
26
  "dependencies": {
28
- "@agentclientprotocol/sdk": "1.7.0",
29
- "@agentshouse/mdmodel": "0.1.0-cabd61587647f",
30
- "@agentshouse/skills": "0.1.0-c6411c39c6d0f",
27
+ "@agentclientprotocol/sdk": "2.0.1",
28
+ "@agentshouse/mdmodel": "0.1.0-c4f4d37bc9d6a",
29
+ "@agentshouse/skills": "0.1.0-ca0e7383018fc",
31
30
  "@lydell/node-pty": "1.2.0-beta.15",
32
31
  "yaml": "2.9.1"
33
32
  },
34
33
  "peerDependencies": {
35
- "@agentclientprotocol/claude-agent-acp": "0.85.1",
36
- "@agentclientprotocol/codex-acp": "2.1.1"
34
+ "@agentclientprotocol/claude-agent-acp": "0.94.0",
35
+ "@agentclientprotocol/codex-acp": "2.2.2"
37
36
  },
38
37
  "peerDependenciesMeta": {
39
38
  "@agentclientprotocol/claude-agent-acp": {
@@ -44,26 +43,26 @@
44
43
  }
45
44
  },
46
45
  "devDependencies": {
47
- "@types/node": "24.13.6",
46
+ "@types/node": "24.19.2",
48
47
  "@types/ws": "8.18.2",
49
48
  "typescript": "7.0.2",
50
- "vitest": "5.0.1",
49
+ "vitest": "5.0.5",
51
50
  "ws": "8.22.0"
52
51
  },
53
- "packageManager": "npm@11.19.0",
52
+ "packageManager": "npm@11.19.1",
54
53
  "engines": {
55
- "node": "24.21.0",
56
- "npm": "11.19.0"
54
+ "node": "24.22.1",
55
+ "npm": "11.19.1"
57
56
  },
58
57
  "devEngines": {
59
58
  "runtime": {
60
59
  "name": "node",
61
- "version": "24.21.0",
60
+ "version": "24.22.1",
62
61
  "onFail": "error"
63
62
  },
64
63
  "packageManager": {
65
64
  "name": "npm",
66
- "version": "11.19.0",
65
+ "version": "11.19.1",
67
66
  "onFail": "error"
68
67
  }
69
68
  }
@@ -1,24 +0,0 @@
1
- {
2
- "schemaVersion": 2,
3
- "version": "0.2.1-c441d29eb63f4",
4
- "hosts": {
5
- "macos": {
6
- "asset": {
7
- "name": "connect-linux.sh",
8
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c441d29eb63f4/connect-linux.sh"
9
- }
10
- },
11
- "linux": {
12
- "asset": {
13
- "name": "connect-linux.sh",
14
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c441d29eb63f4/connect-linux.sh"
15
- }
16
- },
17
- "windows": {
18
- "asset": {
19
- "name": "connect-windows.ps1",
20
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c441d29eb63f4/connect-windows.ps1"
21
- }
22
- }
23
- }
24
- }
package/dist/clis-main.js DELETED
@@ -1,23 +0,0 @@
1
- import { setGlobalProxyFromEnv } from 'node:http';
2
- import { post } from './api.js';
3
- import { CLIS, below, locate, versionOf } from './clis.js';
4
- import { readEnrolment } from './home.js';
5
- setGlobalProxyFromEnv();
6
- try {
7
- const { house, credential } = (await readEnrolment());
8
- const { agents } = await post(house, '/kit/agents/desired', {}, credential);
9
- for (const kind of agents) {
10
- const cli = await locate(kind);
11
- if (cli === null)
12
- continue;
13
- const release = await versionOf(cli);
14
- if (release === null)
15
- continue;
16
- const { bin, minimum } = CLIS[kind];
17
- process.stdout.write(`${[bin, cli, release, minimum, below(release, minimum) ? 'old' : 'current'].join('\t')}\n`);
18
- }
19
- }
20
- catch (error) {
21
- process.stderr.write(`kit: ${error.message}\n`);
22
- process.exitCode = 1;
23
- }
@@ -1,100 +0,0 @@
1
- import { loginPath, onLoginPath, withoutProxy } from './clis.js';
2
- import { killTree } from './scope.js';
3
- const TERMINAL = 'xterm-256color';
4
- const SIGNALLED = 128;
5
- const NO_BASH = "bash is not on the login shell's PATH";
6
- const LOST = { refused: 'the Kit lost its connection to House' };
7
- function causeOf(error) {
8
- return error instanceof Error ? error.message : String(error);
9
- }
10
- function failed(cause) {
11
- process.stderr.write(`kit: a Web Wizard could not start: ${cause}\n`);
12
- return { refused: `the script could not start: ${cause}` };
13
- }
14
- export class WebWizards {
15
- calls;
16
- send;
17
- runs = new Map();
18
- constructor(calls, send) {
19
- this.calls = calls;
20
- this.send = send;
21
- }
22
- start(call) {
23
- void this.calls.carry(call.input_id, () => this.run(call));
24
- }
25
- notified(notification) {
26
- const run = this.runs.get(String(notification.wizard));
27
- if (run === undefined)
28
- return;
29
- if (notification.type === 'wizard_input')
30
- this.typed(run, String(notification.data));
31
- if (notification.type === 'wizard_size')
32
- this.resized(run, { cols: Number(notification.cols), rows: Number(notification.rows) });
33
- if (notification.type === 'wizard_end')
34
- this.end(run, {});
35
- }
36
- disconnected() {
37
- for (const run of this.runs.values())
38
- this.end(run, LOST);
39
- }
40
- async run(call) {
41
- const run = { terminal: null, size: null, typed: [], ended: null, sized: Promise.withResolvers() };
42
- this.runs.set(call.input_id, run);
43
- try {
44
- return { result: await this.ran(call.input_id, call.script, run) };
45
- }
46
- finally {
47
- this.runs.delete(call.input_id);
48
- }
49
- }
50
- async ran(wizard, script, run) {
51
- let terminal;
52
- try {
53
- const { spawn } = await import('@lydell/node-pty');
54
- const shell = await onLoginPath('bash');
55
- if (shell === null)
56
- return failed(NO_BASH);
57
- const path = await loginPath();
58
- await run.sized.promise;
59
- if (run.ended !== null)
60
- return run.ended;
61
- terminal = spawn(shell, ['-c', script], {
62
- name: TERMINAL,
63
- ...run.size,
64
- cwd: process.cwd(),
65
- env: { ...withoutProxy(), PATH: path },
66
- });
67
- }
68
- catch (error) {
69
- return failed(causeOf(error));
70
- }
71
- run.terminal = terminal;
72
- terminal.onData((data) => this.send({ type: 'wizard_output', wizard, data }));
73
- for (const data of run.typed.splice(0))
74
- terminal.write(data);
75
- const exited = await new Promise((resolve) => terminal.onExit(resolve));
76
- return run.ended ?? { exit_code: exited.signal ? SIGNALLED + exited.signal : exited.exitCode };
77
- }
78
- typed(run, data) {
79
- if (run.terminal === null)
80
- run.typed.push(data);
81
- else
82
- run.terminal.write(data);
83
- }
84
- resized(run, size) {
85
- run.size = size;
86
- run.sized.resolve();
87
- try {
88
- run.terminal?.resize(size.cols, size.rows);
89
- }
90
- catch { }
91
- }
92
- end(run, result) {
93
- if (run.ended !== null)
94
- return;
95
- run.ended = result;
96
- run.sized.resolve();
97
- if (run.terminal !== null)
98
- killTree(run.terminal);
99
- }
100
- }