From 7779dc37d11383c88de5f073ff857bb74a8b0010 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 23 Aug 2026 02:59:14 +0300 Subject: [PATCH 1/2] fix: protect agent canary database credentials --- cmd/agent-canary/main.go | 35 ++++++++++++++++++- cmd/agent-canary/main_test.go | 23 ++++++++++++ deploy/docker-compose.prod.yml | 6 +++- dev/ACTIVE_TASK.json | 4 +-- ...000194-agent-canary-protected-storage.json | 23 ++++++++++++ 5 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 cmd/agent-canary/main_test.go create mode 100644 dev/tasks/DEV-000194-agent-canary-protected-storage.json diff --git a/cmd/agent-canary/main.go b/cmd/agent-canary/main.go index 5fc397b..f6d6964 100644 --- a/cmd/agent-canary/main.go +++ b/cmd/agent-canary/main.go @@ -6,12 +6,16 @@ import ( "flag" "fmt" "os" + "path/filepath" + "strings" "time" "git.cin.su/m/Assistant/adapters/postgres" + secretcontracts "git.cin.su/m/Assistant/contracts/secrets" "git.cin.su/m/Assistant/contracts/work" "git.cin.su/m/Assistant/modules/work/application" "git.cin.su/m/Assistant/modules/work/ports" + platformsecrets "git.cin.su/m/Assistant/platform/secrets" ) type memoryStore struct{ writes int } @@ -37,7 +41,12 @@ func main() { var store ports.ExecutionStore = &memoryStore{} var closeStore func() if *postgresMode { - repository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: os.Getenv("WORK_POSTGRES_PASSWORD")}) + password, err := resolveDatabasePassword() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + repository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: password}) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) @@ -67,3 +76,27 @@ func main() { os.Exit(1) } } + +func resolveDatabasePassword() (string, error) { + if _, ok := os.LookupEnv("WORK_POSTGRES_PASSWORD"); ok { + return "", fmt.Errorf("WORK_POSTGRES_PASSWORD direct environment secret is forbidden") + } + path, ref := os.Getenv("WORK_POSTGRES_PASSWORD_FILE"), os.Getenv("WORK_POSTGRES_CREDENTIAL_REF") + if path == "" || ref == "" { + return "", fmt.Errorf("WORK_POSTGRES_PASSWORD_FILE and WORK_POSTGRES_CREDENTIAL_REF are required") + } + root := os.Getenv("SECRET_BACKEND_ROOT") + if root == "" { + root = "/run/secrets" + } + cleanRoot := filepath.Clean(root) + cleanPath := filepath.Clean(path) + if cleanPath != cleanRoot && !strings.HasPrefix(cleanPath, cleanRoot+string(os.PathSeparator)) { + return "", fmt.Errorf("database password file escapes protected secret root") + } + resolver, err := platformsecrets.NewFileResolver(cleanRoot) + if err != nil { + return "", err + } + return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerWorkWorker, Path: cleanPath}) +} diff --git a/cmd/agent-canary/main_test.go b/cmd/agent-canary/main_test.go new file mode 100644 index 0000000..7f86f8c --- /dev/null +++ b/cmd/agent-canary/main_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "path/filepath" + "testing" +) + +func TestResolveDatabasePasswordRejectsDirectEnvironmentSecret(t *testing.T) { + t.Setenv("WORK_POSTGRES_PASSWORD", "literal-secret") + if _, err := resolveDatabasePassword(); err == nil { + t.Fatal("direct database password environment variable was accepted") + } +} + +func TestResolveDatabasePasswordRejectsPathOutsideProtectedRoot(t *testing.T) { + root := t.TempDir() + t.Setenv("SECRET_BACKEND_ROOT", root) + t.Setenv("WORK_POSTGRES_PASSWORD_FILE", filepath.Join(root, "..", "outside")) + t.Setenv("WORK_POSTGRES_CREDENTIAL_REF", "credential://domovoy/work/db-password/v1") + if _, err := resolveDatabasePassword(); err == nil { + t.Fatal("database password path outside protected root was accepted") + } +} diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index 65895c1..d337469 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -99,8 +99,12 @@ services: POSTGRES_PORT: "5432" POSTGRES_DATABASE: ${POSTGRES_DB:-assistant} WORK_POSTGRES_USER: work_role - WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD} + WORK_POSTGRES_CREDENTIAL_REF: credential://domovoy/work/db-password/v1 + WORK_POSTGRES_PASSWORD_FILE: /run/secrets/work_db_password + SECRET_BACKEND_ROOT: /run/secrets depends_on: { postgres: { condition: service_healthy } } + volumes: + - assistant_core_secrets:/run/secrets:ro agent-worker: build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: agent-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } } diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 4633337..b7a5e03 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000193-fleet-live-degraded-evidence.json", - "task_id": "DEV-000193", + "task_file": "dev/tasks/DEV-000194-agent-canary-protected-storage.json", + "task_id": "DEV-000194", "task_revision": 1 } diff --git a/dev/tasks/DEV-000194-agent-canary-protected-storage.json b/dev/tasks/DEV-000194-agent-canary-protected-storage.json new file mode 100644 index 0000000..0ed1fe4 --- /dev/null +++ b/dev/tasks/DEV-000194-agent-canary-protected-storage.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000194", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "bb18cb4f1c18aee7c22ddbe7259123f96f779b8c", + "risk_tier": "E", + "intent": "Move the explicit production Agent Execution dry-run canary from direct database secret environment input to protected file/CredentialRef storage and preserve durable lease/callback evidence.", + "requirement_ids": ["Agent Execution runtime and recovery", "protected canary configuration", "durable lease/callback canary"], + "allowed_change_scope": {"modules": ["agent-canary", "deployment"], "paths": ["cmd/agent-canary/main.go", "cmd/agent-canary/main_test.go", "deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000194-agent-canary-protected-storage.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", ".gitea/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["existing protected secret resolver", "existing ExecutionStore and dry-run canary contract"], "may_change": ["agent-canary runtime configuration"]}, + "semantic_owner": "work-agent-execution", + "canonical_state": ["work.agent_dispatch_intents", "work.agent_leases", "work.agent_callback_heads"], + "state_transitions": ["explicit canary -> durable dispatch intent -> lease -> heartbeat callback -> result callback"], + "side_effects_and_privileges": ["manual opt-in dry-run only; protected database credential read; no external provider or source mutation"], + "persistence_and_migrations": ["none"], + "failure_retry_restart": ["missing/invalid protected secret fails before database open; direct secret env is rejected; canary remains explicit opt-in"], + "assumptions": ["the production compose secret volume already contains the work database credential reference used by agent-worker"], + "acceptance_scenarios": ["direct password environment is rejected", "path outside protected root is rejected", "compose renders canary with protected file/ref only", "production opt-in canary writes lease and callback evidence", "normal agent-worker remains recovery-only without an external bridge"], + "required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "git diff --check", "CI", "production canary and public health check"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"] +} -- 2.54.0 From 348856a9c24dfb4ab6d671eb5cafb8da57419df9 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 23 Aug 2026 03:03:43 +0300 Subject: [PATCH 2/2] chore: align canary task base with main --- dev/tasks/DEV-000194-agent-canary-protected-storage.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tasks/DEV-000194-agent-canary-protected-storage.json b/dev/tasks/DEV-000194-agent-canary-protected-storage.json index 0ed1fe4..1d30773 100644 --- a/dev/tasks/DEV-000194-agent-canary-protected-storage.json +++ b/dev/tasks/DEV-000194-agent-canary-protected-storage.json @@ -2,7 +2,7 @@ "task_id": "DEV-000194", "task_revision": 1, "architecture_revision": "ADR-0018", - "base_repo_revision": "bb18cb4f1c18aee7c22ddbe7259123f96f779b8c", + "base_repo_revision": "c1db571a39514e65156123b7f9394645183cf2ab", "risk_tier": "E", "intent": "Move the explicit production Agent Execution dry-run canary from direct database secret environment input to protected file/CredentialRef storage and preserve durable lease/callback evidence.", "requirement_ids": ["Agent Execution runtime and recovery", "protected canary configuration", "durable lease/callback canary"], -- 2.54.0