diff --git a/scripts/packed-core-release-acceptance.mjs b/scripts/packed-core-release-acceptance.mjs
index 63ab1d6..39c4587 100644
--- a/scripts/packed-core-release-acceptance.mjs
+++ b/scripts/packed-core-release-acceptance.mjs
@@ -192,6 +192,15 @@ function json(response, status, body) {
response.end(payload);
}
+function htmlNotFound(response) {
+ const body = "
404: This page could not be found.";
+ response.writeHead(404, {
+ "content-type": "text/html; charset=utf-8",
+ "content-length": Buffer.byteLength(body)
+ });
+ response.end(body);
+}
+
function readBody(request) {
return new Promise((resolveBody, reject) => {
const chunks = [];
@@ -666,7 +675,7 @@ cases:
return;
}
await readBody(request);
- json(response, 404, { error: { code: "not_found" } });
+ htmlNotFound(response);
}, async (mock) => {
const env = {
...baseEnv,
@@ -755,7 +764,7 @@ cases:
return;
}
await readBody(request);
- json(response, 404, { error: { code: "not_found" } });
+ htmlNotFound(response);
}, async (mock) => {
const env = {
...baseEnv,
@@ -794,7 +803,7 @@ cases:
return;
}
await readBody(request);
- json(response, 404, { error: { code: "not_found" } });
+ htmlNotFound(response);
}, async (mock) => {
const env = {
...baseEnv,
diff --git a/scripts/packed-real-data-privacy.mjs b/scripts/packed-real-data-privacy.mjs
index 414d93f..5118596 100644
--- a/scripts/packed-real-data-privacy.mjs
+++ b/scripts/packed-real-data-privacy.mjs
@@ -387,8 +387,12 @@ async function main() {
response.end(payload);
return;
}
- response.writeHead(404, { "content-type": "application/json" });
- response.end(JSON.stringify({ error: "not found" }));
+ const missing = "404: This page could not be found.";
+ response.writeHead(404, {
+ "content-type": "text/html; charset=utf-8",
+ "content-length": Buffer.byteLength(missing)
+ });
+ response.end(missing);
});
await new Promise((resolveListen) => server.listen(0, "127.0.0.1", resolveListen));
const origin = `http://127.0.0.1:${server.address().port}`;
diff --git a/scripts/packed-report-fixture.mjs b/scripts/packed-report-fixture.mjs
index b5766cf..f2ecf65 100644
--- a/scripts/packed-report-fixture.mjs
+++ b/scripts/packed-report-fixture.mjs
@@ -80,6 +80,15 @@ function send(response, status, value) {
response.end(body);
}
+function sendHtmlNotFound(response) {
+ const body = "404: This page could not be found.";
+ response.writeHead(404, {
+ "content-type": "text/html; charset=utf-8",
+ "content-length": Buffer.byteLength(body)
+ });
+ response.end(body);
+}
+
function cloneMutate(fixtures, name, origin, mutate) {
const fixture = fixtureNamed(fixtures, name, origin);
const body = JSON.parse(JSON.stringify(fixture.body));
@@ -213,7 +222,7 @@ function startFixtureServer(fixtures, producer) {
send(response, fixture.status, fixture.body);
return;
}
- send(response, 404, { error: { code: "NOT_FOUND", message: "missing packed report fixture route" } });
+ sendHtmlNotFound(response);
});
return { httpServer, requests, state };
}
diff --git a/scripts/packed-saved-suite-release.mjs b/scripts/packed-saved-suite-release.mjs
index 697c374..1c5841e 100644
--- a/scripts/packed-saved-suite-release.mjs
+++ b/scripts/packed-saved-suite-release.mjs
@@ -297,6 +297,15 @@ function send(response, status, value) {
response.end(body);
}
+function sendHtmlNotFound(response) {
+ const body = "404: This page could not be found.";
+ response.writeHead(404, {
+ "content-type": "text/html; charset=utf-8",
+ "content-length": Buffer.byteLength(body)
+ });
+ response.end(body);
+}
+
function rewriteOrigin(value, from, to) {
if (typeof value === "string") return value.split(from).join(to);
if (Array.isArray(value)) return value.map((item) => rewriteOrigin(item, from, to));
@@ -491,7 +500,7 @@ function startServer(billing, reports, producer) {
);
return;
}
- send(response, 404, { error: { code: "NOT_FOUND", message: `${request.method} ${url.pathname}` } });
+ sendHtmlNotFound(response);
});
});
return { httpServer, requests, counts };
diff --git a/scripts/smoke-pack.mjs b/scripts/smoke-pack.mjs
index 24076c3..1ed5344 100644
--- a/scripts/smoke-pack.mjs
+++ b/scripts/smoke-pack.mjs
@@ -475,8 +475,12 @@ async function assertPackedSelectionCompile(packedBin, consumerDirectory) {
response.end(payload);
return;
}
- response.writeHead(404, { "content-type": "application/json" });
- response.end(JSON.stringify({ error: { code: "NOT_FOUND", message: "missing" } }));
+ const missing = "404: This page could not be found.";
+ response.writeHead(404, {
+ "content-type": "text/html; charset=utf-8",
+ "content-length": Buffer.byteLength(missing)
+ });
+ response.end(missing);
});
});
diff --git a/src/cloud/client.ts b/src/cloud/client.ts
index 2a745d7..35e4294 100644
--- a/src/cloud/client.ts
+++ b/src/cloud/client.ts
@@ -719,10 +719,7 @@ export class CloudClient {
if (!looksLikeExecutionScopeResponse(value)) return undefined;
return parseExecutionScopeResponse(value);
} catch (error) {
- if (
- error instanceof AwError &&
- (error.details?.["http_status"] === 404 || error.code === "CLOUD_REQUEST_FAILED")
- ) {
+ if (error instanceof AwError && error.details?.["http_status"] === 404) {
return undefined;
}
throw error;
@@ -871,15 +868,20 @@ export class CloudClient {
externalSignal?.removeEventListener("abort", onAbort);
}
let value: unknown;
+ let nonJsonErrorBody = false;
try {
value = text.length === 0 ? undefined : JSON.parse(text);
} catch (error) {
- throw new AwError({
- code: "INVALID_CLOUD_RESPONSE",
- category: "protocol",
- message: "AugmentWorks returned an invalid JSON response.",
- cause: error
- });
+ if (response.ok || errorFamily === "billing") {
+ throw new AwError({
+ code: "INVALID_CLOUD_RESPONSE",
+ category: "protocol",
+ message: "AugmentWorks returned an invalid JSON response.",
+ cause: error
+ });
+ }
+ value = undefined;
+ nonJsonErrorBody = true;
}
if (value !== undefined) assertJsonLimits(value, "AugmentWorks response");
if (response.status === 401 && allowAuthRefresh && this.#accessTokenProvider !== undefined) {
@@ -915,7 +917,7 @@ export class CloudClient {
profileRecoveryUrl(this.apiUrl)
);
}
- throw cloudHttpError(
+ const httpError = cloudHttpError(
response.status,
value,
method,
@@ -923,6 +925,8 @@ export class CloudClient {
profileRecoveryUrl(this.apiUrl),
response.headers.get("retry-after")
);
+ if (!nonJsonErrorBody) throw httpError;
+ throw nameNonJsonRoute(httpError, method, path);
}
if (value === undefined) {
throw new AwError({
@@ -1099,6 +1103,20 @@ function responseTooLarge(): AwError {
});
}
+function nameNonJsonRoute(error: AwError, method: "GET" | "POST", path: string): AwError {
+ if (error.message.includes(method) && error.message.includes(path)) return error;
+ return new AwError({
+ code: error.code,
+ category: error.category,
+ message: `${error.message} ${method} ${path}`,
+ retryable: error.retryable,
+ ...(error.operation === undefined ? {} : { operation: error.operation }),
+ ...(error.commandId === undefined ? {} : { commandId: error.commandId }),
+ ...(error.details === undefined ? {} : { details: error.details }),
+ cause: error
+ });
+}
+
function cloudHttpError(
status: number,
value: unknown,
diff --git a/src/real-data/hosted.ts b/src/real-data/hosted.ts
index 8d9016b..99d1f42 100644
--- a/src/real-data/hosted.ts
+++ b/src/real-data/hosted.ts
@@ -25,29 +25,30 @@ import {
assertBindingMatches
} from "./scope-store.js";
-export async function requireHostedAuthorizedScope(
- cloud: CloudClient,
- ref: SuiteExecutionScopeRef,
+export async function assertHostedScopeUsable(
+ response: ExecutionScopeResponse,
options: {
+ readonly cloud: CloudClient;
readonly workspaceId: string;
readonly resolved?: ResolvedConfig;
readonly signal?: AbortSignal;
readonly now?: number;
+ readonly revision?: number;
+ readonly scopeHash?: string;
}
-): Promise {
- const capabilities = await cloud.getRealDataCapabilities(options.signal);
+): Promise {
+ const capabilities = await options.cloud.getRealDataCapabilities(options.signal);
assertHostedRealDataRelease(capabilities);
- const response = await cloud.getExecutionScope(ref.scopeId, options.signal);
if (response.scope.workspaceId !== options.workspaceId) {
throw realDataError(
"TARGET_SCOPE_MISMATCH",
"The admitted execution scope belongs to a different workspace."
);
}
- if (ref.revision !== undefined && response.scope.revision !== ref.revision) {
+ if (options.revision !== undefined && response.scope.revision !== options.revision) {
throw realDataError("EXECUTION_SCOPE_STALE", "The admitted scope revision does not match the suite binding.");
}
- if (ref.scopeHash !== undefined && response.scope.scopeHash !== ref.scopeHash) {
+ if (options.scopeHash !== undefined && response.scope.scopeHash !== options.scopeHash) {
throw realDataError("EXECUTION_SCOPE_STALE", "The admitted scope hash does not match the suite binding.");
}
if (response.availability === "revoked") {
@@ -76,6 +77,28 @@ export async function requireHostedAuthorizedScope(
if (options.resolved !== undefined) {
assertBoundaryMatchesConfig(response.targetBoundary, options.resolved);
}
+}
+
+export async function requireHostedAuthorizedScope(
+ cloud: CloudClient,
+ ref: SuiteExecutionScopeRef,
+ options: {
+ readonly workspaceId: string;
+ readonly resolved?: ResolvedConfig;
+ readonly signal?: AbortSignal;
+ readonly now?: number;
+ }
+): Promise {
+ const response = await cloud.getExecutionScope(ref.scopeId, options.signal);
+ await assertHostedScopeUsable(response, {
+ cloud,
+ workspaceId: options.workspaceId,
+ ...(options.resolved === undefined ? {} : { resolved: options.resolved }),
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
+ ...(options.now === undefined ? {} : { now: options.now }),
+ ...(ref.revision === undefined ? {} : { revision: ref.revision }),
+ ...(ref.scopeHash === undefined ? {} : { scopeHash: ref.scopeHash })
+ });
return response;
}
@@ -241,11 +264,18 @@ export async function resolveDispatchPolicyForBinding(options: {
return dispatchPolicyFromHostedScope(response);
}
+ if (!packetRequiresHostedScope(options.binding.packet)) {
+ return undefined;
+ }
+
const fromRun = await options.cloud.getRunExecutionScope(options.binding.run_id, options.signal);
if (fromRun !== undefined) {
- if (options.resolved !== undefined) {
- assertBoundaryMatchesConfig(fromRun.targetBoundary, options.resolved);
- }
+ await assertHostedScopeUsable(fromRun, {
+ cloud: options.cloud,
+ workspaceId: options.workspaceId,
+ ...(options.resolved === undefined ? {} : { resolved: options.resolved }),
+ ...(options.signal === undefined ? {} : { signal: options.signal })
+ });
await persistHostedScopeForRun({
runId: options.binding.run_id,
configSha256: options.binding.config_sha256,
@@ -256,11 +286,8 @@ export async function resolveDispatchPolicyForBinding(options: {
return dispatchPolicyFromHostedScope(fromRun);
}
- if (packetRequiresHostedScope(options.binding.packet)) {
- throw realDataError(
- "INVALID_EXECUTION_SCOPE",
- "This authorized run has no persisted or server-admitted execution scope. Re-admit from the original suite, assessment, or manifest; do not reconstruct authority from YAML."
- );
- }
- return undefined;
+ throw realDataError(
+ "INVALID_EXECUTION_SCOPE",
+ "This authorized run has no persisted or server-admitted execution scope. Re-admit from the original suite, assessment, or manifest; do not reconstruct authority from YAML."
+ );
}
diff --git a/src/real-data/index.ts b/src/real-data/index.ts
index f97bf33..1f3be22 100644
--- a/src/real-data/index.ts
+++ b/src/real-data/index.ts
@@ -105,6 +105,7 @@ export {
localBinding
} from "./scope-store.js";
export {
+ assertHostedScopeUsable,
requireHostedAuthorizedScope,
dispatchPolicyFromHostedScope,
persistHostedScopeForRun,
diff --git a/test/catalog/cli.test.ts b/test/catalog/cli.test.ts
index d5b79ed..5c8a722 100644
--- a/test/catalog/cli.test.ts
+++ b/test/catalog/cli.test.ts
@@ -66,7 +66,12 @@ describe("catalog CLI", () => {
send(response, 200, current?.response, current?.headers);
return;
}
- send(response, 404, { error: { code: "not_found", message: "missing" } });
+ const missing = "404: This page could not be found.";
+ response.writeHead(404, {
+ "content-type": "text/html; charset=utf-8",
+ "content-length": Buffer.byteLength(missing)
+ });
+ response.end(missing);
});
const state = await mkdtemp(join(tmpdir(), "aw-catalog-"));
temporaryDirectories.push(state);
diff --git a/test/cloud/request-errors.test.ts b/test/cloud/request-errors.test.ts
new file mode 100644
index 0000000..19be00d
--- /dev/null
+++ b/test/cloud/request-errors.test.ts
@@ -0,0 +1,59 @@
+import { describe, expect, it, vi } from "vitest";
+
+import { CloudClient } from "../../src/cloud/client.js";
+
+const HTML_NOT_FOUND = "404: This page could not be found.";
+
+function htmlResponse(status: number): Response {
+ return new Response(HTML_NOT_FOUND, {
+ status,
+ headers: { "content-type": "text/html; charset=utf-8" }
+ });
+}
+
+function client(fetchMock: typeof fetch): CloudClient {
+ return new CloudClient({
+ apiUrl: "http://127.0.0.1:8787",
+ accessToken: "project-token",
+ fetch: fetchMock
+ });
+}
+
+describe("cloud HTML error pages", () => {
+ it("keeps http_status on an HTML 404 instead of INVALID_CLOUD_RESPONSE", async () => {
+ const fetchMock = vi.fn(async () => htmlResponse(404));
+ const error = await client(fetchMock).getRunStatus("run-html").then(
+ () => {
+ throw new Error("expected HTML 404 to fail");
+ },
+ (caught: unknown) => caught
+ );
+ expect(error).toMatchObject({
+ code: "CLOUD_REQUEST_FAILED",
+ details: {
+ http_status: 404,
+ http_method: "GET",
+ http_path: "/v1/relay/runs/run-html"
+ }
+ });
+ expect(error).not.toMatchObject({ code: "INVALID_CLOUD_RESPONSE" });
+ expect((error as { message: string }).message).toContain("GET /v1/relay/runs/run-html");
+ });
+
+ it("still rejects an HTML 200 as INVALID_CLOUD_RESPONSE", async () => {
+ const fetchMock = vi.fn(async () => htmlResponse(200));
+ await expect(client(fetchMock).getRunStatus("run-html")).rejects.toMatchObject({
+ code: "INVALID_CLOUD_RESPONSE",
+ message: "AugmentWorks returned an invalid JSON response."
+ });
+ });
+
+ it("keeps a billing HTML error body on the existing invalid JSON path", async () => {
+ const fetchMock = vi.fn(async () => htmlResponse(502));
+ await expect(client(fetchMock).getBillingUsage()).rejects.toMatchObject({
+ code: "INVALID_CLOUD_RESPONSE",
+ message: "AugmentWorks returned an invalid JSON response."
+ });
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/test/investigation/admission.test.ts b/test/investigation/admission.test.ts
index 47539fd..5d66ead 100644
--- a/test/investigation/admission.test.ts
+++ b/test/investigation/admission.test.ts
@@ -4,6 +4,7 @@ import { resolve } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { runEstimate, runTest } from "../../src/commands/test.js";
+import type { RelayRunner } from "../../src/relay/runner.js";
import { runReleasePolicyCommand } from "../../src/baseline/execute.js";
import { inspectInvestigation, fetchInvestigation } from "../../src/investigation/execute.js";
import { canonicalize, sha256 } from "../../src/util/canonical.js";
@@ -606,4 +607,87 @@ describe("investigation reproduction admission", () => {
expect(paths.some((path) => path.startsWith("POST /v1/runs/"))).toBe(true);
expect(paths.some((path) => path.includes("/v1/relay/runs"))).toBe(false);
});
+
+ it("completes an investigation rerun against an HTML 404 fallback", async () => {
+ const cwd = await tempDir("aw-inv-html-");
+ temporaryDirectories.push(cwd);
+ await copyInvestigation("response-only.json", cwd, "investigation.json");
+ const paths: string[] = [];
+ const fetchMock = vi.fn(async (input, init) => {
+ const url = new URL(String(input));
+ paths.push(`${init?.method ?? "GET"} ${url.pathname}`);
+ const body = init?.body === undefined ? undefined : (JSON.parse(String(init.body)) as Record);
+ if (url.pathname === `/v1/suites/${FAQ_SUITE_ID}/revisions/${FAQ_REVISION}`) {
+ return Response.json({
+ suiteId: FAQ_SUITE_ID,
+ revisionId: FAQ_REVISION,
+ contentHash: FAQ_HASH,
+ canonicalDocument: faqPinnedDocument()
+ });
+ }
+ if (url.pathname === "/v1/billing/capabilities") {
+ return Response.json(billingFixtures.fixtures["eligible_trial"]?.response);
+ }
+ if (url.pathname === "/v1/billing/quote") {
+ return Response.json(billingFixtures.fixtures["quote_success_with_balance"]?.response);
+ }
+ if (url.pathname === "/v1/relay/runs") {
+ const request = body ?? {};
+ return Response.json({
+ protocol_version: "aw-relay/0.3",
+ create_request_id: request["create_request_id"],
+ create_request_sha256: sha256(canonicalize(request)),
+ create_disposition: "created",
+ run_id: "run-repro-faq",
+ session_id: "session-1",
+ packet: { key: "aw-customer-suite", version: "1.0.0", sha256: "a".repeat(64) },
+ config_sha256: request["config_sha256"],
+ fencing_epoch: 1,
+ status: "running",
+ dashboard_url: "http://127.0.0.1:8787/portal/runs/run-repro-faq",
+ run_expires_at: "2099-09-06T00:00:00.000Z",
+ credit_state: "reserved"
+ });
+ }
+ const missing = "404: This page could not be found.";
+ return new Response(missing, {
+ status: 404,
+ headers: { "content-type": "text/html; charset=utf-8" }
+ });
+ });
+ const result = await runTest(
+ {
+ cwd,
+ investigation: "investigation.json",
+ maxCredits: "30",
+ yes: true,
+ stateDirectory: cwd,
+ handleSignals: false,
+ env: { AUGMENTWORKS_API_URL: "http://127.0.0.1:8787" }
+ },
+ {
+ doctor: doctorFor(),
+ isInteractive: () => false,
+ accessToken: async () => "token",
+ identity: async () => identity(),
+ cloud: hostedCloud(fetchMock),
+ runner: () =>
+ ({
+ run: async () => ({
+ protocol_version: "aw-relay/0.1",
+ run_id: "run-repro-faq",
+ status: "completed",
+ credit_state: "reserved",
+ outcome: "passed"
+ }),
+ requestCancellation: async () => {
+ throw new Error("investigation rerun must not cancel");
+ }
+ }) as unknown as RelayRunner
+ }
+ );
+ expect(result.binding.run_id).toBe("run-repro-faq");
+ expect(result.run.status).toBe("completed");
+ expect(paths.some((path) => path.includes("execution-scope"))).toBe(false);
+ });
});
diff --git a/test/real-data/contract.test.ts b/test/real-data/contract.test.ts
index d77f290..32eecb9 100644
--- a/test/real-data/contract.test.ts
+++ b/test/real-data/contract.test.ts
@@ -3,15 +3,19 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
-import { afterEach, describe, expect, it } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { CloudClient } from "../../src/cloud/client.js";
+import { resolveConfig } from "../../src/config/resolve.js";
+import type { AugmentWorksConfig } from "../../src/config/types.js";
import { AW_REAL_DATA_CONTRACT } from "../../src/real-data/generated/contract.js";
import {
parseCapabilities,
parseExecutionScope,
parseExecutionScopeResponse,
parseLocalExecutionScope,
- looksLikeHostedExecutionScope
+ looksLikeHostedExecutionScope,
+ type ExecutionScopeResponse
} from "../../src/real-data/documents.js";
import { assertHostedRealDataRelease, DISABLED_REAL_DATA_CAPABILITIES } from "../../src/real-data/capabilities.js";
import { applyRedactionProfile, failClosedPrivacyService, minimizeForUpload, registerPrivacyService } from "../../src/real-data/privacy.js";
@@ -29,6 +33,7 @@ import type { RelayCommand } from "../../src/cloud/protocol.js";
import { AwError } from "../../src/errors.js";
import {
ADVERTISED_DISABLED_CAPABILITIES,
+ ENABLED_CAPABILITIES,
FUTURE,
hostedScopeResponse,
localAuthorizedPacketManifest,
@@ -36,6 +41,7 @@ import {
canaryRedactionPolicy,
sealedBoundary,
sealedLocalScope,
+ UUID_A,
UUID_C
} from "./helpers.js";
@@ -363,3 +369,255 @@ describe("local authorized packets and private targets", () => {
).rejects.toMatchObject({ code: "INVALID_EXECUTION_SCOPE" });
});
});
+
+const HTML_NOT_FOUND = "404: This page could not be found.";
+
+function resolvedTarget(baseUrl: string, path = "/chat") {
+ const config: AugmentWorksConfig = {
+ version: 1,
+ target: {
+ name: "support",
+ connector: "http",
+ base_url: baseUrl,
+ operations: {
+ send: {
+ method: "POST",
+ path,
+ request: { message: "$input.message" },
+ response: { content: "$.answer" }
+ }
+ }
+ }
+ };
+ const inspection = resolveConfig(config, "/tmp/augmentworks.yaml", "/tmp", {});
+ if (inspection.resolvedConfig === undefined) {
+ throw new Error(`test config did not resolve: ${JSON.stringify(inspection.diagnostics)}`);
+ }
+ return inspection.resolvedConfig;
+}
+
+function runBinding(packet: { readonly key: string; readonly version: string }, runId = "run-scope") {
+ return {
+ protocol_version: "aw-relay/0.1" as const,
+ create_request_id: "crq_aaaaaaaaaaaaaaaaaaaaaaaa",
+ create_request_sha256: "c".repeat(64),
+ create_disposition: "created" as const,
+ run_id: runId,
+ session_id: "session-1",
+ packet: { ...packet, sha256: "d".repeat(64) },
+ config_sha256: "a".repeat(64),
+ fencing_epoch: 1,
+ status: "connected" as const,
+ dashboard_url: `https://example.test/runs/${runId}`,
+ run_expires_at: FUTURE,
+ credit_state: "reserved" as const
+ };
+}
+
+describe("hosted run scope lookup", () => {
+ it("does not request a scope for a synthetic or v2 binding", async () => {
+ const calls: string[] = [];
+ const cloud = {
+ async getRealDataCapabilities() {
+ calls.push("capabilities");
+ return ENABLED_CAPABILITIES;
+ },
+ async getRunExecutionScope() {
+ calls.push("scope");
+ return hostedScopeResponse();
+ }
+ };
+ for (const packet of [
+ { key: "support-refunds", version: "0.1.0" },
+ { key: "aw-customer-suite", version: "2.0.0" }
+ ]) {
+ await expect(
+ resolveDispatchPolicyForBinding({
+ cloud: cloud as never,
+ binding: runBinding(packet),
+ workspaceId: UUID_C
+ })
+ ).resolves.toBeUndefined();
+ }
+ expect(calls).toEqual([]);
+ });
+
+ it("uses a looked-up scope that passes the shared checks", async () => {
+ const directory = await mkdtemp(join(tmpdir(), "aw-scope-lookup-"));
+ temporaryDirectories.push(directory);
+ const response = hostedScopeResponse();
+ const calls: string[] = [];
+ const cloud = {
+ async getRealDataCapabilities() {
+ calls.push("capabilities");
+ return ENABLED_CAPABILITIES;
+ },
+ async getRunExecutionScope() {
+ calls.push("scope");
+ return response;
+ }
+ };
+ const policy = await resolveDispatchPolicyForBinding({
+ cloud: cloud as never,
+ binding: runBinding({ key: "aw-customer-suite", version: "3.0.0" }, "run-looked-up"),
+ workspaceId: UUID_C,
+ resolved: resolvedTarget("https://support.example.com"),
+ stateDirectory: directory
+ });
+ expect(calls).toEqual(["scope", "capabilities"]);
+ expect(policy).toMatchObject({
+ kind: "authorized",
+ origin: "https://support.example.com"
+ });
+ });
+
+ it.each([
+ {
+ name: "release capability",
+ code: "EXECUTION_RELEASE_UNAVAILABLE",
+ capabilities: ADVERTISED_DISABLED_CAPABILITIES,
+ response: () => hostedScopeResponse()
+ },
+ {
+ name: "workspace",
+ code: "TARGET_SCOPE_MISMATCH",
+ workspaceId: UUID_A,
+ response: () => hostedScopeResponse()
+ },
+ {
+ name: "revoked availability",
+ code: "TARGET_AUTHORITY_REVOKED",
+ response: () => ({ ...hostedScopeResponse(), availability: "revoked" as const })
+ },
+ {
+ name: "expired availability",
+ code: "TARGET_AUTHORITY_EXPIRED",
+ response: () => ({ ...hostedScopeResponse(), availability: "expired" as const })
+ },
+ {
+ name: "release-unavailable availability",
+ code: "EXECUTION_RELEASE_UNAVAILABLE",
+ response: () => ({ ...hostedScopeResponse(), availability: "release_unavailable" as const })
+ },
+ {
+ name: "authority verification",
+ code: "TARGET_AUTHORITY_REQUIRED",
+ response: (): ExecutionScopeResponse => {
+ const response = hostedScopeResponse();
+ return {
+ ...response,
+ authority: { ...response.authority, verificationStatus: "pending" }
+ };
+ }
+ },
+ {
+ name: "scope expiry",
+ code: "TARGET_AUTHORITY_EXPIRED",
+ response: (): ExecutionScopeResponse => {
+ const response = hostedScopeResponse();
+ return { ...response, scope: { ...response.scope, expiresAt: "2000-01-01T00:00:00Z" } };
+ }
+ },
+ {
+ name: "authority expiry",
+ code: "TARGET_AUTHORITY_EXPIRED",
+ response: (): ExecutionScopeResponse => {
+ const response = hostedScopeResponse();
+ return {
+ ...response,
+ authority: { ...response.authority, expiresAt: "2000-01-01T00:00:00Z" }
+ };
+ }
+ },
+ {
+ name: "target boundary",
+ code: "TARGET_SCOPE_MISMATCH",
+ resolved: true,
+ response: () => hostedScopeResponse()
+ }
+ ])("rejects a looked-up scope that fails $name", async (entry) => {
+ const cloud = {
+ async getRealDataCapabilities() {
+ return entry.capabilities ?? ENABLED_CAPABILITIES;
+ },
+ async getRunExecutionScope() {
+ return entry.response();
+ }
+ };
+ await expect(
+ resolveDispatchPolicyForBinding({
+ cloud: cloud as never,
+ binding: runBinding({ key: "aw-customer-suite", version: "3.0.0" }),
+ workspaceId: entry.workspaceId ?? UUID_C,
+ ...(entry.resolved === true ? { resolved: resolvedTarget("https://other.example.com") } : {})
+ })
+ ).rejects.toMatchObject({ code: entry.code });
+ });
+
+ it("turns an HTML 404 from the lookup into INVALID_EXECUTION_SCOPE", async () => {
+ const fetchMock = vi.fn(async () =>
+ new Response(HTML_NOT_FOUND, {
+ status: 404,
+ headers: { "content-type": "text/html; charset=utf-8" }
+ })
+ );
+ const cloud = new CloudClient({
+ apiUrl: "http://127.0.0.1:8787",
+ accessToken: "token",
+ fetch: fetchMock
+ });
+ await expect(
+ resolveDispatchPolicyForBinding({
+ cloud,
+ binding: runBinding({ key: "aw-customer-suite", version: "3.0.0" }, "run-html-404"),
+ workspaceId: UUID_C
+ })
+ ).rejects.toMatchObject({ code: "INVALID_EXECUTION_SCOPE" });
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ expect(String(fetchMock.mock.calls[0]?.[0])).toContain("/v1/relay/runs/run-html-404/execution-scope");
+ });
+
+ it("propagates a 503 from the lookup", async () => {
+ const fetchMock = vi.fn(async () =>
+ Response.json({ error: "unavailable" }, { status: 503 })
+ );
+ const cloud = new CloudClient({
+ apiUrl: "http://127.0.0.1:8787",
+ accessToken: "token",
+ fetch: fetchMock
+ });
+ await expect(
+ resolveDispatchPolicyForBinding({
+ cloud,
+ binding: runBinding({ key: "aw-customer-suite", version: "3.0.0" }, "run-503"),
+ workspaceId: UUID_C
+ })
+ ).rejects.toMatchObject({
+ code: "CLOUD_REQUEST_FAILED",
+ details: { http_status: 503 }
+ });
+ });
+
+ it("still rejects a stale pinned revision before availability", async () => {
+ const response = hostedScopeResponse();
+ const cloud = {
+ async getRealDataCapabilities() {
+ return ENABLED_CAPABILITIES;
+ },
+ async getExecutionScope() {
+ return { ...response, availability: "revoked" as const };
+ }
+ };
+ await expect(
+ requireHostedAuthorizedScope(
+ cloud as never,
+ {
+ schemaVersion: "aw-execution-scope/1",
+ scopeId: response.scope.scopeId,
+ revision: response.scope.revision + 1
+ },
+ { workspaceId: UUID_C }
+ )
+ ).rejects.toMatchObject({ code: "EXECUTION_SCOPE_STALE" });
+ });
+});
diff --git a/test/recovery/recovery.test.ts b/test/recovery/recovery.test.ts
index c34c54c..d3cd41c 100644
--- a/test/recovery/recovery.test.ts
+++ b/test/recovery/recovery.test.ts
@@ -998,6 +998,87 @@ describe("CLI recovery (B04)", () => {
expect(targetRuns).toBe(1);
expect(report.outcome).toBe("resumed");
});
+
+ it("completes recover --resume for a synthetic run against an HTML 404 fallback", async () => {
+ const stateDirectory = await stateDir();
+ const resolved = resolvedConfig();
+ const boundary = targetBoundarySha256(resolved);
+ const store = new RunIntentStore({
+ apiOrigin: new URL("http://127.0.0.1:8787/"),
+ tenant: { workspace_id: "workspace-1", connector_id: "connector-1" },
+ stateDirectory,
+ createRequestId: () => `crq_${"k".repeat(32)}`
+ });
+ await store.open();
+ const loaded = await store.loadOrCreate({
+ protocol_version: RELAY_PROTOCOL_VERSION,
+ packet: { key: PACKET.key, version: PACKET.version },
+ config_sha256: resolved.configDigest,
+ target: {
+ name: resolved.config.target.name,
+ boundary_sha256: boundary,
+ capabilities: {
+ prepare: false,
+ observation: false,
+ cleanup: false,
+ tool_events: false,
+ observation_keys: []
+ }
+ }
+ });
+ await store.bind(
+ bindingFor(loaded.intent.request as unknown as Record, {
+ config_sha256: loaded.intent.request.config_sha256,
+ status: "running",
+ credit_state: "consumed"
+ }) as never
+ );
+ await store.close();
+
+ const paths: string[] = [];
+ const fetchMock = vi.fn(async (input) => {
+ const url = new URL(String(input));
+ paths.push(url.pathname);
+ if (url.pathname === "/v1/relay/runs") {
+ throw new Error("resume must not create");
+ }
+ if (url.pathname === "/v1/relay/runs/run-1") {
+ return Response.json(statusFor({ status: "running", outcome: null, credit_state: "consumed" }));
+ }
+ const body = "404: This page could not be found.";
+ return new Response(body, {
+ status: 404,
+ headers: { "content-type": "text/html; charset=utf-8" }
+ });
+ });
+ const report = await runRecover(
+ { resume: true, stateDirectory, env: {}, handleSignals: false },
+ {
+ doctor: doctorFor(),
+ apiOrigin: () => new URL("http://127.0.0.1:8787/"),
+ accessToken: async () => "token",
+ identity: async () => identity(),
+ cloud: ({ accessToken, accessTokenProvider }) =>
+ new CloudClient({
+ apiUrl: "http://127.0.0.1:8787/",
+ accessToken,
+ accessTokenProvider,
+ fetch: fetchMock
+ }),
+ runner: () =>
+ ({
+ run: async () => statusFor(),
+ requestCancellation: async () => {
+ throw new Error("resume must not cancel");
+ }
+ }) as unknown as RelayRunner,
+ stdout: { write: () => true },
+ stderr: { write: () => true }
+ }
+ );
+ expect(report.outcome).toBe("resumed");
+ expect(paths.some((path) => path.includes("execution-scope"))).toBe(false);
+ });
});
describe("generic create errors are not treated as no-create", () => {
diff --git a/test/selection/execution.test.ts b/test/selection/execution.test.ts
index c60f9e3..abaeb9c 100644
--- a/test/selection/execution.test.ts
+++ b/test/selection/execution.test.ts
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it, vi } from "vitest";
import { CloudClient } from "../../src/cloud/client.js";
+import type { RelayRunner } from "../../src/relay/runner.js";
import {
createTestCommand,
installSelectionExecutionInterruptHandler,
@@ -44,6 +45,7 @@ import {
SelectionExecutionSchema,
type SuiteSelectionManifest
} from "../../src/selection/schema.js";
+import { CANONICAL_HASH, SUITE_ID, SUITE_REVISION, savedSuiteManifest } from "./saved-suite-fixtures.js";
const projectRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
const fixtures = JSON.parse(
@@ -1472,4 +1474,80 @@ describe("selection execution tenant pinning", () => {
expect(second.execution.shards.every((shard) => shard.runId === null)).toBe(true);
await second.lock.release();
});
+
+ it("completes a saved v2 suite run against an HTML 404 fallback", async () => {
+ const cwd = await stateDir();
+ const stateDirectory = await stateDir();
+ const manifest = savedSuiteManifest();
+ await writeFile(join(cwd, "suite-selection.manifest.json"), `${JSON.stringify(manifest)}\n`, "utf8");
+ const apiOrigin = new URL("http://127.0.0.1:8787/");
+ const paths: string[] = [];
+ const fetchMock = vi.fn(async (input, init) => {
+ const url = new URL(String(input));
+ paths.push(`${init?.method ?? "GET"} ${url.pathname}`);
+ if (url.pathname === "/v1/billing/capabilities") {
+ return Response.json(billingFixtures.fixtures["eligible_trial"]?.response);
+ }
+ if (url.pathname === "/v1/billing/quote") {
+ return Response.json(billingFixtures.fixtures["quote_success_with_balance"]?.response);
+ }
+ if (url.pathname === "/v1/relay/runs" && (init?.method ?? "GET") === "POST") {
+ const request = JSON.parse(String(init?.body)) as Record;
+ return Response.json({
+ ...createRunJson(request, "run-saved-v2", apiOrigin),
+ status: "running"
+ });
+ }
+ if (url.pathname === `/v1/suites/${SUITE_ID}/revisions/${SUITE_REVISION}`) {
+ return Response.json({
+ suiteId: SUITE_ID,
+ revisionId: SUITE_REVISION,
+ contentHash: CANONICAL_HASH,
+ canonicalDocument: { schemaVersion: "aw-customer-suite/2", syntheticOnly: false }
+ });
+ }
+ const missing = "404: This page could not be found.";
+ return new Response(missing, {
+ status: 404,
+ headers: { "content-type": "text/html; charset=utf-8" }
+ });
+ });
+ const result = await runTest(
+ {
+ manifest: "suite-selection.manifest.json",
+ allShards: true,
+ maxCredits: "100",
+ yes: true,
+ cwd,
+ stateDirectory,
+ handleSignals: false,
+ env: { CI: "1", AUGMENTWORKS_API_URL: apiOrigin.origin }
+ },
+ {
+ doctor: doctorFor(),
+ apiOrigin: () => apiOrigin,
+ accessToken: async () => "token",
+ identity: async () => hostedIdentity(),
+ cloud: (options) =>
+ new CloudClient({
+ apiUrl: options.apiOrigin,
+ accessToken: options.accessToken,
+ accessTokenProvider: options.accessTokenProvider,
+ fetch: fetchMock
+ }),
+ runner: () =>
+ ({
+ run: async () => runStatusJson("run-saved-v2", apiOrigin),
+ requestCancellation: async () => {
+ throw new Error("saved suite run must not cancel");
+ }
+ }) as unknown as RelayRunner,
+ stdout: { write: () => true },
+ stderr: { write: () => true }
+ }
+ );
+ expect(result.binding.run_id).toBe("run-saved-v2");
+ expect(result.run.status).toBe("completed");
+ expect(paths.some((path) => path.includes("execution-scope"))).toBe(false);
+ });
});