@agentshouse/kit 0.1.0-alpha.9 → 0.2.1-alpha.1

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 (93) hide show
  1. package/README.md +14 -1740
  2. package/dist/acp.js +120 -0
  3. package/dist/agents.js +90 -0
  4. package/dist/api.js +45 -0
  5. package/dist/blocks.js +51 -0
  6. package/dist/bridge.js +124 -0
  7. package/dist/clis.js +110 -0
  8. package/dist/conversations.js +538 -0
  9. package/dist/files.js +21 -0
  10. package/dist/home.js +24 -0
  11. package/dist/house-main.js +92 -0
  12. package/dist/instructions.js +28 -0
  13. package/dist/kit-main.js +16 -0
  14. package/dist/login.js +112 -0
  15. package/dist/resident.js +66 -0
  16. package/dist/secret-input.js +16 -0
  17. package/dist/sign-in.js +83 -0
  18. package/dist/stream.js +47 -0
  19. package/package.json +34 -10
  20. package/bin/connect-linux.sh +0 -134
  21. package/bin/kit.js +0 -1023
  22. package/bootstrap.json +0 -17
  23. package/src/acp.js +0 -99
  24. package/src/adapter-mcp.js +0 -44
  25. package/src/adapter.js +0 -252
  26. package/src/agent-logins.js +0 -65
  27. package/src/agents.js +0 -504
  28. package/src/apply.js +0 -29
  29. package/src/attach.js +0 -52
  30. package/src/binding.js +0 -90
  31. package/src/blocks.js +0 -156
  32. package/src/bridge.js +0 -449
  33. package/src/bytes.js +0 -89
  34. package/src/channel.js +0 -37
  35. package/src/command.js +0 -109
  36. package/src/configuration.js +0 -465
  37. package/src/connector-login.js +0 -91
  38. package/src/connector.js +0 -107
  39. package/src/continuation.js +0 -61
  40. package/src/contract.js +0 -363
  41. package/src/conversation-originals.js +0 -143
  42. package/src/custom/authorization.js +0 -63
  43. package/src/declaration.js +0 -35
  44. package/src/delivery.js +0 -191
  45. package/src/directory.js +0 -102
  46. package/src/draft.js +0 -89
  47. package/src/google/calendar.js +0 -177
  48. package/src/google/declared.js +0 -146
  49. package/src/google/drive.js +0 -435
  50. package/src/google/gateway.js +0 -166
  51. package/src/google/gmail.js +0 -249
  52. package/src/google/id.js +0 -1
  53. package/src/google/oauth.js +0 -273
  54. package/src/google/source.js +0 -25
  55. package/src/hook.js +0 -376
  56. package/src/host.js +0 -121
  57. package/src/house-main.js +0 -79
  58. package/src/house.js +0 -1090
  59. package/src/inbound.js +0 -175
  60. package/src/instance/google.js +0 -1
  61. package/src/kit-work-stream.js +0 -84
  62. package/src/launcher-main.js +0 -19
  63. package/src/launcher.js +0 -276
  64. package/src/local-files.js +0 -113
  65. package/src/login.js +0 -274
  66. package/src/mcp.js +0 -71
  67. package/src/migrator.js +0 -248
  68. package/src/mount.js +0 -582
  69. package/src/operation.js +0 -20
  70. package/src/original.js +0 -631
  71. package/src/peer.js +0 -124
  72. package/src/plan.js +0 -57
  73. package/src/question.js +0 -316
  74. package/src/refusal.js +0 -79
  75. package/src/repository.js +0 -521
  76. package/src/session-bin/house +0 -2
  77. package/src/settlement.js +0 -57
  78. package/src/setup.js +0 -190
  79. package/src/source-originals.js +0 -116
  80. package/src/source.js +0 -286
  81. package/src/starter-set/grilling/LICENSE +0 -21
  82. package/src/starter-set/grilling/SKILL.md +0 -30
  83. package/src/starter-set/grilling/agents/openai.yaml +0 -3
  84. package/src/starter-set.js +0 -17
  85. package/src/state.js +0 -157
  86. package/src/sync-library.js +0 -293
  87. package/src/sync-state.js +0 -120
  88. package/src/sync.js +0 -482
  89. package/src/telegram.js +0 -1045
  90. package/src/terminal.js +0 -57
  91. package/src/tmux.js +0 -119
  92. package/src/worker.js +0 -925
  93. package/src/workspace.js +0 -500
@@ -0,0 +1,28 @@
1
+ const HOUSE_LINE = 'Work with House through the `house` CLI: run `house --help`.';
2
+ const FILE_LINE = 'To give the User a file, upload it with `house upload_attachment` and link it in your answer.';
3
+ const HOW_WE_WORK = '/private/library/how-we-work.md';
4
+ const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
5
+ const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
6
+ async function howWeWork(bridge) {
7
+ let document = '';
8
+ for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
9
+ const read = await bridge.tool('shell', { command });
10
+ const text = read.content.map((content) => content.text).join('\n');
11
+ const lines = text.split('\n');
12
+ let at = lines[5]?.startsWith('authority: ') ? 6 : 5;
13
+ if (read.isError === true || lines[2] !== 'exit: 0') {
14
+ const refusal = read.isError === true ? text : lines.slice(at).join('\n').replace(/^stderr: /gm, '');
15
+ if (UNREADABLE.test(refusal))
16
+ return '';
17
+ throw new Error(refusal);
18
+ }
19
+ const cut = lines[3] === 'truncation: egress' ? CUT.exec(lines[at++] ?? '') : null;
20
+ const shown = Buffer.from(`${lines.slice(at).join('\n')}\n`);
21
+ document += (cut === null ? shown : shown.subarray(0, Number(cut[1]))).toString();
22
+ command = cut?.[2];
23
+ }
24
+ return document.replace(/\n$/, '');
25
+ }
26
+ export async function instructions(bridge, base) {
27
+ return [`${HOUSE_LINE}\n${FILE_LINE}`, base, await howWeWork(bridge)].filter((part) => part !== '').join('\n\n');
28
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { login } from './login.js';
3
+ import { resident } from './resident.js';
4
+ const [command, ...argv] = process.argv.slice(2);
5
+ async function run() {
6
+ if (command === 'login')
7
+ return login(argv);
8
+ if (command === 'resident')
9
+ return resident();
10
+ process.stderr.write('usage: kit <login|resident>\n');
11
+ process.exitCode = 2;
12
+ }
13
+ run().catch((error) => {
14
+ process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
15
+ process.exitCode = 1;
16
+ });
package/dist/login.js ADDED
@@ -0,0 +1,112 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { createServer } from 'node:http';
3
+ import { hostname } from 'node:os';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { parseArgs } from 'node:util';
6
+ import { post } from './api.js';
7
+ import { readEnrolment, writeEnrolment } from './home.js';
8
+ const DEFAULT_HOUSE = 'https://agents.house';
9
+ function listen() {
10
+ return new Promise((resolve, reject) => {
11
+ let deliver = () => undefined;
12
+ let held = null;
13
+ const code = new Promise((settle) => {
14
+ deliver = settle;
15
+ });
16
+ const server = createServer((request, response) => {
17
+ const received = new URL(request.url ?? '/', 'http://127.0.0.1').searchParams.get('code');
18
+ if (received === null || held !== null) {
19
+ response.writeHead(404).end();
20
+ return;
21
+ }
22
+ held = response;
23
+ deliver(received);
24
+ });
25
+ const close = () => {
26
+ server.close();
27
+ server.closeAllConnections();
28
+ };
29
+ server.once('error', reject);
30
+ server.listen(0, '127.0.0.1', () => {
31
+ const address = server.address();
32
+ if (address === null || typeof address === 'string') {
33
+ reject(new Error('the login callback has no port'));
34
+ return;
35
+ }
36
+ resolve({
37
+ uri: `http://127.0.0.1:${address.port}/callback`,
38
+ code,
39
+ answer: (text) => new Promise((written) => {
40
+ if (held === null || held.destroyed || !held.socket || held.socket.destroyed)
41
+ return written();
42
+ held.once('close', () => written());
43
+ held.writeHead(200, { 'content-type': 'text/plain; charset=utf-8', connection: 'close' }).end(`${text}\n`);
44
+ }),
45
+ close,
46
+ });
47
+ });
48
+ });
49
+ }
50
+ async function typed(question) {
51
+ const reader = createInterface({ input: process.stdin, output: process.stdout });
52
+ try {
53
+ return (await reader.question(question)).trim();
54
+ }
55
+ finally {
56
+ reader.close();
57
+ }
58
+ }
59
+ export async function login(argv) {
60
+ const { values } = parseArgs({
61
+ args: argv,
62
+ options: {
63
+ house: { type: 'string' },
64
+ environment: { type: 'string' },
65
+ manual: { type: 'boolean', default: false },
66
+ },
67
+ });
68
+ const enrolled = await readEnrolment();
69
+ const house = values.house ?? enrolled?.house ?? DEFAULT_HOUSE;
70
+ const environment = values.environment ?? (enrolled?.house === house ? enrolled.environment : undefined);
71
+ const verifier = randomBytes(32).toString('base64url');
72
+ const started = {
73
+ ...(environment === undefined
74
+ ? { act: 'new_environment', label: hostname() }
75
+ : { act: 'existing_environment', environment_id: environment }),
76
+ code_challenge: createHash('sha256').update(verifier).digest('base64url'),
77
+ };
78
+ const exchange = async (code) => {
79
+ const exchanged = await post(house, '/kit/token', { code, code_verifier: verifier });
80
+ await writeEnrolment({ house, environment: exchanged.environment, credential: exchanged.credential });
81
+ process.stdout.write(`Environment ${exchanged.environment} is connected to ${house}.\n`);
82
+ };
83
+ if (values.manual) {
84
+ const { start_url } = await post(house, '/kit', {
85
+ ...started,
86
+ presentation: 'manual',
87
+ });
88
+ process.stdout.write(`Open this link and confirm: ${start_url}\n`);
89
+ await exchange(await typed('Code: '));
90
+ return;
91
+ }
92
+ const callback = await listen();
93
+ try {
94
+ const { start_url } = await post(house, '/kit', {
95
+ ...started,
96
+ presentation: 'loopback',
97
+ loopback_uri: callback.uri,
98
+ });
99
+ process.stdout.write(`Open this link and confirm: ${start_url}\n`);
100
+ try {
101
+ await exchange(await callback.code);
102
+ }
103
+ catch (error) {
104
+ await callback.answer(`This Environment was not connected: ${error.message}`);
105
+ throw error;
106
+ }
107
+ await callback.answer('This Environment is connected. You can close this tab.');
108
+ }
109
+ finally {
110
+ callback.close();
111
+ }
112
+ }
@@ -0,0 +1,66 @@
1
+ import { killDescendants } from './acp.js';
2
+ import { Agents } from './agents.js';
3
+ import { house } from './api.js';
4
+ import { Conversations } from './conversations.js';
5
+ import { readEnrolment } from './home.js';
6
+ import { SignIns } from './sign-in.js';
7
+ import { holdStream } from './stream.js';
8
+ function report(error) {
9
+ process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
10
+ }
11
+ function logged(work) {
12
+ work.catch(report);
13
+ }
14
+ export async function resident() {
15
+ if ((await readEnrolment()) === null) {
16
+ process.stderr.write('kit: this Environment is not enrolled; run kit login\n');
17
+ process.exitCode = 1;
18
+ return;
19
+ }
20
+ process.env.HOUSE_KIT_RESIDENT = String(process.pid);
21
+ const agents = new Agents(house);
22
+ let stream = null;
23
+ let reported = false;
24
+ const idle = () => conversations.idle() && signIns.idle();
25
+ const changed = () => {
26
+ if (!idle()) {
27
+ reported = false;
28
+ }
29
+ else if (!reported) {
30
+ reported = true;
31
+ logged(house.deliver('/kit/idle', {}, idle));
32
+ }
33
+ };
34
+ const conversations = new Conversations({
35
+ house,
36
+ agents,
37
+ send: (frame) => stream?.send(frame) ?? false,
38
+ changed,
39
+ });
40
+ const signIns = new SignIns(house, agents, changed);
41
+ for (const signal of ['SIGTERM', 'SIGINT']) {
42
+ process.once(signal, () => {
43
+ killDescendants();
44
+ process.kill(process.pid, signal);
45
+ });
46
+ }
47
+ await house.deliver('/kit/restarted', {}).catch(report);
48
+ logged(agents.refresh());
49
+ stream = holdStream({
50
+ opened: () => {
51
+ conversations.opened();
52
+ changed();
53
+ },
54
+ frame: (received) => {
55
+ if (received.type === 'work_available' && received.subject === 'agents')
56
+ logged(agents.refresh());
57
+ if (received.type === 'input') {
58
+ reported = false;
59
+ if (received.kind === 'sign_in')
60
+ signIns.start(received);
61
+ else
62
+ conversations.input(received);
63
+ }
64
+ },
65
+ });
66
+ }
@@ -0,0 +1,16 @@
1
+ export async function holdSecretInput(house, subject, steps, waiting) {
2
+ while (waiting()) {
3
+ try {
4
+ const held = await house.post(`/kit/secret-input/${subject}`, { steps });
5
+ if (held.outcome === 'collected')
6
+ return held.content;
7
+ if (held.release !== 'held')
8
+ return null;
9
+ }
10
+ catch (error) {
11
+ process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
12
+ await new Promise((resolve) => setTimeout(resolve, 1000));
13
+ }
14
+ }
15
+ return null;
16
+ }
@@ -0,0 +1,83 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createInterface } from 'node:readline';
3
+ import { killTree } from './acp.js';
4
+ import { CLIS, cliCommand } from './clis.js';
5
+ import { holdSecretInput } from './secret-input.js';
6
+ const ESCAPE = /\u001b\[[\d;]*m/g;
7
+ const LINK = /https:\/\/\S+/;
8
+ const CODE = /^[A-Z0-9]+-[A-Z0-9]+$/;
9
+ function logged(error) {
10
+ process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
11
+ }
12
+ export class SignIns {
13
+ house;
14
+ agents;
15
+ changed;
16
+ running = new Set();
17
+ constructor(house, agents, changed) {
18
+ this.house = house;
19
+ this.agents = agents;
20
+ this.changed = changed;
21
+ }
22
+ idle() {
23
+ return this.running.size === 0;
24
+ }
25
+ start(input) {
26
+ if (this.running.has(input.input_id))
27
+ return;
28
+ this.running.add(input.input_id);
29
+ this.changed();
30
+ this.login(input)
31
+ .then(() => this.agents.report())
32
+ .then(() => this.house.deliver(`/kit/inputs/${input.input_id}/ack`, {}))
33
+ .catch(logged)
34
+ .finally(() => {
35
+ this.running.delete(input.input_id);
36
+ this.changed();
37
+ });
38
+ }
39
+ async login(input) {
40
+ await this.agents.installed();
41
+ const login = CLIS[input.cli].login;
42
+ const child = spawn(cliCommand(input.cli), login.args, { stdio: ['pipe', 'pipe', 'pipe'], detached: true });
43
+ const running = () => child.exitCode === null && child.signalCode === null;
44
+ let link;
45
+ let code;
46
+ let last = '';
47
+ let held = false;
48
+ const read = (line) => {
49
+ const plain = line.replace(ESCAPE, '').trim();
50
+ if (plain === '')
51
+ return;
52
+ last = plain;
53
+ link ??= LINK.exec(plain)?.[0];
54
+ if (CODE.test(plain))
55
+ code ??= plain;
56
+ const step = login.code === 'collect'
57
+ ? { kind: 'collect', label: 'Code shown after you sign in', name: 'code' }
58
+ : code === undefined
59
+ ? undefined
60
+ : { kind: 'show', label: 'One-time code', text: code };
61
+ if (held || link === undefined || step === undefined)
62
+ return;
63
+ held = true;
64
+ const steps = [{ kind: 'visit', label: 'Open this link and sign in', url: link }, step];
65
+ void holdSecretInput(this.house, input.input_id, steps, running).then((content) => {
66
+ if (!running())
67
+ return;
68
+ if (content === null)
69
+ killTree(child);
70
+ else
71
+ child.stdin.end(`${content.code}\n`);
72
+ });
73
+ };
74
+ for (const output of [child.stdout, child.stderr])
75
+ createInterface({ input: output }).on('line', read);
76
+ const failed = await new Promise((resolve) => {
77
+ child.on('error', (error) => resolve(error.message));
78
+ child.on('close', (status, signal) => resolve(status === 0 ? null : `exited with ${status ?? signal}${last ? `: ${last}` : ''}`));
79
+ });
80
+ if (failed !== null)
81
+ process.stderr.write(`kit: the ${input.cli} sign-in failed: ${failed}\n`);
82
+ }
83
+ }
package/dist/stream.js ADDED
@@ -0,0 +1,47 @@
1
+ import { readEnrolment } from './home.js';
2
+ const SUBPROTOCOL = 'house.kit.stream.1';
3
+ function streamUrl(house) {
4
+ const url = new URL('/kit/stream', house);
5
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
6
+ return url;
7
+ }
8
+ export function holdStream(handlers) {
9
+ let socket = null;
10
+ let failures = 0;
11
+ const connect = async () => {
12
+ const { house, credential } = (await readEnrolment());
13
+ const opening = new WebSocket(streamUrl(house), {
14
+ protocols: [SUBPROTOCOL],
15
+ headers: { authorization: `Bearer ${credential}` },
16
+ });
17
+ let opened = false;
18
+ opening.onopen = () => {
19
+ opened = true;
20
+ failures = 0;
21
+ socket = opening;
22
+ handlers.opened();
23
+ };
24
+ opening.onmessage = (event) => {
25
+ handlers.frame(JSON.parse(String(event.data)));
26
+ };
27
+ opening.onerror = () => undefined;
28
+ opening.onclose = (event) => {
29
+ if (socket === opening)
30
+ socket = null;
31
+ process.stderr.write(`kit: control stream closed ${event.code} ${event.reason}\n`);
32
+ const delay = opened
33
+ ? 1000 + Math.random() * 2000
34
+ : Math.min(30_000, 1000 * 2 ** failures++);
35
+ setTimeout(() => void connect(), delay);
36
+ };
37
+ };
38
+ void connect();
39
+ return {
40
+ send(frame) {
41
+ if (socket === null || socket.readyState !== WebSocket.OPEN)
42
+ return false;
43
+ socket.send(JSON.stringify(frame));
44
+ return true;
45
+ },
46
+ };
47
+ }
package/package.json CHANGED
@@ -1,25 +1,49 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.1.0-alpha.9",
3
+ "version": "0.2.1-alpha.1",
4
+ "description": "House Kit connects an Environment to agents.house.",
4
5
  "license": "MIT",
5
6
  "repository": {
6
7
  "type": "git",
7
8
  "url": "git+https://github.com/agentshouse/kit.git"
8
9
  },
9
- "private": false,
10
10
  "type": "module",
11
11
  "bin": {
12
- "kit": "bin/kit.js"
12
+ "kit": "dist/kit-main.js",
13
+ "house": "dist/house-main.js"
13
14
  },
14
- "exports": {
15
- "./bootstrap": "./bootstrap.json"
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.build.json",
20
+ "test": "tsc -p tsconfig.json && vitest run"
16
21
  },
17
22
  "dependencies": {
18
- "@agentclientprotocol/sdk": "1.5.0",
19
- "@agentshouse/mdmodel": "0.1.0-alpha.13",
20
- "ws": "8.22.0",
21
- "yaml": "2.9.1",
22
- "zod": "4.6.5"
23
+ "@agentclientprotocol/sdk": "1.7.0"
24
+ },
25
+ "peerDependencies": {
26
+ "@agentclientprotocol/claude-agent-acp": "0.85.1",
27
+ "@agentclientprotocol/codex-acp": "2.1.1",
28
+ "@xai-official/grok": "1.0.46"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "@agentclientprotocol/claude-agent-acp": {
32
+ "optional": true
33
+ },
34
+ "@agentclientprotocol/codex-acp": {
35
+ "optional": true
36
+ },
37
+ "@xai-official/grok": {
38
+ "optional": true
39
+ }
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "24.13.6",
43
+ "@types/ws": "8.18.2",
44
+ "typescript": "7.0.2",
45
+ "vitest": "5.0.1",
46
+ "ws": "8.22.0"
23
47
  },
24
48
  "packageManager": "npm@11.19.0",
25
49
  "engines": {
@@ -1,134 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- IMAGE='ghcr.io/agentshouse/kit@sha256:__IMAGE_DIGEST__'
5
- NAME='house-kit'
6
- HOUSE='https://agents.house'
7
- WORKSPACE="$HOME/AgentsHouse"
8
- MANUAL=0
9
- WORKSPACE_SELECTED=0
10
- HOUSE_SELECTED=0
11
-
12
- refuse() {
13
- printf 'kit_bootstrap_refused: %s\n' "$1" >&2
14
- exit 1
15
- }
16
-
17
- bind_mount() {
18
- local source=${1//\"/\"\"}
19
- printf 'type=bind,"src=%s",dst=%s' "$source" "$2"
20
- }
21
-
22
- while (($#)); do
23
- case "$1" in
24
- --workspace|--house)
25
- (($# >= 2)) || refuse "$1 requires one value"
26
- if [[ "$1" == --workspace ]]; then
27
- ((WORKSPACE_SELECTED == 0)) || refuse 'supply exactly one workspace root'
28
- WORKSPACE="$2"
29
- WORKSPACE_SELECTED=1
30
- else
31
- ((HOUSE_SELECTED == 0)) || refuse 'supply exactly one House origin'
32
- HOUSE="$2"
33
- HOUSE_SELECTED=1
34
- fi
35
- shift 2
36
- ;;
37
- --manual)
38
- MANUAL=1
39
- shift
40
- ;;
41
- *) refuse "unknown argument $1" ;;
42
- esac
43
- done
44
-
45
- [[ "$IMAGE" =~ ^ghcr\.io/agentshouse/kit@sha256:[0-9a-f]{64}$ ]] || refuse 'this bootstrap has not been published with an immutable image digest'
46
- [[ "$(uname -s)" == Linux ]] || refuse "unsupported host $(uname -s); this bootstrap is for Linux"
47
- case "$(uname -m)" in
48
- x86_64|amd64) ARCH=amd64 ;;
49
- aarch64|arm64) ARCH=arm64 ;;
50
- *) refuse "unsupported Linux architecture $(uname -m)" ;;
51
- esac
52
- [[ "$WORKSPACE" == /* ]] || refuse 'workspace root must be an absolute path'
53
- command -v docker >/dev/null || refuse 'Docker Engine client is unavailable'
54
- DOCKER_PLATFORM=$(docker info --format '{{.OSType}}/{{.Architecture}}' 2>/dev/null) || refuse 'Docker Engine is unavailable'
55
- case "$DOCKER_PLATFORM" in
56
- linux/x86_64|linux/amd64) DOCKER_ARCH=amd64 ;;
57
- linux/aarch64|linux/arm64) DOCKER_ARCH=arm64 ;;
58
- *) refuse "unsupported Docker Engine platform $DOCKER_PLATFORM" ;;
59
- esac
60
- [[ "$DOCKER_ARCH" == "$ARCH" ]] || refuse "host linux/$ARCH does not match Docker Engine linux/$DOCKER_ARCH"
61
-
62
- umask 077
63
- KIT_HOME="$HOME/.house-kit"
64
- for path in "$KIT_HOME" "$WORKSPACE"; do
65
- [[ ! -L "$path" ]] || refuse "$path is a symbolic link"
66
- mkdir -p -m 700 "$path" || refuse "$path could not be created"
67
- [[ -d "$path" ]] || refuse "$path is not a directory"
68
- [[ "$(stat -c %u "$path")" == "$(id -u)" ]] || refuse "$path is not owned by this User"
69
- chmod 700 "$path" || refuse "$path could not be owner-protected"
70
- done
71
- KIT_HOME=$(cd "$KIT_HOME" && pwd -P)
72
- WORKSPACE=$(cd "$WORKSPACE" && pwd -P)
73
- KIT_HOME_MOUNT=$(bind_mount "$KIT_HOME" /kit-home)
74
- WORKSPACE_MOUNT=$(bind_mount "$WORKSPACE" /agents/house)
75
- if [[ -f "$KIT_HOME/credential" || -f "$KIT_HOME/environment" ]] && [[ ! -f "$KIT_HOME/house" ]]; then
76
- refuse 'stored House origin is missing for an enrolled Environment'
77
- fi
78
- if [[ -f "$KIT_HOME/house" ]]; then
79
- IFS= read -r BOUND_HOUSE < "$KIT_HOME/house" || refuse 'stored House origin is unreadable'
80
- if ((HOUSE_SELECTED)) && [[ "$HOUSE" != "$BOUND_HOUSE" ]]; then
81
- refuse 'this Environment is bound to another House origin'
82
- fi
83
- HOUSE="$BOUND_HOUSE"
84
- fi
85
- [[ "$HOUSE" == https://* || "$HOUSE" == http://127.0.0.1:* || "$HOUSE" == http://localhost:* ]] || refuse 'House origin must use HTTPS or local loopback HTTP'
86
- DOCKER_RUN=(docker run --rm --network host --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 --env "HOUSE_KIT_HOSTNAME=$(hostname)" "$IMAGE")
87
-
88
- if docker container inspect "$NAME" >/dev/null 2>&1; then
89
- [[ "$(docker inspect --format '{{.Config.Image}}' "$NAME")" == "$IMAGE" ]] || refuse 'an installed Kit uses a different image; update it explicitly'
90
- [[ "$(docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' "$NAME")" == unless-stopped ]] || refuse 'the installed Kit has a different lifecycle'
91
- [[ "$(docker inspect --format '{{len .Mounts}}' "$NAME")" == 2 ]] || refuse 'the installed Kit has unexpected mounts'
92
- [[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/kit-home"}}{{.Source}}{{end}}{{end}}' "$NAME")" == "$KIT_HOME" ]] || refuse 'the installed Kit uses another Kit home'
93
- [[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/kit-home"}}{{.Type}}/{{.RW}}{{end}}{{end}}' "$NAME")" == bind/true ]] || refuse 'the installed Kit home is not a writable bind mount'
94
- [[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/agents/house"}}{{.Source}}{{end}}{{end}}' "$NAME")" == "$WORKSPACE" ]] || refuse 'the installed Kit uses another workspace root'
95
- [[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/agents/house"}}{{.Type}}/{{.RW}}{{end}}{{end}}' "$NAME")" == bind/true ]] || refuse 'the installed workspace is not a writable bind mount'
96
- [[ "$(docker inspect --format '{{index .Config.Labels "agentshouse.house"}}' "$NAME")" == "$HOUSE" ]] || refuse 'the installed Kit uses another House origin'
97
- [[ -f "$KIT_HOME/credential" && -f "$KIT_HOME/environment" && -f "$KIT_HOME/house" ]] || refuse 'the installed Kit has no enrolled authority'
98
- "${DOCKER_RUN[@]}" credential check --house "$HOUSE"
99
- if [[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]; then
100
- printf 'House Kit is already running for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
101
- else
102
- docker start "$NAME" >/dev/null
103
- printf 'House Kit restarted for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
104
- fi
105
- exit 0
106
- fi
107
-
108
- docker pull --quiet --platform "linux/$ARCH" "$IMAGE" >/dev/null
109
-
110
- if [[ ! -f "$KIT_HOME/kit.json" ]]; then
111
- printf '{"availability":"intermittent","execution_authority":"allow","migrators":[],"instances":[],"clients":[],"host":null}\n' > "$KIT_HOME/kit.json"
112
- fi
113
-
114
- if [[ -f "$KIT_HOME/credential" ]]; then
115
- "${DOCKER_RUN[@]}" credential check --house "$HOUSE"
116
- else
117
- if ((MANUAL)); then
118
- "${DOCKER_RUN[@]:0:2}" -i "${DOCKER_RUN[@]:2}" login --house "$HOUSE" --manual
119
- else
120
- command -v xdg-open >/dev/null || refuse 'no host browser opener; rerun with --manual'
121
- "${DOCKER_RUN[@]}" login --house "$HOUSE" --host-browser | while IFS= read -r line; do
122
- if [[ "$line" == 'Open '* ]]; then
123
- printf 'Opening House Login in the host browser.\n'
124
- xdg-open "${line#Open }" >/dev/null 2>&1 || refuse 'host browser did not open; rerun with --manual'
125
- else
126
- printf '%s\n' "$line"
127
- fi
128
- done
129
- fi
130
- fi
131
-
132
- "${DOCKER_RUN[@]}" apply --house "$HOUSE"
133
- docker run -d --name "$NAME" --restart unless-stopped --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --label "agentshouse.house=$HOUSE" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 "$IMAGE" resident --house "$HOUSE" >/dev/null
134
- printf 'House Kit connected for Environment %s.\n' "$(cat "$KIT_HOME/environment")"