Skip to content
Open
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
5 changes: 3 additions & 2 deletions .env.quickstart.example
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ GIT_USER_EMAIL=

# GITHUB_ORG
# What: GitHub organisation where workspace repos are created
# (e.g. "my-org" creates repos like my-org/specflow-workspace-ws-1).
# (e.g. "my-org" creates repos like my-org/specflow-workspace1).
# How: Leave blank to use GIT_USER_NAME (your GitHub login).
# Set explicitly if your git user.name is not your GitHub account
# or organization name.
Expand All @@ -156,7 +156,8 @@ GIT_COMMITTER_USER_EMAIL=specflow@system.local

# WORKSPACE_REPO_PREFIX
# What: Naming prefix for auto-created workspace repositories.
# Repos are created as {GITHUB_ORG}/{prefix}-ws-1, {prefix}-ws-2, etc.
# Repos are created as {GITHUB_ORG}/{prefix}1, {prefix}2, ... (no separator before
# the number). Adding sets later continues this numbering.
# How: Change if you want a different naming convention for workspace repos.
WORKSPACE_REPO_PREFIX=specflow-workspace

Expand Down
10 changes: 6 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ MCP server: mcp_server

**Business constraints**: All external deps mocked. Sandboxed agents, no credentials. No infra provisioning, no customer system access, no deploys during generation.

**Key technical decisions**: K8s over Cloud Run (8+ hr tasks). Firestore (distributed locking, crash recovery). NFS/Filestore (git perf, persistence). State machines (workspace safety). Workspace pool (isolation, P10Y per-repo).
**Key technical decisions**: K8s over Cloud Run (8+ hr tasks). Pluggable persistence behind `IDatabase` — SQLite by default locally/in Docker (`DATABASE_TYPE=sqlite`, one file at `~/.specflow/db/specflow.db` shared across projects), Firestore only when pointing at a hosted GCP instance; transactional locking + crash recovery either way. NFS/Filestore (git perf, persistence). State machines (workspace safety). Workspace pool (isolation, P10Y per-repo).

**Coding patterns**: TDD. Small functions. Do Not Repeat Yourself. Maximize code reuse. Patterns should enforce compile time and unit tests validation. Use Pydantic, dataclasses, OOP over simple Python collections and raw strings. Always use Enums, SRP and Open/Close principle to have precise changes.
Imports always on top of file! No lazy imports.
Expand All @@ -27,6 +27,8 @@ Imports always on top of file! No lazy imports.
- **While editing a specific file or package:** `make check-complexity-cc FILE=app/...` and `make check-complexity-mi FILE=app/...`.
- **Quick local average only** (no diff to `main`): `make check-complexity`.

**Workspace pool operator management** (listing sets, reclaiming workspaces, expanding/shrinking the pool, and the TUI workspaces screen) is documented in `docs/backend/workspace-pool-management.md`. Two rules to uphold when touching it: `classify_reclaim` (`backend/app/schemas/workspace_pool_management.py`) is the **only** definition of what reclaiming a workspace does — the listing badge and the reclaim dispatch both read it; and pool expansion seeds workspace rows **last** and with `replace=False`, so a slot is never published before its repo has a P10Y id and a live document is never reset.

**Key paths:**
- `backend/app/services/` — business logic (estimation, workspace_pool, crash_recovery, retry)
- `backend/app/workflows/` — orchestration (generate_poc, multi_workspace_estimation_p10y)
Expand All @@ -47,7 +49,7 @@ Imports always on top of file! No lazy imports.
Spec analysis and implementation planning happen **locally in the user's IDE** via MCP tools (`check_specification_completeness`, `run_planning`) — the backend is not involved until `run_generation` is called. Once invoked, the backend runs as one continuous, locked execution — the user cannot edit plans or intervene while the workflow is active:

0. **Local (no backend)** — user calls `check_specification_completeness` and `run_planning` in their IDE; both produce markdown files in the user's project directory. Repeatable, free, no session created.
1. **File upload + contract validation** — `run_generation` uploads the user's `specs/`, optional `src/`, and `outputs_dir/` to the primary workspace. The contract validator (`backend/app/services/contract_validator.py` + `run_contract_validator` in `workflow_steps.py`) fuzzy-matches required files, runs keyword-only MCP prune, converts plans markdown→JSON, and writes Firestore plan data. If any required file is missing or unparseable, `run_generation` fails immediately with a human-readable message. No spec/planning/KB agents in this step (plan conversion agent only).
1. **File upload + contract validation** — `run_generation` uploads the user's `specs/`, optional `src/`, and `outputs_dir/` to the primary workspace. The contract validator (`backend/app/services/contract_validator.py` + `run_contract_validator` in `workflow_steps.py`) fuzzy-matches required files, runs keyword-only MCP prune, converts plans markdown→JSON, and persists the plan data. If any required file is missing or unparseable, `run_generation` fails immediately with a human-readable message. No spec/planning/KB agents in this step (plan conversion agent only).
2. **KB init + Generation** — Rosetta unpacking and KB init run as the first generation step (not before). Then code generation runs across all workspaces in parallel, committing incrementally.
3. **Deploy & E2E** — deploy loop starts immediately after generation; no user pause in between.

Expand Down Expand Up @@ -83,7 +85,7 @@ The local MCP tools (`check_specification_completeness`, `run_planning`) and the
- Files found in wrong subdirectory (e.g. `IMPLEMENTATION_PLAN.md` at outputs_dir root, or under `analysis/`) are moved to the canonical location.
- Multiple candidate files matching the same canonical name → error (ambiguous, refuse to guess).
- Missing required file → `run_generation` returns short error: "Missing required files: ... — run the relevant MCP tool (`check_specification_completeness` or `run_planning`) to produce them."
- After normalization, the validator runs markdown→JSON conversion and writes `planning_data` / `e2e_planning_data` to Firestore. If conversion fails, return a short error referencing the offending file.
- After normalization, the validator runs markdown→JSON conversion and persists `planning_data` / `e2e_planning_data`. If conversion fails, return a short error referencing the offending file.

**Skill ↔ MCP-tool argument parity:** Skills are drop-in replacements for backend tools. They MUST accept the same arguments (`spec_path`, `outputs_dir`) and write to paths derived from those arguments. No hardcoded `docs/...` in skill instructions.

Expand Down Expand Up @@ -161,7 +163,7 @@ Retry with unarchived code reuses exact same `workspace_ids`. If inaccessible, r
Background jobs call `stuck_detected()` on estimation only. Workspace stays ALLOCATED with code intact.

### VII — State machine is only writer of status/checkpoint/workspace_phases
Nothing outside `backend/app/state/` may write these to Firestore. CI guard enforces. Direct write outside `state/` = bug.
Nothing outside `backend/app/state/` may write these to the database. CI guard enforces. Direct write outside `state/` = bug.

### VIII — Checkpoints never go backward
`advance_checkpoint()` validates strictly forward. Retry resumes from saved checkpoint.
Expand Down
19 changes: 13 additions & 6 deletions backend/app/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,16 @@


def _has_admin_permission(request: Request) -> bool:
"""Whether the caller holds the explicit admin role.

The single admin predicate — ``require_admin`` delegates here so an ownership bypass and
an admin-only route can never disagree about who counts as an admin. ``"*"`` is *not*
accepted: wildcard permissions are rejected at key creation (see
``test_wildcard_permission_rejected``), so honouring one here would grant admin through a
value the API refuses to issue.
"""
permissions = getattr(request.state, "permissions", [])
return "admin" in permissions or "*" in permissions
return "admin" in permissions


async def verify_generation_session_owner(
Expand Down Expand Up @@ -249,8 +257,8 @@ async def require_admin(request: Request) -> None:
"""
FastAPI dependency: Verify the authenticated user has admin permissions.

Admin access is granted if the user's permissions list contains either
"*" (wildcard / all permissions) or "admin" (explicit admin role).
Admin access requires the explicit "admin" role in the key's permissions list. Wildcard
("*") is not accepted — key creation rejects it, so it can never be a legitimate grant.

Usage:
@router.post("/keys")
Expand All @@ -262,12 +270,11 @@ async def create_api_key(
Raises:
HTTPException: 403 if user lacks admin permissions
"""
permissions = getattr(request.state, "permissions", [])
if "admin" in permissions:
if _has_admin_permission(request):
return
logger.warning(
f"Admin access denied for user {getattr(request.state, 'user_email', 'unknown')} "
f"with permissions {permissions}"
f"with permissions {getattr(request.state, 'permissions', [])}"
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/v1/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ async def create_api_key(
"user_id": "john.doe@company.com",
"user_name": "John Doe",
"expires_days": 365,
"permissions": ["*"]
"permissions": ["admin"]
}'
```

Expand Down
Loading