Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
ded10a0
docs(canary-release): flag as aspirational; link to current state
Apr 22, 2026
691de28
Merge pull request #1649 from Molecule-AI/docs/reconcile-canary-relea…
HongmingWang-Rabbit Apr 22, 2026
bc82fa4
docs(security): move sensitive runbooks to private internal repo
Apr 22, 2026
0db8445
Merge pull request #1661 from Molecule-AI/docs/move-sensitive-to-inte…
HongmingWang-Rabbit Apr 22, 2026
f6e6a64
fix(canvas): forward-port dynamic runtime dropdown from staging (PR #…
Apr 22, 2026
ed6f4c6
Merge pull request #1666 from Molecule-AI/fix/canvas-dynamic-runtime-…
HongmingWang-Rabbit Apr 22, 2026
7c102db
ci: canary-verify graceful-skip + draft auto-promote staging→main
Apr 22, 2026
557394f
Merge pull request #1667 from Molecule-AI/fix/canary-verify-graceful-…
HongmingWang-Rabbit Apr 22, 2026
6c618c9
feat: add tool_trace to activity_logs for platform-level agent observ…
HongmingWang-Rabbit Apr 22, 2026
d7afd15
feat: platform instructions system with global/team/workspace scope
HongmingWang-Rabbit Apr 22, 2026
e1d77a1
ci: trigger CI from PAT push
HongmingWang-Rabbit Apr 22, 2026
f4207cd
fix(F1085): scope rm to /configs/<path> not /configs + <path>
HongmingWang-Rabbit Apr 22, 2026
41316ee
Merge pull request #1682 from Molecule-AI/fix/f1085-rm-scope-v4
HongmingWang-Rabbit Apr 22, 2026
7e3cd04
feat(provision): propagate workspace model into runtime env
Apr 22, 2026
2b60316
Merge pull request #1685 from Molecule-AI/feat/propagate-model-env-to…
HongmingWang-Rabbit Apr 22, 2026
ed26f27
fix(review): address code review blockers on tool-trace + instructions
HongmingWang-Rabbit Apr 22, 2026
dcbcf19
fix(test): guard msg.metadata assignment for non-Message returns
HongmingWang-Rabbit Apr 22, 2026
d956164
refactor(secrets): strip Service dropdown from Add-Key form
Apr 22, 2026
6d87408
fix(ssrf): honour saasMode for RFC-1918 private IPs
Apr 23, 2026
8b1af97
feat(canvas): default tier T3 and hide T1/T2 on SaaS
Apr 23, 2026
4c0cb48
fix(cp-provisioner): use CP_ADMIN_API_TOKEN bearer for /cp/admin/* ro…
Apr 23, 2026
7d01f13
fix(orgtoken): cast org_id to text in COALESCE to prevent 500
Apr 23, 2026
0baa6ab
ci: retrigger after retarget to main
Apr 23, 2026
9a20fdb
ci: retrigger after retarget to main
Apr 23, 2026
7252428
ci: retrigger after retarget to main
Apr 23, 2026
aacd8c9
ci: retrigger after retarget to main
Apr 23, 2026
8a2345e
Merge PR #1692: fix(ssrf): honour saasMode for RFC-1918 private IPs
HongmingWang-Rabbit Apr 23, 2026
9d0d213
docs(marketing+research): move sensitive strategy + research to inter…
Apr 23, 2026
0574e7c
feat(canvas): add T4 tier (full-host access); SaaS default T4
Apr 23, 2026
03741d1
feat(files-api): SSH-backed write for SaaS workspaces (fixes 500 dock…
Apr 23, 2026
470e824
Merge pull request #1696 from Molecule-AI/fix/orgtokens-uuid-coalesce
HongmingWang-Rabbit Apr 23, 2026
4bee15f
Merge pull request #1695 from Molecule-AI/fix/cp-admin-bearer-for-con…
HongmingWang-Rabbit Apr 23, 2026
7207133
Merge pull request #1702 from Molecule-AI/fix/files-api-saas-ssh-write
HongmingWang-Rabbit Apr 23, 2026
e8523d7
Merge pull request #1693 from Molecule-AI/feat/saas-tier-default-t3
HongmingWang-Rabbit Apr 23, 2026
66de81f
Merge pull request #1689 from Molecule-AI/refactor/strip-secret-servi…
HongmingWang-Rabbit Apr 23, 2026
0582651
Merge remote-tracking branch 'origin/main' into docs/move-marketing-s…
Apr 23, 2026
59e0fd6
Merge pull request #1697 from Molecule-AI/docs/move-marketing-strateg…
HongmingWang-Rabbit Apr 23, 2026
e08ea7b
fix(canvas): require hermes model at create + send to CP (fixes silen…
Apr 23, 2026
47e459c
Merge pull request #1714 from Molecule-AI/fix/hermes-require-model-at…
HongmingWang-Rabbit Apr 23, 2026
16b2e5d
Merge branch 'main' into feat/tool-trace-v2
molecule-ai[bot] Apr 23, 2026
2df644f
fix(handlers): unblock Platform (Go) CI — sqlmock budget-check + test…
Apr 23, 2026
32555a8
Merge pull request #1686 from Molecule-AI/feat/tool-trace-v2
molecule-ai[bot] Apr 23, 2026
2885583
feat(workspace): 45-min gh-token refresh daemon + credential helper c…
Apr 23, 2026
2849a9a
feat(scheduler): sweepPhantomBusy — clear stuck active_tasks from cra…
Apr 23, 2026
d4cead5
chore: extract ContextMenu Zustand fix + a2a_proxy local-docker SSRF …
Apr 23, 2026
7c81b08
fix(registry): auto-recover failed/provisioning workspaces on success…
Apr 23, 2026
0d820bd
Merge pull request #1735 from Molecule-AI/chore/extract-1664-small-fixes
HongmingWang-Rabbit Apr 23, 2026
c5d81aa
Merge pull request #1730 from Molecule-AI/fix/workspace-gh-token-refr…
HongmingWang-Rabbit Apr 23, 2026
3c785bc
Merge pull request #1731 from Molecule-AI/fix/scheduler-sweep-phantom…
HongmingWang-Rabbit Apr 23, 2026
d5ec0a9
Merge pull request #1734 from Molecule-AI/fix/registry-heartbeat-auto…
HongmingWang-Rabbit Apr 23, 2026
64e4c7b
Merge pull request #1725 from Molecule-AI/fix/platform-go-ci-tests
HongmingWang-Rabbit Apr 23, 2026
24500bc
fix(canvas): add type=button to ApprovalBanner Approve/Deny buttons
Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions .github/workflows/auto-promote-staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
name: Auto-promote staging → main

# Fires after any of the staging-branch quality gates complete. When ALL
# required gates are green on the same staging SHA, fast-forwards `main`
# to that SHA automatically — closing the gap that historically let
# features sit on staging for weeks waiting for a bulk promotion PR
# (see molecule-core#1496 for the 1172-commit example).
#
# Safety model:
# - Runs ONLY on workflow_run events for the staging branch.
# - Requires EVERY named gate workflow to have the same head_sha and
# all be `conclusion == success`. If any of them is red, skipped,
# cancelled, or pending, we abort (stay on the current main).
# - Uses --ff-only: refuses to advance main if main has diverged from
# the staging history (e.g. a hotfix landed directly on main). In
# that case a human resolves the fork.
# - Writes a commit summary so the promote shows up in git log as a
# deliberate act, not a stealth move.
#
# **Initial rollout:** ship this file but leave the `enabled` input set
# such that nothing auto-promotes until staging CI has been reliably
# green for a few days. Toggle via repo variable `AUTO_PROMOTE_ENABLED`.

on:
workflow_run:
workflows:
- CI
- E2E Staging Canvas (Playwright)
- E2E API Smoke Test
- CodeQL
types: [completed]
workflow_dispatch:
inputs:
force:
description: "Force promote even when AUTO_PROMOTE_ENABLED is unset (manual override)"
required: false
default: "false"

permissions:
contents: write

jobs:
check-all-gates-green:
# Only consider staging pushes. PRs into staging don't promote.
if: >
(github.event_name == 'workflow_run' &&
github.event.workflow_run.head_branch == 'staging' &&
github.event.workflow_run.event == 'push')
|| github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
outputs:
all_green: ${{ steps.gates.outputs.all_green }}
head_sha: ${{ steps.gates.outputs.head_sha }}
steps:
- name: Check all required gates on this SHA
id: gates
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail

# Required gate workflow names. Must match the `name:` field
# in the respective .github/workflows/*.yml files.
GATES=(
"CI"
"E2E Staging Canvas (Playwright)"
"E2E API Smoke Test"
"CodeQL"
)

echo "head_sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT"
echo "Checking gates on SHA ${HEAD_SHA}"

ALL_GREEN=true
for gate in "${GATES[@]}"; do
# Query the most recent run of this workflow on this SHA.
# event=push to avoid picking up PR runs. branch=staging to
# guard against someone dispatching the gate on a non-staging
# branch at the same SHA.
RESULT=$(gh run list \
--repo "$REPO" \
--workflow "$gate" \
--branch staging \
--event push \
--commit "$HEAD_SHA" \
--limit 1 \
--json status,conclusion \
--jq '.[0] | "\(.status)/\(.conclusion // "none")"' \
2>/dev/null || echo "missing/none")

echo " $gate → $RESULT"

# Only completed/success counts. completed/failure or
# in_progress/anything or no record at all = abort.
if [ "$RESULT" != "completed/success" ]; then
ALL_GREEN=false
fi
done

echo "all_green=${ALL_GREEN}" >> "$GITHUB_OUTPUT"
if [ "$ALL_GREEN" != "true" ]; then
echo "::notice::auto-promote: not all gates are green on ${HEAD_SHA} — staying on current main"
fi

promote:
needs: check-all-gates-green
if: needs.check-all-gates-green.outputs.all_green == 'true'
runs-on: ubuntu-latest
steps:
- name: Check rollout gate
env:
AUTO_PROMOTE_ENABLED: ${{ vars.AUTO_PROMOTE_ENABLED }}
FORCE_INPUT: ${{ github.event.inputs.force }}
run: |
set -eu
# Repo variable AUTO_PROMOTE_ENABLED=true flips this on. While
# it's unset, the workflow dry-runs (logs what it would have
# done) but doesn't actually push to main. Set the variable in
# Settings → Secrets and variables → Actions → Variables.
if [ "${AUTO_PROMOTE_ENABLED:-}" != "true" ] && [ "${FORCE_INPUT:-false}" != "true" ]; then
{
echo "## ⏸ Auto-promote disabled"
echo
echo "Repo variable \`AUTO_PROMOTE_ENABLED\` is not set to \`true\`."
echo "All gates are green on staging; would have promoted to \`main\`."
echo
echo "To enable: Settings → Secrets and variables → Actions → Variables → \`AUTO_PROMOTE_ENABLED=true\`."
echo "To test once manually: workflow_dispatch with \`force=true\`."
} >> "$GITHUB_STEP_SUMMARY"
echo "::notice::auto-promote disabled — dry run only"
exit 0
fi

- name: Checkout main
if: ${{ vars.AUTO_PROMOTE_ENABLED == 'true' || github.event.inputs.force == 'true' }}
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Fast-forward main → staging HEAD
if: ${{ vars.AUTO_PROMOTE_ENABLED == 'true' || github.event.inputs.force == 'true' }}
env:
TARGET_SHA: ${{ needs.check-all-gates-green.outputs.head_sha }}
run: |
set -eu
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

git fetch origin staging
git fetch origin main

# Refuse to advance main if it's diverged from staging history.
# Someone landed a commit directly on main that's not on
# staging → human needs to decide how to reconcile.
if ! git merge-base --is-ancestor "$(git rev-parse origin/main)" "$TARGET_SHA"; then
{
echo "## ❌ Auto-promote refused — main has diverged"
echo
echo "\`main\` (\`$(git rev-parse --short origin/main)\`) is not an ancestor of staging (\`${TARGET_SHA:0:7}\`)."
echo "Someone committed directly to main or the histories forked."
echo
echo "Resolve manually: merge main into staging, get CI green on the merged commit,"
echo "then the auto-promote will succeed on the next run."
} >> "$GITHUB_STEP_SUMMARY"
exit 1
fi

# Fast-forward main to the target SHA.
git checkout main
git merge --ff-only "$TARGET_SHA"
git push origin main

{
echo "## ✅ Auto-promoted main → ${TARGET_SHA:0:7}"
echo
echo "All gate workflows green on staging at this SHA."
echo "\`main\` fast-forwarded to match."
} >> "$GITHUB_STEP_SUMMARY"
34 changes: 32 additions & 2 deletions .github/workflows/canary-verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
sha: ${{ steps.compute.outputs.sha }}
smoke_ran: ${{ steps.smoke.outputs.ran }}
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down Expand Up @@ -85,12 +86,38 @@ jobs:
echo "Timeout after ${MAX_WAIT}s — proceeding anyway (smoke suite will validate)"

- name: Run canary smoke suite
id: smoke
# Graceful-skip when no canary fleet is configured (Phase 2 not yet
# stood up — see molecule-controlplane/docs/canary-tenants.md).
# Sets `ran=false` on skip so promote-to-latest stays off (we don't
# want every main merge auto-promoting without gating). Manual
# promote-latest.yml is the release gate while canary is absent.
# Once the fleet is real: delete the early-exit branch.
env:
CANARY_TENANT_URLS: ${{ secrets.CANARY_TENANT_URLS }}
CANARY_ADMIN_TOKENS: ${{ secrets.CANARY_ADMIN_TOKENS }}
CANARY_CP_BASE_URL: https://staging-api.moleculesai.app
CANARY_CP_SHARED_SECRET: ${{ secrets.CANARY_CP_SHARED_SECRET }}
run: bash scripts/canary-smoke.sh
run: |
set -euo pipefail
if [ -z "${CANARY_TENANT_URLS:-}" ] \
|| [ -z "${CANARY_ADMIN_TOKENS:-}" ] \
|| [ -z "${CANARY_CP_SHARED_SECRET:-}" ]; then
{
echo "## ⚠️ canary-verify skipped"
echo
echo "One or more canary secrets are unset (\`CANARY_TENANT_URLS\`, \`CANARY_ADMIN_TOKENS\`, \`CANARY_CP_SHARED_SECRET\`)."
echo "Phase 2 canary fleet has not been stood up yet —"
echo "see [canary-tenants.md](https://github.com/Molecule-AI/molecule-controlplane/blob/main/docs/canary-tenants.md)."
echo
echo "**Skipped — promote-to-latest will NOT auto-fire.** Dispatch \`promote-latest.yml\` manually when ready."
} >> "$GITHUB_STEP_SUMMARY"
echo "ran=false" >> "$GITHUB_OUTPUT"
echo "::notice::canary-verify: skipped — no canary fleet configured"
exit 0
fi
bash scripts/canary-smoke.sh
echo "ran=true" >> "$GITHUB_OUTPUT"

- name: Summary on failure
if: ${{ failure() }}
Expand All @@ -109,8 +136,11 @@ jobs:
# On green, retag :staging-<sha> → :latest for BOTH images.
# crane is a lightweight registry client (no Docker daemon needed on
# the runner) that can retag remotely with a single API call each.
# Gated on smoke_ran=true — without a real canary fleet the smoke
# step no-ops with success, and we don't want that to silently
# auto-promote every main merge.
needs: canary-smoke
if: ${{ needs.canary-smoke.result == 'success' }}
if: ${{ needs.canary-smoke.result == 'success' && needs.canary-smoke.outputs.smoke_ran == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: imjasonh/setup-crane@v0.4
Expand Down
2 changes: 2 additions & 0 deletions canvas/src/components/ApprovalBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ export function ApprovalBanner() {
)}
<div className="flex gap-2 mt-3">
<button
type="button"
onClick={() => handleDecide(approval, "approved")}
className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-xs rounded-lg text-white font-medium transition-colors"
>
Approve
</button>
<button
type="button"
onClick={() => handleDecide(approval, "denied")}
className="px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-xs rounded-lg text-zinc-300 transition-colors"
>
Expand Down
17 changes: 5 additions & 12 deletions canvas/src/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCanvasStore, type WorkspaceNodeData } from "@/store/canvas";
import { api } from "@/lib/api";
import { showToast } from "./Toaster";
Expand All @@ -23,17 +23,9 @@
const setPanelTab = useCanvasStore((s) => s.setPanelTab);
const nestNode = useCanvasStore((s) => s.nestNode);
const contextNodeId = contextMenu?.nodeId ?? null;
// Select the full nodes array (stable reference across unrelated store
// updates) and derive children via useMemo. Filtering inside the
// selector returned a new array every call, which Zustand's
// useSyncExternalStore saw as "snapshot changed" → schedule
// re-render → loop → React error #185. See canvas-store-snapshots.
const nodes = useCanvasStore((s) => s.nodes);
const children = useMemo(
() => (contextNodeId ? nodes.filter((n) => n.data.parentId === contextNodeId) : []),
[nodes, contextNodeId],
const hasChildren = useCanvasStore((s) =>
contextNodeId ? s.nodes.some((n) => n.data.parentId === contextNodeId) : false
);
const hasChildren = children.length > 0;
const setPendingDelete = useCanvasStore((s) => s.setPendingDelete);
const ref = useRef<HTMLDivElement>(null);
const [actionLoading, setActionLoading] = useState(false);
Expand Down Expand Up @@ -174,7 +166,8 @@
// it survives ContextMenu unmount. Closing the menu here avoids the
// prior race where the portal dialog's Confirm click was treated as
// "outside" by the menu's outside-click handler.
setPendingDelete({ id: contextMenu.nodeId, name: contextMenu.nodeData.name, hasChildren, children: children.map(c => ({ id: c.id, name: c.data.name })) });
const childNodes = useCanvasStore.getState().nodes.filter((n) => n.data.parentId === contextMenu.nodeId);

Check failure on line 169 in canvas/src/components/ContextMenu.tsx

View workflow job for this annotation

GitHub Actions / Canvas (Next.js)

Unhandled error

TypeError: useCanvasStore.getState is not a function ❯ src/components/ContextMenu.tsx:169:39 ❯ executeDispatch node_modules/react-dom/cjs/react-dom-client.development.js:19116:9 ❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13 ❯ processDispatchQueue node_modules/react-dom/cjs/react-dom-client.development.js:19166:19 ❯ node_modules/react-dom/cjs/react-dom-client.development.js:19767:9 ❯ batchedUpdates$1 node_modules/react-dom/cjs/react-dom-client.development.js:3255:40 ❯ dispatchEventForPluginEventSystem node_modules/react-dom/cjs/react-dom-client.development.js:19320:7 ❯ dispatchEvent node_modules/react-dom/cjs/react-dom-client.development.js:23585:11 ❯ dispatchDiscreteEvent node_modules/react-dom/cjs/react-dom-client.development.js:23553:11 ❯ HTMLDivElement.callTheUserObjectsOperation node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30 This error originated in "src/components/__tests__/ContextMenu.keyboard.test.tsx" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "clicking 'Delete' hoists state to the store and closes the menu". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 169 in canvas/src/components/ContextMenu.tsx

View workflow job for this annotation

GitHub Actions / Canvas (Next.js)

Unhandled error

TypeError: useCanvasStore.getState is not a function ❯ src/components/ContextMenu.tsx:169:39 ❯ executeDispatch node_modules/react-dom/cjs/react-dom-client.development.js:19116:9 ❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13 ❯ processDispatchQueue node_modules/react-dom/cjs/react-dom-client.development.js:19166:19 ❯ node_modules/react-dom/cjs/react-dom-client.development.js:19767:9 ❯ batchedUpdates$1 node_modules/react-dom/cjs/react-dom-client.development.js:3255:40 ❯ dispatchEventForPluginEventSystem node_modules/react-dom/cjs/react-dom-client.development.js:19320:7 ❯ dispatchEvent node_modules/react-dom/cjs/react-dom-client.development.js:23585:11 ❯ dispatchDiscreteEvent node_modules/react-dom/cjs/react-dom-client.development.js:23553:11 ❯ HTMLDivElement.callTheUserObjectsOperation node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30 This error originated in "src/components/__tests__/ContextMenu.keyboard.test.tsx" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "clicking 'Delete' hoists state to the store and closes the menu". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.
setPendingDelete({ id: contextMenu.nodeId, name: contextMenu.nodeData.name, hasChildren, children: childNodes.map(c => ({ id: c.id, name: c.data.name })) });
closeContextMenu();
}, [contextMenu, setPendingDelete, closeContextMenu]);

Expand Down
Loading
Loading