skill-sentry-cli 2.0.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.
- skill_sentry_cli/__init__.py +5 -0
- skill_sentry_cli/__main__.py +4 -0
- skill_sentry_cli/cli.py +918 -0
- skill_sentry_cli/core/__init__.py +1 -0
- skill_sentry_cli/core/anchors/__init__.py +5 -0
- skill_sentry_cli/core/anchors/credential_flow.py +440 -0
- skill_sentry_cli/core/anchors/declared_hosts.py +78 -0
- skill_sentry_cli/core/cache/__init__.py +35 -0
- skill_sentry_cli/core/cache/hash_utils.py +206 -0
- skill_sentry_cli/core/cache/sqlite_store.py +925 -0
- skill_sentry_cli/core/config.py +299 -0
- skill_sentry_cli/core/contracts.py +551 -0
- skill_sentry_cli/core/decisions/__init__.py +44 -0
- skill_sentry_cli/core/decisions/base.py +174 -0
- skill_sentry_cli/core/decisions/calibration.py +325 -0
- skill_sentry_cli/core/decisions/laya.py +439 -0
- skill_sentry_cli/core/decisions/mapping.py +218 -0
- skill_sentry_cli/core/decisions/mock.py +118 -0
- skill_sentry_cli/core/decisions/questions.py +118 -0
- skill_sentry_cli/core/decisions/report.py +65 -0
- skill_sentry_cli/core/decisions/selector.py +151 -0
- skill_sentry_cli/core/engine.py +1412 -0
- skill_sentry_cli/core/evasion/__init__.py +27 -0
- skill_sentry_cli/core/evasion/detector.py +210 -0
- skill_sentry_cli/core/exit_codes.py +13 -0
- skill_sentry_cli/core/models/__init__.py +20 -0
- skill_sentry_cli/core/models/_json.py +147 -0
- skill_sentry_cli/core/models/base.py +50 -0
- skill_sentry_cli/core/models/gemma_cloud.py +240 -0
- skill_sentry_cli/core/models/gemma_local.py +273 -0
- skill_sentry_cli/core/models/image_utils.py +112 -0
- skill_sentry_cli/core/models/mock.py +152 -0
- skill_sentry_cli/core/models/selector.py +123 -0
- skill_sentry_cli/core/parser/__init__.py +29 -0
- skill_sentry_cli/core/parser/skill_parser.py +520 -0
- skill_sentry_cli/core/parser/typosquat.py +167 -0
- skill_sentry_cli/core/poisoned_well/__init__.py +52 -0
- skill_sentry_cli/core/poisoned_well/attacker_profile.py +230 -0
- skill_sentry_cli/core/poisoned_well/canary_gen.py +413 -0
- skill_sentry_cli/core/poisoned_well/exfil_tracker.py +470 -0
- skill_sentry_cli/core/reports.py +114 -0
- skill_sentry_cli/core/sandbox/__init__.py +74 -0
- skill_sentry_cli/core/sandbox/assets/Dockerfile.sandbox +39 -0
- skill_sentry_cli/core/sandbox/assets/masquerade.c +484 -0
- skill_sentry_cli/core/sandbox/assets/run_skill.sh +41 -0
- skill_sentry_cli/core/sandbox/assets/sinkhole_sidecar.py +171 -0
- skill_sentry_cli/core/sandbox/canary_fs.py +108 -0
- skill_sentry_cli/core/sandbox/context_honeypot.py +97 -0
- skill_sentry_cli/core/sandbox/dns_sinkhole.py +218 -0
- skill_sentry_cli/core/sandbox/fake_http.py +194 -0
- skill_sentry_cli/core/sandbox/instructions.py +170 -0
- skill_sentry_cli/core/sandbox/masquerade.py +265 -0
- skill_sentry_cli/core/sandbox/monitor.py +278 -0
- skill_sentry_cli/core/sandbox/runner.py +776 -0
- skill_sentry_cli/core/sandbox/sidecar.py +327 -0
- skill_sentry_cli/core/semantic/__init__.py +9 -0
- skill_sentry_cli/core/semantic/analyzer.py +443 -0
- skill_sentry_cli/core/semantic/ast_anchor.py +371 -0
- skill_sentry_cli/core/semantic/ast_python.py +156 -0
- skill_sentry_cli/core/semantic/candidates.py +78 -0
- skill_sentry_cli/core/semantic/laya_analyzer.py +241 -0
- skill_sentry_cli/core/semantic/prompts.py +61 -0
- skill_sentry_cli/core/verdict/__init__.py +24 -0
- skill_sentry_cli/core/verdict/fusion.py +392 -0
- skill_sentry_cli/core/verdict/markdown_report.py +211 -0
- skill_sentry_cli/core/verdict/sarif_report.py +139 -0
- skill_sentry_cli/core/verdict/status.py +35 -0
- skill_sentry_cli/core/verdict/stix_report.py +132 -0
- skill_sentry_cli/core/version.py +38 -0
- skill_sentry_cli/core/vision/__init__.py +8 -0
- skill_sentry_cli/core/vision/image_extractor.py +91 -0
- skill_sentry_cli/core/vision/vision_scanner.py +274 -0
- skill_sentry_cli/data/__init__.py +1 -0
- skill_sentry_cli/data/known_skills_v1.json +276 -0
- skill_sentry_cli/data/sandbox_image.json +4 -0
- skill_sentry_cli/rules/credential_rules.toml +94 -0
- skill_sentry_cli/server/__init__.py +5 -0
- skill_sentry_cli/server/app.py +129 -0
- skill_sentry_cli/server/backend.py +63 -0
- skill_sentry_cli/server/demo_assets.py +62 -0
- skill_sentry_cli/server/demo_backend.py +475 -0
- skill_sentry_cli/server/demo_data.py +814 -0
- skill_sentry_cli/server/demo_mode.py +40 -0
- skill_sentry_cli/server/errors.py +48 -0
- skill_sentry_cli/server/live_backend.py +576 -0
- skill_sentry_cli/server/routes.py +191 -0
- skill_sentry_cli/skill/SKILL.md +67 -0
- skill_sentry_cli/skill/scripts/audit.sh +48 -0
- skill_sentry_cli/ui/dist/assets/index-Bf0h24IX.css +1 -0
- skill_sentry_cli/ui/dist/assets/index-CrYBKSSB.js +8 -0
- skill_sentry_cli/ui/dist/index.html +14 -0
- skill_sentry_cli-2.0.0.dist-info/METADATA +76 -0
- skill_sentry_cli-2.0.0.dist-info/RECORD +96 -0
- skill_sentry_cli-2.0.0.dist-info/WHEEL +4 -0
- skill_sentry_cli-2.0.0.dist-info/entry_points.txt +2 -0
- skill_sentry_cli-2.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Shared REST + WebSocket routes (used by demo and live backends)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Query, Request, Response, WebSocket, WebSocketDisconnect
|
|
10
|
+
|
|
11
|
+
from skill_sentry_cli.core.contracts import (
|
|
12
|
+
CacheStatsResponse,
|
|
13
|
+
DeleteCacheResponse,
|
|
14
|
+
ErrorResponse,
|
|
15
|
+
ExportFormat,
|
|
16
|
+
HealthResponse,
|
|
17
|
+
MetaResponse,
|
|
18
|
+
ScanListResponse,
|
|
19
|
+
ScanRequest,
|
|
20
|
+
ScanStartResponse,
|
|
21
|
+
ScanStatus,
|
|
22
|
+
ScanStatusResponse,
|
|
23
|
+
SkillDetail,
|
|
24
|
+
Stage,
|
|
25
|
+
VerdictsResponse,
|
|
26
|
+
WSEventType,
|
|
27
|
+
WSMessage,
|
|
28
|
+
utc_now,
|
|
29
|
+
)
|
|
30
|
+
from skill_sentry_cli.server.backend import ScanBackend
|
|
31
|
+
from skill_sentry_cli.server.errors import ApiError
|
|
32
|
+
|
|
33
|
+
router = APIRouter()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _backend(request: Request) -> ScanBackend:
|
|
37
|
+
return request.app.state.backend
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.post("/scan", response_model=ScanStartResponse, status_code=202)
|
|
41
|
+
async def start_scan(payload: ScanRequest, request: Request) -> ScanStartResponse:
|
|
42
|
+
return await _backend(request).start_scan(payload)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@router.get("/scans", response_model=ScanListResponse)
|
|
46
|
+
async def list_scans(
|
|
47
|
+
request: Request,
|
|
48
|
+
limit: int = Query(default=100, ge=1, le=1000),
|
|
49
|
+
offset: int = Query(default=0, ge=0),
|
|
50
|
+
status: ScanStatus | None = Query(default=None),
|
|
51
|
+
) -> ScanListResponse:
|
|
52
|
+
return await _backend(request).list_scans(limit=limit, offset=offset, status=status)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.get("/scan/{scan_id}", response_model=ScanStatusResponse)
|
|
56
|
+
async def get_scan(scan_id: UUID, request: Request) -> ScanStatusResponse:
|
|
57
|
+
return await _backend(request).get_scan(scan_id)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@router.get("/scan/{scan_id}/verdicts", response_model=VerdictsResponse)
|
|
61
|
+
async def get_verdicts(scan_id: UUID, request: Request) -> VerdictsResponse:
|
|
62
|
+
return await _backend(request).get_verdicts(scan_id)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@router.get("/skills/{skill_id}", response_model=SkillDetail)
|
|
66
|
+
async def get_skill(skill_id: UUID, request: Request) -> SkillDetail:
|
|
67
|
+
return await _backend(request).get_skill(skill_id)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@router.get("/skills/{skill_id}/export")
|
|
71
|
+
async def export_skill(
|
|
72
|
+
skill_id: UUID,
|
|
73
|
+
request: Request,
|
|
74
|
+
fmt: ExportFormat = Query(..., alias="format"),
|
|
75
|
+
) -> Response:
|
|
76
|
+
report = _backend(request).export(skill_id, fmt)
|
|
77
|
+
return Response(
|
|
78
|
+
content=report.content,
|
|
79
|
+
media_type=report.media_type,
|
|
80
|
+
headers={"Content-Disposition": f'attachment; filename="{report.filename}"'},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@router.get("/cache/stats", response_model=CacheStatsResponse)
|
|
85
|
+
async def cache_stats(request: Request) -> CacheStatsResponse:
|
|
86
|
+
return await _backend(request).cache_stats()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@router.delete("/cache", response_model=DeleteCacheResponse)
|
|
90
|
+
async def clear_cache(
|
|
91
|
+
request: Request,
|
|
92
|
+
older_than: str | None = Query(default=None),
|
|
93
|
+
) -> DeleteCacheResponse:
|
|
94
|
+
return await _backend(request).clear_cache(older_than)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@router.get("/health", response_model=HealthResponse)
|
|
98
|
+
async def health(request: Request) -> HealthResponse:
|
|
99
|
+
return await _backend(request).health()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@router.get("/meta", response_model=MetaResponse)
|
|
103
|
+
async def meta(request: Request) -> MetaResponse:
|
|
104
|
+
return await _backend(request).meta()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.websocket("/scan/{scan_id}/stream")
|
|
108
|
+
async def scan_stream(websocket: WebSocket, scan_id: UUID) -> None:
|
|
109
|
+
await websocket.accept()
|
|
110
|
+
backend: ScanBackend = websocket.app.state.backend
|
|
111
|
+
cancel = asyncio.Event()
|
|
112
|
+
|
|
113
|
+
async def client_reader() -> None:
|
|
114
|
+
try:
|
|
115
|
+
while True:
|
|
116
|
+
message = await websocket.receive_json()
|
|
117
|
+
if isinstance(message, dict) and message.get("op") == "cancel":
|
|
118
|
+
cancel.set()
|
|
119
|
+
return
|
|
120
|
+
except (WebSocketDisconnect, RuntimeError, ValueError):
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
reader = asyncio.create_task(client_reader())
|
|
124
|
+
events = backend.stream(scan_id)
|
|
125
|
+
queue: asyncio.Queue[WSMessage | None] = asyncio.Queue(maxsize=64)
|
|
126
|
+
|
|
127
|
+
def error_message(exc: ApiError) -> WSMessage:
|
|
128
|
+
error: ErrorResponse = exc.to_response()
|
|
129
|
+
return WSMessage(
|
|
130
|
+
stage=exc.stage or Stage.VERDICT,
|
|
131
|
+
skill="",
|
|
132
|
+
event=WSEventType.ERROR,
|
|
133
|
+
ts=utc_now(),
|
|
134
|
+
detail=error.model_dump(by_alias=True, mode="json")["error"],
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
async def pump() -> None:
|
|
138
|
+
"""Move backend events into a queue without cancelling the generator.
|
|
139
|
+
|
|
140
|
+
``asyncio.wait_for(agen.__anext__(), ...)`` closes async generators
|
|
141
|
+
when it times out, which truncates slow stages. The queue lets the
|
|
142
|
+
route poll for client cancellation while the producer runs freely.
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
async for message in events:
|
|
146
|
+
await queue.put(message)
|
|
147
|
+
except ApiError as exc:
|
|
148
|
+
await queue.put(error_message(exc))
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
await queue.put(
|
|
151
|
+
error_message(
|
|
152
|
+
ApiError(
|
|
153
|
+
500,
|
|
154
|
+
"internal_error",
|
|
155
|
+
f"stream failed: {exc}",
|
|
156
|
+
degraded=True,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
finally:
|
|
161
|
+
await queue.put(None)
|
|
162
|
+
|
|
163
|
+
producer = asyncio.create_task(pump())
|
|
164
|
+
try:
|
|
165
|
+
while not cancel.is_set():
|
|
166
|
+
try:
|
|
167
|
+
message = await asyncio.wait_for(queue.get(), timeout=0.25)
|
|
168
|
+
except TimeoutError:
|
|
169
|
+
continue
|
|
170
|
+
if message is None:
|
|
171
|
+
break
|
|
172
|
+
await websocket.send_json(message.model_dump(by_alias=True, mode="json"))
|
|
173
|
+
if cancel.is_set():
|
|
174
|
+
with suppress(ApiError):
|
|
175
|
+
await backend.cancel(scan_id)
|
|
176
|
+
except WebSocketDisconnect:
|
|
177
|
+
pass
|
|
178
|
+
finally:
|
|
179
|
+
reader.cancel()
|
|
180
|
+
with suppress(asyncio.CancelledError):
|
|
181
|
+
await reader
|
|
182
|
+
producer.cancel()
|
|
183
|
+
with suppress(asyncio.CancelledError):
|
|
184
|
+
await producer
|
|
185
|
+
with suppress(Exception):
|
|
186
|
+
await events.aclose()
|
|
187
|
+
with suppress(RuntimeError):
|
|
188
|
+
await websocket.close()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
__all__ = ["router"]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skillsentry-audit
|
|
3
|
+
description: >-
|
|
4
|
+
Audit AI Agent Skills for malware before installation with SkillSentry, a
|
|
5
|
+
fully-local open-source scanner powered by Gemma 4. Use when the user asks to
|
|
6
|
+
audit a skill, scan a skill, check if a skill is safe, detect malicious
|
|
7
|
+
skills, review a SKILL.md package, or verify a skill from a registry such as
|
|
8
|
+
ClawHub before installing it. Runs an 8-stage inspection: SHA-256 incremental
|
|
9
|
+
cache, spec validation with typosquat detection, AST anchors fused with Gemma
|
|
10
|
+
semantic analysis, vision scanning for image-hidden prompt injection,
|
|
11
|
+
sandboxed execution with context-aware honeypots, Poisoned Well active
|
|
12
|
+
defense (fake credentials, exfil tracked to C2), sandbox-evasion detection,
|
|
13
|
+
weighted verdict fusion. Returns a SAFE, SUSPICIOUS, or MALICIOUS verdict
|
|
14
|
+
with a 0-100 risk score, line-accurate citations, and SARIF/STIX reports.
|
|
15
|
+
Trigger keywords: audit skill, scan skill, skill security, malicious skill,
|
|
16
|
+
pre-install check, SKILL.md review, supply-chain audit, typosquat check.
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# SkillSentry Audit
|
|
20
|
+
|
|
21
|
+
Pre-install security gate for Agent Skills. Everything runs **locally** (Ollama + Docker + SQLite) — audited skill content never leaves the machine unless the user explicitly enabled the Gemini API fallback.
|
|
22
|
+
|
|
23
|
+
## When to use
|
|
24
|
+
|
|
25
|
+
- Before installing any skill from a registry (ClawHub, marketplaces) or a third-party repo.
|
|
26
|
+
- When re-auditing installed skills after upstream updates (supply-chain poisoning check).
|
|
27
|
+
- When the user asks: *"is this skill safe?"*, *"scan this skill"*, *"audit SKILL.md"*, *"check for typosquats"*.
|
|
28
|
+
|
|
29
|
+
## How to run
|
|
30
|
+
|
|
31
|
+
Use the bundled script (preferred):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
scripts/audit.sh <path-to-skill-directory> [extra skill-sentry-cli flags]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Equivalent direct CLI:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
skill-sentry-cli scan <path-to-skill-directory> --fail-on malicious
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Useful flags: `--demo-mode` (offline canned demo), `--no-cache` (force full re-scan), `--sarif-out report.sarif`, `--format json`, `--backend local|cloud`.
|
|
44
|
+
|
|
45
|
+
## Interpreting results
|
|
46
|
+
|
|
47
|
+
| Verdict | Risk score | Recommended action |
|
|
48
|
+
|:---|:---|:---|
|
|
49
|
+
| ✅ `SAFE` | < 30 | Install normally |
|
|
50
|
+
| ⚠️ `SUSPICIOUS` | 30–70 | Review the cited `file:line` findings with the user before deciding |
|
|
51
|
+
| 🚫 `MALICIOUS` | > 70, or hard-evidence override (confidence 1.0) | **Do not install.** Report the SARIF/STIX exports; note any C2 indicators |
|
|
52
|
+
|
|
53
|
+
Always surface to the user: the verdict, risk score, top findings with `file:line` citations, whether the result was ⚡ cached (< 2 s), and — if triggered — the Poisoned Well summary (targeted assets, exfil channel, C2 indicators). Never suppress a `MALICIOUS` verdict, and never present a degraded scan (e.g., `sandbox-unavailable`) as a clean pass.
|
|
54
|
+
|
|
55
|
+
## Requirements
|
|
56
|
+
|
|
57
|
+
- `skill-sentry-cli` CLI installed (`pip install skill-sentry-cli`)
|
|
58
|
+
- Ollama running with Gemma 4 E4B (Q4) pulled — or `--backend cloud` with `GEMINI_API_KEY`, or `--demo-mode`
|
|
59
|
+
- Docker daemon available for sandbox stages (Stage 4/4.5/5); without it the scan degrades to static-only analysis and says so explicitly
|
|
60
|
+
|
|
61
|
+
## Safety notes
|
|
62
|
+
|
|
63
|
+
- SkillSentry never connects to attacker infrastructure; C2 indicators are harvested passively from the sinkhole.
|
|
64
|
+
- All canary credentials served during analysis are fake by construction (marker `SKS-<8hex>`).
|
|
65
|
+
- Audited packages are executed **only** inside the hardened sandbox (60 s, 512 MB, 1 CPU, sinkhole-only egress).
|
|
66
|
+
|
|
67
|
+
*Powered by Gemma 4 · open weights.*
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# audit.sh — bundled entrypoint for the SkillSentry Agent Skill (skill_sentry_cli/skill/)
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# scripts/audit.sh <path-to-skill-directory> [extra skill-sentry-cli flags]
|
|
6
|
+
#
|
|
7
|
+
# Examples:
|
|
8
|
+
# scripts/audit.sh ./some-downloaded-skill
|
|
9
|
+
# scripts/audit.sh ./some-downloaded-skill --sarif-out report.sarif
|
|
10
|
+
# scripts/audit.sh ./some-downloaded-skill --demo-mode
|
|
11
|
+
#
|
|
12
|
+
# Exit codes (CI contract, docs/07-cli-reference.md §7.5):
|
|
13
|
+
# 0 all skills within --fail-on threshold (default: no MALICIOUS)
|
|
14
|
+
# 1 verdict above threshold found -> DO NOT INSTALL
|
|
15
|
+
# 2 usage error
|
|
16
|
+
# 3 operational error (Docker down, DB locked) — not a verdict
|
|
17
|
+
# 4 model unavailable and no fallback enabled
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
SKILL_DIR="${1:-}"
|
|
21
|
+
[ $# -gt 0 ] && shift || true
|
|
22
|
+
|
|
23
|
+
if [ -z "${SKILL_DIR}" ]; then
|
|
24
|
+
echo "usage: audit.sh <skill-directory> [extra skill-sentry-cli flags]" >&2
|
|
25
|
+
exit 2
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if [ ! -d "${SKILL_DIR}" ]; then
|
|
29
|
+
echo "error: '${SKILL_DIR}' is not a directory" >&2
|
|
30
|
+
exit 2
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if [ ! -f "${SKILL_DIR}/SKILL.md" ] && [ -z "$(find "${SKILL_DIR}" -maxdepth 2 -name SKILL.md -print -quit 2>/dev/null)" ]; then
|
|
34
|
+
echo "warning: no SKILL.md found under '${SKILL_DIR}' — scanning anyway (spec validation will flag it)" >&2
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if ! command -v skill-sentry-cli >/dev/null 2>&1; then
|
|
38
|
+
echo "error: skill-sentry-cli CLI not found — install with: pip install skill-sentry-cli" >&2
|
|
39
|
+
exit 3
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Warn (don't fail) when sandbox dependencies are missing: the engine degrades
|
|
43
|
+
# to static-only analysis and marks the verdict accordingly (fail-safe rule).
|
|
44
|
+
command -v docker >/dev/null 2>&1 || echo "warning: Docker not found — sandbox stages will be skipped (verdict flagged 'sandbox-unavailable')" >&2
|
|
45
|
+
curl -sf --max-time 2 "${OLLAMA_HOST:-http://localhost:11434}/api/tags" >/dev/null 2>&1 \
|
|
46
|
+
|| echo "warning: Ollama not reachable — set --backend cloud or --demo-mode, otherwise semantic/vision stages degrade" >&2
|
|
47
|
+
|
|
48
|
+
exec skill-sentry-cli scan "${SKILL_DIR}" --fail-on malicious "$@"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:#f5f6f8;--surface:#fff;--fg:#161b26;--muted:#5b6475;--rule:#d9dee6;--accent:#1f5fa8;--ok:#1a7f4b;--ok-soft:#e2f3ea;--warn:#8a5a00;--warn-soft:#fbf0d9;--bad:#b42318;--bad-soft:#fbe6e4;--code-bg:#eef1f5;--radius:8px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:15px;line-height:1.5}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}:root:not([data-theme=light]){--bg:#0e131b;--surface:#141b26;--fg:#e6eaf1;--muted:#9aa4b5;--rule:#26303f;--accent:#7eb3f0;--ok:#5fd39a;--ok-soft:#13291e;--warn:#f0c26b;--warn-soft:#2d2412;--bad:#ff8b82;--bad-soft:#3a1a18;--code-bg:#1b2330}}*{box-sizing:border-box}body{background:var(--bg);color:var(--fg);padding-left:max(16px, env(safe-area-inset-left,0px));padding-right:max(16px, env(safe-area-inset-right,0px));margin:0}a{color:var(--accent)}h1{margin:0;font-size:1.6rem}h2{margin:0;font-size:1.05rem}h3{margin:1.2rem 0 .4rem;font-size:.95rem}.app{max-width:1120px;margin:0 auto}.topbar{border-bottom:1px solid var(--rule);flex-wrap:wrap;align-items:center;gap:12px 20px;padding:14px 0;display:flex}.brand{color:var(--fg);font-weight:700;text-decoration:none}nav{gap:14px;display:flex}nav a{color:var(--muted);text-decoration:none}nav a[aria-current=page]{color:var(--fg);font-weight:600}.status{flex-wrap:wrap;align-items:center;gap:8px;margin-left:auto;display:flex}main{padding:20px 0 48px}.page-head{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px;display:flex}.head-meta{align-items:center;gap:12px;display:flex}.pill{background:var(--code-bg);color:var(--muted);white-space:nowrap;border-radius:999px;padding:2px 9px;font-size:.75rem;font-weight:600;display:inline-block}.pill.ok{background:var(--ok-soft);color:var(--ok)}.pill.warn{background:var(--warn-soft);color:var(--warn)}.pill.bad{background:var(--bad-soft);color:var(--bad)}.pill.cached{margin-left:6px}.dot{color:var(--muted);font-size:.8rem}.dot:before{content:"";background:var(--bad);border-radius:50%;width:8px;height:8px;margin-right:5px;display:inline-block}.dot.ok:before{background:var(--ok)}.toolbar{flex-wrap:wrap;align-items:center;gap:10px 14px;margin-bottom:12px;display:flex}.chips{gap:6px;display:flex}.chip{border:1px solid var(--rule);background:var(--surface);color:var(--fg);cursor:pointer;font:inherit;border-radius:999px;padding:4px 12px;font-size:.85rem}.chip.active{border-color:var(--accent);color:var(--accent);font-weight:600}input[type=search],input[type=text],input[type=number],select{font:inherit;border:1px solid var(--rule);background:var(--surface);color:var(--fg);border-radius:6px;padding:6px 10px}input[type=text]{width:100%}button,.button{font:inherit;border:1px solid var(--accent);background:var(--accent);color:#fff;cursor:pointer;border-radius:6px;padding:7px 14px;font-weight:600;text-decoration:none;display:inline-block}button:disabled{opacity:.6;cursor:default}button.ghost{color:var(--accent);background:0 0}button.danger{background:var(--bad);border-color:var(--bad)}.table-wrap{border:1px solid var(--rule);border-radius:var(--radius);background:var(--surface);overflow-x:auto}table{border-collapse:collapse;width:100%;font-size:.92rem}th,td{text-align:left;border-bottom:1px solid var(--rule);vertical-align:top;padding:10px 12px}th{letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-size:.72rem}tr:last-child td{border-bottom:0}.finding-cell{max-width:360px}.risk{background:var(--code-bg);border-radius:999px;width:120px;height:18px;position:relative;overflow:hidden}.risk-fill{background:var(--accent);height:100%}.risk-value{place-items:center;font-size:.75rem;font-weight:700;display:grid;position:absolute;inset:0}.card{background:var(--surface);border:1px solid var(--rule);border-radius:var(--radius);min-width:0;margin-bottom:16px;padding:16px 18px}.card-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;display:flex}.facts{grid-template-columns:max-content 1fr;gap:6px 16px;margin:0;display:grid}.facts dt{color:var(--muted)}.facts dd{overflow-wrap:anywhere;margin:0}.plain{gap:8px;margin:0;padding:0;list-style:none;display:grid}.findings li{overflow-wrap:anywhere}.code{background:var(--code-bg);white-space:pre;border-radius:6px;margin:6px 0;padding:8px 10px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:.85rem;overflow-x:auto}.mono{overflow-wrap:anywhere;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:.86em}.code-findings{gap:12px;margin:0;padding:0;list-style:none;display:grid}.code-findings li{border-left:3px solid var(--rule);padding-left:12px}.code-findings li.highlight{border-left-color:var(--bad)}.code-head{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.tabs{border-bottom:1px solid var(--rule);flex-wrap:wrap;gap:4px;margin-bottom:14px;display:flex}.tab{color:var(--muted);background:0 0;border:0;border-bottom:2px solid #0000;border-radius:0;padding:8px 12px}.tab.active{color:var(--fg);border-bottom-color:var(--accent)}.console{gap:4px;margin:0;padding-left:20px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:.85rem;display:grid}.scan-form{gap:16px;max-width:680px;display:grid}.field{gap:4px;display:grid}.field.inline{align-items:center;gap:10px;display:flex}.options{border:1px solid var(--rule);border-radius:var(--radius);gap:8px;padding:12px 16px;display:grid}.options label{align-items:center;gap:8px;display:flex}.actions{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.error{background:var(--bad-soft);color:var(--bad);border-radius:6px;margin:10px 0;padding:10px 14px}.empty,.muted{color:var(--muted)}.nowrap{white-space:nowrap}.back{color:var(--muted);margin-bottom:8px;text-decoration:none;display:inline-block}.timeline{gap:4px;margin:0;padding-left:20px;display:grid}.vision{border-top:1px solid var(--rule);margin-top:10px;padding-top:10px}@media (width<=640px){.status{margin-left:0}.facts{grid-template-columns:1fr}.toolbar input[type=search]{flex:100%}}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
|