mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 12:47:42 +02:00
[eric] skills/settings-agent: scan community SKILL.md+files for secrets & reframe install risk (agent-instructions, not just scripts); extract apply_settings_update so the tool stops calling the PUT route as a fn
This commit is contained in:
@@ -148,6 +148,16 @@ SERVER_OWNED_FIELDS = (
|
||||
|
||||
@settings.router.put("")
|
||||
async def update_settings(body: AppSettings):
|
||||
saved = await apply_settings_update(body)
|
||||
return {"ok": True, "settings": saved.model_dump()}
|
||||
|
||||
|
||||
async def apply_settings_update(body: AppSettings) -> AppSettings:
|
||||
"""Persist a full settings object with all the safety side effects: restore
|
||||
server-owned fields, hand the wheel back from the free trial when a real
|
||||
model is connected, reconcile 9router provider connections, and sync
|
||||
analytics/identity. The PUT route and the agent settings tool both call this
|
||||
so the write semantics can't drift between them. Returns the saved body."""
|
||||
from backend.apps.service.client import sync as _sync
|
||||
|
||||
old = load_settings()
|
||||
@@ -271,7 +281,7 @@ async def update_settings(body: AppSettings):
|
||||
any_keyed_added,
|
||||
))
|
||||
|
||||
return {"ok": True, "settings": body.model_dump()}
|
||||
return body
|
||||
|
||||
|
||||
class AppThemeOverridePayload(BaseModel):
|
||||
|
||||
@@ -365,6 +365,11 @@ async def resolve_community_skill(source: str, skill_id: str) -> dict:
|
||||
raise ValueError("SKILL.md could not be fetched")
|
||||
|
||||
meta, _body = _parse_frontmatter(files["SKILL.md"])
|
||||
# Reuse the .swarm importer's content scan: flag files holding secret-shaped
|
||||
# literals (the author's leaked key, or a sketchy skill) so the user sees it
|
||||
# before installing from an unvetted repo.
|
||||
from backend.apps.swarm.redact import find_secrets_in_files
|
||||
secret_findings = find_secrets_in_files({rel: data.encode("utf-8", "ignore") for rel, data in files.items()})
|
||||
return {
|
||||
"name": meta.get("name") or skill_id,
|
||||
"description": meta.get("description", ""),
|
||||
@@ -372,6 +377,7 @@ async def resolve_community_skill(source: str, skill_id: str) -> dict:
|
||||
"skill_id": skill_id,
|
||||
"files": files,
|
||||
"scripts": sorted(rel for rel in files if _is_script_path(rel)),
|
||||
"secret_findings": secret_findings,
|
||||
}
|
||||
|
||||
|
||||
@@ -436,6 +442,7 @@ async def registry_install(req: _InstallRequest):
|
||||
"files": sorted(resolved["files"].keys()),
|
||||
"scripts": resolved["scripts"],
|
||||
"has_scripts": bool(resolved["scripts"]),
|
||||
"secret_findings": resolved.get("secret_findings", []),
|
||||
}
|
||||
if not req.confirm:
|
||||
return {"installed": False, "disclosure": disclosure}
|
||||
|
||||
+3
-3
@@ -766,7 +766,7 @@ async def settings_meta(action: str, request: Request):
|
||||
from backend.apps.settings.store import load_settings
|
||||
from backend.apps.settings.models import AppSettings
|
||||
from backend.apps.settings.redaction import redact_settings
|
||||
from backend.apps.settings.settings import SERVER_OWNED_FIELDS, update_settings
|
||||
from backend.apps.settings.settings import SERVER_OWNED_FIELDS, apply_settings_update
|
||||
from backend.apps.agents.session_credential import (
|
||||
PoweringCredential, resolve_powering_credential, write_would_suicide,
|
||||
)
|
||||
@@ -786,7 +786,7 @@ async def settings_meta(action: str, request: Request):
|
||||
|
||||
valid_fields = set(AppSettings.model_fields.keys())
|
||||
outcomes: dict[str, dict] = {}
|
||||
# Serialize the read-modify-write: SettingsWrite goes through update_settings,
|
||||
# Serialize the read-modify-write: SettingsWrite goes through apply_settings_update,
|
||||
# which awaits (so two autonomous agents would interleave and clobber each
|
||||
# other's fields while BOTH got an "applied" result). The lock makes agent
|
||||
# writes serial so the last load always sees the prior write. (Agent vs the
|
||||
@@ -828,7 +828,7 @@ async def settings_meta(action: str, request: Request):
|
||||
new_body = AppSettings(**merged)
|
||||
if staged and new_body is not None:
|
||||
try:
|
||||
await update_settings(new_body)
|
||||
await apply_settings_update(new_body)
|
||||
for f in staged:
|
||||
outcomes[f] = {"status": "applied"}
|
||||
except Exception as e:
|
||||
|
||||
@@ -47,6 +47,19 @@ def test_missing_skill_raises():
|
||||
_select_skill_paths([{"type": "blob", "path": "a/SKILL.md"}], "nonexistent")
|
||||
|
||||
|
||||
def test_install_disclosure_flags_secret_shaped_files():
|
||||
# The scan we wire into the install disclosure (reused from the .swarm importer)
|
||||
# must flag a community skill shipping credentials, and leave clean files alone.
|
||||
from backend.apps.swarm.redact import find_secrets_in_files
|
||||
files = {
|
||||
"SKILL.md": b"Renders PDFs. No secrets.",
|
||||
"config.py": b'API_KEY = "sk-ant-api03-AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH"',
|
||||
}
|
||||
hits = find_secrets_in_files(files)
|
||||
assert "config.py" in hits
|
||||
assert "SKILL.md" not in hits
|
||||
|
||||
|
||||
def test_script_classification():
|
||||
assert _is_script_path("run.sh")
|
||||
assert _is_script_path("helper.py")
|
||||
|
||||
@@ -177,6 +177,18 @@ const CommunitySkillsDialog: React.FC<Props> = ({ open, onClose, onInstalled })
|
||||
{selected.source} <OpenInNewIcon sx={{ fontSize: 13 }} />
|
||||
</Box>
|
||||
|
||||
{/* The real risk for an agent platform: SKILL.md is injected as instructions the
|
||||
agent follows, with its full tool surface. Say that plainly, not just "scripts". */}
|
||||
<Alert severity="info" icon={<WarningAmberIcon fontSize="small" />} sx={{ fontSize: '0.78rem', py: 0 }}>
|
||||
This is an unvetted community skill. Its SKILL.md becomes instructions your agent will follow, and it can use your agent's tools (files, browser, settings). Only install from a source you trust, read it below first.
|
||||
</Alert>
|
||||
|
||||
{disclosure.secret_findings.length > 0 && (
|
||||
<Alert severity="error" icon={<WarningAmberIcon fontSize="small" />} sx={{ fontSize: '0.78rem', py: 0 }}>
|
||||
{disclosure.secret_findings.length} file{disclosure.secret_findings.length === 1 ? '' : 's'} contain secret-shaped text ({disclosure.secret_findings.slice(0, 3).join(', ')}{disclosure.secret_findings.length > 3 ? '…' : ''}). A trustworthy skill shouldn't ship credentials; treat this as a red flag.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{disclosure.has_scripts && (
|
||||
<Alert severity="warning" icon={<WarningAmberIcon fontSize="small" />} sx={{ fontSize: '0.78rem', py: 0 }}>
|
||||
Includes {disclosure.scripts.length} script file{disclosure.scripts.length === 1 ? '' : 's'} that can run code when an agent uses this skill. Installing only writes the files; nothing runs until an agent does, and that still goes through normal command approval.
|
||||
|
||||
@@ -149,6 +149,7 @@ export interface InstallDisclosure {
|
||||
files: string[];
|
||||
scripts: string[];
|
||||
has_scripts: boolean;
|
||||
secret_findings: string[];
|
||||
}
|
||||
|
||||
export async function searchCommunitySkills(q: string): Promise<CommunitySkill[]> {
|
||||
|
||||
Reference in New Issue
Block a user