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.
- package/README.md +7 -1
- package/generated/plugins/agdf/.claude-plugin/plugin.json +1 -1
- package/generated/plugins/agdf/.codex-plugin/plugin.json +2 -3
- package/generated/plugins/agdf/hooks/session-start.sh +31 -0
- package/generated/plugins/agdf/meta/agdf-plugin.definition.json +30 -9
- package/generated/plugins/agdf/runtime/agdf-local.js +8 -1
- package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +30 -9
- package/generated/plugins/agdf/runtime/create-agdf/lib/runtime/local-validator.js +116 -31
- package/generated/plugins/agdf/runtime/create-agdf/lib/runtime/plugin-provenance.js +206 -0
- package/generated/plugins/agdf/runtime/create-agdf/package.json +1 -1
- package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
- package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +64 -19
- package/generated/plugins/agdf/submission/openai/availability.md +1 -1
- package/generated/plugins/agdf/submission/openai/capability-matrix.json +1 -1
- package/generated/plugins/agdf/submission/openai/release-notes.md +1 -1
- package/generated/plugins/agdf/submission/openai/reviewer-cases.json +1 -1
- package/generated/submissions/openai/agdf/.codex-plugin/plugin.json +1 -1
- package/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json +30 -9
- package/generated/submissions/openai/agdf/submission/openai/availability.md +1 -1
- package/generated/submissions/openai/agdf/submission/openai/capability-matrix.json +1 -1
- package/generated/submissions/openai/agdf/submission/openai/inventory.json +9 -9
- package/generated/submissions/openai/agdf/submission/openai/listing.json +1 -1
- package/generated/submissions/openai/agdf/submission/openai/readiness.json +2 -2
- package/generated/submissions/openai/agdf/submission/openai/readiness.md +2 -2
- package/generated/submissions/openai/agdf/submission/openai/release-notes.md +1 -1
- package/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json +1 -1
- package/lib/cli/application.js +4 -3
- package/lib/fs-swap.js +26 -0
- package/lib/installers/local-development.js +202 -0
- package/lib/installers/local-marketplace.js +181 -36
- package/lib/installers/opencode.js +17 -2
- package/lib/installers/plugin-installers.js +86 -22
- package/lib/lifecycle/operations.js +9 -5
- package/lib/lifecycle/status.js +10 -2
- package/lib/public-plugin/builder.js +5 -4
- package/lib/runtime/local-validator.js +116 -31
- package/lib/runtime/plugin-provenance.js +206 -0
- package/package.json +6 -2
package/lib/cli/application.js
CHANGED
|
@@ -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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
222
|
+
renameSyncWithRetry(stableRoot, failedRoot);
|
|
160
223
|
}
|
|
161
|
-
|
|
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
|
|
189
|
-
|
|
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
|
|
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))
|
|
347
|
+
if (existsSync(stableRoot)) renameSyncWithRetry(stableRoot, backupRoot);
|
|
237
348
|
try {
|
|
238
|
-
|
|
349
|
+
renameSyncWithRetry(stageRoot, stableRoot);
|
|
239
350
|
} catch (error) {
|
|
240
|
-
if (existsSync(backupRoot) && !existsSync(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))
|
|
260
|
-
|
|
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
|
});
|