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.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/bin/avorelo +9 -0
- package/package.json +135 -0
- package/scripts/README.md +40 -0
- package/scripts/cco-dashboard.js +252 -0
- package/scripts/cco-status.js +430 -0
- package/scripts/lib/activation/account-state.js +37 -0
- package/scripts/lib/activation/activation-runner.js +546 -0
- package/scripts/lib/activation/activation-self-healing.js +480 -0
- package/scripts/lib/activation/activation-state.js +83 -0
- package/scripts/lib/activation/activation-summary.js +191 -0
- package/scripts/lib/activation/adapters/claude-code.js +77 -0
- package/scripts/lib/activation/adapters/codex-cli.js +52 -0
- package/scripts/lib/activation/adapters/cursor.js +37 -0
- package/scripts/lib/activation/adapters/github-agent.js +39 -0
- package/scripts/lib/activation/adapters/terminal.js +42 -0
- package/scripts/lib/activation/adapters/vscode.js +39 -0
- package/scripts/lib/activation/adapters/windsurf.js +37 -0
- package/scripts/lib/activation/ai-surface-detector.js +151 -0
- package/scripts/lib/activation/connect-account.js +145 -0
- package/scripts/lib/activation/detect-environment.js +75 -0
- package/scripts/lib/activation/detect-hosts.js +62 -0
- package/scripts/lib/activation/format-activation-output.js +109 -0
- package/scripts/lib/activation/next-action.js +43 -0
- package/scripts/lib/activation/repair-engine.js +219 -0
- package/scripts/lib/activation-distribution-readiness.js +507 -0
- package/scripts/lib/adapter-conformance.js +176 -0
- package/scripts/lib/adapter-readiness.js +417 -0
- package/scripts/lib/adapter-safety-boundaries.js +335 -0
- package/scripts/lib/adapter-technical-readiness-gate.js +205 -0
- package/scripts/lib/agent-access-governance.js +455 -0
- package/scripts/lib/agent-enforcement.js +765 -0
- package/scripts/lib/agent-policy-profile.js +210 -0
- package/scripts/lib/agent-security/action-evaluator.js +507 -0
- package/scripts/lib/agent-security/adapter-registry.js +98 -0
- package/scripts/lib/agent-security/auto-policy.js +139 -0
- package/scripts/lib/agent-security/bounded-scan.js +93 -0
- package/scripts/lib/agent-security/enforcement-adapter.js +174 -0
- package/scripts/lib/agent-security/enforcement-engine.js +1129 -0
- package/scripts/lib/agent-security/file-write-adapter.js +183 -0
- package/scripts/lib/agent-security/file-write-rules.js +178 -0
- package/scripts/lib/agent-security/index.js +3342 -0
- package/scripts/lib/agent-security/instruction-risk.js +181 -0
- package/scripts/lib/agent-security/mcp-action-adapter.js +185 -0
- package/scripts/lib/agent-security/mcp-action-rules.js +184 -0
- package/scripts/lib/agent-security/package-action-adapter.js +175 -0
- package/scripts/lib/agent-security/package-action-rules.js +233 -0
- package/scripts/lib/agent-security/performance.js +148 -0
- package/scripts/lib/agent-security/permission-minimizer.js +403 -0
- package/scripts/lib/agent-security/scan-cache.js +74 -0
- package/scripts/lib/agent-security/source-trust.js +146 -0
- package/scripts/lib/ai-install-prompt.js +288 -0
- package/scripts/lib/ai-workspace-hygiene.js +1499 -0
- package/scripts/lib/alpha-activation.js +520 -0
- package/scripts/lib/alpha-feedback.js +263 -0
- package/scripts/lib/alpha-readiness-gate.js +332 -0
- package/scripts/lib/anti-gaming.js +169 -0
- package/scripts/lib/artifact-health.js +431 -0
- package/scripts/lib/attribution.js +180 -0
- package/scripts/lib/audit.js +289 -0
- package/scripts/lib/avorelo-skill-registry.js +810 -0
- package/scripts/lib/batch-jobs.js +71 -0
- package/scripts/lib/brain-pack.js +578 -0
- package/scripts/lib/brand-boundary.js +424 -0
- package/scripts/lib/brand.js +74 -0
- package/scripts/lib/browser-capability.js +1048 -0
- package/scripts/lib/browser-proof-preflight.js +321 -0
- package/scripts/lib/cache-readiness.js +187 -0
- package/scripts/lib/canonical-reentry.js +162 -0
- package/scripts/lib/capability-packs.js +314 -0
- package/scripts/lib/capability-recommender.js +512 -0
- package/scripts/lib/capability-registry.js +1059 -0
- package/scripts/lib/carry-forward-surfacing.js +194 -0
- package/scripts/lib/ccusage-adapter.js +188 -0
- package/scripts/lib/company-loop.js +1149 -0
- package/scripts/lib/config.js +637 -0
- package/scripts/lib/context-acquisition-plan.js +287 -0
- package/scripts/lib/context-budget-guard.js +170 -0
- package/scripts/lib/context-budget-scanner.js +257 -0
- package/scripts/lib/context-optimizer.js +715 -0
- package/scripts/lib/context-reduction-plan.js +178 -0
- package/scripts/lib/context-safety.js +88 -0
- package/scripts/lib/context-savings-engine.js +158 -0
- package/scripts/lib/cost-evidence.js +254 -0
- package/scripts/lib/cross-host-install-plan.js +308 -0
- package/scripts/lib/cross-host-install-readiness.js +237 -0
- package/scripts/lib/cross-host-value-flow.js +268 -0
- package/scripts/lib/dashboard.js +900 -0
- package/scripts/lib/design-partner-feedback.js +346 -0
- package/scripts/lib/entitlements.js +100 -0
- package/scripts/lib/execution-packet.js +559 -0
- package/scripts/lib/experimentation-events.js +547 -0
- package/scripts/lib/external-capability-compliance.js +107 -0
- package/scripts/lib/external-user-simulation.js +166 -0
- package/scripts/lib/failure-recovery-readiness.js +81 -0
- package/scripts/lib/failure-recovery.js +419 -0
- package/scripts/lib/feedback-intelligence.js +537 -0
- package/scripts/lib/feedback-signals.js +205 -0
- package/scripts/lib/file-integrity.js +68 -0
- package/scripts/lib/fsx.js +127 -0
- package/scripts/lib/full-readiness-gate.js +451 -0
- package/scripts/lib/guidance-builder.js +174 -0
- package/scripts/lib/hook-apply.js +1019 -0
- package/scripts/lib/hook-baseline.js +310 -0
- package/scripts/lib/hook-config-preview.js +275 -0
- package/scripts/lib/hook-contracts.js +290 -0
- package/scripts/lib/hook-safety-boundary-readiness.js +80 -0
- package/scripts/lib/host-capability-matrix.js +351 -0
- package/scripts/lib/host-support-context.js +254 -0
- package/scripts/lib/http-hook-action.js +538 -0
- package/scripts/lib/install-ai-readiness.js +84 -0
- package/scripts/lib/install-intake-risk.js +1037 -0
- package/scripts/lib/install-journey-intelligence.js +329 -0
- package/scripts/lib/intervention-guidance.js +57 -0
- package/scripts/lib/known-limitations.js +115 -0
- package/scripts/lib/l8-path-truth.js +146 -0
- package/scripts/lib/launch-hardening-gate.js +436 -0
- package/scripts/lib/launch-readiness.js +628 -0
- package/scripts/lib/learning-memory.js +686 -0
- package/scripts/lib/lifecycle-hooks.js +802 -0
- package/scripts/lib/local-package-smoke.js +423 -0
- package/scripts/lib/local-pricing.js +299 -0
- package/scripts/lib/mcp-enforcement.js +311 -0
- package/scripts/lib/mcp-least-privilege-policy.js +303 -0
- package/scripts/lib/mcp-tool-inventory.js +388 -0
- package/scripts/lib/mcp-tool-risk.js +0 -0
- package/scripts/lib/memory.js +335 -0
- package/scripts/lib/metrics.js +699 -0
- package/scripts/lib/micro-proof.js +133 -0
- package/scripts/lib/next-run-context.js +436 -0
- package/scripts/lib/operating-value.js +1648 -0
- package/scripts/lib/optimization-v3.js +122 -0
- package/scripts/lib/orchestration/adapters/_shared.js +49 -0
- package/scripts/lib/orchestration/adapters/aider.js +18 -0
- package/scripts/lib/orchestration/adapters/claude-code.js +35 -0
- package/scripts/lib/orchestration/adapters/codex.js +35 -0
- package/scripts/lib/orchestration/adapters/gemini-cli.js +18 -0
- package/scripts/lib/orchestration/adapters/git.js +25 -0
- package/scripts/lib/orchestration/adapters/index.js +31 -0
- package/scripts/lib/orchestration/adapters/lm-studio.js +18 -0
- package/scripts/lib/orchestration/adapters/ollama.js +18 -0
- package/scripts/lib/orchestration/adapters/opencode.js +18 -0
- package/scripts/lib/orchestration/adapters/openrouter.js +18 -0
- package/scripts/lib/orchestration/adapters/test-runner.js +25 -0
- package/scripts/lib/orchestration/cli.js +438 -0
- package/scripts/lib/orchestration/execution-manager.js +279 -0
- package/scripts/lib/orchestration/handoff.js +314 -0
- package/scripts/lib/orchestration/index.js +456 -0
- package/scripts/lib/orchestration/inventory.js +47 -0
- package/scripts/lib/orchestration/model-discovery.js +498 -0
- package/scripts/lib/orchestration/model-profiler.js +170 -0
- package/scripts/lib/orchestration/model-profiles.js +252 -0
- package/scripts/lib/orchestration/model-refresh-policy.js +72 -0
- package/scripts/lib/orchestration/proof-writer.js +349 -0
- package/scripts/lib/orchestration/provider-discovery/aider.js +49 -0
- package/scripts/lib/orchestration/provider-discovery/claude-code.js +56 -0
- package/scripts/lib/orchestration/provider-discovery/codex.js +49 -0
- package/scripts/lib/orchestration/provider-discovery/common.js +186 -0
- package/scripts/lib/orchestration/provider-discovery/gemini.js +106 -0
- package/scripts/lib/orchestration/provider-discovery/lm-studio.js +118 -0
- package/scripts/lib/orchestration/provider-discovery/models-dev.js +12 -0
- package/scripts/lib/orchestration/provider-discovery/ollama.js +100 -0
- package/scripts/lib/orchestration/provider-discovery/opencode.js +47 -0
- package/scripts/lib/orchestration/provider-discovery/openrouter.js +44 -0
- package/scripts/lib/orchestration/risk-classifier.js +130 -0
- package/scripts/lib/orchestration/routing-policy.js +486 -0
- package/scripts/lib/orchestration/settings.js +112 -0
- package/scripts/lib/orchestration/state.js +165 -0
- package/scripts/lib/orchestration/verification-manager.js +138 -0
- package/scripts/lib/output-profiles.js +146 -0
- package/scripts/lib/package-content-audit.js +368 -0
- package/scripts/lib/package-runtime.js +278 -0
- package/scripts/lib/plan-surface.js +53 -0
- package/scripts/lib/plans.js +2318 -0
- package/scripts/lib/policy-provider.js +27 -0
- package/scripts/lib/prelaunch-activation-readiness.js +409 -0
- package/scripts/lib/prelaunch-evidence-store.js +816 -0
- package/scripts/lib/prelaunch-intelligence.js +869 -0
- package/scripts/lib/pricing-experiment.js +118 -0
- package/scripts/lib/pro-moment-events.js +77 -0
- package/scripts/lib/pro-moment-state.js +227 -0
- package/scripts/lib/pro-moments.js +1216 -0
- package/scripts/lib/product-learning-events.js +629 -0
- package/scripts/lib/project-profile.js +555 -0
- package/scripts/lib/prompt-compiler.js +280 -0
- package/scripts/lib/prompt-lint.js +32 -0
- package/scripts/lib/prompt-suggestions.js +52 -0
- package/scripts/lib/proof-canonical.js +398 -0
- package/scripts/lib/proof-drilldown.js +383 -0
- package/scripts/lib/proof-events.js +342 -0
- package/scripts/lib/proof-history.js +243 -0
- package/scripts/lib/proof-metrics.js +296 -0
- package/scripts/lib/proof-outcome-evidence.js +134 -0
- package/scripts/lib/proof-receipt.js +335 -0
- package/scripts/lib/proof-record.js +461 -0
- package/scripts/lib/public-activation-distribution-gate.js +258 -0
- package/scripts/lib/public-cli.js +3891 -0
- package/scripts/lib/public-distribution-truth.js +211 -0
- package/scripts/lib/public-install-claim-checker.js +294 -0
- package/scripts/lib/publish-provenance-readiness.js +283 -0
- package/scripts/lib/readiness-delta.js +218 -0
- package/scripts/lib/readiness-evidence-closure.js +196 -0
- package/scripts/lib/reentry-memory-capture.js +241 -0
- package/scripts/lib/reentry-memory-retrieval.js +302 -0
- package/scripts/lib/reentry-memory-status.js +146 -0
- package/scripts/lib/reentry-memory-store.js +178 -0
- package/scripts/lib/reentry-state.js +66 -0
- package/scripts/lib/release-candidate-bundle.js +166 -0
- package/scripts/lib/remediation.js +81 -0
- package/scripts/lib/repo-map.js +391 -0
- package/scripts/lib/run-improvements-lifecycle.js +330 -0
- package/scripts/lib/run-improvements.js +789 -0
- package/scripts/lib/runtime-decision-policy.js +387 -0
- package/scripts/lib/safe-path-engine.js +705 -0
- package/scripts/lib/safe-run-controller.js +887 -0
- package/scripts/lib/score.js +262 -0
- package/scripts/lib/seamless-enforcement.js +329 -0
- package/scripts/lib/seamless-outcome.js +689 -0
- package/scripts/lib/seamless-reality-gate.js +5043 -0
- package/scripts/lib/security-risk-classifier.js +511 -0
- package/scripts/lib/security-scan.js +384 -0
- package/scripts/lib/session-context-optimizer.js +1211 -0
- package/scripts/lib/session-timing.js +315 -0
- package/scripts/lib/skill-hygiene.js +805 -0
- package/scripts/lib/skill-packs.js +161 -0
- package/scripts/lib/skills-operating-layer.js +580 -0
- package/scripts/lib/smart-work-routing.js +768 -0
- package/scripts/lib/source-catalog.js +700 -0
- package/scripts/lib/status-value-summary.js +32 -0
- package/scripts/lib/support-bundle.js +578 -0
- package/scripts/lib/task-continuation.js +440 -0
- package/scripts/lib/test-helpers.js +15 -0
- package/scripts/lib/tier.js +38 -0
- package/scripts/lib/token-context-quality-gate.js +370 -0
- package/scripts/lib/token-cost-capture.js +187 -0
- package/scripts/lib/token-cost-intelligence.js +358 -0
- package/scripts/lib/token-efficiency-evidence.js +213 -0
- package/scripts/lib/token-evidence.js +699 -0
- package/scripts/lib/tokenish.js +17 -0
- package/scripts/lib/tool-output-sandbox.js +304 -0
- package/scripts/lib/trust-audit.js +136 -0
- package/scripts/lib/unified-events.js +396 -0
- package/scripts/lib/upgrade-interruption-recovery.js +407 -0
- package/scripts/lib/usage-ledger.js +201 -0
- package/scripts/lib/value-ledger.js +130 -0
- package/scripts/lib/value-proof-calibration.js +531 -0
- package/scripts/lib/visual-qa.js +231 -0
- package/scripts/lib/voice-alpha.js +29 -0
- package/scripts/lib/work-aware-orchestration.js +976 -0
- package/scripts/lib/work-control-receipts.js +577 -0
- package/scripts/lib/work-ledger.js +1123 -0
- package/scripts/lib/work-panel-preview.js +352 -0
- package/scripts/lib/workflow-discipline.js +280 -0
- package/scripts/lib/workflow-signals.js +419 -0
- package/scripts/lib/workspace-map.js +281 -0
- package/scripts/lib/workspace-registry.js +1367 -0
- package/scripts/lib/workspace-resolver.js +480 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// ── Local Pricing Source v1 ───────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Provides a local, offline pricing config for Avorelo cost evidence.
|
|
6
|
+
// No network fetch. No API keys. No secrets.
|
|
7
|
+
// Default-local pricing is allowed only with confidence low/medium and caveat.
|
|
8
|
+
// User override via .claude/cco/config/local-pricing.json.
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const crypto = require("crypto");
|
|
13
|
+
|
|
14
|
+
const SCHEMA_VERSION = 1;
|
|
15
|
+
const CONTRACT = "avorelo.localPricing.v1";
|
|
16
|
+
const LOCAL_PRICING_CONFIG_REL = ".claude/cco/config/local-pricing.json";
|
|
17
|
+
const LOCAL_PRICING_EVENTS_REL = ".claude/cco/events/pricing.jsonl";
|
|
18
|
+
|
|
19
|
+
function shortId() {
|
|
20
|
+
return crypto.randomBytes(4).toString("hex");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function nowIso() {
|
|
24
|
+
return new Date().toISOString();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ensureDirFor(filePath) {
|
|
28
|
+
const dir = path.dirname(filePath);
|
|
29
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Default local pricing entries ─────────────────────────────────────────────
|
|
33
|
+
// Based on publicly documented Anthropic/OpenAI pricing tiers as of 2025.
|
|
34
|
+
// These are DEFAULTS only. Confidence: low. User must validate against actual invoices.
|
|
35
|
+
// Do not use for billing reconciliation or exact cost claims.
|
|
36
|
+
|
|
37
|
+
const DEFAULT_PRICING_ENTRIES = [
|
|
38
|
+
{
|
|
39
|
+
provider: "anthropic",
|
|
40
|
+
model: "claude-sonnet-4-6",
|
|
41
|
+
modelAlias: "claude-sonnet-4.6",
|
|
42
|
+
currency: "USD",
|
|
43
|
+
unit: "per_1m_tokens",
|
|
44
|
+
inputTokenPrice: 3.0,
|
|
45
|
+
cachedInputTokenPrice: 0.30,
|
|
46
|
+
outputTokenPrice: 15.0,
|
|
47
|
+
cacheWriteTokenPrice: 3.75,
|
|
48
|
+
cacheReadTokenPrice: 0.30,
|
|
49
|
+
batchInputTokenPrice: 1.50,
|
|
50
|
+
batchOutputTokenPrice: 7.50,
|
|
51
|
+
priorityInputTokenPrice: null,
|
|
52
|
+
priorityCachedInputTokenPrice: null,
|
|
53
|
+
priorityOutputTokenPrice: null,
|
|
54
|
+
sessionRuntimePricePerHour: null,
|
|
55
|
+
source: "default_local",
|
|
56
|
+
confidence: "low",
|
|
57
|
+
caveat: "Default local pricing — may not match actual API billing. Verify with your Anthropic billing dashboard.",
|
|
58
|
+
effectiveDate: "2025-01-01",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
provider: "anthropic",
|
|
62
|
+
model: "claude-haiku-4-5",
|
|
63
|
+
modelAlias: "claude-haiku-4.5",
|
|
64
|
+
currency: "USD",
|
|
65
|
+
unit: "per_1m_tokens",
|
|
66
|
+
inputTokenPrice: 0.80,
|
|
67
|
+
cachedInputTokenPrice: 0.08,
|
|
68
|
+
outputTokenPrice: 4.0,
|
|
69
|
+
cacheWriteTokenPrice: 1.0,
|
|
70
|
+
cacheReadTokenPrice: 0.08,
|
|
71
|
+
batchInputTokenPrice: 0.40,
|
|
72
|
+
batchOutputTokenPrice: 2.0,
|
|
73
|
+
priorityInputTokenPrice: null,
|
|
74
|
+
priorityCachedInputTokenPrice: null,
|
|
75
|
+
priorityOutputTokenPrice: null,
|
|
76
|
+
sessionRuntimePricePerHour: null,
|
|
77
|
+
source: "default_local",
|
|
78
|
+
confidence: "low",
|
|
79
|
+
caveat: "Default local pricing — may not match actual API billing. Verify with your Anthropic billing dashboard.",
|
|
80
|
+
effectiveDate: "2025-01-01",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
provider: "anthropic",
|
|
84
|
+
model: "claude-opus-4-7",
|
|
85
|
+
modelAlias: "claude-opus-4.7",
|
|
86
|
+
currency: "USD",
|
|
87
|
+
unit: "per_1m_tokens",
|
|
88
|
+
inputTokenPrice: 15.0,
|
|
89
|
+
cachedInputTokenPrice: 1.50,
|
|
90
|
+
outputTokenPrice: 75.0,
|
|
91
|
+
cacheWriteTokenPrice: 18.75,
|
|
92
|
+
cacheReadTokenPrice: 1.50,
|
|
93
|
+
batchInputTokenPrice: 7.50,
|
|
94
|
+
batchOutputTokenPrice: 37.50,
|
|
95
|
+
priorityInputTokenPrice: null,
|
|
96
|
+
priorityCachedInputTokenPrice: null,
|
|
97
|
+
priorityOutputTokenPrice: null,
|
|
98
|
+
sessionRuntimePricePerHour: null,
|
|
99
|
+
source: "default_local",
|
|
100
|
+
confidence: "low",
|
|
101
|
+
caveat: "Default local pricing — may not match actual API billing. Verify with your Anthropic billing dashboard.",
|
|
102
|
+
effectiveDate: "2025-01-01",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
provider: "openai",
|
|
106
|
+
model: "gpt-4o",
|
|
107
|
+
modelAlias: "gpt-4o",
|
|
108
|
+
currency: "USD",
|
|
109
|
+
unit: "per_1m_tokens",
|
|
110
|
+
inputTokenPrice: 2.50,
|
|
111
|
+
cachedInputTokenPrice: 1.25,
|
|
112
|
+
outputTokenPrice: 10.0,
|
|
113
|
+
cacheWriteTokenPrice: null,
|
|
114
|
+
cacheReadTokenPrice: 1.25,
|
|
115
|
+
batchInputTokenPrice: 1.25,
|
|
116
|
+
batchOutputTokenPrice: 5.0,
|
|
117
|
+
priorityInputTokenPrice: null,
|
|
118
|
+
priorityCachedInputTokenPrice: null,
|
|
119
|
+
priorityOutputTokenPrice: null,
|
|
120
|
+
sessionRuntimePricePerHour: null,
|
|
121
|
+
source: "default_local",
|
|
122
|
+
confidence: "low",
|
|
123
|
+
caveat: "Default local pricing — may not match actual API billing. Verify with your OpenAI usage dashboard.",
|
|
124
|
+
effectiveDate: "2025-01-01",
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
// ── Build default config ───────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
function buildDefaultLocalPricingConfig() {
|
|
131
|
+
return {
|
|
132
|
+
schemaVersion: SCHEMA_VERSION,
|
|
133
|
+
contract: CONTRACT,
|
|
134
|
+
pricingId: `lp-${Date.now()}-${shortId()}`,
|
|
135
|
+
createdAt: nowIso(),
|
|
136
|
+
updatedAt: nowIso(),
|
|
137
|
+
currency: "USD",
|
|
138
|
+
source: "default_local",
|
|
139
|
+
sourceName: "Avorelo default local pricing",
|
|
140
|
+
effectiveDate: "2025-01-01",
|
|
141
|
+
entries: JSON.parse(JSON.stringify(DEFAULT_PRICING_ENTRIES)),
|
|
142
|
+
redacted: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Init ───────────────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
function initLocalPricing(cwd) {
|
|
149
|
+
const configPath = path.resolve(cwd, LOCAL_PRICING_CONFIG_REL);
|
|
150
|
+
if (fs.existsSync(configPath)) {
|
|
151
|
+
return { created: false, path: configPath, message: "Local pricing config already exists." };
|
|
152
|
+
}
|
|
153
|
+
const config = buildDefaultLocalPricingConfig();
|
|
154
|
+
ensureDirFor(configPath);
|
|
155
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
156
|
+
return { created: true, path: configPath, message: "Local pricing config created with default entries." };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Validate ───────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
function validateLocalPricing(config) {
|
|
162
|
+
const errors = [];
|
|
163
|
+
if (!config) { errors.push("Config is null or undefined"); return { valid: false, errors }; }
|
|
164
|
+
if (config.contract !== CONTRACT) errors.push(`contract must be ${CONTRACT}`);
|
|
165
|
+
if (config.schemaVersion !== SCHEMA_VERSION) errors.push(`schemaVersion must be ${SCHEMA_VERSION}`);
|
|
166
|
+
if (!config.entries || !Array.isArray(config.entries)) errors.push("entries must be an array");
|
|
167
|
+
else {
|
|
168
|
+
config.entries.forEach((e, i) => {
|
|
169
|
+
if (!e.provider) errors.push(`entries[${i}].provider required`);
|
|
170
|
+
if (!e.model) errors.push(`entries[${i}].model required`);
|
|
171
|
+
if (!e.unit) errors.push(`entries[${i}].unit required`);
|
|
172
|
+
if (!e.confidence) errors.push(`entries[${i}].confidence required`);
|
|
173
|
+
if (!e.effectiveDate) errors.push(`entries[${i}].effectiveDate required`);
|
|
174
|
+
if (!e.caveat) errors.push(`entries[${i}].caveat required`);
|
|
175
|
+
if (e.source === "default_local" && e.confidence === "high")
|
|
176
|
+
errors.push(`entries[${i}]: default_local source cannot have confidence high`);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (!config.source) errors.push("source required");
|
|
180
|
+
if (!config.effectiveDate) errors.push("effectiveDate required");
|
|
181
|
+
return { valid: errors.length === 0, errors };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Read ───────────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
function readLocalPricing(cwd) {
|
|
187
|
+
const configPath = path.resolve(cwd, LOCAL_PRICING_CONFIG_REL);
|
|
188
|
+
if (!fs.existsSync(configPath)) return null;
|
|
189
|
+
try {
|
|
190
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
191
|
+
return JSON.parse(raw);
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Get pricing source for a provider/model ───────────────────────────────────
|
|
198
|
+
|
|
199
|
+
function getLocalPricingEntry(cwd, provider, model) {
|
|
200
|
+
const config = readLocalPricing(cwd);
|
|
201
|
+
if (!config || !config.entries) return null;
|
|
202
|
+
|
|
203
|
+
const normalizedModel = (model || "").toLowerCase().replace(/[\s.]/g, "-");
|
|
204
|
+
const entry = config.entries.find((e) => {
|
|
205
|
+
const eModel = (e.model || "").toLowerCase().replace(/[\s.]/g, "-");
|
|
206
|
+
const eAlias = (e.modelAlias || "").toLowerCase().replace(/[\s.]/g, "-");
|
|
207
|
+
const eProvider = (e.provider || "").toLowerCase();
|
|
208
|
+
const matchProvider = !provider || eProvider === (provider || "").toLowerCase();
|
|
209
|
+
return matchProvider && (eModel === normalizedModel || eAlias === normalizedModel || normalizedModel.startsWith(eModel) || eModel.startsWith(normalizedModel));
|
|
210
|
+
});
|
|
211
|
+
return entry || null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Get pricing source summary ────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
function getLocalPricingSource(cwd) {
|
|
217
|
+
const config = readLocalPricing(cwd);
|
|
218
|
+
if (!config) {
|
|
219
|
+
return {
|
|
220
|
+
available: false,
|
|
221
|
+
source: "unavailable",
|
|
222
|
+
entryCount: 0,
|
|
223
|
+
confidence: "unavailable",
|
|
224
|
+
effectiveDate: null,
|
|
225
|
+
caveat: "No local pricing config found. Run `avorelo pricing --init` to create one.",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const validation = validateLocalPricing(config);
|
|
229
|
+
if (!validation.valid) {
|
|
230
|
+
return {
|
|
231
|
+
available: false,
|
|
232
|
+
source: "invalid",
|
|
233
|
+
entryCount: 0,
|
|
234
|
+
confidence: "unavailable",
|
|
235
|
+
effectiveDate: null,
|
|
236
|
+
caveat: `Local pricing config is invalid: ${validation.errors.join("; ")}`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
available: true,
|
|
241
|
+
source: config.source,
|
|
242
|
+
sourceName: config.sourceName,
|
|
243
|
+
entryCount: config.entries.length,
|
|
244
|
+
confidence: config.source === "default_local" ? "low" : "medium",
|
|
245
|
+
effectiveDate: config.effectiveDate,
|
|
246
|
+
caveat: config.source === "default_local"
|
|
247
|
+
? "Default local pricing in use — confidence low. Verify against actual billing."
|
|
248
|
+
: `Local pricing from: ${config.sourceName || config.source}`,
|
|
249
|
+
pricingId: config.pricingId,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Text format ────────────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
function formatPricingText(cwd) {
|
|
256
|
+
const source = getLocalPricingSource(cwd);
|
|
257
|
+
if (!source.available) {
|
|
258
|
+
return [
|
|
259
|
+
"Avorelo Local Pricing",
|
|
260
|
+
"",
|
|
261
|
+
" Status: unavailable",
|
|
262
|
+
` ${source.caveat}`,
|
|
263
|
+
"",
|
|
264
|
+
" To initialize: node bin/avorelo pricing --init",
|
|
265
|
+
"",
|
|
266
|
+
].join("\n");
|
|
267
|
+
}
|
|
268
|
+
const config = readLocalPricing(cwd);
|
|
269
|
+
const lines = [
|
|
270
|
+
"Avorelo Local Pricing",
|
|
271
|
+
"",
|
|
272
|
+
` Source: ${source.source}`,
|
|
273
|
+
` Confidence: ${source.confidence}`,
|
|
274
|
+
` Effective: ${source.effectiveDate}`,
|
|
275
|
+
` Entries: ${source.entryCount}`,
|
|
276
|
+
` Caveat: ${source.caveat}`,
|
|
277
|
+
"",
|
|
278
|
+
" Models:",
|
|
279
|
+
];
|
|
280
|
+
(config.entries || []).forEach((e) => {
|
|
281
|
+
lines.push(` ${e.provider}/${e.model}: input $${e.inputTokenPrice}/1M, output $${e.outputTokenPrice}/1M (${e.confidence})`);
|
|
282
|
+
});
|
|
283
|
+
lines.push("");
|
|
284
|
+
lines.push(" Note: Cost estimates use this config. Update entries to match your actual pricing.");
|
|
285
|
+
lines.push("");
|
|
286
|
+
return lines.join("\n");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = {
|
|
290
|
+
CONTRACT,
|
|
291
|
+
LOCAL_PRICING_CONFIG_REL,
|
|
292
|
+
buildDefaultLocalPricingConfig,
|
|
293
|
+
initLocalPricing,
|
|
294
|
+
validateLocalPricing,
|
|
295
|
+
readLocalPricing,
|
|
296
|
+
getLocalPricingEntry,
|
|
297
|
+
getLocalPricingSource,
|
|
298
|
+
formatPricingText,
|
|
299
|
+
};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// ── MCP PreToolUse Enforcement ────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Contract: avorelo.mcpPreToolUseDecision.v1
|
|
6
|
+
//
|
|
7
|
+
// Governs MCP tool use at the PreToolUse hook boundary.
|
|
8
|
+
// Does NOT execute tools. Does NOT leak raw input/output.
|
|
9
|
+
// Always provides safeNextAction when blocking.
|
|
10
|
+
|
|
11
|
+
const fs = require("node:fs");
|
|
12
|
+
const path = require("node:path");
|
|
13
|
+
const { buildMcpToolInventory } = require("./mcp-tool-inventory");
|
|
14
|
+
const { buildMcpToolRiskReport, REASON_CODES } = require("./mcp-tool-risk");
|
|
15
|
+
const { buildMcpLeastPrivilegePolicy, DECISIONS } = require("./mcp-least-privilege-policy");
|
|
16
|
+
|
|
17
|
+
const CONTRACT = "avorelo.mcpPreToolUseDecision.v1";
|
|
18
|
+
const SCHEMA_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
const ENFORCEMENT_DIR_REL = ".claude/cco/orchestration/mcp-tool-governance";
|
|
21
|
+
const DECISION_REL = `${ENFORCEMENT_DIR_REL}/latest-pretooluse-decision.json`;
|
|
22
|
+
|
|
23
|
+
// MCP tool name prefixes that identify MCP calls
|
|
24
|
+
const MCP_TOOL_PREFIXES = ["mcp__", "mcp_", "mcp:"];
|
|
25
|
+
|
|
26
|
+
// Patterns suggesting a tool is destructive/deploy/prod at the call level
|
|
27
|
+
const CALL_LEVEL_DESTRUCTIVE = [
|
|
28
|
+
/\b(delete|remove|drop|truncate|purge|wipe|destroy)\b/i,
|
|
29
|
+
/\brm\s+-rf\b/i,
|
|
30
|
+
/\b(deploy|publish|push.to.prod|ship.to.prod)\b/i,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
function nowIso() {
|
|
36
|
+
return new Date().toISOString();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function safeWriteJson(filePath, data) {
|
|
40
|
+
try {
|
|
41
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
42
|
+
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
43
|
+
} catch {
|
|
44
|
+
// non-fatal
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function safeReadJson(absPath) {
|
|
49
|
+
try {
|
|
50
|
+
if (!fs.existsSync(absPath)) return null;
|
|
51
|
+
return JSON.parse(fs.readFileSync(absPath, "utf8").replace(/^/, ""));
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function makeDecisionId() {
|
|
58
|
+
return `mcp-dec-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Tool signal extraction ─────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function extractToolUseSignal(input, options = {}) {
|
|
64
|
+
if (!input || typeof input !== "object") {
|
|
65
|
+
return { toolName: null, isMcpTool: false, serverName: null, actionSummary: null };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const toolName = String(input.tool_name || input.toolName || input.tool || "");
|
|
69
|
+
if (!toolName) {
|
|
70
|
+
return { toolName: null, isMcpTool: false, serverName: null, actionSummary: null };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const isMcpTool = MCP_TOOL_PREFIXES.some((prefix) => toolName.toLowerCase().startsWith(prefix));
|
|
74
|
+
|
|
75
|
+
// Extract server name from MCP tool naming convention: mcp__ServerName__toolName
|
|
76
|
+
let serverName = null;
|
|
77
|
+
if (isMcpTool) {
|
|
78
|
+
const parts = toolName.split(/__+/);
|
|
79
|
+
if (parts.length >= 3) {
|
|
80
|
+
serverName = parts[1];
|
|
81
|
+
} else if (parts.length === 2) {
|
|
82
|
+
serverName = parts[1];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Summarize action without leaking raw input values (keys only)
|
|
87
|
+
const toolInput = input.tool_input || input.toolInput || input.input || {};
|
|
88
|
+
const inputKeys = Object.keys(toolInput || {}).slice(0, 8);
|
|
89
|
+
const actionSummary = inputKeys.length > 0
|
|
90
|
+
? `tool: ${toolName}, input_keys: [${inputKeys.join(", ")}]`
|
|
91
|
+
: `tool: ${toolName}`;
|
|
92
|
+
|
|
93
|
+
return { toolName, isMcpTool, serverName, actionSummary };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Policy lookup ─────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function applyMcpPolicyToToolUse(cwd, signal, policy, options = {}) {
|
|
99
|
+
if (!signal || !signal.toolName) {
|
|
100
|
+
return {
|
|
101
|
+
decision: DECISIONS.ALLOW,
|
|
102
|
+
matchedPolicy: null,
|
|
103
|
+
reasonCodes: ["NO_TOOL_SIGNAL"],
|
|
104
|
+
safeNextAction: null,
|
|
105
|
+
taskStillExecutable: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!policy || !policy.decisions || policy.decisions.length === 0) {
|
|
110
|
+
// No policy = no inventory = no MCP servers configured
|
|
111
|
+
if (!signal.isMcpTool) {
|
|
112
|
+
return {
|
|
113
|
+
decision: DECISIONS.ALLOW,
|
|
114
|
+
matchedPolicy: null,
|
|
115
|
+
reasonCodes: ["NOT_AN_MCP_TOOL"],
|
|
116
|
+
safeNextAction: null,
|
|
117
|
+
taskStillExecutable: true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// Unknown MCP tool with no policy → approval_required
|
|
121
|
+
return {
|
|
122
|
+
decision: DECISIONS.APPROVAL_REQUIRED,
|
|
123
|
+
matchedPolicy: null,
|
|
124
|
+
reasonCodes: [REASON_CODES.UNKNOWN_SERVER, "NO_POLICY_AVAILABLE"],
|
|
125
|
+
safeNextAction: "Run `avorelo mcp inventory` to discover and classify this tool before use.",
|
|
126
|
+
taskStillExecutable: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Find matching policy by server name or tool name
|
|
131
|
+
const toolNameLower = signal.toolName.toLowerCase();
|
|
132
|
+
const serverNameLower = (signal.serverName || "").toLowerCase();
|
|
133
|
+
|
|
134
|
+
let matched = policy.decisions.find((d) => {
|
|
135
|
+
const dn = (d.name || "").toLowerCase();
|
|
136
|
+
const ds = (d.serverId || "").toLowerCase();
|
|
137
|
+
return dn === toolNameLower || ds.includes(serverNameLower) || toolNameLower.includes(dn);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!matched) {
|
|
141
|
+
// Unknown tool not in policy → approval_required
|
|
142
|
+
return {
|
|
143
|
+
decision: DECISIONS.APPROVAL_REQUIRED,
|
|
144
|
+
matchedPolicy: null,
|
|
145
|
+
reasonCodes: [REASON_CODES.UNKNOWN_SERVER, "TOOL_NOT_IN_POLICY"],
|
|
146
|
+
safeNextAction: "This tool was not found in the MCP inventory. Run `avorelo mcp inventory` and review before use.",
|
|
147
|
+
taskStillExecutable: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
decision: matched.decision,
|
|
153
|
+
matchedPolicy: {
|
|
154
|
+
toolId: matched.toolId,
|
|
155
|
+
name: matched.name,
|
|
156
|
+
riskLevel: matched.riskLevel,
|
|
157
|
+
},
|
|
158
|
+
reasonCodes: matched.reasonCodes || [],
|
|
159
|
+
safeNextAction: matched.safeNextAction || null,
|
|
160
|
+
taskStillExecutable: matched.taskStillExecutable !== false,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Call-level safety check ───────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
function checkCallLevelSafety(signal) {
|
|
167
|
+
if (!signal || !signal.toolName) return null;
|
|
168
|
+
const toolText = signal.toolName + " " + (signal.actionSummary || "");
|
|
169
|
+
for (const pattern of CALL_LEVEL_DESTRUCTIVE) {
|
|
170
|
+
if (pattern.test(toolText)) {
|
|
171
|
+
return {
|
|
172
|
+
decision: DECISIONS.BLOCK,
|
|
173
|
+
reasonCodes: [REASON_CODES.DESTRUCTIVE_ACTION, "CALL_LEVEL_DESTRUCTIVE_PATTERN"],
|
|
174
|
+
safeNextAction: "This tool call matches a destructive/deploy pattern. Use a scoped safe tool or get explicit approval.",
|
|
175
|
+
taskStillExecutable: false,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Main decision function ────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
function decideMcpPreToolUse(cwd, input, options = {}) {
|
|
185
|
+
const decisionId = makeDecisionId();
|
|
186
|
+
const signal = extractToolUseSignal(input, options);
|
|
187
|
+
|
|
188
|
+
const baseDecision = {
|
|
189
|
+
contract: CONTRACT,
|
|
190
|
+
schemaVersion: SCHEMA_VERSION,
|
|
191
|
+
decisionId,
|
|
192
|
+
createdAt: nowIso(),
|
|
193
|
+
decision: DECISIONS.ALLOW,
|
|
194
|
+
toolSignal: {
|
|
195
|
+
toolName: signal.toolName,
|
|
196
|
+
isMcpTool: signal.isMcpTool,
|
|
197
|
+
serverName: signal.serverName,
|
|
198
|
+
actionSummary: signal.actionSummary,
|
|
199
|
+
},
|
|
200
|
+
matchedPolicy: null,
|
|
201
|
+
reasonCodes: [],
|
|
202
|
+
safeNextAction: null,
|
|
203
|
+
taskStillExecutable: true,
|
|
204
|
+
evidenceRefs: [],
|
|
205
|
+
redacted: true,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// If no tool signal, allow (don't block non-MCP tools)
|
|
209
|
+
if (!signal.toolName) {
|
|
210
|
+
baseDecision.decision = DECISIONS.ALLOW;
|
|
211
|
+
baseDecision.reasonCodes = ["NO_TOOL_SIGNAL"];
|
|
212
|
+
return baseDecision;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Non-MCP tool: check call-level safety, then allow
|
|
216
|
+
if (!signal.isMcpTool) {
|
|
217
|
+
const callCheck = checkCallLevelSafety(signal);
|
|
218
|
+
if (callCheck) {
|
|
219
|
+
return { ...baseDecision, ...callCheck };
|
|
220
|
+
}
|
|
221
|
+
baseDecision.decision = DECISIONS.ALLOW;
|
|
222
|
+
baseDecision.reasonCodes = ["NOT_AN_MCP_TOOL"];
|
|
223
|
+
return baseDecision;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// MCP tool: call-level safety check first
|
|
227
|
+
const callCheck = checkCallLevelSafety(signal);
|
|
228
|
+
if (callCheck) {
|
|
229
|
+
return { ...baseDecision, ...callCheck };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Load existing policy (if available)
|
|
233
|
+
let policy = null;
|
|
234
|
+
try {
|
|
235
|
+
const policyPath = path.join(cwd, ".claude/cco/orchestration/mcp-tool-governance/latest-policy.json");
|
|
236
|
+
policy = safeReadJson(policyPath);
|
|
237
|
+
} catch {
|
|
238
|
+
// non-fatal — will build fresh
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// If no cached policy, build one now
|
|
242
|
+
if (!policy) {
|
|
243
|
+
try {
|
|
244
|
+
const inventory = buildMcpToolInventory(cwd, options);
|
|
245
|
+
const riskReport = buildMcpToolRiskReport(cwd, inventory, options);
|
|
246
|
+
policy = buildMcpLeastPrivilegePolicy(cwd, riskReport, options);
|
|
247
|
+
} catch {
|
|
248
|
+
// If we can't build policy, be conservative
|
|
249
|
+
const result = { ...baseDecision };
|
|
250
|
+
result.decision = DECISIONS.APPROVAL_REQUIRED;
|
|
251
|
+
result.reasonCodes = [REASON_CODES.UNKNOWN_SERVER, "POLICY_BUILD_FAILED"];
|
|
252
|
+
result.safeNextAction = "Cannot evaluate MCP tool risk. Run `avorelo mcp inventory` before proceeding.";
|
|
253
|
+
result.taskStillExecutable = true;
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const policyResult = applyMcpPolicyToToolUse(cwd, signal, policy, options);
|
|
259
|
+
const result = {
|
|
260
|
+
...baseDecision,
|
|
261
|
+
decision: policyResult.decision,
|
|
262
|
+
matchedPolicy: policyResult.matchedPolicy,
|
|
263
|
+
reasonCodes: policyResult.reasonCodes,
|
|
264
|
+
safeNextAction: policyResult.safeNextAction,
|
|
265
|
+
taskStillExecutable: policyResult.taskStillExecutable !== false,
|
|
266
|
+
evidenceRefs: [".claude/cco/orchestration/mcp-tool-governance/latest-policy.json"],
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Blocked decisions MUST have safeNextAction
|
|
270
|
+
if (result.decision === DECISIONS.BLOCK && !result.safeNextAction) {
|
|
271
|
+
result.safeNextAction = "This MCP tool is blocked. Use a scoped alternative or get explicit operator approval.";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── Write decision ────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
function writeMcpPreToolUseDecision(cwd, decision) {
|
|
280
|
+
const absPath = path.join(cwd, DECISION_REL);
|
|
281
|
+
safeWriteJson(absPath, decision);
|
|
282
|
+
return DECISION_REL;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function formatMcpPreToolUseText(decision, options = {}) {
|
|
286
|
+
const d = decision.decision || "unknown";
|
|
287
|
+
const tool = decision.toolSignal?.toolName || "unknown";
|
|
288
|
+
const lines = [
|
|
289
|
+
`MCP PreToolUse: ${tool} → ${d.toUpperCase()}`,
|
|
290
|
+
];
|
|
291
|
+
if (decision.reasonCodes && decision.reasonCodes.length > 0) {
|
|
292
|
+
lines.push(`Reason: ${decision.reasonCodes.slice(0, 4).join(", ")}`);
|
|
293
|
+
}
|
|
294
|
+
if (decision.safeNextAction) {
|
|
295
|
+
lines.push(`Next: ${decision.safeNextAction}`);
|
|
296
|
+
}
|
|
297
|
+
return lines.join("\n");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── Exports ───────────────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
module.exports = {
|
|
303
|
+
CONTRACT,
|
|
304
|
+
SCHEMA_VERSION,
|
|
305
|
+
DECISION_REL,
|
|
306
|
+
decideMcpPreToolUse,
|
|
307
|
+
extractToolUseSignal,
|
|
308
|
+
applyMcpPolicyToToolUse,
|
|
309
|
+
writeMcpPreToolUseDecision,
|
|
310
|
+
formatMcpPreToolUseText,
|
|
311
|
+
};
|