Imported from factorial-io/scotty (
AGENTS.md). Install upstream withnpx skills add factorial-io/scotty. Copyright stays with the author.
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Scotty is a micro Platform-as-a-Service (PaaS) for managing Docker Compose-based applications:
- scotty: HTTP server (REST API + WebSocket) for managing Docker Compose apps
- scottyctl: CLI client for the scotty server
- scotty-core: Shared business logic (Docker operations, settings, tasks)
- scotty-types: Shared type definitions (TypeScript-compatible via ts-rs)
- frontend: SvelteKit web interface (tightly coupled with API, no backwards compatibility needed)
- ts-generator: Utility for generating TypeScript bindings from Rust types
Development Commands
# Tests
cargo test # Run all tests
cargo test test_name -- --nocapture # Specific test with output
RUST_LOG=debug cargo test test_name -p scotty -- --nocapture # With debug logging
# Server (use .env file for SCOTTY__API__AUTH_MODE=dev etc.)
SCOTTY__API__AUTH_MODE=dev cargo run --bin scotty # Dev mode (no auth)
RUST_LOG=info cargo run --bin scotty # With logging
cargo run --bin scotty -- config # View configuration
# scottyctl
cargo run --bin scottyctl -- <command>
cargo run --bin scottyctl -- --server http://localhost:21342 --access-token <token> app:list
# Or via env: SCOTTY_SERVER=http://localhost:21342 SCOTTY_ACCESS_TOKEN=<token>
# Frontend (uses bun, not npm)
cd frontend && bun install && bun run dev # Development server
bun run build # Production build
bun run check # Type checking
bun run lint # Prettier + ESLint (must pass before push)
# Prerequisites: start Traefik for local development
cd apps/traefik && docker compose up -d
Architecture
Scotty Server (scotty/src/)
Entry Point: main.rs — initializes AppState (settings, Docker client, task manager), sets up OpenTelemetry, spawns HTTP server and background tasks.
Key Modules:
api/router.rs: Axum router with OpenAPI docs (utoipa)api/rest/handlers/: REST endpoints —apps/(create, list, run, actions, notifications),admin/(assignments, permissions, roles, scopes),scopes/(user-facing),blueprints.rs,landing.rs(Traefik fallback routing),login.rs,tasks.rs,health.rs,info.rsapi/websocket/: Real-time features —handlers/(auth, logs, shell, tasks),messaging.rs(protocol),client.rs(connection mgmt)api/auth_core.rs: Core authentication logicapi/middleware/: Casbin RBAC authorizationapi/rate_limiting/: Per-tier rate limitingdocker/state_machine_handlers/: App lifecycle steps (create dir, save files, docker login, compose up, load balancer config, post actions, wait for containers, etc.)docker/services/: Long-running log streaming and shell sessionsdocker/loadbalancer/: Traefik/HAProxy config generationonepassword/: 1Password secrets — resolvesop://URIs in app env vars (two-pass: 1Password lookup, then env var substitution)oauth/: OAuth 2.0 — device flow (CLI) and web flow (/oauth/authorize,/api/oauth/callback,/oauth/exchange)services/authorization/: Casbin RBAC (scopes, roles, permissions)tasks/: Task execution and output streamingnotification/: Log, Webhook, Mattermost, GitLab notificationsstatic_files.rs: Embedded frontend servingmetrics/: Collectors for app list, HTTP requests, memory usage, Tokio runtime, etc.
AppState (shared via Arc): Settings, Docker client (Bollard), task manager, authorization service, metrics collectors.
Authorization System
Uses Casbin for RBAC. Config: config/casbin/policy.yaml. Implementation: scotty/src/services/authorization/casbin.rs. Tests: scotty/tests/authorization_domain_test.rs.
Permissions: view, manage, create, destroy, shell, logs, admin_read, admin_write, action_read, action_write, action_manage, action_approve
Assignment matching (by precedence): exact email (user@factorial.io) > domain pattern (@factorial.io) > wildcard (*). Wildcard is always additive. Domain patterns prevent subdomain attacks. Case-insensitive per RFC 5321.
# config/casbin/policy.yaml
scopes:
client-a: { description: "Client A Production" }
qa: { description: "QA Environment" }
roles:
admin: { permissions: ['*'], description: "Full access" }
developer: { permissions: ['view', 'manage', 'create', 'shell', 'logs'], description: "Dev access" }
viewer: { permissions: ['view'], description: "Read-only" }
assignments:
stephan@factorial.io: # Exact match (highest priority)
- { role: admin, scopes: ['*'] }
'@factorial.io': # Domain match (fallback)
- { role: developer, scopes: ['client-a', 'qa'] }
'*': # Wildcard (always additive)
- { role: viewer, scopes: ['default'] }
scottyctl (scottyctl/src/)
Commands (colon-separated namespace):
app:list, create, destroy, run, start, stop, rebuild, purge, adopt, info, action, logs, shelladmin:scopes:*, roles:*, assignments:*, permissions:*auth:login, logout, status, refreshblueprint:list, infonotify:add, removecompletion,test
Global flags: --server, --access-token, --debug, --bypass-version-check
Preflight (preflight.rs): Checks client/server version compatibility via /api/v1/info before running commands. Bypass with --bypass-version-check.
File upload (app:create): Files collected via utils/files.rs:collect_files(), gzip-compressed and base64-encoded. Supports .scottyignore (gitignore-style patterns via ignore crate). Auto-excludes .DS_Store, .git/.
Auth: OAuth device flow + bearer tokens via env vars or CLI args. Core logic in auth/ (device flow, token storage, caching).
Blueprints
Reusable app templates defining required/public services, port mappings, lifecycle actions (PostCreate, PostRun, PostRebuild), and custom actions per service. Available via blueprint:list/blueprint:info and GET /api/v1/authenticated/blueprints.
Custom Actions
Custom actions allow users to define and execute arbitrary commands on app services. They support an approval workflow for security control.
Permission Model
Custom actions use four dedicated permissions:
| Permission | Description |
|---|---|
action_read |
Execute read-only actions (no side effects) |
action_write |
Execute actions that modify state |
action_manage |
Create, list, and delete custom actions for apps in user's scope |
action_approve |
Approve/reject pending actions (admin-level) |
Action Status Workflow
Actions go through an approval workflow:
┌─────────┐ approve ┌──────────┐
│ Pending │ ───────────────► │ Approved │ ◄─── Can be executed
└─────────┘ └──────────┘
│ │
│ reject │ revoke
▼ ▼
┌──────────┐ ┌─────────┐
│ Rejected │ │ Revoked │
└──────────┘ └─────────┘
Actions can also expire if a TTL is configured → Expired
Only Approved actions can be executed.
CLI Commands
Creating a custom action:
# Create an action that requires action_write permission
scottyctl action:create my-app deploy-db \
--description "Run database migrations" \
--permission action_write \
--command "web:php artisan migrate" \
--command "worker:php artisan queue:restart"
# Create a read-only action (action_read permission)
scottyctl action:create my-app check-status \
--description "Check application health" \
--permission action_read \
--command "web:php artisan health:check"
Listing actions:
# List all custom actions for an app
scottyctl action:list my-app
Getting action details:
# View full details of an action
scottyctl action:get my-app deploy-db
Running an action:
# Execute an approved action
scottyctl app:action my-app deploy-db
Deleting an action:
# Remove a custom action
scottyctl action:delete my-app deploy-db
Admin Commands
Users with action_approve permission can manage the approval workflow:
# List all pending actions across all apps
scottyctl admin:actions:pending
# View details of a pending action
scottyctl admin:actions:get my-app deploy-db
# Approve an action (with optional comment)
scottyctl admin:actions:approve my-app deploy-db --comment "Reviewed and approved"
# Reject an action
scottyctl admin:actions:reject my-app deploy-db --comment "Security concern with command"
# Revoke a previously approved action
scottyctl admin:actions:revoke my-app deploy-db --comment "No longer needed"
Role Configuration
To grant custom action permissions, update config/casbin/policy.yaml:
roles:
# Developers can manage and execute actions
developer:
permissions: ['view', 'manage', 'action_read', 'action_write', 'action_manage']
description: Developer with action management
# Action approvers (security team)
action_approver:
permissions: ['view', 'action_approve']
description: Can approve/reject custom actions
# Full admin
admin:
permissions: ['*']
description: Full access including action approval
API Endpoints
| Method | Endpoint | Permission | Description |
|---|---|---|---|
POST |
/api/v1/authenticated/apps/{app}/custom-actions |
action_manage |
Create action |
GET |
/api/v1/authenticated/apps/{app}/custom-actions |
action_manage |
List actions |
GET |
/api/v1/authenticated/apps/{app}/custom-actions/{name} |
action_manage |
Get action details |
DELETE |
/api/v1/authenticated/apps/{app}/custom-actions/{name} |
action_manage |
Delete action |
POST |
/api/v1/authenticated/apps/{app}/actions |
action_read or action_write |
Execute action |
GET |
/api/v1/authenticated/admin/actions/pending |
action_approve |
List pending |
POST |
/api/v1/authenticated/admin/actions/{app}/{name}/approve |
action_approve |
Approve action |
POST |
/api/v1/authenticated/admin/actions/{app}/{name}/reject |
action_approve |
Reject action |
POST |
/api/v1/authenticated/admin/actions/{app}/{name}/revoke |
action_approve |
Revoke action |
Implementation Details
- Data Model:
scotty-core/src/settings/custom_action.rs - API Handlers:
scotty/src/api/rest/handlers/apps/custom_action*.rs - CLI:
scottyctl/src/commands/apps/actions.rs,scottyctl/src/commands/admin.rs - Tests:
scotty-core/src/settings/custom_action_tests.rs
Configuration
Settings loaded via config crate: 1) defaults in code, 2) config files (YAML/TOML), 3) env vars (prefix: SCOTTY__).
Server env vars: SCOTTY__API__AUTH_MODE=dev (disable auth), SCOTTY__TELEMETRY=metrics,traces, SCOTTY__API__BEARER_TOKENS__<NAME> (use env vars, not config files).
scottyctl env vars: SCOTTY_SERVER (default: http://localhost:21342), SCOTTY_ACCESS_TOKEN.
Testing
Unit tests colocated with implementation. Integration tests in scotty/tests/. Uses axum-test for HTTP testing, wiremock for mocking external services.
Observability
cd observability && docker compose up -d
Grafana: http://grafana.ddev.site (admin/admin) | Jaeger: http://jaeger.ddev.site | VictoriaMetrics: http://vm.ddev.site
Release Process
Uses release-please (PR-driven, fully in CI). Do not manually bump versions or edit changelogs.
- Write conventional commits (
feat:,fix:,feat!:/BREAKING CHANGE:for majors). The commit type determines the version bump and the changelog section. - On every push to
main, therelease-pleaseworkflow maintains a standing release PR that bumps the shared workspace version ([workspace.package].versioninCargo.toml) and regeneratesCHANGELOG.md. - Merging that release PR is the release: release-please creates the
vX.Y.Ztag + GitHub Release, then the same workflow uploadsscottyctlbinaries, bumps the Homebrew tap, and publishes the versioned Docker image.
Config lives in release-please-config.json and .release-please-manifest.json. The pre-push hook via cargo-husky still enforces local quality checks.
Project Management
Uses beans, an agentic-first issue tracker. Issues ("beans") are managed via the beans CLI. The .beans/ directory is committed to the repository. Agents should use beans instead of todo lists to track work, create/update issues, and manage task dependencies.
Git Rules
- Never delete
frontend/build/.gitkeepfrom git - No emojis in commit messages
- Use conventional commits
You are required to load .ai/kenkeep/ENTRY.md, the small curated entry catalog for this repo. Enter there and descend using progressive disclosure principles.