@event4u/agent-config 14.15.0 → 14.17.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 +84 -153
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +172 -6
- package/dist/agent-src/commands/fix/commit-messages.md +336 -0
- package/dist/agent-src/commands/fix.md +5 -3
- package/dist/agent-src/rules/commit-conventions.md +1 -1
- package/dist/agent-src/rules/security-sensitive-stop.md +7 -5
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +20 -2
- package/dist/agent-src/skills/aws-infrastructure/SKILL.md +41 -0
- package/dist/agent-src/skills/conventional-commits-writing/SKILL.md +234 -4
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +99 -99
- package/dist/agent-src/skills/fe-design/SKILL.md +6 -3
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +28 -0
- package/dist/agent-src/skills/iconography/SKILL.md +37 -6
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/terraform/SKILL.md +29 -0
- package/dist/agent-src/skills/threat-modeling/data/reasoning.csv +1 -0
- package/dist/agent-src/skills/threat-modeling/data/surfaces.csv +1 -0
- package/dist/agent-src/skills/threat-modeling/data/threats.csv +5 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/cmd_conformance.js +158 -33
- package/dist/cli-delegate/cmd_explain.js +24 -112
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +47 -21
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +169 -41
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +160 -35
- package/dist/router.json +1 -1
- package/docs/guidelines/design-antipatterns.md +10 -2
- package/package.json +1 -1
- package/src/config/gate-violation-baselines.json +2 -2
- package/src/config/web-launch-readiness.json +1 -1
- package/src/scripts/_cli/explain_last/index.ts +1 -1
- package/src/scripts/_cli/explain_last/memory.ts +8 -104
- package/src/scripts/check_artefact_count_messaging.ts +9 -0
- package/src/scripts/check_release_highlights.ts +77 -4
- package/src/scripts/check_web_launch_readiness.ts +295 -15
- package/src/scripts/design_slop_rules.ts +19 -9
- package/src/scripts/git_authorization_hook.ts +257 -35
- package/src/scripts/hooks/block_unauthorized_git.ts +16 -4
- package/src/scripts/hooks/merge_impact.ts +68 -0
- package/src/scripts/measure_release_mix.ts +384 -0
- package/src/scripts/release.ts +4 -17
- package/src/scripts/release_drill.ts +154 -1
- package/src/scripts/release_mix_taxonomy.json +172 -0
- package/src/scripts/release_publication.ts +91 -0
- package/src/scripts/attest_artifact.ts +0 -193
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
{
|
|
2
|
+
"taxonomy_version": "1.0.0",
|
|
3
|
+
"updated": "2026-09-04",
|
|
4
|
+
"contract": "docs/decisions/ADR-253-per-pr-user-artifact-gate-declined.md",
|
|
5
|
+
"reader": "src/scripts/measure_release_mix.ts",
|
|
6
|
+
"note": [
|
|
7
|
+
"Path -> category mapping for the release governance-vs-product measurement.",
|
|
8
|
+
"Versioned so a later reading is comparable to an earlier one: a reading records",
|
|
9
|
+
"the taxonomy_version it was computed under, and a bump means prior readings are",
|
|
10
|
+
"recomputed rather than silently compared across two different rulers.",
|
|
11
|
+
"Matching is LONGEST-PREFIX-WINS over the repository-relative path. A path that",
|
|
12
|
+
"matches no rule is reported as `unclassified` -- a first-class state, never a",
|
|
13
|
+
"silent default. `unclassified` alone does not make a commit `mixed`.",
|
|
14
|
+
"The `consumer` label names a SHIPPED SURFACE, not delivered consumer value.",
|
|
15
|
+
"openai/codex-default asked for the label `shipped-product` on exactly that",
|
|
16
|
+
"ground and anthropic/claude-sonnet-4-5 preferred keeping `consumer` for",
|
|
17
|
+
"consistency with the roadmap's own wording; the roadmap's wording is kept and",
|
|
18
|
+
"the objection is recorded here rather than dropped."
|
|
19
|
+
],
|
|
20
|
+
"categories": {
|
|
21
|
+
"consumer": "Shipped surface an installing consumer receives or invokes: skills, rules, commands, guidelines, packs, the agent-config CLI runtime, and user-facing documentation.",
|
|
22
|
+
"governance": "This repository's own steering layer: roadmaps, evidence ledgers, verdicts, decision records, archive and promotion records, and governance-only contracts.",
|
|
23
|
+
"maintenance": "Repository infrastructure: CI gates, tests, build and release automation, packaging, dependencies, tooling configuration.",
|
|
24
|
+
"unclassified": "No rule matches, or the path is genuinely shared between categories and is left visible rather than assigned."
|
|
25
|
+
},
|
|
26
|
+
"excluded_generated": [
|
|
27
|
+
"dist/",
|
|
28
|
+
".augment/",
|
|
29
|
+
".claude/",
|
|
30
|
+
".cursor/",
|
|
31
|
+
".clinerules/",
|
|
32
|
+
".windsurfrules",
|
|
33
|
+
"GEMINI.md",
|
|
34
|
+
".claude-plugin/",
|
|
35
|
+
".augment-plugin/",
|
|
36
|
+
"package-lock.json",
|
|
37
|
+
"docs/decisions/INDEX.md",
|
|
38
|
+
"agents/roadmaps/archive/INDEX.md",
|
|
39
|
+
"agents/roadmaps/archive/index.json"
|
|
40
|
+
],
|
|
41
|
+
"excluded_note": [
|
|
42
|
+
"Generated projections and lockfiles are removed BEFORE a commit is classified,",
|
|
43
|
+
"and are excluded from both the commit view and the diff view. A commit left with",
|
|
44
|
+
"no path after the exclusion is counted in the `generated_only` diagnostic rather",
|
|
45
|
+
"than as maintenance -- otherwise a release's ratio would depend on whether the",
|
|
46
|
+
"regeneration was committed separately from the source edit that caused it."
|
|
47
|
+
],
|
|
48
|
+
"rules": [
|
|
49
|
+
{ "prefix": "src/skills/", "category": "consumer" },
|
|
50
|
+
{ "prefix": "src/rules/", "category": "consumer" },
|
|
51
|
+
{ "prefix": "src/agent-src/", "category": "consumer" },
|
|
52
|
+
{ "prefix": "src/domains/", "category": "consumer" },
|
|
53
|
+
{ "prefix": "src/packs/", "category": "consumer" },
|
|
54
|
+
{ "prefix": "src/profiles/", "category": "consumer" },
|
|
55
|
+
{ "prefix": "src/templates/", "category": "consumer" },
|
|
56
|
+
{ "prefix": "src/patterns/", "category": "consumer" },
|
|
57
|
+
{ "prefix": "src/subagents/", "category": "consumer" },
|
|
58
|
+
{ "prefix": "src/missions/", "category": "consumer" },
|
|
59
|
+
{ "prefix": "src/flows/", "category": "consumer" },
|
|
60
|
+
{ "prefix": "src/config/", "category": "consumer" },
|
|
61
|
+
{ "prefix": "src/cli/", "category": "consumer" },
|
|
62
|
+
{ "prefix": "src/install/", "category": "consumer" },
|
|
63
|
+
{ "prefix": "src/server/", "category": "consumer" },
|
|
64
|
+
{ "prefix": "src/ui/", "category": "consumer" },
|
|
65
|
+
{ "prefix": "src/shared/", "category": "unclassified" },
|
|
66
|
+
|
|
67
|
+
{ "prefix": "src/scripts/", "category": "maintenance" },
|
|
68
|
+
{ "prefix": "src/scripts/ai_council/", "category": "consumer" },
|
|
69
|
+
{ "prefix": "src/scripts/ai_team/", "category": "consumer" },
|
|
70
|
+
{ "prefix": "src/scripts/ai-video/", "category": "consumer" },
|
|
71
|
+
{ "prefix": "src/scripts/ai-image/", "category": "consumer" },
|
|
72
|
+
{ "prefix": "src/scripts/media/", "category": "consumer" },
|
|
73
|
+
{ "prefix": "src/scripts/hooks/", "category": "consumer" },
|
|
74
|
+
{ "prefix": "src/scripts/mcp_server/", "category": "consumer" },
|
|
75
|
+
{ "prefix": "src/scripts/skill_tools/", "category": "consumer" },
|
|
76
|
+
{ "prefix": "src/scripts/command_suggester/", "category": "consumer" },
|
|
77
|
+
{ "prefix": "src/scripts/code_graph/", "category": "consumer" },
|
|
78
|
+
{ "prefix": "src/scripts/prediction-pool/", "category": "consumer" },
|
|
79
|
+
{ "prefix": "src/scripts/cost/", "category": "consumer" },
|
|
80
|
+
|
|
81
|
+
{ "prefix": "agents/", "category": "governance" },
|
|
82
|
+
{ "prefix": "internal/", "category": "governance" },
|
|
83
|
+
{ "prefix": "provenance/", "category": "governance" },
|
|
84
|
+
|
|
85
|
+
{ "prefix": "docs/", "category": "consumer" },
|
|
86
|
+
{ "prefix": "docs/decisions/", "category": "governance" },
|
|
87
|
+
{ "prefix": "docs/adrs/", "category": "governance" },
|
|
88
|
+
{ "prefix": "docs/archive/", "category": "governance" },
|
|
89
|
+
{ "prefix": "docs/maintainers/", "category": "governance" },
|
|
90
|
+
{ "prefix": "docs/parity/", "category": "governance" },
|
|
91
|
+
{ "prefix": "docs/wedge/", "category": "governance" },
|
|
92
|
+
{ "prefix": "docs/experiences/", "category": "governance" },
|
|
93
|
+
{ "prefix": "docs/case-studies/", "category": "governance" },
|
|
94
|
+
{ "prefix": "docs/announcements/", "category": "governance" },
|
|
95
|
+
|
|
96
|
+
{ "prefix": "tests/", "category": "maintenance" },
|
|
97
|
+
{ "prefix": ".github/", "category": "maintenance" },
|
|
98
|
+
{ "prefix": "taskfiles/", "category": "maintenance" },
|
|
99
|
+
{ "prefix": "deploy/", "category": "maintenance" },
|
|
100
|
+
{ "prefix": "site/", "category": "maintenance" },
|
|
101
|
+
{ "prefix": "hooks/", "category": "consumer" },
|
|
102
|
+
{ "prefix": "user-types/", "category": "consumer" },
|
|
103
|
+
{ "prefix": "tokens/", "category": "consumer" },
|
|
104
|
+
{ "prefix": "LICENSES/", "category": "consumer" },
|
|
105
|
+
|
|
106
|
+
{ "prefix": "README.md", "category": "consumer" },
|
|
107
|
+
{ "prefix": "ONBOARDING.md", "category": "consumer" },
|
|
108
|
+
{ "prefix": "MIGRATION.md", "category": "consumer" },
|
|
109
|
+
{ "prefix": "BREAKING_CHANGES.md", "category": "consumer" },
|
|
110
|
+
{ "prefix": "SECURITY.md", "category": "consumer" },
|
|
111
|
+
{ "prefix": "LEGAL_NOTICE.md", "category": "consumer" },
|
|
112
|
+
{ "prefix": "CHANGELOG.md", "category": "consumer" },
|
|
113
|
+
{ "prefix": "CAPABILITIES.yaml", "category": "consumer" },
|
|
114
|
+
{ "prefix": "CREDITS.md", "category": "consumer" },
|
|
115
|
+
{ "prefix": "REUSE.toml", "category": "consumer" },
|
|
116
|
+
{ "prefix": "LICENSE", "category": "consumer" },
|
|
117
|
+
{ "prefix": "NOTICE", "category": "consumer" },
|
|
118
|
+
{ "prefix": "llms.txt", "category": "consumer" },
|
|
119
|
+
|
|
120
|
+
{ "prefix": "AGENTS.md", "category": "governance" },
|
|
121
|
+
{ "prefix": "CLAUDE.md", "category": "governance" },
|
|
122
|
+
{ "prefix": "CONTRIBUTING.md", "category": "governance" },
|
|
123
|
+
|
|
124
|
+
{ "prefix": "Taskfile.yml", "category": "maintenance" },
|
|
125
|
+
{ "prefix": "agent-config", "category": "maintenance" },
|
|
126
|
+
{ "prefix": "scripts-run", "category": "maintenance" },
|
|
127
|
+
{ "prefix": "setup.sh", "category": "maintenance" },
|
|
128
|
+
{ "prefix": "package.json", "category": "maintenance" },
|
|
129
|
+
{ "prefix": "eslint.config.js", "category": "maintenance" },
|
|
130
|
+
{ "prefix": "tsconfig", "category": "maintenance" },
|
|
131
|
+
{ "prefix": "vite.config.ts", "category": "maintenance" },
|
|
132
|
+
{ "prefix": "vitest.config.ts", "category": "maintenance" },
|
|
133
|
+
{ "prefix": "playwright.config.ts", "category": "maintenance" },
|
|
134
|
+
{ "prefix": "glama.json", "category": "maintenance" },
|
|
135
|
+
{ "prefix": "mcp.json", "category": "maintenance" },
|
|
136
|
+
{ "prefix": "allowlist_backup.json", "category": "maintenance" },
|
|
137
|
+
{ "prefix": ".augmentignore", "category": "maintenance" },
|
|
138
|
+
{ "prefix": ".dockerignore", "category": "maintenance" },
|
|
139
|
+
{ "prefix": ".gitattributes", "category": "maintenance" },
|
|
140
|
+
{ "prefix": ".gitignore", "category": "maintenance" },
|
|
141
|
+
{ "prefix": ".mailmap", "category": "maintenance" },
|
|
142
|
+
{ "prefix": ".npmignore", "category": "maintenance" },
|
|
143
|
+
{ "prefix": ".path-leak-allow", "category": "maintenance" },
|
|
144
|
+
{ "prefix": ".secret-allow", "category": "maintenance" }
|
|
145
|
+
],
|
|
146
|
+
"governance_contracts": [
|
|
147
|
+
"docs/contracts/adr-layout.md",
|
|
148
|
+
"docs/contracts/adr-architectural-consensus-mechanism.md",
|
|
149
|
+
"docs/contracts/ai-council-config.md",
|
|
150
|
+
"docs/contracts/adversarial-review-protocol.md",
|
|
151
|
+
"docs/contracts/branch-protection-policy.md",
|
|
152
|
+
"docs/contracts/ci-green-floor.md",
|
|
153
|
+
"docs/contracts/ci-cost-budget.md",
|
|
154
|
+
"docs/contracts/release-pr-gating.md",
|
|
155
|
+
"docs/contracts/release-sizing.md",
|
|
156
|
+
"docs/contracts/release-trunk-sync.md",
|
|
157
|
+
"docs/contracts/CHANGELOG-conventions.md",
|
|
158
|
+
"docs/contracts/evidence-artifact-types.md",
|
|
159
|
+
"docs/contracts/kernel-membership.md",
|
|
160
|
+
"docs/contracts/package-self-orientation.md"
|
|
161
|
+
],
|
|
162
|
+
"governance_contracts_note": [
|
|
163
|
+
"`docs/contracts/` defaults to `consumer` because most contracts on that page",
|
|
164
|
+
"describe behavior the package promises an installing consumer -- classifying the",
|
|
165
|
+
"whole directory as governance would systematically undercount shipped work.",
|
|
166
|
+
"The files listed above are the exceptions: contracts that govern this",
|
|
167
|
+
"repository's own process (decision recording, council, release cutting, CI",
|
|
168
|
+
"floors, evidence discipline) rather than promised behavior. They resolve to",
|
|
169
|
+
"`governance`. A contract absent from this list and absent from the tree is a",
|
|
170
|
+
"stale entry and is reported by `--audit`."
|
|
171
|
+
]
|
|
172
|
+
}
|
|
@@ -515,6 +515,97 @@ export function guard_release_branch_push(branch: string): void {
|
|
|
515
515
|
* and every line above 1500 is charged by the growth ratchet, while this module
|
|
516
516
|
* is far below it.
|
|
517
517
|
*/
|
|
518
|
+
/**
|
|
519
|
+
* Own the whole step-1 branch decision: reuse an existing release branch, and
|
|
520
|
+
* make sure whatever we end up on carries current `main`.
|
|
521
|
+
*
|
|
522
|
+
* Three defects it closes, all measured on 14.15.0 (2026-09-03) in one sitting,
|
|
523
|
+
* and all the same underlying mistake — a release branch that is not on current
|
|
524
|
+
* `main`, discovered later than it could have been.
|
|
525
|
+
*
|
|
526
|
+
* 1. **Reuse was gated on `--resume`.** The two "branch exists" arms read
|
|
527
|
+
* `resume && _branch_exists_local(branch)`, so a PLAIN `task release` over an
|
|
528
|
+
* existing branch fell through to `git checkout -b` and died with exit 128.
|
|
529
|
+
* That is exactly the state `guard_release_curation` leaves behind — it stops
|
|
530
|
+
* before the commit, and the branch it stops on already exists — while its
|
|
531
|
+
* own message says to re-run `task release`. The message was right about
|
|
532
|
+
* what to do and the code refused to do it. Reuse no longer consults
|
|
533
|
+
* `resume`: an existing release branch is a fact about the repository, not
|
|
534
|
+
* about the flag.
|
|
535
|
+
* 2. **A branch cut from an older `main` failed at the PUSH.** The pre-push
|
|
536
|
+
* preflight reported `branch is BEHIND origin/main` after steps 2 and 3 had
|
|
537
|
+
* already bumped and committed — the cheapest moment to integrate `main` is
|
|
538
|
+
* the moment we check the branch out, not eight steps later.
|
|
539
|
+
* 3. **A fresh branch was cut from whatever local `main` happened to be.** The
|
|
540
|
+
* create arm never pulled, so a stale local `main` produced a stale release
|
|
541
|
+
* branch and defect 2 followed on the next run. Fixed on the same terms
|
|
542
|
+
* rather than left as the half that still bites: leaving it would reproduce
|
|
543
|
+
* the "fix that does not fix it" pattern the other two came from.
|
|
544
|
+
*
|
|
545
|
+
* `say` is a callback rather than a direct write because the `[N/total]` step
|
|
546
|
+
* prefix belongs to the orchestrator, which knows the total; this module does
|
|
547
|
+
* not. Keeping the printing there and the decision here is also what lets
|
|
548
|
+
* `release.ts` shed twenty lines — it sits ~500 lines past the 1500-line cap,
|
|
549
|
+
* where every line is charged by the growth ratchet, and this module is under
|
|
550
|
+
* it.
|
|
551
|
+
*
|
|
552
|
+
* The merge is `git merge`, never a rebase and never a force: a release branch
|
|
553
|
+
* can already carry a pushed commit and somebody else's merge, and neither is
|
|
554
|
+
* ours to rewrite.
|
|
555
|
+
*/
|
|
556
|
+
export function checkout_release_branch(
|
|
557
|
+
branch: string,
|
|
558
|
+
prMerged: boolean,
|
|
559
|
+
say: (msg: string) => void,
|
|
560
|
+
): void {
|
|
561
|
+
if (prMerged) {
|
|
562
|
+
say(`PR for ${branch} already merged — staying on ${MAIN_BRANCH}`);
|
|
563
|
+
if (git(['rev-parse', '--abbrev-ref', 'HEAD'], { capture: true }) !== MAIN_BRANCH) {
|
|
564
|
+
run(['git', 'checkout', MAIN_BRANCH]);
|
|
565
|
+
}
|
|
566
|
+
run(['git', 'pull', '--ff-only', REMOTE, MAIN_BRANCH]);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (_branch_exists_local(branch)) {
|
|
570
|
+
say(`Branch ${branch} exists locally — checkout`);
|
|
571
|
+
run(['git', 'checkout', branch]);
|
|
572
|
+
} else if (_branch_exists_remote(branch)) {
|
|
573
|
+
say(`Branch ${branch} exists on ${REMOTE} — fetch + checkout`);
|
|
574
|
+
run(['git', 'fetch', REMOTE, branch]);
|
|
575
|
+
run(['git', 'checkout', '-b', branch, `${REMOTE}/${branch}`]);
|
|
576
|
+
} else {
|
|
577
|
+
// Cut from CURRENT main, not from whatever the local ref happens to be.
|
|
578
|
+
say(`Create branch ${branch} from current ${MAIN_BRANCH}`);
|
|
579
|
+
if (git(['rev-parse', '--abbrev-ref', 'HEAD'], { capture: true }) === MAIN_BRANCH) {
|
|
580
|
+
run(['git', 'pull', '--ff-only', REMOTE, MAIN_BRANCH]);
|
|
581
|
+
}
|
|
582
|
+
run(['git', 'checkout', '-b', branch]);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
integrate_main_if_behind(branch, say);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Merge `origin/<default>` into the checked-out release branch when it is
|
|
590
|
+
* behind, so the pre-push preflight is never the first place that surfaces.
|
|
591
|
+
*
|
|
592
|
+
* Silent when the branch is already current — a release run should not print a
|
|
593
|
+
* merge it did not make. Counting with `rev-list --count` against the fetched
|
|
594
|
+
* remote ref rather than the local one: the local `main` is exactly what went
|
|
595
|
+
* stale in the case this exists for.
|
|
596
|
+
*/
|
|
597
|
+
export function integrate_main_if_behind(branch: string, say: (msg: string) => void): void {
|
|
598
|
+
run(['git', 'fetch', REMOTE, MAIN_BRANCH], { check: false });
|
|
599
|
+
const behind = git(['rev-list', '--count', `HEAD..${REMOTE}/${MAIN_BRANCH}`], {
|
|
600
|
+
capture: true,
|
|
601
|
+
}).trim();
|
|
602
|
+
if (behind === '' || behind === '0') {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
say(`Branch ${branch} is ${behind} commit(s) behind ${REMOTE}/${MAIN_BRANCH} — merging it in`);
|
|
606
|
+
run(['git', 'merge', `${REMOTE}/${MAIN_BRANCH}`, '--no-edit']);
|
|
607
|
+
}
|
|
608
|
+
|
|
518
609
|
export function guard_release_curation(version: string, prMerged = false): void {
|
|
519
610
|
if (prMerged) {
|
|
520
611
|
return;
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
/**
|
|
3
|
-
* Content attestation for any artifact a hook would INJECT.
|
|
4
|
-
*
|
|
5
|
-
* `road-to-skill-ecosystem-runtime-enforcement` Phase 5, Steps 6-7, and the
|
|
6
|
-
* ordering in that step is the argument: attestation is the **precondition** for
|
|
7
|
-
* auto-injection, not a follow-up to it.
|
|
8
|
-
*
|
|
9
|
-
* ── The threat, concretely ──────────────────────────────────────────────────
|
|
10
|
-
* A hook that reads a tracked file and injects its contents into the model's
|
|
11
|
-
* context turns that file into a **standing injection amplifier**. Whoever can
|
|
12
|
-
* write the file can write the agent's instructions, on every session, without
|
|
13
|
-
* a review — which is `untrusted-input-defense`'s found-instructions case
|
|
14
|
-
* arriving through a file the repository itself vouches for. The file being
|
|
15
|
-
* tracked is not the control: a tracked file changes in a pull request, and a
|
|
16
|
-
* pull request that changes an injected file is indistinguishable in the diff
|
|
17
|
-
* from one that changes documentation.
|
|
18
|
-
*
|
|
19
|
-
* ── The contract ────────────────────────────────────────────────────────────
|
|
20
|
-
* An injectable artifact carries a sidecar `<file>.attest.json` holding the
|
|
21
|
-
* sha256 of its bytes and the date the attestation was recorded. Before
|
|
22
|
-
* injecting, a caller asks {@link verifyArtifact}:
|
|
23
|
-
*
|
|
24
|
-
* - hash matches → `ok`, inject.
|
|
25
|
-
* - hash differs → `mismatch`, REFUSE. The content changed since a human
|
|
26
|
-
* vouched for it; re-attesting is a deliberate act with a diff.
|
|
27
|
-
* - no sidecar → `unattested`, REFUSE. **Not "allow because nothing said
|
|
28
|
-
* no"** — that default makes the whole mechanism opt-out, and the file an
|
|
29
|
-
* attacker adds is exactly the one with no sidecar.
|
|
30
|
-
* - file missing → `absent`, REFUSE.
|
|
31
|
-
*
|
|
32
|
-
* ── What this does NOT claim ────────────────────────────────────────────────
|
|
33
|
-
* The sidecar is committed beside the artifact, so an attacker who can write
|
|
34
|
-
* both can write a consistent pair. This is NOT a cryptographic signature and
|
|
35
|
-
* does not pretend to be one. What it buys is that the change becomes VISIBLE:
|
|
36
|
-
* a diff that touches an injected file must also touch its attestation, and a
|
|
37
|
-
* hash line changing in a review is a question, where a prose change in the same
|
|
38
|
-
* file is not. Making a change reviewable is the honest claim; making it
|
|
39
|
-
* impossible would need a key this repository does not have.
|
|
40
|
-
*
|
|
41
|
-
* Exit codes: 0 verified · 1 refused (mismatch / unattested / absent) · 2 usage.
|
|
42
|
-
*
|
|
43
|
-
* Usage:
|
|
44
|
-
* ./scripts-run src/scripts/attest_artifact --record <file>
|
|
45
|
-
* ./scripts-run src/scripts/attest_artifact --verify <file>
|
|
46
|
-
* ./scripts-run src/scripts/attest_artifact --verify <file> --json
|
|
47
|
-
*/
|
|
48
|
-
import * as crypto from 'node:crypto';
|
|
49
|
-
import * as fs from 'node:fs';
|
|
50
|
-
import * as path from 'node:path';
|
|
51
|
-
import process from 'node:process';
|
|
52
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
53
|
-
|
|
54
|
-
const _HERE = fileURLToPath(import.meta.url);
|
|
55
|
-
|
|
56
|
-
/** Sidecar suffix. Beside the artifact, so a move takes the attestation with it. */
|
|
57
|
-
export const ATTEST_SUFFIX = '.attest.json';
|
|
58
|
-
|
|
59
|
-
export type AttestStatus = 'ok' | 'mismatch' | 'unattested' | 'absent';
|
|
60
|
-
|
|
61
|
-
export interface Attestation {
|
|
62
|
-
schema_version: 1;
|
|
63
|
-
/** sha256 of the artifact's bytes, hex. */
|
|
64
|
-
sha256: string;
|
|
65
|
-
/** `YYYY-MM-DD` the attestation was recorded. */
|
|
66
|
-
recorded_at: string;
|
|
67
|
-
/** Bytes at record time — a cheap second signal that costs nothing to store. */
|
|
68
|
-
bytes: number;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface Verdict {
|
|
72
|
-
status: AttestStatus;
|
|
73
|
-
/** True only for `ok`. Every other status refuses. */
|
|
74
|
-
inject: boolean;
|
|
75
|
-
detail: string;
|
|
76
|
-
expected?: string;
|
|
77
|
-
actual?: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function sidecarPath(artifact: string): string {
|
|
81
|
-
return `${artifact}${ATTEST_SUFFIX}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function hashFile(artifact: string): string {
|
|
85
|
-
return crypto.createHash('sha256').update(fs.readFileSync(artifact)).digest('hex');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Verify an artifact against its sidecar.
|
|
90
|
-
*
|
|
91
|
-
* Every non-`ok` status sets `inject: false`. That is the whole safety property
|
|
92
|
-
* and it is expressed as data rather than left to each caller's `if`: a caller
|
|
93
|
-
* that reads `verdict.inject` cannot accidentally treat `unattested` as
|
|
94
|
-
* permissive, which is the mistake an `status === 'mismatch'` check invites.
|
|
95
|
-
*/
|
|
96
|
-
export function verifyArtifact(artifact: string, today?: string): Verdict {
|
|
97
|
-
void today;
|
|
98
|
-
if (!fs.existsSync(artifact)) {
|
|
99
|
-
return { status: 'absent', inject: false, detail: `${artifact} does not exist` };
|
|
100
|
-
}
|
|
101
|
-
const side = sidecarPath(artifact);
|
|
102
|
-
if (!fs.existsSync(side)) {
|
|
103
|
-
return {
|
|
104
|
-
status: 'unattested',
|
|
105
|
-
inject: false,
|
|
106
|
-
detail:
|
|
107
|
-
`${artifact} has no ${ATTEST_SUFFIX} sidecar — REFUSED. An unattested artifact is ` +
|
|
108
|
-
'not "allowed because nothing said no": that default makes attestation opt-out, and ' +
|
|
109
|
-
'the file an attacker adds is precisely the one with no sidecar.',
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
let att: Partial<Attestation>;
|
|
113
|
-
try {
|
|
114
|
-
att = JSON.parse(fs.readFileSync(side, 'utf8')) as Partial<Attestation>;
|
|
115
|
-
} catch (e) {
|
|
116
|
-
return {
|
|
117
|
-
status: 'unattested',
|
|
118
|
-
inject: false,
|
|
119
|
-
detail: `${side} does not parse (${e instanceof Error ? e.message : String(e)}) — treated as absent`,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
const actual = hashFile(artifact);
|
|
123
|
-
if (typeof att.sha256 !== 'string' || att.sha256 === '') {
|
|
124
|
-
return { status: 'unattested', inject: false, detail: `${side} carries no sha256`, actual };
|
|
125
|
-
}
|
|
126
|
-
if (att.sha256 !== actual) {
|
|
127
|
-
return {
|
|
128
|
-
status: 'mismatch',
|
|
129
|
-
inject: false,
|
|
130
|
-
detail:
|
|
131
|
-
`${artifact} changed since it was attested — REFUSED. Re-attest deliberately ` +
|
|
132
|
-
`(--record) so the new hash lands in a diff a reviewer sees.`,
|
|
133
|
-
expected: att.sha256,
|
|
134
|
-
actual,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
return { status: 'ok', inject: true, detail: `${artifact} matches its attestation`, expected: att.sha256, actual };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Record (or re-record) an attestation. Deliberate, and it lands in a diff. */
|
|
141
|
-
export function recordAttestation(artifact: string, today = new Date().toISOString().slice(0, 10)): Attestation {
|
|
142
|
-
const att: Attestation = {
|
|
143
|
-
schema_version: 1,
|
|
144
|
-
sha256: hashFile(artifact),
|
|
145
|
-
recorded_at: today,
|
|
146
|
-
bytes: fs.statSync(artifact).size,
|
|
147
|
-
};
|
|
148
|
-
fs.writeFileSync(sidecarPath(artifact), `${JSON.stringify(att, null, 2)}\n`, 'utf8');
|
|
149
|
-
return att;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
153
|
-
const usage =
|
|
154
|
-
'usage: attest_artifact (--record | --verify) <file> [--json]\n' +
|
|
155
|
-
' --verify exits 1 on mismatch, unattested or absent. Refusal is the default.\n';
|
|
156
|
-
if (argv.includes('-h') || argv.includes('--help')) {
|
|
157
|
-
process.stdout.write(usage);
|
|
158
|
-
return 0;
|
|
159
|
-
}
|
|
160
|
-
const asJson = argv.includes('--json');
|
|
161
|
-
const record = argv.includes('--record');
|
|
162
|
-
const verify = argv.includes('--verify');
|
|
163
|
-
const file = argv.find((a) => !a.startsWith('--'));
|
|
164
|
-
if (record === verify || file === undefined) {
|
|
165
|
-
process.stderr.write(`attest_artifact: error: exactly one of --record / --verify plus a file\n${usage}`);
|
|
166
|
-
return 2;
|
|
167
|
-
}
|
|
168
|
-
const target = path.resolve(file);
|
|
169
|
-
if (record) {
|
|
170
|
-
if (!fs.existsSync(target)) {
|
|
171
|
-
process.stderr.write(`attest_artifact: ${file} does not exist\n`);
|
|
172
|
-
return 2;
|
|
173
|
-
}
|
|
174
|
-
const att = recordAttestation(target);
|
|
175
|
-
process.stdout.write(`✅ attested ${file} — sha256 ${att.sha256.slice(0, 16)}… (${String(att.bytes)} bytes)\n`);
|
|
176
|
-
return 0;
|
|
177
|
-
}
|
|
178
|
-
const v = verifyArtifact(target);
|
|
179
|
-
if (asJson) {
|
|
180
|
-
process.stdout.write(`${JSON.stringify(v, null, 2)}\n`);
|
|
181
|
-
} else if (v.inject) {
|
|
182
|
-
process.stdout.write(`✅ ${v.detail}\n`);
|
|
183
|
-
} else {
|
|
184
|
-
process.stderr.write(`❌ [${v.status}] ${v.detail}\n`);
|
|
185
|
-
}
|
|
186
|
-
return v.inject ? 0 : 1;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/* c8 ignore start */
|
|
190
|
-
if (process.argv[1] !== undefined && pathToFileURL(path.resolve(process.argv[1])).href === pathToFileURL(_HERE).href) {
|
|
191
|
-
process.exit(main());
|
|
192
|
-
}
|
|
193
|
-
/* c8 ignore stop */
|