@agentshouse/kit 0.2.1-c441d29eb63f4 → 0.2.1-c44c14117631a

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/acp.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { ndJsonStream, PROTOCOL_VERSION, } from '@agentclientprotocol/sdk';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { Readable, Writable } from 'node:stream';
4
- import { CLIS, KIT_RELEASE, adapterCommand, withoutProxy } from './clis.js';
4
+ import { CLIS, KIT_RELEASE, adapterCommand } from './clis.js';
5
+ import { childEnvironment } from './relay.js';
5
6
  import { killTree } from './scope.js';
6
7
  const RETIRED = 'the CLI was retired';
7
8
  export async function startAdapter(kind, cli, cwd, app, events = () => undefined, added = {}, signal, scope = null) {
@@ -10,7 +11,7 @@ export async function startAdapter(kind, cli, cwd, app, events = () => undefined
10
11
  const [command, args] = scope?.command(...started) ?? started;
11
12
  const child = spawn(command, args, {
12
13
  cwd,
13
- env: { ...withoutProxy(), ...(adapter === null ? {} : { ...adapter.env, [adapter.executable]: cli }), ...added },
14
+ env: { ...childEnvironment(), ...(adapter === null ? {} : { ...adapter.env, [adapter.executable]: cli }), ...added },
14
15
  stdio: ['pipe', 'pipe', 'pipe'],
15
16
  detached: true,
16
17
  });
@@ -1,10 +1,10 @@
1
- import { setGlobalProxyFromEnv } from 'node:http';
2
1
  import { HouseRefusal, post } from './api.js';
3
2
  import { readEnrolment } from './home.js';
3
+ import { proxyUnlessRelayed } from './relay.js';
4
4
  function unanswered(error) {
5
5
  return error instanceof HouseRefusal ? error.status >= 500 : error instanceof TypeError && error.message === 'fetch failed';
6
6
  }
7
- setGlobalProxyFromEnv();
7
+ proxyUnlessRelayed();
8
8
  try {
9
9
  const { house, credential } = (await readEnrolment());
10
10
  try {
package/dist/bridge.js CHANGED
@@ -13,7 +13,7 @@ import { callHouse, mcpBody, rpc, UNREACHABLE } from './mcp.js';
13
13
  import { operationId } from './operation.js';
14
14
  import { refusalOf, retryable } from './refusals.js';
15
15
  import { relayed } from './relay.js';
16
- const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
16
+ const MCP_REQUEST_BYTES = 4_000_000;
17
17
  export async function received(request) {
18
18
  let text = '';
19
19
  request.setEncoding('utf8');
@@ -0,0 +1,46 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createWriteStream } from 'node:fs';
3
+ import { rm } from 'node:fs/promises';
4
+ import { Transform } from 'node:stream';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import { readEnrolment } from './home.js';
7
+ import { relayed } from './relay.js';
8
+ const MANIFEST = 'bootstrap.json';
9
+ async function served(name, signal) {
10
+ const enrolment = await readEnrolment();
11
+ if (enrolment === null)
12
+ throw new Error('this Kit is connected to no House');
13
+ const answer = await fetch(relayed(new URL(`/kit-installer/${encodeURIComponent(name)}`, enrolment.house)), { signal });
14
+ if (answer.status === 200 && answer.body !== null)
15
+ return answer;
16
+ await answer.body?.cancel();
17
+ throw new Error(`House answered ${answer.status} for ${name}`);
18
+ }
19
+ export async function servedDigest(name, signal) {
20
+ const manifest = (await (await served(MANIFEST, signal)).json());
21
+ const sha256 = manifest.files?.[name]?.sha256;
22
+ return typeof sha256 === 'string' ? sha256 : null;
23
+ }
24
+ export async function downloadServed(name, sha256, destination, signal, most = Infinity) {
25
+ const answer = await served(name, signal);
26
+ const digest = createHash('sha256');
27
+ let bytes = 0;
28
+ const hashed = new Transform({
29
+ transform(chunk, _encoding, done) {
30
+ bytes += chunk.length;
31
+ if (bytes > most)
32
+ return done(new Error(`${name} is larger than its pinned ${most} bytes`));
33
+ digest.update(chunk);
34
+ done(null, chunk);
35
+ },
36
+ });
37
+ try {
38
+ await pipeline(answer.body, hashed, createWriteStream(destination), { signal });
39
+ if (digest.digest('hex') !== sha256)
40
+ throw new Error(`${name} does not match its sha256`);
41
+ }
42
+ catch (error) {
43
+ await rm(destination, { force: true });
44
+ throw error;
45
+ }
46
+ }
package/dist/clis-main.js CHANGED
@@ -1,8 +1,8 @@
1
- import { setGlobalProxyFromEnv } from 'node:http';
2
1
  import { post } from './api.js';
3
2
  import { CLIS, below, locate, versionOf } from './clis.js';
4
3
  import { readEnrolment } from './home.js';
5
- setGlobalProxyFromEnv();
4
+ import { proxyUnlessRelayed } from './relay.js';
5
+ proxyUnlessRelayed();
6
6
  try {
7
7
  const { house, credential } = (await readEnrolment());
8
8
  const { agents } = await post(house, '/kit/agents/desired', {}, credential);
package/dist/clis.js CHANGED
@@ -1,10 +1,12 @@
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, writeFile } from 'node:fs/promises';
4
+ import { access, mkdir, readFile, rename, rm, 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';
7
8
  import { kitHome } from './home.js';
9
+ import { childEnvironment } from './relay.js';
8
10
  const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
9
11
  function houseRule(houses) {
10
12
  return `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`;
@@ -69,7 +71,7 @@ function bridgeInClaudeSandbox(bridge) {
69
71
  export const CLIS = {
70
72
  'codex-acp': {
71
73
  bin: 'codex',
72
- minimum: '0.159.1',
74
+ minimum: '0.160.1',
73
75
  install: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh',
74
76
  adapter: {
75
77
  package: '@agentclientprotocol/codex-acp',
@@ -92,7 +94,7 @@ export const CLIS = {
92
94
  },
93
95
  'claude-agent-acp': {
94
96
  bin: 'claude',
95
- minimum: '2.1.286',
97
+ minimum: '2.1.303',
96
98
  install: 'curl -fsSL https://claude.ai/install.sh | bash',
97
99
  adapter: {
98
100
  package: '@agentclientprotocol/claude-agent-acp',
@@ -138,7 +140,6 @@ export const CLIS = {
138
140
  };
139
141
  const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
140
142
  export const KIT_RELEASE = KIT_PACKAGE.version;
141
- const PROXY = /^https?_proxy$/i;
142
143
  const VERSION = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/;
143
144
  // An install still running after fifteen minutes is ended and reported as failed, so a hung download cannot hold its CLI's report.
144
145
  const INSTALL_MS = 15 * 60_000;
@@ -160,15 +161,12 @@ async function installedAdapter(kind) {
160
161
  return null;
161
162
  }
162
163
  }
163
- export function withoutProxy() {
164
- return Object.fromEntries(Object.entries(process.env).filter(([name]) => !PROXY.test(name)));
165
- }
166
164
  export function run(command, args, timeoutMs, detached = false, signal) {
167
165
  return new Promise((resolve) => {
168
166
  const child = spawn(command, args, {
169
167
  stdio: ['ignore', 'pipe', 'pipe'],
170
168
  timeout: timeoutMs,
171
- env: withoutProxy(),
169
+ env: childEnvironment(),
172
170
  detached,
173
171
  signal: detached ? undefined : signal,
174
172
  });
@@ -202,15 +200,37 @@ export function run(command, args, timeoutMs, detached = false, signal) {
202
200
  export function failureOf(ran, program) {
203
201
  return ran.output.trim().slice(-CAUSE_CHARACTERS) || `${program} exited with ${ran.status ?? 'a signal'}`;
204
202
  }
203
+ export function adapterArchive(kind) {
204
+ return `${kind}-${KIT_PACKAGE.peerDependencies[CLIS[kind].adapter.package]}.tar.gz`;
205
+ }
205
206
  export async function installAdapter(kind, signal) {
206
207
  const adapter = CLIS[kind].adapter;
207
208
  if (adapter === null)
208
209
  return null;
209
- const release = KIT_PACKAGE.peerDependencies[adapter.package];
210
- if ((await installedAdapter(kind)) === release)
210
+ if ((await installedAdapter(kind)) === KIT_PACKAGE.peerDependencies[adapter.package])
211
211
  return null;
212
- const ran = await run('npm', ['install', '--prefix', prefix(kind), '--cache', join(kitHome(), 'npm-cache'), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
213
- return ran.status === 0 ? null : failureOf(ran, 'npm');
212
+ const name = adapterArchive(kind);
213
+ const archive = `${prefix(kind)}.tar.gz`;
214
+ const deadline = AbortSignal.timeout(INSTALL_MS);
215
+ const bounded = signal === undefined ? deadline : AbortSignal.any([signal, deadline]);
216
+ try {
217
+ const sha256 = await servedDigest(name, bounded);
218
+ if (sha256 === null)
219
+ return `House serves no ${name}`;
220
+ await rm(prefix(kind), { recursive: true, force: true });
221
+ await mkdir(prefix(kind), { recursive: true });
222
+ await downloadServed(name, sha256, archive, bounded);
223
+ const ran = await run('tar', ['-xzf', archive, '-C', prefix(kind)], INSTALL_MS, true, signal);
224
+ return ran.status === 0 ? null : failureOf(ran, 'tar');
225
+ }
226
+ catch (error) {
227
+ if (deadline.aborted && signal?.aborted !== true)
228
+ return `${name} did not arrive within ${INSTALL_MS / 1000} seconds`;
229
+ return error instanceof Error ? error.message : String(error);
230
+ }
231
+ finally {
232
+ await rm(archive, { force: true });
233
+ }
214
234
  }
215
235
  export async function installCli(kind, signal) {
216
236
  const ran = await run('bash', ['-o', 'pipefail', '-c', CLIS[kind].install], INSTALL_MS, true, signal);
@@ -78,8 +78,8 @@ function exitedWith(running, cause) {
78
78
  function answerMessage(request, response) {
79
79
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
80
80
  }
81
- async function sentPart(url, bytes) {
82
- const answer = await fetch(relayed(url), { method: 'PUT', body: bytes });
81
+ async function sentPart(url, bytes, headers) {
82
+ const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
83
83
  await answer.arrayBuffer();
84
84
  if (!answer.ok)
85
85
  throw storeRefusal('upload', answer.status);
@@ -914,17 +914,21 @@ export class Conversations {
914
914
  async image(conversation, turn, reserved, data, mediaType) {
915
915
  const bytes = Buffer.from(data, 'base64');
916
916
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
917
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
917
918
  try {
918
919
  const declared = (await this.deliver(conversation, turn, 'images', {
919
920
  name,
920
921
  media_type: mediaType,
921
922
  bytes: bytes.length,
922
- sha256: createHash('sha256').update(bytes).digest('hex'),
923
+ sha256,
923
924
  }));
924
925
  const saved = await uploaded(declared.upload, {
926
+ name,
925
927
  bytes: bytes.length,
926
- send: (url, start, end) => sentPart(url, bytes.subarray(start, end)),
927
- finish: (upload) => this.kit.house.deliver('/kit/uploads/finish', { upload }),
928
+ sha256,
929
+ read: (start, end) => [bytes.subarray(start, end)],
930
+ send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
931
+ finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
928
932
  });
929
933
  turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
930
934
  }
package/dist/files.js CHANGED
@@ -9,6 +9,7 @@ import { retryDelay } from './api.js';
9
9
  import { Unsettled } from './mcp.js';
10
10
  import { relayed } from './relay.js';
11
11
  const STALLED_ROUNDS = 8;
12
+ const PART_CHECKSUM = 'x-amz-checksum-sha256';
12
13
  const LINK = /\]\((\/rooms\/[^/)\s]+\/[^)\s]+)\)/gu;
13
14
  export function linkedPaths(text) {
14
15
  return [...new Set(Array.from(text.matchAll(LINK), (link) => link[1]))];
@@ -32,17 +33,39 @@ function paused(milliseconds) {
32
33
  export function storeRefusal(act, status) {
33
34
  return new Error(`the ${act} answered ${status}; ${act} the file again`);
34
35
  }
36
+ async function partDigests(uploading, partBytes) {
37
+ const whole = createHash('sha256');
38
+ const digests = [];
39
+ for (let start = 0; digests.length === 0 || start < uploading.bytes; start += partBytes) {
40
+ const end = Math.min(start + partBytes, uploading.bytes);
41
+ const digest = createHash('sha256');
42
+ for await (const chunk of uploading.read(start, end)) {
43
+ digest.update(chunk);
44
+ whole.update(chunk);
45
+ }
46
+ digests.push(digest.digest());
47
+ }
48
+ if (whole.digest('hex') !== uploading.sha256)
49
+ throw new Error(`${uploading.name} changed while it was uploaded; upload it again`);
50
+ return digests;
51
+ }
35
52
  export async function uploaded(declared, uploading) {
36
- let upload = declared;
53
+ const partBytes = declared.part_bytes;
54
+ const digests = await partDigests(uploading, partBytes);
55
+ const partSha256 = digests.map((digest) => digest.toString('hex'));
56
+ let answer = await finishing(uploading, declared.id, partSha256);
37
57
  let idle = 0;
38
58
  let stalled = 0;
39
- for (;;) {
59
+ while ('upload' in answer) {
60
+ const { id, parts } = answer.upload;
40
61
  let sent = 0;
41
62
  let failure = null;
42
- for (const { part, url } of upload.parts) {
43
- const start = (part - 1) * upload.part_bytes;
63
+ for (const { part, url } of parts ?? []) {
64
+ const start = (part - 1) * partBytes;
44
65
  try {
45
- await uploading.send(url, start, Math.min(start + upload.part_bytes, uploading.bytes));
66
+ await uploading.send(url, start, Math.min(start + partBytes, uploading.bytes), {
67
+ [PART_CHECKSUM]: digests[part - 1].toString('base64'),
68
+ });
46
69
  sent++;
47
70
  }
48
71
  catch (error) {
@@ -57,24 +80,17 @@ export async function uploaded(declared, uploading) {
57
80
  else {
58
81
  if (failure !== null && ++stalled >= STALLED_ROUNDS)
59
82
  throw failure;
60
- // A round that sent no part, its store unreachable or not yet started, is followed by the next finishing call on the jittered curve.
83
+ // A round that sent no part, its store unreachable, is followed by the next finishing call on the jittered curve.
61
84
  await paused(retryDelay(idle++));
62
85
  }
63
- let answer = await finishing(uploading, upload.id);
64
- while ('checking' in answer) {
65
- // A finishing call that finds House still checking the stored bytes is sent again after five seconds, the cadence at which House takes up a check it handed over.
66
- await paused(5_000);
67
- answer = await finishing(uploading, upload.id);
68
- }
69
- if (!('upload' in answer))
70
- return answer;
71
- upload = answer.upload;
86
+ answer = await finishing(uploading, id, partSha256);
72
87
  }
88
+ return answer;
73
89
  }
74
- async function finishing(uploading, upload) {
90
+ async function finishing(uploading, upload, partSha256) {
75
91
  for (let unsettled = 1;; unsettled++) {
76
92
  try {
77
- return await uploading.finish(upload);
93
+ return await uploading.finish(upload, partSha256);
78
94
  }
79
95
  catch (error) {
80
96
  if (!(error instanceof Unsettled) || unsettled === STALLED_ROUNDS)
package/dist/git.js CHANGED
@@ -2,8 +2,8 @@ import { spawn } from 'node:child_process';
2
2
  import { isUtf8 } from 'node:buffer';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { join } from 'node:path';
5
- import { withoutProxy } from './clis.js';
6
5
  import { workspaceRoot } from './home.js';
6
+ import { childEnvironment } from './relay.js';
7
7
  export function localCopies() {
8
8
  return join(workspaceRoot(), 'local-copies');
9
9
  }
@@ -11,7 +11,7 @@ export const HOUSE_REF = 'refs/house/received';
11
11
  const COMMITTER = 'Kit <house-kit@localhost>';
12
12
  export function run(cwd, args, input, env = {}) {
13
13
  return new Promise((resolve, reject) => {
14
- const child = spawn('git', args, { cwd, env: { ...withoutProxy(), ...env } });
14
+ const child = spawn('git', args, { cwd, env: { ...childEnvironment(), ...env } });
15
15
  const stdout = [];
16
16
  let stderr = '';
17
17
  child.stdout.on('data', (chunk) => stdout.push(chunk));
package/dist/house-cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { createReadStream } from 'node:fs';
3
- import { request, setGlobalProxyFromEnv } from 'node:http';
3
+ import { request } from 'node:http';
4
4
  import { request as httpsRequest } from 'node:https';
5
5
  import { extname, join, resolve } from 'node:path';
6
6
  import { Readable } from 'node:stream';
@@ -10,7 +10,7 @@ import { HOUSE_REF } from './git.js';
10
10
  import { ownerSocket, readOwnAgent } from './home.js';
11
11
  import { callHouse, rpc, UNREACHABLE, Unsettled } from './mcp.js';
12
12
  import { refusalOf, retryable } from './refusals.js';
13
- import { relayed, selectRelay } from './relay.js';
13
+ import { proxyUnlessRelayed, relayed, selectRelay } from './relay.js';
14
14
  import { parse } from 'yaml';
15
15
  const USAGE = "usage: house <tool> ['<arguments as JSON>']";
16
16
  const PUSH_USAGE = 'usage: house push [commit] [--owner]';
@@ -89,7 +89,6 @@ async function direct(message) {
89
89
  if (ownAgent === null)
90
90
  throw new Error(UNCONNECTED);
91
91
  selectRelay(process.env.HOUSE_KIT_RELAY || ownAgent.relay);
92
- setGlobalProxyFromEnv();
93
92
  const answer = await callHouse(ownAgent.house, ownAgent.credential, message).catch(() => {
94
93
  throw new Unsettled(UNREACHABLE);
95
94
  });
@@ -120,7 +119,7 @@ function opened(url, method, headers, body) {
120
119
  const local = socketPath !== undefined;
121
120
  const target = local ? null : relayed(url);
122
121
  if (!local)
123
- setGlobalProxyFromEnv();
122
+ proxyUnlessRelayed();
124
123
  const sent = target === null
125
124
  ? request({ socketPath, path: url, method, headers }, answered)
126
125
  : (target.protocol === 'https:' ? httpsRequest : request)(target, { method, headers }, answered);
@@ -131,8 +130,19 @@ function opened(url, method, headers, body) {
131
130
  pipeline(body, sent).catch(() => undefined);
132
131
  });
133
132
  }
134
- async function sentPart(url, file, start, end) {
135
- const answer = await opened(url, 'PUT', { 'content-length': String(end - start) }, end === start ? undefined : createReadStream(file, { start, end: end - 1 }));
133
+ function ranged(file, start, end) {
134
+ return end === start ? undefined : createReadStream(file, { start, end: end - 1 });
135
+ }
136
+ async function* read(file, start, end) {
137
+ try {
138
+ yield* ranged(file, start, end) ?? [];
139
+ }
140
+ catch (error) {
141
+ throw unreadable(file, error);
142
+ }
143
+ }
144
+ async function sentPart(url, file, start, end, headers) {
145
+ const answer = await opened(url, 'PUT', { ...headers, 'content-length': String(end - start) }, ranged(file, start, end));
136
146
  await answer.toArray();
137
147
  if (answer.statusCode !== 200)
138
148
  throw storeRefusal('upload', answer.statusCode);
@@ -171,10 +181,10 @@ async function described(file) {
171
181
  const mediaType = MEDIA_TYPES.get(extname(file).slice(1).toLowerCase());
172
182
  return { bytes, sha256: digest.digest('hex'), ...(mediaType === undefined ? {} : { media_type: mediaType }) };
173
183
  }
174
- async function finished(upload, room) {
175
- const text = await called('upload', { upload, room_ref: room });
184
+ async function finished(upload, room, partSha256) {
185
+ const text = await called('upload', { upload, room_ref: room, part_sha256: partSha256 });
176
186
  const answer = parse(text);
177
- return answer !== null && ('upload' in answer || 'checking' in answer) ? answer : { text };
187
+ return answer !== null && 'upload' in answer ? answer : { text };
178
188
  }
179
189
  async function upload({ file, ...destination }) {
180
190
  if (typeof file !== 'string' || file === '')
@@ -185,9 +195,12 @@ async function upload({ file, ...destination }) {
185
195
  if (declared?.upload === undefined)
186
196
  return answer;
187
197
  const accepted = await uploaded(declared.upload, {
198
+ name: file,
188
199
  bytes: description.bytes,
189
- send: (url, start, end) => sentPart(url, file, start, end),
190
- finish: (id) => finished(id, destination.room_ref),
200
+ sha256: description.sha256,
201
+ read: (start, end) => read(file, start, end),
202
+ send: (url, start, end, headers) => sentPart(url, file, start, end, headers),
203
+ finish: (id, partSha256) => finished(id, destination.room_ref, partSha256),
191
204
  });
192
205
  return accepted.text;
193
206
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
3
- if (major < 24 || (major === 24 && minor < 14)) {
4
- process.stderr.write(`house: the Kit needs Node 24.14.0 or newer; this is ${process.versions.node}\n`);
2
+ const [major = 0, minor = 0, patch = 0] = process.versions.node.split('.').map(Number);
3
+ if (major < 24 || (major === 24 && (minor < 22 || (minor === 22 && patch < 1)))) {
4
+ process.stderr.write(`house: the Kit needs Node 24.22.1 or newer; this is ${process.versions.node}\n`);
5
5
  process.exitCode = 1;
6
6
  }
7
7
  else {
package/dist/kit-cli.js CHANGED
@@ -1,9 +1,9 @@
1
- import { setGlobalProxyFromEnv } from 'node:http';
2
1
  import { parseArgs } from 'node:util';
3
2
  import { answerGitCredential } from './app-git.js';
4
3
  import { chatgpt } from './chatgpt.js';
5
4
  import { login } from './login.js';
6
5
  import { logout } from './logout.js';
6
+ import { proxyUnlessRelayed } from './relay.js';
7
7
  import { resident } from './resident.js';
8
8
  const USAGE = 'usage: kit <login|logout|resident|chatgpt>\n';
9
9
  const COMMANDS = new Map([
@@ -36,7 +36,7 @@ const COMMANDS = new Map([
36
36
  ],
37
37
  ]);
38
38
  const [command, ...argv] = process.argv.slice(2);
39
- setGlobalProxyFromEnv();
39
+ proxyUnlessRelayed();
40
40
  async function run() {
41
41
  if (command === 'login')
42
42
  return login(argv);
package/dist/kit-main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
3
- if (major < 24 || (major === 24 && minor < 14)) {
4
- process.stderr.write(`kit: the Kit needs Node 24.14.0 or newer; this is ${process.versions.node}\n`);
2
+ const [major = 0, minor = 0, patch = 0] = process.versions.node.split('.').map(Number);
3
+ if (major < 24 || (major === 24 && (minor < 22 || (minor === 22 && patch < 1)))) {
4
+ process.stderr.write(`kit: the Kit needs Node 24.22.1 or newer; this is ${process.versions.node}\n`);
5
5
  process.exitCode = 1;
6
6
  }
7
7
  else {
package/dist/relay.js CHANGED
@@ -1,6 +1,11 @@
1
+ import { setGlobalProxyFromEnv } from 'node:http';
2
+ const PROXY = /^https?_proxy$/i;
3
+ const LOOPBACK = ['127.0.0.1', 'localhost', '::1'];
1
4
  let selected;
5
+ let restoreProxy;
2
6
  export function selectRelay(relay) {
3
7
  selected = relay === undefined || relay === '' ? null : relay;
8
+ proxyUnlessRelayed();
4
9
  }
5
10
  export function relayOrigin() {
6
11
  if (selected !== undefined)
@@ -8,6 +13,22 @@ export function relayOrigin() {
8
13
  const ambient = process.env.HOUSE_KIT_RELAY;
9
14
  return ambient === undefined || ambient === '' ? undefined : ambient;
10
15
  }
16
+ function unlessRelayed() {
17
+ if (relayOrigin() === undefined)
18
+ return { ...process.env };
19
+ return Object.fromEntries(Object.entries(process.env).filter(([name]) => !PROXY.test(name)));
20
+ }
21
+ export function proxyUnlessRelayed() {
22
+ restoreProxy?.();
23
+ restoreProxy = setGlobalProxyFromEnv(unlessRelayed());
24
+ }
25
+ export function childEnvironment() {
26
+ const environment = unlessRelayed();
27
+ if (!Object.keys(environment).some((name) => PROXY.test(name)))
28
+ return environment;
29
+ const bypassed = [environment.NO_PROXY ?? environment.no_proxy, ...LOOPBACK].filter((entry) => entry !== undefined && entry !== '').join(',');
30
+ return { ...environment, NO_PROXY: bypassed, no_proxy: bypassed };
31
+ }
11
32
  export function relayed(target) {
12
33
  const url = new URL(target);
13
34
  const relay = relayOrigin();
package/dist/sign-in.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
- import { CLIS, withoutProxy } from './clis.js';
3
+ import { CLIS } from './clis.js';
4
+ import { childEnvironment } from './relay.js';
4
5
  import { killTree } from './scope.js';
5
6
  import { holdSecretInput } from './secret-input.js';
6
7
  const ESCAPE = /\u001b\[[\d;]*m/g;
@@ -56,7 +57,7 @@ export class SignIns {
56
57
  const child = spawn(cli, login.args, {
57
58
  stdio: ['pipe', 'pipe', 'pipe'],
58
59
  detached: true,
59
- env: withoutProxy(),
60
+ env: childEnvironment(),
60
61
  });
61
62
  const running = () => child.exitCode === null && child.signalCode === null;
62
63
  const stop = () => killTree(child);
@@ -1,31 +1,31 @@
1
- import { createHash } from 'node:crypto';
2
- import { createReadStream } from 'node:fs';
3
1
  import { chmod, mkdir, mkdtemp, readdir, readFile, rename, rm, stat } from 'node:fs/promises';
4
2
  import { availableParallelism, tmpdir } from 'node:os';
5
3
  import { basename, join } from 'node:path';
6
- import { pipeline } from 'node:stream/promises';
7
4
  import { unlessAborted } from './abort.js';
5
+ import { downloadServed } from './building.js';
8
6
  import { failureOf, run } from './clis.js';
9
7
  import { downloaded, fetched } from './files.js';
10
8
  import { kitHome } from './home.js';
11
9
  const RELEASE = 'https://github.com/agentshouse/kit/releases/download/engine-2';
12
10
  export const MODEL = {
13
- url: 'https://huggingface.co/ggml-org/parakeet-GGUF/resolve/35156454d1a39de06863303dd209fd2bed6ee079/ggml-parakeet-tdt-0.6b-v3-q4_k.bin',
11
+ name: 'ggml-parakeet-tdt-0.6b-v3-q4_k.bin',
12
+ from: 'https://huggingface.co/ggml-org/parakeet-GGUF/resolve/35156454d1a39de06863303dd209fd2bed6ee079/ggml-parakeet-tdt-0.6b-v3-q4_k.bin',
14
13
  sha256: '8b205b8b39c6535e153de6fb11c51db46125d45c4f16ba496fe41a0fe71b885e',
14
+ bytes: 415_611_879,
15
15
  executable: false,
16
16
  };
17
17
  export const ENGINES = {
18
18
  'linux-x64': {
19
- decoder: { url: `${RELEASE}/ffmpeg-linux-x86_64`, sha256: '1f8235d206428305afb89c6e8ab99d100425a0ba61e22bb4c7aff017db8c8de5', executable: true },
20
- engine: { url: `${RELEASE}/parakeet-cli-linux-x86_64`, sha256: 'bb2585695992fe1927a33960002cdf51b8a90a96ad65593c3d0a9f1fbc0865c2', executable: true },
19
+ decoder: { name: 'ffmpeg-linux-x86_64', from: `${RELEASE}/ffmpeg-linux-x86_64`, sha256: '1f8235d206428305afb89c6e8ab99d100425a0ba61e22bb4c7aff017db8c8de5', bytes: 5_175_832, executable: true },
20
+ engine: { name: 'parakeet-cli-linux-x86_64', from: `${RELEASE}/parakeet-cli-linux-x86_64`, sha256: 'bb2585695992fe1927a33960002cdf51b8a90a96ad65593c3d0a9f1fbc0865c2', bytes: 3_801_616, executable: true },
21
21
  },
22
22
  'linux-arm64': {
23
- decoder: { url: `${RELEASE}/ffmpeg-linux-aarch64`, sha256: 'a7e08a0fae0179d1078312f6bb13d2a9aa4d4c71a1d82a022953f25dee452f5e', executable: true },
24
- engine: { url: `${RELEASE}/parakeet-cli-linux-aarch64`, sha256: 'cbc0bedc8daae97f1ef9dcee4200c06226218e300e0fd62ead1306d22742b7ed', executable: true },
23
+ decoder: { name: 'ffmpeg-linux-aarch64', from: `${RELEASE}/ffmpeg-linux-aarch64`, sha256: 'a7e08a0fae0179d1078312f6bb13d2a9aa4d4c71a1d82a022953f25dee452f5e', bytes: 4_590_208, executable: true },
24
+ engine: { name: 'parakeet-cli-linux-aarch64', from: `${RELEASE}/parakeet-cli-linux-aarch64`, sha256: 'cbc0bedc8daae97f1ef9dcee4200c06226218e300e0fd62ead1306d22742b7ed', bytes: 3_477_624, executable: true },
25
25
  },
26
26
  'darwin-arm64': {
27
- decoder: { url: `${RELEASE}/ffmpeg-darwin-arm64`, sha256: '7abe154aaed3d6b49f5167ca7221bb66bed51388925d99a7e4261a41fe52fba6', executable: true },
28
- engine: { url: `${RELEASE}/parakeet-cli-darwin-arm64`, sha256: '9a2aafc0922ad7c0b6db6eff1641e96572ba32ac40bf995a1df242ed2febf297', executable: true },
27
+ decoder: { name: 'ffmpeg-darwin-arm64', from: `${RELEASE}/ffmpeg-darwin-arm64`, sha256: '7abe154aaed3d6b49f5167ca7221bb66bed51388925d99a7e4261a41fe52fba6', bytes: 3_696_768, executable: true },
28
+ engine: { name: 'parakeet-cli-darwin-arm64', from: `${RELEASE}/parakeet-cli-darwin-arm64`, sha256: '9a2aafc0922ad7c0b6db6eff1641e96572ba32ac40bf995a1df242ed2febf297', bytes: 1_939_728, executable: true },
29
29
  },
30
30
  };
31
31
  const CHUNK_SECONDS = 20;
@@ -46,7 +46,7 @@ function installed(artifact) {
46
46
  return join(directory(), artifact.sha256);
47
47
  }
48
48
  async function executed(command, args, signal) {
49
- // A download or a transcription runs as long as its size needs; the setting turning off or the turn's interrupt or end aborts it instead.
49
+ // A decode or a transcription runs as long as its audio needs; the setting turning off or the turn's interrupt or end aborts it instead.
50
50
  const ran = await run(command, args, 0, false, signal);
51
51
  if (ran.status !== 0)
52
52
  throw new Error(failureOf(ran, basename(command)));
@@ -60,11 +60,6 @@ async function threads() {
60
60
  const allowed = quota === 'max' ? Infinity : Math.ceil(Number(quota) / Number(period));
61
61
  return Math.max(1, Math.floor(Math.min(availableParallelism(), allowed) / 2));
62
62
  }
63
- async function sha256(path) {
64
- const hash = createHash('sha256');
65
- await pipeline(createReadStream(path), hash);
66
- return hash.digest('hex');
67
- }
68
63
  function causeOf(error) {
69
64
  return error instanceof Error ? error.message : String(error);
70
65
  }
@@ -85,6 +80,25 @@ function ready() {
85
80
  function pinnedHere() {
86
81
  return ENGINES[`${process.platform}-${process.arch}`];
87
82
  }
83
+ const DOWNLOADS = 3;
84
+ async function downloadedAgain(artifact, signal) {
85
+ // An artifact arrives at a hundred kilobytes a second or its download ends, so a stalled Building cannot hold the install open.
86
+ const deadlineMs = Math.ceil(artifact.bytes / 100);
87
+ const deadline = AbortSignal.timeout(deadlineMs);
88
+ for (let attempt = 1;; attempt++) {
89
+ try {
90
+ return await downloadServed(artifact.name, artifact.sha256, `${installed(artifact)}.part`, AbortSignal.any([signal, deadline]), artifact.bytes);
91
+ }
92
+ catch (error) {
93
+ if (signal.aborted)
94
+ throw error;
95
+ if (deadline.aborted)
96
+ throw new Error(`${artifact.name} did not arrive within ${Math.ceil(deadlineMs / 1000)} seconds`);
97
+ if (attempt === DOWNLOADS)
98
+ throw error;
99
+ }
100
+ }
101
+ }
88
102
  async function install(artifacts, signal) {
89
103
  await mkdir(directory(), { recursive: true });
90
104
  const pinned = new Set(artifacts.map((artifact) => artifact.sha256));
@@ -97,13 +111,8 @@ async function install(artifacts, signal) {
97
111
  if (!(await present(installed(artifact))))
98
112
  missing.push(artifact);
99
113
  try {
100
- for (const artifact of missing) {
101
- const part = `${installed(artifact)}.part`;
102
- // Three retries carry a large download past a dropped connection before the download fails.
103
- await executed('curl', ['-fsSL', '--retry', '3', '-o', part, artifact.url], signal);
104
- if ((await sha256(part)) !== artifact.sha256)
105
- throw new Error(`${artifact.url} does not match its sha256`);
106
- }
114
+ for (const artifact of missing)
115
+ await downloadedAgain(artifact, signal);
107
116
  for (const artifact of missing) {
108
117
  if (artifact.executable)
109
118
  await chmod(`${installed(artifact)}.part`, 0o755);
package/dist/update.js CHANGED
@@ -6,6 +6,10 @@ import { join } from 'node:path';
6
6
  import { run } from './clis.js';
7
7
  import { kitHome, workspaceRoot } from './home.js';
8
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;
9
13
  function directory() {
10
14
  return join(kitHome(), 'update');
11
15
  }
@@ -66,6 +70,17 @@ async function finished(callId) {
66
70
  await new Promise((resolve) => setTimeout(resolve, 1000));
67
71
  }
68
72
  }
73
+ async function bounded(body, most) {
74
+ const chunks = [];
75
+ let bytes = 0;
76
+ for await (const chunk of body ?? []) {
77
+ bytes += chunk.length;
78
+ if (bytes > most)
79
+ return null;
80
+ chunks.push(chunk);
81
+ }
82
+ return Buffer.concat(chunks, bytes);
83
+ }
69
84
  function rerun(script, callId, flags) {
70
85
  return new Promise((resolve) => {
71
86
  const [command, ...args] = [
@@ -119,10 +134,21 @@ export class KitUpdate {
119
134
  async carry(call) {
120
135
  if ((await pendingCall()) === call.input_id)
121
136
  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());
137
+ const deadline = AbortSignal.timeout(BOOTSTRAP_MS);
138
+ let script;
139
+ try {
140
+ const answer = await fetch(relayed(call.bootstrap), { signal: deadline });
141
+ if (!answer.ok)
142
+ return { refused: `the Kit bootstrap could not be downloaded: ${answer.status}` };
143
+ script = await bounded(answer.body, BOOTSTRAP_BYTES);
144
+ }
145
+ catch (error) {
146
+ if (!deadline.aborted)
147
+ throw error;
148
+ return { refused: `the Kit bootstrap did not arrive within ${BOOTSTRAP_MS / 1000} seconds` };
149
+ }
150
+ if (script === null)
151
+ return { refused: `the Kit bootstrap is larger than ${BOOTSTRAP_BYTES} bytes` };
126
152
  if (createHash('sha256').update(script).digest('hex') !== call.sha256) {
127
153
  return { refused: 'the Kit bootstrap does not match its sha256' };
128
154
  }
@@ -1,4 +1,5 @@
1
- import { loginPath, onLoginPath, withoutProxy } from './clis.js';
1
+ import { loginPath, onLoginPath } from './clis.js';
2
+ import { childEnvironment } from './relay.js';
2
3
  import { killTree } from './scope.js';
3
4
  const TERMINAL = 'xterm-256color';
4
5
  const SIGNALLED = 128;
@@ -62,7 +63,7 @@ export class WebWizards {
62
63
  name: TERMINAL,
63
64
  ...run.size,
64
65
  cwd: process.cwd(),
65
- env: { ...withoutProxy(), PATH: path },
66
+ env: { ...childEnvironment(), PATH: path },
66
67
  });
67
68
  }
68
69
  catch (error) {
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-c44c14117631a",
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-ccce31cfa5fec",
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
- }