Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/consumer/jarvis-backend-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,44 @@ describe("createJarvisBackendClient", () => {
});
});

it("preserves sanitized managed utility backend details on HTTP failures", async () => {
const fetchResponse = vi.fn(async (params) => {
return await params.onResponse(
new Response(
JSON.stringify({
detail: {
provider: "google_places",
status: 403,
payload: {
error: {
code: 403,
message: "The caller does not have permission",
status: "PERMISSION_DENIED",
},
},
},
}),
{ status: 502 },
),
);
});
const client = createJarvisBackendClient(
{
jarvis: {
backend: { baseUrl: "https://jarvis.example", accessToken: "backend-token" },
managedServices: { mode: "managed" },
},
},
{ fetchResponse },
);

// Provider failures are already sanitized by the backend. Keeping that JSON
// in the local error tells operators which upstream config is broken.
await expect(client.callManagedUtility({ utility: "google_places.search" })).rejects.toThrow(
/Jarvis managed utility failed with HTTP 502: .*google_places.*PERMISSION_DENIED/,
);
});

it("uses resolved runtime SecretInput refs for managed utility auth", async () => {
const snapshot = await prepareSecretsRuntimeSnapshot({
config: {
Expand Down
28 changes: 27 additions & 1 deletion src/consumer/jarvis-backend-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type JarvisBackendClientDeps = {
};

const DEFAULT_TIMEOUT_MS = 10_000;
const BACKEND_ERROR_DETAIL_MAX_BYTES = 16_000;
const DISABLED_LICENSE_STATUS: JarvisLicenseStatus = {
state: "disabled",
managedServicesMode: "off",
Expand Down Expand Up @@ -202,6 +203,29 @@ function buildHeaders(accessToken: string | undefined): Record<string, string> {
return headers;
}

async function readBackendErrorDetail(response: Response): Promise<string | null> {
const text = await response.text();
const trimmed = text.trim();
if (!trimmed) {
return null;
}

// The backend owns provider-key redaction. The client keeps the sanitized
// failure body visible and bounded so operators see useful provider truth
// instead of a generic HTTP status.
try {
return JSON.stringify(JSON.parse(trimmed)).slice(0, BACKEND_ERROR_DETAIL_MAX_BYTES);
} catch {
return trimmed.slice(0, BACKEND_ERROR_DETAIL_MAX_BYTES);
}
}

async function buildBackendHttpErrorMessage(prefix: string, response: Response): Promise<string> {
const detail = await readBackendErrorDetail(response);
const base = `${prefix} with HTTP ${response.status}`;
return detail ? `${base}: ${detail}` : base;
}

export function createJarvisBackendClient(
config: OpenClawConfig,
deps: JarvisBackendClientDeps = {},
Expand Down Expand Up @@ -327,7 +351,9 @@ export function createJarvisBackendClient(
},
onResponse: async (response) => {
if (!response.ok) {
throw new Error(`Jarvis managed utility failed with HTTP ${response.status}`);
throw new Error(
await buildBackendHttpErrorMessage("Jarvis managed utility failed", response),
);
}
return parseManagedUtilityResponse<T>(await response.json());
},
Expand Down
Loading