oasr 0.5.2__py3-none-any.whl → 0.6.0__py3-none-any.whl
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.
- agents/base.py +17 -3
- agents/claude.py +12 -2
- agents/codex.py +12 -2
- agents/copilot.py +12 -2
- agents/opencode.py +12 -2
- cli.py +3 -2
- commands/config.py +284 -37
- commands/exec.py +21 -1
- commands/profile.py +84 -0
- commands/update.py +89 -7
- completions/bash.sh +19 -6
- completions/fish.fish +21 -7
- completions/powershell.ps1 +77 -8
- completions/zsh.sh +31 -5
- config/__init__.py +11 -0
- config/defaults.py +4 -21
- config/schema.py +3 -29
- {oasr-0.5.2.dist-info → oasr-0.6.0.dist-info}/METADATA +31 -21
- {oasr-0.5.2.dist-info → oasr-0.6.0.dist-info}/RECORD +32 -24
- policy/defaults.py +3 -19
- policy/profile.py +5 -7
- profiles/__init__.py +23 -0
- profiles/builtins.py +63 -0
- profiles/loader.py +74 -0
- profiles/paths.py +22 -0
- profiles/registry.py +19 -0
- profiles/summary.py +23 -0
- profiles/validation.py +34 -0
- {oasr-0.5.2.dist-info → oasr-0.6.0.dist-info}/WHEEL +0 -0
- {oasr-0.5.2.dist-info → oasr-0.6.0.dist-info}/entry_points.txt +0 -0
- {oasr-0.5.2.dist-info → oasr-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {oasr-0.5.2.dist-info → oasr-0.6.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oasr
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: CLI for managing agent skills across IDE integrations
|
|
5
5
|
Project-URL: Homepage, https://github.com/jgodau/asr
|
|
6
6
|
Project-URL: Repository, https://github.com/jgodau/asr
|
|
@@ -210,6 +210,7 @@ Classifier: Topic :: Software Development :: Build Tools
|
|
|
210
210
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
211
211
|
Requires-Python: >=3.10
|
|
212
212
|
Requires-Dist: pyyaml>=6.0
|
|
213
|
+
Requires-Dist: questionary>=2.0.1
|
|
213
214
|
Requires-Dist: tomli-w>=1.0.0
|
|
214
215
|
Requires-Dist: tomli>=2.0.0; python_version < '3.11'
|
|
215
216
|
Provides-Extra: dev
|
|
@@ -219,7 +220,7 @@ Description-Content-Type: text/markdown
|
|
|
219
220
|
|
|
220
221
|
# OASR
|
|
221
222
|
|
|
222
|
-
**Open Agent Skill Registry** —
|
|
223
|
+
**Open Agent Skill Registry** — Register, sync, and reuse AI agent skills across IDEs with a single source of truth.
|
|
223
224
|
|
|
224
225
|
---
|
|
225
226
|
|
|
@@ -229,16 +230,22 @@ You've built useful skills for your AI coding assistant. They work great in Curs
|
|
|
229
230
|
|
|
230
231
|
Each tool expects skills in different locations with different formats:
|
|
231
232
|
|
|
232
|
-
- Cursor: `.cursor/
|
|
233
|
-
- Windsurf: `.windsurf/
|
|
233
|
+
- Cursor: `.cursor/commands/`
|
|
234
|
+
- Windsurf: `.windsurf/workflows/`
|
|
234
235
|
- Claude: `.claude/commands/`
|
|
235
|
-
- Copilot: `.github
|
|
236
|
+
- Copilot: `.github/prompts/`
|
|
236
237
|
|
|
237
238
|
So you copy your skills everywhere. Then you improve one. Now the copies are stale. You forget which version is current. Some break silently. This is **skill drift**.
|
|
238
239
|
|
|
239
240
|
## The Solution
|
|
240
241
|
|
|
241
|
-
ASR keeps your skills in
|
|
242
|
+
ASR keeps your skills in a registry, syncs local and remote sources, and generates thin adapters for each IDE.
|
|
243
|
+
It also lets you execute skills safely with policy profiles.
|
|
244
|
+
|
|
245
|
+
Key capabilities:
|
|
246
|
+
- Register skills once (local folders or GitHub/GitLab URLs)
|
|
247
|
+
- Sync and track drift across sources
|
|
248
|
+
- Generate IDE adapters and run skills via `oasr exec`
|
|
242
249
|
|
|
243
250
|
```text
|
|
244
251
|
┌─────────────────────────────────────────────────────────┐
|
|
@@ -267,12 +274,15 @@ No copying. No drift. One source of truth.
|
|
|
267
274
|
|
|
268
275
|
```bash
|
|
269
276
|
# Register local skills
|
|
270
|
-
oasr add ~/skills/git-commit
|
|
271
|
-
oasr add ~/skills/code-review
|
|
277
|
+
oasr registry add ~/skills/git-commit
|
|
278
|
+
oasr registry add ~/skills/code-review
|
|
279
|
+
|
|
280
|
+
# List registered skills
|
|
281
|
+
oasr registry list
|
|
272
282
|
|
|
273
283
|
# Register remote skills from GitHub/GitLab
|
|
274
|
-
oasr add https://github.com/user/skills-repo/tree/main/my-skill
|
|
275
|
-
oasr add https://gitlab.com/org/project/tree/main/cool-skill
|
|
284
|
+
oasr registry add https://github.com/user/skills-repo/tree/main/my-skill
|
|
285
|
+
oasr registry add https://gitlab.com/org/project/tree/main/cool-skill
|
|
276
286
|
|
|
277
287
|
# Generate adapters for a project
|
|
278
288
|
oasr adapter --output-dir ~/projects/my-app
|
|
@@ -294,16 +304,16 @@ ASR supports registering skills directly from GitHub and GitLab repositories:
|
|
|
294
304
|
|
|
295
305
|
```bash
|
|
296
306
|
# Add a skill from GitHub
|
|
297
|
-
oasr add https://github.com/user/repo/tree/main/skills/my-skill
|
|
307
|
+
oasr registry add https://github.com/user/repo/tree/main/skills/my-skill
|
|
298
308
|
|
|
299
309
|
# Add a skill from GitLab
|
|
300
|
-
oasr add https://gitlab.com/org/project/tree/dev/cool-skill
|
|
310
|
+
oasr registry add https://gitlab.com/org/project/tree/dev/cool-skill
|
|
301
311
|
|
|
302
312
|
# Sync remote skills (check for updates)
|
|
303
|
-
oasr sync
|
|
313
|
+
oasr registry sync
|
|
304
314
|
|
|
305
315
|
# Use remote skills
|
|
306
|
-
oasr
|
|
316
|
+
oasr clone my-skill -d ./output
|
|
307
317
|
```
|
|
308
318
|
|
|
309
319
|
**Authentication** (optional, for private repos and higher rate limits):
|
|
@@ -313,7 +323,7 @@ export GITHUB_TOKEN=ghp_your_token_here
|
|
|
313
323
|
export GITLAB_TOKEN=glpat_your_token_here
|
|
314
324
|
```
|
|
315
325
|
|
|
316
|
-
Remote skills are fetched on-demand during `adapter` and `
|
|
326
|
+
Remote skills are fetched on-demand during `adapter` and `clone` operations. The registry stores the URL, and `oasr registry sync` checks if the remote source has changed.
|
|
317
327
|
|
|
318
328
|
---
|
|
319
329
|
|
|
@@ -345,7 +355,7 @@ See [`oasr completion --help`](docs/commands/COMPLETION.md) for details.
|
|
|
345
355
|
|
|
346
356
|
---
|
|
347
357
|
|
|
348
|
-
## Supported `
|
|
358
|
+
## Supported `oasr adapter` IDEs
|
|
349
359
|
|
|
350
360
|
| IDE | Adapter | Output |
|
|
351
361
|
|----------------|------------|-------------------------------|
|
|
@@ -367,11 +377,11 @@ See [LICENSE](LICENSE).
|
|
|
367
377
|
|
|
368
378
|
| Command | Screenshot |
|
|
369
379
|
|---------|-----------|
|
|
370
|
-
| **oasr list** |  |
|
|
371
|
-
| **oasr add** (local) |  |
|
|
372
|
-
| **oasr add** (remote) |  |
|
|
373
|
-
| **oasr sync** |  |
|
|
374
|
-
| **oasr
|
|
380
|
+
| **oasr registry list** |  |
|
|
381
|
+
| **oasr registry add** (local) |  |
|
|
382
|
+
| **oasr registry add** (remote) |  |
|
|
383
|
+
| **oasr registry sync** |  |
|
|
384
|
+
| **oasr registry -v** |  |
|
|
375
385
|
| **oasr find** |  |
|
|
376
386
|
| **oasr adapter** |  |
|
|
377
387
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
__init__.py,sha256=cYuwXNht5J2GDPEbHz57rmXRyWzaUgAaCXz8okR0rKE,84
|
|
2
2
|
__main__.py,sha256=Due_Us-4KNlLZhf8MkmoP1hWS5qMWmpZvz2ZaCqPHT4,120
|
|
3
3
|
adapter.py,sha256=WEpYkKDTb7We0zU9i6Z-r5ydtUdghNhxTZ5Eq58h4fU,10027
|
|
4
|
-
cli.py,sha256=
|
|
4
|
+
cli.py,sha256=gJLlOplekDkoFKceG3D5tXZ8m0Jm8tnOu0x9n1y2MW0,2883
|
|
5
5
|
discovery.py,sha256=WWF8SN2LH88mOUBJLavM7rvXcxi6uDQGpqRK20GysxA,3298
|
|
6
6
|
manifest.py,sha256=feNCjkFWfhoVubevKjLtKoIEuzT1YGQn6wWgs9XM8_o,12229
|
|
7
7
|
registry.py,sha256=zGutwVP39xaYqc3KDEXMWCV1tORYpqc5JISO8OaWP1Q,4470
|
|
@@ -16,11 +16,11 @@ adapters/copilot.py,sha256=090aS0N7SApSK5kcmm2xErjyUmEsOMrj0n36B0M3fAM,7274
|
|
|
16
16
|
adapters/cursor.py,sha256=BoS3Xo_hZyV4zrZKpNjDAaeGmJPTn_j_GI5QXuW5AOo,2244
|
|
17
17
|
adapters/windsurf.py,sha256=VZidyuFxiHQWl64w0-OgUsfdWeGXV7bf7_XYkiGchyw,2369
|
|
18
18
|
agents/__init__.py,sha256=Dwdwzb01DM2ZJ5pZBsbLPUYRM4Emai7p3oeayiYcj1w,546
|
|
19
|
-
agents/base.py,sha256=
|
|
20
|
-
agents/claude.py,sha256=
|
|
21
|
-
agents/codex.py,sha256=
|
|
22
|
-
agents/copilot.py,sha256=
|
|
23
|
-
agents/opencode.py,sha256=
|
|
19
|
+
agents/base.py,sha256=NBBhpJ4KMBLpi8UjGWaKNx9RNJkSxpobDq2LzIa0EYw,3213
|
|
20
|
+
agents/claude.py,sha256=J3LIha5npufT0dD5ZK2eWvM7BZjb615QYHGu-C-pFXI,857
|
|
21
|
+
agents/codex.py,sha256=tYZQEJCLPTxm8zlpwxADyZEbz9EAWmZPrJM3CPRX6lI,853
|
|
22
|
+
agents/copilot.py,sha256=5gieLBlXcpHu6Ci5JZo2QwJoSKeD-Xprb7nCtU3ODXU,878
|
|
23
|
+
agents/opencode.py,sha256=wTUF2_1jaitQkxjs7oRgvpRmdtyOlLBdvLdJRAMdjAw,881
|
|
24
24
|
agents/registry.py,sha256=i6YUAdNUXq1LNqLDNqSg2RlHxqF5Ig5Z40ryteXoLu4,1434
|
|
25
25
|
commands/__init__.py,sha256=g_ZxSKLVZwCVAPpn-Ga_gj53BS2473SOg72ivGph--U,147
|
|
26
26
|
commands/adapter.py,sha256=_68v3t-dRU0mszzL4udKs1bKennyg7RfBTaK2fDGTsE,3215
|
|
@@ -28,39 +28,47 @@ commands/add.py,sha256=NJLQ-8-3zy7o6S9VLfL_wauP-Vz0oNGwN3nvtiwxNYM,15255
|
|
|
28
28
|
commands/clean.py,sha256=RQBAfe6iCLsjMqUyVR55JdYX9MBqgrUuIrA8rFKs1J0,1102
|
|
29
29
|
commands/clone.py,sha256=4APH34-yHjiXQIQwBnKOSEQ_sxV24_GKypcOJMfncvs,5912
|
|
30
30
|
commands/completion.py,sha256=Y2KshaJ64vI1fcTR5z2KTJT7u9PPK_w8qMf5HK_q9ns,8570
|
|
31
|
-
commands/config.py,sha256=
|
|
31
|
+
commands/config.py,sha256=zUh65j4N1R5ikRst18eKCvGFQWnhm7fphu314J9lLpo,15211
|
|
32
32
|
commands/diff.py,sha256=37JMjvfAEfvK7-4X5iFbD-IGkS8ae4YSY7ZDIZF5B9E,5766
|
|
33
|
-
commands/exec.py,sha256=
|
|
33
|
+
commands/exec.py,sha256=rO18v7HOcJ9FQ71OHcR4Pplj_YTDD9Z6GxfoWGpK0f0,9518
|
|
34
34
|
commands/find.py,sha256=zgqwUnaG5aLX6gJIU2ZeQzxsFh2s7oDNNtmV-e-62Jg,1663
|
|
35
35
|
commands/help.py,sha256=5yhIpgGs1xPs2f39lg-ELE7D0tV_uUTjxQsgkWusIwo,1449
|
|
36
36
|
commands/info.py,sha256=zywaUQsrvcPXcX8W49P7Jqnr90pX8nBPqnH1XcIs0Uk,4396
|
|
37
37
|
commands/list.py,sha256=P3E_PItNoqAStNTcLCY7dV-Db2_Yb7TRCYXgP4G19rQ,3185
|
|
38
|
+
commands/profile.py,sha256=bjfdXJ6HeM8NRSsEn4t4kbHxSK0o6M3fHc7Cr9RH90U,2732
|
|
38
39
|
commands/registry.py,sha256=nerDoR0Uvd_2WlDMQZshJunMW31HsENklAy19A00x4U,16065
|
|
39
40
|
commands/rm.py,sha256=bOPXgifAbgx7kp4ZuNanJbv4wP1l1sIsI8vD6Rlgi8g,4160
|
|
40
41
|
commands/status.py,sha256=8si3iEVu0AUE2qojSCVoU0BLwpLm4UiFyY-Ln7Uo36k,3944
|
|
41
42
|
commands/sync.py,sha256=ZQoB5hBqrzvM6LUQVlKqHQVJib4dB5qe5M-pVG2vtGM,4946
|
|
42
|
-
commands/update.py,sha256=
|
|
43
|
+
commands/update.py,sha256=gWBBETMxi1ZQ3BV049eAkA_AuCEvvjA9FRzIOBpwdb4,15062
|
|
43
44
|
commands/use.py,sha256=ggB28g2BDg3Lv3nF40wnDAJ7p0mo6C1pc1KgahvQYXM,1452
|
|
44
45
|
commands/validate.py,sha256=Y8TLHxW4Z98onmzu-h-kDIET-48lVaIdQXOvuyBemLw,2361
|
|
45
46
|
completions/__init__.py,sha256=cLGMHifEf91ElOIMSVffVWcifjGZ7oStb0Ot4ivLmmE,41
|
|
46
|
-
completions/bash.sh,sha256=
|
|
47
|
-
completions/fish.fish,sha256=
|
|
48
|
-
completions/powershell.ps1,sha256=
|
|
49
|
-
completions/zsh.sh,sha256=
|
|
50
|
-
config/__init__.py,sha256=
|
|
51
|
-
config/defaults.py,sha256=
|
|
47
|
+
completions/bash.sh,sha256=2gk60fm17lPfBddpOvO0c8o0anGmqzbVrtic9pabjnQ,6647
|
|
48
|
+
completions/fish.fish,sha256=N2xXseX2djUNRGDs9TaUNU6YltmNT0PSEK0rYJJSuXc,10160
|
|
49
|
+
completions/powershell.ps1,sha256=m-EsgUpyvg1-wO6Afo2QnmHkzdmBfetDUMgaHn4vlO4,7614
|
|
50
|
+
completions/zsh.sh,sha256=XZRLOEi2_grWACeGiDeucsnyFlKwx_SuU7RMBOPe7Ng,7624
|
|
51
|
+
config/__init__.py,sha256=KcrfpKlWkDqVYooupSqMcfYstUDMpVgdSeOJv38nGS0,4063
|
|
52
|
+
config/defaults.py,sha256=1M5rxaOV-51vKAX_CaUZ8-AFL0vnklqYQeo7BpbOsJ8,525
|
|
52
53
|
config/env.py,sha256=WgnQXjhfvV7m1oxZCK9WdIX_rqLy_-BOSuPjbpjdI1c,7163
|
|
53
|
-
config/schema.py,sha256=
|
|
54
|
+
config/schema.py,sha256=kQ1EeepRHjjf4dy9y_qRw7DKEEjeTtyvzd2MZLSIAYY,2816
|
|
54
55
|
policy/__init__.py,sha256=0sPJaruOyc9ioNyIcrTW72RgpaE64FgibS0h5mQELb8,1353
|
|
55
|
-
policy/defaults.py,sha256=
|
|
56
|
+
policy/defaults.py,sha256=7DHRzeW79pEPx7x0_8dTmaFz9cpCosmto_yzb-cpMDs,233
|
|
56
57
|
policy/enforcement.py,sha256=djsosjjfdyr0SjnHF2kz4u3glvMNgd1CJztN6yZE-fM,2749
|
|
57
|
-
policy/profile.py,sha256=
|
|
58
|
+
policy/profile.py,sha256=j9C4Ut5Hwa6DotbbFR1ggmDPVmEC4dNMW7D4oxz2kxM,6839
|
|
59
|
+
profiles/__init__.py,sha256=IqlTPlQdKibdLFMbYIk_tpn_B7dmMSnPhN_OJwPzgAo,760
|
|
60
|
+
profiles/builtins.py,sha256=cu3lvUIUZw-OxEYJgGQeDJvQMPuwTnJqxYTOvnf5oiM,1430
|
|
61
|
+
profiles/loader.py,sha256=n6FJm1r_PhAtuCVlIom2I3yqxnTQUryU8b7ggYyUJjI,2487
|
|
62
|
+
profiles/paths.py,sha256=l0TbuVOOLrJBXOR-BAnxxA__2wZ9rHDTNHANTK1xafo,536
|
|
63
|
+
profiles/registry.py,sha256=hkLUXaN-57iAxQtTwnDkHzLrzl3HTJPF44JcBfg_rjg,659
|
|
64
|
+
profiles/summary.py,sha256=jLbmROC4TqyUJgswsn73rJqPkYq0YmAoKKoWxx5XBRg,853
|
|
65
|
+
profiles/validation.py,sha256=J9pZBs9l-PpNmYoRdn43UhA07FSDM8EdGpZMpsTikvY,1920
|
|
58
66
|
skillcopy/__init__.py,sha256=YUglUkDzKfnCt4ar_DU33ksI9fGyn2UYbV7qn2c_BcU,2322
|
|
59
67
|
skillcopy/local.py,sha256=QH6484dCenjg8pfNOyTRbQQBklEWhkkTnfQok5ssf_4,1049
|
|
60
68
|
skillcopy/remote.py,sha256=83jRA2VfjtSDGO-YM1x3WGJjKvWzK1RmSTL7SdUOz8s,3155
|
|
61
|
-
oasr-0.
|
|
62
|
-
oasr-0.
|
|
63
|
-
oasr-0.
|
|
64
|
-
oasr-0.
|
|
65
|
-
oasr-0.
|
|
66
|
-
oasr-0.
|
|
69
|
+
oasr-0.6.0.dist-info/METADATA,sha256=ueWMiIfDSfj_PC73af_g5NRMgGcVaxiEYU_YM9k-3Fo,18919
|
|
70
|
+
oasr-0.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
71
|
+
oasr-0.6.0.dist-info/entry_points.txt,sha256=VnMuOi6XYMbzAD2bP0X5uV1sQXjOqoDWJ33Lsxwq8u8,52
|
|
72
|
+
oasr-0.6.0.dist-info/licenses/LICENSE,sha256=nQ1j9Ldb8FlJ-z7y2WuXPIlyfnYC7YPasjGdOBgcfP4,10561
|
|
73
|
+
oasr-0.6.0.dist-info/licenses/NOTICE,sha256=EsfkCN0ZRDS0oj3ADvMKeKrAXaPlC8YfpSjvjGVv9jE,207
|
|
74
|
+
oasr-0.6.0.dist-info/RECORD,,
|
policy/defaults.py
CHANGED
|
@@ -6,22 +6,6 @@ Conservative defaults that fail closed. Used when:
|
|
|
6
6
|
- Config parsing errors occur
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"fs_write_roots": ["./out", "./.oasr"],
|
|
13
|
-
"deny_paths": [
|
|
14
|
-
"~/.ssh",
|
|
15
|
-
"~/.aws",
|
|
16
|
-
"~/.gnupg",
|
|
17
|
-
"~/.config",
|
|
18
|
-
".env",
|
|
19
|
-
"~/.bashrc",
|
|
20
|
-
"~/.zshrc",
|
|
21
|
-
"~/.profile",
|
|
22
|
-
],
|
|
23
|
-
"allowed_commands": ["rg", "fd", "jq", "cat"],
|
|
24
|
-
"deny_shell": True,
|
|
25
|
-
"network": False,
|
|
26
|
-
"allow_env": False,
|
|
27
|
-
}
|
|
9
|
+
from profiles.builtins import SAFE
|
|
10
|
+
|
|
11
|
+
__all__ = ["SAFE"]
|
policy/profile.py
CHANGED
|
@@ -11,7 +11,7 @@ from dataclasses import dataclass, field
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
-
from
|
|
14
|
+
from profiles import BUILTIN_PROFILES, merge_profile_data
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
@dataclass
|
|
@@ -107,17 +107,15 @@ def load(config: dict[str, Any], profile_name: str, cwd: Path | None = None) ->
|
|
|
107
107
|
def _load_impl(config: dict[str, Any], profile_name: str) -> Profile:
|
|
108
108
|
"""Internal implementation of load()."""
|
|
109
109
|
# Start with safe defaults
|
|
110
|
-
profile_data =
|
|
110
|
+
profile_data = BUILTIN_PROFILES["safe"].copy()
|
|
111
111
|
|
|
112
112
|
# Try to load user-defined profile
|
|
113
113
|
try:
|
|
114
114
|
profiles = config.get("profiles", {})
|
|
115
115
|
if profile_name in profiles:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if key in user_profile:
|
|
120
|
-
profile_data[key] = user_profile[key]
|
|
116
|
+
profile_data = merge_profile_data(profile_data, profiles[profile_name])
|
|
117
|
+
elif profile_name in BUILTIN_PROFILES:
|
|
118
|
+
profile_data = merge_profile_data(profile_data, BUILTIN_PROFILES[profile_name])
|
|
121
119
|
elif profile_name != "safe":
|
|
122
120
|
# Warn if non-safe profile doesn't exist, but continue with safe defaults
|
|
123
121
|
print(
|
profiles/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Profile loading utilities for OASR."""
|
|
2
|
+
|
|
3
|
+
from profiles.builtins import BUILTIN_PROFILE_ORDER, BUILTIN_PROFILES
|
|
4
|
+
from profiles.loader import load_profiles, merge_profile_data
|
|
5
|
+
from profiles.paths import ensure_profile_dir, get_profile_dir
|
|
6
|
+
from profiles.registry import get_profiles, list_profiles
|
|
7
|
+
from profiles.summary import format_profile_summary, sorted_profile_names
|
|
8
|
+
from profiles.validation import validate_profile_data, validate_profiles
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BUILTIN_PROFILES",
|
|
12
|
+
"BUILTIN_PROFILE_ORDER",
|
|
13
|
+
"load_profiles",
|
|
14
|
+
"get_profile_dir",
|
|
15
|
+
"ensure_profile_dir",
|
|
16
|
+
"format_profile_summary",
|
|
17
|
+
"get_profiles",
|
|
18
|
+
"list_profiles",
|
|
19
|
+
"merge_profile_data",
|
|
20
|
+
"sorted_profile_names",
|
|
21
|
+
"validate_profile_data",
|
|
22
|
+
"validate_profiles",
|
|
23
|
+
]
|
profiles/builtins.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Built-in execution policy profiles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
SAFE = {
|
|
8
|
+
"fs_read_roots": ["./"],
|
|
9
|
+
"fs_write_roots": ["./out", "./.oasr"],
|
|
10
|
+
"deny_paths": [
|
|
11
|
+
"~/.ssh",
|
|
12
|
+
"~/.aws",
|
|
13
|
+
"~/.gnupg",
|
|
14
|
+
"~/.config",
|
|
15
|
+
".env",
|
|
16
|
+
"~/.bashrc",
|
|
17
|
+
"~/.zshrc",
|
|
18
|
+
"~/.profile",
|
|
19
|
+
],
|
|
20
|
+
"allowed_commands": ["rg", "fd", "jq", "cat"],
|
|
21
|
+
"deny_shell": True,
|
|
22
|
+
"network": False,
|
|
23
|
+
"allow_env": False,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
STRICT = {
|
|
27
|
+
"fs_read_roots": ["./"],
|
|
28
|
+
"fs_write_roots": ["./.oasr"],
|
|
29
|
+
"deny_paths": SAFE["deny_paths"],
|
|
30
|
+
"allowed_commands": ["rg", "cat"],
|
|
31
|
+
"deny_shell": True,
|
|
32
|
+
"network": False,
|
|
33
|
+
"allow_env": False,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
DEV = {
|
|
37
|
+
"fs_read_roots": ["./", "~/projects"],
|
|
38
|
+
"fs_write_roots": ["./", "./out", "./.oasr", "~/projects"],
|
|
39
|
+
"deny_paths": SAFE["deny_paths"],
|
|
40
|
+
"allowed_commands": ["bash", "python", "node", "git", "curl", "rg", "fd", "jq", "cat", "npm", "pip"],
|
|
41
|
+
"deny_shell": False,
|
|
42
|
+
"network": True,
|
|
43
|
+
"allow_env": True,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
UNSAFE = {
|
|
47
|
+
"fs_read_roots": ["/"],
|
|
48
|
+
"fs_write_roots": ["/"],
|
|
49
|
+
"deny_paths": [],
|
|
50
|
+
"allowed_commands": ["*"],
|
|
51
|
+
"deny_shell": False,
|
|
52
|
+
"network": True,
|
|
53
|
+
"allow_env": True,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
BUILTIN_PROFILES: dict[str, dict[str, Any]] = {
|
|
57
|
+
"safe": SAFE,
|
|
58
|
+
"strict": STRICT,
|
|
59
|
+
"dev": DEV,
|
|
60
|
+
"unsafe": UNSAFE,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
BUILTIN_PROFILE_ORDER = ("safe", "strict", "dev", "unsafe")
|
profiles/loader.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Profile loading and merging utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
if sys.version_info >= (3, 11):
|
|
10
|
+
import tomllib
|
|
11
|
+
else:
|
|
12
|
+
import tomli as tomllib
|
|
13
|
+
|
|
14
|
+
from profiles.builtins import BUILTIN_PROFILES
|
|
15
|
+
from profiles.paths import get_profile_dir
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def list_profile_files(profile_dir: Path | None = None) -> list[Path]:
|
|
19
|
+
"""Return profile files from ~/.oasr/profile."""
|
|
20
|
+
directory = profile_dir or get_profile_dir()
|
|
21
|
+
if not directory.exists():
|
|
22
|
+
return []
|
|
23
|
+
return sorted(p for p in directory.glob("*.toml") if p.is_file())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_profile_file(path: Path) -> dict[str, Any]:
|
|
27
|
+
"""Load a profile TOML file containing only profile keys."""
|
|
28
|
+
with open(path, "rb") as f:
|
|
29
|
+
data = tomllib.load(f)
|
|
30
|
+
if not isinstance(data, dict):
|
|
31
|
+
raise ValueError("Profile file must contain a table of profile settings")
|
|
32
|
+
return data
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_profile_files(profile_dir: Path | None = None) -> dict[str, dict[str, Any]]:
|
|
36
|
+
"""Load profile files into a name->profile map."""
|
|
37
|
+
profiles: dict[str, dict[str, Any]] = {}
|
|
38
|
+
for path in list_profile_files(profile_dir):
|
|
39
|
+
name = path.stem
|
|
40
|
+
try:
|
|
41
|
+
profiles[name] = load_profile_file(path)
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
print(f"⚠ Warning: Failed to load profile '{name}': {exc}", file=sys.stderr)
|
|
44
|
+
return profiles
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def merge_profiles(
|
|
48
|
+
builtin: dict[str, dict[str, Any]],
|
|
49
|
+
file_profiles: dict[str, dict[str, Any]],
|
|
50
|
+
inline_profiles: dict[str, dict[str, Any]],
|
|
51
|
+
) -> dict[str, dict[str, Any]]:
|
|
52
|
+
"""Merge profiles with precedence: inline > file > builtin."""
|
|
53
|
+
merged: dict[str, dict[str, Any]] = {}
|
|
54
|
+
for source in (builtin, file_profiles, inline_profiles):
|
|
55
|
+
for name, profile in source.items():
|
|
56
|
+
merged[name] = profile.copy()
|
|
57
|
+
return merged
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def merge_profile_data(base: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
|
|
61
|
+
"""Merge profile values with override precedence."""
|
|
62
|
+
merged = base.copy()
|
|
63
|
+
merged.update(overrides)
|
|
64
|
+
return merged
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_profiles(
|
|
68
|
+
inline_profiles: dict[str, dict[str, Any]] | None = None,
|
|
69
|
+
profile_dir: Path | None = None,
|
|
70
|
+
) -> dict[str, dict[str, Any]]:
|
|
71
|
+
"""Load merged profiles from builtin + profile files + inline config."""
|
|
72
|
+
inline_profiles = inline_profiles or {}
|
|
73
|
+
file_profiles = load_profile_files(profile_dir)
|
|
74
|
+
return merge_profiles(BUILTIN_PROFILES, file_profiles, inline_profiles)
|
profiles/paths.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Profile directory paths and helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_profile_dir() -> Path:
|
|
9
|
+
"""Return ~/.oasr/profile directory (dynamic)."""
|
|
10
|
+
try:
|
|
11
|
+
from config import OASR_DIR
|
|
12
|
+
|
|
13
|
+
return OASR_DIR / "profile"
|
|
14
|
+
except Exception:
|
|
15
|
+
return Path.home() / ".oasr" / "profile"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ensure_profile_dir() -> Path:
|
|
19
|
+
"""Ensure ~/.oasr/profile directory exists."""
|
|
20
|
+
directory = get_profile_dir()
|
|
21
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
return directory
|
profiles/registry.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Profile registry operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from profiles.loader import load_profiles
|
|
8
|
+
from profiles.summary import sorted_profile_names
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def list_profiles(inline_profiles: dict[str, dict[str, Any]] | None = None) -> list[str]:
|
|
12
|
+
"""Return ordered profile names with all sources merged."""
|
|
13
|
+
profiles = load_profiles(inline_profiles=inline_profiles or {})
|
|
14
|
+
return sorted_profile_names(profiles)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_profiles(inline_profiles: dict[str, dict[str, Any]] | None = None) -> dict[str, dict[str, Any]]:
|
|
18
|
+
"""Return merged profiles."""
|
|
19
|
+
return load_profiles(inline_profiles=inline_profiles or {})
|
profiles/summary.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Profile listing and summary helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from profiles.builtins import BUILTIN_PROFILE_ORDER
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def sorted_profile_names(profiles: dict[str, Any]) -> list[str]:
|
|
11
|
+
"""Return profile names sorted with built-ins first."""
|
|
12
|
+
names = sorted(profiles.keys())
|
|
13
|
+
ordered = [name for name in BUILTIN_PROFILE_ORDER if name in profiles]
|
|
14
|
+
remaining = [name for name in names if name not in ordered]
|
|
15
|
+
return ordered + remaining
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_profile_summary(name: str, profile: dict[str, Any]) -> str:
|
|
19
|
+
"""Format a single-line profile summary."""
|
|
20
|
+
network = "on" if profile.get("network") else "off"
|
|
21
|
+
env = "on" if profile.get("allow_env") else "off"
|
|
22
|
+
shell = "on" if not profile.get("deny_shell", True) else "off"
|
|
23
|
+
return f"{name:12} network={network} env={env} shell={shell}"
|
profiles/validation.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Profile validation helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def validate_profile_data(profile_name: str, profile_data: dict[str, Any]) -> None:
|
|
9
|
+
"""Validate a single profile data structure."""
|
|
10
|
+
if "fs_read_roots" in profile_data and not isinstance(profile_data["fs_read_roots"], list):
|
|
11
|
+
raise ValueError(f"Profile '{profile_name}': fs_read_roots must be a list")
|
|
12
|
+
if "fs_write_roots" in profile_data and not isinstance(profile_data["fs_write_roots"], list):
|
|
13
|
+
raise ValueError(f"Profile '{profile_name}': fs_write_roots must be a list")
|
|
14
|
+
if "deny_paths" in profile_data and not isinstance(profile_data["deny_paths"], list):
|
|
15
|
+
raise ValueError(f"Profile '{profile_name}': deny_paths must be a list")
|
|
16
|
+
if "allowed_commands" in profile_data and not isinstance(profile_data["allowed_commands"], list):
|
|
17
|
+
raise ValueError(f"Profile '{profile_name}': allowed_commands must be a list")
|
|
18
|
+
if "deny_shell" in profile_data and not isinstance(profile_data["deny_shell"], bool):
|
|
19
|
+
raise ValueError(f"Profile '{profile_name}': deny_shell must be a boolean")
|
|
20
|
+
if "network" in profile_data and not isinstance(profile_data["network"], bool):
|
|
21
|
+
raise ValueError(f"Profile '{profile_name}': network must be a boolean")
|
|
22
|
+
if "allow_env" in profile_data and not isinstance(profile_data["allow_env"], bool):
|
|
23
|
+
raise ValueError(f"Profile '{profile_name}': allow_env must be a boolean")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate_profiles(profiles: dict[str, Any]) -> None:
|
|
27
|
+
"""Validate profile map structure."""
|
|
28
|
+
if not isinstance(profiles, dict):
|
|
29
|
+
raise ValueError("profiles must be a table (dictionary)")
|
|
30
|
+
|
|
31
|
+
for profile_name, profile_data in profiles.items():
|
|
32
|
+
if not isinstance(profile_data, dict):
|
|
33
|
+
raise ValueError(f"Profile '{profile_name}' must be a table (dictionary)")
|
|
34
|
+
validate_profile_data(profile_name, profile_data)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|