@agentshouse/kit 0.1.0-alpha.39 → 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 CHANGED
@@ -13,13 +13,13 @@ The Skill set Kit installs is Matt Pocock's
13
13
  Linux (`amd64`, `arm64`) or macOS (Apple silicon, Intel):
14
14
 
15
15
  ```sh
16
- bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-linux.sh)
16
+ bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.40/connect-linux.sh)
17
17
  ```
18
18
 
19
19
  Windows 11 (`amd64`), in PowerShell:
20
20
 
21
21
  ```powershell
22
- & ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-windows.ps1)))
22
+ & ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.40/connect-windows.ps1)))
23
23
  ```
24
24
 
25
25
  The command sets up Docker if it is missing, creates `~/.house-kit` (Kit home)
package/bootstrap.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.1.0-alpha.39",
3
+ "version": "0.1.0-alpha.40",
4
4
  "hosts": {
5
5
  "linux": {
6
- "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-linux.sh)",
6
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.40/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.39/connect-linux.sh"
9
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.40/connect-linux.sh"
10
10
  },
11
11
  "workspace": {
12
12
  "argument": "--workspace",
@@ -14,10 +14,10 @@
14
14
  }
15
15
  },
16
16
  "windows": {
17
- "command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-windows.ps1)))",
17
+ "command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.40/connect-windows.ps1)))",
18
18
  "asset": {
19
19
  "name": "connect-windows.ps1",
20
- "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.40/connect-windows.ps1"
21
21
  },
22
22
  "workspace": {
23
23
  "argument": "--workspace",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.1.0-alpha.39",
3
+ "version": "0.1.0-alpha.40",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/contract.js CHANGED
@@ -279,6 +279,7 @@ export const WORKSPACE_LABEL = "house-kit.workspace";
279
279
  export const WORKSPACE_KIT_HOME = "/kit-home";
280
280
  export const WORKSPACE_DISK_IMAGE = "disk.img";
281
281
  export const WORKSPACE_DISK_MOUNT = "mnt";
282
+ export const WORKSPACE_PREVIOUS_SUFFIX = "-previous";
282
283
  export const WORKSPACE_DISK_KIT_HOME = "kit-home";
283
284
  export const WORKSPACE_DISK_HOUSE = "house";
284
285
  export const WORKSPACE_SELECTED_IMAGE = "/opt/house-kit/image";
@@ -43,9 +43,7 @@ export async function holdConversationFile(context, file, bytes) {
43
43
  const path = statePath(context.directory, identity);
44
44
  const noted = await readNote(path, null);
45
45
  const observed = bytes === null || oversized ? null : describedBytes(bytes);
46
- // House fixes a version's descriptor the first time one is declared and refuses
47
- // every later one, so a file held again keeps the descriptor its note carries.
48
- // Bytes that no longer answer that descriptor are not this original's bytes.
46
+
49
47
  const descriptor = noted?.descriptor ?? observed;
50
48
  const beyondBound = descriptor === null && (oversized || noted?.exceeded === true);
51
49
  const held = {
package/src/workspace.js CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  WORKSPACE_LOCK_POLL_MS,
20
20
  WORKSPACE_OPERATION_WITHDRAWN,
21
21
  WORKSPACE_POLL_MS,
22
+ WORKSPACE_PREVIOUS_SUFFIX,
22
23
  WORKSPACE_READY_ATTEMPTS,
23
24
  WORKSPACE_READY_POLL_MS,
24
25
  WORKSPACE_RELEASE_IMAGE,
@@ -353,17 +354,72 @@ async function heldWorkspace(host, identity) {
353
354
  };
354
355
  }
355
356
 
356
- async function replaceWorkspace(host, settings, identity, held) {
357
+ function previousName(identity) {
358
+ return `${identity}${WORKSPACE_PREVIOUS_SUFFIX}`;
359
+ }
360
+
361
+ async function replaceWorkspace(host, settings, identity, held, running) {
357
362
  const mountRefusal = await mountWorkspace(host, settings, identity);
358
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) {
359
389
  await onDisk(host, settings, identity, `rm -f ${updateDrain(`${PROVISIONED_DISK}/${WORKSPACE_DISK_KIT_HOME}`)}`);
360
- const removed = await docker(host, ["rm", "--force", identity]);
361
- if (removed.status !== 0) return `the workspace was not replaced: ${removed.stderr.trim()}`;
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()}`;
362
392
  const created = await docker(
363
393
  host,
364
394
  workspaceCreateArguments(identity, held, settings.image, settings.house, settings.root),
365
395
  );
366
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()}`;
367
423
  return null;
368
424
  }
369
425
 
@@ -381,7 +437,7 @@ export async function startWorkspace(host, settings, operation) {
381
437
  const mountRefusal = await mountWorkspace(host, settings, identity);
382
438
  if (mountRefusal !== null) return { outcome: "unsupported", refusal: mountRefusal };
383
439
  if (STOPPED_STATES.includes(held.state) && held.image !== settings.image) {
384
- const refusal = await replaceWorkspace(host, settings, identity, held);
440
+ const refusal = await replaceWorkspace(host, settings, identity, held, false);
385
441
  if (refusal !== null) return { outcome: "failed", refusal };
386
442
  }
387
443
  const started = await docker(host, ["start", identity]);
@@ -469,14 +525,15 @@ export async function managedWorkspaces(host) {
469
525
  if (listed.status !== 0) {
470
526
  throw new KitRefusal("kit_workspace_inventory_unavailable", listed.stderr.trim() || null);
471
527
  }
472
- return listed.stdout
473
- .split("\n")
474
- .map((line) => line.trim())
475
- .filter((line) => line !== "")
476
- .map((line) => {
477
- const [name, environment, state] = line.split(/\s+/);
478
- return { name, environment, state };
479
- });
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()];
480
537
  }
481
538
 
482
539
  export async function reconcileWorkspaces(host, authorized) {
@@ -487,6 +544,7 @@ export async function reconcileWorkspaces(host, authorized) {
487
544
  for (const managed of await managedWorkspaces(host)) {
488
545
  if (expected.get(managed.environment) === "running") continue;
489
546
  const settled = await exclusively(host, managed.name, async () => {
547
+ if ((await restorePrevious(host, managed.name)) !== null) return false;
490
548
  if ((await workspacePresent(host, managed.name)) !== "running") return false;
491
549
  const stopping = await docker(host, [
492
550
  "stop",
@@ -528,7 +586,10 @@ async function exclusively(host, identity, act) {
528
586
 
529
587
  export async function performOperation(host, settings, operation) {
530
588
  const selected = operation.kind === "stop" ? settings : { ...settings, image: await selectedImage(host) };
531
- return exclusively(host, operation.workspace ?? workspaceIdentity(operation), () => {
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 };
532
593
  if (operation.kind === "stop") return stopWorkspace(host, operation);
533
594
  if (operation.kind === "create") return createWorkspace(host, selected, operation);
534
595
  if (operation.kind === "start") return startWorkspace(host, selected, operation);
@@ -559,6 +620,8 @@ async function hostSettings(options) {
559
620
 
560
621
  async function updateWorkspace(host, settings, name) {
561
622
  try {
623
+ const unrestored = await restorePrevious(host, name);
624
+ if (unrestored !== null) return { workspace: name, cause: unrestored };
562
625
  const held = await heldWorkspace(host, name);
563
626
  if (held === null || held.image === settings.image) return null;
564
627
  const running = !STOPPED_STATES.includes(held.state);
@@ -568,14 +631,8 @@ async function updateWorkspace(host, settings, name) {
568
631
  return { workspace: name, cause: drained.stdout.trim() || drained.stderr.trim() };
569
632
  }
570
633
  }
571
- const refusal = await replaceWorkspace(host, settings, name, held);
634
+ const refusal = await replaceWorkspace(host, settings, name, held, running);
572
635
  if (refusal !== null) return { workspace: name, cause: refusal };
573
- if (running) {
574
- const started = await docker(host, ["start", name]);
575
- if (started.status !== 0) {
576
- return { workspace: name, cause: `the workspace did not start: ${started.stderr.trim()}` };
577
- }
578
- }
579
636
  return { workspace: name, running };
580
637
  } catch (error) {
581
638
  if (!(error instanceof KitRefusal)) throw error;