avorelo 0.1.0

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 (258) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/bin/avorelo +9 -0
  4. package/package.json +135 -0
  5. package/scripts/README.md +40 -0
  6. package/scripts/cco-dashboard.js +252 -0
  7. package/scripts/cco-status.js +430 -0
  8. package/scripts/lib/activation/account-state.js +37 -0
  9. package/scripts/lib/activation/activation-runner.js +546 -0
  10. package/scripts/lib/activation/activation-self-healing.js +480 -0
  11. package/scripts/lib/activation/activation-state.js +83 -0
  12. package/scripts/lib/activation/activation-summary.js +191 -0
  13. package/scripts/lib/activation/adapters/claude-code.js +77 -0
  14. package/scripts/lib/activation/adapters/codex-cli.js +52 -0
  15. package/scripts/lib/activation/adapters/cursor.js +37 -0
  16. package/scripts/lib/activation/adapters/github-agent.js +39 -0
  17. package/scripts/lib/activation/adapters/terminal.js +42 -0
  18. package/scripts/lib/activation/adapters/vscode.js +39 -0
  19. package/scripts/lib/activation/adapters/windsurf.js +37 -0
  20. package/scripts/lib/activation/ai-surface-detector.js +151 -0
  21. package/scripts/lib/activation/connect-account.js +145 -0
  22. package/scripts/lib/activation/detect-environment.js +75 -0
  23. package/scripts/lib/activation/detect-hosts.js +62 -0
  24. package/scripts/lib/activation/format-activation-output.js +109 -0
  25. package/scripts/lib/activation/next-action.js +43 -0
  26. package/scripts/lib/activation/repair-engine.js +219 -0
  27. package/scripts/lib/activation-distribution-readiness.js +507 -0
  28. package/scripts/lib/adapter-conformance.js +176 -0
  29. package/scripts/lib/adapter-readiness.js +417 -0
  30. package/scripts/lib/adapter-safety-boundaries.js +335 -0
  31. package/scripts/lib/adapter-technical-readiness-gate.js +205 -0
  32. package/scripts/lib/agent-access-governance.js +455 -0
  33. package/scripts/lib/agent-enforcement.js +765 -0
  34. package/scripts/lib/agent-policy-profile.js +210 -0
  35. package/scripts/lib/agent-security/action-evaluator.js +507 -0
  36. package/scripts/lib/agent-security/adapter-registry.js +98 -0
  37. package/scripts/lib/agent-security/auto-policy.js +139 -0
  38. package/scripts/lib/agent-security/bounded-scan.js +93 -0
  39. package/scripts/lib/agent-security/enforcement-adapter.js +174 -0
  40. package/scripts/lib/agent-security/enforcement-engine.js +1129 -0
  41. package/scripts/lib/agent-security/file-write-adapter.js +183 -0
  42. package/scripts/lib/agent-security/file-write-rules.js +178 -0
  43. package/scripts/lib/agent-security/index.js +3342 -0
  44. package/scripts/lib/agent-security/instruction-risk.js +181 -0
  45. package/scripts/lib/agent-security/mcp-action-adapter.js +185 -0
  46. package/scripts/lib/agent-security/mcp-action-rules.js +184 -0
  47. package/scripts/lib/agent-security/package-action-adapter.js +175 -0
  48. package/scripts/lib/agent-security/package-action-rules.js +233 -0
  49. package/scripts/lib/agent-security/performance.js +148 -0
  50. package/scripts/lib/agent-security/permission-minimizer.js +403 -0
  51. package/scripts/lib/agent-security/scan-cache.js +74 -0
  52. package/scripts/lib/agent-security/source-trust.js +146 -0
  53. package/scripts/lib/ai-install-prompt.js +288 -0
  54. package/scripts/lib/ai-workspace-hygiene.js +1499 -0
  55. package/scripts/lib/alpha-activation.js +520 -0
  56. package/scripts/lib/alpha-feedback.js +263 -0
  57. package/scripts/lib/alpha-readiness-gate.js +332 -0
  58. package/scripts/lib/anti-gaming.js +169 -0
  59. package/scripts/lib/artifact-health.js +431 -0
  60. package/scripts/lib/attribution.js +180 -0
  61. package/scripts/lib/audit.js +289 -0
  62. package/scripts/lib/avorelo-skill-registry.js +810 -0
  63. package/scripts/lib/batch-jobs.js +71 -0
  64. package/scripts/lib/brain-pack.js +578 -0
  65. package/scripts/lib/brand-boundary.js +424 -0
  66. package/scripts/lib/brand.js +74 -0
  67. package/scripts/lib/browser-capability.js +1048 -0
  68. package/scripts/lib/browser-proof-preflight.js +321 -0
  69. package/scripts/lib/cache-readiness.js +187 -0
  70. package/scripts/lib/canonical-reentry.js +162 -0
  71. package/scripts/lib/capability-packs.js +314 -0
  72. package/scripts/lib/capability-recommender.js +512 -0
  73. package/scripts/lib/capability-registry.js +1059 -0
  74. package/scripts/lib/carry-forward-surfacing.js +194 -0
  75. package/scripts/lib/ccusage-adapter.js +188 -0
  76. package/scripts/lib/company-loop.js +1149 -0
  77. package/scripts/lib/config.js +637 -0
  78. package/scripts/lib/context-acquisition-plan.js +287 -0
  79. package/scripts/lib/context-budget-guard.js +170 -0
  80. package/scripts/lib/context-budget-scanner.js +257 -0
  81. package/scripts/lib/context-optimizer.js +715 -0
  82. package/scripts/lib/context-reduction-plan.js +178 -0
  83. package/scripts/lib/context-safety.js +88 -0
  84. package/scripts/lib/context-savings-engine.js +158 -0
  85. package/scripts/lib/cost-evidence.js +254 -0
  86. package/scripts/lib/cross-host-install-plan.js +308 -0
  87. package/scripts/lib/cross-host-install-readiness.js +237 -0
  88. package/scripts/lib/cross-host-value-flow.js +268 -0
  89. package/scripts/lib/dashboard.js +900 -0
  90. package/scripts/lib/design-partner-feedback.js +346 -0
  91. package/scripts/lib/entitlements.js +100 -0
  92. package/scripts/lib/execution-packet.js +559 -0
  93. package/scripts/lib/experimentation-events.js +547 -0
  94. package/scripts/lib/external-capability-compliance.js +107 -0
  95. package/scripts/lib/external-user-simulation.js +166 -0
  96. package/scripts/lib/failure-recovery-readiness.js +81 -0
  97. package/scripts/lib/failure-recovery.js +419 -0
  98. package/scripts/lib/feedback-intelligence.js +537 -0
  99. package/scripts/lib/feedback-signals.js +205 -0
  100. package/scripts/lib/file-integrity.js +68 -0
  101. package/scripts/lib/fsx.js +127 -0
  102. package/scripts/lib/full-readiness-gate.js +451 -0
  103. package/scripts/lib/guidance-builder.js +174 -0
  104. package/scripts/lib/hook-apply.js +1019 -0
  105. package/scripts/lib/hook-baseline.js +310 -0
  106. package/scripts/lib/hook-config-preview.js +275 -0
  107. package/scripts/lib/hook-contracts.js +290 -0
  108. package/scripts/lib/hook-safety-boundary-readiness.js +80 -0
  109. package/scripts/lib/host-capability-matrix.js +351 -0
  110. package/scripts/lib/host-support-context.js +254 -0
  111. package/scripts/lib/http-hook-action.js +538 -0
  112. package/scripts/lib/install-ai-readiness.js +84 -0
  113. package/scripts/lib/install-intake-risk.js +1037 -0
  114. package/scripts/lib/install-journey-intelligence.js +329 -0
  115. package/scripts/lib/intervention-guidance.js +57 -0
  116. package/scripts/lib/known-limitations.js +115 -0
  117. package/scripts/lib/l8-path-truth.js +146 -0
  118. package/scripts/lib/launch-hardening-gate.js +436 -0
  119. package/scripts/lib/launch-readiness.js +628 -0
  120. package/scripts/lib/learning-memory.js +686 -0
  121. package/scripts/lib/lifecycle-hooks.js +802 -0
  122. package/scripts/lib/local-package-smoke.js +423 -0
  123. package/scripts/lib/local-pricing.js +299 -0
  124. package/scripts/lib/mcp-enforcement.js +311 -0
  125. package/scripts/lib/mcp-least-privilege-policy.js +303 -0
  126. package/scripts/lib/mcp-tool-inventory.js +388 -0
  127. package/scripts/lib/mcp-tool-risk.js +0 -0
  128. package/scripts/lib/memory.js +335 -0
  129. package/scripts/lib/metrics.js +699 -0
  130. package/scripts/lib/micro-proof.js +133 -0
  131. package/scripts/lib/next-run-context.js +436 -0
  132. package/scripts/lib/operating-value.js +1648 -0
  133. package/scripts/lib/optimization-v3.js +122 -0
  134. package/scripts/lib/orchestration/adapters/_shared.js +49 -0
  135. package/scripts/lib/orchestration/adapters/aider.js +18 -0
  136. package/scripts/lib/orchestration/adapters/claude-code.js +35 -0
  137. package/scripts/lib/orchestration/adapters/codex.js +35 -0
  138. package/scripts/lib/orchestration/adapters/gemini-cli.js +18 -0
  139. package/scripts/lib/orchestration/adapters/git.js +25 -0
  140. package/scripts/lib/orchestration/adapters/index.js +31 -0
  141. package/scripts/lib/orchestration/adapters/lm-studio.js +18 -0
  142. package/scripts/lib/orchestration/adapters/ollama.js +18 -0
  143. package/scripts/lib/orchestration/adapters/opencode.js +18 -0
  144. package/scripts/lib/orchestration/adapters/openrouter.js +18 -0
  145. package/scripts/lib/orchestration/adapters/test-runner.js +25 -0
  146. package/scripts/lib/orchestration/cli.js +438 -0
  147. package/scripts/lib/orchestration/execution-manager.js +279 -0
  148. package/scripts/lib/orchestration/handoff.js +314 -0
  149. package/scripts/lib/orchestration/index.js +456 -0
  150. package/scripts/lib/orchestration/inventory.js +47 -0
  151. package/scripts/lib/orchestration/model-discovery.js +498 -0
  152. package/scripts/lib/orchestration/model-profiler.js +170 -0
  153. package/scripts/lib/orchestration/model-profiles.js +252 -0
  154. package/scripts/lib/orchestration/model-refresh-policy.js +72 -0
  155. package/scripts/lib/orchestration/proof-writer.js +349 -0
  156. package/scripts/lib/orchestration/provider-discovery/aider.js +49 -0
  157. package/scripts/lib/orchestration/provider-discovery/claude-code.js +56 -0
  158. package/scripts/lib/orchestration/provider-discovery/codex.js +49 -0
  159. package/scripts/lib/orchestration/provider-discovery/common.js +186 -0
  160. package/scripts/lib/orchestration/provider-discovery/gemini.js +106 -0
  161. package/scripts/lib/orchestration/provider-discovery/lm-studio.js +118 -0
  162. package/scripts/lib/orchestration/provider-discovery/models-dev.js +12 -0
  163. package/scripts/lib/orchestration/provider-discovery/ollama.js +100 -0
  164. package/scripts/lib/orchestration/provider-discovery/opencode.js +47 -0
  165. package/scripts/lib/orchestration/provider-discovery/openrouter.js +44 -0
  166. package/scripts/lib/orchestration/risk-classifier.js +130 -0
  167. package/scripts/lib/orchestration/routing-policy.js +486 -0
  168. package/scripts/lib/orchestration/settings.js +112 -0
  169. package/scripts/lib/orchestration/state.js +165 -0
  170. package/scripts/lib/orchestration/verification-manager.js +138 -0
  171. package/scripts/lib/output-profiles.js +146 -0
  172. package/scripts/lib/package-content-audit.js +368 -0
  173. package/scripts/lib/package-runtime.js +278 -0
  174. package/scripts/lib/plan-surface.js +53 -0
  175. package/scripts/lib/plans.js +2318 -0
  176. package/scripts/lib/policy-provider.js +27 -0
  177. package/scripts/lib/prelaunch-activation-readiness.js +409 -0
  178. package/scripts/lib/prelaunch-evidence-store.js +816 -0
  179. package/scripts/lib/prelaunch-intelligence.js +869 -0
  180. package/scripts/lib/pricing-experiment.js +118 -0
  181. package/scripts/lib/pro-moment-events.js +77 -0
  182. package/scripts/lib/pro-moment-state.js +227 -0
  183. package/scripts/lib/pro-moments.js +1216 -0
  184. package/scripts/lib/product-learning-events.js +629 -0
  185. package/scripts/lib/project-profile.js +555 -0
  186. package/scripts/lib/prompt-compiler.js +280 -0
  187. package/scripts/lib/prompt-lint.js +32 -0
  188. package/scripts/lib/prompt-suggestions.js +52 -0
  189. package/scripts/lib/proof-canonical.js +398 -0
  190. package/scripts/lib/proof-drilldown.js +383 -0
  191. package/scripts/lib/proof-events.js +342 -0
  192. package/scripts/lib/proof-history.js +243 -0
  193. package/scripts/lib/proof-metrics.js +296 -0
  194. package/scripts/lib/proof-outcome-evidence.js +134 -0
  195. package/scripts/lib/proof-receipt.js +335 -0
  196. package/scripts/lib/proof-record.js +461 -0
  197. package/scripts/lib/public-activation-distribution-gate.js +258 -0
  198. package/scripts/lib/public-cli.js +3891 -0
  199. package/scripts/lib/public-distribution-truth.js +211 -0
  200. package/scripts/lib/public-install-claim-checker.js +294 -0
  201. package/scripts/lib/publish-provenance-readiness.js +283 -0
  202. package/scripts/lib/readiness-delta.js +218 -0
  203. package/scripts/lib/readiness-evidence-closure.js +196 -0
  204. package/scripts/lib/reentry-memory-capture.js +241 -0
  205. package/scripts/lib/reentry-memory-retrieval.js +302 -0
  206. package/scripts/lib/reentry-memory-status.js +146 -0
  207. package/scripts/lib/reentry-memory-store.js +178 -0
  208. package/scripts/lib/reentry-state.js +66 -0
  209. package/scripts/lib/release-candidate-bundle.js +166 -0
  210. package/scripts/lib/remediation.js +81 -0
  211. package/scripts/lib/repo-map.js +391 -0
  212. package/scripts/lib/run-improvements-lifecycle.js +330 -0
  213. package/scripts/lib/run-improvements.js +789 -0
  214. package/scripts/lib/runtime-decision-policy.js +387 -0
  215. package/scripts/lib/safe-path-engine.js +705 -0
  216. package/scripts/lib/safe-run-controller.js +887 -0
  217. package/scripts/lib/score.js +262 -0
  218. package/scripts/lib/seamless-enforcement.js +329 -0
  219. package/scripts/lib/seamless-outcome.js +689 -0
  220. package/scripts/lib/seamless-reality-gate.js +5043 -0
  221. package/scripts/lib/security-risk-classifier.js +511 -0
  222. package/scripts/lib/security-scan.js +384 -0
  223. package/scripts/lib/session-context-optimizer.js +1211 -0
  224. package/scripts/lib/session-timing.js +315 -0
  225. package/scripts/lib/skill-hygiene.js +805 -0
  226. package/scripts/lib/skill-packs.js +161 -0
  227. package/scripts/lib/skills-operating-layer.js +580 -0
  228. package/scripts/lib/smart-work-routing.js +768 -0
  229. package/scripts/lib/source-catalog.js +700 -0
  230. package/scripts/lib/status-value-summary.js +32 -0
  231. package/scripts/lib/support-bundle.js +578 -0
  232. package/scripts/lib/task-continuation.js +440 -0
  233. package/scripts/lib/test-helpers.js +15 -0
  234. package/scripts/lib/tier.js +38 -0
  235. package/scripts/lib/token-context-quality-gate.js +370 -0
  236. package/scripts/lib/token-cost-capture.js +187 -0
  237. package/scripts/lib/token-cost-intelligence.js +358 -0
  238. package/scripts/lib/token-efficiency-evidence.js +213 -0
  239. package/scripts/lib/token-evidence.js +699 -0
  240. package/scripts/lib/tokenish.js +17 -0
  241. package/scripts/lib/tool-output-sandbox.js +304 -0
  242. package/scripts/lib/trust-audit.js +136 -0
  243. package/scripts/lib/unified-events.js +396 -0
  244. package/scripts/lib/upgrade-interruption-recovery.js +407 -0
  245. package/scripts/lib/usage-ledger.js +201 -0
  246. package/scripts/lib/value-ledger.js +130 -0
  247. package/scripts/lib/value-proof-calibration.js +531 -0
  248. package/scripts/lib/visual-qa.js +231 -0
  249. package/scripts/lib/voice-alpha.js +29 -0
  250. package/scripts/lib/work-aware-orchestration.js +976 -0
  251. package/scripts/lib/work-control-receipts.js +577 -0
  252. package/scripts/lib/work-ledger.js +1123 -0
  253. package/scripts/lib/work-panel-preview.js +352 -0
  254. package/scripts/lib/workflow-discipline.js +280 -0
  255. package/scripts/lib/workflow-signals.js +419 -0
  256. package/scripts/lib/workspace-map.js +281 -0
  257. package/scripts/lib/workspace-registry.js +1367 -0
  258. package/scripts/lib/workspace-resolver.js +480 -0
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+
3
+ // ── MCP Least-Privilege Policy ────────────────────────────────────────────────
4
+ //
5
+ // Contract: avorelo.mcpLeastPrivilegePolicy.v1
6
+ //
7
+ // Builds least-privilege policy from risk report.
8
+ // Preview only — does NOT modify any MCP config.
9
+
10
+ const fs = require("node:fs");
11
+ const path = require("node:path");
12
+ const { REASON_CODES } = require("./mcp-tool-risk");
13
+
14
+ const CONTRACT = "avorelo.mcpLeastPrivilegePolicy.v1";
15
+ const SCHEMA_VERSION = 1;
16
+
17
+ const POLICY_DIR_REL = ".claude/cco/orchestration/mcp-tool-governance";
18
+ const POLICY_REL = `${POLICY_DIR_REL}/latest-policy.json`;
19
+ const PREVIEW_REL = `${POLICY_DIR_REL}/latest-policy-preview.json`;
20
+
21
+ // ── Policy decisions ──────────────────────────────────────────────────────────
22
+
23
+ const DECISIONS = Object.freeze({
24
+ ALLOW: "allow",
25
+ WARN: "warn",
26
+ APPROVAL_REQUIRED: "approval_required",
27
+ BLOCK: "block",
28
+ NOT_ENOUGH_INFORMATION: "not_enough_information",
29
+ });
30
+
31
+ // ── Helpers ───────────────────────────────────────────────────────────────────
32
+
33
+ function nowIso() {
34
+ return new Date().toISOString();
35
+ }
36
+
37
+ function safeWriteJson(filePath, data) {
38
+ try {
39
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
40
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
41
+ } catch {
42
+ // non-fatal
43
+ }
44
+ }
45
+
46
+ // ── Policy decision logic ─────────────────────────────────────────────────────
47
+
48
+ function decideMcpToolPolicy(toolRisk, context, options = {}) {
49
+ if (!toolRisk) {
50
+ return {
51
+ decision: DECISIONS.NOT_ENOUGH_INFORMATION,
52
+ reasonCodes: ["NO_TOOL_RISK_DATA"],
53
+ safeAlternative: "Run `avorelo mcp risk` to classify tools first.",
54
+ safeNextAction: "Run `avorelo mcp inventory` then `avorelo mcp risk`.",
55
+ taskStillExecutable: true,
56
+ approvalRequired: false,
57
+ redacted: true,
58
+ };
59
+ }
60
+
61
+ const codes = toolRisk.reasonCodes || [];
62
+ const riskLevel = toolRisk.riskLevel || "unknown";
63
+ let decision = DECISIONS.NOT_ENOUGH_INFORMATION;
64
+ let safeAlternative = null;
65
+ let safeNextAction = "Review tool risk before proceeding.";
66
+ let approvalRequired = false;
67
+ let taskStillExecutable = true;
68
+
69
+ // Critical: block suspicious metadata / tool poisoning signals
70
+ if (
71
+ codes.includes(REASON_CODES.SUSPICIOUS_TOOL_DESCRIPTION) ||
72
+ codes.includes(REASON_CODES.HIDDEN_UNICODE_OR_OBFUSCATION)
73
+ ) {
74
+ decision = DECISIONS.BLOCK;
75
+ safeAlternative = "Remove or replace the tool with a verified alternative.";
76
+ safeNextAction = "Quarantine this tool: do not use until metadata is reviewed and verified clean.";
77
+ taskStillExecutable = false;
78
+ approvalRequired = false;
79
+ return { decision, reasonCodes: codes, safeAlternative, safeNextAction, taskStillExecutable, approvalRequired, redacted: true };
80
+ }
81
+
82
+ // Critical level: destructive/deploy/prod/shell → block or approval_required
83
+ if (
84
+ riskLevel === "critical" ||
85
+ codes.includes(REASON_CODES.DESTRUCTIVE_ACTION) ||
86
+ codes.includes(REASON_CODES.DEPLOY_OR_PROD_ACTION) ||
87
+ codes.includes(REASON_CODES.SHELL_COMMAND_CAPABILITY)
88
+ ) {
89
+ if (riskLevel === "critical") {
90
+ decision = DECISIONS.BLOCK;
91
+ taskStillExecutable = false;
92
+ safeAlternative = "Use a scoped read-only alternative if the task only needs inspection.";
93
+ safeNextAction = "This tool requires explicit operator approval before use. Escalate to a human reviewer.";
94
+ } else {
95
+ decision = DECISIONS.APPROVAL_REQUIRED;
96
+ approvalRequired = true;
97
+ taskStillExecutable = true;
98
+ safeAlternative = "Scope the tool to read-only or limit to the minimum required action.";
99
+ safeNextAction = "Get explicit approval before using this tool. Document the task scope.";
100
+ }
101
+ return { decision, reasonCodes: codes, safeAlternative, safeNextAction, taskStillExecutable, approvalRequired, redacted: true };
102
+ }
103
+
104
+ // High level → approval_required by default
105
+ if (riskLevel === "high") {
106
+ decision = DECISIONS.APPROVAL_REQUIRED;
107
+ approvalRequired = true;
108
+ taskStillExecutable = true;
109
+ safeAlternative = "Prefer a scoped read-only tool if available.";
110
+ safeNextAction = "Approval required before using this tool. State the task scope and get sign-off.";
111
+ return { decision, reasonCodes: codes, safeAlternative, safeNextAction, taskStillExecutable, approvalRequired, redacted: true };
112
+ }
113
+
114
+ // Unknown server → approval_required
115
+ if (codes.includes(REASON_CODES.UNKNOWN_SERVER) || codes.includes(REASON_CODES.UNTRUSTED_SOURCE)) {
116
+ decision = DECISIONS.APPROVAL_REQUIRED;
117
+ approvalRequired = true;
118
+ taskStillExecutable = true;
119
+ safeAlternative = "Register the server with a known owner before use.";
120
+ safeNextAction = "Identify and verify the server source before allowing. Add to approved server list.";
121
+ return { decision, reasonCodes: codes, safeAlternative, safeNextAction, taskStillExecutable, approvalRequired, redacted: true };
122
+ }
123
+
124
+ // Medium level: approval_required for broad access, warn for scoped
125
+ if (riskLevel === "medium") {
126
+ if (
127
+ codes.includes(REASON_CODES.BROAD_FILESYSTEM_ACCESS) ||
128
+ codes.includes(REASON_CODES.AUTH_REQUIRED) ||
129
+ codes.includes(REASON_CODES.NETWORK_ACCESS)
130
+ ) {
131
+ decision = DECISIONS.APPROVAL_REQUIRED;
132
+ approvalRequired = true;
133
+ taskStillExecutable = true;
134
+ safeAlternative = "Scope to minimum required paths/endpoints.";
135
+ safeNextAction = "Confirm the tool is scoped to the task before proceeding.";
136
+ } else {
137
+ decision = DECISIONS.WARN;
138
+ safeNextAction = "Review tool access scope before use.";
139
+ }
140
+ return { decision, reasonCodes: codes, safeAlternative, safeNextAction, taskStillExecutable, approvalRequired, redacted: true };
141
+ }
142
+
143
+ // Low / read-only → allow or warn
144
+ if (riskLevel === "low") {
145
+ if (codes.includes(REASON_CODES.LOW_RISK_READ_ONLY)) {
146
+ decision = DECISIONS.ALLOW;
147
+ safeNextAction = null;
148
+ } else {
149
+ decision = DECISIONS.WARN;
150
+ safeNextAction = "Verify read-only scope before use.";
151
+ }
152
+ return { decision, reasonCodes: codes, safeAlternative, safeNextAction, taskStillExecutable, approvalRequired, redacted: true };
153
+ }
154
+
155
+ // Fallback
156
+ decision = DECISIONS.NOT_ENOUGH_INFORMATION;
157
+ safeNextAction = "Run `avorelo mcp risk` to classify before proceeding.";
158
+ return { decision, reasonCodes: codes, safeAlternative, safeNextAction, taskStillExecutable, approvalRequired, redacted: true };
159
+ }
160
+
161
+ // ── Full policy build ─────────────────────────────────────────────────────────
162
+
163
+ function buildMcpLeastPrivilegePolicy(cwd, riskReport, options = {}) {
164
+ if (!riskReport || riskReport.status === "pass" && riskReport.summary?.serverCount === 0) {
165
+ return {
166
+ contract: CONTRACT,
167
+ schemaVersion: SCHEMA_VERSION,
168
+ createdAt: nowIso(),
169
+ status: "pass",
170
+ policyType: "least_privilege",
171
+ decisions: [],
172
+ summary: { allow: 0, warn: 0, approvalRequired: 0, blocked: 0, notEnoughInfo: 0 },
173
+ caveats: ["No MCP servers found — no policy decisions needed."],
174
+ previewOnly: true,
175
+ configNotModified: true,
176
+ redacted: true,
177
+ };
178
+ }
179
+
180
+ const decisions = [];
181
+ const context = options.context || {};
182
+
183
+ for (const toolRisk of riskReport.tools || []) {
184
+ const pd = decideMcpToolPolicy(toolRisk, context, options);
185
+ decisions.push({
186
+ toolId: toolRisk.toolId,
187
+ serverId: toolRisk.serverId,
188
+ name: toolRisk.name,
189
+ riskLevel: toolRisk.riskLevel,
190
+ ...pd,
191
+ });
192
+ }
193
+
194
+ const summary = {
195
+ allow: decisions.filter((d) => d.decision === DECISIONS.ALLOW).length,
196
+ warn: decisions.filter((d) => d.decision === DECISIONS.WARN).length,
197
+ approvalRequired: decisions.filter((d) => d.decision === DECISIONS.APPROVAL_REQUIRED).length,
198
+ blocked: decisions.filter((d) => d.decision === DECISIONS.BLOCK).length,
199
+ notEnoughInfo: decisions.filter((d) => d.decision === DECISIONS.NOT_ENOUGH_INFORMATION).length,
200
+ };
201
+
202
+ const overallStatus =
203
+ summary.blocked > 0 ? "blocked" :
204
+ summary.approvalRequired > 0 ? "needs_approval" :
205
+ summary.warn > 0 ? "warn" :
206
+ summary.notEnoughInfo > 0 ? "needs_review" : "pass";
207
+
208
+ return {
209
+ contract: CONTRACT,
210
+ schemaVersion: SCHEMA_VERSION,
211
+ createdAt: nowIso(),
212
+ status: overallStatus,
213
+ policyType: "least_privilege",
214
+ decisions,
215
+ summary,
216
+ caveats: riskReport.caveats || [],
217
+ previewOnly: true,
218
+ configNotModified: true,
219
+ redacted: true,
220
+ };
221
+ }
222
+
223
+ // ── Preview build ─────────────────────────────────────────────────────────────
224
+
225
+ function buildMcpPolicyPreview(cwd, policy, options = {}) {
226
+ return {
227
+ contract: "avorelo.mcpPolicyPreview.v1",
228
+ schemaVersion: 1,
229
+ createdAt: nowIso(),
230
+ status: policy.status,
231
+ previewOnly: true,
232
+ configNotModified: true,
233
+ note: "This preview shows what policy WOULD be applied. No MCP config has been modified.",
234
+ summary: policy.summary,
235
+ topDecisions: (policy.decisions || [])
236
+ .filter((d) => ["block", "approval_required"].includes(d.decision))
237
+ .slice(0, 10)
238
+ .map((d) => ({
239
+ name: d.name,
240
+ riskLevel: d.riskLevel,
241
+ decision: d.decision,
242
+ safeNextAction: d.safeNextAction,
243
+ reasonCodes: (d.reasonCodes || []).slice(0, 4),
244
+ })),
245
+ caveats: policy.caveats || [],
246
+ redacted: true,
247
+ };
248
+ }
249
+
250
+ function writeMcpPolicy(cwd, policy) {
251
+ const absPath = path.join(cwd, POLICY_REL);
252
+ safeWriteJson(absPath, policy);
253
+ return POLICY_REL;
254
+ }
255
+
256
+ function writeMcpPolicyPreview(cwd, preview) {
257
+ const absPath = path.join(cwd, PREVIEW_REL);
258
+ safeWriteJson(absPath, preview);
259
+ return PREVIEW_REL;
260
+ }
261
+
262
+ function formatMcpPolicyText(policy, options = {}) {
263
+ const { debug } = options;
264
+ const s = policy.summary || {};
265
+ const lines = [
266
+ `MCP Least-Privilege Policy [${policy.status}]`,
267
+ `Allow: ${s.allow} · Warn: ${s.warn} · Approval: ${s.approvalRequired} · Blocked: ${s.blocked}`,
268
+ `Preview only — config not modified.`,
269
+ ];
270
+
271
+ if (debug && policy.decisions && policy.decisions.length > 0) {
272
+ for (const d of policy.decisions) {
273
+ lines.push(` ${d.name || d.toolId} → ${d.decision} [${d.riskLevel}]`);
274
+ if (d.safeNextAction) lines.push(` Next: ${d.safeNextAction}`);
275
+ }
276
+ } else if (s.blocked > 0 || s.approvalRequired > 0) {
277
+ const topIssues = (policy.decisions || [])
278
+ .filter((d) => ["block", "approval_required"].includes(d.decision))
279
+ .slice(0, 3);
280
+ for (const d of topIssues) {
281
+ lines.push(` ${d.name || d.toolId} → ${d.decision}`);
282
+ }
283
+ }
284
+
285
+ lines.push(`Next: Run \`avorelo mcp doctor --json\` for full governance status.`);
286
+ return lines.join("\n");
287
+ }
288
+
289
+ // ── Exports ───────────────────────────────────────────────────────────────────
290
+
291
+ module.exports = {
292
+ CONTRACT,
293
+ SCHEMA_VERSION,
294
+ POLICY_REL,
295
+ PREVIEW_REL,
296
+ DECISIONS,
297
+ buildMcpLeastPrivilegePolicy,
298
+ decideMcpToolPolicy,
299
+ buildMcpPolicyPreview,
300
+ writeMcpPolicy,
301
+ writeMcpPolicyPreview,
302
+ formatMcpPolicyText,
303
+ };
@@ -0,0 +1,388 @@
1
+ "use strict";
2
+
3
+ // ── MCP Tool Inventory ────────────────────────────────────────────────────────
4
+ //
5
+ // Contract: avorelo.mcpToolInventory.v1
6
+ //
7
+ // Discovers MCP config files locally, parses them statically, and builds a
8
+ // redacted inventory of servers and tool surfaces.
9
+ //
10
+ // SAFETY RULES (never violated):
11
+ // - Does NOT execute MCP servers
12
+ // - Does NOT call MCP tools
13
+ // - Does NOT resolve remote URLs
14
+ // - Does NOT read raw env values / secrets / auth headers
15
+ // - Normalizes paths, handles invalid JSON gracefully (needs_review, not crash)
16
+
17
+ const fs = require("node:fs");
18
+ const path = require("node:path");
19
+
20
+ const CONTRACT = "avorelo.mcpToolInventory.v1";
21
+ const SCHEMA_VERSION = 1;
22
+
23
+ const INVENTORY_DIR_REL = ".claude/cco/orchestration/mcp-tool-governance";
24
+ const INVENTORY_REL = `${INVENTORY_DIR_REL}/latest-inventory.json`;
25
+
26
+ // Known MCP config file locations (relative to cwd), in priority order
27
+ const MCP_CONFIG_CANDIDATES = [
28
+ ".mcp.json",
29
+ "mcp.json",
30
+ ".claude/settings.json",
31
+ ".claude/settings.local.json",
32
+ ".cursor/mcp.json",
33
+ ".vscode/mcp.json",
34
+ ".codex/mcp.json",
35
+ "AGENTS.md", // reference-only: may mention MCP servers
36
+ "CLAUDE.md", // reference-only
37
+ ];
38
+
39
+ // Known MCP config formats we can parse structurally
40
+ const SUPPORTED_FORMATS = new Set([
41
+ ".mcp.json",
42
+ "mcp.json",
43
+ ".claude/settings.json",
44
+ ".claude/settings.local.json",
45
+ ".cursor/mcp.json",
46
+ ".vscode/mcp.json",
47
+ ".codex/mcp.json",
48
+ ]);
49
+
50
+ // Patterns that look like secrets / env references (for redaction)
51
+ const SECRET_REF_PATTERN = /\$\{?[A-Z_]{4,}\}?|sk-[A-Za-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|password|api[_-]?key|auth[_-]?token|bearer\s/i;
52
+
53
+ // ── Helpers ───────────────────────────────────────────────────────────────────
54
+
55
+ function nowIso() {
56
+ return new Date().toISOString();
57
+ }
58
+
59
+ function safeWriteJson(filePath, data) {
60
+ try {
61
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
62
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
63
+ } catch {
64
+ // non-fatal
65
+ }
66
+ }
67
+
68
+ function safeReadFile(absPath) {
69
+ try {
70
+ return fs.readFileSync(absPath, "utf8").replace(/^/, "");
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ function safeParseJson(text) {
77
+ try {
78
+ return { ok: true, data: JSON.parse(text) };
79
+ } catch (e) {
80
+ return { ok: false, error: e.message };
81
+ }
82
+ }
83
+
84
+ function redactValue(value) {
85
+ if (typeof value !== "string") return "[non-string]";
86
+ if (SECRET_REF_PATTERN.test(value)) return "[redacted]";
87
+ // Redact long tokens / base64-like blobs
88
+ if (value.length > 80 && /^[A-Za-z0-9+/=_-]{40,}$/.test(value.trim())) return "[redacted-long-token]";
89
+ // Trim long values
90
+ if (value.length > 120) return value.slice(0, 60) + "…[truncated]";
91
+ return value;
92
+ }
93
+
94
+ function summarizeArgs(args) {
95
+ if (!Array.isArray(args) || args.length === 0) return "none";
96
+ return args.map((a) => {
97
+ if (typeof a === "string") return redactValue(a);
98
+ return "[non-string-arg]";
99
+ }).join(" ");
100
+ }
101
+
102
+ function summarizeEnv(envObj) {
103
+ if (!envObj || typeof envObj !== "object") return "none";
104
+ const keys = Object.keys(envObj);
105
+ if (keys.length === 0) return "none";
106
+ // Never log values — keys only, with count
107
+ return `${keys.length} env vars: ${keys.map((k) => `${k}=[redacted]`).join(", ")}`;
108
+ }
109
+
110
+ function classifyTransport(serverDef) {
111
+ if (!serverDef || typeof serverDef !== "object") return "unknown";
112
+ if (serverDef.transport === "stdio" || serverDef.command) return "stdio";
113
+ if (serverDef.transport === "http" || serverDef.url?.startsWith("http")) return "http";
114
+ if (serverDef.transport === "sse") return "sse";
115
+ return "unknown";
116
+ }
117
+
118
+ function makeSummaryRef(value, label) {
119
+ if (!value) return null;
120
+ const str = typeof value === "string" ? value : JSON.stringify(value);
121
+ return `[${label}: ${redactValue(str.length > 80 ? str.slice(0, 40) + "…" : str)}]`;
122
+ }
123
+
124
+ // ── Config discovery ──────────────────────────────────────────────────────────
125
+
126
+ function discoverMcpConfigFiles(cwd, options = {}) {
127
+ const found = [];
128
+ const unsupported = [];
129
+ for (const rel of MCP_CONFIG_CANDIDATES) {
130
+ const absPath = path.join(cwd, rel);
131
+ try {
132
+ if (!fs.existsSync(absPath)) continue;
133
+ } catch {
134
+ continue;
135
+ }
136
+ const isSupported = SUPPORTED_FORMATS.has(rel);
137
+ const isMarkdown = rel.endsWith(".md");
138
+ if (isMarkdown) {
139
+ unsupported.push({ rel, reason: "markdown_reference_only" });
140
+ continue;
141
+ }
142
+ const stat = (() => { try { return fs.statSync(absPath); } catch { return null; } })();
143
+ if (!stat || !stat.isFile()) continue;
144
+ found.push({ rel, absPath, format: isSupported ? "json" : "unknown", supported: isSupported });
145
+ }
146
+ return { found, unsupported };
147
+ }
148
+
149
+ // ── Config parsing ────────────────────────────────────────────────────────────
150
+
151
+ function parseMcpConfigFile(cwd, filePath, options = {}) {
152
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
153
+ const relPath = path.isAbsolute(filePath) ? path.relative(cwd, filePath) : filePath;
154
+ const raw = safeReadFile(absPath);
155
+ if (raw === null) {
156
+ return {
157
+ status: "not_found",
158
+ configRef: relPath,
159
+ servers: [],
160
+ caveats: [`Config file not found: ${relPath}`],
161
+ redacted: true,
162
+ };
163
+ }
164
+
165
+ const parsed = safeParseJson(raw);
166
+ if (!parsed.ok) {
167
+ return {
168
+ status: "invalid_json",
169
+ configRef: relPath,
170
+ servers: [],
171
+ caveats: [`Invalid JSON in ${relPath}: ${parsed.error}`],
172
+ redacted: true,
173
+ };
174
+ }
175
+
176
+ const data = parsed.data;
177
+ const servers = [];
178
+ const caveats = [];
179
+
180
+ // Extract MCP servers from known config shapes
181
+ // Shape 1: { mcpServers: { name: { command, args, env, ... } } }
182
+ // Shape 2: { servers: { name: { ... } } }
183
+ // Shape 3: { mcp: { servers: { ... } } }
184
+ const serverMap =
185
+ (data && data.mcpServers) ||
186
+ (data && data.servers) ||
187
+ (data && data.mcp && data.mcp.servers) ||
188
+ null;
189
+
190
+ if (!serverMap || typeof serverMap !== "object" || Array.isArray(serverMap)) {
191
+ caveats.push(`No mcpServers/servers block found in ${relPath}.`);
192
+ return { status: "no_servers", configRef: relPath, servers: [], caveats, redacted: true };
193
+ }
194
+
195
+ const names = Object.keys(serverMap);
196
+ for (const name of names) {
197
+ const def = serverMap[name];
198
+ if (!def || typeof def !== "object") {
199
+ caveats.push(`Server "${name}" has invalid definition (not an object).`);
200
+ continue;
201
+ }
202
+
203
+ const transport = classifyTransport(def);
204
+ const commandRef = def.command ? makeSummaryRef(def.command, "cmd") : null;
205
+ const argsSummary = summarizeArgs(def.args);
206
+ const envRefSummary = summarizeEnv(def.env);
207
+ const urlRef = def.url ? makeSummaryRef(def.url, "url") : null;
208
+
209
+ // Derive tools from schema (if present in config)
210
+ const rawTools = (def.tools && Array.isArray(def.tools)) ? def.tools : [];
211
+ const parsedTools = rawTools.map((t, i) => parseToolDef(t, name, i));
212
+
213
+ servers.push({
214
+ serverId: `server-${name}`,
215
+ name,
216
+ sourceConfigRef: relPath,
217
+ transport,
218
+ commandRef,
219
+ argsSummary,
220
+ envRefSummary,
221
+ urlRef,
222
+ trustedSource: "unknown",
223
+ owner: "unknown",
224
+ tools: parsedTools,
225
+ redacted: true,
226
+ });
227
+ }
228
+
229
+ return { status: "ok", configRef: relPath, servers, caveats, redacted: true };
230
+ }
231
+
232
+ function parseToolDef(toolDef, serverId, index) {
233
+ if (!toolDef || typeof toolDef !== "object") {
234
+ return {
235
+ toolId: `${serverId}-tool-${index}`,
236
+ serverId,
237
+ name: `unknown-tool-${index}`,
238
+ descriptionSummary: null,
239
+ schemaSummary: null,
240
+ declaredReadOnly: false,
241
+ declaredDestructive: false,
242
+ declaredNetwork: false,
243
+ declaredFilesystem: false,
244
+ declaredAuth: false,
245
+ metadataRiskSignals: ["MALFORMED_TOOL_DEF"],
246
+ redacted: true,
247
+ };
248
+ }
249
+
250
+ const name = toolDef.name || `tool-${index}`;
251
+ const desc = toolDef.description || toolDef.desc || "";
252
+ const schema = toolDef.inputSchema || toolDef.input_schema || toolDef.parameters || null;
253
+
254
+ const descSummary = desc.length > 0 ? (desc.length > 200 ? desc.slice(0, 100) + "…[truncated]" : desc) : null;
255
+ const schemaSummary = schema ? `properties:${Object.keys(schema.properties || {}).length}` : null;
256
+
257
+ return {
258
+ toolId: `${serverId}-${name}`,
259
+ serverId,
260
+ name,
261
+ descriptionSummary: descSummary,
262
+ schemaSummary,
263
+ declaredReadOnly: Boolean(toolDef.readOnly || toolDef.readonly),
264
+ declaredDestructive: Boolean(toolDef.destructive || toolDef.dangerous),
265
+ declaredNetwork: Boolean(toolDef.network || toolDef.http),
266
+ declaredFilesystem: Boolean(toolDef.filesystem || toolDef.files),
267
+ declaredAuth: Boolean(toolDef.auth || toolDef.authorization),
268
+ metadataRiskSignals: [],
269
+ redacted: true,
270
+ };
271
+ }
272
+
273
+ // ── Full inventory build ──────────────────────────────────────────────────────
274
+
275
+ function buildMcpToolInventory(cwd, options = {}) {
276
+ const { found, unsupported } = discoverMcpConfigFiles(cwd, options);
277
+ const configFiles = [];
278
+ const allServers = [];
279
+ const allTools = [];
280
+ const caveats = [];
281
+
282
+ if (found.length === 0 && unsupported.length === 0) {
283
+ return {
284
+ contract: CONTRACT,
285
+ schemaVersion: SCHEMA_VERSION,
286
+ createdAt: nowIso(),
287
+ status: "none",
288
+ configFiles: [],
289
+ servers: [],
290
+ tools: [],
291
+ unsupportedConfigs: unsupported,
292
+ caveats: ["No MCP config files found. This is safe — no tool surfaces to govern."],
293
+ redacted: true,
294
+ };
295
+ }
296
+
297
+ for (const configFile of found) {
298
+ const parsed = parseMcpConfigFile(cwd, configFile.rel, options);
299
+ configFiles.push({ rel: configFile.rel, status: parsed.status });
300
+ caveats.push(...(parsed.caveats || []));
301
+
302
+ for (const server of parsed.servers) {
303
+ allServers.push(server);
304
+ for (const tool of server.tools || []) {
305
+ allTools.push(tool);
306
+ }
307
+ }
308
+ }
309
+
310
+ const hasInvalidJson = configFiles.some((f) => f.status === "invalid_json");
311
+ const hasNoServers = configFiles.every((f) => ["no_servers", "not_found"].includes(f.status));
312
+ const status = hasInvalidJson || unsupported.length > 0
313
+ ? "needs_review"
314
+ : hasNoServers && allServers.length === 0
315
+ ? "none"
316
+ : allServers.length > 0
317
+ ? "ready"
318
+ : "partial";
319
+
320
+ return {
321
+ contract: CONTRACT,
322
+ schemaVersion: SCHEMA_VERSION,
323
+ createdAt: nowIso(),
324
+ status,
325
+ configFiles,
326
+ servers: allServers,
327
+ tools: allTools,
328
+ unsupportedConfigs: unsupported,
329
+ caveats: caveats.length > 0 ? caveats : [],
330
+ redacted: true,
331
+ };
332
+ }
333
+
334
+ function writeMcpToolInventory(cwd, inventory) {
335
+ const absPath = path.join(cwd, INVENTORY_REL);
336
+ safeWriteJson(absPath, inventory);
337
+ return INVENTORY_REL;
338
+ }
339
+
340
+ function buildMcpToolInventorySurface(cwd, options = {}) {
341
+ const inventory = buildMcpToolInventory(cwd, options);
342
+ writeMcpToolInventory(cwd, inventory);
343
+ return inventory;
344
+ }
345
+
346
+ function formatMcpToolInventoryText(inventory, options = {}) {
347
+ const { debug } = options;
348
+ const lines = [
349
+ `MCP Tool Inventory [${inventory.status}]`,
350
+ `Config files: ${inventory.configFiles.length} · Servers: ${inventory.servers.length} · Tools: ${inventory.tools.length}`,
351
+ ];
352
+
353
+ if (inventory.servers.length > 0) {
354
+ for (const s of inventory.servers) {
355
+ lines.push(` Server: ${s.name} [${s.transport}] trust=${s.trustedSource}`);
356
+ if (debug && s.tools && s.tools.length > 0) {
357
+ for (const t of s.tools) {
358
+ lines.push(` Tool: ${t.name}`);
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ if (inventory.unsupportedConfigs.length > 0) {
365
+ lines.push(`Unsupported configs: ${inventory.unsupportedConfigs.map((u) => u.rel).join(", ")}`);
366
+ }
367
+
368
+ if (inventory.caveats && inventory.caveats.length > 0 && debug) {
369
+ lines.push(`Caveats: ${inventory.caveats.join("; ")}`);
370
+ }
371
+
372
+ lines.push(`Next: Run \`avorelo mcp risk --json\` to classify tool risk.`);
373
+ return lines.join("\n");
374
+ }
375
+
376
+ // ── Exports ───────────────────────────────────────────────────────────────────
377
+
378
+ module.exports = {
379
+ CONTRACT,
380
+ SCHEMA_VERSION,
381
+ INVENTORY_REL,
382
+ discoverMcpConfigFiles,
383
+ parseMcpConfigFile,
384
+ buildMcpToolInventory,
385
+ writeMcpToolInventory,
386
+ buildMcpToolInventorySurface,
387
+ formatMcpToolInventoryText,
388
+ };
Binary file