Skip to content

docs: add ARCHITECTURE.md#8

Open
tumberger wants to merge 11 commits intomainfrom
docs/architecture
Open

docs: add ARCHITECTURE.md#8
tumberger wants to merge 11 commits intomainfrom
docs/architecture

Conversation

@tumberger
Copy link
Copy Markdown
Contributor

@tumberger tumberger commented Apr 6, 2026

Explains the CLI's design to someone who has never seen it before.

🤖 Generated with Claude Code


Open with Devin

tumberger and others added 11 commits April 5, 2026 19:57
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>
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines 316 to 328
}
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant