* feat(core): relay unsolicited agent events between user turns
Add a background goroutine per interactive session that consumes agent
events produced between user-initiated turns (e.g. Claude Code background
task completions via run_in_background). Previously these events piled up
in the buffered channel and were unconditionally discarded by drainEvents()
when the next user message arrived.
Key changes:
- Add startUnsolicitedReader/stopUnsolicitedReader with context-based
lifecycle management and bounded handoff to foreground turns
- Replace unconditional drainEvents with conditional resync via
eventsNeedResync flag (default true, cleared on clean EventResult)
- Integrate unsolicited reader teardown into all cleanup paths:
cleanupInteractiveState, stopInteractiveSession, session recycle,
/compress, and idle reaper
- Track BeginTurn/EndTurn for unsolicited turns so idle reaper does
not kill workspaces with active background processing
- Make workspace pool idle timeout configurable via
workspace_idle_timeout_mins in project config (was hardcoded 15min)
- Export SetWorkspaceIdleTimeout and DefaultWorkspaceIdleTimeout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(core): add unit tests for unsolicited events feature
Add 10 tests covering the unsolicited events implementation:
- TestUnsolicitedReader_RelaysEventResult: verifies EventResult relay
- TestUnsolicitedReader_StopsOnCancel: verifies clean goroutine shutdown
- TestUnsolicitedReader_SetsResyncOnChannelClose: abnormal exit path
- TestUnsolicitedReader_SetsResyncOnEventError: error event handling
- TestUnsolicitedReader_PermissionDeny: permission auto-deny when no user
- TestEventsNeedResync_DefaultTrue: constructor initialization
- TestEventsNeedResync_ClearedOnCleanResult: clean EventResult path
- TestCleanupInteractiveState_StopsUnsolicitedReader: cleanup teardown
- TestWorkspaceIdleTimeout_Configurable: timeout config API
- TestReapIdle_DisabledWhenZeroTimeout: zero timeout disables reaping
Tests use event-driven synchronization (channel waits with timeouts)
instead of fixed time.Sleep to avoid CI flakiness.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): stop unsolicited reader in drainOrphanedQueue to prevent race
drainOrphanedQueue calls drainPendingMessages which reads from the
Events() channel. Without stopping the unsolicited reader first, two
goroutines can concurrently read from the same channel — a data race.
Also restart the unsolicited reader after draining completes so
background events continue to be relayed.
Found during self-review concurrency audit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(integration): add end-to-end tests for unsolicited events
Add two integration tests covering the full flow:
TestIntegration_UnsolicitedEventsEndToEnd — verifies the happy path:
1. User sends msg → agent turn completes (EventResult)
2. Agent emits new events later (simulating run_in_background completion)
→ unsolicited reader relays them to platform
3. User sends second msg → conditional drain skipped (clean exit) →
new turn response delivered correctly
TestIntegration_StaleEventsDrainedAfterAbnormalExit — verifies the safety
net: after EventError sets eventsNeedResync=true, buffered leftover events
are drained on the next user message (not relayed as unsolicited, not
attributed to the new turn).
Uses a persistentEventsSession that keeps the Events() channel open across
turns (unlike FakeAgentSession which closes per-call). Synchronization is
event-driven via waitForMessage and waitForPromptCount — no fixed sleeps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): address PR #608 review comments
Addresses all five review comments from @chenhg5 on PR #608:
1. **[blocker]** Promote workspace_idle_timeout_mins to a global config.
Adds top-level Config.WorkspaceIdleTimeoutMins; per-project
ProjectConfig.WorkspaceIdleTimeoutMins retains precedence so existing
configs keep working. main.go picks project > global > default.
2. Close the concurrent-reader window around stopUnsolicitedReader.
After cancel + bounded 5s wait, the reader now double-checks ctx
after reading an event — if cancellation happened between the outer
select firing and the check, the event is dropped and resync is
requested, preventing the old reader from silently consuming an event
that belongs to the incoming foreground turn. The wait stays bounded
because getOrCreateInteractiveStateWith and stopInteractiveSession
call this while holding interactiveMu, and an unbounded wait there
would stall unrelated sessions.
3. Notify the user on auto-deny. When approveAll=false, the reader now
sends a localized MsgBackgroundAutoDenied message (EN/ZH/ZH-TW/JA/ES)
identifying the requested tool so a silently blocked background task
isn't invisible. RespondPermission is dispatched on a detached
goroutine so the slow adapter call cannot block reader shutdown.
4. Accumulate and log tool events. EventToolUse / EventToolResult now
record tool names and emit debug logs. If the channel closes mid-turn
with buffered context, a warning log captures the tools/text that
were lost — removes the misleading "accumulate silently" comments.
5. Document Session.AddHistory thread-safety. Session.AddHistory is
already internally locked; stopUnsolicitedReader's bounded wait
orders unsolicited-vs-foreground history writes. Added an explanatory
comment so the invariant is explicit.
Verified with go test -race ./... and go test -race -tags=integration
./tests/integration/. Reviewed end-to-end by Codex (gpt-5.3-codex, high
reasoning effort) — no regressions flagged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(config): make workspace_idle_timeout_mins global-only
Addresses the remaining blocker on PR #608: the maintainer asked for
workspace_idle_timeout_mins to be a global config rather than per-project.
Changes:
- Remove ProjectConfig.WorkspaceIdleTimeoutMins as a supported per-project
knob; the canonical setting is now the existing top-level
Config.WorkspaceIdleTimeoutMins.
- Keep parsing the legacy per-project key (renamed to
WorkspaceIdleTimeoutMinsLegacy in code) so existing configs continue to
work. When the top-level field is unset, the legacy per-project value is
honored as a fallback and a deprecation warning is logged at startup.
This avoids silently breaking deployments that used `0` to disable
reaping or a custom timeout.
- Clarify the comment on the global field to state that per-project
configuration is intentionally not supported.
Also commits the unsolicited-reader test additions in core/engine_test.go
(437 lines covering reader relay, cancel/stop, channel close, error
handling, permission deny, resync flag defaults/clearing, cleanup, and
workspace idle timeout configurability) that landed alongside the prior
review-fix commit but were not previously committed.
Verified via `codex exec review --uncommitted` (gpt-5.3-codex). Build
clean, all relevant tests pass; the 3 known macOS-specific pre-existing
failures (TestProcessInteractiveEvents_AppendsReplyFooter*,
TestResolveLocalDirPath_AcceptsSubdir) are out of scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the user deletes all projects via web UI, config.toml ends up with
an empty [[project]] section. Previously this caused a panic downstream.
Now cc-connect exits early with a clear error message guiding the user
to add a project or run cc-connect init.
Closes#749
Co-authored-by: Claude <noreply@anthropic.com>
* feat(platform/max): add MAX messenger bridge with bidirectional file and voice support
- Implement core.Platform plus InlineButtonSender, ImageSender, FileSender,
AudioSender, MessageUpdater, FormattingInstructionProvider,
ReplyContextReconstructor
- Two-step attachment upload via POST /uploads + multipart to CDN
- Route incoming audio (type=audio and audio/* file mime) to
core.Message.Audio so the speech pipeline transcribes it for the agent
- Outgoing voice replies via SendAudio for the TTS pipeline
- Retry on attachment.not.ready with exponential backoff
- Long poll /updates with reconnect backoff and allow_from filtering
- Register max in Makefile ALL_PLATFORMS and document in config.example.toml
- Tighten .gitignore: ignore config.*.toml (except example) and *.old
- 19 unit/integration tests with httptest mock, passes -race
Refs #741
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(platform/max): handle real MAX CDN response shapes and audio download
Issues discovered when testing end-to-end with the live MAX API:
1. Image CDN returns nested {"photos": {<id>: {"token": ...}}}, not a flat
{"token": ...}. Parse the right shape per kind.
2. Audio CDN returns XML "<retval>1</retval>" rather than JSON. The real
attachment token is delivered in the /uploads response itself for
video/audio, so we use urlInfo.Token as the source of truth and only
scan the CDN body opportunistically.
3. /audios/{token} endpoint does not exist in MAX (returns 404). Incoming
voice payloads already carry the download URL directly, mirroring
images/files. Use payload.url; fall back to resolveMediaURL only if
empty (for video where token resolution may still apply).
Also raise the speech HTTP client timeout from 60s to 5m — enough for the
first-request cold-start case when a local Whisper backend has to download
or JIT-load the model before responding.
Tests updated to emit the real CDN response shapes per attachment type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(platform/max): address review — dedup + idiomatic session key parse
- Add core.MessageDedup to Platform and check msg.Body.Mid / callback ID
in handleMessage and handleCallback. Guards against reprocessing on
poll-loop reconnect edge cases, matching the pattern used by feishu/
dingtalk/wecom platforms.
- Replace fmt.Sscanf in ReconstructReplyCtx with strings.CutPrefix +
strings.Cut — more idiomatic Go, simpler to read, and handles the
"max:" prefix and optional ":userID" suffix without a manual loop.
- Add TestHandleMessageDedupsByID regression test.
Addresses review feedback on #742.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: add Homebrew as install option (brew install cc-connect)
Made-with: Cursor
* feat(core): make message queue depth configurable (#688)
Replace hardcoded maxQueuedMessages=5 with a configurable value via
[queue] max_depth in config.toml. When the queue is full, send a new
MsgQueueFull message with the current depth instead of the misleading
MsgPreviousProcessing.
- Add QueueConfig struct in config with max_depth field
- Add Engine.SetMaxQueuedMessages() setter, default remains 5
- Add MsgQueueFull i18n key with all 5 languages
- Wire config in main.go
- Update existing overflow test for new behavior
Closes#688
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Adds native support for Devin CLI (Cognition, https://cli.devin.ai/)
as a named cc-connect agent, alongside Claude Code / Codex / Cursor /
Gemini / iFlow / Qoder / OpenCode. Users can now configure Devin with
a minimal `type = "devin"` block rather than hand-rolling a generic
ACP config.
[[projects]]
name = "my-project"
[projects.agent]
type = "devin" # NEW
[projects.agent.options]
work_dir = "/path/to/code"
# command / args / display_name default to "devin" / ["acp"] / "Devin"
# mode, auth_method, env (for WINDSURF_API_KEY) passed through to ACP
Credentials come from the local `devin auth login` store — cc-connect
never sees or forwards any token. Windsurf Enterprise users can inject
WINDSURF_API_KEY via the agent env option.
The new `agent/devin/` package is a thin wrapper around `agent/acp/`:
Devin speaks the Agent Client Protocol over stdio via its `devin acp`
subcommand, so the session/prompt, session/update, permission, and
tool-call plumbing is shared. Embedding *acp.Agent and shadowing only
Name() gives us identity and defaulting without duplicating transport
code:
type Agent struct { *acp.Agent }
func (a *Agent) Name() string { return "devin" }
Wiring Devin revealed two protocol features that agent/acp had left
as MVP-skipped TODOs. Both are needed for `/list` and `/mode` to work
in IM, so this PR fills them in as part of the Devin integration —
Cursor Agent, OpenClaw, and Copilot CLI all pick up the same
improvements automatically.
ListSessions now spawns a short-lived `<command>` process that runs
just initialize + session/list, then tears down. Capability is
discovered via `agentCapabilities.sessionCapabilities.list`, and a
negative result is cached on the Agent so non-supporting servers pay
the spawn cost only once. Soft-degrades on JSON-RPC -32601 (method
not found) and -32600 (invalid request). Measured cost against
`devin acp` 2026.4.9-0: ~40 ms round trip.
Agent implements core.ModeSwitcher (SetMode / GetMode /
PermissionModes); the modes list is sourced from the modes block
returned by session/new or session/load. GetMode precedence: the most
recent explicit SetMode wins over the last server-advertised
currentModeId, so `/mode plan` reports the new mode back to the user
immediately rather than the cached previous one.
acpSession implements core.LiveModeSwitcher via session/set_mode with
client-side modeId validation against the cached availableModes.
Client-side validation is mandatory because at least one ACP server
(devin acp 2026.4.9-0) silently accepts unknown modeIds with an empty
result — server-only validation would let typos go undetected.
session/update notifications with sessionUpdate = current_mode_update
are absorbed so cc-connect stays in sync when the user switches modes
from another client (e.g. the Windsurf IDE).
newACPSession moved from seven positional args to an acpSessionConfig
struct. Private API, not a breaking change externally; the one
exported consumer (cursor_integration_test.go) is updated.
A sessionCallbacks interface lets running acpSession instances report
handshake-derived capabilities and modes back to the parent Agent,
since the engine (not the agent) owns the session lifetime.
- README.md / README.zh-CN.md: Devin added to the Support Matrix and
the agent count bumped from 7 to 8.
- config.example.toml: dedicated Devin section placed before the
generic ACP examples, with prerequisites and override notes.
- web/src/pages/Projects/ProjectList.tsx: Devin added to the agent
dropdown so the web admin UI surfaces it alongside other
first-class agents.
- Makefile: ALL_AGENTS += devin (enables selective compilation tags
`no_devin` / AGENTS=devin,...).
22 new tests total (7 for agent/devin, 15 for agent/acp). All pass
with -race. agent/acp wrapper tests avoid requiring `devin` in PATH
by pinning `command = "true"` to satisfy the upstream exec.LookPath
check while still exercising the Name() shadow and default injection.
End-to-end verified against live `devin acp` 2026.4.9-0 through the
cc-connect bridge:
- /mode lists Devin's five modes with ▶ Code marked current
- /mode plan switches and the engine reports "switched to **Plan**"
- /list shows "devin Sessions (1)" (confirms Name() propagates to UI)
- agent field in engine logs = "devin" (not "acp")
Closes none — net-new agent support.
Generated with [Devin](https://cli.devin.ai/docs)
Co-authored-by: youngshook <youngshook@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Session filtering that hides external CLI sessions is now opt-in via
`filter_external_sessions = true` in project config. By default, /list,
/switch, /delete show all agent sessions regardless of origin.
Includes integration tests covering /list, /switch, /delete, card
rendering, and dynamic toggle under both modes, plus config parse tests.
Made-with: Cursor
- Add /skills route with local skills browser and recommended presets
- Backend: ListSkills/SkillDirs/AgentTypeName on Engine, skill presets API
- Provider preset cards: pin register/add buttons to card bottom
- Fix layout footer overlap by removing min-h-0 from content wrapper
- Replace logo with styled text brand, remove temporary local JSON override
- Add i18n translations for skill-related strings (en/zh/zh-TW/ja/es)
Made-with: Cursor
- Add `cc-connect web` CLI subcommand: reads config, auto-enables web
admin if needed, opens browser with token pre-filled for instant login
- Support --no-browser flag to just print URL and token
- Browser open supports macOS, Linux, Windows, and WSL
- Frontend Login page reads ?token= query param and auto-authenticates
- Add global 401 interceptor in API client: automatically logs out and
redirects to login page when token is invalid or changed
Made-with: Cursor
- Fix provider switch not taking effect: clear agent session ID on switch
to force fresh session instead of resuming with stale config
- Fix ANTHROPIC_MODEL env leak from cc-switch: explicitly set model env
when provider has custom BaseURL
- Add "clear provider" option in Feishu card for unsetting active provider
- Add debug logging for provider env vars and session startup commands
(with secret redaction via RedactEnv/RedactArgs)
- Web UI: add agent type switcher in project settings with provider
compatibility filtering and restart-required flow
- Web UI: filter global providers by agent_types when linking to project
- Web UI: fix heartbeat tab showing "running" for all projects (now shows
"not configured" when heartbeat is not enabled)
- Web UI: fix page not refreshing after agent type change + restart
- Add comprehensive integration tests for both claudecode and codex
provider switching (env vars, session args, config writing, send/receive)
- Complete i18n for all 5 languages: replyFooter, injectSender,
thinkingMessages, toolMessages and their hints
Made-with: Cursor
Add a "Import from CC-Switch" button to the web provider management
page. The backend reads the cc-switch SQLite database directly using
modernc.org/sqlite (pure Go, no CGO/sqlite3 CLI dependency), and the
frontend shows a modal with checkboxes to select which providers to
import as global providers.
- Add GET/POST /api/v1/providers/cc-switch management API endpoints
- Replace sqlite3 CLI usage with database/sql + modernc.org/sqlite
- Add CCSwitchImportModal component with i18n support (5 languages)
Made-with: Cursor
doctor_runas.go and runas_startup.go reference Unix-only symbols from
core/runas_check.go and core/runas_audit.go (both //go:build !windows)
but lacked build tags themselves, breaking Windows builds. Add
!windows tags and corresponding _windows.go stubs.
Made-with: Cursor
Provider switching was using the top-level BaseURL/Model from global
providers instead of per-agent-type overrides (Endpoints/AgentModels/
AgentModelLists). This caused agents like Codex to hit the wrong API
endpoint (e.g. /api/responses instead of /api/v1/responses), resulting
in 404 errors.
Also:
- Extract ProviderConfig.ResolveForAgent() to deduplicate override logic
- Cascade-delete provider_refs when a global provider is removed
- Purge deleted providers from running engines at runtime
- Add per-agent-type config UI for global provider form (tabbed when ≥2 agents)
- Add i18n translations for new form fields
Made-with: Cursor
- Restructure provider-presets.json (v1→v2): replace flat endpoints/
agent_models/models/agents/codex_config fields with a unified
`agents` map keyed by agent type, each containing base_url, model,
models list, and optional codex_config
- Update ProviderPreset Go struct with PresetAgentConfig and helper
methods SupportsAgent()/AgentConfig()
- Add AgentModelLists to config.ProviderConfig for per-agent model
lists; ResolveProviderRefs now resolves models per agent type
- Filter global provider linking by agent type in IM /provider add
card and executeProviderLink to prevent incompatible associations
- Fix Codex 404 caused by shengsuanyun agent_types including "codex"
with wrong base_url; remove codex from its agent_types
- Update web frontend ProviderPreset type and preset display/add logic
to use the new agents map structure
Made-with: Cursor
- Add global [[providers]] config with project-level provider_refs
- Add provider management page in web admin with preset support
- Implement card-based /provider add flow: preset selection card
with sponsor presets, API key prompt, and manual "Other" option
- Add inline button support for Telegram-style platforms
- Add remote provider-presets.json with sponsor data
- Add REST API endpoints for global providers and provider refs
- Update project detail providers tab with unified list UI
Made-with: Cursor
Implement Weibo private message platform via the Open IM WebSocket
protocol (open-im.api.weibo.com). Supports app_id/app_secret auth,
automatic token refresh, heartbeat keep-alive, reconnect with
exponential backoff, message dedup, and chunked sending for long
messages (2000 char limit).
Includes plugin registration, build tag (no_weibo), unit tests,
setup documentation, and cross-references in all platform docs.
Made-with: Cursor
doctor_runas.go and runas_startup.go reference core types gated
behind //go:build !windows. Add the same constraint and provide
a stub file so Windows cross-compilation succeeds.
Made-with: Cursor
* feat(cli): add `cc-connect agent-sid` command to query agent session ID
Reads the agent session ID (e.g. Claude Code / Codex / Gemini CLI session
ID used with --resume) from persisted session files. Auto-detects project
and session key from CC_PROJECT and CC_SESSION_KEY env vars, making it
usable directly inside agent subprocess sessions.
Handles all session file naming variants: plain, hashed, workspace (_ws_),
legacy paths, and .sessions.json. When multiple files contain the same key,
prefers the one with the newest UpdatedAt. Includes nil-safety, file modtime
fallback for zero-UpdatedAt entries, and proper error propagation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(core): add /agentsid chat command to show agent session ID
Registers /agentsid (alias: /agent-sid) as a builtin chat command so
users can query the agent session ID directly from Discord/Telegram/
Feishu etc. without asking the agent to run a Bash command.
- Added to builtinCommands registry, handleCommand switch, and help
card (session group)
- Added to text-based /help output for all 5 languages (EN/ZH/ZH-TW/JA/ES)
- Added i18n description key MsgBuiltinCmdAgentSID with translations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(lint): use strings.TrimPrefix instead of HasPrefix+slice
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(core): add optional reply footer metadata
* feat(config): enable reply footer by default
* feat(codex): use runtime footer metadata from app-server
* feat(codex): resolve exec footer metadata at runtime
* feat(core): style reply footer and prefer context left
* feat(codex): use real runtime context for reply footer
* feat(core): use english left footer text
* fix(codex): use current context tokens for footer left
* fix(codex): match cli context left calculation
* test(codex): adapt footer tests to session base url arg
High priority:
- PR #420: fix Telegram reply context lost by alias resolution —
resolveAlias now runs on user text before ExtraContent merge
- PR #567: fix message queue race — adoptPendingFromPlaceholder
copies queued messages when placeholder state is replaced
- PR #437: harden /diff command — reject targets starting with '-',
use '--' separator to prevent git option injection
- PR #560: add run_as_env dangerous variable blacklist (LD_PRELOAD,
PATH, HOME, etc.); fix doc claiming PATH is in default allowlist
Medium priority:
- PR #497: ACP permission params log level Info→Debug to prevent
sensitive data leaking; add toolInputByID cache size cap (1000)
- PR #559: observe new JSONL files start from EOF instead of offset 0,
preventing full session history replay to Slack
- PR #510: add idempotent Close() test for claude session
- PR #514: bridge ReconstructReplyCtx already has structured JSON +
backward compat; added decode helper + tests
- PR #440: add instance lock acquire/release unit tests
- PR #569: document ContinueSession behavioral change in code comments
All changes backward-compatible. Tests added for all fixes.
Made-with: Cursor
* feat: add run_as_user for per-project OS-user isolation
Implements #496. Adds an optional `run_as_user` field at the project
level that, when set, spawns the project's agent command under a
different Unix user via `sudo -n -iu <user> --`. This provides OS-level
file-system isolation from the supervisor user that runs cc-connect
itself: a misbehaving agent can no longer reach files outside the
target user's permission scope, even via python -c / shell redirection
/ other non-hooked code paths.
## What lands in this PR
### Config (config/)
- new fields `run_as_user *string` and `run_as_env []string` on each
`[[projects]]` entry
- validation: rejects root, rejects invalid usernames, rejects use on
Windows, enforces POSIX username charset to close a sudo argv
injection vector
### Core primitives (core/runas*.go)
POSIX-only (Windows stubs provided for API compatibility).
- `BuildSpawnCommand(ctx, opts, name, args...)` — wraps exec.Cmd with
`sudo -n -iu <user> --preserve-env=<allowlist> -- name args...` when
opts.RunAsUser is set; passthrough otherwise
- `FilterEnvForSpawn(env, opts)` — strips supervisor env to a minimal
allowlist (PATH, LANG, LC_*, TERM) plus any project-level extensions
- `VerifyRunAsUserCheap` — the two cheap sudo probes, suitable for
per-spawn re-verification (catches sudoers edits between startup and
spawn)
- `PreflightRunAsUser` — the three go/no-go startup gates from #496:
(1) passwordless sudo to target works,
(2) target cannot passwordless sudo (if escalation succeeds, captures
`sudo -n -l` output in the error message), and
(3) target can read AND write work_dir root. Plus a non-fatal
descendant scan warning on unreadable/unwritable descendants with
prune-paths, 10s timeout, and a 50-entry cap.
- `RunIsolationProbe` + embedded runas_probe.sh (//go:embed) — the
leak-audit probe codex asked for: spawns a fixed shell script as the
target user and parses its output into a structured IsolationReport.
Reports effective id/groups/umask/pwd, target-home config presence,
cross-user denial attempts for every other configured run_as_user,
and supervisor-denial attempts. Never reads file contents, only
existence and access.
All of these are testable without a real second user via an injectable
`SudoRunner` interface; production uses ExecSudoRunner.
### Failure policy
Per-spec: all checks are hard go/no-go, not warnings.
- Any preflight fatal in any project: cc-connect refuses to start
entirely; no half-spawned processes.
- Any audit-level `CROSS_LEAKED` (target reads another project user's
secrets) or `SUPERVISOR_LEAKED` (target reads the supervisor's
secrets) or `WORKDIR_WRITABLE=no`: cc-connect refuses to start.
- Descendant-scan findings are warnings (inaccessibility inside a
project tree is sometimes intentional).
### Startup wiring (cmd/cc-connect/)
- `runRunAsUserStartupChecks` runs preflight + isolation probe for
every project with run_as_user set, in parallel, BEFORE any agent
or engine is constructed. Logs every outcome via slog. On any fatal,
prints the full list and `os.Exit(1)`.
- injects project-level run_as_user / run_as_env into the agent's
options map before calling `core.CreateAgent`, so agents can opt in
without new top-level plumbing.
### Doctor subcommand (cmd/cc-connect/doctor_runas.go)
New `cc-connect doctor user-isolation [--project NAME] [--out PATH]`
subcommand. Runs preflight + isolation probe for one or all
`run_as_user` projects, writes a JSON report per project, and prints a
human-readable summary. Default output is
`~/.cc-connect/audits/<timestamp>-<project>.json`.
Also supports `--print-script` to dump the embedded probe source for
inspection (and for running shellcheck against it).
### Claude Code wiring (agent/claudecode/)
- `Agent` struct gains `runAsUser` and `runAsEnv` fields
- `New()` parses them from the opts map and skips the
`exec.LookPath("claude")` check when run_as_user is set (the target
user's PATH is what matters, not the supervisor's)
- `newClaudeSession` takes `core.SpawnOptions`, runs the per-spawn
cheap re-check immediately before building the command, uses
`core.BuildSpawnCommand` instead of `exec.CommandContext`, and
filters env via `core.FilterEnvForSpawn`
### Docs
- new "Running agents as a different Unix user" section in
`docs/usage.md` with setup steps, sudoers example, ACL example,
environment-propagation migration checklist (PGSSL*, ~/.claude/
settings.json, ~/.pgpass, ~/keys/, ~/.ssh/, MCP binaries, shell
profile, language toolchains), opt-out instructions, and
failure-mode debug guide
- README section with a short configuration example and a pointer to
the full docs
- `config.example.toml` commented example with setup recipe
### Tests
- config: parse run_as_user/run_as_env, reject root, reject invalid
chars, Windows rejection, username validator valid/invalid cases
- core: BuildSpawnCommand legacy vs. sudo form, FilterEnvForSpawn
filtering, VerifyRunAsUserCheap success/no-sudo/escalation-possible/
empty-user, PreflightRunAsUser all-pass/no-sudo/escalation-fatal/
workdir-fatal/descendant-warnings/descendant-cap, probe parser
clean/cross-leak-fatal/supervisor-leak-fatal/workdir-not-writable-
fatal, shell quoting, other-users filtering, embedded probe script
sanity
- agent/claudecode: New() parses run_as_user/run_as_env, run_as_user
skips LookPath
- cmd/cc-connect: default audit dir suffix, printHumanReport doesn't
panic on populated report
Uses stubbed SudoRunners throughout — no tests require a real second
Unix user or real sudo binary.
## Scope
Only Claude Code is wired up in this PR. Each non-claudecode agent has
its own session/spawn lifecycle (codex re-spawns per turn, cursor has
its own state model, etc.) so the sweep needs per-agent reasoning and
is deferred to a follow-up. The core primitives land here so each
future agent migration is a small, self-contained PR.
Follow-up issue tracks: codex, cursor, gemini, iflow, opencode, qoder,
acp, pi.
Refs: #496
Credit: issue framing via user + codex adversarial review
(go/no-go gates, leak-audit probe, env propagation docs,
sudo -iu rationale pinning)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(run_as_user): simplify after code review
Addresses findings from a parallel code-reuse / code-quality / efficiency
review of #498. Behavior unchanged; tests still pass.
Highlights:
- cache VerifyRunAsUserCheap results for 30s keyed by runAsUser, so
rapid-fire messages from the same Slack user don't pay the ~100ms
per-spawn sudo tax. Cache evicts on failure so the next spawn
re-verifies fresh. Adds ResetVerifyCache for tests + operators.
- doctor user-isolation now fans out across projects in parallel
(mirroring runas_startup.go) instead of iterating serially. Per-project
output is buffered and printed in order so the report stays grouped.
- collapse Agent.runAsUser + Agent.runAsEnv into a single
core.SpawnOptions field — eliminates the reassemble-at-call-site
pattern.
- fold core/runas_username.go into core/runas.go (one 17-line file →
one helper near its only caller).
- IsolationReport.RawOutput is now only populated when the audit had a
fatal — clean reports are smaller on disk.
- introduce typed string constants for the probe wire format
(tagBegin / tagWorkDirReadable / tagCrossLeaked / ...) so the parser
and runas_probe.sh share a discoverable contract.
- itoa() in tests → strconv.Itoa
- contains() helper in tests → slices.Contains
- mass comment trim across runas.go / runas_check.go / runas_audit.go:
delete WHAT-restates-name comments (~70 lines) while keeping the
load-bearing WHY blocks (sudo -n -iu rationale, env propagation
reasoning, find expression format note, scan timeout justification).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(runas): drop PATH from default env allowlist
Preserving PATH across the sudo boundary defeats the whole point of
-i: it overrides the target user's login-profile PATH with the
supervisor's, and leaks supervisor work directories (e.g. the
cc-connect source tree on the supervisor's PATH) into the isolated
session.
sudo -i now rebuilds PATH from /etc/profile + the target user's
~/.profile. Binaries the isolated agent needs (like the claude CLI)
should be installed system-wide (e.g. /usr/local/bin symlinks) or put
on the target user's own shell profile.
Caught by Q6/Q8 daemon-spawn verification in data-worklog Phase 0a:
partseeker-coder sessions were running with leigh's PATH intact.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(engine): propagate run_as_user to per-workspace agents
In multi-workspace mode, getOrCreateWorkspaceAgent constructs a fresh
opts map for each per-workspace agent instead of inheriting the
project-level opts that main.go decorated at startup. As a result,
run_as_user (and run_as_env) silently dropped out of the per-workspace
spawn path — the project-level config said partseeker-coder but every
spawn actually ran as the supervisor.
Fix: expose GetRunAsUser/GetRunAsEnv accessors on *claudecode.Agent
and copy them across in getOrCreateWorkspaceAgent via interface
assertion, mirroring the existing GetModel/GetMode pattern. Two
regression tests cover the propagation and the no-propagation case
so multi-workspace deployments that don't use run_as_user are
unaffected.
Caught by Q6/Q8 daemon-spawn verification in data-worklog Phase 0a.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add OAuth credential sharing guidance for run_as_user setups
OAuth access tokens expire after a few hours and only get refreshed by
active Claude CLI sessions. When the target user has no active session
between cc-connect spawns, its token expires and auth fails with 401.
Document the symlink + ACL pattern: target user's credentials.json
symlinks to the supervisor's copy so both share one auto-refreshed token.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: graceful session shutdown — close stdin before signals
The old Close() called context.Cancel() which sends SIGKILL immediately
via Go's exec.CommandContext, giving Claude Code zero time to run Stop
hooks. This broke claude-mem session summaries (120s timeout) and any
other cleanup that depends on a clean exit.
New three-phase shutdown:
1. Close stdin (EOF) — Claude Code exits cleanly, running Stop hooks
2. SIGTERM after 30s grace period if still alive
3. SIGKILL after 5s more as last resort
Also increases engine-level close timeout from 10s to 45s to accommodate
the longer graceful window.
Tested: all agent/claudecode and core tests pass. Verified Claude Code
exits cleanly on stdin EOF (exit code 0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: 120s graceful close + hold message during session transition
Increases the default graceful stop timeout from 30s to 120s to match
claude-mem's Stop hook timeout. The wait ends as soon as the process
exits — typical shutdowns take seconds, not the full 120s.
When a user messages during an idle-reset that triggers a graceful
close, they now see "Wrapping up your previous session (usually a few
seconds, up to 2 minutes). Your new session will start automatically."
instead of a silent delay. The hold message is sent before the blocking
close, and the new session starts immediately when the old one finishes.
Engine-level close timeout raised to 130s (120s grace + 5s SIGTERM +
5s buffer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Leigh Stillard <leigh@stillard.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(observe): add --observe CLI flag and engine config
Add observe fields to the Engine struct and a SetObserveConfig setter
to enable terminal session observation in a later task. Define --observe
and --observe-channel CLI flags (wiring deferred).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): add ObserverTarget interface, implement for Slack
Define ObserverTarget optional interface in core/ for platforms that can
receive terminal observation messages. Implement SendObservation on Slack
platform using PostMessageContext with link unfurl disabled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): implement JSONL session file watcher and parser
Adds sessionObserver to core/observer.go which tails ~/.claude/projects/{projectKey}/*.jsonl
files, parses user/assistant events, filters out sdk-cli sessions (cc-connect's own),
and forwards new messages to ObserverTarget. Includes full test coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): wire observer into engine startup and shutdown
Add findObserverTarget to locate the first ObserverTarget platform,
startObserver to launch the JSONL watcher goroutine when configured,
wired into Engine.Start() and cancelled in Engine.Stop().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): wire --observe and --observe-channel flags in CLI
Removes the TODO suppressor and connects the --observe/--observe-channel
CLI flags to engine.SetObserveConfig(), resolving the Claude Code project
directory from the configured work_dir via the new resolveClaudeProjectDir helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): add config.toml support for observe settings
Add ObserveConfig struct to ProjectConfig so users can configure terminal
session observation via [projects.observe] in config.toml, with CLI flags
(--observe, --observe-channel) taking precedence over config file values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(observe): rune-safe truncation, robust offset tracking, interface assertion
- Use file size for offset tracking instead of line-length calculation
to avoid drift from \r\n line endings or partial final lines
- Truncate messages at valid rune boundaries to prevent garbled UTF-8
- Add compile-time ObserverTarget interface assertion for Slack
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Leigh Stillard <leigh@stillard.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(core): add single-instance guard to prevent duplicate processes
When two cc-connect instances run with the same config, both poll
the same connections and process messages, causing duplicate replies.
Changes:
- Add instance_lock.go with flock-based exclusive locking (Unix)
- Add instance_lock_windows.go stub for Windows compatibility
- Lock file is placed in config directory as .<config>.lock
- Add --force flag to kill existing instance before starting
- Write PID to lock file for better error messages
Fixes#439
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(lint): handle errcheck errors in instance_lock.go
Explicitly ignore return values from Truncate, Seek, Flock, Remove,
and WriteFile calls where the errors are non-critical (best-effort
cleanup and diagnostics). Fixes CI lint failure on PR #440.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(references): normalize and render local agent references
* feat(show): add reference-aware file and directory viewing
* docs: document references config and show usage
* chore: remove local working docs from PR branch
* fix: address lint issues in reference pipeline
* fix: drop stale quiet leftovers after main rebase
* fix: clean remaining rebase markers in show tests
- PR #445: auto-compress skips when agent has no CompressCommand;
--max-context-tokens is now configurable via agent options instead
of being hardcoded to 900000
- PR #448: codex app-server RPC timeout cleans pending map entry;
emit() uses non-blocking send to prevent readLoop stall;
readLoop uses scanner with 10MB max line size
- PR #462: restore backward-compatible quiet config — maps to
display.thinking_messages/tool_messages when [display] not set
- PR #479: Gemini STT sends API key via x-goog-api-key header
instead of URL query param; model name is URL-escaped
- PR #489: feishu fetchQuotedMessage uses caller context instead
of context.Background; extractPostPlainText includes code_block
- PR #447/#474: SendToSessionWithAttachments uses workspace-prefix
stripping fallback matching cron/heartbeat logic
- PR #446: PATH prepend for custom commands limited to Windows only
All changes are backward-compatible; new behavior gated by config.
Tests added for quiet config, workspace prefix, Gemini header, and
feishu code_block extraction.
Made-with: Cursor
Add Google Gemini as a speech-to-text provider, complementing the
existing OpenAI Whisper, Groq, and Qwen options. Gemini's multimodal
API accepts audio as inline base64 data and returns transcribed text.
This is useful for users who already have a Google AI Studio API key
(free tier available) and prefer not to set up separate STT service
accounts.
Changes:
- core/speech.go: GeminiSTT struct implementing SpeechToText interface
via Gemini's generateContent endpoint with query-param auth
- config/config.go: [speech.gemini] config section (api_key, model)
- cmd/cc-connect/main.go: "gemini" case in provider switch
- core/speech_test.go: 6 unit tests covering success, language param,
API errors, empty response, and invalid JSON
Config example:
[speech]
enabled = true
provider = "gemini"
[speech.gemini]
api_key = "AIza..."
model = "gemini-flash-latest"
Co-authored-by: Cigarrr <cigarrr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(cron): add global session_mode config for cron jobs
Allow setting a default session_mode ("reuse" or "new_per_run") for all
cron jobs via [cron] config, so users don't need to set it per-job.
Per-job session_mode still overrides the global default.
* test(cron): add unit tests for global session_mode default
---------
Co-authored-by: ma_k <ma_k@ctrip.com>
- Enhanced formatTOML to also strip trailing whitespace per line,
collapse consecutive blank lines, and trim leading blank lines
- writeRawConfig now applies formatTOML before writing, so every
config save path produces consistently formatted output
- Added `cc-connect config` subcommand group:
- `config format` (alias: `fmt`) — format the config file in place
- `config example` — print annotated config example
- `config path` — print resolved config file path
- Old `config-example` subcommand kept for backwards compatibility
- Added FormatConfigFile() with TOML validation before writing
- Added tests for formatTOML and FormatConfigFile
Made-with: Cursor
Replace the npm-based install/upgrade workflow with compile-time
embedding. The web/dist/ directory is now embedded directly into the
binary using go:embed and registered through core's plugin registry
pattern (web/embed.go → core.RegisterWebAssets).
- Add web/embed.go (go:build !no_web) and web/embed_stub.go
- Add core/web_assets.go as the embedded FS registry
- Add cmd/cc-connect/plugin_web.go to import the web package
- Rewrite management.go static serving to use io/fs instead of disk
- Simplify /web command to setup + status (remove install/upgrade/uninstall)
- Clean up web_manager.go (remove npm logic, keep GenerateToken)
- Update Makefile with web build target and NO_WEB/build-noweb support
- Remove .npmignore and npm publishing fields from package.json
Binary size impact: ~1MB (16M → 17M). Use `no_web` tag to exclude.
Made-with: Cursor
Merge github/main into feature/add_web_admin_v2.
- Go backend files auto-merged cleanly
- All web/ conflicts resolved by keeping feature branch version
(feature branch has the latest, most complete web admin)
- Remove Logs.tsx brought in from main (no backend endpoint)
- Fix go.sum checksum for BurntSushi/toml
Made-with: Cursor
- Add /web install|upgrade|status|uninstall slash command (privileged)
- Auto-install cc-connect-web via npm into ~/.cc-connect/web/
- Auto-configure [bridge] and [management] in config.toml with random tokens
- Management server serves SPA static files from dist/ with fallback to index.html
- Add config.EnableWebAdmin() for atomic bridge+management setup
- Add core/web_manager.go with npm registry integration
- Prepare web/package.json for npm publish as cc-connect-web (files: dist/)
- Add i18n translations for all /web messages (en, zh, zh-TW, ja, es)
Made-with: Cursor
Lowercase fmt.Errorf prefixes in daemon/update CLI paths and collapse
systemd manager error to a single line without trailing punctuation.
</think>
mcp_GitKraken_git_push
- Add GET/PATCH /api/v1/settings endpoint for global config
- Add GlobalSettings component with editable sections: general (language,
quiet, attachment_send, idle_timeout), display (thinking/tool max len),
stream preview, rate limit, and log level
- Integrate global settings into System page with collapsible raw config
- Add "View All" link to Recent Sessions on Dashboard
- Add i18n translations for all global settings (en, zh, zh-TW, ja, es)
Made-with: Cursor