forked from molecule-ai/molecule-core
fix(platform): reject self-delegation to prevent _run_lock deadlock (#570)
When a workspace delegated a task to itself, it would acquire
_run_lock twice on the same goroutine mutex, blocking permanently.
Add an early-return guard in `DelegationHandler.Delegate` that
returns HTTP 400 {"error": "self-delegation not permitted"} as soon
as sourceID == body.TargetID, before any DB or A2A work is done.
Adds TestDelegate_SelfDelegation_Rejected to delegation_test.go.
Closes #548
Co-authored-by: Molecule AI Backend Engineer <backend-engineer@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a360b64157
commit
d08f237de9
@ -54,6 +54,13 @@ func (h *DelegationHandler) Delegate(c *gin.Context) {
|
||||
return // response already written
|
||||
}
|
||||
|
||||
// #548 — prevent self-delegation: a workspace delegating to itself
|
||||
// acquires _run_lock twice on the same mutex, deadlocking permanently.
|
||||
if sourceID == body.TargetID {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "self-delegation not permitted"})
|
||||
return
|
||||
}
|
||||
|
||||
// #124 — idempotency. If the caller supplies an idempotency_key, return
|
||||
// the existing delegation when (workspace_id, idempotency_key) already
|
||||
// exists and is not in a failed terminal state.
|
||||
|
||||
@ -88,6 +88,37 @@ func TestDelegate_InvalidUUIDTargetID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Delegate: self-delegation → 400 ----------
|
||||
|
||||
func TestDelegate_SelfDelegation_Rejected(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
broadcaster := newTestBroadcaster()
|
||||
wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
|
||||
dh := NewDelegationHandler(wh, broadcaster)
|
||||
|
||||
// Use the same UUID for both source and target to trigger the self-delegation guard.
|
||||
selfID := "11111111-2222-3333-4444-555555555555"
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: selfID}}
|
||||
body := `{"target_id":"` + selfID + `","task":"do something"}`
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/"+selfID+"/delegate", bytes.NewBufferString(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
dh.Delegate(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["error"] != "self-delegation not permitted" {
|
||||
t.Errorf("expected 'self-delegation not permitted', got %v", resp["error"])
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Delegate: success → 202 with delegation_id ----------
|
||||
|
||||
func TestDelegate_Success(t *testing.T) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user