@awebai/oats 0.24.12 → 0.25.0
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/bin/oats.mjs +936 -2822
- package/docs/capabilities.md +136 -323
- package/docs/configuration.md +68 -533
- package/docs/design/2026-09-16-fresh-operator-walkthrough.md +19 -14
- package/docs/design/2026-09-16-portable-migration-evidence.md +2 -0
- package/docs/design/2026-09-16-portable-onboarding.md +4 -2
- package/docs/design/2026-09-20-redesign-program-board.md +1 -1
- package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +2 -0
- package/docs/design/2026-09-23-simplified-workspace-model.md +711 -0
- package/docs/design/2026-09-23-workspace-module-contracts.md +309 -0
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +65 -0
- package/docs/design/README.md +20 -8
- package/docs/design/operations-contract.md +1 -0
- package/docs/design/package-engine-contract.md +1 -1
- package/docs/design/package-runtime-api.md +1 -1
- package/docs/desktop-cli-api.md +386 -6
- package/docs/desktop-succession.md +3 -4
- package/docs/first-team.md +107 -224
- package/docs/implementation.md +6 -4
- package/docs/integrations.md +45 -44
- package/docs/knowledge-capability-authoring.md +10 -4
- package/docs/knowledge-migration.md +5 -4
- package/docs/knowledge-reference/package-craft.md +11 -3
- package/docs/knowledge.md +24 -8
- package/docs/layers.md +3 -3
- package/docs/oats-local.schema.json +50 -0
- package/docs/oats-membership.schema.json +23 -0
- package/docs/oats-workspace.schema.json +133 -48
- package/docs/official-marketplace.md +9 -6
- package/docs/packages.md +229 -440
- package/docs/rebuild-to-v2.md +233 -0
- package/docs/release-notes/v0.24.13.md +51 -0
- package/docs/release-notes/v0.25.0.md +99 -0
- package/docs/soul.schema.json +41 -68
- package/docs/souls-and-instances.md +175 -108
- package/docs/workspace-adoption.md +70 -345
- package/docs/workspaces.md +429 -119
- package/lib/core.mjs +419 -55
- package/lib/instance-resolution.mjs +312 -0
- package/lib/materialize.mjs +580 -0
- package/lib/packages.mjs +501 -1273
- package/lib/remote.mjs +639 -0
- package/lib/resolve.mjs +576 -0
- package/lib/schedule.mjs +194 -34
- package/lib/workspace.mjs +635 -0
- package/package.json +1 -1
- package/lib/portable-migration-artifacts.mjs +0 -135
- package/lib/portable-migration-evidence.mjs +0 -305
- package/lib/portable-migration-store.mjs +0 -199
- package/lib/portable-migration.mjs +0 -104
- package/lib/portable-onboarding-acceptance.mjs +0 -66
- package/lib/setup-expert-source.mjs +0 -100
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/** Read-only verification of explicit historical materialized-v2 artifact
|
|
2
|
-
* candidates. Verification proves the old lock/artifact relation and observes a
|
|
3
|
-
* new owner-exec digest today; it does not prove per-home selection, historical
|
|
4
|
-
* modes, retention, reconstruction or approval. */
|
|
5
|
-
import { lstatSync, realpathSync } from "node:fs";
|
|
6
|
-
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
-
import { capabilityArtifactIntegrity } from "./artifact-tree.mjs";
|
|
8
|
-
import { CAPABILITY_INSTALLATION_FILE, isMaterializedCapabilityId } from "./capability-provenance.mjs";
|
|
9
|
-
import { bytesIntegrity, TREE_FORMAT, treeIntegrity } from "./portable-digest.mjs";
|
|
10
|
-
import { readPortableBytes } from "./portable-files.mjs";
|
|
11
|
-
import { parseStrictJson } from "./portable-values.mjs";
|
|
12
|
-
import { portableScope } from "./portable-state.mjs";
|
|
13
|
-
import { verifyHistoricalLockCandidate } from "./portable-migration-evidence.mjs";
|
|
14
|
-
import { oatsError } from "./errors.mjs";
|
|
15
|
-
|
|
16
|
-
export const LEGACY_CAPABILITY_FORMAT = "oats.capability-legacy.v1";
|
|
17
|
-
export const MATERIALIZED_CAPABILITY_FORMAT = "oats.capability-artifact.v1";
|
|
18
|
-
|
|
19
|
-
function verifyLegacyManifestIdentity(dir, capability, row) {
|
|
20
|
-
const path = join(dir, "oats.json"), bytes = readPortableBytes(path, { missingCode: "invalid-lock", invalidCode: "invalid-lock" });
|
|
21
|
-
let document;
|
|
22
|
-
try { document = parseStrictJson(bytes); }
|
|
23
|
-
catch (error) { const failure = oatsError("invalid-lock", `historical oats.json is malformed: ${error.message}`); failure.cause = error; throw failure; }
|
|
24
|
-
if (document?.capability !== capability || document?.version !== row.version) {
|
|
25
|
-
throw oatsError("invalid-lock", "historical oats.json identity/version differs from the literal v1 lock row");
|
|
26
|
-
}
|
|
27
|
-
return { path: "oats.json", integrity: bytesIntegrity(bytes) };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function verifyInstallationProvenance(dir, capability, row, pkg) {
|
|
31
|
-
const path = join(dir, CAPABILITY_INSTALLATION_FILE);
|
|
32
|
-
const bytes = readPortableBytes(path, { missingCode: "invalid-lock", invalidCode: "invalid-lock" });
|
|
33
|
-
let document;
|
|
34
|
-
try { document = parseStrictJson(bytes); }
|
|
35
|
-
catch (error) { const failure = oatsError("invalid-lock", `historical ${CAPABILITY_INSTALLATION_FILE} is malformed: ${error.message}`); failure.cause = error; throw failure; }
|
|
36
|
-
const expected = { schemaVersion: 1, capability, version: row.version, package: row.package,
|
|
37
|
-
packageVersion: pkg.version, source: pkg.source, commit: pkg.commit,
|
|
38
|
-
packagePath: pkg.path, capabilityPath: row.path };
|
|
39
|
-
for (const [key, value] of Object.entries(expected)) if (document?.[key] !== value) {
|
|
40
|
-
throw oatsError("invalid-lock", `historical ${CAPABILITY_INSTALLATION_FILE} differs from the verified lock rows at ${key}`);
|
|
41
|
-
}
|
|
42
|
-
return { path: CAPABILITY_INSTALLATION_FILE, integrity: bytesIntegrity(bytes) };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function explicitArtifactDirectory(scope, input) {
|
|
46
|
-
if (typeof input !== "string" || !input || input.includes("\0")) throw oatsError("invalid-evidence", "historical artifact target must be a non-empty path without NUL");
|
|
47
|
-
const deployment = portableScope(scope), lexical = resolve(deployment, input);
|
|
48
|
-
const part = relative(deployment, lexical);
|
|
49
|
-
if (!part || part === ".." || part.startsWith(`..${sep}`) || isAbsolute(part)) throw oatsError("path-escape", "historical artifact target is outside the explicit deployment");
|
|
50
|
-
let actual;
|
|
51
|
-
try { actual = realpathSync(lexical); }
|
|
52
|
-
catch (error) { if (error.code === "ENOENT") throw oatsError("artifact-not-found", "historical artifact candidate is absent"); throw error; }
|
|
53
|
-
if (actual !== lexical || !lstatSync(actual).isDirectory()) throw oatsError("invalid-artifact", "historical artifact target must be a real non-symlinked directory");
|
|
54
|
-
return { deployment, dir: actual, path: part.split(sep).join("/") };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Verify one explicit v1/v2 flat artifact against one explicit verified
|
|
58
|
-
* lock. V1's literal digest implementation is supplied as an acyclic callback;
|
|
59
|
-
* the migration module never imports core or silently substitutes the v2 hash. */
|
|
60
|
-
export function verifyHistoricalCapabilityCandidate(scope, inventory, { lockPath, capability, artifactDir }, adapters = {}) {
|
|
61
|
-
const { legacyLockDecoder, legacyCapabilityDigest } = adapters;
|
|
62
|
-
if (!isMaterializedCapabilityId(capability)) throw oatsError("invalid-evidence", "invalid historical capability identity");
|
|
63
|
-
const lock = verifyHistoricalLockCandidate(scope, inventory, lockPath, { legacyLockDecoder });
|
|
64
|
-
const row = lock.version === 1 ? lock.legacyCapabilities[capability] : lock.capabilities[capability];
|
|
65
|
-
if (!row) throw oatsError("migration-held", "historical lock does not name this capability candidate");
|
|
66
|
-
const target = explicitArtifactDirectory(scope, artifactDir);
|
|
67
|
-
if (lock.version === 1) {
|
|
68
|
-
if (typeof legacyCapabilityDigest !== "function") throw oatsError("migration-held", "legacy v1 capability candidate requires its literal historical digest verifier");
|
|
69
|
-
const manifest = verifyLegacyManifestIdentity(target.dir, capability, row);
|
|
70
|
-
const observedIntegrity = treeIntegrity(target.dir, { format: TREE_FORMAT });
|
|
71
|
-
const historical = legacyCapabilityDigest(target.dir);
|
|
72
|
-
if (historical !== row.integrity) throw oatsError("integrity-drift", "historical legacy artifact differs from its literal v1 lock digest");
|
|
73
|
-
const finalHistorical = legacyCapabilityDigest(target.dir), finalObserved = treeIntegrity(target.dir, { format: TREE_FORMAT });
|
|
74
|
-
const finalManifest = verifyLegacyManifestIdentity(target.dir, capability, row);
|
|
75
|
-
if (finalHistorical !== historical || finalObserved.value !== observedIntegrity.value
|
|
76
|
-
|| finalManifest.integrity.value !== manifest.integrity.value) {
|
|
77
|
-
throw oatsError("integrity-drift", "historical legacy artifact or manifest changed while its migration witness was measured");
|
|
78
|
-
}
|
|
79
|
-
return { schemaVersion: 1, lock: { path: lock.path, integrity: lock.integrity }, capability: { id: capability, row }, package: null,
|
|
80
|
-
artifact: { path: target.path, historicalIntegrity: { format: LEGACY_CAPABILITY_FORMAT, value: historical },
|
|
81
|
-
observedIntegrity, manifest, modeEvidence: "observed-at-migration" },
|
|
82
|
-
selectionAuthority: "none", approvalAuthority: "none", retentionStatus: "not-retained", evidence: lock.evidence };
|
|
83
|
-
}
|
|
84
|
-
const pkg = lock.packages[row.package];
|
|
85
|
-
if (!pkg) throw oatsError("invalid-lock", "historical capability candidate has no verified provider package row");
|
|
86
|
-
// Preflight bounded/no-follow provenance before the unbounded historical
|
|
87
|
-
// hasher can read it, then require the same exact bytes after both digest
|
|
88
|
-
// measurements. The old hash algorithm itself remains literal and unchanged.
|
|
89
|
-
const provenance = verifyInstallationProvenance(target.dir, capability, row, pkg);
|
|
90
|
-
const observedIntegrity = treeIntegrity(target.dir, { format: TREE_FORMAT });
|
|
91
|
-
const historical = capabilityArtifactIntegrity(target.dir);
|
|
92
|
-
if (historical !== row.integrity) throw oatsError("integrity-drift", "historical materialized artifact differs from its literal v2 lock digest");
|
|
93
|
-
const finalHistorical = capabilityArtifactIntegrity(target.dir);
|
|
94
|
-
const finalObserved = treeIntegrity(target.dir, { format: TREE_FORMAT });
|
|
95
|
-
const finalProvenance = verifyInstallationProvenance(target.dir, capability, row, pkg);
|
|
96
|
-
if (finalHistorical !== historical || finalObserved.value !== observedIntegrity.value
|
|
97
|
-
|| finalProvenance.integrity.value !== provenance.integrity.value) {
|
|
98
|
-
throw oatsError("integrity-drift", "historical artifact or provenance changed while its migration witness was measured");
|
|
99
|
-
}
|
|
100
|
-
return {
|
|
101
|
-
schemaVersion: 1,
|
|
102
|
-
lock: { path: lock.path, integrity: lock.integrity },
|
|
103
|
-
capability: { id: capability, row }, package: { id: row.package, row: pkg },
|
|
104
|
-
artifact: { path: target.path,
|
|
105
|
-
historicalIntegrity: { format: MATERIALIZED_CAPABILITY_FORMAT, value: historical },
|
|
106
|
-
observedIntegrity, provenance,
|
|
107
|
-
modeEvidence: "observed-at-migration" },
|
|
108
|
-
selectionAuthority: "none", approvalAuthority: "none", retentionStatus: "not-retained",
|
|
109
|
-
evidence: lock.evidence,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Establish only that one unchanged home metadata document named one exact v2
|
|
114
|
-
* artifact digest. This is per-capability selection evidence, not a complete
|
|
115
|
-
* composition, source/resource closure or transferred approval. */
|
|
116
|
-
export function verifyHistoricalHomeCapabilityCandidate(scope, inventory, request, adapters = {}) {
|
|
117
|
-
if (typeof request?.homePath !== "string" || !request.homePath) throw oatsError("invalid-evidence", "historical home candidate needs an explicit inventory path");
|
|
118
|
-
const artifact = verifyHistoricalCapabilityCandidate(scope, inventory, request, adapters);
|
|
119
|
-
const home = inventory.homes.find((entry) => entry.path === request.homePath);
|
|
120
|
-
if (!home || home.document.state !== "present") throw oatsError("migration-held", "historical home evidence is absent or invalid");
|
|
121
|
-
if (home.executionBinding.state === "valid-shape") throw oatsError("migration-held", "home already carries a captured execution binding; verify that authority instead of reconstructing from legacy rows");
|
|
122
|
-
const matches = home.capabilities.filter((entry) => entry.id === request.capability);
|
|
123
|
-
if (matches.length !== 1 || matches[0].integrity !== artifact.artifact.historicalIntegrity.value) {
|
|
124
|
-
throw oatsError("migration-held", "home metadata does not uniquely bind this capability to the verified historical artifact digest");
|
|
125
|
-
}
|
|
126
|
-
const runtime = matches[0];
|
|
127
|
-
return { schemaVersion: 1, home: { path: home.path, document: home.document }, artifact,
|
|
128
|
-
association: "capability-runtime-integrity-match", completeness: "partial",
|
|
129
|
-
legacyTrustedClaim: runtime.trustedClaim, approvalAuthority: "none",
|
|
130
|
-
unresolved: [
|
|
131
|
-
"source revision and retained soul projection",
|
|
132
|
-
"complete managed resources, helpers and runtime packages",
|
|
133
|
-
"new-format exact-artifact approval",
|
|
134
|
-
] };
|
|
135
|
-
}
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
/** Read-only historical migration evidence inventory.
|
|
2
|
-
*
|
|
3
|
-
* Inputs are an explicit bounded target list supplied by the deployment owner.
|
|
4
|
-
* This module does not discover topology, resolve current config/source, verify
|
|
5
|
-
* legacy lock semantics, execute providers, or write migration state. */
|
|
6
|
-
import { lstatSync, realpathSync } from "node:fs";
|
|
7
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
|
-
import { readPortableBytes } from "./portable-files.mjs";
|
|
9
|
-
import { bytesIntegrity, validateIntegrity, BYTES_FORMAT } from "./portable-digest.mjs";
|
|
10
|
-
import { canonicalJson, compareUtf8, parseStrictJson } from "./portable-values.mjs";
|
|
11
|
-
import { portableScope } from "./portable-state.mjs";
|
|
12
|
-
import { validateExecutionBinding, validateExecutionCapsule, validateExecutionTemplate } from "./schedule-capsule.mjs";
|
|
13
|
-
import { oatsError } from "./errors.mjs";
|
|
14
|
-
|
|
15
|
-
export const MIGRATION_INVENTORY_VERSION = 1;
|
|
16
|
-
const MAX_TARGETS = 10_000;
|
|
17
|
-
const SHA256 = /^sha256-[a-f0-9]{64}$/;
|
|
18
|
-
|
|
19
|
-
const plain = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
20
|
-
const problem = (code, message, pointer = "") => ({ code, message, pointer });
|
|
21
|
-
const errorProblem = (error, pointer = "") => problem(error?.code || "invalid-evidence", String(error?.message || error), pointer);
|
|
22
|
-
|
|
23
|
-
function canonicalTarget(deployment, input, kind, { allowRoot = false } = {}) {
|
|
24
|
-
if (typeof input !== "string" || !input || input.includes("\0")) throw oatsError("invalid-declaration", `${kind} target must be a non-empty path without NUL`);
|
|
25
|
-
const lexical = resolve(deployment, input);
|
|
26
|
-
const part = relative(deployment, lexical);
|
|
27
|
-
if ((!allowRoot && !part) || part === ".." || part.startsWith(`..${sep}`) || isAbsolute(part)) {
|
|
28
|
-
throw oatsError("path-escape", `${kind} target is outside the explicit migration deployment`);
|
|
29
|
-
}
|
|
30
|
-
let parent;
|
|
31
|
-
try { parent = realpathSync(dirname(lexical)); }
|
|
32
|
-
catch (error) {
|
|
33
|
-
if (error.code === "ENOENT") return { absolute: lexical, path: part || ".", parentMissing: true };
|
|
34
|
-
throw error;
|
|
35
|
-
}
|
|
36
|
-
if (parent !== dirname(lexical)) throw oatsError("path-escape", `${kind} target has a symlinked or aliased parent`);
|
|
37
|
-
return { absolute: lexical, path: part || ".", parentMissing: false };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function targetDirectory(deployment, input, kind, { allowRoot = false } = {}) {
|
|
41
|
-
const target = canonicalTarget(deployment, input, kind, { allowRoot });
|
|
42
|
-
if (target.parentMissing) return target;
|
|
43
|
-
let stat;
|
|
44
|
-
try { stat = lstatSync(target.absolute); }
|
|
45
|
-
catch (error) { if (error.code === "ENOENT") return { ...target, missing: true }; throw error; }
|
|
46
|
-
if (!stat.isDirectory() || realpathSync(target.absolute) !== target.absolute) {
|
|
47
|
-
throw oatsError("path-escape", `${kind} target must be a real, non-symlinked directory`);
|
|
48
|
-
}
|
|
49
|
-
return target;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function uniqueTargets(deployment, values, kind, options) {
|
|
53
|
-
if (!Array.isArray(values) || values.length > MAX_TARGETS || values.some((value) => typeof value !== "string")) {
|
|
54
|
-
throw oatsError("resource-limit", `${kind} target list must be an array of at most ${MAX_TARGETS} paths`);
|
|
55
|
-
}
|
|
56
|
-
const targets = values.map((value) => options?.directory
|
|
57
|
-
? targetDirectory(deployment, value, kind, options)
|
|
58
|
-
: canonicalTarget(deployment, value, kind, options));
|
|
59
|
-
targets.sort((a, b) => compareUtf8(a.path, b.path));
|
|
60
|
-
for (let i = 1; i < targets.length; i++) if (targets[i - 1].path === targets[i].path) {
|
|
61
|
-
throw oatsError("invalid-declaration", `duplicate ${kind} migration target ${targets[i].path}`);
|
|
62
|
-
}
|
|
63
|
-
return targets;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function readDocument(target) {
|
|
67
|
-
if (target.parentMissing) return { receipt: { path: target.path, state: "absent" }, value: null, bytes: null };
|
|
68
|
-
const bytes = readPortableBytes(target.absolute, { allowMissing: true, invalidCode: "invalid-evidence" });
|
|
69
|
-
if (bytes === null) return { receipt: { path: target.path, state: "absent" }, value: null, bytes: null };
|
|
70
|
-
const integrity = bytesIntegrity(bytes);
|
|
71
|
-
try {
|
|
72
|
-
return { receipt: { path: target.path, state: "present", integrity }, value: parseStrictJson(bytes), bytes };
|
|
73
|
-
} catch (error) {
|
|
74
|
-
return { receipt: { path: target.path, state: "invalid", integrity, problems: [errorProblem(error)] }, value: null, bytes };
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function mapIds(value, key, pointer, problems) {
|
|
79
|
-
const map = value?.[key];
|
|
80
|
-
if (map === undefined) return [];
|
|
81
|
-
if (!plain(map)) { problems.push(problem("invalid-evidence", `${key} must be an object map`, pointer)); return []; }
|
|
82
|
-
return Object.keys(map).sort(compareUtf8);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function summarizeLock(target, legacyLockDecoder) {
|
|
86
|
-
const { receipt, value, bytes } = readDocument(target);
|
|
87
|
-
if (receipt.state !== "present") return { ...receipt, format: "unknown", rowCounts: { packages: 0, capabilities: 0 }, rowIds: { packages: [], capabilities: [] }, legacyTrustClaims: 0, semanticVerification: "required" };
|
|
88
|
-
const problems = [];
|
|
89
|
-
if (!plain(value)) problems.push(problem("invalid-evidence", "historical lock root is not an object", ""));
|
|
90
|
-
const rawVersion = plain(value) ? value.lockfileVersion : undefined;
|
|
91
|
-
const version = rawVersion === undefined ? 1 : rawVersion;
|
|
92
|
-
const format = version === 1 ? "legacy-v1" : version === 2 ? "materialized-v2" : version === 3 ? "portable-v3" : "unknown";
|
|
93
|
-
if (!Number.isSafeInteger(version)) problems.push(problem("invalid-evidence", "lockfileVersion is not an integer", "/lockfileVersion"));
|
|
94
|
-
else if (![1, 2, 3].includes(version)) problems.push(problem("unsupported-wire-version", `unsupported historical lockfileVersion ${version}`, "/lockfileVersion"));
|
|
95
|
-
const rowIds = {
|
|
96
|
-
packages: plain(value) ? mapIds(value, "packages", "/packages", problems) : [],
|
|
97
|
-
capabilities: plain(value) ? mapIds(value, "capabilities", "/capabilities", problems) : [],
|
|
98
|
-
};
|
|
99
|
-
const capabilityRows = plain(value?.capabilities) ? Object.values(value.capabilities) : [];
|
|
100
|
-
const legacyTrustClaims = capabilityRows.filter((row) => plain(row) && (row.trusted === true || row.trustedExecutables === true)).length;
|
|
101
|
-
const rowCounts = { packages: rowIds.packages.length, capabilities: rowIds.capabilities.length };
|
|
102
|
-
let semanticVerification = version === 3 ? "current-reader-required" : "legacy-reader-required";
|
|
103
|
-
if ((version === 1 || version === 2) && legacyLockDecoder) {
|
|
104
|
-
try {
|
|
105
|
-
const decoded = legacyLockDecoder(Buffer.from(bytes), Object.freeze({ path: receipt.path, version }));
|
|
106
|
-
if (!plain(decoded) || decoded.version !== version) throw oatsError("invalid-lock", "legacy lock decoder returned a different version");
|
|
107
|
-
const expectedPackages = version === 2 ? rowIds.packages : [];
|
|
108
|
-
const expectedCapabilities = rowIds.capabilities;
|
|
109
|
-
const packages = plain(decoded.packages) ? Object.keys(decoded.packages).sort(compareUtf8) : [];
|
|
110
|
-
const capabilitiesSource = version === 1 ? decoded.legacyCapabilities : decoded.capabilities;
|
|
111
|
-
const capabilities = plain(capabilitiesSource) ? Object.keys(capabilitiesSource).sort(compareUtf8) : [];
|
|
112
|
-
if (canonicalJson(packages) !== canonicalJson(expectedPackages) || canonicalJson(capabilities) !== canonicalJson(expectedCapabilities)) {
|
|
113
|
-
throw oatsError("invalid-lock", "legacy lock decoder changed the historical row identity set");
|
|
114
|
-
}
|
|
115
|
-
semanticVerification = "verified";
|
|
116
|
-
} catch (error) { problems.push(errorProblem(error)); }
|
|
117
|
-
}
|
|
118
|
-
return { ...receipt, ...(problems.length ? { state: "invalid", problems } : {}), format, rowCounts, rowIds, legacyTrustClaims, semanticVerification };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function bindingSummary(value, problems) {
|
|
122
|
-
if (!Object.hasOwn(value, "executionBinding")) return { state: "absent" };
|
|
123
|
-
try {
|
|
124
|
-
validateExecutionBinding(value.executionBinding);
|
|
125
|
-
return { state: "valid-shape", value: JSON.parse(canonicalJson(value.executionBinding)) };
|
|
126
|
-
} catch (error) {
|
|
127
|
-
problems.push(errorProblem(error, "/executionBinding"));
|
|
128
|
-
return { state: "invalid" };
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function capabilitySummaries(value, pointer, problems) {
|
|
133
|
-
if (value === undefined) return [];
|
|
134
|
-
if (!Array.isArray(value)) { problems.push(problem("invalid-evidence", "capabilityRuntime must be an array", pointer)); return []; }
|
|
135
|
-
const out = [];
|
|
136
|
-
for (let index = 0; index < value.length; index++) {
|
|
137
|
-
const row = value[index], at = `${pointer}/${index}`;
|
|
138
|
-
if (!plain(row) || typeof row.id !== "string" || !row.id) { problems.push(problem("invalid-evidence", "capability runtime row lacks an identity", at)); continue; }
|
|
139
|
-
const integrity = typeof row.trust?.integrity === "string" && SHA256.test(row.trust.integrity) ? row.trust.integrity : null;
|
|
140
|
-
const hookNames = plain(row.hooks) ? Object.keys(row.hooks).sort(compareUtf8) : [];
|
|
141
|
-
out.push({ id: row.id, integrity, trustedClaim: row.trust?.trusted === true, hookNames });
|
|
142
|
-
}
|
|
143
|
-
return out.sort((a, b) => compareUtf8(a.id, b.id));
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function summarizeCleanup(target) {
|
|
147
|
-
const document = canonicalTarget(target.deployment, join(target.absolute, ".oats-rollback-incomplete.json"), "cleanup evidence");
|
|
148
|
-
const { receipt, value } = readDocument(document);
|
|
149
|
-
const problems = [];
|
|
150
|
-
if (receipt.state !== "present") return { document: receipt, capabilities: [], outstanding: { hooks: 0, git: 0, directory: false } };
|
|
151
|
-
if (!plain(value)) problems.push(problem("invalid-evidence", "cleanup descriptor root is not an object", ""));
|
|
152
|
-
const cleanup = plain(value?.cleanup) ? value.cleanup : null;
|
|
153
|
-
if (!cleanup) problems.push(problem("invalid-evidence", "cleanup descriptor has no cleanup object", "/cleanup"));
|
|
154
|
-
const outstanding = plain(cleanup?.outstanding) ? cleanup.outstanding : {};
|
|
155
|
-
const capabilities = capabilitySummaries(cleanup?.capabilityRuntime, "/cleanup/capabilityRuntime", problems);
|
|
156
|
-
return {
|
|
157
|
-
document: { ...receipt, ...(problems.length ? { state: "invalid", problems } : {}) },
|
|
158
|
-
capabilities,
|
|
159
|
-
outstanding: { hooks: Array.isArray(outstanding.hooks) ? outstanding.hooks.length : 0,
|
|
160
|
-
git: Array.isArray(outstanding.git) ? outstanding.git.length : 0, directory: outstanding.directory === true },
|
|
161
|
-
...(problems.length ? { problems } : {}),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function summarizeHome(deployment, target) {
|
|
166
|
-
const document = canonicalTarget(deployment, join(target.absolute, "instance.json"), "instance evidence");
|
|
167
|
-
const { receipt, value } = readDocument(document), problems = [];
|
|
168
|
-
let instance = basename(target.absolute), executionBinding = { state: "absent" }, capabilities = [];
|
|
169
|
-
if (receipt.state === "present") {
|
|
170
|
-
if (!plain(value)) problems.push(problem("invalid-evidence", "instance metadata root is not an object", ""));
|
|
171
|
-
else {
|
|
172
|
-
if (typeof value.instance === "string" && value.instance) instance = value.instance;
|
|
173
|
-
else problems.push(problem("invalid-evidence", "instance metadata has no stable instance name", "/instance"));
|
|
174
|
-
executionBinding = bindingSummary(value, problems);
|
|
175
|
-
capabilities = capabilitySummaries(value.capabilityRuntime, "/capabilityRuntime", problems);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
const cleanup = summarizeCleanup({ ...target, deployment });
|
|
179
|
-
return { path: target.path, instance, document: { ...receipt, ...(problems.length ? { state: "invalid", problems } : {}) },
|
|
180
|
-
executionBinding, capabilities, cleanup };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function executionSummary(value, kind, pointer, problems) {
|
|
184
|
-
if (value === undefined) return { state: "absent" };
|
|
185
|
-
try {
|
|
186
|
-
if (kind === "capsule") validateExecutionCapsule(value); else validateExecutionTemplate(value);
|
|
187
|
-
return { state: "valid-shape", schemaVersion: value.schemaVersion,
|
|
188
|
-
...(Object.hasOwn(value, "executionId") ? { executionId: value.executionId } : {}),
|
|
189
|
-
deployment: value.deployment, resolution: JSON.parse(canonicalJson(value.resolution)),
|
|
190
|
-
action: JSON.parse(canonicalJson(value.action)) };
|
|
191
|
-
} catch (error) {
|
|
192
|
-
problems.push(errorProblem(error, pointer));
|
|
193
|
-
return { state: "invalid" };
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function summarizeScheduleJob(id, definition, state) {
|
|
198
|
-
const problems = [];
|
|
199
|
-
if (definition !== undefined && !plain(definition)) problems.push(problem("invalid-evidence", "schedule definition is not an object", `/definitions/jobs/${id}`));
|
|
200
|
-
if (state !== undefined && !plain(state)) problems.push(problem("invalid-evidence", "schedule state is not an object", `/state/jobs/${id}`));
|
|
201
|
-
const attempt = plain(state?.attempt)
|
|
202
|
-
? state.attempt.execution === undefined ? { state: "legacy" }
|
|
203
|
-
: executionSummary(state.attempt.execution, "capsule", `/state/jobs/${id}/attempt/execution`, problems)
|
|
204
|
-
: state?.attempt === undefined ? { state: "absent" } : (problems.push(problem("invalid-evidence", "schedule attempt is not an object", `/state/jobs/${id}/attempt`)), { state: "invalid" });
|
|
205
|
-
const lastRun = plain(state?.lastRun)
|
|
206
|
-
? state.lastRun.execution === undefined ? state.lastRun.outcome === "unknown" ? { state: "legacy" } : { state: "absent" }
|
|
207
|
-
: executionSummary(state.lastRun.execution, "capsule", `/state/jobs/${id}/lastRun/execution`, problems)
|
|
208
|
-
: state?.lastRun === undefined ? { state: "absent" } : (problems.push(problem("invalid-evidence", "schedule lastRun is not an object", `/state/jobs/${id}/lastRun`)), { state: "invalid" });
|
|
209
|
-
const template = plain(definition) ? executionSummary(definition.execution, "template", `/definitions/jobs/${id}/execution`, problems) : { state: "absent" };
|
|
210
|
-
return { id, kind: typeof definition?.kind === "string" ? definition.kind : null,
|
|
211
|
-
definitionVersion: Number.isSafeInteger(definition?.definitionVersion) ? definition.definitionVersion : null,
|
|
212
|
-
recurrencePolicy: typeof definition?.recurrencePolicy === "string" ? definition.recurrencePolicy : null,
|
|
213
|
-
execution: template, attempt, lastRun, ...(problems.length ? { problems } : {}) };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function summarizeSchedules(deployment, target) {
|
|
217
|
-
const definitionsTarget = canonicalTarget(deployment, join(target.absolute, "oats-schedules.json"), "schedule definitions evidence");
|
|
218
|
-
const stateTarget = canonicalTarget(deployment, join(target.absolute, ".agents", "schedules", "state.json"), "schedule state evidence");
|
|
219
|
-
const definitions = readDocument(definitionsTarget), state = readDocument(stateTarget), problems = [];
|
|
220
|
-
const definitionsJobs = plain(definitions.value?.jobs) ? definitions.value.jobs : Object.create(null);
|
|
221
|
-
const stateJobs = plain(state.value?.jobs) ? state.value.jobs : Object.create(null);
|
|
222
|
-
if (definitions.receipt.state === "present" && !plain(definitions.value?.jobs)) problems.push(problem("invalid-evidence", "schedule definitions have no jobs map", "/definitions/jobs"));
|
|
223
|
-
if (state.receipt.state === "present" && !plain(state.value?.jobs)) problems.push(problem("invalid-evidence", "schedule state has no jobs map", "/state/jobs"));
|
|
224
|
-
const ids = [...new Set([...Object.keys(definitionsJobs), ...Object.keys(stateJobs)])].sort(compareUtf8);
|
|
225
|
-
return { path: target.path, definitions: definitions.receipt, state: state.receipt,
|
|
226
|
-
jobs: ids.map((id) => summarizeScheduleJob(id, definitionsJobs[id], stateJobs[id])), ...(problems.length ? { problems } : {}) };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** Inventory only explicitly supplied targets. No recursive discovery occurs. */
|
|
230
|
-
export function readPortableMigrationInventory(scope, { lockFiles = [], instanceHomes = [], scheduleScopes = [] } = {}, { legacyLockDecoder } = {}) {
|
|
231
|
-
if (legacyLockDecoder !== undefined && typeof legacyLockDecoder !== "function") throw oatsError("invalid-declaration", "legacyLockDecoder must be a function");
|
|
232
|
-
const deployment = portableScope(scope);
|
|
233
|
-
const locks = uniqueTargets(deployment, lockFiles, "lock evidence").map((target) => summarizeLock(target, legacyLockDecoder));
|
|
234
|
-
const homes = uniqueTargets(deployment, instanceHomes, "instance home", { directory: true }).map((target) => summarizeHome(deployment, target));
|
|
235
|
-
const schedules = uniqueTargets(deployment, scheduleScopes, "schedule scope", { directory: true, allowRoot: true }).map((target) => summarizeSchedules(deployment, target));
|
|
236
|
-
const inventory = { schemaVersion: MIGRATION_INVENTORY_VERSION, deployment, locks, homes, schedules };
|
|
237
|
-
canonicalJson(inventory);
|
|
238
|
-
return inventory;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function assertReceipt(receipt) {
|
|
242
|
-
if (!plain(receipt) || typeof receipt.path !== "string" || !["present", "absent", "invalid"].includes(receipt.state)) {
|
|
243
|
-
throw oatsError("invalid-evidence", "migration inventory contains an invalid document receipt");
|
|
244
|
-
}
|
|
245
|
-
if (receipt.state !== "absent") validateIntegrity(receipt.integrity, [BYTES_FORMAT]);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** Minimal persisted-inventory validation; evidence authority still comes from
|
|
249
|
-
* re-reading and comparing all literal document witnesses below. */
|
|
250
|
-
export function validatePortableMigrationInventory(inventory) {
|
|
251
|
-
canonicalJson(inventory);
|
|
252
|
-
if (!plain(inventory) || inventory.schemaVersion !== MIGRATION_INVENTORY_VERSION || typeof inventory.deployment !== "string"
|
|
253
|
-
|| !Array.isArray(inventory.locks) || !Array.isArray(inventory.homes) || !Array.isArray(inventory.schedules)) {
|
|
254
|
-
throw oatsError("invalid-evidence", "invalid migration inventory envelope");
|
|
255
|
-
}
|
|
256
|
-
for (const lock of inventory.locks) assertReceipt(lock);
|
|
257
|
-
for (const home of inventory.homes) { assertReceipt(home.document); assertReceipt(home.cleanup.document); }
|
|
258
|
-
for (const schedule of inventory.schedules) { assertReceipt(schedule.definitions); assertReceipt(schedule.state); }
|
|
259
|
-
return inventory;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** Re-read every explicit witness and require the complete inventory projection
|
|
263
|
-
* to be unchanged. This is still read-only and performs no migration apply. */
|
|
264
|
-
export function verifyPortableMigrationInventory(scope, inventory, adapters = {}) {
|
|
265
|
-
validatePortableMigrationInventory(inventory);
|
|
266
|
-
const deployment = portableScope(scope);
|
|
267
|
-
if (deployment !== inventory.deployment) throw oatsError("selection-changed", "migration deployment identity changed");
|
|
268
|
-
const current = readPortableMigrationInventory(deployment, {
|
|
269
|
-
lockFiles: inventory.locks.map((entry) => entry.path),
|
|
270
|
-
instanceHomes: inventory.homes.map((entry) => entry.path),
|
|
271
|
-
scheduleScopes: inventory.schedules.map((entry) => entry.path),
|
|
272
|
-
}, adapters);
|
|
273
|
-
if (canonicalJson(current) !== canonicalJson(inventory)) throw oatsError("selection-changed", "historical migration evidence changed after inventory");
|
|
274
|
-
return current;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** Re-read one explicit lock witness and return its strictly decoded candidate
|
|
278
|
-
* rows. This does not correlate the lock to a home/job, verify artifact bytes,
|
|
279
|
-
* infer missing source, or transfer its historical trust fields. */
|
|
280
|
-
export function verifyHistoricalLockCandidate(scope, inventory, lockPath, { legacyLockDecoder } = {}) {
|
|
281
|
-
if (typeof legacyLockDecoder !== "function") throw oatsError("migration-required", "historical lock candidate verification needs the strict legacy-lock decoder");
|
|
282
|
-
const current = verifyPortableMigrationInventory(scope, inventory, { legacyLockDecoder });
|
|
283
|
-
const target = canonicalTarget(current.deployment, lockPath, "lock evidence");
|
|
284
|
-
const evidence = current.locks.find((entry) => entry.path === target.path);
|
|
285
|
-
if (!evidence) throw oatsError("invalid-evidence", "historical lock target is not in the verified inventory");
|
|
286
|
-
if (evidence.state !== "present" || evidence.semanticVerification !== "verified" || !["legacy-v1", "materialized-v2"].includes(evidence.format)) {
|
|
287
|
-
throw oatsError("migration-held", "historical lock candidate is absent, invalid, unsupported or not strictly verified");
|
|
288
|
-
}
|
|
289
|
-
const document = readDocument(target);
|
|
290
|
-
if (document.receipt.state !== "present" || canonicalJson(document.receipt.integrity) !== canonicalJson(evidence.integrity)) {
|
|
291
|
-
throw oatsError("selection-changed", "historical lock changed during candidate verification");
|
|
292
|
-
}
|
|
293
|
-
const version = evidence.format === "legacy-v1" ? 1 : 2;
|
|
294
|
-
const decoded = legacyLockDecoder(Buffer.from(document.bytes), Object.freeze({ path: evidence.path, version }));
|
|
295
|
-
const copied = parseStrictJson(canonicalJson(decoded));
|
|
296
|
-
if (copied.version !== version) throw oatsError("invalid-lock", "legacy lock decoder returned a different candidate version");
|
|
297
|
-
const packageIds = Object.keys(copied.packages || {}).sort(compareUtf8);
|
|
298
|
-
const capabilityIds = Object.keys(version === 1 ? copied.legacyCapabilities || {} : copied.capabilities || {}).sort(compareUtf8);
|
|
299
|
-
if (canonicalJson(packageIds) !== canonicalJson(evidence.rowIds.packages) || canonicalJson(capabilityIds) !== canonicalJson(evidence.rowIds.capabilities)) {
|
|
300
|
-
throw oatsError("invalid-lock", "legacy lock decoder changed the verified candidate row identity set");
|
|
301
|
-
}
|
|
302
|
-
return { schemaVersion: 1, path: evidence.path, integrity: evidence.integrity, version,
|
|
303
|
-
packages: copied.packages, capabilities: copied.capabilities, legacyCapabilities: copied.legacyCapabilities,
|
|
304
|
-
trustAuthority: "none", evidence: [{ kind: "migration-evidence", document: { kind: "deployment", path: evidence.path, integrity: evidence.integrity }, pointer: "" }] };
|
|
305
|
-
}
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/** Immutable, unselectable historical evidence records.
|
|
2
|
-
*
|
|
3
|
-
* Partial/unknown evidence lives outside `.agents/resolutions` and can never be
|
|
4
|
-
* consumed as dispatch authority. Publication re-verifies all inventoried input
|
|
5
|
-
* witnesses first and changes no lock, home, schedule, session or approval. */
|
|
6
|
-
import { linkSync, lstatSync, mkdirSync, mkdtempSync, opendirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import { ensureStoreIgnore } from "./capability-artifacts.mjs";
|
|
9
|
-
import { readPortableBytes } from "./portable-files.mjs";
|
|
10
|
-
import { JSON_FORMAT, jsonIntegrity, validateIntegrity } from "./portable-digest.mjs";
|
|
11
|
-
import { canonicalJson, compareUtf8, parseStrictJson } from "./portable-values.mjs";
|
|
12
|
-
import { portableScope } from "./portable-state.mjs";
|
|
13
|
-
import { objectAt, stringAt, versionAt } from "./portable-shape.mjs";
|
|
14
|
-
import { validateOrigin, validateResolutionRef } from "./resolution-shape.mjs";
|
|
15
|
-
import { validatePortableMigrationInventory, verifyPortableMigrationInventory } from "./portable-migration-evidence.mjs";
|
|
16
|
-
import { planPortableMigration } from "./portable-migration.mjs";
|
|
17
|
-
import { verifyHistoricalHomeCapabilityCandidate } from "./portable-migration-artifacts.mjs";
|
|
18
|
-
import { oatsError } from "./errors.mjs";
|
|
19
|
-
|
|
20
|
-
export const RESOLUTION_EVIDENCE_VERSION = 1;
|
|
21
|
-
const TARGET_KINDS = new Set(["selection-lock", "instance-home", "scheduled-job"]);
|
|
22
|
-
const ACTIONS = new Set(["preserve", "verify", "hold"]);
|
|
23
|
-
const exists = (path) => { try { return lstatSync(path); } catch (error) { if (error.code === "ENOENT") return null; throw error; } };
|
|
24
|
-
|
|
25
|
-
export function validateResolutionEvidence(value) {
|
|
26
|
-
canonicalJson(value);
|
|
27
|
-
objectAt(value, ["schemaVersion", "target", "status", "evidence", "knownInputs", "unresolved", "resolution"],
|
|
28
|
-
["schemaVersion", "target", "status", "evidence", "knownInputs", "unresolved", "resolution"]);
|
|
29
|
-
versionAt(value.schemaVersion);
|
|
30
|
-
objectAt(value.target, ["kind", "id"], ["kind", "id"]);
|
|
31
|
-
if (!TARGET_KINDS.has(value.target.kind)) throw oatsError("invalid-evidence", "unsupported migration evidence target kind");
|
|
32
|
-
stringAt(value.target.id, "/target/id");
|
|
33
|
-
if (!Array.isArray(value.evidence)) throw oatsError("invalid-evidence", "migration evidence origins must be an array");
|
|
34
|
-
value.evidence.forEach(validateOrigin);
|
|
35
|
-
objectAt(value.knownInputs, ["inventory", "action", "preserve"], ["inventory", "action", "preserve"]);
|
|
36
|
-
validateIntegrity(value.knownInputs.inventory, [JSON_FORMAT]);
|
|
37
|
-
if (!ACTIONS.has(value.knownInputs.action)) throw oatsError("invalid-evidence", "invalid planned migration action");
|
|
38
|
-
canonicalJson(value.knownInputs.preserve);
|
|
39
|
-
if (!Array.isArray(value.unresolved)) throw oatsError("invalid-evidence", "migration unresolved inputs must be an array");
|
|
40
|
-
for (const item of value.unresolved) {
|
|
41
|
-
objectAt(item, ["pointer", "code", "origins", "reason"], ["pointer", "code", "origins", "reason"]);
|
|
42
|
-
stringAt(item.pointer, "/unresolved/pointer", { empty: true, pattern: /^(?:\/(?:[^~]|~[01])*)*$/ });
|
|
43
|
-
stringAt(item.code, "/unresolved/code"); stringAt(item.reason, "/unresolved/reason");
|
|
44
|
-
if (!Array.isArray(item.origins)) throw oatsError("invalid-evidence", "unresolved origins must be an array");
|
|
45
|
-
item.origins.forEach(validateOrigin);
|
|
46
|
-
}
|
|
47
|
-
if (!['reconstructed', 'partial', 'unknown'].includes(value.status)) throw oatsError("invalid-evidence", "invalid migration evidence status");
|
|
48
|
-
if (value.status === "reconstructed") {
|
|
49
|
-
validateResolutionRef(value.resolution);
|
|
50
|
-
if (!value.evidence.length) throw oatsError("resolution-incomplete", "reconstructed evidence requires historical witnesses");
|
|
51
|
-
if (value.unresolved.length) throw oatsError("resolution-incomplete", "reconstructed evidence cannot retain unresolved managed inputs");
|
|
52
|
-
} else if (value.resolution !== null) {
|
|
53
|
-
throw oatsError("resolution-incomplete", "partial or unknown evidence cannot reference a captured resolution");
|
|
54
|
-
}
|
|
55
|
-
return value;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function validateResolutionEvidenceRef(value) {
|
|
59
|
-
canonicalJson(value, { maxBytes: 1024, maxDepth: 4, maxEntries: 8 });
|
|
60
|
-
objectAt(value, ["schemaVersion", "id"], ["schemaVersion", "id"]);
|
|
61
|
-
versionAt(value.schemaVersion);
|
|
62
|
-
stringAt(value.id, "/id", { pattern: /^sha256-[a-f0-9]{64}$/ });
|
|
63
|
-
return value;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function store(scope, create) {
|
|
67
|
-
let root = portableScope(scope);
|
|
68
|
-
for (const name of [".agents", "resolution-evidence"]) {
|
|
69
|
-
root = join(root, name);
|
|
70
|
-
if (create) { try { mkdirSync(root, { mode: 0o700 }); } catch (error) { if (error.code !== "EEXIST") throw error; } }
|
|
71
|
-
const stat = exists(root);
|
|
72
|
-
if (!stat) throw oatsError("evidence-not-found", "resolution evidence store is absent");
|
|
73
|
-
if (!stat.isDirectory() || realpathSync(root) !== root) throw oatsError("invalid-evidence", "resolution evidence store component is not a real directory");
|
|
74
|
-
}
|
|
75
|
-
if (create) ensureStoreIgnore(root);
|
|
76
|
-
return root;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function decode(path, reference) {
|
|
80
|
-
const bytes = readPortableBytes(path, { missingCode: "evidence-not-found", invalidCode: "invalid-evidence" });
|
|
81
|
-
const value = parseStrictJson(bytes);
|
|
82
|
-
if (jsonIntegrity(value).value !== reference.id || !bytes.equals(Buffer.from(canonicalJson(value)))) {
|
|
83
|
-
throw oatsError("integrity-drift", "resolution evidence bytes differ from their canonical address");
|
|
84
|
-
}
|
|
85
|
-
validateResolutionEvidence(value);
|
|
86
|
-
return value;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function readResolutionEvidence(scope, reference) {
|
|
90
|
-
validateResolutionEvidenceRef(reference);
|
|
91
|
-
return decode(join(store(scope, false), `${reference.id}.json`), reference);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Bounded diagnostic enumeration of immutable evidence. Staging and managed
|
|
95
|
-
* ignore metadata are not records; every other entry must be one valid file. */
|
|
96
|
-
export function listResolutionEvidence(scope, { maxEntries = 512, maxRecords = 256, maxBytes = 32 * 1024 * 1024, includeDocuments = false } = {}) {
|
|
97
|
-
if (!Number.isSafeInteger(maxEntries) || maxEntries < 1 || maxEntries > 10_000
|
|
98
|
-
|| !Number.isSafeInteger(maxRecords) || maxRecords < 1 || maxRecords > 10_000
|
|
99
|
-
|| !Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 256 * 1024 * 1024
|
|
100
|
-
|| typeof includeDocuments !== "boolean") {
|
|
101
|
-
throw oatsError("invalid-evidence", "invalid resolution evidence listing options");
|
|
102
|
-
}
|
|
103
|
-
let root;
|
|
104
|
-
try { root = store(scope, false); }
|
|
105
|
-
catch (error) { if (error.code === "evidence-not-found") return []; throw error; }
|
|
106
|
-
const entries = [];
|
|
107
|
-
let visited = 0;
|
|
108
|
-
const directory = opendirSync(root);
|
|
109
|
-
try {
|
|
110
|
-
for (;;) {
|
|
111
|
-
const entry = directory.readSync();
|
|
112
|
-
if (entry === null) break;
|
|
113
|
-
if (++visited > maxEntries) throw oatsError("resource-limit", "resolution evidence visited-entry limit exceeded");
|
|
114
|
-
if (entry.name === ".gitignore" || entry.name.startsWith(".evidence-")) continue;
|
|
115
|
-
if (entries.length >= maxRecords) throw oatsError("resource-limit", "resolution evidence record limit exceeded");
|
|
116
|
-
entries.push(entry);
|
|
117
|
-
}
|
|
118
|
-
} finally { directory.closeSync(); }
|
|
119
|
-
const results = [];
|
|
120
|
-
let totalBytes = 0;
|
|
121
|
-
for (const entry of entries.sort((a, b) => compareUtf8(a.name, b.name))) {
|
|
122
|
-
const match = /^(sha256-[a-f0-9]{64})\.json$/.exec(entry.name);
|
|
123
|
-
if (!entry.isFile() || !match) throw oatsError("invalid-evidence", "resolution evidence store contains an unsupported entry");
|
|
124
|
-
const reference = { schemaVersion: 1, id: match[1] }, document = decode(join(root, entry.name), reference);
|
|
125
|
-
totalBytes += Buffer.byteLength(canonicalJson(document));
|
|
126
|
-
if (totalBytes > maxBytes) throw oatsError("resource-limit", "aggregate resolution evidence byte limit exceeded");
|
|
127
|
-
results.push({ reference, target: document.target, status: document.status, unresolved: document.unresolved.length,
|
|
128
|
-
...(includeDocuments ? { document } : {}) });
|
|
129
|
-
}
|
|
130
|
-
return results;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function targetKey(target) { return canonicalJson(target); }
|
|
134
|
-
function entryFor(plan, target) {
|
|
135
|
-
const wanted = targetKey(target), entry = plan.targets.find((candidate) => targetKey(candidate.target) === wanted);
|
|
136
|
-
if (!entry) throw oatsError("invalid-evidence", "migration target is not in the verified inventory plan");
|
|
137
|
-
if (entry.status === "reconstructed") throw oatsError("migration-required", "reconstructed publication requires the dedicated historical evidence verifier");
|
|
138
|
-
return entry;
|
|
139
|
-
}
|
|
140
|
-
function publishEvidence(deployment, document) {
|
|
141
|
-
validateResolutionEvidence(document);
|
|
142
|
-
const reference = { schemaVersion: 1, id: jsonIntegrity(document).value };
|
|
143
|
-
const root = store(deployment, true), destination = join(root, `${reference.id}.json`);
|
|
144
|
-
if (exists(destination)) { decode(destination, reference); return reference; }
|
|
145
|
-
const staging = mkdtempSync(join(root, ".evidence-"));
|
|
146
|
-
let failed = false, primary;
|
|
147
|
-
try {
|
|
148
|
-
const candidate = join(staging, "record.json");
|
|
149
|
-
writeFileSync(candidate, canonicalJson(document), { flag: "wx", mode: 0o600 });
|
|
150
|
-
decode(candidate, reference);
|
|
151
|
-
try { linkSync(candidate, destination); } catch (error) { if (error.code !== "EEXIST") throw error; }
|
|
152
|
-
decode(destination, reference);
|
|
153
|
-
return reference;
|
|
154
|
-
} catch (error) { failed = true; primary = error; throw error; }
|
|
155
|
-
finally {
|
|
156
|
-
try { rmSync(staging, { recursive: true, force: true }); }
|
|
157
|
-
catch (cleanup) {
|
|
158
|
-
const error = failed ? new AggregateError([primary, cleanup], "evidence publication and cleanup failed", { cause: primary })
|
|
159
|
-
: new Error("evidence staging cleanup failed", { cause: cleanup });
|
|
160
|
-
error.code = failed ? primary.code : cleanup.code; error.stagingPath = staging; throw error;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Publish exactly one target from the deterministic plan after re-reading all
|
|
166
|
-
* inventory witnesses. Reconstructed records require a later dedicated verifier
|
|
167
|
-
* and are deliberately outside this function. */
|
|
168
|
-
export function commitPlannedResolutionEvidence(scope, inventory, target, adapters = {}) {
|
|
169
|
-
validatePortableMigrationInventory(inventory);
|
|
170
|
-
const current = verifyPortableMigrationInventory(scope, inventory, adapters);
|
|
171
|
-
const plan = planPortableMigration(current), entry = entryFor(plan, target);
|
|
172
|
-
const document = { schemaVersion: RESOLUTION_EVIDENCE_VERSION,
|
|
173
|
-
target: entry.target, status: entry.status, evidence: entry.evidence,
|
|
174
|
-
knownInputs: { inventory: plan.inventory, action: entry.action, preserve: entry.preserve ?? null },
|
|
175
|
-
unresolved: entry.unresolved, resolution: null };
|
|
176
|
-
return publishEvidence(current.deployment, document);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Persist a narrow verified home→historical capability association as
|
|
180
|
-
* partial evidence. Raw legacy source/settings/commands are intentionally not
|
|
181
|
-
* copied; their literal document remains addressed by the evidence origins. */
|
|
182
|
-
export function commitHistoricalHomeCapabilityEvidence(scope, inventory, request, adapters = {}) {
|
|
183
|
-
validatePortableMigrationInventory(inventory);
|
|
184
|
-
const candidate = verifyHistoricalHomeCapabilityCandidate(scope, inventory, request, adapters);
|
|
185
|
-
const current = verifyPortableMigrationInventory(scope, inventory, adapters), plan = planPortableMigration(current);
|
|
186
|
-
const entry = entryFor(plan, { kind: "instance-home", id: candidate.home.path });
|
|
187
|
-
const origins = [...entry.evidence, ...candidate.artifact.evidence];
|
|
188
|
-
const seen = new Set(), evidence = origins.filter((origin) => { const key = canonicalJson(origin); if (seen.has(key)) return false; seen.add(key); return true; })
|
|
189
|
-
.sort((a, b) => compareUtf8(canonicalJson(a), canonicalJson(b)));
|
|
190
|
-
const proof = { kind: candidate.artifact.package ? "home-capability-v2" : "home-capability-v1", home: candidate.home.path,
|
|
191
|
-
capability: { id: candidate.artifact.capability.id, version: candidate.artifact.capability.row.version,
|
|
192
|
-
package: candidate.artifact.package?.id ?? null, path: candidate.artifact.package ? candidate.artifact.capability.row.path : null },
|
|
193
|
-
artifact: candidate.artifact.artifact, association: candidate.association,
|
|
194
|
-
legacyTrustedClaim: candidate.legacyTrustedClaim, selectionAuthority: candidate.artifact.selectionAuthority,
|
|
195
|
-
approvalAuthority: candidate.approvalAuthority, retentionStatus: candidate.artifact.retentionStatus };
|
|
196
|
-
const document = { schemaVersion: RESOLUTION_EVIDENCE_VERSION, target: entry.target, status: "partial", evidence,
|
|
197
|
-
knownInputs: { inventory: plan.inventory, action: "hold", preserve: proof }, unresolved: entry.unresolved, resolution: null };
|
|
198
|
-
return publishEvidence(current.deployment, document);
|
|
199
|
-
}
|