Standalone per-card browser model/security/view modules for HUX-01..10 plus node+pytest suites that read the hux.v1 contract schemas directly. Reconciled drift found on integration: the activity model now accepts all 32 hux.event.v1 kinds (delegation.*, memory.suppressed, memory.retrieval_removed, budget.exhausted, side_effect.*), the autonomy model carries the external_side_effect capability, and the foundation boundary test now asserts the shipped static HUX surface exists on disk and that images never bake activated HUX_FLAGS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
243 lines
14 KiB
JavaScript
243 lines
14 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import test from "node:test";
|
||
|
||
import {createResearchEndpointContract} from "../../dockerfiles/hermes-webui-hux/research/endpoints.ts";
|
||
import {
|
||
FOUNDATION_FLAG, FRIENDLY_MODES_FLAG, RESEARCH_FLAG, normalizeResearchPage, researchEnabled,
|
||
} from "../../dockerfiles/hermes-webui-hux/research/model.ts";
|
||
import {
|
||
asRecord, boundedText, isHash, isId, isUtc, normalizeIdentity, normalizeScope,
|
||
safeMessageId, safePathId, safeSourceUri, sameIdentity, sameScope,
|
||
} from "../../dockerfiles/hermes-webui-hux/research/security.ts";
|
||
|
||
const IDENTITY = {tenantRef: "tnt_0123456789abcdef", userRef: "usr_0123456789abcdef", surface: "chat"};
|
||
const RAW_IDENTITY = {tenant_ref: IDENTITY.tenantRef, user_ref: IDENTITY.userRef, surface: "chat"};
|
||
const SCOPE = {projectId: "prj_project123", conversationId: "conv_converse1"};
|
||
const RAW_SCOPE = {project_id: SCOPE.projectId, conversation_id: SCOPE.conversationId};
|
||
const HASH = `sha256:${"a".repeat(64)}`;
|
||
const FLAGS = [FOUNDATION_FLAG, FRIENDLY_MODES_FLAG, RESEARCH_FLAG];
|
||
|
||
function source(extra = {}) {
|
||
return {schema: "hux.source.v1", id: "src_primary12", kind: "web",
|
||
uri: "https://example.com/report", title: "Primary report", publisher: "Example Institute",
|
||
published_at: "2026-08-20T10:00:00Z", retrieved_at: "2026-08-24T10:00:00Z",
|
||
classification: "primary", content_hash: HASH, provenance: {surface: "chat",
|
||
actor: {type: "assistant", id: "hermes"}, recorded_at: "2026-08-24T10:00:01Z",
|
||
conversation_id: SCOPE.conversationId, run_id: "run_research"}, ...extra};
|
||
}
|
||
|
||
function passage(extra = {}) {
|
||
return {schema: "hux.passage.v1", id: "psg_passage12", source_id: "src_primary12",
|
||
text: "The exact supporting passage.\nSecond line.", hash: HASH,
|
||
locator: {page: 2, selector: "#finding", line_start: 10, line_end: 12,
|
||
char_start: 50, char_end: 91}, ...extra};
|
||
}
|
||
|
||
function citation(extra = {}) {
|
||
return {schema: "hux.citation.v1", id: "cit_citation1", message_id: "msg-44",
|
||
claim: "The report supports this claim.", passage_ids: ["psg_passage12"],
|
||
support: "supports", note: "Check the methodology.", ...extra};
|
||
}
|
||
|
||
function notebook(extra = {}) {
|
||
return {schema: "hux.research_notebook.v1", id: "nb_notebook12",
|
||
conversation_id: SCOPE.conversationId, question: "What does the evidence show?", status: "open",
|
||
source_ids: ["src_primary12"], passage_ids: ["psg_passage12"], citation_ids: ["cit_citation1"],
|
||
assumptions: ["The publication date is accurate."], unresolved_questions: ["Was it peer reviewed?"],
|
||
updated_at: "2026-08-24T10:00:02Z", ...extra};
|
||
}
|
||
|
||
function page(extra = {}) {
|
||
return {schema: "hux.research_page.v1", api_version: "hux.v1", identity: RAW_IDENTITY,
|
||
binding: RAW_SCOPE, sources: [source()], passages: [passage()], citations: [citation()],
|
||
notebooks: [notebook()], ...extra};
|
||
}
|
||
|
||
test("research stays default-off until its declared capabilities are active", () => {
|
||
assert.equal(researchEnabled(), false);
|
||
assert.equal(researchEnabled([]), false);
|
||
for (const flag of FLAGS) assert.equal(researchEnabled(FLAGS.filter((item) => item !== flag)), false);
|
||
assert.equal(researchEnabled(FLAGS), true);
|
||
});
|
||
|
||
test("security helpers enforce exact identity, scope, text, IDs, timestamps and hashes", () => {
|
||
assert.deepEqual(asRecord({a: 1}), {a: 1});
|
||
assert.equal(asRecord(null), null);
|
||
assert.equal(asRecord([]), null);
|
||
assert.equal(isId("src_primary12", "src"), true);
|
||
assert.equal(isId("psg_passage12", "src"), false);
|
||
assert.equal(isId(4), false);
|
||
assert.equal(isUtc("2026-08-24T10:00:00.123Z"), true);
|
||
assert.equal(isUtc("today"), false);
|
||
assert.equal(isUtc(4), false);
|
||
assert.equal(isHash(HASH), true);
|
||
assert.equal(isHash(`sha256:${"z".repeat(64)}`), false);
|
||
assert.equal(isHash(4), false);
|
||
assert.equal(boundedText("hello", 1, 5), true);
|
||
assert.equal(boundedText("", 1, 5), false);
|
||
assert.equal(boundedText("bad\u0000", 1, 10), false);
|
||
assert.equal(boundedText(4, 1, 5), false);
|
||
assert.deepEqual(normalizeIdentity(RAW_IDENTITY), IDENTITY);
|
||
for (const bad of [null, [], {...RAW_IDENTITY, tenant_ref: "bad"},
|
||
{...RAW_IDENTITY, user_ref: "bad"}, {...RAW_IDENTITY, surface: "browser"}]) {
|
||
assert.equal(normalizeIdentity(bad), null);
|
||
}
|
||
assert.deepEqual(normalizeScope(RAW_SCOPE), SCOPE);
|
||
assert.equal(normalizeScope(null), null);
|
||
assert.equal(normalizeScope({...RAW_SCOPE, project_id: "bad"}), null);
|
||
assert.equal(normalizeScope({...RAW_SCOPE, conversation_id: "bad"}), null);
|
||
assert.equal(sameIdentity(IDENTITY, IDENTITY), true);
|
||
assert.equal(sameIdentity(null, IDENTITY), false);
|
||
assert.equal(sameIdentity({...IDENTITY, surface: "voice"}, IDENTITY), false);
|
||
assert.equal(sameScope(SCOPE, SCOPE), true);
|
||
assert.equal(sameScope(null, SCOPE), false);
|
||
assert.equal(sameScope({...SCOPE, conversationId: "conv_other1234"}, SCOPE), false);
|
||
});
|
||
|
||
test("source URLs allow HTTPS and HUX paths but reject active or ambiguous schemes", () => {
|
||
assert.equal(safeSourceUri(undefined), null);
|
||
assert.equal(safeSourceUri(null), null);
|
||
assert.equal(safeSourceUri(""), null);
|
||
assert.equal(safeSourceUri("https://example.com/a?q=1"), "https://example.com/a?q=1");
|
||
assert.equal(safeSourceUri("/hux/v1/artifacts/art_safe1234"), "/hux/v1/artifacts/art_safe1234");
|
||
for (const unsafe of ["http://example.com", "javascript:alert(1)", "data:text/html,x",
|
||
"//example.com/x", "/hux/v1/../secret", "/hux/v1/x?q=1", "https://u:p@example.com", "not a url",
|
||
`https://example.com/${"x".repeat(2000)}`, "https://example.com/\u0000"] ) {
|
||
assert.equal(safeSourceUri(unsafe), null);
|
||
}
|
||
assert.equal(safeMessageId("msg-44"), "msg-44");
|
||
assert.throws(() => safeMessageId("bad/path"), TypeError);
|
||
assert.throws(() => safeMessageId(""), TypeError);
|
||
assert.equal(safePathId("nb_notebook12", "nb"), "nb_notebook12");
|
||
assert.throws(() => safePathId("bad", "nb"), TypeError);
|
||
});
|
||
|
||
test("valid research keeps provenance, bounded passages, support, and notebook notes", () => {
|
||
const result = normalizeResearchPage(page(), IDENTITY, SCOPE);
|
||
assert.equal(result.rejected, 0);
|
||
assert.equal(result.sources[0].classification, "primary");
|
||
assert.equal(result.sources[0].provenance.actorType, "assistant");
|
||
assert.equal(result.sources[0].provenance.runId, "run_research");
|
||
assert.equal(result.passages[0].text, "The exact supporting passage.\nSecond line.");
|
||
assert.equal(result.passages[0].locator, "page 2 · #finding · lines 10–12 · characters 50–91");
|
||
assert.equal(result.citations[0].support, "supports");
|
||
assert.deepEqual(result.notebooks[0].notes, ["Check the methodology."]);
|
||
assert.deepEqual(result.notebooks[0].unresolvedQuestions, ["Was it peer reviewed?"]);
|
||
});
|
||
|
||
test("envelope rejects cross-tenant, user, project and conversation data", () => {
|
||
const bad = [null, {}, page({schema: "old"}), page({api_version: "hux.v0"}),
|
||
page({identity: {...RAW_IDENTITY, tenant_ref: "tnt_ffffffffffffffff"}}),
|
||
page({identity: {...RAW_IDENTITY, user_ref: "usr_ffffffffffffffff"}}),
|
||
page({identity: {...RAW_IDENTITY, surface: "voice"}}),
|
||
page({binding: {...RAW_SCOPE, project_id: "prj_other1234"}}),
|
||
page({binding: {...RAW_SCOPE, conversation_id: "conv_other1234"}}),
|
||
page({sources: null}), page({sources: Array(501).fill(source())}),
|
||
page({passages: null}), page({passages: Array(501).fill(passage())}),
|
||
page({citations: null}), page({citations: Array(501).fill(citation())}),
|
||
page({notebooks: null}), page({notebooks: Array(101).fill(notebook())})];
|
||
for (const raw of bad) assert.equal(normalizeResearchPage(raw, IDENTITY, SCOPE), null);
|
||
});
|
||
|
||
test("invalid source metadata, provenance and URL are withheld with dependants", () => {
|
||
const badSources = [null, source({schema: "old"}), source({id: "bad"}), source({kind: "video"}),
|
||
source({title: ""}), source({publisher: "x".repeat(201)}), source({retrieved_at: "today"}),
|
||
source({classification: "official"}), source({published_at: "today"}), source({content_hash: "bad"}),
|
||
source({published_at: "2026-08-25T10:00:00Z"}),
|
||
source({uri: "javascript:alert(1)"}), source({provenance: null}),
|
||
source({provenance: {...source().provenance, surface: "browser"}}),
|
||
source({provenance: {...source().provenance, actor: null}}),
|
||
source({provenance: {...source().provenance, actor: {type: "intruder"}}}),
|
||
source({provenance: {...source().provenance, actor: {type: "assistant", id: ""}}}),
|
||
source({provenance: {...source().provenance, recorded_at: "today"}}),
|
||
source({provenance: {...source().provenance, conversation_id: "conv_other1234"}}),
|
||
source({provenance: {...source().provenance, run_id: ""}})];
|
||
for (const raw of badSources) {
|
||
const result = normalizeResearchPage(page({sources: [raw]}), IDENTITY, SCOPE);
|
||
assert.equal(result.sources.length, 0);
|
||
assert.equal(result.passages.length, 0);
|
||
assert.equal(result.citations.length, 0);
|
||
assert.equal(result.notebooks.length, 0);
|
||
}
|
||
const withoutOptional = normalizeResearchPage(page({sources: [source({uri: undefined,
|
||
publisher: undefined, published_at: undefined, content_hash: undefined, provenance: undefined})]}), IDENTITY, SCOPE);
|
||
assert.equal(withoutOptional.sources[0].uri, null);
|
||
assert.equal(withoutOptional.sources[0].provenance, null);
|
||
});
|
||
|
||
test("passage locators and citations are schema-bounded and referentially complete", () => {
|
||
const invalidPassages = [null, passage({schema: "old"}), passage({id: "bad"}),
|
||
passage({source_id: "src_missing12"}), passage({text: ""}), passage({text: "x".repeat(4001)}),
|
||
passage({hash: "bad"}), passage({locator: null}), passage({locator: {unknown: 1}}),
|
||
passage({locator: {page: 0}}), passage({locator: {selector: "x".repeat(501)}}),
|
||
passage({locator: {line_start: 0}}), passage({locator: {line_end: 0}}),
|
||
passage({locator: {char_start: -1}}), passage({locator: {char_end: -1}}),
|
||
passage({locator: {line_start: 10, line_end: 9}}), passage({locator: {char_start: 10, char_end: 9}})];
|
||
for (const raw of invalidPassages) {
|
||
assert.equal(normalizeResearchPage(page({passages: [raw]}), IDENTITY, SCOPE).passages.length, 0);
|
||
}
|
||
assert.equal(normalizeResearchPage(page({passages: [passage({locator: undefined})]}), IDENTITY, SCOPE)
|
||
.passages[0].locator, "");
|
||
assert.equal(normalizeResearchPage(page({passages: [passage({locator: {line_start: 4}})]}), IDENTITY, SCOPE)
|
||
.passages[0].locator, "lines 4");
|
||
|
||
const invalidCitations = [null, citation({schema: "old"}), citation({id: "bad"}),
|
||
citation({message_id: ""}), citation({claim: ""}), citation({passage_ids: null}),
|
||
citation({passage_ids: []}), citation({passage_ids: Array(33).fill("psg_passage12")}),
|
||
citation({passage_ids: ["psg_passage12", "psg_passage12"]}),
|
||
citation({passage_ids: ["psg_missing12"]}), citation({support: "maybe"}),
|
||
citation({note: "x".repeat(501)})];
|
||
for (const raw of invalidCitations) {
|
||
assert.equal(normalizeResearchPage(page({citations: [raw]}), IDENTITY, SCOPE).citations.length, 0);
|
||
}
|
||
const allSupport = ["supports", "partially_supports", "contradicts", "unverified"];
|
||
for (const support of allSupport) assert.equal(normalizeResearchPage(page({citations: [citation({support})]}),
|
||
IDENTITY, SCOPE).citations[0].support, support);
|
||
});
|
||
|
||
test("notebooks validate references, lists, status, conversation and duplicates", () => {
|
||
const invalid = [null, notebook({schema: "old"}), notebook({id: "bad"}),
|
||
notebook({conversation_id: "conv_other1234"}), notebook({question: ""}), notebook({status: "paused"}),
|
||
notebook({updated_at: "today"}), notebook({source_ids: null}), notebook({source_ids: ["src_missing12"]}),
|
||
notebook({source_ids: ["src_primary12", "src_primary12"]}), notebook({passage_ids: null}),
|
||
notebook({passage_ids: ["psg_missing12"]}), notebook({citation_ids: null}),
|
||
notebook({citation_ids: ["cit_missing12"]}), notebook({assumptions: null}),
|
||
notebook({assumptions: Array(65).fill("x")}), notebook({assumptions: [""]}),
|
||
notebook({unresolved_questions: null}), notebook({unresolved_questions: Array(65).fill("x")}),
|
||
notebook({unresolved_questions: [""]})];
|
||
for (const raw of invalid) {
|
||
assert.equal(normalizeResearchPage(page({notebooks: [raw]}), IDENTITY, SCOPE).notebooks.length, 0);
|
||
}
|
||
for (const status of ["open", "answered", "abandoned"]) {
|
||
assert.equal(normalizeResearchPage(page({notebooks: [notebook({status})]}), IDENTITY, SCOPE)
|
||
.notebooks[0].status, status);
|
||
}
|
||
const duplicate = normalizeResearchPage(page({sources: [source(), source()],
|
||
passages: [passage(), passage()], citations: [citation(), citation()], notebooks: [notebook(), notebook()]}),
|
||
IDENTITY, SCOPE);
|
||
assert.deepEqual([duplicate.sources.length, duplicate.passages.length, duplicate.citations.length,
|
||
duplicate.notebooks.length], [1, 1, 1, 1]);
|
||
assert.equal(duplicate.rejected, 4);
|
||
});
|
||
|
||
test("endpoint contract is scoped, inert, optimistic, and same-origin delegated", () => {
|
||
const client = {apiVersion: "hux.v1", identity: IDENTITY,
|
||
endpoint(path) { assert.match(path, /^\/projects\/prj_[^/]+\/conversations\/conv_[^/]+\/research/);
|
||
return `/hux/v1${path}`; }};
|
||
assert.equal(createResearchEndpointContract(client, SCOPE), null);
|
||
assert.equal(createResearchEndpointContract(client, SCOPE, FLAGS.slice(1)), null);
|
||
const contract = createResearchEndpointContract(client, SCOPE, FLAGS);
|
||
assert.equal(contract.page.path, "/hux/v1/projects/prj_project123/conversations/conv_converse1/research");
|
||
assert.match(contract.citationsForMessage("msg-44").path, /messages\/msg-44\/citations$/);
|
||
assert.match(contract.notebook("nb_notebook12").path, /notebooks\/nb_notebook12$/);
|
||
assert.deepEqual(contract.updateNotebook("nb_notebook12").requiredBody,
|
||
["expected_updated_at", "status", "assumptions", "unresolved_questions"]);
|
||
assert.deepEqual(contract.attachCitation("msg-44").requiredBody,
|
||
["claim", "passage_ids", "support", "note"]);
|
||
for (const call of [() => contract.citationsForMessage("bad/path"), () => contract.notebook("bad"),
|
||
() => contract.updateNotebook("bad")]) assert.throws(call, TypeError);
|
||
assert.throws(() => createResearchEndpointContract({...client, apiVersion: "hux.v2"}, SCOPE, FLAGS), TypeError);
|
||
assert.throws(() => createResearchEndpointContract({...client, identity: {...IDENTITY, tenantRef: "bad"}}, SCOPE, FLAGS), TypeError);
|
||
assert.throws(() => createResearchEndpointContract(client, {...SCOPE, projectId: "bad"}, FLAGS), TypeError);
|
||
});
|