@agentshouse/kit 0.2.1-alpha.9 → 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.
- package/README.md +48 -29
- package/dist/abort.js +9 -0
- package/dist/acp.js +102 -88
- package/dist/agents.js +356 -52
- package/dist/api.js +40 -4
- package/dist/app-git.js +48 -0
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +93 -78
- package/dist/building.js +50 -0
- package/dist/calls.js +71 -0
- package/dist/chatgpt.js +32 -0
- package/dist/chunks.js +49 -0
- package/dist/clis.js +224 -88
- package/dist/codex-main.js +58 -0
- package/dist/connection.js +105 -0
- package/dist/conversations.js +749 -270
- package/dist/copies.js +475 -260
- package/dist/end.js +33 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +546 -0
- package/dist/files.js +140 -17
- package/dist/git-main.js +2 -2
- package/dist/git.js +62 -22
- package/dist/home.js +120 -9
- package/dist/house-cli.js +299 -0
- package/dist/house-main.js +5 -102
- package/dist/instructions.js +17 -18
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -16
- package/dist/login.js +130 -95
- package/dist/logout.js +19 -0
- package/dist/mcp.js +43 -0
- package/dist/operation.js +7 -4
- package/dist/parts.js +82 -0
- package/dist/placement.js +31 -0
- package/dist/refusals.js +53 -0
- package/dist/relay.js +41 -0
- package/dist/resident.js +144 -52
- package/dist/scope.js +214 -0
- package/dist/scripts.js +255 -0
- package/dist/secret-input.js +7 -2
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +52 -32
- package/dist/silence.js +28 -0
- package/dist/skills.js +48 -39
- package/dist/transcription.js +210 -0
- package/dist/translate.js +2 -8
- package/dist/update.js +127 -0
- package/dist/wire.js +40 -0
- package/dist/work.js +346 -0
- package/package.json +21 -24
- package/dist/blocks.js +0 -51
- package/dist/bootstrap.json +0 -28
- package/dist/stream.js +0 -47
- package/how-we-work.md +0 -25
- package/skills/ask-matt/LICENSE +0 -21
- package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
- package/skills/ask-matt/SKILL.md +0 -91
- package/skills/ask-matt/agents/openai.yaml +0 -4
- package/skills/code-review/LICENSE +0 -21
- package/skills/code-review/SKILL.md +0 -89
- package/skills/code-review/agents/openai.yaml +0 -3
- package/skills/codebase-design/DEEPENING.md +0 -37
- package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
- package/skills/codebase-design/LICENSE +0 -21
- package/skills/codebase-design/SKILL.md +0 -116
- package/skills/codebase-design/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/LICENSE +0 -21
- package/skills/diagnosing-bugs/SKILL.md +0 -140
- package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
- package/skills/domain-modeling/ADR-FORMAT.md +0 -47
- package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
- package/skills/domain-modeling/LICENSE +0 -21
- package/skills/domain-modeling/SKILL.md +0 -76
- package/skills/domain-modeling/agents/openai.yaml +0 -3
- package/skills/grill-me/LICENSE +0 -21
- package/skills/grill-me/SKILL.md +0 -8
- package/skills/grill-me/agents/openai.yaml +0 -4
- package/skills/grill-with-docs/LICENSE +0 -21
- package/skills/grill-with-docs/SKILL.md +0 -8
- package/skills/grill-with-docs/agents/openai.yaml +0 -4
- package/skills/grilling/LICENSE +0 -21
- package/skills/grilling/SKILL.md +0 -30
- package/skills/grilling/agents/openai.yaml +0 -3
- package/skills/handoff/LICENSE +0 -21
- package/skills/handoff/SKILL.md +0 -17
- package/skills/handoff/agents/openai.yaml +0 -4
- package/skills/implement/LICENSE +0 -21
- package/skills/implement/SKILL.md +0 -16
- package/skills/implement/agents/openai.yaml +0 -4
- package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
- package/skills/improve-codebase-architecture/LICENSE +0 -21
- package/skills/improve-codebase-architecture/SKILL.md +0 -72
- package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
- package/skills/prototype/LICENSE +0 -21
- package/skills/prototype/LOGIC.md +0 -67
- package/skills/prototype/SKILL.md +0 -28
- package/skills/prototype/UI.md +0 -112
- package/skills/prototype/agents/openai.yaml +0 -3
- package/skills/research/LICENSE +0 -21
- package/skills/research/SKILL.md +0 -14
- package/skills/research/agents/openai.yaml +0 -3
- package/skills/resolving-merge-conflicts/LICENSE +0 -21
- package/skills/resolving-merge-conflicts/SKILL.md +0 -16
- package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
- package/skills/tdd/LICENSE +0 -21
- package/skills/tdd/SKILL.md +0 -40
- package/skills/tdd/agents/openai.yaml +0 -3
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/tests.md +0 -77
- package/skills/teach/GLOSSARY-FORMAT.md +0 -35
- package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
- package/skills/teach/LICENSE +0 -21
- package/skills/teach/MISSION-FORMAT.md +0 -31
- package/skills/teach/RESOURCES-FORMAT.md +0 -32
- package/skills/teach/SKILL.md +0 -141
- package/skills/teach/agents/openai.yaml +0 -4
- package/skills/to-questionnaire/LICENSE +0 -21
- package/skills/to-questionnaire/SKILL.md +0 -55
- package/skills/to-questionnaire/agents/openai.yaml +0 -4
- package/skills/to-spec/LICENSE +0 -21
- package/skills/to-spec/SKILL.md +0 -76
- package/skills/to-spec/agents/openai.yaml +0 -4
- package/skills/to-tickets/LICENSE +0 -21
- package/skills/to-tickets/SKILL.md +0 -106
- package/skills/to-tickets/agents/openai.yaml +0 -4
- package/skills/wait-what/LICENSE +0 -21
- package/skills/wait-what/SKILL.md +0 -8
- package/skills/wait-what/agents/openai.yaml +0 -4
- package/skills/wayfinder/LICENSE +0 -21
- package/skills/wayfinder/SKILL.md +0 -129
- package/skills/wayfinder/agents/openai.yaml +0 -4
- package/skills/writing-for-agents/LICENSE +0 -21
- package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
- package/skills/writing-for-agents/SKILL.md +0 -83
- package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/files.js
CHANGED
|
@@ -1,21 +1,144 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import { mkdir, rename, rm } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
|
+
import { Readable, Transform } from 'node:stream';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
import { parseDocument } from 'yaml';
|
|
8
|
+
import { retryDelay } from './api.js';
|
|
9
|
+
import { Unsettled } from './mcp.js';
|
|
10
|
+
import { relayed } from './relay.js';
|
|
11
|
+
const STALLED_ROUNDS = 8;
|
|
12
|
+
const PART_CHECKSUM = 'x-amz-checksum-sha256';
|
|
13
|
+
const LINK = /\]\((\/rooms\/[^/)\s]+\/[^)\s]+)\)/gu;
|
|
14
|
+
export function linkedPaths(text) {
|
|
15
|
+
return [...new Set(Array.from(text.matchAll(LINK), (link) => link[1]))];
|
|
16
|
+
}
|
|
17
|
+
export function inspectedFile(answer) {
|
|
18
|
+
const document = parseDocument(answer);
|
|
19
|
+
if (document.errors.length > 0)
|
|
20
|
+
return null;
|
|
21
|
+
const inspected = document.toJS();
|
|
22
|
+
if (typeof inspected?.download !== 'string')
|
|
23
|
+
return null;
|
|
24
|
+
return {
|
|
25
|
+
download: inspected.download,
|
|
26
|
+
sha256: String(inspected.sha256),
|
|
27
|
+
media_type: typeof inspected.media_type === 'string' ? inspected.media_type : null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function paused(milliseconds) {
|
|
31
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
32
|
+
}
|
|
33
|
+
export function storeRefusal(act, status) {
|
|
34
|
+
return new Error(`the ${act} answered ${status}; ${act} the file again`);
|
|
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
|
+
}
|
|
52
|
+
export async function uploaded(declared, uploading) {
|
|
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);
|
|
57
|
+
let idle = 0;
|
|
58
|
+
let stalled = 0;
|
|
59
|
+
while ('upload' in answer) {
|
|
60
|
+
const { id, parts } = answer.upload;
|
|
61
|
+
let sent = 0;
|
|
62
|
+
let failure = null;
|
|
63
|
+
for (const { part, url } of parts ?? []) {
|
|
64
|
+
const start = (part - 1) * partBytes;
|
|
65
|
+
try {
|
|
66
|
+
await uploading.send(url, start, Math.min(start + partBytes, uploading.bytes), {
|
|
67
|
+
[PART_CHECKSUM]: digests[part - 1].toString('base64'),
|
|
68
|
+
});
|
|
69
|
+
sent++;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
failure = error;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (sent > 0) {
|
|
77
|
+
idle = 0;
|
|
78
|
+
stalled = 0;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
if (failure !== null && ++stalled >= STALLED_ROUNDS)
|
|
82
|
+
throw failure;
|
|
83
|
+
// A round that sent no part, its store unreachable, is followed by the next finishing call on the jittered curve.
|
|
84
|
+
await paused(retryDelay(idle++));
|
|
85
|
+
}
|
|
86
|
+
answer = await finishing(uploading, id, partSha256);
|
|
87
|
+
}
|
|
88
|
+
return answer;
|
|
89
|
+
}
|
|
90
|
+
async function finishing(uploading, upload, partSha256) {
|
|
91
|
+
for (let unsettled = 1;; unsettled++) {
|
|
92
|
+
try {
|
|
93
|
+
return await uploading.finish(upload, partSha256);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (!(error instanceof Unsettled) || unsettled === STALLED_ROUNDS)
|
|
13
97
|
throw error;
|
|
98
|
+
}
|
|
99
|
+
// A finishing call House did not settle, unreached or refused short of final, is sent again on the jittered curve as a conversation resends it, so a dropped connection or a House restart resumes the upload instead of ending it.
|
|
100
|
+
await paused(retryDelay(unsettled - 1));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function drained(body) {
|
|
104
|
+
for await (const _chunk of body)
|
|
105
|
+
;
|
|
106
|
+
}
|
|
107
|
+
export async function downloaded(address, fetching, destination, name) {
|
|
108
|
+
for (let asked = 1;; asked++) {
|
|
109
|
+
const { download, sha256 } = await address();
|
|
110
|
+
const answer = await fetching(download);
|
|
111
|
+
if (answer.status === 403 && asked === 1) {
|
|
112
|
+
await drained(answer.body);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (answer.status !== 200) {
|
|
116
|
+
await drained(answer.body);
|
|
117
|
+
throw storeRefusal('download', answer.status);
|
|
118
|
+
}
|
|
119
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
120
|
+
const partial = join(dirname(destination), `.${basename(destination)}.${randomUUID()}`);
|
|
121
|
+
const digest = createHash('sha256');
|
|
122
|
+
const hashed = new Transform({
|
|
123
|
+
transform(chunk, _encoding, done) {
|
|
124
|
+
digest.update(chunk);
|
|
125
|
+
done(null, chunk);
|
|
126
|
+
},
|
|
14
127
|
});
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
128
|
+
try {
|
|
129
|
+
await pipeline(answer.body, hashed, createWriteStream(partial, { flags: 'wx' }));
|
|
130
|
+
if (digest.digest('hex') !== sha256)
|
|
131
|
+
throw new Error(`${name} did not arrive whole; download it again`);
|
|
132
|
+
await rename(partial, destination);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
await rm(partial, { force: true });
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
19
139
|
}
|
|
20
|
-
|
|
140
|
+
}
|
|
141
|
+
export async function fetched(url, signal) {
|
|
142
|
+
const answer = await fetch(relayed(url), { signal });
|
|
143
|
+
return { status: answer.status, body: answer.body === null ? Readable.from([]) : Readable.fromWeb(answer.body) };
|
|
21
144
|
}
|
package/dist/git-main.js
CHANGED
|
@@ -4,7 +4,7 @@ import { accessSync, constants, realpathSync, statSync } from 'node:fs';
|
|
|
4
4
|
import { request } from 'node:http';
|
|
5
5
|
import { constants as system } from 'node:os';
|
|
6
6
|
import { delimiter, dirname, join } from 'node:path';
|
|
7
|
-
import {
|
|
7
|
+
import { localCopies } from './git.js';
|
|
8
8
|
import { kitHome } from './home.js';
|
|
9
9
|
const VALUED = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source', '--super-prefix']);
|
|
10
10
|
const READS = new Set([
|
|
@@ -125,7 +125,7 @@ async function observed(git, args) {
|
|
|
125
125
|
return delegated(git, args);
|
|
126
126
|
const located = captured(git, [...globals, 'rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
127
127
|
const [common, worktree] = located?.split('\n') ?? [];
|
|
128
|
-
if (common === undefined || worktree === undefined || !dirname(common).startsWith(`${
|
|
128
|
+
if (common === undefined || worktree === undefined || !dirname(common).startsWith(`${localCopies()}/`)) {
|
|
129
129
|
return delegated(git, args);
|
|
130
130
|
}
|
|
131
131
|
const listing = [...globals, '-C', worktree, 'ls-files', '--stage', '-z'];
|
package/dist/git.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { isUtf8 } from 'node:buffer';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import {
|
|
4
|
-
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { workspaceRoot } from './home.js';
|
|
6
|
+
import { childEnvironment } from './relay.js';
|
|
7
|
+
export function localCopies() {
|
|
8
|
+
return join(workspaceRoot(), 'local-copies');
|
|
9
|
+
}
|
|
5
10
|
export const HOUSE_REF = 'refs/house/received';
|
|
6
|
-
const COMMITTER = '
|
|
11
|
+
const COMMITTER = 'Kit <house-kit@localhost>';
|
|
7
12
|
export function run(cwd, args, input, env = {}) {
|
|
8
13
|
return new Promise((resolve, reject) => {
|
|
9
|
-
const child = spawn('git', args, { cwd, env: { ...
|
|
14
|
+
const child = spawn('git', args, { cwd, env: { ...childEnvironment(), ...env } });
|
|
10
15
|
const stdout = [];
|
|
11
16
|
let stderr = '';
|
|
12
17
|
child.stdout.on('data', (chunk) => stdout.push(chunk));
|
|
@@ -44,14 +49,15 @@ export async function tree(repository, commit) {
|
|
|
44
49
|
}
|
|
45
50
|
return entries;
|
|
46
51
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
async function blob(repository, object) {
|
|
53
|
+
return (await run(repository, ['cat-file', 'blob', object])).stdout;
|
|
54
|
+
}
|
|
55
|
+
export async function blobText(repository, object) {
|
|
56
|
+
return (await blob(repository, object)).toString('utf8');
|
|
57
|
+
}
|
|
58
|
+
export async function isText(repository, object, maxBytes) {
|
|
59
|
+
const bytes = await blob(repository, object);
|
|
60
|
+
return bytes.length <= maxBytes && isUtf8(bytes) && !bytes.includes(0);
|
|
55
61
|
}
|
|
56
62
|
export function blobId(content) {
|
|
57
63
|
const bytes = Buffer.from(content, 'utf8');
|
|
@@ -64,18 +70,52 @@ function data(text) {
|
|
|
64
70
|
const bytes = Buffer.from(text, 'utf8');
|
|
65
71
|
return [Buffer.from(`data ${bytes.length}\n`), bytes, Buffer.from('\n')];
|
|
66
72
|
}
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
function written(stream, bytes) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
stream.write(bytes, (error) => (error ? reject(error) : resolve()));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function imported(stream, commits) {
|
|
79
|
+
await written(stream, Buffer.from('feature done\n'));
|
|
69
80
|
for (const [index, commit] of commits.entries()) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
await written(stream, Buffer.concat([
|
|
82
|
+
Buffer.from(`commit ${HOUSE_REF}\nmark :${index + 1}\ncommitter ${COMMITTER} ${Math.floor(Date.now() / 1000)} +0000\n`),
|
|
83
|
+
...data(commit.message),
|
|
84
|
+
...(commit.from === null ? [] : [Buffer.from(`from ${commit.from}\n`)]),
|
|
85
|
+
...(commit.merge === undefined ? [] : [Buffer.from(`merge ${commit.merge}\n`)]),
|
|
86
|
+
...(commit.replaces === true ? [Buffer.from('deleteall\n')] : []),
|
|
87
|
+
]));
|
|
88
|
+
for await (const [path, content] of commit.files) {
|
|
89
|
+
await written(stream, content === null
|
|
90
|
+
? Buffer.from(`D ${quoted(path)}\n`)
|
|
91
|
+
: Buffer.concat([Buffer.from(`M 100644 inline ${quoted(path)}\n`), ...data(content)]));
|
|
76
92
|
}
|
|
77
|
-
stream
|
|
93
|
+
await written(stream, Buffer.from('\n'));
|
|
94
|
+
}
|
|
95
|
+
await written(stream, Buffer.from('done\n'));
|
|
96
|
+
}
|
|
97
|
+
export async function importCommits(repository, commits) {
|
|
98
|
+
const child = spawn('git', ['fast-import', '--quiet', '--force'], { cwd: repository, env: childEnvironment() });
|
|
99
|
+
let stderr = '';
|
|
100
|
+
child.stdout.resume();
|
|
101
|
+
child.stderr.on('data', (chunk) => {
|
|
102
|
+
stderr += chunk.toString('utf8');
|
|
103
|
+
});
|
|
104
|
+
child.stdin.on('error', () => undefined);
|
|
105
|
+
const closed = new Promise((resolve, reject) => {
|
|
106
|
+
child.on('error', reject);
|
|
107
|
+
child.on('close', resolve);
|
|
108
|
+
});
|
|
109
|
+
try {
|
|
110
|
+
await imported(child.stdin, commits);
|
|
111
|
+
child.stdin.end();
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
child.stdin.destroy();
|
|
115
|
+
await closed;
|
|
116
|
+
throw error.code === 'EPIPE' ? new Error(`git fast-import failed: ${stderr.trim()}`) : error;
|
|
78
117
|
}
|
|
79
|
-
|
|
118
|
+
if ((await closed) !== 0)
|
|
119
|
+
throw new Error(`git fast-import failed: ${stderr.trim()}`);
|
|
80
120
|
return revision(repository, HOUSE_REF);
|
|
81
121
|
}
|
package/dist/home.js
CHANGED
|
@@ -1,10 +1,68 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { accessSync, chmodSync, constants, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir, tmpdir } from 'node:os';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { selectRelay } from './relay.js';
|
|
4
7
|
const ENROLMENT = 'credential.json';
|
|
8
|
+
const DISCONNECTED = 'disconnected.json';
|
|
5
9
|
const CONFIGURATION = 'kit.json';
|
|
10
|
+
const OWN_AGENT = 'own-agent.json';
|
|
11
|
+
let defaulted;
|
|
12
|
+
function writable(directory) {
|
|
13
|
+
try {
|
|
14
|
+
accessSync(directory, constants.W_OK | constants.X_OK);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function privateDirectory(path) {
|
|
22
|
+
try {
|
|
23
|
+
mkdirSync(path, { mode: 0o700 });
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code !== 'EEXIST')
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
const found = lstatSync(path);
|
|
30
|
+
if (found.isSymbolicLink())
|
|
31
|
+
throw new Error(`${path} is a symbolic link`);
|
|
32
|
+
if (!found.isDirectory())
|
|
33
|
+
throw new Error(`${path} is not a directory`);
|
|
34
|
+
if (found.uid !== process.geteuid())
|
|
35
|
+
throw new Error(`${path} is not owned by this User`);
|
|
36
|
+
chmodSync(path, 0o700);
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
function ownTemporaryHome() {
|
|
40
|
+
return privateDirectory(join(tmpdir(), `house-kit-${process.getuid()}`));
|
|
41
|
+
}
|
|
42
|
+
function defaultHome() {
|
|
43
|
+
const home = join(homedir(), '.house-kit');
|
|
44
|
+
return existsSync(home) || writable(homedir()) ? home : ownTemporaryHome();
|
|
45
|
+
}
|
|
6
46
|
export function kitHome() {
|
|
7
|
-
return process.env.HOUSE_KIT_HOME
|
|
47
|
+
return process.env.HOUSE_KIT_HOME || (defaulted ??= defaultHome());
|
|
48
|
+
}
|
|
49
|
+
export function socketDirectory() {
|
|
50
|
+
const sockets = privateDirectory(join(realpathSync('/tmp'), `hk-${process.geteuid()}`));
|
|
51
|
+
const home = kitHome();
|
|
52
|
+
const physical = existsSync(home) ? realpathSync(home) : resolve(home);
|
|
53
|
+
return privateDirectory(join(sockets, createHash('sha256').update(physical).digest('hex').slice(0, 12)));
|
|
54
|
+
}
|
|
55
|
+
export function ownerSocket() {
|
|
56
|
+
return join(socketDirectory(), 'owner.sock');
|
|
57
|
+
}
|
|
58
|
+
export function ownerLock() {
|
|
59
|
+
return join(socketDirectory(), 'owner.lock');
|
|
60
|
+
}
|
|
61
|
+
export function workspaceRoot() {
|
|
62
|
+
return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
|
|
63
|
+
}
|
|
64
|
+
export function agentBase() {
|
|
65
|
+
return join(workspaceRoot(), 'agents');
|
|
8
66
|
}
|
|
9
67
|
export async function readHome(name) {
|
|
10
68
|
try {
|
|
@@ -19,15 +77,68 @@ export async function readHome(name) {
|
|
|
19
77
|
export async function writeHome(name, value) {
|
|
20
78
|
const home = kitHome();
|
|
21
79
|
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
22
|
-
const temporary = join(home, `${name}.${
|
|
80
|
+
const temporary = join(home, `${name}.${randomUUID()}`);
|
|
23
81
|
await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
24
82
|
await rename(temporary, join(home, name));
|
|
25
83
|
}
|
|
26
|
-
export function readEnrolment() {
|
|
27
|
-
|
|
84
|
+
export async function readEnrolment() {
|
|
85
|
+
const enrolment = await readHome(ENROLMENT);
|
|
86
|
+
if (enrolment !== null)
|
|
87
|
+
selectRelay(process.env.HOUSE_KIT_RELAY || enrolment.relay);
|
|
88
|
+
return enrolment;
|
|
89
|
+
}
|
|
90
|
+
export async function writeEnrolment(enrolment) {
|
|
91
|
+
await writeHome(ENROLMENT, enrolment);
|
|
92
|
+
await rm(join(kitHome(), DISCONNECTED), { force: true });
|
|
93
|
+
}
|
|
94
|
+
export function readDisconnected() {
|
|
95
|
+
return readHome(DISCONNECTED);
|
|
96
|
+
}
|
|
97
|
+
export function forgetDisconnected() {
|
|
98
|
+
return rm(join(kitHome(), DISCONNECTED), { force: true });
|
|
99
|
+
}
|
|
100
|
+
export async function forgetEnrolment({ house, environment }) {
|
|
101
|
+
await writeHome(DISCONNECTED, { house, environment });
|
|
102
|
+
await rm(join(kitHome(), ENROLMENT), { force: true });
|
|
103
|
+
}
|
|
104
|
+
export async function forgetRejected(rejected) {
|
|
105
|
+
const held = await readHome(ENROLMENT);
|
|
106
|
+
if (held?.credential !== rejected)
|
|
107
|
+
return;
|
|
108
|
+
await writeHome(DISCONNECTED, { house: held.house, environment: held.environment });
|
|
109
|
+
const enrolment = join(kitHome(), ENROLMENT);
|
|
110
|
+
const taken = `${enrolment}.${process.pid}`;
|
|
111
|
+
try {
|
|
112
|
+
renameSync(enrolment, taken);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error.code === 'ENOENT')
|
|
116
|
+
return;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
if (JSON.parse(readFileSync(taken, 'utf8')).credential !== rejected) {
|
|
120
|
+
try {
|
|
121
|
+
linkSync(taken, enrolment);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error.code !== 'EEXIST')
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
rmSync(join(kitHome(), DISCONNECTED), { force: true });
|
|
128
|
+
}
|
|
129
|
+
rmSync(taken);
|
|
130
|
+
}
|
|
131
|
+
export function enrolled() {
|
|
132
|
+
return existsSync(join(kitHome(), ENROLMENT));
|
|
133
|
+
}
|
|
134
|
+
export function readOwnAgent() {
|
|
135
|
+
return readHome(OWN_AGENT);
|
|
136
|
+
}
|
|
137
|
+
export function writeOwnAgent(ownAgent) {
|
|
138
|
+
return writeHome(OWN_AGENT, ownAgent);
|
|
28
139
|
}
|
|
29
|
-
export function
|
|
30
|
-
return
|
|
140
|
+
export function forgetOwnAgent() {
|
|
141
|
+
return rm(join(kitHome(), OWN_AGENT), { force: true });
|
|
31
142
|
}
|
|
32
143
|
export async function readConfiguration() {
|
|
33
144
|
return { skills: (await readHome(CONFIGURATION))?.skills !== false };
|