@event4u/agent-config 16.1.0 → 16.2.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 +237 -248
- package/dist/agent-src/commands/roadmap/process-full.md +1 -1
- package/dist/agent-src/commands/work.md +4 -0
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
- package/dist/agent-src/rules/ui-audit-gate.md +1 -2
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
- package/dist/agent-src/skills/design-review/SKILL.md +30 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +30 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/dist/cli/python/knowledge_ingest.js +1 -1
- package/dist/cli/python/workspace_drive.js +1 -1
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
- package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
- package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
- package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
- package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
- package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
- package/dist/cli-delegate/cmd_conformance.js +902 -782
- 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 +4 -4
- 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 +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- 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 +10 -10
- 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 +6666 -2581
- package/dist/hooks/dispatch.sha256 +1 -0
- package/dist/install/install.mjs +848 -733
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2747 -1510
- package/dist/server/schemas/settings.js +23 -0
- package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
- package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/docs/contracts/settings-classes.md +6 -4
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/src/config/agent-settings.template.yml +29 -0
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/continuity-surface.json +2 -2
- package/src/config/gate-coverage.yml +234 -3
- package/src/config/gate-violation-baselines.json +7 -3
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +2 -2
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/scripts/_cli/cmd_doctor.ts +5 -19
- package/src/scripts/_cli/doctor_execution.ts +100 -17
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- package/src/scripts/_lib/forge_protection.ts +114 -0
- package/src/scripts/_lib/forge_reader.ts +558 -0
- package/src/scripts/_lib/host_env_write.ts +111 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
- package/src/scripts/_lib/obligations.ts +119 -8
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +2 -1
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- package/src/scripts/_lib/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -0
- package/src/scripts/_lib/turn_end_refusals.ts +525 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/verification_command.ts +324 -0
- package/src/scripts/_lib/verification_evidence.ts +647 -0
- package/src/scripts/_lib/verify_clause.ts +280 -0
- package/src/scripts/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/before_complete_hook.ts +340 -23
- package/src/scripts/bench_hook_latency.ts +412 -5
- package/src/scripts/cache_realization_report.ts +61 -1
- package/src/scripts/check_argument_hint.ts +287 -0
- package/src/scripts/check_claims.ts +125 -1
- package/src/scripts/check_enforcement_coverage.ts +38 -9
- package/src/scripts/check_enforcement_matrix.ts +245 -3
- package/src/scripts/check_finding_dispositions.ts +22 -3
- package/src/scripts/check_generator_sync.ts +71 -0
- package/src/scripts/check_host_docs_digest.ts +786 -0
- package/src/scripts/check_host_format_column.ts +559 -0
- package/src/scripts/check_input_references.ts +228 -0
- package/src/scripts/check_inputs_parity.ts +235 -0
- package/src/scripts/check_kernel_edit_ratified.ts +47 -5
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_holds.ts +2 -1
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +115 -1
- package/src/scripts/compile_hook_manifest.ts +60 -16
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +42 -5
- package/src/scripts/hooks/block_config_weakening.ts +229 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
- package/src/scripts/hooks/bundle_integrity.ts +291 -0
- package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
- package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/concern_timings.ts +94 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
- package/src/scripts/hooks/design_pass_hook.ts +100 -9
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +25 -19
- package/src/scripts/hooks/edit_shape_hook.ts +1 -2
- package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
- package/src/scripts/hooks/evidence_independence.ts +1 -2
- package/src/scripts/hooks/exit_codes.ts +128 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +128 -3
- package/src/scripts/hooks/host_lowering.yaml +262 -46
- package/src/scripts/hooks/host_semantics.ts +30 -2
- package/src/scripts/hooks/injection_budget.ts +9 -2
- package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
- package/src/scripts/hooks/journal_record_hook.ts +1 -2
- package/src/scripts/hooks/memory_recall_hook.ts +1 -2
- package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
- package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- package/src/scripts/hooks/permission_gate.ts +1 -2
- package/src/scripts/hooks/push_settle_hook.ts +1 -3
- package/src/scripts/hooks/replay_hook.ts +2 -1
- package/src/scripts/hooks/reread_guard_hook.ts +1 -2
- package/src/scripts/hooks/review_baseline_hook.ts +1 -2
- package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
- package/src/scripts/hooks/rule_inject_hook.ts +8 -5
- package/src/scripts/hooks/run_continuation_hook.ts +13 -15
- package/src/scripts/hooks/skill_route_hook.ts +1 -2
- package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
- package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
- package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
- package/src/scripts/hooks/table_fingerprint.ts +49 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/install.ts +3 -4
- package/src/scripts/lint_deny_text.ts +419 -0
- package/src/scripts/lint_exit_codes.ts +262 -0
- package/src/scripts/lint_hook_manifest.ts +55 -0
- package/src/scripts/lint_roadmap_blockers.ts +78 -12
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- package/src/scripts/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/release_publication.ts +13 -1
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_skill_menu_census.ts +208 -0
- package/src/scripts/report_standing_payload_by_host.ts +1 -1
- package/src/scripts/roadmap_verify_share.ts +213 -0
- package/src/scripts/schemas/command.schema.json +44 -0
- package/src/scripts/schemas/review-findings.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/self_review_gate.ts +359 -10
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- package/src/scripts/trigger_eval_grandfather.json +0 -2
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +156 -4
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +31 -0
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/ui/assets/index-DH1xc62v.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
- /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse guard: refuse a hand edit to a hook BUILD OUTPUT.
|
|
4
|
+
*
|
|
5
|
+
* road-to-a-kernel-that-guards-its-plumbing 1.2 — the other half of a
|
|
6
|
+
* deliberate split, and the split is the whole design:
|
|
7
|
+
*
|
|
8
|
+
* · plumbing SOURCES (`hook_manifest.yaml`, `host_lowering.yaml`, the
|
|
9
|
+
* `*-dispatcher.sh` trampolines, both hook budget files) are edited
|
|
10
|
+
* legitimately all the time. They carry a RECORD — the ADR-268 § 4
|
|
11
|
+
* ratification gate, path set extended by step 1.1 — not a deny.
|
|
12
|
+
* · plumbing BUILD OUTPUTS have no legitimate hand edit at all. They carry a
|
|
13
|
+
* DENY, which is this file.
|
|
14
|
+
*
|
|
15
|
+
* One mechanism per file class. A record for a file whose every hand edit is a
|
|
16
|
+
* mistake would be a record of a mistake; a deny on a file people edit on
|
|
17
|
+
* purpose would be a wedge.
|
|
18
|
+
*
|
|
19
|
+
* WHAT IS REFUSED.
|
|
20
|
+
*
|
|
21
|
+
* - `dist/hooks/dispatch.js` — the esbuild bundle (`npm run build:hooks`).
|
|
22
|
+
* Every concern the dispatcher runs is inlined here. A hand edit survives
|
|
23
|
+
* until the next build, reaches every dispatch in the meantime, and is
|
|
24
|
+
* invisible in a source review.
|
|
25
|
+
* - `hooks/hooks.json` — the host binding file, written by `condense.ts` via
|
|
26
|
+
* `task sync`. A hand edit here silently unbinds a guard.
|
|
27
|
+
*
|
|
28
|
+
* Both are matched under any parent directory, because a consumer install
|
|
29
|
+
* carries them under its own project root rather than under this repository's.
|
|
30
|
+
*
|
|
31
|
+
* WHAT STILL PASSES, and why that is not a hole.
|
|
32
|
+
*
|
|
33
|
+
* `npm run build:hooks` and `task sync` are the legitimate writers, and both
|
|
34
|
+
* reach these files through a build tool rather than through a shell write
|
|
35
|
+
* shape: the command string carries no redirect into the path, no in-place
|
|
36
|
+
* `sed`, no `tee`/`mv`/`cp` naming it. `shellWriteTarget` matches shapes, not
|
|
37
|
+
* effects, so the builds pass and a hand edit does not. Stated here because it
|
|
38
|
+
* is the property that makes this guard safe to bind `fail_closed`.
|
|
39
|
+
*
|
|
40
|
+
* KNOWN OPEN, stated rather than implied.
|
|
41
|
+
*
|
|
42
|
+
* A write whose target never appears as a path-shaped token — built inside an
|
|
43
|
+
* interpreter's own argument, as in `python3 -c "open('hooks/hooks.json','w')…"`
|
|
44
|
+
* — carries none of the recognised shapes and is NOT detected. This is the same
|
|
45
|
+
* residual `block_kernel_rule_writes.ts` declares for itself and the same
|
|
46
|
+
* boundary: a guard, not write mediation. The honest claim is that the cheap
|
|
47
|
+
* and the accidental paths are closed, not that the determined one is.
|
|
48
|
+
*
|
|
49
|
+
* "Through an interpreter" does not by itself mean undetected, and the tests
|
|
50
|
+
* pin both directions: `sh -c 'printf x > hooks/hooks.json'` IS refused,
|
|
51
|
+
* because the redirect survives the tokenizer inside the quoted argument.
|
|
52
|
+
*
|
|
53
|
+
* MANIFEST DECLARATION.
|
|
54
|
+
*
|
|
55
|
+
* `fail_closed: true`, `severity: blocking`. The fail-closed guarantee is
|
|
56
|
+
* about a DETECTED write: a malformed envelope with no target at all allows,
|
|
57
|
+
* exactly as the kernel guard does, because a guard that blocks what it cannot
|
|
58
|
+
* read turns a shape defect into a wedged session.
|
|
59
|
+
*
|
|
60
|
+
* No agent-accessible kill switch — deliberately, and it is also what
|
|
61
|
+
* `lint_deny_text` (step 1.3) requires of every blocking concern's deny text.
|
|
62
|
+
* The legitimate change is to rebuild the file from its source.
|
|
63
|
+
*
|
|
64
|
+
* Exit codes (docs/contracts/hook-architecture-v1.md):
|
|
65
|
+
* 0 — allow
|
|
66
|
+
* 1 — block
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
import * as fs from 'node:fs';
|
|
70
|
+
import * as path from 'node:path';
|
|
71
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
72
|
+
|
|
73
|
+
import { shellWriteTarget } from '../_lib/shell_write_shapes.js';
|
|
74
|
+
import { EDIT_TOOLS } from '../minimal_safe_diff_hook.js';
|
|
75
|
+
import { COMMAND_TOOLS } from '../before_complete_hook.js';
|
|
76
|
+
import { readHookStdin } from './hook_stdin.js';
|
|
77
|
+
|
|
78
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
79
|
+
|
|
80
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
81
|
+
type JsonObject = { [k: string]: JsonValue };
|
|
82
|
+
|
|
83
|
+
function _isObject(v: JsonValue | undefined): v is JsonObject {
|
|
84
|
+
return v !== null && v !== undefined && typeof v === 'object' && !Array.isArray(v);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The generated plumbing artifacts, as trailing path segments.
|
|
89
|
+
*
|
|
90
|
+
* A trailing match rather than a repo-relative one because the same two files
|
|
91
|
+
* exist under every consumer project root and under this repository's, and a
|
|
92
|
+
* guard anchored to one tree would be silent in the other.
|
|
93
|
+
*/
|
|
94
|
+
export const PLUMBING_BUILD_OUTPUTS: readonly string[] = [
|
|
95
|
+
'dist/hooks/dispatch.js',
|
|
96
|
+
'hooks/hooks.json',
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Where each output comes from, printed in the deny so the reader is told what
|
|
101
|
+
* to run instead of only what not to do.
|
|
102
|
+
*/
|
|
103
|
+
const REGENERATED_BY: Readonly<Record<string, string>> = {
|
|
104
|
+
'dist/hooks/dispatch.js': 'npm run build:hooks',
|
|
105
|
+
'hooks/hooks.json': 'task sync',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Keys across platforms that carry the tool's target file path.
|
|
109
|
+
const _PATH_KEYS: readonly string[] = [
|
|
110
|
+
'file_path',
|
|
111
|
+
'path',
|
|
112
|
+
'target_file',
|
|
113
|
+
'filename',
|
|
114
|
+
'filePath',
|
|
115
|
+
'notebook_path',
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
/** Normalize separators; strip a leading "./" run so segment checks are robust. */
|
|
119
|
+
function _normalize(p: string): string {
|
|
120
|
+
return p.replace(/\\/g, '/').replace(/^(\.\/)+/, '');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The build output `filePath` names, or `null`.
|
|
125
|
+
*
|
|
126
|
+
* Two matching rules, because the guard has two kinds of input and only one of
|
|
127
|
+
* them carries enough information to be exact:
|
|
128
|
+
*
|
|
129
|
+
* · a RELATIVE path is matched EXACTLY, after a leading `./` or `../` run is
|
|
130
|
+
* stripped. `dist/hooks/dispatch.js` and `./hooks/hooks.json` are the
|
|
131
|
+
* governed files; `docs/hooks/hooks.json` is a different file that happens
|
|
132
|
+
* to end the same way, and refusing it would be an over-block on a file
|
|
133
|
+
* nothing generates.
|
|
134
|
+
* · an ABSOLUTE path is matched on its trailing segments, because the guard
|
|
135
|
+
* does not know where the project root is and a host that sends absolute
|
|
136
|
+
* paths — Claude Code sends every `file_path` absolute — would otherwise
|
|
137
|
+
* escape it entirely. A fail-open on the main path is the worse of the two
|
|
138
|
+
* errors.
|
|
139
|
+
*
|
|
140
|
+
* The residual is named rather than hidden: an absolute path ending in
|
|
141
|
+
* `…/hooks/hooks.json` under an unrelated parent is refused too. That is the
|
|
142
|
+
* conservative direction for a two-file guard, and the fix if it ever bites is
|
|
143
|
+
* to relativise against the envelope's `workspace_root`.
|
|
144
|
+
*
|
|
145
|
+
* A bare basename (`dispatch.js`) never matches under either rule — it is not
|
|
146
|
+
* evidence that the governed file is the target.
|
|
147
|
+
*/
|
|
148
|
+
export function targets_plumbing_output(filePath: string): string | null {
|
|
149
|
+
if (!filePath) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const normalized = _normalize(filePath);
|
|
153
|
+
const absolute = normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized);
|
|
154
|
+
const segments = normalized.split('/').filter((s) => s.length > 0 && s !== '.');
|
|
155
|
+
const relevant = absolute ? segments : segments.filter((s) => s !== '..');
|
|
156
|
+
|
|
157
|
+
for (const output of PLUMBING_BUILD_OUTPUTS) {
|
|
158
|
+
const want = output.split('/');
|
|
159
|
+
if (absolute) {
|
|
160
|
+
if (relevant.length < want.length) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const tail = relevant.slice(relevant.length - want.length);
|
|
164
|
+
if (tail.every((s, i) => s === want[i])) {
|
|
165
|
+
return output;
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (relevant.length === want.length && relevant.every((s, i) => s === want[i])) {
|
|
170
|
+
return output;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Best-effort (toolName, candidate target paths) read off a PreToolUse envelope. */
|
|
177
|
+
function _extract(envelope: JsonObject): { tool: string; paths: string[] } {
|
|
178
|
+
const payload = _isObject(envelope['payload']) ? envelope['payload'] : envelope;
|
|
179
|
+
const nameVal =
|
|
180
|
+
payload['tool_name'] ??
|
|
181
|
+
payload['toolName'] ??
|
|
182
|
+
payload['tool'] ??
|
|
183
|
+
envelope['tool_name'] ??
|
|
184
|
+
envelope['tool'];
|
|
185
|
+
const tool = typeof nameVal === 'string' ? nameVal : '';
|
|
186
|
+
|
|
187
|
+
const ti = _isObject(payload['tool_input'])
|
|
188
|
+
? payload['tool_input']
|
|
189
|
+
: _isObject(envelope['tool_input'])
|
|
190
|
+
? envelope['tool_input']
|
|
191
|
+
: null;
|
|
192
|
+
|
|
193
|
+
const paths: string[] = [];
|
|
194
|
+
if (ti !== null) {
|
|
195
|
+
for (const key of _PATH_KEYS) {
|
|
196
|
+
const v = ti[key];
|
|
197
|
+
if (typeof v === 'string' && v) {
|
|
198
|
+
paths.push(v);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { tool, paths };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Best-effort read of a shell command off a PreToolUse envelope. */
|
|
206
|
+
function _extract_command(envelope: JsonObject): string | null {
|
|
207
|
+
const payload = _isObject(envelope['payload']) ? envelope['payload'] : envelope;
|
|
208
|
+
const ti = _isObject(payload['tool_input'])
|
|
209
|
+
? payload['tool_input']
|
|
210
|
+
: _isObject(envelope['tool_input'])
|
|
211
|
+
? envelope['tool_input']
|
|
212
|
+
: null;
|
|
213
|
+
for (const src of [ti, payload]) {
|
|
214
|
+
if (src === null) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const v = src['command'];
|
|
218
|
+
if (typeof v === 'string' && v) {
|
|
219
|
+
return v;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Return (blocked, reason) for one PreToolUse envelope. Pure — no I/O. */
|
|
226
|
+
export function check_envelope(envelope: JsonObject): [boolean, string] {
|
|
227
|
+
const { tool, paths } = _extract(envelope);
|
|
228
|
+
if (!tool) {
|
|
229
|
+
return [false, ''];
|
|
230
|
+
}
|
|
231
|
+
const deny = (output: string): [boolean, string] => [
|
|
232
|
+
true,
|
|
233
|
+
`${output} is a build output, not a source — regenerate it with ` +
|
|
234
|
+
`\`${REGENERATED_BY[output] ?? 'the build that writes it'}\`. ` +
|
|
235
|
+
'See docs/contracts/hook-architecture-v1.md § Plumbing.',
|
|
236
|
+
];
|
|
237
|
+
if (EDIT_TOOLS.has(tool)) {
|
|
238
|
+
for (const p of paths) {
|
|
239
|
+
const output = targets_plumbing_output(p);
|
|
240
|
+
if (output !== null) {
|
|
241
|
+
return deny(output);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return [false, ''];
|
|
245
|
+
}
|
|
246
|
+
if (COMMAND_TOOLS.has(tool)) {
|
|
247
|
+
const cmd = _extract_command(envelope);
|
|
248
|
+
if (cmd !== null) {
|
|
249
|
+
const output = shellWriteTarget(cmd, targets_plumbing_output);
|
|
250
|
+
if (output !== null) {
|
|
251
|
+
return deny(output);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return [false, ''];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function _asObject(v: JsonValue | undefined): JsonObject | null {
|
|
259
|
+
return _isObject(v) ? v : null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function main(): number {
|
|
263
|
+
const raw = readHookStdin();
|
|
264
|
+
let envelope: JsonObject = {};
|
|
265
|
+
if (raw.trim()) {
|
|
266
|
+
try {
|
|
267
|
+
const obj = JSON.parse(raw) as JsonValue;
|
|
268
|
+
envelope = _asObject(obj) ?? {};
|
|
269
|
+
} catch {
|
|
270
|
+
// Malformed envelope — no target to decide about; allow. The
|
|
271
|
+
// fail-closed guarantee applies to a DETECTED write.
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const [blocked, reason] = check_envelope(envelope);
|
|
277
|
+
if (blocked) {
|
|
278
|
+
process.stderr.write(
|
|
279
|
+
`block-plumbing-writes: BLOCKED — ${reason}\n` +
|
|
280
|
+
' This file is generated. Edit the source it is built from, then rebuild.\n' +
|
|
281
|
+
' Contract: docs/contracts/hook-architecture-v1.md\n',
|
|
282
|
+
);
|
|
283
|
+
return 1; // EXIT_BLOCK
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return 0; // EXIT_ALLOW
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
290
|
+
// every module shares the bundle's `import.meta.url`.
|
|
291
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
292
|
+
function _isCliEntry(): boolean {
|
|
293
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
if (process.argv[1] === undefined) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
300
|
+
if (import.meta.url === argvUrl) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
305
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
306
|
+
return here === argv;
|
|
307
|
+
} catch {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (_isCliEntry() || (typeof __AGENT_CONFIG_BUNDLE__ === 'undefined' && process.argv[1] === _HERE)) {
|
|
313
|
+
process.exitCode = main();
|
|
314
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { fix_hint, log_dispatch_issue } from './dispatch_issues.js';
|
|
7
|
+
import { EXIT_BLOCK } from './exit_codes.js';
|
|
8
|
+
import { emitFor } from './host_semantics.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* bundle_integrity — does the dispatcher's own bundle still match what the
|
|
12
|
+
* build produced?
|
|
13
|
+
*
|
|
14
|
+
* Step 3.1 of `road-to-a-kernel-that-guards-its-plumbing`. `dist/hooks/
|
|
15
|
+
* dispatch.js` is a generated file with no legitimate hand edit, and until
|
|
16
|
+
* this landed an edit to it survived until the next build, reached every
|
|
17
|
+
* dispatch meanwhile, and was invisible in a source review.
|
|
18
|
+
* `check_hook_bundle_content` catches that in CI and `block_plumbing_writes`
|
|
19
|
+
* refuses the edit at tool-call time; neither of them is looking at the bundle
|
|
20
|
+
* the process is EXECUTING at the moment it runs a blocking guard.
|
|
21
|
+
*
|
|
22
|
+
* Three verdicts, and the gap between two of them is the whole design:
|
|
23
|
+
*
|
|
24
|
+
* - `ok` — the bundle hashes to the sidecar.
|
|
25
|
+
* - `mismatch` — it does not. The caller refuses on a slot carrying a blocking
|
|
26
|
+
* concern and warns otherwise.
|
|
27
|
+
* - `unverifiable` — there is no sidecar, or it cannot be read or parsed.
|
|
28
|
+
*
|
|
29
|
+
* `unverifiable` NEVER refuses, and that is a decision rather than an
|
|
30
|
+
* oversight. A consumer install from a package built before the sidecar
|
|
31
|
+
* existed has no sidecar; so does any tree where `dist/` was produced by
|
|
32
|
+
* something other than `npm run build:hooks`. Refusing there would convert a
|
|
33
|
+
* missing optional artifact into a total outage of the agent's tooling, which
|
|
34
|
+
* is the failure `hook-architecture-v1.md` already rules on one level up when
|
|
35
|
+
* it fixes a MISSING dispatcher as a silent allow even for a `fail_closed`
|
|
36
|
+
* concern. An absent control is not a tripped control.
|
|
37
|
+
*
|
|
38
|
+
* This is not a signature. Anyone who can rewrite the bundle can rewrite the
|
|
39
|
+
* sidecar beside it, and saying otherwise would be the coverage inflation this
|
|
40
|
+
* roadmap exists to remove. What it catches is the hand edit and the partial
|
|
41
|
+
* write — the cases where somebody changed one file and not the other.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** What a verification produced. */
|
|
45
|
+
export type IntegrityVerdict =
|
|
46
|
+
| { state: 'ok'; sha: string }
|
|
47
|
+
| { state: 'mismatch'; expected: string; actual: string }
|
|
48
|
+
| { state: 'unverifiable'; reason: string };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The cheap identity of a file — what a later dispatch compares instead of
|
|
52
|
+
* re-hashing 1.5 MB.
|
|
53
|
+
*
|
|
54
|
+
* Size AND mtime, never mtime alone: a rebuild that produces a byte-identical
|
|
55
|
+
* bundle bumps mtime and would force a needless re-hash (harmless), while a
|
|
56
|
+
* same-size edit at the same mtime is the one pair this cannot see. That pair
|
|
57
|
+
* needs a deliberate timestamp forge, which is a strictly harder act than the
|
|
58
|
+
* hand edit this defends against, and the honest statement is that the cache
|
|
59
|
+
* trades that case for 0.0101 ms per dispatch against 0.470 ms.
|
|
60
|
+
*
|
|
61
|
+
* 0.0101 ms is the WHOLE cached path measured — reading the stamp back plus
|
|
62
|
+
* the stat — not the stat alone, which is 0.0010 ms. The first version of this
|
|
63
|
+
* comment quoted the stat, which described one of the two syscalls the
|
|
64
|
+
* dispatcher actually pays.
|
|
65
|
+
*/
|
|
66
|
+
export interface BundleStamp {
|
|
67
|
+
sha: string;
|
|
68
|
+
size: number;
|
|
69
|
+
mtimeMs: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Read the expected digest out of a `shasum -a 256` style sidecar. */
|
|
73
|
+
export function parseSidecar(text: string): string | null {
|
|
74
|
+
// `<64 hex> <filename>` — the two-space form `shasum` emits. Tolerant of
|
|
75
|
+
// any run of whitespace and of a trailing newline, because an operator who
|
|
76
|
+
// regenerates the file by hand should not be punished for one space.
|
|
77
|
+
const m = /^([0-9a-f]{64})\s/u.exec(text.trim());
|
|
78
|
+
return m === null ? null : (m[1] as string);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Where the sidecar for a given bundle lives. */
|
|
82
|
+
export function sidecarPathFor(bundlePath: string): string {
|
|
83
|
+
return bundlePath.replace(/\.js$/u, '') + '.sha256';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** SHA-256 of a file's bytes, or null when it cannot be read. */
|
|
87
|
+
export function hashFile(p: string): string | null {
|
|
88
|
+
try {
|
|
89
|
+
return createHash('sha256').update(fs.readFileSync(p)).digest('hex');
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Compare a bundle against its sidecar.
|
|
97
|
+
*
|
|
98
|
+
* Pure apart from the two reads, so the caller's caching and the verdict logic
|
|
99
|
+
* can be tested without a dispatch.
|
|
100
|
+
*/
|
|
101
|
+
export function verifyBundle(bundlePath: string): IntegrityVerdict {
|
|
102
|
+
const sidecar = sidecarPathFor(bundlePath);
|
|
103
|
+
let expected: string | null;
|
|
104
|
+
try {
|
|
105
|
+
expected = parseSidecar(fs.readFileSync(sidecar, 'utf-8'));
|
|
106
|
+
} catch {
|
|
107
|
+
return { state: 'unverifiable', reason: `no sidecar at ${path.basename(sidecar)}` };
|
|
108
|
+
}
|
|
109
|
+
if (expected === null) {
|
|
110
|
+
return { state: 'unverifiable', reason: `sidecar at ${path.basename(sidecar)} is malformed` };
|
|
111
|
+
}
|
|
112
|
+
const actual = hashFile(bundlePath);
|
|
113
|
+
if (actual === null) {
|
|
114
|
+
return { state: 'unverifiable', reason: `bundle at ${path.basename(bundlePath)} is unreadable` };
|
|
115
|
+
}
|
|
116
|
+
return actual === expected ? { state: 'ok', sha: actual } : { state: 'mismatch', expected, actual };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The cheap identity of the bundle right now, or null when it is not there. */
|
|
120
|
+
export function stampOf(bundlePath: string, sha: string): BundleStamp | null {
|
|
121
|
+
try {
|
|
122
|
+
const st = fs.statSync(bundlePath);
|
|
123
|
+
return { sha, size: st.size, mtimeMs: Math.floor(st.mtimeMs) };
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Is a cached stamp still describing the file on disk?
|
|
131
|
+
*
|
|
132
|
+
* Returns false whenever the answer is not a confident yes — an unreadable
|
|
133
|
+
* file re-verifies rather than reusing a stamp that may describe something
|
|
134
|
+
* else.
|
|
135
|
+
*/
|
|
136
|
+
export function stampMatches(bundlePath: string, cached: BundleStamp | null | undefined): boolean {
|
|
137
|
+
if (cached === null || cached === undefined) return false;
|
|
138
|
+
try {
|
|
139
|
+
const st = fs.statSync(bundlePath);
|
|
140
|
+
return st.size === cached.size && Math.floor(st.mtimeMs) === cached.mtimeMs;
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Where the once-per-bundle stamp is cached.
|
|
148
|
+
*
|
|
149
|
+
* The step wrote "caches `{sha, size, mtime}` in session state"; this uses the
|
|
150
|
+
* OS temp dir instead, and the deviation is deliberate. Session state under
|
|
151
|
+
* `agents/runtime/state/` is not written in replay mode — which is the mode
|
|
152
|
+
* `bench_hook_latency` runs in — so a session-state cache would be skipped in
|
|
153
|
+
* exactly the harness that has to measure the cached path, and the step's own
|
|
154
|
+
* verify line asks for `bench_hook_latency --gate` green with a per-dispatch
|
|
155
|
+
* cost of a stat call. It also means a host that never emits `session_start`
|
|
156
|
+
* would get no cache and no check at all.
|
|
157
|
+
*
|
|
158
|
+
* A temp-dir cache is content-addressed by the bundle's own path, so it needs
|
|
159
|
+
* no session identity, self-heals on the first dispatch of any session, works
|
|
160
|
+
* in a consumer install, and degrades to "verify every time" when the temp dir
|
|
161
|
+
* is unwritable — slower, never wrong.
|
|
162
|
+
*/
|
|
163
|
+
export function stampCachePath(bundlePath: string, tmpDir: string): string {
|
|
164
|
+
const key = createHash('sha256').update(bundlePath).digest('hex').slice(0, 16);
|
|
165
|
+
return path.join(tmpDir, `agent-config-bundle-integrity-${key}.json`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Read a cached stamp, or null when there is not a usable one. */
|
|
169
|
+
export function readStamp(cachePath: string): BundleStamp | null {
|
|
170
|
+
try {
|
|
171
|
+
const raw = JSON.parse(fs.readFileSync(cachePath, 'utf-8')) as Partial<BundleStamp>;
|
|
172
|
+
if (
|
|
173
|
+
typeof raw.sha === 'string' &&
|
|
174
|
+
typeof raw.size === 'number' &&
|
|
175
|
+
typeof raw.mtimeMs === 'number'
|
|
176
|
+
) {
|
|
177
|
+
return { sha: raw.sha, size: raw.size, mtimeMs: raw.mtimeMs };
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// Absent, unreadable or malformed — all three mean "no cache", which
|
|
181
|
+
// costs a re-verification and never a wrong answer.
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Persist a stamp. Failure is silent: a cache that cannot be written is a cache miss. */
|
|
187
|
+
export function writeStamp(cachePath: string, stamp: BundleStamp): void {
|
|
188
|
+
try {
|
|
189
|
+
fs.writeFileSync(cachePath, JSON.stringify(stamp), 'utf-8');
|
|
190
|
+
} catch {
|
|
191
|
+
/* a cache is an optimisation, never a control */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The whole check, cache included — hash once per bundle identity, stat after.
|
|
197
|
+
*
|
|
198
|
+
* Returns the verdict plus whether this call had to do the expensive part, so
|
|
199
|
+
* a caller (and a test) can assert the cache actually caches rather than
|
|
200
|
+
* trusting that it does.
|
|
201
|
+
*/
|
|
202
|
+
export function verifyBundleCached(
|
|
203
|
+
bundlePath: string,
|
|
204
|
+
tmpDir: string,
|
|
205
|
+
): { verdict: IntegrityVerdict; hashed: boolean } {
|
|
206
|
+
const cachePath = stampCachePath(bundlePath, tmpDir);
|
|
207
|
+
const cached = readStamp(cachePath);
|
|
208
|
+
if (stampMatches(bundlePath, cached) && cached !== null) {
|
|
209
|
+
return { verdict: { state: 'ok', sha: cached.sha }, hashed: false };
|
|
210
|
+
}
|
|
211
|
+
const verdict = verifyBundle(bundlePath);
|
|
212
|
+
if (verdict.state === 'ok') {
|
|
213
|
+
const stamp = stampOf(bundlePath, verdict.sha);
|
|
214
|
+
if (stamp !== null) writeStamp(cachePath, stamp);
|
|
215
|
+
}
|
|
216
|
+
// A `mismatch` is deliberately NOT cached. Caching it would be caching a
|
|
217
|
+
// refusal, and the next legitimate rebuild has to be able to clear it
|
|
218
|
+
// without anyone knowing a cache existed.
|
|
219
|
+
return { verdict, hashed: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** What the dispatcher should do about the verdict it just got. */
|
|
223
|
+
export type IntegrityAction =
|
|
224
|
+
| { action: 'allow' }
|
|
225
|
+
| { action: 'warn'; detail: string }
|
|
226
|
+
| { action: 'block'; detail: string };
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Turn a verdict into a dispatch decision.
|
|
230
|
+
*
|
|
231
|
+
* Separated from the dispatcher so the SEVERITY half of the rule is testable
|
|
232
|
+
* without a dispatch — and because `dispatch_hook.ts` sits against the
|
|
233
|
+
* 1,500-line source budget, which refused the inline version.
|
|
234
|
+
*
|
|
235
|
+
* `hasBlockingConcern` is the discriminator rather than the slot name: a slot
|
|
236
|
+
* carrying only advisory concerns could not have refused anything anyway, so
|
|
237
|
+
* blocking there would let a stale digest wedge a chain with no authority to
|
|
238
|
+
* begin with.
|
|
239
|
+
*/
|
|
240
|
+
export function integrityAction(
|
|
241
|
+
verdict: IntegrityVerdict,
|
|
242
|
+
hasBlockingConcern: boolean,
|
|
243
|
+
): IntegrityAction {
|
|
244
|
+
if (verdict.state !== 'mismatch') {
|
|
245
|
+
// `ok` and `unverifiable` are both allow, and they are both SILENT.
|
|
246
|
+
// See the header for why an absent sidecar is not a tripped control.
|
|
247
|
+
return { action: 'allow' };
|
|
248
|
+
}
|
|
249
|
+
const detail =
|
|
250
|
+
`dist/hooks/dispatch.js does not match its build digest ` +
|
|
251
|
+
`(expected ${verdict.expected.slice(0, 12)}…, got ${verdict.actual.slice(0, 12)}…). ` +
|
|
252
|
+
`The bundle is generated by \`npm run build:hooks\` and has no legitimate ` +
|
|
253
|
+
`hand edit; rebuild to clear this.`;
|
|
254
|
+
return hasBlockingConcern ? { action: 'block', detail } : { action: 'warn', detail };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The whole gate, side effects included — verify, log, and decide the exit.
|
|
259
|
+
*
|
|
260
|
+
* Returns an exit code when the dispatch must stop, `null` when it continues.
|
|
261
|
+
* It lives here rather than inline in `dispatch_hook.ts` for two reasons, and
|
|
262
|
+
* the second is the load-bearing one: that file sits against the 1,500-line
|
|
263
|
+
* source budget and `check_source_size_budget` refused the inline version, and
|
|
264
|
+
* a gate whose logging and emission are inside the dispatcher can only be
|
|
265
|
+
* tested by running a dispatch.
|
|
266
|
+
*/
|
|
267
|
+
export function runBundleIntegrityGate(
|
|
268
|
+
bundlePath: string,
|
|
269
|
+
platform: string,
|
|
270
|
+
event: string,
|
|
271
|
+
hasBlockingConcern: boolean,
|
|
272
|
+
tmpDir: string = os.tmpdir(),
|
|
273
|
+
): number | null {
|
|
274
|
+
const { verdict } = verifyBundleCached(bundlePath, tmpDir);
|
|
275
|
+
const act = integrityAction(verdict, hasBlockingConcern);
|
|
276
|
+
if (act.action === 'allow') return null;
|
|
277
|
+
const line = `plumbing-integrity: ${act.detail}`;
|
|
278
|
+
try {
|
|
279
|
+
log_dispatch_issue(process.cwd(), 'dispatcher', 'execution_failed', line, fix_hint());
|
|
280
|
+
} catch {
|
|
281
|
+
/* observability never breaks the hook */
|
|
282
|
+
}
|
|
283
|
+
if (act.action === 'warn') {
|
|
284
|
+
process.stderr.write(`dispatch_hook: ${line}\n`);
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const em = emitFor(platform, event, 'block', [line], EXIT_BLOCK);
|
|
288
|
+
if (em.stdout) process.stdout.write(em.stdout);
|
|
289
|
+
if (em.stderr) process.stderr.write(em.stderr);
|
|
290
|
+
return em.exit;
|
|
291
|
+
}
|
|
@@ -84,10 +84,9 @@ import path from 'node:path';
|
|
|
84
84
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
85
85
|
|
|
86
86
|
import { readHookStdin } from './hook_stdin.js';
|
|
87
|
+
import { EXIT_ALLOW, EXIT_WARN } from './exit_codes.js';
|
|
87
88
|
|
|
88
89
|
const SETTINGS_FILE = '.agent-settings.yml';
|
|
89
|
-
const EXIT_ALLOW = 0;
|
|
90
|
-
const EXIT_WARN = 2;
|
|
91
90
|
|
|
92
91
|
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
93
92
|
type JsonObject = { [k: string]: JsonValue };
|
|
@@ -41,9 +41,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
41
41
|
|
|
42
42
|
import { type GraphState, graphState } from '../code_graph/detect.js';
|
|
43
43
|
import { readHookStdin } from './hook_stdin.js';
|
|
44
|
+
import { EXIT_ALLOW, EXIT_WARN } from './exit_codes.js';
|
|
44
45
|
|
|
45
|
-
const EXIT_ALLOW = 0;
|
|
46
|
-
const EXIT_WARN = 2;
|
|
47
46
|
const CODE_EXT = /\.(php|ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i;
|
|
48
47
|
|
|
49
48
|
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
@@ -47,9 +47,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
47
47
|
import { isScannable, scanText, type CommentFinding } from '../lint_code_comments.js';
|
|
48
48
|
import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
|
|
49
49
|
import { readHookStdin } from './hook_stdin.js';
|
|
50
|
-
|
|
51
|
-
const EXIT_ALLOW = 0;
|
|
52
|
-
const EXIT_WARN = 2;
|
|
50
|
+
import { EXIT_ALLOW, EXIT_WARN } from './exit_codes.js';
|
|
53
51
|
|
|
54
52
|
/** Findings named in the advisory line. The rest are counted, never listed. */
|
|
55
53
|
export const MAX_REPORTED = 3;
|
|
@@ -52,6 +52,7 @@ import { main as gitAuthorizationMain } from '../git_authorization_hook.js';
|
|
|
52
52
|
import { main as evidenceIndependenceMain } from './evidence_independence.js';
|
|
53
53
|
import { main as blockNoVerifyMain } from './block_no_verify.js';
|
|
54
54
|
import { main as blockKernelRuleWritesMain } from './block_kernel_rule_writes.js';
|
|
55
|
+
import { main as blockPlumbingWritesMain } from './block_plumbing_writes.js';
|
|
55
56
|
import { main as blockConfigWeakeningMain } from './block_config_weakening.js';
|
|
56
57
|
import { main as blockSpeakingInboxDirMain } from './block_speaking_inbox_dir.js';
|
|
57
58
|
import { main as oneQuestionPerAskMain } from './one_question_per_ask_hook.js';
|
|
@@ -118,6 +119,7 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
|
|
|
118
119
|
'src/scripts/hooks/evidence_independence.ts': evidenceIndependenceMain as ConcernMain,
|
|
119
120
|
'src/scripts/hooks/block_no_verify.ts': blockNoVerifyMain as ConcernMain,
|
|
120
121
|
'src/scripts/hooks/block_kernel_rule_writes.ts': blockKernelRuleWritesMain as ConcernMain,
|
|
122
|
+
'src/scripts/hooks/block_plumbing_writes.ts': blockPlumbingWritesMain as ConcernMain,
|
|
121
123
|
'src/scripts/hooks/block_config_weakening.ts': blockConfigWeakeningMain as ConcernMain,
|
|
122
124
|
'src/scripts/hooks/block_speaking_inbox_dir.ts': blockSpeakingInboxDirMain as ConcernMain,
|
|
123
125
|
'src/scripts/hooks/one_question_per_ask_hook.ts': oneQuestionPerAskMain as ConcernMain,
|