forked from molecule-ai/molecule-core
Merge pull request #2812 from Molecule-AI/staging
staging → main: auto-promote 9c9be4c
This commit is contained in:
commit
31f9a5e85e
10
.github/workflows/canary-staging.yml
vendored
10
.github/workflows/canary-staging.yml
vendored
@ -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
|
||||
|
||||
10
.github/workflows/e2e-staging-canvas.yml
vendored
10
.github/workflows/e2e-staging-canvas.yml
vendored
@ -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
|
||||
|
||||
10
.github/workflows/e2e-staging-external.yml
vendored
10
.github/workflows/e2e-staging-external.yml
vendored
@ -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
|
||||
|
||||
10
.github/workflows/e2e-staging-saas.yml
vendored
10
.github/workflows/e2e-staging-saas.yml
vendored
@ -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
|
||||
|
||||
10
.github/workflows/e2e-staging-sanity.yml
vendored
10
.github/workflows/e2e-staging-sanity.yml
vendored
@ -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
|
||||
|
||||
94
.github/workflows/lint-curl-status-capture.yml
vendored
Normal file
94
.github/workflows/lint-curl-status-capture.yml
vendored
Normal 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
|
||||
18
.github/workflows/redeploy-tenants-on-main.yml
vendored
18
.github/workflows/redeploy-tenants-on-main.yml
vendored
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
9
.github/workflows/sweep-stale-e2e-orgs.yml
vendored
9
.github/workflows/sweep-stale-e2e-orgs.yml
vendored
@ -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"
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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
|
||||
`
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user