mirai-graph 1.4.0 → 1.5.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/CHANGELOG.md +16 -0
- package/CITATION.cff +2 -2
- package/package.json +2 -2
- package/packages/cli/project-technology.js +8 -0
- package/packages/cli/validate-provider-archive.js +165 -0
- package/packages/project-technology/index.js +118 -10
- package/releases/1.5.0.md +37 -0
- package/releases/README.md +2 -0
- package/schemas/project-technology-target-export.schema.json +1 -0
- package/standard/project-technology.md +53 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to Mirai Graph will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.5.0] - 2026-09-03
|
|
8
|
+
|
|
9
|
+
Compatible 1.x maintenance line, published under `legacy-1`; the npm `latest`
|
|
10
|
+
channel and the separate 2.x development line are unchanged.
|
|
11
|
+
|
|
12
|
+
- Project Technology can connect and refresh a bounded provider export without
|
|
13
|
+
Git using explicit authenticated-release trust supplied by the consumer.
|
|
14
|
+
Exact export bytes, graph identity, revision and forward ancestry are checked;
|
|
15
|
+
target/architecture/permission gates are unchanged. No automatic trust discovery.
|
|
16
|
+
- Read-only source export verification proves the archive anchor against the
|
|
17
|
+
accepted target and Git revision before a distributor seals it in release
|
|
18
|
+
metadata. CLI trust can be supplied on stdin; no temporary trust file is needed.
|
|
19
|
+
- No-Git inventory covers explicitly declared graph and raw-source paths, detects
|
|
20
|
+
stale content and blocks unsafe or missing sources before sync writes. Existing
|
|
21
|
+
Git inventories remain revision-bound; this does not invent source history.
|
|
22
|
+
|
|
7
23
|
## [1.4.0] - 2026-08-29
|
|
8
24
|
|
|
9
25
|
### Added
|
package/CITATION.cff
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mirai-graph",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "An evolutionary graph operating model for managing the growth of complex systems.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"pretest": "npm run test:project-technology && npm run test:context-traversal && npm run test:project-continuity && npm run test:artifact-releases && npm run test:technology-courses && npm run validate:project-technology-schemas",
|
|
17
|
-
"test:project-technology": "node packages/cli/validate-project-technology.js",
|
|
17
|
+
"test:project-technology": "node packages/cli/validate-project-technology.js && node packages/cli/validate-provider-archive.js",
|
|
18
18
|
"test:context-traversal": "node packages/cli/validate-context-traversal.js",
|
|
19
19
|
"test:project-continuity": "node packages/cli/validate-project-continuity.js",
|
|
20
20
|
"test:artifact-releases": "node packages/cli/validate-artifact-releases.js",
|
|
@@ -13,6 +13,7 @@ function usage() {
|
|
|
13
13
|
console.error("Options: --apply --task <text> --source <file> --target-id <id>");
|
|
14
14
|
console.error(" --semantic-digest <sha256:...> --provider-revision <sha>");
|
|
15
15
|
console.error(" --refresh-binding --significant-work --max-objects <count>");
|
|
16
|
+
console.error("Archive connect: --provider-archive-trust <verified-release-anchor.json>");
|
|
16
17
|
console.error("Continuity: sync --boundary task_start|stage_complete|task_complete [--evidence <json>] [--expected-graph-digest <sha256:...>]");
|
|
17
18
|
console.error(" verify --significant-work [--receipt-digest <sha256:...>]");
|
|
18
19
|
console.error("Context: --phase discover --task <text>");
|
|
@@ -46,7 +47,9 @@ function parse(argv) {
|
|
|
46
47
|
const options = {};
|
|
47
48
|
const values = new Set(["--task", "--source", "--target-id", "--semantic-digest", "--provider-revision", "--max-objects", "--phase", "--input", "--select", "--selection", "--packet", "--evidence", "--selector", "--reason", "--confidence", "--context-budget", "--boundary", "--expected-graph-digest", "--receipt-digest", "--state-root", "--matter-id", "--direction", "--artifact-root", "--release-date", "--release-id", "--parent-release", "--base-release", "--target-release", "--client-note", "--technology", "--scenario", "--audience", "--course-pack", "--projection"]);
|
|
48
49
|
const flags = new Set(["--apply", "--refresh-binding", "--significant-work", "--create-export"]);
|
|
50
|
+
values.add("--provider-archive-trust");
|
|
49
51
|
const names = {
|
|
52
|
+
"--provider-archive-trust": "providerArchiveTrustFile",
|
|
50
53
|
"--task": "task", "--source": "source", "--target-id": "targetId",
|
|
51
54
|
"--semantic-digest": "semanticDigest", "--provider-revision": "providerRevision",
|
|
52
55
|
"--max-objects": "maxObjects", "--apply": "apply",
|
|
@@ -89,6 +92,11 @@ function readJsonFile(file) {
|
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
function hydrateContextOptions(request) {
|
|
95
|
+
if (request.options.providerArchiveTrustFile) {
|
|
96
|
+
if (request.operation !== "connect") throw new Error("archive trust is only supported by connect");
|
|
97
|
+
request.options.providerArchive = request.options.providerArchiveTrustFile === "-"
|
|
98
|
+
? JSON.parse(fs.readFileSync(0, "utf8")) : readJsonFile(request.options.providerArchiveTrustFile);
|
|
99
|
+
}
|
|
92
100
|
if (request.operation === "sync" && request.options.boundary) {
|
|
93
101
|
if (request.options.boundary !== "task_start" && !request.options.evidenceFile) throw new Error(`${request.options.boundary} requires --evidence`);
|
|
94
102
|
if (request.options.evidenceFile) request.options.continuityEvidence = readJsonFile(request.options.evidenceFile);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const assert = require("assert");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const vm = require("vm");
|
|
8
|
+
const { createRequire } = require("module");
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
const technology = require("../project-technology");
|
|
11
|
+
const fixtureFile = path.join(__dirname, "validate-project-technology.js");
|
|
12
|
+
const fixtureText = fs.readFileSync(fixtureFile, "utf8");
|
|
13
|
+
const sandbox = { require: createRequire(fixtureFile), module: { exports: {} } };
|
|
14
|
+
vm.runInNewContext(fixtureText.slice(0, fixtureText.indexOf("const checks = [];")) +
|
|
15
|
+
"\nmodule.exports={initRepo,addTarget,git,TARGET_ID,SEMANTIC_DIGEST};", sandbox);
|
|
16
|
+
const fixture = sandbox.module.exports;
|
|
17
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "mirai-archive-test-")));
|
|
18
|
+
const originalPath = process.env.PATH;
|
|
19
|
+
const checks = [];
|
|
20
|
+
function check(id, condition, details) { checks.push({ id, passed: Boolean(condition) }); assert.ok(condition, `${id}: ${JSON.stringify(details)}`); }
|
|
21
|
+
function digestTree(dir) {
|
|
22
|
+
const files = [];
|
|
23
|
+
function walk(current) {
|
|
24
|
+
if (!fs.existsSync(current)) return;
|
|
25
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
26
|
+
const file = path.join(current, entry.name);
|
|
27
|
+
if (entry.isDirectory()) walk(file);
|
|
28
|
+
else files.push([path.relative(dir, file), technology.sha256(fs.readFileSync(file))]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
walk(dir); return technology.sha256(JSON.stringify(files.sort()));
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const sourceRoot = path.join(root, "source"); fs.mkdirSync(sourceRoot);
|
|
35
|
+
const provider = fixture.initRepo(sourceRoot, "fixture-provider");
|
|
36
|
+
const consumer = fixture.initRepo(sourceRoot, "fixture-consumer");
|
|
37
|
+
let revision = fixture.addTarget(provider);
|
|
38
|
+
const stateRoot = path.join(root, "state");
|
|
39
|
+
const base = { targetId: fixture.TARGET_ID, semanticDigest: fixture.SEMANTIC_DIGEST, stateRoot, apply: true };
|
|
40
|
+
for (const repo of [provider, consumer]) assert.equal(technology.execute("enable", repo, base).status, "success");
|
|
41
|
+
fixture.git(provider, "add", "graph.json"); fixture.git(provider, "commit", "-qm", "accepted activation");
|
|
42
|
+
revision = fixture.git(provider, "rev-parse", "HEAD");
|
|
43
|
+
const releases = [];
|
|
44
|
+
for (let n = 0; n < 2; n++) {
|
|
45
|
+
if (n) { fixture.git(provider, "commit", "--allow-empty", "-qm", "compatible revision"); revision = fixture.git(provider, "rev-parse", "HEAD"); }
|
|
46
|
+
const provided = technology.execute("provide", provider, { ...base, providerRevision: revision });
|
|
47
|
+
assert.equal(provided.status, "success");
|
|
48
|
+
const beforeSourceProof = digestTree(root);
|
|
49
|
+
const sourceProof = technology.execute("verify", provider, { source: path.join(provider, provided.export_ref) });
|
|
50
|
+
check(`source_export_proven_${n}`, sourceProof.status === "success" && sourceProof.provider_archive.providerRevision === revision, sourceProof);
|
|
51
|
+
check(`source_proof_zero_write_${n}`, digestTree(root) === beforeSourceProof);
|
|
52
|
+
check(`source_proof_not_authority_${n}`, technology.execute("verify", provider, { source: path.join(provider, provided.export_ref), significantWork: true }).status === "blocked");
|
|
53
|
+
const providerManifestPath = path.join(provider, "graph.json");
|
|
54
|
+
const providerManifestBytes = fs.readFileSync(providerManifestPath);
|
|
55
|
+
fs.appendFileSync(providerManifestPath, " ");
|
|
56
|
+
check(`uncommitted_manifest_blocks_source_proof_${n}`, technology.execute("verify", provider, { source: path.join(provider, provided.export_ref) }).blockers.includes("provider_manifest_not_revision_bound"));
|
|
57
|
+
fs.writeFileSync(providerManifestPath, providerManifestBytes);
|
|
58
|
+
const bytes = fs.readFileSync(path.join(provider, provided.export_ref));
|
|
59
|
+
const source = path.join(root, `export-${n}.json`); fs.writeFileSync(source, bytes);
|
|
60
|
+
check(`external_export_blocks_source_proof_${n}`, technology.execute("verify", provider, { source }).blockers.includes("provider_export_outside_repository"));
|
|
61
|
+
// Build anchor on the verified SOURCE side, before Git is removed.
|
|
62
|
+
const ancestors = n ? [releases[0].providerRevision] : [];
|
|
63
|
+
for (const ancestor of ancestors) fixture.git(provider, "merge-base", "--is-ancestor", ancestor, revision);
|
|
64
|
+
releases.push({ ...base, source, providerRevision: revision, providerArchive: {
|
|
65
|
+
exportSha256: technology.sha256(bytes), graphId: "fixture-provider", providerRevision: revision, ancestorRevisions: ancestors,
|
|
66
|
+
} });
|
|
67
|
+
}
|
|
68
|
+
const packedConsumer = path.join(root, "consumer");
|
|
69
|
+
fs.cpSync(consumer, packedConsumer, { recursive: true, filter: file => path.basename(file) !== ".git" });
|
|
70
|
+
const emptyPath = path.join(root, "no-tools"); fs.mkdirSync(emptyPath); process.env.PATH = emptyPath;
|
|
71
|
+
check("git_is_absent", spawnSync("git", ["--version"]).error?.code === "ENOENT");
|
|
72
|
+
const invoke = options => technology.execute("connect", packedConsumer, options);
|
|
73
|
+
const noTrust = { ...releases[0] }; delete noTrust.providerArchive;
|
|
74
|
+
check("untrusted_archive_blocked", invoke(noTrust).blockers.includes("provider_revision_order_unverifiable"));
|
|
75
|
+
const before = digestTree(root);
|
|
76
|
+
check("preview", invoke({ ...releases[0], apply: false }).status === "preview");
|
|
77
|
+
check("preview_zero_write", digestTree(root) === before);
|
|
78
|
+
const connected = invoke(releases[0]);
|
|
79
|
+
check("archive_connect", connected.status === "success", connected);
|
|
80
|
+
check("complete_contract_imported", connected.target_binding.status === "ready", connected);
|
|
81
|
+
const after = digestTree(root);
|
|
82
|
+
check("repeat_changed_false", invoke(releases[0]).changed === false);
|
|
83
|
+
check("repeat_byte_identical", digestTree(root) === after);
|
|
84
|
+
const status = technology.execute("status", packedConsumer, base);
|
|
85
|
+
check("read_only_ready", status.target_binding.status === "ready", status);
|
|
86
|
+
technology.execute("verify", packedConsumer, base);
|
|
87
|
+
check("diagnostics_zero_write", digestTree(root) === after);
|
|
88
|
+
function denied(id, options, blocker) {
|
|
89
|
+
const prior = digestTree(root); const result = invoke(options);
|
|
90
|
+
check(id, result.status === "fail" && (!blocker || result.blockers.includes(blocker)), result);
|
|
91
|
+
check(`${id}_zero_write`, prior === digestTree(root));
|
|
92
|
+
}
|
|
93
|
+
denied("null_trust", { ...releases[0], providerArchive: null }, "provider_archive_trust_invalid");
|
|
94
|
+
denied("bad_digest", { ...releases[0], providerArchive: { ...releases[0].providerArchive, exportSha256: "0".repeat(64) } }, "provider_archive_export_digest_mismatch");
|
|
95
|
+
denied("bad_graph", { ...releases[0], providerArchive: { ...releases[0].providerArchive, graphId: "other.provider" } }, "provider_archive_graph_mismatch");
|
|
96
|
+
denied("bad_revision", { ...releases[0], providerArchive: { ...releases[0].providerArchive, providerRevision: "f".repeat(40) } }, "provider_archive_revision_mismatch");
|
|
97
|
+
denied("extra_trust_fields", { ...releases[0], providerArchive: { ...releases[0].providerArchive, approved: true } }, "provider_archive_trust_invalid");
|
|
98
|
+
denied("explicit_refresh_required", releases[1], "target_provider_refresh_required");
|
|
99
|
+
denied("missing_ancestry", { ...releases[1], refreshBinding: true, providerArchive: { ...releases[1].providerArchive, ancestorRevisions: [] } }, "provider_revision_not_forward");
|
|
100
|
+
denied("target_mismatch", { ...releases[1], targetId: "other.target", refreshBinding: true }, "target_provider_target_mismatch");
|
|
101
|
+
denied("semantic_mismatch", { ...releases[1], semanticDigest: `sha256:${"3".repeat(64)}`, refreshBinding: true }, "target_provider_semantic_mismatch");
|
|
102
|
+
const refreshed = invoke({ ...releases[1], refreshBinding: true });
|
|
103
|
+
check("forward_refresh", refreshed.status === "success" && refreshed.changed, refreshed);
|
|
104
|
+
const refreshedTree = digestTree(root);
|
|
105
|
+
check("refresh_idempotent", invoke({ ...releases[1], refreshBinding: true }).changed === false);
|
|
106
|
+
check("refresh_byte_identical", digestTree(root) === refreshedTree);
|
|
107
|
+
denied("downgrade", { ...releases[0], refreshBinding: true }, "provider_revision_not_forward");
|
|
108
|
+
const original = fs.readFileSync(releases[1].source);
|
|
109
|
+
fs.appendFileSync(releases[1].source, " ");
|
|
110
|
+
denied("tampered_bytes", { ...releases[1], refreshBinding: true }, "provider_archive_export_digest_mismatch");
|
|
111
|
+
fs.writeFileSync(releases[1].source, original);
|
|
112
|
+
function alteredExport(id, alter, expected) {
|
|
113
|
+
const payload = JSON.parse(original); alter(payload);
|
|
114
|
+
fs.writeFileSync(releases[1].source, technology.canonicalBytes(payload));
|
|
115
|
+
const options = { ...releases[1], refreshBinding: true, providerArchive: { ...releases[1].providerArchive,
|
|
116
|
+
exportSha256: technology.sha256(fs.readFileSync(releases[1].source)), graphId: payload.provider_graph_id } };
|
|
117
|
+
denied(id, options, expected); fs.writeFileSync(releases[1].source, original);
|
|
118
|
+
}
|
|
119
|
+
alteredExport("provider_conflict", p => { p.provider_graph_id = "another.provider"; }, "target_provider_graph_conflict");
|
|
120
|
+
alteredExport("incomplete_contract", p => { p.requirement_bindings = []; });
|
|
121
|
+
alteredExport("contract_tamper", p => { p.goal_binding.done_when_ids = ["done.other"]; });
|
|
122
|
+
alteredExport("valid_but_different_contract", p => {
|
|
123
|
+
p.allowed_change_scope[0].file_patterns = ["packages/other/**"];
|
|
124
|
+
p.execution_contract_digest = technology.sha256(technology.canonicalBytes(technology.normalizeExecutionContract(p).contract));
|
|
125
|
+
}, "provider_execution_contract_refresh_mismatch");
|
|
126
|
+
alteredExport("reviewed_not_accepted", p => { p.architecture_contract.lifecycle = "reviewed"; });
|
|
127
|
+
alteredExport("private_extra_field", p => { p.private_source = "not allowed"; }, "provider_export_not_bounded");
|
|
128
|
+
const graphFile = path.join(packedConsumer, "graph.json"); const graphBefore = fs.readFileSync(graphFile);
|
|
129
|
+
const graph = JSON.parse(graphBefore); graph.extensions["mirai.project_technology"].enabled = false;
|
|
130
|
+
fs.writeFileSync(graphFile, JSON.stringify(graph));
|
|
131
|
+
denied("disabled", releases[1], "project_technology_not_enabled");
|
|
132
|
+
check("disabled_diagnostics_available", technology.execute("status", packedConsumer, base).operation_mode === "read_only");
|
|
133
|
+
fs.writeFileSync(graphFile, graphBefore);
|
|
134
|
+
const trustFile = path.join(root, "trust.json"); fs.writeFileSync(trustFile, JSON.stringify(releases[1].providerArchive));
|
|
135
|
+
const cli = spawnSync(process.execPath, [path.join(__dirname, "project-technology.js"), "connect", packedConsumer,
|
|
136
|
+
"--source", releases[1].source, "--target-id", base.targetId, "--semantic-digest", base.semanticDigest,
|
|
137
|
+
"--provider-revision", releases[1].providerRevision, "--state-root", stateRoot,
|
|
138
|
+
"--provider-archive-trust", trustFile, "--apply"], { encoding: "utf8" });
|
|
139
|
+
check("cli_no_git_idempotent", cli.status === 0 && JSON.parse(cli.stdout).changed === false, cli.stdout);
|
|
140
|
+
// A new identity field is optional for historical Git exports, but required
|
|
141
|
+
// for authenticated archive imports. Existing stored contracts remain valid.
|
|
142
|
+
const legacy = JSON.parse(original); delete legacy.provider_graph_id;
|
|
143
|
+
fs.writeFileSync(releases[1].source, technology.canonicalBytes(legacy));
|
|
144
|
+
check("historical_export_readable", technology.readExport(releases[1].source).blockers.length === 0);
|
|
145
|
+
denied("legacy_archive_needs_graph_identity", { ...releases[1], providerArchive: { ...releases[1].providerArchive,
|
|
146
|
+
exportSha256: technology.sha256(fs.readFileSync(releases[1].source)) } }, "provider_archive_graph_mismatch");
|
|
147
|
+
fs.writeFileSync(releases[1].source, original);
|
|
148
|
+
const folderInventory = technology.inventory(packedConsumer);
|
|
149
|
+
check("folder_inventory_nonempty", folderInventory.files.some(file => file.path === "graph.json") && folderInventory.files.some(file => file.path === "README.md"), folderInventory);
|
|
150
|
+
check("folder_inventory_sync", technology.execute("sync", packedConsumer, base).status === "success");
|
|
151
|
+
check("folder_inventory_sync_noop", technology.execute("sync", packedConsumer, base).changed === false);
|
|
152
|
+
const readme = path.join(packedConsumer, "README.md"); const readmeBefore = fs.readFileSync(readme);
|
|
153
|
+
fs.appendFileSync(readme, "\nChanged declared raw source\n");
|
|
154
|
+
const staleTree = digestTree(root);
|
|
155
|
+
check("folder_changed_source_is_stale", technology.execute("status", packedConsumer, base).blockers.includes("project_technology_inventory_stale"));
|
|
156
|
+
check("folder_stale_status_zero_write", digestTree(root) === staleTree);
|
|
157
|
+
fs.writeFileSync(readme, readmeBefore);
|
|
158
|
+
const graphForMissing = JSON.parse(fs.readFileSync(graphFile)); graphForMissing.graph.raw_sources.push("missing-required-source.md");
|
|
159
|
+
fs.writeFileSync(graphFile, JSON.stringify(graphForMissing));
|
|
160
|
+
const missingBefore = digestTree(root);
|
|
161
|
+
check("missing_declared_source_blocks_sync", technology.execute("sync", packedConsumer, base).blockers.includes("inventory_declared_source_missing"));
|
|
162
|
+
check("missing_source_sync_zero_write", digestTree(root) === missingBefore);
|
|
163
|
+
fs.writeFileSync(graphFile, graphBefore);
|
|
164
|
+
console.log(JSON.stringify({ status: "success", check_count: checks.length, checks }, null, 2));
|
|
165
|
+
} finally { process.env.PATH = originalPath; fs.rmSync(root, { recursive: true, force: true }); }
|
|
@@ -36,7 +36,7 @@ const TARGET_EXPORT_KEYS = new Set([
|
|
|
36
36
|
"schema_version", "target_id", "semantic_digest", "provider_revision",
|
|
37
37
|
"decision_refs", "goal_binding", "requirement_bindings", "constraint_ids",
|
|
38
38
|
"non_goal_ids", "deferred_boundary_ids", "allowed_change_scope",
|
|
39
|
-
"architecture_contract", "execution_contract_digest",
|
|
39
|
+
"architecture_contract", "execution_contract_digest", "provider_graph_id",
|
|
40
40
|
]);
|
|
41
41
|
const SECRET_PARTS = [".env", "credential", "secret", "token", "password", "private-key", "id_rsa", ".pem", ".p12"];
|
|
42
42
|
const EXCLUDED_PARTS = new Set([".git", ".mirai-graph", ".simai", "node_modules", "vendor", "dist", "build", "coverage", "generated"]);
|
|
@@ -425,6 +425,7 @@ function readExport(filePath) {
|
|
|
425
425
|
try { payload = JSON.parse(bytes.toString("utf8").replace(/^\uFEFF/, "")); } catch (_) { return { export: {}, blockers: ["provider_export_invalid"] }; }
|
|
426
426
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { export: {}, blockers: ["provider_export_invalid"] };
|
|
427
427
|
if (Object.keys(payload).some((key) => !TARGET_EXPORT_KEYS.has(key))) blockers.push("provider_export_not_bounded");
|
|
428
|
+
if (Object.hasOwn(payload, "provider_graph_id") && (typeof payload.provider_graph_id !== "string" || !REF_RE.test(payload.provider_graph_id))) blockers.push("provider_graph_id_invalid");
|
|
428
429
|
const identity = bindingValues(payload); blockers.push(...identity.blockers);
|
|
429
430
|
const executionFields = [
|
|
430
431
|
"decision_refs", "goal_binding", "requirement_bindings", "constraint_ids",
|
|
@@ -435,7 +436,7 @@ function readExport(filePath) {
|
|
|
435
436
|
const digest = sha256(canonicalBytes(normalized.contract));
|
|
436
437
|
if (!payload.execution_contract_digest) blockers.push("provider_execution_contract_digest_missing");
|
|
437
438
|
else if (payload.execution_contract_digest !== digest) blockers.push("provider_execution_contract_digest_mismatch");
|
|
438
|
-
return { export: { schema_version: "1.0.0", ...identity.values, ...normalized.contract, execution_contract_digest: digest }, blockers: [...new Set(blockers)].sort(), raw_sha256: sha256(bytes) };
|
|
439
|
+
return { export: { schema_version: "1.0.0", ...identity.values, ...normalized.contract, execution_contract_digest: digest, ...(payload.provider_graph_id ? { provider_graph_id: payload.provider_graph_id } : {}) }, blockers: [...new Set(blockers)].sort(), raw_sha256: sha256(bytes) };
|
|
439
440
|
}
|
|
440
441
|
|
|
441
442
|
function trackedFiles(repo) {
|
|
@@ -453,7 +454,42 @@ function safeTrackedFile(relative) {
|
|
|
453
454
|
|
|
454
455
|
function inventory(repo) {
|
|
455
456
|
const entries = [];
|
|
456
|
-
|
|
457
|
+
const blockers = [];
|
|
458
|
+
let files = trackedFiles(repo);
|
|
459
|
+
const revision = git(repo, "rev-parse", "HEAD") || null;
|
|
460
|
+
if (!revision) {
|
|
461
|
+
if (fs.existsSync(path.join(repo, ".git"))) blockers.push("inventory_git_unavailable");
|
|
462
|
+
else {
|
|
463
|
+
// Ordinary folders and immutable distributions have no Git index. Use
|
|
464
|
+
// only explicitly declared graph/raw sources, never scan arbitrary data.
|
|
465
|
+
const graph = readManifest(repo).manifest?.graph || {};
|
|
466
|
+
const refs = ["graph.json", ...(graph.source_of_truth || []), ...(graph.objects || []),
|
|
467
|
+
...(graph.relations || []), ...(graph.schemas || []), ...(graph.raw_sources || [])];
|
|
468
|
+
const found = new Set();
|
|
469
|
+
const visited = new Set();
|
|
470
|
+
function collect(relative) {
|
|
471
|
+
if (typeof relative !== "string" || !relative || path.isAbsolute(relative) || relative.includes("\\") || relative.includes(":") || relative.split("/").includes("..") || /[?*\[\]]/.test(relative)) {
|
|
472
|
+
blockers.push("inventory_declared_source_unsafe_or_unsupported"); return;
|
|
473
|
+
}
|
|
474
|
+
if (!safeTrackedFile(relative)) return;
|
|
475
|
+
if (visited.has(relative)) return;
|
|
476
|
+
visited.add(relative);
|
|
477
|
+
if (visited.size > 10000) { blockers.push("inventory_declared_source_budget_exceeded"); return; }
|
|
478
|
+
const absolute = path.join(repo, relative);
|
|
479
|
+
if (!fs.existsSync(absolute)) { blockers.push("inventory_declared_source_missing"); return; }
|
|
480
|
+
if (fs.lstatSync(absolute).isSymbolicLink() || !fs.realpathSync(absolute).startsWith(`${fs.realpathSync(repo)}${path.sep}`)) {
|
|
481
|
+
blockers.push("inventory_declared_source_unsafe_or_unsupported"); return;
|
|
482
|
+
}
|
|
483
|
+
if (fs.statSync(absolute).isDirectory()) {
|
|
484
|
+
for (const name of fs.readdirSync(absolute).sort()) collect(`${relative.replace(/\/$/, "")}/${name}`);
|
|
485
|
+
} else if (fs.statSync(absolute).isFile()) found.add(relative);
|
|
486
|
+
else blockers.push("inventory_declared_source_unsafe_or_unsupported");
|
|
487
|
+
}
|
|
488
|
+
refs.forEach(collect);
|
|
489
|
+
files = [...found];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
for (const relative of files.filter(safeTrackedFile).sort()) {
|
|
457
493
|
const absolute = path.join(repo, relative);
|
|
458
494
|
if (!fs.existsSync(absolute) || fs.lstatSync(absolute).isSymbolicLink() || !fs.statSync(absolute).isFile()) continue;
|
|
459
495
|
const bytes = fs.readFileSync(absolute);
|
|
@@ -462,10 +498,11 @@ function inventory(repo) {
|
|
|
462
498
|
const payload = {
|
|
463
499
|
schema_version: "1.0.0",
|
|
464
500
|
repository_id: readManifest(repo).manifest?.id || path.basename(repo),
|
|
465
|
-
revision
|
|
501
|
+
revision,
|
|
466
502
|
files: entries,
|
|
467
503
|
};
|
|
468
504
|
payload.inventory_digest = sha256(canonicalBytes(payload), true);
|
|
505
|
+
if (blockers.length) payload.blockers = [...new Set(blockers)].sort();
|
|
469
506
|
return payload;
|
|
470
507
|
}
|
|
471
508
|
|
|
@@ -521,6 +558,7 @@ function targetBindingStatus(repo, options = {}) {
|
|
|
521
558
|
provider_export_sha256: binding.provider_export_sha256 || null,
|
|
522
559
|
execution_contract: Object.fromEntries(["decision_refs", "goal_binding", "requirement_bindings", "constraint_ids", "non_goal_ids", "deferred_boundary_ids", "allowed_change_scope", "architecture_contract"].map((key) => [key, exported.export[key]])),
|
|
523
560
|
execution_contract_digest: exported.export.execution_contract_digest || null,
|
|
561
|
+
provider_graph_id: exported.export.provider_graph_id || null,
|
|
524
562
|
blockers: [...new Set(blockers)].sort(),
|
|
525
563
|
next_action: blockers.length ? "refresh or repair the exact provider binding" : "none",
|
|
526
564
|
};
|
|
@@ -535,7 +573,7 @@ function status(repoArg, options = {}) {
|
|
|
535
573
|
try { if (fs.existsSync(path.join(root, INVENTORY_FILE))) stored = readJson(path.join(root, INVENTORY_FILE)); } catch (_) { /* stale */ }
|
|
536
574
|
const current = inventory(repo);
|
|
537
575
|
const freshness = stored && stored.inventory_digest === current.inventory_digest ? "current" : stored ? "stale" : "missing";
|
|
538
|
-
const blockers = [...manifestState.blockers, ...ext.blockers];
|
|
576
|
+
const blockers = [...manifestState.blockers, ...ext.blockers, ...(current.blockers || [])];
|
|
539
577
|
if (!stored && legacyRuntimeState(repo).present) blockers.push("project_technology_host_state_migration_required");
|
|
540
578
|
if (ext.contract && ext.contract.enabled === false) blockers.push("project_technology_disabled");
|
|
541
579
|
if (freshness !== "current") blockers.push(`project_technology_inventory_${freshness}`);
|
|
@@ -580,6 +618,8 @@ function enable(repoArg, options = {}) {
|
|
|
580
618
|
const repo = normalizeRepo(repoArg);
|
|
581
619
|
const manifestState = readManifest(repo);
|
|
582
620
|
if (!manifestState.manifest || manifestState.blockers.length) return result("enable", "transactional", "fail", { blockers: manifestState.blockers, next_action: "repair graph.json" });
|
|
621
|
+
const inventoryPreflight = inventory(repo);
|
|
622
|
+
if (inventoryPreflight.blockers?.length) return result("enable", "transactional", "blocked", { blockers: inventoryPreflight.blockers, next_action: "repair declared graph sources" });
|
|
583
623
|
const manifest = structuredClone(manifestState.manifest);
|
|
584
624
|
const extensions = { ...(manifest.extensions || {}) };
|
|
585
625
|
const legacy = extensions[LEGACY_EXTENSION_KEY];
|
|
@@ -617,7 +657,7 @@ function sync(repoArg, options = {}) {
|
|
|
617
657
|
const repo = normalizeRepo(repoArg);
|
|
618
658
|
const manifestState = readManifest(repo);
|
|
619
659
|
const ext = extensionState(manifestState.manifest);
|
|
620
|
-
const blockers = [...manifestState.blockers, ...ext.blockers];
|
|
660
|
+
const blockers = [...manifestState.blockers, ...ext.blockers, ...(inventory(repo).blockers || [])];
|
|
621
661
|
const hostRoot = runtimeRoot(repo, manifestState.manifest, options);
|
|
622
662
|
if (legacyRuntimeState(repo).present && !fs.existsSync(path.join(hostRoot, INVENTORY_FILE))) blockers.push("project_technology_host_state_migration_required");
|
|
623
663
|
if (!ext.contract || ext.legacy || ext.contract.enabled !== true) blockers.push("project_technology_not_enabled");
|
|
@@ -661,7 +701,7 @@ function provide(repoArg, options = {}) {
|
|
|
661
701
|
blockers.push(...target.blockers);
|
|
662
702
|
if (blockers.length) return result("provide", "transactional", "fail", { blockers: [...new Set(blockers)].sort(), next_action: "repair the accepted target contract" });
|
|
663
703
|
const executionContractDigest = sha256(canonicalBytes(target.contract));
|
|
664
|
-
const payload = { schema_version: "1.0.0", ...identity.values, ...target.contract, execution_contract_digest: executionContractDigest };
|
|
704
|
+
const payload = { schema_version: "1.0.0", ...identity.values, ...target.contract, execution_contract_digest: executionContractDigest, provider_graph_id: manifestState.manifest.id };
|
|
665
705
|
const changed = atomicWrite(path.join(repo, EXPORT_FILE), canonicalBytes(payload));
|
|
666
706
|
return result("provide", "transactional", "success", { changed, export_ref: EXPORT_FILE, target_binding: payload });
|
|
667
707
|
}
|
|
@@ -671,6 +711,32 @@ function providerRootFor(exportPath) {
|
|
|
671
711
|
return completed.status === 0 ? completed.stdout.trim() : null;
|
|
672
712
|
}
|
|
673
713
|
|
|
714
|
+
// This is explicit consumer trust, NOT an assertion read from the provider.
|
|
715
|
+
// The caller must obtain it from authenticated, checksum-bound release metadata.
|
|
716
|
+
// Keeping it out of the export prevents an archive from authenticating itself.
|
|
717
|
+
function archiveProviderIdentity(source, exported, anchor) {
|
|
718
|
+
const fail = code => ({ blockers: [code], ancestors: [] });
|
|
719
|
+
if (!anchor || typeof anchor !== "object" || Array.isArray(anchor)) return fail("provider_archive_trust_invalid");
|
|
720
|
+
const keys = ["exportSha256", "graphId", "providerRevision", "ancestorRevisions"];
|
|
721
|
+
if (Object.keys(anchor).some(key => !keys.includes(key)) || keys.some(key => !Object.hasOwn(anchor, key))) return fail("provider_archive_trust_invalid");
|
|
722
|
+
if (!/^[0-9a-f]{64}$/.test(anchor.exportSha256 || "") ||
|
|
723
|
+
typeof anchor.graphId !== "string" || !REF_RE.test(anchor.graphId) ||
|
|
724
|
+
!REVISION_RE.test(anchor.providerRevision || "") ||
|
|
725
|
+
!Array.isArray(anchor.ancestorRevisions) || anchor.ancestorRevisions.length > 4096 ||
|
|
726
|
+
anchor.ancestorRevisions.some(rev => typeof rev !== "string" || !REVISION_RE.test(rev) || rev === anchor.providerRevision) ||
|
|
727
|
+
new Set(anchor.ancestorRevisions).size !== anchor.ancestorRevisions.length) return fail("provider_archive_trust_invalid");
|
|
728
|
+
try {
|
|
729
|
+
// Parent aliases such as macOS /var are allowed; the exact bytes are pinned.
|
|
730
|
+
if (fs.lstatSync(source).isSymbolicLink()) return fail("provider_archive_source_unsafe");
|
|
731
|
+
if (!fs.statSync(source).isFile() || fs.statSync(source).size > 1024 * 1024) return fail("provider_archive_source_unsafe");
|
|
732
|
+
const bytes = fs.readFileSync(source);
|
|
733
|
+
if (sha256(bytes) !== anchor.exportSha256 || canonicalBytes(JSON.parse(bytes)) !== canonicalBytes(exported)) return fail("provider_archive_export_digest_mismatch");
|
|
734
|
+
} catch (_) { return fail("provider_archive_source_unsafe"); }
|
|
735
|
+
if (exported.provider_graph_id !== anchor.graphId) return fail("provider_archive_graph_mismatch");
|
|
736
|
+
if (exported.provider_revision !== anchor.providerRevision) return fail("provider_archive_revision_mismatch");
|
|
737
|
+
return { blockers: [], ancestors: anchor.ancestorRevisions };
|
|
738
|
+
}
|
|
739
|
+
|
|
674
740
|
function connect(repoArg, options = {}) {
|
|
675
741
|
const repo = normalizeRepo(repoArg);
|
|
676
742
|
const source = path.resolve(String(options.source || ""));
|
|
@@ -681,14 +747,20 @@ function connect(repoArg, options = {}) {
|
|
|
681
747
|
const blockers = [...identity.blockers, ...read.blockers, ...manifestState.blockers, ...ext.blockers];
|
|
682
748
|
if (!ext.contract || ext.legacy || ext.contract.enabled !== true) blockers.push("project_technology_not_enabled");
|
|
683
749
|
for (const key of Object.keys(identity.values)) if (read.export[key] !== identity.values[key]) blockers.push(`${key}_mismatch`);
|
|
684
|
-
const
|
|
685
|
-
|
|
750
|
+
const archiveRequested = Object.hasOwn(options, "providerArchive");
|
|
751
|
+
const archive = archiveRequested ? archiveProviderIdentity(source, read.export, options.providerArchive) : null;
|
|
752
|
+
const providerRoot = archiveRequested ? null : providerRootFor(source);
|
|
753
|
+
if (archive) blockers.push(...archive.blockers);
|
|
754
|
+
else if (!providerRoot) blockers.push("provider_revision_order_unverifiable");
|
|
686
755
|
else if (git(providerRoot, "rev-parse", "HEAD").toLowerCase() !== identity.values.provider_revision) blockers.push("provider_revision_does_not_match_head");
|
|
687
756
|
const root = runtimeRoot(repo, manifestState.manifest, options);
|
|
688
757
|
const currentPath = path.join(root, BINDING_FILE);
|
|
689
758
|
let current = null;
|
|
690
759
|
try { if (fs.existsSync(currentPath)) current = readJson(currentPath); } catch (_) { blockers.push("current_target_binding_invalid"); }
|
|
691
760
|
if (current) {
|
|
761
|
+
const currentExport = targetBindingStatus(repo, options);
|
|
762
|
+
const existingGraphId = currentExport.provider_graph_id;
|
|
763
|
+
if (existingGraphId && existingGraphId !== read.export.provider_graph_id) blockers.push("target_provider_graph_conflict");
|
|
692
764
|
if (!options.refreshBinding) {
|
|
693
765
|
const same = Object.keys(identity.values).every((key) => current[key] === identity.values[key]);
|
|
694
766
|
if (!same) blockers.push("target_provider_conflict", "target_provider_refresh_required");
|
|
@@ -703,7 +775,9 @@ function connect(repoArg, options = {}) {
|
|
|
703
775
|
const currentState = targetBindingStatus(repo, options);
|
|
704
776
|
if (currentState.status !== "ready") blockers.push(...currentState.blockers);
|
|
705
777
|
if (currentState.execution_contract_digest !== read.export.execution_contract_digest) blockers.push("provider_execution_contract_refresh_mismatch");
|
|
706
|
-
if (
|
|
778
|
+
if (archive) {
|
|
779
|
+
if (current.provider_revision !== identity.values.provider_revision && !archive.ancestors.includes(current.provider_revision)) blockers.push("provider_revision_not_forward");
|
|
780
|
+
} else if (!providerRoot) blockers.push("provider_revision_order_unverifiable");
|
|
707
781
|
else {
|
|
708
782
|
if (current.provider_revision !== identity.values.provider_revision) {
|
|
709
783
|
const ancestry = spawnSync("git", ["-C", providerRoot, "merge-base", "--is-ancestor", current.provider_revision, identity.values.provider_revision]);
|
|
@@ -768,7 +842,40 @@ function repair(repoArg, options = {}) {
|
|
|
768
842
|
return sync(repo, options);
|
|
769
843
|
}
|
|
770
844
|
|
|
845
|
+
function verifyProviderExport(repoArg, options = {}) {
|
|
846
|
+
const repo = normalizeRepo(repoArg);
|
|
847
|
+
const source = path.resolve(options.source || path.join(repo, EXPORT_FILE));
|
|
848
|
+
const exported = readExport(source);
|
|
849
|
+
const manifestState = readManifest(repo);
|
|
850
|
+
const ext = extensionState(manifestState.manifest);
|
|
851
|
+
const identity = bindingValues(exported.export);
|
|
852
|
+
const blockers = [...exported.blockers, ...manifestState.blockers, ...ext.blockers, ...identity.blockers];
|
|
853
|
+
if (options.significantWork) blockers.push("provider_export_verification_is_not_execution_authority");
|
|
854
|
+
if (!trackedFiles(repo).includes("graph.json") || spawnSync("git", ["diff", "--quiet", "HEAD", "--", "graph.json"], { cwd: repo }).status !== 0) blockers.push("provider_manifest_not_revision_bound");
|
|
855
|
+
if (!source.startsWith(`${repo}${path.sep}`)) blockers.push("provider_export_outside_repository");
|
|
856
|
+
if (ext.contract?.enabled !== true || ext.legacy) blockers.push("project_technology_not_enabled");
|
|
857
|
+
const head = git(repo, "rev-parse", "HEAD");
|
|
858
|
+
if (head !== identity.values.provider_revision) blockers.push("provider_revision_does_not_match_head");
|
|
859
|
+
if (exported.export.provider_graph_id !== manifestState.manifest?.id) blockers.push("provider_archive_graph_mismatch");
|
|
860
|
+
const target = targetContract(repo, identity.values.target_id, identity.values.semantic_digest, manifestState.manifest);
|
|
861
|
+
blockers.push(...target.blockers);
|
|
862
|
+
if (sha256(canonicalBytes(target.contract)) !== exported.export.execution_contract_digest) blockers.push("provider_execution_contract_source_mismatch");
|
|
863
|
+
// Bound supported ancestry; deeper histories need a narrower supported
|
|
864
|
+
// release window rather than unbounded metadata in every consumer.
|
|
865
|
+
const history = git(repo, "rev-list", "--max-count=4098", "HEAD").split("\n").filter(Boolean);
|
|
866
|
+
if (history[0] !== head || history.length > 4097) blockers.push("provider_archive_ancestry_unverifiable_or_too_large");
|
|
867
|
+
return result("verify", "read_only", blockers.length ? "blocked" : "success", {
|
|
868
|
+
blockers: [...new Set(blockers)].sort(),
|
|
869
|
+
provider_archive: blockers.length ? null : {
|
|
870
|
+
exportSha256: exported.raw_sha256, graphId: manifestState.manifest.id,
|
|
871
|
+
providerRevision: head, ancestorRevisions: history.slice(1),
|
|
872
|
+
},
|
|
873
|
+
next_action: blockers.length ? "repair the canonical provider export before packaging" : "seal this anchor in authenticated release metadata",
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
771
877
|
function verify(repoArg, options = {}) {
|
|
878
|
+
if (options.source) return verifyProviderExport(repoArg, options);
|
|
772
879
|
const state = status(repoArg, options);
|
|
773
880
|
const blockers = [...state.blockers];
|
|
774
881
|
const repo = normalizeRepo(repoArg);
|
|
@@ -835,6 +942,7 @@ module.exports = {
|
|
|
835
942
|
sync,
|
|
836
943
|
targetBindingStatus,
|
|
837
944
|
verify,
|
|
945
|
+
verifyProviderExport,
|
|
838
946
|
verifyArtifactRelease: artifacts.verifyArtifactRelease,
|
|
839
947
|
verifyTechnologyCourse: technologyCourse.verifyTechnologyCourse,
|
|
840
948
|
verifyContext: traversal.verifyContext,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Mirai Graph 1.5.0
|
|
2
|
+
|
|
3
|
+
Compatible additive release for existing 1.x consumers. Published to npm under
|
|
4
|
+
`legacy-1`; it does not replace the 2.x `latest` channel or migrate consumers.
|
|
5
|
+
|
|
6
|
+
## Changes
|
|
7
|
+
|
|
8
|
+
- Existing Project Technology connect/refresh can import a bounded provider
|
|
9
|
+
export without Git when an authenticated distributor supplies its exact
|
|
10
|
+
export digest, graph identity, revision and forward ancestry proof.
|
|
11
|
+
- Read-only source verification binds that proof to the accepted target and
|
|
12
|
+
real Git revision before packaging. Caller-supplied trust is not inferred
|
|
13
|
+
from an export's own claims and never grants permission to perform work.
|
|
14
|
+
- Folder inventory follows only explicitly declared graph and raw-source paths,
|
|
15
|
+
detects stale content and rejects unsafe or missing sources.
|
|
16
|
+
- CLI archive trust can be passed through stdin without a temporary trust file.
|
|
17
|
+
|
|
18
|
+
## Compatibility and safety
|
|
19
|
+
|
|
20
|
+
Existing Git provider operations, complete target and execution contracts,
|
|
21
|
+
transactional rollback and fail-closed behavior remain in force. No new profile,
|
|
22
|
+
registry, daemon or schema family is introduced. The graph manifest remains
|
|
23
|
+
2.0.0 and the Project Technology activation contract remains 1.0.0.
|
|
24
|
+
|
|
25
|
+
Archive metadata must come from an independently authenticated release; this
|
|
26
|
+
package does not establish a distributor's trust or copy private source content.
|
|
27
|
+
|
|
28
|
+
## Verification
|
|
29
|
+
|
|
30
|
+
`npm run release:check` runs the current suite, including 39 existing Project
|
|
31
|
+
Technology checks and 71 archive/source-proof checks. The archive cases cover
|
|
32
|
+
no-Git import, exact replay, forward refresh, downgrade, tamper, graph conflict,
|
|
33
|
+
missing trust, source inventory and read-only behavior.
|
|
34
|
+
|
|
35
|
+
Local validation uses macOS ARM64 with Node 22.23.2. Federation's native
|
|
36
|
+
Windows/macOS/Linux complete installation matrix is a separate acceptance gate;
|
|
37
|
+
this release does not claim that matrix or a Federation release has passed.
|
package/releases/README.md
CHANGED
|
@@ -15,6 +15,8 @@ Release notes must separate:
|
|
|
15
15
|
|
|
16
16
|
## Release Notes
|
|
17
17
|
|
|
18
|
+
- [v1.5.0](1.5.0.md) - compatible 1.x authenticated archive providers and
|
|
19
|
+
declared-source inventory without Git (`legacy-1`, not npm `latest`).
|
|
18
20
|
- [v1.2.0](1.2.0.md) - portable task-boundary project continuity in Project
|
|
19
21
|
Technology.
|
|
20
22
|
- [v1.1.0](1.1.0.md) - universal sequential context traversal and usage
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"target_id": { "type": "string", "minLength": 2 },
|
|
16
16
|
"semantic_digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" },
|
|
17
17
|
"provider_revision": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
|
|
18
|
+
"provider_graph_id": { "type": "string", "minLength": 2, "maxLength": 256, "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/@?=+*-]+$" },
|
|
18
19
|
"decision_refs": { "$ref": "#/$defs/nonEmptyRefs" },
|
|
19
20
|
"goal_binding": {
|
|
20
21
|
"type": "object", "additionalProperties": false,
|
|
@@ -7,6 +7,59 @@ not a profile, a second graph or a source of domain methodology.
|
|
|
7
7
|
|
|
8
8
|
## One Mechanism, Different Graphs
|
|
9
9
|
|
|
10
|
+
### Archived providers (unreleased compatibility extension)
|
|
11
|
+
|
|
12
|
+
The normal source provider path continues to verify Git HEAD and ancestry.
|
|
13
|
+
For an immutable distribution, `connect` also accepts `providerArchive` in the
|
|
14
|
+
JavaScript options, or `--provider-archive-trust <file>` in the CLI (use `-`
|
|
15
|
+
to read that bounded JSON from stdin without creating a temporary file):
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"exportSha256": "<SHA-256 of the exact bounded export bytes>",
|
|
20
|
+
"graphId": "example.provider",
|
|
21
|
+
"providerRevision": "<exact 40-character source revision>",
|
|
22
|
+
"ancestorRevisions": ["<previous supported revision, proven during release build>"]
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This is a **consumer trust input**, not provider self-authentication. The caller
|
|
27
|
+
must derive it from an authenticated release lock/manifest after checking its
|
|
28
|
+
integrity and issuer. Never calculate a hash from an untrusted download and
|
|
29
|
+
call that authentication. Mirai does not discover, download or trust adjacent
|
|
30
|
+
metadata automatically. Packaging must verify the export against the accepted
|
|
31
|
+
source target and the declared source revision before publishing its digest.
|
|
32
|
+
Ancestor entries must be proven by the source Git history during that build;
|
|
33
|
+
they are not inferred from version numbers, dates or commit hash ordering.
|
|
34
|
+
|
|
35
|
+
The existing `technology verify <repository> --source <bounded-export>` and
|
|
36
|
+
`verifyProviderExport(repository, options)` provide a read-only source-side
|
|
37
|
+
proof for packaging. They require a revision-bound enabled manifest, exact HEAD,
|
|
38
|
+
accepted target and matching execution contract, and return the anchor with at
|
|
39
|
+
most 4096 proven ancestors. A packaging caller must then authenticate the release
|
|
40
|
+
metadata containing this anchor. This proof is not permission to execute a
|
|
41
|
+
significant task; requesting significant work through it is blocked.
|
|
42
|
+
|
|
43
|
+
Exports produced by `provide` now include `provider_graph_id`. Old Git-backed
|
|
44
|
+
exports remain readable; archive import requires the graph identity. `connect`
|
|
45
|
+
uses the same full accepted execution contract validation and atomic import.
|
|
46
|
+
`--refresh-binding` still requires the same target and semantic/architecture
|
|
47
|
+
contract; a changed revision must explicitly descend from the current binding
|
|
48
|
+
in the authenticated release metadata. A wrong or incomplete anchor fails,
|
|
49
|
+
even when a working Git checkout happens to be available. The caller cannot
|
|
50
|
+
use archive trust to authorize work, change owners, expand scope or bypass
|
|
51
|
+
acceptance. `provide` continues to require the canonical Git source; archive
|
|
52
|
+
consumers import its immutable output, never regenerate acceptance from copies.
|
|
53
|
+
|
|
54
|
+
Status/verify use the hash-bound local import and remain read-only. Without a
|
|
55
|
+
Git repository, inventory reads only the manifest and explicitly declared graph
|
|
56
|
+
and raw-source paths, with a bounded traversal. Missing, unsafe or unsupported
|
|
57
|
+
declared paths block sync before writes. Symlinks and secret/excluded paths are
|
|
58
|
+
not imported. Ordinary folders use content digests and a null inventory revision,
|
|
59
|
+
never a fabricated Git revision. If Git metadata exists but Git is unavailable,
|
|
60
|
+
inventory fails closed instead of silently treating a checkout as an archive.
|
|
61
|
+
Raw-source access and release authenticity remain separate responsibilities.
|
|
62
|
+
|
|
10
63
|
The root `graph.json` declares the repository scope and profiles. The same
|
|
11
64
|
Project Technology operations therefore work for:
|
|
12
65
|
|