@zq-silk/yui 0.6.6 → 0.6.8

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.
@@ -59,7 +59,7 @@ export class GitIntegrationService {
59
59
  environment;
60
60
  runtimeIsolation;
61
61
  #resourceRegistrarValue;
62
- constructor(home, store, git = new NodeGitWorkspace(), now = () => new Date(), environment = process.env, runtimeIsolation = defaultIntegrationRuntimeIsolation(home), jobPort) {
62
+ constructor(home, store, git = new NodeGitWorkspace(), now = () => new Date(), environment = process.env, runtimeIsolation = defaultIntegrationRuntimeIsolation(home, store.getHomeIdentity().homeId), jobPort) {
63
63
  this.store = store;
64
64
  this.git = git;
65
65
  this.now = now;
@@ -883,14 +883,14 @@ function isNodeCode(error, code) {
883
883
  && "code" in error
884
884
  && error.code === code;
885
885
  }
886
- function defaultIntegrationRuntimeIsolation(home) {
886
+ function defaultIntegrationRuntimeIsolation(home, homeId) {
887
887
  const controlHome = resolve(home);
888
888
  return new FileTaskRuntimeIsolation({
889
889
  runtimeRoot: integrationRuntimeRoot(),
890
890
  pathLayout: "compact",
891
891
  controlPlane: {
892
892
  yuiHome: controlHome,
893
- controllerSocketPath: controllerSocketPath(controlHome),
893
+ controllerSocketPath: controllerSocketPath(homeId),
894
894
  tmuxNamespace: yuiTmuxServerName(controlHome),
895
895
  globalInstallPaths: [process.execPath]
896
896
  }
@@ -1,6 +1,12 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  export const HOME_IDENTITY_PATTERN = /^home-[a-f0-9]{16}$/;
3
3
  const HOME_ENTROPY_BYTES = 16;
4
+ export function validateHomeId(homeId) {
5
+ if (typeof homeId !== "string" || !HOME_IDENTITY_PATTERN.test(homeId)) {
6
+ throw new Error("Home identity id is invalid.");
7
+ }
8
+ return homeId;
9
+ }
4
10
  export function generateHomeIdentity(now, source = () => randomBytes(HOME_ENTROPY_BYTES)) {
5
11
  const entropy = source().toString("hex");
6
12
  const homeId = `home-${randomBytes(8).toString("hex")}`;
@@ -15,9 +21,7 @@ export function validateHomeIdentity(identity) {
15
21
  if (identity.schemaVersion !== 1) {
16
22
  throw new Error("Home identity must use schemaVersion 1.");
17
23
  }
18
- if (typeof identity.homeId !== "string" || !HOME_IDENTITY_PATTERN.test(identity.homeId)) {
19
- throw new Error("Home identity id is invalid.");
20
- }
24
+ validateHomeId(identity.homeId);
21
25
  if (typeof identity.entropy !== "string" || !/^[a-f0-9]{32}$/.test(identity.entropy)) {
22
26
  throw new Error("Home identity entropy is invalid.");
23
27
  }
@@ -12,6 +12,9 @@ import { join, resolve } from "node:path";
12
12
  import { promisify } from "node:util";
13
13
  import { parseExactTaskRuntimeDescriptor } from "../runtime/exactControlPlane.js";
14
14
  import { readRuntimeIdentity, releasesDirectory, resolveActiveRelease } from "../release/runtimeRelease.js";
15
+ import { isControllerSocketPathForHome } from "../core/controllerEndpoint.js";
16
+ import { parseControllerDiscovery } from "../core/protocol.js";
17
+ import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
15
18
  const executeFile = promisify(execFile);
16
19
  /**
17
20
  * Collect every live reference for the given paths. The scan is fail-closed:
@@ -481,7 +484,11 @@ export function readControllerDiscovery(home) {
481
484
  }
482
485
  let record;
483
486
  try {
484
- record = JSON.parse(readFileSync(path, "utf8"));
487
+ const value = JSON.parse(readFileSync(path, "utf8"));
488
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
489
+ throw new Error("controller.json is not an object");
490
+ }
491
+ record = value;
485
492
  }
486
493
  catch (error) {
487
494
  return Object.freeze({
@@ -537,9 +544,35 @@ export function readControllerDiscovery(home) {
537
544
  }
538
545
  });
539
546
  }
547
+ let discovery;
548
+ try {
549
+ const homeId = record.homeId;
550
+ const socketPath = record.socketPath;
551
+ if (typeof homeId !== "string"
552
+ || typeof socketPath !== "string"
553
+ || !isControllerSocketPathForHome(homeId, socketPath)) {
554
+ throw new Error("Controller endpoint identity is invalid.");
555
+ }
556
+ discovery = parseControllerDiscovery(record, {
557
+ homeFilesystemId: readHomeFilesystemId(home),
558
+ socketPath
559
+ });
560
+ }
561
+ catch (error) {
562
+ return Object.freeze({
563
+ protects: false,
564
+ token: "",
565
+ protectedPaths: Object.freeze([]),
566
+ diagnostic: {
567
+ source: "controller",
568
+ severity: "error",
569
+ message: `controller.json identity is invalid: ${error instanceof Error ? error.message : "unknown error"}`
570
+ }
571
+ });
572
+ }
540
573
  return Object.freeze({
541
574
  protects: true,
542
- token: `controller:${record.pid}`,
575
+ token: `controller:${discovery.homeId}:${discovery.controllerInstanceId}:${record.pid}`,
543
576
  protectedPaths: Object.freeze([path])
544
577
  });
545
578
  }
@@ -3,8 +3,9 @@ import { join } from "node:path";
3
3
  import { StorageCompatibilityError, loadCompatibleSnapshot } from "./migration/index.js";
4
4
  import { createProductionStorageRegistry } from "./migration/productionRegistry.js";
5
5
  import { FileTaskStore, STORAGE_STATE_FILE, stateFileFingerprint, StorageRecordError, validateCurrentStorageStateSnapshot } from "./taskStore.js";
6
- import { SqliteTaskStore } from "./sqliteStore.js";
6
+ import { readSqliteHomeIdentity, SqliteTaskStore } from "./sqliteStore.js";
7
7
  import { COMMITTED_DATABASE_FILENAME } from "./upgrade/sqliteStateMigration.js";
8
+ import { validateHomeIdentity } from "../repository/homeIdentity.js";
8
9
  import { ensureStorageSchema, inspectStorageSchema, readStorageSchemaManifest, STORAGE_SCHEMA_FILE } from "./storageSchema.js";
9
10
  import { classifyHome } from "./upgrade/homeClassification.js";
10
11
  import { inspectSnapshotVersionState } from "./upgrade/homeMigrationTarget.js";
@@ -81,6 +82,40 @@ export function openCompatibleFileTaskStore(home, options = {}) {
81
82
  throw new StorageCompatibilityError(describeUnsupported(classification.classification));
82
83
  }
83
84
  }
85
+ /**
86
+ * Read the one authoritative durable Home identity without creating runtime
87
+ * state. SQLite is opened read-only; file-backed Homes read only the immutable
88
+ * identity field, so an aggregate that requires offline migration can still
89
+ * authenticate its already-running Controller before the migration stops it.
90
+ */
91
+ export function readCompatibleHomeIdentity(home, options = {}) {
92
+ const schema = inspectStorageSchema(home);
93
+ if (!options.forceStateSource
94
+ && (schema.status === "current" || schema.status === "unsupported")
95
+ && schema.currentLayoutVersion >= 7
96
+ && existsSync(join(home, COMMITTED_DATABASE_FILENAME))) {
97
+ return readSqliteHomeIdentity(home, COMMITTED_DATABASE_FILENAME);
98
+ }
99
+ if (!existsSync(join(home, STORAGE_STATE_FILE))) {
100
+ throw new StorageRecordError("Durable Home identity is missing.");
101
+ }
102
+ let parsed;
103
+ try {
104
+ parsed = JSON.parse(readFileSync(join(home, STORAGE_STATE_FILE), "utf8"));
105
+ }
106
+ catch {
107
+ throw new StorageRecordError("Durable Home identity state is invalid JSON.");
108
+ }
109
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
110
+ throw new StorageRecordError("Durable Home identity state is invalid.");
111
+ }
112
+ try {
113
+ return validateHomeIdentity(parsed.homeIdentity);
114
+ }
115
+ catch {
116
+ throw new StorageRecordError("Durable Home identity is invalid.");
117
+ }
118
+ }
84
119
  /**
85
120
  * The one-snapshot open for a Home whose manifest is already current. A single
86
121
  * fingerprint-fenced read of `state.json` supplies:
@@ -43,7 +43,7 @@ import { mailboxTargetKey } from "../coordination/workMailbox.js";
43
43
  import { validatePendingTurnCompletion } from "../executor/turnCompletion.js";
44
44
  import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
45
45
  import { validateReviewFinding } from "../review/reviewFinding.js";
46
- import { generateHomeIdentity } from "../repository/homeIdentity.js";
46
+ import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeIdentity.js";
47
47
  import { validateIntegrationQueueEntry } from "../integration/integrationQueueEntry.js";
48
48
  import { validDurableJobTransition, validateDurableJob } from "../job/durableJob.js";
49
49
  import { TASK_RECORD_ID_PREFIXES } from "../task/taskRecordReference.js";
@@ -53,6 +53,30 @@ import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_PENDING_WAKEUP_SCHEMA_VERSION, C
53
53
  import { gateArtifactKey, validateGateArtifact } from "../verification/gateArtifact.js";
54
54
  import { migrateSqliteSchema, SQLITE_AGGREGATE_VERSION, SQLITE_LAYOUT_VERSION, TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_RUN_CAP } from "./sqliteSchema.js";
55
55
  import { inspectStorageSchema } from "./storageSchema.js";
56
+ /** Read the immutable Home identity without opening a writable Store connection. */
57
+ export function readSqliteHomeIdentity(rootDir, databaseFilename = "yui.db") {
58
+ const database = new Database(join(rootDir, databaseFilename), {
59
+ readonly: true,
60
+ fileMustExist: true
61
+ });
62
+ try {
63
+ const row = database.prepare("SELECT home_identity FROM home_meta WHERE id = 1").get();
64
+ if (typeof row?.home_identity !== "string") {
65
+ throw new StorageRecordError("SQLite Home identity is missing.");
66
+ }
67
+ let parsed;
68
+ try {
69
+ parsed = JSON.parse(row.home_identity);
70
+ }
71
+ catch {
72
+ throw new StorageRecordError("SQLite Home identity is invalid JSON.");
73
+ }
74
+ return validateHomeIdentity(parsed);
75
+ }
76
+ finally {
77
+ database.close();
78
+ }
79
+ }
56
80
  const DEFAULT_CONFIG = { schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION };
57
81
  function numericCompare(left, right) {
58
82
  return left.localeCompare(right, undefined, { numeric: true });
@@ -1,6 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { join, resolve } from "node:path";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { validateConfiguredAgent } from "../agent/agent.js";
6
6
  import { validateCapabilityGrant } from "../grant/capabilityGrant.js";
@@ -1735,11 +1735,56 @@ export class StorageCancelledError extends Error {
1735
1735
  constructor(message) { super(message); this.name = "StorageCancelledError"; }
1736
1736
  }
1737
1737
  export function resolveYuiHome(env) {
1738
- return env.YUI_HOME === undefined || env.YUI_HOME.length === 0
1738
+ const requested = env.YUI_HOME === undefined || env.YUI_HOME.length === 0
1739
1739
  ? join(homedir(), ".yui")
1740
1740
  : resolve(env.YUI_HOME);
1741
+ return canonicalizeYuiHome(requested);
1741
1742
  }
1742
1743
  export function ensureYuiHome(rootDir) { mkdirSync(rootDir, { recursive: true, mode: 0o700 }); }
1744
+ /**
1745
+ * YUI_HOME is a runtime identity, not only a storage path. Resolve every
1746
+ * existing symlink component before Controller/tmux namespaces or exact
1747
+ * descriptors derive identity from it. A Home may not exist before `setup`,
1748
+ * so retain proven-missing trailing components below the longest existing
1749
+ * physical ancestor. Existing-but-unresolvable paths fail closed.
1750
+ */
1751
+ function canonicalizeYuiHome(value) {
1752
+ const absolute = resolve(value);
1753
+ let current = absolute;
1754
+ const trailing = [];
1755
+ for (;;) {
1756
+ try {
1757
+ const physical = realpathSync(current);
1758
+ return trailing.length === 0 ? physical : join(physical, ...trailing);
1759
+ }
1760
+ catch (realpathError) {
1761
+ if (!isErrno(realpathError, "ENOENT")) {
1762
+ throw unstableYuiHome(absolute, realpathError);
1763
+ }
1764
+ try {
1765
+ lstatSync(current);
1766
+ }
1767
+ catch (lstatError) {
1768
+ if (!isErrno(lstatError, "ENOENT")) {
1769
+ throw unstableYuiHome(absolute, lstatError);
1770
+ }
1771
+ const parent = dirname(current);
1772
+ if (parent === current)
1773
+ return absolute;
1774
+ trailing.unshift(basename(current));
1775
+ current = parent;
1776
+ continue;
1777
+ }
1778
+ throw unstableYuiHome(absolute, realpathError);
1779
+ }
1780
+ }
1781
+ }
1782
+ function unstableYuiHome(path, cause) {
1783
+ return new Error(`YUI_HOME cannot be resolved to a stable physical path: ${path}.`, { cause });
1784
+ }
1785
+ function isErrno(error, code) {
1786
+ return error instanceof Error && "code" in error && error.code === code;
1787
+ }
1743
1788
  function emptyState() {
1744
1789
  return {
1745
1790
  schemaVersion: CURRENT_STORAGE_STATE_SCHEMA_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,