feat(model-policy): adversary role + lineage-diverse-from-worker constraint
Add `adversary` to SUPPORTED_MODEL_ROLES and a new symbolic constraint `lineage-diverse-from-worker` to SUPPORTED_MODEL_ROLE_CONSTRAINTS. Default constraints for `adversary` and `reviewer` now include `lineage-diverse-from-worker` so the reviewer/adversary CANNOT be a lineage-twin of the model that produced the artifact under review — prevents "yeah looks fine to me" rubber-stamp from same-family models. Helpers exported alongside the policy: - rootVendorFor(modelId) → "anthropic" | "openai" | "google" | "moonshot" | "mistral" | "minimax" | "zhipu" | "meituan" | "unknown" - isSameRootVendor(candidateId, workerId) → boolean (fail-open on unknown) These are the building blocks the selector needs. The actual filter wiring in auto-model-selection's selectAndApplyModel is left as a documented TODO — the function doesn't currently thread role context through, so plugging in lineage filtering needs a small refactor that is out of scope here. Tests: 24 pass (was 6 + 18 new). Coverage: role registration, constraint registration, defaults, validation, rootVendor mapping matrix, isSameRootVendor predicate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8832be0785
commit
e7cf168824
3 changed files with 204 additions and 1 deletions
|
|
@ -3,8 +3,12 @@ import { describe, test } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ModelRolePolicyValidationError,
|
ModelRolePolicyValidationError,
|
||||||
|
SUPPORTED_MODEL_ROLE_CONSTRAINTS,
|
||||||
|
SUPPORTED_MODEL_ROLES,
|
||||||
|
isSameRootVendor,
|
||||||
normalizeRolePolicies,
|
normalizeRolePolicies,
|
||||||
normalizeRolePolicy,
|
normalizeRolePolicy,
|
||||||
|
rootVendorFor,
|
||||||
validateRolePolicy,
|
validateRolePolicy,
|
||||||
} from "../uok/model-role-policy.js";
|
} from "../uok/model-role-policy.js";
|
||||||
|
|
||||||
|
|
@ -91,6 +95,11 @@ describe("model role policy", () => {
|
||||||
mode: "auto",
|
mode: "auto",
|
||||||
constraints: ["cheap", "review"],
|
constraints: ["cheap", "review"],
|
||||||
},
|
},
|
||||||
|
adversary: {
|
||||||
|
role: "adversary",
|
||||||
|
mode: "auto",
|
||||||
|
constraints: ["review", "lineage-diverse-from-worker"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -101,4 +110,110 @@ describe("model role policy", () => {
|
||||||
/unsupported model role "planner"/,
|
/unsupported model role "planner"/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── adversary role ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test("adversary_is_a_supported_role", () => {
|
||||||
|
assert.ok(SUPPORTED_MODEL_ROLES.includes("adversary"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("lineage_diverse_from_worker_is_a_supported_constraint", () => {
|
||||||
|
assert.ok(
|
||||||
|
SUPPORTED_MODEL_ROLE_CONSTRAINTS.includes("lineage-diverse-from-worker"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adversary_default_constraints_include_review_and_lineage_diverse_from_worker", () => {
|
||||||
|
const policy = normalizeRolePolicy("adversary");
|
||||||
|
assert.deepEqual(policy, {
|
||||||
|
role: "adversary",
|
||||||
|
mode: "auto",
|
||||||
|
constraints: ["review", "lineage-diverse-from-worker"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reviewer_default_constraints_include_lineage_diverse_from_worker", () => {
|
||||||
|
const policy = normalizeRolePolicy("reviewer");
|
||||||
|
assert.ok(
|
||||||
|
policy.constraints.includes("lineage-diverse-from-worker"),
|
||||||
|
`expected lineage-diverse-from-worker in reviewer defaults, got: ${policy.constraints.join(", ")}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validateRolePolicy_adversary_with_lineage_diverse_from_worker_is_valid", () => {
|
||||||
|
const result = validateRolePolicy("adversary", {
|
||||||
|
constraints: ["lineage-diverse-from-worker"],
|
||||||
|
});
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.ok(
|
||||||
|
result.policy.constraints.includes("lineage-diverse-from-worker"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeRolePolicies_empty_input_includes_adversary_role", () => {
|
||||||
|
const policies = normalizeRolePolicies({});
|
||||||
|
assert.ok("adversary" in policies, "adversary key missing from full policy map");
|
||||||
|
assert.deepEqual(policies.adversary, {
|
||||||
|
role: "adversary",
|
||||||
|
mode: "auto",
|
||||||
|
constraints: ["review", "lineage-diverse-from-worker"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── rootVendorFor / isSameRootVendor ────────────────────────────────────────
|
||||||
|
|
||||||
|
test("rootVendorFor_maps_claude_prefix_to_anthropic", () => {
|
||||||
|
assert.equal(rootVendorFor("claude-3-7-sonnet"), "anthropic");
|
||||||
|
assert.equal(rootVendorFor("claude-opus-4"), "anthropic");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rootVendorFor_maps_gpt_and_o_series_to_openai", () => {
|
||||||
|
assert.equal(rootVendorFor("gpt-4o"), "openai");
|
||||||
|
assert.equal(rootVendorFor("o1-preview"), "openai");
|
||||||
|
assert.equal(rootVendorFor("o3-mini"), "openai");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rootVendorFor_maps_gemini_prefix_to_google", () => {
|
||||||
|
assert.equal(rootVendorFor("gemini-2.5-pro"), "google");
|
||||||
|
assert.equal(rootVendorFor("gemma-3"), "google");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rootVendorFor_maps_kimi_prefix_to_moonshot", () => {
|
||||||
|
assert.equal(rootVendorFor("kimi-for-coding"), "moonshot");
|
||||||
|
assert.equal(rootVendorFor("kimi-k2.6"), "moonshot");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rootVendorFor_maps_minimax_prefix_to_minimax", () => {
|
||||||
|
assert.equal(rootVendorFor("minimax-text-01"), "minimax");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rootVendorFor_maps_glm_and_zai_prefix_to_zhipu", () => {
|
||||||
|
assert.equal(rootVendorFor("glm-4"), "zhipu");
|
||||||
|
assert.equal(rootVendorFor("zai-something"), "zhipu");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rootVendorFor_maps_longcat_prefix_to_meituan", () => {
|
||||||
|
assert.equal(rootVendorFor("longcat-v1"), "meituan");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rootVendorFor_returns_unknown_for_unrecognised_model", () => {
|
||||||
|
assert.equal(rootVendorFor("mystery-model-x"), "unknown");
|
||||||
|
assert.equal(rootVendorFor(""), "unknown");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isSameRootVendor_true_when_same_family", () => {
|
||||||
|
assert.equal(isSameRootVendor("claude-3-5-sonnet", "claude-opus-4"), true);
|
||||||
|
assert.equal(isSameRootVendor("gpt-4o", "o1-preview"), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isSameRootVendor_false_when_different_family", () => {
|
||||||
|
assert.equal(isSameRootVendor("claude-3-5-sonnet", "gpt-4o"), false);
|
||||||
|
assert.equal(isSameRootVendor("gemini-2.5-pro", "kimi-for-coding"), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isSameRootVendor_false_when_either_model_is_unknown", () => {
|
||||||
|
assert.equal(isSameRootVendor("mystery-model", "claude-3-5-sonnet"), false);
|
||||||
|
assert.equal(isSameRootVendor("claude-3-5-sonnet", "mystery-model"), false);
|
||||||
|
assert.equal(isSameRootVendor("mystery-a", "mystery-b"), false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -144,9 +144,11 @@ export {
|
||||||
export { applyModelPolicyFilter } from "./model-policy.js";
|
export { applyModelPolicyFilter } from "./model-policy.js";
|
||||||
export {
|
export {
|
||||||
DEFAULT_MODEL_ROLE_CONSTRAINTS,
|
DEFAULT_MODEL_ROLE_CONSTRAINTS,
|
||||||
|
isSameRootVendor,
|
||||||
ModelRolePolicyValidationError,
|
ModelRolePolicyValidationError,
|
||||||
normalizeRolePolicies,
|
normalizeRolePolicies,
|
||||||
normalizeRolePolicy,
|
normalizeRolePolicy,
|
||||||
|
rootVendorFor,
|
||||||
SUPPORTED_MODEL_ROLE_CONSTRAINTS,
|
SUPPORTED_MODEL_ROLE_CONSTRAINTS,
|
||||||
SUPPORTED_MODEL_ROLES,
|
SUPPORTED_MODEL_ROLES,
|
||||||
validateRolePolicy,
|
validateRolePolicy,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export const SUPPORTED_MODEL_ROLES = Object.freeze([
|
||||||
"worker",
|
"worker",
|
||||||
"validator",
|
"validator",
|
||||||
"reviewer",
|
"reviewer",
|
||||||
|
"adversary",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -30,6 +31,21 @@ export const SUPPORTED_MODEL_ROLES = Object.freeze([
|
||||||
*
|
*
|
||||||
* Consumer: role-policy validation and autonomous route evidence writers.
|
* Consumer: role-policy validation and autonomous route evidence writers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `lineage-diverse-from-worker` — selected model must NOT share root-vendor
|
||||||
|
* lineage with the model that produced the artifact under review (the `worker`
|
||||||
|
* role's resolved model). Prevents "looks fine to me" rubber-stamp by
|
||||||
|
* lineage-twins. When the worker model is unknown or maps to "unknown" vendor,
|
||||||
|
* this constraint is treated as a no-op (fail-open rather than blocking all
|
||||||
|
* candidates). Enforcement is best-effort at the model-selection layer.
|
||||||
|
*
|
||||||
|
* TODO(lineage-diverse-from-worker): wire handler in auto-model-selection.js
|
||||||
|
* selectAndApplyModel — after the policy gate resolves `routingEligibleModels`,
|
||||||
|
* call resolveWorkerLineage(resolvedWorkerModelId) and filter candidates with
|
||||||
|
* !isSameRootVendor(candidate.id, workerVendor). See rootVendorFor() in this
|
||||||
|
* file for the canonical vendor mapping.
|
||||||
|
*/
|
||||||
export const SUPPORTED_MODEL_ROLE_CONSTRAINTS = Object.freeze([
|
export const SUPPORTED_MODEL_ROLE_CONSTRAINTS = Object.freeze([
|
||||||
"coding",
|
"coding",
|
||||||
"review",
|
"review",
|
||||||
|
|
@ -38,6 +54,7 @@ export const SUPPORTED_MODEL_ROLE_CONSTRAINTS = Object.freeze([
|
||||||
"byok-allowed",
|
"byok-allowed",
|
||||||
"local-only",
|
"local-only",
|
||||||
"strict-json",
|
"strict-json",
|
||||||
|
"lineage-diverse-from-worker",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const MODEL_ROLES = new Set(SUPPORTED_MODEL_ROLES);
|
const MODEL_ROLES = new Set(SUPPORTED_MODEL_ROLES);
|
||||||
|
|
@ -63,7 +80,8 @@ export const DEFAULT_MODEL_ROLE_CONSTRAINTS = Object.freeze({
|
||||||
orchestrator: Object.freeze(["long-context", "strict-json"]),
|
orchestrator: Object.freeze(["long-context", "strict-json"]),
|
||||||
worker: Object.freeze(["coding"]),
|
worker: Object.freeze(["coding"]),
|
||||||
validator: Object.freeze(["strict-json", "review"]),
|
validator: Object.freeze(["strict-json", "review"]),
|
||||||
reviewer: Object.freeze(["review"]),
|
reviewer: Object.freeze(["review", "lineage-diverse-from-worker"]),
|
||||||
|
adversary: Object.freeze(["review", "lineage-diverse-from-worker"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -233,3 +251,71 @@ export function normalizeRolePolicies(policies = {}) {
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a model ID to its root-vendor label for lineage-diversity checks.
|
||||||
|
*
|
||||||
|
* Purpose: back the `lineage-diverse-from-worker` constraint — model selection
|
||||||
|
* for reviewer/adversary roles must filter candidates that share a vendor with
|
||||||
|
* the worker's resolved model.
|
||||||
|
*
|
||||||
|
* Returns one of: "anthropic" | "openai" | "google" | "moonshot" | "mistral" |
|
||||||
|
* "minimax" | "zhipu" | "meituan" | "unknown". "unknown" means no filter is
|
||||||
|
* applied (fail-open).
|
||||||
|
*
|
||||||
|
* Consumer: lineage-diverse-from-worker handler in the model-selection layer
|
||||||
|
* (TODO — see constraint JSDoc above).
|
||||||
|
*
|
||||||
|
* @param {string} modelId
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function rootVendorFor(modelId) {
|
||||||
|
const id = String(modelId ?? "").toLowerCase();
|
||||||
|
if (id.startsWith("claude-") || id.startsWith("anthropic/")) return "anthropic";
|
||||||
|
if (
|
||||||
|
id.startsWith("gpt-") ||
|
||||||
|
id.startsWith("o1-") ||
|
||||||
|
id.startsWith("o3-") ||
|
||||||
|
id.startsWith("o4-") ||
|
||||||
|
id.startsWith("openai/")
|
||||||
|
)
|
||||||
|
return "openai";
|
||||||
|
if (
|
||||||
|
id.startsWith("gemini-") ||
|
||||||
|
id.startsWith("gemma-") ||
|
||||||
|
id.startsWith("google/") ||
|
||||||
|
id.startsWith("google-")
|
||||||
|
)
|
||||||
|
return "google";
|
||||||
|
if (id.startsWith("kimi-") || id.startsWith("moonshotai/")) return "moonshot";
|
||||||
|
if (id.startsWith("mistral-") || id.startsWith("mistral/")) return "mistral";
|
||||||
|
if (id.startsWith("minimax-") || id.startsWith("minimax/")) return "minimax";
|
||||||
|
if (
|
||||||
|
id.startsWith("glm-") ||
|
||||||
|
id.startsWith("zai-") ||
|
||||||
|
id.startsWith("zhipu/")
|
||||||
|
)
|
||||||
|
return "zhipu";
|
||||||
|
if (id.startsWith("longcat-")) return "meituan";
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true when two model IDs share root-vendor lineage.
|
||||||
|
*
|
||||||
|
* Purpose: allow model-selection layer to implement the
|
||||||
|
* `lineage-diverse-from-worker` constraint as a simple predicate.
|
||||||
|
*
|
||||||
|
* "unknown" vendor is treated as non-matching so it never blocks candidates
|
||||||
|
* (fail-open for unrecognised models).
|
||||||
|
*
|
||||||
|
* @param {string} candidateModelId
|
||||||
|
* @param {string} workerModelId
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isSameRootVendor(candidateModelId, workerModelId) {
|
||||||
|
const a = rootVendorFor(candidateModelId);
|
||||||
|
const b = rootVendorFor(workerModelId);
|
||||||
|
if (a === "unknown" || b === "unknown") return false;
|
||||||
|
return a === b;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue