create-agdf 0.13.5 → 0.13.7

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.
Files changed (38) hide show
  1. package/README.md +7 -1
  2. package/generated/plugins/agdf/.claude-plugin/plugin.json +1 -1
  3. package/generated/plugins/agdf/.codex-plugin/plugin.json +2 -3
  4. package/generated/plugins/agdf/hooks/session-start.sh +31 -0
  5. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +30 -9
  6. package/generated/plugins/agdf/runtime/agdf-local.js +8 -1
  7. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +30 -9
  8. package/generated/plugins/agdf/runtime/create-agdf/lib/runtime/local-validator.js +116 -31
  9. package/generated/plugins/agdf/runtime/create-agdf/lib/runtime/plugin-provenance.js +206 -0
  10. package/generated/plugins/agdf/runtime/create-agdf/package.json +1 -1
  11. package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
  12. package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +64 -19
  13. package/generated/plugins/agdf/submission/openai/availability.md +1 -1
  14. package/generated/plugins/agdf/submission/openai/capability-matrix.json +1 -1
  15. package/generated/plugins/agdf/submission/openai/release-notes.md +1 -1
  16. package/generated/plugins/agdf/submission/openai/reviewer-cases.json +1 -1
  17. package/generated/submissions/openai/agdf/.codex-plugin/plugin.json +1 -1
  18. package/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json +30 -9
  19. package/generated/submissions/openai/agdf/submission/openai/availability.md +1 -1
  20. package/generated/submissions/openai/agdf/submission/openai/capability-matrix.json +1 -1
  21. package/generated/submissions/openai/agdf/submission/openai/inventory.json +9 -9
  22. package/generated/submissions/openai/agdf/submission/openai/listing.json +1 -1
  23. package/generated/submissions/openai/agdf/submission/openai/readiness.json +2 -2
  24. package/generated/submissions/openai/agdf/submission/openai/readiness.md +2 -2
  25. package/generated/submissions/openai/agdf/submission/openai/release-notes.md +1 -1
  26. package/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json +1 -1
  27. package/lib/cli/application.js +4 -3
  28. package/lib/fs-swap.js +26 -0
  29. package/lib/installers/local-development.js +202 -0
  30. package/lib/installers/local-marketplace.js +253 -46
  31. package/lib/installers/opencode.js +17 -2
  32. package/lib/installers/plugin-installers.js +88 -22
  33. package/lib/lifecycle/operations.js +9 -5
  34. package/lib/lifecycle/status.js +10 -2
  35. package/lib/public-plugin/builder.js +5 -4
  36. package/lib/runtime/local-validator.js +116 -31
  37. package/lib/runtime/plugin-provenance.js +206 -0
  38. package/package.json +6 -2
@@ -1,25 +1,35 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { pluginDefinition } from "../cli/runtime-context.js";
3
- import { classifyMarketplaceList, prepareLocalMarketplace } from "./local-marketplace.js";
3
+ import { CODEX_REGISTRATION_REVISION, classifyMarketplaceList, inspectLocalMarketplaceProjection, isCodexLocalInstallVersion, prepareLocalMarketplace } from "./local-marketplace.js";
4
4
 
5
5
  export function installCodexGlobalPlugin({ exec = execFileSync, prepare = prepareLocalMarketplace, dataRoot } = {}) {
6
6
  const expectedVersion = pluginDefinition.version;
7
7
  const nativeOutput = [];
8
- const transaction = prepare({ expectedVersion, ...(dataRoot ? { dataRoot } : {}) });
9
- const migration = { state: "unknown", source: "", addedLocal: false, removedLegacy: false };
8
+ const transaction = prepare({ expectedVersion, codexRegistrationRevision: CODEX_REGISTRATION_REVISION, ...(dataRoot ? { dataRoot } : {}) });
9
+ const expectedInstallVersion = transaction.codexInstallVersion ?? expectedVersion;
10
+ const migration = { state: "unknown", source: "", addedLocal: false, removedLegacy: false, refreshOwnedLocal: false, removedOwnedLocal: false };
10
11
  try {
11
12
  const marketplaceOutput = runPluginPhase(exec, "codex", ["plugin", "marketplace", "list", "--json"], "marketplace", captureOptions());
12
- migrateMarketplace({ surface: "codex", exec, output: marketplaceOutput, root: transaction.root, nativeOutput, migration });
13
- nativeOutput.push(runPluginPhase(exec, "codex", ["plugin", "add", "agdf", "--marketplace", "agdf"], "plugin_operation", captureOptions()));
13
+ migrateMarketplace({ surface: "codex", exec, output: marketplaceOutput, root: transaction.root, nativeOutput, migration, refreshOwnedLocal: transaction.changed === true });
14
+ nativeOutput.push(runPluginPhase(exec, "codex", ["plugin", "add", "agdf@agdf", "--json"], "plugin_operation", captureOptions()));
14
15
  const listOutput = runPluginPhase(exec, "codex", ["plugin", "list"], "verification", captureOptions());
15
16
  const installedVersion = pluginVersionFromList(listOutput, "agdf@agdf");
16
- if (installedVersion !== expectedVersion) {
17
- throw lifecycleAdapterError("version", versionMismatchMessage("Codex", "agdf@agdf", expectedVersion, installedVersion, "npx --yes @agdf/cli@latest codex"));
17
+ if (installedVersion !== expectedInstallVersion) {
18
+ throw lifecycleAdapterError("version", versionMismatchMessage("Codex", "agdf@agdf", expectedInstallVersion, installedVersion, "npx --yes @agdf/cli@latest codex"));
18
19
  }
19
20
  transaction.commit();
20
21
  return {
21
- surface: "codex", operation: migration.state === "owned_local_current" ? "update" : "install", expectedVersion, installedVersion, verificationStatus: "healthy",
22
- evidence: ["durable_local_marketplace", `marketplace:${migration.state}`, "codex plugin list"],
22
+ surface: "codex", operation: migration.state === "owned_local_current" ? "update" : "install", expectedVersion: expectedInstallVersion, canonicalVersion: expectedVersion, installedVersion, verificationStatus: "healthy",
23
+ evidence: [
24
+ "durable_local_marketplace",
25
+ `marketplace:${migration.state}`,
26
+ "codex plugin list",
27
+ ...(transaction.pluginRoot ? [`staged_plugin_root:${transaction.pluginRoot}`, "staged_installation_provenance:matched"] : []),
28
+ ...(transaction.sourceDigest ? [`source_digest:${transaction.sourceDigest}`] : []),
29
+ ...(transaction.digest ? [`plugin_digest:${transaction.digest}`] : []),
30
+ ...(transaction.existingClassification === "owned_pre_provenance_rebuild" ? ["marketplace_recovery:owned_pre_provenance_rebuild", "loaded_session:restart_required"] : []),
31
+ ...(expectedInstallVersion === expectedVersion ? [] : [`canonical_version:${expectedVersion}`, `local_install_version:${expectedInstallVersion}`]),
32
+ ],
23
33
  nativeOutput: nativeOutput.filter(Boolean).map(String),
24
34
  };
25
35
  } catch (error) {
@@ -39,7 +49,12 @@ export function installClaudeGlobalPlugin({ exec = execFileSync, prepare = prepa
39
49
  nativeOutput.push(runPluginPhase(exec, "claude", ["plugin", "marketplace", "update", "agdf"], "marketplace", captureOptions()));
40
50
  const beforeList = runPluginPhase(exec, "claude", ["plugin", "list"], "verification", captureOptions());
41
51
  const alreadyInstalled = pluginListHasPlugin(beforeList, "agdf@agdf");
42
- nativeOutput.push(runPluginPhase(exec, "claude", ["plugin", alreadyInstalled ? "update" : "install", "agdf@agdf"], "plugin_operation", captureOptions()));
52
+ // `claude plugin update` keeps the cached copy when the version is unchanged, so a
53
+ // same-version local source change never reaches the host; reinstall replaces the content.
54
+ if (alreadyInstalled) {
55
+ nativeOutput.push(runPluginPhase(exec, "claude", ["plugin", "uninstall", "agdf@agdf"], "plugin_operation", captureOptions()));
56
+ }
57
+ nativeOutput.push(runPluginPhase(exec, "claude", ["plugin", "install", "agdf@agdf"], "plugin_operation", captureOptions()));
43
58
  const afterList = runPluginPhase(exec, "claude", ["plugin", "list"], "verification", captureOptions());
44
59
  const installedVersion = pluginVersionFromList(afterList, "agdf@agdf");
45
60
  if (installedVersion && installedVersion !== expectedVersion) {
@@ -52,7 +67,16 @@ export function installClaudeGlobalPlugin({ exec = execFileSync, prepare = prepa
52
67
  expectedVersion,
53
68
  installedVersion,
54
69
  verificationStatus: installedVersion ? "healthy" : "degraded",
55
- evidence: ["durable_local_marketplace", `marketplace:${migration.state}`, "claude plugin list", ...(installedVersion ? [] : ["host_did_not_expose_version"])],
70
+ evidence: [
71
+ "durable_local_marketplace",
72
+ `marketplace:${migration.state}`,
73
+ "claude plugin list",
74
+ ...(transaction.pluginRoot ? [`staged_plugin_root:${transaction.pluginRoot}`, "staged_installation_provenance:matched"] : []),
75
+ ...(transaction.sourceDigest ? [`source_digest:${transaction.sourceDigest}`] : []),
76
+ ...(transaction.digest ? [`plugin_digest:${transaction.digest}`] : []),
77
+ ...(transaction.existingClassification === "owned_pre_provenance_rebuild" ? ["marketplace_recovery:owned_pre_provenance_rebuild", "loaded_session:restart_required"] : []),
78
+ ...(installedVersion ? [] : ["host_did_not_expose_version"]),
79
+ ],
56
80
  nativeOutput: nativeOutput.filter(Boolean).map(String),
57
81
  };
58
82
  } catch (error) {
@@ -61,13 +85,21 @@ export function installClaudeGlobalPlugin({ exec = execFileSync, prepare = prepa
61
85
  }
62
86
  }
63
87
 
64
- function migrateMarketplace({ surface, exec, output, root, nativeOutput, migration }) {
88
+ function migrateMarketplace({ surface, exec, output, root, nativeOutput, migration, refreshOwnedLocal = false }) {
65
89
  const classification = classifyMarketplaceList(surface, output, root);
66
90
  Object.assign(migration, classification);
67
91
  if (["conflict", "unknown"].includes(classification.state)) {
68
92
  throw lifecycleAdapterError("marketplace", `Refusing to replace non-AGDF marketplace registration agdf (${classification.reason || classification.state}).`, classification);
69
93
  }
70
94
  const executable = surface === "claude" ? "claude" : "codex";
95
+ if (surface === "codex" && classification.state === "owned_local_current" && refreshOwnedLocal) {
96
+ migration.refreshOwnedLocal = true;
97
+ nativeOutput.push(runPluginPhase(exec, executable, ["plugin", "marketplace", "remove", "agdf", "--json"], "marketplace", captureOptions()));
98
+ migration.removedOwnedLocal = true;
99
+ nativeOutput.push(runPluginPhase(exec, executable, ["plugin", "marketplace", "add", root, "--json"], "marketplace", captureOptions()));
100
+ migration.addedLocal = true;
101
+ return;
102
+ }
71
103
  if (classification.state === "legacy_github") {
72
104
  const args = surface === "claude"
73
105
  ? ["plugin", "marketplace", "remove", "agdf", "--scope", "user"]
@@ -91,10 +123,23 @@ function recoverMarketplace({ surface, exec, migration, transaction, error }) {
91
123
  try {
92
124
  exec(executable, args, captureOptions());
93
125
  recovery.push({ args, status: "restored" });
126
+ return true;
94
127
  } catch (rollbackError) {
95
128
  recovery.push({ args, status: "failed", message: commandErrorText(rollbackError) });
129
+ return false;
96
130
  }
97
131
  };
132
+ if (migration?.refreshOwnedLocal) {
133
+ const refreshedRegistrationRemoved = migration.addedLocal
134
+ ? attempt(["plugin", "marketplace", "remove", "agdf", "--json"])
135
+ : true;
136
+ rollbackMarketplaceFilesystem(transaction, recovery);
137
+ if (migration.removedOwnedLocal && refreshedRegistrationRemoved) {
138
+ attempt(["plugin", "marketplace", "add", migration.source || transaction.root, "--json"]);
139
+ }
140
+ error.evidence = { ...(error.evidence ?? {}), rollback: recovery };
141
+ return;
142
+ }
98
143
  if (migration?.addedLocal) {
99
144
  attempt(surface === "claude"
100
145
  ? ["plugin", "marketplace", "remove", "agdf", "--scope", "user"]
@@ -105,32 +150,44 @@ function recoverMarketplace({ surface, exec, migration, transaction, error }) {
105
150
  ? ["plugin", "marketplace", "add", migration.source, "--scope", "user"]
106
151
  : ["plugin", "marketplace", "add", migration.source, "--json"]);
107
152
  }
153
+ rollbackMarketplaceFilesystem(transaction, recovery);
154
+ error.evidence = { ...(error.evidence ?? {}), rollback: recovery };
155
+ }
156
+
157
+ function rollbackMarketplaceFilesystem(transaction, recovery) {
108
158
  try {
109
159
  transaction.rollback();
110
160
  recovery.push({ filesystem: transaction.root, status: "restored" });
111
161
  } catch (rollbackError) {
112
162
  recovery.push({ filesystem: transaction.root, status: "failed", message: rollbackError.message });
113
163
  }
114
- error.evidence = { ...(error.evidence ?? {}), rollback: recovery };
115
164
  }
116
165
 
117
166
  function captureOptions() {
118
167
  return { encoding: "utf8", stdio: "pipe" };
119
168
  }
120
169
 
121
- export function inspectPluginSurface(surface, exec = execFileSync) {
170
+ export function inspectPluginSurface(surface, exec = execFileSync, options = {}) {
122
171
  const executable = surface === "claude" ? "claude" : "codex";
123
172
  const pluginId = "agdf@agdf";
124
173
  try {
125
174
  const output = exec(executable, ["plugin", "list"], { encoding: "utf8", stdio: "pipe" });
126
175
  const installed = pluginListHasPlugin(output, pluginId);
127
176
  const version = installed ? pluginVersionFromList(output, pluginId) : "";
177
+ let localDevelopmentVersion = false;
178
+ if (surface === "codex" && isCodexLocalInstallVersion(pluginDefinition.version, version)) {
179
+ try {
180
+ localDevelopmentVersion = inspectLocalMarketplaceProjection(options)?.codexInstallVersion === version;
181
+ } catch {
182
+ localDevelopmentVersion = false;
183
+ }
184
+ }
128
185
  return {
129
- status: !installed ? "not_installed" : version === pluginDefinition.version ? "healthy" : "degraded",
186
+ status: !installed ? "not_installed" : version === pluginDefinition.version || localDevelopmentVersion ? "healthy" : "degraded",
130
187
  surface,
131
188
  version: version || null,
132
189
  expected_version: pluginDefinition.version,
133
- evidence: [`${executable} plugin list`, ...(installed && !version ? ["host_did_not_expose_version"] : [])],
190
+ evidence: [`${executable} plugin list`, ...(localDevelopmentVersion ? [`canonical_version:${pluginDefinition.version}`, `local_install_version:${version}`] : []), ...(installed && !version ? ["host_did_not_expose_version"] : [])],
134
191
  };
135
192
  } catch (error) {
136
193
  return { status: "unknown", surface, version: null, expected_version: pluginDefinition.version, evidence: [commandErrorText(error)] };
@@ -167,14 +224,23 @@ export function pluginListHasPlugin(output, pluginId) {
167
224
  .some((line) => line.includes(pluginId));
168
225
  }
169
226
 
227
+ const VERSION_PATTERN = "(\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?)";
228
+
170
229
  export function pluginVersionFromList(output, pluginId) {
171
230
  const escapedPluginId = pluginId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
172
- const line = output
173
- .split(/\r?\n/)
174
- .find((entry) => new RegExp(`(^|\\s)${escapedPluginId}(\\s|$)`).test(entry));
175
- if (!line) return "";
176
- const versionMatch = line.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/);
177
- return versionMatch?.[1] ?? "";
231
+ const lines = output.split(/\r?\n/);
232
+ const entryIndex = lines.findIndex((entry) => new RegExp(`(^|\\s)${escapedPluginId}(\\s|$)`).test(entry));
233
+ if (entryIndex < 0) return "";
234
+ const sameLine = lines[entryIndex].match(new RegExp(`\\b${VERSION_PATTERN}\\b`));
235
+ if (sameLine) return sameLine[1];
236
+ // Current `claude plugin list` prints the plugin id and its `Version:` on separate
237
+ // lines; scan the entry's block until the next plugin id line.
238
+ for (let index = entryIndex + 1; index < lines.length; index += 1) {
239
+ if (/\S+@\S+/.test(lines[index])) break;
240
+ const blockMatch = lines[index].match(new RegExp(`\\bVersion:?\\s*${VERSION_PATTERN}\\b`, "i"));
241
+ if (blockMatch) return blockMatch[1];
242
+ }
243
+ return "";
178
244
  }
179
245
 
180
246
  function versionMismatchMessage(surface, pluginId, expectedVersion, installedVersion, correctiveCommand) {
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
4
4
  import { pluginDefinition } from "../cli/runtime-context.js";
5
5
  import { evaluateOpenCodeStatus, openCodeNpmInvocation } from "../installers/opencode.js";
6
6
  import { inspectPluginSurface } from "../installers/plugin-installers.js";
7
+ import { inspectGeneratedRepositoryMarketplace } from "../runtime/plugin-provenance.js";
7
8
 
8
9
  const CODEX_DISABLE_MARKER = "# AGDF-OWNED-REPOSITORY-PLUGIN-STATE";
9
10
 
@@ -12,8 +13,10 @@ function pluginSection(content, selector) {
12
13
  return content.match(new RegExp(`\\[plugins\\.${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`))?.[0] ?? "";
13
14
  }
14
15
 
15
- function repositorySelector(targetDir) {
16
- return existsSync(join(targetDir, ".agents", "plugins", "marketplace.json")) ? "agdf@agdf-repo" : "agdf@agdf";
16
+ function repositorySelector(targetDir, config = "") {
17
+ const repository = inspectGeneratedRepositoryMarketplace(targetDir);
18
+ if (repository.status === "matched") return repository.selector;
19
+ return pluginSection(config, "agdf@agdf-repo") ? "agdf@agdf-repo" : "agdf@agdf";
17
20
  }
18
21
 
19
22
  export function planRepositoryDisable(targetDir, surface) {
@@ -22,7 +25,7 @@ export function planRepositoryDisable(targetDir, surface) {
22
25
  }
23
26
  const path = join(targetDir, ".codex", "config.toml");
24
27
  const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
25
- const selector = repositorySelector(targetDir);
28
+ const selector = repositorySelector(targetDir, existing);
26
29
  const escaped = JSON.stringify(selector).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
30
  const sectionPattern = new RegExp(`\\[plugins\\.${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`);
28
31
  const currentSection = pluginSection(existing, selector);
@@ -49,8 +52,9 @@ export function planRepositoryDisable(targetDir, surface) {
49
52
  export function verifyRepositoryDisabled(targetDir) {
50
53
  const path = join(targetDir, ".codex", "config.toml");
51
54
  if (!existsSync(path)) return { status: "failed", evidence: [`missing:${path}`] };
52
- const selector = repositorySelector(targetDir);
53
- const section = pluginSection(readFileSync(path, "utf8"), selector);
55
+ const config = readFileSync(path, "utf8");
56
+ const selector = repositorySelector(targetDir, config);
57
+ const section = pluginSection(config, selector);
54
58
  const matches = [...section.matchAll(/^enabled\s*=\s*(true|false)\s*$/gm)];
55
59
  return matches.length === 1 && matches[0][1] === "false"
56
60
  ? { status: "healthy", evidence: [`${path}:${selector}:enabled=false`] }
@@ -5,6 +5,7 @@ import { evaluateDoctor } from "../control-evaluation/doctor.js";
5
5
  import { evaluateGateCheck } from "../control-evaluation/gate-check.js";
6
6
  import { evaluateOpenCodeStatus } from "../installers/opencode.js";
7
7
  import { inspectPluginSurface } from "../installers/plugin-installers.js";
8
+ import { inspectGeneratedRepositoryMarketplace } from "../runtime/plugin-provenance.js";
8
9
 
9
10
  function deliveryStatus(targetDir, selection, dependencies) {
10
11
  const controlDir = join(targetDir, ".agdf", "control");
@@ -29,7 +30,8 @@ function deliveryStatus(targetDir, selection, dependencies) {
29
30
 
30
31
  function repositoryStatus(targetDir, surface) {
31
32
  if (surface === "codex") {
32
- const marketplace = join(targetDir, ".agents", "plugins", "marketplace.json");
33
+ const repository = inspectGeneratedRepositoryMarketplace(targetDir);
34
+ const marketplace = repository.marketplacePath;
33
35
  const disabled = join(targetDir, ".codex", "config.toml");
34
36
  const config = existsSync(disabled) ? readFileSync(disabled, "utf8") : "";
35
37
  const isDisabled = ["agdf@agdf-repo", "agdf@agdf"].some((selector) => {
@@ -37,7 +39,11 @@ function repositoryStatus(targetDir, surface) {
37
39
  const section = config.match(new RegExp(`\\[plugins\\.${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`))?.[0] ?? "";
38
40
  return /^enabled\s*=\s*false\s*$/m.test(section);
39
41
  });
40
- return { status: isDisabled ? "disabled" : existsSync(marketplace) ? "active" : "not_configured", scope: "repository", evidence: [marketplace] };
42
+ return {
43
+ status: isDisabled ? "disabled" : repository.status === "matched" ? "active" : repository.status === "invalid" ? "degraded" : "not_configured",
44
+ scope: "repository",
45
+ evidence: [marketplace, ...(repository.reason ? [repository.reason] : [])],
46
+ };
41
47
  }
42
48
  if (surface === "opencode") {
43
49
  const instructions = join(targetDir, ".opencode", pluginDefinition.opencode.instructionsFileName);
@@ -97,6 +103,8 @@ export function evaluateGeneralStatus(targetDir, options = {}, dependencies = {}
97
103
  ? `Run npx --yes @agdf/cli@latest ${surface} to install or repair AGDF.`
98
104
  : repository.status === "disabled"
99
105
  ? "Restart the host to apply the repository-local disable; the healthy global AGDF installation remains available."
106
+ : repository.status === "degraded"
107
+ ? "The repository marketplace is incomplete or invalid; use the supported repository scaffold or the healthy durable global AGDF installation."
100
108
  : repository.status === "not_configured"
101
109
  ? `Run npx --yes @agdf/cli@latest ${surface === "codex" ? "codex-repo" : `${surface}-repo`} in this repository.`
102
110
  : delivery.status === "blocked"
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
+ import { renameSyncWithRetry } from "../fs-swap.js";
4
5
  import { assertPublicPluginContract, loadJson } from "./contract.js";
5
6
  import { renderCodexPluginManifest } from "./manifest.js";
6
7
  import { createReadinessReport, renderReadinessReport } from "./report.js";
@@ -66,11 +67,11 @@ export function buildPublicPluginCandidate({ repoRoot, outputRoot }) {
66
67
  const backupRoot = `${outputRoot}.previous`;
67
68
  if (existsSync(backupRoot)) throw new Error(`Refusing candidate swap while recovery backup exists: ${backupRoot}`);
68
69
  const hadPrevious = existsSync(outputRoot);
69
- if (hadPrevious) renameSync(outputRoot, backupRoot);
70
+ if (hadPrevious) renameSyncWithRetry(outputRoot, backupRoot);
70
71
  try {
71
- renameSync(temporaryRoot, outputRoot);
72
+ renameSyncWithRetry(temporaryRoot, outputRoot);
72
73
  } catch (error) {
73
- if (hadPrevious && existsSync(backupRoot) && !existsSync(outputRoot)) renameSync(backupRoot, outputRoot);
74
+ if (hadPrevious && existsSync(backupRoot) && !existsSync(outputRoot)) renameSyncWithRetry(backupRoot, outputRoot);
74
75
  throw error;
75
76
  }
76
77
  if (hadPrevious) rmSync(backupRoot, { recursive: true, force: true });
@@ -1,8 +1,14 @@
1
- import { createHash } from "node:crypto";
2
1
  import { spawnSync } from "node:child_process";
3
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
4
3
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
4
  import process from "node:process";
5
+ import {
6
+ INSTALLATION_PROVENANCE_FILE,
7
+ digestDirectory,
8
+ inspectGeneratedRepositoryMarketplace,
9
+ inspectInstallationProvenance,
10
+ validateDistributionProfiles,
11
+ } from "./plugin-provenance.js";
6
12
 
7
13
  function readJson(path) {
8
14
  try {
@@ -12,27 +18,12 @@ function readJson(path) {
12
18
  }
13
19
  }
14
20
 
15
- export function digestDirectory(root) {
16
- const files = [];
17
- function visit(directory) {
18
- for (const name of readdirSync(directory).sort()) {
19
- const path = join(directory, name);
20
- const stats = statSync(path);
21
- if (stats.isDirectory()) visit(path);
22
- else if (stats.isFile()) files.push(path);
23
- }
24
- }
25
- visit(root);
26
- const hash = createHash("sha256");
27
- for (const path of files) {
28
- hash.update(relative(root, path).replaceAll("\\", "/"));
29
- hash.update("\0");
30
- hash.update(readFileSync(path));
31
- hash.update("\0");
32
- }
33
- return hash.digest("hex");
21
+ function canonicalPath(path) {
22
+ try { return realpathSync(path); } catch { return resolve(path); }
34
23
  }
35
24
 
25
+ export { digestDirectory };
26
+
36
27
  function envelope(machineValidation, options, evidence = {}) {
37
28
  return {
38
29
  schema_version: "1",
@@ -42,6 +33,14 @@ function envelope(machineValidation, options, evidence = {}) {
42
33
  observed_version: evidence.observedVersion ?? null,
43
34
  source: evidence.source ?? null,
44
35
  registry_access: false,
36
+ distribution_profile: evidence.distributionProfile ?? null,
37
+ evidence_plane: evidence.evidencePlane ?? null,
38
+ canonical_version: evidence.canonicalVersion ?? options.expectedVersion ?? null,
39
+ plugin_version: evidence.pluginVersion ?? null,
40
+ runtime_digest: evidence.runtimeDigest ?? null,
41
+ source_digest: evidence.sourceDigest ?? null,
42
+ plugin_root: evidence.pluginRoot ?? null,
43
+ provenance_status: evidence.provenanceStatus ?? null,
45
44
  ...(evidence.reason ? { reason: evidence.reason } : {}),
46
45
  };
47
46
  }
@@ -51,17 +50,17 @@ export function resolveLocalValidator(options) {
51
50
  const packageManifest = readJson(join(options.ownedPackageRoot, "package.json"));
52
51
  const observedVersion = packageManifest?.version ?? null;
53
52
  if (!packageManifest) {
54
- return { envelope: envelope("unavailable", options, { source: "config_local_package", reason: "package_missing" }) };
53
+ return { envelope: envelope("unavailable", options, { source: "config_local_package", evidencePlane: "opencode_config_local", distributionProfile: "opencode-config-local", reason: "package_missing" }) };
55
54
  }
56
55
  if (observedVersion !== options.expectedVersion) {
57
- return { envelope: envelope("version_mismatch", options, { observedVersion, source: "config_local_package" }) };
56
+ return { envelope: envelope("version_mismatch", options, { observedVersion, source: "config_local_package", evidencePlane: "opencode_config_local", distributionProfile: "opencode-config-local" }) };
58
57
  }
59
58
  const entrypoint = join(options.ownedPackageRoot, "bin", "create-agdf.js");
60
59
  if (!existsSync(entrypoint)) {
61
- return { envelope: envelope("unavailable", options, { observedVersion, source: "config_local_package", reason: "entrypoint_missing" }) };
60
+ return { envelope: envelope("unavailable", options, { observedVersion, source: "config_local_package", evidencePlane: "opencode_config_local", distributionProfile: "opencode-config-local", reason: "entrypoint_missing" }) };
62
61
  }
63
62
  return {
64
- envelope: envelope("owned_version_matched", options, { observedVersion, source: "config_local_package" }),
63
+ envelope: envelope("owned_version_matched", options, { observedVersion, source: "config_local_package", evidencePlane: "opencode_config_local", distributionProfile: "opencode-config-local", provenanceStatus: "matched" }),
65
64
  executable: process.execPath,
66
65
  prefixArgs: [entrypoint],
67
66
  };
@@ -73,30 +72,116 @@ export function resolveLocalValidator(options) {
73
72
  return { envelope: envelope("version_mismatch", options, { source: "plugin_bundle", reason: "manifest_invalid" }) };
74
73
  }
75
74
  if (manifest) {
75
+ const pluginRoot = canonicalPath(options.pluginRoot ?? join(options.runtimeRoot, ".."));
76
+ if (options.expectedPluginRoot && canonicalPath(options.expectedPluginRoot) !== pluginRoot) {
77
+ return { envelope: envelope("version_mismatch", options, { source: "plugin_bundle", pluginRoot, evidencePlane: "loaded_session", distributionProfile: "runtime-plugin", provenanceStatus: "mismatch", reason: "plugin_root_mismatch" }) };
78
+ }
79
+ const definition = readJson(join(pluginRoot, "meta", "agdf-plugin.definition.json"));
80
+ const profile = validateDistributionProfiles(definition);
81
+ if (profile.status !== "matched") {
82
+ return { envelope: envelope("version_mismatch", options, { source: "plugin_bundle", pluginRoot, distributionProfile: "runtime-plugin", provenanceStatus: "invalid", reason: "profile_invalid" }) };
83
+ }
84
+ const codexManifest = readJson(join(pluginRoot, ".codex-plugin", "plugin.json"));
85
+ const claudeManifest = readJson(join(pluginRoot, ".claude-plugin", "plugin.json"));
86
+ const pluginVersion = codexManifest?.version ?? null;
87
+ if (!codexManifest || !claudeManifest || claudeManifest.version !== definition.version) {
88
+ return { envelope: envelope("version_mismatch", options, { source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, distributionProfile: "runtime-plugin", provenanceStatus: "invalid", reason: "manifest_invalid" }) };
89
+ }
76
90
  const packageRoot = join(options.runtimeRoot, "create-agdf");
77
91
  if (!existsSync(packageRoot)) {
78
- return { envelope: envelope("unavailable", options, { source: "plugin_bundle", reason: "package_missing" }) };
92
+ return { envelope: envelope("unavailable", options, { source: "plugin_bundle", pluginRoot, pluginVersion, distributionProfile: "runtime-plugin", reason: "runtime_missing" }) };
79
93
  }
80
94
  const packageManifest = readJson(join(packageRoot, "package.json"));
81
95
  const observedVersion = packageManifest?.version ?? manifest.version ?? null;
82
96
  if (observedVersion !== options.expectedVersion || manifest.version !== options.expectedVersion) {
83
- return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle" }) };
97
+ return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, distributionProfile: "runtime-plugin" }) };
84
98
  }
85
99
  let observedDigest = null;
86
100
  try { observedDigest = digestDirectory(packageRoot); } catch {}
87
101
  if (!observedDigest || manifest.digest !== observedDigest) {
88
- return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", reason: "digest_mismatch" }) };
102
+ return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, runtimeDigest: observedDigest, distributionProfile: "runtime-plugin", reason: "runtime_digest_mismatch" }) };
89
103
  }
90
104
  const entrypoint = resolve(options.runtimeRoot, manifest.entrypoint ?? "");
91
105
  const entrypointRelative = relative(options.runtimeRoot, entrypoint);
92
106
  if (!manifest.entrypoint || entrypointRelative === ".." || entrypointRelative.startsWith(`..${sep}`) || isAbsolute(entrypointRelative)) {
93
- return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", reason: "invalid_entrypoint" }) };
107
+ return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, runtimeDigest: observedDigest, distributionProfile: "runtime-plugin", reason: "invalid_entrypoint" }) };
94
108
  }
95
109
  if (!existsSync(entrypoint)) {
96
- return { envelope: envelope("unavailable", options, { observedVersion, source: "plugin_bundle", reason: "entrypoint_missing" }) };
110
+ return { envelope: envelope("unavailable", options, { observedVersion, source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, runtimeDigest: observedDigest, distributionProfile: "runtime-plugin", reason: "entrypoint_missing" }) };
111
+ }
112
+ const installed = existsSync(join(pluginRoot, INSTALLATION_PROVENANCE_FILE));
113
+ let provenanceStatus = "not_applicable";
114
+ let sourceDigest = null;
115
+ let evidencePlane = "loaded_session";
116
+ if (installed) {
117
+ const provenance = inspectInstallationProvenance(pluginRoot, {
118
+ definition,
119
+ runtimeManifest: manifest,
120
+ pluginVersion,
121
+ });
122
+ provenanceStatus = provenance.status;
123
+ sourceDigest = provenance.marker?.source_digest ?? null;
124
+ if (provenance.status !== "matched") {
125
+ return { envelope: envelope("version_mismatch", options, {
126
+ observedVersion,
127
+ source: "plugin_bundle",
128
+ pluginRoot,
129
+ pluginVersion,
130
+ canonicalVersion: definition.version,
131
+ runtimeDigest: observedDigest,
132
+ sourceDigest,
133
+ distributionProfile: "runtime-plugin",
134
+ evidencePlane: "loaded_session",
135
+ provenanceStatus,
136
+ reason: provenance.reason,
137
+ }) };
138
+ }
139
+ } else if (existsSync(join(pluginRoot, ".agdf-local-install.json"))) {
140
+ return { envelope: envelope("unavailable", options, {
141
+ observedVersion,
142
+ source: "plugin_bundle",
143
+ pluginRoot,
144
+ pluginVersion,
145
+ canonicalVersion: definition.version,
146
+ runtimeDigest: observedDigest,
147
+ distributionProfile: "runtime-plugin",
148
+ evidencePlane: "loaded_session",
149
+ provenanceStatus: "missing",
150
+ reason: "installation_provenance_missing",
151
+ }) };
152
+ } else {
153
+ const generatedRepository = inspectGeneratedRepositoryMarketplace(resolve(pluginRoot, "..", ".."));
154
+ if (pluginVersion !== definition.version
155
+ || generatedRepository.status !== "matched"
156
+ || canonicalPath(generatedRepository.pluginRoot) !== pluginRoot) {
157
+ return { envelope: envelope("unavailable", options, {
158
+ observedVersion,
159
+ source: "plugin_bundle",
160
+ pluginRoot,
161
+ pluginVersion,
162
+ canonicalVersion: definition.version,
163
+ runtimeDigest: observedDigest,
164
+ distributionProfile: "runtime-plugin",
165
+ evidencePlane: "installed_plugin_root",
166
+ provenanceStatus: "missing",
167
+ reason: "installation_provenance_missing",
168
+ }) };
169
+ }
170
+ evidencePlane = "generated_bundle";
97
171
  }
98
172
  return {
99
- envelope: envelope("owned_version_matched", options, { observedVersion, source: "plugin_bundle" }),
173
+ envelope: envelope("owned_version_matched", options, {
174
+ observedVersion,
175
+ source: "plugin_bundle",
176
+ pluginRoot,
177
+ pluginVersion,
178
+ canonicalVersion: definition.version,
179
+ runtimeDigest: observedDigest,
180
+ sourceDigest,
181
+ distributionProfile: "runtime-plugin",
182
+ evidencePlane,
183
+ provenanceStatus,
184
+ }),
100
185
  executable: process.execPath,
101
186
  prefixArgs: [entrypoint],
102
187
  };