@agentshouse/kit 0.1.0-alpha.17 → 0.1.0-alpha.19
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 +59 -10
- package/bin/connect-linux.sh +14 -4
- package/bootstrap.json +3 -3
- package/package.json +2 -1
- package/src/bridge.js +1 -1
- package/src/contract.js +6 -2
- package/src/house-main.js +65 -8
- package/src/house.js +50 -37
- package/src/local-files.js +3 -11
- package/src/mcp.js +13 -0
- package/src/sync-library.js +7 -11
- package/src/sync.js +1 -1
- package/src/worker.js +64 -6
- package/src/working-copy.js +516 -47
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ On Linux `amd64` or `arm64` with a working Docker Engine, run the published,
|
|
|
10
10
|
versioned bootstrap once:
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
13
|
+
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.19/connect-linux.sh)
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The release asset pins one immutable multi-architecture image digest. It creates
|
|
@@ -105,6 +105,14 @@ Forwarding installs no host-native Kit and gives the container no Docker socket
|
|
|
105
105
|
or further mount; it refuses when no Kit is installed or the installed one
|
|
106
106
|
differs from this bootstrap's.
|
|
107
107
|
|
|
108
|
+
The owner's working copy push is forwarded the same way: run
|
|
109
|
+
`bash <(curl -fsSL …/connect-linux.sh) house git push [commit] --owner` from a
|
|
110
|
+
directory beneath the workspace root, such as a Room copy under
|
|
111
|
+
`working-copies/rooms/`. The bootstrap runs the container's `house` in the
|
|
112
|
+
matching directory beneath `/agents/house`, through the resident's lease when
|
|
113
|
+
the container runs, and refuses a directory outside the workspace before
|
|
114
|
+
anything reaches House.
|
|
115
|
+
|
|
108
116
|
## Building the package
|
|
109
117
|
|
|
110
118
|
The repository checkout is the development source. Build the package
|
|
@@ -829,7 +837,9 @@ nonzero exit status. The CLI holds
|
|
|
829
837
|
no credential: it calls over its Session's bridge, which attaches the Session
|
|
830
838
|
credential and refuses a caller without that Session's nonce as
|
|
831
839
|
`kit_bridge_unauthorized`. House network calls use only the selected
|
|
832
|
-
`2026-07-28` profile.
|
|
840
|
+
`2026-07-28` profile. `house git push` is the one local verb House does not list;
|
|
841
|
+
[working copies](#working-copies) describe it, and `house --help` and
|
|
842
|
+
`house git push --help` explain it.
|
|
833
843
|
|
|
834
844
|
Two verbs name files on the Session's own computer: `upload_attachment` takes
|
|
835
845
|
one `path`, and `append_record` takes `attachments` as local paths. The CLI
|
|
@@ -1248,14 +1258,54 @@ current grant. Every other file is read-only House content: the generated
|
|
|
1248
1258
|
grants anything, and House refuses a change to a read-only path however it is
|
|
1249
1259
|
committed.
|
|
1250
1260
|
|
|
1251
|
-
Local edits, staging and commits change nothing in House
|
|
1252
|
-
records the House content last received
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1261
|
+
Local edits, staging and commits change nothing in House until a commit is
|
|
1262
|
+
pushed. `refs/house/received` records the House content last received; each
|
|
1263
|
+
commit Kit wrote there, and each commit it acknowledged for a push, is a House
|
|
1264
|
+
state. The House revision of every file content Kit received, keyed by path and
|
|
1265
|
+
Git blob, stays in Kit home under `state/working-copies/`, outside the
|
|
1266
|
+
repository. A local commit is a local tree, never a House revision.
|
|
1267
|
+
|
|
1268
|
+
`house git push [commit]`, run anywhere inside a copy or one of its linked
|
|
1269
|
+
worktrees, submits one commit, `HEAD` by default, resolved once. Kit compares
|
|
1270
|
+
that commit's tree with the newest House state in its history, so every linked
|
|
1271
|
+
worktree keeps its own base, and sends the difference as one ordinary House edit:
|
|
1272
|
+
a changed field, section or preamble against its own prior value, any other
|
|
1273
|
+
change as the whole file against its exact revision, a new file as a create and
|
|
1274
|
+
a committed deletion as a removal. Staged and unstaged work, and files absent
|
|
1275
|
+
only from the working tree, are never sent. Native `git push` reaches no House
|
|
1276
|
+
remote. Inside an Agent Session the push goes through that Session's bridge,
|
|
1277
|
+
under its own authority and provenance; outside one it needs
|
|
1278
|
+
`house git push [commit] --owner`, which the resident Kit carries out under its
|
|
1279
|
+
control lease when a resident runs, and which otherwise acquires and releases
|
|
1280
|
+
its own. A Session refuses `--owner`, and a missing bridge is never replaced by
|
|
1281
|
+
owner authority.
|
|
1282
|
+
|
|
1283
|
+
House decides before any local Git step. A refusal changes nothing in House;
|
|
1284
|
+
after `edit_conflict` Kit records the current content House names for the
|
|
1285
|
+
refused paths in `refs/house/received`, reading by revision any content the
|
|
1286
|
+
refusal does not carry, so the agent merges or rebases onto it with native Git,
|
|
1287
|
+
commits, and pushes again. After acceptance Kit records House's result for the
|
|
1288
|
+
pushed paths, the contents an owner push receives or each accepted revision
|
|
1289
|
+
read by name, in a commit that covers the pushed one and in
|
|
1290
|
+
`refs/house/received`; a read-only path House regenerates, such as the Wiki
|
|
1291
|
+
index, stays out of Git. Kit then
|
|
1292
|
+
fast-forwards a clean branch still at the pushed commit, or rebases only its
|
|
1293
|
+
later commits. A result not yet readable, uncommitted tracked changes,
|
|
1294
|
+
a moved branch or a rebase conflict leave the branch as it was, with no stash or
|
|
1295
|
+
reset, and the push answers that House accepted it while local integration is
|
|
1296
|
+
pending; the next `house git push` finishes that integration without another
|
|
1297
|
+
House edit. A push whose answer was lost is recorded in Kit home before it is
|
|
1298
|
+
sent, and the next push by the same Session, or by the owner for an owner push,
|
|
1299
|
+
resends it under its original operation. Another caller cannot
|
|
1300
|
+
recover it: while the pushing Session lives it is refused as
|
|
1301
|
+
`kit_working_copy_push_pending`, and once that Session has ended Kit reads House
|
|
1302
|
+
again and drops it. When a retained push's operation has expired, Kit reads the
|
|
1303
|
+
Room again: if House holds the pushed content the push finishes as accepted,
|
|
1304
|
+
otherwise it is dropped and the next push submits it as a new operation.
|
|
1256
1305
|
|
|
1257
1306
|
Deselecting a Room, or losing read access to it, marks its copy inactive:
|
|
1258
|
-
launches omit it
|
|
1307
|
+
launches omit it, a new push is refused as `kit_working_copy_inactive`, and
|
|
1308
|
+
nothing local is deleted; a push already sent still settles and integrates. Selecting it again reads the Room
|
|
1259
1309
|
again, advances `refs/house/received`, and replaces only the read-only content
|
|
1260
1310
|
Kit wrote, never the branch, the index or a tracked file. A Room the owner may
|
|
1261
1311
|
read without sync is refused as `operation_denied` and stays uncopied, while its
|
|
@@ -1298,8 +1348,7 @@ Library paths are House metadata, not authored Library documents. `pull` present
|
|
|
1298
1348
|
each recorded position with its log epoch, applies each enumerated path's whole
|
|
1299
1349
|
content or removal, and advances those positions. A position older than House's
|
|
1300
1350
|
declared retention, or one of a log epoch before House's database was restored,
|
|
1301
|
-
is reported as `position_expired
|
|
1302
|
-
`rebootstrap` rebuilds the directory.
|
|
1351
|
+
is reported as `position_expired`; only `rebootstrap` rebuilds the directory.
|
|
1303
1352
|
|
|
1304
1353
|
`submit` sends field, section, and preamble operations with that value's prior
|
|
1305
1354
|
content as base, and whole-file, rename, and removal operations with the file
|
package/bin/connect-linux.sh
CHANGED
|
@@ -9,6 +9,7 @@ MANUAL=0
|
|
|
9
9
|
WORKSPACE_SELECTED=0
|
|
10
10
|
HOUSE_SELECTED=0
|
|
11
11
|
FORWARDING=0
|
|
12
|
+
FORWARDED=kit
|
|
12
13
|
FORWARD=()
|
|
13
14
|
|
|
14
15
|
refuse() {
|
|
@@ -41,8 +42,9 @@ start_resident() {
|
|
|
41
42
|
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
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
if [[ "${1:-}" == kit ]]; then
|
|
45
|
+
if [[ "${1:-}" == kit || "${1:-}" == house ]]; then
|
|
45
46
|
FORWARDING=1
|
|
47
|
+
FORWARDED="$1"
|
|
46
48
|
FORWARD=("${@:2}")
|
|
47
49
|
set --
|
|
48
50
|
fi
|
|
@@ -129,10 +131,18 @@ if docker container inspect "$NAME" >/dev/null 2>&1; then
|
|
|
129
131
|
[[ "$(docker inspect --format '{{index .Config.Labels "agentshouse.house"}}' "$NAME")" == "$HOUSE" ]] || refuse 'the installed Kit uses another House origin'
|
|
130
132
|
[[ -f "$KIT_HOME/credential" && -f "$KIT_HOME/environment" && -f "$KIT_HOME/house" ]] || refuse 'the installed Kit has no enrolled authority'
|
|
131
133
|
if ((FORWARDING)); then
|
|
132
|
-
if [[ "${FORWARD[0]:-}" == login ]]; then
|
|
134
|
+
if [[ "$FORWARDED" == kit && "${FORWARD[0]:-}" == login ]]; then
|
|
133
135
|
login_kit "${FORWARD[@]:1}"
|
|
134
136
|
exit 0
|
|
135
137
|
fi
|
|
138
|
+
PLACED=()
|
|
139
|
+
ENTRY=()
|
|
140
|
+
if [[ "$FORWARDED" == house ]]; then
|
|
141
|
+
HERE=$(pwd -P)
|
|
142
|
+
[[ "$HERE" == "$WORKSPACE" || "$HERE" == "$WORKSPACE"/* ]] || refuse "the current directory is outside the workspace $WORKSPACE"
|
|
143
|
+
PLACED=(--workdir "/agents/house${HERE#"$WORKSPACE"}")
|
|
144
|
+
ENTRY=(--entrypoint house)
|
|
145
|
+
fi
|
|
136
146
|
TERMINAL=()
|
|
137
147
|
if [[ -t 0 && -t 1 ]]; then TERMINAL=(-t); fi
|
|
138
148
|
if [[ -t 0 && ! -t 1 ]]; then
|
|
@@ -141,9 +151,9 @@ if docker container inspect "$NAME" >/dev/null 2>&1; then
|
|
|
141
151
|
stty -echo
|
|
142
152
|
fi
|
|
143
153
|
if [[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]; then
|
|
144
|
-
docker exec -i "${TERMINAL[@]}" "$NAME"
|
|
154
|
+
docker exec -i "${TERMINAL[@]}" "${PLACED[@]}" "$NAME" "$FORWARDED" "${FORWARD[@]}"
|
|
145
155
|
else
|
|
146
|
-
"${DOCKER_RUN[@]:0:2}" -i "${TERMINAL[@]}" "${DOCKER_RUN[@]:2}" "${FORWARD[@]}"
|
|
156
|
+
"${DOCKER_RUN[@]:0:2}" -i "${TERMINAL[@]}" "${PLACED[@]}" "${ENTRY[@]}" "${DOCKER_RUN[@]:2}" "${FORWARD[@]}"
|
|
147
157
|
fi
|
|
148
158
|
exit
|
|
149
159
|
fi
|
package/bootstrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.19",
|
|
4
4
|
"hosts": {
|
|
5
5
|
"linux": {
|
|
6
|
-
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
6
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.19/connect-linux.sh)",
|
|
7
7
|
"asset": {
|
|
8
8
|
"name": "connect-linux.sh",
|
|
9
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
9
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.19/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentshouse/kit",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.19",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"private": false,
|
|
10
10
|
"type": "module",
|
|
11
11
|
"bin": {
|
|
12
|
+
"house": "src/session-bin/house",
|
|
12
13
|
"kit": "bin/kit.js"
|
|
13
14
|
},
|
|
14
15
|
"exports": {
|
package/src/bridge.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { operationId } from "./operation.js";
|
|
19
19
|
import { houseFailed } from "./refusal.js";
|
|
20
20
|
|
|
21
|
-
function bridgedFrame(request, operation) {
|
|
21
|
+
export function bridgedFrame(request, operation) {
|
|
22
22
|
if (typeof request.method !== "string") return request;
|
|
23
23
|
const params = typeof request.params === "object" && request.params !== null ? request.params : {};
|
|
24
24
|
const declared = typeof params._meta === "object" && params._meta !== null ? params._meta : {};
|
package/src/contract.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CLAUDE_PASTED_CODE, CODEX_DEVICE_CODE, GROK_DEVICE_CODE } from "./agent-logins.js";
|
|
2
2
|
|
|
3
|
-
export const KIT_CONTRACT_VERSION = "
|
|
3
|
+
export const KIT_CONTRACT_VERSION = "1";
|
|
4
4
|
export const KIT_CAPTURE_BATCH_MAX = 100;
|
|
5
5
|
export const COMMAND_INPUT_MAX_BYTES = 4 * 1024;
|
|
6
6
|
export const AUTHORIZATION_OUTPUT_MAX_BYTES = 4 * 1024;
|
|
@@ -292,17 +292,21 @@ export const KIT_DOOR_DISCOVER_PATH = "/kit/door/discover";
|
|
|
292
292
|
export const KIT_DOOR_BOOTSTRAP_PATH = "/kit/door/bootstrap";
|
|
293
293
|
export const KIT_DOOR_ENUMERATE_PATH = "/kit/door/enumerate";
|
|
294
294
|
export const KIT_DOOR_BATCH_PATH = "/kit/door/batch";
|
|
295
|
+
export const KIT_DOOR_REVISIONS_PATH = "/kit/door/revisions";
|
|
295
296
|
export const EXPLICIT_SYNC_VERSION = "explicit-sync/2";
|
|
296
297
|
export const KIT_DOOR_UNMOUNT_PATH = "/kit/door/unmount";
|
|
297
298
|
|
|
298
299
|
export const KIT_WORKING_COPY_SELECTION_PATH = "/kit/working-copy/selection";
|
|
299
|
-
export const WORKING_COPY_VERSION = "working-copy-state/
|
|
300
|
+
export const WORKING_COPY_VERSION = "working-copy-state/2";
|
|
300
301
|
export const WORKING_COPIES_HOME = "working-copies";
|
|
301
302
|
export const WORKING_COPY_ROOM_HOMES = ["capture", "library", "collaboration"];
|
|
302
303
|
export const WORKING_COPY_PRIVATE_HOMES = ["library", "collaboration", "rooms", "agents", "house"];
|
|
303
304
|
export const WORKING_COPY_RECEIVED_REF = "refs/house/received";
|
|
304
305
|
export const WORKING_COPY_BRANCH = "main";
|
|
305
306
|
export const ROOM_REFERENCE = /^r_[0-9a-z]+\.[0-9a-z]$/;
|
|
307
|
+
export const WORKING_COPY_PUSH_METHOD = "house/git/push";
|
|
308
|
+
export const WORKING_COPY_PUSH_HELP =
|
|
309
|
+
"Submit a Room commit to House; default HEAD. Use --owner only outside an Agent Session. Native git push is not a House remote.";
|
|
306
310
|
|
|
307
311
|
export const MIGRATOR_CONTRACT = "kit-migrator/1";
|
|
308
312
|
export const MIGRATOR_CATEGORY = "migrator";
|
package/src/house-main.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
BRIDGE_NONCE_ENVIRONMENT,
|
|
5
|
+
BRIDGE_SOCKET_ENVIRONMENT,
|
|
6
|
+
WORKING_COPY_PUSH_HELP,
|
|
7
|
+
WORKING_COPY_PUSH_METHOD,
|
|
8
|
+
} from "./contract.js";
|
|
9
|
+
import { storedHouse } from "./login.js";
|
|
10
|
+
import { answerText, bridgeCall } from "./mcp.js";
|
|
11
|
+
import { formatCommandError, KitRefusal } from "./refusal.js";
|
|
12
|
+
import { ownerPush } from "./working-copy.js";
|
|
5
13
|
|
|
6
14
|
const HELP = ["--help", "-h"];
|
|
15
|
+
const OWNER = "--owner";
|
|
16
|
+
const GIT_PUSH_USAGE = `usage: house git push [commit] [${OWNER}]`;
|
|
7
17
|
const usage = (verb) => `usage: house ${verb} ['<arguments as one JSON object>' | - for standard input]`;
|
|
8
18
|
|
|
9
19
|
async function answered(method, params) {
|
|
@@ -49,10 +59,59 @@ function located(verb, args) {
|
|
|
49
59
|
return args;
|
|
50
60
|
}
|
|
51
61
|
|
|
62
|
+
function printed(result) {
|
|
63
|
+
const text = answerText(result);
|
|
64
|
+
return result?.isError === true ? { err: text, status: 1 } : { out: text };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function ownerPushed(request) {
|
|
68
|
+
const directory = process.env.HOUSE_KIT_HOME ?? join(homedir(), ".house-kit");
|
|
69
|
+
const cancellation = new AbortController();
|
|
70
|
+
const cancel = () => cancellation.abort();
|
|
71
|
+
process.once("SIGINT", cancel);
|
|
72
|
+
process.once("SIGTERM", cancel);
|
|
73
|
+
try {
|
|
74
|
+
const pushed = await ownerPush(
|
|
75
|
+
{ house: await storedHouse(directory), directory, signal: cancellation.signal },
|
|
76
|
+
request,
|
|
77
|
+
);
|
|
78
|
+
return pushed.refused ? { err: pushed.text, status: 1 } : { out: pushed.text };
|
|
79
|
+
} catch (refusal) {
|
|
80
|
+
return { err: formatCommandError(refusal), status: 1 };
|
|
81
|
+
} finally {
|
|
82
|
+
process.removeListener("SIGINT", cancel);
|
|
83
|
+
process.removeListener("SIGTERM", cancel);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function gitPush([action, ...rest]) {
|
|
88
|
+
if (action !== "push") throw new KitRefusal("kit_house_arguments_invalid", GIT_PUSH_USAGE);
|
|
89
|
+
if (rest.some((argument) => HELP.includes(argument))) return { out: `${GIT_PUSH_USAGE}\n\n${WORKING_COPY_PUSH_HELP}` };
|
|
90
|
+
const owner = rest.includes(OWNER);
|
|
91
|
+
const named = rest.filter((argument) => argument !== OWNER);
|
|
92
|
+
if (named.length > 1 || named.some((argument) => argument.startsWith("-"))) {
|
|
93
|
+
throw new KitRefusal("kit_house_arguments_invalid", GIT_PUSH_USAGE);
|
|
94
|
+
}
|
|
95
|
+
const request = { cwd: process.cwd(), commit: named[0] ?? "HEAD" };
|
|
96
|
+
if (process.env[BRIDGE_SOCKET_ENVIRONMENT] === undefined) {
|
|
97
|
+
if (!owner) throw new KitRefusal("kit_bridge_unavailable");
|
|
98
|
+
return ownerPushed(request);
|
|
99
|
+
}
|
|
100
|
+
if (owner) throw new KitRefusal("kit_house_arguments_invalid", `${OWNER} is only for use outside an Agent Session`);
|
|
101
|
+
return printed(await answered(WORKING_COPY_PUSH_METHOD, request));
|
|
102
|
+
}
|
|
103
|
+
|
|
52
104
|
async function house([verb, argument, ...rest]) {
|
|
105
|
+
if (verb === "git") return gitPush([argument, ...rest].filter((part) => part !== undefined));
|
|
53
106
|
if (verb === undefined || HELP.includes(verb)) {
|
|
54
107
|
const tools = await listed();
|
|
55
|
-
return {
|
|
108
|
+
return {
|
|
109
|
+
out: [
|
|
110
|
+
usage("<verb>"),
|
|
111
|
+
...tools.map((tool) => `${tool.name}\n${tool.description}`),
|
|
112
|
+
`git push [commit]\n${WORKING_COPY_PUSH_HELP}`,
|
|
113
|
+
].join("\n\n"),
|
|
114
|
+
};
|
|
56
115
|
}
|
|
57
116
|
if (HELP.includes(argument)) {
|
|
58
117
|
const tool = (await listed()).find((one) => one.name === verb);
|
|
@@ -61,9 +120,7 @@ async function house([verb, argument, ...rest]) {
|
|
|
61
120
|
}
|
|
62
121
|
if (rest.length > 0) throw new KitRefusal("kit_house_arguments_invalid", usage(verb));
|
|
63
122
|
const args = located(verb, await argumentsOf(verb, argument));
|
|
64
|
-
|
|
65
|
-
const text = (result?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
66
|
-
return result?.isError === true ? { err: text, status: 1 } : { out: text };
|
|
123
|
+
return printed(await answered("tools/call", { name: verb, arguments: args }));
|
|
67
124
|
}
|
|
68
125
|
|
|
69
126
|
house(process.argv.slice(2)).then(
|
package/src/house.js
CHANGED
|
@@ -172,7 +172,7 @@ async function residentRelay(session, request, signal, draining) {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
export async function serveResident(session, directory, signal, drain) {
|
|
175
|
+
export async function serveResident(session, directory, signal, drain, push) {
|
|
176
176
|
const socket = residentSocket(directory);
|
|
177
177
|
await mkdir(dirname(socket), { recursive: true, mode: 0o700 });
|
|
178
178
|
await rm(socket, { force: true });
|
|
@@ -229,12 +229,15 @@ export async function serveResident(session, directory, signal, drain) {
|
|
|
229
229
|
}
|
|
230
230
|
const cancelled = new AbortController();
|
|
231
231
|
relayed.set(request.id, cancelled);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
)
|
|
232
|
+
const relaySignal = AbortSignal.any([signal, cancelled.signal]);
|
|
233
|
+
const answered =
|
|
234
|
+
request.push === undefined
|
|
235
|
+
? residentRelay(session, request, relaySignal, draining)
|
|
236
|
+
: push(request.push, relaySignal).then((pushed) => ({ pushed }));
|
|
237
|
+
void answered.then((answer) => {
|
|
238
|
+
relayed.delete(request.id);
|
|
239
|
+
if (!connection.destroyed) writeFrame(connection, { id: request.id, ...answer });
|
|
240
|
+
});
|
|
238
241
|
},
|
|
239
242
|
onRefusal: () => connection.destroy(),
|
|
240
243
|
});
|
|
@@ -302,45 +305,54 @@ async function residentDelegation(directory, fetcher) {
|
|
|
302
305
|
}
|
|
303
306
|
connection.unref();
|
|
304
307
|
let next = 0;
|
|
308
|
+
function exchange(frame, signal) {
|
|
309
|
+
signal?.throwIfAborted();
|
|
310
|
+
if (connection.destroyed) return Promise.resolve(unavailable());
|
|
311
|
+
return new Promise((resolve, reject) => {
|
|
312
|
+
const id = (next += 1);
|
|
313
|
+
const cancel = () => {
|
|
314
|
+
pending.delete(id);
|
|
315
|
+
settle(id);
|
|
316
|
+
if (!connection.destroyed) writeFrame(connection, { id, cancel: true });
|
|
317
|
+
reject(signal.reason);
|
|
318
|
+
};
|
|
319
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
320
|
+
pending.set(id, (answer) => {
|
|
321
|
+
signal?.removeEventListener("abort", cancel);
|
|
322
|
+
resolve(answer);
|
|
323
|
+
});
|
|
324
|
+
connection.ref();
|
|
325
|
+
writeFrame(connection, { id, ...frame });
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async function push(request, signal) {
|
|
329
|
+
const answer = await exchange({ push: request }, signal);
|
|
330
|
+
if (answer.pushed === undefined) throw new KitRefusal("kit_resident_unavailable");
|
|
331
|
+
return answer.pushed;
|
|
332
|
+
}
|
|
305
333
|
async function delegated(url, init = {}) {
|
|
306
334
|
const { authorization, ...headers } = init.headers ?? {};
|
|
307
335
|
if (authorization !== RESIDENT) return fetcher(url, init);
|
|
308
|
-
init.signal?.throwIfAborted();
|
|
309
336
|
const address = new URL(url);
|
|
310
|
-
const answer =
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
resolve(answer);
|
|
324
|
-
});
|
|
325
|
-
connection.ref();
|
|
326
|
-
writeFrame(connection, {
|
|
327
|
-
id,
|
|
328
|
-
method: init.method ?? "GET",
|
|
329
|
-
path: `${address.pathname}${address.search}`,
|
|
330
|
-
headers,
|
|
331
|
-
...(typeof init.body === "string"
|
|
332
|
-
? { text: init.body }
|
|
333
|
-
: init.body === undefined
|
|
334
|
-
? {}
|
|
335
|
-
: { bytes: Buffer.from(init.body).toString("base64") }),
|
|
336
|
-
});
|
|
337
|
-
});
|
|
337
|
+
const answer = await exchange(
|
|
338
|
+
{
|
|
339
|
+
method: init.method ?? "GET",
|
|
340
|
+
path: `${address.pathname}${address.search}`,
|
|
341
|
+
headers,
|
|
342
|
+
...(typeof init.body === "string"
|
|
343
|
+
? { text: init.body }
|
|
344
|
+
: init.body === undefined
|
|
345
|
+
? {}
|
|
346
|
+
: { bytes: Buffer.from(init.body).toString("base64") }),
|
|
347
|
+
},
|
|
348
|
+
init.signal,
|
|
349
|
+
);
|
|
338
350
|
return new Response(answer.body === null ? null : Buffer.from(answer.body, "base64"), {
|
|
339
351
|
status: answer.status,
|
|
340
352
|
headers: answer.type === null ? {} : { "content-type": answer.type },
|
|
341
353
|
});
|
|
342
354
|
}
|
|
343
|
-
return { connection, fetch: delegated };
|
|
355
|
+
return { connection, fetch: delegated, push };
|
|
344
356
|
}
|
|
345
357
|
|
|
346
358
|
export async function openSession(options, dependencies = {}) {
|
|
@@ -359,6 +371,7 @@ export async function openSession(options, dependencies = {}) {
|
|
|
359
371
|
: await residentDelegation(options.directory, dependencies.fetch ?? fetch);
|
|
360
372
|
if (resident !== null) {
|
|
361
373
|
session.resident = resident.connection;
|
|
374
|
+
session.push = resident.push;
|
|
362
375
|
session.dependencies = { ...dependencies, fetch: resident.fetch };
|
|
363
376
|
return session;
|
|
364
377
|
}
|
package/src/local-files.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { stringify } from "yaml";
|
|
3
3
|
import { AGENT_OPERATION_META, ATTACHMENT_FILE_META } from "./contract.js";
|
|
4
|
+
import { answerText, answerYaml } from "./mcp.js";
|
|
4
5
|
import { derivedOperationId } from "./operation.js";
|
|
5
6
|
import { localAttachment, sendLocalConversationFile, uploadLocalAttachment } from "./original.js";
|
|
6
7
|
import { KitRefusal } from "./refusal.js";
|
|
@@ -12,18 +13,9 @@ class HouseRefused extends Error {
|
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function parsed(text) {
|
|
16
|
-
try {
|
|
17
|
-
return parse(text);
|
|
18
|
-
} catch {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
16
|
function answerOf(frame) {
|
|
24
17
|
if (frame.error !== undefined || frame.result?.isError === true) throw new HouseRefused(frame);
|
|
25
|
-
const
|
|
26
|
-
const answer = parsed(text);
|
|
18
|
+
const answer = answerYaml(answerText(frame.result));
|
|
27
19
|
if (answer === null || typeof answer !== "object") throw new KitRefusal("kit_house_answer_unreadable");
|
|
28
20
|
return answer;
|
|
29
21
|
}
|
package/src/mcp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { parse } from "yaml";
|
|
2
3
|
import { bridgePeer } from "./peer.js";
|
|
3
4
|
import { KitRefusal } from "./refusal.js";
|
|
4
5
|
|
|
@@ -69,3 +70,15 @@ export async function bridgeCall(bridge, request, signal = undefined) {
|
|
|
69
70
|
await peer.end();
|
|
70
71
|
}
|
|
71
72
|
}
|
|
73
|
+
|
|
74
|
+
export function answerText(result) {
|
|
75
|
+
return (result?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function answerYaml(text) {
|
|
79
|
+
try {
|
|
80
|
+
return parse(text);
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/sync-library.js
CHANGED
|
@@ -100,13 +100,12 @@ function recordedMap(authority) {
|
|
|
100
100
|
return new Map(authority.files.map((file) => [file.path, file]));
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function structuredOrReplace(root, relative, recorded, content) {
|
|
103
|
+
export function structuredOrReplace(root, relative, recorded, content) {
|
|
104
104
|
const path = housePath(root, relative);
|
|
105
|
+
const whole = [{ op: "replace", path, base: recorded.revision, content }];
|
|
105
106
|
const before = parse(recorded.content, relative);
|
|
106
107
|
const after = parse(content, relative);
|
|
107
|
-
if (!before.ok || !after.ok)
|
|
108
|
-
return [{ op: "replace", path, base: recorded.revision, content }];
|
|
109
|
-
}
|
|
108
|
+
if (!before.ok || !after.ok) return whole;
|
|
110
109
|
const ops = [];
|
|
111
110
|
if (before.document.preamble !== after.document.preamble) {
|
|
112
111
|
ops.push({
|
|
@@ -124,9 +123,7 @@ function structuredOrReplace(root, relative, recorded, content) {
|
|
|
124
123
|
if (JSON.stringify(before.document.fields[name]) === JSON.stringify(after.document.fields[name])) {
|
|
125
124
|
continue;
|
|
126
125
|
}
|
|
127
|
-
if (after.document.fields[name] === undefined)
|
|
128
|
-
return [{ op: "replace", path, base: recorded.revision, content }];
|
|
129
|
-
}
|
|
126
|
+
if (after.document.fields[name] === undefined) return whole;
|
|
130
127
|
const change = { op: "set_field", path, field: name, value: after.document.fields[name] };
|
|
131
128
|
if (before.document.fields[name] !== undefined) change.base = before.document.fields[name];
|
|
132
129
|
ops.push(change);
|
|
@@ -139,12 +136,11 @@ function structuredOrReplace(root, relative, recorded, content) {
|
|
|
139
136
|
const prior = before.document.sections[name];
|
|
140
137
|
const next = after.document.sections[name];
|
|
141
138
|
if (prior === next) continue;
|
|
142
|
-
if (prior === undefined || next === undefined)
|
|
143
|
-
return [{ op: "replace", path, base: recorded.revision, content }];
|
|
144
|
-
}
|
|
139
|
+
if (prior === undefined || next === undefined) return whole;
|
|
145
140
|
ops.push({ op: "replace_section", path, section: name, base: prior, content: next });
|
|
146
141
|
}
|
|
147
|
-
|
|
142
|
+
if (ops.length === 0) return whole;
|
|
143
|
+
return applyChanges({ [relative]: recorded.content }, ops, root)[relative] === content ? ops : whole;
|
|
148
144
|
}
|
|
149
145
|
|
|
150
146
|
export function deriveChanges(state, authority, localFiles) {
|
package/src/sync.js
CHANGED
|
@@ -41,7 +41,7 @@ function sessionDependencies(options, dependencies) {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async function withSession(options, dependencies, operation) {
|
|
44
|
+
export async function withSession(options, dependencies, operation) {
|
|
45
45
|
const session = await openSession(options, sessionDependencies(options, dependencies));
|
|
46
46
|
return maintainControl(session, (signal) => operation(session, signal), options.signal);
|
|
47
47
|
}
|