skill-customs 0.1.0__tar.gz
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.
- skill_customs-0.1.0/.gitignore +32 -0
- skill_customs-0.1.0/LICENSE +21 -0
- skill_customs-0.1.0/PKG-INFO +208 -0
- skill_customs-0.1.0/README.md +181 -0
- skill_customs-0.1.0/pyproject.toml +42 -0
- skill_customs-0.1.0/src/skill_customs/__init__.py +77 -0
- skill_customs-0.1.0/src/skill_customs/attribution.py +32 -0
- skill_customs-0.1.0/src/skill_customs/cli.py +266 -0
- skill_customs-0.1.0/src/skill_customs/discover.py +170 -0
- skill_customs-0.1.0/src/skill_customs/errors.py +37 -0
- skill_customs-0.1.0/src/skill_customs/fetch.py +168 -0
- skill_customs-0.1.0/src/skill_customs/frontmatter.py +75 -0
- skill_customs-0.1.0/src/skill_customs/hashing.py +20 -0
- skill_customs-0.1.0/src/skill_customs/install.py +237 -0
- skill_customs-0.1.0/src/skill_customs/licenses.py +152 -0
- skill_customs-0.1.0/src/skill_customs/lockfile.py +137 -0
- skill_customs-0.1.0/src/skill_customs/policy.py +49 -0
- skill_customs-0.1.0/src/skill_customs/py.typed +0 -0
- skill_customs-0.1.0/src/skill_customs/scan/__init__.py +233 -0
- skill_customs-0.1.0/src/skill_customs/scan/rules.py +222 -0
- skill_customs-0.1.0/tests/conftest.py +97 -0
- skill_customs-0.1.0/tests/test_sc_install.py +641 -0
- skill_customs-0.1.0/tests/test_sc_license.py +117 -0
- skill_customs-0.1.0/tests/test_sc_scan.py +244 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.venv/
|
|
5
|
+
venv/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.mypy_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.coverage
|
|
13
|
+
htmlcov/
|
|
14
|
+
|
|
15
|
+
# Secrets
|
|
16
|
+
.env
|
|
17
|
+
.env.*
|
|
18
|
+
!.env.example
|
|
19
|
+
|
|
20
|
+
# Local runtime data
|
|
21
|
+
.trypro/
|
|
22
|
+
fastembed-cache/
|
|
23
|
+
|
|
24
|
+
# OS and editors
|
|
25
|
+
.DS_Store
|
|
26
|
+
Thumbs.db
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
29
|
+
|
|
30
|
+
# Claude Code settings and generated role agents
|
|
31
|
+
.claude/
|
|
32
|
+
.mcp.json
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Try Professional contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: skill-customs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent skills security: a lockfile, a license check, and commit pinning for the skills you install from GitHub. Scanners plug in.
|
|
5
|
+
Project-URL: Homepage, https://github.com/goranai/try-professional
|
|
6
|
+
Project-URL: Repository, https://github.com/goranai/try-professional
|
|
7
|
+
Project-URL: Documentation, https://github.com/goranai/try-professional/blob/main/packages/skill-customs/README.md
|
|
8
|
+
Project-URL: Issues, https://github.com/goranai/try-professional/issues
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent-skills,agent-skills-security,ai-agents,claude-skills,license-check,lockfile,security,supply-chain-security
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: pyyaml<7,>=6
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# skill-customs
|
|
29
|
+
|
|
30
|
+
The lockfile, license, and pinning layer for agent skills.
|
|
31
|
+
|
|
32
|
+
Agent skills are folders of instructions, and sometimes scripts, that you download from strangers and give to an agent. `skill-customs` records exactly what you installed, from which commit, under which license, and how far you trust it, the way a lockfile does for code. It works with any agent framework and has one dependency, PyYAML.
|
|
33
|
+
|
|
34
|
+
**It doesn't compete on scanning.** A static scanner ships as the default, and other scanners plug in. Use the best one you can find.
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
GitHub ─► fetch by full SHA ─► license gate ─► scan ─► skills.lock
|
|
38
|
+
│ │
|
|
39
|
+
rejected: nothing clean: skills/installed/ (indexed)
|
|
40
|
+
is installed else: skills/quarantine/ (until a person approves)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Try it in under 5 minutes
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install skill-customs
|
|
47
|
+
|
|
48
|
+
# Scan and license-check any skill folder. Nothing needs to be set up.
|
|
49
|
+
skill-customs scan ./my-skill # exit 0 pass, 1 review, 2 block
|
|
50
|
+
skill-customs license ./my-skill
|
|
51
|
+
|
|
52
|
+
# Pin a skill. Branches and tags aren't accepted, so get a full SHA first.
|
|
53
|
+
skill-customs resolve obra/superpowers@main
|
|
54
|
+
skill-customs add obra/superpowers --commit <40-character-sha> --path skills/test-driven-development
|
|
55
|
+
|
|
56
|
+
skill-customs list # what's locked, and how far it's trusted
|
|
57
|
+
skill-customs verify # do the files still match skills.lock? Run this in CI.
|
|
58
|
+
skill-customs attribution --out THIRD_PARTY_SKILLS.md
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Commit `skills.lock`. Point your agent at `skills/installed/`, which never contains anything quarantined.
|
|
62
|
+
|
|
63
|
+
Set `GITHUB_TOKEN` for higher rate limits. It's optional, read-only access to public repositories is enough, and it's never sent to any host but GitHub's API.
|
|
64
|
+
|
|
65
|
+
## What it guarantees, and what it doesn't
|
|
66
|
+
|
|
67
|
+
- **Nothing from a skill is ever run.** It downloads a tarball, so no git hooks can fire, and it writes files itself.
|
|
68
|
+
- **A pin is exact.** Only a full 40-character SHA is accepted, and a hash of the whole folder is recorded. `verify` catches any edited, added, or removed file.
|
|
69
|
+
- **Archives aren't trusted.** Links, path traversal, special files, and oversized content stop the install.
|
|
70
|
+
- **Quarantine is a real location.** A skill that isn't approved isn't in the folder your agent reads.
|
|
71
|
+
- **An approval covers one exact version.** Any change to the content voids it.
|
|
72
|
+
|
|
73
|
+
**A passing scan doesn't mean a skill is safe.** It means no known signal was found. Static rules can be bypassed by rephrasing, and they will sometimes flag innocent text: a skill that teaches prompt writing may well quote the phrases the rules look for. That's why `indexed` skills should still be treated as untrusted data, and why your runtime should limit what an agent can do whatever a skill tells it. Scanning reduces risk, and doesn't remove it.
|
|
74
|
+
|
|
75
|
+
## Trust levels
|
|
76
|
+
|
|
77
|
+
| Level | Meaning | Where it lives |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `indexed` | Passed automated checks. Instructions only, with no scripts or findings | `installed/` |
|
|
80
|
+
| `quarantined` | Has scripts or findings, or was written locally. Waiting for a person | `quarantine/` |
|
|
81
|
+
| `verified` | A person reviewed this exact content | `installed/` |
|
|
82
|
+
|
|
83
|
+
To approve a skill, read its files, then run this at a terminal:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
skill-customs approve owner/repo/skills/name --by "Your Name"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The command refuses to run without a terminal, and asks you to type the skill ID back, so it can't be casually run by a script or an agent. It isn't a hard security boundary: the Python function exists, and code with write access to your project can do anything. **Don't expose approval as an agent tool.**
|
|
90
|
+
|
|
91
|
+
## Licenses
|
|
92
|
+
|
|
93
|
+
Only these are accepted by default: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC. To change that, add `skill-customs.yaml`:
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
allowed_licenses: [MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, MPL-2.0]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
It reads the skill's declared license, its own license file, and the repository's license file. An unknown license is rejected. If the sources disagree, the stricter reading wins, so a skill can't declare MIT inside a GPL repository. Non-commercial and proprietary terms are flagged. This is an automated check of license text, and isn't legal advice.
|
|
100
|
+
|
|
101
|
+
## Use it from Python
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from pathlib import Path
|
|
105
|
+
from skill_customs import GitHubClient, LicenseRejected, Lockfile, Policy, check_license, pin, scan, verify
|
|
106
|
+
|
|
107
|
+
report = scan(Path("./my-skill"))
|
|
108
|
+
print(report.verdict) # pass, review, or block
|
|
109
|
+
|
|
110
|
+
policy = Policy.default()
|
|
111
|
+
print(check_license(Path("./my-skill"), policy).allowed)
|
|
112
|
+
|
|
113
|
+
lock = Lockfile.load("skills.lock")
|
|
114
|
+
commit = GitHubClient().resolve_ref("obra/superpowers", "main")
|
|
115
|
+
try:
|
|
116
|
+
result = pin("obra/superpowers", commit, path="skills/test-driven-development", dest=Path("skills"), policy=policy)
|
|
117
|
+
lock.add(result.entry).save()
|
|
118
|
+
except LicenseRejected as error:
|
|
119
|
+
print(error.verdict.reason)
|
|
120
|
+
|
|
121
|
+
assert verify(lock, dest=Path("skills")) == []
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Plug in another scanner
|
|
125
|
+
|
|
126
|
+
Any of these run alongside the built-in one, and a scanner that crashes counts as a blocking finding.
|
|
127
|
+
|
|
128
|
+
**An external command.** Exit 0 means clean, and anything else sends the skill to a person. Its output isn't interpreted.
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
# skill-customs.yaml
|
|
132
|
+
command_scanners:
|
|
133
|
+
my-scanner: [my-scanner, scan, --strict] # the skill's folder is added as the last argument
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**A Python class:**
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from pathlib import Path
|
|
140
|
+
from skill_customs import Finding, scan
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class MyScanner:
|
|
144
|
+
name = "my-scanner"
|
|
145
|
+
|
|
146
|
+
def scan(self, root: Path) -> list[Finding]:
|
|
147
|
+
return [Finding("my-rule", "review", "SKILL.md", 1, "Why this needs a look", scanner=self.name)]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
report = scan(Path("./my-skill"), scanners=[MyScanner()])
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**An installed package,** through the `skill_customs.scanners` entry point group. It runs only if `scanners:` in the policy names it, so installing a package doesn't turn anything on.
|
|
154
|
+
|
|
155
|
+
## Use it with your framework
|
|
156
|
+
|
|
157
|
+
It needs nothing from your framework, and your framework needs only a folder and `skills.lock`.
|
|
158
|
+
|
|
159
|
+
**Any framework.** Read skills from `skills/installed/`, and treat the text as data rather than as trusted instructions. To read the lockfile:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from pathlib import Path
|
|
163
|
+
from skill_customs import Lockfile, read_skill_metadata, skill_dir
|
|
164
|
+
|
|
165
|
+
lock = Lockfile.load("skills.lock")
|
|
166
|
+
for skill_id, entry in lock.entries.items():
|
|
167
|
+
if entry.trust != "quarantined":
|
|
168
|
+
folder = skill_dir(entry, Path("skills"))
|
|
169
|
+
print(skill_id, entry.trust, read_skill_metadata(folder).description)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Hermes Agent.** Add the installed folder as an external skill directory in `~/.hermes/config.yaml`. Hermes treats skills there as instructions, so consider listing a folder that holds only `verified` skills:
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
skills:
|
|
176
|
+
external_dirs:
|
|
177
|
+
- /path/to/project/skills/installed
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**MCP clients and CrewAI.** There's no packaged integration yet. The approach above works, and these are good first issues.
|
|
181
|
+
|
|
182
|
+
## In CI
|
|
183
|
+
|
|
184
|
+
```yaml
|
|
185
|
+
- run: pip install skill-customs
|
|
186
|
+
- run: skill-customs verify
|
|
187
|
+
- run: skill-customs attribution --check THIRD_PARTY_SKILLS.md
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Finding candidates
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
skill-customs discover "code review agent skills" --top 40
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
This ranks public repositories by stars, recent activity, and open issues, and installs nothing. Stars can be bought, so ranking only decides what to look at first, and every candidate still passes the same gates.
|
|
197
|
+
|
|
198
|
+
To refresh a whole list, map each category to a search query, and run it on your own schedule. There's no daemon.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
skill-customs discover --categories categories.yaml --out candidates.json
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Status
|
|
205
|
+
|
|
206
|
+
Pre-release. Part of the [Try Professional](https://github.com/goranai/try-professional) repository, and usable entirely without it.
|
|
207
|
+
|
|
208
|
+
MIT licensed.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# skill-customs
|
|
2
|
+
|
|
3
|
+
The lockfile, license, and pinning layer for agent skills.
|
|
4
|
+
|
|
5
|
+
Agent skills are folders of instructions, and sometimes scripts, that you download from strangers and give to an agent. `skill-customs` records exactly what you installed, from which commit, under which license, and how far you trust it, the way a lockfile does for code. It works with any agent framework and has one dependency, PyYAML.
|
|
6
|
+
|
|
7
|
+
**It doesn't compete on scanning.** A static scanner ships as the default, and other scanners plug in. Use the best one you can find.
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
GitHub ─► fetch by full SHA ─► license gate ─► scan ─► skills.lock
|
|
11
|
+
│ │
|
|
12
|
+
rejected: nothing clean: skills/installed/ (indexed)
|
|
13
|
+
is installed else: skills/quarantine/ (until a person approves)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Try it in under 5 minutes
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install skill-customs
|
|
20
|
+
|
|
21
|
+
# Scan and license-check any skill folder. Nothing needs to be set up.
|
|
22
|
+
skill-customs scan ./my-skill # exit 0 pass, 1 review, 2 block
|
|
23
|
+
skill-customs license ./my-skill
|
|
24
|
+
|
|
25
|
+
# Pin a skill. Branches and tags aren't accepted, so get a full SHA first.
|
|
26
|
+
skill-customs resolve obra/superpowers@main
|
|
27
|
+
skill-customs add obra/superpowers --commit <40-character-sha> --path skills/test-driven-development
|
|
28
|
+
|
|
29
|
+
skill-customs list # what's locked, and how far it's trusted
|
|
30
|
+
skill-customs verify # do the files still match skills.lock? Run this in CI.
|
|
31
|
+
skill-customs attribution --out THIRD_PARTY_SKILLS.md
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Commit `skills.lock`. Point your agent at `skills/installed/`, which never contains anything quarantined.
|
|
35
|
+
|
|
36
|
+
Set `GITHUB_TOKEN` for higher rate limits. It's optional, read-only access to public repositories is enough, and it's never sent to any host but GitHub's API.
|
|
37
|
+
|
|
38
|
+
## What it guarantees, and what it doesn't
|
|
39
|
+
|
|
40
|
+
- **Nothing from a skill is ever run.** It downloads a tarball, so no git hooks can fire, and it writes files itself.
|
|
41
|
+
- **A pin is exact.** Only a full 40-character SHA is accepted, and a hash of the whole folder is recorded. `verify` catches any edited, added, or removed file.
|
|
42
|
+
- **Archives aren't trusted.** Links, path traversal, special files, and oversized content stop the install.
|
|
43
|
+
- **Quarantine is a real location.** A skill that isn't approved isn't in the folder your agent reads.
|
|
44
|
+
- **An approval covers one exact version.** Any change to the content voids it.
|
|
45
|
+
|
|
46
|
+
**A passing scan doesn't mean a skill is safe.** It means no known signal was found. Static rules can be bypassed by rephrasing, and they will sometimes flag innocent text: a skill that teaches prompt writing may well quote the phrases the rules look for. That's why `indexed` skills should still be treated as untrusted data, and why your runtime should limit what an agent can do whatever a skill tells it. Scanning reduces risk, and doesn't remove it.
|
|
47
|
+
|
|
48
|
+
## Trust levels
|
|
49
|
+
|
|
50
|
+
| Level | Meaning | Where it lives |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `indexed` | Passed automated checks. Instructions only, with no scripts or findings | `installed/` |
|
|
53
|
+
| `quarantined` | Has scripts or findings, or was written locally. Waiting for a person | `quarantine/` |
|
|
54
|
+
| `verified` | A person reviewed this exact content | `installed/` |
|
|
55
|
+
|
|
56
|
+
To approve a skill, read its files, then run this at a terminal:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
skill-customs approve owner/repo/skills/name --by "Your Name"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The command refuses to run without a terminal, and asks you to type the skill ID back, so it can't be casually run by a script or an agent. It isn't a hard security boundary: the Python function exists, and code with write access to your project can do anything. **Don't expose approval as an agent tool.**
|
|
63
|
+
|
|
64
|
+
## Licenses
|
|
65
|
+
|
|
66
|
+
Only these are accepted by default: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC. To change that, add `skill-customs.yaml`:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
allowed_licenses: [MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, MPL-2.0]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
It reads the skill's declared license, its own license file, and the repository's license file. An unknown license is rejected. If the sources disagree, the stricter reading wins, so a skill can't declare MIT inside a GPL repository. Non-commercial and proprietary terms are flagged. This is an automated check of license text, and isn't legal advice.
|
|
73
|
+
|
|
74
|
+
## Use it from Python
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from pathlib import Path
|
|
78
|
+
from skill_customs import GitHubClient, LicenseRejected, Lockfile, Policy, check_license, pin, scan, verify
|
|
79
|
+
|
|
80
|
+
report = scan(Path("./my-skill"))
|
|
81
|
+
print(report.verdict) # pass, review, or block
|
|
82
|
+
|
|
83
|
+
policy = Policy.default()
|
|
84
|
+
print(check_license(Path("./my-skill"), policy).allowed)
|
|
85
|
+
|
|
86
|
+
lock = Lockfile.load("skills.lock")
|
|
87
|
+
commit = GitHubClient().resolve_ref("obra/superpowers", "main")
|
|
88
|
+
try:
|
|
89
|
+
result = pin("obra/superpowers", commit, path="skills/test-driven-development", dest=Path("skills"), policy=policy)
|
|
90
|
+
lock.add(result.entry).save()
|
|
91
|
+
except LicenseRejected as error:
|
|
92
|
+
print(error.verdict.reason)
|
|
93
|
+
|
|
94
|
+
assert verify(lock, dest=Path("skills")) == []
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Plug in another scanner
|
|
98
|
+
|
|
99
|
+
Any of these run alongside the built-in one, and a scanner that crashes counts as a blocking finding.
|
|
100
|
+
|
|
101
|
+
**An external command.** Exit 0 means clean, and anything else sends the skill to a person. Its output isn't interpreted.
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
# skill-customs.yaml
|
|
105
|
+
command_scanners:
|
|
106
|
+
my-scanner: [my-scanner, scan, --strict] # the skill's folder is added as the last argument
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**A Python class:**
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from pathlib import Path
|
|
113
|
+
from skill_customs import Finding, scan
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class MyScanner:
|
|
117
|
+
name = "my-scanner"
|
|
118
|
+
|
|
119
|
+
def scan(self, root: Path) -> list[Finding]:
|
|
120
|
+
return [Finding("my-rule", "review", "SKILL.md", 1, "Why this needs a look", scanner=self.name)]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
report = scan(Path("./my-skill"), scanners=[MyScanner()])
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**An installed package,** through the `skill_customs.scanners` entry point group. It runs only if `scanners:` in the policy names it, so installing a package doesn't turn anything on.
|
|
127
|
+
|
|
128
|
+
## Use it with your framework
|
|
129
|
+
|
|
130
|
+
It needs nothing from your framework, and your framework needs only a folder and `skills.lock`.
|
|
131
|
+
|
|
132
|
+
**Any framework.** Read skills from `skills/installed/`, and treat the text as data rather than as trusted instructions. To read the lockfile:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from pathlib import Path
|
|
136
|
+
from skill_customs import Lockfile, read_skill_metadata, skill_dir
|
|
137
|
+
|
|
138
|
+
lock = Lockfile.load("skills.lock")
|
|
139
|
+
for skill_id, entry in lock.entries.items():
|
|
140
|
+
if entry.trust != "quarantined":
|
|
141
|
+
folder = skill_dir(entry, Path("skills"))
|
|
142
|
+
print(skill_id, entry.trust, read_skill_metadata(folder).description)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Hermes Agent.** Add the installed folder as an external skill directory in `~/.hermes/config.yaml`. Hermes treats skills there as instructions, so consider listing a folder that holds only `verified` skills:
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
skills:
|
|
149
|
+
external_dirs:
|
|
150
|
+
- /path/to/project/skills/installed
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**MCP clients and CrewAI.** There's no packaged integration yet. The approach above works, and these are good first issues.
|
|
154
|
+
|
|
155
|
+
## In CI
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
- run: pip install skill-customs
|
|
159
|
+
- run: skill-customs verify
|
|
160
|
+
- run: skill-customs attribution --check THIRD_PARTY_SKILLS.md
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Finding candidates
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
skill-customs discover "code review agent skills" --top 40
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
This ranks public repositories by stars, recent activity, and open issues, and installs nothing. Stars can be bought, so ranking only decides what to look at first, and every candidate still passes the same gates.
|
|
170
|
+
|
|
171
|
+
To refresh a whole list, map each category to a search query, and run it on your own schedule. There's no daemon.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
skill-customs discover --categories categories.yaml --out candidates.json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Status
|
|
178
|
+
|
|
179
|
+
Pre-release. Part of the [Try Professional](https://github.com/goranai/try-professional) repository, and usable entirely without it.
|
|
180
|
+
|
|
181
|
+
MIT licensed.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "skill-customs"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Agent skills security: a lockfile, a license check, and commit pinning for the skills you install from GitHub. Scanners plug in."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["agent-skills", "agent-skills-security", "claude-skills", "ai-agents", "lockfile", "license-check", "supply-chain-security", "security"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Security",
|
|
24
|
+
"Topic :: Software Development :: Build Tools",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["pyyaml>=6,<7"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/goranai/try-professional"
|
|
31
|
+
Repository = "https://github.com/goranai/try-professional"
|
|
32
|
+
Documentation = "https://github.com/goranai/try-professional/blob/main/packages/skill-customs/README.md"
|
|
33
|
+
Issues = "https://github.com/goranai/try-professional/issues"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
skill-customs = "skill_customs.cli:main"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.version]
|
|
39
|
+
path = "src/skill_customs/__init__.py"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/skill_customs"]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""skill-customs: the lockfile, license, and pinning layer for agent skills.
|
|
2
|
+
|
|
3
|
+
Fetching never runs anything from a skill. A passing scan means no known signal was
|
|
4
|
+
found. It does not mean a skill is safe.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .attribution import render_attribution
|
|
8
|
+
from .discover import Candidate, SkillFile, discover, list_skill_paths, search_skill_files
|
|
9
|
+
from .errors import (
|
|
10
|
+
ApprovalError,
|
|
11
|
+
FetchError,
|
|
12
|
+
LicenseRejected,
|
|
13
|
+
LockfileError,
|
|
14
|
+
NotASkill,
|
|
15
|
+
SkillCustomsError,
|
|
16
|
+
UnsafeArchive,
|
|
17
|
+
)
|
|
18
|
+
from .fetch import GitHubClient, is_full_sha
|
|
19
|
+
from .frontmatter import SkillMetadata, read_skill_metadata
|
|
20
|
+
from .hashing import content_hash
|
|
21
|
+
from .install import PinResult, approve, pin, pin_local, remove, skill_dir, verify
|
|
22
|
+
from .licenses import LicenseVerdict, check_license
|
|
23
|
+
from .lockfile import Approval, LockEntry, Lockfile
|
|
24
|
+
from .policy import Policy
|
|
25
|
+
from .scan import (
|
|
26
|
+
BuiltinScanner,
|
|
27
|
+
CommandScanner,
|
|
28
|
+
Finding,
|
|
29
|
+
Scanner,
|
|
30
|
+
ScanReport,
|
|
31
|
+
decide_trust,
|
|
32
|
+
scan,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"Approval",
|
|
39
|
+
"ApprovalError",
|
|
40
|
+
"BuiltinScanner",
|
|
41
|
+
"Candidate",
|
|
42
|
+
"CommandScanner",
|
|
43
|
+
"FetchError",
|
|
44
|
+
"Finding",
|
|
45
|
+
"GitHubClient",
|
|
46
|
+
"LicenseRejected",
|
|
47
|
+
"LicenseVerdict",
|
|
48
|
+
"LockEntry",
|
|
49
|
+
"Lockfile",
|
|
50
|
+
"LockfileError",
|
|
51
|
+
"NotASkill",
|
|
52
|
+
"PinResult",
|
|
53
|
+
"Policy",
|
|
54
|
+
"ScanReport",
|
|
55
|
+
"Scanner",
|
|
56
|
+
"SkillCustomsError",
|
|
57
|
+
"SkillFile",
|
|
58
|
+
"SkillMetadata",
|
|
59
|
+
"UnsafeArchive",
|
|
60
|
+
"__version__",
|
|
61
|
+
"approve",
|
|
62
|
+
"check_license",
|
|
63
|
+
"content_hash",
|
|
64
|
+
"decide_trust",
|
|
65
|
+
"discover",
|
|
66
|
+
"is_full_sha",
|
|
67
|
+
"list_skill_paths",
|
|
68
|
+
"pin",
|
|
69
|
+
"pin_local",
|
|
70
|
+
"read_skill_metadata",
|
|
71
|
+
"remove",
|
|
72
|
+
"render_attribution",
|
|
73
|
+
"scan",
|
|
74
|
+
"search_skill_files",
|
|
75
|
+
"skill_dir",
|
|
76
|
+
"verify",
|
|
77
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Generate THIRD_PARTY_SKILLS.md from the lockfile, so it can't drift from what's installed."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .lockfile import Lockfile
|
|
6
|
+
|
|
7
|
+
HEADER = """# Third-party skills
|
|
8
|
+
|
|
9
|
+
Generated by `skill-customs attribution` from `skills.lock`. Don't edit by hand.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _cell(text: str) -> str:
|
|
14
|
+
"""Skill names come from outside. Keep them from breaking out of a table cell."""
|
|
15
|
+
return text.replace("|", "\\|").replace("\n", " ")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def render_attribution(lock: Lockfile) -> str:
|
|
19
|
+
entries = [lock.entries[key] for key in sorted(lock.entries) if lock.entries[key].host != "local"]
|
|
20
|
+
if not entries:
|
|
21
|
+
return HEADER + "\nNo third-party skills are installed.\n"
|
|
22
|
+
lines = [HEADER, "| Skill | Source | Commit | License | Commercial use | Trust |", "|---|---|---|---|---|---|"]
|
|
23
|
+
for entry in entries:
|
|
24
|
+
source = f"https://github.com/{entry.repo}"
|
|
25
|
+
lines.append(
|
|
26
|
+
f"| {_cell(entry.skill_id)} | {source} | `{entry.commit[:12]}` | {_cell(entry.license_spdx or 'unknown')} "
|
|
27
|
+
f"| {entry.commercial_use} | {entry.trust} |"
|
|
28
|
+
)
|
|
29
|
+
restricted = [e.skill_id for e in entries if e.commercial_use != "allowed"]
|
|
30
|
+
if restricted:
|
|
31
|
+
lines += ["", "## Check before commercial use", ""] + [f"- {_cell(skill)}" for skill in restricted]
|
|
32
|
+
return "\n".join(lines) + "\n"
|