Compare commits

..

8 Commits

Author SHA1 Message Date
fullstack-engineer 631d1bae5f fix(workspace): replace asyncio.get_event_loop().run_until_complete with asyncio.run() (#307)
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 6s
cascade-list-drift-gate / check (pull_request) Successful in 11s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 9s
Harness Replays / detect-changes (pull_request) Successful in 12s
sop-tier-check / tier-check (pull_request) Successful in 13s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 14s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 21s
E2E API Smoke Test / detect-changes (pull_request) Successful in 21s
CI / Detect changes (pull_request) Successful in 22s
Harness Replays / Harness Replays (pull_request) Successful in 7s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 24s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 26s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 6s
CI / Canvas (Next.js) (pull_request) Successful in 7s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 2m33s
audit-force-merge / audit (pull_request) Successful in 27s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 4m35s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m17s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 5m38s
CI / Python Lint & Test (pull_request) Failing after 7m33s
CI / Platform (Go) (pull_request) Failing after 13m9s
test_a2a_tools_inbox_wrappers.py's _run() helper used
asyncio.get_event_loop().run_until_complete() to run coroutines from
sync test methods. When pytest-asyncio is active in OTHER test files in
the same suite, get_event_loop() can return the shared pytest-asyncio
loop, and run_until_complete() raises "loop already running" errors.

Fix: replace with asyncio.run(), which creates a fresh loop each call.

Result (full suite, 14→0 for inbox wrappers):
  Without fix: 10 failures (6 TestToolWaitForMessage + 4 delegation)
  With fix:    4 failures (all pre-existing delegation polling)

Closes #307.
2026-05-11 15:34:18 +00:00
core-be bad69295ca fix(workspace): auto-suffix duplicate names on POST /workspaces (closes 500 on double-click)
The Canvas template-deploy path returned HTTP 500 with raw pq error
when a user clicked a template card twice in quick succession. Root
cause: migration 20260506000000 added the partial-unique index
`workspaces_parent_name_uniq` on (COALESCE(parent_id, sentinel), name)
WHERE status != 'removed' to close TOCTOU on /org/import (#2872). The
org-import handler resolves the constraint via ON CONFLICT DO NOTHING
+ idempotent re-select. The Canvas Create handler did not — it
bubbled the pq violation as a generic 500.

Fix: auto-suffix the user-typed name on collision via a small retry
helper that pins on SQLSTATE 23505 + constraint name (so unrelated
unique indexes still fail loud), retries with " (2)", " (3)" up to
N=20, and threads the actually-persisted name back into the response
+ broadcast payload (so the canvas displays what the DB actually
holds). Exhaustion maps to a clean 409 Conflict instead of a 500.

#2872 protection is preserved unchanged — the index stays in place,
and /org/import's ON CONFLICT path is unaffected. The bundle-import
INSERT (handlers/bundle.go) is a separate code path and is not
touched here; if it surfaces the same UX issue a follow-up can adopt
the same helper.

Verification (against running localhost:8080 platform):

  Three back-to-back POSTs with name="ManualVerify-1778459812":
    POST #1 -> 201, id=db2dacf7-…, persisted name="ManualVerify-1778459812"
    POST #2 -> 201, id=f468083d-…, persisted name="ManualVerify-1778459812 (2)"
    POST #3 -> 201, id=5f5ae905-…, persisted name="ManualVerify-1778459812 (3)"
  Log lines: "name collision auto-suffix \"…\" -> \"… (N)\""

Tests:
- workspace_create_name_test.go — 4 unit tests via sqlmock pin the
  retry contract (happy path no-suffix, single-collision -> " (2)",
  non-retryable error pass-through, exhaustion -> errWorkspaceNameExhausted).
- workspace_create_name_integration_test.go — 2 real-Postgres tests
  (build tag `integration`) confirm the partial-unique index
  behaviour AND the WHERE status != 'removed' tombstone exemption.
- Watch-it-fail confirmed: temporarily removing the
  `fmt.Sprintf("%s (%d)", baseName, attempt+1)` candidate-naming
  line makes TestInsertWorkspaceWithNameRetry_SecondAttemptSuffixed
  fail with the expected argument-mismatch from sqlmock.

Pre-existing test failures in handlers/ (TestExecuteDelegation_…,
TestMCPHandler_CommitMemory_GlobalScope_Blocked) reproduce on
unmodified staging and are NOT caused by this change.
2026-05-11 15:34:18 +00:00
infra-runtime-be 2581db713e fix(workspace): OFFSEC-003 sanitize read_delegation_results()
Adds _sanitize_a2a.py (from PR #346) and integrates sanitize_a2a_result()
into read_delegation_results() so peer-supplied summary and response_preview
fields are escaped before being injected into the agent prompt.

Output is wrapped in [A2A_RESULT_FROM_PEER]...[/A2A_RESULT_FROM_PEER]
boundary markers so content after the block is clearly not from a peer.

Fixes:
- test_a2a_executor.py: correct mock patch path to executor_helpers
- test_executor_helpers.py: fix boundary-injection test assertion to match
  _strip_closed_blocks behaviour (closes marker, removes following text)

Follow-up to PR #346 (OFFSEC-003 boundary escape) which noted
"read_delegation_results() path still needs sanitization" as a gap.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:33:52 +00:00
fullstack-engineer 0434f5e7fd fix(workspace): inject plugins_registry into sys.modules before loading adapters (closes #296)
Plugin adapters in molecule-skill-* repos do:
  from plugins_registry.builtins import AgentskillsAdaptor as Adaptor

But _load_module_from_path() used exec_module() with a fresh module
namespace that did NOT have plugins_registry or its submodules in sys.modules,
causing:
  ModuleNotFoundError: No module named 'plugins_registry'

Fix: before exec_module(), import and register plugins_registry + all three
submodules (builtins, protocol, raw_drop) in sys.modules so adapter imports
resolve correctly.  Follows the Option 1 recommendation from issue #296.

Also adds test_resolve_plugin.py verifying the fix for both the
AgentskillsAdaptor import and the full InstallContext/resolve/protocol import.

Closes #296.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:32:59 +00:00
fullstack-engineer 3b729a970a fix(a2a): handle string-form errors in delegate_task
The A2A proxy can return three error shapes:
  {"error": "plain string"}
  {"error": {"message": "...", "code": ...}}
  {"error": {"message": {"nested": "object"}}}   ← value at .message is a string

builtin_tools/a2a_tools.py:72 called data["error"].get("message")
without guarding against error being a string, which raised:
  AttributeError: 'str' object has no attribute 'get'

This broke every delegation attempt through the legacy a2a_tools path
(the LangChain-wrapped version used by adapter templates). The
SSOT parser a2a_response.py already handled string errors; the
legacy inline sniffer in a2a_tools.py did not.

Fix: branch on isinstance(err, dict/str/other) before calling .get().

Also update both publish-workflow files to remove the dead
`staging` branch trigger — trunk-based migration (PR #109,
2026-05-08) removed the staging branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:32:59 +00:00
integration-tester 103b76b0a7 chore: restore manifest.json after trigger test 2026-05-11 15:32:33 +00:00
integration-tester 77e0bb719f chore: trigger publish workflow [Integration Tester 2026-05-10T08:45Z] 2026-05-11 15:32:33 +00:00
integration-tester c0fc11c562 chore: staging trigger commit from Integration Tester 2026-05-11 15:32:33 +00:00
2 changed files with 25 additions and 64 deletions
-42
View File
@@ -1,42 +0,0 @@
#!/usr/bin/env python3
"""Extract changed-file list from a Gitea push event's commits JSON array.
Each commit in a push event has `added`, `removed`, and `modified` file lists.
This script aggregates all of them and prints unique filenames one per line.
Usage:
push-commits-diff-files.py < COMMITS_JSON
Exits 0 always (caller handles empty output as "no files").
"""
from __future__ import annotations
import sys
import json
def main() -> None:
try:
data = json.load(sys.stdin)
except Exception:
sys.exit(0) # Don't fail the step — treat malformed JSON as empty
if not isinstance(data, list):
sys.exit(0)
files: set[str] = set()
for commit in data:
if not isinstance(commit, dict):
continue
for key in ("added", "removed", "modified"):
for f in commit.get(key) or []:
if isinstance(f, str) and f:
files.add(f)
if files:
sys.stdout.write("\n".join(sorted(files)))
sys.stdout.write("\n")
if __name__ == "__main__":
main()
+25 -22
View File
@@ -84,31 +84,23 @@ jobs:
exit 0
fi
# Determine changed files.
# workflow_dispatch: always run.
# pull_request: use Compare API (branch-to-branch works fine).
# push: use github.event.commits array (Compare API rejects SHA-to-branch).
# new-branch: run everything.
# Determine base and head refs for the Compare API call.
# Gitea Compare API accepts branch names OR commit SHAs as base/head.
# Pull request: base.ref + head.ref are in the event payload (branch names).
# Push: github.event.before (SHA of previous tip) as BASE, $GITHUB_REF
# (branch name) as HEAD. These are different, so the Compare API
# returns the actual diff — unlike the broken form which set both
# BASE and HEAD to the same branch name, making
# "compare/main...main" always return zero files.
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="${{ github.event.pull_request.base.ref }}"
HEAD="${{ github.event.pull_request.head.ref }}"
elif [ -n "${{ github.event.before }}" ] && \
! echo "${{ github.event.before }}" | grep -qE '^0+$'; then
# Push event: extract changed files from github.event.commits array.
# Gitea Compare API rejects SHA-to-branch comparisons (BaseNotExist),
# so we use the commits array instead. This array contains all commits
# in the push, each with their added/removed/modified file lists.
echo '${{ toJSON(github.event.commits) }}' \
| bash .gitea/scripts/push-commits-diff-files.py \
> .push-diff-files.txt 2>/dev/null || true
DIFF_FILES=$(cat .push-diff-files.txt 2>/dev/null || true)
if [ -n "$DIFF_FILES" ] && echo "$DIFF_FILES" | grep -qE '^workspace-server/|^canvas/|^tests/harness/|^.gitea/workflows/harness-replays\.yml$'; then
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "run=false" >> "$GITHUB_OUTPUT"
fi
echo "debug=push-files=$DIFF_FILES" >> "$GITHUB_OUTPUT"
exit 0
# Push event: BASE = previous tip (SHA), HEAD = current branch name.
BASE="${{ github.event.before }}"
HEAD_REF="${GITHUB_REF#refs/heads/}"
HEAD="${HEAD_REF:-main}"
else
# New branch or github.event.before unavailable — run everything.
echo "run=true" >> "$GITHUB_OUTPUT"
@@ -116,12 +108,23 @@ jobs:
exit 0
fi
# Call Gitea Compare API (pull_request path only — branch-to-branch).
# Push uses github.event.commits array above.
# Call Gitea Compare API to get the list of changed files.
# This is a Gitea-to-Gitea API call from within the Gitea Actions
# runner — it hits the local Gitea process, not the external network.
# No git network access needed from the runner container
# (runbooks/gitea-operational-quirks.md §runner-network-isolation).
#
# API shape: GET /repos/{owner}/{repo}/compare/{base}...{head}
# Returns { commits: [{ files: [{filename}] }] } — files are
# nested inside commits (Gitea quirk, not at top level).
RESP=$(curl -sS --fail --max-time 30 \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/json" \
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/compare/$BASE...$HEAD")
# compare-api-diff-files.py: extracts filenames from Gitea Compare API
# JSON. Script extracted from workflow to avoid YAML parser choking on
# nested Python indentation (pyyaml safe_load interprets it as YAML
# structure). See runbooks/gitea-operational-quirks.md §large-repo-fetch.
DIFF_FILES=$(echo "$RESP" | bash .gitea/scripts/compare-api-diff-files.py 2>/dev/null || true)
echo "debug=diff-base=$BASE diff-files=$DIFF_FILES" >> "$GITHUB_OUTPUT"