@agentshouse/kit 0.2.1-ceeaf32286734 → 0.2.1-cf268dff1869b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,25 +9,25 @@ Run `sh -c 'kit=$(curl -fsSL "$0") || { echo "Kit could not be downloaded; run t
9
9
  or `& { try { $kit = irm agents.house/kit.ps1 } catch { throw 'Kit could not be downloaded; run this command again.' }; & ([scriptblock]::Create($kit)) @args }` on Windows, which run nothing of a failed download, and again to update; options follow `agents.house/kit`.
10
10
  On macOS and Linux it installs Kit natively under your own account, without an admin password, as a service that runs
11
11
  while you are logged in. `kit` and `house` are then on your login shell's `PATH`, and your workspace is `~/AgentsHouse` unless you
12
- pass `--workspace <path>`.
12
+ pass `--workspace <path>`. Every run places the Kit release your Building serves, with the Node.js and agent adapters it runs on, in a directory of its own beside the one that runs, then starts it; the earlier one stays until the next release. Kit also updates itself: when your Building takes another release, Kit places it the same way and, once no turn is open, ends so its service, or the container's restart policy, starts it. When House refuses the Kit that runs as too old or too new, Kit places your Building's release at once, ending any running turn, and ends the same way; while it cannot, it stays offline, prints why with the command that installs it again, and tries again regularly.
13
13
 
14
- Add `--container` to run Kit in one Docker container instead, with your
15
- workspace mounted at `/agents/house`; your Agents then reach only that
16
- workspace. On Windows 11 the container is the one choice. Append a `kit` or `house` command to the container's options to run it in that container.
14
+ Add `--container` to run Kit in one Docker container instead, with your workspace mounted at `/agents/house`; your Agents then reach only that
15
+ workspace. The container's image holds no Kit release and no Node.js: every run places them in Kit home's `kit` folder, which a container created again keeps. On Windows,
16
+ Kit home is the Docker volume `house-kit-home` and the container is the one choice. Append a `kit` or `house` command to the container's options to run it in that container.
17
17
  Running the command for the other choice on a connected computer asks you to
18
18
  confirm, then replaces its Environment, whose Agents end, with a new one. Running it with a different `HOUSE_KIT_HOME`
19
19
  from the running Kit process asks you to confirm before it replaces that process, and leaves the earlier Kit home as it was.
20
20
 
21
- On your own Linux server, run the Linux command with `--linux` to
22
- install Kit natively, with its workspace at `/agents/house`; `kit` and
23
- `house` are then on the server's `PATH`.
21
+ On your own Linux server, run the Linux command with `--linux` to install Kit natively, with its workspace at `/agents/house` and a missing `ca-certificates`, `curl` or `git` installed through apt; `kit` and `house` are then on the server's `PATH`.
24
22
 
25
- Kit runs the agent CLIs your login shell finds and installs a missing one
26
- through that CLI's own installer. Running the command again offers to update
27
- each CLI older than Kit needs; add `--update-clis` to update every one to
28
- its latest release without asking. Kit also gives every Agent on the
29
- Environment the agent skills of `@agentshouse/skills`, at the version each Kit
30
- release installs. Add `--no-skills` to the command to leave them out; running it again without the flag brings them back.
23
+ Kit is built for macOS 27 on Apple silicon, Ubuntu 26.04 LTS on amd64 and Windows 11 on amd64. On another release, distribution or architecture the command prints one warning line, such as `Warning: macOS 26.7.1 is outdated; update to macOS 27.`, and installs anyway. It refuses only an architecture other than amd64 and arm64, and a native install on a Linux without glibc, where `--container` still runs Kit. On a Linux without systemd it installs and connects Kit but starts nothing: run the `kit resident` command it prints under your own service manager, restarting it whenever it ends with a failure. `--container` installs Docker Engine on Ubuntu and Debian; another Linux needs a working Docker Engine first.
24
+
25
+ Kit runs each agent CLI through the link your login shell finds and installs a missing one through that CLI's own
26
+ installer. When it starts and regularly after, Kit runs each CLI's own update command, such as `claude update`, beside
27
+ your conversations: a running conversation keeps its version and the next one runs the new release. A CLI older than
28
+ the oldest release Kit is built for still runs, though its conversations may fail: Kit tells House its version, that
29
+ oldest release and its update command. Kit also gives every Agent on the Environment the agent skills of
30
+ `@agentshouse/skills`, at the version each Kit release installs. Add `--no-skills` to the command to leave them out; running it again without the flag brings them back.
31
31
 
32
32
  ## Commands
33
33
 
package/dist/agents.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { client } from '@agentclientprotocol/sdk';
2
- import { readFile, realpath } from 'node:fs/promises';
2
+ import { readFile } from 'node:fs/promises';
3
3
  import { setTimeout as delay } from 'node:timers/promises';
4
4
  import { signedOut, startAdapter } from './acp.js';
5
- import { CLIS, KIT_RELEASE, below, installAdapter, installCli, locate, run, versionOf } from './clis.js';
5
+ import { CLIS, KIT_RELEASE, below, installCli, locate, run, updateCli, versionOf } from './clis.js';
6
6
  import { agentBase, kitHome } from './home.js';
7
- import { updatesItself } from './placement.js';
8
7
  import { holdTranscription, transcriptionReport } from './transcription.js';
9
8
  import { KIT_WIRE } from './wire.js';
10
9
  // Two quiet seconds let a fresh conversation start before background model discovery competes for its CPU.
11
10
  const MODEL_PROBE_QUIET_MS = 2_000;
11
+ // A CLI's maker can publish several releases a day, so running each chosen CLI's update command hourly brings a new one to the next conversation within the hour for one small check.
12
+ export const CLI_UPDATE_MS = 60 * 60_000;
12
13
  // A sign-in's model read that has not answered in ten seconds leaves its sign-in to the status check, so a hung CLI never holds the sign-in's result.
13
14
  const SIGNED_IN_READ_MS = 10_000;
14
15
  const SIGNED_OUT = { signed_in: false, models: [], modes: [] };
@@ -110,10 +111,11 @@ async function offer(kind, cli, known, signal) {
110
111
  }
111
112
  }
112
113
  function reported(kind, reading) {
114
+ const { minimum, update } = CLIS[kind];
113
115
  return {
114
116
  kind,
115
117
  release: reading.release,
116
- ...(reading.minimum === undefined ? {} : { minimum: reading.minimum }),
118
+ ...(reading.release !== null && below(reading.release, minimum) ? { minimum, update } : {}),
117
119
  failure: reading.failure,
118
120
  ...(reading.installing === undefined ? {} : { installing: reading.installing }),
119
121
  signed_in: reading.signed_in,
@@ -123,10 +125,6 @@ function reported(kind, reading) {
123
125
  };
124
126
  }
125
127
  const NOT_READY = "the Agent's CLI is not ready on this computer";
126
- function tooOld(kind, release) {
127
- const cli = CLIS[kind];
128
- return `${cli.bin} ${release} is older than ${cli.minimum}, the oldest this Kit runs`;
129
- }
130
128
  async function sshHostKey() {
131
129
  // Listing a local host key takes a moment, so ten seconds only bounds a stuck ssh-keygen.
132
130
  const listed = await run('ssh-keygen', ['-l', '-E', 'sha256', '-f', '/etc/ssh/ssh_host_ed25519_key.pub'], 10_000);
@@ -168,13 +166,15 @@ export class Agents {
168
166
  sending = Promise.resolve();
169
167
  sent = null;
170
168
  refreshing = 0;
169
+ loaded = Promise.resolve();
170
+ updating = null;
171
171
  changed;
172
172
  constructor(house, changed = () => undefined) {
173
173
  this.house = house;
174
174
  this.changed = changed;
175
175
  }
176
176
  idle() {
177
- return this.refreshing === 0;
177
+ return this.refreshing === 0 && this.updating === null;
178
178
  }
179
179
  read() {
180
180
  return this.reading.then(() => undefined, () => undefined);
@@ -190,7 +190,7 @@ export class Agents {
190
190
  return this.desired;
191
191
  });
192
192
  this.reading = reading;
193
- reading
193
+ this.loaded = reading
194
194
  .then(({ agents, transcribe }) => Promise.all([
195
195
  holdTranscription(transcribe)
196
196
  .then((changed) => (changed ? this.report() : undefined))
@@ -206,18 +206,22 @@ export class Agents {
206
206
  });
207
207
  return reading.then(() => undefined);
208
208
  }
209
+ keepCurrent() {
210
+ this.updateAll(this.loaded);
211
+ setInterval(() => this.updateAll(Promise.resolve()), CLI_UPDATE_MS);
212
+ }
209
213
  cli(kind) {
210
214
  this.starts += 1;
211
215
  return this.queue(kind, async () => {
212
216
  const resolved = await this.resolve(kind);
213
217
  if (!('cli' in resolved)) {
214
218
  this.update(kind, resolved);
215
- throw new Error(resolved.failure === null ? tooOld(kind, resolved.release) : NOT_READY);
219
+ throw new Error(NOT_READY);
216
220
  }
217
221
  const { cli, release } = resolved;
218
222
  const known = this.readings.get(kind);
219
- const kept = known?.release === null || known?.minimum !== undefined ? SIGNED_OUT : (known ?? SIGNED_OUT);
220
- if (known?.release !== release || known.minimum !== undefined || this.installs.has(kind)) {
223
+ const kept = known?.release === null ? SIGNED_OUT : (known ?? SIGNED_OUT);
224
+ if (known?.release !== release || this.installs.has(kind)) {
221
225
  this.update(kind, { release, failure: null, signed_in: kept.signed_in, models: kept.models, modes: kept.modes });
222
226
  }
223
227
  const read = this.nextRead(kind);
@@ -284,9 +288,6 @@ export class Agents {
284
288
  return signal === undefined ? queued : Promise.race([queued, ended(signal)]);
285
289
  }
286
290
  async ensure(kind, signal, owner = {}) {
287
- const failed = await installAdapter(kind, signal);
288
- if (failed !== null)
289
- throw new Error(failed);
290
291
  const found = await locate(kind, signal);
291
292
  if (found !== null)
292
293
  return found;
@@ -305,7 +306,7 @@ export class Agents {
305
306
  async resolve(kind, signal, owner) {
306
307
  let cli;
307
308
  try {
308
- cli = await realpath(await this.ensure(kind, signal, owner));
309
+ cli = await this.ensure(kind, signal, owner);
309
310
  }
310
311
  catch (error) {
311
312
  const failure = causeOf(error);
@@ -315,9 +316,6 @@ export class Agents {
315
316
  const release = await versionOf(cli, signal);
316
317
  if (release === null)
317
318
  return { release: null, failure: `${cli} --version names no version`, ...SIGNED_OUT };
318
- const minimum = CLIS[kind].minimum;
319
- if (below(release, minimum))
320
- return { release, minimum, failure: null, ...SIGNED_OUT };
321
319
  return { cli, release };
322
320
  }
323
321
  async examine(kind, known, read, signal, owner) {
@@ -327,6 +325,35 @@ export class Agents {
327
325
  if (this.reads.get(kind) === current && !signal?.aborted)
328
326
  this.hold(kind, reading);
329
327
  }
328
+ updateAll(after) {
329
+ if (this.updating !== null)
330
+ return;
331
+ this.updating = after
332
+ .then(async () => {
333
+ for (const kind of this.desired.agents) {
334
+ if (this.installs.has(kind) || !this.readings.get(kind)?.release)
335
+ continue;
336
+ const failure = await updateCli(kind);
337
+ if (failure !== null)
338
+ process.stderr.write(`kit: ${kind} did not update: ${failure}\n`);
339
+ await this.readVersion(kind);
340
+ }
341
+ })
342
+ .catch(logged)
343
+ .finally(() => {
344
+ this.updating = null;
345
+ this.changed();
346
+ });
347
+ this.changed();
348
+ }
349
+ async readVersion(kind) {
350
+ const cli = await locate(kind);
351
+ const release = cli === null ? null : await versionOf(cli);
352
+ const current = this.readings.get(kind);
353
+ if (release === null || !current?.release || current.release === release)
354
+ return;
355
+ this.update(kind, { ...current, release });
356
+ }
330
357
  async quietOffer(kind, cli, known) {
331
358
  for (let seen = -1; seen !== this.starts;) {
332
359
  seen = this.starts;
@@ -336,7 +363,7 @@ export class Agents {
336
363
  }
337
364
  async load(kind) {
338
365
  const known = this.readings.get(kind);
339
- if (known !== undefined && known.failure === null && known.minimum === undefined)
366
+ if (known !== undefined && known.failure === null)
340
367
  return;
341
368
  await this.examine(kind, SIGNED_OUT, (kind, cli, known) => this.quietOffer(kind, cli, known));
342
369
  await this.report();
@@ -353,7 +380,6 @@ export class Agents {
353
380
  ...(form === undefined ? {} : { form }),
354
381
  kit_release: KIT_RELEASE,
355
382
  wire: KIT_WIRE,
356
- updates_itself: await updatesItself(),
357
383
  agent_base: agentBase(),
358
384
  agents,
359
385
  transcription: transcriptionReport(),
package/dist/building.js CHANGED
@@ -16,10 +16,14 @@ async function served(name, signal) {
16
16
  await answer.body?.cancel();
17
17
  throw new Error(`House answered ${answer.status} for ${name}`);
18
18
  }
19
- export async function servedDigest(name, signal) {
19
+ export async function servedRelease(signal) {
20
20
  const manifest = (await (await served(MANIFEST, signal)).json());
21
- const sha256 = manifest.files?.[name]?.sha256;
22
- return typeof sha256 === 'string' ? sha256 : null;
21
+ const installer = manifest.hosts?.linux;
22
+ const sha256 = typeof installer === 'string' ? manifest.files?.[installer]?.sha256 : undefined;
23
+ if (typeof manifest.version !== 'string' || typeof installer !== 'string' || typeof sha256 !== 'string') {
24
+ throw new Error(`House serves no Kit release in ${MANIFEST}`);
25
+ }
26
+ return { version: manifest.version, installer, sha256 };
23
27
  }
24
28
  export async function downloadServed(name, sha256, destination, signal, most = Infinity) {
25
29
  const answer = await served(name, signal);
package/dist/clis.js CHANGED
@@ -1,11 +1,10 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { constants, readFileSync } from 'node:fs';
4
- import { access, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
5
5
  import { homedir, userInfo } from 'node:os';
6
6
  import { delimiter, dirname, isAbsolute, join } from 'node:path';
7
- import { downloadServed, servedDigest } from './building.js';
8
- import { kitHome } from './home.js';
7
+ import { releaseDirectory } from './placement.js';
9
8
  import { childEnvironment } from './relay.js';
10
9
  const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
11
10
  function houseRule(houses) {
@@ -74,6 +73,7 @@ export const CLIS = {
74
73
  bin: 'codex',
75
74
  minimum: '0.160.1',
76
75
  install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
76
+ update: 'codex update',
77
77
  adapter: {
78
78
  package: '@agentclientprotocol/codex-acp',
79
79
  bin: 'codex-acp',
@@ -97,6 +97,7 @@ export const CLIS = {
97
97
  bin: 'claude',
98
98
  minimum: '2.1.303',
99
99
  install: 'curl -fsSL https://claude.ai/install.sh | bash',
100
+ update: 'claude update',
100
101
  adapter: {
101
102
  package: '@agentclientprotocol/claude-agent-acp',
102
103
  bin: 'claude-agent-acp',
@@ -127,6 +128,7 @@ export const CLIS = {
127
128
  bin: 'grok',
128
129
  minimum: '1.0.46',
129
130
  install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
131
+ update: 'grok update',
130
132
  adapter: null,
131
133
  args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
132
134
  fullAccess: null,
@@ -142,25 +144,13 @@ export const CLIS = {
142
144
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
143
145
  export const KIT_RELEASE = KIT_PACKAGE.version;
144
146
  const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
145
- // An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
147
+ // An install or update still running after fifteen minutes is ended as failed, so a hung download can neither hold its CLI's report nor keep the Kit busy.
146
148
  const INSTALL_MS = 15 * 60_000;
147
149
  // A login shell or a version read is ended after half a minute, so a profile that waits for input cannot stall the report.
148
150
  const READ_MS = 30_000;
149
151
  const CAUSE_CHARACTERS = 4000;
150
- function prefix(kind) {
151
- return join(kitHome(), 'agents', kind);
152
- }
153
152
  export function adapterCommand(kind) {
154
- return join(prefix(kind), 'node_modules', '.bin', CLIS[kind].adapter.bin);
155
- }
156
- async function installedAdapter(kind) {
157
- try {
158
- const manifest = join(prefix(kind), 'node_modules', CLIS[kind].adapter.package, 'package.json');
159
- return JSON.parse(await readFile(manifest, 'utf8')).version;
160
- }
161
- catch {
162
- return null;
163
- }
153
+ return join(releaseDirectory(KIT_RELEASE), 'adapters', kind, 'node_modules', '.bin', CLIS[kind].adapter.bin);
164
154
  }
165
155
  export function run(command, args, timeoutMs, detached = false, signal) {
166
156
  return new Promise((resolve) => {
@@ -204,38 +194,15 @@ export function failureOf(ran, program) {
204
194
  export function adapterArchive(kind) {
205
195
  return `${kind}-${KIT_PACKAGE.peerDependencies[CLIS[kind].adapter.package]}.tar.gz`;
206
196
  }
207
- export async function installAdapter(kind, signal) {
208
- const adapter = CLIS[kind].adapter;
209
- if (adapter === null)
210
- return null;
211
- if ((await installedAdapter(kind)) === KIT_PACKAGE.peerDependencies[adapter.package])
212
- return null;
213
- const name = adapterArchive(kind);
214
- const archive = `${prefix(kind)}.tar.gz`;
197
+ async function bounded(program, work, signal) {
215
198
  const deadline = AbortSignal.timeout(INSTALL_MS);
216
- const bounded = signal === undefined ? deadline : AbortSignal.any([signal, deadline]);
217
- try {
218
- const sha256 = await servedDigest(name, bounded);
219
- if (sha256 === null)
220
- return `House serves no ${name}`;
221
- await rm(prefix(kind), { recursive: true, force: true });
222
- await mkdir(prefix(kind), { recursive: true });
223
- await downloadServed(name, sha256, archive, bounded);
224
- const ran = await run('tar', ['-xzf', archive, '-C', prefix(kind)], INSTALL_MS, true, signal);
225
- return ran.status === 0 ? null : failureOf(ran, 'tar');
226
- }
227
- catch (error) {
228
- if (deadline.aborted && signal?.aborted !== true)
229
- return `${name} did not arrive within ${INSTALL_MS / 1000} seconds`;
230
- return error instanceof Error ? error.message : String(error);
231
- }
232
- finally {
233
- await rm(archive, { force: true });
234
- }
199
+ const ran = await work(signal === undefined ? deadline : AbortSignal.any([signal, deadline]));
200
+ if (deadline.aborted && signal?.aborted !== true)
201
+ return `${program} did not end within ${INSTALL_MS / 1000} seconds`;
202
+ return ran.status === 0 ? null : failureOf(ran, program);
235
203
  }
236
- export async function installCli(kind, signal) {
237
- const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
238
- return ran.status === 0 ? null : failureOf(ran, 'the install');
204
+ export function installCli(kind, signal) {
205
+ return bounded('the install', (within) => run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, within), signal);
239
206
  }
240
207
  function loginShell() {
241
208
  return process.env.SHELL || userInfo().shell;
@@ -243,8 +210,8 @@ function loginShell() {
243
210
  const FOUND = 'house-kit-cli ';
244
211
  const LOOKUP = `found=$(command -v "$1") || exit 0; case $found in /*) ;; *) found=$(pwd -P)/$found ;; esac; printf "${FOUND}%s\\n" "$found"`;
245
212
  const USER_BIN = 'case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) PATH="$PATH:$HOME/.local/bin" ;; esac; export PATH';
246
- function inLoginShell(script, argument = '', signal) {
247
- return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], READ_MS, true, signal);
213
+ function inLoginShell(script, argument = '', signal, timeoutMs = READ_MS) {
214
+ return run(loginShell(), ['-l', '-i', '-c', `exec /bin/sh -c '${USER_BIN}; ${script}' sh ${argument}`], timeoutMs, true, signal);
248
215
  }
249
216
  export async function onLoginPath(program, signal) {
250
217
  const ran = await inLoginShell(LOOKUP, program, signal);
@@ -253,6 +220,9 @@ export async function onLoginPath(program, signal) {
253
220
  export function locate(kind, signal) {
254
221
  return onLoginPath(CLIS[kind].bin, signal);
255
222
  }
223
+ export function updateCli(kind) {
224
+ return bounded('the update', (within) => inLoginShell(CLIS[kind].update, '', within, INSTALL_MS));
225
+ }
256
226
  export async function loginPath() {
257
227
  const marker = 'house-kit-path ';
258
228
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
@@ -4,7 +4,7 @@ import { end, REPLACED, signInAgain } from './end.js';
4
4
  import { readEnrolment } from './home.js';
5
5
  import { relayed } from './relay.js';
6
6
  import { Silence } from './silence.js';
7
- import { endUnserved, KIT_WIRE } from './wire.js';
7
+ import { endUnserved, KIT_WIRE, unserved } from './wire.js';
8
8
  const PATH = '/kit/stream';
9
9
  const SUBPROTOCOL = `house.kit.stream.${KIT_WIRE}`;
10
10
  const REFUSAL_HEADER = 'x-house-refusal';
@@ -32,11 +32,10 @@ subscribe('undici:request:headers', (message) => {
32
32
  export function holdConnection(handlers) {
33
33
  let connection = null;
34
34
  let socket = null;
35
- let paused = false;
36
35
  let failures = 0;
37
36
  const connect = async () => {
38
37
  const { house, credential } = (await readEnrolment());
39
- if (paused || socket !== null)
38
+ if (socket !== null || unserved())
40
39
  return;
41
40
  refused = null;
42
41
  const opening = new WebSocket(relayed(connectionUrl(house)), {
@@ -65,8 +64,6 @@ export function holdConnection(handlers) {
65
64
  endUnserved(refused.text);
66
65
  if (!opened && refused !== null)
67
66
  process.stderr.write(`kit: ${refused.message}\n`);
68
- if (paused)
69
- return;
70
67
  // A closed connection reopens in one to three seconds, spread so Kit processes one restart closed return apart; an opening House refused or left unanswered backs off on the jittered curve, retryable or not, since a resident outlives a House deploy.
71
68
  setTimeout(() => void connect(), opened ? 1000 + Math.random() * 2000 : retryDelay(failures++));
72
69
  };
@@ -104,13 +101,5 @@ export function holdConnection(handlers) {
104
101
  connection.send(JSON.stringify(notification));
105
102
  return true;
106
103
  },
107
- pause() {
108
- paused = true;
109
- socket?.close();
110
- return () => {
111
- paused = false;
112
- void connect();
113
- };
114
- },
115
104
  };
116
105
  }
package/dist/end.js CHANGED
@@ -14,7 +14,7 @@ export function end(reason) {
14
14
  export function restart(reason) {
15
15
  exit(reason, 1);
16
16
  }
17
- function updateLine(lead) {
17
+ export function updateLine(lead) {
18
18
  const command = installCommand();
19
19
  return command === null ? `${lead}; install it again.` : `${lead}. Update it: ${command}`;
20
20
  }
package/dist/home.js CHANGED
@@ -8,7 +8,6 @@ const ENROLMENT = 'credential.json';
8
8
  const DISCONNECTED = 'disconnected.json';
9
9
  const CONFIGURATION = 'kit.json';
10
10
  const OWN_AGENT = 'own-agent.json';
11
- const RESTART = 'restart';
12
11
  let defaulted;
13
12
  function writable(directory) {
14
13
  try {
@@ -132,12 +131,6 @@ export async function forgetRejected(rejected) {
132
131
  export function enrolled() {
133
132
  return existsSync(join(kitHome(), ENROLMENT));
134
133
  }
135
- export function restartAsked() {
136
- return existsSync(join(kitHome(), RESTART));
137
- }
138
- export function forgetRestart() {
139
- return rm(join(kitHome(), RESTART), { force: true });
140
- }
141
134
  export function readOwnAgent() {
142
135
  return readHome(OWN_AGENT);
143
136
  }
package/dist/placement.js CHANGED
@@ -1,23 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { run } from './clis.js';
2
+ import { join, resolve } from 'node:path';
4
3
  import { kitHome } from './home.js';
5
- export function service() {
6
- if (process.platform !== 'linux' || !process.env.INVOCATION_ID)
7
- return null;
8
- return process.env.MANAGERPID ? 'user' : 'system';
9
- }
10
- export function throughSudo() {
11
- return service() === 'system' && process.getuid() !== 0;
12
- }
13
- export async function updatesItself() {
14
- if (service() === null)
15
- return false;
16
- if (!throughSudo())
17
- return true;
18
- // sudo answers at once, so ten seconds only bounds a stuck sudo.
19
- return (await run('sudo', ['-n', 'true'], 10_000)).status === 0;
20
- }
21
4
  function recorded(name) {
22
5
  try {
23
6
  return readFileSync(join(kitHome(), name), 'utf8').trim();
@@ -37,3 +20,12 @@ export function machine() {
37
20
  return placed;
38
21
  return process.platform === 'darwin' ? 'macos' : 'linux';
39
22
  }
23
+ export function placedPrefix() {
24
+ const prefix = process.env.HOUSE_KIT_PREFIX;
25
+ if (prefix === undefined || prefix === '')
26
+ throw new Error('this Kit process was not started by a launcher its installer placed');
27
+ return resolve(prefix);
28
+ }
29
+ export function releaseDirectory(release) {
30
+ return join(placedPrefix(), 'releases', release);
31
+ }
package/dist/resident.js CHANGED
@@ -11,7 +11,7 @@ import { received } from './bridge.js';
11
11
  import { CONVERSATION_METHODS, Conversations } from './conversations.js';
12
12
  import { LocalCopies } from './copies.js';
13
13
  import { restart, unknownMethod } from './end.js';
14
- import { agentBase, enrolled, forgetRestart, kitHome, ownerLock, ownerSocket, readEnrolment, restartAsked } from './home.js';
14
+ import { agentBase, enrolled, kitHome, ownerLock, ownerSocket, readEnrolment } from './home.js';
15
15
  import { Calls } from './calls.js';
16
16
  import { killDescendants } from './scope.js';
17
17
  import { removeInputs, Scripts } from './scripts.js';
@@ -20,6 +20,7 @@ import { SignIns } from './sign-in.js';
20
20
  import { placeSkillSet } from './skills.js';
21
21
  import { holdConnection } from './connection.js';
22
22
  import { KitUpdate } from './update.js';
23
+ import { takeUnservedRelease } from './wire.js';
23
24
  import { codexLauncher } from './work.js';
24
25
  function report(error) {
25
26
  process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -110,7 +111,6 @@ function connected() {
110
111
  }
111
112
  async function started() {
112
113
  await connected();
113
- await forgetRestart();
114
114
  watch(kitHome(), () => {
115
115
  if (enrolled())
116
116
  return;
@@ -140,11 +140,8 @@ export async function resident() {
140
140
  if (!idle()) {
141
141
  reported = false;
142
142
  }
143
- else if (restartAsked() && conversations.quiet()) {
144
- restart('restarting for an updated CLI');
145
- }
146
- else if (update.held() && conversations.quiet()) {
147
- update.apply(connection.pause());
143
+ else if (update.placedRelease() !== null && conversations.quiet()) {
144
+ restart(`Kit ${update.placedRelease()} is placed; its supervisor starts it`);
148
145
  }
149
146
  else if (!reported) {
150
147
  reported = true;
@@ -161,7 +158,9 @@ export async function resident() {
161
158
  changed,
162
159
  });
163
160
  const signIns = new SignIns(house, agents, calls);
164
- const update = new KitUpdate(calls, changed);
161
+ const update = new KitUpdate(calls);
162
+ if (process.env.HOUSE_KIT_SANDBOX !== '1')
163
+ takeUnservedRelease((release) => logged(update.takeRefused(release)));
165
164
  removeInputs();
166
165
  const scripts = new Scripts(calls, (notification) => connection?.send(notification) ?? false);
167
166
  for (const signal of ['SIGTERM', 'SIGINT']) {
@@ -172,8 +171,8 @@ export async function resident() {
172
171
  });
173
172
  }
174
173
  await house.deliver('/kit/restarted', {}).catch(report);
175
- await update.resumed().catch(report);
176
174
  logged(agents.refresh());
175
+ agents.keepCurrent();
177
176
  connection = holdConnection({
178
177
  opened: () => {
179
178
  conversations.opened();
@@ -185,7 +184,7 @@ export async function resident() {
185
184
  if (call.kind === 'sign_in')
186
185
  signIns.start(call);
187
186
  else if (call.kind === 'update')
188
- update.hold(call);
187
+ update.take(call);
189
188
  else if (call.kind === 'run_wizard' || call.kind === 'run_function')
190
189
  scripts.start(call);
191
190
  else if (CONVERSATION_METHODS.has(call.kind))
package/dist/scope.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { mkdirSync, readdirSync, readFileSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import { mkdirSync, readdirSync, readFileSync, realpathSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  const LISTING_BYTES = 1 << 28;
5
5
  const CGROUPS = '/sys/fs/cgroup';
@@ -122,15 +122,23 @@ function members(group) {
122
122
  }
123
123
  return pids;
124
124
  }
125
+ function besideCli(cli) {
126
+ try {
127
+ return `${realpathSync(cli)}-`;
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
125
133
  export class Scope {
126
134
  marker;
127
- cli;
135
+ helpers;
128
136
  group;
129
137
  session = null;
130
138
  recorded = null;
131
139
  constructor(marker, name, cli) {
132
140
  this.marker = marker;
133
- this.cli = cli;
141
+ this.helpers = besideCli(cli);
134
142
  const own = ownGroup();
135
143
  this.group = own === null ? null : join(own, name);
136
144
  try {
@@ -168,7 +176,7 @@ export class Scope {
168
176
  return true;
169
177
  if (this.recorded === null)
170
178
  return found.size > 0;
171
- return [...found].some(([pid, { session, command }]) => session !== this.session || !(this.recorded.has(pid) || command.startsWith(`${this.cli}-`)));
179
+ return [...found].some(([pid, { session, command }]) => session !== this.session || !(this.recorded.has(pid) || (this.helpers !== null && command.startsWith(this.helpers))));
172
180
  }
173
181
  empty() {
174
182
  return this.found()?.size === 0;
package/dist/update.js CHANGED
@@ -1,181 +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
- import { relayed } from './relay.js';
10
- const BOOTSTRAP_BYTES = 1_000_000;
11
- // A bootstrap is at most a million bytes, so two minutes carry it over ten kilobytes a second and no stalled host holds the update queue.
12
- const BOOTSTRAP_MS = 120_000;
13
- // A bootstrap's downloads and CLI updates take minutes, so one still running after an hour has hung and is ended before it keeps the Kit offline.
14
- const RERUN_MS = 60 * 60_000;
15
- const TIMED_OUT = '124';
16
- function directory() {
17
- return join(kitHome(), 'update');
18
- }
19
- function pending() {
20
- return join(directory(), 'pending.json');
21
- }
22
- function outcome() {
23
- return join(directory(), 'outcome');
24
- }
25
- function unit(callId) {
26
- return `house-kit-update-${callId}`;
27
- }
28
- function manager() {
29
- return service() === 'user' ? ['--user'] : [];
30
- }
31
- function elevated() {
32
- return throughSudo() ? ['sudo', '-n'] : [];
33
- }
34
- function account() {
35
- return throughSudo() ? [`--uid=${process.getuid()}`, `--gid=${process.getgid()}`] : [];
36
- }
37
- function placement() {
38
- return service() === 'user' ? ['--workspace', workspaceRoot()] : ['--linux'];
39
- }
40
- function record() {
41
- return `outcome=$1; shift; timeout ${RERUN_MS / 1000} bash "$0" "$@"; printf %s "$?" > "$outcome"; ${[...elevated(), 'systemctl', ...manager(), 'start', 'house-kit.service'].join(' ')}`;
42
- }
43
- async function pendingCall() {
44
- try {
45
- return JSON.parse(await readFile(pending(), 'utf8')).input_id;
46
- }
47
- catch {
48
- return null;
49
- }
50
- }
51
- async function recorded() {
52
- const status = await readFile(outcome(), 'utf8').catch(() => null);
53
- if (status === null)
54
- return null;
55
- if (status.trim() === TIMED_OUT)
56
- return { refused: `the Kit bootstrap did not finish within ${RERUN_MS / 1000} seconds` };
57
- return status.trim() === '0' ? {} : { refused: `the Kit bootstrap exited with ${status.trim()}` };
58
- }
59
- async function forget(callId) {
60
- if ((await pendingCall()) !== callId)
61
- return;
62
- await rm(pending(), { force: true });
63
- await rm(outcome(), { force: true });
64
- }
65
- async function finished(callId) {
66
- for (;;) {
67
- const result = await recorded();
68
- if (result !== null)
69
- return result;
70
- // systemctl answers at once, so ten seconds only bounds a stuck service manager.
71
- if ((await run('systemctl', [...manager(), 'is-active', '--quiet', unit(callId)], 10_000)).status !== 0) {
72
- return (await recorded()) ?? { refused: 'the Kit bootstrap did not finish' };
73
- }
74
- // The bootstrap runs for minutes, so checking each second sees its outcome within a second at little cost.
75
- await new Promise((resolve) => setTimeout(resolve, 1000));
76
- }
77
- }
78
- async function bounded(body, most) {
79
- const chunks = [];
80
- let bytes = 0;
81
- for await (const chunk of body ?? []) {
82
- bytes += chunk.length;
83
- if (bytes > most)
84
- return null;
85
- chunks.push(chunk);
86
- }
87
- return Buffer.concat(chunks, bytes);
88
- }
89
- function rerun(script, callId, flags) {
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);
21
+ }
22
+ function placing(installer, house, prefix, signal) {
90
23
  return new Promise((resolve) => {
91
- const [command, ...args] = [
92
- ...elevated(),
93
- 'systemd-run',
94
- ...manager(),
95
- ...account(),
96
- '--wait',
97
- '--collect',
98
- '--quiet',
99
- `--unit=${unit(callId)}`,
100
- `--setenv=HOME=${homedir()}`,
101
- `--setenv=HOUSE_KIT_HOME=${kitHome()}`,
102
- '/bin/sh',
103
- '-c',
104
- record(),
105
- script,
106
- outcome(),
107
- ...placement(),
108
- ...flags,
109
- ];
110
- const child = spawn(command, args, { stdio: ['ignore', 'ignore', 'inherit'] });
111
- child.on('error', () => resolve({ refused: 'no service manager runs this Kit process' }));
112
- 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
+ });
113
59
  });
114
60
  }
115
61
  export class KitUpdate {
116
62
  calls;
117
- changed;
118
- waiting = new Map();
119
63
  queue = Promise.resolve();
120
- constructor(calls, changed) {
64
+ placed = null;
65
+ constructor(calls) {
121
66
  this.calls = calls;
122
- this.changed = changed;
123
67
  }
124
- async resumed() {
125
- const callId = await pendingCall();
126
- if (callId === null)
127
- return;
128
- 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)) }));
129
70
  }
130
- hold(call) {
131
- this.waiting.set(call.input_id, call);
132
- this.changed();
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
+ }
133
79
  }
134
- held() {
135
- return this.waiting.size > 0;
80
+ placedRelease() {
81
+ return this.placed;
136
82
  }
137
- apply(resume) {
138
- const held = [...this.waiting.values()];
139
- this.waiting.clear();
140
- void Promise.all(held.map((call) => this.enqueue(call.input_id, () => this.carry(call)))).then(resume);
83
+ queued(work) {
84
+ const taken = this.queue.then(work);
85
+ this.queue = taken.catch(() => undefined);
86
+ return taken;
141
87
  }
142
- enqueue(callId, work) {
143
- const before = this.queue;
144
- const carried = this.calls.carry(callId, async () => {
145
- await before;
146
- return { result: await work(), returned: () => forget(callId) };
147
- });
148
- this.queue = before.then(() => carried);
149
- return carried;
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 {};
150
98
  }
151
- async carry(call) {
152
- if ((await pendingCall()) === call.input_id)
153
- return finished(call.input_id);
154
- const deadline = AbortSignal.timeout(BOOTSTRAP_MS);
155
- let script;
99
+ async place(release, which) {
100
+ const signal = AbortSignal.timeout(PLACEMENT_MS);
101
+ let version = release;
102
+ let failure;
156
103
  try {
157
- const answer = await fetch(relayed(call.bootstrap), { signal: deadline });
158
- if (!answer.ok)
159
- return { refused: `the Kit bootstrap could not be downloaded: ${answer.status}` };
160
- script = await bounded(answer.body, BOOTSTRAP_BYTES);
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);
161
115
  }
162
116
  catch (error) {
163
- if (!deadline.aborted)
164
- throw error;
165
- return { refused: `the Kit bootstrap did not arrive within ${BOOTSTRAP_MS / 1000} seconds` };
117
+ failure = signal.aborted ? `the placement did not end within ${PLACEMENT_MS / 1000} seconds` : causeOf(error);
166
118
  }
167
- if (script === null)
168
- return { refused: `the Kit bootstrap is larger than ${BOOTSTRAP_BYTES} bytes` };
169
- if (createHash('sha256').update(script).digest('hex') !== call.sha256) {
170
- return { refused: 'the Kit bootstrap does not match its sha256' };
119
+ if (failure !== null) {
120
+ const refused = `Kit ${version} could not be placed: ${failure}`;
121
+ process.stderr.write(`kit: ${refused}\n`);
122
+ return refused;
171
123
  }
172
- await mkdir(directory(), { recursive: true, mode: 0o700 });
173
- const path = join(directory(), 'connect-linux.sh');
174
- await writeFile(path, script, { mode: 0o700 });
175
- await rm(outcome(), { force: true });
176
- await writeFile(pending(), `${JSON.stringify({ input_id: call.input_id })}\n`, { mode: 0o600 });
177
- return (await rerun(path, call.input_id, call.update_clis ? ['--update-clis'] : [])) ?? (await recorded()) ?? {
178
- refused: 'the Kit bootstrap did not finish',
179
- };
124
+ this.placed = version;
125
+ return null;
180
126
  }
181
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-ceeaf32286734",
3
+ "version": "0.2.1-cf268dff1869b",
4
4
  "description": "Kit connects an Environment to agents.house.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "@agentclientprotocol/sdk": "2.0.1",
28
28
  "@agentshouse/mdmodel": "0.1.0-c4f4d37bc9d6a",
29
- "@agentshouse/skills": "0.1.0-c27d55a17e1d8",
29
+ "@agentshouse/skills": "0.1.0-cdd0f15bef719",
30
30
  "@lydell/node-pty": "1.2.0-beta.15",
31
31
  "yaml": "2.9.1"
32
32
  },
package/dist/clis-main.js DELETED
@@ -1,23 +0,0 @@
1
- import { post } from './api.js';
2
- import { CLIS, below, locate, versionOf } from './clis.js';
3
- import { readEnrolment } from './home.js';
4
- import { proxyUnlessRelayed } from './relay.js';
5
- proxyUnlessRelayed();
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
- }