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.
* 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>
* feat(plugins): implement Host data API write RPCs (CreateTask/UpdateTask/CreateComment)
The api_write manifest capability was a stored no-op. Wire the deferred
phase-2 write RPCs (ADR 0043) so closed-loop plugins (Sentry->task,
Slack slash-command task creation, refactor fan-out) can create and
update tasks and post comments.
- SDK: Host.Tasks() gains Create/Update; new Host.Comments() writer;
CreateTaskInput/UpdateTaskInput/Comment DTOs (pkg/pluginsdk).
- Gating: api_write:tasks gates Create/UpdateTask, api_write:comments
gates CreateComment; undeclared -> gRPC PermissionDenied, per-method
and independent of the read caps on the same resource.
- Writes route through the first-party service layer (task service for
tasks, office service for comments) via backendapp adapters, never a
repository -- so task.*/comment-created events fire and WS updates.
Server stamps source="plugin:<id>"; CreateTask resolves workspace/
workflow defaults and best-effort honors start_agent; UpdateTask uses
a conservative title/description/state/workflow_step_id mask.
- Fixture plugins exercise a CreateTask+CreateComment round-trip;
in-process, real-broker wire, and subprocess tests cover the round-trip
and per-capability denial.
- Docs: spec, GRPC-CONTRACT, HOST-DATA-API.proto, proto comments moved
writes deferred -> implemented; ADR 0043 amended with the concrete
service-layer routing decision.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(plugins): send a message to a task session via Host SendMessage (replaces CreateComment)
The office TaskComment write was the wrong primitive — it's an office/
dashboard construct, not a way to message the agent working a task, and
it only did anything under office reactivity. Replace the CreateComment
write RPC with SendMessage, which delivers a prompt to a task session
through the orchestrator's real delivery path (the same one message_task
uses).
- Proto: swap CreateComment/Comment for SendMessage(SendMessageRequest)
returns SendMessageResponse{session_id, status}; regenerated stubs.
- SDK: Host.Comments() removed; Host.Messages().Send(taskID, sessionID,
text) added (pairs with Messages().List), returning MessageDispatch.
- Gating: api_write:comments dropped; SendMessage gated api_write:messages
(independent of api_read:messages on the same accessor, like Tasks()).
- Delivery: a new pluginsTaskMessengerAdapter (backendapp) resolves the
target session (explicit id verified against the task, or the primary
session), records a user message stamped source="plugin:<id>", then
dispatches by state — running → queue; idle/completed → prompt (resume
if the agent process is gone); never-started → launch; failed dispatch
deletes the recorded message. Wired via SetWriteDeps (live-read).
- Tests: adapter state-machine unit tests (queue/start/prompt/resume/
delete-on-failure/belongs-to-task/no-primary/terminal); SDK, in-process
gating, wire, and subprocess round-trips updated to SendMessage.
- Docs: spec, GRPC-CONTRACT, HOST-DATA-API.proto, proto comments, and the
ADR 0043 amendment updated from comment-create to message-send.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(plugins): address PR review — nil guards, detached contexts, state validation
Follow-up fixes from the #1970 review (Claude + CodeRabbit bots):
- grpcHostServer.CreateTask/UpdateTask/SendMessage: nil-guard the returned
value before toProto() so a custom/test Host returning (nil, nil) yields a
gRPC Internal error, not a server panic (matches GetTask's pattern).
- host.go: fix stale server doc comment (Comments()/CreateComment →
Messages()/SendMessage).
- startTaskBestEffort and the post-resume wait+retry now run on
context.WithoutCancel(ctx): the task/resume is already committed, so a
plugin's incoming deadline must not silently abort the launch/resume.
- pluginsTaskWriterAdapter.UpdateTask validates the state string against the
known task-state enum (excluding orchestrator-owned SCHEDULING) and returns
InvalidArgument, so a plugin typo can't forward a bogus state to the service.
- Tests: add the resume-wait-timeout path (message deleted so a retry can't
duplicate), and a dedicated pluginsTaskWriterAdapter test covering
source→metadata mapping and state validation (both adapters now depend on
narrow interfaces for testability).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(plugins): launch start_agent asynchronously so CreateTask doesn't block
Review nit follow-up: startTaskBestEffort called StartTask synchronously, so
with the detached context the plugin's CreateTask RPC blocked on the
orchestrator's full inline launch path (office lookup, state update, workflow
step move, task fetch) with no bound — and the "asynchronous" comment was wrong.
Move the fire-and-forget into pluginsTaskStarterAdapter (backendapp, no goleak),
mirroring MCP's launchAutoStartTask: the launch runs in a goroutine on a
context.WithTimeout(context.WithoutCancel(ctx), AgentLaunchTimeout), and
StartTask returns immediately. The plugin host stays a simple synchronous call
(its taskStarter contract now documents "returns promptly, launches async").
Adds adapters_plugin_starter_test.go: launch invoked, returns before the launch
completes, detaches from a cancelled caller context, and swallows launch errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(plugins): document write validation + SendMessage rollback; harden starter test cleanup
Addresses the final PR #1970 review nits:
- adapters_plugin_starter_test.go: register the block-channel release via
t.Cleanup (sync.Once-guarded) before the goroutine can block, per AGENTS.md.
- GRPC-CONTRACT.md: document CreateTask/UpdateTask validation + error rules
(title required, id required, NotFound on missing task, invalid-state reject).
- spec.md: document that a failed SendMessage dispatch deletes the recorded
message, plus the required-field/NotFound/terminal-session rules.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(automations): reliable cron scheduling and webhook-driven push/CI triggers
Automations were "wounded": cron schedules mishandled timezones and some
never fired, and the GitHub push/CI trigger types were coming-soon stubs.
Cron (fix):
- The scheduler reduced every cron expression to a fixed interval derived
only from a */N step in the minute/hour field, so any pinned schedule
(0 9 * * *, 30 14 * * *, weekday ranges, day-of-month) never fired, and
the configured timezone was parsed but never used.
- Replace the interval approximation with robfig/cron/v3: shouldFire now
computes the true next fire time in the configured timezone (DST-aware,
defaulting to UTC for host-independence), anchored to last-evaluated or
creation time. Dead interval-parsing helpers and shorthand constants
removed.
GitHub push + CI triggers (feat):
- Implement push and CI-failure triggers end to end via the existing
per-workspace GitHub App webhook plumbing (#1810): the HMAC-verified
webhook service now parses push and completed check_run events, resolves
the installation to its workspaces, and publishes normalized bus events.
- A new automation subscriber matches enabled github_push / github_ci
triggers by workspace + repo + branch/conclusion/check-name filters and
fires them (deduped by commit SHA / check-run id). The old poll-based
stub evaluators are removed; the PR poll path is unchanged.
- Subscribe the App manifest to push/check_run and enable both trigger
types in the registry (CI defaults to the failure conclusion).
Docs:
- Update docs/public/feature-status.md and automation-and-mcp.md so the
cron and push/CI rows reflect shipped reality.
Tests: regression tests pin the previously-never-firing cron shapes,
timezone/DST behavior, and host-independence; backend tests cover the
webhook parse->publish and subscriber match/fire/dedup flows for both
push and CI-failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(automations): populate push.message and ci.url placeholders in fired trigger data
Address PR review blockers: the default push and CI automation prompts
reference {{push.message}} and {{ci.url}}, which the interpolator maps to
data["message"] / data["html_url"] — but the webhook subscriber never
populated those fields, so both always rendered empty.
- Parse head_commit.message (push) and check_run.html_url (CI) in the
GitHub webhook service and carry them on the published bus payloads.
- Include "message" / "html_url" in the fired trigger data so the
placeholders resolve; extend the webhook and subscriber tests to assert
both flow through end to end.
Non-blocking review cleanups:
- Replace the misleading reuse of matchesAuthors for CI conclusions/check
names with a clearly-named matchesFilterValue helper.
- Drop dead context.Background() line and its unused import from the cron
regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(automations): scope webhook triggers to active installs and per-branch dedup
Address CodeRabbit review on the push/CI webhook triggers:
- Security: resolveWorkspaceIDs now only returns connections whose status is
active, so a suspended or revoked GitHub App installation can no longer fire
push/CI automations.
- Correctness: include the branch in the push dedup key
(push:owner/name@branch@sha) so the same commit pushed to two matching
branches fires once per branch instead of being suppressed.
- Correctness: add an optional head-branch filter to github_ci triggers and
gate CI matching on it, matching the push trigger's branch-scoping.
- Stability: make the webhook subscriber's Start retryable — a subscription
failure rolls back partial subscriptions and leaves the subscriber
un-started so a later Start re-attempts, instead of permanently disabling
push/CI processing.
- Tests: cover multi-connection workspace resolution (dedup + inactive
filtering), per-branch push dedup, and CI branch filtering.
- Docs: note the CI branch filter and per-branch push dedup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(automations): cover webhook subscriber Start retry-after-failure path
Add TestGitHubWebhookSubscriberStartRetryableAfterFailure with a minimal
flaky bus whose second Subscribe fails, pinning the retry contract: a
subscription failure rolls back the partial subscription and leaves the
subscriber un-started, and a later Start re-attempts and succeeds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(plugins): add auth capability for plugin-provided OIDC/SAML login
Plugins can now authenticate a visitor against an external IdP (OIDC/SAML)
and contribute a login button to the pre-auth screen:
- new `auth` manifest capability + a capability-gated X-Kandev-Auth-Login
webhook-response directive; the host mints the kandev_session (ADR 0050),
the plugin never receives the raw token. Plugin-set Set-Cookie is dropped.
- auth.Service.AuthenticateExternal: existing-identity / link-by-email / JIT
member provisioning; refuses to auto-link to an admin; verified-email is
the plugin's contract.
- auth_providers manifest block + plugins.SSOProviders() populate the
anonymous boot payload (auth.ssoProviders); the login page renders a
"Continue with <provider>" button per provider.
- ADR 0050 + plugin-manifest / plugin-authoring / auth spec docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: exclude publish_review_findings_kandev from public-docs coverage
The native code-review agent (#1957) added the publish_review_findings_kandev
MCP tool but did not update docs/public/coverage.json; that PR did not touch
docs/public so the path-filtered validator never ran. It's an internal review
reporting tool, not a user-facing feature, so record it as an exclusion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(plugins,auth): address review on SSO directive and JIT provisioning
- takeAuthLoginDirective strips every case variant of X-Kandev-Auth-Login and
rejects (403) when a plugin sends more than one, so no variant leaks to the
browser and login never uses a nondeterministically-picked assertion.
- provisionExternalUser rolls back the just-created user when the identity
insert fails (transient error or a lost UNIQUE(provider,subject) race) and
resolves to the winning identity, so no account is left without a login and
no email is reserved by an unusable row. Adds userstore.DeleteUser.
- auth spec: mark the login-page SSO buttons + provider discovery as landed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: drop redundant coverage exclusion; main now covers publish_review_findings_kandev
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>