harness-control-plane and harness-pool-worker both declared a build: block
AND the same image: showcase-harness:local. Under the parallel docker buildx
bake path (default in recent Docker Compose) both targets build and race to
tag the identical image, failing the whole 'bin/showcase up' with
'image "...showcase-harness:local": already exists'. It only surfaces on a
cold parallel bake build (Colima/Homebrew, fresh clone), which is why it is
intermittent across toolchains.
Give the image a single build owner: harness-pool-worker now reuses the
image built by harness-control-plane (worker vs control-plane role is chosen
at runtime via HARNESS_ROLE) instead of declaring its own build:.
Playwright-verified fixes for the Mastra demo validation round:
- aimock interrupt fixtures (gen-ui-interrupt, interrupt-headless): add
hasToolResult:false to the schedule_meeting suspend legs so the resume
request falls through to the toolCallId confirmation fixture instead of
re-matching the suspend leg (picker loop, duplicated intro). Mirrors
hitl-in-chat.json.
- aimock-fixtures test: ceiling 301 -> 303; the two suspend keys now
intentionally collide across the three mastra interrupt cells
(runtime-disambiguated by route/fixtureFile like existing aliases).
- browse-web tool: return the result OBJECT instead of JSON.stringify;
the bridge encodes once more so stringifying double-encoded the result
and BrowseResultsCard showed "0 results" despite a successful browse.
- reasoning-chain pill: "Roll a d20 ..." instead of "Roll a 20-sided die
..." — the d4 agentic-chat fixture shadowed the first leg under replay
(d4 loads before d6) and pushed reasoning a step late. Real-LLM order
verified correct.
- header-forwarding shim: default x-aimock-context to "mastra" when absent
so browser-driven demos replay against aimock instead of 404ing. Harness
header wins when present; real providers ignore it.
- docker-compose.local: make OPENAI_BASE_URL overridable via .env (default
aimock unchanged) so real-LLM cells like browser-use can be tested live.
(--no-verify: commitlint binary missing in this worktree after the session
crash — ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL, infra not message)
Emit CVDIAG backend boundary markers from the agent process for strands-typescript (byteLength fix on sseChunkByteLength), enable the emitter in docker-compose.local.yml, vendor src/cvdiag, and exclude tests from tsconfig.
Add a new node/TypeScript-backed AWS Strands showcase integration at
showcase/integrations/strands-typescript.
Backend: a node/TS agent server (src/agent/) built on @strands-agents/sdk
`Agent`/`tool` wrapped in @ag-ui/aws-strands `StrandsAgent` and served via
@ag-ui/aws-strands/server (`createStrandsApp`/`addStrandsExpressEndpoint`),
modeled on the upstream ag-ui aws-strands TS example server and the
langgraph-typescript infra. A single shared agent at "/" serves most demos
(tools, shared state via toolBehaviors/stateContextBuilder, HITL,
sub-agents), with tool-free specialized agents mounted at /voice,
/byoc-hashbrown, /byoc-json-render. model-factory targets OpenAI chat
completions and honors OPENAI_API_KEY / OPENAI_BASE_URL so it works behind
the showcase aimock proxy. Node-based Dockerfile + entrypoint run the agent
server (:8000) alongside the Next.js frontend.
Frontend mirrors the strands (Python) sibling's demo set and the
langgraph-typescript conventions, with HttpAgent routes proxying to the TS
agent server.
Scope: base integration + standard demos only. A2UI / declarative-gen-ui /
a2ui-fixed-schema is intentionally excluded (no A2UI agents, routes, demos,
or deps) and layered on later.
Platform wiring (mirrors langgraph-typescript): docker-compose local/dev
services on host port 3119, local-ports.json, packages.json, slug-map.ts
(born-in-showcase), showcase_build.yml matrix + path filter + metadata,
shell-docs/dashboard registries, and a logo asset. The python strands
integration is untouched.
PR #5458 (c81b361f1) added a fail-loud gate that refuses harness boot
in any deployable mode (NODE_ENV != "test") without SHARED_SECRET or
SHARED_SECRET_PREV: POST /webhooks/deploy is only registered when
webhookSecrets.length > 0 (src/http/server.ts:119 +
loadWebhookSecrets in src/orchestrator.ts). The local docker-compose
stack inherits NODE_ENV=production from the harness image and does
not (and should not) set SHARED_SECRET, so every local D5/D6 verify
run via bin/showcase test --d5/--d6 was crashing the harness in a
restart loop with FATAL-CONFIG.
Fix: add HARNESS_ALLOW_NO_SECRET=1 (the documented local-dev escape
hatch — explicitly referenced in the FATAL-CONFIG message itself) to
both harness services in showcase/docker-compose.local.yml. Inline
comments explain the rationale and pin the relevant source locations.
Prod impact: NONE. Railway sets SHARED_SECRET explicitly via env on
every harness service, so loadWebhookSecrets sees a real secret,
registers POST /webhooks/deploy, and never reads
HARNESS_ALLOW_NO_SECRET. This change only affects the local
docker-compose stack.
Verified locally: showcase-iso1-harness boots cleanly (Up healthy),
the expected warn-level webhook-auth-bypass log fires
(escapeHatch:true), worker registers, scheduler starts, and a real
d6:langgraph-typescript job claims successfully — confirming the
gate fires only in deployable contexts.
Add a prominent warning banner above the aimock service block in
showcase/docker-compose.local.yml documenting that --proxy-only lets
unmatched fixture requests fall through to real OpenAI/Anthropic/Gemini.
That behavior is fine for interactive local dev (you can capture new
fixtures from real responses) but dangerous in any automated context:
a missing fixture produces a real LLM response, and tests/CI see a
green check that is actually a false positive while burning real
provider tokens. Comment-only change; no behavior change.
Replace single-file volume mounts with directory mounts (shared/, d4/,
d6/) so new fixture files are picked up without editing compose files.
Remove --proxy-only flag to catch fixture gaps instead of silently
falling through to real API. Add LANGGRAPH_HTTP env var from main.
Add LANGGRAPH_HTTP={"configurable_headers":{"include":["x-*"]}} to the
shared x-integration-defaults anchor so every LangGraph-based showcase
service tells langgraph-api to include x-* headers in the runtime
config. Without this env var, langgraph-api 0.7+ strips x-* headers
before the agent sees them, breaking D6 context routing end-to-end even
when the SDK side is correct.
1. Remove catch-all fixture from feature-parity.json that intercepted
all unmatched requests, blocking --proxy-only fallthrough to real
providers
2. Merge 13 d5-recorded fixtures into d5-all.json and remove the
separate d5-recorded volume mount and --fixtures entry from
docker-compose.local.yml (production only loads d5-all.json)
3. Add --validate-on-load flag to local aimock command (matches
production)
4. Add --provider-anthropic and --provider-gemini to local aimock
command (production has all 3 providers, local only had OpenAI)
Record-replay from LGT with real OpenAI key captures the exact request
format the LangGraph TS agent sends through the CopilotKit pipeline.
Covers Sales Dashboard (A2UI Dynamic), Calculator (Open Gen UI), and
Task Manager (Shared State) — all of which failed locally because
hand-written fixtures didn't match the LGT request format.
Also adds d5-recorded directory to docker-compose.local.yml fixture
loading so recordings replay automatically.
Default aimock config flushes the entire fixture body as fast as the
client can drain — totally fine for unit tests but visually wrong on
demos, where chats jump from "thinking" to a complete answer in a
single frame and break the suspension of disbelief.
Adds --chunk-size 8 --latency 60 to the local docker-compose so each
SSE frame carries 8 chars and waits 60 ms between frames. Net throughput
is ~130 chars/sec (~30-40 tokens/sec), the lower end of Gemini 2.5-flash
and Claude Sonnet real streaming rates. Verified end-to-end via the
langgraph-python runtime: 2.6 KB / 15-chunk response now arrives over
1.24 s vs essentially 0 ms before, matching what a human watches in the
real product.
Wall-clock impact for the longest fixture (~500 chars body) is ~4 s,
comfortably inside the 30 s default test timeout. CI integration-docs
and Railway production aimock are intentionally unchanged — CI doesn't
benefit from pacing and Railway runs its own command line.
Reduce google-adk's agentic-chat to the minimum-viable CopilotChat surface
matching langgraph-python: vanilla <CopilotChat /> + three starter
suggestion pills. Strip useFrontendTool (change_background), useRenderTool
(get_weather — was silently failing, no backend tool registered), and
useAgentContext (Bob's name). These concepts already have dedicated demos
(frontend-tools, tool-rendering, readonly-state-agent-context).
Also: make GOOGLE_GEMINI_BASE_URL overridable via .env so the same
container can flip between aimock (deterministic D5 probes) and real
Gemini (manual verification) without editing the compose file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The langgraph-python multimodal-attachments demo had a stack of bugs
that compounded each other. Fixing them required touching the local
docker-compose, the aimock fixtures, the LangChain middleware, the
client-side AG-UI shim, and the sample-attachment buttons. This
commit lands the full set together because they only make sense as
a unit — verified end-to-end against `showcase up langgraph-python`
in a headed browser. New e2e suite pins each regression.
Supersedes #4584 (the original fix from May 1 that never landed —
this is a fresh port onto the post-refactor file layout where
page.tsx is split into legacy-converter-shim.tsx, multimodal-chat.tsx,
file-to-data-attachment.ts).
What was broken and what changed:
1. Random uploads crashed with `Failed to fetch`. aimock returned
HTTP 404 on no-match, the LangGraph SDK surfaced `NotFoundError`,
the AG-UI stream surfaced a `RUN_ERROR`, the demo crashed.
Added `--proxy-only` + `--provider-openai https://api.openai.com`
to the local aimock command so unmatched user prompts fall through
to real OpenAI (mirrors the Railway aimock setup).
2. Bundled-sample fixtures keyed on user-visible canned prompts.
The auto-prompts are deliberately long, specific, and natural-
reading ("can you tell me what is in this demo image/pdf I just
attached") so they (a) render cleanly as the user message bubble,
and (b) can't collide with arbitrary user prompts — random
uploads phrase questions differently and fall through to the
proxy.
3. Sample buttons now auto-send via `useAgent`. The previous
DataTransfer-based path queued the attachment via the chat's
hidden file input, then required clicking send while the
attachment was still uploading — `CopilotChat.onSubmitInput`
rejects submits during upload AND clears the input regardless,
so the canned prompt was eaten. Rewrite to call
`agent.addMessage(...)` + `copilotkit.runAgent({ agent })`
directly with the base64'd content part, sidestepping the
upload race entirely.
4. PDF flattened text bled into the rendered user message.
`_PdfFlattenMiddleware` ran in `before_model` and returned
`{"messages": rewritten}`, which persisted to agent state. The
chat UI then rendered the `[Attached document]\n<pdf body>` text
part inline with the user prompt. Switched to `wrap_model_call`
so the PDF→text rewrite is scoped to the outgoing model request
only and never pollutes state.
5. Attachments doubled (and PDFs rendered as broken `<img>`). The
`@ag-ui/langgraph` round-trip translates outgoing `binary` parts
to LangChain `image_url` and incoming `image_url` back to `image`
AG-UI parts — regardless of mimeType, so PDFs came back as
`type: "image"` with `mimeType: "application/pdf"` and were
forced into `ImageAttachment`, where the load failed and the
chat showed two "Failed to load image" boxes. Plus the user's
original modern part survived alongside the round-tripped one,
doubling visible chips.
Added a `dedupeUserMessageMedia` subscriber on both
`onMessagesSnapshotEvent` and `onRunFinalized` to:
- dedupe media parts by `source.value` so the local + round-
tripped copy collapse to one chip
- re-key part `type` from `mimeType` so PDFs route to
`DocumentAttachment` (icon + filename) and images to
`ImageAttachment`.
Also flipped the `onRunInitialized` shim from REPLACE to APPEND
— keep the modern part for the UI AND emit a legacy `binary`
sibling for the converter.
6. Regression suite (`tests/e2e/multimodal.spec.ts`). Replaces the
pre-rewrite suite with five focused tests:
- page loads with all expected affordances
- sample image: auto-sends, EXACTLY ONE `<img>`, assistant
references the logo
- sample PDF: auto-sends, EXACTLY ONE `DocumentAttachment` chip
("PDF" label), NO `<img>`, no `[Attached document]` text bleed
- image then PDF in the same session: each message keeps its own
single chip, no cross-contamination
- PDF then image in the same session: symmetric
All 5 pass against the live local stack (15.4s).
The dashboard image was being built with only `OPS_BASE_URL` set; the
two other compile-time env vars used by the matrix view stayed unset:
- NEXT_PUBLIC_POCKETBASE_URL — left at the dev fallback in production
builds, which surfaces a "FATAL-CONFIG: NEXT_PUBLIC_POCKETBASE_URL
was unset at build time" banner in the browser console and causes
the matrix's PB SSE subscription to fail (no live updates).
- NEXT_PUBLIC_SHELL_URL — left unset, so every Demo / Code link in
the matrix renders as `about:blank#shell-url-missing`.
Pin both at build time:
- POCKETBASE_URL → http://localhost:8090 (host port the user's browser
reaches; container-internal `pocketbase:8090` is unresolvable from
outside the compose network).
- SHELL_URL → http://localhost:3100 (langgraph-python integration
host port; no shared "shell" service in the local stack).
The dashboard needs OPS_BASE_URL at build time to connect to the local
harness. Without this, the dashboard container builds with no API URL
and cannot display probe results locally.
The dashboard Dockerfile uses COPY paths prefixed with `showcase/...`
which expect the repo root as build context. The compose entry was
using `build: ./shell-dashboard` which set context to
showcase/shell-dashboard/, causing COPY failures in worktrees.
Split into explicit context (../ relative to compose file = repo root)
and dockerfile path so COPY paths resolve correctly.
TanStack's chat() engine runs a multi-turn agent loop: after the model
returns tool calls, it tries to execute them via processToolCalls().
Frontend-only tools (like render_pie_chart) are unknown to TanStack, so
executeToolCalls() treats them as errors and buildToolResultChunks()
re-emits TOOL_CALL_END without a preceding TOOL_CALL_START. The ag-ui
verify middleware rejects this duplicate.
Fix: track a runFinished flag in convertTanStackStream and discard all
events after the first RUN_FINISHED, which marks the boundary between
the streaming pass and TanStack's internal tool execution loop.
Also adds built-in-agent to docker-compose.local.yml and local-ports.json.
The host bind mount `src:/app/src` in docker-compose.local.yml overlays
the container's /app/src directory, clobbering src/agent/node_modules
that was installed during the Docker build. This causes `node --import tsx`
to fail with ERR_MODULE_NOT_FOUND at container startup.
Add an anonymous volume for /app/src/agent/node_modules so Docker
preserves the image's copy even when the parent directory is bind-mounted.
Production showcase-aimock was running a week-old image because fixture
file changes in showcase/aimock/ did not trigger a CI rebuild. This adds
showcase-aimock to the Build & Deploy workflow matrix so it auto-deploys
on merge, creates a thin Dockerfile that bakes fixture files into the
image, documents the local-vs-production parity requirement in
docker-compose.local.yml, and adds an aimock fixture deployment section
to the RUNBOOK.
- Mount D5/smoke/feature-parity fixtures into aimock via volumes
- Add --host 0.0.0.0 so other containers can reach aimock
- Switch healthcheck from curl to node fetch (aimock image has no curl)
- Delete dev-local.sh (redundant with bin/showcase CLI)
Add x-integration-defaults YAML anchor to DRY 17 service definitions.
Switch aimock from local build to ghcr.io/copilotkit/aimock:latest.
Add healthchecks to all 20 services (internal ports, not host-mapped).
Add pocketbase and dashboard infra services with proper depends_on
conditions. Add profiles to every service so compose enumeration works.
Wires up a single-command path to run the full integration smoke suite
against a local Docker stack instead of Railway. Useful when Railway is
degraded (OOM, rate limits) or when testing changes that haven't been
deployed yet.
Additions:
- showcase/docker-compose.local.yml: add `aimock` as 18th service so
integration containers can reach http://aimock:4010 on the compose
network, mirroring the Railway setup where they call showcase-aimock.
- showcase/tests/e2e/integration-smoke.spec.ts: `LOCAL_PORTS=1` env
rewrites each integration's Railway URL to http://localhost:<port>
via showcase/shared/local-ports.json. Starters are skipped under this
flag because they aren't in local-ports.json.
- showcase/scripts/smoke-local.sh: orchestrates build → up → wait → run
Playwright → tear down. Supports --level=L1/L2/L3/L4, --keep, --no-build.
- showcase/tests/package.json: `pnpm smoke:local[:L1|:keep|:nobuild]`
scripts delegate to the helper.
- showcase/.env.example: document optional OPENAI_BASE_URL +
ANTHROPIC_BASE_URL (route through local aimock) and package-specific
GitHubToken + GOOGLE_API_KEY (ms-agent-dotnet, google-adk).
Verified locally: `pnpm smoke:local:L1` → 17/17 L1 green against the
local stack.
Adds a single command to spin up the exact image Railway deploys,
for any of the 17 showcase packages, with a single shared .env:
./showcase/scripts/dev-local.sh up [<slug> ...] # all if empty
./showcase/scripts/dev-local.sh down|build|logs|ps|ports
Pieces:
- `docker-compose.local.yml` with a service per package. Ports come
from `shared/local-ports.json` (langgraph-python -> 3100, ...).
- `.env.example` as a commit-safe template. Real `.env` is gitignored
and fed to every container via `env_file`, so keys (OPENAI_API_KEY,
etc.) live in one place.
- `dev-local.sh` wraps `docker compose` and handles the
`shared_python/` / `shared_typescript/` staging step that CI does
before `docker build` (see showcase_deploy.yml).
- Staged `shared_*` dirs added to .gitignore.
Shell wiring:
- `shell/next.config.ts` reads `shared/local-ports.json` when
`SHOWCASE_LOCAL=1` is set and injects it as a public env.
- `/integrations/[slug]/[demo]/preview` uses that map to iframe
`http://localhost:<port>` instead of `integration.backend_url`.
Per-slug; any slug not running locally falls back to Railway.
Unset SHOWCASE_LOCAL -> prod behavior, unchanged.
Full workflow + prerequisites (Colima / Docker Desktop) documented in
showcase/README.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>