This commit is contained in:
@@ -9,7 +9,11 @@ import (
|
||||
|
||||
func main() {
|
||||
addr := envOr("LISTEN_ADDR", ":8084")
|
||||
handler, err := newHandler(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), &http.Client{Timeout: 10 * time.Second})
|
||||
handler, err := newHandler(
|
||||
envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"),
|
||||
envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"),
|
||||
&http.Client{Timeout: 10 * time.Second},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
+46
-17
@@ -20,15 +20,27 @@ const (
|
||||
var staticFiles embed.FS
|
||||
|
||||
type gateway struct {
|
||||
upstream *url.URL
|
||||
client *http.Client
|
||||
static http.Handler
|
||||
core *url.URL
|
||||
continuity *url.URL
|
||||
client *http.Client
|
||||
static http.Handler
|
||||
}
|
||||
|
||||
func newHandler(upstream string, client *http.Client) (http.Handler, error) {
|
||||
parsed, err := url.Parse(upstream)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
return nil, fmt.Errorf("invalid core runtime URL")
|
||||
type routeTarget uint8
|
||||
|
||||
const (
|
||||
targetCore routeTarget = iota
|
||||
targetContinuity
|
||||
)
|
||||
|
||||
func newHandler(coreURL, continuityURL string, client *http.Client) (http.Handler, error) {
|
||||
core, err := parseRuntimeURL(coreURL, "Core")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continuity, err := parseRuntimeURL(continuityURL, "Continuity")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assets, err := fs.Sub(staticFiles, "static")
|
||||
if err != nil {
|
||||
@@ -37,7 +49,15 @@ func newHandler(upstream string, client *http.Client) (http.Handler, error) {
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return securityHeaders(&gateway{upstream: parsed, client: client, static: http.FileServer(http.FS(assets))}), nil
|
||||
return securityHeaders(&gateway{core: core, continuity: continuity, client: client, static: http.FileServer(http.FS(assets))}), nil
|
||||
}
|
||||
|
||||
func parseRuntimeURL(rawURL, name string) (*url.URL, error) {
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
return nil, fmt.Errorf("invalid %s Runtime URL", name)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -61,7 +81,7 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (g *gateway) forwardAPI(w http.ResponseWriter, r *http.Request) {
|
||||
upstreamPath, allowed := creatorRoute(r.Method, r.URL.Path)
|
||||
routeTarget, upstreamPath, allowed := creatorRoute(r.Method, r.URL.Path)
|
||||
if !allowed {
|
||||
http.Error(w, "creator route denied", http.StatusNotFound)
|
||||
return
|
||||
@@ -75,7 +95,11 @@ func (g *gateway) forwardAPI(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "request too large", http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
target := *g.upstream
|
||||
upstream := g.core
|
||||
if routeTarget == targetContinuity {
|
||||
upstream = g.continuity
|
||||
}
|
||||
target := *upstream
|
||||
target.Path = upstreamPath
|
||||
target.RawQuery = r.URL.RawQuery
|
||||
request, err := http.NewRequestWithContext(r.Context(), r.Method, target.String(), strings.NewReader(string(body)))
|
||||
@@ -98,26 +122,31 @@ func (g *gateway) forwardAPI(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.Copy(w, io.LimitReader(response.Body, maxResponseBytes))
|
||||
}
|
||||
|
||||
func creatorRoute(method, path string) (string, bool) {
|
||||
func creatorRoute(method, path string) (routeTarget, string, bool) {
|
||||
switch {
|
||||
case method == http.MethodPost && path == "/api/session":
|
||||
return "/v1/creator/sessions", true
|
||||
return targetCore, "/v1/creator/sessions", true
|
||||
case method == http.MethodGet && path == "/api/session/current":
|
||||
return "/v1/creator/sessions/current", true
|
||||
return targetCore, "/v1/creator/sessions/current", true
|
||||
case method == http.MethodGet && path == "/api/focus":
|
||||
return "/v1/creator/focus", true
|
||||
return targetCore, "/v1/creator/focus", true
|
||||
case method == http.MethodGet && strings.HasPrefix(path, "/api/tasks/"):
|
||||
id := strings.TrimPrefix(path, "/api/tasks/")
|
||||
if validID(id) {
|
||||
return "/v1/creator/tasks/" + id, true
|
||||
return targetCore, "/v1/creator/tasks/" + id, true
|
||||
}
|
||||
case method == http.MethodGet && strings.HasPrefix(path, "/api/capabilities/") && strings.HasSuffix(path, "/health"):
|
||||
id := strings.TrimSuffix(strings.TrimPrefix(path, "/api/capabilities/"), "/health")
|
||||
if validID(id) {
|
||||
return "/v1/creator/capabilities/" + id + "/health", true
|
||||
return targetCore, "/v1/creator/capabilities/" + id + "/health", true
|
||||
}
|
||||
case method == http.MethodGet && strings.HasPrefix(path, "/api/prospective-items/"):
|
||||
id := strings.TrimPrefix(path, "/api/prospective-items/")
|
||||
if validID(id) {
|
||||
return targetContinuity, "/v1/creator/prospective-items/" + id, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
return targetCore, "", false
|
||||
}
|
||||
|
||||
func validID(value string) bool {
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestGatewayForwardsOnlyCreatorAllowlist(t *testing.T) {
|
||||
_, _ = io.WriteString(w, `{"TaskID":"task-1"}`)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
handler, err := newHandler(upstream.URL, upstream.Client())
|
||||
handler, err := newHandler(upstream.URL, upstream.URL, upstream.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func TestGatewayForwardsOnlyCreatorAllowlist(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGatewayDeniesInternalAndMutationRoutes(t *testing.T) {
|
||||
handler, err := newHandler("http://core-runtime:8080", http.DefaultClient)
|
||||
handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", http.DefaultClient)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -42,6 +42,7 @@ func TestGatewayDeniesInternalAndMutationRoutes(t *testing.T) {
|
||||
{http.MethodPost, "/api/tasks/task-1"},
|
||||
{http.MethodGet, "/api/principal-bindings/actor/principal"},
|
||||
{http.MethodPost, "/api/focus-task-commits"},
|
||||
{http.MethodPost, "/api/prospective-items/item-1"},
|
||||
{http.MethodGet, "/api/tasks/../../healthz"},
|
||||
} {
|
||||
request := httptest.NewRequest(test.method, test.path, nil)
|
||||
@@ -53,8 +54,32 @@ func TestGatewayDeniesInternalAndMutationRoutes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayRoutesProspectiveItemToContinuity(t *testing.T) {
|
||||
continuity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/creator/prospective-items/item-1" {
|
||||
t.Fatalf("unexpected Continuity target %s", r.URL.Path)
|
||||
}
|
||||
if r.Header.Get("Authorization") != "Bearer bounded-session" {
|
||||
t.Fatal("authorization header was not forwarded")
|
||||
}
|
||||
_, _ = io.WriteString(w, `{"ID":"item-1"}`)
|
||||
}))
|
||||
defer continuity.Close()
|
||||
handler, err := newHandler("http://core-runtime.invalid", continuity.URL, continuity.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/prospective-items/item-1", nil)
|
||||
request.Header.Set("Authorization", "Bearer bounded-session")
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "item-1") {
|
||||
t.Fatalf("response = %d %s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsoleAndSecurityHeaders(t *testing.T) {
|
||||
handler, err := newHandler("http://core-runtime:8080", http.DefaultClient)
|
||||
handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", http.DefaultClient)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--signal-brig
|
||||
.workspace-head { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 34px; }
|
||||
.workspace-head h1 { margin: 0; font: 400 clamp(36px, 5vw, 58px)/1 var(--serif); letter-spacing: -.035em; }
|
||||
.last-update { color: var(--faint); font: 10px/1.4 var(--mono); text-align: right; }
|
||||
.signal-strip { display: grid; grid-template-columns: repeat(3, 1fr); margin-bottom: 18px; border: 1px solid var(--line); background: var(--panel); }
|
||||
.signal-strip { display: grid; grid-template-columns: repeat(4, 1fr); margin-bottom: 18px; border: 1px solid var(--line); background: var(--panel); }
|
||||
.signal-strip div { padding: 16px 18px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-right: 1px solid var(--line); }
|
||||
.signal-strip div:last-child { border: 0; }
|
||||
.signal-strip span { color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; }
|
||||
@@ -96,7 +96,8 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--signal-brig
|
||||
.data-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
|
||||
.data-card { min-width: 0; padding: 24px; border: 1px solid var(--line); background: linear-gradient(145deg, rgba(22, 31, 27, .94), rgba(13, 20, 17, .98)); box-shadow: 0 10px 40px rgba(0,0,0,.12); animation: reveal .55s both; }
|
||||
.task-card { animation-delay: .08s; }
|
||||
.capability-card { grid-column: 1 / -1; animation-delay: .16s; }
|
||||
.prospective-card { grid-column: 1 / -1; animation-delay: .16s; }
|
||||
.capability-card { grid-column: 1 / -1; animation-delay: .24s; }
|
||||
.data-card header { display: grid; grid-template-columns: 30px 1fr auto; gap: 12px; align-items: start; margin-bottom: 26px; }
|
||||
.card-number { color: var(--signal); font: 10px/1.5 var(--mono); }
|
||||
.data-card header p { margin: 0 0 5px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase; }
|
||||
@@ -112,6 +113,15 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--signal-brig
|
||||
.data-card dt { margin-bottom: 7px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
|
||||
.data-card dd { margin: 0; overflow: hidden; color: var(--ink); font: 11px/1.4 var(--mono); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.card-message { min-height: 18px; margin: 16px 0 0; color: var(--muted); font-size: 11px; line-height: 1.5; }
|
||||
.prospective-core { margin-bottom: 12px; display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
|
||||
.prospective-core > div { min-width: 0; padding: 22px 0; }
|
||||
.prospective-core > div:first-child { padding-right: 24px; border-right: 1px solid var(--line-soft); }
|
||||
.prospective-core > div:last-child { padding-left: 24px; }
|
||||
.prospective-core span { display: block; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; }
|
||||
.prospective-core strong { display: block; margin-top: 8px; font: 400 clamp(30px, 5vw, 45px)/1 var(--serif); }
|
||||
.time-marker { position: relative; }
|
||||
.time-marker i { position: absolute; left: 24px; right: 0; bottom: -1px; height: 1px; background: linear-gradient(90deg, var(--signal), transparent); }
|
||||
.time-marker time { display: block; margin-top: 10px; color: var(--ink); font: 13px/1.4 var(--mono); }
|
||||
.capability-core { display: grid; grid-template-columns: 110px 1fr; gap: 28px; align-items: center; padding: 8px 0 26px; }
|
||||
.capability-core > div:last-child > span { display: block; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; }
|
||||
.capability-core strong { display: block; margin: 8px 0; font: 400 clamp(30px, 5vw, 46px)/1 var(--serif); }
|
||||
@@ -137,6 +147,7 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--signal-brig
|
||||
.workspace { padding: 32px 24px; }
|
||||
.data-grid { grid-template-columns: 1fr; }
|
||||
.capability-card { grid-column: auto; }
|
||||
.prospective-card { grid-column: auto; }
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
@@ -160,4 +171,8 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--signal-brig
|
||||
.data-card dl div { padding-right: 0 !important; }
|
||||
.capability-core { grid-template-columns: 76px 1fr; gap: 18px; }
|
||||
.radar { width: 72px; height: 72px; }
|
||||
.prospective-core { grid-template-columns: 1fr; }
|
||||
.prospective-core > div:first-child { padding-right: 0; border-right: 0; border-bottom: 1px solid var(--line-soft); }
|
||||
.prospective-core > div:last-child { padding-left: 0; }
|
||||
.time-marker i { left: 0; }
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ const loginForm = byId("login-form");
|
||||
const contextForm = byId("context-form");
|
||||
const loginError = byId("login-error");
|
||||
const toast = byId("toast");
|
||||
const contextFields = ["session-id", "workspace-id", "task-id", "capability-id"];
|
||||
const contextFields = ["session-id", "workspace-id", "task-id", "prospective-id", "capability-id"];
|
||||
let sessionToken = sessionStorage.getItem("domovoy.creator.session") || "";
|
||||
let toastTimer;
|
||||
|
||||
@@ -145,6 +145,12 @@ async function loadDashboard() {
|
||||
setBadge("task", "НЕ ЗАГРУЖЕНА", "neutral");
|
||||
setText("task-message", "Укажите задачу для проверки относительно активного фокуса.");
|
||||
}
|
||||
if (context["prospective-id"]) work.push(loadProspective(context["prospective-id"]));
|
||||
else {
|
||||
setSignal("prospective", "не выбрано", "idle");
|
||||
setBadge("prospective", "НЕ ЗАГРУЖЕНА", "neutral");
|
||||
setText("prospective-message", "Укажите идентификатор ProspectiveItem для проверки владельца и состояния.");
|
||||
}
|
||||
if (context["capability-id"]) work.push(loadCapability(context["capability-id"]));
|
||||
else setSignal("capability", "не выбрана", "idle");
|
||||
|
||||
@@ -189,6 +195,33 @@ async function loadTask(taskID, query) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProspective(prospectiveID) {
|
||||
setSignal("prospective", "загрузка", "loading");
|
||||
try {
|
||||
const item = await request(`/api/prospective-items/${encodeURIComponent(prospectiveID)}`);
|
||||
const state = item.State || "UNKNOWN";
|
||||
const goodStates = ["REGISTERED", "ARMED", "SATISFIED"];
|
||||
const warningStates = ["TRIGGERED", "REVALIDATING", "DELIVERED"];
|
||||
const tone = goodStates.includes(state) ? "good" : warningStates.includes(state) ? "warn" : "bad";
|
||||
setText("prospective-state", state);
|
||||
setText("prospective-trigger", item.TriggerAtUnixMillis ? new Date(item.TriggerAtUnixMillis).toLocaleString("ru-RU") : "не указано");
|
||||
setText("prospective-value", item.ID);
|
||||
setText("prospective-commitment", item.CommitmentID);
|
||||
setText("prospective-goal", item.GoalID);
|
||||
setText("prospective-scope", item.ScopeID);
|
||||
setText("prospective-owner", item.OwnerPrincipalID);
|
||||
setText("prospective-revision", item.Revision);
|
||||
setText("prospective-message", "Запись принадлежит текущему Principal и прочитана из канонического Continuity state.");
|
||||
setSignal("prospective", state.toLowerCase(), tone === "bad" ? "error" : "ok");
|
||||
setBadge("prospective", state, tone);
|
||||
} catch (error) {
|
||||
setSignal("prospective", "ошибка", "error");
|
||||
setBadge("prospective", "НЕДОСТУПНА", "bad");
|
||||
setText("prospective-state", "UNKNOWN");
|
||||
setText("prospective-message", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCapability(capabilityID) {
|
||||
setSignal("capability", "загрузка", "loading");
|
||||
try {
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
<input id="workspace-id" autocomplete="off" placeholder="workspace-id">
|
||||
<label for="task-id">Задача</label>
|
||||
<input id="task-id" autocomplete="off" placeholder="task-id">
|
||||
<label for="prospective-id">Ожидание</label>
|
||||
<input id="prospective-id" autocomplete="off" placeholder="prospective-item-id">
|
||||
<label for="capability-id">Возможность</label>
|
||||
<input id="capability-id" autocomplete="off" value="cognitive-proposal">
|
||||
<button class="secondary-button" type="submit">Загрузить контекст</button>
|
||||
@@ -84,6 +86,7 @@
|
||||
<section class="signal-strip" aria-label="Сводка состояния">
|
||||
<div><span>ФОКУС</span><strong id="focus-signal" data-state="idle">ожидает</strong></div>
|
||||
<div><span>ЗАДАЧА</span><strong id="task-signal" data-state="idle">ожидает</strong></div>
|
||||
<div><span>CONTINUITY</span><strong id="prospective-signal" data-state="idle">ожидает</strong></div>
|
||||
<div><span>ВОЗМОЖНОСТЬ</span><strong id="capability-signal" data-state="idle">ожидает</strong></div>
|
||||
</section>
|
||||
|
||||
@@ -110,8 +113,25 @@
|
||||
<p id="task-message" class="card-message">Укажите задачу для проверки относительно активного фокуса.</p>
|
||||
</article>
|
||||
|
||||
<article class="data-card prospective-card">
|
||||
<header><span class="card-number">03</span><div><p>Continuity</p><h2>Будущая значимость</h2></div><span id="prospective-badge" class="status-badge neutral">НЕ ЗАГРУЖЕНА</span></header>
|
||||
<div class="prospective-core">
|
||||
<div><span>СОСТОЯНИЕ</span><strong id="prospective-state">—</strong></div>
|
||||
<div class="time-marker"><i aria-hidden="true"></i><span>СРАБОТАЕТ</span><time id="prospective-trigger">—</time></div>
|
||||
</div>
|
||||
<dl class="compact-list">
|
||||
<div><dt>Ожидание</dt><dd id="prospective-value">—</dd></div>
|
||||
<div><dt>Обязательство</dt><dd id="prospective-commitment">—</dd></div>
|
||||
<div><dt>Цель</dt><dd id="prospective-goal">—</dd></div>
|
||||
<div><dt>Область</dt><dd id="prospective-scope">—</dd></div>
|
||||
<div><dt>Principal</dt><dd id="prospective-owner">—</dd></div>
|
||||
<div><dt>Ревизия</dt><dd id="prospective-revision">—</dd></div>
|
||||
</dl>
|
||||
<p id="prospective-message" class="card-message">Укажите идентификатор ProspectiveItem для проверки владельца и состояния.</p>
|
||||
</article>
|
||||
|
||||
<article class="data-card capability-card">
|
||||
<header><span class="card-number">03</span><div><p>Capability</p><h2>Готовность</h2></div><span id="capability-badge" class="status-badge neutral">НЕИЗВЕСТНО</span></header>
|
||||
<header><span class="card-number">04</span><div><p>Capability</p><h2>Готовность</h2></div><span id="capability-badge" class="status-badge neutral">НЕИЗВЕСТНО</span></header>
|
||||
<div class="capability-core">
|
||||
<div class="radar" aria-hidden="true"><span></span><i></i></div>
|
||||
<div><span>ТЕКУЩЕЕ СОСТОЯНИЕ</span><strong id="capability-state">—</strong><small id="capability-reason">Причина не получена</small></div>
|
||||
|
||||
@@ -100,8 +100,9 @@ services:
|
||||
environment:
|
||||
LISTEN_ADDR: ":8084"
|
||||
CORE_RUNTIME_URL: http://core-runtime:8080
|
||||
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
|
||||
ports: ["19084:8084"]
|
||||
depends_on: [core-runtime]
|
||||
depends_on: [core-runtime, continuity-runtime]
|
||||
|
||||
volumes:
|
||||
assistant_pg_dev:
|
||||
|
||||
@@ -88,8 +88,9 @@ services:
|
||||
environment:
|
||||
LISTEN_ADDR: ":8084"
|
||||
CORE_RUNTIME_URL: http://core-runtime:8080
|
||||
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
|
||||
ports: ["${CREATOR_WEB_PORT:-18080}:8084"]
|
||||
depends_on: [core-runtime]
|
||||
depends_on: [core-runtime, continuity-runtime]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
|
||||
interval: 10s
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"task_id": "DEV-000027",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "bootstrap-1",
|
||||
"base_repo_revision": "eb343cd76eb025949cd0543276930528ad2f2085",
|
||||
"risk_tier": "C",
|
||||
"intent": "Expose the authenticated Continuity read endpoint through the narrow Creator gateway and present an owned ProspectiveItem in the Domovoy console.",
|
||||
"allowed_change_scope": {"paths": ["cmd/creator-web/**", "deploy/docker-compose.prod.yml", "deploy/docker-compose.dev.yml", "tests/creator_web/**", "dev/tasks/DEV-000027-creator-continuity-console.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "cmd/core-runtime/**", "cmd/continuity-runtime/**", "cmd/commit-runtime/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0},
|
||||
"contracts": {"consume": ["contracts.continuity@v1"], "may_change": []},
|
||||
"semantic_owner": "none-interface-adapter",
|
||||
"assumptions": ["Continuity Runtime exposes DEV-000026 inside the assistant Docker network", "ProspectiveItem identifiers are explicit operator context and are not credentials"],
|
||||
"acceptance_scenarios": ["gateway allowlists the Creator ProspectiveItem GET and no Continuity mutation", "console displays state, trigger, owner-safe references and revision", "missing or denied items produce explicit UI state", "desktop and mobile layouts remain usable"],
|
||||
"required_tests": ["unit", "gateway allowlist", "runtime HTTP", "responsive browser", "architecture", "remote Docker smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -5,6 +5,22 @@ param(
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$coreContainer = "assistant-core-runtime-1"
|
||||
$continuityContainer = "assistant-continuity-runtime-1"
|
||||
|
||||
function Post-ContinuityJson([string]$url, [object]$body) {
|
||||
$json = $body | ConvertTo-Json -Depth 8 -Compress
|
||||
$local = New-TemporaryFile
|
||||
$remote = "/tmp/domovoy-$([guid]::NewGuid().ToString('N')).json"
|
||||
try {
|
||||
[IO.File]::WriteAllText($local, $json, [Text.UTF8Encoding]::new($false))
|
||||
docker --context $DockerContext cp $local "${continuityContainer}:$remote" | Out-Null
|
||||
docker --context $DockerContext exec $continuityContainer wget -qO- --header "Content-Type: application/json" --post-file $remote $url
|
||||
if ($LASTEXITCODE -ne 0) { throw "Continuity fixture creation failed" }
|
||||
} finally {
|
||||
docker --context $DockerContext exec --user root $continuityContainer rm -f $remote 2>$null
|
||||
Remove-Item -LiteralPath $local -Force
|
||||
}
|
||||
}
|
||||
|
||||
$health = Invoke-RestMethod -Uri "$BaseURL/healthz" -Method Get
|
||||
if ($health.status -ne "ok" -or $health.runtime -ne "creator-web") { throw "Creator web health response is invalid" }
|
||||
@@ -15,6 +31,8 @@ if (-not $page.Headers["Content-Security-Policy"]) { throw "Creator console secu
|
||||
|
||||
$denied = Invoke-WebRequest -Uri "$BaseURL/api/focus-task-commits" -Method Post -SkipHttpErrorCheck
|
||||
if ($denied.StatusCode -ne 404) { throw "Internal mutation route was exposed" }
|
||||
$continuityMutation = Invoke-WebRequest -Uri "$BaseURL/api/prospective-items/blocked" -Method Post -SkipHttpErrorCheck
|
||||
if ($continuityMutation.StatusCode -ne 404) { throw "Continuity mutation route was exposed" }
|
||||
$invalid = Invoke-WebRequest -Uri "$BaseURL/api/session/current" -Headers @{Authorization="Bearer invalid"} -SkipHttpErrorCheck
|
||||
if ($invalid.StatusCode -ne 401) { throw "Invalid Creator session was not denied" }
|
||||
|
||||
@@ -30,4 +48,14 @@ if ($current.ActorID -ne "domovoy-creator-actor") { throw "Creator identity was
|
||||
$capability = Invoke-RestMethod -Uri "$BaseURL/api/capabilities/cognitive-proposal/health" -Method Get -Headers @{Authorization="Bearer $($issued.Token)"}
|
||||
if ($capability.CapabilityID -ne "cognitive-proposal") { throw "Capability health was not returned through the gateway" }
|
||||
|
||||
Write-Output "DEV-000025 remote Creator web console: PASS"
|
||||
$suffix = [guid]::NewGuid().ToString("N")
|
||||
$prospectiveID = "web-prospective-$suffix"
|
||||
Post-ContinuityJson "http://localhost:8081/v1/prospective-items" @{
|
||||
ID=$prospectiveID;CommitmentID="web-commitment-$suffix";OwnerPrincipalID="domovoy-creator-principal"
|
||||
ScopeID="domovoy-global-scope";GoalID="web-goal-$suffix";TriggerAtUnixMillis=1800000000000
|
||||
State="ARMED";Revision=1
|
||||
}
|
||||
$prospective = Invoke-RestMethod -Uri "$BaseURL/api/prospective-items/$prospectiveID" -Method Get -Headers @{Authorization="Bearer $($issued.Token)"}
|
||||
if ($prospective.ID -ne $prospectiveID -or $prospective.State -ne "ARMED") { throw "ProspectiveItem was not returned through the gateway" }
|
||||
|
||||
Write-Output "DEV-000027 remote Creator Continuity console: PASS"
|
||||
|
||||
Reference in New Issue
Block a user