Commit Graph

548 Commits

Author SHA1 Message Date
Hongming Wang
4cb74b91ed Merge pull request #509 from Molecule-AI/docs/devrel-feat-379
docs(devrel): gemini-cli runtime tutorial (feat #379)
2026-04-16 13:46:13 -07:00
molecule-ai[bot]
1e3cf704ec docs: add Gemini CLI landing page brief for /runtimes/gemini-cli (issue #514) 2026-04-16 20:34:32 +00:00
molecule-ai[bot]
26916cc86d docs: add Gemini CLI keyword research (issue #514) 2026-04-16 20:33:32 +00:00
Hongming Wang
f3c229db83 Merge pull request #508 from Molecule-AI/fix/507-crlf-hook-breakage
fix: enforce LF for .py hook files — fix #507 (all agents "no response generated")
2026-04-16 13:30:48 -07:00
molecule-ai[bot]
0320b71315 docs(devrel): gemini-cli runtime tutorial for PR #379 2026-04-16 20:22:26 +00:00
rabbitblood
995f51f950 fix: enforce LF for .py hook files to fix #507
CRLF line endings in .claude hook files caused claude-code SessionStart
hooks to fail silently on Windows checkouts — python3 received a filename
ending in '\r' (e.g. 'session-start-context.py\r'), failed with ENOENT,
and the claude-code query short-circuited with result='' across every
A2A call. Observed symptom: all 22 agents returned '(no response
generated)' on every pulse despite the model never being called
(input_tokens=0, output_tokens=0).

Existing *.sh rule covered the shebang line; adding *.py covers the
Python hook target that the shell script invokes. Shipped alongside
the same fix in molecule-ai-plugin-molecule-session-context (which
is the primary source of these hooks via the platform plugin loader).

Fixes #507

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:18:17 -07:00
Hongming Wang
e76aee6022 Merge pull request #506 from Molecule-AI/feat/github-app-auth-plugin
feat(platform): wire github-app-auth plugin for per-installation tokens
2026-04-16 12:59:11 -07:00
rabbitblood
2492f8c806 feat(platform): wire github-app-auth plugin for per-installation tokens
Integrates github.com/Molecule-AI/molecule-ai-plugin-github-app-auth.
When GITHUB_APP_ID is set, the platform constructs a plugin
Authenticator at boot and registers it as an EnvMutator on the
WorkspaceHandler. Every workspace provision then gets a fresh
GITHUB_TOKEN / GH_TOKEN injected from the App's installation token
(rotates ~hourly, refresh 5 min before expiry).

Verified live this turn:
- Platform boot log: `github-app-auth: registered, 1 mutator(s) in chain`
- `docker exec ws-<id> gh auth status` → `Logged in as molecule-ai[bot] (GH_TOKEN)`
- `gh issue list --repo Molecule-AI/molecule-core` returns real data
  (Hermes #498/#499/#500 visible from inside a workspace container)

## Changes
- platform/go.mod + go.sum: new dep on the plugin
- platform/cmd/server/main.go: import + conditional registration
  (soft-skip when GITHUB_APP_ID is unset for self-hosted/dev)
- docker-compose.yml: pass GITHUB_APP_* env + bind-mount private key

## Drive-by
.gitignore: exclude /org-templates /plugins /workspace-configs-templates
— these dirs are populated locally by clone-manifest.sh from the
standalone repos, should never be committed to core. Without this rule
my previous git add -A staged 33 embedded git dirs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:52:20 -07:00
Hongming Wang
7bfb91d46a Merge pull request #504 from Molecule-AI/fix/code-review-final-batch
fix: code review — dead code, DRY, rate limit, docs
2026-04-16 12:09:53 -07:00
Hongming Wang
8f4d0997c8 fix: code review findings — dead code, DRY, rate limit, docs
1. Delete fly_provisioner.go — superseded by control plane architecture.
   Direct Fly provisioning from tenant was intentionally removed.

2. Extract loadWorkspaceSecrets() — shared by Docker + CP provisioner
   paths. Eliminates 30-line secret-loading duplication.

3. Token rate limit — max 50 active tokens per workspace. Returns 429
   if exceeded. Prevents unbounded token creation by compromised client.

4. CLAUDE.md — add GET/POST/DELETE /workspaces/:id/tokens to route table.

5. .env.example — document MOLECULE_ORG_ID and CP_PROVISION_URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:04:37 -07:00
Hongming Wang
77d42268d4 Merge pull request #503 from Molecule-AI/feat/controlplane-provisioner
feat(platform): control plane provisioner (CONTAINER_BACKEND=controlplane)
2026-04-16 11:54:07 -07:00
Hongming Wang
a152342e8c feat(platform): auto-detect SaaS tenant → control plane provisioner
No env vars to configure. The platform auto-detects the backend:

  MOLECULE_ORG_ID set → SaaS tenant → control plane provisioner
  MOLECULE_ORG_ID empty → self-hosted → Docker provisioner

The control plane URL defaults to https://api.moleculesai.app (override
with CP_PROVISION_URL for testing). No FLY_API_TOKEN on the tenant.

Removed: direct Fly provisioner (FlyProvisioner) — all SaaS workspace
provisioning goes through the control plane which holds the Fly token
and manages billing, quotas, and cleanup.

Two backends: CPProvisioner (SaaS) and Docker Provisioner (self-hosted).

Closes #494

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:50:52 -07:00
Hongming Wang
2cecd4ee3d Merge pull request #502 from Molecule-AI/fix/update-delete-same-origin
fix(auth): nesting + delete from tenant canvas
2026-04-16 11:26:27 -07:00
Hongming Wang
3db589770e fix(auth): allow nesting + delete from tenant canvas (same-origin)
PATCH /workspaces/:id field-level auth for parent_id/tier/runtime
required a bearer token, blocking canvas nesting (drag-to-nest).
Added IsSameOriginCanvas check so the tenant canvas can update
sensitive fields without a bearer.

Exported IsSameOriginCanvas from middleware package so workspace.go
can call it for the field-level auth path.

DELETE /workspaces/:id is behind AdminAuth which already has the
same-origin check — if delete still fails, it's a different issue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:22:45 -07:00
Hongming Wang
bb25d54daa Merge pull request #501 from Molecule-AI/feat/fly-provisioner
feat(platform): Fly Machines provisioner (CONTAINER_BACKEND=flyio)
2026-04-16 11:05:52 -07:00
Hongming Wang
c2c80fd269 feat(platform): Fly Machines provisioner for SaaS workspace deployment
When CONTAINER_BACKEND=flyio, workspaces are provisioned as Fly Machines
instead of local Docker containers. This enables workspace deployment
on SaaS tenants where no Docker daemon is available.

New files:
- provisioner/fly_provisioner.go: FlyProvisioner with Start/Stop/
  IsRunning/Restart/Close via Fly Machines API (api.machines.dev/v1)
- FlyRuntimeImages maps runtimes to GHCR image tags

Changes:
- main.go: select Docker vs Fly based on CONTAINER_BACKEND env var
- workspace.go: SetFlyProvisioner() setter, Create checks flyProv first
- workspace_provision.go: provisionWorkspaceFly() loads secrets, calls
  FlyProvisioner.Start, issues auth token for the new machine

Env vars for Fly backend:
- CONTAINER_BACKEND=flyio (activates Fly provisioner)
- FLY_API_TOKEN (Fly deploy token)
- FLY_WORKSPACE_APP (Fly app name for workspace machines)
- FLY_REGION (default: ord)

Closes #494

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:51:15 -07:00
Hongming Wang
bb44b1f03b Merge pull request #491 from Molecule-AI/fix/code-review-findings-batch
fix: token UI, auth hardening, WS dedup, pagination
2026-04-16 10:46:28 -07:00
Hongming Wang
54bb543ff7 fix: code review findings — token UI, auth hardening, WS dedup
1. Settings panel: wire TokensTab into "API Tokens" tab (was imported
   but not rendered). Rename "API Keys" → "Secrets", add "API Tokens"
   tab. Fix docs link → doc.moleculesai.app/docs/tokens.

2. Referer match hardening: require exact host match or trailing slash
   to prevent evil.com subdomain bypass. Cache CANVAS_PROXY_URL at
   init time instead of per-request os.Getenv.

3. Extract shared deriveWsBaseUrl() to lib/ws-url.ts — eliminates
   duplicate 12-line derivation in socket.ts and TerminalTab.tsx.

4. Token list pagination: add ?limit= and ?offset= params (default
   50, max 200) to GET /workspaces/:id/tokens.

507/507 canvas tests pass, Go build + vet clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:42:26 -07:00
Hongming Wang
5d4ee18c72 Merge pull request #490 from Molecule-AI/fix/workspace-auth-same-origin
fix(auth): WorkspaceAuth same-origin canvas on tenant
2026-04-16 10:17:12 -07:00
Hongming Wang
807b4c1b45 fix(auth): allow same-origin canvas requests through WorkspaceAuth on tenant
WorkspaceAuth only accepted bearer tokens, blocking the canvas from
calling per-workspace routes (restart, config, secrets, chat) on the
tenant image where canvas + API share the same origin.

Added isSameOriginCanvas() fallback (same check used by AdminAuth):
checks Referer matches request Host, gated behind CANVAS_PROXY_URL
so only tenant deployments are affected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:06:33 -07:00
Hongming Wang
6ea559079e Merge pull request #489 from Molecule-AI/fix/tenant-dockerfile-in-publish
fix(ci): use Dockerfile.tenant for Fly registry (Go + Canvas)
2026-04-16 09:34:44 -07:00
Hongming Wang
c5ef9a71fc fix(ci): use Dockerfile.tenant for Fly registry image (Go + Canvas)
The publish workflow was pushing platform/Dockerfile (Go-only) to the
Fly registry, but tenant machines run the combined image (Go + Canvas
reverse proxy). This caused "canvas unavailable" after machine update.

Changes:
- Fly registry build: platform/Dockerfile → platform/Dockerfile.tenant
- GHCR: keeps Go-only image (for self-hosted/dev use)
- Path triggers: add canvas/** and manifest.json (tenant image includes both)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:31:51 -07:00
Hongming Wang
f9cb6396f8 Merge pull request #487 from Molecule-AI/fix/ci-publish-skip-docker-login-v2
fix(ci): bypass docker login + macOS Keychain (real fix)
2026-04-16 09:30:45 -07:00
Hongming Wang
0c9fda559a fix(ci): bypass docker login + macOS Keychain for image publish
Six prior PRs (#273, #319, #322, #341, #484, #486) all kept calling
`docker login` and tried to coerce credsStore via increasingly elaborate
config tricks. None worked. The latest publish-canvas-image and
publish-platform-image runs on main are still failing with:

    error storing credentials - err: exit status 1,
    out: `User interaction is not allowed. (-25308)`

Verified locally on the runner host (2026-04-16): `docker login` on
macOS unconditionally writes credentials to osxkeychain after a
successful login, regardless of the config presented to it.

    # I wrote this:
    { "auths": {}, "credsStore": "", "credHelpers": {} }
    # After `docker login --config <dir> ghcr.io ...` succeeded:
    {
      "auths": { "ghcr.io": {} },        # empty — auth is in Keychain
      "credsStore": "osxkeychain"        # Docker rewrote it back
    }

So `--config` flag, DOCKER_CONFIG env var, credsStore="" etc. all share
the same fate: Docker re-enables osxkeychain after every successful
login. The Mac mini runner is a launchd user agent with a locked
Keychain, so storage fails with -25308.

This PR replaces the `docker login` invocation entirely. We write
`base64(user:pat)` directly into the disposable DOCKER_CONFIG's `auths`
map. `docker/build-push-action@v5` and the daemon honor the auths map
for push without ever calling `docker login`, so the Keychain is never
involved.

Same shape in both workflows:
- publish-canvas-image.yml — single registry (ghcr.io)
- publish-platform-image.yml — two registries (ghcr.io + registry.fly.io)
  Fly username remains literal "x".

Security:
- Token env vars never echoed. Heredoc writes the auth blob via
  `umask 077` (file mode 600). The temp config dir lives under
  RUNNER_TEMP and is reaped at job end.
- Diagnostics preserved (docker version + binary ls + registry keys
  only, no values) so future runner permission regressions remain
  visible without leaking secrets.

Equivalent to closed PR #464 — re-opening because main is still
broken (verified by inspecting the most recent failure). The closing
comment on #464 stated the issue was already addressed by #341, but
it isn't.
2026-04-16 09:25:20 -07:00
Hongming Wang
a06a9baca5 Merge pull request #485 from Molecule-AI/feat/mcp-docs-tokens-external-agent
feat(platform): token management API + MCP setup + external agent guide
2026-04-16 09:00:04 -07:00
Hongming Wang
8451af0683 docs: update remote-workspaces-readiness for Phase 30.1 shipped status
- Mark Phase 30.1 (auth tokens) as shipped
- Update hard-problem A (spoofing) from blocker → resolved
- Cross-reference new guides: external-agent-registration, token-management, mcp-server-setup
- Update last-reviewed date

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:49:07 -07:00
Hongming Wang
6f3c16eb78 fix(ci): use docker login CLI instead of login-action to bypass macOS Keychain
docker/login-action@v3 ignores DOCKER_CONFIG and still tries the
macOS system keychain on the self-hosted runner, producing:
  error storing credentials: User interaction is not allowed. (-25308)

Switch to `docker login ... --password-stdin` which respects
DOCKER_CONFIG and writes credentials to the per-run config.json
we created in the isolate step. Applied to both GHCR and Fly
registry logins in both publish workflows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:45:20 -07:00
Hongming Wang
071fb0da88 fix(tenant): WebSocket URL derivation + AdminAuth same-origin for tenant image
Two bugs on the combined tenant image (canvas + API same-origin):

1. WebSocket URL: NEXT_PUBLIC_WS_URL="" (empty string for same-origin)
   was preserved by ?? operator, producing an invalid WS URL. Now derives
   from window.location when both env vars are empty. Same fix applied
   to TerminalTab.

2. AdminAuth blocking canvas: same-origin requests have no Origin header,
   so neither AdminAuth nor CanvasOrBearer could authenticate the canvas.
   Added isSameOriginCanvas() that checks Referer against request Host,
   gated behind CANVAS_PROXY_URL (only active on tenant image). This
   lets the canvas create/list workspaces, view events, etc. without a
   bearer token when served from the same Go process.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:43:01 -07:00
Hongming Wang
3892e4dee1 feat(platform): token management API + MCP setup + external agent guide
1. Token Management API (closes production gap):
   - GET /workspaces/:id/tokens — list tokens (prefix + metadata, never plaintext)
   - POST /workspaces/:id/tokens — create new token (plaintext returned once)
   - DELETE /workspaces/:id/tokens/:tokenId — revoke specific token
   - Behind WorkspaceAuth middleware (need existing token to manage tokens)
   - Tests skip gracefully when no DB available

2. MCP Server Setup:
   - Fix .mcp.json to use npx @molecule-ai/mcp-server (was referencing
     non-existent local ./mcp-server/dist/index.js)
   - Add comprehensive tool→API mapping doc (87 tools across 15 categories)

3. External Agent Registration Guide:
   - Step-by-step: create workspace, register, heartbeat, A2A messaging
   - Python (Flask) and Node.js (Express) complete working examples
   - Communication rules, lifecycle, security, troubleshooting

4. Token Management Guide:
   - Bootstrap flow, rotation procedure, security properties

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:37:42 -07:00
Hongming Wang
15abfca106 Merge pull request #484 from Molecule-AI/fix/publish-workflow-yaml
fix(ci): fix YAML parse error in publish workflows
2026-04-16 08:22:37 -07:00
Hongming Wang
f93ec926cb fix(ci): replace heredoc JSON with printf in publish workflows
The heredoc block writing Docker config.json had unindented `{` at
column 1, which GitHub Actions' YAML parser interpreted as a flow
mapping start — causing every publish-platform-image and
publish-canvas-image run to fail with 0 jobs (startup_failure).

Replace `cat <<'JSON' ... JSON` with a single `printf` call that
produces identical config.json content without confusing the parser.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:20:43 -07:00
Hongming Wang
08a8c06cc3 Merge pull request #481 from Molecule-AI/feat/fly-deploy-step
feat(ci): deploy to Fly after image push
2026-04-16 08:15:46 -07:00
Hongming Wang
40c825b8ed Merge pull request #483 from Molecule-AI/fix/platform-modular-template-support
fix(platform): unblock org-template imports against modular workspace templates
2026-04-16 07:55:26 -07:00
Hongming Wang
54e1500e6a Merge pull request #482 from Molecule-AI/fix/canvas-ux-improvements
fix(canvas): UX improvements — tokens, focus, loading, a11y
2026-04-16 07:54:48 -07:00
Hongming Wang
bf17209d6c Merge pull request #480 from Molecule-AI/feat/lark-channel-adapter
feat(channels): Lark / Feishu channel adapter + idempotent migration 023
2026-04-16 07:54:45 -07:00
rabbitblood
aeb9308c7d fix(platform): unblock org-template imports against modular workspace templates
Two adjacent fixes that surfaced trying to bring the molecule-dev org
template back up against the new standalone workspace-template-* repos.

1) handlers/org.go — expand ${VAR} in workspace_dir before validation.
   The molecule-dev pm/workspace.yaml (and any operator's per-host
   binding) ships `workspace_dir: ${WORKSPACE_DIR}` so each operator
   can pick the host path PM bind-mounts. Without expansion the literal
   "${WORKSPACE_DIR}" string reaches validateWorkspaceDir and fails with
   "must be an absolute path", aborting the whole org import.
   Other fields (channel config, prompts) already go through expandWithEnv;
   workspace_dir was the last hold-out.

2) provisioner/provisioner.go — inject PYTHONPATH=/app for every
   workspace container. Standalone template Dockerfiles COPY adapter.py
   to /app and set ENV ADAPTER_MODULE=adapter, but molecule-runtime is
   a pip console_script entry point so cwd isn't on sys.path
   automatically. Setting PYTHONPATH here fixes every adapter image at
   once instead of needing 8 PRs against template repos. Operator
   override still wins (workspace EnvVars are appended after, so Docker
   takes the later duplicate).

   Note: this unblocks the import path but does NOT make claude-code /
   hermes / etc. boot. The runtime itself has a separate top-level
   `from adapters import` that breaks against modular templates —
   tracked at workspace-runtime#1.

Tests: TestBuildContainerEnv_InjectsPYTHONPATH +
TestBuildContainerEnv_WorkspaceEnvVarsCanOverridePYTHONPATH lock the
default + operator-override invariants. expandWithEnv is already covered
by TestExpandWithEnv_* — the workspace_dir use site is a one-line call
to that primitive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:49:45 -07:00
Hongming Wang
a4b1886c35 fix(canvas): address all code review findings on PR #482
- Reconcile TIER_CONFIG/TIER_COLORS into single TIER_CONFIG with both
  `color` (pill style) and `border` (bordered badge style) fields
- Remove TemplatePalette alias indirection (TIER_LABELS_SHARED → direct import)
- Extract inline spinner SVGs to shared Spinner component (3 copies → 1)
- Migrate status dot colors from 6 remaining files to shared tokens:
  SearchDialog, StatusDot, Legend, ContextMenu, Toolbar + add statusDotClass()
- Add COMM_TYPE_LABELS to design-tokens, used by CommunicationOverlay sr-only
- Update reduced-motion tests: components that delegate to design-tokens
  pass the guard check via import detection; add design-tokens.ts own test
- 507/507 tests pass, build clean

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:48:47 -07:00
Hongming Wang
ac6d6ce5bd fix(canvas): UX improvements — shared tokens, focus rings, loading spinners, a11y
- Extract STATUS_CONFIG, TIER_CONFIG, TIER_COLORS to shared design-tokens.ts
  (eliminates 3 duplicate definitions across WorkspaceNode, EmptyState, TemplatePalette)
- Add focus-visible:ring-2 ring-blue-500 to WorkspaceNode, SidePanel tabs,
  EmptyState buttons, TemplatePalette buttons (keyboard navigation now visible)
- Replace "Loading..." text with animated spinner SVG in EmptyState,
  TemplatePalette sidebar, and OrgTemplatesSection
- Add disabled:cursor-not-allowed + suppress hover styling when disabled
  on EmptyState template buttons and TemplatePalette deploy buttons
- Brighten SidePanel tab hover from bg-zinc-800/20 to bg-zinc-800/40
  and text from zinc-300 to zinc-200
- Add screen reader labels to CommunicationOverlay directional arrows
  and status icons (sr-only text for "sent", "received", "to", status)

Fixes #422, #424, #427

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:35:44 -07:00
Hongming Wang
d7161f5877 feat(ci): add Fly deploy step to publish-platform-image workflow
After pushing the tenant image to registry.fly.io, the workflow now
lists all running/stopped molecule-tenant machines and updates each
to the newly pushed image tag. Gracefully skips if no machines exist
(control plane provisions on demand).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:29:42 -07:00
rabbitblood
a394ae55c3 feat(channels): Lark / Feishu adapter (outbound webhook + Events API inbound)
New ChannelAdapter implementation for Lark (international, open.larksuite.com)
and Feishu (China, open.feishu.cn). Both speak the same payload format —
only the host differs — so a single adapter covers both.

Outbound: POST text to a Custom Bot webhook URL with msg_type:"text".
Lark returns 200 OK even when delivery fails — the body's `code` field is
the truth. Adapter parses the response and returns a Go error when
code != 0 so callers don't think a revoked-webhook send succeeded.

Inbound: handles both v1 url_verification (handshake) and v2 event_callback
(im.message.receive_v1) shapes. Optional verify_token field — when set,
inbound payloads with mismatching tokens are rejected via constant-time
compare (#337 class — never raw == against a stored secret).

Sender ID resolution prefers user_id → falls back to open_id (open_id is
always present; user_id only when the bot has the contacts permission).
Non-text message types and non-message events return nil, nil so the
receiver responds 200 OK without dispatching.

Tests: 23 cases — identity, ValidateConfig (6 sub-cases incl. URL prefix
matrix), SendMessage (no URL / invalid prefix / happy-path body shape /
api-error-code surfacing), ParseWebhook (handshake + token mismatch +
text message + open_id fallback + non-message + non-text + token mismatch
+ malformed JSON + malformed content + empty text), StartPolling no-op,
registry presence.

Also: make migration 023 idempotent (ADD COLUMN IF NOT EXISTS) — the
platform's migration runner has no schema_migrations tracking table, so
every .up.sql replays on every boot. Without IF NOT EXISTS the second
boot against an existing volume crashes with "column already exists".
Followup issue to be filed for proper migration tracking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:10:58 -07:00
Hongming Wang
1453ef91b6 Merge pull request #478 from Molecule-AI/feat/provision-env-mutator-hook
feat(platform): provision-time env mutator hook for plugins
2026-04-16 06:56:21 -07:00
rabbitblood
4065a7edee feat(platform): provision-time env mutator hook for plugins
Add `provisionhook.EnvMutator` extension point so out-of-tree plugins
(e.g. github-app-auth, vault-secrets) can inject or override env vars
right before container Start, without forking core or piling more
provider-specific code into the handlers package.

WorkspaceHandler gains an optional `envMutators *provisionhook.Registry`
wired in via SetEnvMutators during boot. The hook fires after built-in
secret loads + per-agent git identity, so plugins can both read what's
already there and override anything they own (GIT_AUTHOR_*, GITHUB_TOKEN).

A nil registry is a no-op via Registry.Run's nil-receiver branch — keeps
the hot path a single nil compare and means existing flows stay green
even with zero plugins registered.

Mutator failure aborts provisioning and marks the workspace failed with
the wrapped error in last_sample_error. Failing fast surfaces the cause
to the operator instead of letting an agent boot into opaque "git push
401" loops it can never recover from on its own.

Tests cover ordered execution, chained env visibility, first-error abort,
nil-receiver no-op, nil-mutator drop, registration order, and concurrent
register-vs-run safety (-race clean).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 06:47:09 -07:00
Hongming Wang
04c1b16871 fix(canvas): template layout + org card styling
- Wider modal (max-w-2xl), 3-col grid, no max-height clipping
- Org template cards: violet→blue, consistent rounded-xl styling
- Container scrolls vertically instead of cutting off
2026-04-16 06:42:41 -07:00
Hongming Wang
3d988f7367 fix(e2e): clear ADMIN_TOKEN after last workspace delete so AdminAuth fail-opens 2026-04-16 06:34:17 -07:00
Hongming Wang
e691065b0a fix(e2e): fall back to test-token when register doesn't return a new token
On re-registration (workspace already has tokens), the register endpoint
doesn't issue a new token — it returns the existing one in the response
or omits it. The e2e_extract_token helper returns empty in that case.
Fall back to the per-workspace token we already minted via test-token.
2026-04-16 06:29:44 -07:00
Hongming Wang
1c00be1d09 fix(e2e): use per-workspace tokens for register + heartbeat + discover
AdminAuth (admin token) gates workspace CRUD operations.
WorkspaceAuth (per-workspace token) gates register, heartbeat, discover.
The test now mints a workspace-specific token via test-token endpoint
for each workspace before calling register.
2026-04-16 06:22:16 -07:00
Hongming Wang
8a070f0077 fix(e2e): use acurl for registry/register + re-register calls (C18 auth) 2026-04-16 06:15:39 -07:00
Hongming Wang
854d2b688d fix(e2e): read auth_token not token from test-token response 2026-04-16 06:11:32 -07:00
Hongming Wang
00ad6b246e debug: add test-token response logging to e2e 2026-04-16 06:08:58 -07:00
Hongming Wang
9f35f1fecf fix(e2e): use admin bearer token for AdminAuth-gated API calls
After the first workspace is created and the test-token endpoint mints
a bearer, HasAnyLiveTokenGlobal returns true. All subsequent calls to
AdminAuth-gated routes (workspace CRUD, events, bundles, etc.) need the
token. Added acurl() helper that attaches the token when available.
2026-04-16 06:05:13 -07:00