create-agdf 0.13.7 → 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 +2 -0
  32. package/lib/installers/opencode.js +21 -5
  33. package/lib/installers/plugin-installers.js +8 -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
@@ -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,
@@ -30,6 +30,10 @@ export function installCodexGlobalPlugin({ exec = execFileSync, prepare = prepar
30
30
  ...(transaction.existingClassification === "owned_pre_provenance_rebuild" ? ["marketplace_recovery:owned_pre_provenance_rebuild", "loaded_session:restart_required"] : []),
31
31
  ...(expectedInstallVersion === expectedVersion ? [] : [`canonical_version:${expectedVersion}`, `local_install_version:${expectedInstallVersion}`]),
32
32
  ],
33
+ pluginRoot: transaction.pluginRoot ?? null,
34
+ digest: transaction.digest ?? null,
35
+ runtimeDigest: transaction.runtimeDigest ?? null,
36
+ sourceDigest: transaction.sourceDigest ?? null,
33
37
  nativeOutput: nativeOutput.filter(Boolean).map(String),
34
38
  };
35
39
  } catch (error) {
@@ -77,6 +81,10 @@ export function installClaudeGlobalPlugin({ exec = execFileSync, prepare = prepa
77
81
  ...(transaction.existingClassification === "owned_pre_provenance_rebuild" ? ["marketplace_recovery:owned_pre_provenance_rebuild", "loaded_session:restart_required"] : []),
78
82
  ...(installedVersion ? [] : ["host_did_not_expose_version"]),
79
83
  ],
84
+ pluginRoot: transaction.pluginRoot ?? null,
85
+ digest: transaction.digest ?? null,
86
+ runtimeDigest: transaction.runtimeDigest ?? null,
87
+ sourceDigest: transaction.sourceDigest ?? null,
80
88
  nativeOutput: nativeOutput.filter(Boolean).map(String),
81
89
  };
82
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
+ }
@@ -0,0 +1,181 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { join } from "node:path";
5
+ import process from "node:process";
6
+ import { generatedRoot, pluginDefinition } from "../cli/runtime-context.js";
7
+ import { defaultOpenCodeConfigDir, resolveOpenCodeInstalledPackage } from "../installers/opencode.js";
8
+ import { defaultAgdfDataRoot } from "../installers/local-marketplace.js";
9
+ import { digestNormalizedPluginSource } from "../runtime/plugin-provenance.js";
10
+ import { fixedRuntimeCheckCommand, runtimeCheckCapabilityIdentity } from "./contract.js";
11
+ import { consentDisclosure, resolveRuntimeCheckDecision } from "./coordinator.js";
12
+ import { claudePermissionRule } from "./adapters.js";
13
+ import { configureClaudeExactRuntimeRule, defaultClaudeSettingsPath, revokeClaudeRuntimeRule } from "./claude-settings.js";
14
+ import { createRuntimeCheckReceipt, readRuntimeCheckReceipt, writeRuntimeCheckReceipt } from "./state.js";
15
+
16
+ export { fixedRuntimeCheckCommand } from "./contract.js";
17
+
18
+ export function prepareInstallConsent(surface, options = {}, adapters = {}) {
19
+ const explicit = options.runtimeChecksDecision;
20
+ const interactive = explicit === undefined && Boolean(adapters.interactive);
21
+ const answer = interactive ? adapters.ask?.(consentDisclosure(surface)) : undefined;
22
+ const decision = resolveRuntimeCheckDecision({ explicitValue: explicit ?? answer, interactive: false });
23
+ return { decision, persist: explicit !== undefined || interactive, disclosure: consentDisclosure(surface) };
24
+ }
25
+
26
+ function prospectiveRuntimeCheckIdentity(surface, platform = process.platform) {
27
+ if (surface === "opencode") {
28
+ const installed = resolveOpenCodeInstalledPackage(defaultOpenCodeConfigDir(), pluginDefinition.opencode.npmPackage);
29
+ if (!installed.loadable || !installed.digest) return null;
30
+ return runtimeCheckCapabilityIdentity({
31
+ capability: pluginDefinition.automaticRuntimeChecks,
32
+ surface,
33
+ runtimeDigest: installed.digest,
34
+ sourceDigest: installed.digest,
35
+ command: fixedRuntimeCheckCommand(surface, installed.root, platform),
36
+ });
37
+ }
38
+ if (!["codex", "claude"].includes(surface)) return null;
39
+ const pluginRoot = join(generatedRoot, "plugins", "agdf");
40
+ let runtimeManifest;
41
+ try { runtimeManifest = JSON.parse(readFileSync(join(pluginRoot, "runtime", "runtime-manifest.json"), "utf8")); } catch { return null; }
42
+ if (!/^[a-f0-9]{64}$/.test(runtimeManifest?.digest ?? "")) return null;
43
+ const sourceDigest = digestNormalizedPluginSource(pluginRoot, pluginDefinition.version);
44
+ const command = fixedRuntimeCheckCommand(surface, pluginRoot, platform);
45
+ return runtimeCheckCapabilityIdentity({
46
+ capability: pluginDefinition.automaticRuntimeChecks,
47
+ surface,
48
+ runtimeDigest: runtimeManifest.digest,
49
+ sourceDigest,
50
+ command,
51
+ });
52
+ }
53
+
54
+ export function retainCurrentInstallConsent(surface, dataRoot = defaultAgdfDataRoot(), platform = process.platform) {
55
+ const capabilityIdentity = prospectiveRuntimeCheckIdentity(surface, platform);
56
+ if (!capabilityIdentity) return null;
57
+ const current = readRuntimeCheckReceipt(dataRoot, surface);
58
+ if (current.status !== "valid" || current.receipt.capability_identity !== capabilityIdentity) return null;
59
+ return {
60
+ decision: current.receipt.requested_state === "enabled" ? "enable" : "manual",
61
+ persist: false,
62
+ retained: true,
63
+ disclosure: consentDisclosure(surface),
64
+ };
65
+ }
66
+
67
+ export function persistInstallConsent({ surface, decision, installed, dataRoot, platform, claudeSettingsPath }) {
68
+ if (decision === "cancel") return { requested: "cancelled", effective: "cancelled", reason: "consent_not_provided", capability_identity: null, verification: "not_attempted", mutation: "none", rollback: "none" };
69
+ const runtimeDigest = installed?.runtimeDigest ?? installed?.digest;
70
+ if (!installed?.pluginRoot || !runtimeDigest || !installed?.sourceDigest) {
71
+ return { requested: decision === "enable" ? "enabled" : "manual", effective: "manual", reason: "host_permission_unverified", capability_identity: null, verification: "unavailable", mutation: "none", rollback: "none" };
72
+ }
73
+ const command = fixedRuntimeCheckCommand(surface, installed.pluginRoot, platform);
74
+ const capabilityIdentity = runtimeCheckCapabilityIdentity({
75
+ capability: pluginDefinition.automaticRuntimeChecks,
76
+ surface,
77
+ runtimeDigest,
78
+ sourceDigest: installed.sourceDigest,
79
+ command,
80
+ });
81
+ const receipt = createRuntimeCheckReceipt({ surface, decision, capabilityIdentity, command });
82
+ let configured = null;
83
+ let configurationReason = null;
84
+ if (surface === "claude" && decision === "enable") {
85
+ configured = configureClaudeExactRuntimeRule({
86
+ path: claudeSettingsPath ?? defaultClaudeSettingsPath(),
87
+ rule: claudePermissionRule({ platform: platform ?? process.platform, command }),
88
+ });
89
+ if (configured.status !== "configured") {
90
+ configurationReason = configured.reason;
91
+ configured = null;
92
+ }
93
+ }
94
+ try {
95
+ writeRuntimeCheckReceipt(dataRoot ?? defaultAgdfDataRoot(), receipt);
96
+ } catch (error) {
97
+ configured?.rollback();
98
+ throw error;
99
+ }
100
+ if (configurationReason) return { requested: "enabled", effective: "degraded", reason: configurationReason, capability_identity: capabilityIdentity, verification: "conflict", mutation: "receipt_written", rollback: "none" };
101
+ return {
102
+ requested: receipt.requested_state,
103
+ effective: receipt.requested_state === "manual" ? "manual" : "decision_required",
104
+ reason: receipt.requested_state === "manual" ? "consent_not_provided" : "host_permission_unverified",
105
+ capability_identity: capabilityIdentity,
106
+ verification: receipt.requested_state === "manual" ? "not_required" : "host_unverified",
107
+ mutation: surface === "claude" && decision === "enable" ? "exact_rule_and_receipt_written" : "receipt_written",
108
+ rollback: "none",
109
+ };
110
+ }
111
+
112
+ export function setRuntimeChecksManual({ dataRoot = defaultAgdfDataRoot(), surface, platform = process.platform, claudeSettingsPath }) {
113
+ const result = readRuntimeCheckReceipt(dataRoot, surface);
114
+ if (result.status === "receipt_missing") return { requested: "manual", effective: "manual", reason: "consent_not_provided", capability_identity: null, verification: "not_required", mutation: "none", rollback: "none", path: result.path };
115
+ if (result.status !== "valid") return { requested: "manual", effective: result.status, reason: result.status, capability_identity: null, verification: "unavailable", mutation: "none", rollback: "none", path: result.path };
116
+ if (surface === "claude" && result.receipt.requested_state === "enabled") {
117
+ revokeClaudeRuntimeRule({
118
+ path: claudeSettingsPath ?? defaultClaudeSettingsPath(),
119
+ rule: claudePermissionRule({ platform, command: result.receipt.command }),
120
+ });
121
+ }
122
+ const receipt = createRuntimeCheckReceipt({
123
+ surface,
124
+ decision: "manual",
125
+ capabilityIdentity: result.receipt.capability_identity,
126
+ command: result.receipt.command,
127
+ });
128
+ writeRuntimeCheckReceipt(dataRoot, receipt);
129
+ return {
130
+ requested: "manual",
131
+ effective: "manual",
132
+ reason: "consent_not_provided",
133
+ capability_identity: receipt.capability_identity,
134
+ verification: "not_required",
135
+ mutation: surface === "claude" ? "exact_owned_rule_removed" : "receipt_updated",
136
+ rollback: "none",
137
+ path: result.path,
138
+ };
139
+ }
140
+
141
+ export function runtimeCheckStatus(dataRoot = defaultAgdfDataRoot(), surface, platform = process.platform) {
142
+ const result = readRuntimeCheckReceipt(dataRoot, surface);
143
+ if (result.status !== "valid") return { requested: "unknown", effective: result.status, reason: result.status, capability_identity: null, verification: "unavailable", mutation: "none", rollback: "none", path: result.path };
144
+ const currentIdentity = prospectiveRuntimeCheckIdentity(surface, platform);
145
+ if (currentIdentity && currentIdentity !== result.receipt.capability_identity) {
146
+ return { requested: result.receipt.requested_state, effective: "renewal_required", reason: "capability_identity_changed", capability_identity: result.receipt.capability_identity, verification: "identity_mismatch", mutation: "none", rollback: "none", path: result.path };
147
+ }
148
+ return { requested: result.receipt.requested_state, effective: result.receipt.requested_state === "manual" ? "manual" : "decision_required", reason: result.receipt.requested_state === "manual" ? "consent_not_provided" : "host_permission_unverified", capability_identity: result.receipt.capability_identity, verification: result.receipt.requested_state === "manual" ? "not_required" : "host_unverified", mutation: "none", rollback: "none", path: result.path };
149
+ }
150
+
151
+ export function executeOpenCodeAutomaticRuntimeCheck({
152
+ directory = process.cwd(),
153
+ dataRoot = defaultAgdfDataRoot(),
154
+ packageRoot = fileURLToPath(new URL("../../", import.meta.url)),
155
+ run = spawnSync,
156
+ executable = process.platform === "win32" ? "node.exe" : "node",
157
+ statusResolver = runtimeCheckStatus,
158
+ entrypointExists = existsSync,
159
+ } = {}) {
160
+ const status = statusResolver(dataRoot, "opencode");
161
+ if (status.requested !== "enabled" || status.effective !== "decision_required") {
162
+ return { ...status, ran: false, output: "" };
163
+ }
164
+ const entrypoint = join(packageRoot, "generated", "plugins", "agdf", "runtime", "agdf-session-check.js");
165
+ if (!entrypointExists(entrypoint)) {
166
+ return { ...status, effective: "unavailable", reason: "unsupported_host_capability", verification: "entrypoint_missing", ran: false, output: "" };
167
+ }
168
+ const child = run(executable, [entrypoint], {
169
+ cwd: directory,
170
+ env: { ...process.env, AGDF_SURFACE: "opencode" },
171
+ encoding: "utf8",
172
+ stdio: ["ignore", "pipe", "pipe"],
173
+ timeout: 10000,
174
+ maxBuffer: 1024 * 1024,
175
+ });
176
+ const output = String(child.stdout || "").trim().slice(0, 20000);
177
+ if (child.status !== 0 || !output.includes("AGDF automatic runtime check:")) {
178
+ return { ...status, effective: "degraded", reason: "host_permission_unverified", verification: "execution_failed", ran: true, output: "" };
179
+ }
180
+ return { ...status, effective: "enabled", reason: "none", verification: "host_observed", ran: true, output };
181
+ }