molecule-core/workspace-server/internal/handlers
Hongming Wang 5adc8a74d5 feat(canvas+org): env preflight, EmptyState parity, shared useTemplateDeploy hook
Builds on #2061. Three internally-cohesive sub-features; easiest to
read in order.

## 1. Org-level env preflight

Server
- `OrgTemplate` + `OrgWorkspace` gain `required_env: string[]` and
  `recommended_env: string[]` YAML fields.
- `GET /org/templates` walks the tree and returns the tree-union
  (deduped, sorted) of both. `collectOrgEnv` dedup prefers required
  when the same key is declared at both tiers.
- `POST /org/import` preflights against `global_secrets` WHERE
  `octet_length(encrypted_value) > 0` (empty-value rows used to be
  counted as "configured" and the per-container preflight still
  failed at start time). 412 Precondition Failed + `missing_env`
  list when required keys are absent. `force=true` bypasses with
  an audit log line. DB lookup failure now returns 500 (was:
  silent fall-through that defeated the guard). Env-var NAMES
  validated against `^[A-Z][A-Z0-9_]{0,127}$` so a malicious
  template can't ship pathological names into the UI or DB.

Canvas
- New `OrgImportPreflightModal`: red "Required" section (blocking)
  and yellow "Recommended" section (non-blocking, import stays
  enabled, shows live missing-count next to the Import button).
- Per-key password input → `PUT /settings/secrets` → strike-through
  on save. Functional `setDrafts` throughout (no stale-closure
  clobbers on rapid successive saves). `useEffect` seed keyed on a
  sorted-join string signature so a parent re-render with a new
  array identity doesn't clobber typed inputs.
- `TemplatePalette.handleImport` branches: zero env declarations →
  straight to import; any declarations → fetch configured global
  secret keys, open the modal.

Tests (Go): `TestCollectOrgEnv_*` (5) cover union-across-levels,
required-wins-over-recommended (including same-struct), dedup,
empty, invalid-name rejection.

## 2. EmptyState parity with TemplatePalette

The "Deploy your first agent" grid used to call `POST /workspaces`
with no preflight while the sidebar palette ran
`checkDeploySecrets` + `MissingKeysModal` first. Same template
deployed two different ways → first-run users saw containers boot
in `failed` state without guidance. Now both surfaces share one
preflight + modal handshake.

EmptyState's previous `interface Template` dropped `runtime`,
`models`, and `required_env` — silently discarding exactly the
fields the preflight needs. `Template` now lives in
`deploy-preflight.ts` and is imported from there by both surfaces.

## 3. useTemplateDeploy hook

With the preflight + modal wiring now duplicated across
EmptyState + TemplatePalette + (going forward) any third surface,
extracted the pattern into `canvas/src/hooks/useTemplateDeploy.tsx`:

  const { deploy, deploying, error, modal } = useTemplateDeploy({
    canvasCoords: ...,   // optional, default random
    onDeployed: (id) => ...,
  });

Closes three drift surfaces that the duplication had created:
- `resolveRuntime` id→runtime fallback table (moved to
  `deploy-preflight.ts`). EmptyState had a narrower fallback that
  would have silently disagreed with the palette on any future id
  needing a non-identity mapping.
- `checkDeploySecrets` call signature. One owner.
- `MissingKeysModal` JSX wiring. One owner.

Narrow try/catch around `checkDeploySecrets` so a preflight network
failure clears `deploying` and surfaces via `setError` instead of
stranding the button forever. `modal: ReactNode` (not a
`renderModal()` function) — the previous memoization bought
nothing since consumers called it inline every render. Named
`MissingKeysInfo` interface for the state shape.

## 4. Viewport auto-fit user-pan gate fix

During org deploy the canvas was meant to pan+zoom to follow each
arriving workspace (`molecule:fit-deploying-org` event → debounced
fitView). In practice the fit stayed stuck on wherever the first
fit landed.

Root cause: React Flow v12 fires `onMoveEnd` with a truthy `event`
at the END of a programmatic `fitView` animation. The original
"respect-user-pan" gate stamped `userPannedAtRef` in `onMoveEnd`,
so our own fit completing looked like a user pan, and every
subsequent auto-fit short-circuited for the rest of the deploy.

Fix: stop trusting `onMoveEnd` for user-intent detection. Register
explicit `wheel` + `pointerdown` listeners on `document` with
capture phase and `target.closest('.react-flow__pane')` filter.
Capture-phase immunity to `stopPropagation`; pane-filter rejects
toolbar / modal / side-panel clicks (the old `window` fallback
caught those). `onMoveEnd` simplified to only drive the debounced
viewport save.

Also: fit event dispatched on root arrivals (not just children),
so the canvas centers on the just-landed root immediately instead
of waiting ~2s for the first child. Animation 600ms → 400ms so
successive per-arrival fits don't pile up visually. End-state fit
stays at 1200ms — intentional asymmetry ("settling" vs
"tracking"), documented in code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:15:33 -07:00
..
a2a_proxy_helpers.go fix(a2a-queue): nil-safe drain + 202-requeue handling (followup to #1893) (#1896) 2026-04-23 22:55:43 +00:00
a2a_proxy_test.go fix(tests): path validation before docker check + a2a queue mock in tests 2026-04-24 11:07:43 +00:00
a2a_proxy.go test(handlers): fix A2A queue drain tests — all pass locally 2026-04-24 13:47:27 +00:00
a2a_queue_test.go test(handlers): fix A2A queue drain tests — all pass locally 2026-04-24 13:47:27 +00:00
a2a_queue.go fix(admin/a2a_queue): add drop-stale endpoint for post-incident queue cleanup 2026-04-24 02:08:35 +00:00
activity_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
activity.go feat: add tool_trace to activity_logs for platform-level agent observability 2026-04-22 15:17:14 -07:00
admin_memories_test.go fix(handlers): unblock Platform (Go) CI — sqlmock budget-check + test loopback 2026-04-22 19:40:06 -07:00
admin_memories.go fix(org-api-tokens): add org_id column, close requireCallerOwnsOrg regression 2026-04-21 01:34:05 +00:00
admin_queue_test.go fix(handlers/admin_queue_test): wire sqlmock to make DropStale tests pass 2026-04-24 04:40:19 +00:00
admin_queue.go fix(handlers/admin_queue): remove unused db import 2026-04-24 02:22:16 +00:00
admin_schedules_health_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
admin_schedules_health.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
admin_test_token_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
admin_test_token.go fix(security): close IDOR gaps on /admin/test-token and /orgs/:id/allowlist 2026-04-20 23:29:27 +00:00
agent_git_identity_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
agent_git_identity.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
agent_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
agent.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
approvals_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
approvals.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
artifacts_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
artifacts.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
audit_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
audit.go fix: guard HMAC slice truncation in audit chain verification (fixes #1332) (#1339) 2026-04-21 07:52:11 +00:00
budget_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
budget.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
bundle.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
channels_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
channels.go chore: sync staging to main — 1188 commits, 5 conflicts resolved (#1743) 2026-04-23 18:30:18 +00:00
chat_files_test.go feat(canvas+platform): chat attachments, model selection, deploy/delete UX 2026-04-24 13:27:51 -07:00
chat_files.go feat(canvas+platform): chat attachments, model selection, deploy/delete UX 2026-04-24 13:27:51 -07:00
checkpoints_integration_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
checkpoints_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
checkpoints.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
config_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
config.go fix(security): cap webhook + config PATCH bodies (H3/H4) 2026-04-19 01:23:03 -07:00
container_files_delete_test.go chore: sync staging to main — 1188 commits, 5 conflicts resolved (#1743) 2026-04-23 18:30:18 +00:00
container_files_test.go fix(handlers): add empty/dot-only path guard to validateRelPath 2026-04-24 07:17:26 +00:00
container_files.go fix(tests): path validation before docker check + a2a queue mock in tests 2026-04-24 11:07:43 +00:00
delegation_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
delegation.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
discovery_test.go fix(canvas,server): address review findings on 3f11df03 2026-04-23 20:29:44 -07:00
discovery.go fix(canvas/a11y): aria-hidden SVGs, MissingKeysModal dialog semantics, session cookie auth (#1992) 2026-04-24 06:20:32 +00:00
events_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
events.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
github_token_test.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
github_token.go fix(go): replace $1 literal with resp.Body.Close() in 7 files (#1247) 2026-04-21 03:18:21 +00:00
handlers_additional_test.go fix(canvas): subtree-aware layout + org-import reliability + UX polish 2026-04-23 23:48:29 -07:00
handlers_extended_test.go Fix TestExtended_WorkspaceDelete missing sqlmock expectations 2026-04-20 01:13:52 -07:00
handlers_test.go Merge pull request #1996 from Molecule-AI/core-fe-ki005-regression-tests 2026-04-24 11:58:31 +00:00
hermes_messages_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
hermes_messages.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
hibernation_test.go feat(platform): 409 guard on /hibernate when active_tasks > 0 (closes #822) 2026-04-18 12:09:52 -07:00
instructions.go fix(review): address code review blockers on tool-trace + instructions 2026-04-22 16:18:06 -07:00
mcp_test.go fix(security): backport SSRF defence (CWE-918) to main — isSafeURL in a2a_proxy.go (#1292) (#1302) 2026-04-21 07:06:42 +00:00
mcp_tools.go fix(restart): support SaaS control-plane provisioner (unblocks Platform Go build too) (#1512) 2026-04-21 22:56:01 +00:00
mcp.go fix: CWE-78 rm scope, go vet failures, delegation idempotency 2026-04-21 18:22:30 +00:00
memories_test.go test: GLOBAL memory delimiter spoofing escape + LOCAL scope untouched 2026-04-18 11:54:52 -07:00
memories.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
memory_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
memory.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
org_helpers.go fix: CWE-78 rm scope, go vet failures, delegation idempotency 2026-04-21 18:22:30 +00:00
org_import.go feat(canvas+org): env preflight, EmptyState parity, shared useTemplateDeploy hook 2026-04-24 15:15:33 -07:00
org_include_test.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
org_include.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
org_path_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
org_plugin_allowlist_test.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
org_plugin_allowlist.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
org_prompt_ref_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
org_test.go feat(canvas+org): env preflight, EmptyState parity, shared useTemplateDeploy hook 2026-04-24 15:15:33 -07:00
org_tokens_test.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
org_tokens.go fix(platform): unblock SaaS workspace registration end-to-end 2026-04-21 03:06:46 -07:00
org.go feat(canvas+org): env preflight, EmptyState parity, shared useTemplateDeploy hook 2026-04-24 15:15:33 -07:00
plugins_install_pipeline_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
plugins_install_pipeline.go fix(security): close F1086 err.Error() leaks in plugin install pipeline + provision (#1206) 2026-04-21 03:54:50 +00:00
plugins_install.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
plugins_listing.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
plugins_sources.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
plugins_test.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
plugins.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
registry_test.go fix(registry): block RFC 5737 TEST-NET and RFC 3849 documentation IPs 2026-04-24 18:27:07 +00:00
registry.go fix(registry): block RFC 5737 TEST-NET and RFC 3849 documentation IPs 2026-04-24 18:27:07 +00:00
restart_context_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
restart_context.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
restart_template_test.go fix(handlers): apply sanitizeRuntime allowlist before Tier 4 filepath.Join (CWE-22) 2026-04-24 11:37:19 +00:00
restart_template.go fix(handlers): apply sanitizeRuntime allowlist before Tier 4 filepath.Join (CWE-22) 2026-04-24 11:37:19 +00:00
schedules_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
schedules.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
secrets_test.go feat(canvas+platform): chat attachments, model selection, deploy/delete UX 2026-04-24 13:27:51 -07:00
secrets.go feat(canvas+platform): chat attachments, model selection, deploy/delete UX 2026-04-24 13:27:51 -07:00
security_regression_685_686_687_688_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
socket.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
sse_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
sse.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
ssrf_test.go test(handlers): add SaaS-mode wrapper tests for isSafeURL and validateAgentURL 2026-04-24 15:05:03 +00:00
ssrf.go Merge remote-tracking branch 'origin/staging' into fix/restore-quickstart-plus-hotfixes 2026-04-23 16:42:41 -07:00
team_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
team.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
template_files_eic_test.go feat(files-api): SSH-backed write for SaaS workspaces (fixes 500 docker not available) 2026-04-22 18:27:12 -07:00
template_files_eic.go feat(files-api): SSH-backed write for SaaS workspaces (fixes 500 docker not available) 2026-04-22 18:27:12 -07:00
template_import_test.go feat(quickstart): default new agents to T3 (Privileged) 2026-04-23 15:34:22 -07:00
template_import.go feat(quickstart): default new agents to T3 (Privileged) 2026-04-23 15:34:22 -07:00
templates_test.go fix(test): TestDeleteFile_WorkspaceNotFound uses relative path "old-file.txt" 2026-04-24 12:45:29 +00:00
templates.go fix(handlers): CWE-78 — reject absolute paths before strip in DeleteFile; drop null_byte test 2026-04-24 12:38:28 +00:00
terminal_test.go fix(terminal): check org_token_id context to allow org-token A2A routing (KI-005 followup) 2026-04-24 16:17:50 +00:00
terminal.go fix(terminal): check org_token_id context to allow org-token A2A routing (KI-005 followup) 2026-04-24 16:17:50 +00:00
tokens_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
tokens.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
traces_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
traces.go fix(go): replace $1 literal with resp.Body.Close() in 7 files (#1247) 2026-04-21 03:18:21 +00:00
transcript_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
transcript.go fix(go): replace $1 literal with resp.Body.Close() in 7 files (#1247) 2026-04-21 03:18:21 +00:00
viewport_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
viewport.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
webhooks_test.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
webhooks_workflow_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
webhooks.go fix: multiple platform handler bug fixes 2026-04-20 05:01:01 +00:00
workspace_bootstrap_test.go feat(platform): bootstrap-failed + console endpoints for CP watcher 2026-04-20 17:11:34 -07:00
workspace_bootstrap.go fix(security): sanitize error details in BootstrapFailed, provision, and plugin install (#1219) 2026-04-21 02:11:38 +00:00
workspace_budget_test.go feat(quickstart): default new agents to T3 (Privileged) 2026-04-23 15:34:22 -07:00
workspace_crud.go fix(canvas): subtree-aware layout + org-import reliability + UX polish 2026-04-23 23:48:29 -07:00
workspace_metrics_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
workspace_metrics.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
workspace_preflight_test.go chore: code-review cleanup on today's shipped PRs 2026-04-20 16:04:57 -07:00
workspace_preflight.go chore: code-review cleanup on today's shipped PRs 2026-04-20 16:04:57 -07:00
workspace_provision_test.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
workspace_provision.go merge(staging): resolve conflicts + fix 7 test regressions on top of #2061 2026-04-24 13:50:39 -07:00
workspace_restart_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
workspace_restart.go test(handlers): add 5 TestKI005 terminal guard regression tests (#1938) 2026-04-24 01:58:31 +00:00
workspace_test.go feat(quickstart): default new agents to T3 (Privileged) 2026-04-23 15:34:22 -07:00
workspace.go fix(canvas): propagate runtime through WORKSPACE_PROVISIONING event 2026-04-23 17:17:49 -07:00