create-agdf 0.13.6 → 0.13.8

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 (44) hide show
  1. package/README.md +26 -0
  2. package/generated/plugins/agdf/.claude-plugin/plugin.json +1 -1
  3. package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
  4. package/generated/plugins/agdf/hooks/hooks.json +3 -2
  5. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +24 -1
  6. package/generated/plugins/agdf/runtime/agdf-session-check.js +92 -0
  7. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +24 -1
  8. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/command-registry.js +9 -0
  9. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/parse-args.js +17 -0
  10. package/generated/plugins/agdf/runtime/create-agdf/lib/runtime-check-consent/contract.js +71 -0
  11. package/generated/plugins/agdf/runtime/create-agdf/package.json +1 -1
  12. package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
  13. package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +19 -2
  14. package/generated/plugins/agdf/submission/openai/availability.md +1 -1
  15. package/generated/plugins/agdf/submission/openai/capability-matrix.json +8 -1
  16. package/generated/plugins/agdf/submission/openai/release-notes.md +12 -1
  17. package/generated/plugins/agdf/submission/openai/reviewer-cases.json +23 -1
  18. package/generated/submissions/openai/agdf/.codex-plugin/plugin.json +1 -1
  19. package/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json +24 -1
  20. package/generated/submissions/openai/agdf/submission/openai/availability.md +1 -1
  21. package/generated/submissions/openai/agdf/submission/openai/capability-matrix.json +8 -1
  22. package/generated/submissions/openai/agdf/submission/openai/inventory.json +9 -9
  23. package/generated/submissions/openai/agdf/submission/openai/listing.json +1 -1
  24. package/generated/submissions/openai/agdf/submission/openai/readiness.json +2 -2
  25. package/generated/submissions/openai/agdf/submission/openai/readiness.md +2 -2
  26. package/generated/submissions/openai/agdf/submission/openai/release-notes.md +12 -1
  27. package/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json +23 -1
  28. package/lib/cli/application.js +228 -13
  29. package/lib/cli/command-registry.js +9 -0
  30. package/lib/cli/parse-args.js +17 -0
  31. package/lib/installers/local-marketplace.js +85 -21
  32. package/lib/installers/opencode.js +21 -5
  33. package/lib/installers/plugin-installers.js +10 -0
  34. package/lib/lifecycle/presentation.js +43 -2
  35. package/lib/lifecycle/result.js +9 -0
  36. package/lib/lifecycle/status.js +5 -1
  37. package/lib/runtime-check-consent/adapters.js +42 -0
  38. package/lib/runtime-check-consent/claude-settings.js +71 -0
  39. package/lib/runtime-check-consent/contract.js +71 -0
  40. package/lib/runtime-check-consent/coordinator.js +37 -0
  41. package/lib/runtime-check-consent/service.js +181 -0
  42. package/lib/runtime-check-consent/state.js +90 -0
  43. package/opencode-plugin.js +30 -15
  44. package/package.json +2 -1
@@ -7,7 +7,7 @@ import {
7
7
  writeFileSync,
8
8
  } from "node:fs";
9
9
  import { homedir } from "node:os";
10
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
+ import { dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
11
11
  import process from "node:process";
12
12
  import { generatedRoot, packageRoot, pluginDefinition } from "../cli/runtime-context.js";
13
13
  import { renameSyncWithRetry } from "../fs-swap.js";
@@ -40,6 +40,11 @@ function semverBase(version) {
40
40
  return version.split("+")[0];
41
41
  }
42
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
+
43
48
  export function codexLocalInstallVersion(version, sourceDigest) {
44
49
  if (!/^[a-f0-9]{64}$/.test(sourceDigest)) throw new Error("AGDF local source digest must be a deterministic SHA-256 value.");
45
50
  return `${semverBase(version)}+codex.local-${sourceDigest.slice(0, 12)}`;
@@ -60,15 +65,20 @@ function readJson(path, label) {
60
65
  }
61
66
  }
62
67
 
68
+ function pathForPlatform(platform = process.platform) {
69
+ return platform === "win32" ? win32 : posix;
70
+ }
71
+
63
72
  export function defaultAgdfDataRoot({ env = process.env, platform = process.platform, home = homedir() } = {}) {
64
- if (env.AGDF_DATA_DIR) return resolve(env.AGDF_DATA_DIR);
65
- if (platform === "darwin") return join(home, "Library", "Application Support", "agdf");
66
- if (platform === "win32") return join(env.LOCALAPPDATA || env.APPDATA || join(home, "AppData", "Local"), "agdf");
67
- 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");
68
78
  }
69
79
 
70
80
  export function localMarketplaceRoot(options = {}) {
71
- return join(defaultAgdfDataRoot(options), "marketplaces", MARKETPLACE_ID);
81
+ return pathForPlatform(options.platform).join(defaultAgdfDataRoot(options), "marketplaces", MARKETPLACE_ID);
72
82
  }
73
83
 
74
84
  function ownership(root, { allowBuilding = false } = {}) {
@@ -134,6 +144,7 @@ function claudeMarketplace(definition = pluginDefinition) {
134
144
  function validateBuiltPlugin(pluginRoot, expectedVersion, expectedCodexInstallVersion = expectedVersion, sourceDigest = "", {
135
145
  requireInstallationProvenance = false,
136
146
  allowLegacyProvenanceForMigration = false,
147
+ allowPreProvenanceShape = false,
137
148
  } = {}) {
138
149
  const definition = readJson(join(pluginRoot, "meta", "agdf-plugin.definition.json"), "built plugin definition");
139
150
  const runtime = readJson(join(pluginRoot, "runtime", "runtime-manifest.json"), "built runtime manifest");
@@ -150,8 +161,11 @@ function validateBuiltPlugin(pluginRoot, expectedVersion, expectedCodexInstallVe
150
161
  if (definition.version !== expectedVersion || runtime.version !== expectedVersion) {
151
162
  throw new Error(`Built plugin version mismatch: expected ${expectedVersion}, definition ${definition.version}, runtime ${runtime.version}`);
152
163
  }
164
+ const profile = validateDistributionProfiles(definition);
165
+ const isPreProvenanceShape = allowPreProvenanceShape
166
+ && !Object.hasOwn(definition, "distributionProfiles");
153
167
  let provenance = null;
154
- if (validateDistributionProfiles(definition).status !== "matched"
168
+ if (profile.status !== "matched"
155
169
  && requireInstallationProvenance
156
170
  && allowLegacyProvenanceForMigration) {
157
171
  provenance = inspectInstallationProvenance(pluginRoot, {
@@ -161,7 +175,7 @@ function validateBuiltPlugin(pluginRoot, expectedVersion, expectedCodexInstallVe
161
175
  allowLegacy: true,
162
176
  });
163
177
  }
164
- if (validateDistributionProfiles(definition).status !== "matched" && provenance?.status !== "legacy") {
178
+ if (profile.status !== "matched" && provenance?.status !== "legacy" && !isPreProvenanceShape) {
165
179
  throw new Error("Built plugin distribution profile contract is invalid.");
166
180
  }
167
181
  if (codex.version !== expectedCodexInstallVersion) {
@@ -212,6 +226,49 @@ function validateMarketplaceRoot(root, definition = pluginDefinition) {
212
226
  return Object.freeze({ codexShape });
213
227
  }
214
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;
270
+ }
271
+
215
272
  function recoverInterruptedTransaction(stableRoot, stageRoot, backupRoot, failedRoot) {
216
273
  const parent = dirname(stableRoot);
217
274
  if (existsSync(backupRoot)) {
@@ -252,25 +309,28 @@ export function prepareLocalMarketplace({
252
309
  mkdirSync(parent, { recursive: true });
253
310
  recoverInterruptedTransaction(stableRoot, stageRoot, backupRoot, failedRoot);
254
311
 
255
- 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
+ }
256
320
  const targetCodexRegistrationRevision = codexRegistrationRevision
257
321
  ?? existing?.codex_registration_revision
258
322
  ?? 0;
259
323
  let existingMarketplace = null;
324
+ let existingClassification = "none";
260
325
  if (existing) {
261
- const existingPluginRoot = join(stableRoot, "plugins", MARKETPLACE_ID);
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);
271
- if (digestDirectory(existingPluginRoot) !== existing.plugin_digest) {
272
- 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);
273
331
  }
332
+ existingClassification = classified.classification;
333
+ existingMarketplace = classified.marketplace;
274
334
  }
275
335
  mkdirSync(stageRoot, { recursive: false });
276
336
  try {
@@ -338,6 +398,8 @@ export function prepareLocalMarketplace({
338
398
  codexInstallVersion,
339
399
  sourceDigest,
340
400
  digest: pluginDigest,
401
+ runtimeDigest: runtimeManifest.digest,
402
+ existingClassification,
341
403
  changed: false,
342
404
  commit() {},
343
405
  rollback() {},
@@ -359,6 +421,8 @@ export function prepareLocalMarketplace({
359
421
  codexInstallVersion,
360
422
  sourceDigest,
361
423
  digest: pluginDigest,
424
+ runtimeDigest: runtimeManifest.digest,
425
+ existingClassification,
362
426
  changed: true,
363
427
  commit() {
364
428
  if (closed) return;
@@ -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 { digestDirectory } from "../runtime/plugin-provenance.js";
9
10
  import { validateLocalOpenCodePackageSource } from "./local-development.js";
10
11
 
11
12
  const contractModules = ["gate-transition.md", "interaction.md", "modes.md", "quality.md", "context-graph.md", "control-scaffold.md", "closeout.md"];
@@ -17,6 +18,7 @@ const globalOpenCodeRuntimeContractOwnershipMarker = "<!-- AGDF-GLOBAL-RUNTIME-C
17
18
  const globalOpenCodeValidatorOwnershipMarker = "// AGDF-GLOBAL-LOCAL-VALIDATOR";
18
19
  const globalOpenCodeAgentOwnershipMarker = `<!-- AGDF-GLOBAL-AGENT: ${pluginDefinition.opencode.evaluatorAgentName} -->`;
19
20
  const globalOpenCodeValidatorPackageOwner = "create-agdf";
21
+ export const openCodePluginEntrypoint = `./node_modules/${pluginDefinition.opencode.npmPackage}/opencode-plugin.js`;
20
22
  const testNpmCliPath = process.env.NODE_ENV === "test" ? process.env.AGDF_TEST_NPM_CLI_PATH || "" : "";
21
23
  const npmCommand = testNpmCliPath ? process.execPath : process.platform === "win32" ? process.execPath : "npm";
22
24
  const npmPrefixArgs = testNpmCliPath ? [testNpmCliPath] : process.platform === "win32" ? [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")] : [];
@@ -67,10 +69,14 @@ export function installOpenCodeGlobalPlugin(configDir, dependencies = {}) {
67
69
  throw openCodeLifecycleError("configuration", `Refusing to update OpenCode config with non-array plugin field: ${configPath}`, { configPath });
68
70
  }
69
71
 
70
- const plugins = [...(config.plugin ?? [])];
71
- const alreadyInstalled = plugins.includes(pluginDefinition.opencode.npmPackage);
72
+ const plugins = (config.plugin ?? []).map((entry) => (
73
+ entry === pluginDefinition.opencode.npmPackage || entry.startsWith(`${pluginDefinition.opencode.npmPackage}@`)
74
+ ? openCodePluginEntrypoint
75
+ : entry
76
+ ));
77
+ const alreadyInstalled = plugins.includes(openCodePluginEntrypoint);
72
78
  if (!alreadyInstalled) {
73
- plugins.push(pluginDefinition.opencode.npmPackage);
79
+ plugins.push(openCodePluginEntrypoint);
74
80
  }
75
81
 
76
82
  const nextConfig = {
@@ -128,6 +134,10 @@ export function installOpenCodeGlobalPlugin(configDir, dependencies = {}) {
128
134
  throw openCodeLifecycleError("configuration", `Failed to write OpenCode config ${configPath}: ${error.message}`, { configPath });
129
135
  }
130
136
  const installedPackage = resolveOpenCodePackage(configDir);
137
+ if (!installedPackage.loadable || !installedPackage.package_root) {
138
+ throw openCodeLifecycleError("verification", "Installed OpenCode AGDF package is not loadable after installation.", { configDir });
139
+ }
140
+ const installedPackageDigest = digestDirectory(installedPackage.package_root);
131
141
  const sdkAlignment = alignOpenCodePluginSdk(configDir);
132
142
 
133
143
  return {
@@ -135,6 +145,7 @@ export function installOpenCodeGlobalPlugin(configDir, dependencies = {}) {
135
145
  added: !alreadyInstalled,
136
146
  transition: openCodePackageTransition(previousPackage, installedPackage),
137
147
  sdk_alignment: sdkAlignment,
148
+ installed_package: { root: installedPackage.package_root, digest: installedPackageDigest },
138
149
  package_source: localPackageSource
139
150
  ? { kind: "local_checkout", path: localPackageSource.tarball, digest: localPackageSource.digest, version: localPackageSource.version }
140
151
  : { kind: "registry", path: "", digest: "", version: pluginDefinition.version },
@@ -736,7 +747,8 @@ export function evaluateOpenCodeStatus(targetDir, configDir = defaultOpenCodeCon
736
747
  const configPath = join(configDir, "opencode.json");
737
748
  const configState = readOpenCodeConfig(configPath);
738
749
  const plugins = Array.isArray(configState.config.plugin) ? configState.config.plugin : [];
739
- const globalConfigured = plugins.includes(pluginDefinition.opencode.npmPackage);
750
+ const globalConfigured = plugins.includes(openCodePluginEntrypoint);
751
+ const legacyPluginConfigured = plugins.some((entry) => entry === pluginDefinition.opencode.npmPackage || entry.startsWith(`${pluginDefinition.opencode.npmPackage}@`));
740
752
  const packageState = resolveOpenCodePackage(configDir);
741
753
  const packageVersionStatus = openCodePackageVersionStatus(packageState);
742
754
  const globalNativeSurface = evaluateGlobalOpenCodeSurface(configDir);
@@ -754,7 +766,9 @@ export function evaluateOpenCodeStatus(targetDir, configDir = defaultOpenCodeCon
754
766
  const findings = [];
755
767
  if (!configState.exists) findings.push("OpenCode global config not found.");
756
768
  if (configState.parseError) findings.push(`OpenCode global config is not valid JSON: ${configState.parseError}`);
757
- if (!globalConfigured) findings.push(`OpenCode global config does not include ${pluginDefinition.opencode.npmPackage}.`);
769
+ if (!globalConfigured) findings.push(legacyPluginConfigured
770
+ ? `OpenCode global config still uses the host-managed npm cache entry for ${pluginDefinition.opencode.npmPackage}; rerun installation to bind the verified local package entrypoint.`
771
+ : `OpenCode global config does not include ${openCodePluginEntrypoint}.`);
758
772
  if (!packageState.loadable) findings.push(`${pluginDefinition.opencode.npmPackage} is not loadable from the OpenCode config directory.`);
759
773
  if (packageVersionStatus === "outdated") findings.push(`${pluginDefinition.opencode.npmPackage} version ${packageState.installed_version} is outdated; expected ${pluginDefinition.version}.`);
760
774
  if (packageVersionStatus === "unknown" && packageState.loadable) findings.push(`${pluginDefinition.opencode.npmPackage} is loadable but its installed version is unknown.`);
@@ -783,6 +797,8 @@ export function evaluateOpenCodeStatus(targetDir, configDir = defaultOpenCodeCon
783
797
  exists: configState.exists,
784
798
  parse_error: configState.parseError,
785
799
  plugin_configured: globalConfigured,
800
+ plugin_entrypoint: openCodePluginEntrypoint,
801
+ legacy_plugin_configured: legacyPluginConfigured,
786
802
  },
787
803
  package: {
788
804
  name: pluginDefinition.opencode.npmPackage,
@@ -27,8 +27,13 @@ export function installCodexGlobalPlugin({ exec = execFileSync, prepare = prepar
27
27
  ...(transaction.pluginRoot ? [`staged_plugin_root:${transaction.pluginRoot}`, "staged_installation_provenance:matched"] : []),
28
28
  ...(transaction.sourceDigest ? [`source_digest:${transaction.sourceDigest}`] : []),
29
29
  ...(transaction.digest ? [`plugin_digest:${transaction.digest}`] : []),
30
+ ...(transaction.existingClassification === "owned_pre_provenance_rebuild" ? ["marketplace_recovery:owned_pre_provenance_rebuild", "loaded_session:restart_required"] : []),
30
31
  ...(expectedInstallVersion === expectedVersion ? [] : [`canonical_version:${expectedVersion}`, `local_install_version:${expectedInstallVersion}`]),
31
32
  ],
33
+ pluginRoot: transaction.pluginRoot ?? null,
34
+ digest: transaction.digest ?? null,
35
+ runtimeDigest: transaction.runtimeDigest ?? null,
36
+ sourceDigest: transaction.sourceDigest ?? null,
32
37
  nativeOutput: nativeOutput.filter(Boolean).map(String),
33
38
  };
34
39
  } catch (error) {
@@ -73,8 +78,13 @@ export function installClaudeGlobalPlugin({ exec = execFileSync, prepare = prepa
73
78
  ...(transaction.pluginRoot ? [`staged_plugin_root:${transaction.pluginRoot}`, "staged_installation_provenance:matched"] : []),
74
79
  ...(transaction.sourceDigest ? [`source_digest:${transaction.sourceDigest}`] : []),
75
80
  ...(transaction.digest ? [`plugin_digest:${transaction.digest}`] : []),
81
+ ...(transaction.existingClassification === "owned_pre_provenance_rebuild" ? ["marketplace_recovery:owned_pre_provenance_rebuild", "loaded_session:restart_required"] : []),
76
82
  ...(installedVersion ? [] : ["host_did_not_expose_version"]),
77
83
  ],
84
+ pluginRoot: transaction.pluginRoot ?? null,
85
+ digest: transaction.digest ?? null,
86
+ runtimeDigest: transaction.runtimeDigest ?? null,
87
+ sourceDigest: transaction.sourceDigest ?? null,
78
88
  nativeOutput: nativeOutput.filter(Boolean).map(String),
79
89
  };
80
90
  } catch (error) {
@@ -9,6 +9,9 @@ const surfaceLabels = Object.freeze({
9
9
  function titleFor(report) {
10
10
  if (report.result === "failed") return "AGDF installation failed";
11
11
  if (report.result === "partial") return "AGDF installation partially completed";
12
+ if (report.result === "preview") return report.operation === "uninstall"
13
+ ? "AGDF uninstall preview"
14
+ : "AGDF installation cancelled";
12
15
  if (report.operation === "repository_setup") return "AGDF repository setup complete";
13
16
  return "AGDF installation complete";
14
17
  }
@@ -28,18 +31,55 @@ export function lifecycleCardLines(report) {
28
31
  `Installation: ${report.installation.status}`,
29
32
  `Activation: ${report.activation.status}`,
30
33
  `Repository delivery: ${report.delivery.status}`,
34
+ `Automatic runtime checks: requested=${report.runtime_checks.requested}; effective=${report.runtime_checks.effective}; reason=${report.runtime_checks.reason}`,
31
35
  `Verification: ${report.verification.status}`,
32
36
  `Restart required: ${report.restart.required ? "yes" : "no"}${report.restart.reason !== "none" ? ` (${report.restart.reason})` : ""}`,
33
37
  `Next action: ${report.next_action.text}`,
34
38
  ];
35
39
  }
36
40
 
37
- export function printLifecycleResult(report, { json = false, io = console } = {}) {
41
+ function friendlyRuntimeCheckState(report) {
42
+ if (report.runtime_checks.requested === "cancelled") return "Cancelled";
43
+ if (report.runtime_checks.effective === "enabled") return "On";
44
+ if (report.runtime_checks.effective === "manual") return "Manual";
45
+ if (report.runtime_checks.effective === "decision_required") return `Waiting for ${surfaceLabels[report.surface]} permission`;
46
+ if (report.runtime_checks.effective === "renewal_required") return "Needs your review";
47
+ if (["degraded", "failed", "unavailable"].includes(report.runtime_checks.effective)) return "Not available; use manual checks";
48
+ return report.runtime_checks.requested === "enabled" ? "Requested; verification pending" : "Not configured";
49
+ }
50
+
51
+ function compactVersionLine(report) {
52
+ if (report.version.transition === "updated" && report.version.previous && report.version.installed) {
53
+ return `Updated: ${report.version.previous} -> ${report.version.installed} (${report.version.status})`;
54
+ }
55
+ if (report.version.installed) return `Version: ${report.version.installed} (${report.version.status})`;
56
+ if (report.version.expected) return `Version: not verified (expected ${report.version.expected})`;
57
+ return "Version: not verified";
58
+ }
59
+
60
+ export function compactLifecycleCardLines(report) {
61
+ if (report.result === "preview") return [
62
+ titleFor(report),
63
+ "No changes were made.",
64
+ `Next: ${report.next_action.text}`,
65
+ ];
66
+ return [
67
+ report.result === "success"
68
+ ? `AGDF ${report.operation === "update" ? "updated" : "installed"} for ${surfaceLabels[report.surface]}`
69
+ : titleFor(report),
70
+ compactVersionLine(report),
71
+ `Installation: ${report.installation.status === "healthy" ? "Ready" : report.installation.status}`,
72
+ `Automatic checks: ${friendlyRuntimeCheckState(report)}`,
73
+ `Next: ${report.next_action.text}`,
74
+ ];
75
+ }
76
+
77
+ export function printLifecycleResult(report, { json = false, compact = false, io = console } = {}) {
38
78
  if (json) {
39
79
  io.log(JSON.stringify(report, null, 2));
40
80
  return;
41
81
  }
42
- for (const line of lifecycleCardLines(report)) io.log(line);
82
+ for (const line of compact ? compactLifecycleCardLines(report) : lifecycleCardLines(report)) io.log(line);
43
83
  if (report.failure) io.log(`Failure phase: ${report.failure.phase}: ${report.failure.message}`);
44
84
  if (report.retained.length) {
45
85
  io.log("Retained:");
@@ -56,5 +96,6 @@ export function printGeneralStatus(report, { json = false, io = console } = {})
56
96
  io.log(`Installation: ${report.installation.status}${report.installation.version ? ` (${report.installation.version})` : ""}`);
57
97
  io.log(`Repository: ${report.repository.status}`);
58
98
  io.log(`Delivery: ${report.delivery.status}${report.delivery.current_gate ? ` (${report.delivery.current_gate})` : ""}`);
99
+ io.log(`Automatic runtime checks: requested=${report.runtime_checks.requested}; effective=${report.runtime_checks.effective}; reason=${report.runtime_checks.reason}`);
59
100
  io.log(`Next action: ${report.next_action.text}`);
60
101
  }
@@ -39,6 +39,15 @@ export function createLifecycleResult(input = {}) {
39
39
  delivery: {
40
40
  status: text(input.delivery?.status) || "not_evaluated",
41
41
  },
42
+ runtime_checks: {
43
+ requested: text(input.runtime_checks?.requested) || "unknown",
44
+ effective: text(input.runtime_checks?.effective) || "unknown",
45
+ reason: text(input.runtime_checks?.reason) || "not_evaluated",
46
+ capability_identity: text(input.runtime_checks?.capability_identity) || null,
47
+ verification: text(input.runtime_checks?.verification) || "not_evaluated",
48
+ mutation: text(input.runtime_checks?.mutation) || "none",
49
+ rollback: text(input.runtime_checks?.rollback) || "none",
50
+ },
42
51
  restart: {
43
52
  required: Boolean(input.restart?.required),
44
53
  reason: text(input.restart?.reason) || "none",
@@ -6,6 +6,7 @@ import { evaluateGateCheck } from "../control-evaluation/gate-check.js";
6
6
  import { evaluateOpenCodeStatus } from "../installers/opencode.js";
7
7
  import { inspectPluginSurface } from "../installers/plugin-installers.js";
8
8
  import { inspectGeneratedRepositoryMarketplace } from "../runtime/plugin-provenance.js";
9
+ import { runtimeCheckStatus } from "../runtime-check-consent/service.js";
9
10
 
10
11
  function deliveryStatus(targetDir, selection, dependencies) {
11
12
  const controlDir = join(targetDir, ".agdf", "control");
@@ -95,6 +96,9 @@ export function evaluateGeneralStatus(targetDir, options = {}, dependencies = {}
95
96
  }
96
97
  const repository = repositoryStatus(targetDir, surface);
97
98
  const delivery = deliveryStatus(targetDir, { runId: options.runId }, deps);
99
+ const runtimeChecks = ["codex", "claude", "opencode"].includes(surface)
100
+ ? runtimeCheckStatus(options.dataRoot, surface)
101
+ : { requested: "unknown", effective: "unavailable", reason: "unsupported_host_capability" };
98
102
  const nextText = surface === "multiple"
99
103
  ? "Multiple AGDF host surfaces were detected; rerun status with an explicit --surface."
100
104
  : surface === "generic"
@@ -112,5 +116,5 @@ export function evaluateGeneralStatus(targetDir, options = {}, dependencies = {}
112
116
  : delivery.status === "not_configured"
113
117
  ? "Start a new task; status created no run and requires no approval. Initialize durable control only when the repository needs it."
114
118
  : "Continue with the reported delivery next step.";
115
- return { schema_version: 1, installation, repository, delivery, next_action: { kind: "action", text: nextText } };
119
+ return { schema_version: 1, installation, repository, delivery, runtime_checks: runtimeChecks, next_action: { kind: "action", text: nextText } };
116
120
  }
@@ -0,0 +1,42 @@
1
+ function fixedCommand(command) {
2
+ if (typeof command !== "string" || !command.trim() || /[\n\r;&|*?]/.test(command)) {
3
+ throw new Error("AGDF_RUNTIME_CHECK_COMMAND_NOT_EXACT");
4
+ }
5
+ return command.trim();
6
+ }
7
+
8
+ export function codexRuntimeCheckEvidence({ capabilityIdentity, observedIdentity, hookEnabled, reviewRequired = false }) {
9
+ if (hookEnabled && observedIdentity === capabilityIdentity) return { status: "enabled", capability_identity: capabilityIdentity, reason: "none" };
10
+ if (observedIdentity && observedIdentity !== capabilityIdentity) return { status: "renewal_required", reason: "capability_identity_changed" };
11
+ return { status: "decision_required", reason: reviewRequired ? "host_review_required" : "host_permission_unverified" };
12
+ }
13
+
14
+ export function claudePermissionRule({ platform, command }) {
15
+ const exact = fixedCommand(command);
16
+ return platform === "win32" ? `PowerShell(${exact})` : `Bash(${exact})`;
17
+ }
18
+
19
+ export function applyClaudeExactRule(settings, { rule, deny = [], ask = [] }) {
20
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) throw new Error("AGDF_CLAUDE_SETTINGS_INVALID");
21
+ if ([...deny, ...ask].includes(rule)) return { status: "degraded", reason: "host_permission_conflict", settings };
22
+ const next = structuredClone(settings);
23
+ next.permissions ??= {};
24
+ if (Array.isArray(next.permissions.allow) && next.permissions.allow.some((entry) => entry.includes("*") || entry !== entry.trim())) {
25
+ throw new Error("AGDF_CLAUDE_BROAD_OR_MALFORMED_RULE");
26
+ }
27
+ next.permissions.allow = [...new Set([...(next.permissions.allow ?? []), rule])];
28
+ return { status: "configured", reason: "none", settings: next };
29
+ }
30
+
31
+ export function revokeClaudeExactRule(settings, rule) {
32
+ const next = structuredClone(settings);
33
+ if (Array.isArray(next.permissions?.allow)) next.permissions.allow = next.permissions.allow.filter((entry) => entry !== rule);
34
+ return next;
35
+ }
36
+
37
+ export function openCodeRuntimeCheckEvidence({ capabilityIdentity, observedIdentity, packageLoadable, hookObserved }) {
38
+ if (!packageLoadable) return { status: "unavailable", reason: "unsupported_host_capability" };
39
+ if (observedIdentity !== capabilityIdentity) return { status: "renewal_required", reason: "installed_identity_mismatch" };
40
+ if (!hookObserved) return { status: "degraded", reason: "host_permission_unverified" };
41
+ return { status: "enabled", capability_identity: capabilityIdentity, reason: "none" };
42
+ }
@@ -0,0 +1,71 @@
1
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import process from "node:process";
5
+ import { renameSyncWithRetry } from "../fs-swap.js";
6
+ import { applyClaudeExactRule, revokeClaudeExactRule } from "./adapters.js";
7
+
8
+ export function defaultClaudeSettingsPath({ env = process.env, home = homedir() } = {}) {
9
+ return join(resolve(env.CLAUDE_CONFIG_DIR || join(home, ".claude")), "settings.json");
10
+ }
11
+
12
+ export function readClaudeSettings(path = defaultClaudeSettingsPath()) {
13
+ if (!existsSync(path)) return { exists: false, settings: {} };
14
+ const stats = lstatSync(path);
15
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("AGDF_CLAUDE_SETTINGS_UNOWNED_PATH");
16
+ try {
17
+ const settings = JSON.parse(readFileSync(path, "utf8"));
18
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) throw new Error("not an object");
19
+ return { exists: true, settings };
20
+ } catch (error) {
21
+ throw new Error(`AGDF_CLAUDE_SETTINGS_INVALID: ${error.message}`);
22
+ }
23
+ }
24
+
25
+ function atomicSettingsWrite(path, settings) {
26
+ mkdirSync(dirname(path), { recursive: true });
27
+ const temp = `${path}.agdf-tmp-${process.pid}`;
28
+ const backup = `${path}.agdf-backup-${process.pid}`;
29
+ const hadPrevious = existsSync(path);
30
+ const mode = hadPrevious ? lstatSync(path).mode & 0o777 : 0o600;
31
+ writeFileSync(temp, `${JSON.stringify(settings, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode });
32
+ if (process.platform !== "win32") chmodSync(temp, mode);
33
+ try {
34
+ if (hadPrevious && process.platform === "win32") renameSyncWithRetry(path, backup);
35
+ renameSyncWithRetry(temp, path);
36
+ if (hadPrevious && process.platform === "win32") rmSync(backup, { force: true });
37
+ } catch (error) {
38
+ try { rmSync(temp, { force: true }); } catch {}
39
+ if (hadPrevious && process.platform === "win32" && existsSync(backup) && !existsSync(path)) renameSyncWithRetry(backup, path);
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ export function configureClaudeExactRuntimeRule({ path = defaultClaudeSettingsPath(), rule }) {
45
+ const before = readClaudeSettings(path);
46
+ const permissions = before.settings.permissions ?? {};
47
+ const applied = applyClaudeExactRule(before.settings, {
48
+ rule,
49
+ deny: Array.isArray(permissions.deny) ? permissions.deny : [],
50
+ ask: Array.isArray(permissions.ask) ? permissions.ask : [],
51
+ });
52
+ if (applied.status !== "configured") return { ...applied, path, rollback() {} };
53
+ atomicSettingsWrite(path, applied.settings);
54
+ return {
55
+ status: "configured",
56
+ reason: "host_permission_unverified",
57
+ path,
58
+ rollback() {
59
+ if (before.exists) atomicSettingsWrite(path, before.settings);
60
+ else rmSync(path, { force: true });
61
+ },
62
+ };
63
+ }
64
+
65
+ export function revokeClaudeRuntimeRule({ path = defaultClaudeSettingsPath(), rule }) {
66
+ const before = readClaudeSettings(path);
67
+ if (!before.exists) return { status: "manual", reason: "configuration_missing", path };
68
+ const next = revokeClaudeExactRule(before.settings, rule);
69
+ atomicSettingsWrite(path, next);
70
+ return { status: "manual", reason: "consent_not_provided", path };
71
+ }
@@ -0,0 +1,71 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ const OPERATIONS = Object.freeze([
4
+ "resolve-local-runtime",
5
+ "inspect-control-state",
6
+ "render-bounded-orientation",
7
+ ]);
8
+
9
+ function stable(value) {
10
+ if (Array.isArray(value)) return value.map(stable);
11
+ if (!value || typeof value !== "object") return value;
12
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
13
+ }
14
+
15
+ export function validateRuntimeCheckCapability(capability) {
16
+ if (capability?.schemaVersion !== 1 || capability.capabilityId !== "automatic-runtime-checks") {
17
+ throw new Error("AGDF_RUNTIME_CHECK_CAPABILITY_INVALID: schema or capability id");
18
+ }
19
+ if (!Number.isInteger(capability.adapterContractVersion) || capability.adapterContractVersion < 1) {
20
+ throw new Error("AGDF_RUNTIME_CHECK_CAPABILITY_INVALID: adapter contract version");
21
+ }
22
+ if (capability.entrypoint !== "runtime/agdf-session-check.js") {
23
+ throw new Error("AGDF_RUNTIME_CHECK_CAPABILITY_INVALID: fixed entrypoint required");
24
+ }
25
+ if (JSON.stringify(capability.operations) !== JSON.stringify(OPERATIONS)) {
26
+ throw new Error("AGDF_RUNTIME_CHECK_CAPABILITY_INVALID: closed operation vocabulary required");
27
+ }
28
+ const constraints = capability.constraints ?? {};
29
+ if (constraints.arguments !== "forbidden" || constraints.filesystemWrites !== "forbidden"
30
+ || constraints.network !== "forbidden" || constraints.gateAuthority !== "none") {
31
+ throw new Error("AGDF_RUNTIME_CHECK_CAPABILITY_INVALID: read-only constraints required");
32
+ }
33
+ const surfaces = capability.surfaces ?? {};
34
+ if (surfaces.codex !== "native-hook-review" || surfaces.claude !== "exact-command-rule"
35
+ || surfaces.opencode !== "plugin-internal"
36
+ || surfaces["portable-skills"] !== "manual-external-required") {
37
+ throw new Error("AGDF_RUNTIME_CHECK_CAPABILITY_INVALID: surface contract");
38
+ }
39
+ return capability;
40
+ }
41
+
42
+ export function runtimeCheckCapabilityIdentity({ capability, surface, runtimeDigest, sourceDigest, command }) {
43
+ validateRuntimeCheckCapability(capability);
44
+ if (!["codex", "claude", "opencode"].includes(surface)) throw new Error("AGDF_RUNTIME_CHECK_SURFACE_INVALID");
45
+ for (const [name, value] of Object.entries({ runtimeDigest, sourceDigest })) {
46
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) throw new Error(`AGDF_RUNTIME_CHECK_IDENTITY_INVALID: ${name}`);
47
+ }
48
+ if (typeof command !== "string" || !command.trim()) throw new Error("AGDF_RUNTIME_CHECK_IDENTITY_INVALID: command");
49
+ const payload = stable({
50
+ schema_version: 1,
51
+ surface,
52
+ capability,
53
+ runtime_digest: runtimeDigest,
54
+ source_digest: sourceDigest,
55
+ command: command.trim(),
56
+ });
57
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
58
+ }
59
+
60
+ export function fixedRuntimeCheckCommand(surface, pluginRoot, platform = process.platform) {
61
+ if (surface === "opencode") return "plugin:agdf:automatic-runtime-checks";
62
+ if (surface === "codex" || surface === "claude") {
63
+ return platform === "win32"
64
+ ? "node \"$([Environment]::GetEnvironmentVariable('PLUGIN_ROOT') + [Environment]::GetEnvironmentVariable('CLAUDE_PLUGIN_ROOT'))\\runtime\\agdf-session-check.js\""
65
+ : "node \"${PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/runtime/agdf-session-check.js\"";
66
+ }
67
+ const normalizedRoot = platform === "win32" ? pluginRoot.replaceAll("/", "\\") : pluginRoot;
68
+ return `node \"${normalizedRoot}${platform === "win32" ? "\\" : "/"}runtime${platform === "win32" ? "\\" : "/"}agdf-session-check.js\"`;
69
+ }
70
+
71
+ export const runtimeCheckOperations = OPERATIONS;
@@ -0,0 +1,37 @@
1
+ export const RUNTIME_CHECK_DECISIONS = Object.freeze(["enable", "manual", "cancel"]);
2
+
3
+ export function resolveRuntimeCheckDecision({ explicitValue, interactive = false, ask }) {
4
+ if (explicitValue !== undefined) {
5
+ if (!RUNTIME_CHECK_DECISIONS.includes(explicitValue)) throw new Error("AGDF_RUNTIME_CHECK_DECISION_INVALID");
6
+ return explicitValue;
7
+ }
8
+ if (!interactive || typeof ask !== "function") return "manual";
9
+ const answer = ask({
10
+ question: "Allow this AGDF installation to run narrow local read-only checks automatically?",
11
+ options: [...RUNTIME_CHECK_DECISIONS],
12
+ default: null,
13
+ });
14
+ if (!RUNTIME_CHECK_DECISIONS.includes(answer)) return "cancel";
15
+ return answer;
16
+ }
17
+
18
+ export function consentDisclosure(surface) {
19
+ const permissionOwner = surface === "codex" ? "Codex native hook trust"
20
+ : surface === "claude" ? "Claude Code user permission settings"
21
+ : "OpenCode plugin and explicit permission configuration";
22
+ return Object.freeze({
23
+ surface,
24
+ installation_scope: "global user installation",
25
+ runs: "one argument-free local session check",
26
+ when: "at session start",
27
+ executable: "runtime/agdf-session-check.js inside the installed AGDF runtime",
28
+ reads: "AGDF runtime identity and .agdf/control state in the current repository",
29
+ writes: "one AGDF-owned intent receipt and, only where supported, one exact host permission rule",
30
+ network: "none",
31
+ permission_owner: permissionOwner,
32
+ renewal: "material command, runtime, source, scope or adapter identity change requires renewed consent",
33
+ revocation: `npx --yes @agdf/cli@latest runtime-checks manual --surface ${surface}`,
34
+ gate_authority: "none",
35
+ choices: [...RUNTIME_CHECK_DECISIONS],
36
+ });
37
+ }