3 Commits

Author SHA1 Message Date
778a6c392a fix(calendar): read agent status by agent_id alone (cross-agent gate)
GET /calendar/agent/status went through _require_agent, which filters by
agent_id AND claw_identifier. But this is a cross-agent READ — the caller asks
about a DIFFERENT agent and passes its OWN X-Claw-Identifier (the CLI falls
back to hostname). So the query required target.claw == caller's-hostname and
spuriously 404'd whenever they differed: `hf agent status-of <other>` worked
only when the caller's host happened to equal the target's registered claw
(e.g. fine on the node whose hostname matched, 404 everywhere else). This broke
the delegate-task / on-call-handoff status gates.

Look the agent up by agent_id alone for the read; accept but ignore
X-Claw-Identifier (now optional). The POST (write own status) still identifies
the caller by agent_id + claw via _require_agent.

Verified: `hf agent status-of <agent>` now returns {agent_id, status} from any
caller/host; unknown agent still 404s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 09:30:02 +01:00
52d71635bc Merge fix/task-list-lenient-enum: tolerate out-of-enum task_type/status on read 2026-06-10 21:24:14 +01:00
3baa990604 fix(tasks): tolerate out-of-enum task_type/status on read (stop list 500)
TaskResponse inherited task_type: TaskTypeEnum (TaskBase) and declared
status: TaskStatusEnum. The tasks table column is a free-form String(32), so
a single row whose task_type/status falls outside the strict enum (e.g.
seeded rows with task_type='setup' or 'recruit') made pydantic raise a
ValidationError while serializing the list — taking down `GET /tasks` /
`hf task list` entirely with HTTP 500 (no --status filter, --status open, and
--project all 500'd; only filters that happened to exclude the bad rows
worked).

Override task_type and status as plain str on TaskResponse so reads surface
legacy/unknown values verbatim instead of crashing the whole list. Writes stay
strict: TaskCreate/TaskBase still use the enums and create is gated by
_validate_task_type_subtype (ALLOWED_TASK_TYPES); TaskUpdate still uses the
enums.

Verified on sim: `hf task list` (and --status open / --project) now return all
rows including task_type='setup'/'recruit' instead of 500.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:24:14 +01:00
2 changed files with 24 additions and 5 deletions

View File

@@ -568,17 +568,28 @@ def agent_update_virtual_slot(
)
def get_agent_status(
agent_id: str = Query(..., description="Target agent_id"),
x_claw_identifier: str = Header(..., alias="X-Claw-Identifier"),
x_claw_identifier: str = Header(None, alias="X-Claw-Identifier"),
db: Session = Depends(get_db),
):
"""Return `{agent_id, status}` so callers (Fabric.OpenclawPlugin's
triage on-call gate, etc.) can decide whether the agent is currently
eligible without flipping their state.
triage on-call gate, delegate-task / on-call-handoff status gates, etc.)
can decide whether the agent is currently eligible without flipping
their state.
This is a cross-agent READ: look the agent up by `agent_id` ALONE. We do
NOT scope by the caller's claw_identifier — the caller is asking about a
DIFFERENT agent, and the X-Claw-Identifier the CLI sends is the caller's
own (often just a hostname fallback), so requiring target.claw == that
header spuriously 404'd whenever the two claws differed (e.g. any
`hf agent status-of` from a host whose hostname != the target's
registered claw). The header is accepted but ignored.
No-op for unknown agents — returns 404 with `{detail: 'Agent not
found'}` so the caller can decide whether to fail-open or fail-closed.
"""
agent = _require_agent(db, agent_id, x_claw_identifier)
agent = db.query(Agent).filter(Agent.agent_id == agent_id).first()
if agent is None:
raise HTTPException(status_code=404, detail="Agent not found")
return {
"agent_id": agent.agent_id,
"status": agent.status.value if hasattr(agent.status, 'value') else str(agent.status),

View File

@@ -74,7 +74,15 @@ class TaskUpdate(BaseModel):
class TaskResponse(TaskBase):
id: int
status: TaskStatusEnum
# Read-side is intentionally lenient: legacy/seeded rows can hold a
# task_type or status outside the strict create-time enums (the column is
# a free-form String(32); e.g. a seeded task_type='setup'). Surfacing them
# as plain strings keeps list endpoints from 500-ing on a single
# out-of-enum row — one bad task used to take down `hf task list`
# entirely. Writes stay strict via TaskCreate/_validate_task_type_subtype
# and TaskUpdate (both still use the enums).
task_type: str = "issue"
status: str
task_code: Optional[str] = None
code: Optional[str] = None
type: Optional[str] = None