@raishin/vanguard-frontier-agentic 3.1.1 → 3.3.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +7 -1
- package/.cursor-plugin/plugin.json +7 -1
- package/.github/plugin/marketplace.json +1 -1
- package/README.md +11 -11
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/AGENT.md +112 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/claude-code.agent.md +111 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/codex.toml +37 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/copilot.agent.md +120 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/cursor.agent.md +112 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/gemini.agent.md +112 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/kiro-ide.agent.md +111 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/metadata.json +42 -0
- package/agents/php/composer-supply-chain-agent/AGENT.md +114 -0
- package/agents/php/composer-supply-chain-agent/harnesses/claude-code.agent.md +113 -0
- package/agents/php/composer-supply-chain-agent/harnesses/codex.toml +119 -0
- package/agents/php/composer-supply-chain-agent/harnesses/copilot.agent.md +122 -0
- package/agents/php/composer-supply-chain-agent/harnesses/cursor.agent.md +114 -0
- package/agents/php/composer-supply-chain-agent/harnesses/gemini.agent.md +114 -0
- package/agents/php/composer-supply-chain-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/php/composer-supply-chain-agent/harnesses/kiro-ide.agent.md +113 -0
- package/agents/php/composer-supply-chain-agent/metadata.json +31 -0
- package/agents/php/php-application-security-agent/AGENT.md +113 -0
- package/agents/php/php-application-security-agent/harnesses/claude-code.agent.md +112 -0
- package/agents/php/php-application-security-agent/harnesses/codex.toml +118 -0
- package/agents/php/php-application-security-agent/harnesses/copilot.agent.md +121 -0
- package/agents/php/php-application-security-agent/harnesses/cursor.agent.md +113 -0
- package/agents/php/php-application-security-agent/harnesses/gemini.agent.md +113 -0
- package/agents/php/php-application-security-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/php/php-application-security-agent/harnesses/kiro-ide.agent.md +112 -0
- package/agents/php/php-application-security-agent/metadata.json +31 -0
- package/agents/php/php-maestro-agent/AGENT.md +81 -0
- package/agents/php/php-maestro-agent/harnesses/claude-code.agent.md +80 -0
- package/agents/php/php-maestro-agent/harnesses/codex.toml +86 -0
- package/agents/php/php-maestro-agent/harnesses/copilot.agent.md +89 -0
- package/agents/php/php-maestro-agent/harnesses/cursor.agent.md +81 -0
- package/agents/php/php-maestro-agent/harnesses/gemini.agent.md +81 -0
- package/agents/php/php-maestro-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/php/php-maestro-agent/harnesses/kiro-ide.agent.md +80 -0
- package/agents/php/php-maestro-agent/metadata.json +31 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/AGENT.md +117 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/claude-code.agent.md +116 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/codex.toml +122 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/copilot.agent.md +125 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/cursor.agent.md +117 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/gemini.agent.md +117 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/kiro-ide.agent.md +116 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/metadata.json +30 -0
- package/agents/php/wordpress-security-agent/AGENT.md +107 -0
- package/agents/php/wordpress-security-agent/harnesses/claude-code.agent.md +106 -0
- package/agents/php/wordpress-security-agent/harnesses/codex.toml +112 -0
- package/agents/php/wordpress-security-agent/harnesses/copilot.agent.md +115 -0
- package/agents/php/wordpress-security-agent/harnesses/cursor.agent.md +107 -0
- package/agents/php/wordpress-security-agent/harnesses/gemini.agent.md +107 -0
- package/agents/php/wordpress-security-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/php/wordpress-security-agent/harnesses/kiro-ide.agent.md +106 -0
- package/agents/php/wordpress-security-agent/metadata.json +31 -0
- package/catalog/agents.json +175 -0
- package/catalog/asset-integrity.json +503 -48
- package/catalog/install-roles.json +26 -4
- package/catalog/model-assignments.json +19811 -0
- package/catalog/model-policy.json +371 -0
- package/catalog/model-registry.json +134 -0
- package/catalog/skill-manifest.json +220 -18
- package/catalog/skills.json +163 -0
- package/package.json +7 -2
- package/plugins/vanguard-frontier-agentic/.codex-plugin/plugin.json +1 -1
- package/powers/README.md +3 -2
- package/powers/vanguard-generic/POWER.md +1 -1
- package/powers/vanguard-php/POWER.md +40 -0
- package/schemas/agent.schema.json +2 -1
- package/schemas/model-policy.schema.json +60 -0
- package/schemas/model-registry.schema.json +127 -0
- package/schemas/skill.schema.json +2 -1
- package/scripts/generate-docs-data.mjs +1 -1
- package/scripts/model-policy.mjs +1353 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/SKILL.md +108 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/metadata.json +29 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/references/idempotency-and-safe-retries.md +155 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/references/official-sources.md +71 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/references/pci-saq-scope-boundaries.md +118 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/references/server-side-revalidation-trust-boundary.md +134 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/references/webhook-delivery-dedup-ordering.md +146 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/references/workflow-and-output.md +100 -0
- package/skills/gcp/gcp-firebase-developer/SKILL.md +1 -1
- package/skills/gcp/gcp-gke-platform-operator/SKILL.md +1 -1
- package/skills/php/composer-audit-supply-chain-review/SKILL.md +108 -0
- package/skills/php/composer-audit-supply-chain-review/metadata.json +20 -0
- package/skills/php/composer-audit-supply-chain-review/references/abandoned-and-advisory-governance.md +30 -0
- package/skills/php/composer-audit-supply-chain-review/references/composer-audit-policy.md +35 -0
- package/skills/php/composer-audit-supply-chain-review/references/lockfile-integrity.md +27 -0
- package/skills/php/php-maestro/SKILL.md +51 -0
- package/skills/php/php-maestro/metadata.json +20 -0
- package/skills/php/php-maestro/references/hard-gates-and-escalation.md +67 -0
- package/skills/php/php-maestro/references/routing-and-dispatch.md +91 -0
- package/skills/php/php-runtime-eol-opcache-fpm-review/SKILL.md +109 -0
- package/skills/php/php-runtime-eol-opcache-fpm-review/metadata.json +19 -0
- package/skills/php/php-runtime-eol-opcache-fpm-review/references/opcache-production-config.md +91 -0
- package/skills/php/php-runtime-eol-opcache-fpm-review/references/php-fpm-pool-tuning.md +87 -0
- package/skills/php/php-runtime-eol-opcache-fpm-review/references/php-version-lifecycle.md +102 -0
- package/skills/php/php-session-upload-deserialization-review/SKILL.md +111 -0
- package/skills/php/php-session-upload-deserialization-review/metadata.json +20 -0
- package/skills/php/php-session-upload-deserialization-review/references/file-upload-security.md +119 -0
- package/skills/php/php-session-upload-deserialization-review/references/session-security.md +126 -0
- package/skills/php/php-session-upload-deserialization-review/references/unserialize-object-injection.md +121 -0
- package/skills/php/wordpress-rest-block-security-review/SKILL.md +106 -0
- package/skills/php/wordpress-rest-block-security-review/metadata.json +20 -0
- package/skills/php/wordpress-rest-block-security-review/references/dynamic-block-output-escaping.md +42 -0
- package/skills/php/wordpress-rest-block-security-review/references/input-sanitize-output-escape.md +52 -0
- package/skills/php/wordpress-rest-block-security-review/references/rest-api-permission-callback.md +48 -0
- package/skills/salesforce/salesforce-agentforce-stdm-observer-skill/SKILL.md +1 -1
- package/skills/salesforce/salesforce-apex-log-analyzer-skill/SKILL.md +1 -1
- package/skills/salesforce/salesforce-apex-test-runner-skill/SKILL.md +1 -1
- package/skills/salesforce/salesforce-soql-explorer-skill/SKILL.md +1 -1
- package/tests/fixtures/php-maestro-routing/expected/001-happy-application-security.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/002-happy-composer-supply-chain.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/003-happy-runtime-upgrade-readiness.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/004-happy-wordpress-security.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/005-happy-unserialize-session.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/006-happy-fpm-opcache.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/007-happy-composer-audit.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/008-happy-wp-permission-callback.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/adv-ambiguous.json +4 -0
- package/tests/fixtures/php-maestro-routing/expected/adv-instruction-injection.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/adv-liveguard-db-migration-prod.json +4 -0
- package/tests/fixtures/php-maestro-routing/expected/adv-liveguard-deploy-prod.json +4 -0
- package/tests/fixtures/php-maestro-routing/expected/adv-persona-replacement.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/adv-secrets-bait.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/001-happy-application-security.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/002-happy-composer-supply-chain.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/003-happy-runtime-upgrade-readiness.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/004-happy-wordpress-security.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/005-happy-unserialize-session.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/006-happy-fpm-opcache.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/007-happy-composer-audit.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/008-happy-wp-permission-callback.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/adv-ambiguous.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/adv-instruction-injection.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/adv-liveguard-db-migration-prod.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/adv-liveguard-deploy-prod.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/adv-persona-replacement.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/adv-secrets-bait.json +7 -0
- package/tests/fixtures/php-maestro-routing/taxonomy.json +69 -0
- package/tests/validate-catalog.py +1 -0
- package/tests/validate-skill-coherence.py +364 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate that every SKILL.md's shell examples are covered by its allowed-tools.
|
|
3
|
+
|
|
4
|
+
Every fenced shell command a SKILL.md demonstrates must be covered by its
|
|
5
|
+
declared `allowed-tools` frontmatter — least-privilege declarations must
|
|
6
|
+
match the skill's own examples. A skill that shows `gcloud container ...`
|
|
7
|
+
but only declares `Read Grep Glob` is under-declared (Claude Code would
|
|
8
|
+
need a Bash grant it never asked for); a skill that declares bare `Bash`
|
|
9
|
+
while only ever running `npm test` is over-declared. This gate catches the
|
|
10
|
+
under-declared case: real command examples with no matching `Bash`/
|
|
11
|
+
`Bash(pattern)` coverage.
|
|
12
|
+
|
|
13
|
+
Scope: only fenced code blocks whose info string is EXACTLY one of
|
|
14
|
+
`bash`, `sh`, `shell`, or `console` are scanned. Untagged (plain ```)
|
|
15
|
+
blocks are deliberately out of scope — they are not reliably shell
|
|
16
|
+
examples (could be pseudo-code, transcripts, etc.) and are not a signal
|
|
17
|
+
the harness's `Bash(...)` permission grammar can be checked against
|
|
18
|
+
anyway. Only `skills/**/SKILL.md` is scanned; `references/*.md` files are
|
|
19
|
+
out of scope per the task spec (SKILL.md is the harness-facing contract
|
|
20
|
+
that carries `allowed-tools`; references are supporting docs).
|
|
21
|
+
|
|
22
|
+
Reuses `parse_frontmatter` and `tokenize_allowed_tools` from
|
|
23
|
+
validate-skill-allowed-tools.py so the two gates never disagree about
|
|
24
|
+
what a token is.
|
|
25
|
+
|
|
26
|
+
Command extraction (per fenced block):
|
|
27
|
+
- iterate physical lines; strip a leading "$ " prompt
|
|
28
|
+
- join trailing-backslash line continuations into one logical line
|
|
29
|
+
- skip blank lines and lines starting with "#"
|
|
30
|
+
- split compound lines on `&&`, `||`, `;`, `|` into segments, tracking
|
|
31
|
+
single-/double-quote state so an operator character *inside* a quoted
|
|
32
|
+
string (e.g. the `|` in a `jq '.a[] | .b'` filter) is not mistaken for
|
|
33
|
+
a shell pipeline operator — this parser is quote-depth-aware only; it
|
|
34
|
+
does not handle backslash-escaped quotes inside single-quoted spans or
|
|
35
|
+
heredocs, which is a correct simplification for the shell one-liners
|
|
36
|
+
this repo's SKILL.md files demonstrate
|
|
37
|
+
- for each segment, skip leading VAR=value assignment words, then take
|
|
38
|
+
the command token
|
|
39
|
+
- ignore shell builtins/environment ops that never need a Bash grant:
|
|
40
|
+
export cd echo printf set unset source . true false exit pushd popd
|
|
41
|
+
- everything else is a command segment that needs `allowed-tools` coverage
|
|
42
|
+
|
|
43
|
+
Coverage semantics: a bare `Bash` token in allowed-tools covers every
|
|
44
|
+
segment. Each `Bash(inner)` token becomes a regex matcher applied to the
|
|
45
|
+
whitespace-normalized full command segment: escape `inner`, turn a
|
|
46
|
+
trailing "<literal>:*" suffix into "<literal>(\\s.*)?$" (command-plus-
|
|
47
|
+
any-args semantics), turn every other escaped `*` into `.*`, and anchor
|
|
48
|
+
at the start. A segment is covered if ANY matcher matches. If a skill
|
|
49
|
+
shows command segments but declares no Bash-ish token at all, every
|
|
50
|
+
segment is a violation.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import importlib.util
|
|
56
|
+
import os
|
|
57
|
+
import re
|
|
58
|
+
import sys
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
|
|
61
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
62
|
+
SKILLS_DIR = ROOT / "skills"
|
|
63
|
+
|
|
64
|
+
# ── reuse the sibling gate's frontmatter/token parsing (don't fork it) ──────
|
|
65
|
+
_vsat_spec = importlib.util.spec_from_file_location(
|
|
66
|
+
"vsat", os.path.join(os.path.dirname(__file__), "validate-skill-allowed-tools.py")
|
|
67
|
+
)
|
|
68
|
+
assert _vsat_spec is not None and _vsat_spec.loader is not None
|
|
69
|
+
vsat = importlib.util.module_from_spec(_vsat_spec)
|
|
70
|
+
_vsat_spec.loader.exec_module(vsat)
|
|
71
|
+
|
|
72
|
+
FENCE_LANGS = {"bash", "sh", "shell", "console"}
|
|
73
|
+
|
|
74
|
+
# Shell builtins / environment ops that never need a Bash allowed-tools grant.
|
|
75
|
+
IGNORE_BUILTINS = {
|
|
76
|
+
"export",
|
|
77
|
+
"cd",
|
|
78
|
+
"echo",
|
|
79
|
+
"printf",
|
|
80
|
+
"set",
|
|
81
|
+
"unset",
|
|
82
|
+
"source",
|
|
83
|
+
".",
|
|
84
|
+
"true",
|
|
85
|
+
"false",
|
|
86
|
+
"exit",
|
|
87
|
+
"pushd",
|
|
88
|
+
"popd",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
|
92
|
+
FENCE_OPEN_RE = re.compile(r"^(`{3,}|~{3,})\s*([A-Za-z0-9_+-]*)")
|
|
93
|
+
|
|
94
|
+
# Cap on '*' wildcards per Bash(inner) token. Each '*' becomes a regex `.*`;
|
|
95
|
+
# an unbounded chain of `.*` wildcards (e.g. Bash(a*a*a*...*a)) can
|
|
96
|
+
# catastrophically backtrack on a near-miss command and hang the gate. 12 is
|
|
97
|
+
# generous for any realistic allowed-tools pattern in this repo.
|
|
98
|
+
MAX_WILDCARDS = 12
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def split_segments(text: str) -> list[str]:
|
|
102
|
+
"""Split on &&, ||, ;, | — but never inside a single- or double-quoted span.
|
|
103
|
+
|
|
104
|
+
A bare regex split on `|` would fracture a real compound pipeline like
|
|
105
|
+
`sf data query ... | jq '.a[] | .b'` at the `|` *inside* the quoted jq
|
|
106
|
+
filter too, producing a bogus non-command fragment. Shell operators are
|
|
107
|
+
only operators outside quotes, so track quote state while scanning.
|
|
108
|
+
"""
|
|
109
|
+
segments: list[str] = []
|
|
110
|
+
buf: list[str] = []
|
|
111
|
+
in_single = False
|
|
112
|
+
in_double = False
|
|
113
|
+
i = 0
|
|
114
|
+
n = len(text)
|
|
115
|
+
while i < n:
|
|
116
|
+
ch = text[i]
|
|
117
|
+
if in_single:
|
|
118
|
+
buf.append(ch)
|
|
119
|
+
if ch == "'":
|
|
120
|
+
in_single = False
|
|
121
|
+
i += 1
|
|
122
|
+
continue
|
|
123
|
+
if in_double:
|
|
124
|
+
buf.append(ch)
|
|
125
|
+
if ch == "\\" and i + 1 < n:
|
|
126
|
+
buf.append(text[i + 1])
|
|
127
|
+
i += 1
|
|
128
|
+
elif ch == '"':
|
|
129
|
+
in_double = False
|
|
130
|
+
i += 1
|
|
131
|
+
continue
|
|
132
|
+
if ch == "'":
|
|
133
|
+
in_single = True
|
|
134
|
+
buf.append(ch)
|
|
135
|
+
i += 1
|
|
136
|
+
continue
|
|
137
|
+
if ch == '"':
|
|
138
|
+
in_double = True
|
|
139
|
+
buf.append(ch)
|
|
140
|
+
i += 1
|
|
141
|
+
continue
|
|
142
|
+
if text[i : i + 2] in ("&&", "||"):
|
|
143
|
+
segments.append("".join(buf))
|
|
144
|
+
buf = []
|
|
145
|
+
i += 2
|
|
146
|
+
continue
|
|
147
|
+
if ch in (";", "|"):
|
|
148
|
+
segments.append("".join(buf))
|
|
149
|
+
buf = []
|
|
150
|
+
i += 1
|
|
151
|
+
continue
|
|
152
|
+
buf.append(ch)
|
|
153
|
+
i += 1
|
|
154
|
+
segments.append("".join(buf))
|
|
155
|
+
return segments
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def extract_allowed_tools_tokens(text: str) -> list[str]:
|
|
159
|
+
"""Return the raw allowed-tools tokens for a SKILL.md's frontmatter."""
|
|
160
|
+
fm = vsat.parse_frontmatter(text)
|
|
161
|
+
if fm is None:
|
|
162
|
+
return []
|
|
163
|
+
raw = fm.get("allowed-tools", "").strip()
|
|
164
|
+
if not raw:
|
|
165
|
+
return []
|
|
166
|
+
if raw.startswith("[") and raw.endswith("]"):
|
|
167
|
+
# Bracketed-list variant, parsed the same way
|
|
168
|
+
# validate-skill-frontmatter-schema.py parses inline YAML sequences.
|
|
169
|
+
inner = raw[1:-1].strip()
|
|
170
|
+
return [t.strip().strip("'\"") for t in inner.split(",") if t.strip()]
|
|
171
|
+
return vsat.tokenize_allowed_tools(raw)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def compile_bash_matcher(inner: str) -> re.Pattern[str]:
|
|
175
|
+
"""Turn a Bash(inner) constraint into a regex matcher on the full segment."""
|
|
176
|
+
if inner.count("*") > MAX_WILDCARDS:
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"Bash(...) inner pattern has too many '*' wildcards "
|
|
179
|
+
f"(>{MAX_WILDCARDS}), refusing to compile (ReDoS risk): {inner!r}"
|
|
180
|
+
)
|
|
181
|
+
escaped = re.escape(inner)
|
|
182
|
+
for suffix in ("\\:\\*", ":\\*"):
|
|
183
|
+
if escaped.endswith(suffix):
|
|
184
|
+
escaped = escaped[: -len(suffix)] + r"(\s.*)?$"
|
|
185
|
+
break
|
|
186
|
+
escaped = escaped.replace(r"\*", ".*")
|
|
187
|
+
return re.compile("^" + escaped)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def build_bash_matchers(tokens: list[str]) -> tuple[bool, list[re.Pattern[str]]]:
|
|
191
|
+
"""Return (has_bare_bash, [compiled Bash(...) matchers])."""
|
|
192
|
+
has_bare_bash = False
|
|
193
|
+
matchers: list[re.Pattern[str]] = []
|
|
194
|
+
for tok in tokens:
|
|
195
|
+
if tok == "Bash":
|
|
196
|
+
has_bare_bash = True
|
|
197
|
+
elif tok.startswith("Bash(") and tok.endswith(")"):
|
|
198
|
+
matchers.append(compile_bash_matcher(tok[len("Bash(") : -1]))
|
|
199
|
+
return has_bare_bash, matchers
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def normalize(segment: str) -> str:
|
|
203
|
+
return " ".join(segment.split())
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def iter_fenced_blocks(lines: list[str]):
|
|
207
|
+
"""Yield (info_string_lower, [(lineno, raw_line), ...]) for each fence."""
|
|
208
|
+
i = 0
|
|
209
|
+
n = len(lines)
|
|
210
|
+
while i < n:
|
|
211
|
+
m = FENCE_OPEN_RE.match(lines[i])
|
|
212
|
+
if not m:
|
|
213
|
+
i += 1
|
|
214
|
+
continue
|
|
215
|
+
fence_char = m.group(1)[0]
|
|
216
|
+
fence_min_len = len(m.group(1))
|
|
217
|
+
info = m.group(2).lower()
|
|
218
|
+
i += 1
|
|
219
|
+
body: list[tuple[int, str]] = []
|
|
220
|
+
while i < n:
|
|
221
|
+
stripped = lines[i].strip()
|
|
222
|
+
if stripped and set(stripped) == {fence_char} and len(stripped) >= fence_min_len:
|
|
223
|
+
i += 1
|
|
224
|
+
break
|
|
225
|
+
body.append((i + 1, lines[i]))
|
|
226
|
+
i += 1
|
|
227
|
+
yield info, body
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def extract_command_segments(block_lines: list[tuple[int, str]]) -> list[tuple[int, str]]:
|
|
231
|
+
"""Return [(lineno, segment_text), ...] for segments that need coverage."""
|
|
232
|
+
results: list[tuple[int, str]] = []
|
|
233
|
+
i = 0
|
|
234
|
+
n = len(block_lines)
|
|
235
|
+
while i < n:
|
|
236
|
+
start_lineno, first_line = block_lines[i]
|
|
237
|
+
text = first_line.strip()
|
|
238
|
+
if text.startswith("$ "):
|
|
239
|
+
text = text[2:]
|
|
240
|
+
elif text == "$":
|
|
241
|
+
text = ""
|
|
242
|
+
|
|
243
|
+
# Join trailing-backslash continuations into one logical line.
|
|
244
|
+
while text.rstrip().endswith("\\") and i + 1 < n:
|
|
245
|
+
text = text.rstrip()[:-1].rstrip()
|
|
246
|
+
i += 1
|
|
247
|
+
_, next_line = block_lines[i]
|
|
248
|
+
text = (text + " " + next_line.strip()).strip()
|
|
249
|
+
|
|
250
|
+
i += 1
|
|
251
|
+
|
|
252
|
+
if not text or text.startswith("#"):
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
for raw_seg in split_segments(text):
|
|
256
|
+
seg = normalize(raw_seg)
|
|
257
|
+
if not seg:
|
|
258
|
+
continue
|
|
259
|
+
words = seg.split(" ")
|
|
260
|
+
idx = 0
|
|
261
|
+
while idx < len(words) and ASSIGNMENT_RE.match(words[idx]):
|
|
262
|
+
idx += 1
|
|
263
|
+
if idx >= len(words):
|
|
264
|
+
continue # segment was only VAR=value assignments
|
|
265
|
+
command_token = words[idx]
|
|
266
|
+
if command_token in IGNORE_BUILTINS:
|
|
267
|
+
continue
|
|
268
|
+
results.append((start_lineno, " ".join(words[idx:])))
|
|
269
|
+
return results
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def scan_skill(
|
|
273
|
+
skill_md: Path,
|
|
274
|
+
) -> tuple[bool, int, list[tuple[int, str]], str | None]:
|
|
275
|
+
"""Return (has_shell_block, segment_count, [(lineno, segment), ...] violations, error).
|
|
276
|
+
|
|
277
|
+
`error` is None unless the skill's allowed-tools declares a Bash(...)
|
|
278
|
+
token that fails to compile (e.g. too many '*' wildcards — see
|
|
279
|
+
MAX_WILDCARDS). In that case the other fields are unpopulated
|
|
280
|
+
(False, 0, []) and the caller must treat this skill as a hard failure
|
|
281
|
+
rather than silently skipping or crashing on it.
|
|
282
|
+
"""
|
|
283
|
+
text = skill_md.read_text(encoding="utf-8")
|
|
284
|
+
lines = text.splitlines()
|
|
285
|
+
tokens = extract_allowed_tools_tokens(text)
|
|
286
|
+
try:
|
|
287
|
+
has_bare_bash, matchers = build_bash_matchers(tokens)
|
|
288
|
+
except ValueError as exc:
|
|
289
|
+
return False, 0, [], f"{skill_md}: {exc}"
|
|
290
|
+
|
|
291
|
+
has_shell_block = False
|
|
292
|
+
segment_count = 0
|
|
293
|
+
violations: list[tuple[int, str]] = []
|
|
294
|
+
|
|
295
|
+
for info, body in iter_fenced_blocks(lines):
|
|
296
|
+
if info not in FENCE_LANGS:
|
|
297
|
+
continue
|
|
298
|
+
has_shell_block = True
|
|
299
|
+
for lineno, seg in extract_command_segments(body):
|
|
300
|
+
segment_count += 1
|
|
301
|
+
if has_bare_bash:
|
|
302
|
+
continue
|
|
303
|
+
if any(m.match(seg) for m in matchers):
|
|
304
|
+
continue
|
|
305
|
+
violations.append((lineno, seg))
|
|
306
|
+
|
|
307
|
+
return has_shell_block, segment_count, violations, None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def main() -> int:
|
|
311
|
+
skill_files = sorted(SKILLS_DIR.glob("*/*/SKILL.md"))
|
|
312
|
+
if not skill_files:
|
|
313
|
+
print("ERROR: no SKILL.md files found", file=sys.stderr)
|
|
314
|
+
return 2
|
|
315
|
+
|
|
316
|
+
skills_with_blocks = 0
|
|
317
|
+
total_segments = 0
|
|
318
|
+
total_violations = 0
|
|
319
|
+
skills_with_violations = 0
|
|
320
|
+
violation_lines: list[str] = []
|
|
321
|
+
scan_errors: list[str] = []
|
|
322
|
+
|
|
323
|
+
for skill_md in skill_files:
|
|
324
|
+
has_shell_block, segment_count, violations, error = scan_skill(skill_md)
|
|
325
|
+
if error is not None:
|
|
326
|
+
scan_errors.append(error)
|
|
327
|
+
continue
|
|
328
|
+
if has_shell_block:
|
|
329
|
+
skills_with_blocks += 1
|
|
330
|
+
total_segments += segment_count
|
|
331
|
+
if violations:
|
|
332
|
+
skills_with_violations += 1
|
|
333
|
+
total_violations += len(violations)
|
|
334
|
+
for lineno, seg in violations:
|
|
335
|
+
violation_lines.append(
|
|
336
|
+
f"{skill_md}:{lineno}: command not covered by allowed-tools: {seg}"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if scan_errors:
|
|
340
|
+
for line in scan_errors:
|
|
341
|
+
print(f"ERROR: {line}", file=sys.stderr)
|
|
342
|
+
|
|
343
|
+
if violation_lines:
|
|
344
|
+
for line in violation_lines:
|
|
345
|
+
print(line, file=sys.stderr)
|
|
346
|
+
print(
|
|
347
|
+
f"ERROR: {total_violations} uncovered command(s) in "
|
|
348
|
+
f"{skills_with_violations} skill(s); fix the skill's allowed-tools "
|
|
349
|
+
f"(narrowest matching pattern), never delete the command.",
|
|
350
|
+
file=sys.stderr,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
if scan_errors or violation_lines:
|
|
354
|
+
return 1
|
|
355
|
+
|
|
356
|
+
print(
|
|
357
|
+
f"OK: skill coherence — {skills_with_blocks} skill(s) with shell blocks, "
|
|
358
|
+
f"{total_segments} command segment(s), all covered"
|
|
359
|
+
)
|
|
360
|
+
return 0
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
if __name__ == "__main__":
|
|
364
|
+
sys.exit(main())
|