@agentshouse/kit 0.1.0-alpha.4 → 0.1.0-alpha.40
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 +41 -1671
- package/bin/connect-linux.sh +501 -53
- package/bin/connect-windows.ps1 +430 -0
- package/bin/kit.js +200 -159
- package/bootstrap.json +28 -0
- package/docs/configuration.md +104 -0
- package/docs/google.md +33 -0
- package/docs/linux.md +77 -0
- package/docs/macos.md +26 -0
- package/docs/telegram.md +29 -0
- package/docs/windows.md +36 -0
- package/package.json +7 -2
- package/src/adapter-mcp.js +92 -12
- package/src/agents.js +63 -8
- package/src/binding.js +52 -0
- package/src/blocks.js +2 -26
- package/src/bridge.js +28 -14
- package/src/bytes.js +19 -8
- package/src/configuration.js +14 -211
- package/src/connector-login.js +3 -84
- package/src/contract.js +44 -12
- package/src/conversation-originals.js +1 -3
- package/src/delivery.js +4 -7
- package/src/directory.js +6 -26
- package/src/draft.js +4 -82
- package/src/git-main.js +138 -0
- package/src/git.js +62 -0
- package/src/google/calendar.js +39 -45
- package/src/google/drive.js +61 -186
- package/src/google/entries.js +349 -0
- package/src/google/gateway.js +3 -6
- package/src/google/gmail.js +33 -23
- package/src/google/oauth.js +106 -66
- package/src/google/source.js +21 -23
- package/src/host.js +6 -2
- package/src/house-main.js +65 -8
- package/src/house.js +446 -127
- package/src/how-we-work.md +25 -0
- package/src/inbound.js +2 -18
- package/src/kit-work-stream.js +120 -0
- package/src/launcher.js +46 -10
- package/src/local-files.js +3 -11
- package/src/login.js +27 -14
- package/src/mcp.js +18 -0
- package/src/mount.js +9 -18
- package/src/original.js +4 -4
- package/src/pacing.js +41 -0
- package/src/plan.js +37 -45
- package/src/repository.js +3 -61
- package/src/session-bin/git +2 -0
- package/src/setup.js +14 -15
- package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
- package/src/skill-set/ask-matt/SKILL.md +91 -0
- package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
- package/src/skill-set/code-review/LICENSE +21 -0
- package/src/skill-set/code-review/SKILL.md +89 -0
- package/src/skill-set/code-review/agents/openai.yaml +3 -0
- package/src/skill-set/codebase-design/DEEPENING.md +37 -0
- package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/src/skill-set/codebase-design/LICENSE +21 -0
- package/src/skill-set/codebase-design/SKILL.md +116 -0
- package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
- package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
- package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
- package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
- package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/src/skill-set/domain-modeling/LICENSE +21 -0
- package/src/skill-set/domain-modeling/SKILL.md +76 -0
- package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
- package/src/skill-set/grill-me/LICENSE +21 -0
- package/src/skill-set/grill-me/SKILL.md +8 -0
- package/src/skill-set/grill-me/agents/openai.yaml +4 -0
- package/src/skill-set/grill-with-docs/LICENSE +21 -0
- package/src/skill-set/grill-with-docs/SKILL.md +8 -0
- package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
- package/src/skill-set/grilling/LICENSE +21 -0
- package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
- package/src/skill-set/handoff/LICENSE +21 -0
- package/src/skill-set/handoff/SKILL.md +17 -0
- package/src/skill-set/handoff/agents/openai.yaml +4 -0
- package/src/skill-set/implement/LICENSE +21 -0
- package/src/skill-set/implement/SKILL.md +16 -0
- package/src/skill-set/implement/agents/openai.yaml +4 -0
- package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
- package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
- package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
- package/src/skill-set/prototype/LICENSE +21 -0
- package/src/skill-set/prototype/LOGIC.md +67 -0
- package/src/skill-set/prototype/SKILL.md +28 -0
- package/src/skill-set/prototype/UI.md +112 -0
- package/src/skill-set/prototype/agents/openai.yaml +3 -0
- package/src/skill-set/research/LICENSE +21 -0
- package/src/skill-set/research/SKILL.md +14 -0
- package/src/skill-set/research/agents/openai.yaml +3 -0
- package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
- package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
- package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/LICENSE +21 -0
- package/src/skill-set/tdd/SKILL.md +40 -0
- package/src/skill-set/tdd/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/mocking.md +59 -0
- package/src/skill-set/tdd/tests.md +77 -0
- package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
- package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/src/skill-set/teach/LICENSE +21 -0
- package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
- package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
- package/src/skill-set/teach/SKILL.md +141 -0
- package/src/skill-set/teach/agents/openai.yaml +4 -0
- package/src/skill-set/to-questionnaire/LICENSE +21 -0
- package/src/skill-set/to-questionnaire/SKILL.md +55 -0
- package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
- package/src/skill-set/to-spec/LICENSE +21 -0
- package/src/skill-set/to-spec/SKILL.md +76 -0
- package/src/skill-set/to-spec/agents/openai.yaml +4 -0
- package/src/skill-set/to-tickets/LICENSE +21 -0
- package/src/skill-set/to-tickets/SKILL.md +106 -0
- package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
- package/src/skill-set/wait-what/LICENSE +21 -0
- package/src/skill-set/wait-what/SKILL.md +8 -0
- package/src/skill-set/wait-what/agents/openai.yaml +4 -0
- package/src/skill-set/wayfinder/LICENSE +21 -0
- package/src/skill-set/wayfinder/SKILL.md +129 -0
- package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
- package/src/skill-set/writing-for-agents/LICENSE +21 -0
- package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/src/skill-set/writing-for-agents/SKILL.md +83 -0
- package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
- package/src/skill-set.js +85 -0
- package/src/source.js +93 -54
- package/src/state.js +19 -1
- package/src/sync-library.js +7 -11
- package/src/sync-state.js +12 -3
- package/src/sync.js +142 -43
- package/src/tasks.js +92 -0
- package/src/telegram-view.js +217 -0
- package/src/telegram.js +204 -69
- package/src/turn-frames.js +69 -0
- package/src/worker.js +738 -189
- package/src/working-copy.js +1274 -0
- package/src/workspace.js +299 -88
- package/src/google/declared.js +0 -146
- package/src/google/id.js +0 -1
- package/src/instance/google.js +0 -1
- package/src/starter-set.js +0 -17
- /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
- /package/src/{starter-set → skill-set}/grilling/agents/openai.yaml +0 -0
package/src/workspace.js
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
import { access, mkdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { createServer } from "node:net";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { readConfiguration } from "./configuration.js";
|
|
4
5
|
import {
|
|
5
6
|
CGROUP_CONTROLLERS,
|
|
6
7
|
HOST_MOUNT_INFO,
|
|
8
|
+
LAUNCH_DIRECTORY_BASE,
|
|
7
9
|
LOOP_CONTROL,
|
|
8
10
|
WORKSPACE_ACQUIRE_MAX,
|
|
9
11
|
WORKSPACE_COMMAND_TIMEOUT_MS,
|
|
12
|
+
WORKSPACE_DISK_HOUSE,
|
|
10
13
|
WORKSPACE_DISK_IMAGE,
|
|
14
|
+
WORKSPACE_DISK_KIT_HOME,
|
|
11
15
|
WORKSPACE_DISK_MOUNT,
|
|
12
16
|
WORKSPACE_KIT_HOME,
|
|
13
17
|
WORKSPACE_MOUNT_OPTIONS,
|
|
14
18
|
WORKSPACE_LABEL,
|
|
15
|
-
|
|
19
|
+
WORKSPACE_LOCK_POLL_MS,
|
|
16
20
|
WORKSPACE_OPERATION_WITHDRAWN,
|
|
17
21
|
WORKSPACE_POLL_MS,
|
|
22
|
+
WORKSPACE_PREVIOUS_SUFFIX,
|
|
18
23
|
WORKSPACE_READY_ATTEMPTS,
|
|
19
24
|
WORKSPACE_READY_POLL_MS,
|
|
25
|
+
WORKSPACE_RELEASE_IMAGE,
|
|
26
|
+
WORKSPACE_SELECTED_IMAGE,
|
|
20
27
|
WORKSPACE_STOP_TIMEOUT_SECONDS,
|
|
21
28
|
WORKSPACE_SYSTEM_RESERVE_BYTES,
|
|
22
29
|
} from "./contract.js";
|
|
@@ -27,10 +34,13 @@ import {
|
|
|
27
34
|
openSession,
|
|
28
35
|
releaseControl,
|
|
29
36
|
reportWorkspaceOutcome,
|
|
37
|
+
updateDrain,
|
|
30
38
|
} from "./house.js";
|
|
31
|
-
import { KitRefusal } from "./refusal.js";
|
|
39
|
+
import { houseFailed, KitRefusal } from "./refusal.js";
|
|
32
40
|
|
|
33
41
|
const REQUIRED_CONTROLLERS = ["cpu", "memory"];
|
|
42
|
+
const PROVISIONED_DISK = "/disk";
|
|
43
|
+
const STOPPED_STATES = ["created", "exited"];
|
|
34
44
|
const MEMINFO = "/proc/meminfo";
|
|
35
45
|
const MEM_AVAILABLE = /^MemAvailable:\s+(\d+)\s+kB$/m;
|
|
36
46
|
|
|
@@ -106,30 +116,35 @@ export function mountedPaths(document) {
|
|
|
106
116
|
);
|
|
107
117
|
}
|
|
108
118
|
|
|
109
|
-
export function workspaceCreateArguments(identity,
|
|
119
|
+
export function workspaceCreateArguments(identity, workspace, image, house, root) {
|
|
120
|
+
const disk = workspaceMountPath(root, identity);
|
|
110
121
|
return [
|
|
111
122
|
"create",
|
|
112
123
|
"--name",
|
|
113
124
|
identity,
|
|
114
125
|
"--label",
|
|
115
|
-
`${WORKSPACE_LABEL}=${
|
|
126
|
+
`${WORKSPACE_LABEL}=${workspace.environment}`,
|
|
116
127
|
"--cpus",
|
|
117
|
-
(
|
|
128
|
+
(workspace.maxima.cpu_millicores / 1000).toFixed(3),
|
|
118
129
|
"--memory",
|
|
119
|
-
`${
|
|
130
|
+
`${workspace.maxima.memory_bytes}b`,
|
|
120
131
|
"--memory-swap",
|
|
121
|
-
`${
|
|
132
|
+
`${workspace.maxima.memory_bytes}b`,
|
|
122
133
|
"--restart",
|
|
123
134
|
"no",
|
|
135
|
+
...(new URL(house).protocol === "https:" ? [] : ["--network", "host"]),
|
|
124
136
|
"--mount",
|
|
125
|
-
`type=bind,source=${
|
|
137
|
+
`type=bind,source=${disk}/${WORKSPACE_DISK_KIT_HOME},target=${WORKSPACE_KIT_HOME}`,
|
|
138
|
+
"--mount",
|
|
139
|
+
`type=bind,source=${disk}/${WORKSPACE_DISK_HOUSE},target=${LAUNCH_DIRECTORY_BASE}`,
|
|
140
|
+
"--env",
|
|
141
|
+
`HOME=${WORKSPACE_KIT_HOME}`,
|
|
126
142
|
"--env",
|
|
127
143
|
`HOUSE_KIT_HOME=${WORKSPACE_KIT_HOME}`,
|
|
128
144
|
"--env",
|
|
129
|
-
|
|
130
|
-
"--workdir",
|
|
131
|
-
WORKSPACE_MOUNT,
|
|
145
|
+
"HOUSE_KIT_CONTAINER=1",
|
|
132
146
|
image,
|
|
147
|
+
"resident",
|
|
133
148
|
];
|
|
134
149
|
}
|
|
135
150
|
|
|
@@ -198,7 +213,7 @@ export async function workspacePrerequisites(host, image) {
|
|
|
198
213
|
if (formatter.status !== 0) {
|
|
199
214
|
return {
|
|
200
215
|
supported: false,
|
|
201
|
-
refusal: `the
|
|
216
|
+
refusal: `the selected Kit image ${image} carries no mkfs.ext4, so no volume ceiling can be created`,
|
|
202
217
|
};
|
|
203
218
|
}
|
|
204
219
|
return { supported: true, runtime: `docker ${version.stdout.trim()}` };
|
|
@@ -215,34 +230,45 @@ function bootstrapConfiguration() {
|
|
|
215
230
|
});
|
|
216
231
|
}
|
|
217
232
|
|
|
218
|
-
function
|
|
233
|
+
function diskArguments(root, identity, image, script) {
|
|
219
234
|
return [
|
|
220
235
|
"run",
|
|
221
236
|
"--rm",
|
|
222
237
|
"--interactive",
|
|
223
238
|
"--mount",
|
|
224
|
-
`type=bind,source=${workspaceMountPath(root, identity)},target=${
|
|
239
|
+
`type=bind,source=${workspaceMountPath(root, identity)},target=${PROVISIONED_DISK}`,
|
|
225
240
|
"--entrypoint",
|
|
226
241
|
"sh",
|
|
227
242
|
image,
|
|
228
243
|
"-c",
|
|
229
|
-
|
|
244
|
+
script,
|
|
230
245
|
];
|
|
231
246
|
}
|
|
232
247
|
|
|
233
|
-
async function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
248
|
+
async function onDisk(host, settings, identity, script, stdin = "") {
|
|
249
|
+
const written = await docker(host, diskArguments(settings.root, identity, settings.image, script), stdin);
|
|
250
|
+
if (written.status !== 0) {
|
|
251
|
+
throw new KitRefusal("kit_workspace_unprovisioned", written.stderr.trim() || identity);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function provisionWorkspaceState(host, settings, identity, operation) {
|
|
256
|
+
const home = `${PROVISIONED_DISK}/${WORKSPACE_DISK_KIT_HOME}`;
|
|
257
|
+
const house = `${PROVISIONED_DISK}/${WORKSPACE_DISK_HOUSE}`;
|
|
258
|
+
const files = [
|
|
259
|
+
...(operation.authority === undefined ? [] : [["credential", operation.authority.credential]]),
|
|
260
|
+
["kit.json", bootstrapConfiguration()],
|
|
261
|
+
["house", settings.house],
|
|
262
|
+
["environment", operation.environment],
|
|
263
|
+
];
|
|
264
|
+
for (const [name, content] of files) {
|
|
265
|
+
await onDisk(
|
|
239
266
|
host,
|
|
240
|
-
|
|
241
|
-
|
|
267
|
+
settings,
|
|
268
|
+
identity,
|
|
269
|
+
`set -e; mkdir -p ${home} ${house}; chmod 700 ${home} ${house}; cat > ${home}/${name}; chmod 600 ${home}/${name}`,
|
|
270
|
+
`${content}\n`,
|
|
242
271
|
);
|
|
243
|
-
if (written.status !== 0) {
|
|
244
|
-
throw new KitRefusal("kit_workspace_unprovisioned", written.stderr.trim() || identity);
|
|
245
|
-
}
|
|
246
272
|
}
|
|
247
273
|
}
|
|
248
274
|
|
|
@@ -289,9 +315,7 @@ export async function createWorkspace(host, settings, operation) {
|
|
|
289
315
|
}
|
|
290
316
|
const mountRefusal = await mountWorkspace(host, settings, identity);
|
|
291
317
|
if (mountRefusal !== null) return { outcome: "unsupported", refusal: mountRefusal };
|
|
292
|
-
|
|
293
|
-
await provisionWorkspaceState(host, settings.root, identity, settings.image, operation.authority);
|
|
294
|
-
}
|
|
318
|
+
await provisionWorkspaceState(host, settings, identity, operation);
|
|
295
319
|
const created = await docker(
|
|
296
320
|
host,
|
|
297
321
|
workspaceCreateArguments(identity, operation, settings.image, settings.house, settings.root),
|
|
@@ -312,9 +336,97 @@ async function workspacePresent(host, identity) {
|
|
|
312
336
|
return inspected.status === 0 ? inspected.stdout.trim() : null;
|
|
313
337
|
}
|
|
314
338
|
|
|
339
|
+
async function heldWorkspace(host, identity) {
|
|
340
|
+
const inspected = await docker(host, [
|
|
341
|
+
"container",
|
|
342
|
+
"inspect",
|
|
343
|
+
"--format",
|
|
344
|
+
`{{.State.Status}} {{.Config.Image}} {{index .Config.Labels "${WORKSPACE_LABEL}"}} {{.HostConfig.NanoCpus}} {{.HostConfig.Memory}}`,
|
|
345
|
+
identity,
|
|
346
|
+
]);
|
|
347
|
+
if (inspected.status !== 0) return null;
|
|
348
|
+
const [state, image, environment, nanoCpus, memory] = inspected.stdout.trim().split(/\s+/);
|
|
349
|
+
return {
|
|
350
|
+
state,
|
|
351
|
+
image,
|
|
352
|
+
environment,
|
|
353
|
+
maxima: { cpu_millicores: Number(nanoCpus) / 1_000_000, memory_bytes: Number(memory) },
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function previousName(identity) {
|
|
358
|
+
return `${identity}${WORKSPACE_PREVIOUS_SUFFIX}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function replaceWorkspace(host, settings, identity, held, running) {
|
|
362
|
+
const mountRefusal = await mountWorkspace(host, settings, identity);
|
|
363
|
+
if (mountRefusal !== null) return mountRefusal;
|
|
364
|
+
if (running) {
|
|
365
|
+
const stopped = await docker(host, [
|
|
366
|
+
"stop",
|
|
367
|
+
"--timeout",
|
|
368
|
+
String(WORKSPACE_STOP_TIMEOUT_SECONDS),
|
|
369
|
+
identity,
|
|
370
|
+
]);
|
|
371
|
+
if (stopped.status !== 0) return `the workspace did not stop: ${stopped.stderr.trim()}`;
|
|
372
|
+
}
|
|
373
|
+
let cause;
|
|
374
|
+
try {
|
|
375
|
+
cause = await placeReplacement(host, settings, identity, held, running);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (!(error instanceof KitRefusal)) throw error;
|
|
378
|
+
cause = error.message;
|
|
379
|
+
}
|
|
380
|
+
if (cause === null) {
|
|
381
|
+
const removed = await docker(host, ["rm", "--force", previousName(identity)]);
|
|
382
|
+
if (removed.status === 0) return null;
|
|
383
|
+
cause = `the previous workspace was not removed: ${removed.stderr.trim()}`;
|
|
384
|
+
}
|
|
385
|
+
return rollBack(host, identity, running, cause);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function placeReplacement(host, settings, identity, held, running) {
|
|
389
|
+
await onDisk(host, settings, identity, `rm -f ${updateDrain(`${PROVISIONED_DISK}/${WORKSPACE_DISK_KIT_HOME}`)}`);
|
|
390
|
+
const setAside = await docker(host, ["rename", identity, previousName(identity)]);
|
|
391
|
+
if (setAside.status !== 0) return `the workspace was not set aside: ${setAside.stderr.trim()}`;
|
|
392
|
+
const created = await docker(
|
|
393
|
+
host,
|
|
394
|
+
workspaceCreateArguments(identity, held, settings.image, settings.house, settings.root),
|
|
395
|
+
);
|
|
396
|
+
if (created.status !== 0) return `the workspace was not recreated: ${created.stderr.trim()}`;
|
|
397
|
+
if (!running) return null;
|
|
398
|
+
const started = await docker(host, ["start", identity]);
|
|
399
|
+
if (started.status !== 0) return `the workspace did not start: ${started.stderr.trim()}`;
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function rollBack(host, identity, running, cause) {
|
|
404
|
+
const unrestored = await restorePrevious(host, identity);
|
|
405
|
+
if (unrestored !== null) return `${cause}, and ${unrestored}`;
|
|
406
|
+
if (!running) return cause;
|
|
407
|
+
const restarted = await docker(host, ["start", identity]);
|
|
408
|
+
if (restarted.status === 0) return cause;
|
|
409
|
+
return `${cause}, and the workspace did not start again: ${restarted.stderr.trim()}`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function restorePrevious(host, identity) {
|
|
413
|
+
const previous = previousName(identity);
|
|
414
|
+
if ((await workspacePresent(host, previous)) === null) return null;
|
|
415
|
+
if ((await workspacePresent(host, identity)) !== null) {
|
|
416
|
+
const discarded = await docker(host, ["rm", "--force", identity]);
|
|
417
|
+
if (discarded.status !== 0) {
|
|
418
|
+
return `the replacement was not removed: ${discarded.stderr.trim()}`;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const restored = await docker(host, ["rename", previous, identity]);
|
|
422
|
+
if (restored.status !== 0) return `the workspace was not restored: ${restored.stderr.trim()}`;
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
|
|
315
426
|
export async function startWorkspace(host, settings, operation) {
|
|
316
427
|
const identity = operation.workspace ?? workspaceIdentity(operation);
|
|
317
|
-
|
|
428
|
+
const held = await heldWorkspace(host, identity);
|
|
429
|
+
if (held === null) return { outcome: "absent" };
|
|
318
430
|
const available = memoryAvailable(await host.read(MEMINFO));
|
|
319
431
|
if (available - operation.maxima.memory_bytes < WORKSPACE_SYSTEM_RESERVE_BYTES) {
|
|
320
432
|
return {
|
|
@@ -324,6 +436,10 @@ export async function startWorkspace(host, settings, operation) {
|
|
|
324
436
|
}
|
|
325
437
|
const mountRefusal = await mountWorkspace(host, settings, identity);
|
|
326
438
|
if (mountRefusal !== null) return { outcome: "unsupported", refusal: mountRefusal };
|
|
439
|
+
if (STOPPED_STATES.includes(held.state) && held.image !== settings.image) {
|
|
440
|
+
const refusal = await replaceWorkspace(host, settings, identity, held, false);
|
|
441
|
+
if (refusal !== null) return { outcome: "failed", refusal };
|
|
442
|
+
}
|
|
327
443
|
const started = await docker(host, ["start", identity]);
|
|
328
444
|
if (started.status !== 0) {
|
|
329
445
|
return { outcome: "failed", refusal: `the workspace did not start: ${started.stderr.trim()}` };
|
|
@@ -409,14 +525,15 @@ export async function managedWorkspaces(host) {
|
|
|
409
525
|
if (listed.status !== 0) {
|
|
410
526
|
throw new KitRefusal("kit_workspace_inventory_unavailable", listed.stderr.trim() || null);
|
|
411
527
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
528
|
+
const managed = new Map();
|
|
529
|
+
for (const line of listed.stdout.split("\n").map((entry) => entry.trim()).filter((entry) => entry !== "")) {
|
|
530
|
+
const [container, environment, state] = line.split(/\s+/);
|
|
531
|
+
const name = container.endsWith(WORKSPACE_PREVIOUS_SUFFIX)
|
|
532
|
+
? container.slice(0, -WORKSPACE_PREVIOUS_SUFFIX.length)
|
|
533
|
+
: container;
|
|
534
|
+
if (!managed.has(name)) managed.set(name, { name, environment, state });
|
|
535
|
+
}
|
|
536
|
+
return [...managed.values()];
|
|
420
537
|
}
|
|
421
538
|
|
|
422
539
|
export async function reconcileWorkspaces(host, authorized) {
|
|
@@ -425,76 +542,170 @@ export async function reconcileWorkspaces(host, authorized) {
|
|
|
425
542
|
);
|
|
426
543
|
const stopped = [];
|
|
427
544
|
for (const managed of await managedWorkspaces(host)) {
|
|
428
|
-
if (managed.state !== "running") continue;
|
|
429
545
|
if (expected.get(managed.environment) === "running") continue;
|
|
430
|
-
const settled = await
|
|
431
|
-
|
|
432
|
-
"
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
546
|
+
const settled = await exclusively(host, managed.name, async () => {
|
|
547
|
+
if ((await restorePrevious(host, managed.name)) !== null) return false;
|
|
548
|
+
if ((await workspacePresent(host, managed.name)) !== "running") return false;
|
|
549
|
+
const stopping = await docker(host, [
|
|
550
|
+
"stop",
|
|
551
|
+
"--timeout",
|
|
552
|
+
String(WORKSPACE_STOP_TIMEOUT_SECONDS),
|
|
553
|
+
managed.name,
|
|
554
|
+
]);
|
|
555
|
+
return stopping.status === 0;
|
|
556
|
+
});
|
|
557
|
+
if (settled) stopped.push(managed.name);
|
|
437
558
|
}
|
|
438
559
|
return stopped;
|
|
439
560
|
}
|
|
440
561
|
|
|
562
|
+
function workspaceLock(identity) {
|
|
563
|
+
return new Promise((resolve, reject) => {
|
|
564
|
+
const lock = createServer();
|
|
565
|
+
lock.once("error", reject);
|
|
566
|
+
lock.listen({ path: `\0${WORKSPACE_LABEL}/${identity}` }, () => resolve(lock));
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function exclusively(host, identity, act) {
|
|
571
|
+
let lock = null;
|
|
572
|
+
while (lock === null) {
|
|
573
|
+
try {
|
|
574
|
+
lock = await workspaceLock(identity);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (error.code !== "EADDRINUSE") throw error;
|
|
577
|
+
await host.wait(WORKSPACE_LOCK_POLL_MS);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
return await act();
|
|
582
|
+
} finally {
|
|
583
|
+
await new Promise((resolve) => lock.close(resolve));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
441
587
|
export async function performOperation(host, settings, operation) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
588
|
+
const selected = operation.kind === "stop" ? settings : { ...settings, image: await selectedImage(host) };
|
|
589
|
+
const identity = operation.workspace ?? workspaceIdentity(operation);
|
|
590
|
+
return exclusively(host, identity, async () => {
|
|
591
|
+
const unrestored = await restorePrevious(host, identity);
|
|
592
|
+
if (unrestored !== null) return { outcome: "failed", refusal: unrestored };
|
|
593
|
+
if (operation.kind === "stop") return stopWorkspace(host, operation);
|
|
594
|
+
if (operation.kind === "create") return createWorkspace(host, selected, operation);
|
|
595
|
+
if (operation.kind === "start") return startWorkspace(host, selected, operation);
|
|
596
|
+
return deleteWorkspace(host, selected, operation);
|
|
597
|
+
});
|
|
446
598
|
}
|
|
447
599
|
|
|
448
|
-
|
|
600
|
+
async function selectedImage(host) {
|
|
601
|
+
let image;
|
|
602
|
+
try {
|
|
603
|
+
image = (await host.read(WORKSPACE_SELECTED_IMAGE)).trim();
|
|
604
|
+
} catch {
|
|
605
|
+
image = null;
|
|
606
|
+
}
|
|
607
|
+
if (image === null || !WORKSPACE_RELEASE_IMAGE.test(image)) {
|
|
608
|
+
throw new KitRefusal("kit_workspace_host_unconfigured", WORKSPACE_SELECTED_IMAGE);
|
|
609
|
+
}
|
|
610
|
+
return image;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function hostSettings(options) {
|
|
449
614
|
const configuration = await readConfiguration(options.configuration);
|
|
450
|
-
if (configuration.host === null
|
|
615
|
+
if (configuration.host === null) {
|
|
451
616
|
throw new KitRefusal("kit_workspace_host_unconfigured", options.configuration);
|
|
452
617
|
}
|
|
618
|
+
return { root: configuration.host.workspace, house: options.house };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function updateWorkspace(host, settings, name) {
|
|
622
|
+
try {
|
|
623
|
+
const unrestored = await restorePrevious(host, name);
|
|
624
|
+
if (unrestored !== null) return { workspace: name, cause: unrestored };
|
|
625
|
+
const held = await heldWorkspace(host, name);
|
|
626
|
+
if (held === null || held.image === settings.image) return null;
|
|
627
|
+
const running = !STOPPED_STATES.includes(held.state);
|
|
628
|
+
if (running) {
|
|
629
|
+
const drained = await docker(host, ["exec", name, "kit", "resident", "drain"]);
|
|
630
|
+
if (drained.status !== 0) {
|
|
631
|
+
return { workspace: name, cause: drained.stdout.trim() || drained.stderr.trim() };
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const refusal = await replaceWorkspace(host, settings, name, held, running);
|
|
635
|
+
if (refusal !== null) return { workspace: name, cause: refusal };
|
|
636
|
+
return { workspace: name, running };
|
|
637
|
+
} catch (error) {
|
|
638
|
+
if (!(error instanceof KitRefusal)) throw error;
|
|
639
|
+
return { workspace: name, cause: error.message };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export async function updateWorkspaces(options, dependencies = {}) {
|
|
644
|
+
const host = adapter(dependencies);
|
|
645
|
+
const updated = [];
|
|
646
|
+
const unchanged = [];
|
|
647
|
+
const managed = await managedWorkspaces(host);
|
|
648
|
+
if (managed.length === 0) return { updated, unchanged };
|
|
649
|
+
const settings = { ...(await hostSettings(options)), image: await selectedImage(host) };
|
|
650
|
+
for (const { name } of managed) {
|
|
651
|
+
const answer = await exclusively(host, name, () => updateWorkspace(host, settings, name));
|
|
652
|
+
if (answer === null) continue;
|
|
653
|
+
if (answer.cause === undefined) updated.push(answer);
|
|
654
|
+
else unchanged.push(answer);
|
|
655
|
+
}
|
|
656
|
+
return { updated, unchanged };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export async function serveWorkspaces(options, dependencies = {}) {
|
|
453
660
|
const host = adapter(dependencies);
|
|
454
|
-
const settings =
|
|
455
|
-
root: configuration.host.workspace,
|
|
456
|
-
image: configuration.host.image,
|
|
457
|
-
house: options.house,
|
|
458
|
-
};
|
|
661
|
+
const settings = await hostSettings(options);
|
|
459
662
|
const session = await openSession(options, dependencies);
|
|
460
663
|
const settled = [];
|
|
461
664
|
const withdrawn = [];
|
|
462
665
|
let reconciled = [];
|
|
463
666
|
const passes = options.passes ?? Number.POSITIVE_INFINITY;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
try {
|
|
479
|
-
await reportWorkspaceOutcome(session, operation.operation, report, signal);
|
|
480
|
-
} catch (error) {
|
|
481
|
-
if (!(error instanceof KitRefusal) || error.code !== WORKSPACE_OPERATION_WITHDRAWN) {
|
|
482
|
-
throw error;
|
|
667
|
+
try {
|
|
668
|
+
for (let pass = 0; pass < passes; pass += 1) {
|
|
669
|
+
const work = await maintainControl(
|
|
670
|
+
session,
|
|
671
|
+
async (signal) => {
|
|
672
|
+
const acquired = await acquireWorkspaces(session, WORKSPACE_ACQUIRE_MAX, signal);
|
|
673
|
+
reconciled = await reconcileWorkspaces(host, acquired.workspaces);
|
|
674
|
+
for (const operation of acquired.operations) {
|
|
675
|
+
let report;
|
|
676
|
+
try {
|
|
677
|
+
report = await performOperation(host, settings, operation);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if (!(error instanceof KitRefusal)) throw error;
|
|
680
|
+
report = { outcome: "failed", refusal: error.message };
|
|
483
681
|
}
|
|
484
|
-
|
|
485
|
-
|
|
682
|
+
try {
|
|
683
|
+
await reportWorkspaceOutcome(session, operation.operation, report, signal);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
if (!(error instanceof KitRefusal) || error.code !== WORKSPACE_OPERATION_WITHDRAWN) {
|
|
686
|
+
throw error;
|
|
687
|
+
}
|
|
688
|
+
withdrawn.push({ operation: operation.operation, kind: operation.kind });
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
settled.push({ operation: operation.operation, kind: operation.kind, ...report });
|
|
486
692
|
}
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
693
|
+
return acquired;
|
|
694
|
+
},
|
|
695
|
+
options.signal,
|
|
696
|
+
{ release: false },
|
|
697
|
+
).catch((error) => {
|
|
698
|
+
if (!houseFailed(error)) throw error;
|
|
699
|
+
return null;
|
|
700
|
+
});
|
|
701
|
+
if (pass + 1 < passes && (work === null || work.operations.length === 0)) {
|
|
702
|
+
await host.wait(WORKSPACE_POLL_MS);
|
|
703
|
+
}
|
|
496
704
|
}
|
|
705
|
+
} catch (error) {
|
|
706
|
+
if (!(error instanceof KitRefusal) || error.code !== "kit_command_cancelled") throw error;
|
|
707
|
+
} finally {
|
|
708
|
+
await releaseControl(session);
|
|
497
709
|
}
|
|
498
|
-
await releaseControl(session);
|
|
499
710
|
return { settled, reconciled, withdrawn };
|
|
500
711
|
}
|
package/src/google/declared.js
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { ITEM_BODY_MAX_BYTES } from "../contract.js";
|
|
2
|
-
import { KitRefusal } from "../refusal.js";
|
|
3
|
-
import { googleJson } from "./gateway.js";
|
|
4
|
-
|
|
5
|
-
function valueAt(value, path) {
|
|
6
|
-
let current = value;
|
|
7
|
-
for (const segment of path.split(".")) {
|
|
8
|
-
if (current === null || current === undefined || typeof current !== "object") return undefined;
|
|
9
|
-
current = current[segment];
|
|
10
|
-
}
|
|
11
|
-
return current;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function requiredText(value, declaration) {
|
|
15
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
16
|
-
throw new KitRefusal("google_provider_response_invalid", declaration);
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function optionalText(value, declaration) {
|
|
22
|
-
if (value === undefined || value === null) return null;
|
|
23
|
-
return requiredText(value, declaration);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function cursorOf(value) {
|
|
27
|
-
if (value === null) return { pageToken: null, checkpoint: null };
|
|
28
|
-
let cursor;
|
|
29
|
-
try {
|
|
30
|
-
cursor = JSON.parse(value);
|
|
31
|
-
} catch {
|
|
32
|
-
throw new KitRefusal("google_checkpoint_invalid", "declared service");
|
|
33
|
-
}
|
|
34
|
-
if (
|
|
35
|
-
typeof cursor !== "object" ||
|
|
36
|
-
cursor === null ||
|
|
37
|
-
(cursor.pageToken !== null && typeof cursor.pageToken !== "string") ||
|
|
38
|
-
(cursor.checkpoint !== null && typeof cursor.checkpoint !== "string")
|
|
39
|
-
) {
|
|
40
|
-
throw new KitRefusal("google_checkpoint_invalid", "declared service");
|
|
41
|
-
}
|
|
42
|
-
return cursor;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function composedBody(item, paths) {
|
|
46
|
-
const parts = paths.map((path) => valueAt(item, path));
|
|
47
|
-
if (parts.some((part) => !["string", "number", "boolean"].includes(typeof part))) {
|
|
48
|
-
throw new KitRefusal("google_provider_response_invalid", "declared body");
|
|
49
|
-
}
|
|
50
|
-
const body = parts.map((part) => `${part}`).join("\n\n");
|
|
51
|
-
if (Buffer.byteLength(body, "utf8") > ITEM_BODY_MAX_BYTES) {
|
|
52
|
-
throw new KitRefusal("google_provider_response_invalid", "declared body");
|
|
53
|
-
}
|
|
54
|
-
return body;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function normalizedItem(service, item) {
|
|
58
|
-
const fields = service.declaration.fields;
|
|
59
|
-
const observedAt = requiredText(valueAt(item, fields.observedAt), "declared observed time");
|
|
60
|
-
if (Number.isNaN(Date.parse(observedAt))) {
|
|
61
|
-
throw new KitRefusal("google_provider_response_invalid", "declared observed time");
|
|
62
|
-
}
|
|
63
|
-
const container = requiredText(valueAt(item, fields.container), "declared container");
|
|
64
|
-
const nativeVersion =
|
|
65
|
-
fields.nativeVersion === null
|
|
66
|
-
? null
|
|
67
|
-
: optionalText(valueAt(item, fields.nativeVersion), "declared native version");
|
|
68
|
-
const providerModifiedAt =
|
|
69
|
-
fields.providerModifiedAt === null
|
|
70
|
-
? null
|
|
71
|
-
: optionalText(
|
|
72
|
-
valueAt(item, fields.providerModifiedAt),
|
|
73
|
-
"declared provider modification time",
|
|
74
|
-
);
|
|
75
|
-
if (providerModifiedAt !== null && Number.isNaN(Date.parse(providerModifiedAt))) {
|
|
76
|
-
throw new KitRefusal("google_provider_response_invalid", "declared provider modification time");
|
|
77
|
-
}
|
|
78
|
-
return {
|
|
79
|
-
namespace: `google/${service.service}`,
|
|
80
|
-
nativeObjectId: requiredText(valueAt(item, fields.nativeId), "declared native id"),
|
|
81
|
-
nativeVersion,
|
|
82
|
-
providerModifiedAt:
|
|
83
|
-
providerModifiedAt === null ? null : new Date(providerModifiedAt).toISOString(),
|
|
84
|
-
observedAt: new Date(observedAt).toISOString(),
|
|
85
|
-
body: composedBody(item, fields.body),
|
|
86
|
-
author:
|
|
87
|
-
fields.author === null
|
|
88
|
-
? null
|
|
89
|
-
: optionalText(valueAt(item, fields.author), "declared author"),
|
|
90
|
-
url:
|
|
91
|
-
fields.url === null ? null : optionalText(valueAt(item, fields.url), "declared url"),
|
|
92
|
-
attachments: [],
|
|
93
|
-
container: `google/${service.service}/${container}`,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export async function declaredPage(options, dependencies = {}) {
|
|
98
|
-
const declared = options.service.declaration;
|
|
99
|
-
const cursor = cursorOf(options.cursor);
|
|
100
|
-
const url = new URL(declared.path, `https://${declared.host}`);
|
|
101
|
-
for (const [name, value] of Object.entries(declared.query)) url.searchParams.set(name, value);
|
|
102
|
-
url.searchParams.set(declared.pageSize, `${options.limit}`);
|
|
103
|
-
if (cursor.pageToken !== null) {
|
|
104
|
-
url.searchParams.set(declared.pageToken.request, cursor.pageToken);
|
|
105
|
-
}
|
|
106
|
-
if (cursor.checkpoint !== null) {
|
|
107
|
-
url.searchParams.set(declared.pageToken.checkpointRequest, cursor.checkpoint);
|
|
108
|
-
}
|
|
109
|
-
const answer = await googleJson(
|
|
110
|
-
{
|
|
111
|
-
directory: options.directory,
|
|
112
|
-
instance: options.instance,
|
|
113
|
-
client: options.client,
|
|
114
|
-
requiredScopes: declared.scopes,
|
|
115
|
-
url: url.toString(),
|
|
116
|
-
signal: options.signal,
|
|
117
|
-
},
|
|
118
|
-
dependencies,
|
|
119
|
-
);
|
|
120
|
-
const entries = valueAt(answer, declared.items);
|
|
121
|
-
if (!Array.isArray(entries) || entries.length > options.limit) {
|
|
122
|
-
throw new KitRefusal("google_provider_response_invalid", "declared items");
|
|
123
|
-
}
|
|
124
|
-
const pageToken = valueAt(answer, declared.pageToken.response);
|
|
125
|
-
let next;
|
|
126
|
-
if (pageToken !== undefined && pageToken !== null) {
|
|
127
|
-
next = {
|
|
128
|
-
pageToken: requiredText(pageToken, "declared page token"),
|
|
129
|
-
checkpoint: cursor.checkpoint,
|
|
130
|
-
};
|
|
131
|
-
} else {
|
|
132
|
-
next = {
|
|
133
|
-
pageToken: null,
|
|
134
|
-
checkpoint: requiredText(
|
|
135
|
-
valueAt(answer, declared.pageToken.checkpointResponse),
|
|
136
|
-
"declared checkpoint",
|
|
137
|
-
),
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
return {
|
|
141
|
-
items: entries.map((entry) => normalizedItem(options.service, entry)),
|
|
142
|
-
dropped: [],
|
|
143
|
-
cursor: JSON.stringify(next),
|
|
144
|
-
more: pageToken !== undefined && pageToken !== null,
|
|
145
|
-
};
|
|
146
|
-
}
|
package/src/google/id.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const GOOGLE_DRIVE_ID = /^[A-Za-z0-9_-]{1,512}$/;
|
package/src/instance/google.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const GOOGLE_OAUTH_CLIENT = Object.freeze({"id":"911965338518-867gdvuddb7kdkjo3qvpv6gds369alb7.apps.googleusercontent.com","secret":"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"});
|