@agentshouse/kit 0.2.1-alpha.8 → 0.2.1-c02dacffca87c

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.
Files changed (57) hide show
  1. package/README.md +49 -24
  2. package/dist/abort.js +9 -0
  3. package/dist/acp.js +102 -88
  4. package/dist/agents.js +356 -52
  5. package/dist/api.js +40 -4
  6. package/dist/app-git.js +48 -0
  7. package/dist/authority-main.js +2 -2
  8. package/dist/bridge.js +93 -78
  9. package/dist/building.js +50 -0
  10. package/dist/calls.js +71 -0
  11. package/dist/chatgpt.js +32 -0
  12. package/dist/chunks.js +49 -0
  13. package/dist/clis.js +224 -88
  14. package/dist/codex-main.js +58 -0
  15. package/dist/configure-main.js +6 -0
  16. package/dist/connection.js +105 -0
  17. package/dist/conversations.js +749 -268
  18. package/dist/copies.js +475 -260
  19. package/dist/end.js +33 -0
  20. package/dist/enrol-main.js +7 -1
  21. package/dist/events.js +546 -0
  22. package/dist/files.js +140 -17
  23. package/dist/git-main.js +2 -2
  24. package/dist/git.js +62 -22
  25. package/dist/home.js +134 -10
  26. package/dist/house-cli.js +299 -0
  27. package/dist/house-main.js +5 -102
  28. package/dist/instructions.js +17 -18
  29. package/dist/kit-cli.js +68 -0
  30. package/dist/kit-main.js +8 -16
  31. package/dist/login.js +130 -95
  32. package/dist/logout.js +19 -0
  33. package/dist/mcp.js +43 -0
  34. package/dist/operation.js +7 -4
  35. package/dist/parts.js +82 -0
  36. package/dist/placement.js +31 -0
  37. package/dist/refusals.js +53 -0
  38. package/dist/relay.js +41 -0
  39. package/dist/resident.js +145 -51
  40. package/dist/scope.js +214 -0
  41. package/dist/scripts.js +255 -0
  42. package/dist/secret-input.js +7 -2
  43. package/dist/service-main.js +9 -0
  44. package/dist/service.js +55 -0
  45. package/dist/shell.js +3 -0
  46. package/dist/sign-in.js +52 -32
  47. package/dist/silence.js +28 -0
  48. package/dist/skills.js +92 -0
  49. package/dist/transcription.js +210 -0
  50. package/dist/translate.js +2 -8
  51. package/dist/update.js +127 -0
  52. package/dist/wire.js +40 -0
  53. package/dist/work.js +346 -0
  54. package/package.json +20 -21
  55. package/dist/blocks.js +0 -51
  56. package/dist/bootstrap.json +0 -28
  57. package/dist/stream.js +0 -47
package/dist/resident.js CHANGED
@@ -1,114 +1,208 @@
1
1
  import { once } from 'node:events';
2
+ import { watch } from 'node:fs';
2
3
  import { mkdir, rm, writeFile } from 'node:fs/promises';
3
- import { createServer } from 'node:http';
4
- import { extname, join } from 'node:path';
4
+ import { createServer, get } from 'node:http';
5
+ import { dirname, extname, join } from 'node:path';
6
+ import { DatabaseSync } from 'node:sqlite';
5
7
  import { fileURLToPath } from 'node:url';
6
- import { killDescendants } from './acp.js';
7
8
  import { Agents } from './agents.js';
8
9
  import { house } from './api.js';
9
10
  import { received } from './bridge.js';
10
- import { Conversations } from './conversations.js';
11
- import { WorkingCopies } from './copies.js';
12
- import { kitHome, readEnrolment } from './home.js';
11
+ import { CONVERSATION_METHODS, Conversations } from './conversations.js';
12
+ import { LocalCopies } from './copies.js';
13
+ import { restart, unknownMethod } from './end.js';
14
+ import { agentBase, enrolled, kitHome, ownerLock, ownerSocket, readEnrolment } from './home.js';
15
+ import { Calls } from './calls.js';
16
+ import { killDescendants } from './scope.js';
17
+ import { removeInputs, Scripts } from './scripts.js';
18
+ import { quoted } from './shell.js';
13
19
  import { SignIns } from './sign-in.js';
14
- import { holdStream } from './stream.js';
20
+ import { placeSkillSet } from './skills.js';
21
+ import { holdConnection } from './connection.js';
22
+ import { KitUpdate } from './update.js';
23
+ import { takeUnservedRelease } from './wire.js';
24
+ import { codexLauncher } from './work.js';
15
25
  function report(error) {
16
26
  process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
17
27
  }
18
28
  function logged(work) {
19
29
  work.catch(report);
20
30
  }
21
- function quoted(value) {
22
- return `'${value.replaceAll("'", `'\\''`)}'`;
31
+ async function installShim(path, module) {
32
+ const main = fileURLToPath(new URL(`./${module}${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
33
+ await mkdir(dirname(path), { recursive: true });
34
+ await writeFile(path, `#!/bin/sh\nexec ${quoted(process.execPath)} ${quoted(main)} "$@"\n`, { mode: 0o755 });
23
35
  }
24
- async function installShim() {
25
- const main = fileURLToPath(new URL(`./git-main${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
26
- await mkdir(join(kitHome(), 'shim'), { recursive: true });
27
- await writeFile(join(kitHome(), 'shim', 'git'), `#!/bin/sh\nexec ${quoted(process.execPath)} ${quoted(main)} "$@"\n`, {
28
- mode: 0o755,
29
- });
36
+ function claimed(lock) {
37
+ try {
38
+ lock.exec('BEGIN EXCLUSIVE');
39
+ return true;
40
+ }
41
+ catch (error) {
42
+ if (error.errcode === 5)
43
+ return false;
44
+ throw error;
45
+ }
30
46
  }
31
47
  async function serveOwner(copies) {
32
- const socketPath = join(kitHome(), 'owner.sock');
33
- await rm(socketPath, { force: true });
48
+ const lock = new DatabaseSync(ownerLock());
49
+ if (!claimed(lock)) {
50
+ lock.close();
51
+ const running = await runningResident();
52
+ throw new Error(`${running === null ? 'another Kit process' : `Kit process ${running}`} already runs this Environment from Kit home ${kitHome()}; stop it first to run another Kit process here`);
53
+ }
34
54
  const server = createServer(async (request, response) => {
35
55
  try {
36
- if (request.url !== '/git/push') {
56
+ const text = await received(request);
57
+ let answered;
58
+ if (request.url === '/resident') {
59
+ response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ pid: process.pid }));
60
+ return;
61
+ }
62
+ if (request.url === '/git/fetch') {
63
+ answered = await copies.fetch();
64
+ }
65
+ else if (request.url === '/git/push') {
66
+ const { cwd, commit } = JSON.parse(text);
67
+ answered = await copies.push(cwd, commit, copies.owner);
68
+ }
69
+ else {
37
70
  response.writeHead(404).end();
38
71
  return;
39
72
  }
40
- const { cwd, commit } = JSON.parse(await received(request));
41
- const pushed = await copies.push(cwd, commit, copies.owner);
42
- response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(pushed));
73
+ response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(answered));
43
74
  }
44
75
  catch (error) {
45
76
  response.writeHead(502).end(error instanceof Error ? error.message : String(error));
46
77
  }
47
78
  });
48
- server.listen(socketPath);
79
+ server.on('close', () => lock.close());
80
+ await rm(ownerSocket(), { force: true });
81
+ server.listen(ownerSocket());
49
82
  await once(server, 'listening');
50
83
  }
84
+ function runningResident() {
85
+ return new Promise((resolve, reject) => {
86
+ get({ socketPath: ownerSocket(), path: '/resident' }, (answer) => {
87
+ received(answer).then((text) => resolve(JSON.parse(text).pid), reject);
88
+ }).on('error', (error) => {
89
+ if (error.code === 'ENOENT' || error.code === 'ECONNREFUSED')
90
+ resolve(null);
91
+ else
92
+ reject(error);
93
+ });
94
+ });
95
+ }
96
+ function connected() {
97
+ return new Promise((resolve) => {
98
+ const watcher = watch(kitHome(), () => {
99
+ if (!enrolled())
100
+ return;
101
+ watcher.close();
102
+ resolve();
103
+ });
104
+ if (!enrolled()) {
105
+ process.stderr.write('kit: this Environment is not connected; run kit login\n');
106
+ return;
107
+ }
108
+ watcher.close();
109
+ resolve();
110
+ });
111
+ }
112
+ async function started() {
113
+ await connected();
114
+ watch(kitHome(), () => {
115
+ if (enrolled())
116
+ return;
117
+ killDescendants();
118
+ process.exit(1);
119
+ });
120
+ await placeSkillSet((await readEnrolment()).house);
121
+ await mkdir(agentBase(), { recursive: true });
122
+ await installShim(join(kitHome(), 'shim', 'git'), 'git-main');
123
+ await installShim(codexLauncher(), 'codex-main');
124
+ }
51
125
  export async function resident() {
52
- if ((await readEnrolment()) === null) {
53
- process.stderr.write('kit: this Environment is not enrolled; run kit login\n');
54
- process.exitCode = 1;
55
- return;
56
- }
57
- process.env.HOUSE_KIT_RESIDENT = String(process.pid);
58
- const agents = new Agents(house);
59
- const copies = new WorkingCopies(house);
126
+ await mkdir(kitHome(), { recursive: true, mode: 0o700 });
127
+ const copies = new LocalCopies(house);
60
128
  await copies.load();
61
- await installShim();
62
129
  await serveOwner(copies);
63
- let stream = null;
130
+ await copies.swept();
131
+ await started().catch((error) => {
132
+ report(error);
133
+ process.exit(1);
134
+ });
135
+ process.env.HOUSE_KIT_RESIDENT = String(process.pid);
136
+ const agents = new Agents(house, () => changed());
137
+ let connection = null;
64
138
  let reported = false;
65
- const idle = () => conversations.idle() && signIns.idle();
139
+ const idle = () => calls.idle() && conversations.idle() && agents.idle();
66
140
  const changed = () => {
67
141
  if (!idle()) {
68
142
  reported = false;
69
143
  }
144
+ else if (update.placedRelease() !== null && conversations.quiet()) {
145
+ restart(`Kit ${update.placedRelease()} is placed; its supervisor starts it`);
146
+ }
70
147
  else if (!reported) {
71
148
  reported = true;
72
149
  logged(house.deliver('/kit/idle', {}, idle));
73
150
  }
74
151
  };
152
+ const calls = new Calls(house, changed);
75
153
  const conversations = new Conversations({
76
154
  house,
77
155
  agents,
78
156
  copies,
79
- send: (frame) => stream?.send(frame) ?? false,
157
+ calls,
158
+ send: (notification) => connection?.send(notification) ?? false,
80
159
  changed,
81
160
  });
82
- const signIns = new SignIns(house, agents, changed);
161
+ const signIns = new SignIns(house, agents, calls);
162
+ const update = new KitUpdate(calls);
163
+ if (process.env.HOUSE_KIT_SANDBOX !== '1')
164
+ takeUnservedRelease((release) => logged(update.takeRefused(release)));
165
+ removeInputs();
166
+ const scripts = new Scripts(calls, (notification) => connection?.send(notification) ?? false);
83
167
  for (const signal of ['SIGTERM', 'SIGINT']) {
84
168
  process.once(signal, () => {
85
169
  killDescendants();
170
+ removeInputs();
86
171
  process.kill(process.pid, signal);
87
172
  });
88
173
  }
89
174
  await house.deliver('/kit/restarted', {}).catch(report);
90
175
  logged(agents.refresh());
91
- stream = holdStream({
176
+ agents.keepCurrent();
177
+ connection = holdConnection({
92
178
  opened: () => {
93
179
  conversations.opened();
94
180
  changed();
95
- logged(copies.select());
181
+ logged(copies.connected());
96
182
  },
97
- frame: (arrived) => {
98
- if (arrived.type === 'work_available' && arrived.subject === 'agents')
99
- logged(agents.refresh());
100
- if (arrived.type === 'work_available' && arrived.subject === 'working_copy')
101
- logged(copies.select());
102
- if (arrived.type === 'entries') {
103
- copies.entries(arrived);
104
- }
105
- if (arrived.type === 'input') {
106
- reported = false;
107
- if (arrived.kind === 'sign_in')
108
- signIns.start(arrived);
109
- else
110
- conversations.input(arrived);
183
+ closed: () => scripts.disconnected(),
184
+ call: (call) => {
185
+ if (call.kind === 'sign_in')
186
+ signIns.start(call);
187
+ else if (call.kind === 'update')
188
+ update.take(call);
189
+ else if (call.kind === 'run_wizard' || call.kind === 'run_function')
190
+ scripts.start(call);
191
+ else if (CONVERSATION_METHODS.has(call.kind))
192
+ conversations.call(call);
193
+ else {
194
+ const refused = { refused: unknownMethod(call.kind) };
195
+ void calls.carry(call.input_id, async () => ({ result: refused }));
111
196
  }
112
197
  },
198
+ notification: (notification) => {
199
+ if (notification.type === 'watching')
200
+ conversations.watching(notification.watching === true);
201
+ if (notification.type === 'authority_changed')
202
+ logged(copies.changed(notification.rooms));
203
+ if (notification.type === 'work_available' && notification.subject === 'agents')
204
+ logged(agents.refresh());
205
+ scripts.notified(notification);
206
+ },
113
207
  });
114
208
  }
package/dist/scope.js ADDED
@@ -0,0 +1,214 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { mkdirSync, readdirSync, readFileSync, realpathSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ const LISTING_BYTES = 1 << 28;
5
+ const CGROUPS = '/sys/fs/cgroup';
6
+ const GONE = new Set(['ENOENT', 'ESRCH']);
7
+ const EXITED = new Set(['Z', 'X']);
8
+ function failure(error) {
9
+ return error.code ?? '';
10
+ }
11
+ function listed(environment) {
12
+ const found = new Map();
13
+ const output = execFileSync('ps', [environment ? '-AEww' : '-Aww', '-o', 'pid=,ppid=,sess=,command='], {
14
+ encoding: 'utf8',
15
+ maxBuffer: LISTING_BYTES,
16
+ });
17
+ for (const line of output.split('\n')) {
18
+ const fields = /^\s*(\d+)\s+(\d+)\s+(\S+)\s?(.*)$/.exec(line);
19
+ if (fields !== null)
20
+ found.set(Number(fields[1]), { parent: Number(fields[2]), session: fields[3], command: fields[4] });
21
+ }
22
+ return found;
23
+ }
24
+ function read(pid) {
25
+ try {
26
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
27
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
28
+ if (EXITED.has(fields[0]))
29
+ return null;
30
+ const command = readFileSync(`/proc/${pid}/cmdline`, 'utf8').replaceAll('\0', ' ');
31
+ return { parent: Number(fields[1]), session: fields[3], command };
32
+ }
33
+ catch (error) {
34
+ if (GONE.has(failure(error)))
35
+ return null;
36
+ throw error;
37
+ }
38
+ }
39
+ function processes() {
40
+ if (process.platform === 'darwin')
41
+ return listed(false);
42
+ const found = new Map();
43
+ for (const entry of readdirSync('/proc')) {
44
+ const running = /^\d+$/.test(entry) ? read(entry) : null;
45
+ if (running !== null)
46
+ found.set(Number(entry), running);
47
+ }
48
+ return found;
49
+ }
50
+ function descendants(roots, table = processes()) {
51
+ const children = new Map();
52
+ for (const [pid, { parent }] of table)
53
+ children.set(parent, [...(children.get(parent) ?? []), pid]);
54
+ const tree = [];
55
+ const pending = roots.flatMap((root) => children.get(root) ?? []);
56
+ while (pending.length > 0) {
57
+ const pid = pending.pop();
58
+ tree.push(pid);
59
+ pending.push(...(children.get(pid) ?? []));
60
+ }
61
+ return tree;
62
+ }
63
+ function marked(marker, doubted) {
64
+ if (process.platform === 'darwin') {
65
+ return [...listed(true)].filter(([, { command }]) => ` ${command} `.includes(` ${marker} `)).map(([pid]) => pid);
66
+ }
67
+ return readdirSync('/proc')
68
+ .filter((entry) => /^\d+$/.test(entry) && carries(entry, marker, doubted))
69
+ .map(Number);
70
+ }
71
+ function carries(pid, marker, doubted) {
72
+ try {
73
+ return readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0').includes(marker);
74
+ }
75
+ catch (error) {
76
+ if (GONE.has(failure(error)))
77
+ return false;
78
+ if (failure(error) !== 'EACCES')
79
+ throw error;
80
+ return doubted && statSync(`/proc/${pid}`, { throwIfNoEntry: false })?.uid === process.getuid();
81
+ }
82
+ }
83
+ function killAll(pids) {
84
+ for (const pid of pids) {
85
+ for (const target of [-pid, pid]) {
86
+ try {
87
+ process.kill(target, 'SIGKILL');
88
+ }
89
+ catch { }
90
+ }
91
+ }
92
+ }
93
+ export function killTree(child) {
94
+ killAll([child.pid, ...descendants([child.pid])]);
95
+ }
96
+ export function killMarked(marker) {
97
+ const pids = marked(marker, false);
98
+ killAll([...pids, ...descendants(pids)]);
99
+ }
100
+ export function killDescendants() {
101
+ const pids = marked(`HOUSE_KIT_RESIDENT=${process.pid}`, false);
102
+ killAll([...pids, ...descendants([process.pid, ...pids])]);
103
+ }
104
+ function ownGroup() {
105
+ if (process.platform !== 'linux')
106
+ return null;
107
+ try {
108
+ const line = readFileSync('/proc/self/cgroup', 'utf8')
109
+ .split('\n')
110
+ .find((entry) => entry.startsWith('0::'));
111
+ return line === undefined ? null : join(CGROUPS, line.slice(3));
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ function members(group) {
118
+ const pids = readFileSync(join(group, 'cgroup.procs'), 'utf8').split('\n').filter(Boolean).map(Number);
119
+ for (const entry of readdirSync(group, { withFileTypes: true })) {
120
+ if (entry.isDirectory())
121
+ pids.push(...members(join(group, entry.name)));
122
+ }
123
+ return pids;
124
+ }
125
+ function besideCli(cli) {
126
+ try {
127
+ return `${realpathSync(cli)}-`;
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ export class Scope {
134
+ marker;
135
+ helpers;
136
+ group;
137
+ session = null;
138
+ recorded = null;
139
+ constructor(marker, name, cli) {
140
+ this.marker = marker;
141
+ this.helpers = besideCli(cli);
142
+ const own = ownGroup();
143
+ this.group = own === null ? null : join(own, name);
144
+ try {
145
+ if (this.group !== null)
146
+ mkdirSync(this.group);
147
+ }
148
+ catch {
149
+ this.group = null;
150
+ }
151
+ }
152
+ command(command, args) {
153
+ if (this.group === null)
154
+ return [command, args];
155
+ return ['/bin/sh', ['-c', 'echo $$ > "$0/cgroup.procs" 2>/dev/null; exec "$@"', this.group, command, ...args]];
156
+ }
157
+ entered(pid) {
158
+ this.session = process.platform === 'darwin' ? (listed(false).get(pid)?.session ?? null) : String(pid);
159
+ if (this.group === null)
160
+ return;
161
+ const joined = readFileSync(`/proc/${pid}/cgroup`, 'utf8').split('\n').find((entry) => entry.startsWith('0::'));
162
+ if (joined !== undefined && join(CGROUPS, joined.slice(3)) === this.group)
163
+ return;
164
+ this.close();
165
+ this.group = null;
166
+ }
167
+ record() {
168
+ const found = this.found();
169
+ if (found === null)
170
+ return;
171
+ this.recorded = new Set([...found].filter(([, { session }]) => session === this.session).map(([pid]) => pid));
172
+ }
173
+ working() {
174
+ const found = this.found();
175
+ if (found === null)
176
+ return true;
177
+ if (this.recorded === null)
178
+ return found.size > 0;
179
+ return [...found].some(([pid, { session, command }]) => session !== this.session || !(this.recorded.has(pid) || (this.helpers !== null && command.startsWith(this.helpers))));
180
+ }
181
+ empty() {
182
+ return this.found()?.size === 0;
183
+ }
184
+ kill() {
185
+ killMarked(this.marker);
186
+ if (this.group === null)
187
+ return;
188
+ try {
189
+ writeFileSync(join(this.group, 'cgroup.kill'), '1');
190
+ }
191
+ catch {
192
+ killAll([...(this.found()?.keys() ?? [])]);
193
+ }
194
+ }
195
+ close() {
196
+ if (this.group === null)
197
+ return;
198
+ try {
199
+ rmdirSync(this.group);
200
+ }
201
+ catch { }
202
+ }
203
+ found() {
204
+ try {
205
+ const table = processes();
206
+ const roots = this.group === null ? marked(this.marker, true) : members(this.group);
207
+ const pids = this.group === null ? [...roots, ...descendants(roots, table)] : roots;
208
+ return new Map(pids.flatMap((pid) => (table.has(pid) ? [[pid, table.get(pid)]] : [])));
209
+ }
210
+ catch {
211
+ return null;
212
+ }
213
+ }
214
+ }