Merge pull request #2812 from Molecule-AI/staging

staging → main: auto-promote 9c9be4c
This commit is contained in:
molecule-ai[bot] 2026-05-05 01:55:48 +00:00 committed by GitHub
commit 31f9a5e85e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 221 additions and 46 deletions

View File

@ -295,12 +295,16 @@ jobs:
# See molecule-controlplane#420.
leaks=()
for slug in $orgs; do
code=$(curl -sS -o /tmp/canary-cleanup.out -w "%{http_code}" \
# Tempfile-routed -w + set +e/-e prevents curl-exit-code
# pollution of the captured status (lint-curl-status-capture.yml).
set +e
curl -sS -o /tmp/canary-cleanup.out -w "%{http_code}" \
-X DELETE "$MOLECULE_CP_URL/cp/admin/tenants/$slug" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"confirm\":\"$slug\"}" \
|| echo "000")
-d "{\"confirm\":\"$slug\"}" >/tmp/canary-cleanup.code 2>/dev/null
set -e
code=$(cat /tmp/canary-cleanup.code 2>/dev/null || echo "000")
if [ "$code" = "200" ] || [ "$code" = "204" ]; then
echo "[teardown] deleted $slug (HTTP $code)"
else

View File

@ -192,12 +192,16 @@ jobs:
# cleanup miss shouldn't fail-flag the canvas test when the
# actual smoke check passed; the sweeper is the safety net.
# See molecule-controlplane#420.
code=$(curl -sS -o /tmp/canvas-cleanup.out -w "%{http_code}" \
# Tempfile-routed -w + set +e/-e prevents curl-exit-code
# pollution of the captured status (lint-curl-status-capture.yml).
set +e
curl -sS -o /tmp/canvas-cleanup.out -w "%{http_code}" \
-X DELETE "$MOLECULE_CP_URL/cp/admin/tenants/$slug" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"confirm\":\"$slug\"}" \
|| echo "000")
-d "{\"confirm\":\"$slug\"}" >/tmp/canvas-cleanup.code 2>/dev/null
set -e
code=$(cat /tmp/canvas-cleanup.code 2>/dev/null || echo "000")
if [ "$code" = "200" ] || [ "$code" = "204" ]; then
echo "[teardown] deleted $slug (HTTP $code)"
else

View File

@ -159,12 +159,16 @@ jobs:
# leaked. Sweeper catches the rest within ~45 min.
leaks=()
for slug in $orgs; do
code=$(curl -sS -o /tmp/external-cleanup.out -w "%{http_code}" \
# Tempfile-routed -w + set +e/-e prevents curl-exit-code
# pollution of the captured status (lint-curl-status-capture.yml).
set +e
curl -sS -o /tmp/external-cleanup.out -w "%{http_code}" \
-X DELETE "$MOLECULE_CP_URL/cp/admin/tenants/$slug" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"confirm\":\"$slug\"}" \
|| echo "000")
-d "{\"confirm\":\"$slug\"}" >/tmp/external-cleanup.code 2>/dev/null
set -e
code=$(cat /tmp/external-cleanup.code 2>/dev/null || echo "000")
if [ "$code" = "200" ] || [ "$code" = "204" ]; then
echo "[teardown] deleted $slug (HTTP $code)"
else

View File

@ -224,12 +224,16 @@ jobs:
leaks=()
for slug in $orgs; do
echo "Safety-net teardown: $slug"
code=$(curl -sS -o /tmp/saas-cleanup.out -w "%{http_code}" \
# Tempfile-routed -w + set +e/-e prevents curl-exit-code
# pollution of the captured status (lint-curl-status-capture.yml).
set +e
curl -sS -o /tmp/saas-cleanup.out -w "%{http_code}" \
-X DELETE "$MOLECULE_CP_URL/cp/admin/tenants/$slug" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"confirm\":\"$slug\"}" \
|| echo "000")
-d "{\"confirm\":\"$slug\"}" >/tmp/saas-cleanup.code 2>/dev/null
set -e
code=$(cat /tmp/saas-cleanup.code 2>/dev/null || echo "000")
if [ "$code" = "200" ] || [ "$code" = "204" ]; then
echo "[teardown] deleted $slug (HTTP $code)"
else

View File

@ -148,12 +148,16 @@ jobs:
# safety net within ~45 min.
leaks=()
for slug in $orgs; do
code=$(curl -sS -o /tmp/sanity-cleanup.out -w "%{http_code}" \
# Tempfile-routed -w + set +e/-e prevents curl-exit-code
# pollution of the captured status (lint-curl-status-capture.yml).
set +e
curl -sS -o /tmp/sanity-cleanup.out -w "%{http_code}" \
-X DELETE "$MOLECULE_CP_URL/cp/admin/tenants/$slug" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"confirm\":\"$slug\"}" \
|| echo "000")
-d "{\"confirm\":\"$slug\"}" >/tmp/sanity-cleanup.code 2>/dev/null
set -e
code=$(cat /tmp/sanity-cleanup.code 2>/dev/null || echo "000")
if [ "$code" = "200" ] || [ "$code" = "204" ]; then
echo "[teardown] deleted $slug (HTTP $code)"
else

View File

@ -0,0 +1,94 @@
name: Lint curl status-code capture
# Pins the workflow-bash anti-pattern that produced "HTTP 000000" on the
# 2026-05-04 redeploy-tenants-on-main run for sha 2b862f6:
#
# HTTP_CODE=$(curl ... -w '%{http_code}' ... || echo "000")
#
# When curl exits non-zero (connection reset → 56, --fail-with-body 4xx/5xx
# → 22), the `-w '%{http_code}'` already wrote a status to stdout — usually
# "000" for connection failures or the actual code for HTTP errors. The
# `|| echo "000"` then fires AND appends ANOTHER "000" to the captured
# stdout, producing values like "000000" or "409000" that fail string
# comparisons against "200" while looking superficially right.
#
# Same class of bug the synth-E2E §7c gate hit twice (PRs #2779/#2783 +
# #2797). Memory: feedback_curl_status_capture_pollution.md.
#
# Fix shape (route -w into a tempfile so curl's exit code can't pollute):
#
# set +e
# curl ... -w '%{http_code}' >code.txt 2>/dev/null
# set -e
# HTTP_CODE=$(cat code.txt 2>/dev/null)
# [ -z "$HTTP_CODE" ] && HTTP_CODE="000"
on:
pull_request:
paths: ['.github/workflows/**']
push:
branches: [main, staging]
paths: ['.github/workflows/**']
merge_group:
types: [checks_requested]
jobs:
scan:
name: Scan workflows for curl status-capture pollution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Find curl ... -w '%{http_code}' ... || echo "000" subshells
run: |
set -uo pipefail
# Multi-line aware: look for `$(curl ... -w '%{http_code}' ... || echo "000")`
# subshell where the entire command-substitution wraps a curl that
# ends with `|| echo "000"`. Must distinguish from the SAFE shape
# `$(cat tempfile 2>/dev/null || echo "000")` — `cat` with a missing
# tempfile produces empty stdout, no pollution.
python3 <<'PY'
import os, re, sys, glob
BAD_FILES = []
# Match the buggy substitution across newlines: $(curl ... -w '%{http_code}' ... || echo "000")
# The `\\n` is the bash line-continuation that lets curl flags span lines.
# We collapse continuation lines first, then look for the single-line bad pattern.
PATTERN = re.compile(
r'\$\(\s*curl\b[^)]*-w\s*[\'"]%\{http_code\}[\'"][^)]*\|\|\s*echo\s+"000"\s*\)',
re.DOTALL,
)
# Self-skip: this lint workflow contains the literal anti-pattern in
# its own docstring — that's intentional, not a bug.
SELF = ".github/workflows/lint-curl-status-capture.yml"
for f in sorted(glob.glob(".github/workflows/*.yml")):
if f == SELF:
continue
with open(f) as fh:
content = fh.read()
# Collapse bash line-continuations (\\\n + leading whitespace)
# into a single logical line so the regex can see the full
# curl invocation as one chunk.
flat = re.sub(r'\\\s*\n\s*', ' ', content)
for m in PATTERN.finditer(flat):
BAD_FILES.append((f, m.group(0)[:120]))
if not BAD_FILES:
print("✓ No curl-status-capture pollution patterns detected")
sys.exit(0)
print(f"::error::Found {len(BAD_FILES)} curl-status-capture pollution site(s):")
for f, snippet in BAD_FILES:
print(f"::error file={f}::Curl status-capture pollution: '|| echo \"000\"' inside a $(curl ... -w '%{{http_code}}' ...) subshell. On non-2xx or connection failure, curl's -w writes a status, then exits non-zero, then the || echo appends another '000' — producing 'HTTP 000000' or '409000' that fails comparisons silently. Fix: route -w into a tempfile so the exit code can't pollute stdout. See memory feedback_curl_status_capture_pollution.md.")
print(f" matched: {snippet}…")
print()
print("Fix template:")
print(' set +e')
print(' curl ... -w \'%{http_code}\' >code.txt 2>/dev/null')
print(' set -e')
print(' HTTP_CODE=$(cat code.txt 2>/dev/null)')
print(' [ -z "$HTTP_CODE" ] && HTTP_CODE="000"')
sys.exit(1)
PY

View File

@ -184,12 +184,26 @@ jobs:
echo " body: $BODY"
HTTP_RESPONSE=$(mktemp)
HTTP_CODE=$(curl -sS -o "$HTTP_RESPONSE" -w '%{http_code}' \
HTTP_CODE_FILE=$(mktemp)
# Route -w into its own tempfile so curl's exit code (e.g. 56
# on connection-reset, 22 on --fail-with-body 4xx/5xx) can't
# pollute the captured stdout. The previous inline-substitution
# shape produced "000000" on connection reset (curl wrote
# "000" via -w, then the inline echo-fallback appended another
# "000") — caught on the 2026-05-04 redeploy of sha 2b862f6.
# set +e/-e keeps the non-zero curl exit from tripping the
# outer pipeline. See lint-curl-status-capture.yml for the
# CI gate that pins this fix shape.
set +e
curl -sS -o "$HTTP_RESPONSE" -w '%{http_code}' \
-m 1200 \
-H "Authorization: Bearer $CP_ADMIN_API_TOKEN" \
-H "Content-Type: application/json" \
-X POST "$CP_URL/cp/admin/tenants/redeploy-fleet" \
-d "$BODY" || echo "000")
-d "$BODY" >"$HTTP_CODE_FILE" 2>/dev/null
set -e
HTTP_CODE=$(cat "$HTTP_CODE_FILE" 2>/dev/null || echo "000")
[ -z "$HTTP_CODE" ] && HTTP_CODE="000"
echo "HTTP $HTTP_CODE"
cat "$HTTP_RESPONSE" | jq . || cat "$HTTP_RESPONSE"

View File

@ -146,12 +146,24 @@ jobs:
echo " body: $BODY"
HTTP_RESPONSE=$(mktemp)
HTTP_CODE=$(curl -sS -o "$HTTP_RESPONSE" -w '%{http_code}' \
HTTP_CODE_FILE=$(mktemp)
# Route -w into its own tempfile so curl's exit code (e.g. 56
# on connection-reset) can't pollute the captured stdout. The
# previous inline-substitution shape produced "000000" on
# connection reset — caught on main variant 2026-05-04
# redeploying sha 2b862f6. Same fix shape as the synth-E2E
# §9c gate (PR #2797). See lint-curl-status-capture.yml for
# the CI gate that pins this fix shape.
set +e
curl -sS -o "$HTTP_RESPONSE" -w '%{http_code}' \
-m 1200 \
-H "Authorization: Bearer $CP_STAGING_ADMIN_API_TOKEN" \
-H "Content-Type: application/json" \
-X POST "$CP_URL/cp/admin/tenants/redeploy-fleet" \
-d "$BODY" || echo "000")
-d "$BODY" >"$HTTP_CODE_FILE" 2>/dev/null
set -e
HTTP_CODE=$(cat "$HTTP_CODE_FILE" 2>/dev/null || echo "000")
[ -z "$HTTP_CODE" ] && HTTP_CODE="000"
echo "HTTP $HTTP_CODE"
cat "$HTTP_RESPONSE" | jq . || cat "$HTTP_RESPONSE"

View File

@ -159,12 +159,17 @@ jobs:
# The DELETE handler requires {"confirm": "<slug>"} matching
# the URL slug — fat-finger guard. Idempotent: re-issuing
# picks up via org_purges.last_step.
http_code=$(curl -sS -o /tmp/del_resp -w "%{http_code}" \
# Tempfile-routed -w + set +e/-e prevents curl-exit-code
# pollution of the captured status (lint-curl-status-capture.yml).
set +e
curl -sS -o /tmp/del_resp -w "%{http_code}" \
--max-time 60 \
-X DELETE "$MOLECULE_CP_URL/cp/admin/tenants/$slug" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"confirm\":\"$slug\"}" || echo "000")
-d "{\"confirm\":\"$slug\"}" >/tmp/del_code 2>/dev/null
set -e
http_code=$(cat /tmp/del_code 2>/dev/null || echo "000")
if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then
deleted=$((deleted+1))
echo " deleted: $slug"

View File

@ -132,6 +132,11 @@ const TAB_HELP: Record<
check:
"TOML rejects duplicate `[mcp_servers.molecule]` tables. Open ~/.codex/config.toml and remove the old block before pasting the new one.",
},
{
symptom: "Canvas messages don't wake codex",
check:
"Step 3 (codex-channel-molecule bridge daemon) is required for inbound push. Check `pgrep -f codex-channel-molecule` and `tail ~/.codex-channel-molecule/daemon.log`.",
},
],
},
openclaw: {

View File

@ -289,35 +289,36 @@ hermes gateway --replace
// externalCodexTemplate — for operators whose external agent is a
// codex CLI (@openai/codex) session. Wires the molecule_runtime A2A
// MCP server into codex's config.toml so the agent can call
// list_peers / delegate_task / send_message_to_user / commit_memory.
// list_peers / delegate_task / send_message_to_user / commit_memory,
// AND surfaces the codex-channel-molecule bridge daemon for inbound
// push parity.
//
// Push parity caveat: codex's MCP client doesn't forward arbitrary
// notifications/* from configured MCP servers (verified by reading
// codex-rs/codex-mcp/src/connection_manager.rs in openai/codex). So
// this snippet gives outbound tools but NOT mid-turn push from
// inbound A2A. For full push parity on a codex external, the
// equivalent of hermes-channel-molecule would be needed — a bridge
// daemon that long-polls the platform inbox and calls codex's
// turn/steer RPC. Tracked separately; this snippet is the
// outbound-tool-only first cut.
const externalCodexTemplate = `# Codex MCP config outbound tool path. For operators whose external
# agent is a codex CLI (@openai/codex) session.
#
# This wires the molecule platform's A2A MCP server into codex so
# the agent can call list_peers / delegate_task / send_message_to_user
# / commit_memory. Inbound A2A (canvas messages, peer-initiated tasks)
# does NOT push into the running codex turn yet codex's MCP runtime
# doesn't route arbitrary notifications/* from configured MCP servers.
# For inbound delivery into a codex session, pair with the Python SDK
# tab for now.
// Push parity:
// - Outbound (codex calls platform tools) — works via the wired
// MCP server (step 2 below).
// - Inbound (canvas messages and peer-initiated tasks wake the
// codex agent) — works via codex-channel-molecule (step 3),
// which long-polls the platform inbox and runs `codex exec
// --resume <session>` per inbound message. Each turn is a fresh
// subprocess but per-thread session continuity is preserved on
// disk so conversation context survives.
//
// Long-term: when openai/codex#17543 lands (codex MCP runtime routes
// inbound notifications/* into the active session as Op::UserInput),
// the bridge daemon becomes redundant — the wired MCP server in
// step 2 will deliver push natively. Until then, run both.
const externalCodexTemplate = `# Codex external setup outbound tools (MCP) + inbound push (bridge).
# For operators whose external agent is a codex CLI (@openai/codex)
# session.
# 1. Install codex CLI + the workspace runtime wheel:
# 1. Install codex CLI, the workspace runtime, and the bridge daemon:
npm install -g @openai/codex@^0.57
pip install molecule-ai-workspace-runtime
pip install 'git+https://github.com/Molecule-AI/codex-channel-molecule.git'
# 2. Edit ~/.codex/config.toml and add the block below. {{PLATFORM_URL}}
# and {{WORKSPACE_ID}} are stamped server-side; paste your auth
# token for MOLECULE_WORKSPACE_TOKEN before saving.
# 2. Wire the molecule MCP server into codex's config.toml this is
# the OUTBOUND path (codex calls list_peers / delegate_task /
# send_message_to_user / commit_memory).
#
# Don't append blindly TOML rejects duplicate
# [mcp_servers.molecule] tables, so re-running on an existing
@ -338,7 +339,31 @@ mkdir -p ~/.codex
# PLATFORM_URL = "{{PLATFORM_URL}}"
# MOLECULE_WORKSPACE_TOKEN = "<paste from create response>"
# 3. Run codex the molecule tools are now available to the agent:
# 3. Run the bridge daemon as a durable background process this
# is the INBOUND path. Long-polls the platform inbox and runs
# "codex exec --resume <session>" per inbound canvas/peer message,
# routes the assistant reply back via send_message_to_user /
# delegate_task. Per-thread session continuity persisted to
# ~/.codex-channel-molecule/sessions.json so conversation context
# survives daemon restarts.
#
# Same env-var contract as the MCP server above.
#
# Without this daemon, codex still works for outbound calls but
# canvas messages won't wake an idle session codex's MCP runtime
# doesn't yet route notifications/* into the chat loop (tracked
# upstream at openai/codex#17543; when that lands, the bridge
# becomes redundant).
WORKSPACE_ID="{{WORKSPACE_ID}}" \
PLATFORM_URL="{{PLATFORM_URL}}" \
MOLECULE_WORKSPACE_TOKEN="<paste from create response>" \
nohup codex-channel-molecule > ~/.codex-channel-molecule/daemon.log 2>&1 &
disown
# 4. Run codex itself for interactive use molecule tools are
# available to the agent, and the bridge wakes a non-interactive
# codex turn for any inbound canvas/peer message:
codex
`