Implements #688.
Adds a SpanExporter that signs exported spans as Ed25519 receipts, chains them,
and anchors the sequence with a signed checkpoint, so a third party can verify
what a run did without trusting the collector, the database, or the operator.
Standard library crypto only. No new dependencies, and no existing file is
modified: it attaches through SetExporter the same way OTLP does.
Design notes worth reviewing:
- Coverage counts spans this exporter received, never spans the runtime
executed. Collector.EmitSpan drops spans when its buffer fills, which is
correct for observability and means an exporter can only sign what reaches
it. Every checkpoint carries a note saying so, so a receipt set reads as a
floor on what happened rather than a complete record.
- The checkpoint is not optional. A hash chain cannot detect truncation:
removing the newest receipts leaves every remaining link valid. Verification
reports Valid=false when no checkpoint is supplied, because without one it
can only say nothing was altered, never that nothing is missing.
- A checkpoint stored beside its receipts is deletable by whoever deletes the
receipts, so Sink keeps the two destinations separate to make publishing the
anchor elsewhere straightforward.
- Content is committed by digest rather than copied, since the package redacts
previews and a second store of that content would widen a leak.
- Ordering comes from an exporter sequence rather than a clock.
- Revocation is not retroactive; RevokedAt covers the compromise case.
- Signature and chain failures are reported separately, and a failed sink write
does not advance the chain head.
26 tests, most adversarial: tampering, forged signatures under a claimed kid,
cross-domain signature replay, validity windows, revocation ordering, middle
deletion, reordering, end truncation, empty bundles, sink failure, and
concurrent export. go vet and go test -race clean.
Co-authored-by: tommylauren <tfarley@utexas.edu>
The CLI agent chat command connects via WebSocket but never sends
user_id in the connect params. The gateway requires user_id for
chat.send, making the CLI unusable for any agent interaction.
All other clients (browser, Telegram, LINE, etc.) send user_id
during connect. The CLI was the only channel missing it.
Adds --user / -u flag that passes user_id in the connect frame.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When X-GoClaw-User-Id header is provided, build a stable session key
using BuildSessionKey() — same canonical format as Telegram, Discord,
Slack channels. This allows HTTP API clients to maintain conversation
history across multiple requests.
Without the header, behavior is unchanged (random session per request).
Session key format:
With userId: agent:{id}:http:direct:{userId} (persistent)
Without: agent:{id}:http-{random} (stateless)
Co-authored-by: Claude Code <claude@anthropic.com>
- Check resp.StatusCode range (2xx) before JSON decoding
- Provide clear actionable errors with HTTP status codes for 5xx failures
- Prevent cryptic JSON parse errors from HTML error pages
The cron job handler reset the session when stateless=false and skipped
reset when stateless=true — the opposite of what every UI locale labels
the field (en/ko/zh/vi all describe stateless as "each run starts fresh
without loading previous messages").
The buggy gate caused stateless=true crons to silently accumulate session
history across every execution, leading to context bloat and increasing
the chance of LLMs short-circuiting tool calls in favor of replaying
prior assistant turns. One affected daily ETL cron grew to 38 messages
over 18 days before the regression was noticed.
Fix: gate the Reset on `if job.Stateless` so the runtime matches the UI
contract. No DB migration is required — existing values were set by users
based on the UI label, so they already encode the intended behavior.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ExecTool path-deny rule blocks any token containing `.goclaw/` unless
it matches one of the AllowPathExemptions prefixes (skills-store, tenants).
This silently rejected legitimate commands invoking the goclaw-managed
Python interpreter via its absolute path:
/home/user/.goclaw/venv/bin/python3 .../script.py
The first token `/home/user/.goclaw/venv/bin/python3` matched the deny
pattern but no exemption, so the entire command was denied.
Naive exemption (".goclaw/venv/bin/") does not work: matchesAnyPathExemption
resolves both tokens and exemption candidates via EvalSymlinks, and the
venv's python3 is a symlink into the host's python cellar (e.g. linuxbrew).
The token canonicalizes to /home/linuxbrew/.../python3.14 while a literal
".goclaw/venv/bin/" prefix never gets touched.
Fix: resolve venv/bin/python3 once at startup and exempt the dirname of
the resolved target. Failure to resolve (no venv present) silently falls
through.
Without this, ACP-driven agents either fail outright or work only via
fragile heuristics (cwd-local symlinks generated on the fly by the LLM).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
session/update notifications carrying a ToolCall (or inline tool_call /
tool_call_update kind) were only dumped at Debug level inside the params
blob. Operators could see `security.tool_granted` (permission granted) but
had no way to tell whether the tool actually executed successfully — both
"permission granted then failed silently" and "permission granted and
succeeded" looked identical in journalctl.
Add a structured Info log emitting toolCallId, name/title, status, and a
content preview (truncated at 400 chars) whenever the notification
contains tool-call state. This is what made it possible to diagnose the
recent .goclaw/-path-deny regression — `status=failed` immediately after
`security.tool_granted` revealed the gap that the granted-only log hid.
No behavior change beyond logging volume; preview is bounded.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bridgeContextMiddleware injects the agent UUID (store.WithAgentID) but never the
agent key, so session tools (sessions_list/history/send/status) that resolve the
caller via tools.ToolAgentKeyFromCtx always get "" and fail with "agent context
required" when invoked over /mcp/bridge (e.g. by a claude-cli provider agent).
Session keys are namespaced by agent key (agent:<key>:...), not UUID, and the
bridge has no RunContext fallback.
Inject the key in the same block that already fetches the agent for shell-deny
overrides — symmetric with store.WithShellDenyGroups, no extra DB call, no new
imports.
#1094 makes the same fix at this site but bundles it into a large, stale
(CONFLICTING) ACP/i18n refactor; this is the minimal extraction of that slice.
It omits #1094's companion store.WithAgentKey write, which nothing on dev reads
(every session tool reads tools.ToolAgentKeyFromCtx).
Add internal/gateway/bridge_context_test.go: a regression guard asserting a
signed X-Agent-ID lands the agent key in tools.ToolAgentKeyFromCtx, plus a
no-store negative control.
Signed-off-by: Jaegeon Oh <zezaeoh@gmail.com>
Upstream returns HTTP 400 `thinking is enabled but reasoning_content
is missing in assistant tool call message at index N` when an
assistant message with tool_calls is replayed in history without a
reasoning_content field. Kimi has server-side thinking enabled by
default for kimi-k2-turbo-preview, so the field is required even when
goclaw doesn't have captured reasoning content to send (e.g. the model
emitted a tool_call without any thinking, or the stream chunk that
carried it was lost).
The existing branch already gates on
openAIWireAssistantReasoningContent(model) (kimi/deepseek/o-series)
and emits the field only when Thinking != "". Extend so kimi_coding
also emits an empty string when Thinking is unset — satisfies Kimi's
"must be present" check without inventing reasoning content. Other
providers in the allowlist keep today's behavior: omit when empty.
Three new tests:
- kimi_coding always carries reasoning_content on assistant
- kimi_coding preserves real Thinking content when set
- non-kimi providers (deepseek) do NOT inject empty reasoning_content
Reference: NousResearch/hermes-agent plugins/model-providers/kimi-coding
documents the same upstream behavior (thinking enabled by default,
reasoning_content roundtrip required).
Moonshot's Kimi Coding endpoint is OpenAI-compatible on the wire but
has two non-standard rules:
1. Every request must carry `User-Agent: claude-code/0.1.0` — without
it the upstream rejects the call outright.
2. `temperature` is locked to the server default; passing any other
value returns HTTP 400 `invalid temperature: only 1 is allowed for
this model`.
Rather than special-case either, this commit generalises both:
- WithExtraHeaders on OpenAIProvider — static headers attached to
every outgoing request. Reusable by any future provider that needs
pinned identity headers; mirrored in adapter_openai.ToRequest so
callers using the adapter path see the same shape.
- The existing skipTemp branch in openai_request.go gets a
provider_type check — kimi_coding joins o1/o3/o4/gpt-5-mini in
omitting `temperature` from the request body.
Provider wiring:
- store.ProviderKimiCoding constant + ValidProviderTypes entry +
KimiCoding{DefaultAPIBase,DefaultModel,RequiredUserAgent}.
- case store.ProviderKimiCoding in both registration switches
(cmd/gateway_providers.go and internal/http/providers.go).
- UI dropdown entry with the API base pre-filled.
5 unit tests cover: real outgoing header injection, adapter-path
header mirroring, empty-map WithExtraHeaders no-op, kimi_coding
strips temperature, and the negative control (other providers still
forward temperature).
Admin flow: Providers → Add → "Kimi Coding (Moonshot)" → paste API
key → save.
- Change outer container from items-center to items-start sm:items-center for mobile-first layout
- Add overflow-y-auto and max-h-dvh constraints to inner content div on mobile
- Desktop (sm breakpoint) retains centered layout with no scroll constraint
- Fixes issue where setup page content exceeds viewport on mobile without zoom
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>