feat: show Creator continuity in Domovoy
CI / test (push) Failing after 15m2s

This commit is contained in:
m
2026-08-16 15:51:18 +03:00
parent eb343cd76e
commit 6a5550b7b0
10 changed files with 201 additions and 28 deletions
+5 -1
View File
@@ -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
View File
@@ -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 {
+28 -3
View File
@@ -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)
}
+17 -2
View File
@@ -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; }
}
+34 -1
View File
@@ -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 {
+21 -1
View File
@@ -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>
+2 -1
View File
@@ -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:
+2 -1
View File
@@ -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"]
}
+29 -1
View File
@@ -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"