159 Commits
Author SHA1 Message Date
Claude 9613fc85c4 fix(opencode): add -- separator before prompt to prevent --file from consuming it
OpenCode CLI's --file flag is a yargs [array] type that greedily
consumes subsequent arguments as file paths. Without a -- separator,
the prompt text following --file was parsed as a filename, causing
"File not found" errors when images were attached.

Reported-by: community user via image analysis
Made-with: Cursor
2026-04-28 22:07:48 +08:00
cg33andClaude a7164eec1e feat(codex): support custom CLI binary via cli_path option (#545) (#679)
Add cliBin and cliExtraArgs fields to the codex Agent, parsed from the
new cli_path option (e.g. cli_path = "omx" or cli_path = "omx --flag").
The binary is used for both LookPath validation and subprocess execution,
allowing users to run codex-compatible CLIs like omx through cc-connect.

Closes #545

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-27 14:54:24 +08:00
bingkxuandvibecoder e81cc1d568 fix(opencode): prevent empty response when agent process crashes (#720)
When the opencode process crashes (e.g. stale session ID causes
'Session not found'), readLoop sends a fallback EventResult before
the deferred cmd.Wait() can detect and emit an EventError. The
engine receives EventResult(Done:true) first, returns from the
event loop, and the subsequent EventError is never consumed.
The user sees an empty response with no error information.

Fix by checking stderrBuf immediately after the scanner loop exits,
before sending the fallback EventResult. If stderr contains errors
(including 'Session not found'), emit EventError instead.
cmd.Wait() remains in a defer for resource cleanup only.

Engine uses a pattern table (agentErrorHandlers) to match known
errors and show localized user-friendly messages with recovery hints.

Co-authored-by: vibecoder <16832299+vibecoder@user.noreply.gitee.com>
2026-04-27 14:30:30 +08:00
7164c949ab fix(claudecode): encode spaces and "~" in project keys (#500) (#706)
Claude Code CLI writes project dirs under ~/.claude/projects/ by
collapsing spaces and "~" to "-" (alongside / : _ and non-ASCII), but
encodeClaudeProjectKey only handled / : _ and non-ASCII. So for work
dirs under macOS iCloud Drive (.../Mobile Documents/com~apple~CloudDocs/...)
the encoded key never matched the on-disk directory — primary lookup,
legacy candidates and the fallback scan all missed, findProjectDir
returned "", and /list always reported "(0) 未找到此项目的会话".

Extend the replacement set to also collapse space and "~" to "-".
Add test cases for paths with spaces, with "~", and the combined
iCloud-style path, plus a findProjectDir regression test.

Co-authored-by: silverzhouyinhe <zhousilver85@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 14:29:47 +08:00
xxb d068ea44a2 fix(acp): preserve options for workspace agents (#704) 2026-04-27 14:28:28 +08:00
cg33andClaude 59a08dd512 fix(kimi): remove CLI dependency from unit tests (#647) (#677)
Construct Agent structs directly instead of calling New() for tests
that only exercise setters, permission modes, providers, memory,
and models. Only TestAgentNew and TestAgentStartSession still
require the kimi CLI on PATH (guarded by skipUnlessKimiAvailable).

Closes #647

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-24 06:48:10 +08:00
cg33andClaude Opus 4.6 1c25dd42ee fix(gemini): pass prompt via stdin to preserve newlines (#695)
* docs: add Homebrew as install option (brew install cc-connect)

Made-with: Cursor

* fix(gemini): pass prompt via stdin to preserve newlines (#687)

The Gemini CLI truncates multi-line prompts passed via the -p flag,
dropping everything after the first newline. Switch to passing the
prompt through stdin using `-p -`, matching the pattern used by the
Codex agent. This preserves newlines in messages from platforms like
Telegram where users frequently send multi-line text.

Closes #687

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-24 06:46:09 +08:00
soaringk d8573c81f6 fix(opencode): pass inbound images as files (#717) 2026-04-24 06:33:54 +08:00
Yu Zhangand张彧 ccb0e366a8 feat(claudecode): add opus[1m] model option and use shorthand descs (#660)
- Add opus[1m] to the default model fallback list (Claude Opus 1M context)
- Drop version numbers from Sonnet/Opus/Haiku descs so they auto-follow
  upstream version bumps (4 -> 4.6/4.7 -> future) without code churn
- Skip sonnet[1m] (needs extra usage) and haiku[1m] (subscription has no
  permission) since the CLI accepts the syntax but usage is gated

Co-authored-by: 张彧 <aaron@mac.tail449498.ts.net>
2026-04-24 06:31:31 +08:00
1b8a0843ef feat(agent): add Devin CLI as a first-class agent (#672)
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>
2026-04-21 21:19:30 +08:00
Claude 20268b2b17 fix(codex): use configured codex_home for session listing/history/delete
ListSessions, GetSessionHistory, DeleteSession, and patchSessionSource
all ignored the Agent.codexHome config field and only checked the
CODEX_HOME environment variable. When codex_home was set in config.toml,
StartSession correctly wrote sessions to the custom directory, but the
listing functions searched the default ~/.codex/sessions/ — making all
sessions invisible.

- Add resolveCodexHomeDir helper (explicit config > env > default)
- Thread codexHome through listCodexSessions, findSessionFile,
  getSessionHistory, and patchSessionSource
- Extract CODEX_HOME from session extraEnv for post-exit patching

Made-with: Cursor
2026-04-20 21:49:37 +08:00
Claude d02a5f1370 feat: provider switch fixes, agent type switching, integration tests & i18n
- 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
2026-04-19 00:10:29 +08:00
Claude f8d2cad68f refactor: restructure provider presets with per-agent-type configuration
- 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
2026-04-18 15:24:32 +08:00
Claude 9e118e631f fix: review fixes for pre-release stability (H1-H3, M1-M7)
H1: Kimi readLoop — move cmd.Wait() before EventResult to prevent
    out-of-order EventError after turn completion.
H2: Kimi tests — add skipUnlessKimiAvailable guard so CI without
    kimi binary skips gracefully instead of failing.
H3: Claude shellJoinArgs — add comprehensive unit tests for shell
    quoting (spaces, single quotes, backslashes, newlines).
M1: cli_path — document that paths with spaces are not supported.
M3: needsDoneReaction — add streaming preview tests covering
    UpdateMessage trigger, discard reset, and disabled state.
M5: /agentsid — add security warning about session ID sensitivity
    in the command response.
M7: i18n — localize reply_footer_remaining for ZH/ZH-TW/JA/ES;
    add MsgAgentSIDWarning translations for all 5 languages.
Made-with: Cursor
2026-04-16 15:13:33 +08:00
cold2600438-lgtm 006718a753 feat(agent): add Kimi CLI support (#616)
- Add agent/kimi/ implementation using kimi --print --output-format stream-json
- Parse assistant text, think blocks, tool_calls and tool results into core.Event
- Support session resume via --resume, list/delete via ~/.kimi/sessions/
- Implement optional interfaces: ModeSwitcher, ModelSwitcher, ProviderSwitcher,
  MemoryFileProvider, SkillProvider, SessionDeleter, AgentDoctorInfo
- Register plugin with //go:build !no_kimi tag
- Update Makefile ALL_AGENTS and config.example.toml
2026-04-16 14:47:29 +08:00
RukawaKaedeandClaude Opus 4.6 249b5acbff feat(claudecode): support Claude-compatible CLI wrappers via cli_path (#640)
Adds `cli_path` and `cli_args_flag` config options so cc-connect can
spawn third-party Claude-compatible CLIs (and wrappers that need Claude
flags bundled through a passthrough flag) instead of hardcoding `claude`.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 14:46:29 +08:00
Jerry fdd7173b78 fix(skills): align static discovery with agent layouts (#633) 2026-04-16 14:45:14 +08:00
Jerry 84c784769f fix(claudecode): honor host-managed provider env (#638) 2026-04-16 14:45:00 +08:00
Jerry d4addfb13f fix(codex): preserve workspace app-server options (#612)
* fix(codex): preserve workspace app-server options

* fix(core): tighten workspace agent option snapshots
2026-04-16 14:44:52 +08:00
xxb e119d54b5d feat(codex): add runtime reply footer aligned with Codex CLI (#578)
* 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
2026-04-16 14:44:11 +08:00
SentixxxandClaude Opus 4.6 fe900318c7 fix(claudecode): unblock readLoop when child subprocesses hold stdout pipe (#585)
* test(claudecode): cover readLoop interruption regressions

- TestReadLoop_ChildHoldsStdoutPipe: a descendant holds the agent's
  stdout fd, so scanner.Scan() blocks forever and readLoop's defer
  never runs. Asserts the agent's last buffered event is delivered.

- TestReadLoop_CtxCancelClosesChannels: ctx cancelled while stderrBuf
  is non-empty exercises the defer's <-ctx.Done() branch that used to
  return without closing events/done.

Both hang on the current code and pass after the fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(claudecode): unblock readLoop on agent exit and ctx cancel

When a tool subprocess inherits the agent's stdout fd, the pipe stays
open after the agent dies and scanner.Scan() blocks forever. readLoop's
defer never runs, cs.done never closes, and Close() blocks until the
130s timeout in closeAgentSessionWithTimeout fires.

Restructure readLoop:
  - Reaper goroutine owns cmd.Wait() and signals waitDone on exit.
  - Stdout-closer goroutine force-closes the read end on either
    <-ctx.Done() or <-waitDone, unblocking scanner in both paths.
  - Defer reads waitErrCh instead of calling cmd.Wait() itself, and
    unconditionally closes events/done (the previous <-ctx.Done()
    branch returned early and stranded the engine event loop).
  - Scanner error suppresses the expected ErrClosed from the
    reaper-triggered close via a channel check.

Also splits readLoop into named helpers per AGENTS.md style guide.

Close() now returns in microseconds in all paths, shrinking the
cleanupInteractiveState window from up to 130 s to negligible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(claudecode): align readLoop with os/exec stdout pipe semantics

* Revert "fix(claudecode): align readLoop with os/exec stdout pipe semantics"

This reverts commit 9008b57f9d16b1dd9400310d8a2c93f9c3165d9a.

* fix(claudecode): add grace period for startReadLoopWait to avoid cutoff json in stdout.close

* “fix(claudecode): fix minor issues by agent reviewer

* style(claudecode): fmt case with space

* fix(claudecode): avoid waiting for 50ms in done branch

* style(claudecode): improve comments

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:40:47 +08:00
cg33 17ab58c61f Merge pull request #589 from leighstillard/feat/claudecode-reasoning-effort
feat(claudecode): support /effort command for reasoning effort
2026-04-16 07:22:15 +08:00
Claude 7980a70a30 chore: fix golangci-lint errors flagged by CI
- management.go: check w.Write return value
- config_cmd.go: check fs.Parse return value
- web_manager.go: remove unused defaultMgmtPort/defaultBridgePort consts
- opencode.go: remove unused discoverModels wrapper
- engine.go: remove ineffectual resolvedWorkspace assignment

Made-with: Cursor
2026-04-15 20:49:13 +08:00
Claude 5f27718a6b fix(core): prevent race condition during session cleanup
Two race conditions fixed:

1. cleanupInteractiveState timing (main issue):
   - Previously: state was deleted from interactiveStates map BEFORE
     closeAgentSessionWithTimeout was called synchronously
   - Problem: during the blocking close (up to 130s for Stop hooks),
     the map was empty but agent session was still shutting down
   - Result: concurrent requests saw conflicting state - some got
     "No execution in progress" while others saw blocking behavior
   - Fix: delete state from map AFTER closeAgentSessionWithTimeout
     completes, with re-check to avoid deleting replaced states

2. readLoop defer leak (secondary issue):
   - Previously: defer had early return on ctx.Done() that skipped
     closing cs.events and cs.done channels
   - Problem: if Close() timed out and called cancel(), the defer
     returned early without closing channels
   - Result: Close() waits forever on cs.done at line 633
   - Fix: remove early return from defer, always close channels

Root cause identified in issue #595:
- Session cleanup during /stop in cleanup window created conflicting
  replies ("Previous request still processing" vs "No execution in progress")
- MCP servers and Stop hook subprocesses with stdio:inherit could
  block scanner.Scan() indefinitely, preventing defer cleanup

Test plan:
- go build ./... passes
- go vet ./... passes
- go test ./... passes

Fixes #595

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 08:11:28 +00:00
Leigh StillardandClaude Opus 4.6 afec8cbcd1 feat(claudecode): implement ReasoningEffortSwitcher for /effort command
The Claude Code agent now supports the /reasoning and /effort commands
that were previously only available for Codex. This passes --effort to
the Claude CLI when starting sessions.

Available levels: low, medium, high, max
Configurable via reasoning_effort in [projects.agent.options].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:50:01 +00:00
Claude d21cee5eb7 fix: pre-release fixes for high/medium priority issues from PR review
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
2026-04-12 20:40:02 +08:00
cg33andClaude 780a81af42 fix(core): filter /list and /switch to only show cc-connect owned sessions (#569)
External Claude CLI sessions in the same work_dir were appearing in /list,
/switch, /delete, and /search results because ListSessions() scans all
.jsonl files in the project directory without filtering.

- Add KnownAgentSessionIDs() to SessionManager to collect tracked agent IDs
- Add filterOwnedSessions() to exclude external CLI sessions from all
  session listing operations (9 call sites in engine.go)
- Remove dead --continue code path from claudecode/session.go (engine no
  longer passes ContinueSession sentinel)
- Add regression tests for the filtering logic

Closes #541

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 06:19:26 +08:00
cg33andClaude 782e4a1d78 fix(codex): pass base URL via -c flag for provider switching (#568)
Codex CLI 0.118.0 deprecated the OPENAI_BASE_URL env var. When users
switch providers via /provider, the new base URL was set as an env var
but ignored by the CLI. Now pass it as `-c openai_base_url=<url>` on
each subprocess invocation so the correct endpoint is used immediately.

Closes #554

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 06:19:23 +08:00
4cf879b2b9 feat: add run_as_user for per-project OS-user isolation (#560)
* 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>
2026-04-11 19:45:08 +08:00
HanandClaude Opus 4.6 c41e3f4ba3 fix(acp): improve permission prompts and suppress input echo (#497)
* fix(acp): suppress user_message_chunk to avoid echoing user input

ACP sends user_message_chunk during session/load history replay, which
was mapped to EventText and sent back to the user as an echo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(acp): extract tool rawInput for meaningful permission prompts

ACP toolCall includes a rawInput field with actual tool parameters
(e.g. bash command, file path), but handlePermissionRequest only used
the title, resulting in empty code blocks in permission prompts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(acp): workaround opencode #7370 for empty permission tool input

OpenCode sends rawInput={} in request_permission, with the actual tool
parameters only appearing in a subsequent tool_call_update event. Cache
rawInput from both tool_call and tool_call_update by toolCallId, and
defer permission event emission to allow the cache to populate.

Also improves summarizeACPToolInput: handles execute kind, extracts
command+description as a commented format, and falls back to indented JSON.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 18:55:48 +08:00
cg33andClaude Opus 4.6 42803f66d1 fix(claudecode): handle non-ASCII characters in work directory paths (#467)
* fix(claudecode): handle non-ASCII characters in work directory paths

Add encodeClaudeProjectKey function that properly encodes paths with
non-ASCII characters (Chinese, Japanese, emoji, etc.) as hyphens,
matching Claude Code's project key encoding algorithm.

Previously, findProjectDir only handled ASCII path characters, causing
/list command to fail when work_dir contained non-ASCII characters.

Changes:
- Add encodeClaudeProjectKey function for non-ASCII path encoding
- Update findProjectDir to use the new encoding as primary candidate
- Add comprehensive tests for encoding and path matching
- Maintain backward compatibility with legacy encoding candidates

Closes #466

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(lint): use strings.EqualFold instead of ToLower comparison (SA6005)

Fixes staticcheck SA6005 in findProjectDir case-insensitive path match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-09 16:50:57 +08:00
Leigh StillardandClaude Opus 4.6 eadc52a096 fix: graceful session close — stdin EOF before signals (#509) (#510)
* 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: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 14:31:01 +08:00
cg33 1c0c6ce5b4 Merge pull request #506 from q107580018/opencode-model-cache
feat(opencode): significantly speed up /model switching
2026-04-09 14:30:37 +08:00
Mr.QiuW 9fc64898ce refactor(opencode): remove unreachable cache path check 2026-04-09 13:07:02 +08:00
Mr.QiuW adeff4ef8d fix(opencode): handle signature writer errors 2026-04-09 01:18:10 +08:00
Mr.QiuW 1641c42269 feat(opencode): significantly speed up model switching 2026-04-09 01:11:19 +08:00
Claude d1d93f60b0 fix: address high/medium priority issues from PR review
- 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
2026-04-08 23:11:39 +08:00
4c6eaf1266 fix: cron process leak (#348), codex /compact (#378), auto-compress notify, context limit (#384) (#445)
- fix(#348): call cleanupInteractiveState after new_per_run cron completes
  to prevent orphaned Claude CLI processes accumulating over time

- fix(#378): codex CompressCommand() returns "" since /compact is not
  reliably executed in exec/resume mode (may be treated as plain text)

- feat: notify user via platform message before auto-compress triggers,
  including token estimate (~Xk tokens) for transparency

- fix(#384): pass --max-context-tokens 900000 to claude CLI, raising the
  effective context limit from ~254K to 900K tokens

Co-authored-by: 菇菇 <gugu@gugudeMac-mini.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 22:27:12 +08:00
IkariKr 9aea457b27 feat(codex): add optional app-server stdio backend (#448) 2026-04-08 22:26:58 +08:00
q107580018andClaude Opus 4.6 bcb3b292c1 feat(opencode): implement SessionDeleter to enable /delete command (#452)
Add DeleteSession to the opencode agent by calling `opencode session delete <id>`
via the CLI, enabling single, batch, and range deletion from any connected platform.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 22:26:53 +08:00
emodesireandjuncun_meng 6698ad4950 fix: prevent qoder session hang when no result event is received (#465)
When qodercli outputs plain text instead of stream-json (observed on
Windows when tool calls are involved), readLoop silently discards all
non-JSON lines and never emits an EventResult. The engine then blocks
forever on the events channel waiting for a result that will never
arrive, causing the conversation to hang permanently.

This commit adds fallback logic after the subprocess exits:
- Track whether a "result" event was received during the read loop
- Accumulate non-JSON lines instead of silently discarding them
- If the process exits without a result event, emit a fallback:
  - Non-JSON text output → forwarded as EventResult (user sees response)
  - Process error with no output → EventError with stderr message
  - Scanner error → EventError
  - Clean exit with no output → empty EventResult

This ensures the engine event loop always receives a terminal event,
preventing hangs regardless of qodercli's output format.

🤖 Generated with [Qoder][https://qoder.com]

Co-authored-by: juncun_meng <juncun_meng@kingdee.com>
2026-04-08 22:26:38 +08:00
Claude 6cd7faeb9f fix: address high/medium priority issues from merged PR review
- fix(relay): process current event before timeout check, close session
  on channel drain (#405)
- fix(claudecode): prevent reader goroutine leak in usage probe, add
  terminal memory bounds (#380)
- fix(cron): validate mode/session_mode on UpdateJob, only restore live
  mode when override succeeds (#367)
- fix(core): simplify idle rotation recovery — remove inconsistent
  fallback to old session (#400)
- fix(feishu): use cache-disabled client for tenant token refresh (#401)
- fix(setup): revert --set-allow-from-empty default to false to avoid
  unintended CLI behavior change (#376)
- feat(telegram): gate message reactions behind enable_reactions config
  flag (#398)

Made-with: Cursor
2026-04-03 06:06:19 +08:00
Junyi Du 1f84997b40 feat(claudecode): add PTY-based usage support (#380) 2026-04-02 09:33:19 +08:00
Claude 2da132b6c8 fix(cursor): revert sandbox flag, document shell restriction in default mode
The --sandbox flag requires kernel v6.2+ and is unavailable on most
headless servers. The actual issue users hit ("only ls allowed") is
the Shell tool command allowlist in --trust (default) mode, which is
resolved by switching to mode = "force" (--force).

Reverts the sandbox config option; updates config.example.toml to
clearly document that default mode restricts shell to safe commands.

Made-with: Cursor
2026-04-01 10:47:32 +08:00
Claude 6152854fa0 feat(cursor): add sandbox option to disable restricted tool mode
Cursor Agent CLI defaults to sandbox mode in headless environments,
limiting available tools to read-only operations (ls, etc.). Add a
configurable `sandbox` option ("enabled"/"disabled"/"") that maps to
the `--sandbox` CLI flag, allowing users to unlock full tool access
(shell, edit, write) when running via cc-connect.

Config example:
  [projects.agent.options]
  sandbox = "disabled"

Made-with: Cursor
2026-04-01 09:59:22 +08:00
Claudeandq107580018 fd9e347563 feat(opencode): dynamic model discovery via opencode models (#381)
AvailableModels now runs `<cmd> models` first and falls back to
provider-configured models and the built-in list only when the CLI
command fails or produces no output. Results are deduplicated and
sorted for stable /model output.

Co-authored-by: q107580018 <107580018@qq.com>
Made-with: Cursor
2026-03-31 19:44:42 +08:00
Claude 8378eb7cee fix(gemini): save attachments to workspace so CLI tools can access them
Images and files were saved to /tmp which is outside Gemini CLI's
workspace sandbox, causing "file not found" when the agent tried to
read them. Now saves to workDir/.cc-connect/attachments/ (consistent
with the claudecode adapter) and provides explicit paths in the prompt.

Made-with: Cursor
2026-03-30 17:48:05 +08:00
Claude 5f81ec49c2 merge: resolve conflicts with main branch
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
2026-03-30 11:54:58 +08:00
Claude f479f6e370 test(codex): fix flaky stdin capture wait in multiline Send test
waitForFileContent returned on first successful open of the file; the
fake codex script uses `cat > file`, which can expose an empty file
briefly. Poll until contents equal the expected prompt so -cover and
CI do not race.
2026-03-28 21:06:58 +08:00
Claude aaabb324fa test(acp): skip Cursor ACP handshake when CI is set
GitHub Actions sets CI; the integration test needs a local agent and
login and can hit its 90s timeout under load or without credentials.
Align with agent/cursor model tests that skip real CLI in CI.
2026-03-28 10:07:34 +08:00