@cratis/pi 7.102.0 → 7.103.1
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/harnesses/copilot/instructions/profiles.instructions.md +10 -1
- package/package/corpus/harnesses/copilot/instructions/pull-requests.instructions.md +2 -0
- package/package/corpus/harnesses/copilot/instructions/secrets.instructions.md +4 -0
- package/package/corpus/hooks/README.md +11 -1
- package/package/corpus/hooks/scripts/cratis-check-pr.mjs +12 -1
- package/package/corpus/hooks/scripts/cratis-guard-secrets.sh +79 -23
- package/package/corpus/hooks/scripts/cratis-guard-writes.sh +145 -40
- package/package/corpus/rules/profiles.md +10 -1
- package/package/corpus/rules/pull-requests.md +2 -0
- package/package/corpus/rules/secrets.md +4 -0
- package/package/corpus/skills/analyzing-dotnet-performance/LICENSE +23 -0
- package/package/corpus/skills/analyzing-dotnet-performance/NOTICE +36 -0
- package/package/corpus/skills/analyzing-dotnet-performance/SKILL.md +214 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/async-patterns.md +122 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/collections-and-linq.md +220 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/critical-patterns.md +291 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/io-and-serialization.md +135 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/memory-and-strings.md +219 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/regex-patterns.md +95 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/structural-patterns.md +38 -0
- package/package/corpus/skills/binlog-generation/LICENSE +23 -0
- package/package/corpus/skills/binlog-generation/NOTICE +22 -0
- package/package/corpus/skills/binlog-generation/SKILL.md +129 -0
- package/package/corpus/skills/check-bin-obj-clash/LICENSE +23 -0
- package/package/corpus/skills/check-bin-obj-clash/NOTICE +27 -0
- package/package/corpus/skills/check-bin-obj-clash/SKILL.md +127 -0
- package/package/corpus/skills/check-bin-obj-clash/references/common-causes-and-fixes.md +206 -0
- package/package/corpus/skills/coverage-analysis/LICENSE +23 -0
- package/package/corpus/skills/coverage-analysis/NOTICE +32 -0
- package/package/corpus/skills/coverage-analysis/SKILL.md +198 -0
- package/package/corpus/skills/coverage-analysis/references/guidelines.md +68 -0
- package/package/corpus/skills/coverage-analysis/references/output-format.md +91 -0
- package/package/corpus/skills/coverage-analysis/references/report-generation.md +58 -0
- package/package/corpus/skills/coverage-analysis/references/setup-discovery.md +307 -0
- package/package/corpus/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 +80 -0
- package/package/corpus/skills/coverage-analysis/scripts/CoverageData.ps1 +272 -0
- package/package/corpus/skills/coverage-analysis/scripts/Extract-MethodCoverage.ps1 +65 -0
- package/package/corpus/skills/cratis-screenplay-command-surface/SKILL.md +26 -1
- package/package/corpus/skills/cratis-screenplay-projections/SKILL.md +17 -0
- package/package/corpus/skills/cratis-screenplay-render-and-gap-fill/SKILL.md +17 -0
- package/package/corpus/skills/cratis-screenplay-specifications/SKILL.md +8 -0
- package/package/corpus/skills/cratis-screenplay-toolchain/SKILL.md +1 -1
- package/package/corpus/skills/cratis-screenplay-toolchain/references/cheat-sheet-example.md +1 -0
- package/package/corpus/skills/cratis-screenplay-toolchain/references/fast-path.md +7 -0
- package/package/corpus/skills/cratis-screenplay-toolchain/references/traps.md +1 -0
- package/package/corpus/skills/cratis-screenplay-ui-composition/SKILL.md +40 -0
- package/package/corpus/skills/cratis-screenplay-ui-composition/references/response-continuations.md +1 -0
- package/package/corpus/skills/cratis-screenplay-ui-composition/references/worked-flow.md +1 -0
- package/package/corpus/skills/cratis-stage-rendering-and-sandbox/references/admission.md +3 -2
- package/package/corpus/skills/csharp-refactoring/LICENSE +23 -0
- package/package/corpus/skills/csharp-refactoring/NOTICE +22 -0
- package/package/corpus/skills/csharp-refactoring/SKILL.md +175 -0
- package/package/corpus/skills/csharp-refactoring/references/operation-catalog.md +37 -0
- package/package/corpus/skills/directory-build-organization/LICENSE +23 -0
- package/package/corpus/skills/directory-build-organization/NOTICE +21 -0
- package/package/corpus/skills/directory-build-organization/SKILL.md +205 -0
- package/package/corpus/skills/directory-build-organization/references/common-patterns.md +56 -0
- package/package/corpus/skills/directory-build-organization/references/multi-level-examples.md +167 -0
- package/package/corpus/skills/directory-build-organization/references/targetframework-props-pitfall.md +54 -0
- package/package/corpus/skills/dotnet-aot-compat/LICENSE +23 -0
- package/package/corpus/skills/dotnet-aot-compat/NOTICE +18 -0
- package/package/corpus/skills/dotnet-aot-compat/SKILL.md +265 -0
- package/package/corpus/skills/dotnet-aot-compat/references/polyfills.md +43 -0
- package/package/corpus/skills/dotnet-trace-collect/LICENSE +23 -0
- package/package/corpus/skills/dotnet-trace-collect/NOTICE +65 -0
- package/package/corpus/skills/dotnet-trace-collect/SKILL.md +116 -0
- package/package/corpus/skills/dotnet-trace-collect/references/dotnet-monitor.md +97 -0
- package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect-linux.md +84 -0
- package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect.md +91 -0
- package/package/corpus/skills/dotnet-trace-collect/references/perfcollect.md +200 -0
- package/package/corpus/skills/dotnet-trace-collect/references/perfview.md +141 -0
- package/package/corpus/skills/dotnet-trace-collect/references/tool-selection.md +160 -0
- package/package/corpus/skills/dump-collect/LICENSE +23 -0
- package/package/corpus/skills/dump-collect/NOTICE +40 -0
- package/package/corpus/skills/dump-collect/SKILL.md +97 -0
- package/package/corpus/skills/dump-collect/references/container-dumps.md +412 -0
- package/package/corpus/skills/dump-collect/references/coreclr-dumps.md +108 -0
- package/package/corpus/skills/dump-collect/references/nativeaot-dumps.md +280 -0
- package/package/corpus/skills/eliminate-redundant-restores/LICENSE +23 -0
- package/package/corpus/skills/eliminate-redundant-restores/NOTICE +19 -0
- package/package/corpus/skills/eliminate-redundant-restores/SKILL.md +112 -0
- package/package/corpus/skills/extension-points/LICENSE +23 -0
- package/package/corpus/skills/extension-points/NOTICE +18 -0
- package/package/corpus/skills/extension-points/SKILL.md +248 -0
- package/package/corpus/skills/filter-syntax/LICENSE +23 -0
- package/package/corpus/skills/filter-syntax/NOTICE +17 -0
- package/package/corpus/skills/filter-syntax/SKILL.md +179 -0
- package/package/corpus/skills/including-generated-files/LICENSE +23 -0
- package/package/corpus/skills/including-generated-files/NOTICE +17 -0
- package/package/corpus/skills/including-generated-files/SKILL.md +139 -0
- package/package/corpus/skills/microbenchmarking/LICENSE +23 -0
- package/package/corpus/skills/microbenchmarking/NOTICE +36 -0
- package/package/corpus/skills/microbenchmarking/SKILL.md +131 -0
- package/package/corpus/skills/microbenchmarking/references/bdn-internals-and-tuning.md +143 -0
- package/package/corpus/skills/microbenchmarking/references/comparison-strategies.md +200 -0
- package/package/corpus/skills/microbenchmarking/references/diagnosers-and-exporters.md +150 -0
- package/package/corpus/skills/microbenchmarking/references/project-setup-and-running.md +189 -0
- package/package/corpus/skills/microbenchmarking/references/writing-benchmarks.md +310 -0
- package/package/corpus/skills/msbuild/LICENSE +23 -0
- package/package/corpus/skills/msbuild/NOTICE +55 -0
- package/package/corpus/skills/msbuild/SKILL.md +117 -0
- package/package/corpus/skills/msbuild/references/antipatterns/additional-antipatterns.md +291 -0
- package/package/corpus/skills/msbuild/references/antipatterns/incremental-build-inputs-outputs.md +57 -0
- package/package/corpus/skills/msbuild/references/antipatterns/private-assets.md +29 -0
- package/package/corpus/skills/msbuild/references/binlog-failure-analysis.md +85 -0
- package/package/corpus/skills/msbuild/references/binlog-generation.md +69 -0
- package/package/corpus/skills/msbuild/references/build-perf-baseline.md +493 -0
- package/package/corpus/skills/msbuild/references/build-perf-diagnostics.md +268 -0
- package/package/corpus/skills/msbuild/references/copy-to-output-directory.md +183 -0
- package/package/corpus/skills/msbuild/references/eval-performance.md +190 -0
- package/package/corpus/skills/msbuild/references/extension-points.md +281 -0
- package/package/corpus/skills/msbuild/references/incremental-build.md +365 -0
- package/package/corpus/skills/msbuild/references/msbuild-antipatterns.md +401 -0
- package/package/corpus/skills/orleans/LICENSE +21 -0
- package/package/corpus/skills/orleans/NOTICE +64 -0
- package/package/corpus/skills/orleans/SKILL.md +140 -0
- package/package/corpus/skills/orleans/references/anti-patterns.md +660 -0
- package/package/corpus/skills/orleans/references/configuration-api.md +933 -0
- package/package/corpus/skills/orleans/references/examples.md +66 -0
- package/package/corpus/skills/orleans/references/grain-api.md +784 -0
- package/package/corpus/skills/orleans/references/grains.md +75 -0
- package/package/corpus/skills/orleans/references/hosting.md +71 -0
- package/package/corpus/skills/orleans/references/implementation.md +591 -0
- package/package/corpus/skills/orleans/references/official-docs-index.md +250 -0
- package/package/corpus/skills/orleans/references/patterns.md +811 -0
- package/package/corpus/skills/orleans/references/persistence-api.md +495 -0
- package/package/corpus/skills/orleans/references/primitive-selection.md +35 -0
- package/package/corpus/skills/orleans/references/scheduling-and-services.md +295 -0
- package/package/corpus/skills/orleans/references/serialization-api.md +299 -0
- package/package/corpus/skills/orleans/references/streaming-api.md +396 -0
- package/package/corpus/skills/orleans/references/testing-patterns.md +210 -0
- package/package/corpus/skills/platform-detection/LICENSE +23 -0
- package/package/corpus/skills/platform-detection/NOTICE +17 -0
- package/package/corpus/skills/platform-detection/SKILL.md +230 -0
- package/package/corpus/skills/platform-detection/references/command-mode.md +27 -0
- package/package/corpus/skills/profiling/LICENSE +21 -0
- package/package/corpus/skills/profiling/NOTICE +50 -0
- package/package/corpus/skills/profiling/SKILL.md +144 -0
- package/package/corpus/skills/profiling/references/commands.md +439 -0
- package/package/corpus/skills/profiling/references/patterns.md +586 -0
- package/package/corpus/skills/profiling/references/profiling.md +190 -0
- package/package/corpus/skills/run-tests/LICENSE +23 -0
- package/package/corpus/skills/run-tests/NOTICE +19 -0
- package/package/corpus/skills/run-tests/SKILL.md +305 -0
- package/package/corpus/skills/target-authoring/LICENSE +23 -0
- package/package/corpus/skills/target-authoring/NOTICE +17 -0
- package/package/corpus/skills/target-authoring/SKILL.md +165 -0
- package/package/corpus/skills/vectorization/LICENSE +23 -0
- package/package/corpus/skills/vectorization/NOTICE +17 -0
- package/package/corpus/skills/vectorization/SKILL.md +189 -0
- package/package/profile-catalog.json +85 -1
- package/package.json +1 -1
|
@@ -87,11 +87,20 @@ profile includes neither Screenplay nor Stage.
|
|
|
87
87
|
| Profile ID | Description | Automatically Includes |
|
|
88
88
|
|---|---|---|
|
|
89
89
|
| `cratis/engineering` | Engineering conventions and workflows | `cratis/engineering/core`, `cratis/engineering/csharp`, `cratis/engineering/elixir`, `cratis/engineering/go`, `cratis/engineering/kotlin`, `cratis/engineering/react`, `cratis/engineering/typescript` |
|
|
90
|
-
| `cratis/engineering/csharp` | C# engineering conventions and framework Orleans jobs (`cratis-orleans-jobs`, alongside `orleans.md`) | `cratis/engineering/core` |
|
|
90
|
+
| `cratis/engineering/csharp` | C# engineering conventions, .NET engineering skills, and framework Orleans jobs (`cratis-orleans-jobs`, alongside `orleans.md`) | `cratis/engineering/core`, `cratis/engineering/dotnet` |
|
|
91
|
+
| `cratis/engineering/dotnet` | Upstream .NET engineering workflows | `cratis/engineering/dotnet/performance`, `cratis/engineering/dotnet/diagnostics`, `cratis/engineering/dotnet/msbuild`, `cratis/engineering/dotnet/orleans`, `cratis/engineering/dotnet/testing`, `cratis/engineering/dotnet/refactoring` |
|
|
92
|
+
| `cratis/engineering/dotnet/performance` | Microbenchmarking, performance patterns, SIMD, and profiling | (no child profiles) |
|
|
93
|
+
| `cratis/engineering/dotnet/diagnostics` | Trace and crash-dump collection | (no child profiles) |
|
|
94
|
+
| `cratis/engineering/dotnet/msbuild` | MSBuild troubleshooting, build authoring, output paths, and binlogs | (no child profiles) |
|
|
95
|
+
| `cratis/engineering/dotnet/orleans` | Orleans primitives, hosting, persistence, and runtime behavior | (no child profiles) |
|
|
96
|
+
| `cratis/engineering/dotnet/testing` | Test-platform detection, runner commands, filters, and coverage | (no child profiles) |
|
|
97
|
+
| `cratis/engineering/dotnet/refactoring` | Behavior-preserving C# refactoring and AOT compatibility | (no child profiles) |
|
|
91
98
|
| `cratis/engineering/go` | Go framework parity and immutable module releases | `cratis/engineering/core`, `cratis/language/go` |
|
|
92
99
|
| `cratis/engineering/typescript` | TypeScript engineering conventions | `cratis/engineering/core` |
|
|
93
100
|
| `cratis/engineering/react` | React engineering conventions | `cratis/engineering/core` |
|
|
94
101
|
|
|
102
|
+
The `cratis/engineering/dotnet` profiles carry upstream skills under MIT, with attribution and changes in each skill's NOTICE. Cratis rules and the `cratis-*` skills win where they differ.
|
|
103
|
+
|
|
95
104
|
The `cratis/engineering` profile also carries [issue-assignment.md](../../../rules/issue-assignment.md): when an agent starts work on a numbered issue in a known repository and nobody is assigned, it assigns the current `gh` user.
|
|
96
105
|
|
|
97
106
|
### Language Profiles
|
|
@@ -9,6 +9,8 @@ applyTo: "**/*"
|
|
|
9
9
|
|
|
10
10
|
**The PR description is published verbatim as the GitHub release. In repositories released by `cratis/release-action`, it also closes every issue written as `(#n)`; elsewhere, follow the repository's own release and issue-closing process.** Write the note the person upgrading should read, not a development write-up. The same contract applies to release notes typed into a manual workflow run or written straight into a release. Where it is installed, the `policy / verify` check enforces it on release-bound PRs into the default branch carrying `major`/`minor`/`patch` (it re-runs when the label is added); its release-note step warns on `no-release` and unlabelled PRs, which must still be written this way because any of them may become releasable. `release-action` does not run for a `no-release` PR, so `(#n)` closes nothing there. Editing the description re-runs the check. HTML comments are not shown on the release page and the check ignores them for headings, keywords and placeholders, and `release-action` removes them before publishing, so a `(#n)` only inside a comment closes nothing; the check fails it, so never put an issue reference in a comment. The template's own comment can stay: its `(#123)` examples are inline code, and the comment is not published.
|
|
11
11
|
|
|
12
|
+
**A change to a hook script or its wiring (`.cratis/ai/hooks/**`) needs a release-note line that tells consumers to run `cratis ai update`; a hook fix does not reach a repository until they do.**
|
|
13
|
+
|
|
12
14
|
### Allowed shape
|
|
13
15
|
|
|
14
16
|
- Follow the repository's pull request template (`.github/pull_request_template.md`) within this contract.
|
|
@@ -21,6 +21,10 @@ Every line is tagged **[contract]** (binding) per the Three Levels of Authority
|
|
|
21
21
|
- **[contract] Project the field you need and say which one:** `-o jsonpath='{.spec.containers[*].image}'`,
|
|
22
22
|
`-o custom-columns=…`, `kubectl get … -o name`, status fields, `kubectl logs`, `docker inspect --format`.
|
|
23
23
|
Read a `.env.example`, never the `.env`.
|
|
24
|
+
- **[contract] The hook reads command text only.** It cannot see a command piped into a shell
|
|
25
|
+
(`… | sh`, `bash -c "$(…)"`), a recursive search with no named file (`grep -r PASSWORD .`), or a
|
|
26
|
+
value reached through a variable, alias or script. These stay your rule to keep; output redaction
|
|
27
|
+
is the only backstop.
|
|
24
28
|
|
|
25
29
|
## Values
|
|
26
30
|
|
|
@@ -11,7 +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
|
+
| 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; also behind `/usr/bin/env`, `command`, attached options such as `-ePATTERN`, a bare `{.}` projection) does not run unless it projects a field; piped shells and recursive searches with no named file stay uncovered; see [`secrets.md`](../rules/secrets.md) |
|
|
15
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 |
|
|
16
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 |
|
|
17
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 |
|
|
@@ -252,6 +252,16 @@ execution, so it cannot validate a body that the command has yet to write, even
|
|
|
252
252
|
already exists. Dependabot's generated description remains exempt (including GitHub CLI's
|
|
253
253
|
`app/dependabot` bot identity), but its release-intent label is still checked.
|
|
254
254
|
|
|
255
|
+
A release that changes a hook script or its wiring must say in its release notes that consumers run
|
|
256
|
+
`cratis ai update`: hooks are managed files, so a fix reaches a repository only once it updates.
|
|
257
|
+
|
|
258
|
+
The direct checker exits non-zero, with remediation text, in a `github.com/Cratis/` repository whose
|
|
259
|
+
workflows hold no policy caller (none of the recognized callers above): without one, nothing enforces the
|
|
260
|
+
pull-request contract there, and a pass would claim otherwise. A repository outside Cratis still falls back to
|
|
261
|
+
the reviewed pin below, and the Bash guard stays a no-op where there is no caller. A contract spec in the AI corpus's own
|
|
262
|
+
verification runs the checker over vendored copies of the callers Cratis/Workflows publishes; with
|
|
263
|
+
`CRATIS_CONTRACT_LIVE=1` it compares them with Cratis/Workflows `main`.
|
|
264
|
+
|
|
255
265
|
Run the same checker directly:
|
|
256
266
|
|
|
257
267
|
```bash
|
|
@@ -71,6 +71,13 @@ function callers(cwd = process.cwd()) {
|
|
|
71
71
|
} catch { return []; }
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function missingCaller(repository) {
|
|
75
|
+
return `No Cratis/Workflows policy caller found for ${repository}, so its pull-request contract is not enforced and this check cannot vouch for it. `
|
|
76
|
+
+ 'Add .github/workflows/verify-pull-request-policy.yml calling Cratis/Workflows/.github/workflows/verify-pull-request-policy.yml '
|
|
77
|
+
+ '(a legacy verify-release-notes, verify-semver-label or verify-release-intent caller also counts), then run this check again. '
|
|
78
|
+
+ 'Cratis/Workflows bootstrap-common-workflows installs the caller; if the repository is meant to have none, do not use this check there.';
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
function repositoryName(value) {
|
|
75
82
|
return value?.replace(/^(?:https?:\/\/github\.com\/|(?:ssh:\/\/)?git@github\.com[:/])/i, '')
|
|
76
83
|
.replace(/^github\.com\//i, '').replace(/\/$/, '').replace(/\.git$/i, '');
|
|
@@ -216,7 +223,11 @@ function check(argv, repositoryCallers, lookupOptions) {
|
|
|
216
223
|
// The release-intent check above still applies to bot-authored pull requests.
|
|
217
224
|
if (author === 'dependabot[bot]') return 0;
|
|
218
225
|
const base = values.base || pull?.baseRefName || defaultBranch;
|
|
219
|
-
const
|
|
226
|
+
const found = repositoryCallers || targetCallers(repository, process.cwd(), warn);
|
|
227
|
+
// A Cratis repository without a policy caller has no enforced pull-request contract; passing here would
|
|
228
|
+
// let the author believe it has one. A fetch failure is not this case: targetCallers falls back to a caller.
|
|
229
|
+
if (!found.length && /^Cratis\/[^/]+$/i.test(repository)) throw new Error(missingCaller(repository));
|
|
230
|
+
const file = rules(warn, found);
|
|
220
231
|
if (!file) return 3;
|
|
221
232
|
const env = { PATH: process.env.PATH, HOME: homedir(), LANG: 'C.UTF-8',
|
|
222
233
|
PR_BODY: body, PR_LABELS: JSON.stringify([...labels]),
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
# Exit 2 blocks the tool call and sends stderr back to the model; every other command exits 0.
|
|
11
11
|
#
|
|
12
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
|
|
14
|
-
# `{.}`) is trusted.
|
|
13
|
+
# file or a pipe into a shell is invisible to it, as is a recursive search with no named file; a projection it
|
|
14
|
+
# cannot see through (a template or `-o jsonpath` that is not a bare `{.}`) is trusted. A redirect does not
|
|
15
|
+
# exempt anything: `env > f` is blocked like `env`. See .cratis/ai/hooks/README.md.
|
|
15
16
|
#
|
|
16
17
|
# Escape hatch for a human who authorized the read — set in the environment the agent harness was started
|
|
17
18
|
# from, never inside the command (an assignment in the command is ignored):
|
|
@@ -54,13 +55,26 @@ function base(word) { word = tolower(word); sub(/.*\//, "", word); return word }
|
|
|
54
55
|
|
|
55
56
|
# ── tokenizer: splits text into simple commands, queueing nested command text ──
|
|
56
57
|
function flush_word() {
|
|
57
|
-
if (inword) W[++nw] = word
|
|
58
|
-
word = ""; inword = 0
|
|
58
|
+
if (inword) { W[++nw] = word; WP[nw] = wplain }
|
|
59
|
+
word = ""; inword = 0; wquoted = 0; wplain = 0
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
# Output redirections are removed before a command is analyzed, so a redirect never shifts a CLI's subcommand
|
|
62
|
+
# or operands (`kubectl get > /dev/stdout secrets`). Nothing is exempted: the command itself is still analyzed.
|
|
63
|
+
# A word is a redirection only when its whole operator lies in the word's unquoted prefix (WP: characters before
|
|
64
|
+
# the first quoted or escaped one), so `1">"b` and `">x"` stay operands; input redirections (`< file`) stay too.
|
|
65
|
+
function end_cmd( k, m, w) {
|
|
61
66
|
flush_word()
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
m = 0
|
|
68
|
+
for (k = 1; k <= nw; k++) {
|
|
69
|
+
w = W[k]
|
|
70
|
+
if (match(w, /^([0-9]*|&)>[>|]?/) && RLENGTH <= WP[k]) {
|
|
71
|
+
if (RLENGTH == length(w)) k++
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
V[++m] = w
|
|
75
|
+
}
|
|
76
|
+
if (m > 0) analyze(V, 1, m)
|
|
77
|
+
split("", W); split("", WP); split("", V); nw = 0
|
|
64
78
|
}
|
|
65
79
|
function cmdsub(s, i, n, start, depth, c, j) {
|
|
66
80
|
start = i; depth = 1
|
|
@@ -97,23 +111,23 @@ function read_heredocs(s, i, n, h, e, line, cmp, body) {
|
|
|
97
111
|
return i
|
|
98
112
|
}
|
|
99
113
|
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)
|
|
114
|
+
n = length(s); i = 1; nw = 0; word = ""; inword = 0; wquoted = 0; wplain = 0; nheredoc = 0; hd_shell = 0
|
|
115
|
+
split("", W); split("", WP)
|
|
102
116
|
while (i <= n) {
|
|
103
117
|
c = substr(s, i, 1)
|
|
104
118
|
if (c == "\\") {
|
|
105
119
|
c2 = substr(s, i + 1, 1)
|
|
106
120
|
if (c2 == "\n") { i += 2; continue }
|
|
107
|
-
word = word c2; inword = 1; i += 2; continue
|
|
121
|
+
wquoted = 1; word = word c2; inword = 1; i += 2; continue
|
|
108
122
|
}
|
|
109
123
|
if (c == "\047") {
|
|
110
|
-
inword = 1
|
|
124
|
+
wquoted = 1; inword = 1
|
|
111
125
|
j = index(substr(s, i + 1), "\047")
|
|
112
126
|
if (j == 0) { word = word substr(s, i + 1); i = n + 1; continue }
|
|
113
127
|
word = word substr(s, i + 1, j - 1); i += j + 1; continue
|
|
114
128
|
}
|
|
115
129
|
if (c == "\"") {
|
|
116
|
-
inword = 1; i++
|
|
130
|
+
wquoted = 1; inword = 1; i++
|
|
117
131
|
while (i <= n) {
|
|
118
132
|
c = substr(s, i, 1)
|
|
119
133
|
if (c == "\\") { word = word substr(s, i + 1, 1); i += 2; continue }
|
|
@@ -128,9 +142,9 @@ function tokenize(s, i, n, c, c2, j, dash, delim) {
|
|
|
128
142
|
}
|
|
129
143
|
continue
|
|
130
144
|
}
|
|
131
|
-
if (c == "$" && substr(s, i + 1, 1) == "(") { inword = 1; i = cmdsub(s, i + 2, n); word = word "x"; continue }
|
|
145
|
+
if (c == "$" && substr(s, i + 1, 1) == "(") { wquoted = 1; inword = 1; i = cmdsub(s, i + 2, n); word = word "x"; continue }
|
|
132
146
|
if (c == "`") {
|
|
133
|
-
inword = 1
|
|
147
|
+
wquoted = 1; inword = 1
|
|
134
148
|
j = index(substr(s, i + 1), "`")
|
|
135
149
|
if (j == 0) { i = n + 1; continue }
|
|
136
150
|
enqueue(substr(s, i + 1, j - 1)); i += j + 1; word = word "x"; continue
|
|
@@ -146,6 +160,9 @@ function tokenize(s, i, n, c, c2, j, dash, delim) {
|
|
|
146
160
|
if (nheredoc > 0) i = read_heredocs(s, i, n)
|
|
147
161
|
continue
|
|
148
162
|
}
|
|
163
|
+
# `>&`, `>|` and `&>` are parts of a redirection operator, not command separators.
|
|
164
|
+
if ((c == "&" || c == "|") && inword && !wquoted && word ~ />$/) { word = word c; wplain++; i++; continue }
|
|
165
|
+
if (c == "&" && substr(s, i + 1, 1) == ">") { flush_word(); word = c; inword = 1; wplain = 1; i++; continue }
|
|
149
166
|
if (c == ";" || c == "&" || c == "|" || c == "(" || c == ")") { end_cmd(); i++; continue }
|
|
150
167
|
if (substr(s, i, 2) == "<<" && substr(s, i, 3) != "<<<") {
|
|
151
168
|
flush_word(); i += 2; dash = 0
|
|
@@ -161,6 +178,7 @@ function tokenize(s, i, n, c, c2, j, dash, delim) {
|
|
|
161
178
|
nheredoc++; HD[nheredoc] = delim; HDDASH[nheredoc] = dash
|
|
162
179
|
continue
|
|
163
180
|
}
|
|
181
|
+
if (!wquoted) wplain++
|
|
164
182
|
word = word c; inword = 1; i++
|
|
165
183
|
}
|
|
166
184
|
end_cmd()
|
|
@@ -169,7 +187,7 @@ function tokenize(s, i, n, c, c2, j, dash, delim) {
|
|
|
169
187
|
# ── option parsing shared by the CLIs: positionals P/PI, output format, -f/--format value, --raw ──
|
|
170
188
|
function kparse(A, s, n, shortvals, i, w, eq, key, val, f, rest) {
|
|
171
189
|
split("", P); split("", PI)
|
|
172
|
-
np = 0; outfmt = ""; fmtval = ""; dd = 0; raw = 0
|
|
190
|
+
np = 0; outfmt = ""; fmtval = ""; tmpl = ""; dd = 0; raw = 0
|
|
173
191
|
for (i = s; i <= n; i++) {
|
|
174
192
|
w = A[i]
|
|
175
193
|
if (w == "--") { dd = i; break }
|
|
@@ -178,6 +196,7 @@ function kparse(A, s, n, shortvals, i, w, eq, key, val, f, rest) {
|
|
|
178
196
|
if (eq) { key = substr(w, 1, eq - 1); val = substr(w, eq + 1) }
|
|
179
197
|
if (key == "--output") { if (!eq) { i++; val = A[i] } outfmt = tolower(val) }
|
|
180
198
|
else if (key == "--format") { if (!eq) { i++; val = A[i] } fmtval = val }
|
|
199
|
+
else if (key == "--template") { if (!eq) { i++; val = A[i] } tmpl = val }
|
|
181
200
|
else if (key == "--raw") raw = 1
|
|
182
201
|
else if (!eq && (key in LV)) i++
|
|
183
202
|
continue
|
|
@@ -196,6 +215,11 @@ function kparse(A, s, n, shortvals, i, w, eq, key, val, f, rest) {
|
|
|
196
215
|
np++; P[np] = w; PI[np] = i
|
|
197
216
|
}
|
|
198
217
|
}
|
|
218
|
+
# `-o jsonpath={.}` and `-o go-template={{.}}` print the whole object, exactly like -o json.
|
|
219
|
+
function proj_leaks(t) {
|
|
220
|
+
t = tolower(t); gsub(/[ \t]/, "", t)
|
|
221
|
+
return t ~ /[{](\.|@|\.items|\.items\[\*\])[}]/
|
|
222
|
+
}
|
|
199
223
|
function skipopts(A, s, n, vals) {
|
|
200
224
|
while (s <= n && substr(A[s], 1, 1) == "-") {
|
|
201
225
|
if (length(A[s]) == 2 && index(vals, substr(A[s], 2, 1))) s += 2
|
|
@@ -257,6 +281,8 @@ function kube(A, s, n, verb, m, parts, k, r, rlist) {
|
|
|
257
281
|
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
282
|
else if (outfmt == "json" || outfmt == "yaml")
|
|
259
283
|
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")
|
|
284
|
+
else if ((outfmt ~ /^(jsonpath|go-template|jsonpath-as-json)=/ || tmpl != "") && proj_leaks(tmpl outfmt))
|
|
285
|
+
report("kubectl get " r " with a projection that selects the whole object 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
286
|
}
|
|
261
287
|
}
|
|
262
288
|
}
|
|
@@ -323,19 +349,36 @@ function gcloud(A, s, n, k) {
|
|
|
323
349
|
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
350
|
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
351
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
352
|
+
# Scans one attached or clustered short option (-ePATTERN, -nePATTERN, -m5, -F:) of a pattern or script tool:
|
|
353
|
+
# sc_pat is set when it supplies the pattern or script, sc_skip when its value is the next word.
|
|
354
|
+
function scan_short(w, shortvals, sed_like, i, c, last) {
|
|
355
|
+
sc_pat = 0; sc_skip = 0; last = length(w)
|
|
356
|
+
for (i = 2; i <= last; i++) {
|
|
357
|
+
c = substr(w, i, 1)
|
|
358
|
+
if (c == "e" || c == "f") { sc_pat = 1; sc_skip = (i == last); return }
|
|
359
|
+
if (index(shortvals, c)) { sc_skip = (i == last); return }
|
|
360
|
+
if (sed_like && c == "i") return
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function reader(A, s, n, b, k, w, mode, havepat, need, afterdd, kind, valued, valued2, shortvals) {
|
|
364
|
+
b = base(A[s]); mode = ""; valued = " "; valued2 = " "; shortvals = ""
|
|
365
|
+
if (b ~ /^(grep|egrep|fgrep|rg|ag)$/) { mode = "pattern"; shortvals = (b ~ /^(grep|egrep|fgrep)$/) ? "mABCdD" : (b == "rg") ? "mABCdjEMrgtT" : "ABCGgmpW"; valued = " --glob --type --type-not --max-count --include --exclude --exclude-dir --context --after-context --before-context --max-depth --threads " }
|
|
366
|
+
else if (b ~ /^(sed|awk|gawk|jq|yq)$/) { mode = "script"; shortvals = "vF"; valued = " --indent "; valued2 = " --arg --argjson --slurpfile --rawfile " }
|
|
330
367
|
# The first operand is the pattern or script unless -e/-f/--regexp/--file/--expression supplies it; only the rest are files.
|
|
331
368
|
havepat = 0
|
|
332
|
-
if (mode != "") for (k = s + 1; k <= n; k++)
|
|
369
|
+
if (mode != "") for (k = s + 1; k <= n; k++) {
|
|
370
|
+
w = A[k]
|
|
371
|
+
if (w == "--") break
|
|
372
|
+
if (w ~ /^--(regexp|file|expression|from-file)(=|$)/) havepat = 1
|
|
373
|
+
else if (w ~ /^-[a-zA-Z]/) { scan_short(w, shortvals, b == "sed"); if (sc_pat) havepat = 1 }
|
|
374
|
+
}
|
|
333
375
|
need = (mode != "" && !havepat); afterdd = 0
|
|
334
376
|
for (k = s + 1; k <= n; k++) {
|
|
335
377
|
w = A[k]
|
|
336
378
|
if (w == "--" && !afterdd) { afterdd = 1; continue }
|
|
337
379
|
if (!afterdd && substr(w, 1, 1) == "-" && length(w) > 1) {
|
|
338
|
-
if (mode != "" &&
|
|
380
|
+
if (mode != "" && w ~ /^-[a-zA-Z]/) { scan_short(w, shortvals, b == "sed"); if (sc_skip) k++ }
|
|
381
|
+
else if (mode != "" && (w ~ /^--(regexp|file|expression|from-file)$/ || index(valued, " " w " "))) k++
|
|
339
382
|
else if (index(valued2, " " w " ")) k += 2
|
|
340
383
|
continue
|
|
341
384
|
}
|
|
@@ -345,13 +388,22 @@ function reader(A, s, n, b, k, w, mode, havepat, need, afterdd, kind, valued,
|
|
|
345
388
|
}
|
|
346
389
|
}
|
|
347
390
|
|
|
391
|
+
function drop_redirects(A, s, n, k, m, w) {
|
|
392
|
+
m = s
|
|
393
|
+
for (k = s + 1; k <= n; k++) {
|
|
394
|
+
w = A[k]
|
|
395
|
+
if (w ~ /^[0-9]*[<>]/) { if (w ~ /^[0-9]*[<>]+&?-?$/) k++; continue }
|
|
396
|
+
A[++m] = w
|
|
397
|
+
}
|
|
398
|
+
return m
|
|
399
|
+
}
|
|
348
400
|
function analyze(A, s, n, w, b, k, names, p) {
|
|
349
401
|
while (s <= n) {
|
|
350
402
|
w = A[s]; b = base(w)
|
|
351
403
|
if (is_assign(w)) { s++; continue }
|
|
352
404
|
if (w ~ /^[0-9]*[<>]/) { s += (w ~ /^[0-9]*[<>]+&?-?$/) ? 2 : 1; continue }
|
|
353
405
|
if (b in KW) { s++; continue }
|
|
354
|
-
if (b == "command" || b == "builtin") { s++; if (s <= n && (A[s] == "-v" || A[s] == "-V")) return; continue }
|
|
406
|
+
if (b == "command" || b == "builtin") { s++; if (s <= n && (A[s] == "-v" || A[s] == "-V")) return; if (s <= n && A[s] == "-p") s++; continue }
|
|
355
407
|
if (b == "sudo" || b == "doas") { s = skipopts(A, s + 1, n, "ugChprtUD"); continue }
|
|
356
408
|
if (b == "xargs") { s = skipopts(A, s + 1, n, "IniPLdsEa"); continue }
|
|
357
409
|
if (b == "timeout") { s = skipopts(A, s + 1, n, "ks") + 1; continue }
|
|
@@ -363,6 +415,7 @@ function analyze(A, s, n, w, b, k, names, p) {
|
|
|
363
415
|
while (s <= n) {
|
|
364
416
|
w = A[s]
|
|
365
417
|
if (is_assign(w)) s++
|
|
418
|
+
else if (w ~ /^[0-9]*[<>]/) s += (w ~ /^[0-9]*[<>]+&?-?$/) ? 2 : 1
|
|
366
419
|
else if (w == "-u" || w == "-C" || w == "-S" || w == "--unset" || w == "--chdir" || w == "--split-string") s += 2
|
|
367
420
|
else if (substr(w, 1, 1) == "-") s++
|
|
368
421
|
else break
|
|
@@ -393,6 +446,8 @@ function analyze(A, s, n, w, b, k, names, p) {
|
|
|
393
446
|
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
447
|
return
|
|
395
448
|
}
|
|
449
|
+
# A redirect is not a variable name: `printenv > f` and `export > f` still dump the whole environment.
|
|
450
|
+
if (b == "printenv" || b == "export" || b == "declare" || b == "typeset") n = drop_redirects(A, s, n)
|
|
396
451
|
if (b == "printenv") {
|
|
397
452
|
names = 0
|
|
398
453
|
for (k = s + 1; k <= n; k++) {
|
|
@@ -426,7 +481,8 @@ BEGIN {
|
|
|
426
481
|
for (i in L) KW[L[i]] = 1
|
|
427
482
|
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
483
|
for (i in L) SENS[L[i]] = 1
|
|
429
|
-
|
|
484
|
+
CMD = ENVIRON["GS_COMMAND"]
|
|
485
|
+
enqueue(CMD)
|
|
430
486
|
while (qi < queued) { qi++; tokenize(queue[qi]) }
|
|
431
487
|
}
|
|
432
488
|
AWK
|
|
@@ -18,23 +18,74 @@ set -euo pipefail
|
|
|
18
18
|
|
|
19
19
|
# SCRIPTDIR, not a path relative to the caller: shellcheck resolves a plain relative `source=`
|
|
20
20
|
# against the current working directory, and these hooks are linted from wherever CI happens to run.
|
|
21
|
+
script_directory="${BASH_SOURCE[0]%/*}"
|
|
22
|
+
[ "$script_directory" != "${BASH_SOURCE[0]}" ] || script_directory=.
|
|
21
23
|
# shellcheck source=SCRIPTDIR/hook-lib.sh
|
|
22
|
-
. "$
|
|
24
|
+
. "$script_directory/hook-lib.sh"
|
|
23
25
|
|
|
24
26
|
[ "${CRATIS_HOOKS_ALLOW_PROTECTED_WRITES:-0}" = "1" ] && exit 0
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
# This guard runs before every write, so it avoids subprocesses where a builtin does the same job:
|
|
29
|
+
# the helpers below return through REPLY instead of printing into a command substitution.
|
|
30
|
+
# Read stdin with the read builtin: opening /dev/stdin fails with ENXIO when stdin is a socket (Linux, Node pipes).
|
|
31
|
+
input=""
|
|
32
|
+
IFS= read -r -d '' input || true
|
|
27
33
|
[ -n "$input" ] || exit 0
|
|
28
34
|
hook_have jq || exit 0
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
# One jq call reads every field the guard uses. Fields are NUL-terminated (embedded NULs are dropped,
|
|
37
|
+
# as a command substitution would drop them); a missing, null, false or unreadable path is empty.
|
|
38
|
+
cwd="" file="" notebook="" new_content="" new_string=""
|
|
39
|
+
{
|
|
40
|
+
IFS= read -r -d '' cwd
|
|
41
|
+
IFS= read -r -d '' file
|
|
42
|
+
IFS= read -r -d '' notebook
|
|
43
|
+
IFS= read -r -d '' new_content
|
|
44
|
+
IFS= read -r -d '' new_string
|
|
45
|
+
} < <(jq -j '
|
|
46
|
+
def field(path): ([try (path // empty) catch empty] | first // "")
|
|
47
|
+
| if type == "string" then . else tojson end
|
|
48
|
+
| gsub("\u0000"; "");
|
|
49
|
+
[field(.cwd), field(.tool_input.file_path), field(.tool_input.notebook_path),
|
|
50
|
+
field(.tool_input.content), field(.tool_input.new_string)]
|
|
51
|
+
| .[] | ., "\u0000"
|
|
52
|
+
' <<<"$input" 2>/dev/null) || true
|
|
53
|
+
# Command substitution stripped trailing newlines from each field; keep that behaviour.
|
|
54
|
+
for field_name in cwd file notebook new_content new_string; do
|
|
55
|
+
while [[ "${!field_name}" == *$'\n' ]]; do printf -v "$field_name" '%s' "${!field_name%$'\n'}"; done
|
|
56
|
+
done
|
|
33
57
|
|
|
34
|
-
|
|
35
|
-
[ -n "$file" ] || file="$
|
|
58
|
+
[ -n "$cwd" ] || cwd="$(hook_repo_root)"
|
|
59
|
+
[ -n "$file" ] || file="$notebook"
|
|
36
60
|
[ -n "$file" ] || exit 0
|
|
37
61
|
|
|
62
|
+
# dir_of / base_of / relative_to mirror dirname, basename and hook_relpath for the absolute,
|
|
63
|
+
# normalized paths used here. abspath mirrors hook_abspath. All return through REPLY.
|
|
64
|
+
dir_of() {
|
|
65
|
+
REPLY="${1%/*}"
|
|
66
|
+
[ -n "$REPLY" ] || REPLY=/
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
base_of() {
|
|
70
|
+
REPLY="${1##*/}"
|
|
71
|
+
[ -n "$REPLY" ] || REPLY=/
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
abspath() {
|
|
75
|
+
case "$1" in
|
|
76
|
+
/*) REPLY="$1" ;;
|
|
77
|
+
*) REPLY="${2%/}/$1" ;;
|
|
78
|
+
esac
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
relative_to() {
|
|
82
|
+
local root="${2%/}"
|
|
83
|
+
case "$1" in
|
|
84
|
+
"$root"/*) REPLY="${1#"$root"/}" ;;
|
|
85
|
+
*) REPLY="$1" ;;
|
|
86
|
+
esac
|
|
87
|
+
}
|
|
88
|
+
|
|
38
89
|
# Lexically normalize before any Git lookup or parent walk, including nonexistent segments.
|
|
39
90
|
# Component parsing preserves spaces and glob characters without depending on realpath.
|
|
40
91
|
normalize_target_path() {
|
|
@@ -52,17 +103,22 @@ normalize_target_path() {
|
|
|
52
103
|
*) normalized="$normalized/$component" ;;
|
|
53
104
|
esac
|
|
54
105
|
done
|
|
55
|
-
|
|
106
|
+
REPLY="${normalized:-/}"
|
|
56
107
|
}
|
|
57
108
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
base="$(basename "$file")"
|
|
109
|
+
abspath "$file" "$cwd"
|
|
110
|
+
normalize_target_path "$REPLY"
|
|
111
|
+
file="$REPLY"
|
|
112
|
+
base_of "$file"
|
|
113
|
+
base="$REPLY"
|
|
64
114
|
|
|
65
115
|
block() {
|
|
116
|
+
# Pi can run the bundled script in a consumer checkout; this root is only for diagnostics,
|
|
117
|
+
# so it is looked up only when a write is actually refused.
|
|
118
|
+
local root rel
|
|
119
|
+
root="$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null || printf '%s\n' "$cwd")"
|
|
120
|
+
relative_to "$file" "$root"
|
|
121
|
+
rel="$REPLY"
|
|
66
122
|
printf 'BLOCKED by cratis-guard-writes: %s\n\n%s\n\n%s\n' "$rel" "$1" "$2" >&2
|
|
67
123
|
exit 2
|
|
68
124
|
}
|
|
@@ -70,36 +126,70 @@ block() {
|
|
|
70
126
|
# Check both the addressed path and its physical destination: settings may be symlinks to
|
|
71
127
|
# templates, and an innocently named link may target a protected file. Preserve missing suffixes.
|
|
72
128
|
physical_target_path() {
|
|
73
|
-
local target="$1" directory suffix="" link hops=0
|
|
129
|
+
local target="$1" directory suffix="" link hops=0 start="$PWD"
|
|
130
|
+
physical_file=""
|
|
74
131
|
while [ -L "$target" ]; do
|
|
75
132
|
hops=$((hops + 1))
|
|
76
133
|
[ "$hops" -le 40 ] || return 1
|
|
77
134
|
link="$(readlink "$target")" || return 1
|
|
78
|
-
|
|
135
|
+
dir_of "$target"
|
|
136
|
+
abspath "$link" "$REPLY"
|
|
137
|
+
target="$REPLY"
|
|
79
138
|
done
|
|
80
|
-
|
|
81
|
-
|
|
139
|
+
dir_of "$target"
|
|
140
|
+
directory="$REPLY"
|
|
141
|
+
base_of "$target"
|
|
142
|
+
suffix="/$REPLY"
|
|
82
143
|
while [ ! -d "$directory" ]; do
|
|
83
144
|
[ "$directory" != "/" ] || return 1
|
|
84
|
-
|
|
85
|
-
|
|
145
|
+
base_of "$directory"
|
|
146
|
+
suffix="/$REPLY$suffix"
|
|
147
|
+
dir_of "$directory"
|
|
148
|
+
directory="$REPLY"
|
|
86
149
|
done
|
|
87
|
-
|
|
88
|
-
|
|
150
|
+
cd -P "$directory" 2>/dev/null || return 1
|
|
151
|
+
directory="$PWD"
|
|
152
|
+
cd "$start" 2>/dev/null || true
|
|
153
|
+
physical_file="${directory%/}$suffix"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# The origin URL, looked up once per repository per run.
|
|
157
|
+
origin_cache_root=""
|
|
158
|
+
origin_cache_value=""
|
|
159
|
+
repository_origin() {
|
|
160
|
+
if [ "$origin_cache_root" != "$1" ]; then
|
|
161
|
+
origin_cache_value="$(git -C "$1" remote get-url origin 2>/dev/null || true)"
|
|
162
|
+
origin_cache_root="$1"
|
|
163
|
+
fi
|
|
164
|
+
REPLY="$origin_cache_value"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Case-insensitive ASCII match of the origin URL, equivalent to grep -Eiq on a single line.
|
|
168
|
+
origin_matches() {
|
|
169
|
+
# C locale, as the grep it replaced ran: a non-ASCII letter (the Kelvin sign) must not match [a-z].
|
|
170
|
+
local LC_ALL=C pattern="$1" matched=1
|
|
171
|
+
shopt -s nocasematch
|
|
172
|
+
# shellcheck disable=SC2034
|
|
173
|
+
[[ "$REPLY" =~ $pattern ]] && matched=0
|
|
174
|
+
shopt -u nocasematch
|
|
175
|
+
return "$matched"
|
|
89
176
|
}
|
|
90
177
|
|
|
91
178
|
# ── 4. Agent controls and installed managed trees ─────────────────────────────
|
|
92
179
|
agent_control_target() {
|
|
93
|
-
local target="$1" directory policy_root target_rel
|
|
94
|
-
|
|
180
|
+
local target="$1" directory policy_root target_rel
|
|
181
|
+
dir_of "$target"
|
|
182
|
+
directory="$REPLY"
|
|
95
183
|
# Keep the addressed spelling here, even when an adapter directory is a symlink.
|
|
96
184
|
# .git is a directory in a checkout and a file in a linked worktree.
|
|
97
185
|
while [ ! -e "$directory/.git" ]; do
|
|
98
186
|
[ "$directory" != "/" ] || return 1
|
|
99
|
-
|
|
187
|
+
dir_of "$directory"
|
|
188
|
+
directory="$REPLY"
|
|
100
189
|
done
|
|
101
190
|
policy_root="$directory"
|
|
102
|
-
|
|
191
|
+
relative_to "$target" "$policy_root"
|
|
192
|
+
target_rel="$REPLY"
|
|
103
193
|
if [ -f "$policy_root/.cratis/ai.manifest.json" ]; then
|
|
104
194
|
case "$target_rel" in
|
|
105
195
|
.cratis/ai/*) return 0 ;;
|
|
@@ -122,8 +212,8 @@ agent_control_target() {
|
|
|
122
212
|
esac
|
|
123
213
|
else
|
|
124
214
|
# The corpus is authored source in Cratis/AI, not a managed installation.
|
|
125
|
-
|
|
126
|
-
if
|
|
215
|
+
repository_origin "$policy_root"
|
|
216
|
+
if origin_matches '^(https://github\.com/|git@github\.com:|ssh://git@github\.com(:[0-9]+)?/)cratis/ai(\.git)?/?$'; then
|
|
127
217
|
return 1
|
|
128
218
|
fi
|
|
129
219
|
fi
|
|
@@ -134,7 +224,7 @@ agent_control_target() {
|
|
|
134
224
|
return 1
|
|
135
225
|
}
|
|
136
226
|
|
|
137
|
-
|
|
227
|
+
physical_target_path "$file" || physical_file=""
|
|
138
228
|
if agent_control_target "$file" || { [ -n "$physical_file" ] && agent_control_target "$physical_file"; }; then
|
|
139
229
|
block \
|
|
140
230
|
"This target controls the agent's permissions or extensions, or belongs to an installed managed corpus/adapter tree. Agent Write/Edit calls may not alter their own controls or managed instructions." \
|
|
@@ -146,19 +236,30 @@ fi
|
|
|
146
236
|
# Merely *mentioning* the string — documentation, a rule file, this corpus — is not a match.
|
|
147
237
|
marker='^[[:space:]]*(//|/\*|#|<!--)[[:space:]]*@generated by Cratis'
|
|
148
238
|
|
|
239
|
+
# True when one of the first five lines of stdin carries the marker (head -n 5 | grep -E, without forks).
|
|
240
|
+
first_lines_carry_marker() {
|
|
241
|
+
local LC_ALL=C line count=0
|
|
242
|
+
while [ "$count" -lt 5 ] && { IFS= read -r line || [ -n "$line" ]; }; do
|
|
243
|
+
count=$((count + 1))
|
|
244
|
+
# shellcheck disable=SC2034
|
|
245
|
+
[[ "$line" =~ $marker ]] && return 0
|
|
246
|
+
done
|
|
247
|
+
return 1
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# The cheap in-shell header test runs first; grep -I then confirms the file is text, as before.
|
|
149
251
|
if [ -f "$file" ] \
|
|
150
|
-
&&
|
|
151
|
-
&&
|
|
252
|
+
&& first_lines_carry_marker 2>/dev/null <"$file" \
|
|
253
|
+
&& LC_ALL=C grep -Iq . "$file" 2>/dev/null; then
|
|
152
254
|
block \
|
|
153
255
|
"This file is generated output (its header marks it as generated by Cratis). Rule 15 [contract] in .cratis/ai/rules/general.md: never edit a generated file — the next build overwrites it." \
|
|
154
256
|
"Instead: change the C# source it is generated from (the [Command] / [ReadModel] record) and rebuild with 'dotnet build -c Debug' to regenerate the proxy."
|
|
155
257
|
fi
|
|
156
258
|
|
|
157
259
|
# The same guard for authoring a *new* file that pretends to be generated output.
|
|
158
|
-
new_content
|
|
159
|
-
[ -n "$new_content" ] || new_content="$(hook_json "$input" '.tool_input.new_string')"
|
|
260
|
+
[ -n "$new_content" ] || new_content="$new_string"
|
|
160
261
|
if [ -n "$new_content" ] \
|
|
161
|
-
&&
|
|
262
|
+
&& first_lines_carry_marker <<<"$new_content"; then
|
|
162
263
|
block \
|
|
163
264
|
"The content being written opens with a Cratis generated-file header. Proxies are produced by the Cratis proxy generator, never hand-written." \
|
|
164
265
|
"Instead: write the C# source and run 'dotnet build -c Debug' to generate the proxy."
|
|
@@ -166,16 +267,20 @@ fi
|
|
|
166
267
|
|
|
167
268
|
# ── 2. Dependency manifests, lockfiles, SDK and NuGet configuration ───────────
|
|
168
269
|
manifest_writes_allowed() {
|
|
169
|
-
local setting=""
|
|
270
|
+
local setting="" directory policy_root start="$PWD"
|
|
170
271
|
# Authorization belongs to the write target, which may be outside the session's checkout.
|
|
171
272
|
# New files can have nonexistent parents; find their nearest existing directory first.
|
|
172
|
-
|
|
273
|
+
dir_of "$file"
|
|
274
|
+
directory="$REPLY"
|
|
173
275
|
while [ ! -d "$directory" ]; do
|
|
174
276
|
[ "$directory" != "/" ] || return 1
|
|
175
|
-
|
|
277
|
+
dir_of "$directory"
|
|
278
|
+
directory="$REPLY"
|
|
176
279
|
done
|
|
177
280
|
# Follow existing directory symlinks before asking Git which repository owns the target.
|
|
178
|
-
|
|
281
|
+
cd -P "$directory" 2>/dev/null || return 1
|
|
282
|
+
directory="$PWD"
|
|
283
|
+
cd "$start" 2>/dev/null || true
|
|
179
284
|
policy_root="$(git -C "$directory" rev-parse --show-toplevel 2>/dev/null)" || return 1
|
|
180
285
|
if [ -f "$policy_root/.cratis/ai.json" ]; then
|
|
181
286
|
# Only actual booleans override the default; jq's `// empty` would discard false.
|
|
@@ -185,9 +290,9 @@ manifest_writes_allowed() {
|
|
|
185
290
|
true) return 0 ;;
|
|
186
291
|
false) return 1 ;;
|
|
187
292
|
esac
|
|
188
|
-
|
|
293
|
+
repository_origin "$policy_root"
|
|
189
294
|
# Anchor host, owner and the single repository segment; never infer ownership from profiles.
|
|
190
|
-
|
|
295
|
+
origin_matches '^(https://github\.com/|git@github\.com:|ssh://git@github\.com(:[0-9]+)?/)cratis/[a-z0-9_.-]+/?$'
|
|
191
296
|
}
|
|
192
297
|
|
|
193
298
|
case "$base" in
|