Compare commits

...

37 Commits

Author SHA1 Message Date
claude-ceo-assistant 691d341fbb fix(a2a): avoid false failure on busy queue fallback
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 9s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 8s
CI / Python Lint & Test (pull_request) Successful in 5s
CI / Detect changes (pull_request) Successful in 8s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 16s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
E2E Chat / detect-changes (pull_request) Successful in 19s
E2E API Smoke Test / detect-changes (pull_request) Successful in 20s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 6s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 8s
Harness Replays / detect-changes (pull_request) Successful in 8s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 7s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 7s
gate-check-v3 / gate-check (pull_request) Successful in 8s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 35s
sop-checklist / review-refire (pull_request) Has been skipped
qa-review / approved (pull_request) Failing after 9s
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 6s
sop-tier-check / tier-check (pull_request) Successful in 6s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 3s
CI / Canvas (Next.js) (pull_request) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 4s
E2E Chat / E2E Chat (pull_request) Successful in 23s
Harness Replays / Harness Replays (pull_request) Successful in 3s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m8s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m51s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2m26s
CI / Platform (Go) (pull_request) Successful in 5m48s
CI / all-required (pull_request) Successful in 6m45s
audit-force-merge / audit (pull_request) Successful in 8s
2026-05-23 16:21:22 -07:00
hongming ef42e17224 Merge pull request 'fix(ci): keep production auto-deploy nonblocking' (#1746) from fix/prod-auto-deploy-nonblocking into main
ci-arm64-advisory / fast-checks (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 9s
publish-workspace-server-image / build-and-push (push) Successful in 3m6s
Block internal-flavored paths / Block forbidden paths (push) Successful in 12s
CI / Detect changes (push) Successful in 16s
CI / Python Lint & Test (push) Successful in 9s
E2E API Smoke Test / detect-changes (push) Successful in 14s
E2E Chat / detect-changes (push) Successful in 32s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 8s
Handlers Postgres Integration / detect-changes (push) Successful in 5s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (push) Successful in 1m16s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (push) Successful in 3s
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
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (push) Successful in 5s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 8s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 4s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (push) Successful in 1m16s
CI / Platform (Go) (push) Successful in 4s
CI / Canvas (Next.js) (push) Successful in 4s
CI / Shellcheck (E2E scripts) (push) Successful in 12s
CI / all-required (push) Successful in 4m30s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 7s
E2E Chat / E2E Chat (push) Successful in 6s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 3s
CI / Canvas Deploy Reminder (push) Successful in 2s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 1m36s
publish-workspace-server-image / Production auto-deploy (push) Successful in 18m24s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 4m59s
main-red-watchdog / watchdog (push) Successful in 41s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 5m11s
gate-check-v3 / gate-check (push) Successful in 24s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 3s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 9s
ci-required-drift / drift (push) Successful in 1m3s
2026-05-23 22:40:30 +00:00
agent-dev-b b13c9f94f1 Merge pull request 'fix(workspace-server): check rows.Err() after iteration in MemoryHandler.List' (#1748) from fix/memory-list-rows-err 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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 9s
publish-workspace-server-image / build-and-push (push) Successful in 3m10s
Block internal-flavored paths / Block forbidden paths (push) Successful in 5s
CI / Detect changes (push) Successful in 6s
CI / Python Lint & Test (push) Successful in 3s
E2E API Smoke Test / detect-changes (push) Successful in 5s
Handlers Postgres Integration / detect-changes (push) Successful in 4s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 7s
E2E Chat / detect-changes (push) Successful in 8s
Harness Replays / detect-changes (push) Successful in 5s
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
Secret scan / Scan diff for credential-shaped strings (push) Successful in 4s
CI / all-required (push) Has been cancelled
publish-workspace-server-image / Production auto-deploy (push) Has been cancelled
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 8s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 31s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 6m18s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 5m26s
Harness Replays / Harness Replays (push) Successful in 3s
2026-05-23 22:20:25 +00:00
Molecule AI Dev Engineer A (Kimi) 600f88b172 fix(workspace-server): check rows.Err() after iteration in MemoryHandler.List
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) compensating
audit-force-merge / audit (pull_request) Successful in 3s
CI / Canvas (Next.js) (pull_request) Has been cancelled
CI / Shellcheck (E2E scripts) (pull_request) Has been cancelled
CI / Platform (Go) (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
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Has been cancelled
Harness Replays / Harness Replays (pull_request) Has been cancelled
The List handler iterated over rows.Next() but never checked rows.Err()
after the loop. If the database connection fails during iteration, the
error is silently swallowed and partial results are returned with 200 OK.

Add a rows.Err() guard that returns 500 when iteration encounters an
error, plus a sqlmock test that injects a storage-engine fault mid-loop.

Tracked: rows.Err() audit gap (follow-up to internal#348 / PR #1743).
2026-05-23 22:12:45 +00:00
claude-ceo-assistant df94fd1764 fix(ci): keep production auto-deploy nonblocking
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 8s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
CI / Detect changes (pull_request) Successful in 8s
CI / Python Lint & Test (pull_request) Successful in 4s
E2E API Smoke Test / detect-changes (pull_request) Successful in 7s
E2E Chat / detect-changes (pull_request) Successful in 7s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 6s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 3s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m26s
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 3s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 3s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m5s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m19s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m10s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 3s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m38s
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) Successful in 4s
security-review / approved (pull_request) Successful in 5s
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request) Successful in 6s
sop-checklist / review-refire (pull_request) Has been skipped
sop-tier-check / tier-check (pull_request) Successful in 5s
CI / Platform (Go) (pull_request) Successful in 1s
CI / Canvas (Next.js) (pull_request) Successful in 2s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 2s
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
CI / all-required (pull_request) Successful in 23m56s
audit-force-merge / audit (pull_request) Successful in 11s
2026-05-23 15:01:39 -07:00
agent-dev-a 8346b06291 Merge pull request 'fix(ci): arm64 pilot runs-on label matches Mac mini registration' (#1744) from fix/arm64-pilot-label 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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Successful in 9s
publish-workspace-server-image / build-and-push (push) Successful in 2m59s
Block internal-flavored paths / Block forbidden paths (push) Successful in 4s
CI / Python Lint & Test (push) Successful in 5s
CI / Detect changes (push) Successful in 10s
E2E API Smoke Test / detect-changes (push) Successful in 8s
E2E Chat / detect-changes (push) Successful in 8s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 6s
Handlers Postgres Integration / detect-changes (push) Successful in 4s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (push) Successful in 3s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 11s
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 5s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 6s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Has started running
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 10s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (push) Successful in 1m13s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (push) Successful in 1m14s
CI / all-required (push) Has been cancelled
publish-workspace-server-image / Production auto-deploy (push) Has been cancelled
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Has started running
main-red-watchdog / watchdog (push) Successful in 45s
gate-check-v3 / gate-check (push) Successful in 24s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 10s
ci-required-drift / drift (push) Successful in 1m1s
2026-05-23 21:57:42 +00:00
agent-dev-b b7da21063e fix(ci): guard review-check against empty PRs (head == base) (#1743)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Detect changes (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
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
E2E API Smoke Test / detect-changes (push) Waiting to run
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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Has been cancelled
review-check-tests / review-check.sh regression tests (push) Successful in 10s
Ops Scripts Tests / Ops scripts (unittest) (push) Successful in 1m9s
Co-authored-by: agent-dev-b <agent-dev-b@agents.moleculesai.app>
Co-committed-by: agent-dev-b <agent-dev-b@agents.moleculesai.app>
2026-05-23 21:56:43 +00:00
agent-dev-a 2f7b5ad871 Merge pull request 'ci: add internal#418 tracker for arm64 advisory continue-on-error' (#1745) from fix/arm64-advisory-tracker into main
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
CI / Detect changes (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 / detect-changes (push) Waiting to run
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-continue-on-error-tracking / lint-continue-on-error-tracking (push) Waiting to run
Lint curl status-code capture / Scan workflows for curl status-capture pollution (push) Waiting to run
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
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Has been cancelled
2026-05-23 21:55:41 +00:00
Molecule AI Dev Engineer B (MiniMax) 213ea06840 fix(ci): arm64 shellcheck pilot resilience
CI / all-required (pull_request) compensating
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
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Waiting to run
Lint curl status-code capture / Scan workflows for curl status-capture pollution (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 pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Waiting to run
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Waiting to run
lint-required-no-paths / lint-required-no-paths (pull_request) Waiting to run
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Waiting to run
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (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 8s
audit-force-merge / audit (pull_request) Successful in 5s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Has been cancelled
CI / Platform (Go) (pull_request) Has been cancelled
E2E Chat / E2E Chat (pull_request) Has been cancelled
Handlers Postgres Integration / Handlers Postgres Integration (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 Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Has been cancelled
- Add continue-on-error on Install shellcheck step
- Add command -v check before running shellcheck (skip if binary
  missing, exit 0 — pilot mode)
- Add continue-on-error on Run shellcheck step

The arm64-darwin Mac mini pilot runner may not have shellcheck
pre-installed; this makes the workflow resilient rather than failing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 21:52:36 +00:00
Molecule AI Dev Engineer A (Kimi) f07dfa7af6 ci: add internal#418 tracker for arm64 advisory continue-on-error (#1731)
CI / all-required (pull_request) compensating
Block internal-flavored paths / Block forbidden paths (pull_request) Waiting to run
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
CI / Python Lint & Test (pull_request) Waiting to run
CI / Detect changes (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
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Waiting to run
Lint curl status-code capture / Scan workflows for curl status-capture pollution (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 pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Waiting to run
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Waiting to run
lint-required-no-paths / lint-required-no-paths (pull_request) Waiting to run
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Waiting to run
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (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
audit-force-merge / audit (pull_request) Successful in 4s
E2E API Smoke Test / E2E API Smoke Test (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 Chat / E2E Chat (pull_request) Has been cancelled
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Has been cancelled
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Has been cancelled
The lint-continue-on-error-tracking script requires a tracker comment
within 2 lines of every advisory continue-on-error directive. This adds
the missing internal#418 tracker to ci-arm64-advisory.yml.

Fixes #1731
2026-05-23 21:49:44 +00:00
hongming 93f5a4aac3 Merge pull request 'fix(ci): use writable Docker config for canvas publish' (#1740) from fix/canvas-publish-docker-config into main
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
CI / Detect changes (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 / detect-changes (push) Waiting to run
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-continue-on-error-tracking / lint-continue-on-error-tracking (push) Waiting to run
Lint curl status-code capture / Scan workflows for curl status-capture pollution (push) Waiting to run
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
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (push) Waiting to run
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-canvas-image / Build & push canvas image (push) Successful in 1m24s
publish-workspace-server-image / build-and-push (push) Successful in 2m56s
publish-workspace-server-image / Production auto-deploy (push) Has been cancelled
2026-05-23 21:48:45 +00:00
Molecule AI Dev Engineer A (Kimi) e5d6e45ab1 fix(ci): arm64 pilot runs-on label matches Mac mini registration (#1679)
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
E2E API Smoke Test / detect-changes (pull_request) Waiting to run
E2E API Smoke Test / E2E API Smoke Test (pull_request) Blocked by required conditions
E2E Chat / detect-changes (pull_request) Waiting to run
E2E Chat / E2E Chat (pull_request) Blocked by required conditions
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Waiting to run
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Blocked by required conditions
Handlers Postgres Integration / detect-changes (pull_request) Waiting to run
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Blocked by required conditions
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Waiting to run
Lint curl status-code capture / Scan workflows for curl status-capture pollution (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 pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Waiting to run
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Waiting to run
lint-required-no-paths / lint-required-no-paths (pull_request) Waiting to run
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Waiting to run
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (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) Failing after 8s
CI / Detect changes (pull_request) Successful in 6s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 3s
CI / Python Lint & Test (pull_request) Successful in 3s
CI / all-required (pull_request) Failing after 40m17s
CI / Shellcheck (E2E scripts) (pull_request) Has been cancelled
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Has been cancelled
CI / Canvas (Next.js) (pull_request) Has been cancelled
Mac mini registers with labels self-hosted, macos-self-hosted-arm64,
arm64-darwin — no plain 'arm64'. The workflow was perpetually CANCELLED
because Gitea could not assign a runner.

Fixes #1679
2026-05-23 21:40:46 +00:00
claude-ceo-assistant a1cf56cdab fix(ci): use writable Docker config for canvas publish
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
CI / Canvas Deploy Reminder (pull_request) Blocked by required conditions
E2E API Smoke Test / E2E API Smoke Test (pull_request) Blocked by required conditions
E2E Chat / E2E Chat (pull_request) Blocked by required conditions
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Blocked by required conditions
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Blocked by required conditions
Harness Replays / Harness Replays (pull_request) Blocked by required conditions
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 3s
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 11s
E2E Chat / detect-changes (pull_request) Successful in 9s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 7s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 7s
Harness Replays / detect-changes (pull_request) Successful in 4s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m12s
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 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 1m8s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m29s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m9s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 3s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m37s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 4s
gate-check-v3 / gate-check (pull_request) Successful in 4s
qa-review / approved (pull_request) Successful in 3s
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
sop-tier-check / tier-check (pull_request) Successful in 3s
CI / Platform (Go) (pull_request) Successful in 2s
CI / Canvas (Next.js) (pull_request) Successful in 3s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 1s
CI / all-required (pull_request) Successful in 18m56s
audit-force-merge / audit (pull_request) Successful in 5s
2026-05-23 14:11:53 -07:00
agent-dev-a 436fae8949 fix: GitHub token HTTP timeout (split from #1700) (#1728)
ci-arm64-advisory / fast-checks (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
Block internal-flavored paths / Block forbidden paths (push) Successful in 4s
CI / Python Lint & Test (push) Successful in 5s
CI / Detect changes (push) Successful in 8s
E2E API Smoke Test / detect-changes (push) Successful in 10s
E2E Chat / detect-changes (push) Successful in 8s
Handlers Postgres Integration / detect-changes (push) Successful in 5s
Harness Replays / detect-changes (push) Successful in 5s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 10s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 5s
Secret scan / Scan diff for credential-shaped strings (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
publish-workspace-server-image / build-and-push (push) Successful in 3m8s
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
E2E API Smoke Test / E2E API Smoke Test (pull_request) Blocked by required conditions
E2E Chat / E2E Chat (pull_request) Blocked by required conditions
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Blocked by required conditions
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Blocked by required conditions
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Waiting to run
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Has started running
CI / Shellcheck (E2E scripts) (push) Successful in 2s
CI / Canvas (Next.js) (push) Successful in 3s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 1m47s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 12s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 2m6s
Harness Replays / Harness Replays (push) Successful in 7s
E2E Chat / E2E Chat (push) Successful in 4m20s
CI / Platform (Go) (push) Successful in 5m14s
CI / all-required (push) Successful in 9m39s
main-red-watchdog / watchdog (push) Successful in 2m4s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
CI / Detect changes (pull_request) Successful in 7s
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 6s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 6s
Handlers Postgres Integration / detect-changes (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 3s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m12s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 4s
gate-check-v3 / gate-check (pull_request) Successful in 3s
qa-review / approved (pull_request) Successful in 4s
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 3s
sop-checklist / review-refire (pull_request) Has been skipped
sop-tier-check / tier-check (pull_request) Successful in 4s
audit-force-merge / audit (pull_request) Waiting to run
CI / Canvas Deploy Reminder (push) Successful in 1s
gate-check-v3 / gate-check (push) Successful in 24s
publish-workspace-server-image / Production auto-deploy (push) Failing after 30m32s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 7s
ci-required-drift / drift (push) Successful in 1m8s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Has started running
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 5s
CI / Shellcheck (E2E scripts) (pull_request) Has been cancelled
CI / Canvas (Next.js) (pull_request) Has been cancelled
CI / Platform (Go) (pull_request) Has been cancelled
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Failing after 38m32s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 5s
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 21:01:09 +00:00
hongming 2d1a853bf9 Merge pull request 'feat(display): add desktop workspace creation flow' (#1732) from feat/1686-display-workspace-flow 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 / 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
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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
Block internal-flavored paths / Block forbidden paths (push) Successful in 11s
publish-canvas-image / Build & push canvas image (push) Failing after 7s
CI / Detect changes (push) Successful in 15s
CI / Python Lint & Test (push) Successful in 19s
E2E API Smoke Test / detect-changes (push) Successful in 8s
E2E Chat / detect-changes (push) Successful in 8s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 11s
publish-workspace-server-image / build-and-push (push) Has been cancelled
CI / all-required (push) Has been cancelled
E2E Staging SaaS (full lifecycle) / pr-validate (push) Successful in 32s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (push) Successful in 5m54s
2026-05-23 20:59:59 +00:00
claude-ceo-assistant 5551ef40e3 feat(display): add desktop workspace creation flow
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Waiting to run
CI / Python Lint & Test (pull_request) Successful in 14s
CI / Detect changes (pull_request) Successful in 17s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 18s
E2E API Smoke Test / detect-changes (pull_request) Successful in 31s
E2E Chat / detect-changes (pull_request) Successful in 31s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 29s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
Handlers Postgres Integration / detect-changes (pull_request) Successful in 11s
Harness Replays / 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 4s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 41s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 8s
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 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 1m5s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 7s
E2E Chat / E2E Chat (pull_request) Successful in 10s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 21s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m49s
Harness Replays / Harness Replays (pull_request) Successful in 5s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2m40s
CI / Platform (Go) (pull_request) Successful in 6m28s
CI / Canvas (Next.js) (pull_request) Successful in 7m35s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 10m21s
audit-force-merge / audit (pull_request) Successful in 13s
2026-05-23 13:46:20 -07:00
agent-dev-a 656176d511 fix(workspace-server): #1687 — alias GH_PAT to GH_TOKEN / GITHUB_TOKEN at provision time (#1697)
ci-arm64-advisory / fast-checks (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
Block internal-flavored paths / Block forbidden paths (push) Successful in 5s
CI / Detect changes (push) Successful in 12s
CI / Python Lint & Test (push) Successful in 12s
E2E API Smoke Test / detect-changes (push) Successful in 10s
E2E Chat / detect-changes (push) Successful in 8s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 7s
E2E Staging SaaS (full lifecycle) / pr-validate (push) Successful in 31s
Handlers Postgres Integration / detect-changes (push) Successful in 2s
Harness Replays / detect-changes (push) Successful in 4s
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 3s
publish-workspace-server-image / build-and-push (push) Successful in 4m42s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (push) Successful in 5m9s
CI / Canvas (Next.js) (push) Successful in 5s
CI / Shellcheck (E2E scripts) (push) Successful in 2s
main-red-watchdog / watchdog (push) Successful in 56s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 9s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 1m50s
Harness Replays / Harness Replays (push) Successful in 4s
gate-check-v3 / gate-check (push) Successful in 56s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 2m9s
E2E Chat / E2E Chat (push) Successful in 4m11s
CI / Platform (Go) (push) Successful in 5m22s
CI / all-required (push) Successful in 13m58s
publish-workspace-server-image / Production auto-deploy (push) Successful in 14m28s
CI / Canvas Deploy Reminder (push) Successful in 3s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 14s
ci-required-drift / drift (push) Successful in 1m11s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 9s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 4m19s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 6m25s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 22s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 20s
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 20:01:40 +00:00
agent-dev-a 1424af51fa fix(tests): make SSRF and admin-token tests hermetic against env vars (#1703)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Canvas (Next.js) (push) Blocked by required conditions
CI / Shellcheck (E2E scripts) (push) Blocked by required conditions
CI / Detect changes (push) Waiting to run
CI / Platform (Go) (push) Blocked by required conditions
CI / Canvas Deploy Reminder (push) Blocked by required conditions
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
E2E API Smoke Test / detect-changes (push) Waiting to run
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
E2E Staging SaaS (full lifecycle) / pr-validate (push) Waiting to run
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (push) Waiting to run
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
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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Has been cancelled
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (push) Successful in 53s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (push) Has been skipped
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 20:01:38 +00:00
agent-dev-a 7f0f33739b fix(e2e): #1644 Part A — peer-visibility scripts consume inline auth_token (#1680)
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Canvas (Next.js) (push) Blocked by required conditions
CI / Shellcheck (E2E scripts) (push) Blocked by required conditions
CI / Platform (Go) (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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Successful in 3m13s
Block internal-flavored paths / Block forbidden paths (push) Successful in 4s
CI / Python Lint & Test (push) Successful in 5s
CI / Detect changes (push) Successful in 7s
E2E API Smoke Test / detect-changes (push) Successful in 7s
E2E Chat / detect-changes (push) Successful in 7s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (push) Successful in 1m8s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 6s
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 3s
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
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (push) Has been cancelled
CI / all-required (push) Has been cancelled
publish-workspace-server-image / Production auto-deploy (push) Has been cancelled
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 3s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 4m20s
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 19:51:59 +00:00
agent-dev-a 339d73d9d4 fix(workspace-crud): add missing descRows.Err() check in CascadeDelete (#1714)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
CI / Detect changes (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 / detect-changes (push) Waiting to run
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
Harness Replays / Harness Replays (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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Has been cancelled
Harness Replays / detect-changes (push) Successful in 5s
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 19:51:47 +00:00
agent-dev-a 50fe4976e6 fix(provisioner): check io.ReadAll + json.Unmarshal errors in CP client (#1719)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
ci-arm64-advisory / fast-checks (push) Waiting to run
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
CI / Detect changes (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 / detect-changes (push) Waiting to run
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
Harness Replays / detect-changes (push) Waiting to run
Harness Replays / Harness Replays (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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Has been cancelled
E2E Staging SaaS (full lifecycle) / pr-validate (push) Successful in 56s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (push) Successful in 5m39s
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 19:51:45 +00:00
hongming e05fc4daae ci(arm64): ADVISORY Mac arm64 fast-check lane (Pilot ②, internal#418 relief) (#1442)
ci-arm64-advisory / fast-checks (push) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
Block internal-flavored paths / Block forbidden paths (push) Successful in 7s
CI / Python Lint & Test (push) Successful in 7s
CI / Detect changes (push) Successful in 12s
E2E API Smoke Test / detect-changes (push) Successful in 13s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (push) Successful in 9s
Handlers Postgres Integration / detect-changes (push) Successful in 14s
E2E Chat / detect-changes (push) Successful in 19s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 18s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (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
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 11s
CI / Platform (Go) (push) Successful in 7s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 7s
CI / Shellcheck (E2E scripts) (push) Successful in 8s
CI / Canvas (Next.js) (push) Successful in 11s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 7s
E2E Chat / E2E Chat (push) Successful in 9s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 12s
CI / all-required (push) Successful in 1m6s
CI / Canvas Deploy Reminder (push) Successful in 4s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (push) Successful in 1m29s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 1m43s
publish-workspace-server-image / build-and-push (push) Successful in 4m12s
publish-workspace-server-image / Production auto-deploy (push) Successful in 2m7s
Railway pin audit (drift detection) / Audit Railway env vars for drift-prone pins (push) Failing after 3s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (push) Failing after 1m11s
main-red-watchdog / watchdog (push) Successful in 2m5s
gate-check-v3 / gate-check (push) Successful in 25s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 9s
ci-required-drift / drift (push) Successful in 1m12s
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 4m19s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 5m55s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 3s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 5s
2026-05-23 11:08:57 +00:00
cp-be 6c7f66fa31 feat(workspace-server): kill DefaultModel + require model at Create (CTO 2026-05-22 SSOT) (#1667)
CI / Canvas Deploy Reminder (push) Blocked by required conditions
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Successful in 2m51s
Block internal-flavored paths / Block forbidden paths (push) Successful in 6s
CI / Detect changes (push) Successful in 8s
CI / Python Lint & Test (push) Successful in 10s
E2E Chat / detect-changes (push) Successful in 14s
E2E API Smoke Test / detect-changes (push) Successful in 16s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (push) Successful in 1m32s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 13s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (push) Failing after 2m9s
E2E Staging SaaS (full lifecycle) / pr-validate (push) Successful in 49s
Handlers Postgres Integration / detect-changes (push) Successful in 6s
Harness Replays / detect-changes (push) Successful in 5s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (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 13s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 9s
ci-required-drift / drift (push) Successful in 1m20s
E2E Staging External Runtime / E2E Staging External Runtime (push) Successful in 5m18s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (push) Successful in 5m10s
CI / Canvas (Next.js) (push) Successful in 3s
CI / Shellcheck (E2E scripts) (push) Successful in 17s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 2m0s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 2s
Harness Replays / Harness Replays (push) Successful in 4s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 1m52s
CI / Platform (Go) (push) Failing after 4m42s
E2E Chat / E2E Chat (push) Successful in 4m30s
publish-workspace-server-image / Production auto-deploy (push) Failing after 18m43s
CI / all-required (push) Failing after 14m56s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Successful in 8s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 5s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 4s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 4m47s
main-red-watchdog / watchdog (push) Successful in 1m57s
gate-check-v3 / gate-check (push) Successful in 25s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 6m19s
Co-authored-by: Molecule AI · cp-be <cp-be@agents.moleculesai.app>
Co-committed-by: Molecule AI · cp-be <cp-be@agents.moleculesai.app>
2026-05-23 10:15:18 +00:00
agent-dev-a acf784cd81 fix(mcp-tools): log scanPeers errors instead of silently dropping them (#1713)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (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
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
E2E API Smoke Test / detect-changes (push) Waiting to run
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 Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (push) Waiting to run
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (push) Waiting to run
E2E Staging Canvas (Playwright) / detect-changes (push) Waiting to run
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Blocked by required conditions
E2E Staging External Runtime / E2E Staging External Runtime (push) Waiting to run
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
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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Has been cancelled
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 10:15:14 +00:00
agent-dev-a 543519ed69 fix(channels): handle io.ReadAll error in Lark adapter (#1724)
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) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Block internal-flavored paths / Block forbidden paths (push) Successful in 7s
CI / Detect changes (push) Successful in 14s
CI / Python Lint & Test (push) Successful in 7s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 14s
E2E Chat / detect-changes (push) Successful in 14s
E2E API Smoke Test / detect-changes (push) Successful in 14s
Handlers Postgres Integration / detect-changes (push) Successful in 5s
Harness Replays / detect-changes (push) Successful in 5s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (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 / all-required (push) Has been cancelled
publish-workspace-server-image / build-and-push (push) Has been cancelled
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 53s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 11s
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 10:11:21 +00:00
agent-dev-a 010ec0f81b fix(server): add ReadHeaderTimeout to http.Server (#1715)
CI / Canvas Deploy Reminder (push) Blocked by required conditions
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Successful in 2m57s
Block internal-flavored paths / Block forbidden paths (push) Successful in 3s
CI / Detect changes (push) Successful in 6s
CI / Python Lint & Test (push) Successful in 3s
E2E API Smoke Test / detect-changes (push) Successful in 6s
E2E Chat / detect-changes (push) Successful in 6s
Handlers Postgres Integration / detect-changes (push) Successful in 5s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 7s
Harness Replays / detect-changes (push) Successful in 5s
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 4s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 5s
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 6s
Harness Replays / Harness Replays (push) Successful in 4s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 17s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 1m50s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 2m5s
E2E Chat / E2E Chat (push) Has been cancelled
CI / Platform (Go) (push) Has been cancelled
CI / all-required (push) Has been cancelled
publish-workspace-server-image / Production auto-deploy (push) Has been cancelled
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 4m30s
main-red-watchdog / watchdog (push) Successful in 38s
gate-check-v3 / gate-check (push) Successful in 44s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 4m53s
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 09:40:04 +00:00
agent-dev-a bc73f6397a fix(channels): handle io.ReadAll error in Discord adapter (#1725)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (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
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
E2E API Smoke Test / detect-changes (push) Waiting to run
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
Harness Replays / detect-changes (push) Waiting to run
Harness Replays / Harness Replays (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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Has been cancelled
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 09:39:27 +00:00
agent-dev-a e79a842859 fix(handlers): add missing rows.Err() checks in schedules/events listers (#1720)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (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
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
E2E API Smoke Test / detect-changes (push) Waiting to run
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
Harness Replays / detect-changes (push) Waiting to run
Harness Replays / Harness Replays (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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Has been cancelled
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 09:35:42 +00:00
agent-dev-a c3bcf903bd fix(channels): log and propagate json.Unmarshal errors in manager (#1717)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (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
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
E2E API Smoke Test / detect-changes (push) Waiting to run
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
Harness Replays / detect-changes (push) Waiting to run
Harness Replays / Harness Replays (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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Has been cancelled
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 09:35:16 +00:00
agent-dev-a 008a19dbdd fix(handlers): handle io.ReadAll error in traces proxy (#1721)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (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
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
E2E API Smoke Test / detect-changes (push) Waiting to run
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
Harness Replays / detect-changes (push) Waiting to run
Harness Replays / Harness Replays (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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Has been cancelled
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 09:35:06 +00:00
agent-dev-a e51dae906f fix(channels): handle io.ReadAll errors and close body in Slack adapter (#1722)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (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
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
E2E API Smoke Test / detect-changes (push) Waiting to run
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
Harness Replays / detect-changes (push) Waiting to run
Harness Replays / Harness Replays (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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Has been cancelled
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 09:35:02 +00:00
agent-dev-a f1f7492b66 fix(pgplugin): log JSON encode errors in writeJSON (#1727)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (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
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
E2E API Smoke Test / detect-changes (push) Waiting to run
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
Harness Replays / detect-changes (push) Waiting to run
Harness Replays / Harness Replays (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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Has been cancelled
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 09:34:50 +00:00
hongming 3161d43cec Merge pull request 'Add display control state to Display tab' (#1726)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (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
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
E2E API Smoke Test / detect-changes (push) Waiting to run
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
Harness Replays / detect-changes (push) Waiting to run
Harness Replays / Harness Replays (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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / Production auto-deploy (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Has been cancelled
publish-canvas-image / Build & push canvas image (push) Successful in 3m9s
2026-05-23 09:34:18 +00:00
agent-dev-a 29349e7af0 fix(memory): handle io.ReadAll error in decodeError (#1723)
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 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) Waiting to run
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Successful in 5m33s
Block internal-flavored paths / Block forbidden paths (push) Successful in 6s
CI / Python Lint & Test (push) Successful in 4s
CI / Detect changes (push) Successful in 7s
E2E API Smoke Test / detect-changes (push) Successful in 10s
Handlers Postgres Integration / detect-changes (push) Successful in 7s
E2E API Smoke Test / E2E API Smoke Test (push) Has been cancelled
E2E Chat / detect-changes (push) Has been cancelled
CI / all-required (push) Has been cancelled
E2E Staging Canvas (Playwright) / detect-changes (push) Has been cancelled
Harness Replays / detect-changes (push) Has been cancelled
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Has been cancelled
publish-workspace-server-image / Production auto-deploy (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
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 9s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 6m1s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 4m49s
Co-authored-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
Co-committed-by: agent-dev-a <agent-dev-a@agents.moleculesai.app>
2026-05-23 09:26:41 +00:00
hongming 78e1025f41 fix: scheduler detectResultKind allowlist + envelope kinds (#1716)
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (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
CI / Python Lint & Test (push) Waiting to run
CI / all-required (push) Waiting to run
E2E API Smoke Test / detect-changes (push) Waiting to run
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
Harness Replays / detect-changes (push) Waiting to run
Harness Replays / Harness Replays (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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Successful in 2m57s
publish-workspace-server-image / Production auto-deploy (push) Has been cancelled
ci-required-drift / drift (push) Successful in 1m18s
Co-authored-by: hongming-ceo-delegated <hongmingwang@moleculesai.app>
Co-committed-by: hongming-ceo-delegated <hongmingwang@moleculesai.app>
2026-05-23 09:15:34 +00:00
fullstack-engineer af3d98e478 Harden display control tab state handling
CI / Canvas Deploy Reminder (pull_request) Blocked by required conditions
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 10s
CI / Detect changes (pull_request) Successful in 16s
CI / Python Lint & Test (pull_request) Successful in 6s
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 10s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 9s
Harness Replays / detect-changes (pull_request) Successful in 9s
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
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 7s
qa-review / approved (pull_request) Successful in 6s
gate-check-v3 / gate-check (pull_request) Successful in 6s
security-review / approved (pull_request) Successful in 4s
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 1m2s
sop-checklist / all-items-acked (pull_request) acked: 7/7
sop-checklist / na-declarations (pull_request) N/A: (none)
CI / Platform (Go) (pull_request) Successful in 4s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 2s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 4s
E2E Chat / E2E Chat (pull_request) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 4s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2s
Harness Replays / Harness Replays (pull_request) Successful in 2s
CI / Canvas (Next.js) (pull_request) Successful in 7m23s
CI / all-required (pull_request) Successful in 21m55s
audit-force-merge / audit (pull_request) Successful in 6s
2026-05-23 01:46:46 -07:00
fullstack-engineer 321d051c9f Add display control state to Display tab
Block internal-flavored paths / Block forbidden paths (pull_request) Waiting to run
CI / Detect changes (pull_request) Waiting to run
CI / Platform (Go) (pull_request) Blocked by required conditions
CI / Canvas (Next.js) (pull_request) Blocked by required conditions
CI / Shellcheck (E2E scripts) (pull_request) Blocked by required conditions
CI / Canvas Deploy Reminder (pull_request) Blocked by required conditions
CI / Python Lint & Test (pull_request) Waiting to run
CI / all-required (pull_request) Waiting to run
E2E API Smoke Test / detect-changes (pull_request) Waiting to run
E2E API Smoke Test / E2E API Smoke Test (pull_request) Blocked by required conditions
E2E Chat / detect-changes (pull_request) Waiting to run
E2E Chat / E2E Chat (pull_request) Blocked by required conditions
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Waiting to run
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Blocked by required conditions
Handlers Postgres Integration / detect-changes (pull_request) Waiting to run
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Blocked by required conditions
Harness Replays / detect-changes (pull_request) Waiting to run
Harness Replays / Harness Replays (pull_request) Blocked by required conditions
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
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (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
2026-05-23 01:40:49 -07:00
65 changed files with 2055 additions and 319 deletions
+5
View File
@@ -128,6 +128,7 @@ fi
PR_AUTHOR=$(jq -r '.user.login // ""' "$PR_JSON")
PR_HEAD_SHA=$(jq -r '.head.sha // ""' "$PR_JSON")
PR_BASE_REF=$(jq -r '.base.ref // ""' "$PR_JSON")
PR_BASE_SHA=$(jq -r '.base.sha // ""' "$PR_JSON")
PR_STATE=$(jq -r '.state // ""' "$PR_JSON")
DEFAULT_BRANCH="${DEFAULT_BRANCH:-main}"
debug "pr_author=${PR_AUTHOR} pr_head=${PR_HEAD_SHA:0:7} pr_base=${PR_BASE_REF} pr_state=${PR_STATE}"
@@ -136,6 +137,10 @@ if [ "$PR_STATE" != "open" ]; then
echo "::notice::PR ${PR_NUMBER} is ${PR_STATE} — exiting 0 (closed PRs do not gate)"
exit 0
fi
if [ "$PR_HEAD_SHA" = "$PR_BASE_SHA" ]; then
echo "::notice::PR ${PR_NUMBER} has no diff (head == base) — exiting 0 (empty PRs do not gate)"
exit 0
fi
if [ "$PR_BASE_REF" != "$DEFAULT_BRANCH" ]; then
echo "::notice::PR ${PR_NUMBER} targets ${PR_BASE_REF:-<unknown>} not ${DEFAULT_BRANCH}${TEAM}-review gate not applicable"
exit 0
+187
View File
@@ -0,0 +1,187 @@
# ci-arm64-advisory — Mac arm64 self-hosted ADVISORY fast-check lane.
#
# === WHY ===
#
# The amd64 Gitea runner pool (molecule-runner-1..20) is queue-contended
# (internal#418). This lane offloads the *genuinely container-independent*
# fast checks (Go build/vet/lint, shellcheck, Python lint) onto the Mac
# arm64 self-hosted runner so developers get a fast arm64 signal WITHOUT
# adding load to the starved amd64 pool — capability-honestly, as an
# additive pilot. Pilot ② of the Mac-CI strategy (CTO-delegated 2026-05-17).
#
# === NON-NEGOTIABLE SAFETY CONTRACT (the prime directive) ===
#
# This lane is **ADVISORY ONLY**. It is provably incapable of hanging a
# merge. Concretely:
#
# 1. It is a SEPARATE workflow file. `ci.yml` is byte-for-byte
# untouched by this PR. The `CI / all-required` aggregator sentinel
# and the five contexts it polls
# (`CI / Detect changes|Platform (Go)|Canvas (Next.js)|
# Shellcheck (E2E scripts)|Python Lint & Test (pull_request)`)
# are unchanged. The canonical required gate stays 100% on the
# existing amd64 pool.
#
# 2. The context this workflow emits is
# `ci-arm64-advisory / fast-checks (pull_request)`. That string is
# DELIBERATELY NOT present in, and this PR does NOT add it to:
# - branch_protections/{main,staging}.status_check_contexts
# (DB-verified pb 86/75 = exactly
# ["CI / all-required (pull_request)",
# "sop-checklist / all-items-acked (pull_request)"])
# - audit-force-merge.yml REQUIRED_CHECKS env
# - ci.yml `all-required` sentinel's hardcoded `required[]` list
# Branch protection therefore never waits on this context. If the
# Mac runner is absent / offline / removed, this workflow's status
# simply never appears — and because nothing requires it, every
# merge proceeds exactly as it does today. There is no path by
# which a missing/red arm64 status blocks a merge.
#
# 3. `continue-on-error: true` on the job — even a genuine arm64-only
# failure (toolchain drift, arch-specific test flake) is surfaced
# as information, never as a merge blocker, for the duration of
# the pilot.
#
# 4. The job carries a `github.event_name` `if:` gate. Beyond its
# functional purpose this also keeps the job OUT of
# `ci-required-drift.py:ci_job_names()` (which excludes
# `github.event_name`/`github.ref`-gated jobs), so the hourly
# ci-required-drift sentinel's F1 ("job not under sentinel needs")
# cannot ever flag this advisory job. F2/F3 are untouched because
# this context is absent from BP and from REQUIRED_CHECKS.
# `lint-bp-context-emit-match` only fails on BP→emitter gaps; an
# emitter without a BP context is explicitly informational there.
#
# === RUNNER TARGETING ===
#
# The Mac runner is `hongming-pc-runner-1`. The bare `self-hosted`
# label is POLLUTED in this Gitea instance: molecule-runner-1..20
# (the contended amd64 pool) also advertise `self-hosted`. Targeting
# bare `self-hosted` would route back onto the very pool we are trying
# to relieve — and onto amd64 hardware. We therefore require an
# AND-set of labels that ONLY the Mac satisfies. `macos-self-hosted`
# is Mac-exclusive (the amd64 pool does not carry it). Until the
# label-install burst (a10862b2) lands `self-hosted`+`macos-self-hosted`
# on the Mac, the runner's current unique label `hongming-pc-laptop`
# is also listed; AND-semantics over the labels a runner advertises
# means a job requiring [self-hosted, macos-self-hosted] can ONLY be
# claimed once the Mac advertises both. If neither label set is yet
# present on the Mac, the workflow stays queued harmlessly and is
# garbage-collected by the normal stale-run reaper — it blocks nothing
# (see safety contract point 2).
#
# === ROLLBACK ===
#
# Delete this single file (`git rm .gitea/workflows/ci-arm64-advisory.yml`)
# and merge. No branch-protection edit, no ci.yml edit, no
# REQUIRED_CHECKS edit is required to roll back, because none were made
# to roll forward. Zero blast radius either direction.
name: ci-arm64-advisory
on:
push:
branches: [main, staging]
pull_request:
branches: [main, staging]
# Per-ref cancel: a newer commit on the same ref supersedes the older
# advisory run. Distinct from ci.yml's `ci-${ref}` group so this lane
# never cancels (or is cancelled by) the canonical required CI.
concurrency:
group: ci-arm64-advisory-${{ github.ref }}
cancel-in-progress: true
env:
GITHUB_SERVER_URL: https://git.moleculesai.app
jobs:
fast-checks:
name: fast-checks
# AND-set: only the Mac arm64 runner advertises macos-self-hosted.
# See "RUNNER TARGETING" header note for why bare self-hosted is unsafe.
runs-on: [self-hosted, macos-self-hosted]
# ADVISORY: never blocks. See safety contract point 3. mc#774
# internal#418 — tracked: arm64 advisory pilot, non-gating by design.
continue-on-error: true
# event_name gate: functional (only meaningful on push/PR) AND keeps
# this job out of ci-required-drift.py:ci_job_names() so F1 can never
# flag it. See safety contract point 4.
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
timeout-minutes: 20
steps:
- name: Provenance — advisory lane, non-gating
run: |
echo "This is the arm64 ADVISORY fast-check lane."
echo "It does NOT gate merges. Canonical required CI is ci.yml"
echo "on the amd64 pool. Arch: $(uname -m) on $(uname -s)."
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# ---- Go: build + vet + lint (container-independent: needs only the
# Go toolchain; no amd64 ECR image, no docker-in-job). Race-detector
# unit-test + coverage gates are deliberately NOT duplicated here —
# those stay authoritative on amd64 ci.yml `Platform (Go)`. This lane
# is fast-feedback for the compile/vet/lint surface only. ----
- name: Setup Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: 'stable'
- name: Go build + vet (workspace-server)
working-directory: workspace-server
run: |
go mod download
go build ./cmd/server
go vet ./...
- name: golangci-lint (workspace-server)
working-directory: workspace-server
run: |
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
"$(go env GOPATH)/bin/golangci-lint" run --timeout 3m ./...
# ---- Shellcheck (container-independent: shellcheck binary only).
# Mirrors ci.yml `Shellcheck (E2E scripts)` bulk pass scope. ----
- name: Install shellcheck (arm64)
run: |
if ! command -v shellcheck >/dev/null 2>&1; then
echo "shellcheck not preinstalled on this self-hosted runner."
echo "Attempting Homebrew install (Mac arm64)."
brew install shellcheck || {
echo "::warning::shellcheck unavailable on runner; advisory shellcheck skipped."
exit 0
}
fi
shellcheck --version
- name: Shellcheck tests/e2e + infra/scripts
run: |
command -v shellcheck >/dev/null 2>&1 || { echo "skip"; exit 0; }
find tests/e2e infra/scripts -type f -name '*.sh' -print0 \
| xargs -0 shellcheck --severity=warning
# ---- Python lint/compile (container-independent: CPython only).
# Lint + import-compile surface; the authoritative pytest + coverage
# floors stay on amd64 ci.yml `Python Lint & Test`. ----
- name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.11'
- name: Python byte-compile (workspace)
working-directory: workspace
run: |
python -m pip install --quiet ruff || true
python -m compileall -q .
if command -v ruff >/dev/null 2>&1; then
ruff check . || echo "::warning::ruff findings (advisory only)"
fi
- name: Advisory summary
if: always()
run: |
{
echo "## arm64 advisory fast-checks complete"
echo ""
echo "This lane is **advisory** — it does not gate merges."
echo "Authoritative required CI remains \`CI / all-required\`"
echo "on the amd64 pool (\`ci.yml\`, unchanged by this PR)."
} >> "$GITHUB_STEP_SUMMARY"
@@ -25,7 +25,7 @@ permissions:
jobs:
shellcheck-arm64:
name: shellcheck-arm64 (pilot)
runs-on: [self-hosted, arm64]
runs-on: [self-hosted, arm64-darwin]
# NOT a required check; safe to sit pending until Mac runner is up.
# If the Mac runner has trouble pulling actions/checkout we fall
# back to a plain git clone (see step 'fallback clone').
@@ -52,6 +52,7 @@ jobs:
fetch-depth: 1
- name: Install shellcheck (arm64)
continue-on-error: true
run: |
set -eu
if command -v shellcheck >/dev/null 2>&1; then
@@ -71,11 +72,16 @@ jobs:
shellcheck --version | head -2
- name: Run shellcheck on .gitea/scripts/*.sh
continue-on-error: true
run: |
set -eu
# Only the scripts we control under .gitea/scripts. Pilot
# scope is intentionally narrow — broaden in a follow-up
# once the lane is proven.
if ! command -v shellcheck >/dev/null 2>&1; then
echo "WARN: shellcheck binary not found — skipping (pilot mode)"
exit 0
fi
mapfile -t TARGETS < <(find .gitea/scripts -maxdepth 2 -type f -name '*.sh' | sort)
if [ "${#TARGETS[@]}" -eq 0 ]; then
echo "No .sh files found under .gitea/scripts — nothing to check"
+11
View File
@@ -73,6 +73,17 @@ jobs:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Keep Docker auth/buildx state inside the job temp dir. Publish
# runners can inherit a HOME/DOCKER_CONFIG path that is host-owned
# and not writable from the job container; docker login otherwise
# fails before the image build starts.
- name: Prepare writable Docker config
run: |
set -euo pipefail
export DOCKER_CONFIG="$RUNNER_TEMP/docker-config"
mkdir -p "$DOCKER_CONFIG/buildx/certs"
echo "DOCKER_CONFIG=$DOCKER_CONFIG" >> "$GITHUB_ENV"
- name: Log in to ECR
env:
IMAGE_NAME: ${{ env.IMAGE_NAME }}
@@ -234,6 +234,8 @@ jobs:
name: Production auto-deploy
needs: build-and-push
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
# Side-effect deploy only; image publish success is the durable artifact. mc#774
continue-on-error: true
# Publish/release lane (internal#462) — production deploy of a merged
# fix; reserved capacity, never queued behind PR-CI.
runs-on: publish
@@ -33,6 +33,8 @@ interface HermesProvider {
models: string[];
}
const DEFAULT_CREATE_MODEL = "anthropic:claude-opus-4-7";
// All providers supported by Hermes runtime via providers.resolve_provider().
// `defaultModel` is the slug injected into the workspace provision request
// when the user picks this provider — template-hermes's derive-provider.sh
@@ -68,6 +70,10 @@ export function CreateWorkspaceButton() {
const [creating, setCreating] = useState(false);
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 [displayResolution, setDisplayResolution] = useState("1920x1080");
// Templates fetched from /api/templates — drives the dynamic provider
// filter below. Same data source ConfigTab uses (PR #2454). When the
// selected template declares `runtime_config.providers` in its
@@ -223,6 +229,10 @@ export function CreateWorkspaceButton() {
setParentId("");
setBudgetLimit("");
setError(null);
setDisplayEnabled(false);
setDisplayInstanceType("t3.xlarge");
setDisplayRootGB("80");
setDisplayResolution("1920x1080");
setHermesProvider("anthropic");
setExternalRuntime("external");
setHermesApiKey("");
@@ -264,6 +274,8 @@ export function CreateWorkspaceButton() {
const parsedBudget = budgetLimit.trim()
? parseFloat(budgetLimit)
: null;
const [displayWidth, displayHeight] = displayResolution.split("x").map((v) => parseInt(v, 10));
const parsedRootGB = parseInt(displayRootGB, 10);
const createResp = await api.post<{
id: string;
@@ -280,6 +292,21 @@ export function CreateWorkspaceButton() {
tier,
parent_id: parentId || undefined,
budget_limit: parsedBudget,
...(!isExternal && !isHermes ? { model: DEFAULT_CREATE_MODEL } : {}),
...(displayEnabled
? {
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,
},
},
}
: {}),
canvas: { x: Math.random() * 400 + 100, y: Math.random() * 300 + 100 },
// Runtime=external flips the backend into awaiting-agent mode:
// no container provisioning, token minted, connection payload
@@ -447,6 +474,73 @@ export function CreateWorkspaceButton() {
</div>
</div>
{!isExternal && (
<div className="rounded-lg border border-line/50 bg-surface-card/40 p-3">
<div className="mb-2 text-[11px] font-medium text-ink-mid">
Container Config
</div>
<label className="flex items-center justify-between gap-3">
<span className="text-xs font-medium text-ink">Display</span>
<input
type="checkbox"
checked={displayEnabled}
onChange={(e) => setDisplayEnabled(e.target.checked)}
aria-label="Enable display"
className="h-4 w-4"
/>
</label>
{displayEnabled && (
<div className="mt-3 grid grid-cols-2 gap-2">
<div>
<label htmlFor="display-instance-type" className="mb-1 block text-[11px] text-ink-mid">
Instance
</label>
<select
id="display-instance-type"
value={displayInstanceType}
onChange={(e) => setDisplayInstanceType(e.target.value)}
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-2 py-2 text-xs text-ink focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors"
>
<option value="t3.large">t3.large</option>
<option value="t3.xlarge">t3.xlarge</option>
<option value="m6i.xlarge">m6i.xlarge</option>
<option value="c6i.xlarge">c6i.xlarge</option>
</select>
</div>
<div>
<label htmlFor="display-root-gb" className="mb-1 block text-[11px] text-ink-mid">
Disk GB
</label>
<input
id="display-root-gb"
type="number"
min="30"
max="500"
value={displayRootGB}
onChange={(e) => setDisplayRootGB(e.target.value)}
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-2 py-2 text-xs text-ink focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors"
/>
</div>
<div className="col-span-2">
<label htmlFor="display-resolution" className="mb-1 block text-[11px] text-ink-mid">
Resolution
</label>
<select
id="display-resolution"
value={displayResolution}
onChange={(e) => setDisplayResolution(e.target.value)}
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-2 py-2 text-xs text-ink focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors"
>
<option value="1920x1080">1920 x 1080</option>
<option value="1600x900">1600 x 900</option>
<option value="1280x720">1280 x 720</option>
</select>
</div>
</div>
)}
</div>
)}
<div>
<label className="text-[11px] text-ink-mid block mb-1">
Parent Workspace
@@ -123,6 +123,46 @@ describe("CreateWorkspaceDialog", () => {
expect(body.parent_id).toBeUndefined();
});
it("omits compute config by default", async () => {
await openDialog();
fireEvent.change(screen.getByPlaceholderText("e.g. SEO Agent"), {
target: { value: "Plain 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");
});
it("sends display compute profile when desktop display is enabled", async () => {
await openDialog();
fireEvent.change(screen.getByPlaceholderText("e.g. SEO Agent"), {
target: { value: "Desktop Agent" },
});
fireEvent.click(screen.getByLabelText("Enable display"));
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("anthropic:claude-opus-4-7");
expect(body.compute).toEqual({
instance_type: "t3.xlarge",
volume: { root_gb: 80 },
display: {
mode: "desktop-control",
protocol: "novnc",
width: 1920,
height: 1080,
},
});
});
it("renders gracefully when GET /workspaces fails", async () => {
mockGet.mockRejectedValueOnce(new Error("Network error"));
await openDialog();
+232 -16
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { api } from "@/lib/api";
interface DisplayStatus {
@@ -11,6 +11,13 @@ interface DisplayStatus {
protocol?: string;
width?: number;
height?: number;
viewer_url?: string;
}
interface DisplayControlStatus {
controller: "none" | "user" | "agent";
controlled_by?: string;
expires_at?: string;
}
interface Props {
@@ -19,25 +26,99 @@ interface Props {
export function DisplayTab({ workspaceId }: Props) {
const [status, setStatus] = useState<DisplayStatus | null>(null);
const [control, setControl] = useState<DisplayControlStatus | null>(null);
const [error, setError] = useState<string | null>(null);
const [controlError, setControlError] = useState<string | null>(null);
const [controlBusy, setControlBusy] = useState(false);
const requestGeneration = useRef(0);
useEffect(() => {
const generation = requestGeneration.current + 1;
requestGeneration.current = generation;
let cancelled = false;
setStatus(null);
setControl(null);
setError(null);
api
.get<DisplayStatus>(`/workspaces/${workspaceId}/display`)
.then((data) => {
if (!cancelled) setStatus(data);
})
.catch((err) => {
if (!cancelled) setError(err instanceof Error ? err.message : "Display status unavailable");
});
setControlError(null);
setControlBusy(false);
async function load() {
try {
const displayStatus = await api.get<DisplayStatus>(`/workspaces/${workspaceId}/display`);
if (cancelled || requestGeneration.current !== generation) return;
setStatus(displayStatus);
if (displayStatus.reason === "display_not_enabled") return;
try {
const displayControl = await api.get<DisplayControlStatus>(`/workspaces/${workspaceId}/display/control`);
if (!cancelled && requestGeneration.current === generation) setControl(displayControl);
} catch (err) {
if (!cancelled && requestGeneration.current === generation) {
setControl(null);
setControlError("Display control unavailable");
}
}
} catch (err) {
if (!cancelled && requestGeneration.current === generation) setError("The display status could not be loaded.");
}
}
load();
return () => {
cancelled = true;
};
}, [workspaceId]);
const acquireControl = async () => {
const generation = requestGeneration.current;
const controlPath = `/workspaces/${workspaceId}/display/control`;
setControlBusy(true);
setControlError(null);
try {
const next = await api.post<DisplayControlStatus>(`${controlPath}/acquire`, {
controller: "user",
ttl_seconds: 300,
});
if (requestGeneration.current !== generation) return;
setControl(next);
} catch (err) {
if (requestGeneration.current !== generation) return;
setControlError("Failed to take control");
try {
const latest = await api.get<DisplayControlStatus>(controlPath);
if (requestGeneration.current !== generation) return;
setControl(latest);
} catch {
if (requestGeneration.current !== generation) return;
setControl(null);
}
} finally {
if (requestGeneration.current === generation) setControlBusy(false);
}
};
const releaseControl = async () => {
const generation = requestGeneration.current;
const controlPath = `/workspaces/${workspaceId}/display/control`;
setControlBusy(true);
setControlError(null);
try {
const next = await api.post<DisplayControlStatus>(`${controlPath}/release`, {});
if (requestGeneration.current !== generation) return;
setControl(next);
} catch (err) {
if (requestGeneration.current !== generation) return;
setControlError("Failed to release control");
try {
const latest = await api.get<DisplayControlStatus>(controlPath);
if (requestGeneration.current !== generation) return;
setControl(latest);
} catch {
if (requestGeneration.current !== generation) return;
setControl(null);
}
} finally {
if (requestGeneration.current === generation) setControlBusy(false);
}
};
if (error) {
return (
<div className="p-5">
@@ -81,16 +162,151 @@ export function DisplayTab({ workspaceId }: Props) {
: "This workspace has display configuration, but the desktop session infrastructure is not configured yet."}
</p>
{!isNotEnabled && (
<dl className="mt-5 grid grid-cols-2 gap-x-4 gap-y-2 text-left text-[11px]">
<dt className="text-ink-mid">Mode</dt>
<dd className="font-mono text-ink">{status.mode || "unknown"}</dd>
<dt className="text-ink-mid">Status</dt>
<dd className="font-mono text-ink">{status.status || "unknown"}</dd>
</dl>
<>
<dl className="mt-5 grid grid-cols-2 gap-x-4 gap-y-2 text-left text-[11px]">
<dt className="text-ink-mid">Mode</dt>
<dd className="font-mono text-ink">{status.mode || "unknown"}</dd>
<dt className="text-ink-mid">Status</dt>
<dd className="font-mono text-ink">{status.status || "unknown"}</dd>
</dl>
<div className="mt-5 w-full max-w-xs border-t border-line/50 pt-4">
{control ? (
<div className="flex items-center justify-between gap-3 text-left">
<div className="min-w-0">
<p className="text-[11px] font-medium text-ink">
{control.controller === "none"
? "No active controller"
: `Controlled by ${displayControlActorLabel(control)}`}
</p>
{control.expires_at && (
<p className="mt-1 truncate font-mono text-[10px] text-ink-mid">
Until {new Date(control.expires_at).toLocaleTimeString()}
</p>
)}
{controlError && <p className="mt-1 text-[10px] leading-snug text-red-200">{controlError}</p>}
</div>
{control.controller === "none" && (
<button
type="button"
onClick={acquireControl}
disabled={controlBusy}
className="h-8 shrink-0 rounded border border-line bg-surface px-3 text-[11px] font-medium text-ink hover:bg-surface-elevated disabled:cursor-not-allowed disabled:opacity-60"
>
Take control
</button>
)}
</div>
) : (
<div className="text-left">
{!controlError && (
<div className="h-8 rounded border border-line/40 bg-surface-sunken/30 motion-safe:animate-pulse" />
)}
{controlError && <p className="mt-2 text-[10px] leading-snug text-red-200">{controlError}</p>}
</div>
)}
</div>
</>
)}
</div>
);
}
return null;
return (
<div className="flex h-full min-h-[360px] flex-col bg-surface-sunken/30">
<div className="flex items-center justify-between gap-3 border-b border-line/50 px-4 py-3">
<div className="min-w-0">
<h3 className="text-sm font-medium text-ink">Desktop</h3>
<p className="mt-0.5 font-mono text-[10px] text-ink-mid">
{status.mode || "desktop-control"} · {status.protocol || "display"}
</p>
</div>
<DisplayControlBar
control={control}
controlBusy={controlBusy}
controlError={controlError}
onAcquire={acquireControl}
onRelease={releaseControl}
/>
</div>
{status.viewer_url ? (
<iframe
title="Workspace desktop"
src={status.viewer_url}
className="min-h-0 flex-1 border-0 bg-black"
allow="clipboard-read; clipboard-write; fullscreen; pointer-lock"
referrerPolicy="no-referrer"
/>
) : (
<div className="flex flex-1 items-center justify-center p-8 text-center">
<div>
<h3 className="mb-1.5 text-sm font-medium text-ink">Display session is not ready.</h3>
<p className="max-w-xs text-[11px] leading-relaxed text-ink-mid">
This workspace has display configuration, but the desktop session URL is not available yet.
</p>
</div>
</div>
)}
</div>
);
}
function DisplayControlBar({
control,
controlBusy,
controlError,
onAcquire,
onRelease,
}: {
control: DisplayControlStatus | null;
controlBusy: boolean;
controlError: string | null;
onAcquire: () => void;
onRelease: () => void;
}) {
return (
<div className="flex min-w-0 items-center gap-3">
{control && (
<div className="min-w-0 text-right">
<p className="truncate text-[11px] font-medium text-ink">
{control.controller === "none"
? "No active controller"
: `Controlled by ${displayControlActorLabel(control)}`}
</p>
{control.expires_at && (
<p className="mt-0.5 truncate font-mono text-[10px] text-ink-mid">
Until {new Date(control.expires_at).toLocaleTimeString()}
</p>
)}
{controlError && <p className="mt-0.5 text-[10px] text-red-200">{controlError}</p>}
</div>
)}
{control?.controller === "none" && (
<button
type="button"
onClick={onAcquire}
disabled={controlBusy}
className="h-8 shrink-0 rounded border border-line bg-surface px-3 text-[11px] font-medium text-ink hover:bg-surface-elevated disabled:cursor-not-allowed disabled:opacity-60"
>
Take control
</button>
)}
{control?.controller === "user" && control.controlled_by === "admin-token" && (
<button
type="button"
onClick={onRelease}
disabled={controlBusy}
className="h-8 shrink-0 rounded border border-line bg-surface px-3 text-[11px] font-medium text-ink hover:bg-surface-elevated disabled:cursor-not-allowed disabled:opacity-60"
>
Release
</button>
)}
</div>
);
}
function displayControlActorLabel(control: DisplayControlStatus): string {
if (control.controller === "agent") return "Agent";
if (control.controlled_by === "admin-token") return "Admin";
if (control.controlled_by?.startsWith("org-token:")) return "Automation";
return "User";
}
@@ -1,12 +1,13 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
const { mockGet } = vi.hoisted(() => ({ mockGet: vi.fn() }));
const { mockGet, mockPost } = vi.hoisted(() => ({ mockGet: vi.fn(), mockPost: vi.fn() }));
vi.mock("@/lib/api", () => ({
api: {
get: mockGet,
post: mockPost,
},
}));
@@ -14,7 +15,9 @@ import { DisplayTab } from "../DisplayTab";
describe("DisplayTab", () => {
beforeEach(() => {
cleanup();
mockGet.mockReset();
mockPost.mockReset();
});
it("renders unavailable state for non-display workspaces", async () => {
@@ -29,5 +32,274 @@ describe("DisplayTab", () => {
expect(screen.getByText("Display is not enabled for this workspace.")).toBeTruthy();
});
expect(mockGet).toHaveBeenCalledWith("/workspaces/ws-no-display/display");
expect(mockGet).not.toHaveBeenCalledWith("/workspaces/ws-no-display/display/control");
});
it("renders control acquisition for display-configured workspaces", async () => {
mockGet
.mockResolvedValueOnce({
available: false,
reason: "display_session_unavailable",
mode: "desktop-control",
status: "not_configured",
})
.mockResolvedValueOnce({
controller: "none",
});
mockPost.mockResolvedValueOnce({
controller: "user",
controlled_by: "admin-token",
expires_at: "2026-05-23T08:48:27Z",
});
render(<DisplayTab workspaceId="ws-display" />);
await waitFor(() => {
expect(screen.getByRole("button", { name: "Take control" })).toBeTruthy();
});
expect(mockGet).toHaveBeenCalledWith("/workspaces/ws-display/display");
expect(mockGet).toHaveBeenCalledWith("/workspaces/ws-display/display/control");
fireEvent.click(screen.getByRole("button", { name: "Take control" }));
await waitFor(() => {
expect(screen.getByText("Controlled by Admin")).toBeTruthy();
});
expect(mockPost).toHaveBeenCalledWith("/workspaces/ws-display/display/control/acquire", {
controller: "user",
ttl_seconds: 300,
});
});
it("renders the desktop stream when a display session is available", async () => {
mockGet
.mockResolvedValueOnce({
available: true,
mode: "desktop-control",
protocol: "dcv",
width: 1920,
height: 1080,
viewer_url: "https://display.example.test/session/ws-display",
})
.mockResolvedValueOnce({
controller: "none",
});
render(<DisplayTab workspaceId="ws-display" />);
await waitFor(() => {
expect(screen.getByTitle("Workspace desktop")).toBeTruthy();
});
const frame = screen.getByTitle("Workspace desktop") as HTMLIFrameElement;
expect(frame.src).toBe("https://display.example.test/session/ws-display");
expect(screen.getByRole("button", { name: "Take control" })).toBeTruthy();
});
it("releases user display control", async () => {
mockGet
.mockResolvedValueOnce({
available: true,
mode: "desktop-control",
protocol: "dcv",
viewer_url: "https://display.example.test/session/ws-display",
})
.mockResolvedValueOnce({
controller: "user",
controlled_by: "admin-token",
expires_at: "2026-05-23T08:48:27Z",
});
mockPost.mockResolvedValueOnce({
controller: "none",
});
render(<DisplayTab workspaceId="ws-display" />);
await waitFor(() => {
expect(screen.getByRole("button", { name: "Release" })).toBeTruthy();
});
fireEvent.click(screen.getByRole("button", { name: "Release" }));
await waitFor(() => {
expect(screen.getByRole("button", { name: "Take control" })).toBeTruthy();
});
expect(mockPost).toHaveBeenCalledWith("/workspaces/ws-display/display/control/release", {});
});
it("renders active display control locks as observe-only", async () => {
mockGet
.mockResolvedValueOnce({
available: false,
reason: "display_session_unavailable",
mode: "desktop-control",
status: "not_configured",
})
.mockResolvedValueOnce({
controller: "agent",
controlled_by: "sidecar",
expires_at: "2026-05-23T08:48:27Z",
});
render(<DisplayTab workspaceId="ws-display" />);
await waitFor(() => {
expect(screen.getByText("Controlled by Agent")).toBeTruthy();
});
expect(screen.queryByRole("button", { name: "Release" })).toBeNull();
expect(screen.queryByRole("button", { name: "Take control" })).toBeNull();
expect(mockPost).not.toHaveBeenCalled();
});
it("labels org-token display control locks as automation", async () => {
mockGet
.mockResolvedValueOnce({
available: false,
reason: "display_session_unavailable",
mode: "desktop-control",
status: "not_configured",
})
.mockResolvedValueOnce({
controller: "user",
controlled_by: "org-token:abc123",
expires_at: "2026-05-23T08:48:27Z",
});
render(<DisplayTab workspaceId="ws-display" />);
await waitFor(() => {
expect(screen.getByText("Controlled by Automation")).toBeTruthy();
});
expect(screen.queryByText("org-token:abc123")).toBeNull();
expect(screen.queryByRole("button", { name: "Take control" })).toBeNull();
});
it("refreshes display control state after failed acquisition", async () => {
mockGet
.mockResolvedValueOnce({
available: false,
reason: "display_session_unavailable",
mode: "desktop-control",
status: "not_configured",
})
.mockResolvedValueOnce({
controller: "none",
})
.mockResolvedValueOnce({
controller: "agent",
controlled_by: "sidecar",
expires_at: "2026-05-23T08:48:27Z",
});
mockPost.mockRejectedValueOnce(new Error("API POST /workspaces/ws-display/display/control/acquire: 409 conflict"));
render(<DisplayTab workspaceId="ws-display" />);
await waitFor(() => {
expect(screen.getByRole("button", { name: "Take control" })).toBeTruthy();
});
fireEvent.click(screen.getByRole("button", { name: "Take control" }));
await waitFor(() => {
expect(screen.getByText("Controlled by Agent")).toBeTruthy();
});
expect(screen.getByText("Failed to take control")).toBeTruthy();
expect(mockGet).toHaveBeenCalledWith("/workspaces/ws-display/display/control");
expect(mockGet).toHaveBeenCalledTimes(3);
expect(mockPost).toHaveBeenCalledWith("/workspaces/ws-display/display/control/acquire", {
controller: "user",
ttl_seconds: 300,
});
});
it("keeps display status visible without takeover actions when control status fails", async () => {
mockGet
.mockResolvedValueOnce({
available: false,
reason: "display_session_unavailable",
mode: "desktop-control",
status: "not_configured",
})
.mockRejectedValueOnce(new Error("API GET /workspaces/ws-display/display/control: 401 unauthorized"));
render(<DisplayTab workspaceId="ws-display" />);
await waitFor(() => {
expect(screen.getByText("Display session is not ready.")).toBeTruthy();
});
expect(screen.queryByRole("button", { name: "Take control" })).toBeNull();
expect(screen.getByText("Display control unavailable")).toBeTruthy();
});
it("does not render raw display status errors", async () => {
mockGet.mockRejectedValueOnce(new Error("API GET /workspaces/ws-display/display: 500 secret backend details"));
render(<DisplayTab workspaceId="ws-display" />);
await waitFor(() => {
expect(screen.getByText("Display status unavailable")).toBeTruthy();
});
expect(screen.queryByText(/secret backend details/)).toBeNull();
});
it("ignores stale acquire responses after workspace changes", async () => {
const acquire = deferred<{ controller: "user"; controlled_by: string; expires_at: string }>();
mockGet
.mockResolvedValueOnce({
available: false,
reason: "display_session_unavailable",
mode: "desktop-control",
status: "not_configured",
})
.mockResolvedValueOnce({
controller: "none",
})
.mockResolvedValueOnce({
available: false,
reason: "display_session_unavailable",
mode: "desktop-control",
status: "not_configured",
})
.mockResolvedValueOnce({
controller: "none",
});
mockPost.mockReturnValueOnce(acquire.promise);
const { rerender } = render(<DisplayTab workspaceId="ws-a" />);
await waitFor(() => {
expect(screen.getByRole("button", { name: "Take control" })).toBeTruthy();
});
fireEvent.click(screen.getByRole("button", { name: "Take control" }));
rerender(<DisplayTab workspaceId="ws-b" />);
await waitFor(() => {
expect(mockGet).toHaveBeenCalledWith("/workspaces/ws-b/display/control");
});
await waitFor(() => {
expect(screen.getByRole("button", { name: "Take control" })).toBeTruthy();
});
acquire.resolve({
controller: "user",
controlled_by: "admin-token",
expires_at: "2026-05-23T08:48:27Z",
});
await acquire.promise;
await waitFor(() => {
expect(screen.queryByText("Controlled by Admin")).toBeNull();
});
expect(screen.getByRole("button", { name: "Take control" })).toBeTruthy();
});
});
function deferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
+5 -2
View File
@@ -50,13 +50,16 @@ docker rm $(docker ps -aq --filter "name=ws-") 2>/dev/null || true
echo ""
echo "--- Create Workspaces ---"
# model is required at the Create boundary (CTO 2026-05-22 SSOT —
# feedback_workspace_model_required_no_platform_default_dynamic_credential_intake).
# Pass the same value the deleted DefaultModel("claude-code") returned.
ROOT=$(curl -s -X POST $PLATFORM/workspaces -H "Content-Type: application/json" \
-d '{"name":"Root Agent","role":"Company coordinator","runtime":"claude-code","tier":3}' \
-d '{"name":"Root Agent","role":"Company coordinator","runtime":"claude-code","model":"sonnet","tier":3}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
check_contains "Create root workspace" "-" "$ROOT"
CHILD=$(curl -s -X POST $PLATFORM/workspaces -H "Content-Type: application/json" \
-d "{\"name\":\"Child Agent\",\"role\":\"Sub-team member\",\"runtime\":\"claude-code\",\"tier\":2,\"parent_id\":\"$ROOT\"}" \
-d "{\"name\":\"Child Agent\",\"role\":\"Sub-team member\",\"runtime\":\"claude-code\",\"model\":\"sonnet\",\"tier\":2,\"parent_id\":\"$ROOT\"}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
check_contains "Create child workspace" "-" "$CHILD"
+5 -1
View File
@@ -92,8 +92,12 @@ for _wid in $PRIOR; do
curl -s -X DELETE "$BASE/workspaces/$_wid?confirm=true" > /dev/null || true
done
# model is required at the Create boundary (CTO 2026-05-22 SSOT — see
# feedback_workspace_model_required_no_platform_default_dynamic_credential_intake).
# Body had no runtime → defaults to langgraph; pass the langgraph-compatible
# default that the deleted DefaultModel("") would have returned.
R=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \
-d '{"name":"Notify E2E","tier":1}')
-d '{"name":"Notify E2E","tier":1,"model":"anthropic:claude-opus-4-7"}')
WSID=$(echo "$R" | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])' 2>/dev/null || true)
[ -n "$WSID" ] || { echo "Failed to create workspace: $R"; exit 1; }
echo "Created workspace $WSID"
+51 -22
View File
@@ -24,14 +24,12 @@
#
# Only PROVISIONING differs from staging:
# - staging: POST /cp/admin/orgs (cold EC2 tenant) + per-tenant admin
# token + each workspace's MCP bearer from create response or an admin
# token-mint fallback.
# token + each workspace's MCP bearer from the POST /workspaces
# create response.
# - local: POST /workspaces directly against the local stack
# (BASE, default http://localhost:8080), MCP bearer minted via
# GET /admin/workspaces/:id/test-token (e2e_mint_test_token —
# deterministic, gated by MOLECULE_ENV != production). Same model
# every other local E2E (test_priority_runtimes_e2e.sh,
# test_api.sh) already uses; no new credential/provision flow.
# (BASE, default http://localhost:8080), MCP bearer consumed inline
# from the create response (auth_token field). Same model every
# other local E2E uses; no new credential/provision flow.
#
# By default the local backend creates external-mode workspace rows and
# drives the literal MCP path directly. That keeps the local peer-visibility
@@ -81,6 +79,17 @@ NAME_PREFIX="PV-Local-$$-$(date +%H%M%S)"
log() { echo "[$(date +%H:%M:%S)] $*"; }
ok() { echo "[$(date +%H:%M:%S)] ✅ $*"; }
extract_auth_token() {
python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
except Exception:
print(''); sys.exit(0)
print(d.get('auth_token') or d.get('connection', {}).get('auth_token') or '')
" 2>/dev/null
}
CREATED_WSIDS=()
ADMIN_BEARER="${MOLECULE_ADMIN_TOKEN:-${ADMIN_TOKEN:-}}"
ADMIN_AUTH=()
@@ -131,17 +140,6 @@ if ! curl -fsS "$BASE/health" -m 5 >/dev/null 2>&1; then
echo "::error::Local stack not healthy at $BASE/health — bring it up (make up) before this gate. Infra, not a workspace bug (feedback_fix_root_not_symptom)." >&2
exit 1
fi
# admin/test-token is the local MCP-bearer mint path; it 404s in
# production. If it is off, this gate cannot drive the literal call.
if ! curl -fsS "$BASE/admin/workspaces/preflight-probe/test-token" ${ADMIN_AUTH[@]+"${ADMIN_AUTH[@]}"} -m 5 >/dev/null 2>&1; then
# A 404 here is EITHER "no such ws" (fine — endpoint is enabled) OR the
# endpoint is disabled (MOLECULE_ENV=production). Distinguish by body.
PROBE=$(curl -s "$BASE/admin/workspaces/preflight-probe/test-token" ${ADMIN_AUTH[@]+"${ADMIN_AUTH[@]}"} -m 5 2>/dev/null)
if echo "$PROBE" | grep -qi 'production\|disabled\|not found.*endpoint'; then
echo "::error::GET /admin/workspaces/:id/test-token disabled (MOLECULE_ENV=production?). Cannot mint a local MCP bearer." >&2
exit 1
fi
fi
ok " local stack healthy"
# ─── Resolve per-runtime provisioning secrets ──────────────────────────
@@ -241,9 +239,31 @@ else
fi
log "1/5 provisioning parent ($PARENT_RUNTIME, mode=$PV_LOCAL_PROVISION_MODE) + one sibling per runtime under test..."
# Map runtime → model per the CTO 2026-05-22 SSOT directive (model is
# required, no platform default). External runtimes are exempt by the
# Create-handler gate — for them the URL is the contract — but we still
# pass model="external:custom" defensively in case a downstream consumer
# of the create body asserts presence.
_model_for_runtime() {
case "$1" in
claude-code) echo "sonnet" ;;
codex) echo "gpt-5.5" ;;
kimi) echo "kimi-coding/kimi-k2-coding-6" ;;
minimax) echo "minimax/MiniMax-M2.7" ;;
external) echo "external:custom" ;;
*) echo "anthropic:claude-opus-4-7" ;;
esac
}
PARENT_MODEL=$(_model_for_runtime "$PARENT_RUNTIME")
P_RESP=$(curl -s -X POST "$BASE/workspaces" ${ADMIN_AUTH[@]+"${ADMIN_AUTH[@]}"} -H "Content-Type: application/json" \
-d "{\"name\":\"${NAME_PREFIX}-parent\",\"runtime\":\"$PARENT_RUNTIME\",\"tier\":3$PARENT_EXTRA,\"secrets\":$PARENT_SECRETS}")
-d "{\"name\":\"${NAME_PREFIX}-parent\",\"runtime\":\"$PARENT_RUNTIME\",\"model\":\"$PARENT_MODEL\",\"tier\":3$PARENT_EXTRA,\"secrets\":$PARENT_SECRETS}")
PARENT_ID=$(echo "$P_RESP" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("id",""))' 2>/dev/null)
# PARENT_TOKEN captured for symmetry with the per-sibling auth-token
# capture in the runtime loop below + reserved for follow-up steps
# that need parent-side auth. Current downstream steps reach the parent
# via admin token, so the variable isn't dereferenced — SC2034.
# shellcheck disable=SC2034 # captured for downstream parent-auth use; see #1644 follow-up
PARENT_TOKEN=$(echo "$P_RESP" | extract_auth_token)
if [ -z "$PARENT_ID" ]; then
echo "::error::parent create failed: $(echo "$P_RESP" | head -c 300)" >&2
exit 1
@@ -259,6 +279,8 @@ log " PARENT_ID=$PARENT_ID runtime=$PARENT_RUNTIME"
WS_IDS_MAP=""
# shellcheck disable=SC2034 # map values are updated through portable eval-based helpers.
VERDICT_MAP=""
# shellcheck disable=SC2034 # map values are updated through portable eval-based helpers.
WS_TOKENS_MAP=""
_map_set() { # _map_set <mapvarname> <key> <value>
local __m="$1" __k="$2" __v="$3" __cur
eval "__cur=\$$__m"
@@ -291,14 +313,21 @@ for rt in $PV_RUNTIMES; do
CREATE_RUNTIME="$rt"
CREATE_EXTRA=""
fi
CREATE_MODEL=$(_model_for_runtime "$CREATE_RUNTIME")
R=$(curl -s -X POST "$BASE/workspaces" ${ADMIN_AUTH[@]+"${ADMIN_AUTH[@]}"} -H "Content-Type: application/json" \
-d "{\"name\":\"${NAME_PREFIX}-$rt\",\"runtime\":\"$CREATE_RUNTIME\",\"tier\":2,\"parent_id\":\"$PARENT_ID\"$CREATE_EXTRA,\"secrets\":$SEC}")
-d "{\"name\":\"${NAME_PREFIX}-$rt\",\"runtime\":\"$CREATE_RUNTIME\",\"model\":\"$CREATE_MODEL\",\"tier\":2,\"parent_id\":\"$PARENT_ID\"$CREATE_EXTRA,\"secrets\":$SEC}")
WID=$(echo "$R" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("id",""))' 2>/dev/null)
WTOK=$(echo "$R" | extract_auth_token)
if [ -z "$WID" ]; then
echo "::error::$rt workspace create failed: $(echo "$R" | head -c 300)" >&2
exit 1
fi
if [ -z "$WTOK" ]; then
echo "::error::$rt workspace create did not return an auth_token — cannot drive the literal MCP call" >&2
exit 1
fi
_map_set WS_IDS_MAP "$rt" "$WID"
_map_set WS_TOKENS_MAP "$rt" "$WTOK"
CREATED_WSIDS+=("$WID")
ALL_WS_IDS="$ALL_WS_IDS $WID"
ACTIVE_RUNTIMES="$ACTIVE_RUNTIMES $rt"
@@ -356,10 +385,10 @@ log "4/5 driving the LITERAL list_peers MCP call per online runtime..."
echo ""
for rt in $ONLINE_RUNTIMES; do
wid="$(_map_get WS_IDS_MAP "$rt")"
WTOK=$(e2e_mint_test_token "$wid" 2>/dev/null || true)
WTOK="$(_map_get WS_TOKENS_MAP "$rt")"
if [ -z "$WTOK" ]; then
echo "--- $rt (ws=$wid) ---"
echo "$rt: could not mint a local MCP bearer (admin/test-token) — cannot drive the literal call"
echo "$rt: workspace create did not return an auth_token — cannot drive the literal call"
_map_set VERDICT_MAP "$rt" "FAIL(no-bearer)"
REGRESSED=1
echo ""
+6 -31
View File
@@ -40,10 +40,10 @@
# drives: POST /cp/admin/orgs (provision), GET
# /cp/admin/orgs/:slug/admin-token (per-tenant token), DELETE
# /cp/admin/tenants/:slug (teardown). The per-tenant admin token drives
# tenant workspace creation; each workspace's OWN auth_token drives its
# MCP call. External-like runtimes may return the token in POST
# /workspaces; managed container runtimes usually require the admin token
# mint fallback below.
# tenant workspace creation; each workspace's OWN auth_token is consumed
# inline from the POST /workspaces 201 response to drive its MCP call.
# No dev-only admin token-mint routes are used in this E2E
# (feedback_no_dev_only_routes_in_e2e).
#
# Required env:
# MOLECULE_ADMIN_TOKEN CP admin bearer — Railway staging CP_ADMIN_API_TOKEN
@@ -265,44 +265,19 @@ log " PARENT_ID=$PARENT_ID"
# WS_IDS[runtime]=id ; WS_TOKENS[runtime]=auth_token (the MCP bearer)
declare -A WS_IDS WS_TOKENS
ALL_WS_IDS="$PARENT_ID"
TOKEN_ERRORS=0
TOKEN_ERROR_SUMMARY=""
for rt in $PV_RUNTIMES; do
R=$(tenant_call POST /workspaces \
-d "{\"name\":\"pv-$rt\",\"runtime\":\"$rt\",\"tier\":2,\"parent_id\":\"$PARENT_ID\",\"secrets\":$SECRETS_JSON}")
WID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
# External-like runtimes may return connection.auth_token on create.
# Managed container runtimes usually return only id/status here, then
# receive their bearer through registry/bootstrap; for this literal MCP
# driver we mint through the production-safe admin token route below.
WTOK=$(echo "$R" | extract_auth_token)
[ -n "$WID" ] || fail "$rt workspace create failed: $(echo "$R" | head -c 300)"
TOKEN_DIAG=""
if [ -z "$WTOK" ]; then
TTOK_FILE=$(mktemp)
TTOK_CODE=$(tenant_call_capture POST "/admin/workspaces/$WID/tokens" "$TTOK_FILE" 2>/dev/null || echo "curl_error")
TTOK_RESP=$(cat "$TTOK_FILE" 2>/dev/null || true)
WTOK=$(echo "$TTOK_RESP" | extract_auth_token)
TOKEN_DIAG="POST /admin/workspaces/$WID/tokens -> HTTP $TTOK_CODE body: $(echo "$TTOK_RESP" | redact_token_body)"
rm -f "$TTOK_FILE"
fi
[ -n "$WID" ] || fail "$rt workspace create failed: $(echo \"$R\" | head -c 300)"
[ -n "$WTOK" ] || fail "$rt workspace create did not return an auth_token — cannot drive its MCP call (workspace_id=$WID; create_resp: $(echo \"$R\" | redact_token_body))"
WS_IDS[$rt]="$WID"
if [ -z "$WTOK" ]; then
TOKEN_ERRORS=$((TOKEN_ERRORS + 1))
TOKEN_ERROR_SUMMARY="${TOKEN_ERROR_SUMMARY}
[$rt] workspace did not return or mint an auth_token — cannot drive its MCP call (workspace_id=$WID; create_resp: $(echo "$R" | redact_token_body); token_fallbacks: $TOKEN_DIAG)"
log " $rt$WID (token acquisition failed; continuing to classify other runtimes)"
continue
fi
WS_TOKENS[$rt]="$WTOK"
ALL_WS_IDS="$ALL_WS_IDS $WID"
log " $rt$WID"
done
if [ "$TOKEN_ERRORS" -gt 0 ]; then
fail "token acquisition failed for $TOKEN_ERRORS runtime(s):$TOKEN_ERROR_SUMMARY"
fi
if [ "${PV_TOKEN_DIAGNOSTIC_ONLY:-0}" = "1" ]; then
ok "token diagnostic passed for runtimes: $PV_RUNTIMES"
exit 0
+4 -2
View File
@@ -188,8 +188,9 @@ import json, os
print(json.dumps({'CLAUDE_CODE_OAUTH_TOKEN': os.environ['CLAUDE_CODE_OAUTH_TOKEN']}))
")
local resp wsid
# model required (CTO 2026-05-22 SSOT) — pass the deleted DefaultModel("claude-code") value.
resp=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \
-d "{\"name\":\"Priority E2E (claude-code)\",\"runtime\":\"claude-code\",\"tier\":1,\"secrets\":$secrets}")
-d "{\"name\":\"Priority E2E (claude-code)\",\"runtime\":\"claude-code\",\"model\":\"sonnet\",\"tier\":1,\"secrets\":$secrets}")
wsid=$(echo "$resp" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("id",""))') || true
if [ -z "$wsid" ]; then
fail "create claude-code workspace" "$resp"
@@ -380,8 +381,9 @@ import json, os
print(json.dumps({'GEMINI_API_KEY': os.environ['E2E_GEMINI_API_KEY']}))
")
local resp wsid
# model required (CTO 2026-05-22 SSOT) — gemini-cli routes via the gemini provider.
resp=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \
-d "{\"name\":\"Priority E2E (gemini-cli)\",\"runtime\":\"gemini-cli\",\"tier\":1,\"secrets\":$secrets}")
-d "{\"name\":\"Priority E2E (gemini-cli)\",\"runtime\":\"gemini-cli\",\"model\":\"gemini-2.0-flash\",\"tier\":1,\"secrets\":$secrets}")
wsid=$(echo "$resp" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("id",""))') || true
if [ -z "$wsid" ]; then fail "create gemini-cli workspace" "$resp"; return 0; fi
CREATED_WSIDS+=("$wsid")
+3 -2
View File
@@ -393,8 +393,9 @@ func main() {
// See molecule-core#7.
bindHost := resolveBindHost()
srv := &http.Server{
Addr: fmt.Sprintf("%s:%s", bindHost, port),
Handler: r,
Addr: fmt.Sprintf("%s:%s", bindHost, port),
Handler: r,
ReadHeaderTimeout: 5 * time.Second,
}
// Start server in goroutine
@@ -116,8 +116,11 @@ func (d *DiscordAdapter) SendMessage(ctx context.Context, config map[string]inte
// would propagate that token into logs and error responses (#659).
return fmt.Errorf("discord: HTTP request failed")
}
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 4096))
_ = resp.Body.Close()
if readErr != nil {
return fmt.Errorf("discord: read response body: %w", readErr)
}
// Discord returns 204 No Content on success.
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
+4 -1
View File
@@ -119,7 +119,10 @@ func (l *LarkAdapter) SendMessage(ctx context.Context, config map[string]interfa
}
defer func() { _ = resp.Body.Close() }()
body, _ := io.ReadAll(resp.Body)
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
return fmt.Errorf("lark: read response body: %w", readErr)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("lark: webhook returned %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
+27 -4
View File
@@ -156,6 +156,9 @@ func (m *Manager) PausePollersForToken(workspaceID, botToken string) func() {
}
}
}
if err := rows.Err(); err != nil {
log.Printf("Channels: pause-pollers rows.Err: %v", err)
}
m.mu.Unlock()
if len(pausedIDs) == 0 {
@@ -204,8 +207,16 @@ func (m *Manager) Reload(ctx context.Context) {
log.Printf("Channels: reload scan error: %v", err)
continue
}
_ = json.Unmarshal(configJSON, &ch.Config)
_ = json.Unmarshal(allowedJSON, &ch.AllowedUsers)
if err := json.Unmarshal(configJSON, &ch.Config); err != nil {
log.Printf("Channels: reload config unmarshal error for %s: %v", truncID(ch.ID), err)
continue
}
if len(allowedJSON) > 0 {
if err := json.Unmarshal(allowedJSON, &ch.AllowedUsers); err != nil {
log.Printf("Channels: reload allowed_users unmarshal error for %s: %v", truncID(ch.ID), err)
continue
}
}
// #319: decrypt at the boundary between DB (ciphertext) and the
// in-memory config adapters consume. A decrypt failure logs and
// skips the channel — downstream getUpdates would fail anyway
@@ -216,6 +227,9 @@ func (m *Manager) Reload(ctx context.Context) {
}
desired[ch.ID] = ch
}
if err := rows.Err(); err != nil {
log.Printf("Channels: reload rows.Err: %v", err)
}
m.mu.Lock()
defer m.mu.Unlock()
@@ -473,6 +487,9 @@ func (m *Manager) BroadcastToWorkspaceChannels(ctx context.Context, workspaceID,
}
}
}
if err := rows.Err(); err != nil {
log.Printf("Channels: broadcast rows.Err: %v", err)
}
}
// FetchWorkspaceChannelContext returns recent Slack channel messages formatted
@@ -555,8 +572,14 @@ func (m *Manager) loadChannel(ctx context.Context, channelID string) (ChannelRow
if err != nil {
return ch, fmt.Errorf("channel %s not found: %w", channelID, err)
}
json.Unmarshal(configJSON, &ch.Config)
json.Unmarshal(allowedJSON, &ch.AllowedUsers)
if err := json.Unmarshal(configJSON, &ch.Config); err != nil {
return ch, fmt.Errorf("channel %s config unmarshal: %w", channelID, err)
}
if len(allowedJSON) > 0 {
if err := json.Unmarshal(allowedJSON, &ch.AllowedUsers); err != nil {
return ch, fmt.Errorf("channel %s allowed_users unmarshal: %w", channelID, err)
}
}
// #319: decrypt bot_token / webhook_secret — SendOutbound and adapter
// methods downstream read them as plaintext strings.
if err := DecryptSensitiveFields(ch.Config); err != nil {
+13 -3
View File
@@ -171,8 +171,11 @@ func (s *SlackAdapter) sendBotMessage(ctx context.Context, config map[string]int
if err != nil {
return fmt.Errorf("slack: send: %w", err)
}
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
respBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 4096))
_ = resp.Body.Close()
if readErr != nil {
return fmt.Errorf("slack: read response body: %w", readErr)
}
var result struct {
OK bool `json:"ok"`
Error string `json:"error"`
@@ -208,9 +211,13 @@ func (s *SlackAdapter) sendWebhookMessage(ctx context.Context, config map[string
if err != nil {
return fmt.Errorf("slack: send: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
return fmt.Errorf("slack: webhook returned %d (read body failed: %v)", resp.StatusCode, readErr)
}
return fmt.Errorf("slack: webhook returned %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
return nil
@@ -524,8 +531,11 @@ func FetchChannelHistory(ctx context.Context, botToken, channelID string, limit
if err != nil {
return nil, err
}
body, _ := io.ReadAll(io.LimitReader(resp.Body, 65536))
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 65536))
_ = resp.Body.Close()
if readErr != nil {
return nil, fmt.Errorf("slack: read history response: %w", readErr)
}
var result struct {
OK bool `json:"ok"`
@@ -111,12 +111,13 @@ const maxProxyResponseBody = 10 << 20
// a generic 502 page to canvas. 10s is well above realistic intra-region
// latencies and well below CF's edge timeout.
//
// 3. Transport.ResponseHeaderTimeout — 180s default. From request-body-end
// 3. Transport.ResponseHeaderTimeout — 5min default. From request-body-end
// to response-headers-start. Configurable via
// A2A_PROXY_RESPONSE_HEADER_TIMEOUT (envx.Duration). Covers cold-start
// first-byte (30-60s OAuth flow above) with enough room for Opus agent
// turns (big context + internal delegate_task round-trips routinely exceed
// the old 60s ceiling). Body streaming after headers is governed by the
// turns and Codex scheduled tasks (big context + internal delegate_task
// round-trips routinely exceed the old 60s/180s ceilings). Body streaming
// after headers is governed by the
// per-request context deadline, NOT this timeout — so multi-minute agent
// responses still work fine.
//
@@ -131,7 +132,7 @@ var a2aClient = &http.Client{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ResponseHeaderTimeout: envx.Duration("A2A_PROXY_RESPONSE_HEADER_TIMEOUT", 180*time.Second),
ResponseHeaderTimeout: envx.Duration("A2A_PROXY_RESPONSE_HEADER_TIMEOUT", 5*time.Minute),
TLSHandshakeTimeout: 10 * time.Second,
// MaxIdleConns / IdleConnTimeout: stdlib defaults are fine; agent
// fan-in is bounded by the platform's broadcaster fan-out, not by
@@ -28,8 +28,8 @@ type proxyDispatchBuildError struct{ err error }
func (e *proxyDispatchBuildError) Error() string { return e.err.Error() }
// handleA2ADispatchError translates a forward-call failure into a proxyA2AError,
// runs the reactive container-health check, and (when `logActivity` is true)
// schedules a detached LogActivity goroutine for the failed attempt.
// runs the reactive container-health check, and records the outcome. Busy
// targets that are successfully queued are logged as queued, not failed.
func (h *WorkspaceHandler) handleA2ADispatchError(ctx context.Context, workspaceID, callerID string, body []byte, a2aMethod string, err error, durationMs int, logActivity bool) (int, []byte, *proxyA2AError) {
// Build-time failure (couldn't even create the http.Request) — return
// a 500 without the reactive-health / busy-retry paths.
@@ -45,10 +45,10 @@ func (h *WorkspaceHandler) handleA2ADispatchError(ctx context.Context, workspace
containerDead := h.maybeMarkContainerDead(ctx, workspaceID)
if logActivity {
h.logA2AFailure(ctx, workspaceID, callerID, body, a2aMethod, err, durationMs)
}
if containerDead {
if logActivity {
h.logA2AFailure(ctx, workspaceID, callerID, body, a2aMethod, err, durationMs)
}
return 0, nil, &proxyA2AError{
Status: http.StatusServiceUnavailable,
Response: gin.H{"error": "workspace agent unreachable — container restart triggered", "restarting": true},
@@ -108,6 +108,9 @@ func (h *WorkspaceHandler) handleA2ADispatchError(ctx context.Context, workspace
ctx, workspaceID, callerID, PriorityTask, body, a2aMethod, idempotencyKey, expiresAt,
); qerr == nil {
log.Printf("ProxyA2A: target %s busy — enqueued as %s (depth=%d)", workspaceID, qid, depth)
if logActivity {
h.logA2ABusyQueued(ctx, workspaceID, callerID, body, a2aMethod, durationMs)
}
respBody, _ := json.Marshal(gin.H{
"queued": true,
"queue_id": qid,
@@ -121,6 +124,9 @@ func (h *WorkspaceHandler) handleA2ADispatchError(ctx context.Context, workspace
// make delegation silently disappear.
log.Printf("ProxyA2A: enqueue for %s failed (%v) — falling back to 503", workspaceID, qerr)
}
if logActivity {
h.logA2AFailure(ctx, workspaceID, callerID, body, a2aMethod, err, durationMs)
}
return 0, nil, &proxyA2AError{
Status: http.StatusServiceUnavailable,
Headers: map[string]string{"Retry-After": strconv.Itoa(busyRetryAfterSeconds)},
@@ -131,6 +137,9 @@ func (h *WorkspaceHandler) handleA2ADispatchError(ctx context.Context, workspace
},
}
}
if logActivity {
h.logA2AFailure(ctx, workspaceID, callerID, body, a2aMethod, err, durationMs)
}
return 0, nil, &proxyA2AError{
Status: http.StatusBadGateway,
Response: gin.H{"error": "failed to reach workspace agent"},
@@ -311,6 +320,33 @@ func (h *WorkspaceHandler) logA2AFailure(ctx context.Context, workspaceID, calle
})
}
// logA2ABusyQueued records that a push attempt reached a live but busy
// workspace and was durably queued for heartbeat drain.
func (h *WorkspaceHandler) logA2ABusyQueued(ctx context.Context, workspaceID, callerID string, body []byte, a2aMethod string, durationMs int) {
var wsName string
db.DB.QueryRowContext(ctx, `SELECT name FROM workspaces WHERE id = $1`, workspaceID).Scan(&wsName)
if wsName == "" {
wsName = workspaceID
}
summary := a2aMethod + " → " + wsName + " (queued: target busy)"
parent := ctx
h.goAsync(func() {
logCtx, cancel := context.WithTimeout(context.WithoutCancel(parent), 30*time.Second)
defer cancel()
LogActivity(logCtx, h.broadcaster, ActivityParams{
WorkspaceID: workspaceID,
ActivityType: "a2a_receive",
SourceID: nilIfEmpty(callerID),
TargetID: &workspaceID,
Method: &a2aMethod,
Summary: &summary,
RequestBody: json.RawMessage(body),
DurationMs: &durationMs,
Status: "ok",
})
})
}
// logA2ASuccess records a successful A2A round-trip and (for canvas-initiated
// 2xx/3xx responses) broadcasts an A2A_RESPONSE event so the frontend can
// receive the reply without polling.
@@ -1779,6 +1779,58 @@ func TestHandleA2ADispatchError_ContextDeadline(t *testing.T) {
}
}
func TestHandleA2ADispatchError_BusyEnqueueLogsQueuedNotFailure(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", t.TempDir())
waitForHandlerAsyncBeforeDBCleanup(t, handler)
mock.ExpectQuery(`INSERT INTO a2a_queue`).
WithArgs("ws-busy", nil, PriorityTask, "{}", "message/send", nil, nil).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11111111-1111-1111-1111-111111111111"))
mock.ExpectQuery(`SELECT COUNT\(\*\) FROM a2a_queue`).
WithArgs("ws-busy").
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
mock.ExpectQuery(`SELECT name FROM workspaces WHERE id =`).
WithArgs("ws-busy").
WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow("Busy Target"))
mock.ExpectExec("INSERT INTO activity_logs").
WithArgs(
"ws-busy",
"a2a_receive",
nil,
sqlmock.AnyArg(),
sqlmock.AnyArg(),
sqlmock.AnyArg(),
sqlmock.AnyArg(),
nil,
nil,
sqlmock.AnyArg(),
"ok",
nil,
).
WillReturnResult(sqlmock.NewResult(0, 1))
status, body, perr := handler.handleA2ADispatchError(
context.Background(), "ws-busy", "", []byte("{}"), "message/send",
context.DeadlineExceeded, 180002, true,
)
if perr != nil {
t.Fatalf("expected busy enqueue success, got proxy error: %+v", perr)
}
if status != http.StatusAccepted {
t.Fatalf("got status %d, want 202", status)
}
if !bytes.Contains(body, []byte(`"queued":true`)) {
t.Fatalf("expected queued response body, got %s", string(body))
}
time.Sleep(80 * time.Millisecond)
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet expectations; busy enqueue must log status=ok, not error: %v", err)
}
}
func TestHandleA2ADispatchError_BuildError(t *testing.T) {
setupTestDB(t)
setupTestRedis(t)
@@ -2354,7 +2406,7 @@ func TestLookupDeliveryMode_ContextCanceled_FailsClosed(t *testing.T) {
// ==================== a2aClient ResponseHeaderTimeout config ====================
func TestA2AClientResponseHeaderTimeout(t *testing.T) {
const defaultTimeout = 180 * time.Second
const defaultTimeout = 5 * time.Minute
// Default (unset env) — a2aClient was initialised at package load time.
if a2aClient.Transport.(*http.Transport).ResponseHeaderTimeout != defaultTimeout {
@@ -2378,7 +2430,7 @@ func TestA2AClientResponseHeaderTimeout(t *testing.T) {
t.Run("invalid A2A_PROXY_RESPONSE_HEADER_TIMEOUT falls back to default", func(t *testing.T) {
t.Setenv("A2A_PROXY_RESPONSE_HEADER_TIMEOUT", "not-a-duration")
// Simulate what envx.Duration does with an invalid value.
var fallback = 180 * time.Second
var fallback = 5 * time.Minute
override := fallback
if v := os.Getenv("A2A_PROXY_RESPONSE_HEADER_TIMEOUT"); v != "" {
if d, err := time.ParseDuration(v); err == nil && d > 0 {
@@ -39,6 +39,7 @@ func TestAdminTestToken_EnabledViaFlagEvenInProd(t *testing.T) {
mock := setupTestDB(t)
t.Setenv("MOLECULE_ENV", "production")
t.Setenv("MOLECULE_ENABLE_TEST_TOKENS", "1")
t.Setenv("ADMIN_TOKEN", "")
mock.ExpectQuery("SELECT id FROM workspaces WHERE id =").
WithArgs("ws-1").
@@ -58,6 +59,7 @@ func TestAdminTestToken_EnabledViaFlagEvenInProd(t *testing.T) {
func TestAdminTestToken_WorkspaceNotFound(t *testing.T) {
mock := setupTestDB(t)
t.Setenv("MOLECULE_ENV", "development")
t.Setenv("ADMIN_TOKEN", "")
mock.ExpectQuery("SELECT id FROM workspaces WHERE id =").
WithArgs("missing").
@@ -75,6 +77,7 @@ func TestAdminTestToken_WorkspaceNotFound(t *testing.T) {
func TestAdminTestToken_HappyPath_TokenValidates(t *testing.T) {
mock := setupTestDB(t)
t.Setenv("MOLECULE_ENV", "development")
t.Setenv("ADMIN_TOKEN", "")
mock.ExpectQuery("SELECT id FROM workspaces WHERE id =").
WithArgs("ws-1").
@@ -104,6 +104,9 @@ func (h *ChannelHandler) List(c *gin.Context) {
}
result = append(result, entry)
}
if err := rows.Err(); err != nil {
log.Printf("Channels: list rows.Err: %v", err)
}
c.JSON(http.StatusOK, result)
}
@@ -514,6 +517,9 @@ func (h *ChannelHandler) Webhook(c *gin.Context) {
candidates = append(candidates, row)
}
}
if err := rows.Err(); err != nil {
log.Printf("Channels: telegram webhook rows.Err: %v", err)
}
if targetSlug != "" {
// [slug] routing — match against config username (lowercased)
@@ -393,6 +393,9 @@ func queryPeerMaps(query string, args ...interface{}) ([]map[string]interface{},
result = append(result, peer)
}
if err := rows.Err(); err != nil {
log.Printf("queryPeerMaps rows.Err: %v", err)
}
return result, nil
}
@@ -49,6 +49,9 @@ func (h *EventsHandler) List(c *gin.Context) {
"created_at": createdAt,
})
}
if err := rows.Err(); err != nil {
log.Printf("Events list rows error: %v", err)
}
c.JSON(http.StatusOK, events)
}
@@ -87,5 +90,8 @@ func (h *EventsHandler) ListByWorkspace(c *gin.Context) {
"created_at": createdAt,
})
}
if err := rows.Err(); err != nil {
log.Printf("WorkspaceEvents list rows error: %v", err)
}
c.JSON(http.StatusOK, events)
}
@@ -159,7 +159,8 @@ func generateAppInstallationToken() (string, time.Time, error) {
req, _ := http.NewRequest("POST", fmt.Sprintf("https://api.github.com/app/installations/%d/access_tokens", installID), nil)
req.Header.Set("Authorization", "Bearer "+signed)
req.Header.Set("Accept", "application/vnd.github+json")
resp, err := http.DefaultClient.Do(req)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", time.Time{}, err
}
@@ -44,7 +44,7 @@ func TestWorkspaceCreate_WithParentID(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Child Agent","parent_id":"parent-ws-123"}`
body := `{"name":"Child Agent","model":"anthropic:claude-opus-4-7","parent_id":"parent-ws-123"}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -80,7 +80,7 @@ func TestWorkspaceCreate_ExplicitClaudeCodeRuntime(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"CC Agent","tier":2,"runtime":"claude-code","canvas":{"x":10,"y":20}}`
body := `{"name":"CC Agent","tier":2,"runtime":"claude-code","model":"sonnet","canvas":{"x":10,"y":20}}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -301,7 +301,7 @@ func TestWorkspaceCreate_MaxConcurrentTasksOverride(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Leader Agent","runtime":"claude-code","max_concurrent_tasks":3}`
body := `{"name":"Leader Agent","runtime":"claude-code","model":"sonnet","max_concurrent_tasks":3}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -777,6 +777,103 @@ func TestCreate_FieldValidation_Returns400(t *testing.T) {
}
}
// TestCreate_ModelRequired_Returns422 pins the CTO 2026-05-22 SSOT
// directive (feedback_workspace_model_required_no_platform_default_dynamic_credential_intake):
// model is required user input; the platform must not supply a default,
// the runtime must not fall back. Empirical trigger: Code Reviewer
// 5ba15d7e was created with `{"name":..., "runtime":"codex", ...}` (no
// model). The legacy DefaultModel fallback returned "anthropic:claude-opus-4-7"
// and codex adapter wedged forever — `picks provider='anthropic' but it
// is not in the providers registry`. The gate at the Create boundary
// turns that silent stuck-workspace failure into an immediate 422 the
// caller can react to.
//
// Three shapes covered:
// 1. bare name (no template, no runtime, no model) — formerly defaulted
// to langgraph + anthropic; now 422 because model is unspecified.
// 2. explicit runtime, no model — the Code Reviewer repro shape.
// 3. explicit runtime+template path, but template (when missing on
// disk or unreadable) would leave model empty — exercised here by
// pointing at a non-existent template under /tmp/configs.
func TestCreate_ModelRequired_Returns422(t *testing.T) {
setupTestDB(t)
setupTestRedis(t)
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", "/tmp/configs")
cases := []struct{ label, body string }{
{"bare_name_no_runtime_no_model", `{"name":"x"}`},
{"explicit_codex_no_model", `{"name":"Code Reviewer","role":"code reviewer","runtime":"codex","tier":4,"max_concurrent_tasks":1}`},
{"explicit_hermes_no_model", `{"name":"researcher","runtime":"hermes"}`},
}
for _, tc := range cases {
t.Run(tc.label, func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(tc.body))
c.Request.Header.Set("Content-Type", "application/json")
handler.Create(c)
if w.Code != http.StatusUnprocessableEntity {
t.Errorf("Create(%s): want 422 MODEL_REQUIRED, got %d: %s", tc.label, w.Code, w.Body.String())
return
}
if !bytes.Contains(w.Body.Bytes(), []byte(`"code":"MODEL_REQUIRED"`)) {
t.Errorf("Create(%s): want body containing code=MODEL_REQUIRED, got %s", tc.label, w.Body.String())
}
})
}
}
// TestCreate_ExternalRuntime_NoModel_OK pins the external-runtime
// exemption from the MODEL_REQUIRED gate. External workspaces
// intentionally do not spawn a Docker container or run an adapter;
// they delegate to a registered URL (workspace_provision.go:497-498:
// "external is a first-class runtime that intentionally does NOT
// spawn a Docker container"). The model field has no meaning for
// them — the URL is the contract, and the gate would 422 every
// legitimate "register my agent at https://..." flow.
//
// Both spellings count as external:
// 1. payload.External == true (the canonical flag, e.g. with any runtime)
// 2. payload.Runtime == "external" (legacy shape some E2E scripts still use)
//
// The isExternalLikeRuntime() helper catches both "external" and any
// future external-like runtime alias.
func TestCreate_ExternalRuntime_NoModel_OK(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", t.TempDir())
// External=true with explicit runtime — the test_api.sh / Echo Agent shape.
mock.ExpectBegin()
mock.ExpectExec("INSERT INTO workspaces").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
mock.ExpectExec("INSERT INTO canvas_layouts").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("INSERT INTO structure_events").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`UPDATE workspaces SET status =`).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("INSERT INTO workspace_auth_tokens").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("INSERT INTO structure_events").
WillReturnResult(sqlmock.NewResult(0, 1))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Echo Agent","tier":1,"runtime":"external","external":true}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.Create(c)
if w.Code != http.StatusCreated {
t.Fatalf("external workspace without model: want 201, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("sqlmock expectations not met: %v", err)
}
}
func TestUpdate_FieldValidation_Returns400(t *testing.T) {
setupTestDB(t)
setupTestRedis(t)
@@ -386,7 +386,13 @@ func TestWorkspaceCreate(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Test Agent","canvas":{"x":100,"y":200}}`
// Note: model is now required at the Create boundary (CTO 2026-05-22
// SSOT directive — see feedback_workspace_model_required_no_platform_default_dynamic_credential_intake
// and TestCreate_ModelRequired_Returns422). This test happens to take
// the bare-defaults path (no template, no runtime → langgraph), so
// the body must declare an explicit model. Using a langgraph-compatible
// id; the test doesn't exercise model semantics beyond presence.
body := `{"name":"Test Agent","model":"anthropic:claude-opus-4-7","canvas":{"x":100,"y":200}}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -1141,6 +1141,8 @@ func TestIsSafeURL_Blocks169_254_Metadata(t *testing.T) {
}
func TestIsSafeURL_Blocks10xPrivate(t *testing.T) {
t.Setenv("MOLECULE_ORG_ID", "")
t.Setenv("MOLECULE_DEPLOY_MODE", "self-hosted")
err := isSafeURL("http://10.0.0.1/agent")
if err == nil {
t.Errorf("isSafeURL: expected 10.x.x.x to be blocked, got nil")
@@ -1148,6 +1150,8 @@ func TestIsSafeURL_Blocks10xPrivate(t *testing.T) {
}
func TestIsSafeURL_Blocks172Private(t *testing.T) {
t.Setenv("MOLECULE_ORG_ID", "")
t.Setenv("MOLECULE_DEPLOY_MODE", "self-hosted")
err := isSafeURL("http://172.16.0.1/agent")
if err == nil {
t.Errorf("isSafeURL: expected 172.16.0.0/12 to be blocked, got nil")
@@ -1155,6 +1159,8 @@ func TestIsSafeURL_Blocks172Private(t *testing.T) {
}
func TestIsSafeURL_Blocks192_168Private(t *testing.T) {
t.Setenv("MOLECULE_ORG_ID", "")
t.Setenv("MOLECULE_DEPLOY_MODE", "self-hosted")
err := isSafeURL("http://192.168.1.100/agent")
if err == nil {
t.Errorf("isSafeURL: expected 192.168.x.x to be blocked, got nil")
@@ -1178,6 +1184,8 @@ func TestIsSafeURL_BlocksInvalidURL(t *testing.T) {
// ==================== SSRF Defence — isPrivateOrMetadataIP ====================
func TestIsPrivateOrMetadataIP_10Range(t *testing.T) {
t.Setenv("MOLECULE_ORG_ID", "")
t.Setenv("MOLECULE_DEPLOY_MODE", "self-hosted")
tests := []string{"10.0.0.0", "10.255.255.255", "10.1.2.3"}
for _, ip := range tests {
if !isPrivateOrMetadataIP(net.ParseIP(ip)) {
@@ -1187,6 +1195,8 @@ func TestIsPrivateOrMetadataIP_10Range(t *testing.T) {
}
func TestIsPrivateOrMetadataIP_172Range(t *testing.T) {
t.Setenv("MOLECULE_ORG_ID", "")
t.Setenv("MOLECULE_DEPLOY_MODE", "self-hosted")
tests := []string{"172.16.0.0", "172.31.255.255", "172.20.1.1"}
for _, ip := range tests {
if !isPrivateOrMetadataIP(net.ParseIP(ip)) {
@@ -1196,6 +1206,8 @@ func TestIsPrivateOrMetadataIP_172Range(t *testing.T) {
}
func TestIsPrivateOrMetadataIP_192_168Range(t *testing.T) {
t.Setenv("MOLECULE_ORG_ID", "")
t.Setenv("MOLECULE_DEPLOY_MODE", "self-hosted")
tests := []string{"192.168.0.0", "192.168.255.255", "192.168.1.1"}
for _, ip := range tests {
if !isPrivateOrMetadataIP(net.ParseIP(ip)) {
@@ -95,14 +95,18 @@ func (h *MCPHandler) toolListPeers(ctx context.Context, workspaceID string) (str
cols+` FROM workspaces w WHERE w.parent_id = $1 AND w.id != $2 AND w.status != 'removed'`,
parentID.String, workspaceID)
if err == nil {
_ = scanPeers(rows)
if scanErr := scanPeers(rows); scanErr != nil {
log.Printf("MCP toolListPeers: sibling scan error: %v", scanErr)
}
}
} else {
rows, err := h.database.QueryContext(ctx,
cols+` FROM workspaces w WHERE w.parent_id IS NULL AND w.id != $1 AND w.status != 'removed'`,
workspaceID)
if err == nil {
_ = scanPeers(rows)
if scanErr := scanPeers(rows); scanErr != nil {
log.Printf("MCP toolListPeers: sibling scan error: %v", scanErr)
}
}
}
@@ -112,7 +116,9 @@ func (h *MCPHandler) toolListPeers(ctx context.Context, workspaceID string) (str
cols+` FROM workspaces w WHERE w.parent_id = $1 AND w.status != 'removed'`,
workspaceID)
if err == nil {
_ = scanPeers(rows)
if scanErr := scanPeers(rows); scanErr != nil {
log.Printf("MCP toolListPeers: children scan error: %v", scanErr)
}
}
}
@@ -122,7 +128,9 @@ func (h *MCPHandler) toolListPeers(ctx context.Context, workspaceID string) (str
cols+` FROM workspaces w WHERE w.id = $1 AND w.status != 'removed'`,
parentID.String)
if err == nil {
_ = scanPeers(rows)
if scanErr := scanPeers(rows); scanErr != nil {
log.Printf("MCP toolListPeers: parent scan error: %v", scanErr)
}
}
}
@@ -54,6 +54,11 @@ func (h *MemoryHandler) List(c *gin.Context) {
entry.Value = json.RawMessage(value)
entries = append(entries, entry)
}
if err := rows.Err(); err != nil {
log.Printf("Memory list iteration error: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "query iteration failed"})
return
}
c.JSON(http.StatusOK, entries)
}
@@ -4,6 +4,7 @@ import (
"bytes"
"database/sql"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
@@ -74,6 +75,34 @@ func TestMemoryList_DBError(t *testing.T) {
}
}
// TestMemoryList_RowsErr_Returns500 verifies that a rows.Err() set during
// iteration causes the handler to return 500 rather than partial results.
func TestMemoryList_RowsErr_Returns500(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
handler := NewMemoryHandler()
cols := []string{"key", "value", "version", "expires_at", "updated_at"}
mock.ExpectQuery("SELECT key, value, version, expires_at, updated_at").
WithArgs("ws-rowerr").
WillReturnRows(sqlmock.NewRows(cols).
AddRow("ok-key", []byte(`"val"`), int64(1), nil, time.Now()).
RowError(0, errors.New("storage engine fault")))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "ws-rowerr"}}
c.Request = httptest.NewRequest("GET", "/workspaces/ws-rowerr/memory", nil)
handler.List(c)
if w.Code != http.StatusInternalServerError {
t.Errorf("rows.Err() must yield 500, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// ==================== GET /workspaces/:id/memory/:key (Get) ====================
func TestMemoryGet_Success(t *testing.T) {
@@ -69,10 +69,15 @@ func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, absX
model = defaults.Model
}
if model == "" {
// SSOT: per-runtime defaults live in models/runtime_defaults.go
// (see RFC #2873). Consolidated from a duplicate of the same
// branch in workspace_provision.go.
model = models.DefaultModel(runtime)
// SSOT (CTO 2026-05-22, feedback_workspace_model_required_no_platform_default_dynamic_credential_intake):
// model is REQUIRED. The org-import template MUST declare a
// model — either per-workspace (`ws.Model`) or via the org
// defaults block (`defaults.Model`). If neither is present
// the template is malformed and the import must fail-closed
// rather than silently provisioning a workspace with a
// runtime-incompatible default (the prior `anthropic:claude-opus-4-7`
// fallback wedged every codex workspace at adapter init).
return fmt.Errorf("org import: workspace %q has no model and the org defaults block does not provide one (runtime=%s) — model is a required field per the workspace-creation contract; either set `model:` on the workspace or under `defaults:`", ws.Name, runtime)
}
tier := ws.Tier
if tier == 0 {
@@ -712,6 +712,8 @@ func TestHeartbeat_SkipsRemovedRows(t *testing.T) {
// ------------------------------------------------------------
func TestValidateAgentURL(t *testing.T) {
t.Setenv("MOLECULE_ORG_ID", "")
t.Setenv("MOLECULE_DEPLOY_MODE", "self-hosted")
cases := []struct {
name string
url string
@@ -133,24 +133,30 @@ func loadRestartContextData(ctx context.Context, workspaceID string) restartCont
// message bus.
keySet := map[string]struct{}{}
if rows, err := db.DB.QueryContext(ctx, `SELECT key FROM global_secrets`); err == nil {
defer rows.Close()
for rows.Next() {
var k string
if rows.Scan(&k) == nil {
keySet[k] = struct{}{}
}
}
rows.Close()
if err := rows.Err(); err != nil {
log.Printf("loadRestartContextData: global_secrets rows.Err: %v", err)
}
}
if rows, err := db.DB.QueryContext(ctx,
`SELECT key FROM workspace_secrets WHERE workspace_id = $1`, workspaceID,
); err == nil {
defer rows.Close()
for rows.Next() {
var k string
if rows.Scan(&k) == nil {
keySet[k] = struct{}{}
}
}
rows.Close()
if err := rows.Err(); err != nil {
log.Printf("loadRestartContextData: workspace_secrets rows.Err: %v", err)
}
}
for k := range keySet {
d.EnvKeys = append(d.EnvKeys, k)
@@ -417,6 +417,9 @@ func (h *ScheduleHandler) History(c *gin.Context) {
e.Request = json.RawMessage(reqStr)
entries = append(entries, e)
}
if err := rows.Err(); err != nil {
log.Printf("ScheduleHistory: rows error: %v", err)
}
c.JSON(http.StatusOK, entries)
}
@@ -95,6 +95,7 @@ func TestSecurity_GetTemplates_NoAuth_Returns401(t *testing.T) {
func TestSecurity_GetTemplates_FreshInstall_FailsOpen(t *testing.T) {
setupTestDB(t)
setupTestRedis(t)
t.Setenv("ADMIN_TOKEN", "")
authDB, authMock := newFreshInstallAuthDB(t)
tmpDir := t.TempDir()
@@ -152,6 +153,7 @@ func TestSecurity_GetOrgTemplates_NoAuth_Returns401(t *testing.T) {
func TestSecurity_GetOrgTemplates_FreshInstall_FailsOpen(t *testing.T) {
setupTestDB(t)
setupTestRedis(t)
t.Setenv("ADMIN_TOKEN", "")
authDB, authMock := newFreshInstallAuthDB(t)
tmpDir := t.TempDir()
+5 -1
View File
@@ -51,6 +51,10 @@ func (h *TracesHandler) List(c *gin.Context) {
}
defer func() { _ = resp.Body.Close() }()
body, _ := io.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read response body"})
return
}
c.Data(resp.StatusCode, "application/json", body)
}
@@ -321,6 +321,51 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
payload.Runtime = "langgraph"
}
// SSOT (CTO 2026-05-22, feedback_workspace_model_required_no_platform_default_dynamic_credential_intake):
// model is REQUIRED user input for SPAWNED-runtime workspaces. The
// platform must not provide a default; the runtime must not fall back.
// The decision belongs to the user (or to the agent acting on the
// user's behalf), never to the platform.
//
// Empirical trigger: Code Reviewer 5ba15d7e was created with
// `{"name":"Code Reviewer","role":"...","runtime":"codex",...}` (no
// model). The legacy `DefaultModel(runtime)` fallback in
// provisionWorkspace returned `"anthropic:claude-opus-4-7"`. Codex
// adapter only supports openai-* providers — it wedged forever with
// `codex adapter: workspace config picks provider='anthropic' but
// it is not in the providers registry`. PATCH /workspaces/:id
// explicitly disallows updating model (the comment literally reads
// `model not patchable`), so the only recovery path was SQL UPDATE
// or delete+recreate.
//
// External workspaces are EXEMPT — they intentionally do not spawn
// a Docker container or run an adapter; they delegate to a registered
// URL (see provision.go: "external is a first-class runtime that
// intentionally does NOT spawn a Docker container"). The MODEL_REQUIRED
// gate is meaningful for spawned-runtime workspaces where the model
// id drives provider selection at adapter init. For external workspaces
// the contract is the URL, not the model — requiring it would be
// ceremony with no payoff, and would 422 every legitimate "register
// my agent at https://..." flow. The SSOT directive concerns
// platform-side defaults; an external workspace genuinely has no
// "model decision" for the user to make.
//
// Fail-closed at the Create boundary so the caller learns the
// contract immediately — same shape as the controlplane#188
// runtime-unresolved gate above. Caller fixes the request, no
// EC2 launched, no stuck workspace, no operator paging.
isExternal := payload.External || isExternalLikeRuntime(payload.Runtime)
if payload.Model == "" && !isExternal {
log.Printf("Create: FAIL-CLOSED — model is required (runtime=%q template=%q); refusing the silent DefaultModel fallback per CTO 2026-05-22 SSOT directive", payload.Runtime, payload.Template)
c.JSON(http.StatusUnprocessableEntity, gin.H{
"error": "model is required and has no platform-side default — pass an explicit \"model\" in the request body, or use a \"template\" whose config.yaml declares one. See feedback_workspace_model_required_no_platform_default_dynamic_credential_intake for the contract.",
"runtime": payload.Runtime,
"template": payload.Template,
"code": "MODEL_REQUIRED",
})
return
}
ctx := c.Request.Context()
// Convert empty role to NULL
@@ -170,7 +170,7 @@ func TestWorkspaceBudget_Create_WithLimit(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Budgeted Agent","budget_limit":1000}`
body := `{"name":"Budgeted Agent","model":"anthropic:claude-opus-4-7","budget_limit":1000}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.Create(c)
@@ -6,6 +6,9 @@ import (
"encoding/json"
"fmt"
"log"
"net/url"
"os"
"strings"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/models"
@@ -15,6 +18,10 @@ import (
const (
workspaceComputeDiskFloorGB = 30
workspaceComputeDiskCeilingGB = 500
workspaceDisplayMinWidth = 800
workspaceDisplayMaxWidth = 3840
workspaceDisplayMinHeight = 600
workspaceDisplayMaxHeight = 2160
)
type workspaceDisplayResponse struct {
@@ -25,6 +32,7 @@ type workspaceDisplayResponse struct {
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Status string `json:"status,omitempty"`
ViewerURL string `json:"viewer_url,omitempty"`
}
var workspaceComputeInstanceAllowlist = map[string]struct{}{
@@ -54,12 +62,12 @@ func validateWorkspaceCompute(compute models.WorkspaceCompute) error {
return fmt.Errorf("unsupported compute.display.mode")
}
switch compute.Display.Protocol {
case "", "dcv":
case "", "dcv", "novnc":
default:
return fmt.Errorf("unsupported compute.display.protocol")
}
if compute.Display.Width < 0 || compute.Display.Height < 0 {
return fmt.Errorf("compute.display width/height must be non-negative")
if err := validateWorkspaceDisplayDimensions(compute.Display.Width, compute.Display.Height); err != nil {
return err
}
return nil
}
@@ -71,13 +79,26 @@ func validateWorkspaceDisplayConfig(display models.WorkspaceComputeDisplay) erro
return fmt.Errorf("unsupported compute.display.mode")
}
switch display.Protocol {
case "", "dcv":
case "", "dcv", "novnc":
default:
return fmt.Errorf("unsupported compute.display.protocol")
}
if display.Width < 0 || display.Height < 0 {
if err := validateWorkspaceDisplayDimensions(display.Width, display.Height); err != nil {
return err
}
return nil
}
func validateWorkspaceDisplayDimensions(width, height int) error {
if width < 0 || height < 0 {
return fmt.Errorf("compute.display width/height must be non-negative")
}
if width != 0 && (width < workspaceDisplayMinWidth || width > workspaceDisplayMaxWidth) {
return fmt.Errorf("compute.display.width must be between %d and %d", workspaceDisplayMinWidth, workspaceDisplayMaxWidth)
}
if height != 0 && (height < workspaceDisplayMinHeight || height > workspaceDisplayMaxHeight) {
return fmt.Errorf("compute.display.height must be between %d and %d", workspaceDisplayMinHeight, workspaceDisplayMaxHeight)
}
return nil
}
@@ -196,6 +217,18 @@ func (h *WorkspaceHandler) Display(c *gin.Context) {
})
return
}
if viewerURL := workspaceDisplayViewerURL(workspaceID); viewerURL != "" {
c.JSON(200, workspaceDisplayResponse{
Available: true,
Mode: compute.Display.Mode,
Protocol: compute.Display.Protocol,
Width: compute.Display.Width,
Height: compute.Display.Height,
Status: "ready",
ViewerURL: viewerURL,
})
return
}
c.JSON(200, workspaceDisplayResponse{
Available: false,
Reason: "display_session_unavailable",
@@ -206,3 +239,15 @@ func (h *WorkspaceHandler) Display(c *gin.Context) {
Status: "not_configured",
})
}
func workspaceDisplayViewerURL(workspaceID string) string {
base := strings.TrimRight(os.Getenv("DISPLAY_VIEWER_BASE_URL"), "/")
if base == "" {
return ""
}
parsed, err := url.Parse(base)
if err != nil || parsed.Scheme != "https" || parsed.Host == "" {
return ""
}
return base + "/" + url.PathEscape(workspaceID)
}
@@ -43,6 +43,20 @@ func TestValidateWorkspaceCompute_RejectsOutOfRangeRootVolume(t *testing.T) {
}
}
func TestValidateWorkspaceCompute_RejectsOutOfRangeDisplayDimensions(t *testing.T) {
for _, display := range []models.WorkspaceComputeDisplay{
{Mode: "desktop-control", Protocol: "novnc", Width: 799, Height: 1080},
{Mode: "desktop-control", Protocol: "novnc", Width: 3841, Height: 1080},
{Mode: "desktop-control", Protocol: "novnc", Width: 1920, Height: 599},
{Mode: "desktop-control", Protocol: "novnc", Width: 1920, Height: 2161},
} {
compute := models.WorkspaceCompute{Display: display}
if err := validateWorkspaceCompute(compute); err == nil {
t.Fatalf("validateWorkspaceCompute accepted display size %dx%d", display.Width, display.Height)
}
}
}
func TestWorkspaceComputeJSON_OmitsEmptyNestedSections(t *testing.T) {
got, err := workspaceComputeJSON(models.WorkspaceCompute{
InstanceType: "m6i.xlarge",
@@ -110,6 +124,7 @@ func TestWorkspaceCreate_WithInvalidCompute_ReturnsBadRequest(t *testing.T) {
c, _ := gin.CreateTestContext(w)
body := `{
"name":"Oversized Agent",
"model":"gpt-4",
"compute":{"instance_type":"p4d.24xlarge"}
}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
@@ -140,6 +155,7 @@ func TestBuildProvisionerConfig_CopiesComputeSizingFromPayload(t *testing.T) {
Compute: models.WorkspaceCompute{
InstanceType: "m6i.xlarge",
Volume: models.WorkspaceComputeVolume{RootGB: 100},
Display: models.WorkspaceComputeDisplay{Mode: "desktop-control", Protocol: "novnc", Width: 1920, Height: 1080},
},
},
nil,
@@ -153,6 +169,12 @@ func TestBuildProvisionerConfig_CopiesComputeSizingFromPayload(t *testing.T) {
if cfg.DiskGB != 100 {
t.Errorf("cfg.DiskGB = %d, want 100", cfg.DiskGB)
}
if cfg.Display.Mode != "desktop-control" || cfg.Display.Protocol != "novnc" {
t.Errorf("cfg.Display mode/protocol = %q/%q, want desktop-control/novnc", cfg.Display.Mode, cfg.Display.Protocol)
}
if cfg.Display.Width != 1920 || cfg.Display.Height != 1080 {
t.Errorf("cfg.Display size = %dx%d, want 1920x1080", cfg.Display.Width, cfg.Display.Height)
}
}
func TestWithStoredCompute_LoadsComputeForRestartPayloads(t *testing.T) {
@@ -216,7 +238,7 @@ func TestWorkspaceDisplay_DisplayConfiguredReturnsSessionUnavailableContract(t *
mock.ExpectQuery(`SELECT COALESCE\(compute, '\{\}'::jsonb\) FROM workspaces WHERE id = \$1`).
WithArgs("ws-display").
WillReturnRows(sqlmock.NewRows([]string{"compute"}).AddRow(`{"display":{"mode":"desktop-control","protocol":"dcv","width":1920,"height":1080}}`))
WillReturnRows(sqlmock.NewRows([]string{"compute"}).AddRow(`{"display":{"mode":"desktop-control","protocol":"novnc","width":1920,"height":1080}}`))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
@@ -241,8 +263,8 @@ func TestWorkspaceDisplay_DisplayConfiguredReturnsSessionUnavailableContract(t *
if resp["status"] != "not_configured" {
t.Fatalf("status = %v, want not_configured", resp["status"])
}
if resp["mode"] != "desktop-control" || resp["protocol"] != "dcv" {
t.Fatalf("mode/protocol = %v/%v, want desktop-control/dcv", resp["mode"], resp["protocol"])
if resp["mode"] != "desktop-control" || resp["protocol"] != "novnc" {
t.Fatalf("mode/protocol = %v/%v, want desktop-control/novnc", resp["mode"], resp["protocol"])
}
if resp["width"] != float64(1920) || resp["height"] != float64(1080) {
t.Fatalf("width/height = %v/%v, want 1920/1080", resp["width"], resp["height"])
@@ -255,6 +277,83 @@ func TestWorkspaceDisplay_DisplayConfiguredReturnsSessionUnavailableContract(t *
}
}
func TestWorkspaceDisplay_DisplayConfiguredWithViewerBaseReturnsAvailableSession(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
t.Setenv("DISPLAY_VIEWER_BASE_URL", "https://display.example.test/sessions")
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", t.TempDir())
mock.ExpectQuery(`SELECT COALESCE\(compute, '\{\}'::jsonb\) FROM workspaces WHERE id = \$1`).
WithArgs("ws-display").
WillReturnRows(sqlmock.NewRows([]string{"compute"}).AddRow(`{"display":{"mode":"desktop-control","protocol":"novnc","width":1920,"height":1080}}`))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "ws-display"}}
c.Request = httptest.NewRequest("GET", "/workspaces/ws-display/display", nil)
handler.Display(c)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse display response: %v", err)
}
if resp["available"] != true {
t.Fatalf("available = %v, want true", resp["available"])
}
if resp["viewer_url"] != "https://display.example.test/sessions/ws-display" {
t.Fatalf("viewer_url = %v, want workspace viewer URL", resp["viewer_url"])
}
if resp["reason"] != nil {
t.Fatalf("reason = %v, want omitted", resp["reason"])
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
func TestWorkspaceDisplay_DisplayConfiguredWithInvalidViewerBaseReturnsUnavailable(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
t.Setenv("DISPLAY_VIEWER_BASE_URL", "http://display.example.test/sessions")
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", t.TempDir())
workspaceID := "ws-display"
mock.ExpectQuery(`SELECT COALESCE\(compute, '\{\}'::jsonb\) FROM workspaces WHERE id = \$1`).
WithArgs(workspaceID).
WillReturnRows(sqlmock.NewRows([]string{"compute"}).AddRow(`{"display":{"mode":"desktop-control","protocol":"novnc","width":1920,"height":1080}}`))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: workspaceID}}
c.Request = httptest.NewRequest("GET", "/workspaces/"+workspaceID+"/display", nil)
handler.Display(c)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse display response: %v", err)
}
if resp["available"] != false {
t.Fatalf("available = %v, want false", resp["available"])
}
if resp["viewer_url"] != nil {
t.Fatalf("viewer_url = %v, want omitted for invalid viewer base", resp["viewer_url"])
}
if resp["reason"] != "display_session_unavailable" {
t.Fatalf("reason = %v, want display_session_unavailable", resp["reason"])
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
func TestWorkspaceDisplay_IgnoresUnrelatedStoredComputeSizingDrift(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
@@ -262,7 +361,7 @@ func TestWorkspaceDisplay_IgnoresUnrelatedStoredComputeSizingDrift(t *testing.T)
mock.ExpectQuery(`SELECT COALESCE\(compute, '\{\}'::jsonb\) FROM workspaces WHERE id = \$1`).
WithArgs("ws-display-sizing-drift").
WillReturnRows(sqlmock.NewRows([]string{"compute"}).AddRow(`{"instance_type":"old.large","display":{"mode":"desktop-control","protocol":"dcv","width":1920,"height":1080}}`))
WillReturnRows(sqlmock.NewRows([]string{"compute"}).AddRow(`{"instance_type":"old.large","display":{"mode":"desktop-control","protocol":"novnc","width":1920,"height":1080}}`))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
@@ -435,13 +435,16 @@ func (h *WorkspaceHandler) CascadeDelete(ctx context.Context, id string) ([]stri
if err != nil {
return nil, nil, fmt.Errorf("descendant query: %w", err)
}
defer descRows.Close()
for descRows.Next() {
var descID string
if descRows.Scan(&descID) == nil {
descendantIDs = append(descendantIDs, descID)
}
}
descRows.Close()
if err := descRows.Err(); err != nil {
return nil, nil, fmt.Errorf("CascadeDelete: failed iterating descendants: %w", err)
}
allIDs := append([]string{id}, descendantIDs...)
@@ -503,6 +503,32 @@ func TestCascadeDelete_DescendantQueryError(t *testing.T) {
// sqlmock verifies all expected queries were executed
}
func TestCascadeDelete_DescendantRowsError(t *testing.T) {
mock, _ := setupWorkspaceCrudTest(t)
wsID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
// RowError(0, ...) requires a real row at index 0 to be reachable —
// sqlmock only invokes nextErr[N] when r.pos-1 == N and the row exists.
// AddRow ensures Next() attempts the first row, triggers the error,
// and rows.Err() returns the injected error.
h := &WorkspaceHandler{}
rows := sqlmock.NewRows([]string{"id"}).AddRow("desc-1").RowError(0, sql.ErrConnDone)
mock.ExpectQuery(`WITH RECURSIVE descendants AS`).
WithArgs(wsID).
WillReturnRows(rows)
deleted, stopErrs, err := h.CascadeDelete(context.Background(), wsID)
if err == nil {
t.Fatal("CascadeDelete returned nil error; want descendant rows error")
}
if deleted != nil {
t.Errorf("deleted = %v; want nil", deleted)
}
if stopErrs != nil {
t.Errorf("stopErrs = %v; want nil", stopErrs)
}
}
// Note: Full CascadeDelete testing requires mocking StopWorkspace, RemoveVolume,
// and provisioner calls — covered in integration tests. Unit tests here focus on
// the validation and pre-condition paths.
@@ -288,16 +288,22 @@ func (h *WorkspaceHandler) buildProvisionerConfig(
}
return provisioner.WorkspaceConfig{
WorkspaceID: workspaceID,
TemplatePath: templatePath,
ConfigFiles: configFiles,
PluginsPath: pluginsPath,
WorkspacePath: workspacePath,
WorkspaceAccess: workspaceAccess,
Tier: payload.Tier,
Runtime: payload.Runtime,
InstanceType: payload.Compute.InstanceType,
DiskGB: int32(payload.Compute.Volume.RootGB),
WorkspaceID: workspaceID,
TemplatePath: templatePath,
ConfigFiles: configFiles,
PluginsPath: pluginsPath,
WorkspacePath: workspacePath,
WorkspaceAccess: workspaceAccess,
Tier: payload.Tier,
Runtime: payload.Runtime,
InstanceType: payload.Compute.InstanceType,
DiskGB: int32(payload.Compute.Volume.RootGB),
Display: provisioner.WorkspaceDisplayConfig{
Mode: payload.Compute.Display.Mode,
Width: payload.Compute.Display.Width,
Height: payload.Compute.Display.Height,
Protocol: payload.Compute.Display.Protocol,
},
EnvVars: envVars,
PlatformURL: h.platformURL,
AwarenessURL: os.Getenv("AWARENESS_URL"),
@@ -550,13 +556,22 @@ func (h *WorkspaceHandler) ensureDefaultConfig(workspaceID string, payload model
// via a crafted runtime string (#241).
runtime := sanitizeRuntime(payload.Runtime)
// Generate a minimal config.yaml
// Generate a minimal config.yaml.
//
// SSOT (CTO 2026-05-22): model is REQUIRED user input. The platform
// must not provide a default; the runtime must not fall back. The
// Create handler is responsible for rejecting empty model BEFORE
// reaching provisionWorkspace; this is a defence-in-depth assertion.
// If we hit here with an empty model the YAML below would still
// render a `model: ""` line — which renders all downstream provider
// derivation undefined. Log loudly and let the workspace boot into
// not_configured rather than masking the contract violation with a
// silently-broken default (the prior `anthropic:claude-opus-4-7`
// fallback was the canonical example — every codex workspace
// created without an explicit model wedged).
model := payload.Model
if model == "" {
// SSOT: per-runtime defaults live in models/runtime_defaults.go
// (see RFC #2873). Was previously duplicated here AND in
// org_import.go; consolidating prevents silent drift.
model = models.DefaultModel(runtime)
log.Printf("ensureDefaultConfig: workspace %s reached provisioning with empty model — Create handler should have rejected this; rendering empty model: \"\" in config.yaml (workspace will boot not_configured)", workspaceID)
}
if runtime == "claude-code" {
model = normalizeClaudeCodeModel(model)
@@ -756,47 +756,55 @@ func TestWorkspaceCreate_FirstDeploy_PersistsModelAndProvider(t *testing.T) {
}
}
// TestWorkspaceCreate_FirstDeploy_NoModel_NoSecretWritten asserts that
// when payload.Model is empty, NEITHER MODEL nor LLM_PROVIDER is
// written. Important: the canvas can omit `model` (template inherits
// the runtime default later); we must not poison workspace_secrets with
// empty rows in that case.
func TestWorkspaceCreate_FirstDeploy_NoModel_NoSecretWritten(t *testing.T) {
// TestWorkspaceCreate_FirstDeploy_NoModel_Returns422 inverts the prior
// premise (CTO 2026-05-22 SSOT directive — see
// feedback_workspace_model_required_no_platform_default_dynamic_credential_intake
// and TestCreate_ModelRequired_Returns422 in handlers_extended_test.go).
//
// Pre-2026-05-22 the canvas was allowed to omit `model` and the workspace
// would 201 with no workspace_secrets rows for MODEL/LLM_PROVIDER (the
// thinking being that templates inherit the runtime default later). That
// "soft fallback" was the load-bearing bug magnet — `DefaultModel(runtime)`
// would later return `anthropic:claude-opus-4-7`, and codex workspaces
// wedged forever at adapter init.
//
// New contract: empty model is a 422 MODEL_REQUIRED, with NO DB writes
// at all. The gate fires at the Create boundary before INSERT INTO
// workspaces. The follow-on workspace_secrets gate (which the original
// test pinned) is therefore unreachable on the empty-model path — there
// is no row to mint secrets for.
func TestWorkspaceCreate_FirstDeploy_NoModel_Returns422(t *testing.T) {
mock := setupTestDB(t)
setupTestRedis(t)
broadcaster := newTestBroadcaster()
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
mock.ExpectBegin()
mock.ExpectExec("INSERT INTO workspaces").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
// NO INSERT INTO workspace_secrets here — the gate is payload.Model != "".
mock.ExpectExec("INSERT INTO canvas_layouts").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("INSERT INTO structure_events").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`UPDATE workspaces SET status =`).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("INSERT INTO workspace_auth_tokens").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("INSERT INTO structure_events").
WillReturnResult(sqlmock.NewResult(0, 1))
// NO mock.ExpectBegin / INSERT INTO workspaces — the Create gate
// MUST fire before any DB write. If the gate fires late, sqlmock
// will surface "call to ExecQuery 'INSERT INTO workspaces' was not
// expected" — which is exactly the failure mode we want to flag.
// Body: hermes runtime WITHOUT external:true (the external-runtime
// exemption — see TestCreate_ExternalRuntime_NoModel_OK — does NOT
// apply here; hermes spawns a real adapter and model selection
// matters at adapter init). This is exactly the shape the old
// "no-model-no-secret-write" test pinned, minus the external flag.
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"No Model Agent","runtime":"hermes","external":true}`
body := `{"name":"No Model Agent","runtime":"hermes"}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.Create(c)
if w.Code != http.StatusCreated {
t.Fatalf("expected status 201, got %d: %s", w.Code, w.Body.String())
if w.Code != http.StatusUnprocessableEntity {
t.Fatalf("expected 422 MODEL_REQUIRED for empty model, got %d: %s", w.Code, w.Body.String())
}
if !bytes.Contains(w.Body.Bytes(), []byte(`"code":"MODEL_REQUIRED"`)) {
t.Errorf("expected code=MODEL_REQUIRED in body, got %s", w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("sqlmock expectations not met — empty payload.Model should NOT trigger workspace_secrets writes: %v", err)
t.Errorf("sqlmock saw an unexpected DB write — the MODEL_REQUIRED gate fired too late: %v", err)
}
}
@@ -193,10 +193,17 @@ func TestEnsureDefaultConfig_Hermes(t *testing.T) {
broadcaster := newTestBroadcaster()
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
// Post-CTO-SSOT-directive (2026-05-22): model is required user input;
// ensureDefaultConfig no longer fills in a runtime default. The Create
// handler gates on empty model and 422s before reaching here, so this
// test now passes the model explicitly to exercise the YAML rendering
// path — same model value the prior implicit DefaultModel("hermes")
// returned.
payload := models.CreateWorkspacePayload{
Name: "Test Agent",
Tier: 1,
Runtime: "hermes",
Model: "anthropic:claude-opus-4-7",
}
files := handler.ensureDefaultConfig("ws-test-123", payload)
@@ -219,7 +226,7 @@ func TestEnsureDefaultConfig_Hermes(t *testing.T) {
t.Errorf("config.yaml missing tier, got:\n%s", content)
}
if !contains(content, `model: "anthropic:claude-opus-4-7"`) {
t.Errorf("config.yaml should use default non-claude model, got:\n%s", content)
t.Errorf("config.yaml should render the supplied model, got:\n%s", content)
}
}
@@ -227,10 +234,14 @@ func TestEnsureDefaultConfig_ClaudeCode(t *testing.T) {
broadcaster := newTestBroadcaster()
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
// Post-CTO-SSOT-directive (2026-05-22): model is supplied explicitly
// instead of relying on the deleted DefaultModel("claude-code") =
// "sonnet" fallback. The Create handler 422s on empty model upstream.
payload := models.CreateWorkspacePayload{
Name: "Code Agent",
Tier: 2,
Runtime: "claude-code",
Model: "sonnet",
}
files := handler.ensureDefaultConfig("ws-code-123", payload)
@@ -407,9 +418,16 @@ func TestEnsureDefaultConfig_EmptyRuntimeDefaultsToClaudeCode(t *testing.T) {
broadcaster := newTestBroadcaster()
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
// Post-CTO-SSOT-directive (2026-05-22): ensureDefaultConfig is no
// longer the source of the model default — it just renders whatever
// the Create handler decided. The "empty runtime → claude-code"
// fallback inside sanitizeRuntime() is still in effect; this test
// continues to pin that behaviour by supplying the explicit
// claude-code model that the Create handler would have required.
payload := models.CreateWorkspacePayload{
Name: "Default Agent",
Tier: 1,
Name: "Default Agent",
Tier: 1,
Model: "sonnet",
}
files := handler.ensureDefaultConfig("ws-empty-rt", payload)
@@ -418,7 +436,7 @@ func TestEnsureDefaultConfig_EmptyRuntimeDefaultsToClaudeCode(t *testing.T) {
t.Errorf("empty runtime should default to claude-code, got:\n%s", configYAML)
}
if !contains(configYAML, `model: "sonnet"`) {
t.Errorf("claude-code default model should be sonnet (quoted), got:\n%s", configYAML)
t.Errorf("claude-code workspace should render the supplied model (quoted), got:\n%s", configYAML)
}
}
@@ -788,6 +806,8 @@ func TestIssueAndInjectToken_HappyPath(t *testing.T) {
mock := setupTestDB(t)
broadcaster := newTestBroadcaster()
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
t.Setenv("MOLECULE_ORG_ID", "")
t.Setenv("MOLECULE_DEPLOY_MODE", "self-hosted")
// RevokeAllForWorkspace UPDATE (0 rows — no prior tokens, still succeeds)
mock.ExpectExec(`UPDATE workspace_auth_tokens SET revoked_at`).
@@ -825,6 +845,8 @@ func TestIssueAndInjectToken_RotatesExistingToken(t *testing.T) {
mock := setupTestDB(t)
broadcaster := newTestBroadcaster()
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
t.Setenv("MOLECULE_ORG_ID", "")
t.Setenv("MOLECULE_DEPLOY_MODE", "self-hosted")
// RevokeAllForWorkspace: 1 existing token revoked
mock.ExpectExec(`UPDATE workspace_auth_tokens SET revoked_at`).
@@ -891,6 +913,8 @@ func TestIssueAndInjectToken_IssueFailSkipsInjection(t *testing.T) {
mock := setupTestDB(t)
broadcaster := newTestBroadcaster()
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
t.Setenv("MOLECULE_ORG_ID", "")
t.Setenv("MOLECULE_DEPLOY_MODE", "self-hosted")
mock.ExpectExec(`UPDATE workspace_auth_tokens SET revoked_at`).
WithArgs("ws-418-issue-fail").
@@ -917,6 +941,8 @@ func TestIssueAndInjectToken_NilConfigFilesAllocated(t *testing.T) {
mock := setupTestDB(t)
broadcaster := newTestBroadcaster()
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
t.Setenv("MOLECULE_ORG_ID", "")
t.Setenv("MOLECULE_DEPLOY_MODE", "self-hosted")
mock.ExpectExec(`UPDATE workspace_auth_tokens SET revoked_at`).
WithArgs("ws-418-nil-cfg").
@@ -858,6 +858,9 @@ func (h *WorkspaceHandler) Pause(c *gin.Context) {
toPause = append(toPause, struct{ id, name string }{cid, cname})
}
}
if err := rows.Err(); err != nil {
log.Printf("Pause: descendant query rows.Err: %v", err)
}
}
// Stop containers and mark all as paused. StopWorkspaceAuto routes
@@ -939,6 +942,9 @@ func (h *WorkspaceHandler) Resume(c *gin.Context) {
toResume = append(toResume, ws)
}
}
if err := rows.Err(); err != nil {
log.Printf("Resume: descendant query rows.Err: %v", err)
}
}
// Re-provision all
@@ -349,7 +349,7 @@ func TestWorkspaceCreate_DBInsertError(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Failing Agent"}`
body := `{"name":"Failing Agent","model":"anthropic:claude-opus-4-7"}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -391,7 +391,7 @@ func TestWorkspaceCreate_DefaultsApplied(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Default Agent"}`
body := `{"name":"Default Agent","model":"anthropic:claude-opus-4-7"}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -438,7 +438,7 @@ func TestWorkspaceCreate_SaaSHardForcesTier4(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"SaaS External Agent","runtime":"external","external":true,"url":"https://example.com/agent","tier":2}`
body := `{"name":"SaaS External Agent","runtime":"external","model":"external:custom","external":true,"url":"https://example.com/agent","tier":2}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -479,7 +479,7 @@ func TestWorkspaceCreate_WithSecrets_Persists(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Hermes Agent","runtime":"hermes","external":true,"secrets":{"HERMES_API_KEY":"sk-test-123"}}`
body := `{"name":"Hermes Agent","runtime":"hermes","model":"anthropic:claude-opus-4-7","external":true,"secrets":{"HERMES_API_KEY":"sk-test-123"}}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -513,7 +513,7 @@ func TestWorkspaceCreate_SecretPersistFails_RollsBack(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Rollback Agent","secrets":{"OPENAI_API_KEY":"sk-fail"}}`
body := `{"name":"Rollback Agent","model":"anthropic:claude-opus-4-7","secrets":{"OPENAI_API_KEY":"sk-fail"}}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -548,7 +548,7 @@ func TestWorkspaceCreate_EmptySecrets_OK(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"No Secrets Agent","external":true,"secrets":{}}`
body := `{"name":"No Secrets Agent","model":"anthropic:claude-opus-4-7","external":true,"secrets":{}}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -587,7 +587,7 @@ func TestWorkspaceCreate_ExternalURL_SSRFSafe(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Ext Agent","runtime":"external","external":true,"url":"http://localhost:8000"}`
body := `{"name":"Ext Agent","runtime":"external","model":"external:custom","external":true,"url":"http://localhost:8000"}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -629,7 +629,7 @@ func TestWorkspaceCreate_KimiRuntime_PreservesLabel(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Kimi Agent","runtime":"kimi","tier":3,"canvas":{"x":100,"y":100}}`
body := `{"name":"Kimi Agent","runtime":"kimi","model":"kimi-coding/kimi-k2-coding-6","tier":3,"canvas":{"x":100,"y":100}}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -659,7 +659,7 @@ func TestWorkspaceCreate_ExternalURL_SSRFMetadataBlocked(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Bad Agent","runtime":"external","external":true,"url":"http://169.254.169.254/latest/meta-data/"}`
body := `{"name":"Bad Agent","runtime":"external","model":"external:custom","external":true,"url":"http://169.254.169.254/latest/meta-data/"}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -690,7 +690,7 @@ func TestWorkspaceCreate_ExternalURL_SSRFLoopbackBlocked(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Bad Loopback","runtime":"external","external":true,"url":"http://127.0.0.1:9000/a2a"}`
body := `{"name":"Bad Loopback","runtime":"external","model":"external:custom","external":true,"url":"http://127.0.0.1:9000/a2a"}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -1844,39 +1844,43 @@ func TestWorkspaceCreate_188_TemplateConfigNoRuntimeKey_FailsClosed(t *testing.T
}
}
// Regression guard: the legitimate default path (no template, no runtime —
// bare {"name":...}) MUST still default to langgraph and return 201. The
// #188 fix must not break this.
func TestWorkspaceCreate_188_NoTemplateNoRuntime_StillDefaultsLanggraph(t *testing.T) {
mock := setupTestDB(t)
// Pre-2026-05-22 this test guarded "bare {name} → langgraph 201" — the
// regression check for controlplane#188 (where an explicit runtime that
// failed to resolve must NOT silently substitute langgraph) had a sibling
// to ensure the LEGITIMATE bare default still landed on langgraph.
//
// Post-CTO-SSOT-directive (2026-05-22) bare body is 422 MODEL_REQUIRED
// before reaching the langgraph branch — the gate runs AFTER the
// langgraph-default assignment so the error body still surfaces
// runtime=langgraph (helps the caller see "ok, langgraph WOULD have
// been the runtime, but you still owe me a model"). The bare-body
// langgraph 201 path no longer exists; what we guard now is the
// 422-shape diagnostic.
//
// Bare-body-with-explicit-model 201 (the new "legitimate default" path)
// is covered by TestWorkspaceCreate in handlers_test.go — no need to
// duplicate the mock dance here.
func TestWorkspaceCreate_188_NoTemplateNoRuntime_NowMODEL_REQUIRED(t *testing.T) {
setupTestDB(t)
setupTestRedis(t)
broadcaster := newTestBroadcaster()
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
mock.ExpectBegin()
mock.ExpectExec("INSERT INTO workspaces").
WithArgs(sqlmock.AnyArg(), "Plain Default", nil, 3, "langgraph", sqlmock.AnyArg(), (*string)(nil), nil, "none", (*int64)(nil), models.DefaultMaxConcurrentTasks, "push").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
mock.ExpectExec("INSERT INTO canvas_layouts").
WithArgs(sqlmock.AnyArg(), float64(0), float64(0)).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("INSERT INTO structure_events").
WillReturnResult(sqlmock.NewResult(0, 1))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Plain Default"}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.Create(c)
if w.Code != http.StatusCreated {
t.Fatalf("expected 201 (legitimate default path), got %d: %s", w.Code, w.Body.String())
if w.Code != http.StatusUnprocessableEntity {
t.Fatalf("bare-body create: expected 422 MODEL_REQUIRED, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
if !bytes.Contains(w.Body.Bytes(), []byte(`"code":"MODEL_REQUIRED"`)) {
t.Errorf("bare-body create: expected code=MODEL_REQUIRED in body, got %s", w.Body.String())
}
if !bytes.Contains(w.Body.Bytes(), []byte(`"runtime":"langgraph"`)) {
t.Errorf("bare-body create: expected runtime=\"langgraph\" in 422 body (the gate runs AFTER the langgraph-default assignment so the diagnostic surfaces what runtime WOULD have been used), got %s", w.Body.String())
}
}
@@ -1901,7 +1905,7 @@ func TestWorkspaceCreate_188_ExplicitRuntimeNoTemplate_OK(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"Explicit Codex","runtime":"codex"}`
body := `{"name":"Explicit Codex","runtime":"codex","model":"gpt-5.5"}`
c.Request = httptest.NewRequest("POST", "/workspaces", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
@@ -329,7 +329,13 @@ func (c *Client) doJSON(ctx context.Context, method, path string, reqBody interf
func decodeError(resp *http.Response) error {
var e contract.Error
body, _ := io.ReadAll(resp.Body)
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
return &contract.Error{
Code: httpStatusToCode(resp.StatusCode),
Message: fmt.Sprintf("status %d (read body failed: %v)", resp.StatusCode, readErr),
}
}
if len(body) == 0 {
return &contract.Error{
Code: httpStatusToCode(resp.StatusCode),
@@ -3,6 +3,7 @@ package pgplugin
import (
"encoding/json"
"errors"
"log"
"net/http"
"strings"
@@ -246,7 +247,9 @@ func (h *Handler) forget(w http.ResponseWriter, r *http.Request, id string) {
func writeJSON(w http.ResponseWriter, status int, body interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(body)
if err := json.NewEncoder(w).Encode(body); err != nil {
log.Printf("pgplugin: JSON encode error: %v", err)
}
}
func writeError(w http.ResponseWriter, status int, code contract.ErrorCode, message string, details map[string]interface{}) {
@@ -256,6 +256,7 @@ func TestWorkspaceAuth_WrongWorkspace_Returns401(t *testing.T) {
// live tokens anywhere) the middleware must let the request through so existing
// deployments keep working during the Phase-30 rollout.
func TestAdminAuth_FailOpen_NoTokensGlobally(t *testing.T) {
t.Setenv("ADMIN_TOKEN", "")
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New: %v", err)
@@ -375,6 +376,7 @@ func TestAdminAuth_C11_DeleteNoBearer_Returns401(t *testing.T) {
// TestAdminAuth_ValidBearer_Passes — a valid bearer token (from any workspace)
// must be accepted for admin routes.
func TestAdminAuth_ValidBearer_Passes(t *testing.T) {
t.Setenv("ADMIN_TOKEN", "")
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New: %v", err)
@@ -418,6 +420,7 @@ func TestAdminAuth_ValidBearer_Passes(t *testing.T) {
// TestAdminAuth_InvalidBearer_Returns401 — wrong token must not grant admin access.
func TestAdminAuth_InvalidBearer_Returns401(t *testing.T) {
t.Setenv("ADMIN_TOKEN", "")
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New: %v", err)
@@ -700,6 +703,7 @@ func TestAdminAuth_Issue180_ApprovalsListing_NoBearer_Returns401(t *testing.T) {
// fail-open contract: on a fresh install (no tokens anywhere), the middleware
// must not block the canvas from polling /approvals/pending.
func TestAdminAuth_Issue180_ApprovalsListing_FailOpen_NoTokens(t *testing.T) {
t.Setenv("ADMIN_TOKEN", "")
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New: %v", err)
@@ -1098,6 +1102,7 @@ func TestCanvasOrBearer_TokensExist_CanvasOrigin_Passes(t *testing.T) {
// issuing workspace has status='removed' must not grant admin access.
// The JOIN in ValidateAnyToken filters the row out, resulting in ErrNoRows.
func TestAdminAuth_RemovedWorkspaceToken_Returns401(t *testing.T) {
t.Setenv("ADMIN_TOKEN", "")
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New: %v", err)
@@ -1251,6 +1256,7 @@ func TestAdminAuth_623_ForgedCORSOrigin_Returns401(t *testing.T) {
// TestAdminAuth_623_ValidBearer_WithOrigin_Passes — bearer + matching Origin
// should still work (the Origin is irrelevant once the bearer validates).
func TestAdminAuth_623_ValidBearer_WithOrigin_Passes(t *testing.T) {
t.Setenv("ADMIN_TOKEN", "")
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock: %v", err)
@@ -1,39 +1,31 @@
package models
// runtime_defaults.go — single source of truth for per-runtime defaults
// the platform applies when the operator/agent didn't supply a value.
// runtime_defaults.go — DELETED helper. Intentionally empty.
//
// Why this lives in models/ (not handlers/): default selection is a
// pure data fact about the runtime, not handler logic. Multiple
// callers (Create-workspace handler, org-import handler, future
// auto-provision paths) need the same answer; concentrating the
// rule here means one edit when a runtime's default changes.
// Previously held `DefaultModel(runtime string) string` which returned
// "sonnet" for claude-code and "anthropic:claude-opus-4-7" for everything
// else. That function was a SOFT-FALLBACK bug magnet:
//
// Related work (RFC #2873): this is the seed for a future
// `RuntimeConfig` interface that will also expose `ProvisioningTimeout()`,
// `CapabilitiesSupported()`, and other per-runtime facts. For now the
// surface is one helper — extracted from the duplicate branch in
// workspace_provision.go:537 and org_import.go:54 that diverged silently
// during refactors before this consolidation.
// DefaultModel returns the model slug to use when a workspace is
// created without an explicit model and the runtime can't infer one
// from its own config.
// - codex workspaces created without an explicit `model` silently
// received `anthropic:claude-opus-4-7`. Codex adapter only supports
// openai-* providers, so they wedged in `not_configured` with
// `codex adapter: workspace config picks provider='anthropic' but
// it is not in the providers registry`. The fallback never matched
// a runtime that could actually use it (only langgraph + hermes
// could even partially execute anthropic:claude-opus-4-7 without
// extra credential plumbing). It existed as a "must return
// something" placeholder that turned every silent miss into a
// prod incident.
//
// - claude-code: "sonnet" — Anthropic's CLI accepts the short
// name and resolves it via the operator's anthropic-oauth or
// ANTHROPIC_API_KEY chain.
// - everything else (hermes, langgraph, autogen, codex, openclaw,
// external, ""): a fully-qualified
// vendor:model slug that the universal MODEL_PROVIDER chain in
// molecule-core PR #247 can route via per-vendor required_env.
// - The fallback hid the contract bug at every callsite: Create
// handler, org_import, anywhere a stale CreateWorkspacePayload
// bubbled through to provisionWorkspace.
//
// The function never returns an empty string; an unknown runtime
// gets the universal default rather than failing closed (matches the
// pre-refactor behavior — both call sites used the same fallback).
func DefaultModel(runtime string) string {
if runtime == "claude-code" {
return "sonnet"
}
return "anthropic:claude-opus-4-7"
}
// SSOT principle (CTO 2026-05-22T03:42Z, feedback_workspace_model_required_no_platform_default_dynamic_credential_intake):
// model / provider / provider-credential are REQUIRED user input at
// create time. The platform must not provide a default. The runtime
// must not fall back. Decision belongs to the user (or to the agent
// acting on the user's behalf), never to the platform.
//
// Callers that previously fell back to DefaultModel must now fail-closed
// when model is empty after template-resolution.
@@ -1,59 +1,11 @@
package models
import "testing"
// TestDefaultModel pins the contract: known runtimes return their
// expected default; unknowns and the empty string fall through to the
// universal default. Add new runtimes here as `case` entries — pre-fix
// adding a runtime required two source edits + an audit; post-SSOT it
// requires one entry in DefaultModel + one assertion here.
func TestDefaultModel(t *testing.T) {
cases := []struct {
runtime string
want string
}{
// Known runtimes.
{"claude-code", "sonnet"},
// Universal fallback for everything else. Each runtime is named
// explicitly so a future drift (e.g., adding a hermes-specific
// branch) shows up as a failure on the runtime that drifted, not
// as a generic "unknown" failure.
{"hermes", "anthropic:claude-opus-4-7"},
{"langgraph", "anthropic:claude-opus-4-7"},
{"autogen", "anthropic:claude-opus-4-7"},
{"codex", "anthropic:claude-opus-4-7"},
{"openclaw", "anthropic:claude-opus-4-7"},
{"external", "anthropic:claude-opus-4-7"},
// Unknown / empty — fall through to universal default rather
// than failing closed. Pre-refactor both call sites also fell
// through; pinning the existing behavior, not changing it.
{"", "anthropic:claude-opus-4-7"},
{"some-future-runtime", "anthropic:claude-opus-4-7"},
{"CLAUDE-CODE", "anthropic:claude-opus-4-7"}, // case-sensitive — matches prior behavior
}
for _, tc := range cases {
t.Run(tc.runtime, func(t *testing.T) {
got := DefaultModel(tc.runtime)
if got != tc.want {
t.Errorf("DefaultModel(%q) = %q, want %q", tc.runtime, got, tc.want)
}
})
}
}
// TestDefaultModel_NeverEmpty — invariant: no input produces an empty
// string. The handlers that consume this would write empty into
// config.yaml, which the runtime then can't dispatch — pinning the
// non-empty contract here protects against a future "return early on
// unknown runtime" change that would silently break workspace creation.
func TestDefaultModel_NeverEmpty(t *testing.T) {
for _, runtime := range []string{
"", "claude-code", "hermes", "unknown-runtime",
} {
if got := DefaultModel(runtime); got == "" {
t.Errorf("DefaultModel(%q) returned empty string", runtime)
}
}
}
// runtime_defaults_test.go — previously pinned DefaultModel's contract
// (claude-code → "sonnet", everything else → "anthropic:claude-opus-4-7").
//
// DefaultModel was removed as a soft-fallback bug magnet (CTO 2026-05-22):
// model is REQUIRED user input; the platform must not provide a default.
// See runtime_defaults.go for the deletion rationale, and the new
// fail-closed gate in `handlers.WorkspaceHandler.Create` for the boundary
// enforcement. No test stub here — the contract is "this function does
// not exist", which the type-checker enforces at compile time.
@@ -152,14 +152,15 @@ func (p *CPProvisioner) adminAuthHeaders(req *http.Request) {
}
type cpProvisionRequest struct {
OrgID string `json:"org_id"`
WorkspaceID string `json:"workspace_id"`
Runtime string `json:"runtime"`
Tier int `json:"tier"`
InstanceType string `json:"instance_type,omitempty"`
DiskGB int32 `json:"disk_gb,omitempty"`
PlatformURL string `json:"platform_url"`
Env map[string]string `json:"env"`
OrgID string `json:"org_id"`
WorkspaceID string `json:"workspace_id"`
Runtime string `json:"runtime"`
Tier int `json:"tier"`
InstanceType string `json:"instance_type,omitempty"`
DiskGB int32 `json:"disk_gb,omitempty"`
Display WorkspaceDisplayConfig `json:"display,omitempty"`
PlatformURL string `json:"platform_url"`
Env map[string]string `json:"env"`
// ConfigFiles are template + generated config files to write into the
// EC2 instance's /configs directory. OFFSEC-010: collected by
// collectCPConfigFiles which rejects symlinks and non-regular files
@@ -214,6 +215,7 @@ func (p *CPProvisioner) Start(ctx context.Context, cfg WorkspaceConfig) (string,
Tier: cfg.Tier,
InstanceType: cfg.InstanceType,
DiskGB: cfg.DiskGB,
Display: cfg.Display,
PlatformURL: cfg.PlatformURL,
Env: env,
ConfigFiles: configFiles,
@@ -241,9 +243,12 @@ func (p *CPProvisioner) Start(ctx context.Context, cfg WorkspaceConfig) (string,
// Cap body read at 64 KiB — the CP only ever returns small JSON
// responses; an unbounded read could be weaponized into log-flood
// DoS by a compromised upstream.
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
respBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
if readErr != nil {
return "", fmt.Errorf("cp provisioner: read response body: %w", readErr)
}
var result cpProvisionResponse
json.Unmarshal(respBody, &result)
unmarshalErr := json.Unmarshal(respBody, &result)
if resp.StatusCode != http.StatusCreated {
// Prefer the structured {"error":"..."} field. Do NOT fall back
@@ -257,6 +262,10 @@ func (p *CPProvisioner) Start(ctx context.Context, cfg WorkspaceConfig) (string,
return "", fmt.Errorf("cp provisioner: provision failed (%d): %s", resp.StatusCode, errMsg)
}
if unmarshalErr != nil {
return "", fmt.Errorf("cp provisioner: decode 201 response: %w", unmarshalErr)
}
log.Printf("CP provisioner: workspace %s → EC2 instance %s (%s)", cfg.WorkspaceID, result.InstanceID, result.State)
provlog.Event("provision.ec2_started", map[string]any{
"workspace_id": cfg.WorkspaceID,
@@ -409,7 +418,11 @@ func (p *CPProvisioner) Stop(ctx context.Context, workspaceID string) error {
// Read a bounded slice of the body so the error message gives ops
// enough to triage without risking a multi-MB log line on a
// pathological response. 512 bytes covers any sane error envelope.
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 512))
if readErr != nil {
return fmt.Errorf("cp provisioner: stop %s: unexpected %d (read body failed: %w)",
workspaceID, resp.StatusCode, readErr)
}
return fmt.Errorf("cp provisioner: stop %s: unexpected %d: %s",
workspaceID, resp.StatusCode, strings.TrimSpace(string(body)))
}
@@ -197,6 +197,12 @@ func TestStart_HappyPath(t *testing.T) {
if body.DiskGB != 100 {
t.Errorf("disk_gb = %d, want 100", body.DiskGB)
}
if body.Display.Mode != "desktop-control" || body.Display.Protocol != "novnc" {
t.Errorf("display mode/protocol = %q/%q, want desktop-control/novnc", body.Display.Mode, body.Display.Protocol)
}
if body.Display.Width != 1920 || body.Display.Height != 1080 {
t.Errorf("display size = %dx%d, want 1920x1080", body.Display.Width, body.Display.Height)
}
w.WriteHeader(http.StatusCreated)
_, _ = io.WriteString(w, `{"instance_id":"i-abc123","state":"pending"}`)
}))
@@ -212,6 +218,7 @@ func TestStart_HappyPath(t *testing.T) {
id, err := p.Start(context.Background(), WorkspaceConfig{
WorkspaceID: "ws-1", Runtime: "python", Tier: 1, PlatformURL: "http://tenant",
InstanceType: "m6i.xlarge", DiskGB: 100,
Display: WorkspaceDisplayConfig{Mode: "desktop-control", Protocol: "novnc", Width: 1920, Height: 1080},
})
if err != nil {
t.Fatalf("Start: %v", err)
@@ -442,6 +449,26 @@ func TestStart_SymlinkTemplatePathError(t *testing.T) {
}
}
// TestStart_Malformed201SurfacesError — when CP returns 201 Created with
// unparseable JSON, Start must return an error instead of silently
// returning an empty instance_id. CR2 blocker from review #5552.
func TestStart_Malformed201SurfacesError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
_, _ = io.WriteString(w, `{"instance_id": broken-json`)
}))
defer srv.Close()
p := &CPProvisioner{baseURL: srv.URL, orgID: "org-1", httpClient: srv.Client()}
_, err := p.Start(context.Background(), WorkspaceConfig{WorkspaceID: "ws-1", Runtime: "py"})
if err == nil {
t.Fatal("expected error on malformed 201, got nil")
}
if !strings.Contains(err.Error(), "decode 201 response") {
t.Errorf("error should mention decode 201 response, got %q", err.Error())
}
}
// TestStop_SendsBothAuthHeaders — verify #118/#130 compliance on the
// teardown path. Any call to /cp/workspaces/:id must carry both the
// platform-wide shared secret AND the per-tenant admin token, or the
@@ -97,9 +97,10 @@ type WorkspaceConfig struct {
PluginsPath string // Host path to plugins directory (mounted at /plugins)
WorkspacePath string // Host path to bind-mount as /workspace (if empty, uses Docker named volume)
Tier int
Runtime string // "langgraph" (default) or "claude-code", "codex", "ollama", "custom"
InstanceType string // Optional CP EC2 instance type override (SaaS only)
DiskGB int32 // Optional CP root volume size override in GiB (SaaS only)
Runtime string // "langgraph" (default) or "claude-code", "codex", "ollama", "custom"
InstanceType string // Optional CP EC2 instance type override (SaaS only)
DiskGB int32 // Optional CP root volume size override in GiB (SaaS only)
Display WorkspaceDisplayConfig
EnvVars map[string]string // Additional env vars (API keys, etc.)
PlatformURL string
AwarenessURL string
@@ -122,6 +123,13 @@ type WorkspaceConfig struct {
Image string
}
type WorkspaceDisplayConfig struct {
Mode string `json:"mode,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Protocol string `json:"protocol,omitempty"`
}
// selectImage resolves the final Docker image ref for a workspace. The handler
// layer is the source of truth — if it set cfg.Image (the digest-pinned form
// supplied by CP, the SSOT for runtime image pins; molecule-core's own
@@ -710,7 +718,19 @@ func buildContainerEnv(cfg WorkspaceConfig) []string {
env = append(env, fmt.Sprintf("AWARENESS_NAMESPACE=%s", cfg.AwarenessNamespace))
env = append(env, fmt.Sprintf("AWARENESS_URL=%s", cfg.AwarenessURL))
}
// #1687: track explicit GH_TOKEN / GITHUB_TOKEN so they win over GH_PAT
// alias. These are normally stripped by the SCM-write guard below, but
// when a user explicitly sets them we preserve the value.
var explicitGHToken, explicitGitHubToken string
for k, v := range cfg.EnvVars {
if k == "GH_TOKEN" {
explicitGHToken = v
continue
}
if k == "GITHUB_TOKEN" {
explicitGitHubToken = v
continue
}
// Forensic #145 hardening: tenant workspace containers run
// agent-controlled code and must NEVER receive a Git SCM *write*
// credential. Without merge/approve creds in-container the
@@ -728,6 +748,19 @@ func buildContainerEnv(cfg WorkspaceConfig) []string {
}
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
// #1687: alias GH_PAT → GH_TOKEN / GITHUB_TOKEN on the READ side
// (container env assembly). Explicit values win: only alias when the
// key was not set in workspace secrets.
if explicitGHToken != "" {
env = append(env, fmt.Sprintf("GH_TOKEN=%s", explicitGHToken))
} else if pat, hasPAT := cfg.EnvVars["GH_PAT"]; hasPAT && pat != "" {
env = append(env, fmt.Sprintf("GH_TOKEN=%s", pat))
}
if explicitGitHubToken != "" {
env = append(env, fmt.Sprintf("GITHUB_TOKEN=%s", explicitGitHubToken))
} else if pat, hasPAT := cfg.EnvVars["GH_PAT"]; hasPAT && pat != "" {
env = append(env, fmt.Sprintf("GITHUB_TOKEN=%s", pat))
}
// Inject ADMIN_TOKEN from the platform server's environment so workspace
// containers can call /admin/liveness and other admin-gated endpoints
// (core#831). cp_provisioner.go handles this separately for SaaS tenants.
@@ -770,9 +770,12 @@ func TestBuildContainerEnv_CustomEnvVarsAppended(t *testing.T) {
// place — i.e. the guard is proven by construction, not by environment
// accident.
func TestBuildContainerEnv_StripsSCMWriteTokens(t *testing.T) {
// GH_TOKEN and GITHUB_TOKEN are preserved when explicitly set (#1687)
// because they win over the GH_PAT alias. The unconditional strip list
// therefore excludes them; see TestBuildContainerEnv_GHPATAliasPrecedence
// for the positive assertion.
scmTokens := []string{
"GITEA_TOKEN", "GITHUB_TOKEN", "GH_TOKEN",
"GITLAB_TOKEN", "GL_TOKEN", "BITBUCKET_TOKEN",
"GITEA_TOKEN", "GITLAB_TOKEN", "GL_TOKEN", "BITBUCKET_TOKEN",
}
t.Run("normal path — SCM tokens explicitly set in EnvVars", func(t *testing.T) {
@@ -780,6 +783,9 @@ func TestBuildContainerEnv_StripsSCMWriteTokens(t *testing.T) {
for _, k := range scmTokens {
envVars[k] = "leaked-write-credential-" + k
}
// Explicit GH_TOKEN / GITHUB_TOKEN are now preserved (#1687).
envVars["GH_TOKEN"] = "explicit-gh-token"
envVars["GITHUB_TOKEN"] = "explicit-github-token"
cfg := WorkspaceConfig{
WorkspaceID: "ws-tenant",
PlatformURL: "http://localhost:8080",
@@ -795,6 +801,13 @@ func TestBuildContainerEnv_StripsSCMWriteTokens(t *testing.T) {
if !envContains(buildContainerEnv(cfg), "ANTHROPIC_API_KEY=sk-keep") {
t.Errorf("filter must not strip non-SCM API keys")
}
// Explicit GH tokens must be preserved (not stripped).
if !envContains(buildContainerEnv(cfg), "GH_TOKEN=explicit-gh-token") {
t.Errorf("explicit GH_TOKEN must be preserved")
}
if !envContains(buildContainerEnv(cfg), "GITHUB_TOKEN=explicit-github-token") {
t.Errorf("explicit GITHUB_TOKEN must be preserved")
}
})
t.Run("persona-file path — simulates loadPersonaEnvFile merge", func(t *testing.T) {
@@ -855,6 +868,106 @@ func TestCPProvisionerEnv_StripsSCMWriteTokens(t *testing.T) {
}
}
// TestBuildContainerEnv_GHPATAliasPrecedence asserts that explicit GH_TOKEN /
// GITHUB_TOKEN in workspace secrets win over the GH_PAT alias (#1687 CR2
// review_id=5646). The alias must only inject a key when it was NOT explicitly
// set.
func TestBuildContainerEnv_GHPATAliasPrecedence(t *testing.T) {
pat := "ghp_pat_from_secrets"
explicitGH := "gh_explicit_token"
explicitGitHub := "github_explicit_token"
t.Run("GH_PAT alone → alias both", func(t *testing.T) {
cfg := WorkspaceConfig{
WorkspaceID: "ws-x",
PlatformURL: "http://localhost:8080",
EnvVars: map[string]string{"GH_PAT": pat},
}
env := buildContainerEnv(cfg)
if !envContains(env, "GH_TOKEN="+pat) {
t.Errorf("GH_PAT alias must set GH_TOKEN, got %v", env)
}
if !envContains(env, "GITHUB_TOKEN="+pat) {
t.Errorf("GH_PAT alias must set GITHUB_TOKEN, got %v", env)
}
})
t.Run("explicit GH_TOKEN wins over GH_PAT alias", func(t *testing.T) {
cfg := WorkspaceConfig{
WorkspaceID: "ws-x",
PlatformURL: "http://localhost:8080",
EnvVars: map[string]string{
"GH_PAT": pat,
"GH_TOKEN": explicitGH,
},
}
env := buildContainerEnv(cfg)
if envContains(env, "GH_TOKEN="+pat) {
t.Errorf("explicit GH_TOKEN must win over GH_PAT alias, got GH_TOKEN=%q", pat)
}
if !envContains(env, "GH_TOKEN="+explicitGH) {
t.Errorf("explicit GH_TOKEN must be preserved, got %v", env)
}
})
t.Run("explicit GITHUB_TOKEN wins over GH_PAT alias", func(t *testing.T) {
cfg := WorkspaceConfig{
WorkspaceID: "ws-x",
PlatformURL: "http://localhost:8080",
EnvVars: map[string]string{
"GH_PAT": pat,
"GITHUB_TOKEN": explicitGitHub,
},
}
env := buildContainerEnv(cfg)
if envContains(env, "GITHUB_TOKEN="+pat) {
t.Errorf("explicit GITHUB_TOKEN must win over GH_PAT alias, got GITHUB_TOKEN=%q", pat)
}
if !envContains(env, "GITHUB_TOKEN="+explicitGitHub) {
t.Errorf("explicit GITHUB_TOKEN must be preserved, got %v", env)
}
})
t.Run("explicit both → both preserved, no alias", func(t *testing.T) {
cfg := WorkspaceConfig{
WorkspaceID: "ws-x",
PlatformURL: "http://localhost:8080",
EnvVars: map[string]string{
"GH_PAT": pat,
"GH_TOKEN": explicitGH,
"GITHUB_TOKEN": explicitGitHub,
},
}
env := buildContainerEnv(cfg)
if envContains(env, "GH_TOKEN="+pat) {
t.Errorf("explicit GH_TOKEN must win, got alias value %q", pat)
}
if envContains(env, "GITHUB_TOKEN="+pat) {
t.Errorf("explicit GITHUB_TOKEN must win, got alias value %q", pat)
}
if !envContains(env, "GH_TOKEN="+explicitGH) {
t.Errorf("explicit GH_TOKEN must be preserved, got %v", env)
}
if !envContains(env, "GITHUB_TOKEN="+explicitGitHub) {
t.Errorf("explicit GITHUB_TOKEN must be preserved, got %v", env)
}
})
t.Run("no GH_PAT → no alias injected", func(t *testing.T) {
cfg := WorkspaceConfig{
WorkspaceID: "ws-x",
PlatformURL: "http://localhost:8080",
EnvVars: map[string]string{"OTHER": "ok"},
}
env := buildContainerEnv(cfg)
for _, e := range env {
if strings.HasPrefix(e, "GH_TOKEN=") || strings.HasPrefix(e, "GITHUB_TOKEN=") {
t.Errorf("no GH_PAT present → no alias should be injected, got %q", e)
}
}
})
}
func assertNoSCMWriteToken(t *testing.T, env []string, scmTokens []string) {
t.Helper()
for _, e := range env {
@@ -93,6 +93,9 @@ func sweepOnlineWorkspaces(ctx context.Context, checker ContainerChecker, onOffl
ids = append(ids, id)
}
}
if err := rows.Err(); err != nil {
log.Printf("Health sweep: rows error: %v", err)
}
for _, id := range ids {
running, err := checker.IsRunning(ctx, id)
@@ -159,6 +162,9 @@ func sweepStaleRemoteWorkspaces(ctx context.Context, onOffline OfflineHandler) {
ids = append(ids, id)
}
}
if err := rows.Err(); err != nil {
log.Printf("Health sweep: rows error: %v", err)
}
for _, id := range ids {
// External workspaces flip to 'awaiting_agent' (re-registrable
@@ -166,6 +166,9 @@ func sweepStuckProvisioning(ctx context.Context, emitter ProvisionTimeoutEmitter
ids = append(ids, c)
}
}
if err := rows.Err(); err != nil {
log.Printf("Provision-timeout sweep: rows error: %v", err)
}
for _, c := range ids {
timeout := provisioningTimeoutFor(c.runtime, lookup)
@@ -81,6 +81,7 @@ func TestTestTokenRoute_RequiresAdminAuth_WhenTokensExist(t *testing.T) {
// bootstrap path still works before the first workspace has registered.
func TestTestTokenRoute_FailOpenOnFreshInstall(t *testing.T) {
t.Setenv("MOLECULE_ENV", "development")
t.Setenv("ADMIN_TOKEN", "")
mock := setupRouterTestDB(t)
// HasAnyLiveTokenGlobal: no tokens yet — fresh install.
@@ -832,9 +832,18 @@ func (s *Scheduler) sweepPhantomBusy(ctx context.Context) {
// exhaustion, SDK internal errors) — the error surfaces only in the response
// body under result.kind or result.result_kind.
//
// Returns an empty string when the response is clean (result_kind is "ok" or
// absent). Returns the result_kind value when it is a non-ok signal, so callers
// can propagate it as the schedule's last_status.
// Returns an empty string when the response is clean (result_kind is "ok",
// "message" — the A2A-SDK canonical successful Message envelope — or absent).
// Returns the result_kind value when it is a non-ok signal, so callers can
// propagate it as the schedule's last_status.
//
// Known successful (= treat-as-ok) kinds (resultOKKinds):
// - "ok" — explicit success signal
// - "message" — A2A-SDK Message envelope (`{"result":{"kind":"message","parts":[...]}}`),
// emitted by every successful agent reply. Fix: #1696 originally allow-listed only
// "ok" / empty, which mis-flagged every successful agent response as an SDK error
// (PM scheduler observed 21 consecutive false-failure ticks before auto-disable;
// screenshot 2026-05-23). See [#1696 follow-up].
//
// Known non-ok kinds:
// - "rate_limited" — LLM API rate-limit hit (Max-plan, etc.)
@@ -842,6 +851,59 @@ func (s *Scheduler) sweepPhantomBusy(ctx context.Context) {
// - "sdk_error" — SDK threw an internal error
//
// See #1696.
//
// resultOKKinds is the allowlist of `result.kind` values that are
// UNCONDITIONALLY successful (no further parsing needed). Anything
// outside this set is treated as a non-ok SDK signal, EXCEPT `task`
// which is gated separately on `result.status.state` (see
// classifyTaskState — A2A Task can be either in-progress or terminally
// failed, depending on its status).
//
// Add to this list when new always-success envelope kinds are introduced
// upstream. NEVER add an envelope that can carry a failure sub-state.
var resultOKKinds = map[string]struct{}{
"": {}, // absent / empty → treat as ok (no signal)
"ok": {}, // explicit success
"message": {}, // A2A-SDK Message envelope (always a successful agent reply)
}
// taskOKStates is the A2A Task `status.state` allowlist for results that
// have `kind: "task"`. Tasks can be in-progress (submitted/working) or
// terminally successful (completed) — those are clean signals to the
// scheduler. Terminal failure states (failed/canceled/rejected) are
// surfaced as the scheduler's last_status so operators can see the real
// state. Cf. CR2 review feedback on #1716.
var taskOKStates = map[string]struct{}{
"": {}, // status.state absent → conservative: don't fire false-failure
"submitted": {}, // task accepted, not yet running
"working": {}, // task in progress
"completed": {}, // task finished successfully
}
// classifyTaskState inspects `result.status.state` (or `result.status_state`
// legacy variant) and returns "" when the state is in taskOKStates (success
// or in-progress) or the state string when it is a terminal failure that
// should propagate as last_status.
func classifyTaskState(result map[string]json.RawMessage) string {
rawStatus, ok := result["status"]
if !ok {
return "" // no status block → no signal, leave clean
}
var status map[string]json.RawMessage
if err := json.Unmarshal(rawStatus, &status); err != nil {
return ""
}
if rawState, ok := status["state"]; ok {
var s string
if json.Unmarshal(rawState, &s) == nil {
if _, isOK := taskOKStates[s]; !isOK {
return s
}
}
}
return ""
}
func detectResultKind(body []byte) string {
if len(body) == 0 {
return ""
@@ -854,18 +916,32 @@ func detectResultKind(body []byte) string {
if rawResult, ok := top["result"]; ok {
var result map[string]json.RawMessage
if err := json.Unmarshal(rawResult, &result); err == nil {
// result.kind (canonical JSON-RPC error envelope field).
// result.kind (canonical JSON-RPC envelope field).
if rawKind, ok := result["kind"]; ok {
var k string
if json.Unmarshal(rawKind, &k) == nil && k != "" && k != "ok" {
return k
if json.Unmarshal(rawKind, &k) == nil {
// Special-case task: success or failure depends on status.state.
if k == "task" {
if bad := classifyTaskState(result); bad != "" {
return bad
}
// task with ok / in-progress state → clean
} else if _, isOK := resultOKKinds[k]; !isOK {
return k
}
}
}
// result.result_kind (legacy / alternative field name).
if rawKind, ok := result["result_kind"]; ok {
var k string
if json.Unmarshal(rawKind, &k) == nil && k != "" && k != "ok" {
return k
if json.Unmarshal(rawKind, &k) == nil {
if k == "task" {
if bad := classifyTaskState(result); bad != "" {
return bad
}
} else if _, isOK := resultOKKinds[k]; !isOK {
return k
}
}
}
}
@@ -702,6 +702,61 @@ func TestDetectResultKind(t *testing.T) {
body: `{"result":{"result_kind":"ok","parts":[{"text":"hello"}]}}`,
wantKind: "",
},
{
// REGRESSION GUARD: A2A-SDK canonical Message envelope.
// Pre-fix, every successful agent reply was mis-flagged as an SDK
// error (PM scheduler hit 21 consecutive false-failure ticks before
// auto-disable; canvas screenshot 2026-05-23).
name: "clean ok response — result.kind=message (A2A Message envelope)",
body: `{"jsonrpc":"2.0","result":{"kind":"message","parts":[{"kind":"text","text":"hello"}]},"id":"1"}`,
wantKind: "",
},
{
name: "clean ok response — result.result_kind=message",
body: `{"result":{"result_kind":"message","parts":[{"text":"hello"}]}}`,
wantKind: "",
},
{
// A2A Task envelope, in-progress — `status.state` discriminator is
// `submitted` or `working` → treat as clean (not an SDK error).
name: "clean ok — task envelope state=working",
body: `{"result":{"kind":"task","task_id":"abc","status":{"state":"working"}}}`,
wantKind: "",
},
{
name: "clean ok — task envelope state=submitted",
body: `{"result":{"kind":"task","status":{"state":"submitted"}}}`,
wantKind: "",
},
{
name: "clean ok — task envelope state=completed",
body: `{"result":{"kind":"task","status":{"state":"completed"}}}`,
wantKind: "",
},
{
// Conservative: missing status.state → don't fire false-failure.
name: "clean ok — task envelope no status block",
body: `{"result":{"kind":"task","task_id":"abc"}}`,
wantKind: "",
},
{
// REGRESSION GUARD: terminal failure states MUST propagate as last_status.
// Without taskOKStates gating, the blanket "task" allowlist would have
// masked these — CR2 review feedback on #1716.
name: "SDK error — task envelope state=failed",
body: `{"result":{"kind":"task","status":{"state":"failed"}}}`,
wantKind: "failed",
},
{
name: "SDK error — task envelope state=canceled",
body: `{"result":{"kind":"task","status":{"state":"canceled"}}}`,
wantKind: "canceled",
},
{
name: "SDK error — task envelope state=rejected",
body: `{"result":{"kind":"task","status":{"state":"rejected"}}}`,
wantKind: "rejected",
},
{
name: "SDK error — result.kind=rate_limited",
body: `{"result":{"kind":"rate_limited","parts":[{"text":"error"}]}}`,