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,48 +1,60 @@
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
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
+ import { dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } 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
+ function isSemanticVersion(version) {
44
+ return typeof version === "string"
45
+ && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.test(version);
46
+ }
47
+
48
+ export function codexLocalInstallVersion(version, sourceDigest) {
49
+ if (!/^[a-f0-9]{64}$/.test(sourceDigest)) throw new Error("AGDF local source digest must be a deterministic SHA-256 value.");
50
+ return `${semverBase(version)}+codex.local-${sourceDigest.slice(0, 12)}`;
51
+ }
52
+
53
+ export function isCodexLocalInstallVersion(version, candidate, sourceDigest = "") {
54
+ if (typeof candidate !== "string") return false;
55
+ if (sourceDigest) return candidate === codexLocalInstallVersion(version, sourceDigest);
56
+ const escaped = semverBase(version).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
57
+ return new RegExp(`^${escaped}\\+codex\\.local-[a-f0-9]{12}$`).test(candidate);
46
58
  }
47
59
 
48
60
  function readJson(path, label) {
@@ -53,15 +65,20 @@ function readJson(path, label) {
53
65
  }
54
66
  }
55
67
 
68
+ function pathForPlatform(platform = process.platform) {
69
+ return platform === "win32" ? win32 : posix;
70
+ }
71
+
56
72
  export function defaultAgdfDataRoot({ env = process.env, platform = process.platform, home = homedir() } = {}) {
57
- if (env.AGDF_DATA_DIR) return resolve(env.AGDF_DATA_DIR);
58
- if (platform === "darwin") return join(home, "Library", "Application Support", "agdf");
59
- if (platform === "win32") return join(env.LOCALAPPDATA || env.APPDATA || join(home, "AppData", "Local"), "agdf");
60
- return join(env.XDG_DATA_HOME || join(home, ".local", "share"), "agdf");
73
+ const targetPath = pathForPlatform(platform);
74
+ if (env.AGDF_DATA_DIR) return targetPath.resolve(env.AGDF_DATA_DIR);
75
+ if (platform === "darwin") return targetPath.join(home, "Library", "Application Support", "agdf");
76
+ if (platform === "win32") return targetPath.join(env.LOCALAPPDATA || env.APPDATA || targetPath.join(home, "AppData", "Local"), "agdf");
77
+ return targetPath.join(env.XDG_DATA_HOME || targetPath.join(home, ".local", "share"), "agdf");
61
78
  }
62
79
 
63
80
  export function localMarketplaceRoot(options = {}) {
64
- return join(defaultAgdfDataRoot(options), "marketplaces", MARKETPLACE_ID);
81
+ return pathForPlatform(options.platform).join(defaultAgdfDataRoot(options), "marketplaces", MARKETPLACE_ID);
65
82
  }
66
83
 
67
84
  function ownership(root, { allowBuilding = false } = {}) {
@@ -93,7 +110,7 @@ function writeJson(path, value) {
93
110
  function codexMarketplace(definition = pluginDefinition) {
94
111
  return {
95
112
  name: MARKETPLACE_ID,
96
- interface: { displayName: definition.displayName },
113
+ interface: { displayName: definition.publicDistribution.publicDisplayName },
97
114
  plugins: [{
98
115
  name: definition.id,
99
116
  source: { source: "local", path: "./plugins/agdf" },
@@ -103,6 +120,13 @@ function codexMarketplace(definition = pluginDefinition) {
103
120
  };
104
121
  }
105
122
 
123
+ function legacyCodexMarketplace(definition = pluginDefinition) {
124
+ return {
125
+ ...codexMarketplace(definition),
126
+ interface: { displayName: definition.displayName },
127
+ };
128
+ }
129
+
106
130
  function claudeMarketplace(definition = pluginDefinition) {
107
131
  return {
108
132
  name: MARKETPLACE_ID,
@@ -117,9 +141,15 @@ function claudeMarketplace(definition = pluginDefinition) {
117
141
  };
118
142
  }
119
143
 
120
- function validateBuiltPlugin(pluginRoot, expectedVersion) {
144
+ function validateBuiltPlugin(pluginRoot, expectedVersion, expectedCodexInstallVersion = expectedVersion, sourceDigest = "", {
145
+ requireInstallationProvenance = false,
146
+ allowLegacyProvenanceForMigration = false,
147
+ allowPreProvenanceShape = false,
148
+ } = {}) {
121
149
  const definition = readJson(join(pluginRoot, "meta", "agdf-plugin.definition.json"), "built plugin definition");
122
150
  const runtime = readJson(join(pluginRoot, "runtime", "runtime-manifest.json"), "built runtime manifest");
151
+ const codex = readJson(join(pluginRoot, ".codex-plugin", "plugin.json"), "built Codex plugin manifest");
152
+ const claude = readJson(join(pluginRoot, ".claude-plugin", "plugin.json"), "built Claude plugin manifest");
123
153
  for (const required of [
124
154
  join(pluginRoot, ".codex-plugin", "plugin.json"),
125
155
  join(pluginRoot, ".claude-plugin", "plugin.json"),
@@ -131,22 +161,112 @@ function validateBuiltPlugin(pluginRoot, expectedVersion) {
131
161
  if (definition.version !== expectedVersion || runtime.version !== expectedVersion) {
132
162
  throw new Error(`Built plugin version mismatch: expected ${expectedVersion}, definition ${definition.version}, runtime ${runtime.version}`);
133
163
  }
164
+ const profile = validateDistributionProfiles(definition);
165
+ const isPreProvenanceShape = allowPreProvenanceShape
166
+ && !Object.hasOwn(definition, "distributionProfiles");
167
+ let provenance = null;
168
+ if (profile.status !== "matched"
169
+ && requireInstallationProvenance
170
+ && allowLegacyProvenanceForMigration) {
171
+ provenance = inspectInstallationProvenance(pluginRoot, {
172
+ definition,
173
+ runtimeManifest: runtime,
174
+ pluginVersion: expectedCodexInstallVersion,
175
+ allowLegacy: true,
176
+ });
177
+ }
178
+ if (profile.status !== "matched" && provenance?.status !== "legacy" && !isPreProvenanceShape) {
179
+ throw new Error("Built plugin distribution profile contract is invalid.");
180
+ }
181
+ if (codex.version !== expectedCodexInstallVersion) {
182
+ throw new Error(`Built Codex plugin version mismatch: expected ${expectedCodexInstallVersion}, observed ${codex.version}`);
183
+ }
184
+ if (claude.version !== expectedVersion) {
185
+ throw new Error(`Built Claude plugin version mismatch: expected ${expectedVersion}, observed ${claude.version}`);
186
+ }
187
+ if (expectedCodexInstallVersion !== expectedVersion) {
188
+ if (!isCodexLocalInstallVersion(expectedVersion, expectedCodexInstallVersion, sourceDigest)) {
189
+ throw new Error(`Invalid AGDF Codex local install version: ${expectedCodexInstallVersion}`);
190
+ }
191
+ }
134
192
  if (!/^[a-f0-9]{64}$/.test(runtime.digest ?? "")) throw new Error("Built plugin runtime digest is invalid.");
135
193
  if (digestDirectory(join(pluginRoot, "runtime", "create-agdf")) !== runtime.digest) {
136
194
  throw new Error("Built plugin runtime digest does not match its payload.");
137
195
  }
196
+ if (requireInstallationProvenance) {
197
+ provenance ??= inspectInstallationProvenance(pluginRoot, {
198
+ definition,
199
+ runtimeManifest: runtime,
200
+ pluginVersion: expectedCodexInstallVersion,
201
+ allowLegacy: allowLegacyProvenanceForMigration,
202
+ });
203
+ if (provenance.status !== "matched"
204
+ && !(allowLegacyProvenanceForMigration && provenance.status === "legacy")) {
205
+ throw new Error(`Invalid AGDF installation provenance (${provenance.reason}): ${pluginRoot}`);
206
+ }
207
+ }
138
208
  return definition;
139
209
  }
140
210
 
141
211
  function validateMarketplaceRoot(root, definition = pluginDefinition) {
142
212
  const codex = readJson(join(root, ".agents", "plugins", "marketplace.json"), "Codex local marketplace manifest");
143
213
  const claude = readJson(join(root, ".claude-plugin", "marketplace.json"), "Claude local marketplace manifest");
144
- if (JSON.stringify(codex) !== JSON.stringify(codexMarketplace(definition))) {
214
+ const serializedCodex = JSON.stringify(codex);
215
+ const codexShape = serializedCodex === JSON.stringify(codexMarketplace(definition))
216
+ ? "current"
217
+ : serializedCodex === JSON.stringify(legacyCodexMarketplace(definition))
218
+ ? "legacy_full_product_label"
219
+ : "invalid";
220
+ if (codexShape === "invalid") {
145
221
  throw new Error(`Codex local marketplace manifest is not owned by ${MARKETPLACE_ID}: ${root}`);
146
222
  }
147
223
  if (JSON.stringify(claude) !== JSON.stringify(claudeMarketplace(definition))) {
148
224
  throw new Error(`Claude local marketplace manifest is not owned by ${MARKETPLACE_ID}: ${root}`);
149
225
  }
226
+ return Object.freeze({ codexShape });
227
+ }
228
+
229
+ function classifyExistingMarketplace(stableRoot, marker) {
230
+ const existingPluginRoot = join(stableRoot, "plugins", MARKETPLACE_ID);
231
+ const existingCodexInstallVersion = marker.codex_install_version ?? marker.version;
232
+ if (!isSemanticVersion(marker.version)
233
+ || !/^[a-f0-9]{64}$/.test(marker.plugin_digest ?? "")) {
234
+ throw new Error(`Refusing invalid AGDF marketplace ownership evidence: ${stableRoot}`);
235
+ }
236
+ const definition = readJson(
237
+ join(existingPluginRoot, "meta", "agdf-plugin.definition.json"),
238
+ "built plugin definition",
239
+ );
240
+ const hasCurrentProvenance = existsSync(join(existingPluginRoot, INSTALLATION_PROVENANCE_FILE));
241
+ const hasLegacyProvenance = existsSync(join(existingPluginRoot, LEGACY_LOCAL_INSTALL_FILE));
242
+ const preProvenanceCandidate = !hasCurrentProvenance
243
+ && !hasLegacyProvenance
244
+ && !Object.hasOwn(definition, "distributionProfiles");
245
+ const existingDefinition = validateBuiltPlugin(
246
+ existingPluginRoot,
247
+ marker.version,
248
+ existingCodexInstallVersion,
249
+ marker.source_digest ?? "",
250
+ preProvenanceCandidate
251
+ ? { allowPreProvenanceShape: true }
252
+ : { requireInstallationProvenance: true, allowLegacyProvenanceForMigration: true },
253
+ );
254
+ const marketplace = validateMarketplaceRoot(stableRoot, existingDefinition);
255
+ if (digestDirectory(existingPluginRoot) !== marker.plugin_digest) {
256
+ throw new Error(`Refusing tampered AGDF marketplace root: ${stableRoot}`);
257
+ }
258
+ return Object.freeze({
259
+ classification: preProvenanceCandidate
260
+ ? "owned_pre_provenance_rebuild"
261
+ : "current_or_marker_migration",
262
+ definition: existingDefinition,
263
+ marketplace,
264
+ });
265
+ }
266
+
267
+ function invalidExistingMarketplace(error) {
268
+ error.existingClassification = "invalid_or_unowned";
269
+ return error;
150
270
  }
151
271
 
152
272
  function recoverInterruptedTransaction(stableRoot, stageRoot, backupRoot, failedRoot) {
@@ -156,9 +276,9 @@ function recoverInterruptedTransaction(stableRoot, stageRoot, backupRoot, failed
156
276
  if (existsSync(failedRoot)) removeOwnedRoot(failedRoot, parent);
157
277
  if (existsSync(stableRoot)) {
158
278
  ownership(stableRoot);
159
- renameSync(stableRoot, failedRoot);
279
+ renameSyncWithRetry(stableRoot, failedRoot);
160
280
  }
161
- renameSync(backupRoot, stableRoot);
281
+ renameSyncWithRetry(backupRoot, stableRoot);
162
282
  if (existsSync(failedRoot)) removeOwnedRoot(failedRoot, parent);
163
283
  }
164
284
  if (existsSync(stageRoot)) removeOwnedRoot(stageRoot, parent, { allowBuilding: true });
@@ -169,6 +289,8 @@ export function prepareLocalMarketplace({
169
289
  dataRoot = defaultAgdfDataRoot(),
170
290
  builtPluginRoot = join(generatedRoot, "plugins", "agdf"),
171
291
  expectedVersion = pluginDefinition.version,
292
+ codexInstallVersion = expectedVersion,
293
+ codexRegistrationRevision = null,
172
294
  } = {}) {
173
295
  dataRoot = resolve(dataRoot);
174
296
  builtPluginRoot = resolve(builtPluginRoot);
@@ -178,18 +300,37 @@ export function prepareLocalMarketplace({
178
300
  const backupRoot = `${stableRoot}.backup`;
179
301
  const failedRoot = `${stableRoot}.failed`;
180
302
  if (!pathInside(dataRoot, stableRoot) || dirname(stableRoot) !== parent) throw new Error(`Unsafe AGDF marketplace root: ${stableRoot}`);
303
+ const sourceDigest = digestPluginSource(builtPluginRoot, expectedVersion);
304
+ if (codexInstallVersion !== expectedVersion
305
+ && !isCodexLocalInstallVersion(expectedVersion, codexInstallVersion, sourceDigest)) {
306
+ throw new Error(`Invalid AGDF Codex local install version: ${codexInstallVersion}`);
307
+ }
181
308
  const targetDefinition = validateBuiltPlugin(builtPluginRoot, expectedVersion);
182
309
  mkdirSync(parent, { recursive: true });
183
310
  recoverInterruptedTransaction(stableRoot, stageRoot, backupRoot, failedRoot);
184
311
 
185
- const existing = existsSync(stableRoot) ? ownership(stableRoot) : null;
312
+ let existing = null;
313
+ if (existsSync(stableRoot)) {
314
+ try {
315
+ existing = ownership(stableRoot);
316
+ } catch (error) {
317
+ throw invalidExistingMarketplace(error);
318
+ }
319
+ }
320
+ const targetCodexRegistrationRevision = codexRegistrationRevision
321
+ ?? existing?.codex_registration_revision
322
+ ?? 0;
323
+ let existingMarketplace = null;
324
+ let existingClassification = "none";
186
325
  if (existing) {
187
- const existingPluginRoot = join(stableRoot, "plugins", MARKETPLACE_ID);
188
- const existingDefinition = validateBuiltPlugin(existingPluginRoot, existing.version);
189
- validateMarketplaceRoot(stableRoot, existingDefinition);
190
- if (digestDirectory(existingPluginRoot) !== existing.plugin_digest) {
191
- throw new Error(`Refusing tampered AGDF marketplace root: ${stableRoot}`);
326
+ let classified;
327
+ try {
328
+ classified = classifyExistingMarketplace(stableRoot, existing);
329
+ } catch (error) {
330
+ throw invalidExistingMarketplace(error);
192
331
  }
332
+ existingClassification = classified.classification;
333
+ existingMarketplace = classified.marketplace;
193
334
  }
194
335
  mkdirSync(stageRoot, { recursive: false });
195
336
  try {
@@ -197,7 +338,10 @@ export function prepareLocalMarketplace({
197
338
  schema_version: 1,
198
339
  owner: "create-agdf",
199
340
  marketplace_id: MARKETPLACE_ID,
341
+ codex_registration_revision: targetCodexRegistrationRevision,
200
342
  version: expectedVersion,
343
+ codex_install_version: codexInstallVersion,
344
+ source_digest: sourceDigest,
201
345
  plugin_digest: null,
202
346
  source_package_version: readJson(join(packageRoot, "package.json"), "create-agdf package manifest").version,
203
347
  staging_state: "building",
@@ -205,6 +349,25 @@ export function prepareLocalMarketplace({
205
349
  const stagedPluginRoot = join(stageRoot, "plugins", MARKETPLACE_ID);
206
350
  cpSync(builtPluginRoot, stagedPluginRoot, { recursive: true });
207
351
  validateBuiltPlugin(stagedPluginRoot, expectedVersion);
352
+ if (codexInstallVersion !== expectedVersion) {
353
+ const codexManifestPath = join(stagedPluginRoot, ".codex-plugin", "plugin.json");
354
+ writeJson(codexManifestPath, { ...readJson(codexManifestPath, "built Codex plugin manifest"), version: codexInstallVersion });
355
+ }
356
+ const runtimeManifest = readJson(join(stagedPluginRoot, "runtime", "runtime-manifest.json"), "built runtime manifest");
357
+ writeJson(join(stagedPluginRoot, INSTALLATION_PROVENANCE_FILE), {
358
+ schema_version: 1,
359
+ owner: "create-agdf",
360
+ profile_id: "runtime-plugin",
361
+ marketplace_id: MARKETPLACE_ID,
362
+ canonical_version: expectedVersion,
363
+ codex_install_version: codexInstallVersion,
364
+ source_digest: sourceDigest,
365
+ runtime_digest: runtimeManifest.digest,
366
+ });
367
+ if (existsSync(join(stagedPluginRoot, LEGACY_LOCAL_INSTALL_FILE))) {
368
+ rmSync(join(stagedPluginRoot, LEGACY_LOCAL_INSTALL_FILE));
369
+ }
370
+ validateBuiltPlugin(stagedPluginRoot, expectedVersion, codexInstallVersion, sourceDigest, { requireInstallationProvenance: true });
208
371
  writeJson(join(stageRoot, ".agents", "plugins", "marketplace.json"), codexMarketplace(targetDefinition));
209
372
  writeJson(join(stageRoot, ".claude-plugin", "marketplace.json"), claudeMarketplace(targetDefinition));
210
373
  validateMarketplaceRoot(stageRoot, targetDefinition);
@@ -213,31 +376,40 @@ export function prepareLocalMarketplace({
213
376
  schema_version: 1,
214
377
  owner: "create-agdf",
215
378
  marketplace_id: MARKETPLACE_ID,
379
+ codex_registration_revision: targetCodexRegistrationRevision,
216
380
  version: expectedVersion,
381
+ codex_install_version: codexInstallVersion,
382
+ source_digest: sourceDigest,
217
383
  plugin_digest: pluginDigest,
218
384
  source_package_version: readJson(join(packageRoot, "package.json"), "create-agdf package manifest").version,
219
385
  staging_state: "ready",
220
386
  };
221
387
  writeJson(join(stageRoot, OWNERSHIP_FILE), marker);
222
388
 
223
- if (existing?.version === expectedVersion && existing?.plugin_digest === pluginDigest) {
389
+ if (existing?.version === expectedVersion
390
+ && existing?.plugin_digest === pluginDigest
391
+ && existing?.codex_registration_revision === targetCodexRegistrationRevision
392
+ && existingMarketplace?.codexShape === "current") {
224
393
  removeOwnedRoot(stageRoot, parent);
225
394
  return Object.freeze({
226
395
  root: stableRoot,
227
396
  pluginRoot: join(stableRoot, "plugins", MARKETPLACE_ID),
228
397
  version: expectedVersion,
398
+ codexInstallVersion,
399
+ sourceDigest,
229
400
  digest: pluginDigest,
401
+ existingClassification,
230
402
  changed: false,
231
403
  commit() {},
232
404
  rollback() {},
233
405
  });
234
406
  }
235
407
 
236
- if (existsSync(stableRoot)) renameSync(stableRoot, backupRoot);
408
+ if (existsSync(stableRoot)) renameSyncWithRetry(stableRoot, backupRoot);
237
409
  try {
238
- renameSync(stageRoot, stableRoot);
410
+ renameSyncWithRetry(stageRoot, stableRoot);
239
411
  } catch (error) {
240
- if (existsSync(backupRoot) && !existsSync(stableRoot)) renameSync(backupRoot, stableRoot);
412
+ if (existsSync(backupRoot) && !existsSync(stableRoot)) renameSyncWithRetry(backupRoot, stableRoot);
241
413
  throw error;
242
414
  }
243
415
  let closed = false;
@@ -245,7 +417,10 @@ export function prepareLocalMarketplace({
245
417
  root: stableRoot,
246
418
  pluginRoot: join(stableRoot, "plugins", MARKETPLACE_ID),
247
419
  version: expectedVersion,
420
+ codexInstallVersion,
421
+ sourceDigest,
248
422
  digest: pluginDigest,
423
+ existingClassification,
249
424
  changed: true,
250
425
  commit() {
251
426
  if (closed) return;
@@ -256,8 +431,8 @@ export function prepareLocalMarketplace({
256
431
  if (closed) return;
257
432
  if (existsSync(backupRoot)) {
258
433
  if (existsSync(failedRoot)) removeOwnedRoot(failedRoot, parent);
259
- if (existsSync(stableRoot)) renameSync(stableRoot, failedRoot);
260
- renameSync(backupRoot, stableRoot);
434
+ if (existsSync(stableRoot)) renameSyncWithRetry(stableRoot, failedRoot);
435
+ renameSyncWithRetry(backupRoot, stableRoot);
261
436
  if (existsSync(failedRoot)) removeOwnedRoot(failedRoot, parent);
262
437
  } else if (existsSync(stableRoot)) {
263
438
  removeOwnedRoot(stableRoot, parent);
@@ -271,6 +446,36 @@ export function prepareLocalMarketplace({
271
446
  }
272
447
  }
273
448
 
449
+ export function inspectLocalMarketplaceProjection({
450
+ dataRoot = defaultAgdfDataRoot(),
451
+ expectedVersion = pluginDefinition.version,
452
+ } = {}) {
453
+ const root = join(resolve(dataRoot), "marketplaces", MARKETPLACE_ID);
454
+ if (!existsSync(root)) return null;
455
+ const marker = ownership(root);
456
+ const codexInstallVersion = marker.codex_install_version ?? marker.version;
457
+ const pluginRoot = join(root, "plugins", MARKETPLACE_ID);
458
+ const definition = validateBuiltPlugin(
459
+ pluginRoot,
460
+ marker.version,
461
+ codexInstallVersion,
462
+ marker.source_digest ?? "",
463
+ { requireInstallationProvenance: true },
464
+ );
465
+ validateMarketplaceRoot(root, definition);
466
+ if (marker.version !== expectedVersion || digestDirectory(pluginRoot) !== marker.plugin_digest) {
467
+ throw new Error(`Refusing stale or tampered AGDF marketplace projection: ${root}`);
468
+ }
469
+ return Object.freeze({
470
+ root,
471
+ pluginRoot,
472
+ version: marker.version,
473
+ codexInstallVersion,
474
+ sourceDigest: marker.source_digest ?? "",
475
+ digest: marker.plugin_digest,
476
+ });
477
+ }
478
+
274
479
  function normalizedLegacySource(value) {
275
480
  if (typeof value !== "string") return "";
276
481
  return value.trim().toLowerCase()
@@ -319,5 +524,7 @@ export function classifyMarketplaceList(surface, output, stableRoot) {
319
524
  export const localMarketplaceConstants = Object.freeze({
320
525
  id: MARKETPLACE_ID,
321
526
  ownershipFile: OWNERSHIP_FILE,
527
+ installationProvenanceFile: INSTALLATION_PROVENANCE_FILE,
528
+ legacyLocalInstallFile: LEGACY_LOCAL_INSTALL_FILE,
322
529
  legacyRepository: LEGACY_REPOSITORY,
323
530
  });
@@ -6,6 +6,7 @@ import process from "node:process";
6
6
  import { generatedRoot, pluginDefinition } from "../cli/runtime-context.js";
7
7
  import { evaluateOpenCodeRepositoryActivation } from "./opencode-activation.js";
8
8
  import { resolveLocalValidator } from "../runtime/local-validator.js";
9
+ import { validateLocalOpenCodePackageSource } from "./local-development.js";
9
10
 
10
11
  const contractModules = ["gate-transition.md", "interaction.md", "modes.md", "quality.md", "context-graph.md", "control-scaffold.md", "closeout.md"];
11
12
  const openCodeSkillNames = pluginDefinition.skillSet.map((skill) => pluginDefinition.opencode.skillPrefix + skill.slug);
@@ -36,7 +37,15 @@ export function defaultOpenCodeConfigDir() {
36
37
  return process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
37
38
  }
38
39
 
39
- export function installOpenCodeGlobalPlugin(configDir) {
40
+ export function resolveOpenCodeInstallPackageSource(packageSource) {
41
+ const local = packageSource ? validateLocalOpenCodePackageSource(packageSource) : null;
42
+ return Object.freeze({
43
+ local,
44
+ specifier: local?.specifier ?? `${pluginDefinition.opencode.npmPackage}@${pluginDefinition.version}`,
45
+ });
46
+ }
47
+
48
+ export function installOpenCodeGlobalPlugin(configDir, dependencies = {}) {
40
49
  try {
41
50
  assertGlobalOpenCodeSurfaceWritable(configDir);
42
51
  } catch (error) {
@@ -97,8 +106,11 @@ export function installOpenCodeGlobalPlugin(configDir) {
97
106
  }
98
107
 
99
108
  mkdirSync(configDir, { recursive: true });
109
+ let localPackageSource = null;
100
110
  try {
101
- const packageSpecifier = `${pluginDefinition.opencode.npmPackage}@${pluginDefinition.version}`;
111
+ const packageSource = resolveOpenCodeInstallPackageSource(dependencies.packageSource);
112
+ localPackageSource = packageSource.local;
113
+ const packageSpecifier = packageSource.specifier;
102
114
  const invocation = openCodeNpmInvocation(["install", "--silent", "--save-prod", "--save-exact", packageSpecifier]);
103
115
  execFileSync(invocation.executable, invocation.args, {
104
116
  cwd: configDir,
@@ -123,6 +135,9 @@ export function installOpenCodeGlobalPlugin(configDir) {
123
135
  added: !alreadyInstalled,
124
136
  transition: openCodePackageTransition(previousPackage, installedPackage),
125
137
  sdk_alignment: sdkAlignment,
138
+ package_source: localPackageSource
139
+ ? { kind: "local_checkout", path: localPackageSource.tarball, digest: localPackageSource.digest, version: localPackageSource.version }
140
+ : { kind: "registry", path: "", digest: "", version: pluginDefinition.version },
126
141
  };
127
142
  }
128
143