@agentshouse/kit 0.1.0-alpha.22 → 0.1.0-alpha.23
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 +14 -1
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/bytes.js +16 -7
- package/src/contract.js +2 -0
- package/src/worker.js +57 -12
- package/src/working-copy.js +18 -2
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.23/connect-linux.sh)
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The release asset pins one immutable multi-architecture image digest. It creates
|
|
@@ -1312,6 +1312,19 @@ control lease when a resident runs, and which otherwise acquires and releases
|
|
|
1312
1312
|
its own. A Session refuses `--owner`, and a missing bridge is never replaced by
|
|
1313
1313
|
owner authority.
|
|
1314
1314
|
|
|
1315
|
+
A Session's edit too large for one MCP request of 4 MiB goes through House's
|
|
1316
|
+
`prepare_edit_upload` Command on that Session's bridge, naming every changed
|
|
1317
|
+
path, the byte count and the SHA-256 of the bytes to upload under the push's
|
|
1318
|
+
operation. Those bytes are the serialized changes, padded with trailing spaces
|
|
1319
|
+
to one byte over 4 MiB when only the request around them passed the bound,
|
|
1320
|
+
because House takes no smaller upload. Kit sends them to the upload address
|
|
1321
|
+
House returns and then calls `edit` with the paths and the returned upload
|
|
1322
|
+
instead of the changes, so House applies them as the same one edit. The upload
|
|
1323
|
+
is recorded in Kit home before the edit is sent, so a push whose answer was lost
|
|
1324
|
+
resends that same edit; when House no longer holds the uploaded bytes and has
|
|
1325
|
+
not applied the edit, the push uploads them again once under the same operation.
|
|
1326
|
+
A change set beyond House's batch bounds is refused whole and never split.
|
|
1327
|
+
|
|
1315
1328
|
Inside an Agent Session `git` is Kit's shim in front of the installed Git, and
|
|
1316
1329
|
every command runs exactly as that Git runs it. In a copy or one of its linked
|
|
1317
1330
|
worktrees the shim compares that worktree's index before and after each
|
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.23",
|
|
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.23/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.23/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
package/package.json
CHANGED
package/src/bytes.js
CHANGED
|
@@ -31,6 +31,11 @@ async function downloadBody(response, signal) {
|
|
|
31
31
|
return Buffer.concat(chunks, size);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function framedBody(request) {
|
|
35
|
+
if (request.method !== "POST" || !Buffer.isBuffer(request.body)) throw new KitRefusal("kit_byte_body_invalid");
|
|
36
|
+
return Buffer.concat([Buffer.from(`${request.operation}\n`, "utf8"), request.body]);
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
export async function transferBytes(request, dependencies = {}) {
|
|
35
40
|
if (request.method !== "GET" && request.method !== "POST") {
|
|
36
41
|
throw new KitRefusal("kit_byte_method_invalid", request.method);
|
|
@@ -41,23 +46,27 @@ export async function transferBytes(request, dependencies = {}) {
|
|
|
41
46
|
if (typeof request.operation !== "string" || request.operation.length === 0) {
|
|
42
47
|
throw new KitRefusal("kit_byte_operation_invalid");
|
|
43
48
|
}
|
|
49
|
+
const framed = request.framed === true;
|
|
50
|
+
const body = framed ? framedBody(request) : request.body;
|
|
44
51
|
if (request.method === "POST") {
|
|
45
|
-
if (!Buffer.isBuffer(
|
|
46
|
-
if (
|
|
52
|
+
if (!Buffer.isBuffer(body)) throw new KitRefusal("kit_byte_body_invalid");
|
|
53
|
+
if (body.length > BYTE_GRANT_POST_BYTES_MAX) {
|
|
47
54
|
throw new KitRefusal("original_bytes_exceeded");
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
const fetcher = dependencies.fetch ?? fetch;
|
|
51
|
-
const headers =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
const headers = framed
|
|
59
|
+
? { "content-type": "text/plain; charset=utf-8" }
|
|
60
|
+
: {
|
|
61
|
+
[BYTE_GRANT_OPERATION_HEADER]: request.operation,
|
|
62
|
+
...(request.method === "POST" ? { "content-type": "application/octet-stream" } : {}),
|
|
63
|
+
};
|
|
55
64
|
let response;
|
|
56
65
|
try {
|
|
57
66
|
response = await fetcher(request.url, {
|
|
58
67
|
method: request.method,
|
|
59
68
|
headers,
|
|
60
|
-
body: request.method === "POST" ?
|
|
69
|
+
body: request.method === "POST" ? body : undefined,
|
|
61
70
|
redirect: "manual",
|
|
62
71
|
signal: request.signal,
|
|
63
72
|
});
|
package/src/contract.js
CHANGED
|
@@ -308,6 +308,8 @@ export const WORKING_COPY_PUSH_METHOD = "house/git/push";
|
|
|
308
308
|
export const WORKING_COPY_INDEX_METHOD = "house/git/index";
|
|
309
309
|
export const WORKING_COPY_PUSH_HELP =
|
|
310
310
|
"Submit a Room commit to House; default HEAD. Use --owner only outside an Agent Session. Native git push is not a House remote.";
|
|
311
|
+
export const WORKING_COPY_UPLOAD_COMMAND = "prepare_edit_upload";
|
|
312
|
+
export const MCP_REQUEST_BODY_MAX_BYTES = 4 * 1024 * 1024;
|
|
311
313
|
|
|
312
314
|
export const MIGRATOR_CONTRACT = "kit-migrator/1";
|
|
313
315
|
export const MIGRATOR_CATEGORY = "migrator";
|
package/src/worker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { readBindings, removeBinding, writeBinding } from "./binding.js";
|
|
12
12
|
import { readContinuation, readContinuationRuns, writeContinuation, removeContinuation } from "./continuation.js";
|
|
13
13
|
import { bridgedFrame, openBridge } from "./bridge.js";
|
|
14
|
+
import { transferBytes } from "./bytes.js";
|
|
14
15
|
import { assistantBlocks } from "./blocks.js";
|
|
15
16
|
import { retryConversationFiles } from "./conversation-originals.js";
|
|
16
17
|
import { openAssistantDrafts } from "./draft.js";
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
ownerCaller,
|
|
26
27
|
pushWorkingCopy,
|
|
27
28
|
recordIndexChange,
|
|
29
|
+
uploadedPaths,
|
|
28
30
|
workingCopyPaths,
|
|
29
31
|
} from "./working-copy.js";
|
|
30
32
|
import { readConfiguration } from "./configuration.js";
|
|
@@ -32,6 +34,7 @@ import { launchBase, readyLaunchDirectory } from "./directory.js";
|
|
|
32
34
|
import {
|
|
33
35
|
BRIDGE_NONCE_ENVIRONMENT,
|
|
34
36
|
BRIDGE_SOCKET_ENVIRONMENT,
|
|
37
|
+
MCP_REQUEST_BODY_MAX_BYTES,
|
|
35
38
|
PROVIDER_CONTINUATION_UNAVAILABLE,
|
|
36
39
|
WORKER_COMMAND_BATCH_MAX,
|
|
37
40
|
WORKER_COMMAND_POLL_MS,
|
|
@@ -42,6 +45,7 @@ import {
|
|
|
42
45
|
WORKER_SUPERVISION_POLL_MS,
|
|
43
46
|
WORKING_COPY_INDEX_METHOD,
|
|
44
47
|
WORKING_COPY_PUSH_METHOD,
|
|
48
|
+
WORKING_COPY_UPLOAD_COMMAND,
|
|
45
49
|
} from "./contract.js";
|
|
46
50
|
import { EpochReportRequired, formatCommandError, houseFailed, KitRefusal } from "./refusal.js";
|
|
47
51
|
import {
|
|
@@ -299,20 +303,61 @@ function acceptedWith(text) {
|
|
|
299
303
|
return { refused: null, written: [...written.values()] };
|
|
300
304
|
}
|
|
301
305
|
|
|
306
|
+
function toolFrame(name, args, operation) {
|
|
307
|
+
return bridgedFrame({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }, operation);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function uploadBody(changes) {
|
|
311
|
+
const body = Buffer.from(JSON.stringify({ changes }), "utf8");
|
|
312
|
+
if (body.length > MCP_REQUEST_BODY_MAX_BYTES) return body;
|
|
313
|
+
return Buffer.concat([body, Buffer.alloc(MCP_REQUEST_BODY_MAX_BYTES + 1 - body.length, " ")]);
|
|
314
|
+
}
|
|
315
|
+
|
|
302
316
|
async function sessionPush(context, bound, authority, frame, signal) {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
317
|
+
const call = async (sent, called) =>
|
|
318
|
+
callHouseMcp(context.session, await sessionAuthority(context.session, bound.session, authority, sent), called, sent);
|
|
319
|
+
const refused = (answer) =>
|
|
320
|
+
answer.error !== undefined
|
|
321
|
+
? refusedWith(answer.error.message ?? "kit_house_refused")
|
|
322
|
+
: answer.result?.isError === true
|
|
323
|
+
? refusedWith(answerText(answer.result))
|
|
324
|
+
: null;
|
|
325
|
+
const stage = async (body, sent) => {
|
|
326
|
+
const bytes = uploadBody(body.changes);
|
|
327
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
328
|
+
const prepare = { paths: uploadedPaths(body.changes), bytes: bytes.length, sha256 };
|
|
329
|
+
const prepared = await call(
|
|
330
|
+
sent,
|
|
331
|
+
toolFrame("run_command", { command: WORKING_COPY_UPLOAD_COMMAND, arguments: prepare }, body.operation_id),
|
|
332
|
+
);
|
|
333
|
+
const refusal = refused(prepared);
|
|
334
|
+
if (refusal !== null) return refusal;
|
|
335
|
+
const transfer = answerYaml(answerText(prepared.result))?.transfer;
|
|
336
|
+
const staged = (
|
|
337
|
+
await transferBytes(
|
|
338
|
+
{ url: transfer?.url, method: "POST", operation: transfer?.operation, body: bytes, framed: true, signal: sent },
|
|
339
|
+
context.session.dependencies,
|
|
340
|
+
)
|
|
341
|
+
).transfer;
|
|
342
|
+
if (typeof staged?.url !== "string" || typeof staged.operation !== "string") {
|
|
343
|
+
throw new KitRefusal("kit_house_answer_unreadable", WORKING_COPY_UPLOAD_COMMAND);
|
|
344
|
+
}
|
|
345
|
+
return { upload: { url: staged.url, operation: staged.operation, bytes: bytes.length, sha256 } };
|
|
346
|
+
};
|
|
347
|
+
const edit = async (body, sent, retain) => {
|
|
348
|
+
let { upload } = body;
|
|
349
|
+
const inline = toolFrame("edit", { changes: body.changes }, body.operation_id);
|
|
350
|
+
if (upload === undefined && Buffer.byteLength(JSON.stringify(inline)) > MCP_REQUEST_BODY_MAX_BYTES) {
|
|
351
|
+
const staged = await stage(body, sent);
|
|
352
|
+
if (staged.upload === undefined) return staged;
|
|
353
|
+
({ upload } = staged);
|
|
354
|
+
await retain(upload);
|
|
355
|
+
}
|
|
356
|
+
const answer = await call(
|
|
311
357
|
sent,
|
|
358
|
+
upload === undefined ? inline : toolFrame("edit", { paths: uploadedPaths(body.changes), upload }, body.operation_id),
|
|
312
359
|
);
|
|
313
|
-
|
|
314
|
-
const text = answerText(answer.result);
|
|
315
|
-
return answer.result?.isError === true ? refusedWith(text) : acceptedWith(text);
|
|
360
|
+
return refused(answer) ?? acceptedWith(answerText(answer.result));
|
|
316
361
|
};
|
|
317
362
|
let outcome;
|
|
318
363
|
try {
|
package/src/working-copy.js
CHANGED
|
@@ -914,6 +914,10 @@ function pushedPaths(changes) {
|
|
|
914
914
|
return new Set(changes.flatMap((change) => (change.op === "rename" ? [change.path, change.to] : [change.path])));
|
|
915
915
|
}
|
|
916
916
|
|
|
917
|
+
export function uploadedPaths(changes) {
|
|
918
|
+
return [...pushedPaths(changes)].sort();
|
|
919
|
+
}
|
|
920
|
+
|
|
917
921
|
async function enumerated(context, copy, since, signal) {
|
|
918
922
|
const answer = await door(
|
|
919
923
|
context.session,
|
|
@@ -1104,12 +1108,18 @@ async function expired(context, copy, text, signal) {
|
|
|
1104
1108
|
return finish(context, accepted, signal);
|
|
1105
1109
|
}
|
|
1106
1110
|
|
|
1107
|
-
async function settle(context, copy, caller, signal) {
|
|
1111
|
+
async function settle(context, copy, caller, signal, restaged = false) {
|
|
1108
1112
|
const { push } = copy;
|
|
1109
1113
|
if (!sameCaller(push.caller, caller.binding)) return orphaned(context, copy, signal);
|
|
1114
|
+
let staged = push.upload !== undefined;
|
|
1115
|
+
const retain = async (upload) => {
|
|
1116
|
+
staged = true;
|
|
1117
|
+
await writeCopy(context.directory, { ...copy, push: { ...push, upload } }, signal);
|
|
1118
|
+
};
|
|
1110
1119
|
let answer;
|
|
1111
1120
|
try {
|
|
1112
|
-
|
|
1121
|
+
const { operation_id, changes, upload } = push;
|
|
1122
|
+
answer = await caller.submit({ operation_id, changes, ...(upload === undefined ? {} : { upload }) }, signal, retain);
|
|
1113
1123
|
} catch (error) {
|
|
1114
1124
|
if (!definite(error)) {
|
|
1115
1125
|
throw new KitRefusal("kit_working_copy_push_uncertain", "run house git push again to recover its outcome");
|
|
@@ -1118,6 +1128,12 @@ async function settle(context, copy, caller, signal) {
|
|
|
1118
1128
|
answer = { refused: { code: error.code, text: formatCommandError(error), current: Array.isArray(current) ? current : null } };
|
|
1119
1129
|
}
|
|
1120
1130
|
const { refused } = answer;
|
|
1131
|
+
if (refused?.code === "upload_unavailable" && staged && !restaged) {
|
|
1132
|
+
const { upload, ...unstaged } = push;
|
|
1133
|
+
const cleared = { ...copy, push: unstaged };
|
|
1134
|
+
await writeCopy(context.directory, cleared, signal);
|
|
1135
|
+
return settle(context, cleared, caller, signal, true);
|
|
1136
|
+
}
|
|
1121
1137
|
if (refused?.code === "operation_expired") return expired(context, copy, refused.text, signal);
|
|
1122
1138
|
if (refused === null) {
|
|
1123
1139
|
const pushed = pushedPaths(push.changes);
|