feat(service): Add optional Warden backing service - #470
Conversation
Add a Vercel-ready Hono and Postgres service for run history, cost analysis, findings, and repository memory. Wire fail-open publishing and recall through CLI, Action, and SDK entry points while preserving existing local outputs. Include Google OAuth, read-only personal tokens, deployment docs, and a compact API-backed dashboard. Co-Authored-By: GPT-5.6 Codex <noreply@anthropic.com>
Harden history filters, memory evidence selection, CLI operations, publishing, and run attribution. Validate cursors and projections earlier, preserve complete usage data, and keep the documented clean-checkout deployment flow executable. Co-Authored-By: GPT-5.6 Codex <noreply@anthropic.com>
Override Vercel's Python analysis helper to use minimatch 10.2.5, which fixes the high-severity ReDoS advisories reported by dependency review. Co-Authored-By: GPT-5.6 Codex <noreply@anthropic.com>
Override path-to-regexp 6.1.0 with the compatible 6.3.0 release to address the high-severity backtracking advisory reported by dependency review. Co-Authored-By: GPT-5.6 Codex <noreply@anthropic.com>
Pin the vulnerable Vercel path matcher and HTTP client versions to compatible patched releases. This removes the remaining high-severity advisories introduced by the service app's local Vercel tooling. Co-Authored-By: GPT-5.6 Codex <noreply@anthropic.com>
Keep the Action publication test deterministic when GitHub Actions injects GITHUB_RUN_ATTEMPT. Rerun identity behavior remains covered separately. Co-Authored-By: GPT-5.6 Codex <noreply@anthropic.com>
Keep service failures from changing Warden results and retain valid run data at protocol limits. Close tenant and token scope escalation paths. Preserve memory context while publishing empty and failed executions consistently. Co-Authored-By: GPT-5.6 Sol <noreply@anthropic.com>
Keep read exports within the history API data boundary and preserve fail-open publication across early Action exits. Resolve service defaults only after overrides, validate CLI inputs strictly, and keep bounded projections internally consistent. Co-Authored-By: GPT-5.6 Sol <noreply@anthropic.com>
Require HTTPS outside loopback and prevent environment tokens from using repository-configured URLs. Keep replay available when config parsing fails and normalize optional pull request metadata at the service protocol boundary. Co-Authored-By: GPT-5.6 Sol <noreply@anthropic.com>
Match the workspace-root output produced by filtered pnpm pack commands so the artifact upload finds both package tarballs. Co-Authored-By: GPT-5.6 Sol <noreply@anthropic.com>
Release unstarted jobs without consuming retries, preserve profile-aware config defaults and mixed usage attribution, and isolate replay tests from service environment variables. Co-Authored-By: GPT-5.6 Sol <noreply@anthropic.com>
Bring the service PR up to date with offline policy and Pi runtime changes from main. Preserve both offline configuration and optional service resolution in the only overlapping CLI path. Co-Authored-By: GPT-5.6 Sol <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a22c1cd. Configure here.
Keep repository memory eligibility enabled after later runs opt out so queued extraction jobs retain access to their evidence. Co-Authored-By: GPT-5.6 Sol <noreply@anthropic.com>
| const paths = context.pullRequest?.files.map((file) => file.filename) ?? []; | ||
| const response = await recallMemoryFailOpen(service, { | ||
| protocolVersion: 1, | ||
| clientRecallId: randomUUID(), | ||
| repository: { | ||
| provider: 'github', | ||
| owner: context.repository.owner, | ||
| name: context.repository.name, | ||
| fullName: context.repository.fullName, | ||
| }, | ||
| skills: [...new Set(skills)], | ||
| languages: [...new Set(paths.map((path) => extname(path).slice(1)).filter(Boolean))], | ||
| paths, |
There was a problem hiding this comment.
recallActionMemoryFailOpen silently drops repository memory when >500 files change
recallActionMemoryFailOpen sends the full changed-file list to the memory endpoint without truncating to the service's 500-item limit, so large pull requests silently forfeit recalled memories.
Evidence
MemoryRecallRequestSchemain the service API package definespaths: z.array(z.string().max(1_024)).max(500)(api.ts).fetchPullRequestFilesin the event layer usesoctokit.paginateand returns every changed file without truncation (event/context.ts).recallActionMemoryFailOpenbuildspathsfromcontext.pullRequest?.files.map(file => file.filename)and passes it directly torecallMemoryFailOpen(service.ts:31-43).recallMemoryFailOpendelegates tocreateWardenServiceClient(...).recallMemory, which parses the request withMemoryRecallRequestSchema.parse; an oversized array throws a ZodError that is caught and returned asundefined(client.ts).recallActionMemoryFailOpenthen returns{ memories: [] }, so the action proceeds without the expected historical context.
Identified by Warden · code-review · JXP-X2L
| location: { | ||
| ...finding.location, | ||
| path: finding.location.path.slice(0, MAX_PATH_LENGTH), | ||
| }, | ||
| } : {}), | ||
| ...(finding.additionalLocations ? { | ||
| additionalLocations: finding.additionalLocations.slice(0, 20).map((location) => ({ | ||
| ...location, | ||
| path: location.path.slice(0, MAX_PATH_LENGTH), | ||
| })), | ||
| } : {}), | ||
| ...(provenance ? { provenance } : {}), | ||
| ...(sourceSnippet ? { | ||
| sourceEvidence: { | ||
| path: sourceSnippet.path.slice(0, MAX_PATH_LENGTH), | ||
| ...(sourceSnippet.language ? { language: sourceSnippet.language } : {}), | ||
| startLine: sourceSnippet.startLine, | ||
| endLine: sourceSnippet.endLine, | ||
| targetStartLine: sourceSnippet.targetStartLine, | ||
| targetEndLine: sourceSnippet.targetEndLine, | ||
| content: sourceSnippet.lines.map((line) => line.content).join('\n').slice(0, 16_000), | ||
| }, | ||
| } : {}), | ||
| }; | ||
| } | ||
|
|
||
| export interface ServiceSkillReport { | ||
| executionId: string; | ||
| report: SkillReport; | ||
| triggerId?: string; | ||
| triggerName?: string; | ||
| skillDigest?: string; | ||
| findingProvenance?: Readonly<Record<string, FindingProvenance>>; | ||
| } | ||
|
|
||
| export interface BuildServiceRunProjectionInput { | ||
| service: Pick<ResolvedServiceOptions, 'data' | 'memory'>; | ||
| clientRunId: string; | ||
| source: 'cli' | 'action' | 'sdk' | 'replay'; | ||
| wardenVersion: string; | ||
| startedAt: Date; | ||
| completedAt: Date; |
There was a problem hiding this comment.
Empty or whitespace-only finding fields silently break service envelope construction
findingRecord slices title, description, and path strings to max length but does not trim them or filter empty/whitespace-only values. Optional fields like verification and language also pass through whitespace-only strings because they are truthy in JavaScript. These values satisfy Warden's local FindingSchema but violate the service ShortTextSchema, DescriptionSchema, and PathSchema, which require z.string().trim().min(1). When buildServiceRunEnvelope later calls redactRunProjection, RunProjectionSchema.parse throws a ZodError; callers catch it and either skip service publication or, during CLI replay, emit the misleading error "Artifact is not a supported completed JSONL or findings-output file."
Evidence
FindingSchema.titleisz.string()(no.min(1)) andvalidateFindingsinsdk/extract.tsdoes not reject empty strings, so empty titles can reachfindingRecord.service/findings.ts:21-30already usesboundedOptionalTextto normalize PR metadata for the exact same service contract, showing the codebase already handles this pattern for service-facing data.packages/warden-service-api/src/protocol.tsdefinesShortTextSchema,DescriptionSchema, andPathSchemawith.trim().min(1).packages/warden-service-api/src/redaction.tscallsRunProjectionSchema.parse(), which throws when empty/whitespace-only strings appear in those fields.- In
projection.tslines 98-99,titleanddescriptionare unconditionally included via.slice(0, MAX)with no empty-string guard, and optional fields at lines 101, 106, 110, 118-119 include whitespace-only strings because...(value ? {...} : {})treats' 'as truthy.
Identified by Warden · code-review · ZCJ-85H

Add an optional backing service for durable Warden run history, cost analysis, finding exploration, and repository-scoped memory.
The service is API-first and self-hostable. It provides a strict versioned ingestion contract, authenticated read APIs, service credentials, read-only personal tokens, and Junior-style Google OAuth for browser access. The reference app deploys to Vercel using Node functions, static dashboard assets, Marketplace Postgres, and Vercel Cron without Redis or an always-on worker.
Warden's CLI, GitHub Action, and SDK publish and recall through fail-open boundaries. A repository without a configured service behaves as it does today. Once a URL and token are configured, the client defaults to the findings profile with memory enabled while continuing to treat local JSONL, Action output, and SDK results as authoritative.
The change also includes replay, migrations, credential administration, retention and deletion boundaries, durable Postgres jobs, deployment documentation, and a compact API-backed dashboard. Passive memory candidates remain inactive by default; automatic promotion requires a separate explicit policy.
The auth scope, profile-redaction boundary, idempotent ingestion transaction, and serverless job leasing are the most important review surfaces.