diff --git a/cmd/creator-web/main.go b/cmd/creator-web/main.go index ae1a6f3..3319c34 100644 --- a/cmd/creator-web/main.go +++ b/cmd/creator-web/main.go @@ -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) diff --git a/cmd/creator-web/server.go b/cmd/creator-web/server.go index 474ff0d..0bf5abd 100644 --- a/cmd/creator-web/server.go +++ b/cmd/creator-web/server.go @@ -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 { diff --git a/cmd/creator-web/server_test.go b/cmd/creator-web/server_test.go index 8c78dd5..160e912 100644 --- a/cmd/creator-web/server_test.go +++ b/cmd/creator-web/server_test.go @@ -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) + } +} diff --git a/cmd/creator-web/static/assets/console.css b/cmd/creator-web/static/assets/console.css index 508b63c..9b0c632 100644 --- a/cmd/creator-web/static/assets/console.css +++ b/cmd/creator-web/static/assets/console.css @@ -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; } diff --git a/cmd/creator-web/static/assets/console.js b/cmd/creator-web/static/assets/console.js index 08f67b2..f49b126 100644 --- a/cmd/creator-web/static/assets/console.js +++ b/cmd/creator-web/static/assets/console.js @@ -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"]; diff --git a/cmd/creator-web/static/index.html b/cmd/creator-web/static/index.html index b97d3cd..5e76c22 100644 --- a/cmd/creator-web/static/index.html +++ b/cmd/creator-web/static/index.html @@ -92,6 +92,26 @@
ВОЗМОЖНОСТЬожидает
+
+
+

COGNITIVE / ADVISORY V1

+

Спросить Домового

+ Ответ — только совет. Он не сохраняется, не создаёт задач и не запускает действий. +
+
+ + +
+ только выбранный контекст + +
+
+ +
+

Continuity / ближайшие

Реестр ожиданий

diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index a7e0753..cfed193 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -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 diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 206d8bd..2242122 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -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 } diff --git a/dev/tasks/DEV-000048-creator-advisory-console.json b/dev/tasks/DEV-000048-creator-advisory-console.json new file mode 100644 index 0000000..665d939 --- /dev/null +++ b/dev/tasks/DEV-000048-creator-advisory-console.json @@ -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"] +} diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 390018c..837796d 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -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.