Compare commits

..

9 Commits

Author SHA1 Message Date
hongming 738e54593c Merge pull request 'fix(platform): install docker-cli in workspace-server image — unblocks RegistryModeLocal' (#765) from infra/dockerfile-add-docker-cli-for-local-build into main
Block internal-flavored paths / Block forbidden paths (push) Successful in 9s
Harness Replays / detect-changes (push) Successful in 9s
CI / Detect changes (push) Successful in 23s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 25s
E2E API Smoke Test / detect-changes (push) Successful in 25s
Handlers Postgres Integration / detect-changes (push) Successful in 25s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 14s
Runtime PR-Built Compatibility / detect-changes (push) Successful in 34s
publish-workspace-server-image / build-and-push (push) Successful in 6m11s
Harness Replays / Harness Replays (push) Successful in 7s
CI / Shellcheck (E2E scripts) (push) Successful in 6s
CI / Canvas (Next.js) (push) Successful in 9s
CI / Python Lint & Test (push) Successful in 7s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 9s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 6s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (push) Successful in 9s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 1m40s
CI / Platform (Go) (push) Failing after 4m44s
CI / Canvas Deploy Reminder (push) Has been skipped
CI / all-required (push) Successful in 6s
SECRET_PATTERNS drift lint / Detect SECRET_PATTERNS drift (push) Successful in 48s
ci-required-drift / drift (push) Successful in 1m44s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Compensated by status-reaper (workflow has no push: trigger; Gitea 1.22.6 hardcoded-suffix bug — see .gitea/scripts/status-reaper.py)
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Compensated by status-reaper (workflow has no push: trigger; Gitea 1.22.6 hardcoded-suffix bug — see .gitea/scripts/status-reaper.py)
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 4s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 7s
main-red-watchdog / watchdog (push) Successful in 1m9s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Compensated by status-reaper (workflow has no push: trigger; Gitea 1.22.6 hardcoded-suffix bug — see .gitea/scripts/status-reaper.py)
gate-check-v3 / gate-check (push) Compensated by status-reaper (workflow has no push: trigger; Gitea 1.22.6 hardcoded-suffix bug — see .gitea/scripts/status-reaper.py)
status-reaper / reap (push) Successful in 1m30s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Compensated by status-reaper (workflow has no push: trigger; Gitea 1.22.6 hardcoded-suffix bug — see .gitea/scripts/status-reaper.py)
2026-05-13 04:39:13 +00:00
hongming b331747f1c Merge pull request 'fix(ci): fail loud on platform Go vet and lint' (#781) from harden/platform-go-lint-fail-loud into main
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (push) Waiting to run
CI / Platform (Go) (push) Blocked by required conditions
CI / Canvas (Next.js) (push) Blocked by required conditions
CI / Shellcheck (E2E scripts) (push) Blocked by required conditions
CI / Canvas Deploy Reminder (push) Blocked by required conditions
CI / Python Lint & Test (push) Blocked by required conditions
CI / all-required (push) Blocked by required conditions
E2E API Smoke Test / detect-changes (push) Waiting to run
E2E API Smoke Test / E2E API Smoke Test (push) Blocked by required conditions
E2E Staging Canvas (Playwright) / detect-changes (push) Waiting to run
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Blocked by required conditions
Handlers Postgres Integration / detect-changes (push) Waiting to run
Handlers Postgres Integration / Handlers Postgres Integration (push) Blocked by required conditions
Runtime PR-Built Compatibility / detect-changes (push) Waiting to run
Runtime PR-Built Compatibility / PR-built wheel + import smoke (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
Lint curl status-code capture / Scan workflows for curl status-capture pollution (push) Successful in 7s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (push) Successful in 1m29s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (push) Successful in 1m48s
2026-05-13 04:31:51 +00:00
hongming 03e7a2d8a5 Merge pull request 'test(handlers): drain preflight restart goroutine' (#780) from fix/core-main-red-race-20260512 into main
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (push) Waiting to run
CI / Platform (Go) (push) Blocked by required conditions
CI / Canvas (Next.js) (push) Blocked by required conditions
CI / Shellcheck (E2E scripts) (push) Blocked by required conditions
CI / Canvas Deploy Reminder (push) Blocked by required conditions
CI / Python Lint & Test (push) Blocked by required conditions
CI / all-required (push) Blocked by required conditions
E2E API Smoke Test / detect-changes (push) Waiting to run
E2E API Smoke Test / E2E API Smoke Test (push) Blocked by required conditions
E2E Staging Canvas (Playwright) / detect-changes (push) Waiting to run
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Blocked by required conditions
Handlers Postgres Integration / detect-changes (push) Waiting to run
Handlers Postgres Integration / Handlers Postgres Integration (push) Blocked by required conditions
Harness Replays / Harness Replays (push) Blocked by required conditions
Runtime PR-Built Compatibility / detect-changes (push) Waiting to run
Runtime PR-Built Compatibility / PR-built wheel + import smoke (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
Harness Replays / detect-changes (push) Successful in 16s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Has started running
publish-workspace-server-image / build-and-push (push) Has been cancelled
2026-05-13 04:30:05 +00:00
hongming f3b01ceefb Merge pull request 'test curl status capture workflow lint' (#764) from chore/curl-status-lint-script into main
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (push) Waiting to run
CI / Platform (Go) (push) Blocked by required conditions
CI / Canvas (Next.js) (push) Blocked by required conditions
CI / Shellcheck (E2E scripts) (push) Blocked by required conditions
CI / Canvas Deploy Reminder (push) Blocked by required conditions
CI / Python Lint & Test (push) Blocked by required conditions
CI / all-required (push) Blocked by required conditions
E2E API Smoke Test / detect-changes (push) Waiting to run
E2E API Smoke Test / E2E API Smoke Test (push) Blocked by required conditions
E2E Staging Canvas (Playwright) / detect-changes (push) Waiting to run
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Blocked by required conditions
Handlers Postgres Integration / detect-changes (push) Waiting to run
Handlers Postgres Integration / Handlers Postgres Integration (push) Blocked by required conditions
lint-continue-on-error-tracking / lint-continue-on-error-tracking (push) Waiting to run
Lint curl status-code capture / Scan workflows for curl status-capture pollution (push) Waiting to run
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (push) Waiting to run
Runtime PR-Built Compatibility / detect-changes (push) Waiting to run
Runtime PR-Built Compatibility / PR-built wheel + import smoke (push) Blocked by required conditions
Secret scan / Scan diff for credential-shaped strings (push) Waiting to run
status-reaper / reap (push) Has started running
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 8s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Compensated by status-reaper (workflow has no push: trigger; Gitea 1.22.6 hardcoded-suffix bug — see .gitea/scripts/status-reaper.py)
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Compensated by status-reaper (workflow has no push: trigger; Gitea 1.22.6 hardcoded-suffix bug — see .gitea/scripts/status-reaper.py)
2026-05-13 04:29:41 +00:00
hongming-codex-laptop eee83dfb94 fix(ci): fail loud on platform Go vet and lint
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 3s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 7s
CI / Detect changes (pull_request) Successful in 11s
E2E API Smoke Test / detect-changes (pull_request) Successful in 16s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 16s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 11s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 22s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 21s
qa-review / approved (pull_request) Failing after 10s
gate-check-v3 / gate-check (pull_request) Successful in 13s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: 7
security-review / approved (pull_request) Failing after 9s
sop-checklist-gate / gate (pull_request) Successful in 11s
sop-tier-check / tier-check (pull_request) Successful in 9s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 9s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 3s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 3s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 2s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m13s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m22s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m22s
lint-mask-pr-atomicity / lint-mask-pr-atomicity (pull_request) Successful in 1m34s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m35s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m43s
CI / Platform (Go) (pull_request) Failing after 2m2s
CI / Canvas (Next.js) (pull_request) Successful in 5m18s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Python Lint & Test (pull_request) Successful in 6m39s
CI / all-required (pull_request) Successful in 1s
audit-force-merge / audit (pull_request) Successful in 18s
2026-05-12 21:14:03 -07:00
hongming-codex-laptop 381c710f8a test(handlers): drain preflight restart goroutine
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 6s
Harness Replays / detect-changes (pull_request) Successful in 9s
CI / Detect changes (pull_request) Successful in 20s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 11s
E2E API Smoke Test / detect-changes (pull_request) Successful in 27s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 28s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 29s
qa-review / approved (pull_request) Failing after 14s
security-review / approved (pull_request) Failing after 12s
Harness Replays / Harness Replays (pull_request) Successful in 5s
CI / Canvas (Next.js) (pull_request) Successful in 7s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 29s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 6s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Python Lint & Test (pull_request) Successful in 7s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 7s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 4s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m9s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m23s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4
sop-tier-check / tier-check (pull_request) Successful in 11s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 3m9s
sop-checklist-gate / gate (pull_request) Successful in 11s
gate-check-v3 / gate-check (pull_request) Successful in 13s
CI / Platform (Go) (pull_request) Successful in 5m11s
CI / all-required (pull_request) Successful in 1s
audit-force-merge / audit (pull_request) Successful in 22s
2026-05-12 21:07:40 -07:00
hongming 06af0bbeb3 Merge pull request 'test(handlers/a2a_proxy_helpers): add a2a_proxy_helpers_test.go — 20 cases for pure helpers' (#700) from feat/a2a-proxy-helpers-test-coverage into main
Block internal-flavored paths / Block forbidden paths (push) Successful in 3s
Harness Replays / detect-changes (push) Successful in 6s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 6s
CI / Detect changes (push) Successful in 14s
Harness Replays / Harness Replays (push) Successful in 3s
E2E API Smoke Test / detect-changes (push) Successful in 14s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 14s
Handlers Postgres Integration / detect-changes (push) Successful in 14s
Runtime PR-Built Compatibility / detect-changes (push) Successful in 15s
CI / Shellcheck (E2E scripts) (push) Successful in 2s
CI / Canvas (Next.js) (push) Successful in 2s
CI / Python Lint & Test (push) Successful in 3s
CI / Canvas Deploy Reminder (push) Has been skipped
Runtime PR-Built Compatibility / PR-built wheel + import smoke (push) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 4s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 1m10s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 2m23s
gate-check-v3 / gate-check (push) Successful in 1m14s
publish-workspace-server-image / build-and-push (push) Successful in 5m28s
CI / Platform (Go) (push) Successful in 6m34s
CI / all-required (push) Successful in 1s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 4s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Compensated by status-reaper (workflow has no push: trigger; Gitea 1.22.6 hardcoded-suffix bug — see .gitea/scripts/status-reaper.py)
ci-required-drift / drift (push) Successful in 1m0s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Compensated by status-reaper (workflow has no push: trigger; Gitea 1.22.6 hardcoded-suffix bug — see .gitea/scripts/status-reaper.py)
status-reaper / reap (push) Successful in 1m53s
2026-05-13 04:05:47 +00:00
hongming b8ccd21c8c fix(platform): install docker-cli in workspace-server image — unblocks RegistryModeLocal
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 18s
CI / Detect changes (pull_request) Successful in 17s
E2E API Smoke Test / detect-changes (pull_request) Successful in 18s
Harness Replays / detect-changes (pull_request) Successful in 13s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 22s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 21s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 13s
qa-review / approved (pull_request) Failing after 13s
security-review / approved (pull_request) Failing after 14s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 25s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m24s
CI / Platform (Go) (pull_request) Has been skipped
CI / Canvas (Next.js) (pull_request) Has been skipped
CI / Shellcheck (E2E scripts) (pull_request) Has been skipped
E2E API Smoke Test / E2E API Smoke Test (pull_request) Has been skipped
CI / Python Lint & Test (pull_request) Has been skipped
Harness Replays / Harness Replays (pull_request) Has been skipped
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Has been skipped
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Has been skipped
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Has been skipped
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 1s
sop-checklist-gate / gate (pull_request) Successful in 37s
gate-check-v3 / gate-check (pull_request) Successful in 38s
sop-tier-check / tier-check (pull_request) Successful in 37s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4
audit-force-merge / audit (pull_request) Successful in 8s
The platform server's internal/provisioner/localbuild.go (Task #194 /
Issue #63 — the post-2026-05-06 GHCR-suspension fallback) shells out
via exec.Command("docker", "image", "inspect"/"build"/"tag", ...) in
the production dockerHasTagProd / dockerBuildProd / dockerTagProd
functions. The colocated workspace-server/Dockerfile installed
`ca-certificates git tzdata wget` in the alpine runtime layer but NOT
`docker-cli`, so every workspace re-provision in the now-permanent
RegistryModeLocal path fails at step 2 (cache check):

  local-build: image inspect for
    molecule-local/workspace-template-claude-code:<sha> failed
    (exec: "docker": executable file not found in $PATH); will rebuild
  Provisioner: workspace start failed for <id>: local-build mode:
    ensure image for runtime "claude-code": local-build:
    docker build molecule-local/workspace-template-claude-code:<sha>:
    exec: "docker": executable file not found in $PATH

Net: ANY ws-* container that dies (auto-restart on container-dead, the
liveness-monitor RestartByID, plugin auto-restart, secrets-set
auto-restart, manual POST /workspaces/:id/restart) cannot come back
up. Already took down CP-QA (ec6cf05b) and sdk-lead (360d42e4); also
blocks the MiniMax LLM-provider switch for the 6 *-lead workspaces
(which requires postgres UPDATE workspace_secrets + POST /restart to
re-bake the env from the updated secrets).

The Docker SOCKET is already mounted into the platform container —
the entrypoint.sh adds the platform user to the docker group derived
from the socket's gid. Only the CLI binary was missing.

Per `registry_mode.go:Resolve()`, MOLECULE_IMAGE_REGISTRY is the
toggle: set ⇒ RegistryModeSaaS pull from a real registry; unset ⇒
RegistryModeLocal clone+build from Gitea. Since 2026-05-06 the env
var has been unset (GHCR was the only SaaS-mode target and it's
unreachable post-suspension), so RegistryModeLocal is the permanent
mode until internal#231 (GHCR→ECR migration) lands. This Dockerfile
needs to support the mode the code is permanently in.

Diff is +16/-1 (mostly comment explaining why). The single
behavioural change: `docker-cli` added to the apk-add line.

Verification: post-deploy, `POST /workspaces/360d42e4-…/restart` (the
known-failed sdk-lead) should succeed and bring the workspace back
up with its current Claude-Opus secrets — that's the first confirmation
the local-build path is unblocked. Then the MiniMax switch can proceed
(postgres UPDATE on each *-lead's workspace_secrets + POST /restart).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:13:55 -07:00
hongming-kimi-laptop 290773ecbc test curl status capture workflow lint
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 5s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 10s
CI / Detect changes (pull_request) Successful in 21s
E2E API Smoke Test / detect-changes (pull_request) Successful in 21s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 22s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 23s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 18s
qa-review / approved (pull_request) Failing after 20s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: 7
security-review / approved (pull_request) Failing after 17s
sop-checklist-gate / gate (pull_request) Successful in 17s
gate-check-v3 / gate-check (pull_request) Successful in 30s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 37s
CI / Platform (Go) (pull_request) Successful in 8s
sop-tier-check / tier-check (pull_request) Successful in 13s
CI / Canvas (Next.js) (pull_request) Successful in 8s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 5s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Python Lint & Test (pull_request) Successful in 5s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 5s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 5s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 5s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 4s
CI / all-required (pull_request) Successful in 3s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Failing after 1m15s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m19s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m35s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m28s
audit-force-merge / audit (pull_request) Successful in 17s
2026-05-12 13:40:31 -07:00
6 changed files with 259 additions and 59 deletions
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Lint workflow bash for curl status-code capture pollution.
The bad shape is:
HTTP_CODE=$(curl ... -w '%{http_code}' ... || echo "000")
`curl -w` writes the HTTP code to stdout before returning non-zero, so
fallback output inside the same command substitution appends another code.
"""
from __future__ import annotations
import argparse
import glob
import re
import sys
from pathlib import Path
from typing import NamedTuple
SELF = ".gitea/workflows/lint-curl-status-capture.yml"
class Finding(NamedTuple):
path: str
snippet: str
BAD_STATUS_CAPTURE = re.compile(
r"""
\$\(\s*
curl\b
[^)]*
-w\s*['"]%\{http_code\}['"]
[^)]*
\|\|\s*
(?:
echo\s+['"]?000['"]?
|
printf\s+['"]000['"]
)
\s*\)
""",
re.DOTALL | re.VERBOSE,
)
def _logical_shell(content: str) -> str:
"""Collapse bash line continuations so one curl command is one string."""
return re.sub(r"\\\s*\n\s*", " ", content)
def scan_content(path: str, content: str) -> list[Finding]:
flat = _logical_shell(content)
return [
Finding(path=path, snippet=re.sub(r"\s+", " ", match.group(0)).strip()[:160])
for match in BAD_STATUS_CAPTURE.finditer(flat)
]
def scan_paths(paths: list[str]) -> list[Finding]:
findings: list[Finding] = []
for path in paths:
if path == SELF:
continue
content = Path(path).read_text(encoding="utf-8")
findings.extend(scan_content(path, content))
return findings
def default_paths() -> list[str]:
return sorted(glob.glob(".gitea/workflows/*.yml"))
def print_report(findings: list[Finding]) -> None:
if not findings:
print("OK No curl-status-capture pollution patterns detected")
return
print(f"::error::Found {len(findings)} curl-status-capture pollution site(s):")
for finding in findings:
print(
f"::error file={finding.path}::Curl status-capture pollution: "
"'|| echo/printf 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 fallback appends another "
"status. Fix: route -w into a tempfile so the exit code cannot "
"pollute stdout."
)
print(f" matched: {finding.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"')
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("paths", nargs="*", help="workflow files to scan")
args = parser.parse_args(argv)
paths = args.paths or default_paths()
findings = scan_paths(paths)
print_report(findings)
return 1 if findings else 0
if __name__ == "__main__":
raise SystemExit(main())
+2 -2
View File
@@ -169,10 +169,10 @@ jobs:
run: go build ./cmd/server
# CLI (molecli) moved to standalone repo: git.moleculesai.app/molecule-ai/molecule-cli
- if: needs.changes.outputs.platform == 'true'
run: go vet ./... || true
run: go vet ./...
- if: needs.changes.outputs.platform == 'true'
name: Run golangci-lint
run: golangci-lint run --timeout 3m ./... || true
run: golangci-lint run --timeout 3m ./...
- if: needs.changes.outputs.platform == 'true'
name: Diagnostic — per-package verbose 60s
run: |
+9 -54
View File
@@ -30,10 +30,16 @@ name: Lint curl status-code capture
on:
pull_request:
paths: ['.gitea/workflows/**']
paths:
- '.gitea/workflows/**'
- '.gitea/scripts/lint-curl-status-capture.py'
- 'tests/test_lint_curl_status_capture.py'
push:
branches: [main, staging]
paths: ['.gitea/workflows/**']
paths:
- '.gitea/workflows/**'
- '.gitea/scripts/lint-curl-status-capture.py'
- 'tests/test_lint_curl_status_capture.py'
env:
GITHUB_SERVER_URL: https://git.moleculesai.app
@@ -51,55 +57,4 @@ jobs:
- 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 = ".gitea/workflows/lint-curl-status-capture.yml"
for f in sorted(glob.glob(".gitea/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("OK 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
python3 .gitea/scripts/lint-curl-status-capture.py
+88
View File
@@ -0,0 +1,88 @@
"""Tests for `.gitea/scripts/lint-curl-status-capture.py`.
Run:
python3 -m pytest tests/test_lint_curl_status_capture.py -v
"""
from __future__ import annotations
import importlib.util
from pathlib import Path
SCRIPT_PATH = (
Path(__file__).resolve().parent.parent
/ ".gitea"
/ "scripts"
/ "lint-curl-status-capture.py"
)
def _load_module():
spec = importlib.util.spec_from_file_location("lint_curl_status_capture", SCRIPT_PATH)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_finds_quoted_echo_fallback_pollution():
lint = _load_module()
content = """
HTTP_CODE=$(curl -sS -o /tmp/body -w "%{http_code}" https://example.test || echo "000")
"""
findings = lint.scan_content("workflow.yml", content)
assert len(findings) == 1
assert "echo" in findings[0].snippet
def test_finds_unquoted_echo_fallback_pollution():
lint = _load_module()
content = """
HTTP_CODE=$(curl -sS -o /tmp/body -w '%{http_code}' https://example.test || echo 000)
"""
findings = lint.scan_content("workflow.yml", content)
assert len(findings) == 1
assert "echo" in findings[0].snippet
def test_finds_printf_fallback_pollution():
lint = _load_module()
content = """
HTTP_CODE=$(curl -sS -o /tmp/body -w '%{http_code}' https://example.test || printf '000')
"""
findings = lint.scan_content("workflow.yml", content)
assert len(findings) == 1
assert "printf" in findings[0].snippet
def test_ignores_tempfile_fallback_after_curl():
lint = _load_module()
content = """
set +e
curl -sS -o /tmp/body -w '%{http_code}' https://example.test >/tmp/code
rc=$?
set -e
HTTP_CODE=$(cat /tmp/code 2>/dev/null || echo "000")
[ -z "$HTTP_CODE" ] && HTTP_CODE="000"
"""
assert lint.scan_content("workflow.yml", content) == []
def test_collapses_bash_line_continuations():
lint = _load_module()
content = """
HTTP_CODE=$(curl -sS -o /tmp/body \\
-w "%{http_code}" \\
https://example.test \\
|| echo "000")
"""
findings = lint.scan_content("workflow.yml", content)
assert len(findings) == 1
+16 -1
View File
@@ -35,7 +35,22 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
-o /memory-plugin ./cmd/memory-plugin-postgres
FROM alpine:3.20@sha256:c64c687cbea9300178b30c95835354e34c4e4febc4badfe27102879de0483b5e
RUN apk add --no-cache ca-certificates git tzdata wget
# docker-cli is required by internal/provisioner/localbuild.go which
# shells out via exec.Command("docker", "image", "inspect"/"build"/"tag", ...)
# whenever Resolve().Mode == RegistryModeLocal — which is the permanent
# mode post-2026-05-06 (Molecule-AI GitHub org suspended → GHCR
# unreachable → MOLECULE_IMAGE_REGISTRY unset → registry_mode.go falls
# through to RegistryModeLocal). Without docker-cli here the platform
# fails every workspace re-provision with `local-build: image inspect
# for molecule-local/workspace-template-<runtime>:<sha> failed
# (exec: "docker": executable file not found in $PATH)` and the
# workspace stays status=failed. The Docker SOCKET is already mounted
# (entrypoint.sh adds the platform user to the docker group) — only
# the CLI binary was missing. Caught after sdk-lead + CP-QA went down
# this way during the MiniMax-switch attempt + after-Class-A audit.
# Related: Task #194 / Issue #63 (local-build path added);
# `feedback_workspace_image_ghcr_dead`.
RUN apk add --no-cache ca-certificates docker-cli git tzdata wget
COPY --from=builder /platform /platform
COPY --from=builder /memory-plugin /memory-plugin
COPY workspace-server/migrations /migrations
@@ -7,6 +7,7 @@ import (
"go/parser"
"go/token"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/models"
@@ -71,6 +72,8 @@ func TestPreflight_ContainerRunning_ReturnsNil(t *testing.T) {
// triggers the offline-flip + WORKSPACE_OFFLINE broadcast + async restart.
// This is the load-bearing case — saves the caller 2-30s of network timeout.
func TestPreflight_ContainerNotRunning_StructuredFastFail(t *testing.T) {
const wsID = "ws-dead-456"
resetRestartStatesFor(wsID)
mock := setupTestDB(t)
_ = setupTestRedis(t)
stub := &preflightLocalProv{running: false, err: nil}
@@ -79,14 +82,14 @@ func TestPreflight_ContainerNotRunning_StructuredFastFail(t *testing.T) {
// Expect the offline-flip UPDATE.
mock.ExpectExec(`UPDATE workspaces SET status =`).
WithArgs(models.StatusOffline, "ws-dead-456").
WithArgs(models.StatusOffline, wsID).
WillReturnResult(sqlmock.NewResult(0, 1))
// Broadcaster's INSERT INTO structure_events fires too — best-effort
// log entry for the WORKSPACE_OFFLINE event. Match permissively.
mock.ExpectExec(`INSERT INTO structure_events`).
WillReturnResult(sqlmock.NewResult(0, 1))
proxyErr := h.preflightContainerHealth(context.Background(), "ws-dead-456")
proxyErr := h.preflightContainerHealth(context.Background(), wsID)
if proxyErr == nil {
t.Fatal("preflight should return *proxyA2AError when container not running")
}
@@ -107,6 +110,32 @@ func TestPreflight_ContainerNotRunning_StructuredFastFail(t *testing.T) {
// h.broadcaster.RecordAndBroadcast call but not asserted here — the
// real *events.Broadcaster doesn't expose received events for inspection.
// The DB UPDATE expectation is sufficient to pin the offline-flip path.
waitRestartByIDGoroutineIdle(t, wsID)
}
func waitRestartByIDGoroutineIdle(t *testing.T, wsID string) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
sawState := false
for time.Now().Before(deadline) {
sv, ok := restartStates.Load(wsID)
if ok {
sawState = true
st := sv.(*restartState)
st.mu.Lock()
running := st.running
st.mu.Unlock()
if !running {
resetRestartStatesFor(wsID)
return
}
}
time.Sleep(time.Millisecond)
}
if !sawState {
t.Fatalf("preflight did not start RestartByID goroutine for %s", wsID)
}
t.Fatalf("RestartByID goroutine for %s did not drain before test cleanup", wsID)
}
// TestPreflight_TransientError_FailsSoftAsAlive — IsRunning(true,err): the