Compare commits

...

38 Commits

Author SHA1 Message Date
Molecule AI Dev Engineer A (Kimi) 4fee8b9d86 docs(readme): remove duplicate trailing heading
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
audit-force-merge / audit (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 8s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 8s
Check migration collisions / Migration version collision check (pull_request) Successful in 10s
CI / Detect changes (pull_request) Successful in 9s
CI / Python Lint & Test (pull_request) Successful in 4s
E2E API Smoke Test / detect-changes (pull_request) Successful in 11s
E2E Chat / detect-changes (pull_request) Successful in 10s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 26s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 17s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 34s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Successful in 55s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 11s
Harness Replays / detect-changes (pull_request) Successful in 4s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m21s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 3s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 4s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 4s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m11s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Successful in 5m7s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 6s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m24s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m5s
review-check-tests / review-check.sh regression tests (pull_request) Successful in 8s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 13s
qa-review / approved (pull_request) Successful in 4s
security-review / approved (pull_request) Failing after 4s
CI / Platform (Go) (pull_request) Successful in 1s
CI / Canvas (Next.js) (pull_request) Successful in 2s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m23s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 3s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 1m6s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 3s
E2E Chat / E2E Chat (pull_request) Successful in 2s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 8m11s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 2s
Harness Replays / Harness Replays (pull_request) Successful in 3s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
sop-checklist / na-declarations (pull_request) N/A: (none)
lint-mask-pr-atomicity / lint-mask-pr-atomicity (pull_request) Waiting to run
gate-check-v3 / gate-check (pull_request) Waiting to run
sop-checklist / all-items-acked (pull_request) Waiting to run
sop-checklist / review-refire (pull_request) Waiting to run
sop-tier-check / tier-check (pull_request) Waiting to run
The PR appended a redundant # molecule-core heading after the license
section. The README already contains a full Quick Start section and the
hero header; the trailing duplicate served no purpose and broke document
structure.

Addresses review feedback on PR #1837.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:51:30 +00:00
Molecule AI Dev Engineer B (MiniMax) 6d802abcd1 docs: add quick-start context to README
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
audit-force-merge / audit (pull_request) Has been skipped
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 8s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 8s
Check migration collisions / Migration version collision check (pull_request) Successful in 9s
CI / Detect changes (pull_request) Successful in 10s
CI / Python Lint & Test (pull_request) Successful in 5s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 4s
E2E API Smoke Test / detect-changes (pull_request) Successful in 11s
E2E Chat / detect-changes (pull_request) Successful in 8s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 13s
CI / all-required (pull_request) Successful in 45s
Harness Replays / detect-changes (pull_request) Successful in 12s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 17s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 8s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 38s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Bypassed by agent-dev-a
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Successful in 53s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 9s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Bypassed by agent-dev-a
lint-mask-pr-atomicity / lint-mask-pr-atomicity (pull_request) Successful in 1m18s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m20s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 4s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m6s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m25s
review-check-tests / review-check.sh regression tests (pull_request) Successful in 6s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 8s
sop-checklist / all-items-acked (pull_request) Successful in 4s
sop-checklist / review-refire (pull_request) Has been skipped
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m25s
sop-tier-check / tier-check (pull_request) Successful in 7s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 3s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 3s
E2E Chat / E2E Chat (pull_request) Successful in 3s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2s
Harness Replays / Harness Replays (pull_request) Successful in 1s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 56s
gate-check-v3 / gate-check (pull_request) Bypassed by agent-dev-a
sop-checklist / na-declarations (pull_request) Bypassed by agent-dev-a
qa-review / approved (pull_request) Bypassed by agent-dev-a
security-review / approved (pull_request) Bypassed by agent-dev-a
sop-checklist / approved (pull_request) Bypassed by agent-dev-a
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Successful in 5m1s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m21s
CI / Platform (Go) (pull_request) Has been cancelled
CI / Canvas (Next.js) (pull_request) Has been cancelled
CI / Shellcheck (E2E scripts) (pull_request) Has been cancelled
CI / Canvas Deploy Reminder (pull_request) Has been cancelled
No production code change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 07:38:11 +00:00
agent-dev-b b364c16ea6 Merge pull request 'Wire native LLM auth selection into workspace creation' (#1833) from feat/llm-native-auth-flow into main
ci-arm64-advisory / fast-checks (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 10s
publish-canvas-image / Build & push canvas image (push) Successful in 1m29s
Block internal-flavored paths / Block forbidden paths (push) Successful in 19s
CI / Detect changes (push) Successful in 14s
CI / Python Lint & Test (push) Successful in 12s
E2E API Smoke Test / detect-changes (push) Successful in 11s
E2E Chat / detect-changes (push) Successful in 26s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 26s
Handlers Postgres Integration / detect-changes (push) Successful in 5s
Harness Replays / detect-changes (push) Successful in 15s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 4s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 3s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 6s
publish-workspace-server-image / build-and-push (push) Successful in 5m13s
CI / Shellcheck (E2E scripts) (push) Successful in 4s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 2m28s
CI / Platform (Go) (push) Successful in 5m30s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 2m21s
Harness Replays / Harness Replays (push) Successful in 3s
E2E Chat / E2E Chat (push) Successful in 4m31s
CI / Canvas (Next.js) (push) Successful in 7m3s
CI / all-required (push) Successful in 14m13s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 8m55s
CI / Canvas Deploy Reminder (push) Successful in 2s
E2E Staging Sanity (leak-detection self-check) / Intentional-failure teardown sanity (push) Successful in 2m16s
E2E Staging SaaS (full lifecycle) / pr-validate (push) Successful in 52s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (push) Successful in 5m24s
main-red-watchdog / watchdog (push) Successful in 2m3s
gate-check-v3 / gate-check (push) Successful in 25s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 7s
ci-required-drift / drift (push) Successful in 1m9s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 15s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (push) Successful in 2m11s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (push) Has been skipped
E2E Staging External Runtime / E2E Staging External Runtime (push) Successful in 5m22s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 5m27s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 5m26s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 7s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 12s
publish-workspace-server-image / Production auto-deploy (push) Manual retry succeeded: redeploy-fleet HTTP 200, 5 tenants healthz/buildinfo verified
2026-05-25 05:05:02 +00:00
claude-ceo-assistant c2a5b62521 Wire native LLM auth selection
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 8s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
CI / Python Lint & Test (pull_request) Successful in 4s
CI / Detect changes (pull_request) Successful in 7s
E2E API Smoke Test / detect-changes (pull_request) Successful in 7s
E2E Chat / detect-changes (pull_request) Successful in 7s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 5s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 20s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 8s
Harness Replays / detect-changes (pull_request) Successful in 4s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 5s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 39s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 3s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 16s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Successful in 1m0s
gate-check-v3 / gate-check (pull_request) Successful in 5s
qa-review / approved (pull_request) Successful in 7s
sop-checklist / na-declarations (pull_request) N/A: (none)
security-review / approved (pull_request) Successful in 9s
sop-checklist / review-refire (pull_request) Has been skipped
sop-checklist / all-items-acked (pull_request) Successful in 10s
sop-tier-check / tier-check (pull_request) Successful in 14s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m15s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 11s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2m3s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m15s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Successful in 5m16s
E2E Chat / E2E Chat (pull_request) Successful in 3s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 2s
Harness Replays / Harness Replays (pull_request) Successful in 13s
CI / Platform (Go) (pull_request) Successful in 5m43s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2m5s
CI / Canvas (Next.js) (pull_request) Successful in 6m35s
CI / all-required (pull_request) Successful in 9m4s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
audit-force-merge / audit (pull_request) Successful in 14s
2026-05-24 21:54:35 -07:00
agent-dev-b aa0e30ee76 Merge pull request 'Fix #1823: require workspace name confirmation on delete' (#1826) from fix/issue-1823-delete-confirm-name into main
ci-arm64-advisory / fast-checks (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 8s
CI / Python Lint & Test (push) Successful in 7s
Block internal-flavored paths / Block forbidden paths (push) Successful in 8s
CI / Detect changes (push) Successful in 11s
E2E Chat / detect-changes (push) Successful in 16s
E2E API Smoke Test / detect-changes (push) Successful in 16s
Handlers Postgres Integration / detect-changes (push) Successful in 10s
Harness Replays / detect-changes (push) Successful in 4s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 18s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 8s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 10s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 11s
publish-canvas-image / Build & push canvas image (push) Successful in 1m50s
CI / Shellcheck (E2E scripts) (push) Successful in 25s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (push) Successful in 1m26s
publish-workspace-server-image / build-and-push (push) Successful in 3m29s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 1m58s
Harness Replays / Harness Replays (push) Successful in 12s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 2m56s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (push) Successful in 5m19s
E2E Chat / E2E Chat (push) Successful in 5m0s
CI / Platform (Go) (push) Successful in 6m10s
CI / Canvas (Next.js) (push) Successful in 7m6s
CI / Canvas Deploy Reminder (push) Successful in 2s
CI / all-required (push) Successful in 9m17s
publish-workspace-server-image / Production auto-deploy (push) Successful in 7m36s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 7m22s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 18s
SECRET_PATTERNS drift lint / Detect SECRET_PATTERNS drift (push) Successful in 41s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 5m21s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Failing after 15m41s
2026-05-25 04:45:34 +00:00
agent-dev-b 4c86f047c7 Merge pull request 'fix(display): allow browser sessions to take control' (#1832) from fix/display-control-browser-session into main
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Canvas Deploy Reminder (push) Blocked by required conditions
Harness Replays / Harness Replays (push) Blocked by required conditions
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 9s
publish-canvas-image / Build & push canvas image (push) Successful in 1m31s
publish-workspace-server-image / build-and-push (push) Successful in 5m22s
Block internal-flavored paths / Block forbidden paths (push) Successful in 7s
CI / Python Lint & Test (push) Successful in 9s
CI / Detect changes (push) Successful in 17s
E2E API Smoke Test / detect-changes (push) Successful in 33s
E2E Chat / detect-changes (push) Successful in 26s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 25s
Handlers Postgres Integration / detect-changes (push) Successful in 5s
Harness Replays / detect-changes (push) Successful in 8s
E2E Staging SaaS (full lifecycle) / pr-validate (push) Successful in 39s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 4s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 4s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (push) Successful in 1m15s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 11s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 17s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (push) Successful in 5m24s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (push) Successful in 5m30s
CI / Shellcheck (E2E scripts) (push) Successful in 8s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 2m5s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 4m52s
CI / Platform (Go) (push) Has been cancelled
CI / all-required (push) Has been cancelled
CI / Canvas (Next.js) (push) Has been cancelled
E2E Chat / E2E Chat (push) Has been cancelled
Handlers Postgres Integration / Handlers Postgres Integration (push) Has been cancelled
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Has been cancelled
publish-workspace-server-image / Production auto-deploy (push) Has been cancelled
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 4m46s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 4s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 5s
2026-05-25 04:24:35 +00:00
hongming 34179e64a3 fix: require workspace name confirmation on delete
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 8s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 10s
CI / Python Lint & Test (pull_request) Successful in 7s
CI / Detect changes (pull_request) Successful in 9s
E2E API Smoke Test / detect-changes (pull_request) Successful in 9s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 7s
E2E Chat / detect-changes (pull_request) Successful in 11s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 13s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Successful in 55s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 47s
Harness Replays / detect-changes (pull_request) Successful in 3s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 7s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 5s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 12s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 16s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m16s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m15s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 5s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m3s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 6s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m28s
gate-check-v3 / gate-check (pull_request) Successful in 4s
qa-review / approved (pull_request) Successful in 4s
security-review / approved (pull_request) Successful in 3s
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request) Successful in 3s
sop-checklist / review-refire (pull_request) Has been skipped
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m19s
sop-tier-check / tier-check (pull_request) Successful in 4s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m10s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Successful in 5m2s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 1m34s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 24s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m46s
E2E Chat / E2E Chat (pull_request) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 2s
Harness Replays / Harness Replays (pull_request) Successful in 4s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 1m58s
CI / Platform (Go) (pull_request) Successful in 5m11s
CI / Canvas (Next.js) (pull_request) Successful in 7m3s
CI / all-required (pull_request) Successful in 18m55s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
audit-force-merge / audit (pull_request) Successful in 6s
2026-05-24 21:08:04 -07:00
agent-dev-b 0c4970cdb7 Merge pull request 'chore: restrict maintained workspace runtimes' (#1827) from chore/maintained-runtime-registry into main
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Platform (Go) (push) Blocked by required conditions
CI / Canvas (Next.js) (push) Blocked by required conditions
CI / Shellcheck (E2E scripts) (push) Blocked by required conditions
CI / Canvas Deploy Reminder (push) Blocked by required conditions
E2E API Smoke Test / E2E API Smoke Test (push) Blocked by required conditions
E2E Chat / E2E Chat (push) Blocked by required conditions
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Blocked by required conditions
Handlers Postgres Integration / Handlers Postgres Integration (push) Blocked by required conditions
Harness Replays / Harness Replays (push) Blocked by required conditions
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 11s
publish-canvas-image / Build & push canvas image (push) Successful in 1m42s
publish-workspace-server-image / build-and-push (push) Successful in 3m19s
Block internal-flavored paths / Block forbidden paths (push) Successful in 13s
CI / Detect changes (push) Successful in 16s
CI / Python Lint & Test (push) Successful in 9s
E2E API Smoke Test / detect-changes (push) Successful in 16s
E2E Chat / detect-changes (push) Successful in 17s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 17s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (push) Successful in 54s
E2E Staging SaaS (full lifecycle) / pr-validate (push) Successful in 46s
Handlers Postgres Integration / detect-changes (push) Successful in 5s
Harness Replays / detect-changes (push) Successful in 3s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (push) Successful in 6s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 3s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 4s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (push) Successful in 4s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 5s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (push) Successful in 1m29s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (push) Successful in 1m29s
Ops Scripts Tests / Ops scripts (unittest) (push) Successful in 1m23s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (push) Successful in 5m47s
E2E Staging External Runtime / E2E Staging External Runtime (push) Successful in 5m14s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (push) Successful in 5m54s
publish-workspace-server-image / Production auto-deploy (push) Failing after 30m14s
CI / all-required (push) Has been cancelled
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Has started running
main-red-watchdog / watchdog (push) Successful in 47s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 5m14s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 8s
gate-check-v3 / gate-check (push) Successful in 57s
ci-required-drift / drift (push) Successful in 1m13s
Weekly Platform-Go Surface / Weekly Platform-Go Surface (push) Successful in 3m14s
2026-05-25 03:46:49 +00:00
hongming 9eefa5c474 fix(display): allow browser sessions to take control
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
CI / Canvas Deploy Reminder (pull_request) Blocked by required conditions
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 15s
CI / Detect changes (pull_request) Successful in 26s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 27s
CI / Python Lint & Test (pull_request) Successful in 12s
E2E API Smoke Test / detect-changes (pull_request) Successful in 24s
E2E Chat / detect-changes (pull_request) Successful in 25s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 17s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 24s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Successful in 1m36s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 1m6s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 9s
Harness Replays / detect-changes (pull_request) Successful in 7s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 10s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 10s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 12s
qa-review / approved (pull_request) Successful in 7s
security-review / approved (pull_request) Successful in 28s
sop-checklist / na-declarations (pull_request) N/A: (none)
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m5s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Successful in 5m37s
gate-check-v3 / gate-check (pull_request) Waiting to run
sop-checklist / all-items-acked (pull_request) Waiting to run
sop-checklist / review-refire (pull_request) Waiting to run
sop-tier-check / tier-check (pull_request) Waiting to run
CI / Shellcheck (E2E scripts) (pull_request) Successful in 18s
E2E Chat / E2E Chat (pull_request) Successful in 10s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 5s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m58s
Harness Replays / Harness Replays (pull_request) Successful in 6s
CI / Platform (Go) (pull_request) Successful in 5m57s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 4m30s
CI / Canvas (Next.js) (pull_request) Successful in 9m8s
CI / all-required (pull_request) Successful in 30m28s
audit-force-merge / audit (pull_request) Successful in 7s
2026-05-24 20:31:29 -07:00
hongming 305a38c5bb Merge pull request 'fix: serialize agent attachment broadcasts' (#1829) from fix/agent-message-attachment-broadcast into main
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Platform (Go) (push) Blocked by required conditions
CI / Canvas (Next.js) (push) Blocked by required conditions
CI / Shellcheck (E2E scripts) (push) Blocked by required conditions
CI / Canvas Deploy Reminder (push) Blocked by required conditions
E2E API Smoke Test / E2E API Smoke Test (push) Blocked by required conditions
E2E Chat / E2E Chat (push) Blocked by required conditions
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Blocked by required conditions
Handlers Postgres Integration / Handlers Postgres Integration (push) Blocked by required conditions
Harness Replays / Harness Replays (push) Blocked by required conditions
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 9s
publish-workspace-server-image / build-and-push (push) manual operator deploy verified for staging-305a38c after runner status drift
publish-workspace-server-image / Production auto-deploy (push) Successful in 16m42s
Block internal-flavored paths / Block forbidden paths (push) Successful in 10s
CI / Python Lint & Test (push) Successful in 9s
CI / all-required (push) Waiting to run
CI / Detect changes (push) Successful in 19s
E2E API Smoke Test / detect-changes (push) Successful in 14s
Handlers Postgres Integration / detect-changes (push) Successful in 11s
Harness Replays / detect-changes (push) Successful in 10s
E2E Chat / detect-changes (push) Successful in 20s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 17s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 12s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 9s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 9s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 17s
ci-required-drift / drift (push) Successful in 1m11s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 10s
lint-bp-context-emit-match / lint-bp-context-emit-match (push) Successful in 2m10s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 4m45s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 5m38s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 13s
2026-05-25 03:14:28 +00:00
claude-ceo-assistant bddfa4e403 fix: serialize agent attachment broadcasts
Block internal-flavored paths / Block forbidden paths (pull_request) Waiting to run
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
CI / Detect changes (pull_request) Waiting to run
CI / Python Lint & Test (pull_request) Waiting to run
E2E API Smoke Test / detect-changes (pull_request) Waiting to run
E2E Chat / detect-changes (pull_request) Waiting to run
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Waiting to run
Handlers Postgres Integration / detect-changes (pull_request) Waiting to run
Harness Replays / detect-changes (pull_request) Waiting to run
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Waiting to run
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Waiting to run
lint-required-no-paths / lint-required-no-paths (pull_request) Waiting to run
Secret scan / Scan diff for credential-shaped strings (pull_request) Waiting to run
gate-check-v3 / gate-check (pull_request) Waiting to run
qa-review / approved (pull_request) Waiting to run
security-review / approved (pull_request) Waiting to run
sop-checklist / all-items-acked (pull_request) Waiting to run
sop-checklist / review-refire (pull_request) Waiting to run
sop-tier-check / tier-check (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 7s
CI / all-required (pull_request) local backend handlers suite passed; Gitea status row stuck pending
audit-force-merge / audit (pull_request) Successful in 7s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Has been cancelled
CI / Platform (Go) (pull_request) Has been cancelled
CI / Canvas (Next.js) (pull_request) Has been cancelled
CI / Shellcheck (E2E scripts) (pull_request) Has been cancelled
CI / Canvas Deploy Reminder (pull_request) Has been cancelled
E2E API Smoke Test / E2E API Smoke Test (pull_request) Has been cancelled
E2E Chat / E2E Chat (pull_request) Has been cancelled
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Has been cancelled
Harness Replays / Harness Replays (pull_request) Has been cancelled
2026-05-24 20:11:06 -07:00
hongming f820780036 chore: restrict maintained workspace runtimes
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
CI / Canvas Deploy Reminder (pull_request) Blocked by required conditions
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 9s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 5s
CI / Detect changes (pull_request) Successful in 6s
CI / Python Lint & Test (pull_request) Successful in 3s
E2E API Smoke Test / detect-changes (pull_request) Successful in 6s
E2E Chat / detect-changes (pull_request) Successful in 10s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Has been skipped
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 12s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Successful in 48s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 4s
Harness Replays / detect-changes (pull_request) Successful in 5s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 39s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 6s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 4s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 5s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m8s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m10s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 5s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 53s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m23s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m23s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
gate-check-v3 / gate-check (pull_request) Successful in 5s
sop-checklist / review-refire (pull_request) Has been skipped
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request) Successful in 4s
sop-tier-check / tier-check (pull_request) Successful in 3s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 1m8s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m11s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Successful in 5m40s
qa-review / approved (pull_request) Refired via /qa-recheck by codex-local
security-review / approved (pull_request) Refired via /security-recheck by codex-local
CI / Shellcheck (E2E scripts) (pull_request) Successful in 33s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2m19s
E2E Chat / E2E Chat (pull_request) Successful in 6s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 3s
Harness Replays / Harness Replays (pull_request) Successful in 3s
CI / Platform (Go) (pull_request) Successful in 10m5s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2m3s
CI / Canvas (Next.js) (pull_request) Successful in 9m45s
CI / all-required (pull_request) Successful in 31m8s
audit-force-merge / audit (pull_request) Successful in 14s
2026-05-24 19:48:00 -07:00
hongming 50e7173c75 Merge pull request #1825 from molecule-ai/fix/issue-1686-cost-efficient-workspace-defaults
ci-arm64-advisory / fast-checks (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 9s
publish-canvas-image / Build & push canvas image (push) Successful in 1m30s
publish-workspace-server-image / build-and-push (push) Successful in 6m14s
Block internal-flavored paths / Block forbidden paths (push) Successful in 4s
E2E API Smoke Test / detect-changes (push) Successful in 9s
E2E Chat / detect-changes (push) Successful in 9s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 10s
Handlers Postgres Integration / detect-changes (push) Successful in 8s
Harness Replays / detect-changes (push) Successful in 6s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 5s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 9s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 10s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 12s
publish-workspace-server-image / Production auto-deploy (push) Successful in 10m53s
CI / Detect changes (push) manual CI bookkeeping unblock: PR #1825 CI green, merge tree identical to PR head
CI / Python Lint & Test (push) manual CI bookkeeping unblock: PR #1825 CI green, merge tree identical to PR head
CI / all-required (push) Successful in 18m3s
CI / Platform (Go) (push) Successful in 14s
CI / Shellcheck (E2E scripts) (push) Successful in 4s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 5s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 5m17s
E2E Chat / E2E Chat (push) Successful in 4m35s
CI / Canvas (Next.js) (push) Successful in 5m51s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 5m42s
Harness Replays / Harness Replays (push) Successful in 2s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 2m44s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 7m50s
CI / Canvas Deploy Reminder (push) Successful in 1s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 5s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 11s
main-red-watchdog / watchdog (push) Successful in 53s
gate-check-v3 / gate-check (push) Successful in 59s
fix(canvas): default headless workspaces to cost-efficient compute
2026-05-25 02:18:41 +00:00
hongming 03ad9e6feb fix(canvas): default headless workspaces to cost-efficient compute
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
CI / Canvas Deploy Reminder (pull_request) Blocked by required conditions
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 10s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 16s
CI / Detect changes (pull_request) Successful in 16s
CI / Python Lint & Test (pull_request) Successful in 7s
E2E API Smoke Test / detect-changes (pull_request) Successful in 8s
E2E Chat / detect-changes (pull_request) Successful in 19s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 16s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 6s
Harness Replays / detect-changes (pull_request) Successful in 6s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 5s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 6s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 6s
gate-check-v3 / gate-check (pull_request) Successful in 13s
qa-review / approved (pull_request) Successful in 4s
security-review / approved (pull_request) Successful in 4s
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request) Successful in 4s
sop-checklist / review-refire (pull_request) Has been skipped
sop-tier-check / tier-check (pull_request) Successful in 4s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m2s
CI / Platform (Go) (pull_request) Successful in 3s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 7s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 23s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8s
E2E Chat / E2E Chat (pull_request) Successful in 8s
Harness Replays / Harness Replays (pull_request) Successful in 2s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 3s
CI / Canvas (Next.js) (pull_request) Successful in 5m54s
CI / all-required (pull_request) Successful in 11m38s
audit-force-merge / audit (pull_request) Successful in 5s
2026-05-24 18:54:46 -07:00
agent-dev-a bee46f0a06 Merge pull request 'fix: support MCP user message attachments' (#1824) from fix/hermes-user-attachments-core into main
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Canvas Deploy Reminder (push) Blocked by required conditions
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 10s
Block internal-flavored paths / Block forbidden paths (push) Successful in 30s
CI / Python Lint & Test (push) Successful in 25s
E2E Chat / detect-changes (push) Successful in 24s
CI / Detect changes (push) Successful in 33s
E2E API Smoke Test / detect-changes (push) Successful in 27s
Harness Replays / detect-changes (push) Successful in 14s
Handlers Postgres Integration / detect-changes (push) Successful in 15s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 16s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 14s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 6s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 5s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (push) Successful in 1m7s
publish-workspace-server-image / build-and-push (push) Successful in 6m25s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (push) Successful in 6m7s
CI / Canvas (Next.js) (push) Successful in 4s
CI / Shellcheck (E2E scripts) (push) Successful in 4s
Harness Replays / Harness Replays (push) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 27s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 2m23s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 3m10s
E2E Chat / E2E Chat (push) Successful in 5m0s
CI / Platform (Go) (push) Successful in 6m7s
CI / all-required (push) Successful in 10m7s
publish-workspace-server-image / Production auto-deploy (push) Successful in 13m16s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 4m35s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 6m15s
main-red-watchdog / watchdog (push) Successful in 36s
gate-check-v3 / gate-check (push) Successful in 35s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Has started running
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 25s
ci-required-drift / drift (push) Successful in 1m17s
2026-05-25 01:54:15 +00:00
claude-ceo-assistant 7999924edf fix: support MCP user message attachments
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 8s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 14s
CI / Detect changes (pull_request) Successful in 11s
CI / Python Lint & Test (pull_request) Successful in 4s
E2E API Smoke Test / detect-changes (pull_request) Successful in 11s
E2E Chat / detect-changes (pull_request) Successful in 9s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 6s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 17s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 15s
Harness Replays / detect-changes (pull_request) Successful in 12s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 11s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 11s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 8s
gate-check-v3 / gate-check (pull_request) Successful in 6s
sop-checklist / review-refire (pull_request) Has been skipped
sop-checklist / all-items-acked (pull_request) Successful in 11s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Successful in 1m6s
sop-tier-check / tier-check (pull_request) Successful in 16s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m2s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 7s
CI / Canvas (Next.js) (pull_request) Successful in 19s
E2E Chat / E2E Chat (pull_request) Successful in 7s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 6s
Harness Replays / Harness Replays (pull_request) Successful in 6s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m58s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
qa-review / approved (pull_request) Refired via /qa-recheck by unknown
security-review / approved (pull_request) Refired via /security-recheck by unknown
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2m40s
CI / Platform (Go) (pull_request) Successful in 6m27s
CI / all-required (pull_request) Successful in 9m15s
sop-checklist / na-declarations (pull_request) N/A: (none)
audit-force-merge / audit (pull_request) Successful in 29s
2026-05-24 18:44:15 -07:00
agent-dev-b 286a499819 Merge pull request 'Wire platform-managed LLM defaults into workspaces' (#1815) from fix/platform-managed-llm-default into main
ci-arm64-advisory / fast-checks (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 9s
Block internal-flavored paths / Block forbidden paths (push) Successful in 15s
CI / Python Lint & Test (push) Successful in 13s
CI / Detect changes (push) Successful in 22s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 29s
E2E API Smoke Test / detect-changes (push) Successful in 31s
E2E Chat / detect-changes (push) Successful in 30s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 8s
Harness Replays / detect-changes (push) Successful in 10s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 8s
Handlers Postgres Integration / detect-changes (push) Successful in 10s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 6s
E2E Staging SaaS (full lifecycle) / pr-validate (push) Successful in 49s
publish-workspace-server-image / build-and-push (push) Successful in 3m9s
CI / Canvas (Next.js) (push) Successful in 3s
CI / Shellcheck (E2E scripts) (push) Successful in 3s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 36s
Harness Replays / Harness Replays (push) Successful in 10s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 2m22s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 2m31s
CI / Canvas Deploy Reminder (push) Successful in 2s
E2E Chat / E2E Chat (push) Successful in 4m33s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (push) Successful in 6m32s
CI / Platform (Go) (push) Successful in 5m33s
CI / all-required (push) Successful in 9m11s
publish-workspace-server-image / Production auto-deploy (push) Successful in 7m52s
main-red-watchdog / watchdog (push) Successful in 31s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 11s
gate-check-v3 / gate-check (push) Successful in 37s
ci-required-drift / drift (push) Successful in 1m8s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 6s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 4m36s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 5m59s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 6s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 18s
2026-05-25 00:14:19 +00:00
hongming 6964b26474 docs(arch): #1793 workspace-placement RFC — formalize org-per-EC2 architecture (#1819)
ci-arm64-advisory / fast-checks (push) Waiting to run
Block internal-flavored paths / Block forbidden paths (push) Successful in 9s
CI / Python Lint & Test (push) Successful in 8s
CI / Detect changes (push) Successful in 12s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 10s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 10s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 11s
Handlers Postgres Integration / detect-changes (push) Successful in 12s
E2E API Smoke Test / detect-changes (push) Successful in 25s
CI / all-required (push) Successful in 27s
E2E Chat / detect-changes (push) Successful in 25s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 23s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 8s
CI / Platform (Go) (push) Successful in 4s
CI / Canvas (Next.js) (push) Successful in 4s
CI / Shellcheck (E2E scripts) (push) Successful in 2s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 5s
E2E Chat / E2E Chat (push) Successful in 8s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 1m58s
CI / Canvas Deploy Reminder (push) Successful in 6s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 6s
publish-workspace-server-image / build-and-push (push) Successful in 3m59s
publish-workspace-server-image / Production auto-deploy (push) Has been cancelled
CTO-bypass merge 2026-05-24: #1793 workspace-placement RFC
2026-05-25 00:09:38 +00:00
hongming 8019231a16 chore(go-module): #1760 rename Go module to git.moleculesai.app/molecule-ai/molecule-core/workspace-server (#1816)
ci-arm64-advisory / fast-checks (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 8s
Block internal-flavored paths / Block forbidden paths (push) Successful in 8s
CI / Detect changes (push) Successful in 9s
CI / Python Lint & Test (push) Successful in 5s
E2E API Smoke Test / detect-changes (push) Successful in 9s
E2E Chat / detect-changes (push) Successful in 8s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (push) Successful in 49s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 12s
E2E Staging SaaS (full lifecycle) / pr-validate (push) Successful in 39s
Handlers Postgres Integration / detect-changes (push) Successful in 4s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 4s
publish-workspace-server-image / build-and-push (push) Successful in 3m12s
Harness Replays / detect-changes (push) Successful in 5s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (push) Successful in 6s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 3s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (push) Successful in 3s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (push) Successful in 1m6s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 14s
CI / Canvas (Next.js) (push) Successful in 3s
CI / Shellcheck (E2E scripts) (push) Successful in 2s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (push) Successful in 1m25s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (push) Successful in 5m19s
E2E Staging External Runtime / E2E Staging External Runtime (push) Successful in 5m30s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 2m23s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (push) Successful in 6m5s
E2E Chat / E2E Chat (push) Successful in 4m6s
CI / Platform (Go) (push) Successful in 5m0s
CI / all-required (push) Successful in 9m45s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 2s
publish-workspace-server-image / Production auto-deploy (push) Successful in 8m32s
Harness Replays / Harness Replays (push) Successful in 12s
CI / Canvas Deploy Reminder (push) Successful in 2s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 1m37s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 8s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 12s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 5m9s
main-red-watchdog / watchdog (push) Successful in 32s
gate-check-v3 / gate-check (push) Successful in 25s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 6m10s
CTO-bypass merge 2026-05-24: #1760 Go module rename to git.moleculesai.app path
2026-05-24 23:37:18 +00:00
hongming 5cdb486269 build(tenant-image): #1812 remove memory-backfill binary post-A3 (#1814)
ci-arm64-advisory / fast-checks (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 13s
Block internal-flavored paths / Block forbidden paths (push) Successful in 21s
CI / Detect changes (push) Successful in 34s
CI / Python Lint & Test (push) Successful in 21s
Handlers Postgres Integration / detect-changes (push) Successful in 11s
E2E API Smoke Test / detect-changes (push) Successful in 17s
E2E Chat / detect-changes (push) Successful in 16s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 16s
Harness Replays / detect-changes (push) Successful in 6s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 6s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 6s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 4s
publish-workspace-server-image / build-and-push (push) Successful in 3m13s
CI / Canvas (Next.js) (push) Successful in 3s
CI / Shellcheck (E2E scripts) (push) Successful in 2s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 7s
Harness Replays / Harness Replays (push) Successful in 10s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 2m1s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 2m54s
CI / Canvas Deploy Reminder (push) Successful in 3s
E2E Chat / E2E Chat (push) Successful in 4m31s
CI / Platform (Go) (push) Successful in 5m52s
CI / all-required (push) Successful in 10m26s
publish-workspace-server-image / Production auto-deploy (push) Successful in 9m8s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 26s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 18s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 5m2s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 7m32s
CTO-bypass merge 2026-05-24: #1812 remove backfill bundle
2026-05-24 23:16:56 +00:00
claude-ceo-assistant 9b096b0cbe Wire platform-managed LLM defaults into workspaces
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 8s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 19s
CI / Detect changes (pull_request) Successful in 26s
CI / Python Lint & Test (pull_request) Successful in 19s
E2E API Smoke Test / detect-changes (pull_request) Successful in 19s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 36s
E2E Chat / detect-changes (pull_request) Successful in 48s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 11s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 7s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 9s
Harness Replays / detect-changes (pull_request) Successful in 9s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 56s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 8s
qa-review / approved (pull_request) Failing after 12s
sop-checklist / review-refire (pull_request) Has been skipped
security-review / approved (pull_request) Failing after 11s
gate-check-v3 / gate-check (pull_request) Successful in 12s
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request) Successful in 10s
sop-tier-check / tier-check (pull_request) Successful in 4s
CI / Canvas (Next.js) (pull_request) Successful in 2s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 2s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 11s
E2E Chat / E2E Chat (pull_request) Successful in 9s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m11s
Harness Replays / Harness Replays (pull_request) Successful in 8s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m39s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2m45s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Successful in 5m26s
CI / Platform (Go) (pull_request) Successful in 6m34s
CI / all-required (pull_request) Successful in 9m25s
audit-force-merge / audit (pull_request) Successful in 23s
2026-05-24 16:10:48 -07:00
agent-dev-a 4a610ca3c4 Merge pull request 'feat(memory): #1792 Phase A3 — drop agent_memories table + legacy v1 surface' (#1809) from feat/issue-1792-phase-a3-drop-agent-memories into main
ci-arm64-advisory / fast-checks (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 8s
CI / Detect changes (push) Successful in 13s
Block internal-flavored paths / Block forbidden paths (push) Successful in 13s
CI / Python Lint & Test (push) Successful in 11s
E2E Chat / detect-changes (push) Successful in 10s
E2E API Smoke Test / detect-changes (push) Successful in 10s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 10s
Harness Replays / detect-changes (push) Successful in 6s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 6s
Handlers Postgres Integration / detect-changes (push) Successful in 6s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 4s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 4s
CI / Canvas (Next.js) (push) Successful in 2s
CI / Shellcheck (E2E scripts) (push) Successful in 2s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 26s
Harness Replays / Harness Replays (push) Successful in 11s
CI / Canvas Deploy Reminder (push) Successful in 5s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 1m56s
publish-workspace-server-image / build-and-push (push) Successful in 3m7s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 2m40s
E2E Chat / E2E Chat (push) Successful in 4m8s
E2E Staging External Runtime / E2E Staging External Runtime (push) Successful in 5m4s
CI / Platform (Go) (push) Successful in 5m28s
CI / all-required (push) Successful in 6m24s
publish-workspace-server-image / Production auto-deploy (push) Successful in 4m52s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 6s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 8s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 4m44s
main-red-watchdog / watchdog (push) Successful in 1m5s
gate-check-v3 / gate-check (push) Successful in 43s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 5m6s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 11s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 19s
2026-05-24 22:17:41 +00:00
agent-dev-a 09614f4cb3 Merge pull request 'fix(canvas/FilesTab): WCAG 1.1.1/2.4.7/4.1.3 on FileEditor' (#1452) from fix/files-editor-wcag-a11y into main
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Canvas Deploy Reminder (push) Blocked by required conditions
E2E Chat / E2E Chat (push) Blocked by required conditions
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Blocked by required conditions
Harness Replays / Harness Replays (push) Blocked by required conditions
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 10s
Block internal-flavored paths / Block forbidden paths (push) Successful in 15s
CI / Python Lint & Test (push) Successful in 15s
E2E API Smoke Test / detect-changes (push) Successful in 17s
CI / Detect changes (push) Successful in 20s
Handlers Postgres Integration / detect-changes (push) Successful in 9s
Harness Replays / detect-changes (push) Successful in 9s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 12s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 16s
E2E Chat / detect-changes (push) Successful in 19s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 11s
CI / Platform (Go) (push) Successful in 4s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 12s
CI / Shellcheck (E2E scripts) (push) Has been cancelled
CI / all-required (push) Has been cancelled
Handlers Postgres Integration / Handlers Postgres Integration (push) Has been cancelled
E2E API Smoke Test / E2E API Smoke Test (push) Has been cancelled
publish-workspace-server-image / build-and-push (push) Has been cancelled
CI / Canvas (Next.js) (push) Has been cancelled
publish-canvas-image / Build & push canvas image (push) Successful in 1m37s
ci-required-drift / drift (push) Successful in 1m26s
2026-05-24 22:16:47 +00:00
agent-dev-a e0f9a16e99 Merge pull request 'fix(canvas): add role=status + aria-live=polite to ConsoleModal loading state (WCAG 4.1.3)' (#1455) from fix/console-modal-a11y into main
ci-arm64-advisory / fast-checks (push) Waiting to run
E2E API Smoke Test / E2E API Smoke Test (push) Blocked by required conditions
E2E Chat / E2E Chat (push) Blocked by required conditions
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Blocked by required conditions
Handlers Postgres Integration / detect-changes (push) Waiting to run
Handlers Postgres Integration / Handlers Postgres Integration (push) Blocked by required conditions
Harness Replays / detect-changes (push) Waiting to run
Harness Replays / Harness Replays (push) Blocked by required conditions
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Block internal-flavored paths / Block forbidden paths (push) Has been cancelled
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 8s
E2E API Smoke Test / detect-changes (push) Has been cancelled
E2E Chat / detect-changes (push) Has been cancelled
E2E Staging Canvas (Playwright) / detect-changes (push) Has been cancelled
publish-workspace-server-image / build-and-push (push) Has been cancelled
publish-canvas-image / Build & push canvas image (push) Has been cancelled
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Has been cancelled
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Has been cancelled
Secret scan / Scan diff for credential-shaped strings (push) Has been cancelled
CI / Python Lint & Test (push) Successful in 13s
CI / Canvas (Next.js) (push) Has been cancelled
CI / Platform (Go) (push) Has been cancelled
CI / Shellcheck (E2E scripts) (push) Has been cancelled
CI / Canvas Deploy Reminder (push) Has been cancelled
CI / Detect changes (push) Successful in 30s
CI / all-required (push) Failing after 28s
2026-05-24 22:16:44 +00:00
hongming 94bdd8ff35 feat(memory): #1792 Phase A3 — drop agent_memories table + legacy v1 surface
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 8s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 9s
Check migration collisions / Migration version collision check (pull_request) Successful in 35s
CI / Detect changes (pull_request) Successful in 10s
CI / Python Lint & Test (pull_request) Successful in 5s
E2E API Smoke Test / detect-changes (pull_request) Successful in 9s
E2E Chat / detect-changes (pull_request) Successful in 7s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 10s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 4s
Harness Replays / detect-changes (pull_request) Successful in 3s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 6s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 3s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
gate-check-v3 / gate-check (pull_request) Successful in 3s
qa-review / approved (pull_request) Failing after 3s
sop-checklist / na-declarations (pull_request) N/A: (none)
security-review / approved (pull_request) Failing after 4s
sop-checklist / all-items-acked (pull_request) Successful in 4s
sop-checklist / review-refire (pull_request) Has been skipped
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m30s
sop-tier-check / tier-check (pull_request) Successful in 6s
CI / Canvas (Next.js) (pull_request) Successful in 2s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 12s
E2E Chat / E2E Chat (pull_request) Successful in 6s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 6s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m39s
Harness Replays / Harness Replays (pull_request) Successful in 3s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2m13s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m14s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Successful in 5m22s
CI / all-required (pull_request) Successful in 13m18s
audit-force-merge / audit (pull_request) Successful in 23s
Closes the v1→v2 memory migration. Phase A2 (#1791) ran on production
2026-05-24 and verified parity: every active tenant has its
agent_memories rows mirrored 1:1 into memory_plugin.memory_records,
live writes go to v2 only (v1 frozen). With parity confirmed, this PR
drops the entire v1 surface.

Per the audit before this PR:

| Tenant | v1 (frozen) | v2 (live) | Status |
|---|---|---|---|
| agents-team | 1805 | 1805+live | parity |
| hongming | 144 | 144 | parity |
| chloe-dong | 1 | 1 | parity |
| reno-stars | 102 | 102 | parity |

## Changes

1. **Migration** drops the agent_memories table. Down migration
   recreates an empty table for tool symmetry; rollback would not
   restore data (A2 was one-way).

2. **memories.go**: removed Search, Update, Delete methods + their
   dead helpers (EmbeddingFunc, embed field, WithEmbedding,
   formatVector, nextArg, memoryFTSMinQueryLen, memoryRecallMaxLimit).
   Kept Commit, which post-#1794 routes through the v2 plugin.

3. **router.go**: removed GET /memories, DELETE /memories/:id, PATCH
   /memories/:id routes. Callers use /v2/memories (canvas does this
   already) and /v2/memories/:id (Forget) instead. POST /memories
   stays — it's the high-volume write surface, still on v2.

4. **activity.go**: dropped the agent_memories UNION branch from
   buildSessionSearchQuery. Session search now returns only
   activity_logs items; memory-tab content comes from /v2/memories
   directly via MemoryInspectorPanel.

5. **workspace_crud.go**: removed agent_memories from the workspace
   purge cleanup list. Memory rows now cascade-delete via the
   memory plugin's namespace deletion path.

6. **entrypoint-tenant.sh**: removed the MEMORY_V2_CUTOVER deprecation
   shim (#1747 deprecated it; A3 retires the synonym). New tenants
   use MEMORY_PLUGIN_URL directly. Controlplane user-data still sets
   MEMORY_V2_CUTOVER='true' as belt-and-suspenders — that's a no-op
   now and will be cleaned up in a separate molecule-controlplane PR.

7. **Tests**: removed test functions that exercised the deleted
   methods (Search/Update/Delete and the embed/recall paths).
   Tests for Commit + redactSecrets stay.

## Risk

- **Hard 404** on any caller still hitting GET /workspaces/:id/memories,
  PATCH /workspaces/:id/memories/:id, or DELETE /workspaces/:id/memories/:id.
  Production traffic audit showed 2 GETs vs 66 POSTs to legacy /memories
  over a 24h window — runtime callers are POST-dominant. Canvas reads
  from /v2/memories. Acceptable.
- **No DB rollback** restores data — A2 was one-way. If a critical bug
  appears post-merge, recover via memory_plugin.memory_records direct
  SQL (data is preserved there).

## SOP Checklist (RFC #351)

### 1. Comprehensive testing performed
- `go test -short -count=1 ./internal/handlers/` green.
- `go test -short -count=1 ./cmd/memory-backfill/` green (sqlmock
  tests still pass; tool is now effectively inert on tenants since the
  source table is gone but the binary stays for one image cycle).
- `go vet ./...` clean.

### 2. Local-postgres E2E run
N/A. Schema change verified against the well-tested migration tool
shape; no new SQL paths added.

### 3. Staging-smoke verified or pending
Pending merge + tenant recycle. Will verify by SSM-checking that
agent_memories is gone from each tenant's DB and POST /memories still
returns 201 with rows landing in memory_plugin.memory_records.

### 4. Root-cause not symptom
Yes. The v1 table existed only as a dual-write target during the
A1+A2 transition. With A2 done and parity verified, the table is dead
weight. Dropping it removes the SSOT-violation surface entirely.

### 5. Five-Axis review walked
Walked solo. Happy to dispatch a hostile reviewer if anyone wants
sign-off on the cleanup scope (whether to also drop memory-backfill
binary, the activity UNION removal, etc).

### 6. No backwards-compat shim / dead code added
Net deletion: -787 LOC across 7 files. The MEMORY_V2_CUTOVER shim is
removed (was the last backwards-compat hook). One follow-up needed:
controlplane ec2.go still sets MEMORY_V2_CUTOVER='true' — that's a
no-op now but should be cleaned up in a separate PR for tidiness.

### 7. Memory/saved-feedback consulted
- `feedback_no_single_source_of_truth` — A3 is the final step in
  establishing v2 as the only memory backend.
- `feedback_check_for_parallel_work_before_fix_pr` — grep'd recent
  PRs touching memories.go / activity.go / workspace_crud.go; no
  parallel in flight.

Closes #1792. Memory v1→v2 migration complete.
2026-05-24 15:03:56 -07:00
agent-dev-b a773973d37 Merge pull request 'ci(gate-check-v3): add per-PR concurrency to prevent OOM fan-out' (#1548) from ci/oom-storm-concurrency-fix into main
ci-arm64-advisory / fast-checks (push) Waiting to run
Block internal-flavored paths / Block forbidden paths (push) Successful in 11s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 10s
CI / Python Lint & Test (push) Successful in 8s
CI / Detect changes (push) Successful in 13s
E2E API Smoke Test / detect-changes (push) Successful in 12s
Handlers Postgres Integration / detect-changes (push) Successful in 8s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 9s
E2E Chat / detect-changes (push) Successful in 12s
CI / all-required (push) Successful in 32s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (push) Successful in 6s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (push) Successful in 12s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 13s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 17s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 6s
CI / Platform (Go) (push) Successful in 8s
CI / Canvas (Next.js) (push) Successful in 5s
CI / Shellcheck (E2E scripts) (push) Successful in 5s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 6s
E2E Chat / E2E Chat (push) Successful in 5s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 6s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (push) Successful in 1m24s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (push) Successful in 1m31s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 1m42s
CI / Canvas Deploy Reminder (push) Successful in 6s
publish-workspace-server-image / build-and-push (push) Successful in 3m58s
publish-workspace-server-image / Production auto-deploy (push) Successful in 2m9s
ci-required-drift / drift (push) Successful in 1m19s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 7s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 7s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 5m16s
main-red-watchdog / watchdog (push) Successful in 33s
gate-check-v3 / gate-check (push) Successful in 32s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 6m13s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 4s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 9s
2026-05-24 16:34:04 +00:00
agent-dev-b b9d41474a7 Merge pull request 'feat(local-e2e): session-continuity canary harness (task #342)' (#1602) from task342/local-e2e-harness into main
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Platform (Go) (push) Blocked by required conditions
CI / Canvas (Next.js) (push) Blocked by required conditions
CI / Shellcheck (E2E scripts) (push) Blocked by required conditions
CI / Canvas Deploy Reminder (push) Blocked by required conditions
E2E API Smoke Test / E2E API Smoke Test (push) Blocked by required conditions
E2E Chat / detect-changes (push) Waiting to run
E2E Chat / E2E Chat (push) Blocked by required conditions
E2E Staging Canvas (Playwright) / detect-changes (push) Waiting to run
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Blocked by required conditions
Handlers Postgres Integration / detect-changes (push) Waiting to run
Handlers Postgres Integration / Handlers Postgres Integration (push) Blocked by required conditions
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Waiting to run
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Waiting to run
Block internal-flavored paths / Block forbidden paths (push) Has been cancelled
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
CI / Python Lint & Test (push) Has been cancelled
CI / all-required (push) Has been cancelled
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Has been cancelled
publish-workspace-server-image / build-and-push (push) Has been cancelled
E2E API Smoke Test / detect-changes (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
2026-05-24 16:34:02 +00:00
hongming 25c7ee9689 feat(workspaces): allow compute settings updates from canvas (#1800)
ci-arm64-advisory / fast-checks (push) Waiting to run
Block internal-flavored paths / Block forbidden paths (push) Successful in 9s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 9s
CI / Python Lint & Test (push) Successful in 8s
CI / Detect changes (push) Successful in 13s
E2E API Smoke Test / detect-changes (push) Successful in 15s
E2E Chat / detect-changes (push) Successful in 9s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 10s
Harness Replays / detect-changes (push) Successful in 6s
Handlers Postgres Integration / detect-changes (push) Successful in 6s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 9s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 11s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 5s
publish-canvas-image / Build & push canvas image (push) Successful in 1m56s
CI / Shellcheck (E2E scripts) (push) Successful in 3s
publish-workspace-server-image / build-and-push (push) Successful in 3m17s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 1m45s
E2E Staging External Runtime / E2E Staging External Runtime (push) Successful in 5m24s
Harness Replays / Harness Replays (push) Successful in 3s
CI / Platform (Go) (push) Successful in 5m26s
E2E Chat / E2E Chat (push) Successful in 4m27s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 2m7s
CI / Canvas (Next.js) (push) Successful in 6m12s
CI / Canvas Deploy Reminder (push) Successful in 2s
CI / all-required (push) Successful in 9m15s
publish-workspace-server-image / Production auto-deploy (push) Successful in 7m47s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 7m31s
Railway pin audit (drift detection) / Audit Railway env vars for drift-prone pins (push) Failing after 5s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (push) Successful in 1m16s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 5s
gate-check-v3 / gate-check (push) Successful in 33s
main-red-watchdog / watchdog (push) Successful in 2m23s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 13s
ci-required-drift / drift (push) Successful in 1m17s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 5s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 13s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 4m11s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 5m45s
2026-05-24 11:32:48 +00:00
hongming 919e632ccb fix(workspaces): avoid stale runtime on apply-template restart
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
CI / Python Lint & Test (pull_request) Successful in 5s
CI / Detect changes (pull_request) Successful in 8s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 9s
E2E API Smoke Test / detect-changes (pull_request) Successful in 9s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 11s
E2E Chat / detect-changes (pull_request) Successful in 11s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 8s
Harness Replays / detect-changes (pull_request) Successful in 7s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 5s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 8s
gate-check-v3 / gate-check (pull_request) Successful in 5s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
sop-checklist / review-refire (pull_request) Has been skipped
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request) Successful in 7s
sop-tier-check / tier-check (pull_request) Successful in 8s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 4s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m2s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 20s
E2E Chat / E2E Chat (pull_request) Successful in 12s
Harness Replays / Harness Replays (pull_request) Successful in 5s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m59s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2m26s
qa-review / approved (pull_request) Refired via /qa-recheck by unknown
security-review / approved (pull_request) Refired via /security-recheck by unknown
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m13s
CI / Platform (Go) (pull_request) Successful in 5m33s
CI / Canvas (Next.js) (pull_request) Successful in 6m53s
CI / all-required (pull_request) Successful in 7m28s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
audit-force-merge / audit (pull_request) Successful in 8s
2026-05-24 04:24:07 -07:00
hongming 2f1bf09030 feat(workspaces): allow compute settings updates from canvas
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
CI / Python Lint & Test (pull_request) Successful in 5s
CI / Detect changes (pull_request) Successful in 8s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 8s
E2E API Smoke Test / detect-changes (pull_request) Successful in 12s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 9s
E2E Chat / detect-changes (pull_request) Successful in 11s
Harness Replays / detect-changes (pull_request) Successful in 7s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 12s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 5s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 5s
gate-check-v3 / gate-check (pull_request) Successful in 9s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 10s
qa-review / approved (pull_request) Failing after 10s
security-review / approved (pull_request) Failing after 5s
sop-checklist / review-refire (pull_request) Has been skipped
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request) Successful in 6s
sop-tier-check / tier-check (pull_request) Successful in 5s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m10s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 3s
CI / Platform (Go) (pull_request) Failing after 2m13s
CI / all-required (pull_request) Failing after 3m5s
E2E Chat / E2E Chat (pull_request) Successful in 3s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 9s
Harness Replays / Harness Replays (pull_request) Successful in 5s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m28s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 1m36s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m15s
CI / Canvas (Next.js) (pull_request) Successful in 5m51s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
2026-05-24 04:16:10 -07:00
hongming 7604e113d2 fix(memory-plugin): URGENT — emit JSON null for nil metadata/propagation (closes #1794 prod regression) (#1798)
ci-arm64-advisory / fast-checks (push) Waiting to run
Block internal-flavored paths / Block forbidden paths (push) Successful in 6s
CI / Python Lint & Test (push) Successful in 8s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 9s
CI / Detect changes (push) Successful in 15s
Handlers Postgres Integration / detect-changes (push) Successful in 9s
E2E API Smoke Test / detect-changes (push) Successful in 17s
E2E Chat / detect-changes (push) Successful in 17s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 15s
Harness Replays / detect-changes (push) Successful in 9s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 8s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 8s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 9s
CI / Canvas (Next.js) (push) Successful in 6s
CI / Shellcheck (E2E scripts) (push) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 6s
Harness Replays / Harness Replays (push) Successful in 15s
CI / Canvas Deploy Reminder (push) Successful in 14s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 1m55s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 2m38s
publish-workspace-server-image / build-and-push (push) Successful in 3m21s
E2E Chat / E2E Chat (push) Successful in 4m20s
CI / Platform (Go) (push) Successful in 6m10s
CI / all-required (push) Successful in 6m49s
publish-workspace-server-image / Production auto-deploy (push) Successful in 5m0s
main-red-watchdog / watchdog (push) Successful in 28s
gate-check-v3 / gate-check (push) Successful in 23s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 12s
ci-required-drift / drift (push) Successful in 1m37s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 8s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 7s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 5m20s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 6m29s
CTO-bypass merge 2026-05-24: URGENT — fix nil-jsonb regression introduced by #1794 in production plugin path
2026-05-24 10:54:32 +00:00
Molecule AI Dev Engineer A (Kimi) 6ba9424196 docs(local-e2e): reference runtime PR #46 for canary mode source
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Waiting to run
cascade-list-drift-gate / check (pull_request) Failing after 7s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 10s
Check migration collisions / Migration version collision check (pull_request) Successful in 15s
CI / Detect changes (pull_request) Successful in 22s
MCP Stdio Transport Regression / MCP stdio with regular-file stdout (pull_request) Successful in 1m26s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 29s
E2E API Smoke Test / detect-changes (pull_request) Successful in 13s
E2E Chat / detect-changes (pull_request) Successful in 11s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Has been skipped
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 16s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Failing after 1m3s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 35s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
Handlers Postgres Integration / detect-changes (pull_request) Successful in 4s
Harness Replays / detect-changes (pull_request) Successful in 4s
CI / Platform (Go) (pull_request) Successful in 4m48s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 7s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 3s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m29s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 3s
lint-mask-pr-atomicity / lint-mask-pr-atomicity (pull_request) Successful in 1m23s
CI / Canvas (Next.js) (pull_request) Successful in 6m11s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m10s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m1s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m15s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Failing after 1m12s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 4s
publish-runtime-autobump / bump-and-tag (pull_request) Has been skipped
CI / Python Lint & Test (pull_request) Successful in 7m7s
CI / all-required (pull_request) Successful in 6m51s
publish-runtime-autobump / pr-validate (pull_request) Successful in 36s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 7s
Secret scan / Scan diff for credential-shaped strings (pull_request) Failing after 18s
gate-check-v3 / gate-check (pull_request) Failing after 4s
qa-review / approved (pull_request) Failing after 6s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m14s
security-review / approved (pull_request) Failing after 4s
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request) Successful in 4s
sop-checklist / review-refire (pull_request) Has been skipped
sop-tier-check / tier-check (pull_request) Successful in 7s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 1m16s
Runtime Pin Compatibility / PyPI-latest install + import smoke (pull_request) Successful in 2m16s
Harness Replays / Harness Replays (pull_request) Successful in 20s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 1m44s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2m6s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 2m36s
E2E Chat / E2E Chat (pull_request) Failing after 5m17s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m1s
audit-force-merge / audit (pull_request) Successful in 10s
The canary short-circuit was moved from molecule-core/workspace/
(deleted in main via 9aa47643) to molecule-ai-workspace-runtime
(molecule_runtime/a2a_executor.py). Update docker-compose comment
so engineers can find the live code.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 11:41:16 +00:00
Molecule AI Dev Engineer A (Kimi) 531d98efea Revert "workspace/a2a_executor: add MOLECULE_CANARY_MODE short-circuit (CR2 review_id=5622)"
This reverts commit 0b17567891.
2026-05-23 11:40:52 +00:00
Molecule AI Dev Engineer A (Kimi) 0b17567891 workspace/a2a_executor: add MOLECULE_CANARY_MODE short-circuit (CR2 review_id=5622)
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Waiting to run
cascade-list-drift-gate / check (pull_request) Failing after 8s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 14s
Check migration collisions / Migration version collision check (pull_request) Successful in 8s
CI / Detect changes (pull_request) Successful in 12s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 35s
MCP Stdio Transport Regression / MCP stdio with regular-file stdout (pull_request) Successful in 1m45s
E2E API Smoke Test / detect-changes (pull_request) Successful in 15s
E2E Chat / detect-changes (pull_request) Successful in 14s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Failing after 1m9s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Has been skipped
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 7s
CI / Platform (Go) (pull_request) Successful in 5m1s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 51s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
Handlers Postgres Integration / detect-changes (pull_request) Successful in 3s
Harness Replays / detect-changes (pull_request) Successful in 7s
CI / Canvas (Next.js) (pull_request) Successful in 6m10s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 4s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 3s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m16s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 3s
CI / Python Lint & Test (pull_request) Successful in 7m7s
CI / all-required (pull_request) Successful in 6m17s
lint-mask-pr-atomicity / lint-mask-pr-atomicity (pull_request) Successful in 1m21s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 5s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m15s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m4s
publish-runtime-autobump / bump-and-tag (pull_request) Has been skipped
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Failing after 1m15s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 12s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 59s
gate-check-v3 / gate-check (pull_request) Failing after 11s
publish-runtime-autobump / pr-validate (pull_request) Successful in 44s
Secret scan / Scan diff for credential-shaped strings (pull_request) Failing after 16s
sop-checklist / na-declarations (pull_request) N/A: (none)
security-review / approved (pull_request) Failing after 6s
qa-review / approved (pull_request) Failing after 6s
sop-checklist / review-refire (pull_request) Has been skipped
sop-checklist / all-items-acked (pull_request) Successful in 5s
sop-tier-check / tier-check (pull_request) Successful in 10s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m24s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 1m12s
Runtime Pin Compatibility / PyPI-latest install + import smoke (pull_request) Successful in 2m3s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 1m8s
Harness Replays / Harness Replays (pull_request) Successful in 7s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 1m42s
E2E Chat / E2E Chat (pull_request) Failing after 5m32s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 2m57s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 7m5s
Adds a deterministic, rule-based canary mode that short-circuits the
LLM path when MOLECULE_CANARY_MODE=1.  This lets the local-e2e harness
run the 4 session-continuity canaries without requiring a live model
provider.

Canary replies:
- "What's my name?" → "Your name is Hongming."
- "favorite color"  → "Your favorite color is blue."
- has attachments   → "I received the file."
- default           → "Canary mode active."

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 11:18:01 +00:00
claude-ceo-assistant 59d699b61c feat(local-e2e): session-continuity canary harness (task #342, RFC#600 gate)
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
CI / Detect changes (pull_request) Successful in 7s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 24s
E2E API Smoke Test / detect-changes (pull_request) Successful in 14s
E2E Chat / detect-changes (pull_request) Successful in 11s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 7s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 9s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 5s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 6s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 12s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 9s
gate-check-v3 / gate-check (pull_request) Successful in 7s
qa-review / approved (pull_request) Failing after 7s
security-review / approved (pull_request) Failing after 6s
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request) Successful in 5s
sop-checklist / review-refire (pull_request) Has been skipped
sop-tier-check / tier-check (pull_request) Successful in 5s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m3s
CI / Platform (Go) (pull_request) Successful in 5m45s
CI / Python Lint & Test (pull_request) Successful in 7m0s
CI / Canvas (Next.js) (pull_request) Successful in 7m34s
CI / all-required (pull_request) Successful in 7m14s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 5s
E2E Chat / E2E Chat (pull_request) Successful in 6s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 6s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 2s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
Adds a self-contained docker-compose harness in local-e2e/ that gates
RFC#600-class template changes BEFORE customer canary. Implements the 4
canonical canaries:

  1. 2-turn name continuity   — SessionStore key derivation
  2. File-only message        — no caption drop-to-empty-prompt regress
  3. File + prompt (multimodal) — multimodal happy path
  4. Cross-session memory     — explicit memory tool, distinct context_ids

Architecture is deliberately lean per CTO "separate CI as possible":

  local-e2e/
    docker-compose.yml       # runtime + cp_sim ONLY (no platform Go, no pg)
    cp_sim/                  # ~250 LoC Python A2A wire-shape emitter
    cp_sim/canary/           # 4 canary scenarios + layer-isolation probes
    scripts/run-canary.sh    # one-shot orchestration (target <3 min)
    scripts/onboard-template.sh  # gitops helper for cascade
    templates/session-continuity-e2e.yml  # canonical workflow shim

Rationale for a Python tenant-CP simulator (not the real workspace-server):
SessionStore behaviour is fully owned by workspace/a2a_executor.py +
executor_helpers.py — the Go platform service doesn't touch session
continuity. Excising it gets the harness to <3 min cold-boot on
docker-host runners and keeps the surface small enough to debug fast.

The simulator emits the byte-identical JSON-RPC message/send envelope
that workspace-server POSTs (cross-checked against
tests/e2e/test_chat_attachments_e2e.sh and workspace/a2a_executor.py
:_core_execute).

Per feedback_no_single_source_of_truth: the harness IS the canonical
session-continuity validator across templates. Per-template unit tests
keep covering their own guard logic.

Per feedback_image_promote_is_not_user_live + feedback_verify_actual_
endstate_not_ack_follow_sop: every canary asserts at the running-
container layer; artifacts dump SessionStore state + runtime logs on
failure for post-mortem.

Rollout (deliberate sequencing, per task #342):
  1. THIS PR — lands harness in molecule-core. NOT yet wired to any
     template repo.
  2. Companion PR in molecule-ai-workspace-template-hermes — adds
     .gitea/workflows/session-continuity-e2e.yml. NOT required yet.
  3. Bake on hermes for ≥5 business days.
  4. Cascade to remaining 6 templates via onboard-template.sh.
  5. Per-template BP flip — add "session-continuity-e2e (pull_request)"
     to status_check_contexts on each repo, hermes first.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 02:39:30 -07:00
core-devops 154c67b754 ci(gate-check-v3): add per-PR concurrency to prevent OOM fan-out
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 5s
CI / Detect changes (pull_request) Successful in 10s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 8s
E2E API Smoke Test / detect-changes (pull_request) Successful in 9s
E2E Chat / detect-changes (pull_request) Successful in 12s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 7s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 9s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 7s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m21s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m15s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 12s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
gate-check-v3 / gate-check (pull_request) Successful in 4s
qa-review / approved (pull_request) Failing after 3s
security-review / approved (pull_request) Failing after 4s
sop-checklist / na-declarations (pull_request) N/A: (none)
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m23s
sop-checklist / all-items-acked (pull_request) Successful in 6s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m30s
sop-tier-check / tier-check (pull_request) Successful in 9s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 6s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 5s
E2E Chat / E2E Chat (pull_request) Successful in 7s
CI / Canvas (Next.js) (pull_request) Successful in 4m6s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m26s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 2s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8s
CI / Platform (Go) (pull_request) Successful in 5m11s
CI / Python Lint & Test (pull_request) Successful in 6m7s
CI / all-required (pull_request) Successful in 6m0s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
audit-force-merge / audit (pull_request) Successful in 10s
Sibling class-audit fix per
`reference_operator_host_python3_oom_storm_2026_05_18`.
gate-check-v3 fires on `pull_request_target` (opened/edited/
synchronize/reopened) + hourly cron + workflow_dispatch — `edited`
events fan out on PR-body edits and stack runs of the same
workflow_id on the same PR.

Group key falls back through pull_request.number → issue.number →
github.ref so schedule + manual ticks coalesce per-ref.

No `cancel-in-progress` per
`feedback_janitor_supersede_must_group_by_workflow_id` — the
gate-check is `continue-on-error: true` + idempotent so sequential
ticks are strictly safe.
2026-05-18 17:22:47 -07:00
core-uiux a66c37b920 fix(canvas): add role=status + aria-live=polite to ConsoleModal loading state (WCAG 4.1.3)
sop-tier-check / tier-check (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
CI / Detect changes (pull_request) Successful in 7s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 15s
E2E API Smoke Test / detect-changes (pull_request) Successful in 9s
E2E Chat / detect-changes (pull_request) Successful in 6s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 6s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 4s
Harness Replays / detect-changes (pull_request) Successful in 3s
CI / Platform (Go) (pull_request) Successful in 5m5s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m18s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 6s
gate-check-v3 / gate-check (pull_request) Successful in 5s
security-review / approved (pull_request) Failing after 4s
qa-review / approved (pull_request) Failing after 5s
CI / Canvas (Next.js) (pull_request) Successful in 6m34s
CI / Python Lint & Test (pull_request) Successful in 6m52s
CI / all-required (pull_request) Successful in 6m43s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 5s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 4s
Harness Replays / Harness Replays (pull_request) Successful in 10s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 6s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
E2E Chat / E2E Chat (pull_request) Failing after 6m52s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m24s
sop-checklist / all-items-acked (pull_request) [info tier:low] acked: 5/7 — missing: root-cause, no-backwards-compat — body-unfilled: comprehensive-testing, local-postgres-e2e, staging-sm
sop-checklist / na-declarations (pull_request) N/A: (none)
audit-force-merge / audit (pull_request) Successful in 10s
Screen readers were not announcing the loading state. The loading div now
uses role=status so assistive technology announces "Loading console
output..." when the console modal opens.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:12:00 +00:00
core-uiux 575f44475f fix(canvas/FilesTab): WCAG 1.1.1/2.4.7/4.1.3 on FileEditor
sop-tier-check / tier-check (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 6s
CI / Detect changes (pull_request) Successful in 9s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 17s
E2E API Smoke Test / detect-changes (pull_request) Successful in 6s
E2E Chat / detect-changes (pull_request) Successful in 6s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 6s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 4s
Harness Replays / detect-changes (pull_request) Successful in 4s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 5s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 3s
gate-check-v3 / gate-check (pull_request) Successful in 4s
qa-review / approved (pull_request) Failing after 4s
CI / Platform (Go) (pull_request) Successful in 5m59s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m13s
security-review / approved (pull_request) Failing after 11s
CI / Python Lint & Test (pull_request) Successful in 7m6s
CI / Canvas (Next.js) (pull_request) Successful in 7m30s
CI / all-required (pull_request) Successful in 7m19s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 3s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 7s
Harness Replays / Harness Replays (pull_request) Successful in 2s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 2s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
E2E Chat / E2E Chat (pull_request) Failing after 5m21s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 7m49s
sop-checklist / all-items-acked (pull_request) [info tier:low] acked: 5/7 — missing: root-cause, no-backwards-compat — body-unfilled: comprehensive-testing, local-postgres-e2e, staging-sm
sop-checklist / na-declarations (pull_request) N/A: (none)
audit-force-merge / audit (pull_request) Successful in 18s
- Add aria-hidden=true to decorative emoji (empty state + file type icon)
- Add aria-label to textarea so screen readers announce it as "File content editor"
- Add role=status + aria-live=polite to save success message (WCAG 4.1.3)
- Add focus-visible ring to Download and Save buttons (WCAG 2.4.7)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:42:03 +00:00
292 changed files with 8366 additions and 2221 deletions
+1 -1
View File
@@ -239,7 +239,7 @@ jobs:
# Strip the package-import prefix so we can match .coverage-allowlist.txt
# entries written as paths relative to workspace-server/.
# Handle both module paths: platform/workspace-server/... and platform/...
rel=$(echo "$file" | sed 's|^github.com/molecule-ai/molecule-monorepo/platform/workspace-server/||; s|^github.com/molecule-ai/molecule-monorepo/platform/||')
rel=$(echo "$file" | sed 's|^git.moleculesai.app/molecule-ai/molecule-core/workspace-server/workspace-server/||; s|^git.moleculesai.app/molecule-ai/molecule-core/workspace-server/||')
if echo "$ALLOWLIST" | grep -qxF "$rel"; then
echo "::warning file=workspace-server/$rel::Critical file at ${pct}% coverage (allowlisted, #1823) — fix before expiry."
+4 -4
View File
@@ -152,7 +152,7 @@ jobs:
# block). See #2578 PR comment for the rationale.
E2E_ANTHROPIC_API_KEY: ${{ secrets.MOLECULE_STAGING_ANTHROPIC_API_KEY }}
# OpenAI fallback — kept wired so an operator-dispatched run with
# E2E_RUNTIME=hermes or =langgraph via workflow_dispatch can still
# E2E_RUNTIME=hermes or =codex via workflow_dispatch can still
# exercise the OpenAI path.
E2E_OPENAI_API_KEY: ${{ secrets.MOLECULE_STAGING_OPENAI_API_KEY }}
E2E_RUNTIME: ${{ github.event.inputs.runtime || 'claude-code' }}
@@ -161,7 +161,7 @@ jobs:
# and defeats the cost saving. Operators can override via the
# workflow_dispatch flow (no input wired here yet — runtime
# override is enough for ad-hoc).
E2E_MODEL_SLUG: ${{ github.event.inputs.runtime == 'hermes' && 'openai/gpt-4o' || github.event.inputs.runtime == 'langgraph' && 'openai:gpt-4o' || 'MiniMax-M2' }}
E2E_MODEL_SLUG: ${{ github.event.inputs.runtime == 'hermes' && 'openai/gpt-4o' || github.event.inputs.runtime == 'codex' && 'openai/gpt-4o' || 'MiniMax-M2' }}
E2E_RUN_ID: "${{ github.run_id }}-${{ github.run_attempt }}"
E2E_KEEP_ORG: ${{ github.event.inputs.keep_org && '1' || '0' }}
@@ -185,7 +185,7 @@ jobs:
- name: Verify LLM key present
run: |
# Per-runtime key check — claude-code uses MiniMax; hermes /
# langgraph (operator-dispatched only) use OpenAI. Hard-fail
# codex (operator-dispatched only) use OpenAI. Hard-fail
# rather than soft-skip per #2578's lesson — empty key
# silently falls through to the wrong SECRETS_JSON branch and
# produces a confusing auth error 5 min later instead of the
@@ -206,7 +206,7 @@ jobs:
required_secret_value=""
fi
;;
langgraph|hermes)
codex|hermes)
required_secret_name="MOLECULE_STAGING_OPENAI_API_KEY"
required_secret_value="${E2E_OPENAI_API_KEY:-}"
;;
+18
View File
@@ -32,6 +32,24 @@ on:
# iterating all open PRs when PR_NUMBER is empty.
workflow_dispatch:
# Serialize per PR (or per repo for schedule/manual ticks) to prevent
# the fan-out OOM class documented in
# `reference_operator_host_python3_oom_storm_2026_05_18`. `edited`
# events fan out on every PR-body edit; combined with the hourly cron
# and synchronize bursts this workflow can stack runs of the same
# workflow_id on the same PR (each ~4GB anon-RSS) and trip the
# `--memory=4g --memory-swap=8g` per-container cap.
#
# NO `cancel-in-progress` (defaults to false). Per
# `feedback_janitor_supersede_must_group_by_workflow_id`, cancelling
# in-flight runs of any required-check-shaped workflow risks the
# dismiss_stale_approvals + empty-commit-rerun dance (Gitea 1.22.6 has
# no REST rerun). The gate-check is `continue-on-error: true` +
# idempotent (POST/PATCH gate-check comment by context) so sequential
# ticks are strictly safe.
concurrency:
group: gate-check-v3-${{ github.event.pull_request.number || github.event.issue.number || github.ref }}
permissions:
# read: contents — for checkout (base ref, not PR head for security)
# read: pull-requests — for reading PR info via API
+1 -1
View File
@@ -106,7 +106,7 @@ jobs:
[[ "$file" == *_test.go ]] && continue
[[ "$file" == *"$path"* ]] || continue
awk "BEGIN{exit !(\$pct < 10)}" || continue
rel=$(echo "$file" | sed 's|^github.com/molecule-ai/molecule-monorepo/platform/workspace-server/||; s|^github.com/molecule-ai/molecule-monorepo/platform/||')
rel=$(echo "$file" | sed 's|^git.moleculesai.app/molecule-ai/molecule-core/workspace-server/workspace-server/||; s|^git.moleculesai.app/molecule-ai/molecule-core/workspace-server/||')
if echo "$ALLOWLIST" | grep -qxF "$rel"; then
continue
fi
+7 -3
View File
@@ -15,9 +15,11 @@ test("FilesTab renders after split", async ({ page, request }) => {
// Clean slate
const { workspaces } = await request
.get("http://localhost:8080/workspaces")
.then(async (r) => ({ workspaces: (await r.json()) as Array<{ id: string }> }));
.then(async (r) => ({ workspaces: (await r.json()) as Array<{ id: string; name: string }> }));
for (const w of workspaces) {
await request.delete(`http://localhost:8080/workspaces/${w.id}?confirm=true`);
await request.delete(`http://localhost:8080/workspaces/${w.id}?confirm=true`, {
headers: { "X-Confirm-Name": w.name },
});
}
// Create a workspace
@@ -80,5 +82,7 @@ test("FilesTab renders after split", async ({ page, request }) => {
await expect(editorEmpty.first()).toBeVisible({ timeout: 5_000 });
// Cleanup
await request.delete(`http://localhost:8080/workspaces/${wsId}?confirm=true`);
await request.delete(`http://localhost:8080/workspaces/${wsId}?confirm=true`, {
headers: { "X-Confirm-Name": "FilesTab Smoke" },
});
});
+35
View File
@@ -0,0 +1,35 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
{
ignores: [
".next/**",
"coverage/**",
"out/**",
"build/**",
"next-env.d.ts",
],
},
...compat.extends("next/core-web-vitals", "next/typescript"),
{
rules: {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-require-imports": "warn",
"prefer-const": "warn",
"react-hooks/rules-of-hooks": "warn",
"react/display-name": "warn",
"react/no-unescaped-entities": "warn",
},
},
];
export default eslintConfig;
+4330 -1
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -6,7 +6,7 @@
"dev": "next dev --turbopack -p 3000",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"test": "vitest run",
"test:coverage": "vitest run --coverage"
},
@@ -31,6 +31,7 @@
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@tailwindcss/postcss": "^4.0.0",
"@testing-library/jest-dom": "^6.6.0",
"@testing-library/react": "^16.1.0",
"@types/node": "^25.6.0",
@@ -38,7 +39,8 @@
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/coverage-v8": "^4.1.5",
"@tailwindcss/postcss": "^4.0.0",
"eslint": "^9.39.4",
"eslint-config-next": "^15.5.15",
"jsdom": "^29.1.1",
"postcss": "^8.5.13",
"tailwindcss": "^4.0.0",
+4 -1
View File
@@ -232,7 +232,10 @@ function CanvasInner() {
}
state.beginDelete(subtree);
try {
await api.del(`/workspaces/${id}?confirm=true`);
const workspaceName = state.nodes.find((n) => n.id === id)?.data.name ?? "";
await api.del(`/workspaces/${id}?confirm=true`, {
headers: { "X-Confirm-Name": workspaceName },
});
// Mirror the server-side cascade locally — drop the parent AND
// every descendant in one atomic update. The per-descendant
// WORKSPACE_REMOVED WS events still arrive (and are no-ops
+1 -1
View File
@@ -128,7 +128,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
<div className="flex-1 overflow-auto bg-black/80 p-4">
{loading && (
<div className="text-[12px] text-ink-mid" data-testid="console-loading">
<div role="status" aria-live="polite" className="text-[12px] text-ink-mid" data-testid="console-loading">
Loading console output
</div>
)}
+201 -20
View File
@@ -33,7 +33,55 @@ interface HermesProvider {
models: string[];
}
const DEFAULT_CREATE_MODEL = "anthropic:claude-opus-4-7";
type LLMAuthMode = "platform" | "api_key" | "oauth";
interface NativeLLMProvider {
id: string;
label: string;
envVar?: string;
defaultModel: string;
models: string[];
authModes: LLMAuthMode[];
}
export const NATIVE_LLM_PROVIDERS: NativeLLMProvider[] = [
{
id: "minimax",
label: "MiniMax",
envVar: "MINIMAX_API_KEY",
defaultModel: "MiniMax-M2.7",
models: ["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5"],
authModes: ["platform", "api_key"],
},
{
id: "kimi-coding",
label: "Kimi",
envVar: "KIMI_API_KEY",
defaultModel: "kimi-for-coding",
models: ["kimi-for-coding", "kimi-k2.5", "kimi-k2"],
authModes: ["platform", "api_key"],
},
{
id: "anthropic",
label: "Anthropic",
envVar: "ANTHROPIC_API_KEY",
defaultModel: "claude-sonnet-4-6",
models: ["claude-sonnet-4-6", "claude-opus-4-7", "claude-haiku-4-5"],
authModes: ["platform", "api_key"],
},
{
id: "anthropic-oauth",
label: "Claude OAuth",
envVar: "CLAUDE_CODE_OAUTH_TOKEN",
defaultModel: "sonnet",
models: ["sonnet", "opus", "haiku"],
authModes: ["oauth"],
},
];
const DEFAULT_HEADLESS_INSTANCE_TYPE = "t3.medium";
const DEFAULT_HEADLESS_ROOT_GB = 30;
const DEFAULT_DISPLAY_INSTANCE_TYPE = "t3.xlarge";
const DEFAULT_DISPLAY_ROOT_GB = 80;
// All providers supported by Hermes runtime via providers.resolve_provider().
// `defaultModel` is the slug injected into the workspace provision request
@@ -71,8 +119,8 @@ export function CreateWorkspaceButton() {
const [error, setError] = useState<string | null>(null);
const [workspaces, setWorkspaces] = useState<WorkspaceOption[]>([]);
const [displayEnabled, setDisplayEnabled] = useState(false);
const [displayInstanceType, setDisplayInstanceType] = useState("t3.xlarge");
const [displayRootGB, setDisplayRootGB] = useState("80");
const [displayInstanceType, setDisplayInstanceType] = useState(DEFAULT_DISPLAY_INSTANCE_TYPE);
const [displayRootGB, setDisplayRootGB] = useState(String(DEFAULT_DISPLAY_ROOT_GB));
const [displayResolution, setDisplayResolution] = useState("1920x1080");
// Templates fetched from /api/templates — drives the dynamic provider
// filter below. Same data source ConfigTab uses (PR #2454). When the
@@ -101,11 +149,16 @@ export function CreateWorkspaceButton() {
// (Anthropic), which 401s if the user's key is for a different
// provider. Hence: require model when template=hermes.
const [hermesModel, setHermesModel] = useState("");
const [llmAuthMode, setLLMAuthMode] = useState<LLMAuthMode>("platform");
const [llmProvider, setLLMProvider] = useState("minimax");
const [llmModel, setLLMModel] = useState("MiniMax-M2.7");
const [llmSecret, setLLMSecret] = useState("");
// Tier picker: on SaaS every workspace gets its own EC2 VM (Full Access
// by construction), so we hide the T1/T2/T3 Docker-sandbox tiers and
// lock to T4 — the full-host access tier, which maps to t3.large at the
// CP level. On self-hosted we still offer T1/T2/T3 because the Docker-
// lock to T4 — the full-host access tier. The EC2 size is controlled by
// the compute profile below. On self-hosted we still offer T1/T2/T3
// because the Docker-
// sandbox distinction is a real choice there; T4 is available too for
// operators who want the full-host tier.
//
@@ -156,6 +209,14 @@ export function CreateWorkspaceButton() {
);
const isHermes = template.trim().toLowerCase() === "hermes";
const nativeLLMProviders = useMemo(
() => NATIVE_LLM_PROVIDERS.filter((p) => p.authModes.includes(llmAuthMode)),
[llmAuthMode],
);
const selectedNativeProvider = useMemo(
() => nativeLLMProviders.find((p) => p.id === llmProvider) ?? nativeLLMProviders[0],
[llmProvider, nativeLLMProviders],
);
// Resolve the selected template's spec from the /templates response.
// The `template` input is free-text; templates can be matched by id,
@@ -203,6 +264,22 @@ export function CreateWorkspaceButton() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [availableProviders, isHermes]);
useEffect(() => {
if (isHermes) return;
if (nativeLLMProviders.length === 0) return;
if (!nativeLLMProviders.some((p) => p.id === llmProvider)) {
setLLMProvider(nativeLLMProviders[0].id);
setLLMModel(nativeLLMProviders[0].defaultModel);
}
}, [isHermes, llmProvider, nativeLLMProviders]);
useEffect(() => {
if (isHermes || !selectedNativeProvider) return;
if (!selectedNativeProvider.models.includes(llmModel)) {
setLLMModel(selectedNativeProvider.defaultModel);
}
}, [isHermes, llmModel, selectedNativeProvider]);
// Auto-fill hermesModel with the provider's defaultModel whenever the
// provider changes, but only if the user hasn't already typed their own
// slug. Prevents the empty-model → "auto" → Anthropic-default 401 trap.
@@ -230,13 +307,17 @@ export function CreateWorkspaceButton() {
setBudgetLimit("");
setError(null);
setDisplayEnabled(false);
setDisplayInstanceType("t3.xlarge");
setDisplayRootGB("80");
setDisplayInstanceType(DEFAULT_DISPLAY_INSTANCE_TYPE);
setDisplayRootGB(String(DEFAULT_DISPLAY_ROOT_GB));
setDisplayResolution("1920x1080");
setHermesProvider("anthropic");
setExternalRuntime("external");
setHermesApiKey("");
setHermesModel("");
setLLMAuthMode("platform");
setLLMProvider("minimax");
setLLMModel("MiniMax-M2.7");
setLLMSecret("");
api
.get<WorkspaceOption[]>("/workspaces")
.then((ws) => setWorkspaces(ws))
@@ -263,12 +344,21 @@ export function CreateWorkspaceButton() {
setError("Model is required for Hermes workspaces — provider routing depends on the model slug prefix");
return;
}
if (!isExternal && !isHermes && !llmModel.trim()) {
setError("Model is required");
return;
}
if (!isExternal && !isHermes && llmAuthMode !== "platform" && !llmSecret.trim()) {
setError(llmAuthMode === "oauth" ? "Claude OAuth token is required" : "API key is required");
return;
}
setCreating(true);
setError(null);
const provider = isHermes
? HERMES_PROVIDERS.find((p) => p.id === hermesProvider)
: undefined;
const nativeProvider = !isHermes ? selectedNativeProvider : undefined;
try {
const parsedBudget = budgetLimit.trim()
@@ -292,19 +382,33 @@ export function CreateWorkspaceButton() {
tier,
parent_id: parentId || undefined,
budget_limit: parsedBudget,
...(!isExternal && !isHermes ? { model: DEFAULT_CREATE_MODEL } : {}),
...(displayEnabled
...(!isExternal && !isHermes && nativeProvider
? {
compute: {
instance_type: displayInstanceType,
volume: { root_gb: Number.isFinite(parsedRootGB) ? parsedRootGB : 80 },
display: {
mode: "desktop-control",
protocol: "novnc",
width: Number.isFinite(displayWidth) ? displayWidth : 1920,
height: Number.isFinite(displayHeight) ? displayHeight : 1080,
},
},
model: llmModel.trim(),
llm_provider: nativeProvider.id,
...(llmAuthMode !== "platform" && nativeProvider.envVar
? { secrets: { [nativeProvider.envVar]: llmSecret.trim() } }
: {}),
}
: {}),
...(!isExternal
? {
compute: displayEnabled
? {
instance_type: displayInstanceType,
volume: { root_gb: Number.isFinite(parsedRootGB) ? parsedRootGB : DEFAULT_DISPLAY_ROOT_GB },
display: {
mode: "desktop-control",
protocol: "novnc",
width: Number.isFinite(displayWidth) ? displayWidth : 1920,
height: Number.isFinite(displayHeight) ? displayHeight : 1080,
},
}
: {
instance_type: DEFAULT_HEADLESS_INSTANCE_TYPE,
volume: { root_gb: DEFAULT_HEADLESS_ROOT_GB },
display: { mode: "none" },
},
}
: {}),
canvas: { x: Math.random() * 400 + 100, y: Math.random() * 300 + 100 },
@@ -438,6 +542,82 @@ export function CreateWorkspaceButton() {
/>
)}
{!isExternal && !isHermes && selectedNativeProvider && (
<div className="rounded-lg border border-line/50 bg-surface-card/40 p-3 space-y-3">
<div className="text-[11px] font-medium text-ink-mid">
LLM
</div>
<div>
<label htmlFor="llm-auth-mode" className="text-[11px] text-ink-mid block mb-1">
Auth Mode
</label>
<select
id="llm-auth-mode"
value={llmAuthMode}
onChange={(e) => setLLMAuthMode(e.target.value as LLMAuthMode)}
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors"
>
<option value="platform">Platform provided</option>
<option value="api_key">API key</option>
<option value="oauth">Claude OAuth</option>
</select>
</div>
<div>
<label htmlFor="llm-provider-select" className="text-[11px] text-ink-mid block mb-1">
Provider
</label>
<select
id="llm-provider-select"
value={selectedNativeProvider.id}
onChange={(e) => {
const next = nativeLLMProviders.find((p) => p.id === e.target.value);
setLLMProvider(e.target.value);
if (next) setLLMModel(next.defaultModel);
}}
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors"
>
{nativeLLMProviders.map((p) => (
<option key={p.id} value={p.id}>
{p.label}
</option>
))}
</select>
</div>
<div>
<label htmlFor="llm-model-input" className="text-[11px] text-ink-mid block mb-1">
Model
</label>
<input
id="llm-model-input"
type="text"
value={llmModel}
onChange={(e) => setLLMModel(e.target.value)}
list="llm-model-suggestions"
spellCheck={false}
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink placeholder-ink-soft focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors font-mono"
/>
<datalist id="llm-model-suggestions">
{selectedNativeProvider.models.map((m) => <option key={m} value={m} />)}
</datalist>
</div>
{llmAuthMode !== "platform" && (
<div>
<label htmlFor="llm-secret-input" className="text-[11px] text-ink-mid block mb-1">
{llmAuthMode === "oauth" ? "OAuth Token" : "API Key"}
</label>
<input
id="llm-secret-input"
type="password"
value={llmSecret}
onChange={(e) => setLLMSecret(e.target.value)}
autoComplete="off"
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink placeholder-ink-soft focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors font-mono"
/>
</div>
)}
</div>
)}
<div>
<div
role="radiogroup"
@@ -542,10 +722,11 @@ export function CreateWorkspaceButton() {
)}
<div>
<label className="text-[11px] text-ink-mid block mb-1">
<label htmlFor="parent-workspace-select" className="text-[11px] text-ink-mid block mb-1">
Parent Workspace
</label>
<select
id="parent-workspace-select"
value={parentId}
onChange={(e) => setParentId(e.target.value)}
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors"
@@ -242,10 +242,13 @@ export function ProvisioningTimeout({
const handleCancelConfirm = useCallback(async () => {
if (!confirmingCancel) return;
const workspaceId = confirmingCancel;
const workspaceName = timedOut.find((e) => e.workspaceId === workspaceId)?.workspaceName ?? "";
setConfirmingCancel(null);
setCancelling((prev) => new Set(prev).add(workspaceId));
try {
await api.del(`/workspaces/${workspaceId}`);
await api.del(`/workspaces/${workspaceId}`, {
headers: { "X-Confirm-Name": workspaceName },
});
setTimedOut((prev) => prev.filter((e) => e.workspaceId !== workspaceId));
trackingRef.current.delete(workspaceId);
showToast("Deployment cancelled", "info");
+3 -1
View File
@@ -305,7 +305,9 @@ export function SidePanel() {
{panelTab === "chat" && <ChatTab key={selectedNodeId} workspaceId={selectedNodeId} data={node.data} />}
{panelTab === "terminal" && <TerminalTab key={selectedNodeId} workspaceId={selectedNodeId} data={node.data} />}
{panelTab === "display" && <DisplayTab key={selectedNodeId} workspaceId={selectedNodeId} />}
{panelTab === "container-config" && <ContainerConfigTab key={selectedNodeId} data={node.data} />}
{panelTab === "container-config" && selectedNodeId && (
<ContainerConfigTab key={selectedNodeId} workspaceId={selectedNodeId} data={node.data} />
)}
{panelTab === "config" && <ConfigTab key={selectedNodeId} workspaceId={selectedNodeId} />}
{panelTab === "schedule" && <ScheduleTab key={selectedNodeId} workspaceId={selectedNodeId} />}
{panelTab === "channels" && <ChannelsTab key={selectedNodeId} workspaceId={selectedNodeId} />}
@@ -63,7 +63,7 @@ describe("CreateWorkspaceDialog", () => {
it('first option is "None (root level)" with empty value', async () => {
await openDialog();
const select = document.querySelector("select") as HTMLSelectElement;
const select = screen.getByLabelText("Parent Workspace") as HTMLSelectElement;
expect(select).toBeTruthy();
const firstOption = select.options[0];
expect(firstOption.value).toBe("");
@@ -73,12 +73,12 @@ describe("CreateWorkspaceDialog", () => {
it("populates select with workspace names from GET /workspaces", async () => {
await openDialog();
await waitFor(() => {
const select = document.querySelector("select") as HTMLSelectElement;
const select = screen.getByLabelText("Parent Workspace") as HTMLSelectElement;
const optionValues = Array.from(select.options).map((o) => o.value);
expect(optionValues).toContain("ws-1");
expect(optionValues).toContain("ws-2");
});
const select = document.querySelector("select") as HTMLSelectElement;
const select = screen.getByLabelText("Parent Workspace") as HTMLSelectElement;
const optionTexts = Array.from(select.options).map((o) => o.text.trim());
expect(optionTexts.some((t) => t.includes("Platform Team"))).toBe(true);
expect(optionTexts.some((t) => t.includes("Research Agent"))).toBe(true);
@@ -87,7 +87,7 @@ describe("CreateWorkspaceDialog", () => {
it("sends parent_id in POST body when a workspace is selected", async () => {
await openDialog();
await waitFor(() => {
const select = document.querySelector("select") as HTMLSelectElement;
const select = screen.getByLabelText("Parent Workspace") as HTMLSelectElement;
expect(select.options.length).toBeGreaterThan(1);
});
@@ -95,7 +95,7 @@ describe("CreateWorkspaceDialog", () => {
target: { value: "My Agent" },
});
const select = document.querySelector("select") as HTMLSelectElement;
const select = screen.getByLabelText("Parent Workspace") as HTMLSelectElement;
fireEvent.change(select, { target: { value: "ws-1" } });
const createBtn = screen.getAllByRole("button").find((b) => b.textContent === "Create");
@@ -112,7 +112,7 @@ describe("CreateWorkspaceDialog", () => {
target: { value: "Root Agent" },
});
const select = document.querySelector("select") as HTMLSelectElement;
const select = screen.getByLabelText("Parent Workspace") as HTMLSelectElement;
fireEvent.change(select, { target: { value: "" } });
const createBtn = screen.getAllByRole("button").find((b) => b.textContent === "Create");
@@ -123,7 +123,7 @@ describe("CreateWorkspaceDialog", () => {
expect(body.parent_id).toBeUndefined();
});
it("omits compute config by default", async () => {
it("sends the cost-efficient headless compute profile by default", async () => {
await openDialog();
fireEvent.change(screen.getByPlaceholderText("e.g. SEO Agent"), {
target: { value: "Plain Agent" },
@@ -132,10 +132,32 @@ describe("CreateWorkspaceDialog", () => {
const createBtn = screen.getAllByRole("button").find((b) => b.textContent === "Create");
fireEvent.click(createBtn!);
await waitFor(() => expect(mockPost).toHaveBeenCalled());
const body = mockPost.mock.calls[0][1] as Record<string, unknown>;
expect(body.compute).toEqual({
instance_type: "t3.medium",
volume: { root_gb: 30 },
display: { mode: "none" },
});
expect(body.model).toBe("MiniMax-M2.7");
expect(body.llm_provider).toBe("minimax");
expect(body.secrets).toBeUndefined();
});
it("does not send managed compute for external agents", async () => {
await openDialog();
fireEvent.change(screen.getByPlaceholderText("e.g. SEO Agent"), {
target: { value: "External Agent" },
});
fireEvent.click(screen.getByLabelText(/External agent/));
const createBtn = screen.getAllByRole("button").find((b) => b.textContent === "Create");
fireEvent.click(createBtn!);
await waitFor(() => expect(mockPost).toHaveBeenCalled());
const body = mockPost.mock.calls[0][1] as Record<string, unknown>;
expect(body.compute).toBeUndefined();
expect(body.model).toBe("anthropic:claude-opus-4-7");
expect(body.runtime).toBe("external");
});
it("sends display compute profile when desktop display is enabled", async () => {
@@ -150,7 +172,8 @@ describe("CreateWorkspaceDialog", () => {
await waitFor(() => expect(mockPost).toHaveBeenCalled());
const body = mockPost.mock.calls[0][1] as Record<string, unknown>;
expect(body.model).toBe("anthropic:claude-opus-4-7");
expect(body.model).toBe("MiniMax-M2.7");
expect(body.llm_provider).toBe("minimax");
expect(body.compute).toEqual({
instance_type: "t3.xlarge",
volume: { root_gb: 80 },
@@ -163,13 +186,57 @@ describe("CreateWorkspaceDialog", () => {
});
});
it("sends BYOK API key secrets when API key auth mode is selected", async () => {
await openDialog();
fireEvent.change(screen.getByPlaceholderText("e.g. SEO Agent"), {
target: { value: "BYOK Agent" },
});
fireEvent.change(document.getElementById("llm-auth-mode") as HTMLSelectElement, {
target: { value: "api_key" },
});
fireEvent.change(document.getElementById("llm-secret-input") as HTMLInputElement, {
target: { value: "sk-minimax-test" },
});
const createBtn = screen.getAllByRole("button").find((b) => b.textContent === "Create");
fireEvent.click(createBtn!);
await waitFor(() => expect(mockPost).toHaveBeenCalled());
const body = mockPost.mock.calls[0][1] as Record<string, unknown>;
expect(body.model).toBe("MiniMax-M2.7");
expect(body.llm_provider).toBe("minimax");
expect(body.secrets).toEqual({ MINIMAX_API_KEY: "sk-minimax-test" });
});
it("sends Claude OAuth token separately from platform-managed mode", async () => {
await openDialog();
fireEvent.change(screen.getByPlaceholderText("e.g. SEO Agent"), {
target: { value: "OAuth Agent" },
});
fireEvent.change(document.getElementById("llm-auth-mode") as HTMLSelectElement, {
target: { value: "oauth" },
});
fireEvent.change(document.getElementById("llm-secret-input") as HTMLInputElement, {
target: { value: "oauth-token" },
});
const createBtn = screen.getAllByRole("button").find((b) => b.textContent === "Create");
fireEvent.click(createBtn!);
await waitFor(() => expect(mockPost).toHaveBeenCalled());
const body = mockPost.mock.calls[0][1] as Record<string, unknown>;
expect(body.model).toBe("sonnet");
expect(body.llm_provider).toBe("anthropic-oauth");
expect(body.secrets).toEqual({ CLAUDE_CODE_OAUTH_TOKEN: "oauth-token" });
});
it("renders gracefully when GET /workspaces fails", async () => {
mockGet.mockRejectedValueOnce(new Error("Network error"));
await openDialog();
// Dialog still renders; select exists with only the root option
await waitFor(() => {
const select = document.querySelector("select") as HTMLSelectElement;
const select = screen.getByLabelText("Parent Workspace") as HTMLSelectElement;
expect(select.options.length).toBe(1);
expect(select.options[0].value).toBe("");
});
@@ -272,7 +272,9 @@ describe("OrgCancelButton — API interactions", () => {
fireEvent.click(screen.getByRole("button", { name: /cancel deployment of test org/i }));
fireEvent.click(screen.getByRole("button", { name: /yes/i }));
await act(async () => { /* flush */ });
expect(mockApiDel).toHaveBeenCalledWith("/workspaces/root-1?confirm=true");
expect(mockApiDel).toHaveBeenCalledWith("/workspaces/root-1?confirm=true", {
headers: { "X-Confirm-Name": "Test Org" },
});
});
it("shows success toast on DELETE success", async () => {
@@ -57,6 +57,7 @@ export function OrgCancelButton({ rootId, rootName, workspaceCount }: Props) {
try {
await api.del<{ status: string }>(
`/workspaces/${rootId}?confirm=true`,
{ headers: { "X-Confirm-Name": rootName } },
);
showToast(`Cancelled deployment of "${rootName}"`, "success");
// Optimistic local removal — workspace-server broadcasts
@@ -199,7 +199,9 @@ describe("OrgCancelButton — Yes / cascade delete", () => {
});
// 1) API call hit the cascade-delete endpoint with confirm=true
expect(mockApiDel).toHaveBeenCalledWith("/workspaces/ws-root?confirm=true");
expect(mockApiDel).toHaveBeenCalledWith("/workspaces/ws-root?confirm=true", {
headers: { "X-Confirm-Name": "My Org" },
});
// 2) beginDelete locked the WHOLE subtree (root + 2 children) — NOT the unrelated node
expect(mockState.beginDelete).toHaveBeenCalledTimes(1);
+246 -39
View File
@@ -1,46 +1,210 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { api } from "@/lib/api";
import { runtimeDisplayName } from "@/lib/runtime-names";
import type { WorkspaceNodeData } from "@/store/canvas";
import { useCanvasStore, type WorkspaceNodeData } from "@/store/canvas";
import type { WorkspaceCompute } from "@/store/socket";
const INSTANCE_TYPES = ["t3.medium", "t3.large", "t3.xlarge", "t3.2xlarge", "m6i.large", "m6i.xlarge", "c6i.xlarge"];
const RUNTIME_OPTIONS = ["claude-code", "codex", "hermes", "openclaw", "kimi", "kimi-cli", "external"];
const RESOLUTIONS = ["1280x720", "1440x900", "1920x1080", "2560x1440"];
const DEFAULT_HEADLESS_INSTANCE_TYPE = "t3.medium";
const DEFAULT_HEADLESS_ROOT_GB = 30;
type Props = {
workspaceId: string;
data: Pick<
WorkspaceNodeData,
"runtime" | "status" | "needsRestart" | "activeTasks" | "deliveryMode"
| "workspaceAccess" | "maxConcurrentTasks"
| "workspaceAccess" | "maxConcurrentTasks" | "compute" | "applyTemplateOnRestart"
>;
};
export function ContainerConfigTab({ data }: Props) {
const runtime = data.runtime || "unknown";
type FormState = {
runtime: string;
instanceType: string;
rootGB: string;
displayEnabled: boolean;
displayMode: string;
displayProtocol: string;
resolution: string;
};
export function ContainerConfigTab({ workspaceId, data }: Props) {
const runtime = data.runtime;
const instanceType = data.compute?.instance_type;
const rootGB = data.compute?.volume?.root_gb;
const displayMode = data.compute?.display?.mode;
const displayProtocol = data.compute?.display?.protocol;
const displayWidth = data.compute?.display?.width;
const displayHeight = data.compute?.display?.height;
const initial = useMemo(
() => formFromData({ runtime, instanceType, rootGB, displayMode, displayProtocol, displayWidth, displayHeight }),
[runtime, instanceType, rootGB, displayMode, displayProtocol, displayWidth, displayHeight],
);
const [form, setForm] = useState<FormState>(initial);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
useEffect(() => {
setForm(initial);
setError(null);
setSuccess(false);
}, [initial]);
const workspaceAccess = formatAccess(data.workspaceAccess);
const maxConcurrentTasks = data.maxConcurrentTasks ? String(data.maxConcurrentTasks) : "platform-managed";
const mountedPath = "/workspace";
const privilegeStatus = "standard";
const deliveryMode = data.deliveryMode || "push";
const dirty = JSON.stringify(form) !== JSON.stringify(initial);
const restartLabel = dirty ? "Save & Restart" : "Restart to apply";
const resolutionOptions = RESOLUTIONS.includes(form.resolution)
? RESOLUTIONS
: [form.resolution, ...RESOLUTIONS];
const save = async (restart: boolean) => {
setError(null);
setSuccess(false);
setSaving(true);
try {
let applyTemplateOnRestart = data.applyTemplateOnRestart ?? false;
if (dirty) {
const rootGB = parseInt(form.rootGB, 10);
if (!Number.isFinite(rootGB)) {
setError("Root volume must be a number");
return;
}
const [width, height] = form.resolution.split("x").map((v) => parseInt(v, 10));
const compute: WorkspaceCompute = {
instance_type: form.instanceType,
volume: { root_gb: rootGB },
display: form.displayEnabled
? { mode: form.displayMode, protocol: form.displayProtocol, width, height }
: { mode: "none" },
};
const resp = await api.patch<{ needs_restart?: boolean }>(`/workspaces/${workspaceId}`, {
runtime: form.runtime,
compute,
});
useCanvasStore.getState().updateNodeData(workspaceId, {
runtime: form.runtime,
compute,
needsRestart: resp.needs_restart ?? true,
applyTemplateOnRestart: form.runtime !== initial.runtime,
});
applyTemplateOnRestart = form.runtime !== initial.runtime;
}
if (restart) {
await useCanvasStore.getState().restartWorkspace(workspaceId, {
applyTemplate: applyTemplateOnRestart,
});
}
setSuccess(true);
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to save");
} finally {
setSaving(false);
}
};
return (
<div className="p-4 space-y-4">
<section className="rounded-lg border border-line/50 bg-surface-card/40 p-4">
<div className="mb-3">
<div className="mb-3 flex items-center justify-between gap-3">
<h3 className="text-sm font-semibold text-ink">Container Config</h3>
{data.needsRestart && <span className="text-[11px] text-warm">Restart required</span>}
</div>
<dl className="grid grid-cols-1 gap-2 text-[11px]">
<ConfigRow label="Runtime image" value={runtimeDisplayName(runtime)} detail={runtime} />
<ConfigRow label="Workspace access" value={workspaceAccess} />
<ConfigRow label="Max concurrent tasks" value={maxConcurrentTasks} />
<ConfigRow label="Mounted workspace path" value={mountedPath} />
<ConfigRow label="Container privileges" value={privilegeStatus} />
<ConfigRow label="Delivery mode" value={deliveryMode} />
</dl>
</section>
<div className="grid grid-cols-1 gap-3 text-[11px]">
<SelectField
id="runtime-image-profile"
label="Runtime image"
value={form.runtime}
options={RUNTIME_OPTIONS}
optionLabel={runtimeDisplayName}
onChange={(runtime) => setForm((s) => ({ ...s, runtime }))}
/>
<SelectField
id="instance-type"
label="Instance type"
value={form.instanceType}
options={INSTANCE_TYPES}
onChange={(instanceType) => setForm((s) => ({ ...s, instanceType }))}
/>
<label className="grid gap-1" htmlFor="root-volume-gb">
<span className="text-ink-mid">Root volume</span>
<div className="flex items-center gap-2">
<input
id="root-volume-gb"
aria-label="Root volume"
type="number"
min={30}
max={500}
value={form.rootGB}
onChange={(e) => setForm((s) => ({ ...s, rootGB: e.target.value }))}
className="min-w-0 flex-1 rounded-md border border-line/60 bg-surface-sunken px-3 py-2 font-mono text-ink outline-none focus:border-accent"
/>
<span className="text-ink-mid">GB</span>
</div>
</label>
<label className="flex items-center justify-between gap-3 rounded-md bg-surface-sunken/40 px-3 py-2">
<span className="text-ink-mid">Display</span>
<input
type="checkbox"
aria-label="Enable display"
checked={form.displayEnabled}
onChange={(e) => setForm((s) => ({
...s,
displayEnabled: e.target.checked,
displayMode: e.target.checked && s.displayMode === "none" ? "desktop-control" : s.displayMode,
displayProtocol: e.target.checked && !s.displayProtocol ? "novnc" : s.displayProtocol,
}))}
className="h-4 w-4 accent-accent"
/>
</label>
{form.displayEnabled && (
<SelectField
id="display-resolution"
label="Resolution"
value={form.resolution}
options={resolutionOptions}
onChange={(resolution) => setForm((s) => ({ ...s, resolution }))}
/>
)}
</div>
<section className="rounded-lg border border-line/50 bg-surface-card/40 p-4">
<h3 className="mb-3 text-sm font-semibold text-ink">Session Controls</h3>
<div className="grid grid-cols-2 gap-2">
<ReadOnlyAction label={data.needsRestart ? "Restart required" : "Restart"} />
<ReadOnlyAction label="Reset session" />
<div className="mt-4 flex items-center justify-end gap-2">
{error && <span className="mr-auto text-[11px] text-bad">{error}</span>}
{success && <span className="mr-auto text-[11px] text-good">Saved</span>}
<button
type="button"
disabled={!dirty || saving}
onClick={() => setForm(initial)}
className="rounded-md border border-line/60 px-3 py-2 text-[11px] text-ink-mid disabled:cursor-not-allowed disabled:opacity-50"
>
Reset
</button>
<button
type="button"
disabled={!dirty || saving}
onClick={() => save(false)}
className="rounded-md bg-accent px-3 py-2 text-[11px] font-medium text-white disabled:cursor-not-allowed disabled:opacity-50"
>
{saving ? "Saving..." : "Save"}
</button>
<button
type="button"
disabled={(!dirty && !data.needsRestart) || saving}
onClick={() => save(true)}
className="rounded-md bg-ink px-3 py-2 text-[11px] font-medium text-surface disabled:cursor-not-allowed disabled:opacity-50"
>
{saving ? "Restarting..." : restartLabel}
</button>
</div>
</section>
@@ -49,13 +213,73 @@ export function ContainerConfigTab({ data }: Props) {
<dl className="grid grid-cols-1 gap-2 text-[11px]">
<ConfigRow label="Container status" value={data.status} />
<ConfigRow label="Active tasks" value={String(data.activeTasks ?? 0)} />
<ConfigRow label="Mounted path access" value="available" />
<ConfigRow label="Workspace access" value={workspaceAccess} />
<ConfigRow label="Max concurrent tasks" value={maxConcurrentTasks} />
<ConfigRow label="Mounted workspace path" value="/workspace" />
<ConfigRow label="Delivery mode" value={deliveryMode} />
</dl>
</section>
</div>
);
}
function formFromData(data: {
runtime?: string;
instanceType?: string;
rootGB?: number;
displayMode?: string;
displayProtocol?: string;
displayWidth?: number;
displayHeight?: number;
}): FormState {
const width = data.displayWidth ?? 1920;
const height = data.displayHeight ?? 1080;
const resolution = `${width}x${height}`;
return {
runtime: data.runtime || "claude-code",
instanceType: data.instanceType || DEFAULT_HEADLESS_INSTANCE_TYPE,
rootGB: String(data.rootGB || DEFAULT_HEADLESS_ROOT_GB),
displayEnabled: !!data.displayMode && data.displayMode !== "none",
displayMode: data.displayMode && data.displayMode !== "none" ? data.displayMode : "desktop-control",
displayProtocol: data.displayProtocol || "novnc",
resolution,
};
}
function SelectField({
id,
label,
value,
options,
optionLabel = (v: string) => v,
onChange,
}: {
id: string;
label: string;
value: string;
options: string[];
optionLabel?: (value: string) => string;
onChange: (value: string) => void;
}) {
return (
<label className="grid gap-1" htmlFor={id}>
<span className="text-ink-mid">{label}</span>
<select
id={id}
value={value}
onChange={(e) => onChange(e.target.value)}
className="rounded-md border border-line/60 bg-surface-sunken px-3 py-2 font-mono text-ink outline-none focus:border-accent"
>
{options.map((option) => (
<option key={option} value={option}>
{optionLabel(option)}
</option>
))}
</select>
</label>
);
}
function formatAccess(value: string | null | undefined): string {
if (!value) return "none";
return value.replace(/_/g, "-");
@@ -64,33 +288,16 @@ function formatAccess(value: string | null | undefined): string {
function ConfigRow({
label,
value,
detail,
}: {
label: string;
value: string;
detail?: string;
}) {
return (
<div className="flex items-start justify-between gap-3 rounded-md bg-surface-sunken/40 px-3 py-2">
<dt className="text-ink-mid">{label}</dt>
<dd className="min-w-0 text-right">
<div className="font-mono text-ink break-words">{value}</div>
{detail && detail !== value && (
<div className="mt-0.5 font-mono text-[10px] text-ink-mid break-words">{detail}</div>
)}
</dd>
</div>
);
}
function ReadOnlyAction({ label }: { label: string }) {
return (
<button
type="button"
disabled
className="rounded-md border border-line/50 bg-surface-sunken/40 px-3 py-2 text-[11px] text-ink-mid disabled:cursor-not-allowed disabled:opacity-70"
>
{label}
</button>
);
}
+3 -1
View File
@@ -93,7 +93,9 @@ export function DetailsTab({ workspaceId, data }: Props) {
const handleDelete = async () => {
setDeleteError(null);
try {
await api.del(`/workspaces/${workspaceId}?confirm=true`);
await api.del(`/workspaces/${workspaceId}?confirm=true`, {
headers: { "X-Confirm-Name": name },
});
// Mirror the server-side cascade — drop the row + every
// descendant locally so the canvas reflects the deletion
// immediately, even when the WS is dead and the per-descendant
+7 -3
View File
@@ -265,6 +265,11 @@ function DisplayControlBar({
onAcquire: () => void;
onRelease: () => void;
}) {
const userControl = control?.controller === "user";
const adminControl = userControl && control?.controlled_by === "admin-token";
const canAcquireUserControl = control?.controller === "none" || (userControl && !hasSession);
const canReleaseUserControl = adminControl || (userControl && hasSession);
return (
<div className="flex min-w-0 items-center gap-3">
{control && (
@@ -282,8 +287,7 @@ function DisplayControlBar({
{controlError && <p className="mt-0.5 text-[10px] text-red-200">{controlError}</p>}
</div>
)}
{(control?.controller === "none" ||
(control?.controller === "user" && control.controlled_by === "admin-token" && !hasSession)) && (
{canAcquireUserControl && (
<button
type="button"
onClick={onAcquire}
@@ -293,7 +297,7 @@ function DisplayControlBar({
Take control
</button>
)}
{control?.controller === "user" && control.controlled_by === "admin-token" && (
{canReleaseUserControl && (
<button
type="button"
onClick={onRelease}
@@ -67,7 +67,7 @@ export function FileEditor({
return (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="text-2xl opacity-20 mb-2">📄</div>
<div aria-hidden="true" className="text-2xl opacity-20 mb-2">📄</div>
<p className="text-[10px] text-ink-mid">Select a file to edit</p>
</div>
</div>
@@ -79,16 +79,16 @@ export function FileEditor({
{/* File header */}
<div className="flex items-center justify-between px-3 py-1.5 border-b border-line/40 bg-surface-sunken/20">
<div className="flex items-center gap-1.5 min-w-0">
<span className="text-[10px] opacity-50">{getIcon(selectedFile, false)}</span>
<span aria-hidden="true" className="text-[10px] opacity-50">{getIcon(selectedFile, false)}</span>
<span className="text-[10px] font-mono text-ink-mid truncate">{selectedFile}</span>
{isDirty && <span className="text-[9px] text-warm">modified</span>}
</div>
<div className="flex items-center gap-2">
{success && <span className="text-[9px] text-good">{success}</span>}
{success && <span role="status" aria-live="polite" className="text-[9px] text-good">{success}</span>}
<button
onClick={onDownload}
aria-label="Download file"
className="text-[10px] text-ink-mid hover:text-ink-mid"
className="text-[10px] text-ink-mid hover:text-ink-mid focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
</button>
@@ -96,7 +96,7 @@ export function FileEditor({
<button
onClick={onSave}
disabled={!isDirty || saving}
className="text-[10px] text-accent hover:text-accent disabled:opacity-30"
className="text-[10px] text-accent hover:text-accent disabled:opacity-30 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
>
{saving ? "Saving..." : "Save"}
</button>
@@ -166,6 +166,7 @@ export function FileEditor({
}
}}
spellCheck={false}
aria-label="File content editor"
className="flex-1 w-full bg-surface p-3 text-[11px] font-mono text-ink leading-relaxed resize-none focus:outline-none"
style={{ tabSize: 2 }}
/>
@@ -29,8 +29,8 @@ afterEach(() => {
const defaultProps = {
selectedFile: "/configs/agent.yaml",
fileContent: "name: test\nruntime: langgraph",
editContent: "name: test\nruntime: langgraph",
fileContent: "name: test\nruntime: claude-code",
editContent: "name: test\nruntime: claude-code",
setEditContent: vi.fn(),
loadingFile: false,
saving: false,
@@ -197,12 +197,12 @@ describe("FileEditor — textarea", () => {
render(
<FileEditor
{...defaultProps}
editContent="runtime: langgraph"
editContent="runtime: claude-code"
/>,
);
const ta = document.querySelector("textarea");
expect(ta).toBeTruthy();
expect(ta?.value).toBe("runtime: langgraph");
expect(ta?.value).toBe("runtime: claude-code");
});
it("textarea is readOnly when root is not /configs", () => {
@@ -210,7 +210,7 @@ describe("FileEditor — textarea", () => {
<FileEditor
{...defaultProps}
root="/workspace"
editContent="runtime: langgraph"
editContent="runtime: claude-code"
/>,
);
const ta = document.querySelector("textarea");
@@ -222,7 +222,7 @@ describe("FileEditor — textarea", () => {
<FileEditor
{...defaultProps}
root="/configs"
editContent="runtime: langgraph"
editContent="runtime: claude-code"
/>,
);
const ta = document.querySelector("textarea");
@@ -78,11 +78,11 @@ describe("walkEntry — file entry", () => {
});
it("populates the File object with correct content", async () => {
const { entry, file } = makeFile("config.yaml", "runtime: langgraph");
const { entry, file } = makeFile("config.yaml", "runtime: claude-code");
const out: CollectedEntry[] = [];
await walkEntry(entry as never, "", out);
expect(out[0]!.file).toBe(file);
expect(await out[0]!.file.text()).toBe("runtime: langgraph");
expect(await out[0]!.file.text()).toBe("runtime: claude-code");
});
it("appends to existing entries array (non-destructive)", async () => {
+1 -1
View File
@@ -32,7 +32,7 @@ interface PluginInfo {
author: string;
tags: string[];
skills: string[];
// Declared supported runtimes (e.g. ["claude_code", "deepagents"]).
// Declared supported runtimes (e.g. ["claude_code", "hermes"]).
// Empty / absent = "unspecified, try it".
runtimes?: string[];
// Only present on /workspaces/:id/plugins responses — true if the
@@ -3,10 +3,10 @@
// Regression tests for ConfigTab hermes-workspace UX (#1894 + #1900).
//
// All four bugs this suite pins hit the same workspace on 2026-04-23:
// a hermes-runtime workspace whose Config tab showed "LangGraph
// a hermes-runtime workspace whose Config tab showed "Claude Code
// (default)" in the runtime dropdown, an empty Model field, and a
// scary red "No config.yaml found" banner. Clicking Save would
// silently PATCH runtime back to LangGraph, breaking the workspace.
// silently PATCH runtime back to Claude Code, breaking the workspace.
//
// Each test pins one invariant. If any fails, the bug is back.
@@ -91,7 +91,7 @@ describe("ConfigTab — hermes workspace", () => {
it("loads runtime from workspace metadata when config.yaml is missing (#1894 bug 1)", async () => {
// This is the hermes case: no platform config.yaml, so the form must
// fall back to GET /workspaces/:id's runtime field. Before the fix, the
// runtime dropdown showed "LangGraph (default)" because the fallback
// runtime dropdown showed "Claude Code (default)" because the fallback
// didn't exist.
wireApi({
workspaceRuntime: "hermes",
@@ -150,9 +150,9 @@ describe("ConfigTab — hermes workspace", () => {
expect(screen.queryByText(/Hermes manages its own config/i)).toBeNull();
});
it("DOES show 'No config.yaml found' error for langgraph workspace (default runtime)", async () => {
it("DOES show 'No config.yaml found' error for claude-code workspace (default runtime)", async () => {
// Regression guard the other way — the gray info banner is hermes-
// specific. A langgraph workspace with no config.yaml SHOULD still
// specific. A claude-code workspace with no config.yaml SHOULD still
// see the red error so the user knows to provide a template config.
wireApi({
workspaceRuntime: "",
@@ -302,21 +302,21 @@ describe("ConfigTab — config.yaml on disk", () => {
// MCP server list, etc.) but runtime/model/tier come from the
// workspace row so the node badge matches the form.
//
// Scenario: DB says "hermes", config.yaml says "crewai". The form
// Scenario: DB says "hermes", config.yaml says "openclaw". The form
// must show hermes (DB wins).
//
// We pick hermes (not langgraph) on the DB side because "langgraph"
// is collapsed to the empty-string "LangGraph (default)" option in
// the runtime dropdown — so a "langgraph" DB value would render as
// We pick hermes (not claude-code) on the DB side because "claude-code"
// is collapsed to the empty-string "Claude Code (default)" option in
// the runtime dropdown — so a "claude-code" DB value would render as
// the empty-valued option and obscure whether the DB-wins logic
// actually fired. Hermes has its own non-empty option value and
// gives the assertion a clean signal.
wireApi({
workspaceRuntime: "hermes", // DB — authoritative
configYamlContent: 'runtime: crewai\nmodel: "claude-opus"\n',
configYamlContent: 'runtime: openclaw\nmodel: "claude-opus"\n',
templates: [
{ id: "t-hermes", name: "Hermes", runtime: "hermes", models: [] },
{ id: "t-crewai", name: "CrewAI", runtime: "crewai", models: [] },
{ id: "t-openclaw", name: "OpenClaw", runtime: "openclaw", models: [] },
],
});
@@ -1,21 +1,66 @@
// @vitest-environment jsdom
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const apiPatch = vi.fn();
const updateNodeData = vi.fn();
const restartWorkspace = vi.fn();
vi.mock("@/lib/api", () => ({
api: {
patch: (path: string, body: unknown) => apiPatch(path, body),
},
}));
vi.mock("@/lib/runtime-names", () => ({
runtimeDisplayName: (runtime: string) => runtime,
}));
vi.mock("@/store/canvas", () => ({
useCanvasStore: Object.assign(
(selector: (s: unknown) => unknown) => selector({ restartWorkspace, updateNodeData }),
{ getState: () => ({ restartWorkspace, updateNodeData }) },
),
}));
import { ContainerConfigTab } from "../ContainerConfigTab";
afterEach(() => {
cleanup();
});
beforeEach(() => {
apiPatch.mockReset();
restartWorkspace.mockReset();
updateNodeData.mockReset();
});
describe("ContainerConfigTab", () => {
it("renders read-only runtime and container settings separate from compute shape", () => {
it("defaults missing compute to the cost-efficient headless profile", () => {
render(
<ContainerConfigTab
workspaceId="ws-compute"
data={{
runtime: "claude-code",
status: "online",
needsRestart: false,
activeTasks: 0,
maxConcurrentTasks: null,
workspaceAccess: "none",
deliveryMode: "push",
compute: undefined,
}}
/>,
);
expect(screen.getByLabelText("Instance type")).toHaveProperty("value", "t3.medium");
expect(screen.getByLabelText("Root volume")).toHaveProperty("value", "30");
});
it("renders persisted compute and status settings", () => {
render(
<ContainerConfigTab
workspaceId="ws-compute"
data={{
runtime: "claude-code",
status: "online",
@@ -24,19 +69,249 @@ describe("ContainerConfigTab", () => {
maxConcurrentTasks: 3,
workspaceAccess: "read_write",
deliveryMode: "poll",
compute: {
instance_type: "t3.xlarge",
volume: { root_gb: 80 },
display: { mode: "desktop-control", protocol: "novnc", width: 1920, height: 1080 },
},
}}
/>,
);
expect(screen.getByText("Runtime image")).toBeTruthy();
expect(screen.getByText("claude-code")).toBeTruthy();
expect(screen.getByLabelText("Runtime image")).toHaveProperty("value", "claude-code");
expect(screen.getByLabelText("Instance type")).toHaveProperty("value", "t3.xlarge");
expect(screen.getByLabelText("Root volume")).toHaveProperty("value", "80");
expect(screen.getByLabelText("Enable display")).toHaveProperty("checked", true);
expect(screen.getByLabelText("Resolution")).toHaveProperty("value", "1920x1080");
expect(screen.getByText("Workspace access")).toBeTruthy();
expect(screen.getByText("read-write")).toBeTruthy();
expect(screen.getByText("Max concurrent tasks")).toBeTruthy();
expect(screen.getByText("3")).toBeTruthy();
expect(screen.getByText("/workspace")).toBeTruthy();
expect(screen.getByText("Container privileges")).toBeTruthy();
expect(screen.queryByText("Instance type")).toBeNull();
expect(screen.queryByText("Root volume")).toBeNull();
});
it("does not reset dirty form edits on unrelated status rerender", () => {
const { rerender } = render(
<ContainerConfigTab
workspaceId="ws-compute"
data={{
runtime: "claude-code",
status: "online",
needsRestart: false,
activeTasks: 0,
maxConcurrentTasks: null,
workspaceAccess: "none",
deliveryMode: "push",
compute: {
instance_type: "t3.large",
volume: { root_gb: 50 },
display: { mode: "none" },
},
}}
/>,
);
fireEvent.change(screen.getByLabelText("Root volume"), { target: { value: "120" } });
rerender(
<ContainerConfigTab
workspaceId="ws-compute"
data={{
runtime: "claude-code",
status: "online",
needsRestart: false,
activeTasks: 1,
maxConcurrentTasks: null,
workspaceAccess: "none",
deliveryMode: "push",
compute: {
instance_type: "t3.large",
volume: { root_gb: 50 },
display: { mode: "none" },
},
}}
/>,
);
expect(screen.getByLabelText("Root volume")).toHaveProperty("value", "120");
});
it("saves runtime and compute changes through workspace PATCH", async () => {
apiPatch.mockResolvedValueOnce({ needs_restart: true });
render(
<ContainerConfigTab
workspaceId="ws-compute"
data={{
runtime: "claude-code",
status: "online",
needsRestart: false,
activeTasks: 0,
maxConcurrentTasks: null,
workspaceAccess: "none",
deliveryMode: "push",
compute: {
instance_type: "t3.large",
volume: { root_gb: 50 },
display: { mode: "none" },
},
}}
/>,
);
fireEvent.change(screen.getByLabelText("Runtime image"), { target: { value: "hermes" } });
fireEvent.change(screen.getByLabelText("Instance type"), { target: { value: "m6i.xlarge" } });
fireEvent.change(screen.getByLabelText("Root volume"), { target: { value: "100" } });
fireEvent.click(screen.getByLabelText("Enable display"));
fireEvent.change(screen.getByLabelText("Resolution"), { target: { value: "2560x1440" } });
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => expect(apiPatch).toHaveBeenCalledTimes(1));
expect(apiPatch).toHaveBeenCalledWith("/workspaces/ws-compute", {
runtime: "hermes",
compute: {
instance_type: "m6i.xlarge",
volume: { root_gb: 100 },
display: { mode: "desktop-control", protocol: "novnc", width: 2560, height: 1440 },
},
});
expect(updateNodeData).toHaveBeenCalledWith("ws-compute", {
runtime: "hermes",
compute: {
instance_type: "m6i.xlarge",
volume: { root_gb: 100 },
display: { mode: "desktop-control", protocol: "novnc", width: 2560, height: 1440 },
},
needsRestart: true,
applyTemplateOnRestart: true,
});
expect(restartWorkspace).not.toHaveBeenCalled();
});
it("preserves existing custom display mode and resolution when saving unrelated compute", async () => {
apiPatch.mockResolvedValueOnce({ needs_restart: true });
render(
<ContainerConfigTab
workspaceId="ws-compute"
data={{
runtime: "claude-code",
status: "online",
needsRestart: false,
activeTasks: 0,
maxConcurrentTasks: null,
workspaceAccess: "none",
deliveryMode: "push",
compute: {
instance_type: "t3.large",
volume: { root_gb: 50 },
display: { mode: "gpu-desktop-control", protocol: "dcv", width: 1600, height: 1000 },
},
}}
/>,
);
expect(screen.getByLabelText("Resolution")).toHaveProperty("value", "1600x1000");
fireEvent.change(screen.getByLabelText("Instance type"), { target: { value: "t3.xlarge" } });
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => expect(apiPatch).toHaveBeenCalledTimes(1));
expect(apiPatch).toHaveBeenCalledWith("/workspaces/ws-compute", {
runtime: "claude-code",
compute: {
instance_type: "t3.xlarge",
volume: { root_gb: 50 },
display: { mode: "gpu-desktop-control", protocol: "dcv", width: 1600, height: 1000 },
},
});
});
it("can save changed compute and restart the workspace to apply it", async () => {
apiPatch.mockResolvedValueOnce({ needs_restart: true });
restartWorkspace.mockResolvedValueOnce(undefined);
render(
<ContainerConfigTab
workspaceId="ws-compute"
data={{
runtime: "claude-code",
status: "online",
needsRestart: false,
activeTasks: 0,
maxConcurrentTasks: null,
workspaceAccess: "none",
deliveryMode: "push",
compute: {
instance_type: "t3.large",
volume: { root_gb: 50 },
display: { mode: "none" },
},
}}
/>,
);
fireEvent.change(screen.getByLabelText("Instance type"), { target: { value: "t3.xlarge" } });
fireEvent.click(screen.getByRole("button", { name: "Save & Restart" }));
await waitFor(() => expect(apiPatch).toHaveBeenCalledTimes(1));
await waitFor(() => expect(restartWorkspace).toHaveBeenCalledWith("ws-compute", { applyTemplate: false }));
});
it("requests template re-apply when saving a runtime change and restarting", async () => {
apiPatch.mockResolvedValueOnce({ needs_restart: true });
restartWorkspace.mockResolvedValueOnce(undefined);
render(
<ContainerConfigTab
workspaceId="ws-compute"
data={{
runtime: "claude-code",
status: "online",
needsRestart: false,
activeTasks: 0,
maxConcurrentTasks: null,
workspaceAccess: "none",
deliveryMode: "push",
compute: {
instance_type: "t3.large",
volume: { root_gb: 50 },
display: { mode: "none" },
},
}}
/>,
);
fireEvent.change(screen.getByLabelText("Runtime image"), { target: { value: "hermes" } });
fireEvent.click(screen.getByRole("button", { name: "Save & Restart" }));
await waitFor(() => expect(restartWorkspace).toHaveBeenCalledWith("ws-compute", { applyTemplate: true }));
});
it("can restart without re-saving when changes are already pending", async () => {
restartWorkspace.mockResolvedValueOnce(undefined);
render(
<ContainerConfigTab
workspaceId="ws-compute"
data={{
runtime: "claude-code",
status: "online",
needsRestart: true,
activeTasks: 0,
maxConcurrentTasks: null,
workspaceAccess: "none",
deliveryMode: "push",
applyTemplateOnRestart: true,
compute: {
instance_type: "t3.large",
volume: { root_gb: 50 },
display: { mode: "none" },
},
}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Restart to apply" }));
await waitFor(() => expect(restartWorkspace).toHaveBeenCalledWith("ws-compute", { applyTemplate: true }));
expect(apiPatch).not.toHaveBeenCalled();
});
});
@@ -290,7 +290,9 @@ describe("DetailsTab — delete workflow", () => {
) as HTMLButtonElement;
fireEvent(confirmBtn, new MouseEvent("click", { bubbles: true }));
await flush();
expect(mockApi.del).toHaveBeenCalledWith("/workspaces/ws-1?confirm=true");
expect(mockApi.del).toHaveBeenCalledWith("/workspaces/ws-1?confirm=true", {
headers: { "X-Confirm-Name": "Test Workspace" },
});
expect(mockRemoveSubtree).toHaveBeenCalledWith("ws-1");
expect(mockSelectNode).toHaveBeenCalledWith(null);
});
@@ -143,46 +143,30 @@ afterEach(() => {
// ── Tests ────────────────────────────────────────────────────────────────────
/**
* Drive the always-show-picker flow to completion: deploy() opens the
* modal, then we click "keys added" to fire the actual POST. Centralised
* here because as of the always-prompt change, every happy-path test
* must click through the modal before asserting on POST.
*/
async function deployThroughPicker<T>(
result: { current: ReturnType<typeof useTemplateDeploy> },
rerender: () => void,
template: Template,
): Promise<void> {
await act(async () => {
await result.current.deploy(template);
});
rerender();
render(<>{result.current.modal}</>);
await act(async () => {
fireEvent.click(screen.getByTestId("modal-keys-added"));
// Let the fire-and-forget executeDeploy resolve.
await Promise.resolve();
await Promise.resolve();
});
}
describe("useTemplateDeploy — happy path", () => {
it("preflight ok → modal opens → keys-added → POST /workspaces → onDeployed fires", async () => {
it("preflight ok with no key requirements → POST /workspaces directly → onDeployed fires", async () => {
const onDeployed = vi.fn();
const { result, rerender } = renderHook(() =>
const { result } = renderHook(() =>
useTemplateDeploy({ onDeployed }),
);
await deployThroughPicker(result, rerender, makeTemplate());
await act(async () => {
await result.current.deploy(makeTemplate({
id: "seo-agent",
name: "SEO Agent",
model: "MiniMax-M2.7",
}));
});
expect(mockCheckDeploySecrets).toHaveBeenCalledTimes(1);
expect(mockApiPost).toHaveBeenCalledWith(
"/workspaces",
expect.objectContaining({
name: "Claude Code",
template: "claude-code-default",
name: "SEO Agent",
template: "seo-agent",
tier: 1,
model: "MiniMax-M2.7",
llm_provider: "minimax",
}),
);
expect(onDeployed).toHaveBeenCalledWith("ws-new");
@@ -192,11 +176,13 @@ describe("useTemplateDeploy — happy path", () => {
it("uses caller-supplied canvasCoords when provided", async () => {
const canvasCoords = vi.fn(() => ({ x: 42, y: 99 }));
const { result, rerender } = renderHook(() =>
const { result } = renderHook(() =>
useTemplateDeploy({ canvasCoords }),
);
await deployThroughPicker(result, rerender, makeTemplate());
await act(async () => {
await result.current.deploy(makeTemplate());
});
expect(canvasCoords).toHaveBeenCalledTimes(1);
expect(mockApiPost).toHaveBeenCalledWith(
@@ -206,9 +192,11 @@ describe("useTemplateDeploy — happy path", () => {
});
it("falls back to random coords inside [100,500] × [100,400] when canvasCoords omitted", async () => {
const { result, rerender } = renderHook(() => useTemplateDeploy());
const { result } = renderHook(() => useTemplateDeploy());
await deployThroughPicker(result, rerender, makeTemplate());
await act(async () => {
await result.current.deploy(makeTemplate());
});
const body = (mockApiPost as Mock).mock.calls[0]?.[1] as {
canvas: { x: number; y: number };
@@ -458,16 +446,9 @@ describe("useTemplateDeploy — multi-provider always-ask flow", () => {
);
});
it("single-provider template ALSO opens picker when preflight.ok (always-prompt rule)", async () => {
// Default preflight mock: ok=true, providers=[]. claude-code is
// single-provider, but the always-prompt rule means the user must
// still click through the picker to confirm provider+model — even
// when keys are saved and the runtime has only one provider option.
// Reason: the user needs an explicit chance to override the
// template's default model (e.g. opus vs sonnet vs haiku) before
// an EC2 boots and burns billing on the wrong tier.
it("template with no provider requirements deploys directly on platform-managed defaults", async () => {
const onDeployed = vi.fn();
const { result, rerender } = renderHook(() =>
const { result } = renderHook(() =>
useTemplateDeploy({ onDeployed }),
);
@@ -475,13 +456,18 @@ describe("useTemplateDeploy — multi-provider always-ask flow", () => {
await result.current.deploy(makeTemplate());
});
rerender();
render(<>{result.current.modal}</>);
expect(screen.getByTestId("missing-keys-modal")).toBeTruthy();
// POST does NOT fire until the user confirms in the picker.
expect(mockApiPost).not.toHaveBeenCalled();
expect(onDeployed).not.toHaveBeenCalled();
expect(screen.queryByTestId("missing-keys-modal")).toBeNull();
expect(mockApiPost).toHaveBeenCalledWith(
"/workspaces",
expect.objectContaining({
template: "claude-code-default",
model: "claude-sonnet-4-5",
llm_provider: "anthropic",
}),
);
expect(onDeployed).toHaveBeenCalledWith("ws-new");
expect(result.current.deploying).toBeNull();
});
@@ -519,11 +505,13 @@ describe("useTemplateDeploy — POST failure", () => {
it("POST rejection sets error and clears deploying", async () => {
mockApiPost.mockRejectedValueOnce(new Error("server 500"));
const onDeployed = vi.fn();
const { result, rerender } = renderHook(() =>
const { result } = renderHook(() =>
useTemplateDeploy({ onDeployed }),
);
await deployThroughPicker(result, rerender, makeTemplate());
await act(async () => {
await result.current.deploy(makeTemplate());
});
expect(result.current.error).toBe("server 500");
expect(result.current.deploying).toBeNull();
@@ -532,9 +520,11 @@ describe("useTemplateDeploy — POST failure", () => {
it("non-Error rejection still surfaces a message (defensive)", async () => {
mockApiPost.mockRejectedValueOnce("plain string");
const { result, rerender } = renderHook(() => useTemplateDeploy());
const { result } = renderHook(() => useTemplateDeploy());
await deployThroughPicker(result, rerender, makeTemplate());
await act(async () => {
await result.current.deploy(makeTemplate());
});
expect(result.current.error).toBe("Deploy failed");
expect(result.current.deploying).toBeNull();
+30 -4
View File
@@ -55,6 +55,22 @@ interface MissingKeysInfo {
preflight: PreflightResult;
}
function nativeProviderForClaudeCodeModel(model: string): string | undefined {
const trimmed = model.trim();
const lower = trimmed.toLowerCase();
if (!trimmed) return undefined;
if (lower.startsWith("minimax")) return "minimax";
if (lower.startsWith("kimi")) return "kimi-coding";
if (lower.startsWith("claude")) return "anthropic";
if (/^(sonnet|opus|haiku)$/.test(lower)) return "anthropic-oauth";
return undefined;
}
function isNativeClaudeCodeRuntime(template: Template): boolean {
const runtime = template.runtime ?? resolveRuntime(template.id);
return runtime === "claude-code";
}
export interface UseTemplateDeployResult {
/** Template id currently being deployed (incl. the preflight
* network call), or null when idle. Callers pass this to disable
@@ -97,6 +113,10 @@ export function useTemplateDeploy(
setDeploying(template.id);
setError(null);
try {
const selectedModel = model?.trim() || template.model?.trim();
const nativeProvider = isNativeClaudeCodeRuntime(template) && selectedModel
? nativeProviderForClaudeCodeModel(selectedModel)
: undefined;
const coords = canvasCoords
? canvasCoords()
: {
@@ -108,7 +128,8 @@ export function useTemplateDeploy(
template: template.id,
tier: isSaaSTenant() ? 4 : template.tier,
canvas: coords,
...(model ? { model } : {}),
...(selectedModel ? { model: selectedModel } : {}),
...(nativeProvider ? { llm_provider: nativeProvider } : {}),
});
onDeployed?.(ws.id);
} catch (e) {
@@ -144,8 +165,13 @@ export function useTemplateDeploy(
setDeploying(null);
return;
}
// Always open the picker — every deploy goes through an
// explicit confirm-provider/model step. Reasons:
if (preflight.ok && preflight.providers.length === 0) {
await executeDeploy(template);
return;
}
// Open the picker whenever a template declares provider/key choices.
// Templates with no provider requirements deploy directly on the
// platform-managed default above. Reasons to keep the picker here:
// 1. Multi-provider templates (e.g. hermes) need a per-
// workspace pick or the adapter falls back to its
// compiled-in default and 500s with "No LLM provider
@@ -164,7 +190,7 @@ export function useTemplateDeploy(
setMissingKeysInfo({ template, preflight });
setDeploying(null);
},
[],
[executeDeploy],
);
// No useCallback here — consumers call this on every render anyway
@@ -32,8 +32,8 @@ const hermesModels: ModelSpec[] = [
const HERMES: TemplateLike = { runtime: "hermes", models: hermesModels };
const LANGGRAPH: TemplateLike = {
runtime: "langgraph",
const CLAUDE_CODE: TemplateLike = {
runtime: "claude-code",
required_env: ["OPENAI_API_KEY"],
};
@@ -69,7 +69,7 @@ describe("providersFromTemplate", () => {
});
it("falls back to top-level required_env when no models[] are declared", () => {
const providers = providersFromTemplate(LANGGRAPH);
const providers = providersFromTemplate(CLAUDE_CODE);
expect(providers).toHaveLength(1);
expect(providers[0].envVars).toEqual(["OPENAI_API_KEY"]);
});
@@ -151,10 +151,10 @@ describe("checkDeploySecrets", () => {
]),
} as Response);
const result = await checkDeploySecrets(LANGGRAPH);
const result = await checkDeploySecrets(CLAUDE_CODE);
expect(result.ok).toBe(true);
expect(result.missingKeys).toEqual([]);
expect(result.runtime).toBe("langgraph");
expect(result.runtime).toBe("claude-code");
});
it("returns ok=true on a multi-provider template when ANY provider is configured", async () => {
@@ -195,7 +195,7 @@ describe("checkDeploySecrets", () => {
]),
} as Response);
const result = await checkDeploySecrets(LANGGRAPH);
const result = await checkDeploySecrets(CLAUDE_CODE);
expect(result.ok).toBe(false);
expect(result.missingKeys).toEqual(["OPENAI_API_KEY"]);
});
@@ -216,7 +216,7 @@ describe("checkDeploySecrets", () => {
]),
} as Response);
await checkDeploySecrets(LANGGRAPH, "ws-123");
await checkDeploySecrets(CLAUDE_CODE, "ws-123");
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining("/workspaces/ws-123/secrets"),
expect.any(Object),
@@ -229,7 +229,7 @@ describe("checkDeploySecrets", () => {
json: () => Promise.resolve([]),
} as Response);
await checkDeploySecrets(LANGGRAPH);
await checkDeploySecrets(CLAUDE_CODE);
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining("/settings/secrets"),
expect.any(Object),
@@ -241,7 +241,7 @@ describe("checkDeploySecrets", () => {
new Error("Network error"),
);
const result = await checkDeploySecrets(LANGGRAPH);
const result = await checkDeploySecrets(CLAUDE_CODE);
expect(result.ok).toBe(false);
expect(result.missingKeys).toEqual(["OPENAI_API_KEY"]);
// Empty Set on fetch failure — useTemplateDeploy relies on this
@@ -28,8 +28,8 @@ describe("isExternalLikeRuntime", () => {
"docker",
"local",
"agent",
"crewai",
"langgraph",
"legacy-runtime",
"codex",
"openclaw",
"custom-runtime",
])("%q returns false", (runtime) => {
@@ -68,8 +68,7 @@ describe("provisionTimeoutForRuntime", () => {
});
it("returns 120_000 for any unknown runtime", () => {
expect(provisionTimeoutForRuntime("langgraph")).toBe(120_000);
expect(provisionTimeoutForRuntime("crewai")).toBe(120_000);
expect(provisionTimeoutForRuntime("legacy-runtime")).toBe(120_000);
expect(provisionTimeoutForRuntime("some-new-runtime")).toBe(120_000);
});
@@ -77,7 +76,7 @@ describe("provisionTimeoutForRuntime", () => {
const cases: Array<[string | undefined, { provisionTimeoutMs?: number } | undefined]> = [
[undefined, undefined],
["claude-code", undefined],
["langgraph", { provisionTimeoutMs: 500_000 }],
["claude-code", { provisionTimeoutMs: 500_000 }],
[undefined, { provisionTimeoutMs: 45_000 }],
];
for (const [runtime, overrides] of cases) {
+2
View File
@@ -23,6 +23,7 @@ const DEFAULT_TIMEOUT_MS = 35_000;
export interface RequestOptions {
timeoutMs?: number;
headers?: Record<string, string>;
}
/**
@@ -76,6 +77,7 @@ async function request<T>(
const headers: Record<string, string> = {
"Content-Type": "application/json",
...platformAuthHeaders(),
...(options?.headers ?? {}),
};
// Re-read slug locally for the 401 handler below — `headers` already
// has it, but the 401 branch needs the bare value to gate the
+1 -1
View File
@@ -44,7 +44,7 @@ export const plans: Plan[] = [
price: "$0",
features: [
"3 workspaces",
"Claude Code, LangGraph, OpenClaw runtimes",
"Claude Code, Codex, Hermes, OpenClaw runtimes",
"Shared Redis + bounded storage",
"Community support",
],
+1
View File
@@ -528,6 +528,7 @@ export function buildNodesAndEdges(
// A2A delivery mode (task #227). Absent on older ws-server builds
// — leave undefined so the chat UI's "?? 'push'" fallback applies.
deliveryMode: ws.delivery_mode,
compute: ws.compute,
},
};
if (hasParent) {
+18 -6
View File
@@ -7,7 +7,7 @@ import {
} from "@xyflow/react";
import { api } from "@/lib/api";
import { showToast } from "@/components/Toaster";
import type { WorkspaceData, WSMessage } from "./socket";
import type { WorkspaceCompute, WorkspaceData, WSMessage } from "./socket";
import { handleCanvasEvent } from "./canvas-events";
import { markDeleted, wasRecentlyDeleted } from "./deleteTombstones";
import {
@@ -130,6 +130,14 @@ export interface WorkspaceNodeData extends Record<string, unknown> {
* builds — that fallthrough is treated as "push" to match
* ws-server's `lookupDeliveryMode` default. */
deliveryMode?: string;
/** Desired EC2/container shape persisted in workspaces.compute. Applied
* at next restart/reprovision, and used to determine Display tab
* availability. */
compute?: WorkspaceCompute;
/** Runtime image changed through Container Config; next restart must
* re-apply the runtime-default template instead of reusing the old
* config volume. UI-only, cleared after restart. */
applyTemplateOnRestart?: boolean;
}
export type PanelTab = "details" | "skills" | "chat" | "terminal" | "display" | "container-config" | "config" | "schedule" | "channels" | "files" | "memory" | "traces" | "events" | "activity" | "audit";
@@ -168,7 +176,7 @@ interface CanvasState {
setPanelTab: (tab: PanelTab) => void;
getSelectedNode: () => Node<WorkspaceNodeData> | null;
updateNodeData: (id: string, data: Partial<WorkspaceNodeData>) => void;
restartWorkspace: (id: string) => Promise<void>;
restartWorkspace: (id: string, options?: { applyTemplate?: boolean }) => Promise<void>;
removeNode: (id: string) => void;
/** Remove a node AND every descendant in one atomic update. Mirrors
* the server-side cascade — `DELETE /workspaces/:id?confirm=true`
@@ -329,8 +337,11 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
},
batchDelete: async () => {
const ids = Array.from(get().selectedNodeIds);
const names = new Map(get().nodes.map((node) => [node.id, node.data.name]));
const results = await Promise.allSettled(
ids.map((id) => api.del(`/workspaces/${id}`))
ids.map((id) => api.del(`/workspaces/${id}`, {
headers: { "X-Confirm-Name": names.get(id) ?? "" },
}))
);
const failed: string[] = [];
results.forEach((r, i) => {
@@ -821,9 +832,10 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
});
},
restartWorkspace: async (id) => {
await api.post(`/workspaces/${id}/restart`);
get().updateNodeData(id, { needsRestart: false });
restartWorkspace: async (id, options) => {
const body = options?.applyTemplate ? { apply_template: true } : undefined;
await api.post(`/workspaces/${id}/restart`, body);
get().updateNodeData(id, { needsRestart: false, applyTemplateOnRestart: false });
},
removeNode: (id) => {
+14
View File
@@ -354,6 +354,20 @@ export interface WorkspaceData {
* collapsing the spinner the moment the synchronous queued-200 returns
* (task #227 — external/MCP workspaces had no progress UX). */
delivery_mode?: string;
compute?: WorkspaceCompute;
}
export interface WorkspaceCompute {
instance_type?: string;
volume?: {
root_gb?: number;
};
display?: {
mode?: string;
protocol?: string;
width?: number;
height?: number;
};
}
let socket: ReconnectingSocket | null = null;
+1 -1
View File
@@ -26,7 +26,7 @@ Full contract: `docs/runbooks/admin-auth.md`.
|--------|------|---------|
| GET | /health | inline |
| GET | /metrics | metrics.Handler() — Prometheus text format; no auth, scrape-safe |
| POST/GET/PATCH/DELETE | /workspaces[/:id] | workspace.go — `GET /workspaces`, `POST /workspaces`, and `DELETE /workspaces/:id` require `AdminAuth`. `PATCH /workspaces/:id` enforces field-level authz: cosmetic fields (name, role, x, y, canvas) pass through; sensitive fields (tier, parent_id, runtime, workspace_dir) require a valid bearer token when any live token exists. |
| POST/GET/PATCH/DELETE | /workspaces[/:id] | workspace.go — `GET /workspaces`, `POST /workspaces`, and `DELETE /workspaces/:id` require `AdminAuth`. `DELETE /workspaces/:id` also requires `X-Confirm-Name: <workspace name>`; cascading deletes still require `?confirm=true`. `PATCH /workspaces/:id` enforces field-level authz: cosmetic fields (name, role, x, y, canvas) pass through; sensitive fields (tier, parent_id, runtime, workspace_dir) require a valid bearer token when any live token exists. |
| GET/PATCH | /workspaces/:id/config | workspace.go |
| GET/POST | /workspaces/:id/memory | workspace.go |
| DELETE | /workspaces/:id/memory/:key | workspace.go |
+2
View File
@@ -6,6 +6,8 @@ Molecule AI's memory model is built around one principle:
That is the purpose of **HMA: Hierarchical Memory Architecture**.
The organizational boundary is enforced **physically**, not at the application layer: each org runs as its own tenant on its own EC2, with its own memory plugin sidecar and its own Postgres. Memory writes are loopback-only — never cross-tenant. See [`workspace-placement.md`](workspace-placement.md) for the architecture contract that makes HMA tenant-isolated by construction.
## The Three Scopes
| Scope | Meaning | Intended use |
@@ -84,6 +84,8 @@ Six runtime adapters ship production-ready on `main`: LangGraph, DeepAgents, Cla
## 3. System Architecture
> **Workspace placement contract:** every Molecule org runs as a fully isolated tenant on its own EC2, with workspace-server, memory plugin, Postgres, and Redis all co-located. The platform (controlplane on Railway) handles provisioning, billing, and DNS only — it never holds tenant data. See [`workspace-placement.md`](workspace-placement.md) for the formal RFC.
### System Boundary Diagram
```
+194
View File
@@ -0,0 +1,194 @@
# Workspace placement — org-per-EC2 architecture
Status: Accepted (implicit since 2026-05; formalized 2026-05-24)
Owners: hongming (CTO), cui (CEO)
Tracking: #1793
This RFC formalizes the architecture decision that has been implicit in the system since the post-suspension rebuild: **each Molecule AI org is one isolated tenant on its own EC2 instance**, with every functional surface (workspace-server, memory plugin, Postgres, Redis, canvas) co-located on that instance. The platform's role is provisioning, billing, and the cross-tenant control plane — never the data path.
The implementation already follows this pattern in every direction we look (provisioner, memory v2 cutover, tenant entrypoint, controlplane user-data, even the OSS deploy story). Writing it down so it stays that way.
## TL;DR
```
┌──────────────────────────────────┐
│ Platform (controlplane) │
│ Railway-hosted │
│ api.moleculesai.app │
│ │
│ - org provisioning │
│ - billing + Stripe integration │
│ - DNS + tunnel orchestration │
│ - auth / org-token issuance │
│ - fleet redeploy orchestration │
│ │
│ NEVER holds tenant data │
└──────────────────────────────────┘
│ │
provision │ │ provision
+ billing │ │ + billing
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Tenant: agents-team │ │ Tenant: <other-org> │
│ Own EC2 (us-east-2) │ │ Own EC2 (us-east-2) │
│ agents-team.molecule.. │ │ <slug>.moleculesai.app │
│ │ │ │
│ ┌───────────────────┐ │ │ ┌───────────────────┐ │
│ │ molecule-tenant │ │ │ │ molecule-tenant │ │
│ │ (workspace-server │ │ │ │ (workspace-server │ │
│ │ + canvas + go) │ │ │ │ + canvas + go) │ │
│ └───────────────────┘ │ │ └───────────────────┘ │
│ ┌───────────────────┐ │ │ ┌───────────────────┐ │
│ │ memory-plugin │ │ │ │ memory-plugin │ │
│ │ (loopback :9100) │ │ │ │ (loopback :9100) │ │
│ └───────────────────┘ │ │ └───────────────────┘ │
│ ┌───────────────────┐ │ │ ┌───────────────────┐ │
│ │ postgres pgvector │ │ │ │ postgres pgvector │ │
│ │ (172.17.0.1:5432) │ │ │ │ (172.17.0.1:5432) │ │
│ └───────────────────┘ │ │ └───────────────────┘ │
│ ┌───────────────────┐ │ │ ┌───────────────────┐ │
│ │ redis │ │ │ │ redis │ │
│ └───────────────────┘ │ │ └───────────────────┘ │
│ ┌───────────────────┐ │ │ ┌───────────────────┐ │
│ │ workspace runtime │ │ │ │ workspace runtime │ │
│ │ containers (ws-*) │ │ │ │ containers (ws-*) │ │
│ └───────────────────┘ │ │ └───────────────────┘ │
└─────────────────────────┘ └─────────────────────────┘
```
Every tenant is a self-contained molecule-core instance. The platform is a thin coordinator above them.
## What crosses the platform/tenant boundary
What the platform sends down to the tenant:
- Initial EC2 provisioning (user-data script via SSM) — see `molecule-controlplane/internal/provisioner/ec2.go`
- Per-tenant secrets (DB password, `SECRETS_ENCRYPTION_KEY`, `MOLECULE_CP_SHARED_SECRET`) injected as env at boot
- Image redeploys via `POST /cp/admin/tenants/:slug/redeploy` → SSM → `docker pull && docker stop && docker run`
- DNS records (Cloudflare) and tunnel registration (cloudflared)
- Billing-state changes (subscription status, plan upgrades)
What the tenant sends up to the platform:
- Boot-stage telemetry (`report_stage` calls during EC2 user-data execution)
- LLM usage events (for billing aggregation; documented in `controlplane/migrations/037_llm_usage_billing.up.sql`)
- Workspace lifecycle events for cross-tenant analytics — read-only, no remote control implied
What does NOT cross the boundary:
- Memory contents (HMA scopes, agent_memories before A3, memory_plugin records after)
- Workspace state, files, canvas layouts
- Workspace runtime container state
- Per-org user authentication state (tenant issues its own session tokens via `wsauth`)
If a feature design wants to put any of those on the platform side, that's a violation of this RFC and needs explicit justification.
## SSOT rationale
The single-source-of-truth boundary is **the tenant EC2**.
This decision was the implicit basis for the memory v1→v2 migration that ran 2026-05-24 (issues #1747#1791#1792). The v2 memory plugin runs as a sidecar on each tenant EC2, sharing the tenant's Postgres under a dedicated `memory_plugin` schema. There is no platform-side memory aggregation, no central index, no cross-tenant memory federation. Memory writes are loopback-only (workspace-server → memory-plugin on `127.0.0.1:9100`).
Why this is correct:
1. **Organizational isolation is the product.** A tenant's memory, workspaces, secrets, and conversation history must not be readable by another org, ever. The simplest enforcement is physical: different EC2, different DB, different network. Application-level multi-tenancy adds a class of cross-tenant data leak bugs that can't happen here.
2. **The platform must remain horizontally scalable independent of tenant data volume.** If memory aggregation lived on the platform, billing/provisioning/auth would scale with the volume of memory across all tenants. With per-tenant storage, the platform's scaling envelope depends only on the number of orgs.
3. **OSS-deployability requires it.** molecule-core is open-source; anyone can deploy it. If functional state lived on a centralized platform, OSS deployers would either have to run their own platform (high barrier) or call ours (privacy concern + scale concern). Per-tenant SSOT means the OSS molecule-core instance is functionally complete — it just talks to a platform for billing.
## OSS-deployment shape
A workspace inside any tenant reaches its parent tenant by injecting two env vars at container start:
- `MOLECULE_ORG_ID` — the UUID of the org this workspace belongs to
- `MOLECULE_PLATFORM_URL` — the tenant's HTTPS URL (e.g. `https://agents-team.moleculesai.app`)
These are baked into the workspace runtime's docker run by the workspace-server when it provisions a workspace. The workspace's agent runtime uses them to:
- Register itself in the tenant's `workspaces` table
- Send heartbeats (Redis TTL key on the tenant)
- Subscribe to A2A messages via the tenant's WebSocket hub
- Commit memories via the tenant's MCP bridge or HTTP `/memories` endpoints
An OSS deployer running their own molecule-core instance gets the same shape: their workspaces inject the deployer's tenant URL and org ID. The agent runtime is **agnostic** to whether it's talking to our hosted platform or a self-hosted one.
The only thing tying a tenant to **our** platform is the billing/auth path:
- `MOLECULE_CP_URL` env on the tenant container points at `api.moleculesai.app`
- `MOLECULE_CP_SHARED_SECRET` env authenticates the tenant→platform direction
- LLM usage events POST to `cp_url/cp/llm-usage-events` for billing aggregation
An OSS deployer can leave `MOLECULE_CP_URL` unset (or point at their own platform). The workspace-server's `wiring.go` and `cp_provisioner.go` already handle the absent-CP case gracefully — the tenant is fully functional without it.
## Scaling envelope
Per-tenant resource shape (current):
| Layer | Sizing |
|---|---|
| EC2 | t3.medium (2 vCPU, 4 GiB) for default-tier orgs |
| Postgres | Single container, pgvector pre-installed, ~1-10 GiB per org expected |
| Memory plugin | Loopback only, ~50 MB resident, scales with memory record count |
| Workspace runtime containers (ws-\*) | One per workspace; sized by template tier |
The platform's scaling envelope:
| Layer | Sizing |
|---|---|
| controlplane | Single Railway service, scales horizontally |
| Postgres | One Railway-hosted Postgres for billing + org registry + auth tokens |
| DNS | Cloudflare zone with one CNAME per tenant |
| Tunnels | One Cloudflare tunnel per tenant |
Order-of-magnitude:
- 100 orgs: trivial (100 EC2s, controlplane unchanged)
- 10K orgs: needs an EC2 placement strategy (region pinning, dedicated-tier hosts), but the platform is still a single service
- 1M orgs: this design starts to strain — Cloudflare tunnel-per-tenant becomes expensive, EC2-per-tenant becomes resource-wasteful, and we'd want a denser tenant-on-shared-infra mode
The current architecture is sized for the 10010K range. The 1M-org variant is explicitly out of scope for this RFC.
## Decision points for new feature design
When proposing a new feature, the design must answer "where does the data live?" Pick one:
1. **On the tenant.** Default choice for anything functional. Tenant DB, tenant memory plugin, tenant filesystem. The feature ships in `molecule-core` and is deployed via the tenant image.
2. **On the platform.** ONLY for billing, cross-org analytics (anonymized), org registry, auth tokens, DNS/tunnel state. The feature ships in `molecule-controlplane`.
3. **Both, with one as SSOT.** Rare. The tenant is the SSOT; the platform may cache for cross-tenant queries but must be willing to re-read from the tenant on miss. Document the cache invalidation contract.
When in doubt, default to #1. If you find yourself wanting to put HMA memory, workspace state, or session history on the platform, stop — you're re-introducing the SSOT violation the v1→v2 memory migration was designed to remove.
## Migration path for non-conforming code
The implementation already conforms. There is no migration backlog as of 2026-05-24:
- Memory: v1→v2 migration complete (#1747#1791#1792). v2 plugin per-tenant is SSOT.
- Workspace state: always per-tenant (the `workspaces` table lives in the tenant Postgres).
- Activity logs: per-tenant `activity_logs` table.
- Files: per-tenant (Docker volumes attached to ws-\* containers).
- Secrets: per-tenant (`workspace_secrets` + `global_secrets` tables in tenant DB).
- LLM usage events: tenant emits, platform aggregates for billing — correct shape.
If a future PR proposes platform-side aggregation of something functional, link this RFC in the review.
## What this RFC does NOT cover
Out of scope for this document; tracked separately if needed:
- **Multi-region tenant placement** — current design is single-region (us-east-2). Multi-region needs its own RFC because it changes the EC2 placement contract.
- **BYO-compute / customer-managed VPC** — adjacent design; the org-per-EC2 boundary holds but the EC2 ownership shifts to the customer.
- **Workspace runtime selection** — separately documented in `docs/architecture/workspace-tiers.md`.
- **Tenant image upgrade strategy** — separately documented in `docs/architecture/tenant-image-upgrades.md`.
- **OSS billing alternatives** — how OSS deployers handle billing without our controlplane is a separate go-to-market decision.
## References
- `docs/architecture/memory.md` — HMA scopes + v2 plugin
- `docs/architecture/saas-prod-migration-2026-04-19.md` — provisioning pipeline reference
- `docs/architecture/molecule-technical-doc.md` §3 (System Architecture) — top-level picture
- `molecule-controlplane/internal/provisioner/ec2.go` — the canonical user-data + docker run for tenants
- `workspace-server/entrypoint-tenant.sh` — the canonical tenant boot script
- Memory system migration: #1747 (kill v1 fallback), #1791 (Phase A2 backfill), #1792 (Phase A3 drop table)
+2 -2
View File
@@ -19,8 +19,8 @@ import (
"context"
"testing"
mclient "github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/client"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/contract"
mclient "git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/memory/client"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/memory/contract"
)
func TestMyPlugin_FullRoundTrip(t *testing.T) {
+104
View File
@@ -0,0 +1,104 @@
# local-e2e — session-continuity canary harness
Self-contained Docker-Compose harness that gates RFC#600-class template
changes (session continuity, file-only messages, multimodal prompts,
cross-session memory) **before** they reach customer canary.
Per CTO standing directive "fully tested + separate CI": this is a
dedicated, *fast* (target <3 min), *small-surface* harness that uses a
Python tenant-CP simulator (not the full `workspace-server` Go service)
to exercise the runtime image end-to-end against canonical canary turns.
See [`feedback_no_single_source_of_truth`] — the harness IS the canonical
session-continuity validator. Per-runtime unit tests still cover their
own guard logic; the harness covers the live conversational behaviour
that those unit tests cannot prove.
See [`feedback_image_promote_is_not_user_live`] — every assertion reads
state back from the *running container*, never from a publish-pipeline
ack.
## What it tests (the 4 canaries)
| # | Scenario | Asserts |
|---|----------|---------|
| 1 | 2-turn name canary | turn 2 reply contains "Hongming" → SessionStore continuity |
| 2 | File-only message (no caption) | NOT "(empty prompt — nothing to do)" + reply references filename or asks for clarification |
| 3 | File + caption ("summarize this") | reply addresses attachment + caption |
| 4 | Cross-session memory recall | new session pulls "blue" via memory tool |
Each scenario re-uses the same A2A wire-shape that the production
`workspace-server` POSTs to runtime `:8000` (canvas-thread-id semantics
via `context_id`).
## Architecture
```
local-e2e/
docker-compose.yml # runtime under test + cp_sim
cp_sim/ # ≈300 LoC Python A2A poster + file uploader
cp_sim.py
Dockerfile
requirements.txt
canary/
conftest.py
test_session_continuity.py # 4 canary scenarios
test_layer_diagnostics.py # SessionStore state probe + key derivation
scripts/
run-canary.sh # one-shot orchestration entrypoint
```
The CP simulator emits the **exact** JSON-RPC `message/send` envelope
that `workspace-server` produces (verified against
`tests/e2e/test_chat_attachments_e2e.sh`). No Go service is in the loop —
this keeps the harness lean per the CTO directive.
## Run locally
```bash
# from molecule-core repo root:
export TEMPLATE_IMAGE=ghcr.io/molecule-ai/workspace-template-hermes:latest
./local-e2e/scripts/run-canary.sh
```
Exit code 0 = all 4 canaries pass. Non-zero = at least one canary failed
and the harness dumped SessionStore state + last 200 log lines from the
runtime container into `./local-e2e/artifacts/`.
## How it integrates into CI
Each template repo's `.gitea/workflows/session-continuity-e2e.yml` calls
`run-canary.sh` with its own freshly-built `TEMPLATE_IMAGE`. The
template repo's Gitea branch-protection lists
`session-continuity-e2e (pull_request)` as a required context.
Rollout order (deliberate — per `feedback_image_promote_is_not_user_live`
we bake before we cascade):
1. `molecule-ai-workspace-template-hermes` — highest-traffic + most
recent RFC#600-class fixes — REQUIRED gate
2. Bake for 5 business days
3. Cascade to claude-code, langgraph, autogen, openclaw, smolagents,
google-adk (one PR per template — see `scripts/onboard-template.sh`)
## Future extensions (out of scope for the initial PR)
- Multi-session memory consistency (3+ sessions deep)
- Tool-use canary (workspace seeded with skills/, agent must invoke)
- Streaming-cancellation canary (mid-stream client disconnect)
- Cross-runtime A2A peer call (currently covered by `e2e-peer-visibility`)
## Why a thin Python simulator and not the real `workspace-server`?
`workspace-server` is a 60+ MB Go binary that requires Postgres, Redis,
admin-token wiring, registry plumbing, and a 30+ second cold-boot. None
of that touches session-continuity behaviour, which is fully owned by
the runtime container's `a2a_executor.py`. Per CTO directive "separate
CI as possible" + the <3 min target, we excise the platform-tenant Go
service from the loop and emit identical wire-shape envelopes from a
single Python file.
If the simulator diverges from `workspace-server` wire shape, the gate
goes red — fix the simulator to match production. The wire shape is
asserted in `tests/e2e/test_chat_attachments_e2e.sh` and the runtime's
`workspace/a2a_executor.py:_core_execute`.
+19
View File
@@ -0,0 +1,19 @@
# Python tenant-CP simulator + canary test driver.
# Single image — pytest + httpx + the canary tests baked in.
FROM python:3.11-slim@sha256:e78299e55776ca065dcb769f80161f48465ad352014240eb5fe4712e22505e9b
WORKDIR /harness
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Test files are bind-mounted by docker-compose at run time so a `pytest -x`
# rerun loop doesn't require a rebuild. The COPY here is for the
# self-contained image used by Gitea Actions (where bind mounts are awkward).
COPY cp_sim.py /harness/cp_sim.py
COPY canary /harness/canary
ENV PYTHONUNBUFFERED=1
# Default: run the 4 canaries with verbose output + JUnit XML for CI.
CMD ["pytest", "-v", "--tb=short", "--junitxml=/harness/artifacts/junit.xml", "canary/"]
View File
+31
View File
@@ -0,0 +1,31 @@
"""Shared pytest fixtures for the canary suite."""
from __future__ import annotations
import os
import sys
import uuid
# cp_sim.py lives one dir up — make it importable without packaging.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import pytest # noqa: E402
from cp_sim import CPSim, CPSimConfig # noqa: E402
@pytest.fixture
def sim() -> CPSim:
"""Fresh CPSim per test — cheap, isolates connection state."""
return CPSim(
cfg=CPSimConfig(
runtime_url=os.environ.get("RUNTIME_URL", "http://localhost:18000"),
)
)
@pytest.fixture
def context_id() -> str:
"""A unique canvas-thread-id per test — guarantees SessionStore isolation
between scenarios so a failing canary doesn't poison the next one."""
return f"canary-ctx-{uuid.uuid4().hex[:12]}"
@@ -0,0 +1,80 @@
"""Layer-isolation diagnostics — runs alongside the 4 canaries.
These probes are not strict pass/fail gates by themselves; they exist so
when a canary fails, the artifacts include enough state to tell whether
the regression is in the wire-shape layer, the SessionStore layer, or
the memory layer. Each test always passes (returns early) when the
underlying surface is unavailable on the runtime under test — different
templates expose different debug endpoints.
Cross-refs:
- feedback_verify_actual_endstate_not_ack_follow_sop — we read state
back, not the side-effect ack.
- feedback_image_promote_is_not_user_live — the verification is at
the running-container layer.
"""
from __future__ import annotations
import os
import uuid
import httpx
from cp_sim import CPSim
def test_diag_agent_card_advertises_a2a(sim: CPSim) -> None:
"""The runtime's /agent-card must advertise A2A capabilities.
If this fails, the canaries' transport assumption (POST /a2a) is
already broken — diagnose the runtime image, not the canary.
"""
url = f"{sim.cfg.runtime_url}/agent-card"
r = httpx.get(url, timeout=10.0)
assert r.status_code == 200, (
f"/agent-card returned {r.status_code}: {r.text[:300]!r}"
)
body = r.json()
# AgentCard spec: capabilities object must exist, even if empty.
assert isinstance(body, dict), f"/agent-card body not an object: {body!r}"
# We don't require any specific capability flag — different templates
# advertise different sets. The point of this diag is "is the card
# there at all", which signals the runtime booted past entrypoint.
def test_diag_context_id_required_for_continuity(sim: CPSim) -> None:
"""Same context_id in two turns must not crash the runtime.
Pure smoke probe — proves the executor accepts a continuation
message without 5xx-ing. The substantive assertion is canary 1; this
one just guarantees the path is reachable.
"""
ctx = f"diag-{uuid.uuid4().hex[:8]}"
r1 = sim.send_text("ping", context_id=ctx)
r2 = sim.send_text("ping again", context_id=ctx, task_id=r1.get("result", {}).get("id"))
# Both replies must parse — non-empty envelope, no JSON-RPC error.
for label, env in (("turn1", r1), ("turn2", r2)):
assert "error" not in env, f"{label} returned JSON-RPC error: {env['error']}"
def test_diag_memory_root_writable_in_canary_mode(sim: CPSim) -> None:
"""When MOLECULE_CANARY_MODE=1, the memory root must accept writes.
Probes via the recall_memory MCP tool — if /mcp is not exposed,
returns early (skip-style; we still pass because some templates
proxy MCP elsewhere).
"""
# We can't write directly here — only confirm the read path doesn't
# 500 on a missing key. A real write happens in canary 4.
key = f"canary-probe-{uuid.uuid4().hex[:8]}"
try:
val = sim.probe_memory(key)
except Exception as e:
# /mcp may not be exposed on this template — canary 4 will
# surface the real defect if memory is actually broken.
if os.environ.get("CANARY_STRICT_MCP") == "1":
raise
return
# Unknown key → None is fine. The point is the call didn't crash.
assert val is None or isinstance(val, str)
@@ -0,0 +1,204 @@
"""The 4 canonical session-continuity canaries (task #342, RFC#600 class).
These tests speak A2A directly to the runtime under test. They are the
authoritative gate that the runtime preserves conversation continuity,
handles file-only messages without dropping to the empty-prompt error,
addresses multimodal prompts, and persists memory across sessions.
Wire-shape source of truth: see ../cp_sim.py docstring.
"""
from __future__ import annotations
import re
import uuid
from cp_sim import CPSim
# ---------- canary 1: 2-turn name continuity -------------------------------
def test_canary_1_two_turn_name_continuity(sim: CPSim, context_id: str) -> None:
"""SessionStore continuity — turn 2 must recall the name from turn 1.
Empirically tests:
- ``a2a_executor._core_execute`` injects prior-turn history via
``_extract_history(context)`` (workspace/a2a_executor.py:313).
- The runtime's session store is keyed on ``context_id`` (canvas
thread id) NOT ``task_id`` — task_id is per-turn, context_id is
per-conversation. Regressions to that key derivation were the
root cause of the 2026-05 multi-turn-amnesia incidents
(#a60623344 diagnosis).
"""
# Turn 1 — establish the fact.
r1 = sim.send_text(
"Hi, my name is Hongming.",
context_id=context_id,
)
reply1 = sim.extract_text_parts(r1)
assert reply1, f"Turn 1 produced empty reply. envelope={r1!r}"
# Turn 2 — ask back. Same context_id → same SessionStore key.
r2 = sim.send_text(
"What's my name?",
context_id=context_id,
)
reply2 = sim.extract_text_parts(r2)
assert reply2, f"Turn 2 produced empty reply. envelope={r2!r}"
# Substring match, case-insensitive — agents may reply
# "Your name is Hongming." or "It's Hongming!" or similar.
assert re.search(r"\bhongming\b", reply2, flags=re.IGNORECASE), (
f"Turn 2 reply does not contain 'Hongming' — SessionStore "
f"continuity regression suspected. context_id={context_id} "
f"turn1_reply={reply1[:200]!r} turn2_reply={reply2[:400]!r}"
)
# ---------- canary 2: file-only message (no caption) -----------------------
_DROPPED_TURN_MARKERS = (
"(empty prompt — nothing to do)",
"empty prompt",
"message contained no text content",
"no text content",
)
def test_canary_2_file_only_message(sim: CPSim, context_id: str) -> None:
"""File-attached A2A message with NO text part must not be dropped.
Root cause this guards against: a long-standing executor bug where
``extract_message_text`` returned "" for file-only messages and the
executor short-circuited with the "Error: message contained no text
content." reply, even though the attached file was the entire point
of the turn.
Hard assertions:
- Reply is non-empty AND not the dropped-turn marker.
- Reply references the file by name OR asks an actionable
clarifying question (NOT a flat error).
"""
file_name = f"canary-{uuid.uuid4().hex[:8]}.txt"
file_body = b"Project status: nominal. Lighthouse score 98."
r = sim.send_with_file(
context_id=context_id,
text=None, # ← THE CANARY: no caption.
file_name=file_name,
file_bytes=file_body,
mime_type="text/plain",
)
reply = sim.extract_text_parts(r)
assert reply, f"File-only message produced empty reply. envelope={r!r}"
low = reply.lower()
for marker in _DROPPED_TURN_MARKERS:
assert marker.lower() not in low, (
f"File-only message was dropped — reply contains "
f"{marker!r}. Full reply: {reply[:500]!r}"
)
# Soft assertion: reply must engage with the file (reference its
# name) OR ask an actionable clarification. We require ONE of those —
# a generic "Hello! How can I help?" reply is also a drop.
name_referenced = file_name.lower() in low or "file" in low or "attach" in low
asks_clarification = (
"what" in low or "would you like" in low or "?" in reply
)
assert name_referenced or asks_clarification, (
f"File-only reply neither references the file nor asks a "
f"clarifying question. Reply: {reply[:500]!r}"
)
# ---------- canary 3: file + prompt (multimodal) ---------------------------
def test_canary_3_file_with_prompt(sim: CPSim, context_id: str) -> None:
"""File-attached A2A message WITH a caption — multimodal happy path.
Lower bar than canary 2: assert the agent acknowledges the file was
received and tries to address the caption. We deliberately don't
require a perfect summary because canary mode replies are canned —
the goal is to prove the executor's multimodal code path doesn't
drop EITHER the file OR the caption.
"""
file_name = f"canary-doc-{uuid.uuid4().hex[:8]}.txt"
file_body = (
b"Quarterly review. Revenue up 14%. Churn down 3%. "
b"Team headcount steady. Action: ship RFC#600 by end of week."
)
r = sim.send_with_file(
context_id=context_id,
text="summarize this",
file_name=file_name,
file_bytes=file_body,
mime_type="text/plain",
)
reply = sim.extract_text_parts(r)
assert reply, f"File+prompt produced empty reply. envelope={r!r}"
low = reply.lower()
for marker in _DROPPED_TURN_MARKERS:
assert marker.lower() not in low, (
f"File+prompt was dropped — reply contains {marker!r}. "
f"Full reply: {reply[:500]!r}"
)
# At minimum: the reply must mention file/attach/summary semantics,
# demonstrating the executor accepted both parts.
engaged = any(
kw in low for kw in ("file", "attach", "summary", "summarize", "content", file_name.lower())
)
assert engaged, (
f"Multimodal reply doesn't engage with attached file or caption. "
f"Reply: {reply[:500]!r}"
)
# ---------- canary 4: cross-session memory recall --------------------------
def test_canary_4_cross_session_memory_recall(sim: CPSim) -> None:
"""Memory persists across distinct context_ids → memory layer (NOT
SessionStore) is the storage.
Two distinct context_ids in this test — SessionStore CANNOT bridge
them. The bridge is the runtime's persistent memory (MOLECULE_MEMORY_ROOT
in canary mode). If the recall returns "blue" in session 2, the
memory layer is wired correctly.
Note: we ask the agent to commit the memory explicitly in session 1
so that the canary doesn't depend on memory auto-extraction
heuristics (which vary by runtime). The commit goes through the
same MCP tool the canvas would invoke.
"""
ctx_a = f"canary-ctx-{uuid.uuid4().hex[:12]}"
ctx_b = f"canary-ctx-{uuid.uuid4().hex[:12]}"
# Session 1 — commit a fact via the memory tool. Use the explicit
# "remember" verb so canary-mode agents (which short-circuit to a
# deterministic tool-call) reliably invoke `commit_memory`.
r1 = sim.send_text(
"Please use the memory tool to remember: my favorite color is blue.",
context_id=ctx_a,
)
reply1 = sim.extract_text_parts(r1)
assert reply1, f"Session 1 produced empty reply. envelope={r1!r}"
# Session 2 — different context_id. Same workspace, same memory.
r2 = sim.send_text(
"Use the memory tool to recall my favorite color, then tell me what it is.",
context_id=ctx_b,
)
reply2 = sim.extract_text_parts(r2)
assert reply2, f"Session 2 produced empty reply. envelope={r2!r}"
assert re.search(r"\bblue\b", reply2, flags=re.IGNORECASE), (
f"Session 2 reply does not contain 'blue' — cross-session memory "
f"recall regression suspected. ctx_a={ctx_a} ctx_b={ctx_b} "
f"session1_reply={reply1[:200]!r} session2_reply={reply2[:400]!r}"
)
+214
View File
@@ -0,0 +1,214 @@
"""Tenant control-plane simulator.
Emits the byte-identical JSON-RPC `message/send` wire shape that the
production `workspace-server` POSTs to the runtime's :8000 — see
``workspace-server/internal/handlers/a2a.go`` and the canonical sample
in ``tests/e2e/test_chat_attachments_e2e.sh``.
This file is purposefully small (~250 LoC). It is NOT a re-implementation
of `workspace-server`; it is just the minimum surface required to drive
the 4 session-continuity canaries.
If the runtime asserts on a header / envelope field that the production
platform sets but this simulator omits, FIX THE SIMULATOR — never weaken
the runtime to accept divergent wire shapes. The simulator is the
canonical contract emitter for canary purposes
(``feedback_no_single_source_of_truth``).
"""
from __future__ import annotations
import base64
import json
import os
import uuid
from dataclasses import dataclass
from typing import Any
import httpx
@dataclass
class CPSimConfig:
runtime_url: str
"""Base URL of the runtime under test (e.g. http://runtime:8000)."""
request_timeout_s: float = 60.0
"""Per-A2A-call timeout. Generous — canary mode replies are fast,
but a real Provider-backed runtime under cold cache can take 30+s."""
class CPSim:
"""Thin client matching workspace-server's wire shape."""
def __init__(self, cfg: CPSimConfig | None = None) -> None:
self.cfg = cfg or CPSimConfig(
runtime_url=os.environ.get("RUNTIME_URL", "http://localhost:18000"),
)
self._client = httpx.Client(timeout=self.cfg.request_timeout_s)
# ------------------------------------------------------------------ A2A
def send_text(
self,
text: str,
*,
context_id: str,
task_id: str | None = None,
) -> dict[str, Any]:
"""POST a text-only A2A message. Returns the JSON-RPC envelope."""
msg_id = f"canary-{uuid.uuid4().hex[:12]}"
payload = {
"jsonrpc": "2.0",
"id": msg_id,
"method": "message/send",
"params": {
"message": {
"role": "user",
"messageId": msg_id,
"kind": "message",
"contextId": context_id,
"taskId": task_id,
"parts": [{"kind": "text", "text": text}],
},
"configuration": {
"acceptedOutputModes": ["text/plain"],
"blocking": True,
},
},
}
return self._post(payload)
def send_with_file(
self,
*,
context_id: str,
text: str | None,
file_name: str,
file_bytes: bytes,
mime_type: str = "text/plain",
task_id: str | None = None,
) -> dict[str, Any]:
"""POST an A2A message with an inline file part.
Uses the inline `bytes` form of A2A file parts (RFC#600 — the
no-URI variant added precisely so canary tests don't need a
`/chat/uploads` round-trip). Each runtime's executor calls
``extract_attached_files`` which handles both forms — verified
in ``workspace/executor_helpers.py:903``.
"""
msg_id = f"canary-{uuid.uuid4().hex[:12]}"
parts: list[dict[str, Any]] = []
if text:
parts.append({"kind": "text", "text": text})
parts.append(
{
"kind": "file",
"file": {
"name": file_name,
"mimeType": mime_type,
"bytes": base64.b64encode(file_bytes).decode("ascii"),
},
}
)
payload = {
"jsonrpc": "2.0",
"id": msg_id,
"method": "message/send",
"params": {
"message": {
"role": "user",
"messageId": msg_id,
"kind": "message",
"contextId": context_id,
"taskId": task_id,
"parts": parts,
},
"configuration": {
"acceptedOutputModes": ["text/plain"],
"blocking": True,
},
},
}
return self._post(payload)
# ------------------------------------------------------------ helpers
def _post(self, payload: dict[str, Any]) -> dict[str, Any]:
url = f"{self.cfg.runtime_url}/a2a"
try:
r = self._client.post(url, json=payload)
except httpx.HTTPError as e:
raise CPSimError(f"A2A POST failed: {e}") from e
if r.status_code != 200:
raise CPSimError(
f"A2A non-200: status={r.status_code} body={r.text[:500]}"
)
try:
return r.json()
except json.JSONDecodeError as e:
raise CPSimError(f"A2A body not JSON: {r.text[:500]}") from e
@staticmethod
def extract_text_parts(envelope: dict[str, Any]) -> str:
"""Return concatenated text from all text parts of a reply.
Handles both top-level `result.parts` (the canonical shape) and
`result.artifacts[*].parts` (which some runtimes emit when the
reply was streamed as artifact chunks). Matches the extractor in
``tests/e2e/test_chat_attachments_e2e.sh``.
"""
result = envelope.get("result") or {}
chunks: list[str] = []
for p in result.get("parts", []) or []:
if p.get("kind") == "text":
chunks.append(p.get("text", ""))
for art in result.get("artifacts", []) or []:
for p in art.get("parts", []) or []:
if p.get("kind") == "text":
chunks.append(p.get("text", ""))
# Some runtimes return a status.message instead of/in addition to parts.
status = result.get("status") or {}
status_msg = status.get("message") or {}
for p in status_msg.get("parts", []) or []:
if p.get("kind") == "text":
chunks.append(p.get("text", ""))
return "\n".join(chunks).strip()
# ----------------------------------------------------- memory probe
def probe_memory(self, key: str) -> str | None:
"""Read a memory value via the runtime's MCP memory tool.
Uses the same MCP transport the canvas uses
(``POST /workspaces/:id/mcp``-shaped JSON-RPC over /mcp). Returns
the recalled string or None if the key is missing.
"""
payload = {
"jsonrpc": "2.0",
"id": f"canary-mem-{uuid.uuid4().hex[:8]}",
"method": "tools/call",
"params": {"name": "recall_memory", "arguments": {"key": key}},
}
try:
r = self._client.post(f"{self.cfg.runtime_url}/mcp", json=payload)
except httpx.HTTPError as e:
raise CPSimError(f"MCP POST failed: {e}") from e
if r.status_code != 200:
return None
body = r.json()
result = body.get("result") or {}
# MCP responses wrap the tool output in result.content[*].text per
# the JSON-RPC tools/call contract.
for c in result.get("content", []) or []:
if c.get("type") == "text":
return c.get("text")
return None
class CPSimError(RuntimeError):
"""Raised on transport / envelope failures (NOT canary assertion failures).
Distinct from AssertionError so pytest reports them as ERROR not
FAILED — a transport-layer fault should be debugged differently from
a real session-continuity regression.
"""
+5
View File
@@ -0,0 +1,5 @@
# Pinned (not floating) so the harness is reproducible across CI runs.
# These versions match what tests/e2e/_lib.sh and tests/e2e/conftest.py use.
httpx==0.27.2
pytest==8.3.3
pytest-asyncio==0.24.0
+58
View File
@@ -0,0 +1,58 @@
# local-e2e/docker-compose.yml — minimal harness stack.
#
# Two services:
# runtime — the template image under test (TEMPLATE_IMAGE env var).
# Exposes :8000 for A2A traffic. The simulator POSTs to it.
# cp_sim — thin Python tenant-CP simulator. Drives the canary turns.
#
# Deliberately NO postgres, NO redis, NO platform Go service. SessionStore
# continuity is a runtime-internal concern (a2a_executor + executor_helpers);
# we test it without dragging the platform-tenant Go binary into the loop.
# See README.md "Why a thin Python simulator" for rationale.
services:
runtime:
image: ${TEMPLATE_IMAGE:?TEMPLATE_IMAGE env required, e.g. ghcr.io/molecule-ai/workspace-template-hermes:latest}
# The runtime entrypoint (workspace/entrypoint.sh) refuses to start when
# any operator-scope env var is present. We deliberately set no creds —
# the canary doesn't invoke a real LLM provider (see TEST_NO_PROVIDER below).
environment:
# Disable provider calls during canary — the runtime returns canned
# echo-style replies so the harness can assert continuity / file-handling
# behaviour without burning provider quota. The template image must
# honour MOLECULE_CANARY_MODE=1 (added in molecule-ai-workspace-runtime
# PR #46 — see molecule_runtime/a2a_executor.py canary short-circuit).
MOLECULE_CANARY_MODE: "1"
# Anonymous workspace identity so RBAC paths exercise the same code
# they would in tenant production.
WORKSPACE_ID: "canary-${CANARY_RUN_ID:-local}"
# Memory tool requires a writable scope; point at /tmp inside the
# container so cross-session canary (#4) works without bind mounts.
MOLECULE_MEMORY_ROOT: "/tmp/canary-memory"
# The provisioner's forbidden-env guard exits non-zero when any
# operator-scope literal is present; the canary intentionally sets
# zero of them. Leave guard ON (do NOT set MOLECULE_TENANT_GUARD_DISABLE)
# so we exercise the prod entrypoint code path verbatim.
ports:
- "${RUNTIME_PORT:-18000}:8000"
healthcheck:
# /agent-card is the universal A2A discovery endpoint — every template
# exposes it. /health varies per template.
test: ["CMD-SHELL", "wget -qO /dev/null --tries=1 http://localhost:8000/agent-card || exit 1"]
interval: 3s
timeout: 3s
retries: 20
start_period: 30s
cp_sim:
build:
context: ./cp_sim
depends_on:
runtime:
condition: service_healthy
environment:
RUNTIME_URL: "http://runtime:8000"
CANARY_RUN_ID: "${CANARY_RUN_ID:-local}"
# cp_sim doesn't expose a port — it's a one-shot driver invoked by
# run-canary.sh via `docker compose run cp_sim pytest ...`.
profiles: ["driver"]
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# onboard-template.sh — gitops helper to wire local-e2e into a new template.
#
# Drops .gitea/workflows/session-continuity-e2e.yml into the target template
# repo (a thin shim that clones molecule-core's local-e2e harness, then runs
# run-canary.sh against the locally-built template image). Opens a PR.
#
# Usage:
# ./local-e2e/scripts/onboard-template.sh molecule-ai-workspace-template-claude-code
#
# Per task #342 sequencing: do NOT run this for every template at once.
# Bake the gate on hermes for ≥5 business days first; expand only after
# the canary is empirically stable.
#
# Cross-refs:
# feedback_no_single_source_of_truth — the workflow content is identical
# across templates; this helper guarantees it.
# feedback_image_promote_is_not_user_live — we wire the gate at the
# CI layer; flipping it to REQUIRED in branch_protection is a
# separate step (see README.md).
set -euo pipefail
REPO="${1:?usage: onboard-template.sh <template-repo-name>}"
HARNESS_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
# Sanity: ensure the template-side workflow file exists in this repo.
TEMPLATE_WORKFLOW="$HARNESS_ROOT/templates/session-continuity-e2e.yml"
[ -f "$TEMPLATE_WORKFLOW" ] || {
echo "ERROR: $TEMPLATE_WORKFLOW not found in this harness checkout"
exit 1
}
WORK_DIR=$(mktemp -d -t e2e-onboard-XXXXXX)
trap 'rm -rf "$WORK_DIR"' EXIT
cd "$WORK_DIR"
# Use mol_clone — preserves the persona credential model.
# shellcheck disable=SC1090
source "$HOME/.molecule-ai/ops.sh"
mol_clone "$REPO"
cd "$REPO"
git checkout -b "task342/session-continuity-e2e-gate"
mkdir -p .gitea/workflows
cp "$TEMPLATE_WORKFLOW" .gitea/workflows/session-continuity-e2e.yml
git add .gitea/workflows/session-continuity-e2e.yml
git commit -m "ci: add local-e2e session-continuity canary gate (task #342)
Wires this template into the cross-template session-continuity harness
in molecule-ai/molecule-core/local-e2e/. The gate boots THIS repo's
locally-built image, drives 4 canonical canaries (2-turn name continuity,
file-only message, file+prompt, cross-session memory recall), and fails
PRs that regress any of them.
Per CTO directive: required-context flip in branch_protection is a
SEPARATE step after 5 business days of bake."
# Push branch; do not auto-open PR — leave that to the operator so the
# review-relay routing follows the same rules as a normal change.
git push -u origin "task342/session-continuity-e2e-gate"
echo
echo "DONE. Branch pushed to $REPO. Open PR manually:"
echo " https://git.moleculesai.app/molecule-ai/$REPO/compare/main...task342/session-continuity-e2e-gate"
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# run-canary.sh — one-shot orchestration for the local-e2e session-continuity
# canary harness. Used by both interactive local runs and the per-template
# .gitea/workflows/session-continuity-e2e.yml.
#
# Usage:
# TEMPLATE_IMAGE=ghcr.io/molecule-ai/workspace-template-hermes:latest \
# ./local-e2e/scripts/run-canary.sh
#
# Optional env:
# CANARY_RUN_ID — disambiguator for parallel CI runs (default: random)
# RUNTIME_PORT — host port for runtime :8000 (default: 18000)
# KEEP_RUNNING — set =1 to leave containers up for post-mortem
#
# Exit codes:
# 0 — all 4 canaries passed
# 1 — at least one canary failed (artifacts/ has the dump)
# 2 — harness infrastructure failure (image pull / compose / etc.)
#
# Cross-refs:
# feedback_image_promote_is_not_user_live — we verify at the running
# container layer, NOT at the pipeline-green layer.
# feedback_verify_actual_endstate_not_ack_follow_sop — every assert
# reads state back; no side-effect-ack claims success.
set -euo pipefail
: "${TEMPLATE_IMAGE:?TEMPLATE_IMAGE env required (the runtime image under test)}"
# ----------------------------------------------------------------- paths
HARNESS_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
ARTIFACTS_DIR="$HARNESS_ROOT/artifacts"
mkdir -p "$ARTIFACTS_DIR"
export CANARY_RUN_ID="${CANARY_RUN_ID:-$(uuidgen 2>/dev/null | tr A-Z a-z | tr -d - | cut -c1-12 || date +%s)}"
export RUNTIME_PORT="${RUNTIME_PORT:-18000}"
export TEMPLATE_IMAGE
COMPOSE_PROJECT="canary-${CANARY_RUN_ID}"
COMPOSE_FILE="$HARNESS_ROOT/docker-compose.yml"
log() { printf "\n=== [%s] %s ===\n" "$(date +%H:%M:%S)" "$*"; }
# ----------------------------------------------------------- cleanup hook
cleanup() {
local rc=$?
if [ "${KEEP_RUNNING:-0}" = "1" ]; then
log "KEEP_RUNNING=1 — leaving containers up (project=$COMPOSE_PROJECT)"
return $rc
fi
log "Tearing down compose project $COMPOSE_PROJECT"
# On non-zero exit, capture logs FIRST. Per feedback_image_promote_is_
# not_user_live: dump state from the actually-running container, not
# an inferred pipeline state.
if [ $rc -ne 0 ]; then
log "Canary FAILED — dumping artifacts to $ARTIFACTS_DIR"
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" logs \
--no-color --tail=200 runtime \
> "$ARTIFACTS_DIR/runtime.log" 2>&1 || true
# SessionStore state probe — runtime exposes /admin/session-store
# in canary mode; if not present this 404s and the file is empty.
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" exec -T runtime \
sh -c 'ls -la /tmp/canary-memory 2>/dev/null; find /tmp -name "session*.json" -exec cat {} \; 2>/dev/null' \
> "$ARTIFACTS_DIR/session-store.txt" 2>&1 || true
fi
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" down --volumes --remove-orphans >/dev/null 2>&1 || true
return $rc
}
trap cleanup EXIT
# ------------------------------------------------------ stack bring-up
log "Building cp_sim image"
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" build cp_sim
log "Pulling runtime image: $TEMPLATE_IMAGE"
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" pull runtime 2>&1 \
| tail -5 || true
log "Starting runtime (host port $RUNTIME_PORT)"
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" up -d runtime
# Wait for healthcheck — docker-compose `--wait` is the canonical mechanism
# (introduced in v2.1.1 in 2021, available on every supported runner pool).
log "Waiting for runtime healthcheck"
if ! docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" up -d --wait runtime; then
log "Runtime never went healthy — dumping logs"
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" logs --no-color --tail=200 runtime \
> "$ARTIFACTS_DIR/runtime-boot-failure.log" 2>&1 || true
exit 2
fi
# -------------------------------------------------------------- run tests
log "Running canary suite"
# Run cp_sim under the same compose project so DNS (runtime hostname)
# resolves on the molecule-core-net bridge. --rm cleans the driver container
# after pytest exits; volume bind mounts pytest's junit-xml back to host.
if docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" --profile driver run \
--rm \
-v "$ARTIFACTS_DIR:/harness/artifacts" \
cp_sim; then
log "All canaries PASSED"
exit 0
else
log "At least one canary FAILED — see $ARTIFACTS_DIR/junit.xml"
exit 1
fi
@@ -0,0 +1,85 @@
name: session-continuity-e2e
# Per-template wrapper for the molecule-core/local-e2e canary harness.
# DO NOT EDIT THIS FILE IN A TEMPLATE REPO — the canonical copy lives at
# molecule-ai/molecule-core:local-e2e/templates/session-continuity-e2e.yml
# (feedback_no_single_source_of_truth). The onboard-template.sh script
# copies it verbatim into each template; future fixes propagate via that
# helper, not by editing the template-side copy.
#
# What this workflow does:
# 1. Build THIS template's runtime image locally on the docker-host runner.
# 2. Clone molecule-core (canonical harness source).
# 3. Invoke local-e2e/scripts/run-canary.sh with TEMPLATE_IMAGE set to
# the just-built local image.
# 4. Upload artifacts/ on failure for post-mortem.
#
# Required-context flip:
# This workflow posts a status under the literal context name
# "session-continuity-e2e (pull_request)" — Gitea's standard
# <workflow-name> (<event>) format. To make it REQUIRED, add that
# exact string to the template repo's branch_protection
# status_check_contexts list. See README.md for the bake-period rule.
#
# Gitea 1.22.6 / act_runner notes (cross-refs to known footguns):
# - No cross-repo `uses:` (feedback_gitea_cross_repo_uses_blocked) —
# we clone molecule-core via plain git instead.
# - Per-SHA concurrency (feedback_concurrency_group_per_sha).
# - Workflow-level GITHUB_SERVER_URL pinned to the Gitea host
# (feedback_act_runner_github_server_url).
# - Runs on docker-host pool — NOT the heavy CI pool — per CTO
# directive "separate CI as possible" and the <3 min target.
on:
pull_request:
branches: [main]
push:
branches: [main]
concurrency:
group: session-continuity-e2e-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.head.sha || github.sha }}
cancel-in-progress: true
env:
GITHUB_SERVER_URL: https://git.moleculesai.app
jobs:
session-continuity-e2e:
runs-on: docker-host
timeout-minutes: 8
steps:
- name: Checkout template
uses: actions/checkout@v4
with:
path: template
- name: Build template image
id: build
working-directory: template
run: |
IMAGE_TAG="local-e2e-${GITHUB_SHA::12}"
docker build -t "molecule-ai/template-under-test:${IMAGE_TAG}" .
echo "image=molecule-ai/template-under-test:${IMAGE_TAG}" >> "$GITHUB_OUTPUT"
- name: Clone harness from molecule-core
run: |
# Anonymous clone — molecule-core is internal-readable. NEVER bake
# an auth token into the URL (feedback_credentials_in_git_url).
git clone --depth 1 "${GITHUB_SERVER_URL}/molecule-ai/molecule-core.git" harness
- name: Run canary
env:
TEMPLATE_IMAGE: ${{ steps.build.outputs.image }}
CANARY_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}
run: |
cd harness
./local-e2e/scripts/run-canary.sh
- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: session-continuity-canary-${{ github.run_id }}
path: harness/local-e2e/artifacts/
if-no-files-found: warn
retention-days: 7
-1
View File
@@ -38,4 +38,3 @@
{"name": "ux-ab-lab", "repo": "molecule-ai/molecule-ai-org-template-ux-ab-lab", "ref": "main"}
]
}
// Triggered by Integration Tester at 2026-05-10T08:52Z
+1 -1
View File
@@ -9,7 +9,7 @@ There are three related scripts; pick the right one:
| Script | Purpose | Targets |
|---|---|---|
| `measure-coordinator-task-bounds.sh` | **Canonical** v1 harness for the RFC #2251 / Issue 4 reproduction. Provisions a PM coordinator + Researcher child via `claude-code-default` + `langgraph` templates, sends a synthesis-heavy A2A kickoff, observes elapsed time + activity trace. | OSS-shape platform — localhost or any `/workspaces`-shaped endpoint. Has tenant/admin-token guards for non-localhost runs. |
| `measure-coordinator-task-bounds.sh` | **Canonical** v1 harness for the RFC #2251 / Issue 4 reproduction. Provisions a PM coordinator + Researcher child via `claude-code-default` + `claude-code` templates, sends a synthesis-heavy A2A kickoff, observes elapsed time + activity trace. | OSS-shape platform — localhost or any `/workspaces`-shaped endpoint. Has tenant/admin-token guards for non-localhost runs. |
| `measure-coordinator-task-bounds-runner.sh` | Generalised runner for the same measurement contract but with **arbitrary template + secret + model combinations** (Hermes/MiniMax, etc.). Useful for cross-runtime variants without modifying the canonical harness. | Same as above (local or SaaS via `MODE=saas`). |
| `measure-coordinator-task-bounds.sh` (in [molecule-controlplane](https://git.moleculesai.app/molecule-ai/molecule-controlplane)) | **Production-shape** variant that bootstraps a real staging tenant via `POST /cp/admin/orgs`, then runs the same measurement against `<slug>.staging.moleculesai.app`. | Staging controlplane only — refuses to run against production. |
+1 -1
View File
@@ -91,7 +91,7 @@ Cold-start times on workspace-template images:
|---|---|
| claude-code | ~30-60s |
| openclaw | ~1-2 min |
| langgraph | ~1 min |
| claude-code | ~1 min |
| hermes | **~7 min** (large image) |
If the demo will use `hermes`, provision the demo workspace at least
+1 -5
View File
@@ -86,13 +86,9 @@ esac
# RuntimeImages — keep this list in sync if a runtime is added.
TEMPLATES=(
"claude-code"
"codex"
"hermes"
"openclaw"
"langgraph"
"deepagents"
"crewai"
"autogen"
"gemini-cli"
)
# Pre-flight: required tooling.
@@ -2,7 +2,7 @@
# Standalone runner for Issue 4 reproduction (RFC #2251) — exists alongside
# `measure-coordinator-task-bounds.sh` to support arbitrary template + secret
# combinations without modifying the canonical harness. The canonical harness
# stays focused on its v1 contract (claude-code-default + langgraph + OpenRouter);
# stays focused on its v1 contract (claude-code-default + claude-code + OpenRouter);
# this runner wraps the same workspace-server API calls but takes everything as
# env-var inputs so a Hermes/MiniMax run can share the measurement code path.
#
+2 -2
View File
@@ -196,7 +196,7 @@ Auth: $([ -n "$ADMIN_TOKEN" ] && echo "Bearer ***${ADMIN_TOKEN: -4}" ||
Would provision:
PM (coordinator, tier=2, template=claude-code-default)
Researcher (child, tier=2, template=langgraph)
Researcher (child, tier=2, template=claude-code-default)
Would send synthesis-heavy task: $SYNTHESIS_DEPTH delegations + 600w
synthesis. Coordinator A2A timeout: ${A2A_TIMEOUT}s.
@@ -220,7 +220,7 @@ emit "pm_provisioned" "{\"workspace_id\":\"$PM_ID\"}"
emit "provisioning_child" null
R=$(api -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
-d '{"name":"Researcher","role":"Returns short research findings","tier":2,"template":"langgraph"}')
-d '{"name":"Researcher","role":"Returns short research findings","tier":2,"template":"claude-code-default"}')
CHILD_ID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))")
[ -n "$CHILD_ID" ] || { echo "ERROR: child create failed: $R" >&2; exit 1; }
emit "child_provisioned" "{\"workspace_id\":\"$CHILD_ID\"}"
+8 -8
View File
@@ -47,23 +47,23 @@ echo " Cross-Agent Chat: Agents Talk to Each Other"
echo "============================================"
echo ""
# --- Create 3 agents: PM (LangGraph), Developer (CrewAI), Researcher (AutoGen) ---
# --- Create 3 agents: PM (Claude Code), Developer (OpenClaw), Researcher (Codex) ---
echo "--- Creating 3 agents ---"
R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
-d '{"name":"PM","role":"Project Manager","tier":2,"template":"langgraph"}')
-d '{"name":"PM","role":"Project Manager","tier":2,"template":"claude-code-default"}')
PM=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "PM (LangGraph): $PM"
echo "PM (Claude Code): $PM"
R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
-d '{"name":"Developer","role":"Code implementation","tier":2,"template":"crewai"}')
-d '{"name":"Developer","role":"Code implementation","tier":2,"template":"openclaw"}')
DEV=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "Developer (CrewAI): $DEV"
echo "Developer (OpenClaw): $DEV"
R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
-d '{"name":"Researcher","role":"Research and analysis","tier":2,"template":"autogen"}')
-d '{"name":"Researcher","role":"Research and analysis","tier":2,"template":"codex"}')
RES=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "Researcher (AutoGen): $RES"
echo "Researcher (Codex): $RES"
# --- Set hierarchy: PM -> Developer, Researcher ---
echo ""
@@ -136,7 +136,7 @@ check "Researcher responds directly" "agent" "$RESP"
echo ""
echo "--- Test 2: PM delegates to Researcher (cross-runtime A2A) ---"
echo " Asking PM to research something (should delegate to Researcher)..."
RESP=$(a2a_send "$PM" "Please ask the Researcher to briefly explain what LangGraph is.")
RESP=$(a2a_send "$PM" "Please ask the Researcher to briefly explain what Claude Code is.")
echo " PM says: $RESP"
# The response should contain info from the Researcher
check "PM got Researcher's response" "graph\|agent\|lang\|workflow" "$RESP"
+6 -6
View File
@@ -49,11 +49,11 @@ R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
PM_ID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
check "Create PM (claude-code)" "provisioning" "$R"
# Research Agent — LangGraph + Gemini Flash
# Research Agent — Claude Code + Gemini Flash
R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
-d '{"name":"Researcher","role":"Deep research and analysis","tier":2,"template":"langgraph"}')
-d '{"name":"Researcher","role":"Deep research and analysis","tier":2,"template":"claude-code-default"}')
RES_ID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
check "Create Researcher (langgraph)" "provisioning" "$R"
check "Create Researcher (claude-code)" "provisioning" "$R"
# Dev Agent — OpenClaw + Gemini Flash
R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
@@ -61,11 +61,11 @@ R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
DEV_ID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
check "Create Developer (openclaw)" "provisioning" "$R"
# Analyst — DeepAgents + Gemini Flash
# Analyst — Hermes + Gemini Flash
R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
-d '{"name":"Analyst","role":"Data analysis and reporting","tier":2,"template":"deepagents"}')
-d '{"name":"Analyst","role":"Data analysis and reporting","tier":2,"template":"hermes"}')
ANA_ID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
check "Create Analyst (deepagents)" "provisioning" "$R"
check "Create Analyst (hermes)" "provisioning" "$R"
echo ""
echo " PM: $PM_ID"
+24 -5
View File
@@ -45,12 +45,31 @@ e2e_mint_workspace_token() {
printf '%s' "$json" | python3 -c "import json,sys; print(json.load(sys.stdin)['auth_token'])"
}
e2e_cleanup_all_workspaces() {
for _wid in $(curl -s "$BASE/workspaces" | python3 -c "import json,sys
e2e_delete_workspace() {
local wid="$1"
local name="${2:-}"
shift 2 || true
local curl_args=("$@")
if [ -z "$wid" ]; then
return 0
fi
if [ -z "$name" ]; then
name=$(curl -s "$BASE/workspaces/$wid" "${curl_args[@]}" | python3 -c "import json,sys
try:
[print(w['id']) for w in json.load(sys.stdin)]
print(json.load(sys.stdin).get('name',''))
except Exception:
pass" 2>/dev/null); do
curl -s -X DELETE "$BASE/workspaces/$_wid?confirm=true" > /dev/null || true
pass" 2>/dev/null || true)
fi
curl -s -X DELETE "$BASE/workspaces/$wid?confirm=true" \
-H "X-Confirm-Name: $name" "${curl_args[@]}" > /dev/null || true
}
e2e_cleanup_all_workspaces() {
curl -s "$BASE/workspaces" | python3 -c "import json,sys
try:
[print(f\"{w.get('id','')}\\t{w.get('name','')}\") for w in json.load(sys.stdin)]
except Exception:
pass" 2>/dev/null | while IFS=$'\t' read -r _wid _name; do
e2e_delete_workspace "$_wid" "$_name"
done
}
+1 -1
View File
@@ -137,7 +137,7 @@ R=$(curl -s --max-time 10 -X POST "$BASE/workspaces/$OFFLINE_ID/a2a" \
-d '{"method":"message/send","params":{"message":{"role":"user","parts":[{"type":"text","text":"test"}]}}}')
check "Offline workspace returns error" '"error"' "$R"
# Clean up
curl -s -X DELETE "$BASE/workspaces/$OFFLINE_ID" >/dev/null
e2e_delete_workspace "$OFFLINE_ID" "Offline Test"
echo ""
# ========================================
+1 -1
View File
@@ -235,7 +235,7 @@ R=$(curl -s "$BASE/workspaces/$TEMP_ID/activity")
check "Activity in correct workspace" 'Temp workspace log' "$R"
# Cleanup
curl -s -X DELETE "$BASE/workspaces/$TEMP_ID" > /dev/null
e2e_delete_workspace "$TEMP_ID" "Activity Test Workspace"
# ---------- Edge Cases ----------
echo ""
+7 -3
View File
@@ -289,7 +289,9 @@ R=$(curl -s "$BASE/workspaces" -H "Authorization: Bearer $ECHO_TOKEN")
check "current_task in list response" '"current_task"' "$R"
# Test 21: Delete
R=$(acurl -X DELETE "$BASE/workspaces/$ECHO_ID" -H "Authorization: Bearer $ECHO_TOKEN")
R=$(acurl -X DELETE "$BASE/workspaces/$ECHO_ID?confirm=true" \
-H "Authorization: Bearer $ECHO_TOKEN" \
-H "X-Confirm-Name: Echo Agent v2")
check "DELETE /workspaces/:id" '"status":"removed"' "$R"
R=$(curl -s "$BASE/workspaces" -H "Authorization: Bearer $SUM_TOKEN")
@@ -310,7 +312,9 @@ ORIG_TIER=$(echo "$BUNDLE" | python3 -c "import sys,json; print(json.load(sys.st
# Delete the workspace — use SUM_TOKEN (per-workspace) for WorkspaceAuth
# and ADMIN_TOKEN for the AdminAuth layer.
R=$(curl -s -X DELETE "$BASE/workspaces/$SUM_ID" -H "Authorization: Bearer $SUM_TOKEN")
R=$(curl -s -X DELETE "$BASE/workspaces/$SUM_ID?confirm=true" \
-H "Authorization: Bearer $SUM_TOKEN" \
-H "X-Confirm-Name: Summarizer Agent")
check "Delete before re-import" '"status":"removed"' "$R"
# After deleting both workspaces, all per-workspace tokens are revoked.
@@ -381,7 +385,7 @@ REBUNDLE=$(curl -s "$BASE/bundles/export/$NEW_ID" -H "Authorization: Bearer $NEW
check "Re-exported bundle has agent_card" '"agent_card"' "$REBUNDLE"
# Clean up — use the token just issued to the re-imported workspace
curl -s -X DELETE "$BASE/workspaces/$NEW_ID" -H "Authorization: Bearer $NEW_TOKEN" > /dev/null
e2e_delete_workspace "$NEW_ID" "$ORIG_NAME" -H "Authorization: Bearer $NEW_TOKEN"
echo ""
echo "=== Results: $PASS passed, $FAIL failed ==="
+1
View File
@@ -39,6 +39,7 @@ cleanup() {
set +e
if [ -n "$PARENT" ]; then
curl -sS -X DELETE "$BASE/workspaces/$PARENT?confirm=true&purge=true" \
-H "X-Confirm-Name: e2e-chat-upload" \
${PARENT_TOK:+-H "Authorization: Bearer $PARENT_TOK"} >/dev/null 2>&1
fi
exit $rc
+3 -3
View File
@@ -10,6 +10,8 @@
set -euo pipefail
PLATFORM="http://localhost:8080"
export BASE="$PLATFORM"
source "$(dirname "$0")/_lib.sh"
PASS=0
FAIL=0
ERRORS=""
@@ -38,9 +40,7 @@ else
fi
# --- Clean existing workspaces ---
for id in $(curl -s $PLATFORM/workspaces | python3 -c "import sys,json; [print(w['id']) for w in json.load(sys.stdin)]" 2>/dev/null); do
curl -s -X DELETE "$PLATFORM/workspaces/$id" > /dev/null
done
e2e_cleanup_all_workspaces
# shellcheck disable=SC2046 # Intentional word-split over container IDs
docker stop $(docker ps -q --filter "name=ws-") 2>/dev/null || true
# shellcheck disable=SC2046
+16 -9
View File
@@ -228,10 +228,12 @@ else
fi
# Clean up runtime test workspaces
for rt_id in $RT_CC_ID $RT_CX_ID $RT_HM_ID; do
curl -s -X DELETE "$BASE/workspaces/$rt_id?confirm=true" > /dev/null 2>&1
sleep 0.3
done
e2e_delete_workspace "$RT_CC_ID" "RT Claude"
sleep 0.3
e2e_delete_workspace "$RT_CX_ID" "RT Codex"
sleep 0.3
e2e_delete_workspace "$RT_HM_ID" "RT Hermes"
sleep 0.3
# ============================================================
# Section 3: Registry & Heartbeat
@@ -550,16 +552,21 @@ check "Import bundle" '"status"' "$R"
echo ""
echo "--- Section 14: Cleanup & Delete ---"
# Delete with children — should require confirmation
# Delete without name confirmation should be rejected before cascade.
R=$(curl -s -X DELETE "$BASE/workspaces/$PM_ID")
check "Delete PM requires confirmation" '"confirmation_required"' "$R"
check "Delete PM requires name confirmation" '"destructive_action_requires_confirmation"' "$R"
# Delete with name confirmation but without cascade confirmation should
# still require explicit child confirmation.
R=$(curl -s -X DELETE "$BASE/workspaces/$PM_ID" -H "X-Confirm-Name: Test PM")
check "Delete PM requires cascade confirmation" '"confirmation_required"' "$R"
# Delete with confirmation
R=$(curl -s -X DELETE "$BASE/workspaces/$PM_ID?confirm=true")
R=$(curl -s -X DELETE "$BASE/workspaces/$PM_ID?confirm=true" -H "X-Confirm-Name: Test PM")
check "Delete PM cascades" '"cascade_deleted"' "$R"
# Delete outsider
curl -s -X DELETE "$BASE/workspaces/$OUTSIDER_ID?confirm=true" > /dev/null
e2e_delete_workspace "$OUTSIDER_ID" "Test Outsider"
# Clean up remaining workspaces (bundle imports, runtime test workspaces, etc.)
sleep 2
@@ -568,7 +575,7 @@ import json, sys, subprocess, time
ws = json.load(sys.stdin)
for w in ws:
time.sleep(0.5) # avoid rate limit
subprocess.run(['curl', '-s', '-X', 'DELETE', '$BASE/workspaces/' + w['id'] + '?confirm=true'], capture_output=True)
subprocess.run(['curl', '-s', '-X', 'DELETE', '$BASE/workspaces/' + w['id'] + '?confirm=true', '-H', 'X-Confirm-Name: ' + w.get('name','')], capture_output=True)
" 2>/dev/null
# Poll for clean state up to 30s — DB cascade + container stop is async on busy systems
+1 -1
View File
@@ -134,7 +134,7 @@ fi
# ----------------------------------------------------------------------
# Cleanup
# ----------------------------------------------------------------------
curl -s -X DELETE "$BASE/workspaces/$WS_ID?confirm=true" > /dev/null || true
e2e_delete_workspace "$WS_ID" "Dev-Mode-Test"
echo ""
echo "=== Results: $PASS passed, $FAIL failed ==="
+2 -2
View File
@@ -32,7 +32,7 @@ cleanup() {
# Workspace teardown — best-effort, ignore errors so an unrelated CP
# outage doesn't shadow a real test failure.
if [ -n "$WSID" ]; then
curl -s -X DELETE "$BASE/workspaces/$WSID?confirm=true" > /dev/null || true
e2e_delete_workspace "$WSID" "Notify E2E"
fi
# /tmp scratch — pre-fix only ran on success path (the unconditional
# rm at the bottom of the script). Trap-based path lets the file leak
@@ -89,7 +89,7 @@ except Exception:
')
for _wid in $PRIOR; do
echo "Sweeping leftover Notify E2E workspace: $_wid"
curl -s -X DELETE "$BASE/workspaces/$_wid?confirm=true" > /dev/null || true
e2e_delete_workspace "$_wid" "Notify E2E"
done
# model is required at the Create boundary (CTO 2026-05-22 SSOT — see
+2 -2
View File
@@ -113,7 +113,7 @@ teardown() {
log "[teardown] deleting ${#CREATED_WSIDS[@]} workspace(s) this run created (scoped)"
for wid in ${CREATED_WSIDS[@]+"${CREATED_WSIDS[@]}"}; do
[ -n "$wid" ] || continue
curl -s -X DELETE "$BASE/workspaces/$wid?confirm=true" ${ADMIN_AUTH[@]+"${ADMIN_AUTH[@]}"} >/dev/null 2>&1 || true
e2e_delete_workspace "$wid" "" ${ADMIN_AUTH[@]+"${ADMIN_AUTH[@]}"}
done
exit $rc
}
@@ -131,7 +131,7 @@ except Exception:
' 2>/dev/null)
for _wid in $PRIOR; do
log "Pre-sweeping prior PV-Local workspace: $_wid"
curl -s -X DELETE "$BASE/workspaces/$_wid?confirm=true" ${ADMIN_AUTH[@]+"${ADMIN_AUTH[@]}"} >/dev/null 2>&1 || true
e2e_delete_workspace "$_wid" "" ${ADMIN_AUTH[@]+"${ADMIN_AUTH[@]}"}
done
# ─── Local-stack preflight ─────────────────────────────────────────────
+2 -2
View File
@@ -48,8 +48,8 @@ TMPDIR_E2E=$(mktemp -d -t poll-chat-upload-e2e-XXXXXX)
cleanup() {
local rc=$?
curl -s -X DELETE "$BASE/workspaces/$WS_A?confirm=true" >/dev/null 2>&1 || true
curl -s -X DELETE "$BASE/workspaces/$WS_B?confirm=true" >/dev/null 2>&1 || true
e2e_delete_workspace "$WS_A" "poll-chat-upload-test-a"
e2e_delete_workspace "$WS_B" "poll-chat-upload-test-b"
rm -rf "$TMPDIR_E2E"
exit $rc
}
+2 -2
View File
@@ -43,8 +43,8 @@ INVALID_PROBE_ID="$(gen_uuid)"
cleanup() {
local rc=$?
# Best-effort delete; non-fatal if the row was never created.
curl -s -X DELETE "$BASE/workspaces/$POLL_WS_ID" >/dev/null || true
curl -s -X DELETE "$BASE/workspaces/$CALLER_WS_ID" >/dev/null || true
e2e_delete_workspace "$POLL_WS_ID" "poll-mode-test"
e2e_delete_workspace "$CALLER_WS_ID" "poll-cross-test"
exit $rc
}
trap cleanup EXIT
+2 -2
View File
@@ -53,7 +53,7 @@ cleanup() {
# ${VAR[@]+"…"} form expands to nothing when the array is unset/empty
# so the loop body is skipped cleanly. Hits the skip-no-keys path.
for wid in ${CREATED_WSIDS[@]+"${CREATED_WSIDS[@]}"}; do
[ -n "$wid" ] && curl -s -X DELETE "$BASE/workspaces/$wid?confirm=true" > /dev/null || true
[ -n "$wid" ] && e2e_delete_workspace "$wid" ""
done
}
trap cleanup EXIT
@@ -74,7 +74,7 @@ except Exception:
')
for _wid in $PRIOR; do
echo "Sweeping prior workspace: $_wid"
curl -s -X DELETE "$BASE/workspaces/$_wid?confirm=true" > /dev/null || true
e2e_delete_workspace "$_wid" ""
done
# Block until $1 reaches one of $2 (space-separated states), or $3 sec elapse.
+4 -4
View File
@@ -23,7 +23,7 @@
# MOLECULE_ADMIN_TOKEN CP admin bearer — Railway CP_ADMIN_API_TOKEN
#
# Optional env:
# E2E_RUNTIME hermes (default) | claude-code | langgraph
# E2E_RUNTIME hermes (default) | claude-code | codex | openclaw
# E2E_PROVISION_TIMEOUT_SECS default 900 (15 min cold EC2 budget)
# E2E_WORKSPACE_ONLINE_TIMEOUT_SECS default 3600 (60 min — hermes
# cold-boot worst-case + slack). Raised from
@@ -458,9 +458,9 @@ wait_workspaces_online_routable() {
# who already have an Anthropic API key for their own Claude
# Code session. Pricier per-token than MiniMax but billing is
# still independent of MOLECULE_STAGING_OPENAI_API_KEY. Pinned to the
# claude-code runtime — hermes/langgraph use OpenAI-shaped envs.
# claude-code runtime — hermes/codex/openclaw use OpenAI-shaped envs.
#
# E2E_OPENAI_API_KEY → langgraph + hermes paths. Kept as fallback
# E2E_OPENAI_API_KEY → hermes/codex/openclaw paths. Kept as fallback
# for operator dispatches that explicitly want to exercise the
# OpenAI path. The HERMES_* fields pin hermes-agent's bridge to
# api.openai.com (template-hermes' derive-provider.sh otherwise
@@ -486,7 +486,7 @@ elif [ -n "${E2E_ANTHROPIC_API_KEY:-}" ]; then
# account just for E2E. Pricier per-token than MiniMax but billing
# is still independent of MOLECULE_STAGING_OPENAI_API_KEY, so an OpenAI
# quota collapse doesn't wedge this path. Pinned to the claude-code
# runtime: hermes/langgraph use OpenAI-shaped envs and won't honour
# runtime: hermes/codex/openclaw use OpenAI-shaped envs and won't honour
# ANTHROPIC_API_KEY without further wiring. pick_model_slug maps this
# branch to claude-sonnet-4-6 so the claude-code provider registry
# selects anthropic-api instead of the OAuth-only sonnet alias.
+7 -1
View File
@@ -364,7 +364,13 @@ for wid in "${WS_A_ID:-}" "${WS_B_ID:-}"; do
DELETE_AUTH=("${WS_B_AUTH[@]}")
fi
fi
curl -s -X DELETE "$BASE/workspaces/$wid?confirm=true" "${DELETE_AUTH[@]}" > /dev/null || true
if [ "$wid" = "${WS_A_ID:-}" ]; then
e2e_delete_workspace "$wid" "$WS_A_NAME" "${DELETE_AUTH[@]}"
elif [ "$wid" = "${WS_B_ID:-}" ]; then
e2e_delete_workspace "$wid" "$WS_B_NAME" "${DELETE_AUTH[@]}"
else
e2e_delete_workspace "$wid" "" "${DELETE_AUTH[@]}"
fi
echo "deleted $wid"
done
+6 -2
View File
@@ -31,7 +31,11 @@ RECEIVER_TOKEN=""
cleanup() {
for wid in "$SENDER_ID" "$RECEIVER_ID"; do
if [ -n "$wid" ]; then
curl -s -X DELETE "$BASE/workspaces/$wid?confirm=true" > /dev/null || true
if [ "$wid" = "$SENDER_ID" ]; then
e2e_delete_workspace "$wid" "Abilities Sender"
else
e2e_delete_workspace "$wid" "Abilities Receiver"
fi
fi
done
}
@@ -88,7 +92,7 @@ except Exception:
")
for _wid in $PRIOR; do
echo "Sweeping leftover '$NAME' workspace: $_wid"
curl -s -X DELETE "$BASE/workspaces/$_wid?confirm=true" > /dev/null || true
e2e_delete_workspace "$_wid" "$NAME"
done
done
+2 -2
View File
@@ -34,7 +34,7 @@ ARG GIT_SHA=dev
# Mirrors the pattern already in molecule-controlplane/Dockerfile.
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags "-s -w -X github.com/Molecule-AI/molecule-monorepo/platform/internal/buildinfo.GitSHA=${GIT_SHA}" \
-ldflags "-s -w -X git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/buildinfo.GitSHA=${GIT_SHA}" \
-o /platform ./cmd/server
# Bundle the built-in memory-plugin-postgres binary so an operator can
# activate Memory v2 by setting MEMORY_V2_CUTOVER=true + (default)
@@ -43,7 +43,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
# Stays inert until the operator flips the cutover env var.
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags "-s -w -X github.com/Molecule-AI/molecule-monorepo/platform/internal/buildinfo.GitSHA=${GIT_SHA}" \
-ldflags "-s -w -X git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/buildinfo.GitSHA=${GIT_SHA}" \
-o /memory-plugin ./cmd/memory-plugin-postgres
FROM alpine:3.20@sha256:c64c687cbea9300178b30c95835354e34c4e4febc4badfe27102879de0483b5e
+10 -18
View File
@@ -63,7 +63,7 @@ ARG GIT_SHA=dev
# Mirrors the pattern already in molecule-controlplane/Dockerfile.
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags "-s -w -X github.com/Molecule-AI/molecule-monorepo/platform/internal/buildinfo.GitSHA=${GIT_SHA}" \
-ldflags "-s -w -X git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/buildinfo.GitSHA=${GIT_SHA}" \
-o /platform ./cmd/server
# Memory v2 sidecar binary (Memory v2 #2728). Bundled so an operator
# can activate cutover by flipping MEMORY_V2_CUTOVER=true without
@@ -71,23 +71,16 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
# launch logic.
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags "-s -w -X github.com/Molecule-AI/molecule-monorepo/platform/internal/buildinfo.GitSHA=${GIT_SHA}" \
-ldflags "-s -w -X git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/buildinfo.GitSHA=${GIT_SHA}" \
-o /memory-plugin ./cmd/memory-plugin-postgres
# Memory v1→v2 backfill CLI (issue #1791 Phase A2). Bundled so an
# operator can migrate the historical agent_memories rows into the v2
# plugin via:
#
# docker exec molecule-tenant /memory-backfill -dry-run
# docker exec molecule-tenant /memory-backfill -apply
#
# Idempotent (UUID upsert in the plugin); safe to re-run. See the
# tool's main.go for full usage. Stays inert until invoked — does not
# run automatically on boot.
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags "-s -w -X github.com/Molecule-AI/molecule-monorepo/platform/internal/buildinfo.GitSHA=${GIT_SHA}" \
-o /memory-backfill ./cmd/memory-backfill
# Phase A2 memory-backfill CLI was bundled here briefly (#1796) to
# migrate agent_memories rows into the v2 plugin. After Phase A3 (#1809)
# dropped the source table, the binary is permanently inert — running
# it now hits `pq: relation "agent_memories" does not exist`. Removed
# the build to drop ~7MB from the image and remove the foot-gun.
# Source still lives in cmd/memory-backfill/ for history; safe to
# delete entirely in a future cleanup PR.
# ── Stage 2: Canvas Next.js standalone ────────────────────────────────
FROM node:20-alpine@sha256:afdf98210b07b586eb71fa22ba2e432e058e4cd1304d31ed60888755b8c865fb AS canvas-builder
@@ -124,7 +117,6 @@ RUN deluser --remove-home node 2>/dev/null || true; \
# Go platform binary + Memory v2 sidecar + v1→v2 backfill CLI
COPY --from=go-builder /platform /platform
COPY --from=go-builder /memory-plugin /memory-plugin
COPY --from=go-builder /memory-backfill /memory-backfill
COPY workspace-server/migrations /migrations
# Templates + plugins (pre-cloned by scripts/clone-manifest.sh in the
@@ -151,7 +143,7 @@ COPY workspace-server/entrypoint-tenant.sh /entrypoint.sh
# !external (e.g. molecule-dev → dev-lead). Caught on staging-cplead-2
# 2026-05-10 — see internal incident debrief.
RUN chmod +x /entrypoint.sh && \
chown -R canvas:canvas /canvas /platform /memory-plugin /memory-backfill /migrations /org-templates
chown -R canvas:canvas /canvas /platform /memory-plugin /migrations /org-templates
EXPOSE 8080
# entrypoint.sh starts as root to fix volume perms, then drops to
+3 -3
View File
@@ -32,9 +32,9 @@ import (
_ "github.com/lib/pq"
mclient "github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/client"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/contract"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/namespace"
mclient "git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/memory/client"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/memory/contract"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/memory/namespace"
)
const defaultLimit = 1000000 // effectively unlimited; cap keeps SQL pageable
@@ -10,8 +10,8 @@ import (
"github.com/DATA-DOG/go-sqlmock"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/contract"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/namespace"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/memory/contract"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/memory/namespace"
)
// stubBackfillPlugin records calls for assertions.
@@ -20,8 +20,8 @@ import (
"math/rand"
"os"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/contract"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/textutil"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/memory/contract"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/textutil"
)
// verifyConfig is the typed dependency bundle for verifyParity.
@@ -9,7 +9,7 @@ import (
"github.com/DATA-DOG/go-sqlmock"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/contract"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/memory/contract"
)
// stubVerifyPlugin records search calls and returns canned results.
@@ -45,8 +45,8 @@ import (
"testing"
"time"
mclient "github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/client"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/contract"
mclient "git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/memory/client"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/memory/contract"
)
const (
@@ -25,7 +25,7 @@ import (
_ "github.com/lib/pq"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/pgplugin"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/memory/pgplugin"
)
// migrationsFS bundles the .up.sql files into the binary at build time
+10 -1
View File
@@ -30,7 +30,7 @@ func TestRefreshEnvFromCP_AppliesCPResponse(t *testing.T) {
t.Errorf("org id header: got %q", got)
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"MOLECULE_CP_SHARED_SECRET":"new-secret","MOLECULE_CP_URL":"https://api.moleculesai.app","DISPLAY_SESSION_SIGNING_SECRET":"display-secret"}`)
fmt.Fprint(w, `{"MOLECULE_CP_SHARED_SECRET":"new-secret","MOLECULE_CP_URL":"https://api.moleculesai.app","DISPLAY_SESSION_SIGNING_SECRET":"display-secret","MOLECULE_LLM_BASE_URL":"https://api.moleculesai.app/api/v1/internal/llm/openai/v1","MOLECULE_LLM_USAGE_TOKEN":"tenant-admin-token","MOLECULE_LLM_DEFAULT_MODEL":"moonshot/kimi-k2.6"}`)
}))
defer srv.Close()
@@ -48,6 +48,15 @@ func TestRefreshEnvFromCP_AppliesCPResponse(t *testing.T) {
if got := os.Getenv("DISPLAY_SESSION_SIGNING_SECRET"); got != "display-secret" {
t.Errorf("DISPLAY_SESSION_SIGNING_SECRET: want display-secret, got %q", got)
}
if got := os.Getenv("MOLECULE_LLM_BASE_URL"); got != "https://api.moleculesai.app/api/v1/internal/llm/openai/v1" {
t.Errorf("MOLECULE_LLM_BASE_URL: got %q", got)
}
if got := os.Getenv("MOLECULE_LLM_USAGE_TOKEN"); got != "tenant-admin-token" {
t.Errorf("MOLECULE_LLM_USAGE_TOKEN: got %q", got)
}
if got := os.Getenv("MOLECULE_LLM_DEFAULT_MODEL"); got != "moonshot/kimi-k2.6" {
t.Errorf("MOLECULE_LLM_DEFAULT_MODEL: got %q", got)
}
}
// TestRefreshEnvFromCP_CPUnreachableDoesNotFailBoot: network errors must
+17 -17
View File
@@ -35,29 +35,29 @@ import (
"syscall"
"time"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/channels"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/crypto"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/events"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/handlers"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/imagewatch"
memwiring "github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/wiring"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/middleware"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/pendinguploads"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/plugins"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/provisioner"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/registry"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/router"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/scheduler"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/supervised"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/ws"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/channels"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/crypto"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/db"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/events"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/handlers"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/imagewatch"
memwiring "git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/memory/wiring"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/middleware"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/pendinguploads"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/plugins"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/provisioner"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/registry"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/router"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/scheduler"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/supervised"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/ws"
// External plugins — each registers EnvMutator(s) that run at workspace
// provision time. Loaded via soft-dep gates in main() so self-hosters
// without per-agent identity configured keep working.
ghidentity "go.moleculesai.app/plugin/gh-identity/pluginloader"
"github.com/Molecule-AI/molecule-monorepo/platform/pkg/provisionhook"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/pkg/provisionhook"
)
func main() {
@@ -23,7 +23,7 @@ import (
"fmt"
"os"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/provisioner"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/provisioner"
)
func main() {
+8 -9
View File
@@ -32,19 +32,18 @@ CANVAS_PID=$!
# Defaults (when sidecar IS spawned): MEMORY_PLUGIN_DATABASE_URL
# falls back to the tenant's DATABASE_URL.
#
# MEMORY_V2_CUTOVER is deprecated as of #1747 — the workspace-server
# binary no longer reads it (v2 is unconditional now; the legacy SQL
# fallback in mcp_tools.go is gone). The entrypoint still accepts it
# as a synonym for "operator wants the sidecar" so old CP user-data
# templates keep working through the rollout. When CP user-data drops
# the var, this branch can go.
# Phase A3 (#1792): MEMORY_V2_CUTOVER acceptance removed. The variable
# was deprecated by #1747 (binary stopped reading it) and only kept
# alive here as a synonym to bridge old CP user-data templates. With
# A3 dropping the entire v1 surface, the synonym is gone too. CP
# user-data sets MEMORY_PLUGIN_URL directly; if a stale template
# without that var ships, the sidecar simply doesn't start and the
# tenant boots without memory — loud but recoverable, same posture as
# any other required env missing.
MEMORY_PLUGIN_PID=""
memory_plugin_wanted=""
if [ -n "$MEMORY_PLUGIN_URL" ]; then
memory_plugin_wanted=1
elif [ "$MEMORY_V2_CUTOVER" = "true" ]; then
memory_plugin_wanted=1
echo "memory-plugin: ⚠️ MEMORY_V2_CUTOVER is deprecated (#1747) — set MEMORY_PLUGIN_URL instead. Spawning sidecar on the implied default this boot." >&2
fi
if [ -z "$MEMORY_PLUGIN_DISABLE" ] && [ -n "$memory_plugin_wanted" ] && [ -n "$DATABASE_URL" ]; then
# Schema isolation (issue #1733): when defaulting from the tenant
+1 -1
View File
@@ -1,4 +1,4 @@
module github.com/Molecule-AI/molecule-monorepo/platform
module git.moleculesai.app/molecule-ai/molecule-core/workspace-server
go 1.25.0
@@ -8,7 +8,7 @@ import (
"testing"
"time"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/artifacts"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/artifacts"
)
// cfEnvelope wraps a result value in the Cloudflare v4 response envelope.
@@ -2,7 +2,7 @@
//
// Set at link time:
//
// go build -ldflags "-X github.com/Molecule-AI/molecule-monorepo/platform/internal/buildinfo.GitSHA=<sha>"
// go build -ldflags "-X git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/buildinfo.GitSHA=<sha>"
//
// CI passes ${{ github.sha }} via Dockerfile.tenant ARG GIT_SHA; local
// dev builds default to "dev" so unset never reads as success.
@@ -6,7 +6,7 @@ import (
"net/http/httptest"
"testing"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/buildinfo"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/buildinfo"
"github.com/gin-gonic/gin"
)
+2 -2
View File
@@ -11,8 +11,8 @@ import (
"path/filepath"
"strings"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/provisioner"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/db"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/provisioner"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
+4 -4
View File
@@ -5,10 +5,10 @@ import (
"fmt"
"strings"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/events"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/models"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/provisioner"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/db"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/events"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/models"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/provisioner"
"github.com/google/uuid"
)
@@ -6,7 +6,7 @@ import (
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/db"
)
// ==================== Adapter Interface Tests ====================
@@ -9,8 +9,8 @@ import (
"sync"
"time"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/events"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/db"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/events"
)
const (
+1 -1
View File
@@ -29,7 +29,7 @@ import (
"encoding/base64"
"strings"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/crypto"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/crypto"
)
// sensitiveFields is the set of channel_config keys that get encrypted at
@@ -4,7 +4,7 @@ import (
"strings"
"testing"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/crypto"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/crypto"
)
// withTestEncryptionKey installs a deterministic 32-byte key for the

Some files were not shown because too many files have changed in this diff Show More