Conversation
Complete end-to-end hook telemetry: - Backend REST bridge client (BackendService interface — swappable to gRPC) - Session lifecycle: create on start, heartbeat every 30s, disconnect on exit - Sidecar: Unix socket server processes hook events, ingests async to backend - Hook registration: generates Claude Code settings.json with PreToolUse, PostToolUse, UserPromptSubmit hooks pointing to `kontext hook` - Hook command: reads stdin, connects to sidecar via KONTEXT_SOCKET, sends EvaluateRequest, writes decision to stdout - Wire protocol: length-prefixed JSON over Unix socket - Proto codegen: buf.yaml + buf.gen.yaml, generated ConnectRPC client stubs - Event types: session.begin, session.end, hook.pre_tool_call, hook.post_tool_call, hook.user_prompt → mcp_events table - Fail-open: if backend/sidecar unreachable, agent launches without telemetry - Graceful degradation: no KONTEXT_CLIENT_ID → launches without governance Closes #2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Separate governance telemetry (built-in, powers the dashboard) from developer observability (external, Langfuse/Dash0 via OTEL, future). Document the full architecture, hook flow, event types, wire protocol, and the REST bridge → ConnectRPC swap path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the REST bridge entirely. The CLI communicates with the Kontext backend exclusively via the proto contract (AgentService). - Backend client uses generated ConnectRPC stubs directly - Sidecar takes *backend.Client (ConnectRPC), not an interface - Run orchestrator uses proto request/response types - Auth transport injects bearer token into ConnectRPC requests - Generated proto code committed (removed gen/ from .gitignore) No backward compatibility layer. Requires server-side AgentService endpoint (kontext-dev/kontext#408) to function. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ProcessHookEventRequest (was HookEventRequest) - ProcessHookEventResponse (was HookEventResponse) - SyncPolicyResponse (was PolicyUpdate) - buf.gen.yaml uses managed mode to override go_package to CLI module path Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Critical: teardown now runs on non-zero agent exit (os.Exit moved
after cleanup, signal goroutine properly closed)
- Discovery endpoint cached after first call, uses context
- Token expiry floor prevents hot-loop on short-lived tokens
- Remove dead code: initTemplate, startTime, traceID, unused httpClient
- Fix os.Getenv("GOOS") → runtime.GOOS
- README: fix stale REST endpoint reference
- Use http.DefaultTransport explicitly
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CLI no longer uses KONTEXT_CLIENT_ID / KONTEXT_CLIENT_SECRET. The user's access token from `kontext login` (stored in the system keyring) is the only credential. Passed as a bearer token on every ConnectRPC request. Removed: - Config struct with clientId/clientSecret - LoadConfig() that reads env vars / config file - client_credentials token flow (discovery, Basic auth, token caching) - authTransport with token refresh Replaced with: - NewClient(baseURL, accessToken) — takes the token directly - bearerTransport — injects the static token, no refresh logic Regenerated proto stubs from kontext-dev/proto (stripped to 4 RPCs, no ExchangeCredential / SyncPolicy). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Block --settings and --setting-sources flags with value-skipping logic to prevent governance bypass via duplicate --settings - Guard sessionID[:8] against panic on empty/short session IDs - Clone request in bearerTransport.RoundTrip to avoid mutating the original (http.RoundTripper contract) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Explains the CLI's design to someone who has never seen it: - What it does and why - Three modes (start, hook, login) in one binary - Why Go (hook startup time) - Sidecar pattern and why it exists - Agent adapter interface - Credential injection via env template - Auth model (user OIDC, no client secrets) - Telemetry vs credentials split - What works today vs what's blocked on server Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| } | ||
| if blocked[arg] { | ||
| fmt.Fprintf(os.Stderr, "⚠ Stripped blocked flag: %s\n", arg) | ||
| // If this flag takes a value, skip the next arg too | ||
| if arg == "--settings" || arg == "--setting-sources" { | ||
| skip = true | ||
| } | ||
| continue | ||
| } | ||
| if blockedWithValue[arg] { | ||
| fmt.Fprintf(os.Stderr, "⚠ Stripped blocked flag: %s\n", arg) | ||
| skip = true // skip the next arg (the value) | ||
| continue | ||
| } | ||
| filtered = append(filtered, arg) | ||
| } | ||
| return filtered |
There was a problem hiding this comment.
🔴 filterArgs does not handle --flag=value syntax, allowing governance bypass
The filterArgs function uses exact string matching (blocked[arg], blockedWithValue[arg]) to detect blocked flags. This means flags passed in --flag=value syntax (e.g., --settings=/path/to/evil.json or --setting-sources=custom) will not be caught by the filter and will be forwarded to the agent.
Since launchAgentWithSettings at internal/run/run.go:269-272 prepends Kontext's own --settings first and then appends the filtered user args, a user passing -- --settings=/tmp/evil.json to kontext start would result in the agent receiving both --settings /tmp/kontext/.../settings.json --settings=/tmp/evil.json. Most CLI argument parsers (including those used by Claude Code) use the last occurrence of a flag, so the user-supplied settings would override Kontext's governance hook configuration, effectively disabling all telemetry and policy enforcement.
(Refers to lines 299-328)
Prompt for agents
The filterArgs function in internal/run/run.go:299-328 only performs exact string matching against blocked flags. It fails to handle the --flag=value syntax (e.g., --settings=/path/to/file), which is a standard alternative accepted by most CLI argument parsers including those used by Claude Code.
To fix this, filterArgs should also check whether each arg starts with a blocked flag name followed by '='. For example:
- Check strings.HasPrefix(arg, "--settings=") for blockedWithValue flags
- Check strings.HasPrefix(arg, "--bare=") or strings.HasPrefix(arg, "--dangerously-skip-permissions=") for blocked boolean flags
A clean approach would be to extract the flag name from each arg (split on '=' if present) and check that against the blocked maps. This should cover both --flag value and --flag=value syntax.
Was this helpful? React with 👍 or 👎 to provide feedback.
Explains the CLI's design to someone who has never seen it before.
🤖 Generated with Claude Code