Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ TRELLO_TOKEN=
# GitHub token for cloning repos and creating PRs
GITHUB_TOKEN=

# GitHub reviewer PAT (separate account for submitting PR reviews)
GITHUB_REVIEWER_TOKEN=

# LLM API keys
OPENROUTER_API_KEY=

Expand Down
39 changes: 30 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,21 @@ Optional (infrastructure):
- `DATABASE_SSL` - Set to `false` to disable SSL for local PostgreSQL (default: enabled)
- `CLAUDE_CODE_OAUTH_TOKEN` - For Claude Code backend (subscription auth)

**Project credentials** (`GITHUB_TOKEN`, `TRELLO_API_KEY`, `TRELLO_TOKEN`, LLM API keys) are stored per-project in the database `project_secrets` table. There is no env var fallback — the database is the sole source of truth for project-scoped secrets.
**Project credentials** (`GITHUB_TOKEN`, `TRELLO_API_KEY`, `TRELLO_TOKEN`, LLM API keys) are stored in the `credentials` table (org-scoped) with optional per-project overrides via `project_credential_overrides`. There is no env var fallback — the database is the sole source of truth for project-scoped secrets.

## Database Configuration

CASCADE stores all project configuration in PostgreSQL (Supabase). The `config/projects.json` file is no longer used at runtime.

### Schema

- `cascade_defaults` - Global defaults (model, iterations, timeouts, budget)
- `projects` - Per-project config (repo, Trello board/lists/labels, budget, backend)
- `agent_configs` - Per-agent-type overrides (model, iterations, backend, prompt), scoped to global or per-project
- `project_secrets` - Per-project credentials (Trello, GitHub, LLM API keys)
- `organizations` - Organization definitions (multi-tenant support)
- `cascade_defaults` - Global defaults per org (model, iterations, timeouts, budget)
- `projects` - Per-project config (repo, base branch, budget, backend)
- `project_integrations` - Integration configs per project (Trello boards/lists/labels as JSONB)
- `agent_configs` - Per-agent-type overrides (model, iterations, backend, prompt), scoped globally, per-org, or per-project
- `credentials` - Org-scoped credentials (API keys, tokens)
- `project_credential_overrides` - Per-project credential overrides (optional, falls back to org defaults)

### Database Scripts

Expand All @@ -96,14 +99,32 @@ npm run db:seed # Seed DB from config/projects.json

### Per-Project Secrets

Store per-project credentials in `project_secrets` table. Falls back to global env vars when not set.
Credentials are stored in the `credentials` table (org-scoped) with optional per-project overrides via `project_credential_overrides`.

```bash
npx tsx tools/manage-secrets.ts set <project-id> TRELLO_API_KEY <value>
npx tsx tools/manage-secrets.ts list <project-id>
npx tsx tools/manage-secrets.ts delete <project-id> <key>
npx tsx tools/manage-secrets.ts create <org-id> <env-var-key> <value> [--name "..."] [--default]
npx tsx tools/manage-secrets.ts list <org-id>
npx tsx tools/manage-secrets.ts set-override <project-id> <env-var-key> <credential-id>
npx tsx tools/manage-secrets.ts remove-override <project-id> <env-var-key>
npx tsx tools/manage-secrets.ts resolve <project-id>
```

### Per-Agent Credential Overrides

Override any credential for a specific agent type. For example, to make the `review` agent use a separate GitHub identity:

```bash
# Create a credential for the reviewer bot
npx tsx tools/manage-secrets.ts create <org-id> GITHUB_TOKEN <reviewer-pat> --name "Reviewer Bot"

# Set agent-scoped overrides (review-related agents use the reviewer token)
npx tsx tools/manage-secrets.ts set-override <project-id> GITHUB_TOKEN <credential-id> --agent-type review
npx tsx tools/manage-secrets.ts set-override <project-id> GITHUB_TOKEN <credential-id> --agent-type respond-to-review
npx tsx tools/manage-secrets.ts set-override <project-id> GITHUB_TOKEN <credential-id> --agent-type respond-to-pr-comment
```

Resolution order: agent+project override → project override → org default → null.

## Claude Code Backend

CASCADE supports using Claude Code SDK as an alternative agent backend. Configure per-project:
Expand Down
3 changes: 2 additions & 1 deletion drizzle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ export default defineConfig({
'./src/db/schema/credentials.ts',
'./src/db/schema/defaults.ts',
'./src/db/schema/projects.ts',
'./src/db/schema/agentConfigs.ts',
'./src/db/schema/integrations.ts',
'./src/db/schema/runs.ts',
'./src/db/schema/secrets.ts',
],
out: './src/db/migrations',
dialect: 'postgresql',
Expand Down
17 changes: 10 additions & 7 deletions src/agents/shared/githubAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ModelSpec } from 'llmist';

import { createProgressMonitor } from '../../backends/progress.js';
import { CUSTOM_MODELS } from '../../config/customModels.js';
import { getProjectReviewerToken } from '../../config/projects.js';
import { getAgentCredential } from '../../config/provider.js';
import { recordInitialComment } from '../../gadgets/sessionState.js';
import { githubClient, withGitHubToken } from '../../github/client.js';
import type { AgentInput, AgentResult, CascadeConfig, ProjectConfig } from '../../types/index.js';
Expand Down Expand Up @@ -219,12 +219,15 @@ export async function executeGitHubAgent<
},
});

// Scope reviewer token for all GitHub agents — if GITHUB_REVIEWER_TOKEN is set,
// all PR interactions (comments, reviews) use the reviewer identity instead of
// the main GITHUB_TOKEN. Individual agents can add further wrapping via wrapExecution.
const reviewerToken = await getProjectReviewerToken(input.project);
const scopedLifecycle = reviewerToken
? () => withGitHubToken(reviewerToken, runLifecycle)
// If this agent type has a dedicated GITHUB_TOKEN override, use it for all
// PR interactions (comments, reviews). Individual agents can add further wrapping via wrapExecution.
const agentGitHubToken = await getAgentCredential(
input.project.id,
definition.agentType,
'GITHUB_TOKEN',
);
const scopedLifecycle = agentGitHubToken
? () => withGitHubToken(agentGitHubToken, runLifecycle)
: runLifecycle;

if (definition.wrapExecution) {
Expand Down
3 changes: 2 additions & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
export { loadEnvConfig, loadEnvConfigSafe, type EnvConfig } from './env.js';
export { getProjectGitHubToken, getProjectReviewerToken } from './projects.js';
export { getProjectGitHubToken } from './projects.js';
export {
loadConfig,
findProjectByBoardId,
findProjectByRepo,
findProjectById,
getAgentCredential,
getProjectSecret,
getProjectSecretOrNull,
getProjectSecrets,
Expand Down
4 changes: 0 additions & 4 deletions src/config/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,3 @@ export async function getProjectGitHubToken(project: ProjectConfig): Promise<str

throw new Error(`Missing GITHUB_TOKEN in database for project '${project.id}'`);
}

export async function getProjectReviewerToken(project: ProjectConfig): Promise<string | null> {
return getProjectSecretOrNull(project.id, 'GITHUB_REVIEWER_TOKEN');
}
14 changes: 14 additions & 0 deletions src/config/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
loadConfigFromDb,
} from '../db/repositories/configRepository.js';
import {
resolveAgentCredential,
resolveAllCredentials,
resolveCredential,
} from '../db/repositories/credentialsRepository.js';
Expand Down Expand Up @@ -92,6 +93,19 @@ export async function getProjectSecrets(projectId: string): Promise<Record<strin
return secrets;
}

/**
* Resolve a credential for a specific agent type.
* Resolution: agent+project override → project override → org default → null.
*/
export async function getAgentCredential(
projectId: string,
agentType: string,
key: string,
): Promise<string | null> {
const orgId = await getOrgIdForProject(projectId);
return resolveAgentCredential(projectId, orgId, agentType, key);
}

export function invalidateConfigCache(): void {
configCache.invalidate();
}
84 changes: 84 additions & 0 deletions src/db/migrations/0004_agent_credential_overrides.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
-- Agent Credential Overrides Migration
-- Adds optional agent_type column to project_credential_overrides,
-- enabling per-agent credential overrides (e.g. review agent uses a different GITHUB_TOKEN).
-- Migrates existing GITHUB_REVIEWER_TOKEN credentials to agent-scoped GITHUB_TOKEN overrides.

BEGIN;

-- 1. Add agent_type column (nullable = project-wide override when NULL)
ALTER TABLE "project_credential_overrides"
ADD COLUMN IF NOT EXISTS "agent_type" text;

-- 2. Drop old unique index (project_id, env_var_key) — replaced by two partial indexes
DROP INDEX IF EXISTS "uq_project_credential_overrides_project_env_var_key";

-- 3. Create partial unique indexes
-- Project-wide overrides: one per (project_id, env_var_key) when agent_type is NULL
CREATE UNIQUE INDEX IF NOT EXISTS "uq_pco_project_env_var_key"
ON "project_credential_overrides" ("project_id", "env_var_key")
WHERE "agent_type" IS NULL;

-- Agent-scoped overrides: one per (project_id, env_var_key, agent_type)
CREATE UNIQUE INDEX IF NOT EXISTS "uq_pco_project_env_var_key_agent_type"
ON "project_credential_overrides" ("project_id", "env_var_key", "agent_type")
WHERE "agent_type" IS NOT NULL;

-- 4. Migrate GITHUB_REVIEWER_TOKEN → agent-scoped GITHUB_TOKEN overrides
-- For each project that has a GITHUB_REVIEWER_TOKEN (via project override OR org default),
-- create agent-scoped GITHUB_TOKEN overrides for review-related agent types.
DO $$
DECLARE
r RECORD;
agent TEXT;
reviewer_agents TEXT[] := ARRAY['review', 'respond-to-review', 'respond-to-pr-comment', 'respond-to-ci'];
BEGIN
-- Case 1: Projects with explicit GITHUB_REVIEWER_TOKEN project overrides
FOR r IN
SELECT pco."project_id", c."id" AS credential_id
FROM "project_credential_overrides" pco
INNER JOIN "credentials" c ON c."id" = pco."credential_id"
WHERE pco."env_var_key" = 'GITHUB_REVIEWER_TOKEN'
AND pco."agent_type" IS NULL
LOOP
FOREACH agent IN ARRAY reviewer_agents
LOOP
INSERT INTO "project_credential_overrides"
("project_id", "env_var_key", "credential_id", "agent_type")
VALUES (r.project_id, 'GITHUB_TOKEN', r.credential_id, agent)
ON CONFLICT DO NOTHING;
END LOOP;
END LOOP;

-- Case 2: Projects using org-default GITHUB_REVIEWER_TOKEN (no project override)
-- Find org defaults for GITHUB_REVIEWER_TOKEN and create agent-scoped overrides
-- for all projects in that org that don't already have a project-level override.
FOR r IN
SELECT p."id" AS project_id, c."id" AS credential_id
FROM "projects" p
INNER JOIN "credentials" c
ON c."org_id" = p."org_id"
AND c."env_var_key" = 'GITHUB_REVIEWER_TOKEN'
AND c."is_default" = true
WHERE NOT EXISTS (
SELECT 1 FROM "project_credential_overrides" pco
WHERE pco."project_id" = p."id"
AND pco."env_var_key" = 'GITHUB_REVIEWER_TOKEN'
AND pco."agent_type" IS NULL
)
LOOP
FOREACH agent IN ARRAY reviewer_agents
LOOP
INSERT INTO "project_credential_overrides"
("project_id", "env_var_key", "credential_id", "agent_type")
VALUES (r.project_id, 'GITHUB_TOKEN', r.credential_id, agent)
ON CONFLICT DO NOTHING;
END LOOP;
END LOOP;

-- 5. Remove old GITHUB_REVIEWER_TOKEN project override rows
DELETE FROM "project_credential_overrides"
WHERE "env_var_key" = 'GITHUB_REVIEWER_TOKEN'
AND "agent_type" IS NULL;
END $$;

COMMIT;
119 changes: 119 additions & 0 deletions src/db/migrations/0005_config_schema_cleanup.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
-- Migration 0005: Configuration Schema Cleanup
--
-- 1. Drop dead project_secrets table
-- 2. Restructure cascade_defaults PK (drop id, promote org_id)
-- 3. Rename backend → agent_backend in agent_configs
-- 4. Rename agent_backend_default → agent_backend in projects
-- 5. Standardize index naming (idx_projects_repo → uq_projects_repo)
-- 6. Extract Trello config into project_integrations table
-- 7. Drop Trello columns from projects

-- ============================================================
-- 1. Drop dead project_secrets table
-- ============================================================
DROP TABLE IF EXISTS "project_secrets";

-- ============================================================
-- 2. Restructure cascade_defaults: drop id, make org_id the PK
-- ============================================================
-- org_id is already NOT NULL UNIQUE, so promote it to PK
ALTER TABLE "cascade_defaults" DROP CONSTRAINT IF EXISTS "cascade_defaults_pkey";
ALTER TABLE "cascade_defaults" DROP COLUMN IF EXISTS "id";
ALTER TABLE "cascade_defaults" ADD PRIMARY KEY ("org_id");

-- Add created_at (every other table has it)
ALTER TABLE "cascade_defaults" ADD COLUMN IF NOT EXISTS "created_at" timestamp DEFAULT now();

-- ============================================================
-- 3. Rename backend → agent_backend in agent_configs
-- ============================================================
ALTER TABLE "agent_configs" RENAME COLUMN "backend" TO "agent_backend";

-- ============================================================
-- 4. Rename agent_backend_default → agent_backend in projects
-- ============================================================
ALTER TABLE "projects" RENAME COLUMN "agent_backend_default" TO "agent_backend";

-- ============================================================
-- 5. Standardize index naming
-- ============================================================
ALTER INDEX IF EXISTS "idx_projects_repo" RENAME TO "uq_projects_repo";

-- ============================================================
-- 6. Create project_integrations table
-- ============================================================
CREATE TABLE IF NOT EXISTS "project_integrations" (
"id" serial PRIMARY KEY,
"project_id" text NOT NULL REFERENCES "projects"("id") ON DELETE CASCADE,
"type" text NOT NULL,
"config" jsonb NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now()
);

CREATE UNIQUE INDEX IF NOT EXISTS "uq_project_integrations_project_type"
ON "project_integrations" ("project_id", "type");

-- ============================================================
-- 7. Migrate Trello data from flat columns to JSONB
-- ============================================================
INSERT INTO "project_integrations" ("project_id", "type", "config")
SELECT
"id",
'trello',
jsonb_strip_nulls(jsonb_build_object(
'boardId', "trello_board_id",
'lists', jsonb_strip_nulls(jsonb_build_object(
'briefing', "trello_list_briefing",
'stories', "trello_list_stories",
'planning', "trello_list_planning",
'todo', "trello_list_todo",
'inProgress', "trello_list_in_progress",
'inReview', "trello_list_in_review",
'done', "trello_list_done",
'merged', "trello_list_merged",
'debug', "trello_list_debug"
)),
'labels', jsonb_strip_nulls(jsonb_build_object(
'readyToProcess', "trello_label_ready_to_process",
'processing', "trello_label_processing",
'processed', "trello_label_processed",
'error', "trello_label_error"
)),
'customFields', CASE
WHEN "trello_custom_field_cost" IS NOT NULL
THEN jsonb_build_object('cost', "trello_custom_field_cost")
ELSE NULL
END
))
FROM "projects"
WHERE "trello_board_id" IS NOT NULL;

-- ============================================================
-- 8. Drop Trello columns from projects
-- ============================================================
ALTER TABLE "projects"
DROP COLUMN IF EXISTS "trello_board_id",
DROP COLUMN IF EXISTS "trello_list_briefing",
DROP COLUMN IF EXISTS "trello_list_stories",
DROP COLUMN IF EXISTS "trello_list_planning",
DROP COLUMN IF EXISTS "trello_list_todo",
DROP COLUMN IF EXISTS "trello_list_in_progress",
DROP COLUMN IF EXISTS "trello_list_in_review",
DROP COLUMN IF EXISTS "trello_list_done",
DROP COLUMN IF EXISTS "trello_list_merged",
DROP COLUMN IF EXISTS "trello_list_debug",
DROP COLUMN IF EXISTS "trello_label_ready_to_process",
DROP COLUMN IF EXISTS "trello_label_processing",
DROP COLUMN IF EXISTS "trello_label_processed",
DROP COLUMN IF EXISTS "trello_label_error",
DROP COLUMN IF EXISTS "trello_custom_field_cost";

DROP INDEX IF EXISTS "idx_projects_trello_board_id";

-- ============================================================
-- 9. Expression index for Trello board lookup via integrations
-- ============================================================
CREATE INDEX IF NOT EXISTS "idx_pi_trello_board"
ON "project_integrations" ((config->>'boardId'))
WHERE "type" = 'trello';
Loading