Tags: angr1it/optio
Tags
Feature/merge with main (#15) * feat: add GitLab support via git platform abstraction layer Introduce a GitPlatform interface (strategy pattern) with GitHub and GitLab implementations, replacing ~25 hardcoded GitHub API fetch calls across 10+ service files. This enables Optio to orchestrate tasks against GitLab repos with full MR watching, CI status, review, auto-merge, and issue sync support. Key changes: - GitPlatform interface with 15 methods (PR/MR, CI, review, issue, repo) - GitHubPlatform + GitLabPlatform implementations - parseRepoUrl()/parsePrUrl() for platform-aware URL parsing - git-token-service with getGitPlatformForRepo() entry point - DB migration adding git_platform column to repos table - GitLabTicketProvider for issue sync - Envoy sidecar proxy support for GitLab API - repo-init.sh credential setup for GitLab (GITLAB_TOKEN, glab CLI) - Setup API endpoints for GitLab token validation and project listing - UI label generalization ("GitHub Issues" → "Issues") Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: support for gitlab * fix: support nested {{#if}} blocks in prompt template renderer The single-pass regex broke when GitLab support introduced nested conditionals. Process innermost blocks first, iterating until all nesting is resolved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: gitlab support * fix: extract actual review summary from agent output instead of hardcoded message The review summary UI showed "Agent completed successfully" instead of the agent's actual findings. Root causes: - ClaudeCodeAdapter.parseResult() hardcoded summary text, ignoring the result event - agent-event-parser truncated tool_result to 300 chars, cutting off review JSON - parseReviewOutput() fallback was ineffective for generic summaries Also includes: - Review draft cleanup on force-redo to prevent stale summaries - Increase DEFAULT_MAX_TURNS_REVIEW from 10 to 30 - Add turn budget warning to review prompt template - Rename githubReviewUrl to reviewUrl for platform-neutral naming Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: improve Jira provider and ticket sync - Switch to searchForIssuesUsingJqlEnhancedSearch (cursor-based pagination) - Smart repo URL matching: match ticket repo field against configured repos - Support full URLs, owner/repo paths, and repo: labels in Jira tickets - Add/delete ticket provider UI in settings page - Add deleteTicketProvider API client method Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: derive WebSocket URL from PUBLIC_API_URL for split-origin setups When web and API run on different origins (e.g. NodePort with separate ports), WebSocket connections failed because ws-client derived the URL from the page origin instead of the API origin. - Inject PUBLIC_API_URL at runtime via window.__OPTIO_CONFIG in layout.tsx - ws-client reads it to derive the correct ws:// or wss:// URL - Helm web-deployment auto-sets PUBLIC_API_URL for NodePort configurations - Fix PGDATA path for postgres to prevent initdb issues on restart Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: update review max turns test to match new default (30) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: as per comments * fix: harden ws-url-config PR per review feedback - Escape </script> sequences in runtime config JSON to prevent XSS - Remove leaked globalThis.__OPTIO_CONFIG in ws-client test cleanup - Remove unscoped PGDATA change (belongs in its own PR) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rename githubReviewUrl to reviewUrl for client compatibility The API returned `githubReviewUrl` but the web client expected `reviewUrl`, causing the review URL to always be undefined after submitting a draft. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore PUBLIC_URL env var in web-deployment.yaml (#294) PR #290 replaced the PUBLIC_URL env var block with PUBLIC_API_URL instead of adding it alongside. This broke OAuth login for deployments with publicUrl set, since the auth callback route uses PUBLIC_URL to construct redirect URLs. Restore the PUBLIC_URL block before PUBLIC_API_URL so both env vars are present in the web container. Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: dynamically import SessionTerminal to prevent SSR "self is not defined" error (#295) The xterm.js library accesses `self` during module initialization, which crashes Next.js server-side rendering. Using next/dynamic with ssr: false ensures the terminal component is only loaded on the client side. Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback from PR #288 (Jira provider improvements) (#296) - Hoist configuredRepos query above the provider loop to avoid redundant DB queries when multiple providers are configured - Add .trim() to Jira repo: label extraction so labels like "repo: acme/backend" match correctly - Move sensitive provider credentials (apiToken, apiKey) from plaintext JSONB config to encrypted secrets store (AES-256-GCM) - Refactor test mock to match on .from() table argument instead of brittle call-count ordering - Use provider config baseUrl for GitLab repo URL construction instead of hardcoded https://gitlab.com fallback Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: include all workspace members in Dockerfile deps stages (#297) The Dockerfile.api deps stage was missing apps/web and apps/site workspace members, and Dockerfile.web was missing apps/site. pnpm needs the full workspace graph to correctly resolve the lockfile with --frozen-lockfile, so omitting members causes the install to fail or produce incorrect workspace links. Also removed the --prod flag from Dockerfile.api so that workspace package dependencies are fully resolved, matching the pattern used in Dockerfile.web. Added a regression test that validates both Dockerfiles include all workspace members defined in pnpm-workspace.yaml. Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: make Helm chart publicly available (#298) - Add .helmignore to exclude unnecessary files from chart packages - Enrich Chart.yaml with home, sources, keywords, and maintainers metadata - Update README with three installation methods (Helm repo, OCI, source) Note: Workflow changes (CI helm-lint job, release.yml OCI visibility fix, and helm-release.yml for chart-releaser) are included in the PR description and need to be applied separately due to token scope limitations. Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(site): serve marketing site from optio.host custom domain Add CNAME file and drop /optio basePath now that the site is served from the apex domain instead of jonwiggins.github.io/optio/. * fix: don't require GITHUB_TOKEN secret when GitHub App auth is configured GITHUB_TOKEN was unconditionally added to the required secrets list, causing resolveSecretsForTask() to throw before reaching the isGitHubAppConfigured() guard. GitHub App users get credentials dynamically via the credential helper — no static PAT needed. * fix(helm): enable readOnlyRootFilesystem on web and API pods (#312) Set readOnlyRootFilesystem: true and add seccompProfile: RuntimeDefault to both web and API container security contexts. Mount emptyDir volumes for /tmp (both pods) and /app/.next/cache (web pod) so the containers can still write to the directories they need while preventing writes to the rest of the root filesystem. Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): add SSRF validation to slackWebhookUrl on PATCH /api/repos/:id (#313) The updateRepoSchema accepted any string for slackWebhookUrl without SSRF checks, while POST /api/slack/test already validated with isSsrfSafeUrl. An admin could store an internal URL that the API pod would later request when sending Slack notifications. Apply the same isSsrfSafeUrl Zod refine used in the Slack test route to the repo update schema. The existing assertSsrfSafe call in slack-service.ts before fetch provides defense-in-depth against DNS rebinding. Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(web): add security headers to Next.js config (#314) Add HTTP security headers via the Next.js headers() config function: - X-Content-Type-Options: nosniff - X-Frame-Options: DENY - Referrer-Policy: strict-origin-when-cross-origin - Permissions-Policy: restrict camera, microphone, geolocation - Content-Security-Policy-Report-Only: restrictive CSP in report-only mode - Strict-Transport-Security: HSTS with preload (production only) CSP is deployed in report-only mode to avoid breaking existing functionality. Once verified clean, it can be switched to enforcing. Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): add rate limiting to auth endpoints (#315) Auth endpoints under /api/auth/* had no stricter rate limiting than the global 100 req/min, enabling credential stuffing, account enumeration, and brute-force attacks on the OAuth state/code namespace. This adds per-route rate limits of 10 req/min on login, callback, exchange-code, and logout endpoints. Also configures Redis as the rate-limit store so limits work correctly across multiple API replicas. Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): use constant-time comparison for session tokens and add sliding-window expiry (#316) - Add timingSafeEqual for session token hash comparison (defense-in-depth) - Replace fixed 30-day session TTL with 7-day sliding window capped at 30 days - Revoke all user sessions on workspace role change or member removal - Move expiry check from DB WHERE clause to application code for proper ordering Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): restrict public route matching for /api/auth/* endpoints (#318) The PUBLIC_ROUTES list used a prefix match on `/api/auth/`, making every endpoint under that path (including `/api/auth/claude-token`) accessible without authentication. Replace the blanket prefix with an explicit allow-list of OAuth login/callback routes so that sensitive endpoints like claude-token, status, usage, and me require a valid session. Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): use 12-byte IV and AAD binding for AES-256-GCM encryption (#319) Switch from 16-byte to NIST SP 800-38D recommended 12-byte IV for AES-256-GCM. Add Additional Authenticated Data (AAD) binding so encrypted blobs are tied to their identity context (name, scope, workspace), preventing row-swap attacks where a ciphertext is moved to a different context and still decrypts. - encrypt()/decrypt() accept optional AAD parameter - buildSecretAAD() helper builds context string for secrets table - storeSecret/retrieveSecret bind AAD to name|scope|workspaceId - repo-service and webhook-service pass entity-specific AAD - Legacy 16-byte IV rows decrypt without AAD (backward compat) - retrieveSecret enforces isNull(workspaceId) for non-global scopes when no workspaceId is provided, preventing cross-workspace leakage Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): remove SYS_ADMIN and NET_ADMIN from DinD pods, require workspace opt-in (#321) Docker-in-Docker pods were granted SYS_ADMIN and NET_ADMIN capabilities, which effectively gave container-escape potential to untrusted code in .optio/setup.sh. This replaces them with rootless Docker (SYS_CHROOT only) and adds an admission-time check requiring explicit workspace admin opt-in via a new allowDockerInDocker flag. Changes: - Replace SYS_ADMIN + NET_ADMIN capabilities with SYS_CHROOT for rootless DinD - Add allowDockerInDocker boolean to workspaces table (default false) - Add admission check in createRepoPod that rejects DinD without workspace opt-in - Pass workspaceId through getOrCreateRepoPod → createRepoPod → admission check - Update workspace service/routes to support the new field - Add tests for all admission check scenarios - Update web UI description to reflect rootless Docker approach Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): replace bearer token with HMAC-SHA256 for internal credential endpoint (#322) The /api/internal/git-credentials endpoint previously authenticated using a shared bearer token (OPTIO_CREDENTIAL_SECRET) sent directly over the wire. A compromised agent pod could harvest this secret and request credentials for any repository. Changes: - Add HMAC-SHA256 request signing: agent pods now sign each request with HMAC(secret, "{timestamp}.{path}") and send the signature in the X-Optio-Signature header. The raw secret never crosses the wire. - Add replay protection: signatures expire after 5 minutes. - Maintain backward compatibility with legacy Bearer token auth for pods running older agent images. - Update optio-git-credential and optio-gh-wrapper scripts to compute and send HMAC signatures using openssl. - Add default-deny NetworkPolicy for repo pods (opt-in via networkPolicy.enabled) restricting cluster-internal traffic to only the API service. - Add comprehensive test coverage for the new HMAC auth service. Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): stop leaking WebSocket auth tokens in URLs (#323) WebSocket authentication tokens were being passed as URL query parameters (`?token=...`), which leaked them into nginx access logs, browser history, proxy logs, and Referer headers. This changes the auth mechanism to use the Sec-WebSocket-Protocol header instead. Changes: - Remove `req.query.token` fallback from the auth middleware (auth.ts) - Switch ws-auth.ts to extract upgrade tokens from Sec-WebSocket-Protocol header instead of URL query params - Configure handleProtocols in server.ts to negotiate the `optio-ws-v1` protocol without echoing back the auth token - Update the web client (ws-client.ts) to send tokens via WebSocket subprotocol header: ["optio-ws-v1", "optio-auth-<TOKEN>"] - Fix optio-chat.ts to use extractSessionToken() (cookie only) instead of reading from req.query.token for session passthrough - Add crypto.timingSafeEqual for WS upgrade token hash comparison in session-service.ts to prevent timing side-channel attacks Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): replace unsafe req.body/params/query as casts with Zod validation (#330) Replace all ~105 instances of `req.body as Type`, `req.params as Type`, and `req.query as Type` casts across route handlers, plugins, and WebSocket handlers with proper Zod schema validation using `.parse()` / `.safeParse()`. This prevents mass assignment, type confusion, and downstream crashes from malformed client input. Invalid requests now return 400 with a descriptive error instead of silently proceeding with unvalidated data. Also adds an ESLint `no-restricted-syntax` rule to ban future `as` casts on `req.body`, `req.params`, and `req.query`, ensuring this class of vulnerability cannot be reintroduced. Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): replace RSA-2048 with Ed25519 for Envoy sidecar CA, cut validity to 30 days (#334) The Envoy secret-proxy sidecar was generating a 2048-bit RSA self-signed CA with 365-day validity at pod init time. Since the cert is ephemeral (never persists beyond pod lifetime) and only terminates intra-pod TLS: - Switch to Ed25519 which is smaller, faster (~500ms → ~10ms gen time), and sufficient for this use case - Reduce validity from 365 to 30 days (pods rarely live >24h) - Add OPTIO_ENVOY_CA_ALG env var override for future PQ migration (e.g. "mldsa44" when OpenSSL 3.5+ stabilizes ML-DSA) Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(security): document Kubernetes 1.33+ as minimum for post-quantum TLS (#335) Kubernetes v1.33 is the first release built on Go 1.24, which enables hybrid X25519MLKEM768 key exchange by default for TLS 1.3. This means the Optio API pod's communication with kube-apiserver gets post-quantum TLS automatically on v1.33+ clusters. Changes: - Add kubeVersion: ">=1.33.0" to Helm Chart.yaml (fails fast on older clusters) - Add K8s v1.33+ prerequisite to README.md - Create docs/pq-readiness.md with PQ status table for all network legs - Add K8s version warning to setup-local.sh for clusters below v1.33 Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(observability): add TLS key-exchange group logging per upstream (#336) Log Node/OpenSSL version and PQ readiness at API startup, and subscribe to undici's diagnostics channel to count negotiated key-exchange groups (e.g. X25519MLKEM768 vs X25519) per upstream host. Counters are flushed to structured debug logs every 60s. Exposes getTlsGroupCounts() for future Prometheus integration. Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): enforce OpenSSL >= 3.5 for post-quantum TLS across all images (#337) Node 22+ bundles OpenSSL 3.5.x which negotiates hybrid post-quantum X25519MLKEM768 key agreement by default in TLS 1.3 handshakes. This ensures all outbound connections benefit from PQ key exchange. - Add build-time OpenSSL version assertion to Dockerfile.api, Dockerfile.web, Dockerfile.agent, Dockerfile.optio, and images/base.Dockerfile - Add runtime assertion in server.ts (via openssl-check.ts) that refuses to start if OpenSSL < 3.5 - Add unit tests for the version check logic Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(security): introduce Signer/Verifier interface for crypto-agility (#338) Add a Signer/Verifier abstraction layer to decouple signature algorithm selection from call sites, enabling future PQ (post-quantum) algorithm swaps with a single-file change. - Create Signer interface (Rs256Signer, HmacSha256Signer, MlDsa65Signer stub) and Verifier interface (HmacSha256Verifier with timingSafeEqual) - Refactor github-app-service.ts to use Rs256Signer via loadAppSigner() with env-configurable algorithm (GITHUB_APP_JWT_ALG) - Refactor tickets.ts webhook verification to use HmacSha256Verifier, centralizing constant-time comparison - Refactor webhook-service.ts signPayload to use HmacSha256Signer - Log active signer configuration on startup - Add 16 unit tests for all signer/verifier implementations Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): enable TLS for in-cluster Postgres (sslmode=verify-full) (#339) The in-cluster Postgres deployment was running without TLS, meaning the Postgres wire protocol (including decrypted secret values from the secrets table) was transmitted in plaintext on the cluster network. Changes: - Add self-signed CA + server cert generation at Helm install time (helm/optio/templates/postgres-tls.yaml) with lookup-based reuse on upgrades to avoid breaking connections - Enable ssl=on in the Postgres container with TLS 1.3 minimum, using an init container to set correct key file permissions (0600) - Update DATABASE_URL helper to append sslmode=verify-full&sslrootcert when postgresql.tls.enabled is true - Mount the CA cert into the API deployment at /etc/optio/pg-ca.crt - Add parseSslConfig() utility (apps/api/src/db/ssl.ts) that reads sslmode and sslrootcert from the connection string and configures the postgres driver's TLS options accordingly - TLS is enabled by default (postgresql.tls.enabled: true) but can be disabled for local development Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): enable TLS and authentication for in-cluster Redis (#340) In-cluster Redis was running plaintext, exposing BullMQ job payloads (rendered prompts, task metadata) and pub/sub log streams (repo content) to any compromised pod or lateral attacker on the cluster network. Changes: - Add redis.tls.enabled (default true) to Helm values with auto-generated self-signed CA + server cert (preserved across upgrades via lookup) - Configure Redis to serve TLSv1.3-only (--tls-port, --port 0) - Add redis.auth.enabled (default true) with auto-generated password - Create centralized redis-config.ts that builds TLS options from env (REDIS_CA_CERT_PATH, REDIS_TLS_REJECT_UNAUTHORIZED) and injects REDIS_PASSWORD into the connection URL - Update all 10 Redis consumers (event-bus, 6 workers, server rate limiter, index.ts) to use the centralized config - Mount Redis CA cert into the API pod for server verification - Update _helpers.tpl to emit rediss:// scheme when TLS is enabled Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(security): tag encrypted blobs with algorithm version byte for crypto-agility (#341) Add an `alg` field to all encryption operations to enable future cipher migrations (AAD binding V2, ML-KEM, KMS-wrapped keys) without coordinated rewrites across every call site. - Add ALG_AES_256_GCM_V1 (0x01) constant and EncryptedBlob interface - Refactor encrypt() to return { alg, ciphertext, iv, authTag } - Refactor decrypt() to dispatch on blob.alg with runtime validation - Add `alg` smallint column to secrets, webhooks, and repos tables - Update storeSecret/retrieveSecret to persist and read alg - Update webhook-service and repo-service to use EncryptedBlob API - Existing rows default to alg=1, null alg falls back to V1 Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add OpenCode as 5th agent adapter (experimental) (#350) Add OpenCode (opencode.ai) as an experimental fifth agent adapter alongside Claude Code, OpenAI Codex, and GitHub Copilot. Gated behind OPTIO_OPENCODE_ENABLED=true feature flag. Key changes: - OpenCodeAdapter implementing AgentAdapter interface with provider-agnostic secret validation, container config, and defensive result parsing - opencode-event-parser.ts for NDJSON streaming with committed fixture - buildAgentCommand/inferExitCode/streaming parser dispatch for opencode - DB migration adding opencodeModel/opencodeAgent/opencodeProvider to repos - agent-entrypoint.sh and base.Dockerfile updated with opencode support - All Zod route schemas and web UI selectors updated with opencode option - Setup status endpoint reports opencodeEnabled/opencodeConfigured - Comprehensive unit tests for adapter, event parser, and task worker Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add mid-task agent messaging and interject (#352) Enable users to send messages to a running Claude Code agent mid-task without cancelling and losing context. Messages are delivered via Claude Code's --input-format stream-json stdin mechanism. Key changes: - New task_messages table with delivery lifecycle (created→delivered→acked) - POST /api/tasks/:id/message route with soft/interrupt modes - GET /api/tasks/:id/messages route for message history - Per-task Redis pub/sub channel bridges API→worker for stdin delivery - buildAgentCommand adds --input-format stream-json --replay-user-messages - WebSocket log stream relays task:message events to connected clients - Activity feed includes messages alongside comments and events - Task detail UI shows message input bar for running claude-code tasks - Rate limiting (10 msgs/min/task), permission checks (creator or admin) - Returns 501 for codex/copilot agents (not yet supported) Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(notifications): add browser push notifications for task lifecycle events (#354) * feat(notifications): add browser push notifications for task lifecycle events Implement opt-in Web Push API notifications so users receive OS-level push alerts for task lifecycle events (PR opened, completed, failed, needs attention) even when the Optio tab is closed. Backend: - New `push_subscriptions` and `notification_preferences` tables - `notification-service.ts` with VAPID config, subscription CRUD, preference management, and push delivery via `web-push` library - 7 API endpoints at `/api/notifications/*` for subscription management, preferences, VAPID key retrieval, and test notifications - Fire-and-forget push dispatch in `transitionTask()` and `addComment()` - Auto-cleanup of expired/invalid subscriptions (HTTP 410) - Per-user, per-event notification preferences with sensible defaults Frontend: - Service worker (`sw.js`) handles push events and click-through navigation - `push-subscription.ts` client helper for registration and subscription - `NotificationPreferences` component with permission state, device list, per-event toggle grid, and test notification button - Replaced legacy `new Notification()` calls with service worker push - Service worker registered on authenticated page load via `PushSwRegistrar` Infrastructure: - VAPID keys configurable via Helm (`notifications.vapid.*`) - `scripts/generate-vapid-keys.sh` for key generation - Graceful degradation when VAPID keys are not configured Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): pin @github/copilot to 1.0.20 to avoid transient npm 404s The unpinned `npm install -g @github/copilot` resolves to 1.0.21 which intermittently returns 404 from the npm registry in CI (no Docker layer cache). Pin to 1.0.20 which is the last known-good version. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add Google Gemini CLI as a 4th agent adapter (#351) * feat: add Google Gemini CLI as a 4th agent adapter Add GeminiAdapter to @optio/agent-adapters alongside Claude Code, Codex, and Copilot, enabling Optio to run Google's @google/gemini-cli as a coding agent with full stream-json event parsing, cost tracking, and two auth modes (API key + Vertex AI ADC). Changes: - New GeminiAdapter class with validateSecrets, buildContainerConfig, parseResult (packages/agent-adapters/src/gemini.ts) - New gemini-event-parser service for NDJSON stream parsing (apps/api/src/services/gemini-event-parser.ts) - Extended AgentTaskInput with GeminiAuthMode and related fields - Added geminiModel/geminiApprovalMode columns to repos table + migration - Updated task-worker with Gemini parser dispatch, command builder, exit code inference, and secret resolution - Added POST /api/setup/validate/gemini-key validation endpoint - Extended PATCH /api/repos/:id with geminiModel, geminiApprovalMode - Updated base.Dockerfile to install @google/gemini-cli - Added gemini case to agent-entrypoint.sh - Setup wizard: Gemini card with API key and Vertex AI auth modes - Repo edit page: Gemini Settings section (model + approval mode) - Task creation: gemini option in agent selector - 40 adapter tests + 17 event parser tests (all passing) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: make @github/copilot install best-effort in base image The @github/copilot npm package is returning 404 (unpublished), which breaks the Docker image build for all branches. Make the install non-fatal so the image can still build without it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: stalled task detection with activity heartbeats (#353) Add soft stall detection for running tasks whose agent has gone silent. Tasks are flagged as "stalled" when no parsed log events arrive within a configurable threshold (default 5 min), surfacing a warning in the UI without prematurely failing the task. Key changes: - New `lastActivityAt` column on tasks, updated via debounced (5s) writes from the task worker whenever the agent emits parsed events - New `activitySubstate` enum (active/stalled/recovered) for tracking stall transitions without adding a new TaskState value - Soft stall detection pass in repo-cleanup-worker (30s cadence) that emits `task:stalled` and `task:recovered` WebSocket events - Per-repo `stallThresholdMs` override on repos table, with fallback to `OPTIO_STALL_THRESHOLD_MS` env var then 5-min default - `stallInfo` object in GET /api/tasks/:id response with isStalled, silentForMs, thresholdMs, and lastLogSummary - `isStalled` boolean enrichment on task list responses - Yellow "Stuck" badge overlay on StateBadge component - Stall indicator row on task cards and warning banner on task detail page - WebSocket event forwarding for stall/recovered events on per-task channel - Pure `isTaskStalled()` / `getSilentDuration()` utilities in shared package The existing hard stale-task fallback (10 min → fail + retry) is preserved as a safety net. Both coexist: soft stall warns at 5 min, hard stall kills at 10 min. Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): add SSRF validation to ticket provider config (#317) * fix(security): add SSRF validation to ticket provider config and outbound calls The ticket provider create endpoint accepted arbitrary config without validating URL fields, allowing SSRF attacks via Jira baseUrl and GitLab host. This fix adds three layers of protection: 1. Zod discriminated union schema at the route level with isSsrfSafeUrl/ isSsrfSafeHost refinements to reject private/internal addresses on input 2. Runtime assertSsrfSafe() calls in Jira and GitLab providers before every outbound request (catches DNS rebinding) 3. redirect: "error" on all GitLab provider fetch() calls to prevent 302-based SSRF bypasses The SSRF utilities are moved to @optio/shared so both apps/api and packages/ticket-providers can use them. The old location re-exports for backwards compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move SSRF utils to subpath export to avoid bundling node:dns in web build The barrel export in @optio/shared pulled node:dns/promises into the Next.js webpack build. Move ssrf to a dedicated subpath export (@optio/shared/ssrf) so server-only code stays out of the browser bundle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve postgres and redis TLS connection failures - Strip libpq-style ssl* query params from DATABASE_URL before passing to postgres-js, which otherwise forwards them as server startup parameters and triggers "unrecognized configuration parameter" errors. - Disable Redis client cert auth (--tls-auth-clients no) since the API connects with server-only TLS and does not present a client cert. * fix(worker): deliver initial claude prompt via stdin stream-json When the mid-task messaging feature added --input-format stream-json, claude started ignoring the -p "$OPTIO_PROMPT" positional arg and instead waits for user messages on stdin. The worker only wrote to stdin on subsequent mid-task interjects, so every fresh task sat silently at "Running Claude Code..." with zero agent output until the stale-task detector killed it. This was hard to spot because the unit tests only asserted the flag strings, never that a real claude invocation produced any output. Switch the command to boolean --print and write the initial prompt to execSession.stdin as a stream-json user message immediately after the exec session is created. The pipe buffer holds the message until bash finishes its setup and execs claude, so no synchronisation is needed. Adds a regression test that fails if the prompt is ever embedded in the claude command again, plus unit coverage for the new helper. * fix(web): show token-refresh banner on recent task auth failures The banner previously only triggered when /api/auth/usage itself returned 401/expired. In practice the Anthropic usage endpoint and messages endpoint can diverge: usage returns 429 (rate limited by token scope) while messages returns 401 (token invalid for task execution). The result was broken tasks with no visible UI path to refresh the token. Add a hasRecentClaudeAuthFailure detector that scans task_logs for authentication markers from the last 15 minutes, expose it on /api/auth/usage as hasRecentAuthFailure, and broaden the banner trigger so it fires on either signal. The banner now also shows when usage meters render successfully but tasks are still 401ing — a real edge case with narrow-scope OAuth tokens. * fix(auth): use drizzle helpers in auth-failure detector The raw sql.join approach silently returned zero rows even when matching logs existed. Switch to and/or/ilike/gt helpers which are the same pattern used elsewhere in the codebase (e.g. task-service listTasks). * fix(worker): exit claude cleanly on result event via stdin EOF After the earlier fix that made the initial prompt arrive over stdin, claude would process the turn, emit the result event, and then sit idle waiting for more stream-json input — the task never transitioned out of running. Two related changes: 1. Flag claude's result event as terminal in the stream parser and, in the task worker, call execSession.stdin.end() when a terminal event arrives. Claude closes on EOF, so this lets it exit cleanly without affecting mid-task messaging (the subscribe handler still writes until the end() is called). 2. The custom Writable wrapping the k8s exec stdin only proxied write; calling .end() on it was a no-op because the underlying stdinStream was never closed. Implement the `_final` hook in both the kubernetes and docker runtimes so .end() propagates as EOF to the in-container process. Verified by running a smoke-test task end-to-end: state=completed, cost captured, tokens captured, transitioned in ~40s. * feat(rbac): allow deployment without ClusterRole for namespace-only K8s (#357) In shared multitenant clusters, users may not have permission to create ClusterRole/ClusterRoleBinding resources. This change makes cluster-wide permissions optional so Optio can run with namespace-scoped RBAC only. - Add `rbac.clusterRole.create` Helm value (default true) to control whether ClusterRole/ClusterRoleBinding are created - Handle 403 Forbidden in ensureNamespace() gracefully — if we can't read the namespace at cluster scope, assume it exists (we're running in it) - Make cluster overview's listNode() call fail gracefully — returns empty nodes list instead of 500 when ClusterRole is unavailable Closes #320 Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(db): prevent duplicate-numbered migrations from concurrent branches (#358) Switch new Drizzle migrations to unix-timestamp prefixes to prevent numeric collisions when concurrent branches each generate the next sequential number. Add a repair migration that idempotently ensures all objects from the 6 historical duplicate-prefix pairs exist on any cluster. - Update drizzle.config.ts with migrations.prefix: "unix" - Add 1775613995_repair_duplicate_migrations.sql (idempotent repair) - Add scripts/check-migration-prefixes.sh with historical allowlist - Wire prefix checker into husky pre-commit hook - Add apps/api/src/db/migrate.ts standalone migration runner - Update CLAUDE.md with new migration conventions Closes #355 Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * [PQ] Add docs/cryptography.md describing every crypto primitive and its migration plan (#359) * docs: add cryptography.md with full primitive inventory and PQ migration plan Consolidates the answer to "which crypto algorithms does Optio use?" into a single reference document for procurement security reviews and compliance audits. Covers AES-256-GCM secrets-at-rest, SHA-256 session tokens, HMAC-SHA256 webhook signing, RS256 GitHub App JWTs, Ed25519 Envoy CA, Helm TLS CAs, and outbound TLS. Includes Mosca shelf-life analysis, key management procedures, CNSA 2.0 / FIPS 140-3 compliance mapping, and runtime PQ verification commands. Closes #333 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: format cryptography.md with prettier Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add OpenTelemetry export for traces, metrics, and logs (#360) Add first-class OpenTelemetry support to the Optio API server so operators can ship traces, metrics, and high-signal logs to any OTLP-compatible backend (Datadog, Honeycomb, Grafana Cloud, etc.). The integration is opt-in via OPTIO_OTEL_ENABLED=true. When disabled (default), no OTel packages are loaded and there is zero runtime cost. New files: - telemetry.ts: SDK initialization with dynamic imports - telemetry/spans.ts: span wrappers, trace context propagation, URL sanitizer - telemetry/metrics.ts: 12 custom counters, histograms, and gauges - telemetry/logs.ts: high-signal OTel log emitter for state transitions, cost reports, pod health events, auth failures, and webhook failures - telemetry/instrument-worker.ts: BullMQ worker processor wrapper - telemetry/attributes.ts: whitelisted attribute constants - telemetry/*.test.ts: 35 tests including sensitive data enforcement - docs/observability.md: setup guide for Datadog, Honeycomb, Grafana Cloud Modified files: - index.ts: dynamic imports after telemetry init for auto-instrumentation - health.ts: otelEnabled field in health response - task-service.ts: state transition metrics and OTel logs - event-bus.ts: trace ID attached to WebSocket events - Helm chart: observability.otel.* values and secret env vars Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: per-repo shared persistent cache directories (#361) Add a first-class "shared directories" feature that lets workspace admins define cache directories which persist across tasks in the same repository. These mount into every task pod at a configurable path inside /workspace or the agent's $HOME, are backed by K8s PVCs, and reduce per-task setup time for repos with heavy install or model-download steps. Key changes: - New `repo_shared_directories` table + migration (0046) - `cache_pvc_name`/`cache_pvc_state` columns on `repo_pods` - shared-directory-service: CRUD + PVC orchestration + clear/usage - REST routes: list/create/update/delete/clear/usage + pod recycle - repo-pool-service integration: mount cache PVCs via extraVolumes - repo-service: cleanup cache + home PVCs on repo deletion - Web UI: SharedDirectoriesSection with 10 preset cache configs - Helm chart: agent.cache.* values + env vars + PVC RBAC patch/update - .optio-cache/ added to git exclude in worktree setup - CLAUDE.md: documented feature, routes, cache recipes Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: kill orphaned agents before stale re-queue and on API restart (#362) Two interacting bugs caused duplicate agents in the same worktree after API restarts: Bug 1 — Stale re-queue spawned new agents without killing orphans: - Add killOrphanedAgentInPod() that finds processes by OPTIO_TASK_ID env var in /proc/*/environ, sends SIGTERM+SIGKILL, and cleans the worktree with git worktree remove --force - Call it in the stale detection path before re-queueing - Mark worktreeState='removed' so the next agent creates a fresh one - Escalate to needs_attention when cleanup fails on a repeated stale recovery, preventing infinite retry loops Bug 2 — API restart left orphaned in-pod processes: - Add orphan kill loop in reconcileOrphanedTasks() startup path - Make the exec script signal-aware: EXIT trap kills child processes (agent + background watchdog) - Add background heartbeat that detects broken stdout pipe (EPIPE from severed SPDY stream) and sends SIGTERM to the main script Tests added for killOrphanedAgentInPod (7 cases) and stale detection with orphan cleanup (4 cases in cleanup worker tests). Closes #356 Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: build the optio CLI — terminal-first client for the API (#363) * feat: build the optio CLI — terminal-first client for the API Introduces `apps/cli/` as a new workspace package shipping a standalone `optio` CLI tool (Node >= 20). The CLI talks to any deployed Optio API instance and gives developers a scriptable, terminal-first way to manage tasks, stream logs, attach to interactive sessions, and manage repos, secrets, and workspaces — without opening the web UI. Server-side changes: - New `api_keys` table + migration for personal access tokens (PATs) - `api-key-service.ts` with CRUD + validation for `optio_pat_*` tokens - Auth plugin extended to validate PAT tokens via Bearer header - WS auth extended to accept PATs via Sec-WebSocket-Protocol header - CLI login flow routes: POST /api/auth/cli/start, POST /api/auth/cli/token - API key management routes: POST/GET/DELETE /api/auth/api-keys - Fix pre-existing handleProtocols typecheck error in server.ts CLI features: - OAuth-over-loopback login flow with PKCE (optio login) - Token precedence: --api-key flag > OPTIO_TOKEN env > credentials file - Task commands: new, list, show, logs (--follow via WS), cancel, retry, review, message - Repo commands: list, show, add, remove - Session commands: new, list, attach (raw PTY passthrough), end - Secret commands: list, set, rm - Workspace commands: list, switch - --json on every command, --no-color, sensible exit codes - XDG-aware config (~/.config/optio/) with 0600 credentials - 37KB bundled output (well under 1MB target) Closes #345 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add apps/cli to Dockerfile COPY stages for workspace graph The Dockerfile workspace completeness test requires all workspace members' package.json files to be copied in the deps stage so pnpm can resolve the full workspace graph during docker builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: update Helm chart defaults to GHCR images for production readiness (#365) - Change values.yaml image defaults from local names (optio-api, optio-web, optio-base) to GHCR paths (ghcr.io/jonwiggins/optio-*) - Change all default imagePullPolicy values from Never to IfNotPresent - Add agent.image.prefix value for configurable preset image resolution, with OPTIO_AGENT_IMAGE_PREFIX and OPTIO_AGENT_IMAGE_TAG env vars - Create values.local.yaml with local dev overrides (short image names, pullPolicy: Never, auth disabled, NodePort services) - Update setup-local.sh and update-local.sh to use -f values.local.yaml - Remove redundant GHCR image overrides from values.production.yaml - Update CLAUDE.md Helm chart section and known issues Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: compute pipeline stats server-side to avoid pagination clamp (#366) The overview dashboard was deriving pipeline stats by filtering a paginated task list (limit: 100), so counters like "Done" stopped growing once a workspace exceeded ~100 tasks. Add GET /api/tasks/stats that runs a single grouped COUNT query, and update the dashboard hook to consume it instead of filtering client-side. The recent-tasks widget now fetches only 5 rows. Closes #364 Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add missing 0046 migration entry to Drizzle journal (#369) * fix: add missing 0046 migration entry to Drizzle journal The migration file 0046_repo_shared_directories.sql was present but had no corresponding entry in _journal.json. Drizzle's migrator uses the journal as its single source of truth, so the migration was silently skipped—leaving the cache_pvc_name and cache_pvc_state columns missing from the repo_pods table and crashing the health-check worker. Add the missing journal entry (idx 48) and bump the two subsequent timestamp-prefixed entries. Also add a journal-consistency test that verifies every .sql file has a journal entry (and vice-versa), so this class of bug is caught in CI going forward. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: mock assertSsrfSafe in jira tests to prevent DNS hangs in CI jira.test.ts was calling the real assertSsrfSafe which does dns.lookup() for the test hostname. In CI, this can hang until the 5s timeout, causing flaky test failures. Add the same mock that jira-ssrf.test.ts already uses. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): replace workflow tables with new Workflows data model (#384) Drop old `workflow_templates` and `workflow_runs` tables and remove all references (routes, services, tests, UI page, sidebar link, api-client methods, task-worker/task-service integration). Create four new tables via migration: `workflows`, `workflow_triggers`, `workflow_runs`, and `workflow_pods` — designed for parameterized agent recipes with manual/schedule/webhook triggers. Add shared types (`WorkflowRunState`, `WorkflowTriggerType`, `Workflow`, `WorkflowTrigger`, `WorkflowRun`, `WorkflowPod`) and a workflow run state machine to `packages/shared/`. Closes #370 Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): add /api/workflows CRUD routes with aggregate run stats (#386) Add enriched workflow endpoints at /api/workflows that return runCount, lastRunAt, and totalCostUsd by joining workflow_runs and tasks tables. - GET /api/workflows — list workflows with aggregate stats, workspace-scoped - POST /api/workflows — create a workflow template (Zod validated) - GET /api/workflows/:id — get workflow with stats - PATCH /api/workflows/:id — update workflow template - DELETE /api/workflows/:id — delete workflow template Closes #371 Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): workflow pod management (#388) * feat(workflows): add workflow-pool-service for workflow environment pods Implement workflow pod management that provides isolated K8s pods for workflow runs without git clone overhead. Pods init with a setup script and sleep, waiting for exec commands from workflow steps. New components: - workflow_pods DB table with state tracking and active run counts - workflow-pool-service.ts: getOrCreateWorkflowPod, createWorkflowPod, execRunInPod, releaseRun, cleanupIdleWorkflowPods, listWorkflowPods - generateWorkflowPodName in shared constants for K8s-safe naming - Integration with repo-cleanup-worker for idle pod cleanup - Full test coverage (11 tests) Closes #374 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts with new workflow data model Remove duplicate workflowPods table definition from schema.ts that was introduced by the `1775791846_replace_workflow_tables` migration on main. Update the `1775792368_workflow_pods` migration to DROP and recreate the table instead of CREATE IF NOT EXISTS. Update shared WorkflowPodState enum values to match the new schema (provisioning/ready/error/terminating). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): add workflow detail page with runs, triggers, and stats (#392) Add /workflows list page and /workflows/:id detail page with: - Definition summary showing name, description, and enabled state - Stats bar: total runs, success rate, total cost, last run time - Tabbed layout: recent runs table, triggers list, configuration panel - Actions: enable/disable, delete, refresh - Auto-refresh every 5s while runs are active - API: add triggers listing endpoint and client methods - Nav: add Workflows link to sidebar Closes #382 Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): workflow run routes (#389) * feat(workflows): add workflow run routes for retry, cancel, and logs Add new API routes and service functions for managing workflow runs: - POST /api/workflows/:id/runs — short alias for starting a workflow run - GET /api/workflows/:id/runs — short alias for listing runs - POST /api/workflow-runs/:id/retry — retry failed/cancelled runs - POST /api/workflow-runs/:id/cancel — cancel running/paused runs - GET /api/workflow-runs/:id/logs — aggregated logs across all tasks Infrastructure additions: - workflowRunId column on task_logs table with index (migration) - BullMQ workflow-runs queue with dedicated worker - Worker registered in API server startup and graceful shutdown Closes #373 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix prettier formatting after rebase conflict resolution Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): workflows list page + sidebar (#390) * feat(workflows): add workflows list page and sidebar entry Add a /workflows page showing all workflow definitions in a table with columns for name, status, runtime, run count, last run, cost, and trigger type icons. Includes empty state, loading skeleton, and a Workflow icon sidebar entry. Backend: new GET /api/workflows endpoint with workflow-service that aggregates run stats (count, last run, total cost) and trigger types. Closes #380 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts with workflow CRUD routes on main Deduplicate workflowRoutes import/registration in server.ts, add workflowTriggers schema mock and trigger query mocking to workflow-service tests, align frontend with backend field names (runCount instead of totalRuns). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): add BullMQ workflow worker for workflow run execution (#394) Implement a workflow-runs BullMQ worker that processes workflow runs with: - Concurrency checks (global OPTIO_MAX_WORKFLOW_CONCURRENT + per-workflow maxConcurrent) - Serialized claim lock to prevent concurrency storms - Pod provisioning via workflow-pool-service - Prompt rendering with {{param}} variable substitution - Agent execution with multi-runtime support (claude-code, codex, copilot, opencode, gemini) - NDJSON log streaming with agent-event-parser - Cost/token/model capture from agent results - State transitions (queued → running → completed/failed) - Retry with exponential backoff (5s × 2^attempt) - Provisioning retry (up to 3x with 30s delay) - Auth mode support (api-key, oauth-token, max-subscription) Also adds: - POST /api/workflows/:id/runs route to create and enqueue workflow runs - createWorkflowRun() service method - WorkflowRunStateChangedEvent and WorkflowRunLogEvent WS event types - Worker registration in index.ts with graceful shutdown Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): manual trigger with params form (#391) * feat(workflows): manual trigger with params form Add UI for manually running workflows with a dynamic parameter form. - WorkflowParamsForm component generates form fields from paramsSchema (string, number, integer, boolean, enum) with defaults and required indicators; falls back to freeform JSON textarea when no schema exists - RunWorkflowDialog opens from the detail page Run button, renders the params form, and POSTs to the new /api/workflows/:id/runs endpoint - Workflow list page (/workflows) with per-row Run shortcut - Workflow detail page (/workflows/[id]) with info cards, schema preview, expandable run history, and the Run dialog - Backend: workflow-service (list, get, createRun, listRuns, listTriggers) and workflow routes registered in server.ts - API client methods: listWorkflows, getWorkflow, runWorkflow, listWorkflowRuns, listWorkflowTriggers - Sidebar navigation updated with Workflows entry - 19 tests covering schema-driven form generation, freeform JSON mode, field interactions, defaults, and required indicators Closes #377 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(workflows): update tests for merged route/service changes After rebasing onto main, the workflow routes now include workspace ownership checks and limit parameters from the feature branch merged with CRUD routes from main. Updated test mocks to match: - Add mockGetWorkflow to runs list test (workspace ownership check) - Add .limit() to mock chain in workflow-service test - Add missing mock exports (createWorkflowRun, listWorkflowTriggers) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(workflows): remove duplicate route and API client entries after rebase Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): add POST /api/hooks/:webhookPath endpoint for webhook trigger ingress (#395) Implement incoming webhook endpoint that triggers workflow runs from external services. The endpoint is exempt from session auth, verifies HMAC-SHA256 signatures when a secret is configured, maps incoming JSON payloads to workflow params via JSON-path expressions, and returns 202 with the created run ID. Rate limited to 60 requests/minute. Closes #379 Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): add cron-based schedule trigger evaluation (#396) Add a BullMQ repeating job (every 60s) that evaluates workflow schedule triggers, creates workflow runs when cron expressions match, and tracks lastFiredAt/nextFireAt to prevent double-firing. - Add lastFiredAt and nextFireAt columns to workflowTriggers with index - Add trigger CRUD functions to workflow-service (create, update, delete) - Add getDueScheduleTriggers() and markTriggerFired() for evaluation - Add createWorkflowRun() for creating runs from triggers - Create workflow-trigger-worker with BullMQ repeating job - Register worker in API server startup and shutdown - Add POST/PATCH/DELETE routes for workflow triggers with cron validation - Update WorkflowTrigger shared type with new fields - Add comprehensive tests for service, worker, and routes Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): add workflow run detail page (#398) Add /workflows/:id/runs/:runId page with metadata bar, params panel, live log streaming via polling, output JSON viewer, error panel with classifier, and retry/cancel action buttons. - New page at apps/web/src/app/workflows/[id]/runs/[runId]/page.tsx - New useWorkflowRunLogs hook for fetching/polling workflow run logs - Extended LogViewer to accept external log data (externalLogs prop) - Added retryWorkflowRun, cancelWorkflowRun, getWorkflowRunLogs to API client - Made run rows clickable in workflow detail page - Added guard in useLogs for empty taskId - Tests for new hook and API client methods Closes #383 Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): create/edit workflow form (#393) * feat(workflows): create/edit workflow form with trigger management Add /workflows/new and /workflows/[id]/edit pages with a shared form component covering: basics (name, description, enabled), environment spec (JSON), agent settings (runtime, model, max turns, budget, and advanced execution params), prompt template with {{PARAM}} detection and auto-build for params schema, and inline trigger management (manual/schedule/webhook CRUD). Also adds: - Workflow trigger API routes (GET/POST/PATCH/DELETE /api/workflows/:id/triggers) - Workflow trigger service methods (list/create/update/delete) - Workflow CRUD + trigger API client methods in web app - Workflows list page at /workflows with enable/disable/delete - Workflows link in sidebar navigation - Tests for all new routes, service methods, and API client methods Closes #381 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: format workflows page after rebase conflict resolution Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update workflow trigger tests for merged updateWorkflowTrigger The rebase merged two versions of updateWorkflowTrigger - incoming's type-update support and main's getWorkflowTrigger+nextFireAt logic. Tests for the simpler update path now mock getWorkflowTrigger too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): WebSocket log streaming for workflow runs (#397) * feat(workflows): add WebSocket log streaming for workflow runs Add real-time log streaming infrastructure for workflow runs, mirroring the existing task log streaming pattern: - Add WorkflowRunStateChangedEvent and WorkflowRunLogEvent shared types - Add workflow_run_logs table with DB migration - Add publishWorkflowRunEvent to event-bus (publishes to global and per-run Redis channels: optio:workflow-run:{id}) - Add /ws/workflow-runs/:workflowRunId/logs WebSocket handler with catch-up support (sends last 50 logs on connect) - Add transitionWorkflowRunState and appendWorkflowRunLog service functions that persist + publish events - Add GET /api/workflow-runs/:id/logs REST endpoint - Add createWorkflowRunLogClient factory to web WsClient - Add workflow_run:state_changed handler to global WebSocket hook - Full test coverage for all new functionality Closes #376 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(workflows): resolve type errors and formatting after rebase - Add workflowId parameter to transitionRun() and all call sites to match WorkflowRunStateChangedEvent type requirements - Map AgentLogEntry fields to flat WorkflowRunLogEvent shape instead of passing entry as a property - Fix workflow-runs logs route tests to mock getWorkflowRun - Run prettier on modified files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): trigger CRUD routes (#387) * feat(workflows): add CRUD routes for workflow triggers Implement workflow trigger management (closes #372): - New `workflow_triggers` table with at-most-one-per-type constraint and webhook path uniqueness index - GET/POST /api/workflow-templates/:id/triggers - PATCH/DELETE /api/workflow-templates/:id/triggers/:triggerId - Per-type config validation (manual, schedule, webhook) - Workspace-scoped access control - 32 tests covering routes and service layer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: format workflow-trigger-service.ts with Prettier Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove duplicate trigger routes from workflows.ts The trigger CRUD routes (GET/POST/PATCH/DELETE /api/workflows/:id/triggers) are now handled by the dedicated workflow-triggers.ts route module. Remove them from workflows.ts and its tests to prevent duplicate route registration when both modules are loaded. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Optio Agent <optio-agent@noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(workflows): improve UX with WS streaming, structured triggers, clone, and visual editors - Fix webhook path bug: config.webhookPath → config.path (triggers never matched) - Switch workflow run logs from 3s REST polling to real-time WebSocket streaming - Add structured trigger config: cron presets/preview for schedule, path/secret inputs for webhook - Add clone/duplicate workflow (POST /api/workflows/:id/clone) with frontend button - Add visual parameters schema builder with auto-detect from prompt - Add runs table filter pills (All/Running/Completed/Failed) - Add edit button, delete confirmation, and duplicate button to detail page - Show full webhook URL with copy button in trigger display - Navigate to run detail page after starting a run - Redirect to workflow detail (not list) after create/edit - Enhance empty states with action buttons (Run Now, Configure Triggers) - Fix check-migration-prefixes.sh for bash 3.x compatibility (macOS) * docs: compact CLAUDE.md by removing info derivable from code Remove directory layout, database schema, API routes catalog, workers list, and detailed execution flow (all readable from the source). Merge duplicated auth/concurrency/migration docs into single mentions. Condense subsystem descriptions into one-liner pointers to code. 669 lines → 217 lines (~67% reduction). * fix(auth): clear stale auth-failure banner when token is updated (#401) Generalize the auth failure detector to support both Claude and GitHub tokens with per-token-type watermarks. When a token is updated via the secrets page, only failures after the update time are considered, immediately clearing stale banners. Key changes: - auth-failure-detector: new getRecentAuthFailures() returns per-type status; uses secrets.updatedAt as watermark to narrow the window - auth_events table: lightweight store for GitHub auth failures from non-task contexts (PR watcher, ticket sync) - secrets route: validates tokens on save, invalidates credential cache, publishes auth:status_changed WebSocket event - auth routes: /api/auth/usage returns authFailures.{claude,github}; /api/auth/refresh returns fresh failure status - GitHub token banner: new GitHubTokenBanner component shown when GITHUB_TOKEN is invalid - WebSocket: auth:status_changed event triggers immediate UI refr…