create-agdf 0.13.5 → 0.13.6

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 +181 -36
  31. package/lib/installers/opencode.js +17 -2
  32. package/lib/installers/plugin-installers.js +86 -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
@@ -35,7 +35,7 @@ import { CliUsageError, parseArgs } from "./parse-args.js";
35
35
  import { pluginDefinition } from "./runtime-context.js";
36
36
  import { createValidationHandlers } from "./validation-handlers.js";
37
37
 
38
- function createHandlers({ io, env, exec, prepare }) {
38
+ function createHandlers({ io, env, exec, prepare, openCodePackageSource }) {
39
39
  const installerAdapters = {
40
40
  ...(exec ? { exec } : {}),
41
41
  ...(prepare ? { prepare } : {}),
@@ -112,7 +112,7 @@ function createHandlers({ io, env, exec, prepare }) {
112
112
  ["opencode", (options) => {
113
113
  const configDir = options.dirExplicit ? options.dir : defaultOpenCodeConfigDir();
114
114
  try {
115
- const result = runLifecyclePhase("plugin_operation", () => installOpenCodeGlobalPlugin(configDir));
115
+ const result = runLifecyclePhase("plugin_operation", () => installOpenCodeGlobalPlugin(configDir, { packageSource: openCodePackageSource }));
116
116
  runLifecyclePhase("global_surface", () => installOpenCodeGlobalSurface(configDir));
117
117
  const report = runLifecyclePhase("verification", () => evaluateOpenCodeStatus(options.dir, configDir, result.transition));
118
118
  const alignmentHealthy = ["already_matching", "aligned"].includes(result.sdk_alignment.status);
@@ -150,6 +150,7 @@ function createHandlers({ io, env, exec, prepare }) {
150
150
  `experimental_hooks=${report.experimental_hooks.aggregate}`,
151
151
  `host_sdk_version=${report.host_sdk_version.status};policy=${report.host_sdk_version.policy}`,
152
152
  `sdk_alignment=${result.sdk_alignment.status};target=${result.sdk_alignment.target_version || "unknown"};installed=${result.sdk_alignment.installed_version || "unknown"}`,
153
+ `package_source=${result.package_source.kind}${result.package_source.digest ? `;digest=${result.package_source.digest}` : ""}`,
153
154
  ],
154
155
  },
155
156
  restart: { required: true, reason: "host_reload" },
@@ -341,7 +342,7 @@ export async function runCli(argv = process.argv.slice(2), adapters = {}) {
341
342
  }
342
343
  const command = resolveCommand(options.target);
343
344
  if (!command) throw new Error(`No handler is registered for ${options.target}.`);
344
- const handler = createHandlers({ io, env, exec: adapters.exec, prepare: adapters.prepare }).get(command.handler);
345
+ const handler = createHandlers({ io, env, exec: adapters.exec, prepare: adapters.prepare, openCodePackageSource: adapters.openCodePackageSource }).get(command.handler);
345
346
  if (!handler) throw new Error(`No implementation is registered for ${command.name}.`);
346
347
  return await handler(options);
347
348
  }
package/lib/fs-swap.js ADDED
@@ -0,0 +1,26 @@
1
+ import { renameSync } from "node:fs";
2
+ import process from "node:process";
3
+
4
+ const RETRY_ATTEMPTS = 5;
5
+ const RETRY_BASE_DELAY_MS = 50;
6
+
7
+ function sleepSync(milliseconds) {
8
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
9
+ }
10
+
11
+ export function renameSyncWithRetry(source, destination, adapters = {}) {
12
+ const rename = adapters.rename ?? renameSync;
13
+ const sleep = adapters.sleep ?? sleepSync;
14
+ const platform = adapters.platform ?? process.platform;
15
+ for (let attempt = 1; ; attempt += 1) {
16
+ try {
17
+ rename(source, destination);
18
+ return;
19
+ } catch (error) {
20
+ // Windows antivirus/indexer handle locks surface as transient EPERM on
21
+ // renames of freshly written directory trees; retry briefly before failing.
22
+ if (platform !== "win32" || error?.code !== "EPERM" || attempt >= RETRY_ATTEMPTS) throw error;
23
+ sleep(RETRY_BASE_DELAY_MS * attempt);
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,202 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import {
4
+ existsSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ renameSync,
10
+ rmSync,
11
+ statSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { join, resolve, win32 as pathWin32 } from "node:path";
15
+ import process from "node:process";
16
+
17
+ const LOCAL_PACKAGE_OWNER = "create-agdf";
18
+ const LOCAL_PACKAGE_KIND = "opencode_local_development_package";
19
+ const OWNERSHIP_FILE = ".agdf-owned.json";
20
+
21
+ function sha256File(path) {
22
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
23
+ }
24
+
25
+ function pathInside(root, candidate) {
26
+ const relativePath = candidate.slice(root.length);
27
+ return candidate === root || (candidate.startsWith(root) && ["/", "\\"].includes(relativePath[0]));
28
+ }
29
+
30
+ function readJson(path, label) {
31
+ try {
32
+ return JSON.parse(readFileSync(path, "utf8"));
33
+ } catch (error) {
34
+ throw new Error(`${label} is invalid: ${error.message}`);
35
+ }
36
+ }
37
+
38
+ function parsePackResult(output) {
39
+ const start = String(output).indexOf("[");
40
+ if (start < 0) throw new Error("AGDF local package build did not return npm pack JSON.");
41
+ const parsed = JSON.parse(String(output).slice(start));
42
+ const result = parsed?.[0];
43
+ const filename = result?.filename;
44
+ if (typeof filename !== "string" || !filename || filename.includes("/") || filename.includes("\\")) {
45
+ throw new Error("AGDF local package build returned an unsafe tarball name.");
46
+ }
47
+ if (!Array.isArray(result.files) || result.files.length === 0) {
48
+ throw new Error("AGDF local package build did not report its packed file inventory.");
49
+ }
50
+ return { filename, files: result.files };
51
+ }
52
+
53
+ export function localNpmExecutable(platform = process.platform, execPath = process.execPath) {
54
+ if (platform !== "win32") return { executable: "npm", prefix: [] };
55
+ return {
56
+ executable: execPath,
57
+ prefix: [pathWin32.join(pathWin32.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js")],
58
+ };
59
+ }
60
+
61
+ function digestPackedFiles(packageRoot, files) {
62
+ const root = resolve(packageRoot);
63
+ const paths = files.map((entry) => {
64
+ if (!entry
65
+ || typeof entry.path !== "string"
66
+ || !entry.path
67
+ || entry.path.includes("\\")
68
+ || !Number.isInteger(entry.mode)) {
69
+ throw new Error("AGDF local package build returned an invalid packed file path.");
70
+ }
71
+ const path = resolve(root, entry.path);
72
+ if (!pathInside(root, path) || path === root) throw new Error(`AGDF local package build returned an unsafe packed file path: ${entry.path}`);
73
+ const stats = lstatSync(path);
74
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new Error(`AGDF local package build returned a non-file package entry: ${entry.path}`);
75
+ return { path, relative: entry.path.replaceAll("\\", "/"), mode: entry.mode };
76
+ }).sort((left, right) => left.relative.localeCompare(right.relative));
77
+ const hash = createHash("sha256");
78
+ for (const entry of paths) {
79
+ hash.update(entry.relative);
80
+ hash.update("\0");
81
+ hash.update(String(entry.mode));
82
+ hash.update("\0");
83
+ hash.update(readFileSync(entry.path));
84
+ hash.update("\0");
85
+ }
86
+ return hash.digest("hex");
87
+ }
88
+
89
+ function validateOwnedPackageDirectory(root, expected = {}) {
90
+ const rootStats = lstatSync(root);
91
+ if (!rootStats.isDirectory() || rootStats.isSymbolicLink()) throw new Error(`Refusing unsafe AGDF local package directory: ${root}`);
92
+ const marker = readJson(join(root, OWNERSHIP_FILE), "AGDF local package ownership marker");
93
+ if (marker.schema_version !== 1
94
+ || marker.owner !== LOCAL_PACKAGE_OWNER
95
+ || marker.kind !== LOCAL_PACKAGE_KIND
96
+ || !/^[a-f0-9]{64}$/.test(marker.digest ?? "")
97
+ || !/^[a-f0-9]{64}$/.test(marker.archive_digest ?? "")
98
+ || typeof marker.filename !== "string"
99
+ || !marker.filename) {
100
+ throw new Error(`Refusing unowned AGDF local package directory: ${root}`);
101
+ }
102
+ if (expected.version && marker.version !== expected.version) throw new Error(`AGDF local package version mismatch: ${root}`);
103
+ if (expected.digest && marker.digest !== expected.digest) throw new Error(`AGDF local package digest mismatch: ${root}`);
104
+ const tarball = join(root, marker.filename);
105
+ if (!existsSync(tarball)
106
+ || !lstatSync(tarball).isFile()
107
+ || lstatSync(tarball).isSymbolicLink()
108
+ || sha256File(tarball) !== marker.archive_digest) {
109
+ throw new Error(`AGDF local package tarball is missing or tampered: ${root}`);
110
+ }
111
+ return { marker, tarball };
112
+ }
113
+
114
+ export function validateLocalOpenCodePackageSource(source) {
115
+ if (!source
116
+ || source.kind !== "local_checkout"
117
+ || typeof source.root !== "string"
118
+ || typeof source.dataRoot !== "string"
119
+ || !/^[a-f0-9]{64}$/.test(source.digest ?? "")) {
120
+ throw new Error("Invalid AGDF OpenCode local package source.");
121
+ }
122
+ const root = resolve(source.root);
123
+ const dataRoot = resolve(source.dataRoot);
124
+ const expectedRoot = join(dataRoot, "packages", "local", source.digest);
125
+ if (root !== expectedRoot) throw new Error("AGDF OpenCode local package source is outside its owned data root.");
126
+ const { marker, tarball } = validateOwnedPackageDirectory(root, {
127
+ version: source.version,
128
+ digest: source.digest,
129
+ });
130
+ const specifier = tarball;
131
+ if (source.specifier && source.specifier !== specifier) throw new Error("AGDF OpenCode local package specifier does not match its owned tarball.");
132
+ return Object.freeze({
133
+ kind: "local_checkout",
134
+ dataRoot,
135
+ root,
136
+ markerPath: join(root, OWNERSHIP_FILE),
137
+ tarball,
138
+ specifier,
139
+ version: marker.version,
140
+ digest: marker.digest,
141
+ archiveDigest: marker.archive_digest,
142
+ });
143
+ }
144
+
145
+ export function prepareLocalOpenCodePackage({
146
+ dataRoot,
147
+ packageRoot,
148
+ expectedVersion,
149
+ exec = execFileSync,
150
+ } = {}) {
151
+ if (!dataRoot || !packageRoot || !expectedVersion) throw new Error("AGDF local package preparation requires dataRoot, packageRoot and expectedVersion.");
152
+ const localRoot = resolve(dataRoot, "packages", "local");
153
+ mkdirSync(localRoot, { recursive: true });
154
+ const stageRoot = mkdtempSync(join(localRoot, ".stage-"));
155
+ try {
156
+ const npm = localNpmExecutable();
157
+ const output = exec(npm.executable, [
158
+ ...npm.prefix,
159
+ "pack",
160
+ "--ignore-scripts",
161
+ "--json",
162
+ "--pack-destination",
163
+ stageRoot,
164
+ ], {
165
+ cwd: resolve(packageRoot),
166
+ encoding: "utf8",
167
+ stdio: "pipe",
168
+ });
169
+ const { filename, files } = parsePackResult(output);
170
+ const stagedTarball = join(stageRoot, filename);
171
+ if (!existsSync(stagedTarball) || !statSync(stagedTarball).isFile()) throw new Error("AGDF local package tarball was not created.");
172
+ const digest = digestPackedFiles(packageRoot, files);
173
+ const archiveDigest = sha256File(stagedTarball);
174
+ const stableRoot = join(localRoot, digest);
175
+ if (existsSync(stableRoot)) {
176
+ const existing = validateLocalOpenCodePackageSource({ kind: "local_checkout", dataRoot, root: stableRoot, version: expectedVersion, digest });
177
+ if (existing.archiveDigest !== archiveDigest) throw new Error(`AGDF local package archive is not deterministic for packed content: ${stableRoot}`);
178
+ rmSync(stageRoot, { recursive: true, force: true });
179
+ return existing;
180
+ }
181
+ writeFileSync(join(stageRoot, OWNERSHIP_FILE), `${JSON.stringify({
182
+ schema_version: 1,
183
+ owner: LOCAL_PACKAGE_OWNER,
184
+ kind: LOCAL_PACKAGE_KIND,
185
+ version: expectedVersion,
186
+ digest,
187
+ archive_digest: archiveDigest,
188
+ filename,
189
+ }, null, 2)}\n`, "utf8");
190
+ renameSync(stageRoot, stableRoot);
191
+ return validateLocalOpenCodePackageSource({ kind: "local_checkout", dataRoot, root: stableRoot, version: expectedVersion, digest });
192
+ } catch (error) {
193
+ if (existsSync(stageRoot)) rmSync(stageRoot, { recursive: true, force: true });
194
+ throw error;
195
+ }
196
+ }
197
+
198
+ export const localDevelopmentConstants = Object.freeze({
199
+ owner: LOCAL_PACKAGE_OWNER,
200
+ kind: LOCAL_PACKAGE_KIND,
201
+ ownershipFile: OWNERSHIP_FILE,
202
+ });
@@ -1,48 +1,55 @@
1
- import { createHash } from "node:crypto";
2
1
  import {
3
2
  cpSync,
4
3
  existsSync,
5
4
  mkdirSync,
6
5
  readFileSync,
7
- readdirSync,
8
- renameSync,
9
6
  rmSync,
10
- statSync,
11
7
  writeFileSync,
12
8
  } from "node:fs";
13
9
  import { homedir } from "node:os";
14
10
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
15
11
  import process from "node:process";
16
12
  import { generatedRoot, packageRoot, pluginDefinition } from "../cli/runtime-context.js";
13
+ import { renameSyncWithRetry } from "../fs-swap.js";
14
+ import {
15
+ INSTALLATION_PROVENANCE_FILE,
16
+ LEGACY_LOCAL_INSTALL_FILE,
17
+ digestDirectory,
18
+ digestNormalizedPluginSource,
19
+ inspectInstallationProvenance,
20
+ validateDistributionProfiles,
21
+ } from "../runtime/plugin-provenance.js";
17
22
 
18
23
  const MARKETPLACE_ID = "agdf";
19
24
  const OWNERSHIP_FILE = ".agdf-owned.json";
20
25
  const LEGACY_REPOSITORY = "arndtgold/ai-native-governance-delivery-framework";
26
+ export const CODEX_REGISTRATION_REVISION = 1;
21
27
 
22
28
  function pathInside(root, candidate) {
23
29
  const rel = relative(root, candidate);
24
30
  return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
25
31
  }
26
32
 
27
- function digestDirectory(root) {
28
- const files = [];
29
- function visit(directory) {
30
- for (const name of readdirSync(directory).sort()) {
31
- const path = join(directory, name);
32
- const stats = statSync(path);
33
- if (stats.isDirectory()) visit(path);
34
- else if (stats.isFile()) files.push(path);
35
- }
36
- }
37
- visit(root);
38
- const hash = createHash("sha256");
39
- for (const path of files) {
40
- hash.update(relative(root, path).replaceAll("\\", "/"));
41
- hash.update("\0");
42
- hash.update(readFileSync(path));
43
- hash.update("\0");
44
- }
45
- return hash.digest("hex");
33
+ export function digestPluginSource(root, canonicalVersion = pluginDefinition.version) {
34
+ return digestNormalizedPluginSource(root, canonicalVersion);
35
+ }
36
+
37
+ export { digestDirectory };
38
+
39
+ function semverBase(version) {
40
+ return version.split("+")[0];
41
+ }
42
+
43
+ export function codexLocalInstallVersion(version, sourceDigest) {
44
+ if (!/^[a-f0-9]{64}$/.test(sourceDigest)) throw new Error("AGDF local source digest must be a deterministic SHA-256 value.");
45
+ return `${semverBase(version)}+codex.local-${sourceDigest.slice(0, 12)}`;
46
+ }
47
+
48
+ export function isCodexLocalInstallVersion(version, candidate, sourceDigest = "") {
49
+ if (typeof candidate !== "string") return false;
50
+ if (sourceDigest) return candidate === codexLocalInstallVersion(version, sourceDigest);
51
+ const escaped = semverBase(version).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
52
+ return new RegExp(`^${escaped}\\+codex\\.local-[a-f0-9]{12}$`).test(candidate);
46
53
  }
47
54
 
48
55
  function readJson(path, label) {
@@ -93,7 +100,7 @@ function writeJson(path, value) {
93
100
  function codexMarketplace(definition = pluginDefinition) {
94
101
  return {
95
102
  name: MARKETPLACE_ID,
96
- interface: { displayName: definition.displayName },
103
+ interface: { displayName: definition.publicDistribution.publicDisplayName },
97
104
  plugins: [{
98
105
  name: definition.id,
99
106
  source: { source: "local", path: "./plugins/agdf" },
@@ -103,6 +110,13 @@ function codexMarketplace(definition = pluginDefinition) {
103
110
  };
104
111
  }
105
112
 
113
+ function legacyCodexMarketplace(definition = pluginDefinition) {
114
+ return {
115
+ ...codexMarketplace(definition),
116
+ interface: { displayName: definition.displayName },
117
+ };
118
+ }
119
+
106
120
  function claudeMarketplace(definition = pluginDefinition) {
107
121
  return {
108
122
  name: MARKETPLACE_ID,
@@ -117,9 +131,14 @@ function claudeMarketplace(definition = pluginDefinition) {
117
131
  };
118
132
  }
119
133
 
120
- function validateBuiltPlugin(pluginRoot, expectedVersion) {
134
+ function validateBuiltPlugin(pluginRoot, expectedVersion, expectedCodexInstallVersion = expectedVersion, sourceDigest = "", {
135
+ requireInstallationProvenance = false,
136
+ allowLegacyProvenanceForMigration = false,
137
+ } = {}) {
121
138
  const definition = readJson(join(pluginRoot, "meta", "agdf-plugin.definition.json"), "built plugin definition");
122
139
  const runtime = readJson(join(pluginRoot, "runtime", "runtime-manifest.json"), "built runtime manifest");
140
+ const codex = readJson(join(pluginRoot, ".codex-plugin", "plugin.json"), "built Codex plugin manifest");
141
+ const claude = readJson(join(pluginRoot, ".claude-plugin", "plugin.json"), "built Claude plugin manifest");
123
142
  for (const required of [
124
143
  join(pluginRoot, ".codex-plugin", "plugin.json"),
125
144
  join(pluginRoot, ".claude-plugin", "plugin.json"),
@@ -131,22 +150,66 @@ function validateBuiltPlugin(pluginRoot, expectedVersion) {
131
150
  if (definition.version !== expectedVersion || runtime.version !== expectedVersion) {
132
151
  throw new Error(`Built plugin version mismatch: expected ${expectedVersion}, definition ${definition.version}, runtime ${runtime.version}`);
133
152
  }
153
+ let provenance = null;
154
+ if (validateDistributionProfiles(definition).status !== "matched"
155
+ && requireInstallationProvenance
156
+ && allowLegacyProvenanceForMigration) {
157
+ provenance = inspectInstallationProvenance(pluginRoot, {
158
+ definition,
159
+ runtimeManifest: runtime,
160
+ pluginVersion: expectedCodexInstallVersion,
161
+ allowLegacy: true,
162
+ });
163
+ }
164
+ if (validateDistributionProfiles(definition).status !== "matched" && provenance?.status !== "legacy") {
165
+ throw new Error("Built plugin distribution profile contract is invalid.");
166
+ }
167
+ if (codex.version !== expectedCodexInstallVersion) {
168
+ throw new Error(`Built Codex plugin version mismatch: expected ${expectedCodexInstallVersion}, observed ${codex.version}`);
169
+ }
170
+ if (claude.version !== expectedVersion) {
171
+ throw new Error(`Built Claude plugin version mismatch: expected ${expectedVersion}, observed ${claude.version}`);
172
+ }
173
+ if (expectedCodexInstallVersion !== expectedVersion) {
174
+ if (!isCodexLocalInstallVersion(expectedVersion, expectedCodexInstallVersion, sourceDigest)) {
175
+ throw new Error(`Invalid AGDF Codex local install version: ${expectedCodexInstallVersion}`);
176
+ }
177
+ }
134
178
  if (!/^[a-f0-9]{64}$/.test(runtime.digest ?? "")) throw new Error("Built plugin runtime digest is invalid.");
135
179
  if (digestDirectory(join(pluginRoot, "runtime", "create-agdf")) !== runtime.digest) {
136
180
  throw new Error("Built plugin runtime digest does not match its payload.");
137
181
  }
182
+ if (requireInstallationProvenance) {
183
+ provenance ??= inspectInstallationProvenance(pluginRoot, {
184
+ definition,
185
+ runtimeManifest: runtime,
186
+ pluginVersion: expectedCodexInstallVersion,
187
+ allowLegacy: allowLegacyProvenanceForMigration,
188
+ });
189
+ if (provenance.status !== "matched"
190
+ && !(allowLegacyProvenanceForMigration && provenance.status === "legacy")) {
191
+ throw new Error(`Invalid AGDF installation provenance (${provenance.reason}): ${pluginRoot}`);
192
+ }
193
+ }
138
194
  return definition;
139
195
  }
140
196
 
141
197
  function validateMarketplaceRoot(root, definition = pluginDefinition) {
142
198
  const codex = readJson(join(root, ".agents", "plugins", "marketplace.json"), "Codex local marketplace manifest");
143
199
  const claude = readJson(join(root, ".claude-plugin", "marketplace.json"), "Claude local marketplace manifest");
144
- if (JSON.stringify(codex) !== JSON.stringify(codexMarketplace(definition))) {
200
+ const serializedCodex = JSON.stringify(codex);
201
+ const codexShape = serializedCodex === JSON.stringify(codexMarketplace(definition))
202
+ ? "current"
203
+ : serializedCodex === JSON.stringify(legacyCodexMarketplace(definition))
204
+ ? "legacy_full_product_label"
205
+ : "invalid";
206
+ if (codexShape === "invalid") {
145
207
  throw new Error(`Codex local marketplace manifest is not owned by ${MARKETPLACE_ID}: ${root}`);
146
208
  }
147
209
  if (JSON.stringify(claude) !== JSON.stringify(claudeMarketplace(definition))) {
148
210
  throw new Error(`Claude local marketplace manifest is not owned by ${MARKETPLACE_ID}: ${root}`);
149
211
  }
212
+ return Object.freeze({ codexShape });
150
213
  }
151
214
 
152
215
  function recoverInterruptedTransaction(stableRoot, stageRoot, backupRoot, failedRoot) {
@@ -156,9 +219,9 @@ function recoverInterruptedTransaction(stableRoot, stageRoot, backupRoot, failed
156
219
  if (existsSync(failedRoot)) removeOwnedRoot(failedRoot, parent);
157
220
  if (existsSync(stableRoot)) {
158
221
  ownership(stableRoot);
159
- renameSync(stableRoot, failedRoot);
222
+ renameSyncWithRetry(stableRoot, failedRoot);
160
223
  }
161
- renameSync(backupRoot, stableRoot);
224
+ renameSyncWithRetry(backupRoot, stableRoot);
162
225
  if (existsSync(failedRoot)) removeOwnedRoot(failedRoot, parent);
163
226
  }
164
227
  if (existsSync(stageRoot)) removeOwnedRoot(stageRoot, parent, { allowBuilding: true });
@@ -169,6 +232,8 @@ export function prepareLocalMarketplace({
169
232
  dataRoot = defaultAgdfDataRoot(),
170
233
  builtPluginRoot = join(generatedRoot, "plugins", "agdf"),
171
234
  expectedVersion = pluginDefinition.version,
235
+ codexInstallVersion = expectedVersion,
236
+ codexRegistrationRevision = null,
172
237
  } = {}) {
173
238
  dataRoot = resolve(dataRoot);
174
239
  builtPluginRoot = resolve(builtPluginRoot);
@@ -178,15 +243,31 @@ export function prepareLocalMarketplace({
178
243
  const backupRoot = `${stableRoot}.backup`;
179
244
  const failedRoot = `${stableRoot}.failed`;
180
245
  if (!pathInside(dataRoot, stableRoot) || dirname(stableRoot) !== parent) throw new Error(`Unsafe AGDF marketplace root: ${stableRoot}`);
246
+ const sourceDigest = digestPluginSource(builtPluginRoot, expectedVersion);
247
+ if (codexInstallVersion !== expectedVersion
248
+ && !isCodexLocalInstallVersion(expectedVersion, codexInstallVersion, sourceDigest)) {
249
+ throw new Error(`Invalid AGDF Codex local install version: ${codexInstallVersion}`);
250
+ }
181
251
  const targetDefinition = validateBuiltPlugin(builtPluginRoot, expectedVersion);
182
252
  mkdirSync(parent, { recursive: true });
183
253
  recoverInterruptedTransaction(stableRoot, stageRoot, backupRoot, failedRoot);
184
254
 
185
255
  const existing = existsSync(stableRoot) ? ownership(stableRoot) : null;
256
+ const targetCodexRegistrationRevision = codexRegistrationRevision
257
+ ?? existing?.codex_registration_revision
258
+ ?? 0;
259
+ let existingMarketplace = null;
186
260
  if (existing) {
187
261
  const existingPluginRoot = join(stableRoot, "plugins", MARKETPLACE_ID);
188
- const existingDefinition = validateBuiltPlugin(existingPluginRoot, existing.version);
189
- validateMarketplaceRoot(stableRoot, existingDefinition);
262
+ const existingCodexInstallVersion = existing.codex_install_version ?? existing.version;
263
+ const existingDefinition = validateBuiltPlugin(
264
+ existingPluginRoot,
265
+ existing.version,
266
+ existingCodexInstallVersion,
267
+ existing.source_digest ?? "",
268
+ { requireInstallationProvenance: true, allowLegacyProvenanceForMigration: true },
269
+ );
270
+ existingMarketplace = validateMarketplaceRoot(stableRoot, existingDefinition);
190
271
  if (digestDirectory(existingPluginRoot) !== existing.plugin_digest) {
191
272
  throw new Error(`Refusing tampered AGDF marketplace root: ${stableRoot}`);
192
273
  }
@@ -197,7 +278,10 @@ export function prepareLocalMarketplace({
197
278
  schema_version: 1,
198
279
  owner: "create-agdf",
199
280
  marketplace_id: MARKETPLACE_ID,
281
+ codex_registration_revision: targetCodexRegistrationRevision,
200
282
  version: expectedVersion,
283
+ codex_install_version: codexInstallVersion,
284
+ source_digest: sourceDigest,
201
285
  plugin_digest: null,
202
286
  source_package_version: readJson(join(packageRoot, "package.json"), "create-agdf package manifest").version,
203
287
  staging_state: "building",
@@ -205,6 +289,25 @@ export function prepareLocalMarketplace({
205
289
  const stagedPluginRoot = join(stageRoot, "plugins", MARKETPLACE_ID);
206
290
  cpSync(builtPluginRoot, stagedPluginRoot, { recursive: true });
207
291
  validateBuiltPlugin(stagedPluginRoot, expectedVersion);
292
+ if (codexInstallVersion !== expectedVersion) {
293
+ const codexManifestPath = join(stagedPluginRoot, ".codex-plugin", "plugin.json");
294
+ writeJson(codexManifestPath, { ...readJson(codexManifestPath, "built Codex plugin manifest"), version: codexInstallVersion });
295
+ }
296
+ const runtimeManifest = readJson(join(stagedPluginRoot, "runtime", "runtime-manifest.json"), "built runtime manifest");
297
+ writeJson(join(stagedPluginRoot, INSTALLATION_PROVENANCE_FILE), {
298
+ schema_version: 1,
299
+ owner: "create-agdf",
300
+ profile_id: "runtime-plugin",
301
+ marketplace_id: MARKETPLACE_ID,
302
+ canonical_version: expectedVersion,
303
+ codex_install_version: codexInstallVersion,
304
+ source_digest: sourceDigest,
305
+ runtime_digest: runtimeManifest.digest,
306
+ });
307
+ if (existsSync(join(stagedPluginRoot, LEGACY_LOCAL_INSTALL_FILE))) {
308
+ rmSync(join(stagedPluginRoot, LEGACY_LOCAL_INSTALL_FILE));
309
+ }
310
+ validateBuiltPlugin(stagedPluginRoot, expectedVersion, codexInstallVersion, sourceDigest, { requireInstallationProvenance: true });
208
311
  writeJson(join(stageRoot, ".agents", "plugins", "marketplace.json"), codexMarketplace(targetDefinition));
209
312
  writeJson(join(stageRoot, ".claude-plugin", "marketplace.json"), claudeMarketplace(targetDefinition));
210
313
  validateMarketplaceRoot(stageRoot, targetDefinition);
@@ -213,19 +316,27 @@ export function prepareLocalMarketplace({
213
316
  schema_version: 1,
214
317
  owner: "create-agdf",
215
318
  marketplace_id: MARKETPLACE_ID,
319
+ codex_registration_revision: targetCodexRegistrationRevision,
216
320
  version: expectedVersion,
321
+ codex_install_version: codexInstallVersion,
322
+ source_digest: sourceDigest,
217
323
  plugin_digest: pluginDigest,
218
324
  source_package_version: readJson(join(packageRoot, "package.json"), "create-agdf package manifest").version,
219
325
  staging_state: "ready",
220
326
  };
221
327
  writeJson(join(stageRoot, OWNERSHIP_FILE), marker);
222
328
 
223
- if (existing?.version === expectedVersion && existing?.plugin_digest === pluginDigest) {
329
+ if (existing?.version === expectedVersion
330
+ && existing?.plugin_digest === pluginDigest
331
+ && existing?.codex_registration_revision === targetCodexRegistrationRevision
332
+ && existingMarketplace?.codexShape === "current") {
224
333
  removeOwnedRoot(stageRoot, parent);
225
334
  return Object.freeze({
226
335
  root: stableRoot,
227
336
  pluginRoot: join(stableRoot, "plugins", MARKETPLACE_ID),
228
337
  version: expectedVersion,
338
+ codexInstallVersion,
339
+ sourceDigest,
229
340
  digest: pluginDigest,
230
341
  changed: false,
231
342
  commit() {},
@@ -233,11 +344,11 @@ export function prepareLocalMarketplace({
233
344
  });
234
345
  }
235
346
 
236
- if (existsSync(stableRoot)) renameSync(stableRoot, backupRoot);
347
+ if (existsSync(stableRoot)) renameSyncWithRetry(stableRoot, backupRoot);
237
348
  try {
238
- renameSync(stageRoot, stableRoot);
349
+ renameSyncWithRetry(stageRoot, stableRoot);
239
350
  } catch (error) {
240
- if (existsSync(backupRoot) && !existsSync(stableRoot)) renameSync(backupRoot, stableRoot);
351
+ if (existsSync(backupRoot) && !existsSync(stableRoot)) renameSyncWithRetry(backupRoot, stableRoot);
241
352
  throw error;
242
353
  }
243
354
  let closed = false;
@@ -245,6 +356,8 @@ export function prepareLocalMarketplace({
245
356
  root: stableRoot,
246
357
  pluginRoot: join(stableRoot, "plugins", MARKETPLACE_ID),
247
358
  version: expectedVersion,
359
+ codexInstallVersion,
360
+ sourceDigest,
248
361
  digest: pluginDigest,
249
362
  changed: true,
250
363
  commit() {
@@ -256,8 +369,8 @@ export function prepareLocalMarketplace({
256
369
  if (closed) return;
257
370
  if (existsSync(backupRoot)) {
258
371
  if (existsSync(failedRoot)) removeOwnedRoot(failedRoot, parent);
259
- if (existsSync(stableRoot)) renameSync(stableRoot, failedRoot);
260
- renameSync(backupRoot, stableRoot);
372
+ if (existsSync(stableRoot)) renameSyncWithRetry(stableRoot, failedRoot);
373
+ renameSyncWithRetry(backupRoot, stableRoot);
261
374
  if (existsSync(failedRoot)) removeOwnedRoot(failedRoot, parent);
262
375
  } else if (existsSync(stableRoot)) {
263
376
  removeOwnedRoot(stableRoot, parent);
@@ -271,6 +384,36 @@ export function prepareLocalMarketplace({
271
384
  }
272
385
  }
273
386
 
387
+ export function inspectLocalMarketplaceProjection({
388
+ dataRoot = defaultAgdfDataRoot(),
389
+ expectedVersion = pluginDefinition.version,
390
+ } = {}) {
391
+ const root = join(resolve(dataRoot), "marketplaces", MARKETPLACE_ID);
392
+ if (!existsSync(root)) return null;
393
+ const marker = ownership(root);
394
+ const codexInstallVersion = marker.codex_install_version ?? marker.version;
395
+ const pluginRoot = join(root, "plugins", MARKETPLACE_ID);
396
+ const definition = validateBuiltPlugin(
397
+ pluginRoot,
398
+ marker.version,
399
+ codexInstallVersion,
400
+ marker.source_digest ?? "",
401
+ { requireInstallationProvenance: true },
402
+ );
403
+ validateMarketplaceRoot(root, definition);
404
+ if (marker.version !== expectedVersion || digestDirectory(pluginRoot) !== marker.plugin_digest) {
405
+ throw new Error(`Refusing stale or tampered AGDF marketplace projection: ${root}`);
406
+ }
407
+ return Object.freeze({
408
+ root,
409
+ pluginRoot,
410
+ version: marker.version,
411
+ codexInstallVersion,
412
+ sourceDigest: marker.source_digest ?? "",
413
+ digest: marker.plugin_digest,
414
+ });
415
+ }
416
+
274
417
  function normalizedLegacySource(value) {
275
418
  if (typeof value !== "string") return "";
276
419
  return value.trim().toLowerCase()
@@ -319,5 +462,7 @@ export function classifyMarketplaceList(surface, output, stableRoot) {
319
462
  export const localMarketplaceConstants = Object.freeze({
320
463
  id: MARKETPLACE_ID,
321
464
  ownershipFile: OWNERSHIP_FILE,
465
+ installationProvenanceFile: INSTALLATION_PROVENANCE_FILE,
466
+ legacyLocalInstallFile: LEGACY_LOCAL_INSTALL_FILE,
322
467
  legacyRepository: LEGACY_REPOSITORY,
323
468
  });