From b1ccc3200b688f0e93721982912925b23d00b179 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 22 Aug 2026 17:00:33 +0300 Subject: [PATCH] feat: expose durable agent state in creator cabinet --- cmd/creator-web/server.go | 10 ++++++ cmd/creator-web/server_test.go | 19 ++++++++++ cmd/creator-web/static/assets/console.css | 13 +++++++ cmd/creator-web/static/assets/console.js | 35 ++++++++++++++++++- cmd/creator-web/static/index.html | 16 +++++++++ dev/ACTIVE_TASK.json | 4 +-- dev/tasks/DEV-000150-agent-operations-ui.json | 23 ++++++++++++ 7 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 dev/tasks/DEV-000150-agent-operations-ui.json diff --git a/cmd/creator-web/server.go b/cmd/creator-web/server.go index 0125905..6d754fa 100644 --- a/cmd/creator-web/server.go +++ b/cmd/creator-web/server.go @@ -663,6 +663,16 @@ func creatorRoute(method, path string) (routeTarget, string, bool) { } case method == http.MethodGet && path == "/api/learning/recent": return targetCore, "/v1/creator/learning/recent", true + case method == http.MethodGet && strings.HasPrefix(path, "/api/delegations/") && strings.HasSuffix(path, "/latest-checkpoint"): + id := strings.TrimSuffix(strings.TrimPrefix(path, "/api/delegations/"), "/latest-checkpoint") + if validID(id) { + return targetCore, "/v1/creator/delegations/" + id + "/latest-checkpoint", true + } + case method == http.MethodGet && strings.HasPrefix(path, "/api/delegations/"): + id := strings.TrimPrefix(path, "/api/delegations/") + if validID(id) { + return targetCore, "/v1/creator/delegations/" + id, true + } case method == http.MethodGet && strings.HasPrefix(path, "/api/prospective-items/"): id := strings.TrimPrefix(path, "/api/prospective-items/") if validID(id) { diff --git a/cmd/creator-web/server_test.go b/cmd/creator-web/server_test.go index 5b13c76..57b4c0f 100644 --- a/cmd/creator-web/server_test.go +++ b/cmd/creator-web/server_test.go @@ -23,6 +23,25 @@ func TestLocalChatContextUsesOnlyActiveProject(t *testing.T) { } } +func TestCreatorRouteAllowsOnlyBoundedDelegationReads(t *testing.T) { + tests := []struct { + path string + want string + }{ + {"/api/delegations/delegation-1", "/v1/creator/delegations/delegation-1"}, + {"/api/delegations/delegation-1/latest-checkpoint", "/v1/creator/delegations/delegation-1/latest-checkpoint"}, + } + for _, test := range tests { + _, got, ok := creatorRoute(http.MethodGet, test.path) + if !ok || got != test.want { + t.Fatalf("creatorRoute(%q) = %q, %t; want %q, true", test.path, got, ok, test.want) + } + } + if _, _, ok := creatorRoute(http.MethodGet, "/api/delegations/../secrets"); ok { + t.Fatal("path traversal-like delegation route must be denied") + } +} + func TestFleetReadinessRequiresCreatorSessionAndProxiesCognitiveState(t *testing.T) { core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/creator/sessions/current" || r.Header.Get("Authorization") != "Bearer bounded-session" { diff --git a/cmd/creator-web/static/assets/console.css b/cmd/creator-web/static/assets/console.css index 441fd01..9f09ffa 100644 --- a/cmd/creator-web/static/assets/console.css +++ b/cmd/creator-web/static/assets/console.css @@ -271,6 +271,17 @@ html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before, .fleet-row .learning-state[data-state="READY"] { color: var(--good); } .fleet-row .learning-state[data-state="DEGRADED"] { color: var(--warn); } +.agent-panel { margin-bottom: 18px; border: 1px solid rgba(120,198,163,.3); background: rgba(13,20,17,.92); box-shadow: inset 3px 0 0 rgba(120,198,163,.38); } +.agent-panel > header { min-height: 66px; padding: 13px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); } +.agent-panel header p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; } +.agent-panel h2 { margin: 0; font: 400 22px/1 var(--serif); } +.agent-empty { padding: 20px 18px; color: var(--muted); font: 10px/1.5 var(--mono); } +.agent-details { padding: 16px 18px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; } +.agent-details div { min-width: 0; } +.agent-details span { display: block; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; } +.agent-details strong { display: block; margin-top: 7px; overflow: hidden; color: var(--ink); font: 600 10px/1.4 var(--mono); text-overflow: ellipsis; white-space: nowrap; } +.agent-details p { grid-column: 1 / -1; margin: 0; padding-top: 12px; border-top: 1px solid var(--line-soft); color: var(--muted); font: 10px/1.5 var(--mono); } + .learning-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); } .learning-grid article { min-width: 0; border-right: 1px solid var(--line-soft); } .learning-grid article:last-child { border-right: 0; } @@ -426,6 +437,8 @@ html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before, .fleet-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } .fleet-row:nth-child(2n) { border-right: 0; } .fleet-row { border-bottom: 1px solid var(--line-soft); } + .agent-details { grid-template-columns: 1fr; gap: 14px; } + .agent-details p { grid-column: auto; } .activity-row { padding: 14px 16px; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 12px; } .activity-state { grid-column: 2; grid-row: 1; } .activity-operation { grid-column: 1; grid-row: 1; } diff --git a/cmd/creator-web/static/assets/console.js b/cmd/creator-web/static/assets/console.js index 2367049..a39ac5d 100644 --- a/cmd/creator-web/static/assets/console.js +++ b/cmd/creator-web/static/assets/console.js @@ -6,7 +6,7 @@ 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"]; +const contextFields = ["session-id", "workspace-id", "task-id", "prospective-id", "capability-id", "delegation-id"]; const settingsStorageKey = "domovoy.creator.settings"; const contextPresetsStorageKey = "domovoy.creator.context.presets"; let sessionToken = sessionStorage.getItem("domovoy.creator.session") || ""; @@ -173,6 +173,7 @@ async function showConsole() { loadActionHistory(); loadLearning(); loadFleetReadiness(); + loadAgentDelegation(); } runDiagnostics(); } @@ -355,6 +356,7 @@ byId("refresh-all").addEventListener("click", loadDashboard); byId("refresh-actions").addEventListener("click", loadActionHistory); byId("refresh-learning").addEventListener("click", loadLearning); byId("refresh-fleet").addEventListener("click", loadFleetReadiness); +byId("refresh-agent").addEventListener("click", loadAgentDelegation); byId("logout").addEventListener("click", () => endSession()); byId("run-diagnostics").addEventListener("click", runDiagnostics); @@ -445,6 +447,7 @@ async function loadDashboard() { work.push(loadActionHistory()); work.push(loadLearning()); work.push(loadFleetReadiness()); + work.push(loadAgentDelegation()); if (context["capability-id"]) work.push(loadCapability(context["capability-id"])); else setSignal("capability", "не выбрана", "idle"); @@ -547,6 +550,36 @@ function createFleetRow(member) { return row; } +async function loadAgentDelegation() { + const delegationID = byId("delegation-id").value.trim(); + const empty = byId("agent-empty"); + const details = byId("agent-details"); + if (!delegationID) { + empty.hidden = false; + details.hidden = true; + setText("agent-summary", "DELEGATION НЕ ВЫБРАНА"); + return; + } + empty.hidden = false; + empty.textContent = "Читаю durable delegation…"; + details.hidden = true; + try { + const delegation = await request(`/api/delegations/${encodeURIComponent(delegationID)}`); + const checkpoint = await request(`/api/delegations/${encodeURIComponent(delegationID)}/latest-checkpoint`).catch(() => ({})); + setText("agent-summary", delegation.State || "СОСТОЯНИЕ НЕИЗВЕСТНО"); + setText("agent-delegation-state", delegation.State); + setText("agent-run-state", `${delegation.LatestRunID || "run не выбран"} · ${delegation.SelectedImplementationID || "implementation не указана"}`); + setText("agent-blocker", checkpoint.BlockerClass || "нет checkpoint"); + setText("agent-next-step", checkpoint.NextStep || checkpoint.RemainingSummary || delegation.DesiredOutcome || "Следующий шаг не записан."); + empty.hidden = true; + details.hidden = false; + } catch (error) { + setText("agent-summary", "ОШИБКА"); + empty.textContent = safeClientError(error); + empty.hidden = false; + } +} + async function loadLearning() { const experienceList = byId("experience-list"); const reflectionList = byId("reflection-list"); diff --git a/cmd/creator-web/static/index.html b/cmd/creator-web/static/index.html index 923b77d..50733d7 100644 --- a/cmd/creator-web/static/index.html +++ b/cmd/creator-web/static/index.html @@ -94,6 +94,8 @@ + +
@@ -190,6 +192,20 @@

Состояние fleet ещё не загружено.

+
+
+

Work / durable execution

Агентская работа

+
DELEGATION НЕ ВЫБРАНА
+
+
Укажите delegation ID в расширенных идентификаторах, чтобы прочитать durable state.
+ +
+

Commit / владелец

Журнал действий

diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index d0bef58..84759a9 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000149-live-model-qualification.json", - "task_id": "DEV-000149", + "task_file": "dev/tasks/DEV-000150-agent-operations-ui.json", + "task_id": "DEV-000150", "task_revision": 1 } diff --git a/dev/tasks/DEV-000150-agent-operations-ui.json b/dev/tasks/DEV-000150-agent-operations-ui.json new file mode 100644 index 0000000..81cb803 --- /dev/null +++ b/dev/tasks/DEV-000150-agent-operations-ui.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000150", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "ee678db42cfc62c930b6fd15e7cf93823a61f58b", + "risk_tier": "E", + "intent": "Expose read-only durable delegation and latest checkpoint state in the authenticated creator operations cabinet.", + "requirement_ids": ["WP-801", "WP-804", "INV-TOPO-001"], + "allowed_change_scope": {"modules": ["interaction", "work"], "paths": ["cmd/core-runtime/main.go", "cmd/creator-web/server.go", "cmd/creator-web/server_test.go", "cmd/creator-web/static/index.html", "cmd/creator-web/static/assets/console.js", "cmd/creator-web/static/assets/console.css", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000150-agent-operations-ui.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", ".gitea/**", "database/**", "modules/**", "contracts/**", "adapters/**", "deploy/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["creator delegation read API", "creator latest checkpoint read API"], "may_change": []}, + "semantic_owner": "work", + "canonical_state": [], + "state_transitions": [], + "side_effects_and_privileges": ["authenticated read-only proxy and rendering only"], + "persistence_and_migrations": [], + "failure_retry_restart": ["missing delegation/checkpoint remains visible as unavailable; no mutation or automatic retry"], + "assumptions": ["creator session authentication remains enforced by Core", "delegation ID is supplied by the user from an existing chat response"], + "acceptance_scenarios": ["unauthenticated delegation reads remain denied", "valid delegation ID renders state/run/implementation", "checkpoint blocker and next step render when present", "invalid path-like ID is denied", "mobile layout has no forced horizontal overflow"], + "required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "git diff --check", "check-architecture", "independent"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"] +} -- 2.54.0