diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 767168e..d00ad64 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000048-creator-advisory-console.json", - "task_id": "DEV-000048", - "task_revision": 2 + "task_file": "dev/tasks/DEV-000049-post-advisory-production-audit.json", + "task_id": "DEV-000049", + "task_revision": 1 } diff --git a/dev/tasks/DEV-000049-post-advisory-production-audit.json b/dev/tasks/DEV-000049-post-advisory-production-audit.json new file mode 100644 index 0000000..98fed22 --- /dev/null +++ b/dev/tasks/DEV-000049-post-advisory-production-audit.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000049", + "task_revision": 1, + "architecture_revision": "ADR-0006", + "base_repo_revision": "4de34ad8f8389d28b1120a3c6203ea32b8c7ff22", + "risk_tier": "E", + "intent": "Add and execute a repeatable read-only production audit for the authenticated Creator advisory boundary after DEV-000048 promotion, proving contract limits, authority-field rejection, explicit degraded behavior and cognitive-worker isolation without selecting or calling an external provider.", + "requirement_ids": ["ADR-0006", "DEV-000046", "DEV-000048", "INV-AUTH-001", "INV-EPI-001"], + "allowed_change_scope": {"modules": [], "paths": ["tests/cognitive_advisory/**", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000049-post-advisory-production-audit.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "ci/**", ".gitea/**", "contracts/**", "modules/**", "adapters/**", "cmd/**", "database/**", "deploy/**", "security/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0}, + "contracts": {"consume": ["creator-advisory-v1", "CreatorSession", "Situation FocusBinding"], "may_change": []}, + "semantic_owner": "none-audit-only", + "canonical_state": [], + "state_transitions": [], + "side_effects_and_privileges": ["read production runtime/container metadata", "issue an ephemeral Creator session and advisory request", "no database mutation, external provider egress or credential persistence"], + "persistence_and_migrations": [], + "failure_retry_restart": ["audit fails closed on missing focus or credential", "audit never prints credentials or session tokens", "advisory content remains ephemeral and is not retried"], + "assumptions": ["production contains at least one active Creator-owned FocusBinding", "Creator credential remains mounted as a Docker secret", "docker-test remains prohibited"], + "acceptance_scenarios": ["authenticated 16 KiB advisory returns only PROVIDER_UNAVAILABLE with empty text and citations", "unauthenticated advisory is denied", "browser authority-field injection is rejected", "the audit sentinel is absent from cognitive-worker logs", "cognitive worker has no database, Commit/Executor, Creator or provider credential environment/mount", "Creator console exposes the advisory UI", "production revision and health remain valid"], + "required_tests": ["PowerShell parse", "architecture preflight", "exact diff scope", "production advisory audit", "production artifact confirmation", "production health", "CI"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"] +} diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 837796d..0a8e2ee 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -1,6 +1,6 @@ # Assistant project status -Audit date: 2026-08-16. Active audit: DEV-000044 revision 3. +Audit date: 2026-08-16. Active audit: DEV-000049 revision 1. ## Delivered @@ -28,4 +28,6 @@ DEV-000045 closes the DEV-000044 artifact-provenance finding for new promotions ADR-0006 and DEV-000046 establish the provider-neutral, non-canonical Creator advisory boundary. DEV-000048 connects it to authenticated Creator Web and adds the advisory console; until a provider/data-egress decision is accepted, it deliberately reports `PROVIDER_UNAVAILABLE` and sends nothing outside the private runtime topology. +DEV-000049 adds and executes a repeatable production audit for that boundary. It proves that an authenticated 16 KiB message reaches only the deterministic unavailable binding, unauthenticated and browser-supplied authority fields fail closed, the request sentinel is absent from cognitive-worker logs, and the cognitive worker has no database, Commit/Executor, Creator or provider credential environment/mount. + These are explicit scope decisions, not hidden incomplete steps in DEV-000043. No production secret is stored in the repository. diff --git a/tests/cognitive_advisory/verify-remote.ps1 b/tests/cognitive_advisory/verify-remote.ps1 new file mode 100644 index 0000000..c127473 --- /dev/null +++ b/tests/cognitive_advisory/verify-remote.ps1 @@ -0,0 +1,86 @@ +[CmdletBinding()] +param( + [string]$DockerContext = 'docker-prod', + [string]$BaseURL = 'https://assistent.cin.su', + [string]$CoreContainer = 'assistant-core-runtime-1', + [string]$PostgresContainer = 'assistant-postgres-1', + [string]$CognitiveContainer = 'assistant-cognitive-worker-1' +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +function Get-ContainerEnvironment([string]$Container) { + $result = @{} + $inspect = ((& docker.exe --context $DockerContext inspect $Container) -join "`n") | ConvertFrom-Json + foreach ($entry in $inspect[0].Config.Env) { + $parts = $entry -split '=', 2 + $result[$parts[0]] = if ($parts.Count -eq 2) { $parts[1] } else { '' } + } + return $result +} + +function Invoke-StatusRequest([string]$Uri, [hashtable]$Headers, [string]$Body) { + $parameters = @{ + Uri = $Uri + Method = 'Post' + ContentType = 'application/json' + SkipHttpErrorCheck = $true + } + if ($Headers.Count -gt 0) { $parameters.Headers = $Headers } + if ($Body) { $parameters.Body = $Body } + return Invoke-WebRequest @parameters +} + +$health = Invoke-RestMethod -Uri "$BaseURL/healthz" -Method Get +if ($health.status -ne 'ok' -or $health.runtime -ne 'creator-web') { throw 'Creator Web health is invalid' } +$page = Invoke-WebRequest -Uri "$BaseURL/" -Method Get +if ($page.StatusCode -ne 200 -or $page.Content -notmatch 'Спросить Домового') { throw 'Advisory console is not exposed' } + +$coreEnvironment = Get-ContainerEnvironment $CoreContainer +$required = @('CREATOR_USERNAME', 'CREATOR_PASSWORD_FILE', 'POSTGRES_DATABASE', 'POSTGRES_USER', 'POSTGRES_PASSWORD') +foreach ($name in $required) { + if (-not $coreEnvironment.ContainsKey($name) -or [string]::IsNullOrWhiteSpace($coreEnvironment[$name])) { + throw "Core runtime is missing $name" + } +} + +$creatorPassword = ((& docker.exe --context $DockerContext exec $CoreContainer sh -c "cat '$($coreEnvironment['CREATOR_PASSWORD_FILE'])'") -join "`n").Trim() +if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($creatorPassword)) { throw 'Creator password secret is unavailable' } +$sessionRequest = @{username = $coreEnvironment['CREATOR_USERNAME']; password = $creatorPassword} | ConvertTo-Json -Compress +$issued = Invoke-RestMethod -Uri "$BaseURL/api/session" -Method Post -ContentType 'application/json' -Body $sessionRequest +$creatorPassword = $null +if (-not $issued.Token) { throw 'Creator session was not issued' } +$authorization = @{Authorization = "Bearer $($issued.Token)"} + +$focusSql = "select session_id,workspace_id from situation.focus_bindings where active=true and actor_id='domovoy-creator-actor' order by session_id desc limit 1" +$focusLine = ((& docker.exe --context $DockerContext exec --env "PGPASSWORD=$($coreEnvironment['POSTGRES_PASSWORD'])" $PostgresContainer psql -U $coreEnvironment['POSTGRES_USER'] -d $coreEnvironment['POSTGRES_DATABASE'] -AtF '|' -c $focusSql) -join '').Trim() +if ($LASTEXITCODE -ne 0 -or $focusLine -notmatch '^[-a-zA-Z0-9._:]+\|[-a-zA-Z0-9._:]+$') { throw 'Active Creator focus is unavailable' } +$focus = $focusLine -split '\|', 2 + +$sentinelPrefix = "audit-$([guid]::NewGuid().ToString('N'))-" +$message = $sentinelPrefix + ('a' * ((16 * 1024) - $sentinelPrefix.Length)) +$advisoryBody = @{message = $message; session_id = $focus[0]; workspace_id = $focus[1]} | ConvertTo-Json -Compress +$advisory = Invoke-RestMethod -Uri "$BaseURL/api/advisory" -Method Post -ContentType 'application/json' -Headers $authorization -Body $advisoryBody +if ($advisory.Version -ne 'creator-advisory-v1' -or $advisory.FinishReason -ne 'PROVIDER_UNAVAILABLE') { throw 'Advisory did not fail closed' } +if (-not [string]::IsNullOrEmpty([string]$advisory.AdvisoryText) -or ($null -ne $advisory.ReferencedExcerptIDs -and @($advisory.ReferencedExcerptIDs).Count -ne 0)) { throw 'Unavailable advisory contained generated content or citations' } + +$unauthenticated = Invoke-StatusRequest "$BaseURL/api/advisory" @{} $advisoryBody +if ($unauthenticated.StatusCode -ne 401) { throw "Unauthenticated advisory status=$($unauthenticated.StatusCode)" } +$injectedBody = @{message = 'authority-injection'; session_id = $focus[0]; workspace_id = $focus[1]; ActorID = 'attacker'} | ConvertTo-Json -Compress +$injected = Invoke-StatusRequest "$BaseURL/api/advisory" $authorization $injectedBody +if ($injected.StatusCode -ne 400) { throw "Authority-field injection status=$($injected.StatusCode)" } + +$cognitiveInspect = ((& docker.exe --context $DockerContext inspect $CognitiveContainer) -join "`n") | ConvertFrom-Json +$forbiddenEnvironment = @($cognitiveInspect[0].Config.Env | Where-Object { ($_ -split '=', 2)[0] -match '(?i)(POSTGRES|DATABASE|COMMIT|EXECUTOR|CREATOR|PASSWORD|TOKEN|PROVIDER|API_KEY)' }) +if ($forbiddenEnvironment.Count -ne 0) { throw 'Cognitive worker has forbidden privileged/provider environment' } +$forbiddenMounts = @($cognitiveInspect[0].Mounts | Where-Object { $_.Destination -match '(?i)(secret|credential|token|postgres|docker)' }) +if ($forbiddenMounts.Count -ne 0) { throw 'Cognitive worker has a forbidden credential or infrastructure mount' } +$recentLogs = ((& docker.exe --context $DockerContext logs --since 5m $CognitiveContainer 2>&1) -join "`n") +if ($recentLogs.Contains($sentinelPrefix)) { throw 'Advisory content leaked into cognitive-worker logs' } + +$issued.Token = $null +$authorization.Clear() +$message = $null +$advisoryBody = $null +Write-Output 'DEV-000049 production Creator advisory boundary audit: PASS'