@event4u/agent-config 14.18.0 → 14.20.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +120 -129
- package/README.md +71 -149
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +44 -43
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +37 -4
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +14 -1
- package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli/mcp/dispatch.js +10 -0
- package/dist/cli/mcp/stdio.js +33 -2
- package/dist/cli/mcp/telemetry.js +227 -0
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-C6IPZRDA.js} +484 -353
- package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-NUMQHLS5.js} +304 -136
- package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-PI2GU3DP.js} +163 -1
- package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
- package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +489 -121
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +42 -12
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +59 -902
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +34 -33
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +4455 -3025
- package/dist/install/install.mjs +975 -689
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2087 -1134
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/scripts/mcp_server/telemetry.js +148 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/naming.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
- package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/docs/guidelines/redundancy-taxonomy.md +28 -0
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/src/config/assurance-capability-registry.json +9 -0
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/gate-coverage.yml +225 -0
- package/src/config/gate-violation-baselines.json +18 -6
- package/src/config/release-gate-locality.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/audit_field_provenance.ts +44 -3
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_capability.ts +38 -0
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/install_reach_checks.ts +33 -1
- package/src/scripts/_lib/mcp_bridge.ts +222 -10
- package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/orchestration_record.ts +23 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
- package/src/scripts/_lib/release_highlights.ts +93 -0
- package/src/scripts/_lib/release_material.ts +64 -1
- package/src/scripts/_lib/release_position.ts +96 -0
- package/src/scripts/_lib/review_skipped_record.ts +17 -1
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/security_lint.ts +179 -7
- package/src/scripts/_lib/self_repair.ts +141 -6
- package/src/scripts/_lib/self_repair_store.ts +273 -7
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/subagent_capsule.ts +155 -3
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/build_rule_trigger_matrix.ts +5 -0
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +66 -9
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_mcp_doc_drift.ts +217 -0
- package/src/scripts/check_pack_size.ts +321 -3
- package/src/scripts/check_release_highlights.ts +74 -1
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/conformance_scan.ts +44 -3
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +75 -1
- package/src/scripts/handoff_context_hook.ts +54 -7
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +14 -11
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/git_command_classifier.ts +156 -1
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/session_eol_hook.ts +18 -7
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install.ts +57 -86
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/lint_agent_security.ts +316 -25
- package/src/scripts/lint_confusables.ts +42 -6
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_handoffs.ts +0 -132
- package/src/scripts/lint_hidden_unicode.ts +49 -7
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_instruction_smuggling.ts +42 -6
- package/src/scripts/lint_mcp_config_security.ts +42 -6
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
- package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/mcp_server/resources.ts +41 -0
- package/src/scripts/mcp_server/telemetry.ts +19 -4
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +24 -24
- package/src/scripts/release_publication.ts +6 -2
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +5 -0
- package/src/scripts/schemas/skill.schema.json +2 -2
- package/src/scripts/self_repair_cli.ts +31 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/skill_scout.ts +69 -0
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
- package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host_permission_checks — report the host permission settings that produce
|
|
3
|
+
* confirmation prompts, and write none of them.
|
|
4
|
+
*
|
|
5
|
+
* WHY REPORT AND NOT WRITE. Three settings decide how often a normal coding
|
|
6
|
+
* task is interrupted, and all three belong to the consumer:
|
|
7
|
+
* `permissions.deny` rules that cover reads, the working tree's absence from
|
|
8
|
+
* `permissions.additionalDirectories`, and `permissions.defaultMode`. Writing
|
|
9
|
+
* any of them would be this package deciding a consumer's permission posture
|
|
10
|
+
* from inside their repository, which is the one thing a permission surface
|
|
11
|
+
* must never do quietly. `src/templates/consumer-settings/claude-settings.json`
|
|
12
|
+
* carries `enabledPlugins` and `hooks` and zero `permissions` keys; this check
|
|
13
|
+
* exists so that stays true and the consumer still gets the answer.
|
|
14
|
+
*
|
|
15
|
+
* WHY IT NAMES ALL THREE EVEN WHEN ALL THREE ARE ABSENT. Absent is a real
|
|
16
|
+
* answer and the most common one. A check that goes silent on the default
|
|
17
|
+
* project reports "nothing found" and "I did not look" identically, which is
|
|
18
|
+
* the measured-zero-versus-not-measured failure this tree's own report
|
|
19
|
+
* contracts argue against. So each setting is named on every run, with its
|
|
20
|
+
* resolved value or the word absent.
|
|
21
|
+
*
|
|
22
|
+
* READ ONLY, AND FAILURE-TOLERANT. An unreadable or malformed settings file is
|
|
23
|
+
* reported, never repaired: a consumer's JSON is theirs, and a doctor that
|
|
24
|
+
* rewrites what it came to inspect is not a doctor.
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from "node:fs";
|
|
27
|
+
import * as os from "node:os";
|
|
28
|
+
import * as path from "node:path";
|
|
29
|
+
|
|
30
|
+
import type { WiringCheck } from "./runtime_wiring_checks.js";
|
|
31
|
+
|
|
32
|
+
/** Injected so a test never reads the developer's own home directory. */
|
|
33
|
+
export interface HostPermissionDeps {
|
|
34
|
+
projectRoot: string;
|
|
35
|
+
homeDir: string;
|
|
36
|
+
readFile: (p: string) => string | null;
|
|
37
|
+
isDirectory: (p: string) => boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The permission block as the host writes it; every field optional. */
|
|
41
|
+
interface PermissionBlock {
|
|
42
|
+
allow?: unknown;
|
|
43
|
+
deny?: unknown;
|
|
44
|
+
ask?: unknown;
|
|
45
|
+
additionalDirectories?: unknown;
|
|
46
|
+
defaultMode?: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One settings layer, in the host's own precedence order. */
|
|
50
|
+
export interface SettingsLayer {
|
|
51
|
+
label: string;
|
|
52
|
+
file: string;
|
|
53
|
+
present: boolean;
|
|
54
|
+
unreadable: boolean;
|
|
55
|
+
permissions: PermissionBlock;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Tool names whose every documented effect is a read, plus the read-shaped
|
|
60
|
+
* `Bash(<cmd>:*)` heads. A deny covering one of these is the class that turns a
|
|
61
|
+
* category-A call back into a prompt, which is why it is worth naming
|
|
62
|
+
* separately from a deny on a write.
|
|
63
|
+
*/
|
|
64
|
+
const READ_SHAPED = [
|
|
65
|
+
"Read", "Glob", "Grep", "LS", "NotebookRead",
|
|
66
|
+
"Bash(cat", "Bash(ls", "Bash(head", "Bash(tail", "Bash(grep",
|
|
67
|
+
"Bash(rg", "Bash(find", "Bash(wc", "Bash(git status", "Bash(git log",
|
|
68
|
+
"Bash(git diff", "Bash(git show",
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/** The four `defaultMode` values probed from Claude Code 2.1.263. */
|
|
72
|
+
const KNOWN_MODES = ["default", "acceptEdits", "plan", "bypassPermissions"];
|
|
73
|
+
|
|
74
|
+
function _strings(value: unknown): string[] {
|
|
75
|
+
return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Default deps — the only place the real filesystem is touched. */
|
|
79
|
+
export function defaultHostPermissionDeps(projectRoot: string): HostPermissionDeps {
|
|
80
|
+
return {
|
|
81
|
+
projectRoot,
|
|
82
|
+
homeDir: os.homedir(),
|
|
83
|
+
readFile: (p) => {
|
|
84
|
+
try {
|
|
85
|
+
return fs.readFileSync(p, "utf8");
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
isDirectory: (p) => {
|
|
91
|
+
try {
|
|
92
|
+
return fs.statSync(p).isDirectory();
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the three settings layers the host reads, in its precedence order.
|
|
102
|
+
*
|
|
103
|
+
* There was no resolver for this before — `security_audit_config.ts` is the
|
|
104
|
+
* only place all three paths appear together, and only as glob strings — so
|
|
105
|
+
* this is the first code in the tree that reads `permissions` at all.
|
|
106
|
+
*/
|
|
107
|
+
export function readSettingsLayers(deps: HostPermissionDeps): SettingsLayer[] {
|
|
108
|
+
const candidates: Array<[string, string]> = [
|
|
109
|
+
["user-global", path.join(deps.homeDir, ".claude", "settings.json")],
|
|
110
|
+
["project", path.join(deps.projectRoot, ".claude", "settings.json")],
|
|
111
|
+
["project-local", path.join(deps.projectRoot, ".claude", "settings.local.json")],
|
|
112
|
+
];
|
|
113
|
+
return candidates.map(([label, file]) => {
|
|
114
|
+
const raw = deps.readFile(file);
|
|
115
|
+
if (raw === null) {
|
|
116
|
+
return { label, file, present: false, unreadable: false, permissions: {} };
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
120
|
+
const perms = parsed["permissions"];
|
|
121
|
+
return {
|
|
122
|
+
label,
|
|
123
|
+
file,
|
|
124
|
+
present: true,
|
|
125
|
+
unreadable: false,
|
|
126
|
+
permissions:
|
|
127
|
+
perms !== null && typeof perms === "object" ? (perms as PermissionBlock) : {},
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return { label, file, present: true, unreadable: true, permissions: {} };
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Deny rules that cover a read, with the layer each came from. */
|
|
136
|
+
export function readDenyRules(layers: readonly SettingsLayer[]): Array<[string, string]> {
|
|
137
|
+
const hits: Array<[string, string]> = [];
|
|
138
|
+
for (const layer of layers) {
|
|
139
|
+
for (const rule of _strings(layer.permissions.deny)) {
|
|
140
|
+
if (READ_SHAPED.some((prefix) => rule.startsWith(prefix))) {
|
|
141
|
+
hits.push([layer.label, rule]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return hits;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Every `additionalDirectories` entry across the layers; the host unions them. */
|
|
149
|
+
export function additionalDirectories(layers: readonly SettingsLayer[]): string[] {
|
|
150
|
+
const seen: string[] = [];
|
|
151
|
+
for (const layer of layers) {
|
|
152
|
+
for (const dir of _strings(layer.permissions.additionalDirectories)) {
|
|
153
|
+
if (!seen.includes(dir)) seen.push(dir);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return seen;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The winning `defaultMode`, or `null` when no layer sets one. */
|
|
160
|
+
export function resolvedDefaultMode(layers: readonly SettingsLayer[]): string | null {
|
|
161
|
+
let winner: string | null = null;
|
|
162
|
+
for (const layer of layers) {
|
|
163
|
+
const mode = layer.permissions.defaultMode;
|
|
164
|
+
if (typeof mode === "string" && mode.trim()) winner = mode.trim();
|
|
165
|
+
}
|
|
166
|
+
return winner;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Is the project root a LINKED worktree?
|
|
171
|
+
*
|
|
172
|
+
* A linked worktree's `.git` is a file holding a `gitdir:` pointer, never a
|
|
173
|
+
* directory. It matters here because a worktree root is a working directory the
|
|
174
|
+
* host was not started in, which is exactly the case
|
|
175
|
+
* `additionalDirectories` exists to cover — and the case a consumer is least
|
|
176
|
+
* likely to have thought about.
|
|
177
|
+
*/
|
|
178
|
+
export function isLinkedWorktree(deps: HostPermissionDeps): boolean {
|
|
179
|
+
const dotGit = path.join(deps.projectRoot, ".git");
|
|
180
|
+
const raw = deps.readFile(dotGit);
|
|
181
|
+
if (raw === null) return false;
|
|
182
|
+
return !deps.isDirectory(dotGit) && raw.trimStart().startsWith("gitdir:");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The copyable snippet — the consumer pastes it, this package never writes it. */
|
|
186
|
+
export function permissionSnippet(projectRoot: string): string {
|
|
187
|
+
return JSON.stringify(
|
|
188
|
+
{ permissions: { additionalDirectories: [projectRoot], defaultMode: "default" } },
|
|
189
|
+
null,
|
|
190
|
+
2,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The doctor row.
|
|
196
|
+
*
|
|
197
|
+
* `warn` when something reportable was found, `ok` otherwise. Never `fail`:
|
|
198
|
+
* these are the consumer's own choices, and a doctor that fails a build over a
|
|
199
|
+
* setting it is forbidden to write would be asking for a change it will not
|
|
200
|
+
* make.
|
|
201
|
+
*/
|
|
202
|
+
export function checkHostPermissionSettings(deps: HostPermissionDeps): WiringCheck {
|
|
203
|
+
const layers = readSettingsLayers(deps);
|
|
204
|
+
const unreadable = layers.filter((l) => l.unreadable);
|
|
205
|
+
const denies = readDenyRules(layers);
|
|
206
|
+
const dirs = additionalDirectories(layers);
|
|
207
|
+
const mode = resolvedDefaultMode(layers);
|
|
208
|
+
const worktree = isLinkedWorktree(deps);
|
|
209
|
+
const rootListed = dirs.some(
|
|
210
|
+
(d) => path.resolve(d) === path.resolve(deps.projectRoot),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const lines: string[] = [];
|
|
214
|
+
lines.push(
|
|
215
|
+
denies.length === 0
|
|
216
|
+
? "read-deny rules: absent — no permissions.deny rule covers a read"
|
|
217
|
+
: `read-deny rules: ${denies.length} — ` +
|
|
218
|
+
denies.map(([layer, rule]) => `${rule} (${layer})`).join(", "),
|
|
219
|
+
);
|
|
220
|
+
lines.push(
|
|
221
|
+
rootListed
|
|
222
|
+
? `working tree in additionalDirectories: present (${deps.projectRoot})`
|
|
223
|
+
: `working tree in additionalDirectories: absent (${deps.projectRoot}` +
|
|
224
|
+
`${worktree ? ", a linked worktree" : ""})`,
|
|
225
|
+
);
|
|
226
|
+
lines.push(
|
|
227
|
+
mode === null
|
|
228
|
+
? "permissions.defaultMode: absent — the host's own built-in default applies"
|
|
229
|
+
: `permissions.defaultMode: ${mode}` +
|
|
230
|
+
(KNOWN_MODES.includes(mode) ? "" : " — not a value this tree has probed"),
|
|
231
|
+
);
|
|
232
|
+
for (const layer of unreadable) {
|
|
233
|
+
lines.push(`unreadable: ${layer.file} (${layer.label}) is not valid JSON`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// A linked worktree unlisted in additionalDirectories is the one finding
|
|
237
|
+
// worth surfacing on its own: the host was started somewhere else, so every
|
|
238
|
+
// call into this tree is a call into a directory it was not granted.
|
|
239
|
+
const reportable =
|
|
240
|
+
denies.length > 0 || unreadable.length > 0 || (worktree && !rootListed);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
id: "host-permission-settings",
|
|
244
|
+
status: reportable ? "warn" : "ok",
|
|
245
|
+
message: lines.join("; "),
|
|
246
|
+
remedy:
|
|
247
|
+
"agent-config never writes permissions — paste this into " +
|
|
248
|
+
`${path.join(deps.projectRoot, ".claude", "settings.local.json")} yourself if you want it: ` +
|
|
249
|
+
permissionSnippet(deps.projectRoot).replace(/\s+/g, " "),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
@@ -34,6 +34,7 @@ import * as fs from 'node:fs';
|
|
|
34
34
|
import * as path from 'node:path';
|
|
35
35
|
|
|
36
36
|
import type { WiringCheck } from './runtime_wiring_checks.js';
|
|
37
|
+
import { TOOL_TO_HOST, residualReport } from './mcp_consent_residual.js';
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Root agent-instruction files, as a multi-vendor ecosystem convention rather
|
|
@@ -477,11 +478,41 @@ export function checkOverrideSet(projectRoot: string): WiringCheck {
|
|
|
477
478
|
};
|
|
478
479
|
}
|
|
479
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Step 3.2 — what remains non-automatic per host, and where that came from.
|
|
483
|
+
*
|
|
484
|
+
* Belongs to reach rather than to a doctor-local check because that is what it
|
|
485
|
+
* measures: the gap between "installed" and "actually running". A residual is
|
|
486
|
+
* exactly the distance the install did not cover.
|
|
487
|
+
*
|
|
488
|
+
* Reports the residual, never a checklist of what worked — a list of successes
|
|
489
|
+
* is noise a reader has to filter, while a named residual is the one thing they
|
|
490
|
+
* can act on. An UNRECORDED host warns too, because "nobody checked" must not
|
|
491
|
+
* read as "nothing to do".
|
|
492
|
+
*/
|
|
493
|
+
export function checkMcpConsentResidual(): WiringCheck {
|
|
494
|
+
const id = 'mcp-consent-residual';
|
|
495
|
+
const lines = residualReport(Object.keys(TOOL_TO_HOST));
|
|
496
|
+
if (lines.length === 0) {
|
|
497
|
+
return { id, status: 'ok', message: 'no MCP consent residual recorded for any known host', remedy: '' };
|
|
498
|
+
}
|
|
499
|
+
const actionable = lines.filter((l) => l.state === 'residual');
|
|
500
|
+
return {
|
|
501
|
+
id,
|
|
502
|
+
status: 'warn',
|
|
503
|
+
message: lines.map((l) => l.text).join(' · '),
|
|
504
|
+
remedy:
|
|
505
|
+
actionable.length > 0
|
|
506
|
+
? 'complete the step named above on each host you use'
|
|
507
|
+
: 'record an observation for the unchecked hosts, or ignore if you do not use them',
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
480
511
|
// ---------------------------------------------------------------------------
|
|
481
512
|
// Wiring — the shape `cmd_doctor` consumes
|
|
482
513
|
// ---------------------------------------------------------------------------
|
|
483
514
|
|
|
484
|
-
export const REACH_CHECK_IDS = ['instruction-path-reach', 'version-axis', 'override-set'] as const;
|
|
515
|
+
export const REACH_CHECK_IDS = ['instruction-path-reach', 'version-axis', 'override-set', 'mcp-consent-residual'] as const;
|
|
485
516
|
|
|
486
517
|
export function reachRunners(opts: {
|
|
487
518
|
projectRoot: string;
|
|
@@ -491,5 +522,6 @@ export function reachRunners(opts: {
|
|
|
491
522
|
'instruction-path-reach': () => checkInstructionPaths(opts.projectRoot),
|
|
492
523
|
'version-axis': () => checkVersionAxis(opts.projectRoot, opts.resolvableVersion),
|
|
493
524
|
'override-set': () => checkOverrideSet(opts.projectRoot),
|
|
525
|
+
'mcp-consent-residual': () => checkMcpConsentResidual(),
|
|
494
526
|
};
|
|
495
527
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import * as fs from 'node:fs';
|
|
17
17
|
import * as path from 'node:path';
|
|
18
|
+
import { probeHostCapabilities } from './host_capability.js';
|
|
18
19
|
|
|
19
20
|
/** `install.ts`'s deploy tuple: [written, skipped, status, paths]. */
|
|
20
21
|
export type DeployTuple = [number, number, string, string[]];
|
|
@@ -35,14 +36,43 @@ export type MergeJsonFile = (
|
|
|
35
36
|
* installer end up with the same entry rather than two that drift.
|
|
36
37
|
*/
|
|
37
38
|
export const MCP_SERVER_KEY = 'agent-config';
|
|
38
|
-
export const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
export const MCP_PACKAGE_NAME = '@event4u/agent-config';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The entry, pinned to the version the installer is running from.
|
|
43
|
+
*
|
|
44
|
+
* Unpinned, `npx -y @event4u/agent-config` resolves the `latest` dist-tag on
|
|
45
|
+
* every server start, so the server a consumer runs is whatever the registry
|
|
46
|
+
* served most recently — not the one their installer approved, and not the one
|
|
47
|
+
* their lockfile records. The pin makes those the same artefact.
|
|
48
|
+
*
|
|
49
|
+
* A version that cannot be read is left unpinned rather than guessed: an
|
|
50
|
+
* invented specifier would fail to resolve at server start, which is a worse
|
|
51
|
+
* failure than the drift it was meant to close.
|
|
52
|
+
*/
|
|
53
|
+
export function mcpBridgeEntry(packageRoot: string): Record<string, unknown> {
|
|
54
|
+
const version = readPackageVersion(packageRoot);
|
|
55
|
+
const spec = version === null ? MCP_PACKAGE_NAME : `${MCP_PACKAGE_NAME}@${version}`;
|
|
56
|
+
return {
|
|
57
|
+
mcpServers: {
|
|
58
|
+
[MCP_SERVER_KEY]: {
|
|
59
|
+
command: 'npx',
|
|
60
|
+
args: ['-y', spec, 'mcp-server'],
|
|
61
|
+
},
|
|
43
62
|
},
|
|
44
|
-
}
|
|
45
|
-
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The manifest `version`, or `null` when it is absent or unreadable. */
|
|
67
|
+
export function readPackageVersion(packageRoot: string): string | null {
|
|
68
|
+
try {
|
|
69
|
+
const raw = fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8');
|
|
70
|
+
const v = (JSON.parse(raw) as { version?: unknown }).version;
|
|
71
|
+
return typeof v === 'string' && v.trim() !== '' ? v : null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
46
76
|
|
|
47
77
|
/**
|
|
48
78
|
* `.mcp.json` — the project-scope MCP config Claude Code reads.
|
|
@@ -66,9 +96,191 @@ export const MCP_BRIDGE_ENTRY = {
|
|
|
66
96
|
*/
|
|
67
97
|
export function makeEnsureMcpBridge(
|
|
68
98
|
mergeJsonFile: MergeJsonFile,
|
|
69
|
-
): (projectRoot: string, force: boolean) => Record<string, unknown>[] {
|
|
70
|
-
return (projectRoot, force) =>
|
|
71
|
-
mergeJsonFile(
|
|
99
|
+
): (projectRoot: string, force: boolean, packageRoot?: string) => Record<string, unknown>[] {
|
|
100
|
+
return (projectRoot, force, packageRoot = projectRoot) =>
|
|
101
|
+
mergeJsonFile(
|
|
102
|
+
path.join(projectRoot, '.mcp.json'),
|
|
103
|
+
mcpBridgeEntry(packageRoot),
|
|
104
|
+
force,
|
|
105
|
+
'.mcp.json',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Where each host reads a PROJECT-scope MCP config, and which capability-manifest
|
|
111
|
+
* row gates writing to it (roadmap step 3.3).
|
|
112
|
+
*
|
|
113
|
+
* Keyed by the installer's `--tools` id; `hostId` is the SEPARATE
|
|
114
|
+
* platform identifier the capability manifest is keyed by (`claude`, not
|
|
115
|
+
* `claude-code`). Conflating the two namespaces is the mistake
|
|
116
|
+
* `host_capability.ts` warns about in its own registry doc, so the mapping is
|
|
117
|
+
* written out rather than derived.
|
|
118
|
+
*
|
|
119
|
+
* Presence in this table is NOT permission to write. It records where the file
|
|
120
|
+
* would go IF the axis said the host reads one; the axis decides, and it says
|
|
121
|
+
* `true` only once someone observed it.
|
|
122
|
+
*/
|
|
123
|
+
export const MCP_PROJECT_CONFIG: Readonly<Record<string, { hostId: string; relPath: string }>> = {
|
|
124
|
+
'claude-code': { hostId: 'claude', relPath: '.mcp.json' },
|
|
125
|
+
cursor: { hostId: 'cursor', relPath: '.cursor/mcp.json' },
|
|
126
|
+
'gemini-cli': { hostId: 'gemini', relPath: '.gemini/settings.json' },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** One host to register, resolved from the `--tools` set and the capability axis. */
|
|
130
|
+
export interface McpRegistrationTarget {
|
|
131
|
+
toolId: string;
|
|
132
|
+
hostId: string;
|
|
133
|
+
relPath: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Injectable capability reader — the installer passes the real probe. */
|
|
137
|
+
export type ReadsProjectMcpConfig = (hostId: string) => boolean;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The hosts an install may register the server on, beyond `claude-code`.
|
|
141
|
+
*
|
|
142
|
+
* TWO gates, both required. `--tools` is the consumer's selection; the
|
|
143
|
+
* capability axis is the evidence. A host is registered only where BOTH say so,
|
|
144
|
+
* so a config file this package already writes for hooks does not silently
|
|
145
|
+
* acquire an MCP entry on the strength of the file existing.
|
|
146
|
+
*
|
|
147
|
+
* `claude-code` is excluded here on purpose: its `.mcp.json` registration
|
|
148
|
+
* predates the axis and is written unconditionally by `makeEnsureMcpBridge`.
|
|
149
|
+
* Routing it through the axis would DELETE a working registration, because the
|
|
150
|
+
* axis reports `false` for every host today — "nobody answered", not "checked
|
|
151
|
+
* and absent". This function adds reach; it never removes any.
|
|
152
|
+
*
|
|
153
|
+
* MEASURED STATE, 2026-09-07: the capability registry carries one row
|
|
154
|
+
* (`claude`) and it does not set `reads_project_mcp_config`, so this returns an
|
|
155
|
+
* EMPTY list for every `--tools` set. That is the honest reading of the axis,
|
|
156
|
+
* not a defect in this function — and it is a recorded absence, never a finding
|
|
157
|
+
* that these hosts do not read such a file.
|
|
158
|
+
*/
|
|
159
|
+
export function mcpRegistrationTargets(
|
|
160
|
+
tools: ReadonlySet<string>,
|
|
161
|
+
readsConfig: ReadsProjectMcpConfig,
|
|
162
|
+
): McpRegistrationTarget[] {
|
|
163
|
+
const out: McpRegistrationTarget[] = [];
|
|
164
|
+
for (const [toolId, spec] of Object.entries(MCP_PROJECT_CONFIG)) {
|
|
165
|
+
if (toolId === 'claude-code') continue;
|
|
166
|
+
if (!tools.has(toolId)) continue;
|
|
167
|
+
if (!readsConfig(spec.hostId)) continue;
|
|
168
|
+
out.push({ toolId, hostId: spec.hostId, relPath: spec.relPath });
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Merge the server entry into every target's config file.
|
|
175
|
+
*
|
|
176
|
+
* Returns the merged keys per tool id, in the shape the installer's
|
|
177
|
+
* `merged_keys_by_tool` lockfile record expects. An empty map is the normal
|
|
178
|
+
* result today and is not an error.
|
|
179
|
+
*/
|
|
180
|
+
export function makeEnsureMcpRegistrations(
|
|
181
|
+
mergeJsonFile: MergeJsonFile,
|
|
182
|
+
): (
|
|
183
|
+
projectRoot: string,
|
|
184
|
+
force: boolean,
|
|
185
|
+
packageRoot: string,
|
|
186
|
+
targets: readonly McpRegistrationTarget[],
|
|
187
|
+
) => Record<string, Record<string, unknown>[]> {
|
|
188
|
+
return (projectRoot, force, packageRoot, targets) => {
|
|
189
|
+
const merged: Record<string, Record<string, unknown>[]> = {};
|
|
190
|
+
for (const t of targets) {
|
|
191
|
+
merged[t.toolId] = mergeJsonFile(
|
|
192
|
+
path.join(projectRoot, ...t.relPath.split('/')),
|
|
193
|
+
mcpBridgeEntry(packageRoot),
|
|
194
|
+
force,
|
|
195
|
+
t.relPath,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return merged;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The axis reader the installer uses. Injected in tests, never overridden there. */
|
|
203
|
+
function readsProjectMcpConfig(hostId: string): boolean {
|
|
204
|
+
return probeHostCapabilities(hostId).reads_project_mcp_config;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Write every MCP registration one install owes, and record the merged keys.
|
|
209
|
+
*
|
|
210
|
+
* ONE call site rather than three, because `install.ts` is 3,900 lines over the
|
|
211
|
+
* source-size ratchet's ceiling and every line added there is a line the ratchet
|
|
212
|
+
* charges for. The composition is the same either way; where it lives is the
|
|
213
|
+
* only thing that differs, and it belongs beside the entry it writes.
|
|
214
|
+
*
|
|
215
|
+
* `claude-code` is registered whenever it is selected — that write predates the
|
|
216
|
+
* axis and routing it through would DELETE it, the axis being `false` for every
|
|
217
|
+
* host today. Every OTHER host needs both gates: the `--tools` selection and an
|
|
218
|
+
* OBSERVED `reads_project_mcp_config`.
|
|
219
|
+
*/
|
|
220
|
+
export function registerMcpHosts(
|
|
221
|
+
mergeJsonFile: MergeJsonFile,
|
|
222
|
+
projectRoot: string,
|
|
223
|
+
force: boolean,
|
|
224
|
+
packageRoot: string,
|
|
225
|
+
tools: ReadonlySet<string>,
|
|
226
|
+
into: Record<string, Record<string, unknown>[]>,
|
|
227
|
+
readsConfig: ReadsProjectMcpConfig = readsProjectMcpConfig,
|
|
228
|
+
): void {
|
|
229
|
+
const ensure = makeEnsureMcpRegistrations(mergeJsonFile);
|
|
230
|
+
const claude = MCP_PROJECT_CONFIG['claude-code'];
|
|
231
|
+
if (tools.has('claude-code') && claude !== undefined) {
|
|
232
|
+
const merged = ensure(projectRoot, force, packageRoot, [
|
|
233
|
+
{ toolId: 'claude-code', hostId: claude.hostId, relPath: claude.relPath },
|
|
234
|
+
]);
|
|
235
|
+
(into['claude-code'] ??= []).push(...(merged['claude-code'] ?? []));
|
|
236
|
+
}
|
|
237
|
+
for (const [toolId, keys] of Object.entries(
|
|
238
|
+
ensure(projectRoot, force, packageRoot, mcpRegistrationTargets(tools, readsConfig)),
|
|
239
|
+
)) {
|
|
240
|
+
(into[toolId] ??= []).push(...keys);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Rewrite a stale `.mcp.json` entry in place, touching only the key we own.
|
|
246
|
+
*
|
|
247
|
+
* Install writes the entry once; nothing re-read it afterwards, so a pin
|
|
248
|
+
* written by one install stayed frozen at that version for the life of the
|
|
249
|
+
* file — which is exactly the drift the pin was added to close, one release
|
|
250
|
+
* later. This is the update path.
|
|
251
|
+
*
|
|
252
|
+
* Scope discipline: the ONLY key rewritten is `mcpServers[MCP_SERVER_KEY]`.
|
|
253
|
+
* A server a consumer added by hand sits beside it and is not read, compared,
|
|
254
|
+
* reordered or rewritten — the same contract `merge_json_file` honours on the
|
|
255
|
+
* install path, and the reason uninstall can subtract exactly one key.
|
|
256
|
+
*
|
|
257
|
+
* Returns `'absent'` (no file, or no entry of ours to repair — install owns
|
|
258
|
+
* that case, not this one), `'current'`, or `'rewritten'`.
|
|
259
|
+
*/
|
|
260
|
+
export function migrateMcpBridge(
|
|
261
|
+
projectRoot: string,
|
|
262
|
+
packageRoot: string,
|
|
263
|
+
): 'absent' | 'current' | 'rewritten' {
|
|
264
|
+
const file = path.join(projectRoot, '.mcp.json');
|
|
265
|
+
let doc: Record<string, unknown>;
|
|
266
|
+
try {
|
|
267
|
+
doc = JSON.parse(fs.readFileSync(file, 'utf-8')) as Record<string, unknown>;
|
|
268
|
+
} catch {
|
|
269
|
+
return 'absent';
|
|
270
|
+
}
|
|
271
|
+
const servers = doc['mcpServers'];
|
|
272
|
+
if (servers === null || typeof servers !== 'object' || Array.isArray(servers)) return 'absent';
|
|
273
|
+
const table = servers as Record<string, unknown>;
|
|
274
|
+
if (table[MCP_SERVER_KEY] === undefined) return 'absent';
|
|
275
|
+
|
|
276
|
+
const wanted = (mcpBridgeEntry(packageRoot)['mcpServers'] as Record<string, unknown>)[
|
|
277
|
+
MCP_SERVER_KEY
|
|
278
|
+
];
|
|
279
|
+
if (JSON.stringify(table[MCP_SERVER_KEY]) === JSON.stringify(wanted)) return 'current';
|
|
280
|
+
|
|
281
|
+
table[MCP_SERVER_KEY] = wanted;
|
|
282
|
+
fs.writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`, 'utf-8');
|
|
283
|
+
return 'rewritten';
|
|
72
284
|
}
|
|
73
285
|
|
|
74
286
|
/**
|