Compare commits

..
Author SHA1 Message Date
m 9818f06e05 docs: propose OpenAI advisory binding
CI / test (push) Successful in 11s
CI / test (pull_request) Successful in 11s
2026-08-16 20:34:32 +03:00
m 67390c8297 Merge pull request 'DEV-000049: audit production advisory boundary' (#22) from codex/post-advisory-production-audit-v1 into main
CI / test (push) Successful in 11s
2026-08-16 17:31:43 +00:00
m 1dc9f40595 test: audit production advisory boundary
CI / test (push) Successful in 11s
CI / test (pull_request) Successful in 11s
2026-08-16 20:30:12 +03:00
m 4de34ad8f8 Merge pull request 'DEV-000048 r2: honor advisory message size contract' (#21) from codex/creator-advisory-console-v1 into main
CI / test (push) Successful in 11s
2026-08-16 17:23:50 +00:00
6 changed files with 263 additions and 4 deletions
+3 -3
View File
@@ -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/ARCH-000009-openai-advisory-binding-proposal.json",
"task_id": "ARCH-000009",
"task_revision": 1
}
@@ -0,0 +1,23 @@
{
"task_id": "ARCH-000009",
"task_revision": 1,
"architecture_revision": "ADR-0006",
"base_repo_revision": "67390c82976aa9ff69c9de5a88e974836c89bc87",
"risk_tier": "E",
"intent": "Produce a proposal-only decision for an initial OpenAI Responses API binding for Creator advisory, with an immutable model snapshot, explicit default-retention disclosure, no tools or provider memory, bounded data egress, isolated credential, evaluation/canary gates and fail-closed rollback.",
"requirement_ids": ["ADR-0006", "INV-EPI-001"],
"allowed_change_scope": {"modules": [], "paths": ["docs/OPENAI_ADVISORY_BINDING_PROPOSAL.md", "dev/ACTIVE_TASK.json", "dev/tasks/ARCH-000009-openai-advisory-binding-proposal.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "ci/**", ".gitea/**", "contracts/**", "modules/**", "adapters/**", "cmd/**", "database/**", "deploy/**", "security/**", "tests/**"]},
"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", "ADR-0006"], "may_change": []},
"semantic_owner": "none-proposal-only",
"canonical_state": [],
"state_transitions": [],
"side_effects_and_privileges": ["select this proposal in the active task pointer", "no API key creation, provider call, egress, production mutation or ADR acceptance"],
"persistence_and_migrations": [],
"failure_retry_restart": ["proposal preserves deterministic PROVIDER_UNAVAILABLE fallback", "no automatic retry", "provider timeout or invalid response cannot fabricate advisory text"],
"assumptions": ["OpenAI is the recommended initial provider but is not accepted by this proposal alone", "default API abuse-monitoring retention may be up to 30 days unless the account is approved for ZDR/MAM", "docker-test remains prohibited"],
"acceptance_scenarios": ["proposal names fixed API origin, endpoint and immutable model snapshot", "proposal defines exact request features and forbidden tools/state", "default-retention and ZDR behavior are explicit", "data classes permitted before and after ZDR are separated", "credential isolation and log redaction are explicit", "evaluation, canary, spend/rate, health and rollback gates are defined", "implementation and production egress remain unapproved"],
"required_tests": ["official-source verification", "architecture preflight", "exact diff scope", "CI"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -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"]
}
+125
View File
@@ -0,0 +1,125 @@
# OpenAI advisory binding proposal V1
Status: **PROPOSED — not accepted and not enabled**
Task: `ARCH-000009` revision 1
Parent boundary: `ADR-0006`
## Recommendation
Use OpenAI only as the first replaceable provider behind the existing `creator-advisory-v1` Cognitive Gateway boundary:
- fixed origin: `https://api.openai.com`;
- endpoint: `POST /v1/responses`;
- model snapshot: `gpt-5.4-mini-2026-03-17`;
- reasoning effort: `low` for the initial evaluation and canary;
- `store: false`, synchronous response only, no background mode;
- no tools, function calls, web/file search, MCP, code interpreter, hosted shell, images, audio, files, conversations or `previous_response_id`;
- one request produces one ephemeral advisory response and no provider-side memory.
OpenAI's current guidance recommends the Responses API for modern reasoning workflows and describes `gpt-5.6-terra` as the balance of capability and cost. This proposal nevertheless selects the published immutable `gpt-5.4-mini-2026-03-17` snapshot: ADR-0006 requires a model revision not to inherit promotion silently, while the current Terra page does not publish a dated snapshot. A later Terra snapshot requires a new binding revision and evaluation rather than alias drift.
Official sources checked on 2026-08-16:
- [OpenAI model guidance](https://developers.openai.com/api/docs/guides/latest-model)
- [GPT-5.4 mini model and snapshot](https://developers.openai.com/api/docs/models/gpt-5.4-mini)
- [OpenAI API data controls](https://developers.openai.com/api/docs/guides/your-data#default-usage-policies-by-endpoint)
## Data decision
OpenAI states that API data is not used for training unless the customer opts in. Under default controls, however, abuse-monitoring logs can include prompts and responses and may be retained for up to 30 days. Zero Data Retention and Modified Abuse Monitoring require OpenAI approval. `store: false` avoids Responses application-state storage but does not by itself remove default abuse-monitoring retention.
Therefore the initial binding has two distinct, non-interchangeable modes:
### Mode A — default retention, first canary
Permitted:
- the Creator's text entered explicitly for this single advisory request;
- a fixed system instruction containing no user or project data;
- locale and output-size controls.
Forbidden:
- automatic context excerpts, even when marked `PRIVATE_ALLOWED`;
- credentials, tokens, passwords, private keys, connection strings or authentication headers;
- Actor, Principal, Scope, session, workspace, Goal, Task, provider-binding or internal request identifiers;
- files, images, database rows, logs, browser history, conversation history and ambient machine context.
The UI must disclose before each request that the explicitly entered text leaves the private network and may be retained by the provider for abuse monitoring for up to 30 days. Submission is the per-request acknowledgement; it is not durable blanket consent.
### Mode B — approved ZDR/MAM project
Enabling `PRIVATE_ALLOWED` context requires separate evidence that the exact OpenAI project has the approved control enabled, a new binding revision, a new evaluation, and explicit Creator promotion. Even then, `SECRET` and `UNKNOWN` remain forbidden, and tools/background/conversations remain disabled.
## Provider request mapping
The adapter constructs provider JSON itself. Provider-specific types stay under `adapters/models`; no provider type crosses into contracts or domain packages.
The request contains only:
- fixed snapshot model;
- fixed non-executable system instruction: answer as non-canonical advice, state uncertainty, never claim an action occurred, never request or expose secrets;
- the single Creator message;
- `store: false`;
- reasoning effort `low`;
- an output limit consistent with the existing 16 KiB response contract.
The adapter must not serialize trusted routing/authority fields into the provider prompt. Those fields authorize local scope selection only; they are not model context.
## Response mapping
The adapter accepts text output only. It rejects:
- tool calls or executable items;
- unknown output item kinds;
- response identifiers offered as future conversation state;
- output larger than the contract limit;
- invalid UTF-8, malformed JSON, redirects or an origin change.
Provider refusal maps to `CONTENT_REJECTED`; deadline expiry to `TIMEOUT`; transport or availability failure to `PROVIDER_UNAVAILABLE`; malformed/forbidden provider output to `INVALID_PROVIDER_RESPONSE`. No failure is retried automatically and no partial output is shown after a failed validation.
## Isolation and transport
- The API key is created in a dedicated OpenAI project and mounted read-only only into `cognitive-worker` as a Docker secret file.
- The secret is absent from Git, PostgreSQL, image layers, environment variables, logs, health responses, browser storage and other containers.
- Standard-library HTTPS client, normal certificate validation, fixed SNI/Host, disabled redirects and disabled ambient proxy variables.
- Egress is limited operationally to the fixed OpenAI API origin; DNS/IP changes are treated as infrastructure, not browser-controlled routing.
- Request bodies, response bodies, authorization headers and message hashes are not logged. Metrics contain binding revision, latency bucket, finish reason and token counters only.
## Limits and cost controls
- 30-second hard deadline inherited from ADR-0006;
- no automatic retry;
- 5 requests per minute and 50 requests per day for the single Creator during canary;
- maximum five simultaneous in-flight requests is reduced to one during canary;
- provider output is capped by both provider token limit and the 16 KiB validated gateway limit;
- OpenAI project spend limit and alert must be configured before the key is mounted;
- any rate, model, prompt or spend-limit change creates a new binding revision.
Official pricing is mutable and is operational metadata rather than architecture. At review time the documented GPT-5.4 mini price is USD 0.75 per million input tokens and USD 4.50 per million output tokens; deployment must re-check the provider dashboard and project limit.
## Evaluation and promotion
1. Implement the adapter against an adversarial local fake server; do not add the production key.
2. Test fixed origin, redirect denial, proxy denial, strict JSON, timeout, body limits, forbidden output kinds and log redaction.
3. Run an offline corpus containing only synthetic/public advisory cases. Record exact snapshot, prompt revision, expected safety properties, latency and estimated cost.
4. Mount a dedicated project key only after Mode A disclosure and spend controls exist.
5. Run shadow calls whose results are never shown, using synthetic/public inputs only.
6. Creator explicitly promotes the exact binding revision to a small canary.
7. Canary permits only explicit message text, one in-flight request, no automatic context and immediate rollback.
8. Broader use requires recorded evaluation evidence and a separate promotion decision.
## Rollback and degraded operation
Rollback disables/retires the OpenAI binding and restores `deterministic-fake` as the only active binding. The UI returns explicit `PROVIDER_UNAVAILABLE`. Creator authentication, Situation, Continuity, Commit, notification delivery and all canonical state remain available. Removing provider cognition never requires a database migration or state repair because advisory requests and responses are not persisted.
## Decisions still requiring explicit acceptance
Before implementation:
1. accept OpenAI Responses API as the first provider;
2. accept `gpt-5.4-mini-2026-03-17` as the first immutable model snapshot;
3. accept Mode A's possible provider abuse-monitoring retention up to 30 days, or require ZDR approval before any real call;
4. accept the per-request disclosure wording and initial rate/spend policy.
Before production egress, a dedicated OpenAI API project and API key must exist. A ChatGPT/Codex subscription is not evidence that an API key or API billing project is available.
+3 -1
View File
@@ -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.
@@ -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'