@arbiterforge/ca-pi 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +558 -0
- package/package.json +35 -0
- package/plugins/ca-pi/CHANGELOG.md +1030 -0
- package/plugins/ca-pi/COMMANDS.md +90 -0
- package/plugins/ca-pi/ORCHESTRATOR.md +159 -0
- package/plugins/ca-pi/SKILLS.md +47 -0
- package/plugins/ca-pi/SPRINT.md +142 -0
- package/plugins/ca-pi/agents/INDEX.md +31 -0
- package/plugins/ca-pi/agents/architecture-drift-reviewer.md +86 -0
- package/plugins/ca-pi/agents/auth-crypto-reviewer.md +60 -0
- package/plugins/ca-pi/agents/backend-author.md +60 -0
- package/plugins/ca-pi/agents/checkpoint-aggregator.md +111 -0
- package/plugins/ca-pi/agents/coverage-auditor.md +71 -0
- package/plugins/ca-pi/agents/decision-challenger.md +116 -0
- package/plugins/ca-pi/agents/dependency-reviewer.md +79 -0
- package/plugins/ca-pi/agents/design-quality-reviewer.md +80 -0
- package/plugins/ca-pi/agents/finding-triage.md +86 -0
- package/plugins/ca-pi/agents/frontend-author.md +64 -0
- package/plugins/ca-pi/agents/grader.md +173 -0
- package/plugins/ca-pi/agents/infra-author.md +64 -0
- package/plugins/ca-pi/agents/map-deps.md +35 -0
- package/plugins/ca-pi/agents/map-structure.md +37 -0
- package/plugins/ca-pi/agents/migration-reviewer.md +65 -0
- package/plugins/ca-pi/agents/scout.md +127 -0
- package/plugins/ca-pi/agents/security-reviewer.md +72 -0
- package/plugins/ca-pi/agents/tribunal-lens-reviewer.md +65 -0
- package/plugins/ca-pi/extensions/codearbiter-child.js +1885 -0
- package/plugins/ca-pi/extensions/codearbiter.js +9802 -0
- package/plugins/ca-pi/generated/command-catalog.json +197 -0
- package/plugins/ca-pi/generated/roles.json +213 -0
- package/plugins/ca-pi/helpers/windows-supervisor.js +205 -0
- package/plugins/ca-pi/hooks/_activationlib.py +196 -0
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +208 -0
- package/plugins/ca-pi/hooks/_babysitlib.py +76 -0
- package/plugins/ca-pi/hooks/_bashguardlib.py +1667 -0
- package/plugins/ca-pi/hooks/_boxlib.py +131 -0
- package/plugins/ca-pi/hooks/_colorlib.py +304 -0
- package/plugins/ca-pi/hooks/_durabilitylib.py +186 -0
- package/plugins/ca-pi/hooks/_entrylib.py +41 -0
- package/plugins/ca-pi/hooks/_fmtlib.py +161 -0
- package/plugins/ca-pi/hooks/_gitexec.py +45 -0
- package/plugins/ca-pi/hooks/_githooks.py +920 -0
- package/plugins/ca-pi/hooks/_gitlib.py +110 -0
- package/plugins/ca-pi/hooks/_hooklib.py +595 -0
- package/plugins/ca-pi/hooks/_host.py +115 -0
- package/plugins/ca-pi/hooks/_intentlib.py +242 -0
- package/plugins/ca-pi/hooks/_ledgerlib.py +1035 -0
- package/plugins/ca-pi/hooks/_metricslib.py +709 -0
- package/plugins/ca-pi/hooks/_pathnorm.py +74 -0
- package/plugins/ca-pi/hooks/_planfilelib.py +664 -0
- package/plugins/ca-pi/hooks/_previewlib.py +193 -0
- package/plugins/ca-pi/hooks/_protectedlib.py +312 -0
- package/plugins/ca-pi/hooks/_protectedstatelib.py +411 -0
- package/plugins/ca-pi/hooks/_provenancelib.py +971 -0
- package/plugins/ca-pi/hooks/_prunelib.py +1398 -0
- package/plugins/ca-pi/hooks/_prunepolicy.py +235 -0
- package/plugins/ca-pi/hooks/_readinjectlib.py +1080 -0
- package/plugins/ca-pi/hooks/_releaselib.py +2657 -0
- package/plugins/ca-pi/hooks/_scopelib.py +262 -0
- package/plugins/ca-pi/hooks/_segmentslib.py +278 -0
- package/plugins/ca-pi/hooks/_sensitivelib.py +270 -0
- package/plugins/ca-pi/hooks/_sessionlib.py +78 -0
- package/plugins/ca-pi/hooks/_sloplib.py +244 -0
- package/plugins/ca-pi/hooks/_standuplib.py +214 -0
- package/plugins/ca-pi/hooks/_subagentslib.py +219 -0
- package/plugins/ca-pi/hooks/_taskboardlib.py +1088 -0
- package/plugins/ca-pi/hooks/_updatelib.py +278 -0
- package/plugins/ca-pi/hooks/babysit.py +47 -0
- package/plugins/ca-pi/hooks/boardsync.py +129 -0
- package/plugins/ca-pi/hooks/doctor.py +420 -0
- package/plugins/ca-pi/hooks/git-enforce.py +325 -0
- package/plugins/ca-pi/hooks/hostapi.py +460 -0
- package/plugins/ca-pi/hooks/init-codearbiter.py +225 -0
- package/plugins/ca-pi/hooks/metrics.py +62 -0
- package/plugins/ca-pi/hooks/migration-pass.py +129 -0
- package/plugins/ca-pi/hooks/pi-bridge.py +543 -0
- package/plugins/ca-pi/hooks/post-write-edit.py +231 -0
- package/plugins/ca-pi/hooks/pre-bash.py +90 -0
- package/plugins/ca-pi/hooks/pre-edit.py +284 -0
- package/plugins/ca-pi/hooks/pre-read.py +81 -0
- package/plugins/ca-pi/hooks/pre-write.py +217 -0
- package/plugins/ca-pi/hooks/preview.py +69 -0
- package/plugins/ca-pi/hooks/prune-transcript.py +232 -0
- package/plugins/ca-pi/hooks/releasehash.py +216 -0
- package/plugins/ca-pi/hooks/security-pass.py +139 -0
- package/plugins/ca-pi/hooks/session-start.py +1218 -0
- package/plugins/ca-pi/hooks/statusline.py +736 -0
- package/plugins/ca-pi/hooks/taskwrite.py +351 -0
- package/plugins/ca-pi/hooks/update-refresh.py +51 -0
- package/plugins/ca-pi/hooks/wire-statusline.py +435 -0
- package/plugins/ca-pi/includes/anti-slop-design/INDEX.md +55 -0
- package/plugins/ca-pi/includes/anti-slop-design/color.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/core.md +244 -0
- package/plugins/ca-pi/includes/anti-slop-design/images.md +32 -0
- package/plugins/ca-pi/includes/anti-slop-design/layout.md +45 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-cli.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-dataviz.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-diagram.md +35 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-documents.md +70 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-slides.md +30 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-web.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/typography.md +51 -0
- package/plugins/ca-pi/includes/author-tdd-workflow.md +14 -0
- package/plugins/ca-pi/includes/compaction-charter.md +16 -0
- package/plugins/ca-pi/includes/cut-docs.md +16 -0
- package/plugins/ca-pi/includes/dev-mode.md +30 -0
- package/plugins/ca-pi/includes/farm.md +237 -0
- package/plugins/ca-pi/includes/fresh-verification.md +14 -0
- package/plugins/ca-pi/includes/harvest.md +69 -0
- package/plugins/ca-pi/includes/maturity-coverage.md +102 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +69 -0
- package/plugins/ca-pi/includes/redirect.md +69 -0
- package/plugins/ca-pi/includes/reference-map.md +22 -0
- package/plugins/ca-pi/includes/review-matrix.md +14 -0
- package/plugins/ca-pi/includes/reviewer-contract.md +53 -0
- package/plugins/ca-pi/includes/routing-table.md +47 -0
- package/plugins/ca-pi/includes/security-gate-record.md +22 -0
- package/plugins/ca-pi/includes/smarts/core.md +90 -0
- package/plugins/ca-pi/includes/smarts/decision-log-format.md +56 -0
- package/plugins/ca-pi/routines/INDEX.md +32 -0
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +122 -0
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +151 -0
- package/plugins/ca-pi/routines/context-check/SKILL.md +85 -0
- package/plugins/ca-pi/routines/context-creation/SKILL.md +171 -0
- package/plugins/ca-pi/routines/crypto-compliance/SKILL.md +41 -0
- package/plugins/ca-pi/routines/debug/SKILL.md +99 -0
- package/plugins/ca-pi/routines/decision-lifecycle/SKILL.md +104 -0
- package/plugins/ca-pi/routines/decision-lifecycle/references/adr-template.md +74 -0
- package/plugins/ca-pi/routines/decision-variance/SKILL.md +147 -0
- package/plugins/ca-pi/routines/decompose/SKILL.md +168 -0
- package/plugins/ca-pi/routines/dispatching-parallel-agents/SKILL.md +76 -0
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +83 -0
- package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +91 -0
- package/plugins/ca-pi/routines/post-merge-cleanup/SKILL.md +233 -0
- package/plugins/ca-pi/routines/refactor/SKILL.md +91 -0
- package/plugins/ca-pi/routines/release/SKILL.md +315 -0
- package/plugins/ca-pi/routines/secret-handling/SKILL.md +67 -0
- package/plugins/ca-pi/routines/security-architecture/SKILL.md +63 -0
- package/plugins/ca-pi/routines/skill-author/SKILL.md +108 -0
- package/plugins/ca-pi/routines/skill-author/references/skill-template.md +58 -0
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +149 -0
- package/plugins/ca-pi/routines/subagent-driven-development/references/farm-dispatch.md +145 -0
- package/plugins/ca-pi/routines/tdd/SKILL.md +139 -0
- package/plugins/ca-pi/routines/tribunal/SKILL.md +109 -0
- package/plugins/ca-pi/routines/tribunal/references/ai-markers.md +29 -0
- package/plugins/ca-pi/routines/tribunal/references/cost-and-models.md +64 -0
- package/plugins/ca-pi/routines/tribunal/references/finding-record.md +27 -0
- package/plugins/ca-pi/routines/tribunal/references/issue-filing.md +47 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/appsec.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/architecture.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/coverage.md +20 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/infra.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/migration.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/observability.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/performance.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/reliability.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/secrets-supply.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/test-fidelity.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/typesafety.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/report.md +19 -0
- package/plugins/ca-pi/routines/tribunal/references/schemas.md +58 -0
- package/plugins/ca-pi/routines/tribunal/references/telemetry.md +28 -0
- package/plugins/ca-pi/routines/tribunal/references/triage.md +53 -0
- package/plugins/ca-pi/routines/using-git-worktrees/SKILL.md +85 -0
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +129 -0
- package/plugins/ca-pi/routines/writing-plans/references/farm-plan.md +50 -0
- package/plugins/ca-pi/skills/ca-add-dep/SKILL.md +88 -0
- package/plugins/ca-pi/skills/ca-adr/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-adr-status/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-arbiter/SKILL.md +36 -0
- package/plugins/ca-pi/skills/ca-audit/SKILL.md +51 -0
- package/plugins/ca-pi/skills/ca-btw/SKILL.md +23 -0
- package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +50 -0
- package/plugins/ca-pi/skills/ca-chore/SKILL.md +58 -0
- package/plugins/ca-pi/skills/ca-cleanup/SKILL.md +55 -0
- package/plugins/ca-pi/skills/ca-commands/SKILL.md +21 -0
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +27 -0
- package/plugins/ca-pi/skills/ca-conflict/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-dev/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-doctor/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +105 -0
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-init/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-metrics/SKILL.md +80 -0
- package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +34 -0
- package/plugins/ca-pi/skills/ca-override/SKILL.md +72 -0
- package/plugins/ca-pi/skills/ca-pr/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-preview/SKILL.md +86 -0
- package/plugins/ca-pi/skills/ca-prune/SKILL.md +100 -0
- package/plugins/ca-pi/skills/ca-reconcile/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-refactor/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-release/SKILL.md +57 -0
- package/plugins/ca-pi/skills/ca-review/SKILL.md +63 -0
- package/plugins/ca-pi/skills/ca-spike/SKILL.md +41 -0
- package/plugins/ca-pi/skills/ca-sprint/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-standup/SKILL.md +112 -0
- package/plugins/ca-pi/skills/ca-status/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-task/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-threat-model/SKILL.md +45 -0
- package/plugins/ca-pi/skills/ca-tribunal/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-watch/SKILL.md +78 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# codeArbiter — pre-tag content-hash confirmation (A-2.10).
|
|
3
|
+
#
|
|
4
|
+
# A row's `pre-tag` commands are operator-authored shell that `/ca:release`
|
|
5
|
+
# EXECUTES before composing a tag. `.codearbiter/release-targets.md` is
|
|
6
|
+
# marker-gated under H-22, so changing those commands already costs a fresh
|
|
7
|
+
# authoring marker — but that gate is about WRITING the file. Nothing made the
|
|
8
|
+
# release lane notice that the commands it is about to run are not the ones
|
|
9
|
+
# anybody last looked at.
|
|
10
|
+
#
|
|
11
|
+
# This closes that: the lane records a hash of a row's pre-tag content when an
|
|
12
|
+
# operator confirms it, and refuses to run silently once that hash changes. A
|
|
13
|
+
# changed hash is not a block — it is a re-confirmation prompt. Per ADR-0010
|
|
14
|
+
# this is COOPERATIVE-grade: an agent that skips the check is not stopped by
|
|
15
|
+
# the operating system, only by the lane's own discipline. Stated plainly here
|
|
16
|
+
# rather than implied, because a reader who mistakes it for enforcement would
|
|
17
|
+
# over-trust it.
|
|
18
|
+
#
|
|
19
|
+
# WHY A PYTHON PRODUCER, not a shell redirect. H-19 (`pre-write.py`) blocks
|
|
20
|
+
# Write-tool writes to `.codearbiter/.markers/*` outright, and
|
|
21
|
+
# `_bashguardlib`'s companion rule blocks shell redirects and interpreter
|
|
22
|
+
# invocations that name a gate marker. Both exist because a hand-forged marker
|
|
23
|
+
# is a forged gate pass. The confirmation record therefore needs a sanctioned
|
|
24
|
+
# producer, which is this module.
|
|
25
|
+
#
|
|
26
|
+
# Ships to every governance plugin byte-identically via tools/sync-core.py, so
|
|
27
|
+
# it carries NO fact about any particular repository: the target name, the
|
|
28
|
+
# commands, and the project root all arrive as arguments or from the declared
|
|
29
|
+
# file.
|
|
30
|
+
#
|
|
31
|
+
# Public API:
|
|
32
|
+
# pre_tag_digest(commands) -> str
|
|
33
|
+
# confirmation_path(root, target) -> str
|
|
34
|
+
# read_confirmation(root, target) -> str | None
|
|
35
|
+
# record_confirmation(root, target, digest) -> str
|
|
36
|
+
# confirmation_state(root, target, commands) -> str
|
|
37
|
+
# main(argv) -> int
|
|
38
|
+
#
|
|
39
|
+
# CLI:
|
|
40
|
+
# releasehash.py digest <target> print the current pre-tag digest
|
|
41
|
+
# releasehash.py check <target> exit 0 confirmed, 1 changed, 2 never
|
|
42
|
+
# confirmed, 3/4 declared-file states
|
|
43
|
+
# releasehash.py record <target> mint the confirmation (the sanctioned
|
|
44
|
+
# producer; run only after an operator has
|
|
45
|
+
# actually READ the commands)
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import hashlib
|
|
50
|
+
import os
|
|
51
|
+
import sys
|
|
52
|
+
|
|
53
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
54
|
+
import _releaselib # noqa: E402 — sibling module, flat hooks/ layout
|
|
55
|
+
|
|
56
|
+
DIGEST_VERSION = "1"
|
|
57
|
+
MARKER_DIRNAME = ".markers"
|
|
58
|
+
CONFIRMED_PREFIX = "pre-tag-confirmed-"
|
|
59
|
+
|
|
60
|
+
CONFIRMED = "confirmed"
|
|
61
|
+
CHANGED = "changed"
|
|
62
|
+
NEVER = "never-confirmed"
|
|
63
|
+
NO_COMMANDS = "no-commands"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def pre_tag_digest(commands):
|
|
67
|
+
"""A stable digest of a row's `pre-tag` command list.
|
|
68
|
+
|
|
69
|
+
ORDER-SENSITIVE on purpose. The commands run in declared order and a
|
|
70
|
+
reordering can change what a release checks — a check that ran after a
|
|
71
|
+
rebuild is not the same check as one that ran before it — so swapping
|
|
72
|
+
two entries must invalidate the confirmation, not preserve it.
|
|
73
|
+
|
|
74
|
+
Each command is length-prefixed before joining, so `["ab", "c"]` and
|
|
75
|
+
`["a", "bc"]` cannot collide into one digest. Versioned, so a future
|
|
76
|
+
change to this construction cannot be mistaken for a content change
|
|
77
|
+
by an old recorded value.
|
|
78
|
+
|
|
79
|
+
Pure and non-raising: a non-list, or a list with non-string members,
|
|
80
|
+
degrades to the digest of an empty list rather than raising inside a
|
|
81
|
+
release lane.
|
|
82
|
+
"""
|
|
83
|
+
if not isinstance(commands, (list, tuple)):
|
|
84
|
+
commands = []
|
|
85
|
+
parts = [DIGEST_VERSION]
|
|
86
|
+
for command in commands:
|
|
87
|
+
if not isinstance(command, str):
|
|
88
|
+
continue
|
|
89
|
+
parts.append(f"{len(command)}:{command}")
|
|
90
|
+
joined = "\n".join(parts)
|
|
91
|
+
return hashlib.sha256(joined.encode("utf-8")).hexdigest()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def confirmation_path(root, target):
|
|
95
|
+
"""Where `target`'s confirmation is recorded.
|
|
96
|
+
|
|
97
|
+
One marker PER TARGET, never a shared one: a multi-target repository
|
|
98
|
+
confirming `ca`'s commands must not thereby confirm `ca-pi`'s.
|
|
99
|
+
"""
|
|
100
|
+
# The sanitized stem alone is NOT injective: every disallowed character
|
|
101
|
+
# folds to "_", so `a.b` and `a_b` -- two legal, distinct target names --
|
|
102
|
+
# would share one marker, and confirming either would silently confirm
|
|
103
|
+
# the other. That is precisely the cross-target confirmation this
|
|
104
|
+
# function promises not to do. A short digest of the RAW name restores
|
|
105
|
+
# the one-marker-per-target property while keeping the stem readable for
|
|
106
|
+
# whoever is looking in `.markers/` by eye.
|
|
107
|
+
raw = str(target)
|
|
108
|
+
safe = "".join(ch if (ch.isalnum() or ch in "-_") else "_" for ch in raw)
|
|
109
|
+
digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()[:8]
|
|
110
|
+
return os.path.join(root, ".codearbiter", MARKER_DIRNAME,
|
|
111
|
+
CONFIRMED_PREFIX + f"{safe}-{digest}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def read_confirmation(root, target):
|
|
115
|
+
"""The recorded digest for `target`, or None when never confirmed.
|
|
116
|
+
|
|
117
|
+
Never raises: an unreadable or empty marker reads as "never confirmed",
|
|
118
|
+
which is the conservative answer — it prompts rather than admits.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
with open(confirmation_path(root, target), encoding="utf-8") as handle:
|
|
122
|
+
value = handle.read().strip()
|
|
123
|
+
except (OSError, UnicodeError):
|
|
124
|
+
return None
|
|
125
|
+
return value or None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def record_confirmation(root, target, digest):
|
|
129
|
+
"""Mint the confirmation marker. Returns the path written.
|
|
130
|
+
|
|
131
|
+
THE SANCTIONED PRODUCER. Call this only after an operator has actually
|
|
132
|
+
read the commands — recording a digest nobody looked at converts this
|
|
133
|
+
from a confirmation into a rubber stamp, which is worse than not
|
|
134
|
+
having it, because the lane then reports "confirmed" about content no
|
|
135
|
+
human has seen.
|
|
136
|
+
"""
|
|
137
|
+
path = confirmation_path(root, target)
|
|
138
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
139
|
+
with open(path, "w", encoding="utf-8", newline="\n") as handle:
|
|
140
|
+
handle.write(str(digest).strip() + "\n")
|
|
141
|
+
return path
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def confirmation_state(root, target, commands):
|
|
145
|
+
"""One of `confirmed` / `changed` / `never-confirmed` / `no-commands`.
|
|
146
|
+
|
|
147
|
+
`no-commands` is deliberately distinct from `confirmed`: a row that
|
|
148
|
+
declares no pre-tag commands has nothing to confirm, and reporting it
|
|
149
|
+
as confirmed would claim an operator approved something that does not
|
|
150
|
+
exist. The lane treats both as "proceed", but only one of them is a
|
|
151
|
+
statement about human review.
|
|
152
|
+
"""
|
|
153
|
+
if not commands:
|
|
154
|
+
return NO_COMMANDS
|
|
155
|
+
recorded = read_confirmation(root, target)
|
|
156
|
+
if recorded is None:
|
|
157
|
+
return NEVER
|
|
158
|
+
return CONFIRMED if recorded == pre_tag_digest(commands) else CHANGED
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _row_commands(target, targets_path=None):
|
|
162
|
+
path = targets_path or _releaselib.default_targets_path()
|
|
163
|
+
rows = _releaselib.load_targets(path)
|
|
164
|
+
row = next((r for r in rows if r["target"] == target), None)
|
|
165
|
+
if row is None:
|
|
166
|
+
return None
|
|
167
|
+
return row.get("pre_tag") or []
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main(argv=None):
|
|
171
|
+
"""CLI. See the module header for subcommands and exit codes."""
|
|
172
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
173
|
+
if len(argv) != 2 or argv[0] not in ("digest", "check", "record"):
|
|
174
|
+
sys.stderr.write("usage: releasehash.py {digest|check|record} <target>\n")
|
|
175
|
+
return 2
|
|
176
|
+
command, target = argv
|
|
177
|
+
root = os.path.dirname(os.path.dirname(_releaselib.default_targets_path()))
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
commands = _row_commands(target)
|
|
181
|
+
except _releaselib.ReleaseTargetsError as error:
|
|
182
|
+
sys.stderr.write(f"{type(error).__name__}: {error}\n")
|
|
183
|
+
return _releaselib._targets_error_exit_code(error)
|
|
184
|
+
if commands is None:
|
|
185
|
+
sys.stderr.write(f"unknown release target: {target}\n")
|
|
186
|
+
return 2
|
|
187
|
+
|
|
188
|
+
if command == "digest":
|
|
189
|
+
print(pre_tag_digest(commands))
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
if command == "record":
|
|
193
|
+
path = record_confirmation(root, target, pre_tag_digest(commands))
|
|
194
|
+
print(f"recorded pre-tag confirmation for {target}: {path}")
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
state = confirmation_state(root, target, commands)
|
|
198
|
+
print(state)
|
|
199
|
+
if state in (CONFIRMED, NO_COMMANDS):
|
|
200
|
+
return 0
|
|
201
|
+
if state == CHANGED:
|
|
202
|
+
sys.stderr.write(
|
|
203
|
+
f"releasehash: {target}'s pre-tag commands have CHANGED since they "
|
|
204
|
+
"were last confirmed. Read them, and re-confirm with "
|
|
205
|
+
f"`releasehash.py record {target}` before releasing. These commands "
|
|
206
|
+
"are executed by the release lane, so a change nobody has read is "
|
|
207
|
+
"the case this check exists for.\n")
|
|
208
|
+
return 1
|
|
209
|
+
sys.stderr.write(
|
|
210
|
+
f"releasehash: {target}'s pre-tag commands have never been confirmed. "
|
|
211
|
+
f"Read them, then run `releasehash.py record {target}`.\n")
|
|
212
|
+
return 2
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
sys.exit(main())
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# codeArbiter v2 — record a security-gate pass BOUND to the lines it approved.
|
|
3
|
+
#
|
|
4
|
+
# The crypto-compliance / secret-handling skills (and /ca:override's heavier
|
|
5
|
+
# security ceiling) run this ON PASS, replacing the old `touch` of an empty
|
|
6
|
+
# marker. An empty marker only proved that *some* gate passed *recently*; any
|
|
7
|
+
# different crypto/secret change committed inside the 30-minute freshness
|
|
8
|
+
# window rode through on it (a TOCTOU hole). This helper hashes every added
|
|
9
|
+
# line in the working tree (vs HEAD, plus untracked files) that matches the
|
|
10
|
+
# shared CRYPTO_RE/SECRET_RE and writes the digests to
|
|
11
|
+
# .codearbiter/.markers/security-gate-passed; pre-bash.py H-09b/H-10b then
|
|
12
|
+
# admits a commit only when every sensitive line being committed is in the
|
|
13
|
+
# recorded set.
|
|
14
|
+
#
|
|
15
|
+
# Invoked by skill prose as (interpreter resolved once by presence, never a
|
|
16
|
+
# `python3 X || python X` fold -- #577; recording itself stays idempotent):
|
|
17
|
+
# "$PY" "<plugin>/hooks/security-pass.py"
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import subprocess
|
|
21
|
+
|
|
22
|
+
from _gitexec import git_executable
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
26
|
+
import hostapi # noqa: E402 — host seam (ADR-0011)
|
|
27
|
+
import _entrylib # noqa: E402 — shared run() dispatch (jscpd dedup)
|
|
28
|
+
from _hooklib import ( # noqa: E402
|
|
29
|
+
CRYPTO_RE, SECRET_RE, SECURITY_DIFF_GIT_ARGS, is_sensitive_scan_exempt,
|
|
30
|
+
line_digest, marker_root, project_root, sensitive_scan_added_lines,
|
|
31
|
+
set_host, utf8_stdio, warn, write_text_atomic,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
MAX_UNTRACKED_BYTES = 1_000_000 # an untracked blob bigger than this is not reviewable prose
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_git(args, cwd):
|
|
38
|
+
return subprocess.run(
|
|
39
|
+
[git_executable()] + args, cwd=cwd, capture_output=True, text=True,
|
|
40
|
+
encoding="utf-8", errors="replace", timeout=30,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def file_lines(root, rel):
|
|
45
|
+
p = os.path.join(root, rel)
|
|
46
|
+
try:
|
|
47
|
+
if os.path.getsize(p) > MAX_UNTRACKED_BYTES:
|
|
48
|
+
return []
|
|
49
|
+
with open(p, encoding="utf-8", errors="replace") as f:
|
|
50
|
+
return f.read().splitlines()
|
|
51
|
+
except Exception: # noqa: BLE001
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def candidate_lines(root):
|
|
56
|
+
"""Every line the next commit could introduce: added lines of the
|
|
57
|
+
worktree-vs-HEAD diff (staged and unstaged alike), plus the full content
|
|
58
|
+
of untracked files — `git diff HEAD` never shows those, but they land in
|
|
59
|
+
the staged diff the moment commit-gate stages them.
|
|
60
|
+
|
|
61
|
+
Excludes gate-events.log (#279, is_sensitive_scan_exempt): the crypto/
|
|
62
|
+
secret gate's own machine-written audit sink, which structurally echoes
|
|
63
|
+
the detector's message text back at itself. The diff branch drops those
|
|
64
|
+
lines via the shared path-aware `sensitive_scan_added_lines`; the
|
|
65
|
+
unborn-branch and untracked-file branches skip the file outright since
|
|
66
|
+
they read whole-file content rather than a diff.
|
|
67
|
+
|
|
68
|
+
The diff is read via SECURITY_DIFF_GIT_ARGS (not a bare `["diff", ...]`):
|
|
69
|
+
it pins the `a/`/`b/` prefix format `sensitive_scan_added_lines` depends
|
|
70
|
+
on for path attribution, regardless of the caller's `diff.mnemonicPrefix`
|
|
71
|
+
/ `diff.noprefix` / external-diff config (#279 review MEDIUM-1)."""
|
|
72
|
+
lines = []
|
|
73
|
+
diff = run_git([*SECURITY_DIFF_GIT_ARGS, "HEAD"], root)
|
|
74
|
+
if diff.returncode == 0:
|
|
75
|
+
lines += sensitive_scan_added_lines(diff.stdout)
|
|
76
|
+
else:
|
|
77
|
+
# Unborn branch (no HEAD yet): every tracked file is new content.
|
|
78
|
+
ls = run_git(["ls-files"], root)
|
|
79
|
+
for rel in ls.stdout.splitlines():
|
|
80
|
+
if is_sensitive_scan_exempt(rel):
|
|
81
|
+
continue
|
|
82
|
+
lines += file_lines(root, rel)
|
|
83
|
+
untracked = run_git(["ls-files", "--others", "--exclude-standard"], root)
|
|
84
|
+
for rel in untracked.stdout.splitlines():
|
|
85
|
+
if is_sensitive_scan_exempt(rel):
|
|
86
|
+
continue
|
|
87
|
+
lines += file_lines(root, rel)
|
|
88
|
+
return lines
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main():
|
|
92
|
+
utf8_stdio()
|
|
93
|
+
root = project_root()
|
|
94
|
+
if not os.path.isdir(os.path.join(root, ".codearbiter")):
|
|
95
|
+
warn("no .codearbiter/ here — security-pass.py records nothing outside "
|
|
96
|
+
"an initialized repo")
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
sensitive = [ln for ln in candidate_lines(root)
|
|
99
|
+
if CRYPTO_RE.search(ln) or SECRET_RE.search(ln)]
|
|
100
|
+
# #604: the MARKER root is deliberately NOT `root` above. `root` (plain
|
|
101
|
+
# project_root()) must stay wherever this process is actually running —
|
|
102
|
+
# candidate_lines(root) just scanned exactly that tree's diff, and binding
|
|
103
|
+
# digests to a DIFFERENT tree would review lines nobody staged. But in a
|
|
104
|
+
# linked git worktree, `root` names the worktree's own (gitignored,
|
|
105
|
+
# never-checked-out) `.codearbiter/.markers/`, while the H-09b/H-10b
|
|
106
|
+
# guard reads the marker from the MAIN checkout (D-2) — marker_root()
|
|
107
|
+
# gives the write the same main-checkout answer the guard's read already
|
|
108
|
+
# has, without moving the scan.
|
|
109
|
+
write_root = marker_root()
|
|
110
|
+
marker_dir = os.path.join(write_root, ".codearbiter", ".markers")
|
|
111
|
+
os.makedirs(marker_dir, exist_ok=True)
|
|
112
|
+
marker = os.path.join(marker_dir, "security-gate-passed")
|
|
113
|
+
digests = sorted({line_digest(ln) for ln in sensitive})
|
|
114
|
+
# Atomic write (migration-002): a crash mid-write never leaves a half-written
|
|
115
|
+
# marker, which the backstop would read as an unrecognized digest and force a
|
|
116
|
+
# spurious gate re-run.
|
|
117
|
+
write_text_atomic(marker, "\n".join(digests) + ("\n" if digests else ""))
|
|
118
|
+
print(f"security-gate pass recorded: {len(digests)} sensitive line(s) "
|
|
119
|
+
f"bound to {os.path.relpath(marker, write_root)}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def run(host, argv=None):
|
|
123
|
+
"""Host-seam entry point (ADR-0011): the __main__ guard calls this with the
|
|
124
|
+
plugin's loaded Host. Wraps main() unchanged — main() still communicates
|
|
125
|
+
via sys.exit/stdout/stderr, and its return value stays discarded exactly
|
|
126
|
+
as the old bare `main()` guard discarded it (so the process still exits 0
|
|
127
|
+
on a normal fall-through).
|
|
128
|
+
|
|
129
|
+
Wires `host` live (#257): primes `_hooklib`'s process-cached Host via
|
|
130
|
+
`set_host()` BEFORE main() runs, so any `get_host()` call downstream
|
|
131
|
+
resolves to the SAME instance the caller passed here — no second
|
|
132
|
+
`hostapi.load_host()`, and `run(fake_host)` genuinely exercises
|
|
133
|
+
`fake_host`."""
|
|
134
|
+
return _entrylib.dispatch(host, argv, main, set_host,
|
|
135
|
+
pass_argv=False, propagate_result=False)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
sys.exit(run(hostapi.load_host()) or 0)
|