@cratis/pi 7.100.0 → 7.102.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/package/corpus/agents/backend-developer.md +1 -0
- package/package/corpus/agents/browser-operator.md +44 -0
- package/package/corpus/agents/code-reviewer.md +4 -0
- package/package/corpus/agents/coordinator.md +1 -0
- package/package/corpus/agents/frontend-developer.md +1 -0
- package/package/corpus/agents/orchestrator.md +1 -0
- package/package/corpus/agents/performance-reviewer.md +4 -0
- package/package/corpus/agents/planner.md +1 -0
- package/package/corpus/agents/repository-investigation-reviewer.md +4 -0
- package/package/corpus/agents/repository-investigator.md +1 -0
- package/package/corpus/agents/screenplay-modeler.md +1 -0
- package/package/corpus/agents/screenplay-renderer.md +1 -0
- package/package/corpus/agents/screenplay-reviewer.md +4 -0
- package/package/corpus/agents/security-reviewer.md +4 -0
- package/package/corpus/agents/slice-implementer.md +1 -0
- package/package/corpus/agents/spec-writer.md +1 -0
- package/package/corpus/harnesses/copilot/instructions/browser.instructions.md +46 -0
- package/package/corpus/harnesses/copilot/instructions/github-actions.instructions.md +17 -0
- package/package/corpus/harnesses/copilot/instructions/local-work-artifacts.instructions.md +3 -0
- package/package/corpus/harnesses/copilot/instructions/review-reports.instructions.md +11 -0
- package/package/corpus/harnesses/copilot/instructions/secrets.instructions.md +35 -0
- package/package/corpus/harnesses/pi/agents/backend-developer.md +1 -0
- package/package/corpus/harnesses/pi/agents/browser-operator.md +41 -0
- package/package/corpus/harnesses/pi/agents/code-reviewer.md +4 -0
- package/package/corpus/harnesses/pi/agents/coordinator.md +1 -0
- package/package/corpus/harnesses/pi/agents/frontend-developer.md +1 -0
- package/package/corpus/harnesses/pi/agents/orchestrator.md +1 -0
- package/package/corpus/harnesses/pi/agents/performance-reviewer.md +4 -0
- package/package/corpus/harnesses/pi/agents/planner.md +1 -0
- package/package/corpus/harnesses/pi/agents/repository-investigation-reviewer.md +4 -0
- package/package/corpus/harnesses/pi/agents/repository-investigator.md +1 -0
- package/package/corpus/harnesses/pi/agents/screenplay-modeler.md +1 -0
- package/package/corpus/harnesses/pi/agents/screenplay-renderer.md +1 -0
- package/package/corpus/harnesses/pi/agents/screenplay-reviewer.md +4 -0
- package/package/corpus/harnesses/pi/agents/security-reviewer.md +4 -0
- package/package/corpus/harnesses/pi/agents/slice-implementer.md +1 -0
- package/package/corpus/harnesses/pi/agents/spec-writer.md +1 -0
- package/package/corpus/harnesses/pi/extensions/cratis-hooks/index.ts +19 -6
- package/package/corpus/harnesses/pi/extensions/cratis-hooks/quality-gate.ts +10 -1
- package/package/corpus/hooks/README.md +8 -0
- package/package/corpus/hooks/scripts/cratis-guard-secrets.sh +458 -0
- package/package/corpus/hooks/settings.template.json +4 -0
- package/package/corpus/rules/browser.md +45 -0
- package/package/corpus/rules/general.md +2 -0
- package/package/corpus/rules/github-actions.md +17 -0
- package/package/corpus/rules/local-work-artifacts.md +3 -0
- package/package/corpus/rules/review-reports.md +11 -0
- package/package/corpus/rules/secrets.md +34 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/LICENSE +2 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/SKILL.md +201 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/build-once-and-shard.md +158 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/caching.md +86 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/flaky-and-hung-jobs.md +70 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/publish-pipelines.md +67 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/required-checks-and-path-filters.md +117 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/runner-sizing-and-images.md +65 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/security-and-pinning.md +73 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/sources.md +33 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/test-setup-cost.md +82 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/ci-timings.sh +356 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1003.json +79 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1007.json +79 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1010.json +79 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/runs.json +102 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/SKILL.md +244 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/docker-images.md +86 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/dotnet-nuget.md +87 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/go-jvm-python-elixir.md +83 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/npm-yarn.md +78 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/other-environments.md +60 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/worked-examples.md +97 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/cratis-local-packages.sh +778 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/npm-restore-check.cjs +109 -0
- package/package/profile-catalog.json +2 -0
- package/package.json +3 -2
|
@@ -285,6 +285,15 @@ export function tailLines(file: string, lines: number): string {
|
|
|
285
285
|
|
|
286
286
|
export type Fingerprint = { kind: "ok"; value: string; root: string } | { kind: "not-repository" } | { kind: "unknown"; reason?: string };
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Git's discovery failure, in both wordings: "(or any of the parent directories)" and, when discovery stops at a
|
|
290
|
+
* filesystem boundary, "(or any parent up to mount point /tmp)". A broken GIT_DIR ("not a git repository: '<path>'")
|
|
291
|
+
* is not a non-repository, so it stays unknown.
|
|
292
|
+
*/
|
|
293
|
+
export function isNotRepositoryError(stderr: string): boolean {
|
|
294
|
+
return stderr.includes("not a git repository (or any ");
|
|
295
|
+
}
|
|
296
|
+
|
|
288
297
|
/** Digest the working tree; a Git failure or deadline is unknown, never proof that no gate applies. */
|
|
289
298
|
export async function workingTreeFingerprint(cwd: string, timeoutMs = FINGERPRINT_TIMEOUT_MS, signal?: AbortSignal): Promise<Fingerprint> {
|
|
290
299
|
const hash = createHash("sha256");
|
|
@@ -297,7 +306,7 @@ export async function workingTreeFingerprint(cwd: string, timeoutMs = FINGERPRIN
|
|
|
297
306
|
};
|
|
298
307
|
const root = await git(["rev-parse", "--show-toplevel"]);
|
|
299
308
|
if (!root || root.aborted || root.timedOut || root.failed) return { kind: "unknown" };
|
|
300
|
-
if (root.code !== 0) return root.stderr
|
|
309
|
+
if (root.code !== 0) return isNotRepositoryError(root.stderr) ? { kind: "not-repository" } : { kind: "unknown" };
|
|
301
310
|
const status = await git(["status", "--porcelain=v2", "-z", "--untracked-files=all", "--ignore-submodules=none"]);
|
|
302
311
|
if (!status || status.code !== 0 || status.aborted || status.timedOut || status.failed || status.stdoutTruncated) return { kind: "unknown" };
|
|
303
312
|
const head = await git(["rev-parse", "--verify", "-q", "HEAD"]);
|
|
@@ -11,6 +11,7 @@ Three layers:
|
|
|
11
11
|
| Pattern pass | `PostToolUse` on a write | `scripts/cratis-pattern-scan.sh` | zero tokens until a match | appends a one-line reminder to context, never blocks |
|
|
12
12
|
| Hard block | `PreToolUse` on a write | `scripts/cratis-guard-writes.sh` | zero | exits **2** — the write does not happen |
|
|
13
13
|
| Hard block | `PreToolUse` on `Bash` | `scripts/cratis-guard-store-mutations.sh` | zero; parses only commands that mention `cratis` | exits **2** — the store-changing `cratis chronicle` command does not run |
|
|
14
|
+
| Hard block | `PreToolUse` on `Bash` | `scripts/cratis-guard-secrets.sh` | zero; parses only commands that mention a credential-bearing tool or file | exits **2** — a command that prints credentials (full `kubectl` specs and secrets, `env`/`printenv`, `docker inspect`, secret-manager reads, `.env`/kubeconfig/key files) does not run unless it projects a field; see [`secrets.md`](../rules/secrets.md) |
|
|
14
15
|
| Hard block | `PreToolUse` on `Bash` | `scripts/cratis-guard-pr-body.sh` | fetches shared rules only for `gh pr create/edit` | exits **2** for inline bodies, invalid notes or conflicting release labels; warns and allows when offline with no cache |
|
|
15
16
|
| Cleanup of delivered work | `PostToolUse` on `Bash` (`gh pr merge`, `git pull/merge/worktree`) and `SessionStart` (Pi: `tool_result` and `session_start`) | `scripts/cratis-cleanup-worktrees.sh --hook` | zero when the command is unrelated | never blocks; removes delivered worktrees and branches, reports what it kept |
|
|
16
17
|
| Quality gate (Claude) | `Stop` | `scripts/cratis-quality-gate.sh` | one build/test run, only when relevant files changed | exits **2** — the turn does not end |
|
|
@@ -674,6 +675,7 @@ Each is an explicit, auditable opt-out — none of them is a default.
|
|
|
674
675
|
| `CRATIS_HOOKS_PATTERNS=<path>` | replaces the pattern file |
|
|
675
676
|
| `CRATIS_HOOKS_GATES=<path>` | replaces the gate file (the project override still merges over it) |
|
|
676
677
|
| `CRATIS_HOOKS_ALLOW_STORE_MUTATIONS=1` | allows `cratis chronicle` commands that change a live store; set by the person who authorized the mutation, in the environment the harness was started from |
|
|
678
|
+
| `CRATIS_HOOKS_ALLOW_SECRET_READS=1` | allows commands the secrets guard blocks; set by the person who authorized the read, in the environment the harness was started from — an assignment inside the command is ignored |
|
|
677
679
|
| `CRATIS_HOOKS_STORE_MUTATIONS=<path>` | replaces the store-mutation guard's command lists (the `.local.json` beside the script still extends them) |
|
|
678
680
|
| `CRATIS_HOOKS_SUBPATH_REPORT=1` | prints every `@cratis/*` subpath reference and how it resolved, not only the failures |
|
|
679
681
|
| `CRATIS_HOOKS_IMPORT_REPORT=1` | prints every `@cratis/*` named import binding and how it resolved, not only the failures |
|
|
@@ -727,6 +729,12 @@ jq -nc '{tool_name:"Bash", tool_input:{command:"cratis chronicle observers repla
|
|
|
727
729
|
jq -nc '{tool_name:"Bash", tool_input:{command:"cratis chronicle observers list -o plain"}}' \
|
|
728
730
|
| bash .cratis/ai/hooks/scripts/cratis-guard-store-mutations.sh; echo "exit=$?"
|
|
729
731
|
|
|
732
|
+
# Secrets guard, both directions: expect exit 2, then exit 0 for the projected neighbor
|
|
733
|
+
jq -nc '{tool_name:"Bash", tool_input:{command:"kubectl get pods -o json"}}' \
|
|
734
|
+
| bash .cratis/ai/hooks/scripts/cratis-guard-secrets.sh; echo "exit=$?"
|
|
735
|
+
jq -nc '{tool_name:"Bash", tool_input:{command:"kubectl get pods -o name"}}' \
|
|
736
|
+
| bash .cratis/ai/hooks/scripts/cratis-guard-secrets.sh; echo "exit=$?"
|
|
737
|
+
|
|
730
738
|
# Quality gate — show the dispatch plan without running anything
|
|
731
739
|
jq -nc '{session_id:"t", cwd:"'"$PWD"'", stop_hook_active:false}' \
|
|
732
740
|
| CRATIS_HOOKS_GATE_DRYRUN=1 bash .cratis/ai/hooks/scripts/cratis-quality-gate.sh
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse hook (Bash) — hard block on shell commands that print credentials into the transcript.
|
|
3
|
+
#
|
|
4
|
+
# secrets.md says a credential must never enter an agent transcript, and lists the command classes that
|
|
5
|
+
# print one: full Kubernetes specs and secrets, the process environment, container inspection, secret-manager
|
|
6
|
+
# reads and credential files. This guard is what enforces it: every simple command in the shell command is
|
|
7
|
+
# found (quotes, $( ), backticks, subshells, pipelines, here-documents, `bash -c`, `eval`, `kubectl exec ... --`
|
|
8
|
+
# and `docker exec`, and the wrappers sudo/env/timeout/xargs/rtk are looked through) and classified. A command
|
|
9
|
+
# that only mentions a word inside an argument (echo, rg, grep, git commit -m) is never a finding.
|
|
10
|
+
# Exit 2 blocks the tool call and sends stderr back to the model; every other command exits 0.
|
|
11
|
+
#
|
|
12
|
+
# What it sees is the command text. A credential reached through a variable, an alias, a function, a script
|
|
13
|
+
# file or a pipe into a shell is invisible to it; a projection it cannot see through (`-o jsonpath` that dumps
|
|
14
|
+
# `{.}`) is trusted. See .cratis/ai/hooks/README.md.
|
|
15
|
+
#
|
|
16
|
+
# Escape hatch for a human who authorized the read — set in the environment the agent harness was started
|
|
17
|
+
# from, never inside the command (an assignment in the command is ignored):
|
|
18
|
+
# CRATIS_HOOKS_ALLOW_SECRET_READS=1
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
# SCRIPTDIR, not a path relative to the caller: shellcheck resolves a plain relative `source=`
|
|
22
|
+
# against the current working directory, and these hooks are linted from wherever CI happens to run.
|
|
23
|
+
# shellcheck source=SCRIPTDIR/hook-lib.sh
|
|
24
|
+
. "$(dirname "${BASH_SOURCE[0]}")/hook-lib.sh"
|
|
25
|
+
|
|
26
|
+
[ "${CRATIS_HOOKS_ALLOW_SECRET_READS:-0}" = "1" ] && exit 0
|
|
27
|
+
|
|
28
|
+
input="$(hook_read_stdin)"
|
|
29
|
+
[ -n "$input" ] || exit 0
|
|
30
|
+
hook_have jq || exit 0
|
|
31
|
+
|
|
32
|
+
command_text="$(hook_json "$input" '.tool_input.command')"
|
|
33
|
+
[ -n "$command_text" ] || exit 0
|
|
34
|
+
|
|
35
|
+
# Cheap exit for the overwhelming majority of shell commands, which mention none of these words.
|
|
36
|
+
case "$(printf '%s' "$command_text" | tr '[:upper:]' '[:lower:]')" in
|
|
37
|
+
*kubectl*|*printenv*|*env*|*docker*|*podman*|*"gh "*|*vault*|*pulumi*|*aws*|*"az "*|*gcloud*|*kubeconfig*|*.kube*|\
|
|
38
|
+
*export*|*declare*|*typeset*|*id_rsa*|*id_ed25519*|*id_ecdsa*|*id_dsa*|*.pem*|*.p12*|*.pfx*|*token*|*.netrc*|*.npmrc*|\
|
|
39
|
+
*credentials*|*.pgpass*|*secrets*) ;;
|
|
40
|
+
*) exit 0 ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
IFS= read -r -d '' awk_program <<'AWK' || true
|
|
44
|
+
function report(what, alt) {
|
|
45
|
+
if (what in seen) return
|
|
46
|
+
seen[what] = 1
|
|
47
|
+
printf "%s\t%s\n", what, alt
|
|
48
|
+
}
|
|
49
|
+
function enqueue(text) {
|
|
50
|
+
if (text == "" || queued >= 64) return
|
|
51
|
+
queue[++queued] = text
|
|
52
|
+
}
|
|
53
|
+
function base(word) { word = tolower(word); sub(/.*\//, "", word); return word }
|
|
54
|
+
|
|
55
|
+
# ── tokenizer: splits text into simple commands, queueing nested command text ──
|
|
56
|
+
function flush_word() {
|
|
57
|
+
if (inword) W[++nw] = word
|
|
58
|
+
word = ""; inword = 0
|
|
59
|
+
}
|
|
60
|
+
function end_cmd() {
|
|
61
|
+
flush_word()
|
|
62
|
+
if (nw > 0) analyze(W, 1, nw)
|
|
63
|
+
split("", W); nw = 0
|
|
64
|
+
}
|
|
65
|
+
function cmdsub(s, i, n, start, depth, c, j) {
|
|
66
|
+
start = i; depth = 1
|
|
67
|
+
while (i <= n) {
|
|
68
|
+
c = substr(s, i, 1)
|
|
69
|
+
if (c == "\\") { i += 2; continue }
|
|
70
|
+
if (c == "\047") { j = index(substr(s, i + 1), "\047"); if (j == 0) { i = n + 1; break } i += j + 1; continue }
|
|
71
|
+
if (c == "\"") {
|
|
72
|
+
i++
|
|
73
|
+
while (i <= n && substr(s, i, 1) != "\"") { if (substr(s, i, 1) == "\\") i++; i++ }
|
|
74
|
+
i++; continue
|
|
75
|
+
}
|
|
76
|
+
if (c == "(") depth++
|
|
77
|
+
else if (c == ")") { depth--; if (depth == 0) break }
|
|
78
|
+
i++
|
|
79
|
+
}
|
|
80
|
+
enqueue(substr(s, start, i - start))
|
|
81
|
+
return i + 1
|
|
82
|
+
}
|
|
83
|
+
function read_heredocs(s, i, n, h, e, line, cmp, body) {
|
|
84
|
+
for (h = 1; h <= nheredoc; h++) {
|
|
85
|
+
body = ""
|
|
86
|
+
while (i <= n) {
|
|
87
|
+
e = index(substr(s, i), "\n")
|
|
88
|
+
if (e == 0) { line = substr(s, i); i = n + 1 } else { line = substr(s, i, e - 1); i += e }
|
|
89
|
+
cmp = line
|
|
90
|
+
if (HDDASH[h]) sub(/^\t+/, "", cmp)
|
|
91
|
+
if (cmp == HD[h]) break
|
|
92
|
+
body = body line "\n"
|
|
93
|
+
}
|
|
94
|
+
if (hd_shell) enqueue(body)
|
|
95
|
+
}
|
|
96
|
+
nheredoc = 0; hd_shell = 0
|
|
97
|
+
return i
|
|
98
|
+
}
|
|
99
|
+
function tokenize(s, i, n, c, c2, j, dash, delim) {
|
|
100
|
+
n = length(s); i = 1; nw = 0; word = ""; inword = 0; nheredoc = 0; hd_shell = 0
|
|
101
|
+
split("", W)
|
|
102
|
+
while (i <= n) {
|
|
103
|
+
c = substr(s, i, 1)
|
|
104
|
+
if (c == "\\") {
|
|
105
|
+
c2 = substr(s, i + 1, 1)
|
|
106
|
+
if (c2 == "\n") { i += 2; continue }
|
|
107
|
+
word = word c2; inword = 1; i += 2; continue
|
|
108
|
+
}
|
|
109
|
+
if (c == "\047") {
|
|
110
|
+
inword = 1
|
|
111
|
+
j = index(substr(s, i + 1), "\047")
|
|
112
|
+
if (j == 0) { word = word substr(s, i + 1); i = n + 1; continue }
|
|
113
|
+
word = word substr(s, i + 1, j - 1); i += j + 1; continue
|
|
114
|
+
}
|
|
115
|
+
if (c == "\"") {
|
|
116
|
+
inword = 1; i++
|
|
117
|
+
while (i <= n) {
|
|
118
|
+
c = substr(s, i, 1)
|
|
119
|
+
if (c == "\\") { word = word substr(s, i + 1, 1); i += 2; continue }
|
|
120
|
+
if (c == "\"") { i++; break }
|
|
121
|
+
if (c == "$" && substr(s, i + 1, 1) == "(") { i = cmdsub(s, i + 2, n); word = word "x"; continue }
|
|
122
|
+
if (c == "`") {
|
|
123
|
+
j = index(substr(s, i + 1), "`")
|
|
124
|
+
if (j == 0) { i = n + 1; break }
|
|
125
|
+
enqueue(substr(s, i + 1, j - 1)); i += j + 1; word = word "x"; continue
|
|
126
|
+
}
|
|
127
|
+
word = word c; i++
|
|
128
|
+
}
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
if (c == "$" && substr(s, i + 1, 1) == "(") { inword = 1; i = cmdsub(s, i + 2, n); word = word "x"; continue }
|
|
132
|
+
if (c == "`") {
|
|
133
|
+
inword = 1
|
|
134
|
+
j = index(substr(s, i + 1), "`")
|
|
135
|
+
if (j == 0) { i = n + 1; continue }
|
|
136
|
+
enqueue(substr(s, i + 1, j - 1)); i += j + 1; word = word "x"; continue
|
|
137
|
+
}
|
|
138
|
+
if (c == "#" && !inword) {
|
|
139
|
+
j = index(substr(s, i), "\n")
|
|
140
|
+
if (j == 0) { i = n + 1 } else { i += j - 1 }
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
if (c == " " || c == "\t" || c == "\r") { flush_word(); i++; continue }
|
|
144
|
+
if (c == "\n") {
|
|
145
|
+
end_cmd(); i++
|
|
146
|
+
if (nheredoc > 0) i = read_heredocs(s, i, n)
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
if (c == ";" || c == "&" || c == "|" || c == "(" || c == ")") { end_cmd(); i++; continue }
|
|
150
|
+
if (substr(s, i, 2) == "<<" && substr(s, i, 3) != "<<<") {
|
|
151
|
+
flush_word(); i += 2; dash = 0
|
|
152
|
+
if (substr(s, i, 1) == "-") { dash = 1; i++ }
|
|
153
|
+
while (substr(s, i, 1) == " " || substr(s, i, 1) == "\t") i++
|
|
154
|
+
delim = ""
|
|
155
|
+
while (i <= n) {
|
|
156
|
+
c = substr(s, i, 1)
|
|
157
|
+
if (c ~ /[ \t\n;&|()<>]/) break
|
|
158
|
+
if (c != "\047" && c != "\"" && c != "\\") delim = delim c
|
|
159
|
+
i++
|
|
160
|
+
}
|
|
161
|
+
nheredoc++; HD[nheredoc] = delim; HDDASH[nheredoc] = dash
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
word = word c; inword = 1; i++
|
|
165
|
+
}
|
|
166
|
+
end_cmd()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# ── option parsing shared by the CLIs: positionals P/PI, output format, -f/--format value, --raw ──
|
|
170
|
+
function kparse(A, s, n, shortvals, i, w, eq, key, val, f, rest) {
|
|
171
|
+
split("", P); split("", PI)
|
|
172
|
+
np = 0; outfmt = ""; fmtval = ""; dd = 0; raw = 0
|
|
173
|
+
for (i = s; i <= n; i++) {
|
|
174
|
+
w = A[i]
|
|
175
|
+
if (w == "--") { dd = i; break }
|
|
176
|
+
if (substr(w, 1, 2) == "--") {
|
|
177
|
+
eq = index(w, "="); key = w; val = ""
|
|
178
|
+
if (eq) { key = substr(w, 1, eq - 1); val = substr(w, eq + 1) }
|
|
179
|
+
if (key == "--output") { if (!eq) { i++; val = A[i] } outfmt = tolower(val) }
|
|
180
|
+
else if (key == "--format") { if (!eq) { i++; val = A[i] } fmtval = val }
|
|
181
|
+
else if (key == "--raw") raw = 1
|
|
182
|
+
else if (!eq && (key in LV)) i++
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
if (substr(w, 1, 1) == "-" && length(w) > 1) {
|
|
186
|
+
f = substr(w, 2, 1); rest = substr(w, 3)
|
|
187
|
+
if (f == "o") {
|
|
188
|
+
if (rest == "") { i++; val = A[i] } else val = (substr(rest, 1, 1) == "=" ? substr(rest, 2) : rest)
|
|
189
|
+
outfmt = tolower(val)
|
|
190
|
+
} else if (f == "f" && index(shortvals, "f")) {
|
|
191
|
+
if (rest == "") { i++; val = A[i] } else val = (substr(rest, 1, 1) == "=" ? substr(rest, 2) : rest)
|
|
192
|
+
fmtval = val
|
|
193
|
+
} else if (rest == "" && f != "" && index(shortvals, f)) i++
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
np++; P[np] = w; PI[np] = i
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function skipopts(A, s, n, vals) {
|
|
200
|
+
while (s <= n && substr(A[s], 1, 1) == "-") {
|
|
201
|
+
if (length(A[s]) == 2 && index(vals, substr(A[s], 2, 1))) s += 2
|
|
202
|
+
else s++
|
|
203
|
+
}
|
|
204
|
+
return s
|
|
205
|
+
}
|
|
206
|
+
function is_assign(w) { return w ~ /^[A-Za-z_][A-Za-z0-9_]*=/ }
|
|
207
|
+
|
|
208
|
+
# ── file classification for the reader commands ──
|
|
209
|
+
function code_file(b) { return b ~ /\.(cs|ts|tsx|js|jsx|mjs|cjs|go|py|java|kt|rs|md)$/ }
|
|
210
|
+
# A design-token file is not a credential: a stylesheet, or a file under a design, theme or style directory, is
|
|
211
|
+
# fine unless it sits in a secrets or credentials directory. A bare token.json (an OAuth refresh-token file) is not.
|
|
212
|
+
function token_ok(l, b) {
|
|
213
|
+
if (l ~ /(^|\/)(secrets?|credentials?)\//) return 0
|
|
214
|
+
if (b ~ /\.(css|scss|less)$/) return 1
|
|
215
|
+
return l ~ /(^|\/)(design|design-tokens|themes?|styles?)\//
|
|
216
|
+
}
|
|
217
|
+
function sensitive_path(w, l, b) {
|
|
218
|
+
l = tolower(w); b = l; sub(/.*\//, "", b)
|
|
219
|
+
if (l ~ /(^|\/)\.kube\// || b == "kubeconfig" || b ~ /\.kubeconfig$/) return "a kubeconfig"
|
|
220
|
+
if (b ~ /^\.env/ || b ~ /\.env$/) { if (b ~ /\.(example|sample|template|dist|defaults?)$/) return ""; return "an .env file" }
|
|
221
|
+
if (b ~ /\.(pem|p12|pfx)$/) return "a key or certificate file"
|
|
222
|
+
if (b == "id_rsa" || b == "id_ed25519" || b == "id_ecdsa" || b == "id_dsa") return "a private key"
|
|
223
|
+
if (b ~ /(^|[._-])tokens?($|[._-])/ && !code_file(b) && !token_ok(l, b)) return "a token file"
|
|
224
|
+
if (b == ".netrc" || b == ".npmrc" || b == ".pgpass" || b == ".git-credentials") return "a credentials file"
|
|
225
|
+
if (b == "credentials" && l ~ /\.aws\//) return "the AWS credentials file"
|
|
226
|
+
if (l ~ /\.docker\/config\.json$/) return "the Docker registry credentials"
|
|
227
|
+
if (l ~ /\/proc\/[^\/]*\/environ$/) return "a process environment"
|
|
228
|
+
if (l ~ /^\/(var\/)?run\/secrets\//) return "mounted secrets"
|
|
229
|
+
return ""
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# ── tools ──
|
|
233
|
+
function kube(A, s, n, verb, m, parts, k, r, rlist) {
|
|
234
|
+
kparse(A, s + 1, n, "nlcsfkLR")
|
|
235
|
+
verb = P[1]
|
|
236
|
+
if (verb == "exec") {
|
|
237
|
+
if (dd) analyze(A, dd + 1, n)
|
|
238
|
+
else if (np >= 3) analyze(A, PI[3], n)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
if (verb == "config") {
|
|
242
|
+
if (P[2] == "view" && raw) report("kubectl config view --raw prints the cluster credentials", "kubectl config view (redacted), kubectl config current-context, or kubectl config get-contexts")
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
if (verb != "get" && verb != "describe") return
|
|
246
|
+
# `get pods,cm` and `get pod web` name one resource type; `get svc/web secret/db` names one per argument.
|
|
247
|
+
rlist = P[2]
|
|
248
|
+
if (index(P[2], "/")) for (k = 3; k <= np; k++) if (index(P[k], "/")) rlist = rlist "," P[k]
|
|
249
|
+
m = split(tolower(rlist), parts, ",")
|
|
250
|
+
for (k = 1; k <= m; k++) {
|
|
251
|
+
r = parts[k]; sub(/\/.*/, "", r); sub(/\..*/, "", r)
|
|
252
|
+
if (r == "secret" || r == "secrets") {
|
|
253
|
+
if (verb == "describe" || outfmt != "name")
|
|
254
|
+
report("kubectl " verb " secret prints secret data", "kubectl get secrets -o name to list them, or name the secret and key in your report instead of reading it")
|
|
255
|
+
} else if (r in SENS) {
|
|
256
|
+
if (verb == "describe")
|
|
257
|
+
report("kubectl describe " r " prints the full container spec, including inline credentials", "kubectl get " r " -o jsonpath=" SQ "{.items[*].spec.containers[*].image}" SQ ", -o custom-columns=NAME:.metadata.name,STATUS:.status.phase, -o name, or kubectl get events / kubectl logs; say which field you need")
|
|
258
|
+
else if (outfmt == "json" || outfmt == "yaml")
|
|
259
|
+
report("kubectl get " r " -o " outfmt " prints the full container spec, including inline credentials", "-o jsonpath=" SQ "{.items[*].spec.containers[*].image}" SQ ", -o custom-columns=NAME:.metadata.name,STATUS:.status.phase, -o name, -o wide, or the status fields you need")
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function fmt_leaks(f) {
|
|
264
|
+
f = tolower(f)
|
|
265
|
+
return f ~ /env|json|\.args/ || f ~ /[{][{] *\.? *[}][}]/ || f ~ /\.config *[}][}]/
|
|
266
|
+
}
|
|
267
|
+
function dock(A, s, n, off, sub1, nxt, insp, k, listing) {
|
|
268
|
+
kparse(A, s + 1, n, "Hceuwlpfv")
|
|
269
|
+
off = (P[1] == "compose") ? 1 : 0
|
|
270
|
+
sub1 = P[1 + off]; nxt = P[2 + off]
|
|
271
|
+
insp = (sub1 == "inspect") || ((sub1 == "container" || sub1 == "image" || sub1 == "service") && nxt == "inspect")
|
|
272
|
+
if (insp && (fmtval == "" || fmt_leaks(fmtval)))
|
|
273
|
+
report("docker inspect prints the container environment, including credentials", "docker inspect --format " SQ "{{.State.Status}}" SQ " (or {{.Config.Image}}, {{.NetworkSettings.IPAddress}}), docker ps, or docker logs")
|
|
274
|
+
if (off && sub1 == "config") {
|
|
275
|
+
for (k = s + 1; k <= n; k++) if (A[k] ~ /^(--services|--volumes|--profiles|--images|--networks|--quiet|-q)$/) listing = 1
|
|
276
|
+
if (!listing) report("docker compose config prints the resolved environment, including credentials", "docker compose config --services, or read the compose file and name the variables instead of printing their values")
|
|
277
|
+
}
|
|
278
|
+
if (sub1 == "exec" && np >= 3 + off) analyze(A, PI[3 + off], n)
|
|
279
|
+
}
|
|
280
|
+
function gh(A, s, n, i, w, mutating, k) {
|
|
281
|
+
kparse(A, s + 1, n, "XHfFqtR")
|
|
282
|
+
if (P[1] == "secret") {
|
|
283
|
+
if (P[2] != "set" && P[2] != "delete" && P[2] != "remove")
|
|
284
|
+
report("gh secret " P[2] " reads the repository secret store", "gh secret set NAME (writes nothing to the output); refer to secrets by name only")
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
if (P[1] == "auth") {
|
|
288
|
+
if (P[2] == "token") report("gh auth token prints the GitHub token", "gh auth status (without --show-token) to confirm you are signed in")
|
|
289
|
+
else if (P[2] == "status")
|
|
290
|
+
for (i = s + 1; i <= n; i++) if (A[i] == "-t" || A[i] == "--show-token") report("gh auth status --show-token prints the GitHub token", "gh auth status without --show-token")
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
if (P[1] == "api") {
|
|
294
|
+
mutating = 0
|
|
295
|
+
for (i = s + 1; i < n; i++) if (A[i] == "-X" || A[i] == "--method") { w = toupper(A[i + 1]); if (w == "PUT" || w == "PATCH" || w == "DELETE") mutating = 1 }
|
|
296
|
+
if (!mutating)
|
|
297
|
+
for (k = 2; k <= np; k++)
|
|
298
|
+
if (tolower(P[k]) ~ /(^|\/)secrets([\/?]|$)/) report("gh api " P[k] " reads the repository secret store", "gh secret set NAME for writes; name the secret instead of reading it")
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function vault(A, s, n) {
|
|
302
|
+
kparse(A, s + 1, n, "")
|
|
303
|
+
if (P[1] == "read" || (P[1] == "kv" && (P[2] == "get" || P[2] == "read")))
|
|
304
|
+
report("vault " P[1] (P[1] == "kv" ? " " P[2] : "") " prints secret values", "vault kv list or vault kv metadata get for the key names; name the path and key in your report")
|
|
305
|
+
}
|
|
306
|
+
function aws(A, s, n, i, decrypt) {
|
|
307
|
+
kparse(A, s + 1, n, "")
|
|
308
|
+
if (P[1] == "secretsmanager" && P[2] == "get-secret-value") report("aws secretsmanager get-secret-value prints the secret", "aws secretsmanager list-secrets or describe-secret (metadata only)")
|
|
309
|
+
if (P[1] == "configure" && P[2] == "export-credentials") report("aws configure export-credentials prints the credentials", "aws sts get-caller-identity to confirm who you are")
|
|
310
|
+
if (P[1] == "ssm" && P[2] ~ /^get-parameter/) {
|
|
311
|
+
decrypt = 0
|
|
312
|
+
for (i = s + 1; i <= n; i++) if (A[i] == "--with-decryption") decrypt = 1
|
|
313
|
+
if (decrypt) report("aws ssm " P[2] " --with-decryption prints decrypted parameters", "aws ssm describe-parameters, or get-parameter without --with-decryption")
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function azure(A, s, n) {
|
|
317
|
+
kparse(A, s + 1, n, "n")
|
|
318
|
+
if (P[1] == "keyvault" && P[2] == "secret" && (P[3] == "show" || P[3] == "download")) report("az keyvault secret " P[3] " prints the secret", "az keyvault secret list, or az keyvault secret show-deleted metadata only; name the secret in your report")
|
|
319
|
+
if (P[1] == "account" && P[2] == "get-access-token") report("az account get-access-token prints a bearer token", "az account show")
|
|
320
|
+
}
|
|
321
|
+
function gcloud(A, s, n, k) {
|
|
322
|
+
kparse(A, s + 1, n, "")
|
|
323
|
+
if (P[1] == "secrets" && P[2] == "versions" && P[3] == "access") report("gcloud secrets versions access prints the secret", "gcloud secrets versions list, or gcloud secrets describe (metadata only)")
|
|
324
|
+
if (P[1] == "auth") for (k = 2; k <= np; k++) if (P[k] ~ /^print-(access|identity)-token$/) report("gcloud auth " P[k] " prints a token", "gcloud auth list")
|
|
325
|
+
}
|
|
326
|
+
function reader(A, s, n, b, k, w, mode, havepat, need, afterdd, kind, valued, valued2) {
|
|
327
|
+
b = base(A[s]); mode = ""; valued = " "; valued2 = " "
|
|
328
|
+
if (b ~ /^(grep|egrep|fgrep|rg|ag)$/) { mode = "pattern"; valued = " -m -A -B -C -g -t -T -d -D --glob --type --type-not --max-count --include --exclude --exclude-dir --context --after-context --before-context --max-depth --threads " }
|
|
329
|
+
else if (b ~ /^(sed|awk|gawk|jq|yq)$/) { mode = "script"; valued = " -v -F --indent "; valued2 = " --arg --argjson --slurpfile --rawfile " }
|
|
330
|
+
# The first operand is the pattern or script unless -e/-f/--regexp/--file/--expression supplies it; only the rest are files.
|
|
331
|
+
havepat = 0
|
|
332
|
+
if (mode != "") for (k = s + 1; k <= n; k++) if (A[k] ~ /^-[a-zA-Z]*[ef]$/ || A[k] ~ /^--(regexp|file|expression|from-file)(=|$)/) havepat = 1
|
|
333
|
+
need = (mode != "" && !havepat); afterdd = 0
|
|
334
|
+
for (k = s + 1; k <= n; k++) {
|
|
335
|
+
w = A[k]
|
|
336
|
+
if (w == "--" && !afterdd) { afterdd = 1; continue }
|
|
337
|
+
if (!afterdd && substr(w, 1, 1) == "-" && length(w) > 1) {
|
|
338
|
+
if (mode != "" && (w ~ /^-[a-zA-Z]*[ef]$/ || w ~ /^--(regexp|file|expression|from-file)$/ || index(valued, " " w " "))) k++
|
|
339
|
+
else if (index(valued2, " " w " ")) k += 2
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
if (need) { need = 0; continue }
|
|
343
|
+
kind = sensitive_path(w)
|
|
344
|
+
if (kind != "") report(b " of " kind " prints credentials", "name the file and the key you need in your report; ask the user to confirm a value instead of printing it; for an env template read the .env.example")
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function analyze(A, s, n, w, b, k, names, p) {
|
|
349
|
+
while (s <= n) {
|
|
350
|
+
w = A[s]; b = base(w)
|
|
351
|
+
if (is_assign(w)) { s++; continue }
|
|
352
|
+
if (w ~ /^[0-9]*[<>]/) { s += (w ~ /^[0-9]*[<>]+&?-?$/) ? 2 : 1; continue }
|
|
353
|
+
if (b in KW) { s++; continue }
|
|
354
|
+
if (b == "command" || b == "builtin") { s++; if (s <= n && (A[s] == "-v" || A[s] == "-V")) return; continue }
|
|
355
|
+
if (b == "sudo" || b == "doas") { s = skipopts(A, s + 1, n, "ugChprtUD"); continue }
|
|
356
|
+
if (b == "xargs") { s = skipopts(A, s + 1, n, "IniPLdsEa"); continue }
|
|
357
|
+
if (b == "timeout") { s = skipopts(A, s + 1, n, "ks") + 1; continue }
|
|
358
|
+
if (b == "nice" || b == "watch") { s = skipopts(A, s + 1, n, "n"); continue }
|
|
359
|
+
if (b == "nohup" || b == "time" || b == "exec" || b == "setsid") { s = skipopts(A, s + 1, n, ""); continue }
|
|
360
|
+
if (b == "rtk") { s++; if (s <= n && base(A[s]) == "proxy") s++; continue }
|
|
361
|
+
if (b == "env") {
|
|
362
|
+
s++
|
|
363
|
+
while (s <= n) {
|
|
364
|
+
w = A[s]
|
|
365
|
+
if (is_assign(w)) s++
|
|
366
|
+
else if (w == "-u" || w == "-C" || w == "-S" || w == "--unset" || w == "--chdir" || w == "--split-string") s += 2
|
|
367
|
+
else if (substr(w, 1, 1) == "-") s++
|
|
368
|
+
else break
|
|
369
|
+
}
|
|
370
|
+
if (s > n) { report("env with no command prints the whole environment, including credentials (env | ... is blocked too)", "printenv NAME for one variable that is not a credential, or [ -n \"${NAME:-}\" ] && echo set to test that a variable exists"); return }
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
break
|
|
374
|
+
}
|
|
375
|
+
if (s > n) return
|
|
376
|
+
b = base(A[s])
|
|
377
|
+
if (b == "bash" || b == "sh" || b == "zsh" || b == "dash" || b == "ksh") {
|
|
378
|
+
for (k = s + 1; k <= n; k++)
|
|
379
|
+
if (A[k] ~ /^-[a-zA-Z]*c[a-zA-Z]*$/) { enqueue(A[k + 1]); return }
|
|
380
|
+
if (nheredoc > 0) hd_shell = 1
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
if (b == "eval") { p = ""; for (k = s + 1; k <= n; k++) p = p " " A[k]; enqueue(p); return }
|
|
384
|
+
if (b == "kubectl" || b == "oc") return kube(A, s, n)
|
|
385
|
+
if (b == "docker" || b == "podman") return dock(A, s, n)
|
|
386
|
+
if (b == "docker-compose") { split("", T); T[1] = "docker"; T[2] = "compose"; for (k = s + 1; k <= n; k++) T[k - s + 2] = A[k]; return dock(T, 1, n - s + 2) }
|
|
387
|
+
if (b == "gh") return gh(A, s, n)
|
|
388
|
+
if (b == "vault") return vault(A, s, n)
|
|
389
|
+
if (b == "aws") return aws(A, s, n)
|
|
390
|
+
if (b == "az") return azure(A, s, n)
|
|
391
|
+
if (b == "gcloud") return gcloud(A, s, n)
|
|
392
|
+
if (b == "pulumi") {
|
|
393
|
+
for (k = s + 1; k <= n; k++) if (A[k] ~ /^--show-secrets/) report("pulumi --show-secrets prints decrypted secrets", "pulumi config / pulumi stack output without --show-secrets (secrets stay masked)")
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
if (b == "printenv") {
|
|
397
|
+
names = 0
|
|
398
|
+
for (k = s + 1; k <= n; k++) {
|
|
399
|
+
if (substr(A[k], 1, 1) == "-") continue
|
|
400
|
+
names++
|
|
401
|
+
if (tolower(A[k]) ~ /token|secret|key|pass|pwd|credential|auth|cert|conn|dsn/) report("printenv " A[k] " prints a credential", "[ -n \"${" A[k] ":-}\" ] && echo set, which confirms it exists without printing it")
|
|
402
|
+
}
|
|
403
|
+
if (names == 0) report("printenv with no variable name prints the whole environment, including credentials", "printenv with the one non-credential variable you need, e.g. printenv HOME")
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
if (b == "export") {
|
|
407
|
+
names = 0; p = 0
|
|
408
|
+
for (k = s + 1; k <= n; k++) { if (A[k] == "-p") p = 1; else if (substr(A[k], 1, 1) != "-") names++ }
|
|
409
|
+
if (p || names == 0) report("export with no variable name prints the whole environment, including credentials", "printenv with the one non-credential variable you need")
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
if (b == "declare" || b == "typeset") {
|
|
413
|
+
names = 0; p = 0
|
|
414
|
+
for (k = s + 1; k <= n; k++) { if (A[k] ~ /^-[a-zA-Z]*[px][a-zA-Z]*$/) p = 1; else if (substr(A[k], 1, 1) != "-") names++ }
|
|
415
|
+
if (p && names == 0) report(b " -p/-x with no variable name prints the whole environment, including credentials", "printenv with the one non-credential variable you need")
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
if (b ~ /^(cat|tac|nl|less|more|head|tail|bat|batcat|strings|xxd|od|hexdump|cut|sort|uniq|base64|grep|egrep|fgrep|rg|ag|sed|awk|gawk|jq|yq)$/) reader(A, s, n)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
BEGIN {
|
|
422
|
+
SQ = "\047"
|
|
423
|
+
split("namespace context kubeconfig server cluster user as as-group selector field-selector container label-columns sort-by request-timeout filename kustomize template chunk-size token env env-file workdir host config project-name file profile detach-keys log-level method header field raw-field jq repo hostname input region query endpoint-url vault-name subscription resource-group name id version project secret account", L, " ")
|
|
424
|
+
for (i in L) LV["--" L[i]] = 1
|
|
425
|
+
split("! { } if then else elif do while until", L, " ")
|
|
426
|
+
for (i in L) KW[L[i]] = 1
|
|
427
|
+
split("pods po pod deployments deployment deploy statefulsets statefulset sts daemonsets daemonset ds jobs job cronjobs cronjob cj configmaps configmap cm replicasets replicaset rs replicationcontrollers rc all", L, " ")
|
|
428
|
+
for (i in L) SENS[L[i]] = 1
|
|
429
|
+
enqueue(ENVIRON["GS_COMMAND"])
|
|
430
|
+
while (qi < queued) { qi++; tokenize(queue[qi]) }
|
|
431
|
+
}
|
|
432
|
+
AWK
|
|
433
|
+
|
|
434
|
+
findings="$(GS_COMMAND="$command_text" LC_ALL=C awk "$awk_program" </dev/null)"
|
|
435
|
+
[ -n "$findings" ] || exit 0
|
|
436
|
+
|
|
437
|
+
{
|
|
438
|
+
while IFS=$'\t' read -r what alternative; do
|
|
439
|
+
[ -n "$what" ] || continue
|
|
440
|
+
printf 'BLOCKED by cratis-guard-secrets: %s. Use instead: %s.\n' "$what" "$alternative"
|
|
441
|
+
done <<EOF
|
|
442
|
+
$findings
|
|
443
|
+
EOF
|
|
444
|
+
cat <<'EOF'
|
|
445
|
+
|
|
446
|
+
secrets.md: a credential must never enter an agent transcript, and a transcript is saved, summarized and
|
|
447
|
+
shared. Ask for the field you need (a jsonpath, custom-columns, -o name, a status field) and say which one;
|
|
448
|
+
refer to a secret by name (secret/key, environment variable) in notes, briefs and messages, never by value.
|
|
449
|
+
|
|
450
|
+
If a value has already appeared in output: stop, do not copy it onward, report "SECURITY INCIDENT" with the
|
|
451
|
+
tool-call location and the credential kind, and ask for rotation.
|
|
452
|
+
|
|
453
|
+
Do not retry this, rephrase it, or work around the guard (another wrapper, a script file, a variable, an
|
|
454
|
+
alias). A person who authorized the read sets CRATIS_HOOKS_ALLOW_SECRET_READS=1 in the environment the agent
|
|
455
|
+
harness was started from; setting it inside the command has no effect.
|
|
456
|
+
EOF
|
|
457
|
+
} >&2
|
|
458
|
+
exit 2
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"type": "command",
|
|
19
19
|
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.cratis/ai/hooks/scripts/cratis-guard-store-mutations.sh"
|
|
20
20
|
},
|
|
21
|
+
{
|
|
22
|
+
"type": "command",
|
|
23
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.cratis/ai/hooks/scripts/cratis-guard-secrets.sh"
|
|
24
|
+
},
|
|
21
25
|
{
|
|
22
26
|
"type": "command",
|
|
23
27
|
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.cratis/ai/hooks/scripts/cratis-guard-pr-body.sh"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Browser use
|
|
6
|
+
|
|
7
|
+
Browser access is a capability, not authority (see
|
|
8
|
+
[`capability-is-not-authority.md`](./capability-is-not-authority.md)). Every line is tagged
|
|
9
|
+
**[contract]** (binding) or **[convention]** (the house default) per the Three Levels of
|
|
10
|
+
Authority in [`general.md`](./general.md). Read this before any browser call.
|
|
11
|
+
|
|
12
|
+
## Permit and handoff
|
|
13
|
+
|
|
14
|
+
- **[contract] Production is read-only.** Write to a production site only when the user's
|
|
15
|
+
request or an applicable policy names that exact action. A configured browser tool is not
|
|
16
|
+
a permit.
|
|
17
|
+
- **[contract] One production tab per session.** No parallel polling or parallel findings on
|
|
18
|
+
production. Run the health check first, close idle browsers, and look for stale tabs from
|
|
19
|
+
other sessions before opening one.
|
|
20
|
+
- **[convention] Prefer staging or a local copy** for interactive and visual smoke runs.
|
|
21
|
+
- **[contract] Only the `browser-operator` agent receives browser tools.** Hand it the URL,
|
|
22
|
+
scope, permit and artifact directory. The main session does not do browser work itself
|
|
23
|
+
unless no such agent is available. No other agent gets browser tools.
|
|
24
|
+
- **[contract] Stop for a human on sign-in, 2FA or CAPTCHA.** Record
|
|
25
|
+
`UI UNVERIFIED — needs human sign-in` and continue the other lanes. Do not block on it.
|
|
26
|
+
|
|
27
|
+
## Artifacts and uploads
|
|
28
|
+
|
|
29
|
+
- **[contract] Write artifacts and stage uploads only under `.ai-work/<task>/browser/`**
|
|
30
|
+
(see [`local-work-artifacts.md`](./local-work-artifacts.md)). If a path guard blocks you,
|
|
31
|
+
stop and report. Never monkey-patch DOM or file APIs to get around it.
|
|
32
|
+
- **[contract] No secrets or tokens in artifacts, URLs or logs.**
|
|
33
|
+
|
|
34
|
+
## Evidence
|
|
35
|
+
|
|
36
|
+
- **[contract] A UI verification records:** URL, build or version, screenshot path under
|
|
37
|
+
`.ai-work/`, correlation id, timestamp and environment. Without these it is unverified.
|
|
38
|
+
|
|
39
|
+
## Headless production-smoke checklist
|
|
40
|
+
|
|
41
|
+
- **[convention]** Port-forward to the service.
|
|
42
|
+
- **[convention]** Front it with a GET-only proxy that injects the required headers.
|
|
43
|
+
- **[convention]** Drive it from a fresh headless browser with no persistent profile.
|
|
44
|
+
- **[convention]** Give any signed-in profile a single owner.
|
|
45
|
+
- **[convention]** Tear down the browser, proxy and port-forward when done.
|
|
@@ -146,7 +146,9 @@ A claim is only as good as the signal behind it — a build result, a test run,
|
|
|
146
146
|
| MongoDB queries — native driver API over LINQ; paging and sorting belong to Arc | `mongodb.md` |
|
|
147
147
|
| Before writing a commit message, committing, pushing or opening/editing a PR: read both | `git-commits.md`, `pull-requests.md` |
|
|
148
148
|
| Reading, citing and superseding a decision record | the **cratis-engineering-decision-record** skill |
|
|
149
|
+
| Before any browser call: production permit, handoff, artifact paths, evidence | `browser.md` |
|
|
149
150
|
| Whether you are allowed to do the thing you are able to do | `capability-is-not-authority.md` |
|
|
151
|
+
| Before running a command that could print a credential, or when a secret value appears in output | `secrets.md` |
|
|
150
152
|
| What must stop and ask a human | `capability-is-not-authority.md` (absent authority for a consequential effect: stop and ask) |
|
|
151
153
|
| What counts as evidence that something works | `verification-discipline.md` |
|
|
152
154
|
| Before writing a session note, plan or handover: where they may live (`.ai-work/` only) | `local-work-artifacts.md` |
|
|
@@ -44,6 +44,23 @@ paths:
|
|
|
44
44
|
`cancel-in-progress: true`, keyed `${{ github.workflow }}-${{ github.ref }}`.
|
|
45
45
|
**Never** cancel-in-progress on publish, release, or deploy workflows — a cancelled
|
|
46
46
|
half-publish is worse than a queued one.
|
|
47
|
+
- **Publish and release workflows never cancel** (`cancel-in-progress: false`) and set
|
|
48
|
+
`queue: max`. A group holds one running and one pending run; a newer arrival replaces
|
|
49
|
+
the pending one. Merges into the default branch share a group (a push, a repository-wide
|
|
50
|
+
key, or `github.ref` on a merged `pull_request` `closed` event, which is the base
|
|
51
|
+
branch), so without `queue: max` a newer merge replaces the older pending release.
|
|
52
|
+
`queue: max` keeps up to 100 pending runs, processed in order, and applies only while
|
|
53
|
+
`cancel-in-progress` is false or unset. Keep a single pending slot only for a documented
|
|
54
|
+
design where each run releases everything since the last release, so a replaced run
|
|
55
|
+
loses nothing; say so in a comment beside the `concurrency` block.
|
|
56
|
+
- **A required check must not be skippable by `paths:` alone.** A workflow that never
|
|
57
|
+
starts never reports, and the pull request hangs. Gate jobs on a change-detection job
|
|
58
|
+
and require one always-run aggregator job (`if: always()`, checks every `needs.*.result`).
|
|
59
|
+
- **Pin third-party actions by full commit SHA** (version in a comment) and set minimal
|
|
60
|
+
`permissions:` on each job. Run `zizmor` locally on edited workflows: its `unpinned-uses`
|
|
61
|
+
and `excessive-permissions` audits check exactly this. `actionlint` checks syntax,
|
|
62
|
+
expressions and embedded shell, not pinning. Do not assume CI runs either tool.
|
|
63
|
+
- To make a workflow faster, use the `cratis-ci-pipeline-performance` skill.
|
|
47
64
|
|
|
48
65
|
## Matrices and operating systems
|
|
49
66
|
|
|
@@ -53,6 +53,9 @@ dumps, and similar coordination files. These are **work records, not documentati
|
|
|
53
53
|
worktree and branch behind, which is why the script exists. If the branch must outlive the
|
|
54
54
|
task unmerged, leave the worktree and say so in the handoff. Never `--force` a removal; a
|
|
55
55
|
refusal means look, not push harder.
|
|
56
|
+
- **Scan for secrets before archiving or sharing.** Before anything from `.ai-work/` is archived,
|
|
57
|
+
attached or shared, run `gitleaks detect --no-git --source <dir>` (or the repository's equivalent)
|
|
58
|
+
and redact every finding. See [`secrets.md`](./secrets.md).
|
|
56
59
|
- **A decision log is not a work record.** A decision — a durable choice with a
|
|
57
60
|
decider and a date — is documentation: it lives in **`decisions/`** (or the
|
|
58
61
|
repository's documented decisions folder, such as `.cratis/ai.json` `process.decisions.path`) and is reviewed like any other
|
|
@@ -65,3 +65,14 @@ scope limits when combining reports.
|
|
|
65
65
|
When a workflow requires a typed review envelope, preserve its schema and express
|
|
66
66
|
these sections, verdict meanings, and scope limits through its existing fields.
|
|
67
67
|
Do not append a competing Markdown report or invent envelope fields.
|
|
68
|
+
|
|
69
|
+
## Reviewer independence
|
|
70
|
+
|
|
71
|
+
- **[contract]** Run a reviewer on a different provider family than the author's model
|
|
72
|
+
where the harness can select one. The reviewer agents declare
|
|
73
|
+
`reviewIndependence: different-provider` and the `reviewer` model tier.
|
|
74
|
+
- **[contract]** Where the harness cannot select a provider, state `single-provider` in the
|
|
75
|
+
report header and on the Not examined line.
|
|
76
|
+
- **[contract]** A provider or tool error makes the review **Incomplete**. Fall back once
|
|
77
|
+
to the other family. Never report a failed verdict, or Approved, because the reviewer
|
|
78
|
+
could not run.
|