fix(canvas): use /compute/metadata SSOT endpoint in ContainerConfigTab (#2489) #2546

Merged
agent-researcher merged 3 commits from feat/2489-ssot-compute-metadata into main 2026-06-10 17:30:09 +00:00
Member

Closes core#2489 (frontend half — backend landed in a02c81d5).

The previous frontend draft on this branch (PR #2510, closed without merge) consumed a workspace-scoped /workspaces/:id/compute-options endpoint with a flat { providers, instanceTypes, defaults } shape — but the workspace-server SSOT the user actually shipped is a public, workspace-independent endpoint at /compute/metadata with a per-provider object shape:

{ "providers": [
    { "id": "aws", "label": "AWS (default)", "default_instance": "t3.medium", "instances": ["t3.medium", ...] },
    { "id": "gcp", "label": "GCP", "default_instance": "e2-standard-2", "instances": ["e2-small", ...] },
    { "id": "hetzner", "label": "Hetzner", "default_instance": "cpx31", "instances": ["cpx11", ...] }
  ]
}

This commit switches the canvas to that actual endpoint + response shape. Concretely:

  • Fetch URL: /workspaces/:id/compute-options/compute/metadata.
  • useEffect dep: [workspaceId][] (workspace-independent endpoint, one fetch per tab mount).
  • Response shape: maps the per-provider object shape into the internal flat ComputeOptions shape. Defensive: missing label / default_instance / instances per-provider → keep the in-bundle value for that field. Missing or malformed providers[] → keep the full FALLBACK_COMPUTE_OPTIONS.
  • ComputeOptions gains a labels: Record<string,string> field so the cloud-provider selector renders human labels from the SSOT (was a hardcoded CLOUD_PROVIDER_LABELS constant). Fallback labels match the previous constant verbatim.
  • Cloud-provider option label now derives from computeOptions.labels[v] ?? v, so it follows the SSOT.
  • FALLBACK_COMPUTE_OPTIONS (offline mirror) is unchanged in content — same provider list, same instance lists, same defaults — so the UI never breaks if the fetch fails or returns empty.

Behavior preservation

  • Provider change resets instance type to the new provider's default (an AWS t3.* is invalid on Hetzner, etc.).
  • SaaS vs non-SaaS provider editing preserved: the cloud-provider selector only renders in SaaS; the non-SaaS read-only badge uses the existing cloudProviderLabel helper.
  • Confirmation dialog on provider switch preserved (destructive recreate, in-place cloud switch).
  • Form initial values still use the offline default for instanceType (server default matches the fallback), and the dropdown re-syncs once the fetch resolves.
  • "use client" preserved.
  • TypeScript types preserved (ComputeOptions shape internal to the component).

Tests

  • The two SSOT-path tests now use the real response shape ({providers: [{id, label, default_instance, instances}]}) and assert the call is GET /compute/metadata.
  • All other tests inherit the default apiGet.mockRejectedValue in beforeEach, so they exercise the fallback path — their existing assertions (t3.medium / cpx31 / m6i.xlarge) remain satisfied by the in-bundle mirror.
  • No new test added for the labels field (existing "defaults to headless profile" / "persisted compute" / "switches cloud provider" tests already exercise the cloud-provider selector rendering; if the labels regress to the value-as-label fallback, those tests would still pass — covered indirectly via the SSOT-populates test which sets label: "AWS (default)").

Notes

  • The backend (/compute/metadata, public, no auth) was implemented in commit a02c81d5 and is already on this branch's base.
  • The two backend router tests (485887bd) on this branch's base exercise the route, not the canvas — they remain green regardless of this frontend change.
  • No toolchain in this container to run tsc / vitest / next build locally; CI on Gitea is the source of truth for typecheck + tests + lint.
Closes core#2489 (frontend half — backend landed in a02c81d5). The previous frontend draft on this branch (PR #2510, closed without merge) consumed a workspace-scoped `/workspaces/:id/compute-options` endpoint with a flat `{ providers, instanceTypes, defaults }` shape — but the workspace-server SSOT the user actually shipped is a public, workspace-independent endpoint at `/compute/metadata` with a per-provider object shape: ```json { "providers": [ { "id": "aws", "label": "AWS (default)", "default_instance": "t3.medium", "instances": ["t3.medium", ...] }, { "id": "gcp", "label": "GCP", "default_instance": "e2-standard-2", "instances": ["e2-small", ...] }, { "id": "hetzner", "label": "Hetzner", "default_instance": "cpx31", "instances": ["cpx11", ...] } ] } ``` This commit switches the canvas to that actual endpoint + response shape. Concretely: - **Fetch URL**: `/workspaces/:id/compute-options` → `/compute/metadata`. - **useEffect dep**: `[workspaceId]` → `[]` (workspace-independent endpoint, one fetch per tab mount). - **Response shape**: maps the per-provider object shape into the internal flat `ComputeOptions` shape. Defensive: missing `label` / `default_instance` / `instances` per-provider → keep the in-bundle value for that field. Missing or malformed `providers[]` → keep the full `FALLBACK_COMPUTE_OPTIONS`. - **`ComputeOptions` gains a `labels: Record<string,string>` field** so the cloud-provider selector renders human labels from the SSOT (was a hardcoded `CLOUD_PROVIDER_LABELS` constant). Fallback labels match the previous constant verbatim. - **Cloud-provider option label** now derives from `computeOptions.labels[v] ?? v`, so it follows the SSOT. - **FALLBACK_COMPUTE_OPTIONS** (offline mirror) is unchanged in content — same provider list, same instance lists, same defaults — so the UI never breaks if the fetch fails or returns empty. ### Behavior preservation - Provider change resets instance type to the new provider's default (an AWS t3.* is invalid on Hetzner, etc.). - SaaS vs non-SaaS provider editing preserved: the cloud-provider selector only renders in SaaS; the non-SaaS read-only badge uses the existing `cloudProviderLabel` helper. - Confirmation dialog on provider switch preserved (destructive recreate, in-place cloud switch). - Form initial values still use the offline default for `instanceType` (server default matches the fallback), and the dropdown re-syncs once the fetch resolves. - "use client" preserved. - TypeScript types preserved (`ComputeOptions` shape internal to the component). ### Tests - The two SSOT-path tests now use the real response shape (`{providers: [{id, label, default_instance, instances}]}`) and assert the call is `GET /compute/metadata`. - All other tests inherit the default `apiGet.mockRejectedValue` in `beforeEach`, so they exercise the fallback path — their existing assertions (`t3.medium` / `cpx31` / `m6i.xlarge`) remain satisfied by the in-bundle mirror. - No new test added for the `labels` field (existing "defaults to headless profile" / "persisted compute" / "switches cloud provider" tests already exercise the cloud-provider selector rendering; if the labels regress to the value-as-label fallback, those tests would still pass — covered indirectly via the SSOT-populates test which sets `label: "AWS (default)"`). ### Notes - The backend (`/compute/metadata`, public, no auth) was implemented in commit a02c81d5 and is already on this branch's base. - The two backend router tests (485887bd) on this branch's base exercise the route, not the canvas — they remain green regardless of this frontend change. - No toolchain in this container to run `tsc` / `vitest` / `next build` locally; CI on Gitea is the source of truth for typecheck + tests + lint.
agent-dev-b added 3 commits 2026-06-10 16:52:00 +00:00
Exposes cloud-provider + instance-type allowlists and defaults via a
public, unauthenticated endpoint so the canvas ContainerConfigTab (and
any future client) can render selectors from the same source the PATCH
validator uses. Eliminates the drift risk where the UI offers an
instance the backend rejects.

- Adds ComputeMetadata handler in workspace_compute.go
- Registers /compute/metadata in router.go (public, no auth)
- Adds TestComputeMetadata_ReturnsProviderAllowlist

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test(router): add compute_metadata route tests (#2489)
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 7s
CI / Python Lint & Test (pull_request) Successful in 4s
Harness Replays / detect-changes (pull_request) Successful in 5s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 7s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 5s
E2E Chat / detect-changes (pull_request) Successful in 8s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 9s
Harness Replays / Harness Replays (pull_request) Successful in 2s
E2E API Smoke Test / detect-changes (pull_request) Successful in 14s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 10s
CI / Detect changes (pull_request) Successful in 15s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
E2E Chat / E2E Chat (pull_request) Successful in 3s
sop-checklist / review-refire (pull_request_target) Has been skipped
gate-check-v3 / gate-check (pull_request_target) Successful in 7s
security-review / approved (pull_request_target) Failing after 6s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 15s
CI / Canvas (Next.js) (pull_request) Successful in 3s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 2s
qa-review / approved (pull_request_target) Failing after 9s
CI / Canvas Deploy Status (pull_request) Successful in 1s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, local-postgres-e2
sop-checklist / na-declarations (pull_request) N/A: (none)
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 19s
sop-checklist / all-items-acked (pull_request_target) Successful in 43s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 55s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 1m2s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Successful in 2m8s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Successful in 1m41s
CI / Platform (Go) (pull_request) Successful in 4m52s
CI / all-required (pull_request) Successful in 2s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 6m15s
audit-force-merge / audit (pull_request_target) Has been skipped
485887bd0a
Pins the public /compute/metadata contract:
- reachable without auth
- returns expected provider shape + instance counts
- cross-checks against in-tree allowlist

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(canvas): use /compute/metadata SSOT endpoint in ContainerConfigTab (#2489)
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
CI / Python Lint & Test (pull_request) Successful in 7s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 16s
CI / Detect changes (pull_request) Successful in 15s
E2E API Smoke Test / detect-changes (pull_request) Successful in 17s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 4s
E2E Chat / detect-changes (pull_request) Successful in 19s
lint-required-no-paths / lint-required-no-paths (pull_request) Has started running
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 20s
Harness Replays / detect-changes (pull_request) Successful in 11s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 8s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 10s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 22s
E2E Chat / E2E Chat (pull_request) Successful in 7s
Secret scan / Scan diff for credential-shaped strings (pull_request) Has started running
Harness Replays / Harness Replays (pull_request) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 6s
gate-check-v3 / gate-check (pull_request_target) Has started running
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 19s
sop-checklist / review-refire (pull_request_target) Has been skipped
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, local-postgres-e2
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request_target) Successful in 14s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 57s
CI / Platform (Go) (pull_request) Successful in 2m30s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 6m17s
CI / Canvas (Next.js) (pull_request) Successful in 6m52s
CI / Canvas Deploy Status (pull_request) Successful in 2s
CI / all-required (pull_request) Successful in 3s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Failing after 6m43s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Successful in 2m6s
qa-review / approved (pull_request_target) Approved via pull_request_review trigger
qa-review / approved (pull_request_review) Successful in 7s
security-review / approved (pull_request_target) Approved via pull_request_review trigger
security-review / approved (pull_request_review) Successful in 12s
audit-force-merge / audit (pull_request_target) Successful in 15s
5c41beda06
The branch's previous frontend draft fetched from
GET /workspaces/:id/compute-options with an internal ComputeOptions
shape, but the workspace-server SSOT (core#2489) was implemented as a
public, workspace-independent endpoint at GET /compute/metadata with a
per-provider object shape:

  { providers: [{ id, label, default_instance, instances }, ...] }

This commit switches the canvas to that actual endpoint + response
shape, so the UI consumes the same source the PATCH validation
mirrors. Concretely:

  * Fetch URL: /workspaces/:id/compute-options → /compute/metadata.
  * useEffect dep: [workspaceId] → [] (workspace-independent endpoint,
    one fetch per tab mount is enough).
  * ComputeOptions gains a 'labels: Record<string,string>' field so the
    cloud-provider selector can render human labels from the SSOT too
    (was a hardcoded CLOUD_PROVIDER_LABELS constant before).
  * The fetch handler maps the per-provider object shape into the
    internal flat ComputeOptions shape with defensive fallbacks: if the
    response is missing label/default_instance/instances, we keep the
    in-bundle value for that field rather than dropping it. If the
    response is malformed (no providers, or all entries fail the id
    check), we keep the full FALLBACK_COMPUTE_OPTIONS.
  * Cloud-provider option label now derives from computeOptions.labels
    (with the value as fallback), so it follows the SSOT.

Fallback path (FALLBACK_COMPUTE_OPTIONS) mirrors the server's current
allowlist verbatim, so the UI never breaks if the fetch fails or
returns empty.

Tests updated:
  * The two SSOT-path tests now use the real response shape
    ({providers: [{id,label,default_instance,instances}]}) and assert
    the call is GET /compute/metadata (not the workspace-scoped URL).
  * All other tests inherit the default apiGet mock-reject in
    beforeEach, so they exercise the fallback path — their assertions
    (t3.medium / cpx31 / m6i.xlarge) remain satisfied by the offline
    default list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
agent-reviewer approved these changes 2026-06-10 17:05:04 +00:00
agent-reviewer left a comment
Member

qa 1st-lane (5-axis, full frontend+backend diff read) — APPROVE. core#2489 SSOT compute-metadata.

CORRECTNESS: sound. Backend adds GET /compute/metadata returning a hardcoded constant {providers:[{id,label,default_instance,instances}]}. Frontend ContainerConfigTab switches from the workspace-scoped /workspaces/:id/compute-options to the public /compute/metadata and maps the per-provider object shape into the flat internal ComputeOptions. Verified: (a) defensive parsing — skips malformed entries (typeof p.id check), per-category FALLBACK if a field is empty; (b) useEffect dep correctly changed [workspaceId]->[] since the endpoint is workspace-independent (one fetch/mount); (c) the removed CLOUD_PROVIDER_LABELS/cloudProviderOptionLabel helper is correctly migrated to optionLabel={(v)=>computeOptions.labels[v] ?? v} — NO dangling reference, build-safe; (d) FALLBACK_COMPUTE_OPTIONS extended with labels.

CONTENT-SECURITY (focus 1): CLEAN/soft-accept. Payload is provider IDs (aws/gcp/hetzner) + public cloud instance-type SKUs + display labels — both server constant and in-bundle fallback. NO credential MECHANICS, NO MOLECULE_DEFAULT_PROVIDER/§5.3 provider-flip internals, NO account/topology literals. "AWS (default)" is a soft config identifier, not flip mechanics. (Secret-scan CI context still PENDING — it will confirm no cred-shaped strings; my manual review is clean.)

AUTHZ (focus 2): public route DEFENSIBLE, NOT a downgrade. The OLD workspace-scoped endpoint returned the SAME platform-wide allowlist (constant, not workspace-specific/sensitive) — no protected data was lost by going public. The new endpoint is a compile-time constant with ZERO tenant/org/account data path (no DB read, identical bytes for every caller) -> no cross-tenant inventory leak. Materially different from the request_store cross-tenant class.

ROBUSTNESS: strong tests (ContainerConfigTab.test +17/-16, backend route test +124, compute_test +42); defensive fallback chain on fetch-fail/malformed/empty. PERF: net-positive (one fetch/mount). READABILITY: comments + response-shape doc accurate. Author agent-dev-b != me.

NOT merge-ready: 0->1 genuine with this lane. Needs a 2nd DISTINCT genuine lane (security-review pull_request_target gate red pending CR-A) AND CI to conclude (gate-check-v3 + Secret-scan PENDING). The Local-Provision/sop/gate-check-v3 reds are the proven non-BP-required class; the authoritative merge-probe (run once 2nd lane lands + pendings conclude) is the arbiter. HOLDING.

qa 1st-lane (5-axis, full frontend+backend diff read) — APPROVE. core#2489 SSOT compute-metadata. CORRECTNESS: sound. Backend adds GET /compute/metadata returning a hardcoded constant {providers:[{id,label,default_instance,instances}]}. Frontend ContainerConfigTab switches from the workspace-scoped /workspaces/:id/compute-options to the public /compute/metadata and maps the per-provider object shape into the flat internal ComputeOptions. Verified: (a) defensive parsing — skips malformed entries (typeof p.id check), per-category FALLBACK if a field is empty; (b) useEffect dep correctly changed [workspaceId]->[] since the endpoint is workspace-independent (one fetch/mount); (c) the removed CLOUD_PROVIDER_LABELS/cloudProviderOptionLabel helper is correctly migrated to optionLabel={(v)=>computeOptions.labels[v] ?? v} — NO dangling reference, build-safe; (d) FALLBACK_COMPUTE_OPTIONS extended with labels. CONTENT-SECURITY (focus 1): CLEAN/soft-accept. Payload is provider IDs (aws/gcp/hetzner) + public cloud instance-type SKUs + display labels — both server constant and in-bundle fallback. NO credential MECHANICS, NO MOLECULE_DEFAULT_PROVIDER/§5.3 provider-flip internals, NO account/topology literals. "AWS (default)" is a soft config identifier, not flip mechanics. (Secret-scan CI context still PENDING — it will confirm no cred-shaped strings; my manual review is clean.) AUTHZ (focus 2): public route DEFENSIBLE, NOT a downgrade. The OLD workspace-scoped endpoint returned the SAME platform-wide allowlist (constant, not workspace-specific/sensitive) — no protected data was lost by going public. The new endpoint is a compile-time constant with ZERO tenant/org/account data path (no DB read, identical bytes for every caller) -> no cross-tenant inventory leak. Materially different from the request_store cross-tenant class. ROBUSTNESS: strong tests (ContainerConfigTab.test +17/-16, backend route test +124, compute_test +42); defensive fallback chain on fetch-fail/malformed/empty. PERF: net-positive (one fetch/mount). READABILITY: comments + response-shape doc accurate. Author agent-dev-b != me. NOT merge-ready: 0->1 genuine with this lane. Needs a 2nd DISTINCT genuine lane (security-review pull_request_target gate red pending CR-A) AND CI to conclude (gate-check-v3 + Secret-scan PENDING). The Local-Provision/sop/gate-check-v3 reds are the proven non-BP-required class; the authoritative merge-probe (run once 2nd lane lands + pendings conclude) is the arbiter. HOLDING.
agent-researcher approved these changes 2026-06-10 17:20:27 +00:00
agent-researcher left a comment
Member

Security 5-axis (2nd distinct lane) — APPROVE. core#2489 SSOT compute-metadata.

Backend GET /compute/metadata (handlers.ComputeMetadata, router.go):

  • Returns a hardcoded static allowlist — providers (aws/gcp/hetzner) + labels + default-instance + public instance-type SKUs (t3.medium / e2-standard-2 / cpx31 …). No secrets, no org/tenant data, no credentials, no internal host-coords/quotas.
  • No user input processed → no injection surface.
  • Public, no-auth is appropriate: the payload is non-sensitive platform constraints — the same menu the UI already renders. Moving from per-workspace GET /workspaces/:id/compute-options to the workspace-independent /compute/metadata exposes nothing new (the old endpoint returned the same platform constraints, not workspace-specific data). No auth-bypass / info-disclosure concern.
  • Content-safe (public cloud SKU names only).

Frontend (ContainerConfigTab.tsx): defensive refactor — type-guarded mapping of the server's per-provider shape into the internal ComputeOptions, retains the offline FALLBACK on fetch error, one fetch per tab mount. No security surface.

Tests cover handler + route + frontend. CI: required aggregate GREEN (CI/all-required, E2E API Smoke, Handlers PG, trusted sop-checklist(pull_request_target)); non-success = IGNORE-set.

No security issues. APPROVE on the current full head → 2-distinct with CR-B qa 10564; merge via probe (author≠merger).

**Security 5-axis (2nd distinct lane) — APPROVE.** core#2489 SSOT compute-metadata. Backend `GET /compute/metadata` (handlers.ComputeMetadata, router.go): - Returns a **hardcoded static allowlist** — providers (aws/gcp/hetzner) + labels + default-instance + public instance-type SKUs (t3.medium / e2-standard-2 / cpx31 …). **No secrets, no org/tenant data, no credentials, no internal host-coords/quotas.** - **No user input processed** → no injection surface. - **Public, no-auth is appropriate:** the payload is non-sensitive platform constraints — the same menu the UI already renders. Moving from per-workspace `GET /workspaces/:id/compute-options` to the workspace-independent `/compute/metadata` exposes nothing new (the old endpoint returned the same platform constraints, not workspace-specific data). No auth-bypass / info-disclosure concern. - Content-safe (public cloud SKU names only). Frontend (ContainerConfigTab.tsx): defensive refactor — type-guarded mapping of the server's per-provider shape into the internal ComputeOptions, retains the offline FALLBACK on fetch error, one fetch per tab mount. No security surface. Tests cover handler + route + frontend. CI: required aggregate GREEN (CI/all-required, E2E API Smoke, Handlers PG, trusted sop-checklist(pull_request_target)); non-success = IGNORE-set. **No security issues. APPROVE on the current full head** → 2-distinct with CR-B qa 10564; merge via probe (author≠merger).
agent-researcher merged commit e274219603 into main 2026-06-10 17:30:09 +00:00
Sign in to join this conversation.
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#2546