diff --git a/cmd/creator-web/server.go b/cmd/creator-web/server.go index 0bf5abd..08c866b 100644 --- a/cmd/creator-web/server.go +++ b/cmd/creator-web/server.go @@ -18,8 +18,9 @@ import ( ) const ( - maxRequestBytes = 8 << 10 - maxResponseBytes = 1 << 20 + maxRequestBytes = 8 << 10 + maxAdvisoryRequestBytes = 20 << 10 + maxResponseBytes = 1 << 20 ) //go:embed static @@ -113,7 +114,7 @@ type browserAdvisoryRequest struct { 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) { + if err := decodeStrictJSON(r.Body, maxAdvisoryRequestBytes, &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 } diff --git a/cmd/creator-web/server_test.go b/cmd/creator-web/server_test.go index 160e912..bd68991 100644 --- a/cmd/creator-web/server_test.go +++ b/cmd/creator-web/server_test.go @@ -8,6 +8,8 @@ import ( "net/http/httptest" "strings" "testing" + + "git.cin.su/m/Assistant/contracts/cognitive" ) func TestGatewayForwardsOnlyCreatorAllowlist(t *testing.T) { @@ -251,6 +253,7 @@ func TestAdvisoryRequiresSessionBeforeUpstreamCalls(t *testing.T) { } func TestAdvisoryAssemblesTrustedIdentityAndScope(t *testing.T) { + longMessage := strings.Repeat("a", 16*1024) 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") @@ -272,8 +275,12 @@ func TestAdvisoryAssemblesTrustedIdentityAndScope(t *testing.T) { 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 trusted cognitive.AdvisoryRequest + if err := json.Unmarshal(body, &trusted); err != nil { + t.Fatal(err) + } + if trusted.ActorID != "creator-actor" || trusted.PrincipalID != "creator-principal" || trusted.ScopeID != "trusted-scope" || trusted.Message != longMessage || strings.Contains(text, "attacker") { + t.Fatal("cognitive request was not assembled from trusted context") } var request map[string]any _ = json.Unmarshal(body, &request) @@ -284,7 +291,7 @@ func TestAdvisoryAssemblesTrustedIdentityAndScope(t *testing.T) { 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 := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(fmt.Sprintf(`{"message":%q,"session_id":"focus-session","workspace_id":"home"}`, longMessage))) request.Header.Set("Authorization", "Bearer bounded-session") request.Header.Set("Content-Type", "application/json") response := httptest.NewRecorder() diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 2242122..767168e 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": 1 + "task_revision": 2 } diff --git a/dev/tasks/DEV-000048-creator-advisory-console.json b/dev/tasks/DEV-000048-creator-advisory-console.json index 665d939..6ce85ad 100644 --- a/dev/tasks/DEV-000048-creator-advisory-console.json +++ b/dev/tasks/DEV-000048-creator-advisory-console.json @@ -1,6 +1,6 @@ { "task_id": "DEV-000048", - "task_revision": 1, + "task_revision": 2, "architecture_revision": "ADR-0006", "base_repo_revision": "861438a4cae844edfe8a17615fb700d17c950d8e", "risk_tier": "E", @@ -17,7 +17,7 @@ "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"], + "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", "the gateway accepts the full 16 KiB advisory message contract plus its JSON envelope", "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"] }