PR #1810 retired the host-global gh token detection that injected GITHUB_TOKEN
into the request env, leaving both remote executors with a `_ = req` placeholder
to silence the unused parameter. resolveGHToken is now just a filter over the
selected credential IDs, so the parameter is gone from the Sprites and SSH
flavors alike. Test asserts the pass-through selection value, not just length.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(mcp): keep spawner attribution on a spawned session's first turn
A session started through spawn_session_kandev never learned who spawned
it, so it had no way to report back. The MCP handler did prepend a
<kandev-system> attribution block, but the first-turn canonicalizer
(sysprompt.canonicalizeKandevContext) drops every system block it cannot
attribute to server state — and the spawn block was not on the trusted
list, so it was deleted before the agent ever saw it. Spawns are always a
first turn, so this happened every time.
Generate the block at the launch site instead, from server-resolved
identity, and whitelist that exact content for the injector — the same
pattern entity references already use:
- config/prompts/spawned-session.md holds the attribution template.
- sysprompt.SpawnedSessionContext builds it; sysprompt.StripTags (lifted
out of the handlers' stripSystemTag) keeps embedded IDs from closing
the block early.
- LaunchSessionRequest.SpawnOrigin carries the spawner identity and is
kept off the wire (json:"-") so a WS client cannot forge a trusted
system block.
- startTask prepends the block and passes the identical content as
trusted content to both the task and Office injectors. Its trailing
env/route parameters move into a startTaskOptions struct rather than
growing the positional list again.
- The MCP handler now only fills SpawnOrigin (including the spawner's
session name) and passes the prompt through untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(mcp): derive spawn attribution from the verified spawner session
Addresses both PR review findings.
CodeRabbit: spawnOrigin copied sender_task_id / sender_session_id straight
out of the request, so an unknown or cross-task sender_session_id became
trusted launch attribution — the spawned agent would be told to report to
a session that never asked for it. Resolve the spawner session once, up
front, require it to belong to the claimed sender task, and derive the
origin (and the inherited agent profile) from that row. An unverifiable
claim now yields no attribution instead of a forged one. This also drops
a duplicate session lookup.
Codex: passthrough profiles skip the Kandev MCP wrap, so the new
attribution never reached a spawned passthrough session even though it
gets MCP configured and can reply. Give that path plain-text attribution
(no <kandev-system> markup, which would show up raw in the TTY the user
is watching). The wrap branch moves into applyLaunchPromptContext, which
keeps startTask under the nesting limit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per-session events are published on a suffixed bus subject
("shell.output.<sessionId>") but stamped with the bare event-type constant
("shell.output"). The plugin deliverer subscribed with the manifest pattern and
then re-checked that pattern against event.Type, which made every suffixed
subject undeliverable: a 3-segment pattern matched the bus subscription but
failed the re-check on segment count, while a 2-segment pattern passed the
re-check but never subscribed to the 3-segment subject.
Carry the concrete publish subject on bus.Event (stamped in place by Publish in
both the memory and NATS implementations; the NATS receive side prefers the
delivering msg.Subject) and re-check the manifest pattern against it via a new
Event.EffectiveSubject helper that falls back to Type. The concrete subject is
also what plugins receive as pluginsdk.Event.EventType -- already documented in
the proto as "bus subject" -- so a plugin subscribed to "shell.output.*" can
read the session id off it.
Audited all 109 EventBus.Publish call sites: 17 subject families were affected;
office.run.event_appended, the office tree-hold subjects and the cross-plugin
plugin.<id>.<name> already stamped Type with the full subject. Three git.ws
sites in the orchestrator publish on git.ws.<sessionId> while stamping
events.GitEvent, so the subject there is not a suffixed form of its own type at
all -- left as-is, since the gateway routes off the subject and never reads
event.Type. Unsuffixed subjects publish with subject == type, so existing
2-segment subscriptions are unaffected, and the re-check still enforces manifest
segment-count strictness over the bus's looser NATS ">" semantics.
wsStatus, wsListInstances and wsTest discarded the ParsePayload error and
carried on with a zero-valued SecretID, so a malformed frame surfaced as a
confusing downstream failure (or a lookup against an empty secret ID) instead
of a clear bad-request. Match the sibling handlers in the same file and return
ws.NewError(..., ErrorCodeBadRequest, "invalid payload", nil).
Absent payloads keep working: Message.ParsePayload treats a nil payload as a
no-op, so only genuinely malformed JSON now errors.
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
All four templates were confirmed against the shipped CLI, not inferred
from the agent name:
- pi-acp -> {home}/.pi (pi-acp@0.0.32 package source)
- qoder-acp -> {home}/.qoder (qodercli 1.1.7, --config-dir proof)
- kiro-acp -> {home}/.kiro (Kiro CLI 2.15.0, ACP session/new)
- trae-acp -> {home}/.cache/trae-cli (Trae CLI 0.120.48, ACP session/new)
Kiro and Trae deliberately break the "{home}/.<agent>" shape: Kiro keeps
its login token in ~/.local/share/kiro-cli/data.sqlite3 while sessions
live in ~/.kiro, and Trae keeps config in ~/.trae but session state in
the cache dir with auth in the OS keyring. Each Runtime() records the
evidence and the residual gaps.
Extends the shared ACP agent spec table with sessionDirTemplate so every
agent's session root is pinned, including the ones already set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GetBlocking unconditionally returned an empty slice behind a TODO, so any
caller asking "which tasks does this task block?" silently got a wrong
answer. The reverse-lookup query the TODO asked for already exists on the
blocker repository as ListTasksBlockedBy (used by the office scheduler and
the handoff context builder) and is already part of the BlockerRepository
interface, so GetBlocking now delegates to it.
Like GetBlockers, it reports the raw blocker edges without filtering
archived tasks — every other ListTasksBlockedBy consumer does the same.
ListTasksBlockedBy also gains a deterministic ORDER BY (mirroring
ListTaskBlockers) and normalizes an empty result to a non-nil slice.
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`apps/web/lib/state/store.ts.backup` was a 1202-line snapshot of the
pre-slice-refactor monolithic Zustand store, accidentally committed in
b090a0157 (#1049). It never compiled and never ran — `tsconfig.json`
includes `**/*.ts`, which does not match a `.backup` suffix — so its only
effect was polluting grep/search results for anyone working on the real
store at `apps/web/lib/state/store.ts`.
No runtime code referenced it: `grep -rn "store.ts.backup" apps/` returns
nothing, and no build, lint, or tsconfig glob picks up `*.backup`.
Verified no logic was dropped. Of the 177 state keys and actions in the
backup, 172 exist in the live slice tree under `lib/state/slices/`. The
five that don't belong to two concepts that were deliberately renamed
after the snapshot was taken:
- `boardId` / `setBoards` / `setActiveBoard` — boards became workflows in
658bd349b (#228); the live kanban slice uses `workflowId` /
`setWorkflows` / `setActiveWorkflow`.
- `environmentsLoaded` / `setEnvironments` — environments became
executors; the live settings slice has `ExecutorsState` /
`setExecutors`.
Also add `*.backup`, `*.bak`, and `*.orig` to `.gitignore` so editor and
merge scratch copies cannot be committed again.
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
http.Get uses http.DefaultClient, which has no timeout and carries no
context. A backend that accepted the TCP connection but never responded
blocked waitForHealth forever, hanging the launcher with no way to
cancel it -- exactly what the suppressed noctx lint was warning about.
Probe through a package-level client with an explicit 2s timeout using
http.NewRequestWithContext, thread a context down from launcher.Run, and
drain each response body before closing so the connection is reused
across poll iterations. The overall wait now derives a deadline from the
caller's context, so cancellation ends it too.
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(backend): bound tool installer downloads with timeouts
The GitHub release and tarball install strategies downloaded archives with
http.DefaultClient, which has no timeout: a slow or stalled mirror left an
install hanging indefinitely with no way out.
Both strategies now share a package-level client whose transport bounds each
phase that can stall silently (connect, TLS handshake, response headers), and
whose response bodies are wrapped in a stall guard that aborts the transfer
when it stops making progress. There is deliberately no overall
http.Client.Timeout — these are tarballs, and one deadline for the whole
transfer would kill a large-but-healthy download on a slow link.
Requests keep using http.NewRequestWithContext, so an install remains
cancellable; the guard derives its own cancellable context from the caller's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(backend): bound only blocked reads in the download stall guard
The guard armed its timer at construction and rearmed it after any read that
returned bytes, so the window also covered whatever the caller did with those
bytes downstream. Expanding one read's worth of a highly compressed archive
onto a slow disk can outlast the window without the mirror having gone quiet
at all, which would surface a false "download stalled" error on a perfectly
healthy install.
Arm the timer around the blocking read alone and stop it as soon as the read
returns, so the bound is on how long a read blocks rather than on wall-clock
time spent downstream.
Reported by Codex review on #2016.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(web): streamline mobile review file headers
* fix(web): toggle mobile review file menu
* test(web): select mobile review header in grouping test
* fix(terminal): expose executor-profile env vars to terminal shells
PR #1971 got the profile env into the agent subprocess and the repository
setup script, but the terminal panel was still a gap: the local user shell
was started with `InteractiveStartRequest.Env` unset, so it inherited only
the backend process environment, and agentctl's per-terminal shells were
started from `shell.DefaultConfig` with no env overrides at all.
Local/worktree path: resolve the task environment's executor profile at
terminal start (revealing secret-backed entries) and pass it through
UserShellOptions.Env into the deferred PTY start. Resolution happens per
terminal rather than being cached at launch, so a terminal opened before
the agent ever runs still gets the vars.
Remote path (Docker/Sprites/SSH): StartTerminalShell now folds the agent
instance's env into the shell config, so a terminal inside the workspace
sees what the agent sees. SSH is unchanged in scope — its instance env is
still only the credential allowlist, not arbitrary profile variables.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(terminal): resolve terminal env from the session's executor profile
Address PR review. The resolver read executor_profile_id off the
task_environments row, but persistTaskEnvironment's reuse branch never
refreshes that column — it keeps whatever the first session stamped on it.
buildLaunchAgentRequest resolves the agent's env from
session.ExecutorProfileID instead, so a task environment reused by a later
session that picked a different profile of the same executor type would give
the terminal stale secrets and omit the ones the agent just received.
Prefer the session's profile and fall back to the environment row only when
the session recorded none. Also document why mergeAgentEnvIntoShellConfig
needs no defensive copy and why StartEnvForTesting is exported.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(terminal): give env-row fallback subtests distinct session IDs
Address PR review nit: both subtests shared the hardcoded "session-3" at
the call site, so the error-path case read as a repeat of the first.
Move the session ID into the table as session-no-profile / session-error.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>