BeforeTool-hook + kort-vy (strukturerade godkännanden) + release på master-push
All checks were successful
build-and-push / build (push) Successful in 22s
release-client / build-release (push) Successful in 48s

Feature: strukturerade verktygsgodkännanden
- shared: protokoll för approval (daemon:approval, server:approval[-resolved],
  web:approval-decision, server:approval-decision)
- daemon: hook-läge ('agent-helm hook' — samma binär), lokal 127.0.0.1-endpoint
  som hooken POST:ar till, long-poll tills beslut; graceful default (allow) när
  ingen UI; timeout-fallback; skickar AGENT_HELM_HOOK_PORT till agentens env
- server: routar daemon:approval -> prenumeranter, beslut -> daemon + döljer kort
- web: reaktiv approvals-lista + kort-overlay (Tillåt/Neka), mobilanpassad

CI:
- release.yaml triggar nu även på master-push -> rullande 'latest'-release
  (versionerade releaser kvar på v*-taggar); install.sh hämtar 'latest'

Docs: hook-konfig (gemini settings.json) + hook-env i .env.example + DEPLOY.md.
Säkerhet: hook-endpoint binder endast 127.0.0.1; inga hemligheter i repot.

Verifierat lokalt: hook->daemon->server->kort->beslut->hook ger {"decision":"allow"};
alla paket typecheckar; web bygger.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 18:03:12 +02:00
parent a06a02927c
commit 98c4d13360
9 changed files with 460 additions and 97 deletions

View File

@@ -146,6 +146,15 @@ function handleDaemonMessage(sessionId: string, text: string): void {
sendWeb(sub, { type: "server:exit", sessionId, code: msg.code });
}
broadcastSessions();
} else if (msg.type === "daemon:approval") {
const out: ServerToWeb = {
type: "server:approval",
sessionId,
approvalId: msg.approvalId,
toolName: msg.toolName,
toolInput: msg.toolInput,
};
for (const sub of session.subscribers) sendWeb(sub, out);
}
}
@@ -166,8 +175,29 @@ function handleWebMessage(ws: WebSocket, text: string): void {
} else if (msg.type === "web:input") {
const session = sessions.get(msg.sessionId);
if (!session?.daemon || session.daemon.readyState !== WebSocket.OPEN) return;
// Strukturerade hook-svar går INTE här — bara terminal-input (text/tangenter).
// Terminal-input (text/tangenter) -> pty. Hook-svar går via web:approval-decision.
session.daemon.send(JSON.stringify({ type: "server:input", data: msg.data }));
} else if (msg.type === "web:approval-decision") {
const session = sessions.get(msg.sessionId);
if (!session) return;
if (session.daemon && session.daemon.readyState === WebSocket.OPEN) {
session.daemon.send(
JSON.stringify({
type: "server:approval-decision",
approvalId: msg.approvalId,
decision: msg.decision,
reason: msg.reason,
}),
);
}
// Dölj kortet hos alla andra klienter som ser samma session.
const resolved: ServerToWeb = {
type: "server:approval-resolved",
sessionId: msg.sessionId,
approvalId: msg.approvalId,
decision: msg.decision,
};
for (const sub of session.subscribers) sendWeb(sub, resolved);
}
}