Commit Graph

3663 Commits

Author SHA1 Message Date
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
Hongming Wang
bf9fb7cb51
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
rabbitblood
e08f28c962 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
1a85ca7656 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
295b1fa8d9 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
3231560fcf 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
f4462a24df 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
a661e1bf55 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
edd17cecaa 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
b1def4a933 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
Hongming Wang
dacc7425ef 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
Hongming Wang
77d6f5e7a0 fix(ci): heredoc indentation in publish workflows + add dev-start.sh
Two fixes:
1. publish-canvas-image.yml + publish-platform-image.yml: the JSON
   heredoc for config.json had leading whitespace from YAML indentation,
   producing invalid JSON. Docker fell back to osxkeychain → -25308.
   Fixed by removing indentation inside the heredoc body.

2. Added scripts/dev-start.sh — one-command local dev environment.
   Starts infra (docker-compose), platform (Go), and canvas (Next.js)
   with proper health checks and cleanup on Ctrl-C.
2026-04-16 05:56:25 -07:00
Hongming Wang
0071b66a59 fix(ci): heredoc indentation in publish workflows + add dev-start.sh
Two fixes:
1. publish-canvas-image.yml + publish-platform-image.yml: the JSON
   heredoc for config.json had leading whitespace from YAML indentation,
   producing invalid JSON. Docker fell back to osxkeychain → -25308.
   Fixed by removing indentation inside the heredoc body.

2. Added scripts/dev-start.sh — one-command local dev environment.
   Starts infra (docker-compose), platform (Go), and canvas (Next.js)
   with proper health checks and cleanup on Ctrl-C.
2026-04-16 05:56:25 -07:00
Hongming Wang
d376c55447 Merge pull request #470 from Molecule-AI/fix/aria-time-sensitive-components
fix(a11y): WCAG ARIA fixes for time-sensitive components
2026-04-16 05:52:23 -07:00
Hongming Wang
d10067697e
Merge pull request #470 from Molecule-AI/fix/aria-time-sensitive-components
fix(a11y): WCAG ARIA fixes for time-sensitive components
2026-04-16 05:52:23 -07:00
Hongming Wang
32abfcf3ae fix: use /bin/sh not bash in clone-manifest (Alpine has no bash) 2026-04-16 05:42:49 -07:00
Hongming Wang
fd719f4d36 fix: use /bin/sh not bash in clone-manifest (Alpine has no bash) 2026-04-16 05:42:49 -07:00
Hongming Wang
cd6b0b75be Merge pull request #462 from Molecule-AI/fix/security-460-461-yaml-injection-error-disclosure
fix(security): YAML-quote skill/prompt names in generateDefaultConfig + opaque file-write errors
2026-04-16 05:40:49 -07:00
Hongming Wang
dc895bb17e
Merge pull request #462 from Molecule-AI/fix/security-460-461-yaml-injection-error-disclosure
fix(security): YAML-quote skill/prompt names in generateDefaultConfig + opaque file-write errors
2026-04-16 05:40:49 -07:00
Security Auditor
6672b5f158 fix(security): YAML-quote skill/prompt names in generateDefaultConfig + opaque file-write errors
Closes #460, #461.

**#460 — YAML injection via unquoted skill/prompt filenames**
`generateDefaultConfig` extracted skill directory names and prompt file
names from user-supplied `body.Files` keys and wrote them directly into
YAML list items without quoting:

  cfg.WriteString("  - " + s + "\n")

`validateRelPath` only blocks path traversal (`../`); it does NOT block
YAML control characters including newlines. On Linux, filenames can
contain newlines, so an attacker with any live workspace bearer token
could submit:

  {"files": {"skills/legit\nruntime: malicious/SKILL.md": "# skill"}}

The generated config.yaml would then contain `runtime: malicious` as a
top-level YAML key, overriding the runtime for workspaces provisioned
from the template.

Fix: extract `yamlEscape` as a reusable local from the same
`strings.NewReplacer` already used for the `name` field (#221) and apply
it to both the `skills:` and `prompt_files:` list items, wrapping each
in double-quotes.

**#461 — Docker error details in ReplaceFiles 500 responses**
`ReplaceFiles` returned `fmt.Sprintf("failed to write files: %v", err)`
in two 500 paths, where `err` comes from Docker API calls and may include
internal container names, volume names, and daemon error messages.

Fix: log the full error server-side and return a static opaque string to
the caller.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 05:40:45 -07:00
Security Auditor
284fb26558 fix(security): YAML-quote skill/prompt names in generateDefaultConfig + opaque file-write errors
Closes #460, #461.

**#460 — YAML injection via unquoted skill/prompt filenames**
`generateDefaultConfig` extracted skill directory names and prompt file
names from user-supplied `body.Files` keys and wrote them directly into
YAML list items without quoting:

  cfg.WriteString("  - " + s + "\n")

`validateRelPath` only blocks path traversal (`../`); it does NOT block
YAML control characters including newlines. On Linux, filenames can
contain newlines, so an attacker with any live workspace bearer token
could submit:

  {"files": {"skills/legit\nruntime: malicious/SKILL.md": "# skill"}}

The generated config.yaml would then contain `runtime: malicious` as a
top-level YAML key, overriding the runtime for workspaces provisioned
from the template.

Fix: extract `yamlEscape` as a reusable local from the same
`strings.NewReplacer` already used for the `name` field (#221) and apply
it to both the `skills:` and `prompt_files:` list items, wrapping each
in double-quotes.

**#461 — Docker error details in ReplaceFiles 500 responses**
`ReplaceFiles` returned `fmt.Sprintf("failed to write files: %v", err)`
in two 500 paths, where `err` comes from Docker API calls and may include
internal container names, volume names, and daemon error messages.

Fix: log the full error server-side and return a static opaque string to
the caller.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 05:40:45 -07:00
Canvas Agent
120b886d00 fix(a11y): WCAG ARIA fixes for time-sensitive components (Fixes #Fix1/#Fix2/#Fix3)
- ApprovalBanner: add role="alert" aria-live="assertive" aria-atomic="true" to
  each pending approval card; aria-hidden="true" on decorative ⚠ icon span
- TerminalTab: add role="status" aria-live="polite" to connection status bar;
  add role="alert" to inline error message div
- BundleDropZone: extract shared processFile(); add hidden <input type="file">
  with id/accept/aria-label; add sr-only focus:not-sr-only keyboard trigger
  button; add role="status" aria-live="polite" to result toast

Tests: 7 new assertions in aria-time-sensitive.test.tsx covering all 3 fixes
(496/496 pass, build clean)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 05:40:40 -07:00
Canvas Agent
66fd7c3ccf fix(a11y): WCAG ARIA fixes for time-sensitive components (Fixes #Fix1/#Fix2/#Fix3)
- ApprovalBanner: add role="alert" aria-live="assertive" aria-atomic="true" to
  each pending approval card; aria-hidden="true" on decorative ⚠ icon span
- TerminalTab: add role="status" aria-live="polite" to connection status bar;
  add role="alert" to inline error message div
- BundleDropZone: extract shared processFile(); add hidden <input type="file">
  with id/accept/aria-label; add sr-only focus:not-sr-only keyboard trigger
  button; add role="status" aria-live="polite" to result toast

Tests: 7 new assertions in aria-time-sensitive.test.tsx covering all 3 fixes
(496/496 pass, build clean)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 05:40:40 -07:00
Canvas Agent
242306d239 fix(canvas): fitView on new workspace provision — respects user zoom level (#426)
Replace setCenter(x, y, {zoom:1}) with fitView({nodes:[{id}]}) in the
molecule:pan-to-node handler (Canvas.tsx). The old implementation forced
zoom=1 regardless of the user's current zoom level, which was jarring when
panned/zoomed away. fitView adapts to whatever zoom the user had and
gracefully fits the new node in view.

Tests:
- Canvas.pan-to-node.test.tsx: fitView called with correct nodeId after
  100ms debounce; debounce coalesces rapid successive events.
- canvas-events-pan.test.ts: molecule:pan-to-node dispatched for new
  provisions only, NOT on restart of an existing node.

Fixes #426.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 05:40:40 -07:00
Canvas Agent
477b6c06b7 fix(canvas): fitView on new workspace provision — respects user zoom level (#426)
Replace setCenter(x, y, {zoom:1}) with fitView({nodes:[{id}]}) in the
molecule:pan-to-node handler (Canvas.tsx). The old implementation forced
zoom=1 regardless of the user's current zoom level, which was jarring when
panned/zoomed away. fitView adapts to whatever zoom the user had and
gracefully fits the new node in view.

Tests:
- Canvas.pan-to-node.test.tsx: fitView called with correct nodeId after
  100ms debounce; debounce coalesces rapid successive events.
- canvas-events-pan.test.ts: molecule:pan-to-node dispatched for new
  provisions only, NOT on restart of an existing node.

Fixes #426.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 05:40:40 -07:00
Hongming Wang
2cb49c1e51 Merge pull request #477 from Molecule-AI/fix/canvas-proxy-test-closenotify
fix(test): canvas proxy test CloseNotify panic
2026-04-16 05:40:36 -07:00
Hongming Wang
16b6e8b53e
Merge pull request #477 from Molecule-AI/fix/canvas-proxy-test-closenotify
fix(test): canvas proxy test CloseNotify panic
2026-04-16 05:40:36 -07:00
Hongming Wang
490a3741a1 fix(test): wrap httptest.ResponseRecorder with CloseNotify for canvas proxy tests
httputil.ReverseProxy calls CloseNotify() which httptest.ResponseRecorder
doesn't implement. Gin casts the writer, causing a panic. Added a
closeNotifyRecorder wrapper with a no-op channel.
2026-04-16 05:40:17 -07:00
Hongming Wang
8b13fff355 fix(test): wrap httptest.ResponseRecorder with CloseNotify for canvas proxy tests
httputil.ReverseProxy calls CloseNotify() which httptest.ResponseRecorder
doesn't implement. Gin casts the writer, causing a panic. Added a
closeNotifyRecorder wrapper with a no-op channel.
2026-04-16 05:40:17 -07:00
rabbitblood
6f414b03ae chore(gitignore): exclude .secrets/ + *.pem from tracking
Local-only secrets (GitHub App private keys, future per-tenant
credentials) live in .secrets/ on the host. Belt-and-braces with the
existing .env exclusion so a stray copy / rename can't leak.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 05:39:31 -07:00
rabbitblood
57870abe98 chore(gitignore): exclude .secrets/ + *.pem from tracking
Local-only secrets (GitHub App private keys, future per-tenant
credentials) live in .secrets/ on the host. Belt-and-braces with the
existing .env exclusion so a stray copy / rename can't leak.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 05:39:31 -07:00
Hongming Wang
d776cc0a61 Merge pull request #476 from Molecule-AI/fix/ci-remove-cli-build
fix(ci): remove molecli build step — CLI in standalone repo
2026-04-16 05:28:35 -07:00
Hongming Wang
2a5bc11ee2
Merge pull request #476 from Molecule-AI/fix/ci-remove-cli-build
fix(ci): remove molecli build step — CLI in standalone repo
2026-04-16 05:28:35 -07:00
Hongming Wang
104ae6ca68 fix(ci): remove molecli build step — CLI moved to standalone repo 2026-04-16 05:28:10 -07:00
Hongming Wang
558d5c456a fix(ci): remove molecli build step — CLI moved to standalone repo 2026-04-16 05:28:10 -07:00
Hongming Wang
f30d678d10 Merge pull request #456 from Molecule-AI/fix/issue-418-persist-auth-token
[Backend Engineer] fix(auth): inject fresh bearer token into config volume on every provision
2026-04-16 05:26:32 -07:00
Hongming Wang
2206117beb
Merge pull request #456 from Molecule-AI/fix/issue-418-persist-auth-token
[Backend Engineer] fix(auth): inject fresh bearer token into config volume on every provision
2026-04-16 05:26:32 -07:00
Molecule AI Backend Engineer
92a2c84d76 fix(auth): inject fresh bearer token into config volume on every provision (closes #418)
Container rebuild or volume wipe caused workspaces to lose /configs/.auth_token.
On re-registration the platform returned no auth_token (HasAnyLiveToken==true →
no re-issue), leaving the workspace unable to authenticate any subsequent API call.

Fix: provisionWorkspaceOpts now calls issueAndInjectToken before Start(). This
revokes any existing live tokens (plaintext is irrecoverable from the stored hash,
so rotation is the only safe path) and issues a fresh token that is written into
cfg.ConfigFiles[".auth_token"]. WriteFilesToContainer delivers it to /configs
immediately after ContainerStart, racing safely ahead of the Python adapter's
1-2s startup time.

Failure modes are soft: revoke or issue errors skip injection with a warning;
provisioning continues and the workspace recovers on the next restart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 05:26:10 -07:00
Molecule AI Backend Engineer
eec59fe63b fix(auth): inject fresh bearer token into config volume on every provision (closes #418)
Container rebuild or volume wipe caused workspaces to lose /configs/.auth_token.
On re-registration the platform returned no auth_token (HasAnyLiveToken==true →
no re-issue), leaving the workspace unable to authenticate any subsequent API call.

Fix: provisionWorkspaceOpts now calls issueAndInjectToken before Start(). This
revokes any existing live tokens (plaintext is irrecoverable from the stored hash,
so rotation is the only safe path) and issues a fresh token that is written into
cfg.ConfigFiles[".auth_token"]. WriteFilesToContainer delivers it to /configs
immediately after ContainerStart, racing safely ahead of the Python adapter's
1-2s startup time.

Failure modes are soft: revoke or issue errors skip injection with a warning;
provisioning continues and the workspace recovers on the next restart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 05:26:10 -07:00
Hongming Wang
cdc1caf6f9 Merge pull request #467 from Molecule-AI/feat/slack-webhook-validation
[Backend Engineer] feat(channels): Slack adapter with webhook URL validation (#384)
2026-04-16 05:22:47 -07:00
Hongming Wang
7fca9723a0
Merge pull request #467 from Molecule-AI/feat/slack-webhook-validation
[Backend Engineer] feat(channels): Slack adapter with webhook URL validation (#384)
2026-04-16 05:22:47 -07:00