@raishin/vanguard-frontier-agentic 3.1.1 → 3.2.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/plugin/marketplace.json +1 -1
- package/catalog/asset-integrity.json +59 -24
- package/catalog/model-assignments.json +19613 -0
- package/catalog/model-policy.json +371 -0
- package/catalog/model-registry.json +134 -0
- package/catalog/skill-manifest.json +18 -18
- package/package.json +7 -2
- package/plugins/vanguard-frontier-agentic/.codex-plugin/plugin.json +1 -1
- package/schemas/model-policy.schema.json +60 -0
- package/schemas/model-registry.schema.json +127 -0
- package/scripts/model-policy.mjs +1353 -0
- package/skills/gcp/gcp-firebase-developer/SKILL.md +1 -1
- package/skills/gcp/gcp-gke-platform-operator/SKILL.md +1 -1
- 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/validate-skill-coherence.py +364 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gcp-firebase-developer
|
|
3
3
|
description: "Build, configure, and operate Firebase-powered web and mobile applications — covering Firestore, Firebase Auth, Firebase Hosting, Cloud Functions for Firebase, Firebase Storage, App Check, Firebase Remote Config, and Firebase Analytics. Use when building mobile/web apps with Firebase, setting up authentication flows, designing Firestore data models, deploying Firebase Hosting, or configuring Firebase security rules."
|
|
4
|
-
allowed-tools: Read Grep Glob
|
|
4
|
+
allowed-tools: Read Grep Glob Bash(npm install:*) Bash(firebase:*)
|
|
5
5
|
metadata:
|
|
6
6
|
author: "github: Raishin"
|
|
7
7
|
version: "0.1.0"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gcp-gke-platform-operator
|
|
3
3
|
description: Operate GKE clusters (Standard and Autopilot), manage node pools, configure Workload Identity, enforce Binary Authorization, plan node pool upgrades, and review cluster security posture.
|
|
4
|
-
allowed-tools: Read Grep Glob
|
|
4
|
+
allowed-tools: Read Grep Glob Bash(gcloud container:*) Bash(kubectl apply:*)
|
|
5
5
|
metadata:
|
|
6
6
|
author: "github: Raishin"
|
|
7
7
|
version: "0.2.0"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: salesforce-agentforce-stdm-observer-skill
|
|
3
3
|
description: "Queries Salesforce Telemetry & Data Management (STDM) and Data Cloud for live Agentforce session traces, faithfulness scores, answer relevance scores, action invocation telemetry, and quality metrics under T1 least-privilege scope (api + refresh_token + cdp_query_api). Answers the production observability question: \"is my Agentforce agent working correctly right now?\" Operational counterpart to the static-review salesforce-agentforce-risk-review-skill. TRIGGER when: user asks for Agentforce session metrics, faithfulness scores, answer relevance, AI Evaluation results, action telemetry, STDM queries, agent performance KPIs, hallucination rates. Trigger phrases: \"how is my agent performing\", \"show STDM data\", \"agent observability\", \"agent telemetry\", \"AiAgentTagAssociation\", \"AiEvaluationDefinition results\", \"agentforce production metrics\". DO NOT TRIGGER when: user wants static configuration review (use salesforce-agentforce-risk-review-skill); when modifying agent configurations (T3 — escalate to salesforce-live-guard-agent); when Agent Script authoring is needed (out of Wave 4 scope, route to forcedotcom/sf-skills developing-agentforce reference)."
|
|
4
4
|
license: MIT
|
|
5
|
-
allowed-tools: Bash(sf data query:*) Bash(sf agent test:*) Bash(sf org display:*) Read Grep Glob
|
|
5
|
+
allowed-tools: Bash(sf data query:*) Bash(sf agent test:*) Bash(sf org display:*) Read Grep Glob Bash(sf api request:*) Bash(sf apex run:*)
|
|
6
6
|
metadata:
|
|
7
7
|
author: "github: Raishin"
|
|
8
8
|
version: 0.1.0
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: salesforce-apex-log-analyzer-skill
|
|
3
3
|
description: "Retrieves and analyzes Apex debug logs from a connected Salesforce org to identify governor-limit hits, SOQL N+1 patterns, unhandled exceptions, and async job failures. T1 read-only runtime — retrieves logs only, never executes code or mutates data. TRIGGER when: user asks to analyze an Apex log, debug a trigger failure, diagnose a governor limit hit, interpret a stack trace from a Salesforce org, or review a DEBUG log for performance issues. Trigger phrases: analyze apex log, debug this trigger, why is my trigger failing, governor limit hit, DEBUG log analysis, check my log file. DO NOT TRIGGER when: user wants to run live tests (use salesforce-apex-test-runner-skill), static code review without logs (use salesforce-apex-lwc-code-review-skill), generating new Apex code (use salesforce-apex-generator-skill), or Agentforce session telemetry (use salesforce-agentforce-stdm-observer-skill)."
|
|
4
4
|
license: MIT
|
|
5
|
-
allowed-tools: Bash(sf apex get log:*) Bash(sf apex tail log:*) Bash(sf data query:*) Read Grep Glob
|
|
5
|
+
allowed-tools: Bash(sf apex get log:*) Bash(sf apex tail log:*) Bash(sf data query:*) Read Grep Glob Bash(sf org display:*)
|
|
6
6
|
metadata:
|
|
7
7
|
author: "github: Raishin"
|
|
8
8
|
version: "0.1.0"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: salesforce-soql-explorer-skill
|
|
3
3
|
description: "Executes read-only SOQL queries against a connected Salesforce org via the sf data query CLI under T1 least-privilege scope (api + refresh_token only, Run As service account with no ModifyAllData/ViewAllData/ViewEncryptedData). Returns sanitized JSON with a structured audit envelope. Live operational counterpart to the static-review skills. TRIGGER when: user asks to query records, run SOQL, fetch live data, inspect records by ID, count records, run aggregate queries, or check field values in a live org. Trigger phrases: query my org, run SOQL, show me records where, how many opportunities, what is the value of field X on record Y. DO NOT TRIGGER when: user pastes a metadata XML export for static review (use salesforce-metadata-review-skill); request requires DML — write, update, delete — those are T3 prohibited; bulk data operations needed (use salesforce-bulk-data-ops-skill); only schema metadata needed without data (use salesforce-metadata-fetcher-skill)."
|
|
4
4
|
license: MIT
|
|
5
|
-
allowed-tools: Bash(sf data query:*) Bash(sf org list:*) Bash(sf org display:*) Read Grep Glob
|
|
5
|
+
allowed-tools: Bash(sf data query:*) Bash(sf org list:*) Bash(sf org display:*) Read Grep Glob Bash(sf sobject describe:*) Bash(jq:*)
|
|
6
6
|
metadata:
|
|
7
7
|
author: "github: Raishin"
|
|
8
8
|
version: "0.1.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())
|