@agentshouse/kit 0.2.1-c3e4a20bb3183 → 0.2.1-c4beedfd8b549
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 +23 -24
- package/dist/acp.js +16 -93
- package/dist/agents.js +81 -39
- package/dist/api.js +32 -3
- package/dist/app-git.js +64 -0
- package/dist/bootstrap.json +10 -17
- package/dist/bridge.js +35 -40
- package/dist/clis.js +95 -65
- package/dist/codex-main.js +58 -0
- package/dist/conversations.js +284 -236
- package/dist/copies.js +34 -41
- package/dist/end.js +8 -0
- package/dist/events.js +521 -0
- package/dist/files.js +16 -1
- package/dist/home.js +15 -2
- package/dist/house-main.js +39 -22
- package/dist/instructions.js +3 -3
- package/dist/kit-main.js +7 -1
- package/dist/login.js +6 -2
- package/dist/logout.js +21 -0
- package/dist/mcp.js +2 -1
- package/dist/parts.js +64 -0
- package/dist/refusals.js +12 -0
- package/dist/relay.js +10 -0
- package/dist/resident.js +37 -18
- package/dist/scope.js +203 -0
- package/dist/secret-input.js +4 -1
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +34 -9
- package/dist/skills.js +1 -1
- package/dist/stream.js +31 -5
- package/dist/transcription.js +54 -6
- package/dist/update.js +2 -0
- package/dist/work.js +348 -0
- package/package.json +1 -1
- package/dist/blocks.js +0 -51
package/dist/house-main.js
CHANGED
|
@@ -6,10 +6,14 @@ import { basename, join } from 'node:path';
|
|
|
6
6
|
import { kitHome, readOwnAgent } from './home.js';
|
|
7
7
|
import { callHouse, rpc, UNREACHABLE } from './mcp.js';
|
|
8
8
|
import { refusalOf } from './refusals.js';
|
|
9
|
+
import { relayed } from './relay.js';
|
|
10
|
+
import { parse } from 'yaml';
|
|
9
11
|
const USAGE = "usage: house <tool> ['<arguments as JSON>']";
|
|
10
|
-
const
|
|
12
|
+
const PUSH_USAGE = 'usage: house push [commit] [--owner]';
|
|
13
|
+
const PUSH = "push [commit]: sends the Local copy's committed changes to House";
|
|
11
14
|
const ARGUMENTS = `the arguments are one JSON object in single quotes, like house search '{"query":"invoice"}'`;
|
|
12
15
|
const CLOSED = "this conversation's House connection is closed; nothing to do from here";
|
|
16
|
+
const SANDBOXED = "your CLI's sandbox kept house from House; run house directly, not through another program";
|
|
13
17
|
const REVOKED = 'this conversation no longer has House access; nothing to do from here';
|
|
14
18
|
const UNCONNECTED = "this computer's own agent has no House connection; run kit login";
|
|
15
19
|
const DISCONNECTED = "House no longer accepts this computer's own agent; run kit login";
|
|
@@ -41,7 +45,7 @@ function bridged(path, body, headers = { 'content-type': 'application/json' }, t
|
|
|
41
45
|
reject(new Error(refused(path, answer.statusCode, text)));
|
|
42
46
|
});
|
|
43
47
|
});
|
|
44
|
-
sent.on('error', () => reject(new Error(CLOSED)));
|
|
48
|
+
sent.on('error', (error) => reject(new Error(error.code === 'EPERM' || error.code === 'EACCES' ? SANDBOXED : CLOSED)));
|
|
45
49
|
sent.end(body instanceof Buffer ? body : JSON.stringify(body));
|
|
46
50
|
});
|
|
47
51
|
}
|
|
@@ -77,11 +81,24 @@ async function localFile(path) {
|
|
|
77
81
|
: `${path} is not a readable file`);
|
|
78
82
|
}
|
|
79
83
|
}
|
|
84
|
+
async function sent(url, bytes, operation) {
|
|
85
|
+
const headers = { 'content-type': 'application/octet-stream', 'x-house-byte-operation': operation };
|
|
86
|
+
if (socketPath !== undefined)
|
|
87
|
+
return bridged(url, bytes, headers);
|
|
88
|
+
setGlobalProxyFromEnv();
|
|
89
|
+
const answer = await fetch(relayed(url), { method: 'POST', headers, body: bytes }).catch(() => {
|
|
90
|
+
throw new Error(UNREACHABLE);
|
|
91
|
+
});
|
|
92
|
+
const text = await answer.text();
|
|
93
|
+
if (answer.status === 200)
|
|
94
|
+
return text;
|
|
95
|
+
throw new Error(refused(url, answer.status, text));
|
|
96
|
+
}
|
|
80
97
|
async function upload(path, roomRef, missing) {
|
|
81
98
|
if (typeof path !== 'string' || path === '')
|
|
82
99
|
throw new Error(missing);
|
|
83
100
|
const bytes = await localFile(path);
|
|
84
|
-
const declared =
|
|
101
|
+
const declared = parse(await called('upload', {
|
|
85
102
|
...(roomRef === undefined ? {} : { room_ref: roomRef }),
|
|
86
103
|
name: basename(path),
|
|
87
104
|
bytes: bytes.length,
|
|
@@ -89,25 +106,20 @@ async function upload(path, roomRef, missing) {
|
|
|
89
106
|
}));
|
|
90
107
|
if (declared.upload === undefined)
|
|
91
108
|
return declared.attachment;
|
|
92
|
-
const saved = JSON.parse(await
|
|
93
|
-
'content-type': 'application/octet-stream',
|
|
94
|
-
'x-house-byte-operation': declared.upload.operation,
|
|
95
|
-
}));
|
|
109
|
+
const saved = JSON.parse(await sent(declared.upload.url, bytes, declared.upload.operation));
|
|
96
110
|
if (saved.upload_failure !== undefined)
|
|
97
111
|
throw new Error(`${path} was not saved: ${saved.upload_failure}; call again later`);
|
|
98
112
|
return saved.attachment;
|
|
99
113
|
}
|
|
100
|
-
async function
|
|
114
|
+
async function push(args) {
|
|
101
115
|
if (args.includes('--help'))
|
|
102
|
-
return
|
|
103
|
-
if (args[0] !== 'push')
|
|
104
|
-
throw new Error('only house git push [commit] exists; use plain git for the rest');
|
|
116
|
+
return PUSH_USAGE;
|
|
105
117
|
const owner = args.includes('--owner');
|
|
106
|
-
const commit = args.
|
|
118
|
+
const commit = args.find((arg) => arg !== '--owner') ?? 'HEAD';
|
|
107
119
|
if (owner && socketPath !== undefined)
|
|
108
|
-
throw new Error('--owner is not for Agent conversations; run house
|
|
120
|
+
throw new Error('--owner is not for Agent conversations; run house push without it');
|
|
109
121
|
if (!owner && socketPath === undefined)
|
|
110
|
-
throw new Error('outside an Agent conversation, house
|
|
122
|
+
throw new Error('outside an Agent conversation, house push needs --owner');
|
|
111
123
|
const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit }, undefined, owner ? join(kitHome(), 'owner.sock') : socketPath));
|
|
112
124
|
if (pushed.refused)
|
|
113
125
|
throw new Error(pushed.text);
|
|
@@ -128,35 +140,40 @@ function argumentsOf(argument) {
|
|
|
128
140
|
return parsed;
|
|
129
141
|
}
|
|
130
142
|
async function house([verb, argument]) {
|
|
131
|
-
if (verb === undefined || verb === '--help') {
|
|
132
|
-
return [USAGE, '
|
|
143
|
+
if (verb === undefined || verb === '--help' || verb === 'help') {
|
|
144
|
+
return [USAGE, 'help', PUSH, ...(await tools()).map((tool) => (tool.description === undefined ? tool.name : `${tool.name}: ${tool.description}`))].join('\n');
|
|
133
145
|
}
|
|
134
146
|
if (argument === '--help') {
|
|
135
147
|
const tool = (await tools()).find((listed) => listed.name === verb);
|
|
136
148
|
if (tool === undefined)
|
|
137
149
|
throw new Error(`${verb} is no Tool; house find_command '{"query":"${verb}"}' finds Commands`);
|
|
138
|
-
return
|
|
150
|
+
return [...(tool.description === undefined ? [] : [tool.description]), JSON.stringify(tool.inputSchema)].join('\n');
|
|
139
151
|
}
|
|
140
152
|
const args = argumentsOf(argument);
|
|
141
|
-
if (
|
|
142
|
-
return JSON.stringify({ attachment: await upload(args.path, args.room_ref, '
|
|
153
|
+
if (verb === 'upload') {
|
|
154
|
+
return JSON.stringify({ attachment: await upload(args.path, args.room_ref, 'upload needs "path", a local file') });
|
|
143
155
|
}
|
|
144
|
-
if (
|
|
156
|
+
if (verb === 'append_record' && Array.isArray(args.attachments)) {
|
|
145
157
|
const attachments = [];
|
|
146
158
|
for (const path of args.attachments) {
|
|
147
159
|
attachments.push(await upload(path, args.room_ref, 'each attachments entry is the path of a local file'));
|
|
148
160
|
}
|
|
149
161
|
args.attachments = attachments;
|
|
150
162
|
}
|
|
151
|
-
|
|
163
|
+
return called(verb, args);
|
|
164
|
+
}
|
|
165
|
+
async function called(name, args) {
|
|
166
|
+
const result = await mcp('tools/call', { name, arguments: args });
|
|
152
167
|
const text = result.content.map((content) => content.text).join('\n');
|
|
153
168
|
if (result.isError === true)
|
|
154
169
|
throw new Error(text.replace(/\n+$/, ''));
|
|
155
170
|
return text;
|
|
156
171
|
}
|
|
157
172
|
const argv = process.argv.slice(2);
|
|
158
|
-
(argv[0] === '
|
|
173
|
+
(argv[0] === 'push' ? push(argv.slice(1)) : house(argv)).then((text) => {
|
|
159
174
|
process.stdout.write(`${text}\n`);
|
|
175
|
+
if (argv[0] === 'shell')
|
|
176
|
+
process.exitCode = Number(/^exit: (\d+)(?:\n|$)/.exec(text)?.[1] ?? 0);
|
|
160
177
|
}, (error) => {
|
|
161
178
|
process.stderr.write(`house: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
162
179
|
process.exitCode = 1;
|
package/dist/instructions.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { HOW_WE_WORK } from './skills.js';
|
|
2
2
|
const HOUSE_LINE = 'Work with House through the `house` CLI: run `house --help`.';
|
|
3
|
-
const FILE_LINE = 'To give the User a file or a page, upload it with `house
|
|
4
|
-
const READ_LINE = 'Before working in a Room, read `/private/rooms/<handle>/AGENTS.md`.';
|
|
5
|
-
const COPIES_LINE = 'House Local copies; commit, then `house
|
|
3
|
+
const FILE_LINE = 'To give the User a file or a page, upload it with `house upload` and link it; never start a server.';
|
|
4
|
+
const READ_LINE = 'Before working in a Room, read `/rooms/private/rooms/<handle>/AGENTS.md`.';
|
|
5
|
+
const COPIES_LINE = 'House Local copies; commit, then `house push`:';
|
|
6
6
|
const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
|
|
7
7
|
const FAILED = /^exit: [1-9]\d*$/;
|
|
8
8
|
const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
|
package/dist/kit-main.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { setGlobalProxyFromEnv } from 'node:http';
|
|
3
|
+
import { answerGitCredential } from './app-git.js';
|
|
3
4
|
import { login } from './login.js';
|
|
5
|
+
import { logout } from './logout.js';
|
|
4
6
|
import { resident } from './resident.js';
|
|
5
7
|
const [command, ...argv] = process.argv.slice(2);
|
|
6
8
|
setGlobalProxyFromEnv();
|
|
7
9
|
async function run() {
|
|
8
10
|
if (command === 'login')
|
|
9
11
|
return login(argv);
|
|
12
|
+
if (command === 'logout')
|
|
13
|
+
return logout();
|
|
10
14
|
if (command === 'resident')
|
|
11
15
|
return resident();
|
|
12
|
-
|
|
16
|
+
if (command === 'git-credential')
|
|
17
|
+
return answerGitCredential(argv[0]);
|
|
18
|
+
process.stderr.write('usage: kit <login|logout|resident>\n');
|
|
13
19
|
process.exitCode = 2;
|
|
14
20
|
}
|
|
15
21
|
run().catch((error) => {
|
package/dist/login.js
CHANGED
|
@@ -4,7 +4,8 @@ import { hostname } from 'node:os';
|
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
6
|
import { post } from './api.js';
|
|
7
|
-
import {
|
|
7
|
+
import { placeAppGit } from './app-git.js';
|
|
8
|
+
import { forgetOwnAgent, readDisconnected, readEnrolment, readOwnAgent, writeEnrolment, writeOwnAgent } from './home.js';
|
|
8
9
|
import { callHouse, rpc } from './mcp.js';
|
|
9
10
|
const DEFAULT_HOUSE = 'https://agents.house';
|
|
10
11
|
function listen() {
|
|
@@ -78,7 +79,7 @@ export async function login(argv) {
|
|
|
78
79
|
manual: { type: 'boolean', default: false },
|
|
79
80
|
},
|
|
80
81
|
});
|
|
81
|
-
const enrolled = await readEnrolment();
|
|
82
|
+
const enrolled = (await readEnrolment()) ?? (await readDisconnected());
|
|
82
83
|
const house = values.house ?? enrolled?.house ?? DEFAULT_HOUSE;
|
|
83
84
|
const replaces = process.env.HOUSE_KIT_REPLACES;
|
|
84
85
|
const environment = replaces === undefined ? (values.environment ?? (enrolled?.house === house ? enrolled.environment : undefined)) : undefined;
|
|
@@ -104,6 +105,7 @@ export async function login(argv) {
|
|
|
104
105
|
process.stdout.write("This computer's own agent was another User's; run kit login again to connect it to you.\n");
|
|
105
106
|
}
|
|
106
107
|
};
|
|
108
|
+
const placeGit = async () => placeAppGit(ownAgent, await readOwnAgent());
|
|
107
109
|
if (values.manual) {
|
|
108
110
|
const { start_url } = await post(house, '/kit', {
|
|
109
111
|
...started,
|
|
@@ -111,6 +113,7 @@ export async function login(argv) {
|
|
|
111
113
|
});
|
|
112
114
|
process.stdout.write(`Open this link and confirm: ${start_url}\n`);
|
|
113
115
|
await exchange(await typed('Code: '));
|
|
116
|
+
await placeGit();
|
|
114
117
|
return;
|
|
115
118
|
}
|
|
116
119
|
const callback = await listen();
|
|
@@ -133,4 +136,5 @@ export async function login(argv) {
|
|
|
133
136
|
finally {
|
|
134
137
|
callback.close();
|
|
135
138
|
}
|
|
139
|
+
await placeGit();
|
|
136
140
|
}
|
package/dist/logout.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { HouseRefusal, post } from './api.js';
|
|
2
|
+
import { forgetAppGit } from './app-git.js';
|
|
3
|
+
import { forgetEnrolment, readEnrolment } from './home.js';
|
|
4
|
+
export async function logout() {
|
|
5
|
+
const enrolled = await readEnrolment();
|
|
6
|
+
if (enrolled === null) {
|
|
7
|
+
await forgetAppGit();
|
|
8
|
+
process.stdout.write('This computer is not connected to House.\n');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
await post(enrolled.house, '/kit/logout', {}, enrolled.credential);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (!(error instanceof HouseRefusal && error.status === 401))
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
await forgetEnrolment(enrolled);
|
|
19
|
+
await forgetAppGit();
|
|
20
|
+
process.stdout.write(`Environment ${enrolled.environment} is disconnected; kit login reconnects it.\n`);
|
|
21
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { KIT_VERSION } from './clis.js';
|
|
3
|
+
import { relayed } from './relay.js';
|
|
3
4
|
const MCP_PROTOCOL_VERSION = '2026-07-28';
|
|
4
5
|
export const UNREACHABLE = 'House is unreachable; call again in a minute';
|
|
5
6
|
export function rpc(method, params) {
|
|
@@ -21,7 +22,7 @@ export function mcpBody(message, operation) {
|
|
|
21
22
|
}
|
|
22
23
|
export async function callHouse(origin, credential, message, operation = null, signal) {
|
|
23
24
|
const tool = message.method === 'tools/call' ? String(message.params.name) : null;
|
|
24
|
-
const answer = await fetch(new URL('/', origin), {
|
|
25
|
+
const answer = await fetch(relayed(new URL('/', origin)), {
|
|
25
26
|
method: 'POST',
|
|
26
27
|
headers: {
|
|
27
28
|
'content-type': 'application/json',
|
package/dist/parts.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export class Parts {
|
|
2
|
+
parts = [];
|
|
3
|
+
context = null;
|
|
4
|
+
emit;
|
|
5
|
+
writing = null;
|
|
6
|
+
planned = null;
|
|
7
|
+
constructor(emit) {
|
|
8
|
+
this.emit = emit;
|
|
9
|
+
}
|
|
10
|
+
write(type, id, text) {
|
|
11
|
+
if (text === '')
|
|
12
|
+
return;
|
|
13
|
+
const last = this.parts.length - 1;
|
|
14
|
+
const part = this.parts[last];
|
|
15
|
+
if (this.writing?.type === type && this.writing.id === id && part?.type === type) {
|
|
16
|
+
part.text += text;
|
|
17
|
+
this.emit({ op: 'append', index: last, text });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
this.start({ type, text });
|
|
21
|
+
this.writing = { type, id };
|
|
22
|
+
}
|
|
23
|
+
command() {
|
|
24
|
+
const last = this.parts.length - 1;
|
|
25
|
+
const part = this.parts[last];
|
|
26
|
+
if (part?.type !== 'commands') {
|
|
27
|
+
this.start({ type: 'commands', count: 1 });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
part.count++;
|
|
31
|
+
this.emit({ op: 'count', index: last, count: part.count });
|
|
32
|
+
}
|
|
33
|
+
start(part) {
|
|
34
|
+
this.writing = null;
|
|
35
|
+
this.parts.push(part);
|
|
36
|
+
const index = this.parts.length - 1;
|
|
37
|
+
this.emit({ op: 'start', index, part });
|
|
38
|
+
return index;
|
|
39
|
+
}
|
|
40
|
+
done(index, result) {
|
|
41
|
+
const part = this.parts[index];
|
|
42
|
+
part.status = 'done';
|
|
43
|
+
part.result = result;
|
|
44
|
+
this.emit({ op: 'status', index, status: 'done', result });
|
|
45
|
+
}
|
|
46
|
+
plan(entries) {
|
|
47
|
+
if (this.planned === null) {
|
|
48
|
+
this.planned = this.start({ type: 'plan', entries });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.parts[this.planned].entries = entries;
|
|
52
|
+
this.emit({ op: 'plan', index: this.planned, entries });
|
|
53
|
+
}
|
|
54
|
+
measure(context) {
|
|
55
|
+
this.context = context;
|
|
56
|
+
this.emit({ op: 'context', ...context });
|
|
57
|
+
}
|
|
58
|
+
operations() {
|
|
59
|
+
return [
|
|
60
|
+
...this.parts.map((part, index) => ({ op: 'start', index, part })),
|
|
61
|
+
...(this.context === null ? [] : [{ op: 'context', ...this.context }]),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/refusals.js
CHANGED
|
@@ -21,6 +21,18 @@ function spoken(text) {
|
|
|
21
21
|
const detail = said.indexOf('\n');
|
|
22
22
|
return { code: head[1], text: said, conflicts: detail === -1 ? [] : conflictCodes(said.slice(detail + 1)) };
|
|
23
23
|
}
|
|
24
|
+
export function retryable(text) {
|
|
25
|
+
let body;
|
|
26
|
+
try {
|
|
27
|
+
body = JSON.parse(text);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const error = body?.error;
|
|
33
|
+
const verdict = error?.retryable ?? error?.data?.retryable;
|
|
34
|
+
return typeof verdict === 'boolean' ? verdict : null;
|
|
35
|
+
}
|
|
24
36
|
export function refusalOf(text) {
|
|
25
37
|
let body;
|
|
26
38
|
try {
|
package/dist/relay.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function relayed(target) {
|
|
2
|
+
const url = new URL(target);
|
|
3
|
+
const relay = process.env.HOUSE_KIT_RELAY;
|
|
4
|
+
if (relay === undefined || relay === '')
|
|
5
|
+
return url;
|
|
6
|
+
const through = new URL(`${url.host}${url.pathname}${url.search}`, `${relay}/`);
|
|
7
|
+
if (url.protocol === 'wss:' || url.protocol === 'ws:')
|
|
8
|
+
through.protocol = 'wss:';
|
|
9
|
+
return through;
|
|
10
|
+
}
|
package/dist/resident.js
CHANGED
|
@@ -1,34 +1,32 @@
|
|
|
1
1
|
import { once } from 'node:events';
|
|
2
|
+
import { watch } from 'node:fs';
|
|
2
3
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { createServer } from 'node:http';
|
|
4
|
-
import { extname, join } from 'node:path';
|
|
5
|
+
import { dirname, extname, join } from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { killDescendants } from './acp.js';
|
|
7
7
|
import { Agents } from './agents.js';
|
|
8
8
|
import { house } from './api.js';
|
|
9
9
|
import { received } from './bridge.js';
|
|
10
10
|
import { Conversations } from './conversations.js';
|
|
11
11
|
import { LocalCopies } from './copies.js';
|
|
12
|
-
import { agentBase,
|
|
12
|
+
import { agentBase, enrolled, kitHome } from './home.js';
|
|
13
|
+
import { killDescendants } from './scope.js';
|
|
14
|
+
import { quoted } from './shell.js';
|
|
13
15
|
import { SignIns } from './sign-in.js';
|
|
14
16
|
import { placeSkillSet } from './skills.js';
|
|
15
17
|
import { holdStream } from './stream.js';
|
|
16
18
|
import { Updates } from './update.js';
|
|
19
|
+
import { codexLauncher } from './work.js';
|
|
17
20
|
function report(error) {
|
|
18
21
|
process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
19
22
|
}
|
|
20
23
|
function logged(work) {
|
|
21
24
|
work.catch(report);
|
|
22
25
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const main = fileURLToPath(new URL(`./git-main${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
|
|
28
|
-
await mkdir(join(kitHome(), 'shim'), { recursive: true });
|
|
29
|
-
await writeFile(join(kitHome(), 'shim', 'git'), `#!/bin/sh\nexec ${quoted(process.execPath)} ${quoted(main)} "$@"\n`, {
|
|
30
|
-
mode: 0o755,
|
|
31
|
-
});
|
|
26
|
+
async function installShim(path, module) {
|
|
27
|
+
const main = fileURLToPath(new URL(`./${module}${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
|
|
28
|
+
await mkdir(dirname(path), { recursive: true });
|
|
29
|
+
await writeFile(path, `#!/bin/sh\nexec ${quoted(process.execPath)} ${quoted(main)} "$@"\n`, { mode: 0o755 });
|
|
32
30
|
}
|
|
33
31
|
async function serveOwner(copies) {
|
|
34
32
|
const socketPath = join(kitHome(), 'owner.sock');
|
|
@@ -50,19 +48,38 @@ async function serveOwner(copies) {
|
|
|
50
48
|
server.listen(socketPath);
|
|
51
49
|
await once(server, 'listening');
|
|
52
50
|
}
|
|
51
|
+
function connected() {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const watcher = watch(kitHome(), () => {
|
|
54
|
+
if (!enrolled())
|
|
55
|
+
return;
|
|
56
|
+
watcher.close();
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
if (!enrolled()) {
|
|
60
|
+
process.stderr.write('kit: this Environment is not connected; run kit login\n');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
watcher.close();
|
|
64
|
+
resolve();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
53
67
|
export async function resident() {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
await connected();
|
|
69
|
+
watch(kitHome(), () => {
|
|
70
|
+
if (enrolled())
|
|
71
|
+
return;
|
|
72
|
+
killDescendants();
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
|
59
75
|
await placeSkillSet();
|
|
60
76
|
await mkdir(agentBase(), { recursive: true });
|
|
61
77
|
process.env.HOUSE_KIT_RESIDENT = String(process.pid);
|
|
62
78
|
const agents = new Agents(house);
|
|
63
79
|
const copies = new LocalCopies(house);
|
|
64
80
|
await copies.load();
|
|
65
|
-
await installShim();
|
|
81
|
+
await installShim(join(kitHome(), 'shim', 'git'), 'git-main');
|
|
82
|
+
await installShim(codexLauncher(), 'codex-main');
|
|
66
83
|
await serveOwner(copies);
|
|
67
84
|
let stream = null;
|
|
68
85
|
let reported = false;
|
|
@@ -101,6 +118,8 @@ export async function resident() {
|
|
|
101
118
|
logged(copies.select());
|
|
102
119
|
},
|
|
103
120
|
frame: (arrived) => {
|
|
121
|
+
if (arrived.type === 'watching')
|
|
122
|
+
conversations.watching(arrived.watching === true);
|
|
104
123
|
if (arrived.type === 'work_available' && arrived.subject === 'agents')
|
|
105
124
|
logged(agents.refresh());
|
|
106
125
|
if (arrived.type === 'work_available' && arrived.subject === 'local_copy')
|
package/dist/scope.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, 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
|
+
function failure(error) {
|
|
8
|
+
return error.code ?? '';
|
|
9
|
+
}
|
|
10
|
+
function listed(environment) {
|
|
11
|
+
const found = new Map();
|
|
12
|
+
const output = execFileSync('ps', [environment ? '-AEww' : '-Aww', '-o', 'pid=,ppid=,sess=,command='], {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
maxBuffer: LISTING_BYTES,
|
|
15
|
+
});
|
|
16
|
+
for (const line of output.split('\n')) {
|
|
17
|
+
const fields = /^\s*(\d+)\s+(\d+)\s+(\S+)\s?(.*)$/.exec(line);
|
|
18
|
+
if (fields !== null)
|
|
19
|
+
found.set(Number(fields[1]), { parent: Number(fields[2]), session: fields[3], command: fields[4] });
|
|
20
|
+
}
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
function read(pid) {
|
|
24
|
+
try {
|
|
25
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
26
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
27
|
+
const command = readFileSync(`/proc/${pid}/cmdline`, 'utf8').replaceAll('\0', ' ');
|
|
28
|
+
return { parent: Number(fields[1]), session: fields[3], command };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (GONE.has(failure(error)))
|
|
32
|
+
return null;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function processes() {
|
|
37
|
+
if (process.platform === 'darwin')
|
|
38
|
+
return listed(false);
|
|
39
|
+
const found = new Map();
|
|
40
|
+
for (const entry of readdirSync('/proc')) {
|
|
41
|
+
const running = /^\d+$/.test(entry) ? read(entry) : null;
|
|
42
|
+
if (running !== null)
|
|
43
|
+
found.set(Number(entry), running);
|
|
44
|
+
}
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
function descendants(roots, table = processes()) {
|
|
48
|
+
const children = new Map();
|
|
49
|
+
for (const [pid, { parent }] of table)
|
|
50
|
+
children.set(parent, [...(children.get(parent) ?? []), pid]);
|
|
51
|
+
const tree = [];
|
|
52
|
+
const pending = roots.flatMap((root) => children.get(root) ?? []);
|
|
53
|
+
while (pending.length > 0) {
|
|
54
|
+
const pid = pending.pop();
|
|
55
|
+
tree.push(pid);
|
|
56
|
+
pending.push(...(children.get(pid) ?? []));
|
|
57
|
+
}
|
|
58
|
+
return tree;
|
|
59
|
+
}
|
|
60
|
+
function marked(marker, doubted) {
|
|
61
|
+
if (process.platform === 'darwin') {
|
|
62
|
+
return [...listed(true)].filter(([, { command }]) => ` ${command} `.includes(` ${marker} `)).map(([pid]) => pid);
|
|
63
|
+
}
|
|
64
|
+
return readdirSync('/proc')
|
|
65
|
+
.filter((entry) => /^\d+$/.test(entry) && carries(entry, marker, doubted))
|
|
66
|
+
.map(Number);
|
|
67
|
+
}
|
|
68
|
+
function carries(pid, marker, doubted) {
|
|
69
|
+
try {
|
|
70
|
+
return readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0').includes(marker);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (GONE.has(failure(error)))
|
|
74
|
+
return false;
|
|
75
|
+
if (failure(error) !== 'EACCES')
|
|
76
|
+
throw error;
|
|
77
|
+
return doubted && statSync(`/proc/${pid}`, { throwIfNoEntry: false })?.uid === process.getuid();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function killAll(pids) {
|
|
81
|
+
for (const pid of pids) {
|
|
82
|
+
for (const target of [-pid, pid]) {
|
|
83
|
+
try {
|
|
84
|
+
process.kill(target, 'SIGKILL');
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function killTree(child) {
|
|
91
|
+
killAll([child.pid, ...descendants([child.pid])]);
|
|
92
|
+
}
|
|
93
|
+
export function killMarked(marker) {
|
|
94
|
+
const pids = marked(marker, false);
|
|
95
|
+
killAll([...pids, ...descendants(pids)]);
|
|
96
|
+
}
|
|
97
|
+
export function killDescendants() {
|
|
98
|
+
const pids = marked(`HOUSE_KIT_RESIDENT=${process.pid}`, false);
|
|
99
|
+
killAll([...pids, ...descendants([process.pid, ...pids])]);
|
|
100
|
+
}
|
|
101
|
+
function ownGroup() {
|
|
102
|
+
if (process.platform !== 'linux')
|
|
103
|
+
return null;
|
|
104
|
+
try {
|
|
105
|
+
const line = readFileSync('/proc/self/cgroup', 'utf8')
|
|
106
|
+
.split('\n')
|
|
107
|
+
.find((entry) => entry.startsWith('0::'));
|
|
108
|
+
return line === undefined ? null : join(CGROUPS, line.slice(3));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function members(group) {
|
|
115
|
+
const pids = readFileSync(join(group, 'cgroup.procs'), 'utf8').split('\n').filter(Boolean).map(Number);
|
|
116
|
+
for (const entry of readdirSync(group, { withFileTypes: true })) {
|
|
117
|
+
if (entry.isDirectory())
|
|
118
|
+
pids.push(...members(join(group, entry.name)));
|
|
119
|
+
}
|
|
120
|
+
return pids;
|
|
121
|
+
}
|
|
122
|
+
export class Scope {
|
|
123
|
+
marker;
|
|
124
|
+
cli;
|
|
125
|
+
group;
|
|
126
|
+
session = null;
|
|
127
|
+
recorded = null;
|
|
128
|
+
constructor(marker, name, cli) {
|
|
129
|
+
this.marker = marker;
|
|
130
|
+
this.cli = cli;
|
|
131
|
+
const own = ownGroup();
|
|
132
|
+
this.group = own === null ? null : join(own, name);
|
|
133
|
+
try {
|
|
134
|
+
if (this.group !== null)
|
|
135
|
+
mkdirSync(this.group);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
this.group = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
command(command, args) {
|
|
142
|
+
if (this.group === null)
|
|
143
|
+
return [command, args];
|
|
144
|
+
return ['/bin/sh', ['-c', 'echo $$ > "$0/cgroup.procs" 2>/dev/null; exec "$@"', this.group, command, ...args]];
|
|
145
|
+
}
|
|
146
|
+
entered(pid) {
|
|
147
|
+
this.session = process.platform === 'darwin' ? (listed(false).get(pid)?.session ?? null) : String(pid);
|
|
148
|
+
if (this.group === null)
|
|
149
|
+
return;
|
|
150
|
+
const joined = readFileSync(`/proc/${pid}/cgroup`, 'utf8').split('\n').find((entry) => entry.startsWith('0::'));
|
|
151
|
+
if (joined !== undefined && join(CGROUPS, joined.slice(3)) === this.group)
|
|
152
|
+
return;
|
|
153
|
+
this.close();
|
|
154
|
+
this.group = null;
|
|
155
|
+
}
|
|
156
|
+
record() {
|
|
157
|
+
const found = this.found();
|
|
158
|
+
if (found === null)
|
|
159
|
+
return;
|
|
160
|
+
this.recorded = new Set([...found].filter(([, { session }]) => session === this.session).map(([pid]) => pid));
|
|
161
|
+
}
|
|
162
|
+
working() {
|
|
163
|
+
const found = this.found();
|
|
164
|
+
if (found === null)
|
|
165
|
+
return true;
|
|
166
|
+
if (this.recorded === null)
|
|
167
|
+
return found.size > 0;
|
|
168
|
+
return [...found].some(([pid, { session, command }]) => session !== this.session || !(this.recorded.has(pid) || command.startsWith(`${this.cli}-`)));
|
|
169
|
+
}
|
|
170
|
+
empty() {
|
|
171
|
+
return this.found()?.size === 0;
|
|
172
|
+
}
|
|
173
|
+
kill() {
|
|
174
|
+
killMarked(this.marker);
|
|
175
|
+
if (this.group === null)
|
|
176
|
+
return;
|
|
177
|
+
try {
|
|
178
|
+
writeFileSync(join(this.group, 'cgroup.kill'), '1');
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
killAll([...(this.found()?.keys() ?? [])]);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
close() {
|
|
185
|
+
if (this.group === null)
|
|
186
|
+
return;
|
|
187
|
+
try {
|
|
188
|
+
rmdirSync(this.group);
|
|
189
|
+
}
|
|
190
|
+
catch { }
|
|
191
|
+
}
|
|
192
|
+
found() {
|
|
193
|
+
try {
|
|
194
|
+
const table = processes();
|
|
195
|
+
const roots = this.group === null ? marked(this.marker, true) : members(this.group);
|
|
196
|
+
const pids = this.group === null ? [...roots, ...descendants(roots, table)] : roots;
|
|
197
|
+
return new Map(pids.flatMap((pid) => (table.has(pid) ? [[pid, table.get(pid)]] : [])));
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
package/dist/secret-input.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { retryDelay } from './api.js';
|
|
1
|
+
import { finalRefusal, retryDelay } from './api.js';
|
|
2
2
|
export async function holdSecretInput(house, subject, steps, waiting) {
|
|
3
3
|
for (let failures = 0; waiting();) {
|
|
4
4
|
try {
|
|
@@ -10,7 +10,10 @@ export async function holdSecretInput(house, subject, steps, waiting) {
|
|
|
10
10
|
failures = 0;
|
|
11
11
|
}
|
|
12
12
|
catch (error) {
|
|
13
|
+
if (finalRefusal(error))
|
|
14
|
+
return null;
|
|
13
15
|
process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
16
|
+
// A wait that did not reach House is held again on the jittered curve for as long as its asker still waits.
|
|
14
17
|
await new Promise((resolve) => setTimeout(resolve, retryDelay(failures++)));
|
|
15
18
|
}
|
|
16
19
|
}
|
package/dist/shell.js
ADDED