Commit Graph
339 Commits
Author SHA1 Message Date
Claude b7304a2da9 feat: global provider management + card-based IM provider add
- 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
2026-04-17 22:08:40 +08:00
Claude c660aff4e8 fix(core): always trigger done_emoji when agent finishes
Previously done_emoji only fired when stream preview had been updated
in-place at least once, which meant quick responses never triggered
the reaction. Remove the needsDoneReaction() gate so the platform's
AddDoneReaction is always called on EventResult.

Made-with: Cursor
2026-04-17 10:12:48 +08:00
Claude 00e6f7600e refactor(core): merge /agentsid into /status, add work_dir display
Remove the standalone /agentsid command and fold its output (Agent SID)
into /status. Also add work_dir to the status output so users can see
the current working directory at a glance.

Changes:
- Remove /agentsid command registration, handler, card button, help
  text, and all i18n entries (MsgAgentSIDWarning, MsgBuiltinCmdAgentSID)
- Add Agent SID line to /status (both plain-text and card variants),
  shown only when a session is active
- Add Work Dir line to /status output (i18n: 5 languages)
- Add MsgStatusAgentSID i18n key

Made-with: Cursor
2026-04-17 09:53:57 +08:00
Claude 3ebdbe9a6b fix(core): preserve old sessions when switching via /switch
Previously, /switch overwrote the active session's AgentSessionID,
causing the old agent session to disappear from KnownAgentSessionIDs
and thus from /list output.

Add SwitchToAgentSession() which finds an existing internal session
for the target agent session ID or creates a new one, preserving all
previous session references.

Made-with: Cursor
2026-04-17 08:14:27 +08:00
9db01586db fix(core): freeze preview card between text segments in quiet mode (#642)
When quiet mode is enabled (ThinkingMessages=false, ToolMessages=false),
EventThinking and EventToolUse events are suppressed but the stream
preview card is not frozen. This causes multiple Claude text outputs
to accumulate into a single preview card.

Fix: when thinking/tool events arrive in quiet mode and there is
accumulated text, freeze + detachPreview to preserve the current text
as a permanent message. When the preview is already degraded (after
the first freeze), send accumulated text directly as a regular message
instead of silently advancing segmentStart past it.

Co-authored-by: leoliang <leoliang@futunn.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:17:48 +08:00
Boyu ZhuandClaude Opus 4.6 e87de5fa1a fix(telegram): render markdown tables in monospace <pre> block (#641)
Tables were rendered as proportional-width plain text, causing columns
to be misaligned in Telegram clients. Wrap tables in <pre> with
per-column padding so they display correctly in monospace.

Related: openclaw/openclaw#36323

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:17:13 +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
ahahahaandClaude Opus 4.6 66efe3256f feat: add agent-sid command to query agent session ID (#605)
* 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>
2026-04-16 14:48:56 +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
HanandClaude Opus 4.6 3d641eb316 feat(feishu): add done_emoji reaction after streaming preview updates (#580)
When the agent's response is delivered via in-place card update (Feishu
streaming preview), the user gets no push notification. This adds a
configurable done_emoji option (e.g. "Done") that adds a reaction after
the "OnIt" reaction is removed, so the user knows the agent finished.

Only activates in quiet mode when the preview card was updated in-place
at least once (via UpdateMessage). Non-quiet mode is unaffected as
intermediate messages already trigger push notifications. Single-round
responses that only use SendPreviewStart are also unaffected, as the
initial send already triggers a push notification.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 14:44:23 +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
q107580018 b8d75f73af fix(feishu): improve delete and /model card refresh flows (#645) 2026-04-16 14:43:53 +08:00
Zeren Chen 41e7470ef5 (#597)
* fix(telegram): omit skill menu commands on overflow

* fix(telegram): clamp bot command descriptions
2026-04-16 14:41:24 +08:00
Claude bd9dc515f4 fix: address high/medium priority issues from PR review (batch 3)
H1: Prevent workspace path traversal via ../
    Add resolveLocalDirPath() that validates resolved path stays under
    baseDir, expands ~/... via os.UserHomeDir, rejects escaping paths.

M2: Sanitize sender_name in inject_sender header
    Replace quotes with single quotes, strip newlines/CR to prevent
    prompt injection via display names.

M3: Warn on unset env var placeholders in config
    resolveEnvPlaceholders now logs slog.Warn when ${VAR} references
    an environment variable that is not set.

M4: Validate WeCom api_base_url
    Reject non-http(s) or malformed URLs at New() to prevent credential
    exfiltration via misconfigured base URL.

M5: Quote systemd Environment= values
    Wrap values in double quotes to handle spaces, #, and special chars
    in proxy URLs and paths.

M7: Discord table detection now requires separator row
    Only wrap pipe-delimited blocks that contain a markdown separator
    row (| --- | --- |), reducing false positives on decorative pipes.
Made-with: Cursor
2026-04-16 09:21:18 +08:00
cg33 9178b77fef Merge pull request #610 from g1f9/feat/workspace-init-local-dir
feat: support local directory paths in /workspace init
2026-04-16 07:45:36 +08:00
cg33 1c87f5cb6c Merge pull request #596 from Zx55/fix/skill-discovery-follow-dir-symlinks
fix(core): follow symlinked skill directories
2026-04-16 07:17:17 +08:00
cg33 cfef710750 Merge pull request #586 from unimountltd/feat/sender-name-injection
feat(core): include sender display name in inject_sender header
2026-04-16 07:12:24 +08:00
cg33 ef160781ed Merge pull request #584 from Yuri0314/master
fix(tts): don't send empty language_type to Qwen TTS API
2026-04-16 07:06:08 +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 0d92d808a3 fix: address high/medium priority issues from PR review
H1: Prevent double-Close on concurrent cleanupInteractiveState calls
    by nil'ing state.agentSession under the lock before closing.

H2: Weixin StartTyping sends typingStatusStop on ctx.Done() and on
    repeat-failure exit, preventing stuck "typing" indicator.

M3: Feishu @mention resolution: downgrade PII log to Debug, HTML-escape
    display names in at tags, skip ambiguous (duplicate) names.

M4: Narrow segmentStart==0 condition in EventResult to only apply when
    tool messages are hidden (!e.display.ToolMessages).

M5: Log warning when cron workspace agent creation fails instead of
    silently falling back to global agent.

M7: Discord file download capped at 50 MiB via io.LimitReader.

M8: Weixin getConfig now checks ret/errcode fields and returns error
    on non-zero values.
Made-with: Cursor
2026-04-15 20:41:32 +08:00
Cigarrr 51e13e65e9 fix(core): lenient bound_at parsing in workspace bindings
The BoundAt field in WorkspaceBinding uses time.Time which requires
strict RFC3339 format. When workspace_bindings.json is hand-edited
(e.g. by an AI agent), a missing timezone suffix causes json.Unmarshal
to fail for the entire file, breaking ALL workspace bindings.

Introduce FlexTime, a time.Time wrapper with lenient UnmarshalJSON
that tries multiple time formats and falls back to zero value on
failure instead of returning an error. This ensures a non-critical
metadata field cannot take down the workspace routing.
2026-04-15 17:01:43 +08:00
cg33 59176eabdb Merge pull request #566 from xxb/feat/bridge-capabilities-control-plane-v1
feat(bridge): publish command capabilities snapshot to bridge adapters
2026-04-15 09:35:17 +08:00
cg33 2c46faf4bc Merge pull request #556 from Cigarrr/fix/cron-prompt-workspace-resolution
fix(core): resolve workspace for cron prompt-mode jobs in multi-workspace mode
2026-04-15 09:27:49 +08:00
cg33 cf4c73eb0e Merge pull request #550 from Cigarrr/fix/quiet-preview-overwrite
fix(core): use full accumulated text for stream preview finalization when tool messages hidden
2026-04-15 09:23:00 +08:00
cg33 c091c63922 Merge pull request #543 from leoliang1997/fix/cron-list-by-project
fix(core): /cron list shows all project cron jobs instead of session-…
2026-04-15 09:18:34 +08:00
cg33 a003056826 Merge pull request #520 from xxb/pr/bridge-progress-cards
feat(bridge): preserve progress rendering hints in reply contexts
2026-04-14 22:26:57 +08:00
chendaxin.tkandClaude Opus 4.6 0553192ad6 feat: support local directory paths in /workspace init
Previously `/workspace init` only accepted git URLs. Users with existing
local directories had no way to bind them to a channel without cloning.

Now `/workspace init <path>` accepts:
- Absolute paths: `/workspace init /data/code/my-project`
- Relative names: `/workspace init my-project` (resolved under base_dir)
- Home-relative: `/workspace init ~/code/my-project`

The conversational init flow also accepts directory paths — users can
send a directory path when prompted instead of only git URLs.

Adds looksLikeLocalDir() helper, MsgWsInitDirNotFound and
MsgWsInitInvalidTarget i18n keys with translations for all 5 languages,
and 4 new test cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:02:38 +08:00
sidney061212-aiandClaude Opus 4.6 7eb6f34b02 fix(core): persist agent_session_id using CurrentSessionID()
event.SessionID can be empty in some cases, causing the
agent_session_id to never be written to disk. When cc-connect
restarts, the session resumes without any agent_session_id and
Claude Code treats it as a brand new conversation, losing all
context.

Fix: use state.agentSession.CurrentSessionID() (interactive mode)
and agentSession.CurrentSessionID() (relay mode) instead, which
always returns the actual session ID held by the agent. Also call
sessions.Save() after SetAgentSessionID() to ensure the change
is flushed to disk.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 02:00:46 +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
chenzeren fbf015a4bd fix(core): follow symlinked skill directories 2026-04-13 11:36:27 +08:00
Claude b7fbc78dd1 feat(weixin): implement SendAudio for TTS voice messages
Weixin platform now implements core.AudioSender interface, enabling TTS
audio output for WeChat personal chat via ilink API.

- Added ConvertAudioToAMR in core/speech.go for audio conversion
- Implemented SendAudio in platform/weixin/media_outbound.go
- Converts WAV/MP3/TTS output to AMR format (Weixin voice codec)
- Uploads via CDN and sends as voice message type

Fixes #587: TTS audio was generated but silently dropped because
weixin did not implement AudioSender interface.

Test plan:
- go build ./... passes
- go test ./... passes
- Unit tests for SendAudio edge cases (empty audio, invalid context)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 22:17:04 +00:00
Jim TangandClaude Opus 4.6 108ab64ccf feat(core): include sender display name in inject_sender header
When inject_sender is enabled, the agent now receives the sender's
human-readable display name alongside the opaque user ID. This lets
agents distinguish between users by name in shared channel sessions
(e.g. Slack channels where multiple people talk to the same bot).

Header format changes from:
  [cc-connect sender_id=U123 platform=slack chat_id=C789]
to:
  [cc-connect sender_id=U123 sender_name="Jim Tang" platform=slack chat_id=C789]

The name is quoted to handle spaces. When no display name is available
(e.g. platform didn't resolve it), the header falls back to the
existing format without sender_name.

Changes:
- Add userName param to buildSenderPrompt and thread it through all 3
  call sites (direct send, queued message drain, event-loop drain)
- Add userName field to queuedMessage so queued messages preserve the
  display name across turns
- Add tests for empty name fallback and names with spaces

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:50:00 +01:00
claude-code 1e022f5cba fix(tts): don't send empty language_type to Qwen TTS API
The Qwen TTS (DashScope) API rejects empty string values for the
language_type parameter with a 400 InvalidParameter error. The field
is optional — when omitted, the API auto-detects the language.

Previously, QwenTTS.Synthesize always included language_type in the
request body, even when opts.LanguageType was empty (the default from
engine.go's sendTTSReply). This caused all TTS synthesis to fail on
Feishu platform with:

  qwen tts API 400: InvalidParameter: Input should be 'Auto',
  'Chinese', 'English', ... : input.language_type

Fix: only include language_type in the request when it is explicitly
set (non-empty). When empty, the field is omitted entirely, letting
the API auto-detect the language from the input text.

This aligns the implementation with the TTSSynthesisOpts comment:
  LanguageType string // e.g. "Chinese", "English"; empty = auto-detect
2026-04-13 00:19:00 +08: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 4bb14d47d0 fix(core): queue messages during session startup instead of dropping (#565) (#567)
When a session lock is held and a new message arrives, the queue
mechanism requires an interactiveState entry to exist. Previously,
this entry was only created inside the async goroutine after the
agent process was spawned — a potentially slow operation. Messages
arriving during this startup window were silently dropped.

Fix:
- Create a placeholder interactiveState before launching the async
  processor so the queue entry point exists immediately
- Relax queueMessageForBusySession guard to allow queueing when
  agentSession is nil (startup phase), while still rejecting when
  the agent session was established but has died

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 06:19:20 +08:00
af18bf8b31 fix(core): fix flaky shell test on macOS due to symlink path resolution (#561)
normalizeWorkspacePath resolves /var/folders/ to /private/var/folders/
on macOS, but shell pwd returns the original path. The test assertion now
checks both the original and resolved paths.

Co-authored-by: happytonakai <hanzr.nju@outlook.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 19:45:11 +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
8dd5868d72 feat(observe): mirror terminal Claude Code sessions to a chat channel (#559)
* 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>
2026-04-11 19:45:05 +08:00
VPS c896120e68 feat(bridge): publish command capabilities snapshot to bridge adapters 2026-04-11 10:18:31 +00:00
CigarrrandClaude Opus 4.6 39ad8d2571 fix(core): resolve workspace for cron prompt-mode jobs in multi-workspace mode
In multi-workspace mode, ExecuteCronJob() for prompt (non-shell) jobs
always used the global e.agent and passed an empty workspaceDir to
processInteractiveMessageWith(). This caused the Claude Code agent to
start with the wrong working directory (often ~/.cc-connect) instead
of the workspace bound to the cron job's channel.

The fix mirrors handleMessage()'s workspace resolution logic:
1. Extract channelID from the cron job's sessionKey
2. Resolve the workspace binding via resolveWorkspace()
3. Get a workspace-specific agent via workspaceContext()
4. Pass the resolved workspace agent and dir to processInteractiveMessageWith()

job.WorkDir, when explicitly set, takes highest priority as an override.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:17:40 +08:00
CigarrrandClaude Opus 4.6 30f289aaa5 fix(core): use full accumulated text for stream preview finalization when tool messages hidden
When tool messages are hidden (quiet mode / display.tool_messages=false),
segmentStart stays 0 and the stream preview accumulates all text across
tool boundaries. However, EventResult used event.Content (last segment
only) to finalize the preview, overwriting the accumulated content.

- Prefer strings.Join(textParts, "") over event.Content when segmentStart
  is 0, ensuring the final card contains the complete response
- Insert a visual line break between text segments at tool boundaries
  when tool messages are hidden, so segments don't run together

Closes #549

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 07:41:39 +08:00
VPS 17e166116f test(core): synchronize bridge hint capture 2026-04-10 17:59:34 +00:00
VPS 0e1d425e9e merge: resolve conflicts with upstream/main 2026-04-10 17:02:36 +00:00
leoliangandClaude Opus 4.6 6a7f97f36e fix(core): /cron list shows all project cron jobs instead of session-scoped
When thread_isolation is enabled, each group message gets a unique
session key (feishu:chat:root:{messageID}). This made /cron list
return empty results in group chat because the current message's
session key never matched any cron job's session key.

Fix: use ListByProject instead of ListBySessionKey so /cron list
shows all cron jobs for the current project regardless of which
session the command is sent from.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:54:51 +08:00
xxb d5ce331ea7 fix(bridge): reconstruct relay reply context (#514) 2026-04-09 18:56:11 +08:00
ConverSun bb37af823a fix: persist multi-workspace /dir overrides without cross-channel leakage (#505)
* fix: persist per-workspace dir overrides across agent recreation

In multi-workspace mode, /dir overrides were lost after daemon restart
or workspace pool eviction because getOrCreateWorkspaceAgent always
recreated agents with work_dir=workspace root, ignoring the override
stored in project state.

Changes:
- Add WorkspaceDirOverrides map to ProjectStateStore for per-workspace
  dir override persistence
- Apply persisted override when recreating workspace agents in
  getOrCreateWorkspaceAgent
- Store workspace-specific overrides in dirApply when in multi-workspace
  mode (set and reset)
- Auto-clear invalid overrides (missing/non-directory paths)

Backward compatible: existing work_dir_override field preserved for
single-workspace mode.

* fix: use per-channel dir overrides instead of per-workspace

Two channels bound to the same workspace were sharing /dir overrides.
Change the override key from workspace path to interactiveKey
(workspace:sessionKey) so each channel maintains independent dir state.

- Remove override from getOrCreateWorkspaceAgent (shared agent layer)
- Add applyChannelDirOverride helper, called in handleMessage and
  commandContext (per-channel routing layer)
- dirApply now stores/clears overrides keyed by interactiveKey

* refactor: centralize multi-workspace context resolution

Use a shared workspaceContext helper so commandContext and message
routing build the same workspace agent/session/interactiveKey state in
one place. This keeps the per-channel dir override fix unchanged while
removing duplicate override application logic.

* fix: route multi-workspace dir overrides by effective work dir

Avoid mutating shared workspace agents when applying per-channel /dir
overrides. Resolve the effective work dir from project state and route
each channel to an agent context keyed by that dir instead.

This removes cross-channel work_dir races while preserving existing
/dir persistence and reset behavior.
2026-04-09 18:55:30 +08:00
Jerry bfb6d69780 Feature: normalize local references for IM rendering and add /show for direct file inspection (#495)
* 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
2026-04-09 16:50:25 +08:00
Claude f70b98f6d5 fix(core): restore /quiet command removed by PR #462
The /quiet slash command was a frequently used user-facing feature
that was accidentally removed when thinking/tool visibility was
split into separate config items. Restore it as a toggle that
flips both ThinkingMessages and ToolMessages together, persisting
the change via displaySaveFunc. Also restores /quiet in the help
card listing.

Made-with: Cursor
2026-04-09 15:21:53 +08:00
VPS 3d103d480c feat(bridge): add progress rendering hints to reply ctx 2026-04-09 06:32:02 +00:00