Force a fresh sop-tier-check run to check if runners have recovered
from infra#241 OOM cascade.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add deferred error checks following rows.Next() iteration in:
- ListDelegations (delegation.go): log on error, continue serving results
- org import reconcile orphan query (org.go): log + append to reconcileErrs
Fixes the rows.Err() gap identified in the delegated rows.Err() check PR
(#302, closed; replaced by this PR). Two additional files already had
the check (activity.go, memories.go) — pattern applied consistently here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Staging branch bea89ce4 introduced duplicate dead code after a `return`
in the delegate_task error-handling block — the first occurrence was the
correct fix (adding isinstance(err, str)), but the second occurrence (now
unreachable) made the block fragile. Main already has the correct code;
this branch adds an explanatory comment and regression tests.
The non-tool delegate_task() in a2a_tools.py uses httpx.AsyncClient
directly (not send_a2a_message) and must handle three A2A proxy error
shapes:
{"error": "plain string"} ← the bug fix: isinstance(err, str)
{"error": {"message": "...", ...}} ← pre-existing path
{"error": {"nested": "object"}} ← falls through to str(err)
Adds TestDelegateTaskDirect:
test_string_form_error_returns_error_message — regression for AttributeError
test_dict_form_error_returns_error_message — pre-existing path still works
test_success_returns_result_text — happy path still works
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Gitea Actions runners (ubuntu-latest) do not bundle jq.
The sop-tier-check script uses jq for all JSON API parsing.
Install jq before the script runs so sop-tier-check can pass.
Uses direct binary download from GitHub releases (faster, more
reliable than apt-get in containerized environments) with
apt-get fallback and jq --version smoke test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 3 of RFC internal#219 (CI/CD hard-gate hardening). molecule-core's
branch protection on main currently requires only Secret scan +
sop-tier-check/tier-check — there is no required gate that asserts the
actual Go code builds. The .github/workflows/ci.yml has six jobs that
would catch build/test/lint/coverage regressions, but Gitea Actions
only reads .gitea/workflows/. So today every Go regression on
molecule-core merges through (recurrence of
feedback_phantom_required_check_after_gitea_migration).
This PR ports the workflow to .gitea/workflows/ci.yml. Per RFC §1, the
port lands with `continue-on-error: true` on every job so we surface
broken jobs without blocking PRs while the team triages anything that
falls out of "first contact with reality". A follow-up PR (Phase 4)
will flip continue-on-error to false, add the `ci/all-required`
aggregator sentinel (mirroring molecule-controlplane#89's pattern),
and PATCH branch protection to require it.
Four-surface migration audit performed
(feedback_gitea_actions_migration_audit_pattern):
1. YAML: dropped merge_group trigger (no Gitea merge queue); no
workflow_dispatch.inputs to worry about
(feedback_gitea_workflow_dispatch_inputs_unsupported); no
environment: blocks; runs-on: ubuntu-latest preserved. Set
workflow-level env.GITHUB_SERVER_URL as belt-and-suspenders
against runner-default regression
(feedback_act_runner_github_server_url +
feedback_act_runner_needs_config_file_env).
2. Cache + artifact: actions/upload-artifact pinned at v3.2.2
(original already had this — Gitea act_runner v0.6 doesn't speak
the v4 artifact protocol). setup-python cache: pip preserved.
3. Token: workflow uses no custom dispatch tokens; auto-injected
GITHUB_TOKEN (Gitea-scoped runner token) handles checkout against
this same repo.
4. Docs: no github.com docs/scripts references to swap. The
canvas-deploy-reminder step references ghcr.io/.../canvas — that's
external documentation prose, not a build dependency, and is a
separate ghcr→ECR sweep if in scope.
actions/* (checkout, setup-go, setup-node, setup-python,
upload-artifact) are verified mirrored on this Gitea instance
(git.moleculesai.app/actions/*); app.ini has
DEFAULT_ACTIONS_URL = self so the @SHA refs resolve locally.
Scope guard (per RFC):
- This PR ports ONLY ci.yml. The other 34 workflows in
.github/workflows/ get swept in a follow-up per the
runbooks/gitea-actions-migration-checklist.md.
- This PR does NOT add the all-required aggregator sentinel (Phase 4).
- This PR does NOT modify branch protection (Phase 4).
- This PR does NOT delete .github/workflows/ci.yml (RFC §1 leaves it
in place initially).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two vulnerable call sites confirmed on origin/main:
1. org_helpers.go:loadWorkspaceEnv (line 101): filesDir from untrusted org YAML
joined directly with orgBaseDir without traversal guard. A malicious filesDir
like "../../../etc" escapes the org root and reads arbitrary files.
2. org_import.go:createWorkspaceTree (line 494): same pattern directly in the
env-loading block — not covered by staging-targeted PR #345.
Fix (both locations): call resolveInsideRoot(orgBaseDir, filesDir) before
filepath.Join. On traversal detection, org_helpers.go returns an empty map
(caller contract); org_import.go silently skips the workspace .env override
(matches existing template-resolution pattern in the same function).
Tests: org_helpers_test.go — 3 cases covering traversal rejection,
workspace-override happy path, and empty filesDir edge case.
Closes: molecule-core#362, molecule-core#321
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Run 5196 (2026-05-11 02:46Z, first-ever successful publish) succeeded
the publish job but failed the cascade job at the wait-for-PyPI-
propagation step:
::error::PyPI propagated 0.1.130 but wheel content SHA256 mismatch.
::error::Expected: 536b123816f3c7fb54690b80be482b28cabd1874690e9e93d8586af3864c7fba
::error::Got: Collecting molecule-ai-workspace-runtime==0.1.130
::error::Fastly may be serving stale content. Refusing to fan out cascade.
The 'Got:' is pip's own stdout, not a SHA. Root cause:
HASH=$(python -m pip download ... 2>/dev/null && sha256sum ... | awk ...)
The shell pipeline captures BOTH commands' stdout into $HASH. `2>/dev/null`
only silences stderr, not stdout. pip download writes 'Collecting ...' to
stdout by default, so it leaks into HASH ahead of sha256sum's output.
Fix: split into two steps, redirect pip stdout to /dev/null explicitly,
capture only sha256sum's output into HASH.
Impact: cascade-to-8-template-repos failed, but PyPI publish itself
succeeded. Users (workspace-template-* maintainers) can pin manually
via 'docker build --build-arg RUNTIME_VERSION=X.Y.Z' until cascade is
healed. hongming-pc is doing exactly this for the plugins_registry rollout.
4th and likely last workflow defect after #353, #355, #357.
Refs: #351, #353, #355, #357, #348 Q3
Close the A2A delegation auto-resume gap.
Root cause: heartbeat.py's _check_delegations already writes completed
delegation rows to DELEGATION_RESULTS_FILE and sends a self-message to
wake the agent. executor_helpers.read_delegation_results() was defined to
atomically consume that file, but a2a_executor._core_execute() never
called it — so delegation results were written but the agent never saw
them.
Fix: call read_delegation_results() at the top of _core_execute() and
prepend the results to the user input context so the agent can act on
them without an explicit check_task_status call. The Temporal durable
workflow path is also covered because it calls _core_execute() directly.
Test: two new cases — delegation results injected when file exists;
user input passed through unchanged when file is empty.
Closes molecule-core#354.
Incorporates valuable extra coverage from fullstack-engineer's PR #336:
- test_push_queued_missing_queue_id_still_parsed: queue_id is optional,
absence must not break parsing
- test_push_queued_is_distinct_from_poll_queued: both envelope shapes
parse correctly and independently, with correct delivery_mode values
Also adds push_queued_no_queue_id fixture and regression gate entry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: a2a_response.py:197 returned Queued(method=method) without passing
delivery_mode, silently defaulting to "poll" for push-mode busy-queue
responses. Callers branching on v.delivery_mode would mis-identify push-mode
responses as poll-mode, causing wrong dispatch logic.
Fix: pass delivery_mode="push" explicitly in the push-mode branch.
Tests: add push_queued_full/notify/no_method fixtures and 4 test cases
asserting delivery_mode="push" for all three envelope shapes. Also add
adversarial {"queued": "yes"} and {"queued": False} → Malformed guards.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Run 5160 publish-runtime build step failed:
error: TOP_LEVEL_MODULES drifted from workspace/*.py contents:
in workspace/ but NOT in TOP_LEVEL_MODULES (will ship un-rewritten): ['_sanitize_a2a']
Edit scripts/build_runtime_package.py:TOP_LEVEL_MODULES to match.
workspace/_sanitize_a2a.py was added recently but the allowlist in
scripts/build_runtime_package.py was not updated. The build script
intentionally aborts (exit 3) when it detects the drift, because
shipping a module un-rewritten breaks the package's flat-layout import
contract.
Fix: add '_sanitize_a2a' to the set. Alphabetical order preserved
(it sorts before 'a2a_*').
Third workflow defect after #353 (workflow_dispatch.inputs parser) and
#355 (Publish step working-directory). After this lands, attempt #4 of
runtime-v0.1.130 should finally succeed.
Refs: #351, #353, #355, #348 Q3
First-ever publish-runtime.yml dispatch (run 5097 post-#353, 2026-05-11
02:06Z) failed at the twine upload step:
ERROR InvalidDistribution: Cannot find file (or expand pattern): 'dist/*'
Cause: the Publish step was missing 'working-directory: ${{ runner.temp
}}/runtime-build' while the preceding Build/Verify steps all had it.
Result: twine ran from the workspace checkout dir where dist/ doesn't
exist.
Fix: add working-directory to match the rest of the publish job.
This is the second of three workflow defects exposed by #353 finally
making the workflow run at all:
1. workflow_dispatch.inputs rejection → fixed in #353
2. Publish step missing working-directory → THIS PR
3. (anything else surfaced by 0.1.130 attempt #2)
After merge: push runtime-v0.1.130 again (tag was already pushed once
post-#353 but the run failed at publish; need a fresh trigger). Should
finally land 0.1.130 on PyPI.
Refs: #351, #348 Q3, #353
test_audit_ledger.py imports sqlalchemy directly (line 42).
Without an explicit sqlalchemy install, pip dependency resolution can
omit it when pytest/pytest-asyncio/pytest-cov are installed as a
separate step after requirements.txt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Gitea Actions reads .gitea/workflows/, not .github/workflows/. The
.github/ copy of this workflow has been kept in lockstep with .gitea/
since the post-suspension migration (e.g. 6d94fd30, 5216e781, 67b2e488
all touch both files). The functional code is identical between the
two; the only differences are comment verbosity and the path-filter
self-reference (each version watches its own location).
Removing the .github/ copy:
- eliminates the dual-edit maintenance tax (two files touched per fix)
- prevents accidental drift where one is updated and the other isn't
- leaves a single source-of-truth at .gitea/workflows/
Cross-references confirmed safe:
- canary-verify.yml + redeploy-tenants-on-{staging,main}.yml all use
`workflows: ['publish-workspace-server-image']` (workflow name,
not file path) — they trigger off the workflow_run event keyed on
`name:`, which is identical in both files.
- No other workflow path-watches .github/workflows/publish-workspace-
server-image.yml.
Other two triplicates from task #287 (publish-runtime.yml and
secret-scan.yml) are NOT addressed in this PR — see PR description for
the ambiguity report flagging them for human review.
Refs: task #287
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trivial empty commit to force a fresh workflow run now that the
PR has tier:low label and approvals on the rebased branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Root cause (from infra-lead PR#7 review id=724):
Sanitization in PR#7 wrapped peer text in [A2A_RESULT_FROM_PEER]
markers, but the markers themselves were not escaped — a malicious
peer could inject "[/A2A_RESULT_FROM_PEER]" to close the trust
boundary early, making subsequent text appear inside the trusted zone.
Fix:
- Create workspace/_sanitize_a2a.py (leaf module, no circular import
risk) with shared sanitize_a2a_result() + _escape_boundary_markers()
- _escape_boundary_markers() escapes boundary open/close markers in the
raw peer text before wrapping (primary security control)
- Defense-in-depth: also escapes SYSTEM/OVERRIDE/INSTRUCTIONS/IGNORE
ALL/YOU ARE NOW patterns (secondary, per PR#7 design intent)
- Update a2a_tools_delegation.py: import from _sanitize_a2a; wrap
tool_delegate_task return and tool_check_task_status response_preview
- Add 15 tests covering boundary escape, injection patterns, integration
shapes (workspace/tests/test_a2a_sanitization.py)
Follow-up (non-blocking, noted in PR#7 infra-lead review):
- Deduplicate if a2a_tools.py also wraps (currently handled in
delegation module only — callers get sanitized output regardless)
- tool_check_task_status: consider sanitizing 'summary' field too
Closes: molecule-ai/molecule-ai-workspace-runtime#7 (wrong-repo PR
that this supersedes)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>