Agent Skillssilo-code/silo › verifier-gui

verifier-gui

GitHub

用于驱动 Silo 桌面应用进行运行时验证,通过 RPC 桥接执行命令、评估 DOM、截图及管理工作区。严格在沙盒环境中操作以隔离真实数据,支持启动或附加到已运行实例,捕获 GUI 证据供 verify skill 判定。

.agents/skills/verifier-gui/SKILL.md silo-code/silo

触发场景

需要验证图形界面功能时 获取应用运行时状态证据时 自动化测试 GUI 交互流程时

安装

npx skills add silo-code/silo --skill verifier-gui -g -y
更多选项

非标准路径

npx skills add https://github.com/silo-code/silo/tree/main/.agents/skills/verifier-gui -g -y

不安装直接使用

npx skills use silo-code/silo@verifier-gui

指定 Agent (Claude Code)

npx skills add silo-code/silo --skill verifier-gui -a claude-code -g -y

安装 repo 全部 skill

npx skills add silo-code/silo --all -g -y

预览 repo 内 skill

npx skills add silo-code/silo --list

SKILL.md

Frontmatter
{
    "name": "verifier-gui",
    "tools": "Bash, Read",
    "description": "Launch (or attach to) the Silo dev app and drive it for runtime verification through the dev automation RPC bridge — exec commands, eval DOM, capture screenshots, and create\/activate\/delete workspaces, terminals, editors. This is the repo's GUI evidence-capture handle for the `verify` skill. Use when verifying a change by running the real app and observing it. Always works inside a throwaway sandbox workspace, never the user's real workspaces."
}

Silo GUI Verifier

The handle the verify skill looks for: how to get the running Silo app under control and capture evidence from it. Silo is a Tauri desktop app; its surface is pixels + a dev-only RPC bridge. This skill drives that bridge.

It does not judge. It launches, drives, captures. The verdict is verify's.

Golden rule 1: verify in a sandbox workspace, never the user's

The app may be the user's live session with real workspaces and terminals. Do all verification in a workspace you create from a temp dir, and delete it when done. Never openTerminal/deleteWorkspace/openFile against an existing workspace — you'd pollute or destroy real state. Create → activate → verify → delete. This also makes destructive paths (workspace delete, session kill) safe to exercise.

Golden rule 2: one turn, not one op per turn

The wall-clock cost here is agent turns, not the RPC bridge — each bridge call is ~milliseconds on localhost, but every separate Bash tool call is a full model round-trip (seconds). So issue a whole drive + capture sequence as a single Bash call. The silo() helper is just curl; bash variables (WS_ID, WS_DIR) persist within one invocation, so create → activate → drive → screenshot → decode all belong in one block (see §2). A 6-step flow then costs 2 turns, not 7.

Only split into a separate turn when you genuinely must:

  • The final Read /tmp/silo.pngRead is its own tool, so capture-in-one-turn then read-in-the-next is the floor (2 turns).
  • Branching on an observed result — if the next op depends on what you saw (a count, a tab list, a pass/fail), end the block, read the output, then decide. A fixed setup sequence has no such dependency — never split it.

Echo any state you'll need next turn (e.g. echo "WS_ID=$WS_ID") — bash vars die at the end of the Bash call.

1. Get the app up (attach or launch)

The bridge listens on 127.0.0.1:7878 (dev builds only — app:dev is built --features automation). Define the request helper first — the contract is strict: header X-Silo-Automation: 1 and a loopback Host, POST /, body {"op", "args"}.

silo(){ curl -s -m30 -X POST http://127.0.0.1:7878/ \
  -H 'X-Silo-Automation: 1' -H 'Content-Type: application/json' \
  --data "$1"; }

Attach if it's already running, else launch:

if [ "$(silo '{"op":"ping"}')" = '{"ok":true,"result":"pong"}' ]; then
  echo "attached to running dev app"
else
  pnpm dev >/tmp/silo-appdev.log 2>&1 &           # first run compiles Rust — slow
  for i in $(seq 1 120); do                       # poll up to ~4 min
    sleep 2
    [ "$(silo '{"op":"ping"}' 2>/dev/null)" = '{"ok":true,"result":"pong"}' ] && break
  done
fi

app:dev runs under the isolated "Silo Dev" identity (separate app data), so launching never touches the user's real Silo install — but if you attached to an already-running instance, the sandbox rule above still applies.

If you attached (ping succeeded on the first try), verify it's actually serving your code before doing anything else. A pong only proves some dev app is listening on 7878 — it can just as easily be a stale instance from a different checkout, a different branch, or a session someone else started hours ago, with none of the changes you're here to verify. Confirm the process identity, not just liveness:

ps aux | grep "target/debug/silo\b" | grep -v grep
# expect the binary path to start with YOUR working directory, e.g.:
#   /path/to/your/repo/apps/desktop/src-tauri/target/debug/silo
# a path pointing anywhere else means you're about to verify the wrong code —
# stop and either ask the user to close it or launch your own (a port conflict
# will make that obvious rather than silently reusing the wrong instance)

This check is cheap (one ps) against the cost of it going wrong: driving and debugging against the wrong checkout produces confident-looking PASS/FAIL results for code you didn't touch, and any oddity you hit sends you chasing a phantom bug in your own change instead of noticing the mismatch.

2. Drive & capture — one block, one turn

Per golden rule 2, do the whole sandbox setup, drive steps, and screenshot in a single Bash call. Bash variables persist within the invocation, so the workspace id flows from one op to the next with no agent round-trip. End the block with the screenshot + decode; the only follow-up turn is Read /tmp/silo.png.

# ── ONE Bash call = ONE turn ──────────────────────────────────────────────
WS_DIR=$(mktemp -d /tmp/silo-verify.XXXXXX)
WS_ID=$(silo "{\"op\":\"openWorkspace\",\"args\":{\"folder\":\"$WS_DIR\",\"name\":\"verify-sandbox\"}}" \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["id"])')
silo "{\"op\":\"activateWorkspace\",\"args\":{\"id\":\"$WS_ID\"}}"

# ── drive steps (add as many as the check needs) ──
silo '{"op":"exec","args":{"command":"core.newTerminal"}}'
# silo "{\"op\":\"openFile\",\"args\":{\"path\":\"$WS_DIR/README.md\"}}"
# silo '{"op":"eval","args":{"expr":"document.querySelectorAll(\".xterm\").length"}}'

# ── capture as the LAST step in the same block ──
silo '{"op":"screenshot"}' > /tmp/shot.json
python3 -c "import json,base64;d=json.load(open('/tmp/shot.json'));r=d['result'];open('/tmp/silo.png','wb').write(base64.b64decode(r['png_base64']));print('shot',r['width'],r['height'])"

echo "WS_ID=$WS_ID"   # surface state needed for next-turn cleanup

Next turn: Read /tmp/silo.png. The capture can be slow (a few seconds), especially the first call — keep the timeout ≥30s and retry once if it returns empty. No OS permission setup is required.

A single-folder workspace also means core.newTerminal won't pop the folder picker (which automation can't click) — it resolves the lone folder directly.

Reading evidence without a screenshot — for structure/counts, an inline eval in the same block is cheaper than a picture: document.querySelectorAll('.xterm').length (terminal count), [...document.querySelectorAll('.dv-tab')].map(t=>t.textContent) (open tabs), document.body.innerText.includes('Session ended') (spawn failure). WebGL caveat: terminals render to a canvas (WebGL addon) — .xterm has no DOM text, so to read what a shell printed you need a screenshot, not textContent.

Op catalog

exec runs a registered command (the real ctx path); eval runs JS in the webview global scope (note: app modules like store are not in scope — use the dedicated ops for state). The full, authoritative list is the switch (op) in apps/desktop/src/automation/bridge.tsread it when in doubt; this table mirrors it. eval is the escape hatch, but for Monaco state prefer the dedicated editor ops below — monaco is not a page global, so eval can't reach it.

Core / liveness

Op Args Returns / use
ping pong — liveness
exec command run a command id (menu/keybinding dispatch) → {ran}
eval expr evaluate JS in the page; awaits a returned promise
screenshot host-side window capture → {png_base64,width,height}

Workspaces / panels (sandbox only — never point at real workspaces)

Op Args Returns / use
listWorkspaces {active, workspaces[{id,name,folder}]}
openWorkspace folder,name create + activate (use a temp dir) → {id}
activateWorkspace id switch active → {active}
addFolder id,folder attach a folder, bypassing the native OS picker ("Add Folder…" in Workspace Properties) → {extraFolders}
closeWorkspace id close (sets closedAt, doesn't remove) → {closed,active}
deleteWorkspace id reap terminals + remove → {deleted,active}
splitActivePanel position? (l/r/top/bottom) split center group → {groups}
activatePanel panelId focus a dock panel → {activated}
showSidePanel id expand slot + activate its tab → {shown,slot}

Editors / terminals

Op Args Returns / use
openFile path open an editor tab → {editorId,panelId}
openDiff path,providerId,args?,title?,preview? open a diff tab via a content provider → {diffId,panelId}
listEditors workspaceId? {previewEditorId, editors[{id,filePath,title,isPreview,mode,providerId}]}
openTerminal cwd? ctx.terminals.create{terminalId,panelId}
sendText terminalId,text,addNewline? write to a terminal's PTY (force-spawns if never mounted) → {sent:true}
listTerminals workspaceId? {terminals[{id,title,sessionId,kind}]}

Monaco introspection / drive (authoritative — straight from Monaco's registry; uri matches by substring of the model URI)

Op Args Returns / use
monacoEditors live editors [{uri,hasTextFocus,valueLength,valueTail}]
editorsDetail per-editor focus + container-visibility ground truth (focus-handoff debugging)
focusLog clear? Monaco focus-event timeline ({clear:true} resets it)
editorContent uri read a model's current text → {uri,value} | null
editorOptions uri resolved Monaco config (font/tab/wrap/minimap/readOnly/…) for that editor
setEditorValue uri,value model.setValue → fires onChange, i.e. the real edit→dirty→save/backup path, no OS focus needed → {uri,valueLength} | null

Output logs (read what the app or extensions have logged)

Op Args Returns / use
outputLogs channel?,level?,search?,limit? (all opt.) {channel,displayName,totalCount,entries[{timestamp,level,message,data?}],channels[{key,displayName}]}
  • channel defaults to the first registered channel. Discover all channels via the channels field in any response.
  • level: "debug" / "info" / "warn" / "error" / "all" (default "all").
  • search: case-insensitive substring on message.
  • limit: most-recent N entries (default 200; ring buffer holds 5 000 per channel).
# All recent logs (first channel, up to 200)
silo '{"op":"outputLogs"}'
# Errors only from notifications channel
silo '{"op":"outputLogs","args":{"channel":"silo:notifications","level":"error","limit":50}}'

Theme / process / introspection

Op Args Returns / use
themeState {activeId, presets[], customThemes[]}
setTheme id switch active theme → {activeId}
processExec command,args?,cwd? one-shot ctx.process.exec{stdout,stderr,code}
contextKeys host context-keys snapshot (activeEditorId/activeViewerId/…)
activeElement describe what holds DOM focus

To dirty an editor without keyboard focus (e.g. verifying save / dirty indicator / hot-exit backups): openFile, then setEditorValue with the file's basename as uri and new value — this drives the real onChange. Read it back with editorContent, or screenshot for the dirty dot.

The typed client src/automation/client.ts (SiloAutomation) wraps these if you prefer TS over curl.

3. Clean up (always)

silo "{\"op\":\"deleteWorkspace\",\"args\":{\"id\":\"$WS_ID\"}}"   # reaps its terminals/panels
rm -rf "$WS_DIR"

If you launched the app yourself, you may leave it running (next verify attaches) or kill the backgrounded pnpm dev — but never kill an instance you attached to (it's the user's).

Gotchas (learned the hard way)

  • Header quoting: -H 'X-Silo-Automation: 1' — an unquoted/space-mangled header gets a 403 {"error":"forbidden"}.
  • exec vs openTerminal: exec("core.newTerminal") drives the real ctx.terminals.create path; the openTerminal op is a lower-level test setup that calls record APIs directly — prefer exec when verifying the ctx path.
  • Focus-sensitive checks (asserting a <textarea> is document.activeElement) only pass while the window is frontmost; an agent session can't hold focus, so gate them on SiloAutomation.foreground() and SKIP otherwise — don't FAIL.
  • Code freshness: confirm the running app is the code under test, not just that a dev app answers ping — see the identity check in §1. An attached instance can be a different checkout entirely (wrong repo clone, wrong branch), not just stale HMR.
  • eval has a hard 5-second reply timeout independent of curl's -m. The bridge's Rust side (REPLY_TIMEOUT in automation.rs) gives up waiting on the webview after 5s and returns {"ok":false,"error":"timed out waiting for webview reply"} — but the JS keeps running in the page regardless, since nothing on the page side knows the host stopped listening. A driver script that does await sleep(15000) internally will report a timeout error to you while still fully executing moments later — actions you think failed actually land, which is deeply confusing to debug from the outside. Never put multi-second waits inside an eval payload. Instead: fire one fast action (a .click() returns essentially instantly), sleep in bash between calls, then a second fast eval to read the result:
    silo '{"op":"eval","args":{"expr":"document.querySelector(\"button\").click(); \"clicked\""}}'
    sleep 2   # bash sleep, not JS sleep — the RPC call itself stays fast
    silo '{"op":"eval","args":{"expr":"document.querySelector(\".result\").textContent"}}'
    
    This also composes with golden rule 2 — each fire/sleep/read trio is still cheap curl calls inside one Bash block, just no longer racing the 5s limit.
  • listWorkspaces returns every real workspace on the machine, closed ones included — when picking one to "switch away to" (e.g. to try forcing a remount), don't just grab the first non-sandbox id from the list. A closed workspace has closedAt set; activateWorkspace on one reopens it (clears closedAt), silently undoing a deliberate close the user made, possibly weeks ago. Check closedAt first, or better, target a workspace you already know is open. If you do this by mistake, closeWorkspace (not deleteWorkspace) restores it.
  • Bash variables never survive across separate Bash tool calls — each is a fresh shell. If a workspace/terminal id from one block is needed in a later one, hardcode the literal id string (from what you echoed) at the top of the new block; don't reference $WS_ID and assume it's still set. An unset variable silently expands to "", so e.g. activateWorkspace with an empty id fails quietly rather than erroring loudly — easy to misread as "it worked" when it didn't do what you intended.
  • Switching workspaces away and back does not remount a backgrounded terminal panel — Silo's entire premise is keeping background work alive, so a terminal that's merely not-visible stays mounted and never re-triggers its attach/reattach effect. There's no bridge op to force just one tab to remount (no terminal-close-tab op, only whole-workspace closeWorkspace/ deleteWorkspace). To test something that only happens on that a real panel mount (e.g. reattach-after-daemon-death), a full app restart is required — which the golden-rule-1 sandbox discipline doesn't license doing on an attached (not self-launched) instance. Report the limitation rather than reaching for eval-driven DOM clicks on the user's live session to work around it.
  • In a dev build (target/debug/silo), the PTY session daemon is not a separately-named process — it's the same silo binary self-exec'd with --session-host <id> <folder> <cols> <rows> -- <shell>, not a distinct pty-host binary (that may differ in a release build). To kill just one session's daemon for a death-transition test, match on --session-host silo-<first 8 hex chars of the terminal's sessionId> and its folder — never a bare pkill -f pty-host-style pattern, which would hit every real session-host process on the machine, not just the sandbox one.

版本历史

  • 95dfc72 当前 2026-08-27 21:33

同 Skill 集合

.agents/skills/docs-screenshot/SKILL.md
.agents/skills/silo-docs-sync/SKILL.md
.agents/skills/silo-domain-modeling/SKILL.md
.agents/skills/silo-release-publish/SKILL.md
.agents/skills/silo-testing/SKILL.md
skills/silo-extension-builder/SKILL.md

元信息

文件数
0
版本
95dfc72
Hash
8865ae75
收录时间
2026-08-27 21:33

首页 - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-28 01:57
浙ICP备14020137号-1 $访客地图$