diff --git a/backend/apps/skill_registry/skill_registry.py b/backend/apps/skill_registry/skill_registry.py index 2cdf4b17..49a07801 100644 --- a/backend/apps/skill_registry/skill_registry.py +++ b/backend/apps/skill_registry/skill_registry.py @@ -33,7 +33,6 @@ p_refresh_task: Optional[asyncio.Task] = None # The curated repo's recursive file tree, warmed hourly alongside the catalog. A curated install reads paths from here and fetches contents over raw, so it makes ZERO GitHub API calls in the normal case (the trees API is the 60/hr-limited part); update detection reads per-folder tree SHAs from it too. Empty until the first refresh warms it; install falls back to one live tree call then. p_curated_tree: list[dict] = [] -p_curated_tree_at: float = 0 # Community repo trees for update detection, cached briefly (best-effort) so an updates check on skills.sh-installed skills doesn't refetch every page load nor burn the API. P_COMMUNITY_TREE_TTL = 600 p_community_tree_cache: dict[str, tuple] = {} @@ -173,14 +172,13 @@ async def p_warm_curated_tree() -> None: over raw). One cheap call per hourly refresh, reused by every install in that hour. Isolated, a failure here never touches the SKILL.md catalog; install falls back to a live tree call while the cache is cold.""" - global p_curated_tree, p_curated_tree_at + global p_curated_tree owner, _, repo = REPO.partition("/") try: async with httpx.AsyncClient(timeout=30.0, headers=github_headers()) as client: tree = await p_tree_at(client, owner, repo, BRANCH) if tree: p_curated_tree = tree - p_curated_tree_at = time.time() logger.info(f"Curated skill tree warmed: {len(p_tree_blob_paths(tree))} file paths cached") except RegistryRateLimited: # Visible on purpose: a rate-limited warm-up means installs stay on the slow live-call path until the IP's quota resets or a token is set. @@ -432,7 +430,7 @@ async def p_build_resolved_skill( if "SKILL.md" not in files: raise ValueError("SKILL.md could not be fetched") - meta, p_body = p_parse_frontmatter(files["SKILL.md"]) + meta, _ = p_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.common.secret_scan import find_secrets_in_files secret_findings = find_secrets_in_files({rel: data.encode("utf-8", "ignore") for rel, data in files.items()}) @@ -624,7 +622,7 @@ async def p_safe_repo_tree(source: str): if owner and repo: try: async with httpx.AsyncClient(timeout=30.0, headers=github_headers()) as client: - p_branch, tree = await p_fetch_repo_tree(client, owner, repo) + _, tree = await p_fetch_repo_tree(client, owner, repo) except Exception: tree = None p_community_tree_cache[source] = (now, tree) @@ -671,7 +669,7 @@ async def registry_update(req: p_UpdateRequest): bumping its version. Re-runs the secret scan and returns any findings so the UI can flag a community update that newly ships secrets. A skill with no source (user-made) can't be updated.""" - from backend.apps.skills.skills import sync_skills, write_folder_skill, p_clear_skill_dir + from backend.apps.skills.skills import sync_skills, write_folder_skill, clear_skill_dir target = next((s for s in sync_skills() if s.id == req.skill_id), None) if target is None: raise HTTPException(status_code=404, detail="skill not found") @@ -690,7 +688,7 @@ async def registry_update(req: p_UpdateRequest): raise HTTPException(status_code=502, detail=f"could not fetch skill: {e}") # Overwrite in place: clear first so files removed upstream don't linger, keep the user's command alias, refresh everything else from source. - p_clear_skill_dir(target.id) + clear_skill_dir(target.id) skill = write_folder_skill( target.id, resolved["files"], diff --git a/backend/apps/skills/skills.py b/backend/apps/skills/skills.py index 166e6772..3d3c54c6 100644 --- a/backend/apps/skills/skills.py +++ b/backend/apps/skills/skills.py @@ -361,7 +361,7 @@ async def get_skill(skill_id: str): raise HTTPException(status_code=404, detail="Skill not found") -def p_clear_skill_dir(skill_id: str) -> None: +def clear_skill_dir(skill_id: str) -> None: """Empty a skill's folder before an in-place update so files removed upstream don't linger as orphans. write_folder_skill recreates the dir right after.""" import shutil diff --git a/backend/key-probe.js b/backend/key-probe.js deleted file mode 100644 index 5dae6b45..00000000 --- a/backend/key-probe.js +++ /dev/null @@ -1,72 +0,0 @@ -// key-probe.js: inspect keyboard events INSIDE a game's guest webview. -// -// Why: the open question on the keyboard fix is whether tool-issued -// sendInputEvent delivers correct key codes once the guest is focused, or -// whether it arrives with an empty `code` / keyCode:0 (which games ignore) and -// we must switch to CDP Input.dispatchKeyEvent. This probe answers it directly -// instead of inferring from whether the sprite moved. -// -// Usage: -// 1. Right-click the game webview -> Inspect -> Console (this is the GUEST -// console, http://127.0.0.1:/..., NOT the host app devtools). -// 2. Paste this whole file and hit Enter. You'll see "[key-probe] armed". -// 3. Fire one tool-issued press_key, e.g. { key: "ArrowRight", hold_ms: 800 }. -// 4. Read the logged lines, or call __keyProbe.dump() for a table. -// -// What to look for per event: -// - isTrusted: should be true (native OS-level event). false = a JS -// dispatchEvent, not our native path. -// - code: should be e.g. "ArrowRight". EMPTY string is the failure signal. -// - keyCode/which: should be e.g. 39. 0 is the failure signal. -// - holdMs (on keyup): wall-clock ms the key was held. ~0 means the hold -// collapsed to an instant tap (down+up same tick); ~800 means hold worked. -// Decision: clean code + nonzero keyCode -> stay on sendInputEvent. Empty -// code / keyCode 0 -> move the keyboard path to CDP Input.dispatchKeyEvent. - -(function () { - if (window.__keyProbe) { - try { window.__keyProbe.disarm(); } catch (_) {} - } - var events = []; - var downAt = Object.create(null); // key -> timestamp, to measure hold duration - - function row(e) { - var now = (performance && performance.now) ? performance.now() : Date.now(); - var holdMs = null; - if (e.type === 'keydown') { - downAt[e.code || e.key] = now; - } else if (e.type === 'keyup') { - var k = e.code || e.key; - if (downAt[k] != null) { holdMs = Math.round(now - downAt[k]); delete downAt[k]; } - } - var r = { - type: e.type, - key: e.key, - code: e.code, // EMPTY = failure signal - keyCode: e.keyCode, // 0 = failure signal - which: e.which, - isTrusted: e.isTrusted, // true = native; false = synthetic JS - repeat: e.repeat, - target: (e.target && (e.target.tagName || e.target.nodeName)) || '(none)', - holdMs: holdMs, // only set on keyup - }; - events.push(r); - var warn = (r.code === '' || r.code == null) ? ' <-- EMPTY code' - : (r.keyCode === 0) ? ' <-- keyCode 0' : ''; - console.log('[key-probe]', r.type, JSON.stringify(r) + warn); - } - - // Capture phase + on window so we see the event even if the game stops - // propagation on its own listener. - var types = ['keydown', 'keyup', 'keypress']; - types.forEach(function (t) { window.addEventListener(t, row, true); }); - - window.__keyProbe = { - events: events, - dump: function () { try { console.table(events); } catch (_) { console.log(events); } return events; }, - clear: function () { events.length = 0; for (var k in downAt) delete downAt[k]; console.log('[key-probe] cleared'); }, - disarm: function () { types.forEach(function (t) { window.removeEventListener(t, row, true); }); console.log('[key-probe] disarmed'); }, - }; - - console.log('[key-probe] armed on', location.href, '- fire a press_key, then __keyProbe.dump(). document.activeElement =', document.activeElement && (document.activeElement.tagName || document.activeElement.nodeName)); -})(); diff --git a/backend/watch-moves.sh b/backend/watch-moves.sh deleted file mode 100644 index ad3d7589..00000000 --- a/backend/watch-moves.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# watch-moves.sh: human-readable, moves-only view of the App Agent. -# Shows just the actions it takes (clicks, keypresses, waits), one per line, -# plus task-start and bridge-missing milestones. Hides all the dispatch/result/ -# electron echo noise. Usage: bash backend/watch-moves.sh [logfile] -LOG="${1:-/tmp/openswarm.log}" -tail -f "$LOG" | awk ' - { gsub(/\033\[[0-9;]*m/, "") } # strip color codes - match($0, /[0-9][0-9]:[0-9][0-9]:[0-9][0-9]/) { t = substr($0, RSTART, 8) } - /\[app-agent\] START loop/ { print ""; print "=== " t " TASK START ==="; next } - /BRIDGE MISSING/ { print t " !! bridge missing: app is NOT agent-operable, driving UI blind"; next } - /\[browser-action\]/ { - sub(/.*\[browser-action\] [A-Za-z]+: /, "") # drop everything up to the action - sub(/ *-> .*/, "") # drop the trailing browser_id - print t " > " $0 - next - } -' diff --git a/electron/main.js b/electron/main.js index 24bf70f0..aee96e17 100644 --- a/electron/main.js +++ b/electron/main.js @@ -2444,7 +2444,7 @@ app.on('web-contents-created', (_event, contents) => { `).catch(() => {}); // Agent bridge (window.OPENSWARM_APP). Injected into EVERY app's main world - // from the shell so it exists regardless of frontend/src — the lightweight + // from the shell so it exists regardless of frontend/src; the lightweight // App Builder mode deletes frontend/src (and with it the template's own // agentBridge.ts), so this is the only entry point a trimmed app can't lose. // Idempotent + guarded: a workspace app that imports its own bridge installs diff --git a/linter/config/config.json b/linter/config/config.json index c46261a7..88c953e5 100644 --- a/linter/config/config.json +++ b/linter/config/config.json @@ -93,6 +93,7 @@ "backend/tests/test_disconnect_resilience.py", "backend/tests/test_outputs_runtime_cleanup.py", "backend/tests/test_service.py", + "backend/tests/test_skill_registry_community.py", "backend/tests/test_streaming_harness.py", "backend/tests/test_swarm_bundle.py", "backend/tests/test_v2_invariants.py", @@ -197,6 +198,7 @@ "p-private": [ "backend/apps/workflows/workflows.py", "backend/tests/test_schedule_recurrence.py", + "backend/tests/test_skill_registry_community.py", "backend/tests/test_ssrf_guard.py", "backend/tests/test_workflows_semantics.py", "backend/tests/test_workflows_storage.py"