PR #1070 stripped `.local` from the daemon-side hostname so CLI and
desktop daemons stop registering as separate devices on macOS, but it
left two gaps:
1. Existing rows in `agent_runtime` for every macOS user still carry
the `.local` suffix. After upgrade the daemon registers under the
new canonical `daemon_id`, hits the `(workspace_id, daemon_id,
provider)` unique key as a miss, and INSERTs a fresh row. The old
`agent.runtime_id` FK keeps pointing at the orphaned `.local` row,
which never receives a heartbeat again — the user's agent appears
offline until they manually rebind it.
2. Older or non-CLI clients (anyone calling /api/daemon/register
directly) can still send the suffixed form and create the same
orphan condition going forward.
Fixes:
- Migration 048 walks `agent_runtime` and, for every (workspace_id,
provider) where both `X` and `X.local` rows exist, redirects the
`agent` and `agent_task_queue` FK references from the `.local` row
to the canonical row inside a single statement (so the RESTRICT
constraint passes), then deletes the duplicate. Orphaned `.local`
rows with no canonical counterpart are renamed in place.
- Handler-side `normalizeDaemonID` strips the suffix on every
/api/daemon/register call before the upsert, so stale clients can't
re-create the orphan.
Tests cover the normalization helper directly and exercise the
register endpoint twice — once with `.local` and once canonical — to
prove both forms upsert into the same row.
Refs: MUL-971
GetActiveTaskForIssue, CancelTask, ListTasksByIssue, and GetIssueUsage
accepted the issueId URL parameter and queried by it without verifying
that the issue belonged to the caller's X-Workspace-ID workspace. The
RequireWorkspaceMember middleware only proves membership in the header
workspace; it does not bind the path-parameter issue to it. A member of
workspace A could therefore enumerate tasks, cancel tasks, and read
usage metadata for any issue UUID in workspace B.
Route every issueId through loadIssueForUser (matching GetIssue and the
existing comment/subscriber handlers). For CancelTask additionally
verify that the task's IssueID matches the loaded issue — the task must
not only belong to the caller's workspace but also to the specific
issue named in the URL, and the access check must run before any
mutation.
Follow-up to MUL-899 / #1112.
Adds TestGetIssueGCCheck_WithDaemonToken_CrossWorkspace alongside the
existing TestGetTaskStatus_WithDaemonToken_CrossWorkspace, covering:
- daemon token scoped to a different workspace → 404 (matches the
"issue not found" status, so no UUID enumeration oracle)
- daemon token scoped to the issue's workspace → 200 with status and
updated_at fields populated
Follow-up to #1121, which fixed the underlying IDOR reported in #1112
but did not ship a regression test. This gates the class of bug at CI
so the next handler to forget requireDaemonWorkspaceAccess will be
caught before merge.
* fix(security): add workspace ownership checks to all daemon API routes
Switch daemon routes from middleware.Auth to middleware.DaemonAuth and
add per-handler workspace ownership verification. This prevents
cross-workspace access to runtimes, tasks, usage, and daemon lifecycle
endpoints (HIGH-1/2/3 + CHAIN-1/2/3).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(security): support mdt_ daemon tokens in DaemonRegister + add regression tests
DaemonRegister now handles both auth paths:
- mdt_ daemon tokens: verify workspace match, skip member check, zero OwnerID
(SQL COALESCE preserves existing owner on upsert)
- PAT/JWT: existing member check + OwnerID from member
Also adds WithDaemonContext helper and regression tests covering:
- Successful register with daemon token
- Workspace mismatch rejection
- Cross-workspace heartbeat rejection
- Cross-workspace task status rejection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>