DEV-000048: add Creator advisory console #20

Merged
m merged 1 commits from codex/creator-advisory-console-v1 into main 2026-08-16 17:16:32 +00:00
10 changed files with 330 additions and 5 deletions
+1
View File
@@ -14,6 +14,7 @@ func main() {
envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"),
envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"),
&http.Client{Timeout: 10 * time.Second},
envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"),
)
if err != nil {
log.Fatal(err)
+138 -2
View File
@@ -9,6 +9,12 @@ import (
"net/http"
"net/url"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/situation"
"git.cin.su/m/Assistant/platform/ids"
)
const (
@@ -23,6 +29,7 @@ type gateway struct {
core *url.URL
continuity *url.URL
commit *url.URL
cognitive *url.URL
client *http.Client
static http.Handler
}
@@ -35,7 +42,7 @@ const (
targetCommit
)
func newHandler(coreURL, continuityURL, commitURL string, client *http.Client) (http.Handler, error) {
func newHandler(coreURL, continuityURL, commitURL string, client *http.Client, cognitiveURLs ...string) (http.Handler, error) {
core, err := parseRuntimeURL(coreURL, "Core")
if err != nil {
return nil, err
@@ -48,6 +55,14 @@ func newHandler(coreURL, continuityURL, commitURL string, client *http.Client) (
if err != nil {
return nil, err
}
cognitiveURL := "http://cognitive-worker:8083"
if len(cognitiveURLs) > 0 {
cognitiveURL = cognitiveURLs[0]
}
cognitiveRuntime, err := parseRuntimeURL(cognitiveURL, "Cognitive")
if err != nil {
return nil, err
}
assets, err := fs.Sub(staticFiles, "static")
if err != nil {
return nil, fmt.Errorf("load embedded console: %w", err)
@@ -55,7 +70,7 @@ func newHandler(coreURL, continuityURL, commitURL string, client *http.Client) (
if client == nil {
client = http.DefaultClient
}
return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, client: client, static: http.FileServer(http.FS(assets))}), nil
return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, client: client, static: http.FileServer(http.FS(assets))}), nil
}
func parseRuntimeURL(rawURL, name string) (*url.URL, error) {
@@ -73,6 +88,10 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
if strings.HasPrefix(r.URL.Path, "/api/") {
if r.Method == http.MethodPost && r.URL.Path == "/api/advisory" {
g.handleAdvisory(w, r)
return
}
g.forwardAPI(w, r)
return
}
@@ -86,6 +105,123 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
g.static.ServeHTTP(w, r)
}
type browserAdvisoryRequest struct {
Message string `json:"message"`
SessionID string `json:"session_id"`
WorkspaceID string `json:"workspace_id"`
}
func (g *gateway) handleAdvisory(w http.ResponseWriter, r *http.Request) {
var browserRequest browserAdvisoryRequest
if err := decodeStrictJSON(r.Body, maxRequestBytes, &browserRequest); err != nil || browserRequest.Message == "" || len(browserRequest.Message) > 16*1024 || !validID(browserRequest.SessionID) || !validID(browserRequest.WorkspaceID) {
http.Error(w, "invalid advisory request", http.StatusBadRequest)
return
}
authorization := r.Header.Get("Authorization")
if authorization == "" {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return
}
var session identity.CreatorSession
status, err := g.getTrustedJSON(r, g.core, "/v1/creator/sessions/current", "", authorization, &session)
if err != nil {
http.Error(w, "creator session unavailable", http.StatusBadGateway)
return
}
if status != http.StatusOK {
http.Error(w, "creator session denied", status)
return
}
query := url.Values{"session_id": {browserRequest.SessionID}, "workspace_id": {browserRequest.WorkspaceID}}.Encode()
var focus situation.FocusBinding
status, err = g.getTrustedJSON(r, g.core, "/v1/creator/focus", query, authorization, &focus)
if err != nil {
http.Error(w, "creator focus unavailable", http.StatusBadGateway)
return
}
if status != http.StatusOK {
http.Error(w, "creator focus unavailable", status)
return
}
if session.ActorID == "" || session.PrincipalID == "" || focus.ActorID != session.ActorID || focus.ScopeID == "" {
http.Error(w, "creator focus denied", http.StatusForbidden)
return
}
requestID, err := ids.New()
if err != nil {
http.Error(w, "advisory unavailable", http.StatusServiceUnavailable)
return
}
trusted := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID), ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: focus.ScopeID, Message: browserRequest.Message, Context: []cognitive.ContextExcerpt{}, Locale: "ru-RU", MaxOutputBytes: 16 * 1024, ProviderBindingID: capability.CapabilityBindingID(ids.ID("deterministic-fake")), ProviderBindingRevision: 1}
payload, err := json.Marshal(trusted)
if err != nil {
http.Error(w, "advisory unavailable", http.StatusServiceUnavailable)
return
}
target := *g.cognitive
target.Path = "/v1/advisories/deterministic"
upstreamRequest, err := http.NewRequestWithContext(r.Context(), http.MethodPost, target.String(), strings.NewReader(string(payload)))
if err != nil {
http.Error(w, "advisory unavailable", http.StatusBadGateway)
return
}
upstreamRequest.Header.Set("Content-Type", "application/json")
response, err := g.client.Do(upstreamRequest)
if err != nil {
http.Error(w, "advisory unavailable", http.StatusBadGateway)
return
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
http.Error(w, "advisory unavailable", http.StatusBadGateway)
return
}
var advisory cognitive.AdvisoryResponse
if err := decodeStrictJSON(response.Body, maxResponseBytes, &advisory); err != nil || advisory.RequestID != trusted.RequestID || advisory.ProviderBindingID != trusted.ProviderBindingID || advisory.ProviderBindingRevision != 1 || advisory.FinishReason != cognitive.FinishProviderUnavailable || advisory.AdvisoryText != "" || len(advisory.ReferencedExcerptIDs) != 0 {
http.Error(w, "invalid advisory response", http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(advisory)
}
func (g *gateway) getTrustedJSON(r *http.Request, upstream *url.URL, path, rawQuery, authorization string, destination any) (int, error) {
target := *upstream
target.Path = path
target.RawQuery = rawQuery
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target.String(), nil)
if err != nil {
return 0, err
}
request.Header.Set("Authorization", authorization)
response, err := g.client.Do(request)
if err != nil {
return 0, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, maxResponseBytes))
return response.StatusCode, nil
}
if err := decodeStrictJSON(response.Body, maxResponseBytes, destination); err != nil {
return response.StatusCode, err
}
return response.StatusCode, nil
}
func decodeStrictJSON(reader io.Reader, limit int64, destination any) error {
decoder := json.NewDecoder(io.LimitReader(reader, limit+1))
decoder.DisallowUnknownFields()
if err := decoder.Decode(destination); err != nil {
return err
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
return fmt.Errorf("trailing JSON")
}
return nil
}
func (g *gateway) forwardAPI(w http.ResponseWriter, r *http.Request) {
routeTarget, upstreamPath, allowed := creatorRoute(r.Method, r.URL.Path)
if !allowed {
+80
View File
@@ -1,6 +1,8 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
@@ -222,6 +224,7 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Домовой") ||
!strings.Contains(response.Body.String(), "satisfaction-dialog") || !strings.Contains(response.Body.String(), "Подтверждаю переход") ||
!strings.Contains(response.Body.String(), "action-history") || !strings.Contains(response.Body.String(), "Журнал действий") ||
!strings.Contains(response.Body.String(), "advisory-form") || !strings.Contains(response.Body.String(), "Спросить Домового") ||
!strings.Contains(response.Body.String(), "creator-username") || !strings.Contains(response.Body.String(), "creator-password") ||
strings.Contains(response.Body.String(), "bootstrap-token") {
t.Fatalf("console response = %d", response.Code)
@@ -230,3 +233,80 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
t.Fatal("content security policy is missing")
}
}
func TestAdvisoryRequiresSessionBeforeUpstreamCalls(t *testing.T) {
called := false
upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true }))
defer upstream.Close()
handler, err := newHandler(upstream.URL, upstream.URL, upstream.URL, upstream.Client(), upstream.URL)
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(`{"message":"help","session_id":"focus-session","workspace_id":"home"}`))
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusUnauthorized || called {
t.Fatalf("status=%d upstream_called=%t", response.Code, called)
}
}
func TestAdvisoryAssemblesTrustedIdentityAndScope(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer bounded-session" {
t.Fatal("missing bounded session")
}
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/v1/creator/sessions/current":
_, _ = io.WriteString(w, `{"ActorID":"creator-actor","PrincipalID":"creator-principal","ExpiresAtUnix":2000000000}`)
case "/v1/creator/focus":
if r.URL.Query().Get("session_id") != "focus-session" || r.URL.Query().Get("workspace_id") != "home" {
t.Fatalf("unexpected focus query: %s", r.URL.RawQuery)
}
_, _ = io.WriteString(w, `{"ID":"focus-1","ActorID":"creator-actor","SessionID":"focus-session","Workspace":"home","ProjectID":"domovoy","GoalID":"goal-1","ScopeID":"trusted-scope","Active":true}`)
default:
t.Fatalf("unexpected core path %s", r.URL.Path)
}
}))
defer core.Close()
cognitive := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
text := string(body)
if !strings.Contains(text, `"ActorID":"creator-actor"`) || !strings.Contains(text, `"PrincipalID":"creator-principal"`) || !strings.Contains(text, `"ScopeID":"trusted-scope"`) || !strings.Contains(text, `"Message":"help"`) || strings.Contains(text, "attacker") {
t.Fatalf("untrusted cognitive request: %s", text)
}
var request map[string]any
_ = json.Unmarshal(body, &request)
_, _ = fmt.Fprintf(w, `{"Version":"creator-advisory-v1","RequestID":%q,"ProviderBindingID":"deterministic-fake","ProviderBindingRevision":1,"AdvisoryText":"","ReferencedExcerptIDs":null,"FinishReason":"PROVIDER_UNAVAILABLE","Usage":null}`, request["RequestID"])
}))
defer cognitive.Close()
handler, err := newHandler(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), cognitive.URL)
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(`{"message":"help","session_id":"focus-session","workspace_id":"home"}`))
request.Header.Set("Authorization", "Bearer bounded-session")
request.Header.Set("Content-Type", "application/json")
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "PROVIDER_UNAVAILABLE") {
t.Fatalf("response=%d %s", response.Code, response.Body.String())
}
}
func TestAdvisoryRejectsBrowserAuthorityFieldsBeforeCognitiveCall(t *testing.T) {
called := false
cognitive := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true }))
defer cognitive.Close()
handler, err := newHandler("http://core.invalid", "http://continuity.invalid", "http://commit.invalid", http.DefaultClient, cognitive.URL)
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(`{"message":"help","session_id":"focus-session","workspace_id":"home","ActorID":"attacker"}`))
request.Header.Set("Authorization", "Bearer bounded-session")
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusBadRequest || called {
t.Fatalf("status=%d cognitive_called=%t", response.Code, called)
}
}
+25
View File
@@ -94,6 +94,26 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.signal-strip strong[data-state="error"] { color: var(--bad); }
.signal-strip strong[data-state="loading"] { color: var(--warn); animation: pulse 1.1s ease-in-out infinite; }
.advisor-panel { margin-bottom: 18px; padding: 24px; display: grid; grid-template-columns: minmax(220px, .72fr) minmax(320px, 1.28fr); gap: 18px 34px; border: 1px solid rgba(201,168,95,.42); background: linear-gradient(125deg, rgba(201,168,95,.1), rgba(16,23,20,.88) 44%, rgba(12,20,17,.96)); box-shadow: inset 3px 0 0 rgba(201,168,95,.58); }
.advisor-intro { align-self: center; }
.advisor-intro > p { margin: 0 0 10px; color: var(--signal); font: 600 9px/1 var(--mono); letter-spacing: .15em; }
.advisor-intro h2 { margin: 0; font: 400 clamp(25px, 3vw, 36px)/1.05 var(--serif); }
.advisor-intro span { display: block; max-width: 44ch; margin-top: 12px; color: var(--muted); font-size: 11px; line-height: 1.55; }
.advisor-form { display: grid; gap: 9px; }
.advisor-form > label { color: var(--faint); font: 600 9px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.advisor-form textarea { width: 100%; min-height: 92px; padding: 13px 14px; resize: vertical; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: rgba(7,12,10,.82); font: 12px/1.55 var(--sans); transition: border-color .18s, box-shadow .18s; }
.advisor-form textarea:focus { outline: 0; border-color: var(--signal); box-shadow: 0 0 0 2px rgba(201,168,95,.12); }
.advisor-controls { display: flex; align-items: center; justify-content: space-between; gap: 18px; }
.advisor-controls > span { color: var(--muted); font: 9px/1.2 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
.advisor-controls i { width: 6px; height: 6px; margin-right: 7px; display: inline-block; border-radius: 50%; background: var(--good); box-shadow: 0 0 10px rgba(120,198,163,.7); }
.advisor-controls .primary-button { width: auto; min-width: 180px; margin: 0; }
.advisor-result { grid-column: 1 / -1; padding-top: 18px; display: grid; grid-template-columns: 150px minmax(0,1fr); gap: 18px; border-top: 1px solid var(--line); }
.advisor-result[hidden] { display: none; }
.advisor-result span { display: block; margin-bottom: 7px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .12em; }
.advisor-result strong { color: var(--warn); font: 600 10px/1.3 var(--mono); }
.advisor-result strong[data-state="COMPLETED"] { color: var(--good); }
.advisor-result p { margin: 0; color: var(--ink); font-size: 13px; line-height: 1.65; white-space: pre-wrap; }
.register-panel { margin-bottom: 18px; border: 1px solid var(--line); background: rgba(16, 23, 20, .82); }
.register-panel > header { padding: 17px 18px; display: flex; align-items: end; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
.register-panel header p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
@@ -228,6 +248,7 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.context-rail { padding-inline: 20px; }
.workspace { padding: 32px 24px; }
.data-grid { grid-template-columns: 1fr; }
.advisor-panel { grid-template-columns: 1fr; }
.capability-card { grid-column: auto; }
.prospective-card { grid-column: auto; }
}
@@ -246,6 +267,10 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.last-update { text-align: left; }
.signal-strip { grid-template-columns: 1fr; }
.signal-strip div { border-right: 0; border-bottom: 1px solid var(--line); }
.advisor-panel { padding: 20px 16px; gap: 20px; }
.advisor-controls { align-items: stretch; flex-direction: column; }
.advisor-controls .primary-button { width: 100%; }
.advisor-result { grid-template-columns: 1fr; gap: 10px; }
.register-panel > header { align-items: start; }
.activity-panel > header { align-items: flex-start; }
.activity-tools { align-items: flex-end; flex-direction: column-reverse; }
+37
View File
@@ -3,6 +3,7 @@ const loginView = byId("login-view");
const consoleView = byId("console-view");
const loginForm = byId("login-form");
const contextForm = byId("context-form");
const advisoryForm = byId("advisory-form");
const loginError = byId("login-error");
const toast = byId("toast");
const contextFields = ["session-id", "workspace-id", "task-id", "prospective-id", "capability-id"];
@@ -96,6 +97,8 @@ function endSession(message = "Сессия завершена.") {
pendingCommand = null;
pendingNotification = null;
lastAuthorizedActionID = "";
advisoryForm.reset();
byId("advisory-result").hidden = true;
notify(message);
}
@@ -142,6 +145,40 @@ byId("refresh-all").addEventListener("click", loadDashboard);
byId("refresh-actions").addEventListener("click", loadActionHistory);
byId("logout").addEventListener("click", () => endSession());
advisoryForm.addEventListener("submit", async (event) => {
event.preventDefault();
const context = saveContext();
const message = byId("advisory-message").value.trim();
const button = byId("advisory-submit");
if (!context["session-id"] || !context["workspace-id"]) {
notify("Сначала укажите сессию и рабочую область контекста.");
return;
}
if (!message) return;
button.disabled = true;
button.firstElementChild.textContent = "Проверяю контекст…";
byId("advisory-result").hidden = true;
try {
const result = await request("/api/advisory", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({message, session_id: context["session-id"], workspace_id: context["workspace-id"]})
});
const state = result.FinishReason || "INVALID_PROVIDER_RESPONSE";
setText("advisory-state", state);
byId("advisory-state").dataset.state = state;
setText("advisory-text", state === "PROVIDER_UNAVAILABLE" ? "Когнитивный провайдер пока не подключён. Контекст проверен, наружу ничего не отправлено." : result.AdvisoryText, "Ответ не получен.");
byId("advisory-result").hidden = false;
} catch (error) {
setText("advisory-state", "ОШИБКА");
setText("advisory-text", error.message);
byId("advisory-result").hidden = false;
} finally {
button.disabled = false;
button.firstElementChild.textContent = "Получить совет";
}
});
async function loadDashboard() {
const context = saveContext();
const sessionID = context["session-id"];
+20
View File
@@ -92,6 +92,26 @@
<div><span>ВОЗМОЖНОСТЬ</span><strong id="capability-signal" data-state="idle">ожидает</strong></div>
</section>
<section class="advisor-panel" aria-labelledby="advisor-title">
<div class="advisor-intro">
<p>COGNITIVE / ADVISORY V1</p>
<h2 id="advisor-title">Спросить Домового</h2>
<span>Ответ — только совет. Он не сохраняется, не создаёт задач и не запускает действий.</span>
</div>
<form id="advisory-form" class="advisor-form">
<label for="advisory-message">Ваш вопрос</label>
<textarea id="advisory-message" rows="3" maxlength="16384" placeholder="Что стоит проверить в выбранном контексте?" required></textarea>
<div class="advisor-controls">
<span id="advisory-privacy"><i></i> только выбранный контекст</span>
<button id="advisory-submit" class="primary-button" type="submit"><span>Получить совет</span><span aria-hidden="true">→</span></button>
</div>
</form>
<div id="advisory-result" class="advisor-result" aria-live="polite" hidden>
<div><span>СТАТУС</span><strong id="advisory-state">—</strong></div>
<p id="advisory-text"></p>
</div>
</section>
<section class="register-panel" aria-labelledby="register-title">
<header>
<div><p>Continuity / ближайшие</p><h2 id="register-title">Реестр ожиданий</h2></div>
+2 -1
View File
@@ -122,8 +122,9 @@ services:
CORE_RUNTIME_URL: http://core-runtime:8080
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
COMMIT_RUNTIME_URL: http://commit-runtime:8082
COGNITIVE_RUNTIME_URL: http://cognitive-worker:8083
ports: ["${CREATOR_WEB_PORT:-18080}:8084"]
depends_on: [core-runtime, continuity-runtime, commit-runtime]
depends_on: [core-runtime, continuity-runtime, commit-runtime, cognitive-worker]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
interval: 10s
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000047-production-cognitive-advisory-fake.json",
"task_id": "DEV-000047",
"task_file": "dev/tasks/DEV-000048-creator-advisory-console.json",
"task_id": "DEV-000048",
"task_revision": 1
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000048",
"task_revision": 1,
"architecture_revision": "ADR-0006",
"base_repo_revision": "861438a4cae844edfe8a17615fb700d17c950d8e",
"risk_tier": "E",
"intent": "Expose creator-advisory-v1 through authenticated Creator Web, assembling trusted identity/scope fields server-side and adding a responsive advisory console that displays explicit provider-unavailable state without persistence, tools or egress.",
"requirement_ids": ["ADR-0006", "DEV-000046", "INV-EPI-001"],
"allowed_change_scope": {"modules": [], "paths": ["cmd/creator-web/**", "deploy/docker-compose.prod.yml", "tests/creator_web/**", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000048-creator-advisory-console.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "ci/**", ".gitea/**", "contracts/**", "modules/**", "adapters/**", "cmd/core-runtime/**", "cmd/commit-runtime/**", "cmd/continuity-runtime/**", "cmd/cognitive-worker/**", "database/**", "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-gateway-and-display",
"canonical_state": [],
"state_transitions": [],
"side_effects_and_privileges": ["Creator Web may call fixed internal cognitive-worker endpoint after Core session/focus validation", "no provider network egress or credentials"],
"persistence_and_migrations": [],
"failure_retry_restart": ["advisory text is not persisted", "session/focus failure prevents cognitive call", "provider unavailable is displayed explicitly", "no automatic retry"],
"assumptions": ["browser supplies only message and existing focus locators", "deterministic fake remains the only binding", "docker-test remains prohibited"],
"acceptance_scenarios": ["unauthenticated advisory denied before cognitive call", "browser cannot select Actor/Principal/Scope/binding/provider/context", "trusted request uses current session and validated focus", "UI labels advisory as non-canonical and unavailable", "message is not stored in local/session storage", "desktop/mobile layout and console are clean"],
"required_tests": ["unit", "gateway adversarial", "static UI", "go test", "go vet", "architecture preflight", "exact diff scope", "browser desktop/mobile", "CI"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
+2
View File
@@ -26,4 +26,6 @@ Audit date: 2026-08-16. Active audit: DEV-000044 revision 3.
DEV-000045 closes the DEV-000044 artifact-provenance finding for new promotions by stamping the merged Git revision into OCI labels, recording exact promoted image IDs outside the repository, and making production monitoring reject label or digest drift. Images that predate DEV-000045 remain historically unverifiable from the image alone.
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.
These are explicit scope decisions, not hidden incomplete steps in DEV-000043. No production secret is stored in the repository.