@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.
- package/README.md +8 -4
- package/dist/cli/commandCatalog.js +6 -1
- package/dist/cli/invocationRouter.js +2 -1
- package/dist/cli/updatePorts.js +63 -9
- package/dist/cli.js +23 -0
- package/dist/controller/clientRuntime.js +26 -3
- package/dist/controller/resourceCleanupLinux.js +2 -1
- package/dist/controller/resourceInventory.js +4 -2
- package/dist/controller/resourceInventoryLinux.js +62 -5
- package/dist/controller/runtime.js +2 -1
- package/dist/controller/updateReconciliation.js +113 -0
- package/dist/core/controllerClient.js +141 -7
- package/dist/core/controllerEndpoint.js +18 -30
- package/dist/core/controllerProcessIdentity.js +56 -0
- package/dist/core/controllerServer.js +97 -9
- package/dist/core/homeFilesystemIdentity.js +24 -0
- package/dist/core/protocol.js +68 -5
- package/dist/integration/gitIntegrationService.js +3 -3
- package/dist/repository/homeIdentity.js +7 -3
- package/dist/resources/liveReferences.js +35 -2
- package/dist/storage/compatibleTaskStore.js +36 -1
- package/dist/storage/sqliteStore.js +25 -1
- package/dist/storage/taskStore.js +48 -3
- package/package.json +1 -1
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|