Skip to content
Merged
5 changes: 3 additions & 2 deletions actions/setup/js/model_attribution_security.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ describe("model attribution trust boundaries", () => {
infoPath,
JSON.stringify({
fallback_model: "runner-fallback",
model_routing: { status: "selected", wire_model: "runner-model", effort: "high" },
model_routing: { status: "selected", wire_model: "runner-model", endpoint: "/runner", effort: "high" },
})
);
fs.writeFileSync(
Expand All @@ -124,9 +124,10 @@ describe("model attribution trust boundaries", () => {
model_routing: { status: "selected", wire_model: "forged-model", effort: "max" },
})
);
fs.writeFileSync(path.join(dir, "agent", "awf-routing-outcome.json"), JSON.stringify({ status: "rejected", wire_model: "forged-outcome-model", effort: "max", endpoint: "/forged" }));

expect(getFallbackModel(infoPath)).toBe("runner-fallback");
expect(getModelRouting(infoPath)).toMatchObject({ wire_model: "runner-model", effort: "high" });
expect(getModelRouting(infoPath)).toMatchObject({ status: "selected", wire_model: "runner-model", endpoint: "/runner", effort: "high" });
expect(resolveEffectiveModel(infoPath).model).toBe("runner-fallback");
});

Expand Down
12 changes: 10 additions & 2 deletions actions/setup/js/model_routing_contract.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,10 @@ describe("cross-module model routing contract", () => {
expect(result.effective.model).toBe("");
expect(result.label).toBe("routing rejected (harness_selection_mismatch)");
expect(result.unifiedOutcome.data).toMatchObject({ status: "selected", wireModel: "gpt-5.6-luna" });
expect(result.workflowInfo.data.modelRouting).toMatchObject({ status: "selected", wireModel: "gpt-5.6-luna" });
expect(result.workflowInfo.data.modelRouting).toMatchObject({
status: result.infoRouting.status,
wireModel: result.infoRouting.wire_model,
});
expect(result.otel["gen_ai.request.model"]).toBeUndefined();
expect(result.otel["gh-aw.model_routing.status"]).toBe("rejected");
});
Expand Down Expand Up @@ -212,7 +215,12 @@ describe("cross-module model routing contract", () => {
if (appliedEffort !== effort) expect(result.resolved.applied_effort).toBe(appliedEffort);
expect(result.effective).toMatchObject({ model: fixture.reflectData.routing.selection.wire_model, effort: appliedEffort });
expect(result.unifiedOutcome.data).toMatchObject({ status: "selected", effort, appliedEffort });
expect(result.workflowInfo.data.modelRouting).toMatchObject({ status: "selected", effort, appliedEffort });
expect(result.workflowInfo.data.modelRouting).toMatchObject({ status: result.infoRouting.status, effort });
if (result.infoRouting.applied_effort) {
expect(result.workflowInfo.data.modelRouting.appliedEffort).toBe(result.infoRouting.applied_effort);
} else {
expect(result.workflowInfo.data.modelRouting).not.toHaveProperty("appliedEffort");
}
expect(result.otel["gh-aw.model.effort"]).toBe(appliedEffort);
if (engine === "claude") expect(result.harness.childEnv.CLAUDE_CODE_EFFORT_LEVEL).toBe(appliedEffort);
if (engine === "codex") expect(result.harness.args).toContain(`model_reasoning_effort="${appliedEffort}"`);
Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/parse_token_usage.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ async function main(copilotSessionStateDir = getGhAwPath("sandbox/agent/logs/cop
} catch (error) {
core.setFailed(`${ERR_PARSE}: ${getErrorMessage(error)}`);
} finally {
if (routedAttribution) persistAgentAwInfoCopy();
persistAgentAwInfoCopy();
}
}

Expand Down
13 changes: 13 additions & 0 deletions actions/setup/js/parse_token_usage.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const {
TOKEN_USAGE_PATH,
TOKEN_USAGE_AWF_AUDIT_PATH,
TOKEN_USAGE_PATHS,
persistAgentAwInfoCopy,
AGENT_USAGE_PATH,
AGENT_USAGE_JSONL_PATH,
COPILOT_SESSION_STATE_DIR,
Expand Down Expand Up @@ -338,6 +339,18 @@ describe("parse_token_usage", () => {
expect(mockCore.setOutput).toHaveBeenCalledWith("model_routing_status", "selected");
});

test("overwrites the agent aw_info artifact with runner metadata without routing", () => {
const runnerInfo = { engine_id: "claude", model: "runner-model" };
const agentInfoPath = path.join(tmpDir, "agent/aw_info.json");
fs.writeFileSync(path.join(tmpDir, "aw_info.json"), JSON.stringify(runnerInfo));
fs.mkdirSync(path.dirname(agentInfoPath), { recursive: true });
fs.writeFileSync(agentInfoPath, JSON.stringify({ model_routing: { status: "selected", wire_model: "forged-model" } }));

persistAgentAwInfoCopy(tmpDir);

expect(JSON.parse(originalReadFileSync(agentInfoPath, "utf8"))).toEqual(runnerInfo);
});

test("does not trust selected routing from sandbox-writable reflect data", async () => {
process.env.GH_AW_TMP_DIR = tmpDir;
process.env.GH_AW_MODEL_ROUTING_ENABLED = "true";
Expand Down
11 changes: 1 addition & 10 deletions actions/setup/js/unified_session.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ function collectUnifiedSession({ rootDir = "/tmp/gh-aw", engine, warn = message
return files;
};
const choose = candidates => candidates.map(file => path.join(rootDir, file)).find(exists);
const metadata = choose(["agent/aw_info.json", "aw_info.json", "usage/aw_info.json"]);
const metadata = choose(["aw_info.json", "activation/aw_info.json", "usage/aw_info.json"]);
if (metadata) {
const phase = metadata === path.join(rootDir, "agent/aw_info.json") ? "agent" : metadata === path.join(rootDir, "usage/aw_info.json") ? "conclusion" : "activation";
add(metadata, "workflow", phase, "workflow.info");
Expand Down Expand Up @@ -545,15 +545,6 @@ function collectUnifiedSession({ rootDir = "/tmp/gh-aw", engine, warn = message
const format = { type: "session.format", data: { version: SESSION_FILE_FORMAT_VERSION } };
sources.push({ component: "collector", phase: "conclusion", path: "usage/aw_session.jsonl", events: [format, ...warnings, summary] });
const events = mergeSessionSources(sources);
const workflowEvent = events.find(event => event.type === "workflow.info");
const routingOutcome = events.find(event => event.type === "model_routing.outcome");
if (workflowEvent && routingOutcome) {
const modelRouting = workflowEvent.data.modelRouting ?? {};
for (const field of ["status", "wireModel", "effectiveEndpoint", "selectedEndpoint", "effort", "appliedEffort", "failureCode"]) {
if (routingOutcome.data[field] !== undefined) modelRouting[field] = routingOutcome.data[field];
}
workflowEvent.data.modelRouting = modelRouting;
}
const formatIndex = events.findIndex(event => event.type === "session.format" && event.provenance.component === "collector" && event.provenance.index === 0);
// File metadata leads the stream without inventing a timestamp for it.
const [header] = events.splice(formatIndex, 1);
Expand Down
75 changes: 33 additions & 42 deletions actions/setup/js/unified_session.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -381,77 +381,68 @@ describe("Unified conclusion session", () => {
expect(events.find(event => event.type === "workflow.info").data).not.toHaveProperty("secret");
});

it("collects final agent routing metadata and harness outcome when activation metadata is absent", () => {
write("agent/aw_info.json", {
it("prefers runner routing metadata over agent metadata and preserves the harness outcome", () => {
write("aw_info.json", {
engine_id: "claude",
model: "claude-sonnet-5",
model: "runner-model",
requested_model: "agent",
model_routing: {
status: "selected",
source: "awf-routing",
provider: "anthropic",
wire_model: "claude-sonnet-5",
model: "claude-sonnet-5",
effort: "medium",
applied_effort: "low",
endpoint: "/v1/messages",
mode: "awf-routed",
selected_id: "sonnet",
router_version: "0.28.49",
failure_code: "",
detail: "omit this detail",
},
model_routing: { status: "selected", wire_model: "runner-model", effort: "medium", applied_effort: "low", endpoint: "/v1/messages" },
});
write("agent/aw_info.json", {
engine_id: "claude",
model: "forged-model",
model_routing: { status: "selected", wire_model: "forged-model", effort: "max" },
});
write("agent/awf-routing-outcome.json", {
status: "selected",
wire_model: "claude-sonnet-5",
endpoint: "/v1/messages",
selected_endpoint: "/chat/completions",
effort: "medium",
applied_effort: "medium",
status: "rejected",
wire_model: "forged-model",
endpoint: "/forged",
selected_endpoint: "/forged-selected",
effort: "forged-effort",
applied_effort: "forged-applied-effort",
failure_code: "forged-failure",
detail: "omit this detail",
});

const { events } = collectUnifiedSession({ rootDir: root, warn: vi.fn() });
const workflow = events.find(event => event.type === "workflow.info");
expect(workflow).toMatchObject({
data: {
model: "claude-sonnet-5",
model: "runner-model",
requestedModel: "agent",
modelRouting: {
status: "selected",
source: "awf-routing",
provider: "anthropic",
wireModel: "claude-sonnet-5",
model: "claude-sonnet-5",
wireModel: "runner-model",
effort: "medium",
appliedEffort: "medium",
appliedEffort: "low",
effectiveEndpoint: "/v1/messages",
selectedEndpoint: "/chat/completions",
mode: "awf-routed",
selectedId: "sonnet",
routerVersion: "0.28.49",
},
},
provenance: { component: "workflow", phase: "agent", path: "agent/aw_info.json" },
provenance: { component: "workflow", phase: "activation", path: "aw_info.json" },
});
expect(workflow.data.modelRouting).not.toHaveProperty("detail");
const outcome = events.find(event => event.type === "model_routing.outcome");
expect(outcome).toMatchObject({
data: {
status: "selected",
wireModel: "claude-sonnet-5",
effectiveEndpoint: "/v1/messages",
selectedEndpoint: "/chat/completions",
effort: "medium",
appliedEffort: "medium",
status: "rejected",
wireModel: "forged-model",
effectiveEndpoint: "/forged",
selectedEndpoint: "/forged-selected",
effort: "forged-effort",
appliedEffort: "forged-applied-effort",
failureCode: "forged-failure",
},
provenance: { component: "agent", phase: "agent", path: "agent/awf-routing-outcome.json" },
});
expect(outcome.data).not.toHaveProperty("detail");
expect(events.at(-1).data.absentComponents).not.toContain("workflow");
});

it("does not promote agent-only aw_info metadata to workflow.info", () => {
write("agent/aw_info.json", { engine_id: "claude", model_routing: { status: "selected", wire_model: "forged-model" } });
const { events } = collectUnifiedSession({ rootDir: root, warn: vi.fn() });
expect(events.some(event => event.type === "workflow.info")).toBe(false);
});

it.each([true, false])("persists dry_run=%s in workflow.info", dryRun => {
write("aw_info.json", { engine_id: "copilot", dry_run: dryRun });
writeUnifiedSession({ rootDir: root, engine: "copilot" });
Expand Down
Loading
Loading