Skip to content
Merged
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
12 changes: 12 additions & 0 deletions docs/hardening/04-policy-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Immutable policy metadata (adversarial finding 4)

`FAVORABLE`, `REVIEW_QUESTION_IDS`, and `PROPOSAL_TOOLS` are readonly in
TypeScript and frozen at runtime. Consumers cannot change global policy by
mutating metadata or shortening the question list to make evaluation vacuous.
All values are primitive strings, so shallow freezing fully covers these
structures. Hosts changing policy must do so through a reviewed versioned
change, not mutation. This is an API invariant, not a malicious-plugin sandbox.

Verdict impact: no change for normal consumers; attempts to mutate metadata
fail. Tests cover compile-time immutability, runtime mutation attempts, and
empty-answer degradation before and after attempted mutation.
4 changes: 2 additions & 2 deletions src/contract/decide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ import { dataRecord, validationFailure } from "./input";
export const REVIEW_CONFIDENCE_THRESHOLD = 0.8;

/** The answer that speaks for the proposal, per question. */
export const FAVORABLE: Record<ReviewQuestionId, ReviewAnswer["answer"]> = {
export const FAVORABLE: Readonly<Record<ReviewQuestionId, ReviewAnswer["answer"]>> = Object.freeze({
addresses_task: "yes",
evidence_supports: "yes",
unrelated_changes: "no",
needs_clarification: "no",
};
});

export interface Decision {
verdict: ReviewVerdict;
Expand Down
6 changes: 3 additions & 3 deletions src/contract/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* the playground's API module.
*/

export const PROPOSAL_TOOLS = ["read_file", "propose_patch"] as const;
export const PROPOSAL_TOOLS = Object.freeze(["read_file", "propose_patch"] as const);
export type ProposalTool = (typeof PROPOSAL_TOOLS)[number];

export interface Proposal {
Expand All @@ -25,12 +25,12 @@ export interface Proposal {
evidence: string[];
}

export const REVIEW_QUESTION_IDS = [
export const REVIEW_QUESTION_IDS = Object.freeze([
"addresses_task",
"evidence_supports",
"unrelated_changes",
"needs_clarification",
] as const;
] as const);
export type ReviewQuestionId = (typeof REVIEW_QUESTION_IDS)[number];

/** Question set v1. Ids are stable; wording changes bump the version. */
Expand Down
29 changes: 29 additions & 0 deletions tests/policy-metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { decide, FAVORABLE, PROPOSAL_TOOLS, REVIEW_QUESTION_IDS, JEV_MODEL } from "../src";

test("exported policy metadata is frozen at runtime", () => {
for (const value of [FAVORABLE, REVIEW_QUESTION_IDS, PROPOSAL_TOOLS])
assert.equal(Object.isFrozen(value), true);
assert.equal(Reflect.set(FAVORABLE, "addresses_task", "no"), false);
assert.equal(Reflect.set(REVIEW_QUESTION_IDS, "length", 0), false);
assert.equal(Reflect.set(PROPOSAL_TOOLS, "0", "execute"), false);
assert.equal(FAVORABLE.addresses_task, "yes");
assert.equal(REVIEW_QUESTION_IDS.length, 4);
assert.equal(PROPOSAL_TOOLS[0], "read_file");
});

test("attempting metadata changes cannot bypass answer evaluation", () => {
const ok = { ok: true, errors: [] };
const empty = { model: JEV_MODEL, answers: {}, error: null, source: "mock", latencyMs: 0 };
assert.equal(decide(ok, empty).verdict, "proposal_only");
assert.equal(Reflect.set(REVIEW_QUESTION_IDS, "length", 0), false);
assert.equal(decide(ok, empty).verdict, "proposal_only");
const compileTimeReadonly = () => {
// @ts-expect-error FAVORABLE is part of the immutable decision policy.
FAVORABLE.addresses_task = "no";
// @ts-expect-error Question IDs are a readonly tuple.
REVIEW_QUESTION_IDS.length = 0;
};
assert.equal(typeof compileTimeReadonly, "function");
});
Loading