Files
78a5f69ae1 MUL-6813: fix: surface private runtime owner mismatch as explicit failure (#7805)
* fix: fail private runtime owner mismatches (PUCK-89)

Co-authored-by: multica-agent <[email protected]>

* fix: keep runtime mismatch repair off idle claim path

* test: cover mixed runtime claim outcomes (PUCK-89)

Co-authored-by: multica-agent <[email protected]>

* fix: settle runtime owner mismatches during claim

* fix: reject ownerless agents on private runtime claims

* chore: restore claim version sampling layout (PUCK-89)

Co-authored-by: multica-agent <[email protected]>

* fix: re-authorize delivery at the finalize boundary, dedicated runtime_access_denied client copy, fixture convention (PUCK-89)

Blocker 1 — final delivery gate: FinalizeTaskClaim now runs a caller-
supplied authorize closure inside its transaction. The singular and
batch claim paths share finalizeClaimDelivery, which re-locks the
runtime row (FOR UPDATE), re-reads the agent, and re-verifies the
private-runtime owner fence against CURRENT ownership before the task
token commits. A concurrent re-registration that would change
owner_id blocks until the gate commits, closing the stale-snapshot
TOCTOU. Mismatched tasks settle through the existing
failClaimedTaskBeforeLaunch -> FailTask path; singular keeps
200 {"task":null}, batch skips the task and keeps returning valid
ones. Regressions cover both paths.

Blocker 2 — client copy: runtime_access_denied gets dedicated
actionable copy (make runtime public / rebind-copy agent) on the
issues blocked-trigger mapping, both chat send-failure toasts, the
chat failure-reason map, the mobile dispatch-reason helper, and the
autopilot run-now toast. Generic fallbacks unchanged; locale keys
added for en/ko/ja/zh-Hans.

Blocker 3 — fixture convention: runtime_access_denied_test.go now
uses testutil.Call; daemon_runtime_access_test.go seeds its chat task
via the dbfx.Task fixture. Test semantics unchanged.

Co-authored-by: multica-agent <[email protected]>

* fix: bind final task token to locked runtime owner

Co-authored-by: multica-agent <[email protected]>

* fix: keep delivered user context on current owner

Co-authored-by: multica-agent <[email protected]>

* test: use testutil.Call in settlement-failure claim regression

Convert the last manual httptest.NewRecorder flow in
TestFinalizeClaimDelivery_SettlementFailureIsUnsettled to the repository's
required testutil.Call helper (hard convention from review) and drop the
now-unused net/http/httptest import. No production code changes.

Co-authored-by: multica-agent <[email protected]>

* PUCK-132: settle queued private-runtime owner mismatches as runtime_access_denied

Align persisted task failure semantics with admission semantics: queued
private-runtime ownership mismatches now settle as runtime_access_denied,
letting clients reach the dedicated recovery copy instead of the generic
invalid_task_identity copy. Actual task/agent identity violations
(agent rebound, agent deleted, response identity mismatch,
error_agent_runtime_changed) keep invalid_task_identity.

- add taskfailure.ReasonRuntimeAccessDenied (permanent, non-retryable
  ownership authorization failure; agent process never launched)
- swap the reason in the two owner-mismatch settlement paths in
  handler/daemon.go (response-assembly recheck + delivery-gate
  authz-error default branch, including ownerless-runtime denial)
- regression A (queued issue task), B (queued chat task + assistant
  failure message via existing FailTask path), C (agent rebind keeps
  invalid_task_identity)

Co-authored-by: multica-agent <[email protected]>

* PUCK-132: map persisted runtime access failures in clients

* fix(i18n): add missing fr translations for runtime_access_denied keys

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
Co-authored-by: worker-opencode <[email protected]>
Co-authored-by: puck-181 <puck-181@local>
2026-09-18 15:39:25 +08:00

42 lines
1.8 KiB
TypeScript

import { RUNTIME_ACCESS_DENIED_RECOVERY_COPY } from "./runtime-access-copy";
/**
* Mobile-owned mirror of `packages/core/api/client.ts:dispatchReasonCode`.
*
* Why mirror instead of import: the core version narrows on core's own
* `ApiError` class, and mobile throws `apps/mobile/data/api.ts:ApiError` — a
* different constructor, so `instanceof` never matches across the two. The
* extraction itself is identical: read the stable `reason_code` the admission
* gate puts on a structured rejection body.
*/
export function dispatchReasonCode(err: unknown): string | undefined {
const body = (err as { body?: unknown } | null)?.body;
if (body && typeof body === "object") {
const code = (body as { reason_code?: unknown }).reason_code;
if (typeof code === "string" && code.length > 0) return code;
}
return undefined;
}
/**
* User-facing sentence for a refused send. `invocation_not_allowed` is the
* revoked-permission case (MUL-4525): the session was created while the user
* could run the agent and the server now refuses, so it must not read as a
* transient failure the user should retry.
*/
export function sendFailureMessage(err: unknown): string {
switch (dispatchReasonCode(err)) {
case "invocation_not_allowed":
return "You no longer have permission to run this agent, so the message was not sent.";
case "agent_runtime_required":
return "Bind a runtime to this agent before sending a message.";
case "runtime_access_denied":
// The agent's owner cannot execute it on the selected private runtime.
// Retrying never fixes this — the fix is making that runtime public or
// rebinding/copying the agent to a runtime its owner can use.
return `Message not sent — ${RUNTIME_ACCESS_DENIED_RECOVERY_COPY}`;
default:
return "Your message could not be sent. Please try again.";
}
}