@agentshouse/kit 0.2.1-c5d51f1e4ce19 → 0.2.1-c64c31420c9bf

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
@@ -5,18 +5,18 @@ your Agents' conversations through the agent CLIs you chose and gives each Agent
5
5
 
6
6
  ## Install
7
7
 
8
- Run `curl -fsSL agents.house/kit | sh`, or `irm agents.house/kit.ps1 | iex` on Windows, and again to update;
9
- options follow `sh -s --`. On macOS and Linux it installs House Kit natively under your own account, without an admin password,
10
- as a service that runs while you are logged in. `kit` and `house` are then on
11
- your login shell's `PATH`, and your workspace is `~/AgentsHouse` unless you
8
+ Run `sh -c 'kit=$(curl -fsSL "$0") || { echo "House Kit could not be downloaded; run this command again." >&2; exit 1; }; eval "$kit"' agents.house/kit`,
9
+ or `& { try { $kit = irm agents.house/kit.ps1 } catch { throw 'House 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
+ On macOS and Linux it installs House Kit natively under your own account, without an admin password, as a service that runs
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
12
  pass `--workspace <path>`.
13
13
 
14
14
  Add `--container` to run House Kit in one Docker container instead, with your
15
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
17
- `house` command to the container's options to run it in that container.
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.
18
17
  Running the command for the other choice on a connected computer asks you to
19
- confirm, then replaces its Environment, whose Agents end, with a new one.
18
+ confirm, then replaces its Environment, whose Agents end, with a new one. Running it with another `HOUSE_KIT_HOME`
19
+ than the installed Kit's asks you to confirm before it replaces that Kit, and leaves that Kit home as it was.
20
20
 
21
21
  On your own Linux server, run the Linux command with `--linux` to
22
22
  install House Kit natively, with its workspace at `/agents/house`; `kit` and
@@ -37,8 +37,8 @@ or another, continues on its own and keeps the Environment's credential in
37
37
  reconnects an Environment you already have; `--rooms <ref>[,<ref>...]` gives it
38
38
  only those Rooms and `--no-private` every Room but your Private Room; `--code <code>` connects immediately without a link.
39
39
  A login also connects your own agent: outside an Agent conversation, `house <tool>` calls
40
- House as you and `git` clones and pushes your Apps at `<house>/app/` as you, until you revoke
41
- that connection on Connections. `house help` lists the Tools. Codex sessions started after a
40
+ House as you and `git` clones and pushes your Apps at `<house>/app/` as you, each through the Kit
41
+ home `$HOUSE_KIT_HOME` names where it runs, until you revoke that connection on Connections. `house help` lists the Tools. Codex sessions started after a
42
42
  login run a lone `house` command outside their sandbox. `kit logout` disconnects this
43
43
  computer, stops House Kit and ends your own agent's connection, so `house` and `git` no
44
44
  longer act as you here; a later `kit login` reconnects the same Environment, or connects
package/dist/agents.js CHANGED
@@ -125,6 +125,7 @@ function reported(kind, reading) {
125
125
  full_access: CLIS[kind].fullAccess,
126
126
  };
127
127
  }
128
+ const NOT_READY = "the Agent's CLI is not ready on this computer";
128
129
  function tooOld(kind, release) {
129
130
  const cli = CLIS[kind];
130
131
  return `${cli.bin} ${release} is older than ${cli.minimum}, the oldest this Kit runs`;
@@ -213,7 +214,7 @@ export class Agents {
213
214
  const resolved = await this.resolve(kind);
214
215
  if (!('cli' in resolved)) {
215
216
  this.update(kind, resolved);
216
- throw new Error(resolved.failure ?? tooOld(kind, resolved.release));
217
+ throw new Error(resolved.failure === null ? tooOld(kind, resolved.release) : NOT_READY);
217
218
  }
218
219
  const { cli, release } = resolved;
219
220
  const known = this.readings.get(kind);
package/dist/api.js CHANGED
@@ -46,7 +46,7 @@ export async function post(house, path, body, credential, signal) {
46
46
  });
47
47
  const text = await answer.text();
48
48
  if (!answer.ok && tooOld(text))
49
- updateRequired(house);
49
+ updateRequired();
50
50
  if (!answer.ok)
51
51
  throw new HouseRefusal(path, answer.status, text);
52
52
  return JSON.parse(text);
package/dist/app-git.js CHANGED
@@ -1,15 +1,9 @@
1
- import { extname, resolve } from 'node:path';
2
- import { fileURLToPath } from 'node:url';
3
1
  import { run } from './git.js';
4
2
  import { kitHome, readOwnAgent } from './home.js';
5
- import { quoted } from './shell.js';
3
+ const HELPER = '!kit git-credential';
6
4
  function section(house) {
7
5
  return `credential.${new URL('/app/', house).href}`;
8
6
  }
9
- function helper() {
10
- const main = fileURLToPath(new URL(`./kit-main${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
11
- return `!HOUSE_KIT_HOME=${quoted(resolve(kitHome()))} ${quoted(process.execPath)} ${quoted(main)} git-credential`;
12
- }
13
7
  async function configured(args) {
14
8
  try {
15
9
  return await run(kitHome(), ['config', '--global', ...args]);
@@ -20,30 +14,18 @@ async function configured(args) {
20
14
  throw error;
21
15
  }
22
16
  }
23
- async function forget(house) {
24
- await configured(['--remove-section', section(house)]);
25
- }
26
- async function register(house) {
27
- await forget(house);
28
- for (const value of ['', helper()]) {
29
- const ran = await configured(['--add', `${section(house)}.helper`, value]);
17
+ export async function placeAppGit(ownAgent) {
18
+ if (ownAgent === null)
19
+ return;
20
+ await configured(['--remove-section', section(ownAgent.house)]);
21
+ for (const value of ['', HELPER]) {
22
+ const ran = await configured(['--add', `${section(ownAgent.house)}.helper`, value]);
30
23
  if (ran === null)
31
24
  return;
32
25
  if (ran.status !== 0)
33
26
  throw new Error(`git config failed: ${ran.stderr.trim()}`);
34
27
  }
35
28
  }
36
- export async function placeAppGit(before, after) {
37
- if (before !== null && before.house !== after?.house)
38
- await forget(before.house);
39
- if (after !== null)
40
- await register(after.house);
41
- }
42
- export async function forgetAppGit() {
43
- const ownAgent = await readOwnAgent();
44
- if (ownAgent !== null)
45
- await forget(ownAgent.house);
46
- }
47
29
  export async function answerGitCredential(operation) {
48
30
  let asked = '';
49
31
  for await (const chunk of process.stdin)
@@ -55,8 +37,10 @@ export async function answerGitCredential(operation) {
55
37
  .filter((line) => line.includes('='))
56
38
  .map((line) => [line.slice(0, line.indexOf('=')), line.slice(line.indexOf('=') + 1)]));
57
39
  const ownAgent = await readOwnAgent();
58
- if (ownAgent === null)
40
+ if (ownAgent === null) {
41
+ process.stderr.write(`kit: Kit home ${kitHome()} is not signed in; run kit login\n`);
59
42
  return;
43
+ }
60
44
  const house = new URL(ownAgent.house);
61
45
  if (attributes.get('protocol') !== house.protocol.slice(0, -1) || attributes.get('host') !== house.host)
62
46
  return;
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "version": "0.2.1-c5d51f1e4ce19",
3
+ "version": "0.2.1-c64c31420c9bf",
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-c5d51f1e4ce19/connect-linux.sh"
8
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c64c31420c9bf/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-c5d51f1e4ce19/connect-linux.sh"
14
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c64c31420c9bf/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-c5d51f1e4ce19/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c64c31420c9bf/connect-windows.ps1"
21
21
  }
22
22
  }
23
23
  }
package/dist/bridge.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { once } from 'node:events';
3
3
  import { setTimeout as delay } from 'node:timers/promises';
4
- import { mkdir } from 'node:fs/promises';
5
4
  import { createServer, request as httpRequest } from 'node:http';
6
5
  import { request as httpsRequest } from 'node:https';
7
6
  import { delimiter, dirname, join } from 'node:path';
@@ -9,7 +8,7 @@ import { parse } from 'yaml';
9
8
  import { finalRefusal, HouseRefusal, retryDelay, retrying } from './api.js';
10
9
  import { loginPath } from './clis.js';
11
10
  import { changedPaths, } from './copies.js';
12
- import { kitHome, readEnrolment } from './home.js';
11
+ import { kitHome, readEnrolment, socketDirectory } from './home.js';
13
12
  import { callHouse, mcpBody, rpc, UNREACHABLE } from './mcp.js';
14
13
  import { operationId } from './operation.js';
15
14
  import { refusalOf, retryable } from './refusals.js';
@@ -167,9 +166,7 @@ export async function openBridge(house, copies, conversationId, signal) {
167
166
  }
168
167
  return path === '/' ? forward(text) : { status: 404, text: '' };
169
168
  };
170
- const sockets = join(kitHome(), 'bridges');
171
- await mkdir(sockets, { recursive: true, mode: 0o700 });
172
- const socketPath = join(sockets, `${randomUUID()}.sock`);
169
+ const socketPath = join(socketDirectory(), `${randomUUID()}.sock`);
173
170
  const bridge = createServer(async (request, response) => {
174
171
  try {
175
172
  const path = request.url ?? '';
package/dist/chatgpt.js CHANGED
@@ -24,7 +24,7 @@ export async function chatgpt(argv) {
24
24
  throw new Error(`${house} does not accept this key; pass one from ${KEY_SOURCE}`);
25
25
  const relay = process.env.HOUSE_KIT_RELAY || undefined;
26
26
  await writeOwnAgent({ house, ...(relay === undefined ? {} : { relay }), credential: values.key });
27
- await placeAppGit(before, await readOwnAgent()).catch((error) => process.stderr.write(`kit: ${error.message}\n`));
27
+ await placeAppGit(await readOwnAgent()).catch((error) => process.stderr.write(`kit: ${error.message}\n`));
28
28
  if (before !== null && before.credential !== values.key) {
29
29
  process.stdout.write('This replaced the key this Kit home held; give each session its own HOUSE_KIT_HOME to keep its own key.\n');
30
30
  }
@@ -18,6 +18,21 @@ import { transcribe } from './transcription.js';
18
18
  import { workFor } from './work.js';
19
19
  const KILLED = 'the conversation was killed';
20
20
  const REFUSED = 'the CLI refused the prompt';
21
+ const NO_CONNECTION = "the Kit could not open this conversation's connection";
22
+ const NO_START = "the Agent's CLI did not start";
23
+ const NO_SESSION = "the Agent's CLI did not open a session";
24
+ const NO_RESUME = "the Agent's CLI could not resume this conversation";
25
+ const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
26
+ class NotStarted extends Error {
27
+ }
28
+ async function step(reason, work) {
29
+ try {
30
+ return await work;
31
+ }
32
+ catch (error) {
33
+ throw new NotStarted(reason, { cause: error });
34
+ }
35
+ }
21
36
  const EXIT_PLAN_MODE = {
22
37
  parse(value) {
23
38
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
@@ -36,6 +51,13 @@ function answerMessage(request, response) {
36
51
  function logged(error) {
37
52
  process.stderr.write(`kit: ${causeOf(error)}\n`);
38
53
  }
54
+ function refusal(conversation, error) {
55
+ if (conversation.kills > 0)
56
+ return KILLED;
57
+ const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
58
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
59
+ return causeOf(error);
60
+ }
39
61
  function marker(bridge) {
40
62
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
41
63
  }
@@ -212,7 +234,7 @@ export class Conversations {
212
234
  return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
213
235
  }
214
236
  catch (error) {
215
- return { refused: causeOf(error) };
237
+ return { refused: refusal(conversation, error) };
216
238
  }
217
239
  }
218
240
  async message(conversation, input) {
@@ -229,7 +251,7 @@ export class Conversations {
229
251
  throw new Error(KILLED);
230
252
  }
231
253
  catch (error) {
232
- return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
254
+ return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
233
255
  }
234
256
  return {
235
257
  ack: new Promise((settle) => {
@@ -274,7 +296,7 @@ export class Conversations {
274
296
  conversation.session = null;
275
297
  await this.kill(conversation);
276
298
  }
277
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
299
+ waiting.settle({ refused: refusal(conversation, error) });
278
300
  return;
279
301
  }
280
302
  this.prompt(conversation, [...prompt, ...waiting.content]);
@@ -360,7 +382,7 @@ export class Conversations {
360
382
  .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
361
383
  .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
362
384
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
363
- const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
385
+ const bridge = await step(NO_CONNECTION, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
364
386
  if (conversation.kills > 0) {
365
387
  bridge.close();
366
388
  throw new Error(KILLED);
@@ -371,24 +393,25 @@ export class Conversations {
371
393
  try {
372
394
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
373
395
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
374
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
396
+ const streamed = await step(NO_CONNECTION, work.open(cli));
397
+ const adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
375
398
  work.read(message);
376
399
  this.read(conversation, reader, message);
377
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
400
+ }, { ...bridge.env, ...streamed }, undefined, scope));
378
401
  void adapter.exited.then(() => bridge.close());
379
402
  const agent = adapter.connection.agent;
380
403
  const cwd = route.working_directory;
381
404
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
382
405
  const opened = sessionId === null
383
- ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
384
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
406
+ ? await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta }))
407
+ : { ...(await step(NO_RESUME, agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta }))), sessionId };
385
408
  reader.session = opened.sessionId;
386
409
  if (sessionId === null && CLIS[route.kind].renames) {
387
410
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
388
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
411
+ await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
389
412
  }
390
413
  const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
391
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
414
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
392
415
  void work.settled().then(() => scope.record());
393
416
  conversation.running = running;
394
417
  conversation.session = running.sessionId;
package/dist/copies.js CHANGED
@@ -15,6 +15,9 @@ const IDENTITY = { GIT_COMMITTER_NAME: 'House Kit', GIT_COMMITTER_EMAIL: 'house-
15
15
  function causeOf(error) {
16
16
  return error instanceof Error ? error.message : String(error);
17
17
  }
18
+ function houseCause(error) {
19
+ return error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
20
+ }
18
21
  function logged(error) {
19
22
  process.stderr.write(`kit: ${causeOf(error)}\n`);
20
23
  }
@@ -261,7 +264,7 @@ export class LocalCopies {
261
264
  }
262
265
  catch (error) {
263
266
  refused = true;
264
- said.push(error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error));
267
+ said.push(houseCause(error));
265
268
  }
266
269
  }
267
270
  return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
@@ -549,7 +552,7 @@ export class LocalCopies {
549
552
  changes = await this.derived(copy, base, commit);
550
553
  }
551
554
  catch (error) {
552
- return { refused: true, text: causeOf(error) };
555
+ return { refused: true, text: houseCause(error) };
553
556
  }
554
557
  if (changes.length === 0)
555
558
  return { refused: false, text: `Nothing to submit: ${short(commit)} matches House.` };
@@ -576,12 +579,17 @@ export class LocalCopies {
576
579
  const after = await tree(copy.repository, commit);
577
580
  const moved = movedPaths(copy.moves, before, after);
578
581
  const arrived = new Set(moved.values());
579
- const binary = [];
580
- for (const [path, entry] of after)
581
- if (before.get(path) !== entry && !(await isText(copy.repository, entry)))
582
- binary.push(path);
583
- if (binary.length > 0)
584
- throw new Error(textOnly(binary));
582
+ const arriving = [...after].filter(([path, entry]) => before.get(path) !== entry);
583
+ if (arriving.length > 0) {
584
+ const { bounds } = await this.house.post('/kit/door/discover', { prefixes: [root] });
585
+ const binary = [];
586
+ for (const [path, entry] of arriving) {
587
+ if (!(await isText(copy.repository, entry, bounds.max_authored_file_bytes)))
588
+ binary.push(path);
589
+ }
590
+ if (binary.length > 0)
591
+ throw new Error(textOnly(binary));
592
+ }
585
593
  const text = (entry) => blobText(copy.repository, entry);
586
594
  const changes = [];
587
595
  for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
package/dist/end.js CHANGED
@@ -13,8 +13,9 @@ export function end(reason) {
13
13
  export function restart(reason) {
14
14
  exit(reason, 1);
15
15
  }
16
- export function updateRequired(house) {
17
- end(`This Kit is too old for House. Update it: ${installCommand(house)}`);
16
+ export function updateRequired() {
17
+ const command = installCommand();
18
+ end(command === null ? 'This Kit is too old for House; install it again.' : `This Kit is too old for House. Update it: ${command}`);
18
19
  }
19
20
  export async function signInAgain(rejected) {
20
21
  await forgetRejected(rejected);
package/dist/git.js CHANGED
@@ -49,16 +49,15 @@ export async function tree(repository, commit) {
49
49
  }
50
50
  return entries;
51
51
  }
52
- const TEXT_MAX_BYTES = 16 * 1024 * 1024;
53
52
  async function blob(repository, object) {
54
53
  return (await run(repository, ['cat-file', 'blob', object])).stdout;
55
54
  }
56
55
  export async function blobText(repository, object) {
57
56
  return (await blob(repository, object)).toString('utf8');
58
57
  }
59
- export async function isText(repository, object) {
58
+ export async function isText(repository, object, maxBytes) {
60
59
  const bytes = await blob(repository, object);
61
- return bytes.length <= TEXT_MAX_BYTES && isUtf8(bytes) && !bytes.includes(0);
60
+ return bytes.length <= maxBytes && isUtf8(bytes) && !bytes.includes(0);
62
61
  }
63
62
  export function blobId(content) {
64
63
  const bytes = Buffer.from(content, 'utf8');
package/dist/home.js CHANGED
@@ -1,8 +1,8 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { accessSync, chmodSync, constants, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync } from 'node:fs';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { accessSync, chmodSync, constants, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync } from 'node:fs';
3
3
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
4
  import { homedir, tmpdir } from 'node:os';
5
- import { join } from 'node:path';
5
+ import { join, resolve } from 'node:path';
6
6
  import { selectRelay } from './relay.js';
7
7
  const ENROLMENT = 'credential.json';
8
8
  const DISCONNECTED = 'disconnected.json';
@@ -19,24 +19,26 @@ function writable(directory) {
19
19
  return false;
20
20
  }
21
21
  }
22
- function ownTemporaryHome() {
23
- const home = join(tmpdir(), `house-kit-${process.getuid()}`);
22
+ function privateDirectory(path) {
24
23
  try {
25
- mkdirSync(home, { mode: 0o700 });
24
+ mkdirSync(path, { mode: 0o700 });
26
25
  }
27
26
  catch (error) {
28
27
  if (error.code !== 'EEXIST')
29
28
  throw error;
30
29
  }
31
- const found = lstatSync(home);
30
+ const found = lstatSync(path);
32
31
  if (found.isSymbolicLink())
33
- throw new Error(`${home} is a symbolic link`);
32
+ throw new Error(`${path} is a symbolic link`);
34
33
  if (!found.isDirectory())
35
- throw new Error(`${home} is not a directory`);
36
- if (found.uid !== process.getuid())
37
- throw new Error(`${home} is not owned by this User`);
38
- chmodSync(home, 0o700);
39
- return home;
34
+ throw new Error(`${path} is not a directory`);
35
+ if (found.uid !== process.geteuid())
36
+ throw new Error(`${path} is not owned by this User`);
37
+ chmodSync(path, 0o700);
38
+ return path;
39
+ }
40
+ function ownTemporaryHome() {
41
+ return privateDirectory(join(tmpdir(), `house-kit-${process.getuid()}`));
40
42
  }
41
43
  function defaultHome() {
42
44
  const home = join(homedir(), '.house-kit');
@@ -45,6 +47,18 @@ function defaultHome() {
45
47
  export function kitHome() {
46
48
  return process.env.HOUSE_KIT_HOME || (defaulted ??= defaultHome());
47
49
  }
50
+ export function socketDirectory() {
51
+ const sockets = privateDirectory(join(realpathSync('/tmp'), `hk-${process.geteuid()}`));
52
+ const home = kitHome();
53
+ const physical = existsSync(home) ? realpathSync(home) : resolve(home);
54
+ return privateDirectory(join(sockets, createHash('sha256').update(physical).digest('hex').slice(0, 12)));
55
+ }
56
+ export function ownerSocket() {
57
+ return join(socketDirectory(), 'owner.sock');
58
+ }
59
+ export function ownerLock() {
60
+ return join(socketDirectory(), 'owner.lock');
61
+ }
48
62
  export function workspaceRoot() {
49
63
  return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
50
64
  }
package/dist/house-cli.js CHANGED
@@ -4,7 +4,7 @@ import { request, setGlobalProxyFromEnv } from 'node:http';
4
4
  import { basename, dirname, extname, join, resolve } from 'node:path';
5
5
  import { verified } from './files.js';
6
6
  import { HOUSE_REF } from './git.js';
7
- import { kitHome, readOwnAgent } from './home.js';
7
+ import { ownerSocket, readOwnAgent } from './home.js';
8
8
  import { callHouse, rpc, UNREACHABLE } from './mcp.js';
9
9
  import { refusalOf } from './refusals.js';
10
10
  import { relayed, selectRelay } from './relay.js';
@@ -220,7 +220,7 @@ async function push(args) {
220
220
  throw new Error('--owner is not for Agent conversations; run house push without it');
221
221
  if (!owner && socketPath === undefined)
222
222
  throw new Error('outside an Agent conversation, house push needs --owner');
223
- const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit }, undefined, owner ? join(kitHome(), 'owner.sock') : socketPath));
223
+ const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit }, undefined, owner ? ownerSocket() : socketPath));
224
224
  if (pushed.refused)
225
225
  throw new Error(pushed.text);
226
226
  return pushed.text;
@@ -228,7 +228,7 @@ async function push(args) {
228
228
  async function fetched(args) {
229
229
  if (accepted(args, ['--help'], 0, FETCH_USAGE).flags.has('--help'))
230
230
  return FETCH_USAGE;
231
- const answer = JSON.parse(await bridged('/git/fetch', {}, undefined, socketPath ?? join(kitHome(), 'owner.sock')));
231
+ const answer = JSON.parse(await bridged('/git/fetch', {}, undefined, socketPath ?? ownerSocket()));
232
232
  if (answer.refused)
233
233
  throw new Error(answer.text);
234
234
  return answer.text;
package/dist/login.js CHANGED
@@ -36,6 +36,13 @@ const USAGE = `usage: kit login [--house <origin>] [--environment <id>] [--rooms
36
36
  --no-private give this Environment every Room but your Private Room
37
37
  --code <code> connect this machine with a login code
38
38
  `;
39
+ function connected(house, named, environment) {
40
+ if (named === undefined)
41
+ return `Environment ${environment} is connected to ${house}.\n`;
42
+ if (named === environment)
43
+ return `Environment ${environment} is reconnected to ${house}.\n`;
44
+ return `Environment ${named} stays with its account; this computer is connected to ${house} as new Environment ${environment} of the account that confirmed.\n`;
45
+ }
39
46
  function gone(error) {
40
47
  return error instanceof HouseRefusal && refusalOf(error.text)?.code === 'kit_environment_not_found';
41
48
  }
@@ -81,6 +88,7 @@ export async function login(argv) {
81
88
  const environment = replaces === undefined ? (values.environment ?? remembered) : undefined;
82
89
  const ownAgent = await readOwnAgent();
83
90
  let held = false;
91
+ let named = environment;
84
92
  let exchanged;
85
93
  if (values.code !== undefined) {
86
94
  exchanged = await post(house, '/kit/token', { code: values.code, label: hostname() });
@@ -88,7 +96,7 @@ export async function login(argv) {
88
96
  else {
89
97
  held = ownAgent !== null && ownAgent.house === house && (await accepted(house, ownAgent.credential));
90
98
  const verifier = randomBytes(32).toString('base64url');
91
- const start = (named) => retrying(() => post(house, '/kit', {
99
+ const start = () => retrying(() => post(house, '/kit', {
92
100
  label: hostname(),
93
101
  ...(named === undefined
94
102
  ? { act: 'new_environment', ...(replaces === undefined ? {} : { replaces }) }
@@ -99,14 +107,15 @@ export async function login(argv) {
99
107
  }));
100
108
  let started;
101
109
  try {
102
- started = await start(environment);
110
+ started = await start();
103
111
  }
104
112
  catch (error) {
105
113
  if (remembered === undefined || !gone(error))
106
114
  throw error;
107
115
  await forgetDisconnected();
108
116
  process.stdout.write(`House no longer holds Environment ${remembered}; connecting this computer as a new one.\n`);
109
- started = await start(undefined);
117
+ named = undefined;
118
+ started = await start();
110
119
  }
111
120
  process.stdout.write(`Open this link and confirm: ${started.start_url}\n`);
112
121
  open(started.start_url);
@@ -121,7 +130,7 @@ export async function login(argv) {
121
130
  environment: exchanged.environment,
122
131
  credential: exchanged.credential,
123
132
  });
124
- process.stdout.write(`Environment ${exchanged.environment} is connected to ${house}.\n`);
133
+ process.stdout.write(connected(house, named, exchanged.environment));
125
134
  if (exchanged.headless_credential !== undefined) {
126
135
  await writeOwnAgent({ house, ...relayRecord, user: exchanged.user, credential: exchanged.headless_credential });
127
136
  }
@@ -132,7 +141,7 @@ export async function login(argv) {
132
141
  else if (held && ownAgent !== null) {
133
142
  await writeOwnAgent({ house, ...relayRecord, user: ownAgent.user, credential: ownAgent.credential });
134
143
  }
135
- await placeAppGit(ownAgent, await readOwnAgent()).catch((error) => process.stderr.write(`kit: ${error.message}\n`));
144
+ await placeAppGit(await readOwnAgent()).catch((error) => process.stderr.write(`kit: ${error.message}\n`));
136
145
  try {
137
146
  startService();
138
147
  }
package/dist/logout.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { HouseRefusal, post } from './api.js';
2
- import { forgetAppGit } from './app-git.js';
3
2
  import { forgetEnrolment, forgetOwnAgent, readEnrolment } from './home.js';
4
3
  export async function logout() {
5
4
  const enrolled = await readEnrolment();
@@ -13,7 +12,6 @@ export async function logout() {
13
12
  }
14
13
  await forgetEnrolment(enrolled);
15
14
  }
16
- await forgetAppGit();
17
15
  await forgetOwnAgent();
18
16
  process.stdout.write(enrolled === null
19
17
  ? 'This computer is not connected to House.\n'
package/dist/placement.js CHANGED
@@ -2,7 +2,6 @@ import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { run } from './clis.js';
4
4
  import { kitHome } from './home.js';
5
- import { relayOrigin } from './relay.js';
6
5
  export function service() {
7
6
  if (process.platform !== 'linux' || !process.env.INVOCATION_ID)
8
7
  return null;
@@ -19,9 +18,9 @@ export async function updatesItself() {
19
18
  // sudo answers at once, so ten seconds only bounds a stuck sudo.
20
19
  return (await run('sudo', ['-n', 'true'], 10_000)).status === 0;
21
20
  }
22
- function installed() {
21
+ export function installCommand() {
23
22
  try {
24
- return readFileSync(join(kitHome(), 'placement'), 'utf8').trim();
23
+ return readFileSync(join(kitHome(), 'install-command'), 'utf8').trim();
25
24
  }
26
25
  catch (error) {
27
26
  if (error.code === 'ENOENT')
@@ -29,20 +28,3 @@ function installed() {
29
28
  throw error;
30
29
  }
31
30
  }
32
- function entries(house) {
33
- const host = new URL(house).host;
34
- const relay = relayOrigin();
35
- if (relay === undefined)
36
- return [`curl -fsSL ${host}/kit | sh`, `irm ${host}/kit.ps1 | iex`];
37
- const entry = `${relay}/${host}/kit`;
38
- return [`curl -fsSL ${entry} | HOUSE_KIT_RELAY=${relay} sh`, `$env:HOUSE_KIT_RELAY='${relay}'; irm ${entry}.ps1 | iex`];
39
- }
40
- export function installCommand(house) {
41
- const [shell, windows] = entries(house);
42
- const placement = installed();
43
- if (placement === 'native')
44
- return shell;
45
- if (placement === 'server')
46
- return `${shell} -s -- --linux`;
47
- return `${shell} -s -- --container, or ${windows} on Windows`;
48
- }
package/dist/resident.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { once } from 'node:events';
2
2
  import { watch } from 'node:fs';
3
3
  import { mkdir, rm, writeFile } from 'node:fs/promises';
4
- import { createServer } from 'node:http';
4
+ import { createServer, get } from 'node:http';
5
5
  import { dirname, extname, join } from 'node:path';
6
+ import { DatabaseSync } from 'node:sqlite';
6
7
  import { fileURLToPath } from 'node:url';
7
8
  import { Agents } from './agents.js';
8
9
  import { house } from './api.js';
@@ -10,7 +11,7 @@ import { received } from './bridge.js';
10
11
  import { CONVERSATION_INPUTS, Conversations } from './conversations.js';
11
12
  import { LocalCopies } from './copies.js';
12
13
  import { restart } from './end.js';
13
- import { agentBase, enrolled, forgetRestart, kitHome, restartAsked } from './home.js';
14
+ import { agentBase, enrolled, forgetRestart, kitHome, ownerLock, ownerSocket, restartAsked } from './home.js';
14
15
  import { killDescendants } from './scope.js';
15
16
  import { quoted } from './shell.js';
16
17
  import { SignIns } from './sign-in.js';
@@ -29,13 +30,32 @@ async function installShim(path, module) {
29
30
  await mkdir(dirname(path), { recursive: true });
30
31
  await writeFile(path, `#!/bin/sh\nexec ${quoted(process.execPath)} ${quoted(main)} "$@"\n`, { mode: 0o755 });
31
32
  }
33
+ function claimed(lock) {
34
+ try {
35
+ lock.exec('BEGIN EXCLUSIVE');
36
+ return true;
37
+ }
38
+ catch (error) {
39
+ if (error.errcode === 5)
40
+ return false;
41
+ throw error;
42
+ }
43
+ }
32
44
  async function serveOwner(copies) {
33
- const socketPath = join(kitHome(), 'owner.sock');
34
- await rm(socketPath, { force: true });
45
+ const lock = new DatabaseSync(ownerLock());
46
+ if (!claimed(lock)) {
47
+ lock.close();
48
+ const running = await runningResident();
49
+ throw new Error(`${running === null ? 'another Kit' : `Kit ${running}`} already runs this Environment from Kit home ${kitHome()}; stop it first to run another here`);
50
+ }
35
51
  const server = createServer(async (request, response) => {
36
52
  try {
37
53
  const text = await received(request);
38
54
  let answered;
55
+ if (request.url === '/resident') {
56
+ response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ pid: process.pid }));
57
+ return;
58
+ }
39
59
  if (request.url === '/git/fetch') {
40
60
  answered = await copies.fetch();
41
61
  }
@@ -53,9 +73,23 @@ async function serveOwner(copies) {
53
73
  response.writeHead(502).end(error instanceof Error ? error.message : String(error));
54
74
  }
55
75
  });
56
- server.listen(socketPath);
76
+ server.on('close', () => lock.close());
77
+ await rm(ownerSocket(), { force: true });
78
+ server.listen(ownerSocket());
57
79
  await once(server, 'listening');
58
80
  }
81
+ function runningResident() {
82
+ return new Promise((resolve, reject) => {
83
+ get({ socketPath: ownerSocket(), path: '/resident' }, (answer) => {
84
+ received(answer).then((text) => resolve(JSON.parse(text).pid), reject);
85
+ }).on('error', (error) => {
86
+ if (error.code === 'ENOENT' || error.code === 'ECONNREFUSED')
87
+ resolve(null);
88
+ else
89
+ reject(error);
90
+ });
91
+ });
92
+ }
59
93
  function connected() {
60
94
  return new Promise((resolve) => {
61
95
  const watcher = watch(kitHome(), () => {
@@ -72,7 +106,7 @@ function connected() {
72
106
  resolve();
73
107
  });
74
108
  }
75
- export async function resident() {
109
+ async function started() {
76
110
  await connected();
77
111
  await forgetRestart();
78
112
  watch(kitHome(), () => {
@@ -83,13 +117,20 @@ export async function resident() {
83
117
  });
84
118
  await placeSkillSet();
85
119
  await mkdir(agentBase(), { recursive: true });
86
- process.env.HOUSE_KIT_RESIDENT = String(process.pid);
87
- const agents = new Agents(house, () => changed());
88
- const copies = new LocalCopies(house);
89
- await copies.load();
90
120
  await installShim(join(kitHome(), 'shim', 'git'), 'git-main');
91
121
  await installShim(codexLauncher(), 'codex-main');
122
+ }
123
+ export async function resident() {
124
+ await mkdir(kitHome(), { recursive: true, mode: 0o700 });
125
+ const copies = new LocalCopies(house);
126
+ await copies.load();
92
127
  await serveOwner(copies);
128
+ await started().catch((error) => {
129
+ report(error);
130
+ process.exit(1);
131
+ });
132
+ process.env.HOUSE_KIT_RESIDENT = String(process.pid);
133
+ const agents = new Agents(house, () => changed());
93
134
  let stream = null;
94
135
  let reported = false;
95
136
  const idle = () => conversations.idle() && signIns.idle() && agents.idle();
package/dist/stream.js CHANGED
@@ -81,7 +81,7 @@ export function holdStream(handlers) {
81
81
  return;
82
82
  }
83
83
  if (refused !== null && tooOld(refused.text))
84
- updateRequired(house);
84
+ updateRequired();
85
85
  if (finalRefusal(refused))
86
86
  end(refused.message);
87
87
  // A closed socket reopens in one to three seconds, spread so Kits one restart closed return apart; an opening House refused as retryable or left unanswered backs off on the jittered curve.
package/dist/work.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { once } from 'node:events';
3
3
  import { readFileSync } from 'node:fs';
4
- import { mkdir, rm } from 'node:fs/promises';
4
+ import { rm } from 'node:fs/promises';
5
5
  import { createServer } from 'node:net';
6
6
  import { join } from 'node:path';
7
7
  import { RUNNING_JOB } from './events.js';
8
- import { kitHome } from './home.js';
8
+ import { kitHome, socketDirectory } from './home.js';
9
9
  const ENDED_TASK = new Set(['completed', 'failed', 'killed']);
10
10
  const SCHEDULING = new Set(['CronCreate', 'CronDelete', 'ScheduleWakeup']);
11
11
  const QUEUE_LIST = 'house-kit-queue-';
@@ -194,9 +194,7 @@ class CodexWork extends Work {
194
194
  proving = -1;
195
195
  proven = -1;
196
196
  async open(cli) {
197
- const sockets = join(kitHome(), 'bridges');
198
- await mkdir(sockets, { recursive: true, mode: 0o700 });
199
- this.path = join(sockets, `${randomUUID()}.sock`);
197
+ this.path = join(socketDirectory(), `${randomUUID()}.sock`);
200
198
  this.server = createServer((socket) => this.connected(socket));
201
199
  this.server.listen(this.path);
202
200
  await once(this.server, 'listening');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.2.1-c5d51f1e4ce19",
3
+ "version": "0.2.1-c64c31420c9bf",
4
4
  "description": "House Kit connects an Environment to agents.house.",
5
5
  "license": "MIT",
6
6
  "repository": {