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
413 lines
12 KiB
JavaScript
413 lines
12 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
ACTIVITY_TIMELINE_FLAG,
|
|
FOUNDATION_FLAG,
|
|
activityTimelineEnabled,
|
|
mergeActivityEvents,
|
|
normalizeCancellationReceipt,
|
|
surfacePolicy,
|
|
} from "../../dockerfiles/hermes-webui-hux/activity/model.ts";
|
|
import {
|
|
isOpaqueId,
|
|
isRfc3339Utc,
|
|
safeEvidenceLabel,
|
|
safeText,
|
|
} from "../../dockerfiles/hermes-webui-hux/activity/security.ts";
|
|
|
|
const CONVERSATION = "conv_test1234";
|
|
const KINDS = JSON.parse(
|
|
readFileSync("services/hermes/contracts/hux/event.schema.json"),
|
|
).properties.kind.enum;
|
|
|
|
function event(seq, kind = "tool.call", extra = {}) {
|
|
return {
|
|
schema: "hux.event.v1",
|
|
id: `evt_test${String(seq).padStart(4, "0")}`,
|
|
seq,
|
|
ts: `2026-08-24T10:${String(Math.floor(seq / 60)).padStart(2, "0")}:${String(seq % 60).padStart(2, "0")}Z`,
|
|
conversation_id: CONVERSATION,
|
|
kind,
|
|
summary: `Safe update ${seq}`,
|
|
evidence: [],
|
|
provenance: { surface: "chat" },
|
|
sensitivity: "personal",
|
|
redaction: { level: "none" },
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
test("feature remains default-off and depends on the foundation", () => {
|
|
assert.equal(activityTimelineEnabled(), false);
|
|
assert.equal(activityTimelineEnabled([]), false);
|
|
assert.equal(activityTimelineEnabled([ACTIVITY_TIMELINE_FLAG]), false);
|
|
assert.equal(activityTimelineEnabled([FOUNDATION_FLAG]), false);
|
|
assert.equal(
|
|
activityTimelineEnabled([FOUNDATION_FLAG, ACTIVITY_TIMELINE_FLAG]),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("Chat policy is intentionally calmer and every returned policy is isolated", () => {
|
|
const chat = surfacePolicy("chat");
|
|
const worker = surfacePolicy("worker");
|
|
assert.ok(chat.maxItems < worker.maxItems);
|
|
assert.ok(chat.initialItems < worker.initialItems);
|
|
assert.ok(chat.evidenceLimit < worker.evidenceLimit);
|
|
assert.equal(chat.expandRoutineEvents, false);
|
|
assert.equal(worker.expandRoutineEvents, true);
|
|
chat.maxItems = 1;
|
|
assert.equal(surfacePolicy("chat").maxItems, 80);
|
|
assert.deepEqual(surfacePolicy("api"), worker);
|
|
assert.ok(
|
|
surfacePolicy("voice").maxItems < surfacePolicy("telegram").maxItems,
|
|
);
|
|
});
|
|
|
|
test("plain-text security redacts credential shapes without serializing objects", () => {
|
|
assert.equal(safeText({ token: "nope" }, "fallback"), "fallback");
|
|
assert.equal(safeText(" \n\t ", "fallback"), "fallback");
|
|
assert.equal(safeText("abc\u0000def", "fallback"), "abc def");
|
|
assert.equal(safeText("abcdef", "fallback", 4), "abc…");
|
|
assert.equal(safeText("abcdef", "fallback", 0), "…");
|
|
const unsafe = [
|
|
"password=hunter2",
|
|
"Authorization: Basic-abcd",
|
|
"Bearer abc.DEF_123",
|
|
"eyJabcdefgh.abcdefghijkl.abcdefghijkl",
|
|
"https://user:pass@example.test/path",
|
|
"Abcdefghijklmnopqrstuvwxyz1234567890ABCD",
|
|
"-----BEGIN PRIVATE KEY----- top secret -----END PRIVATE KEY-----",
|
|
].join(" ");
|
|
const scrubbed = safeText(unsafe, "fallback", 1000);
|
|
assert.doesNotMatch(
|
|
scrubbed,
|
|
/hunter2|Basic-abcd|abc\.DEF_123|user:pass|top secret|1234567890ABCD/,
|
|
);
|
|
assert.match(scrubbed, /redacted/);
|
|
});
|
|
|
|
test("only allow-listed evidence labels cross the display boundary", () => {
|
|
assert.deepEqual(safeEvidenceLabel("tool_result"), {
|
|
kind: "tool_result",
|
|
label: "Tool result",
|
|
});
|
|
assert.deepEqual(safeEvidenceLabel("url"), { kind: "url", label: "Website" });
|
|
assert.equal(safeEvidenceLabel("raw_tool_arguments"), null);
|
|
assert.equal(safeEvidenceLabel(7), null);
|
|
assert.equal(isOpaqueId("evt_abcd", "evt"), true);
|
|
assert.equal(isOpaqueId("evt_abcd"), true);
|
|
assert.equal(isOpaqueId("mem_abcd", "evt"), false);
|
|
assert.equal(isOpaqueId("not opaque"), false);
|
|
assert.equal(isOpaqueId(7), false);
|
|
assert.equal(isRfc3339Utc("2026-08-24T10:00:00.123Z"), true);
|
|
assert.equal(isRfc3339Utc("today"), false);
|
|
assert.equal(isRfc3339Utc("2026-99-99T10:00:00Z"), false);
|
|
assert.equal(isRfc3339Utc(7), false);
|
|
});
|
|
|
|
test("every contract event maps to a compact semantic phase", () => {
|
|
const records = KINDS.map((kind, seq) =>
|
|
event(seq, kind, {
|
|
parent_event_id: kind === "run.started" ? "evt_parent1234" : undefined,
|
|
}),
|
|
);
|
|
const result = mergeActivityEvents([], records, CONVERSATION, "worker");
|
|
assert.equal(result.invalid, 0);
|
|
assert.equal(result.items.length, KINDS.length);
|
|
const phases = new Set(result.items.map((item) => item.phase));
|
|
assert.deepEqual(
|
|
phases,
|
|
new Set([
|
|
"intent",
|
|
"action",
|
|
"evidence",
|
|
"delegation",
|
|
"decision",
|
|
"completion",
|
|
"failure",
|
|
]),
|
|
);
|
|
assert.equal(
|
|
result.items.find((item) => item.kind === "run.failed").tone,
|
|
"warning",
|
|
);
|
|
assert.equal(
|
|
result.items.find((item) => item.kind === "run.completed").tone,
|
|
"success",
|
|
);
|
|
assert.equal(
|
|
result.items.find((item) => item.kind === "approval.requested").tone,
|
|
"warning",
|
|
);
|
|
assert.equal(
|
|
result.items.find((item) => item.kind === "tool.call").routine,
|
|
true,
|
|
);
|
|
assert.equal(
|
|
result.items.find((item) => item.kind === "message.user").routine,
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("root runs are actions while child runs are explicit delegations", () => {
|
|
const result = mergeActivityEvents(
|
|
[],
|
|
[
|
|
event(1, "run.started"),
|
|
event(2, "run.started", { parent_event_id: "evt_parent1234" }),
|
|
],
|
|
CONVERSATION,
|
|
"chat",
|
|
);
|
|
assert.deepEqual(
|
|
result.items.map((item) => item.phase),
|
|
["action", "delegation"],
|
|
);
|
|
assert.deepEqual(
|
|
result.items.map((item) => item.tone),
|
|
["active", "active"],
|
|
);
|
|
});
|
|
|
|
test("normalization redacts summaries and never projects detail, IDs, or URIs", () => {
|
|
const raw = event(1, "tool.result", {
|
|
summary: "token=secret-value result is ready",
|
|
run_id: "run-safe",
|
|
detail: { arguments: "--token secret", result: "raw secret result" },
|
|
evidence: [
|
|
{
|
|
kind: "tool_result",
|
|
id: "secret-id",
|
|
uri: "https://user:pass@example.test",
|
|
},
|
|
{ kind: "file", id: "/secret/path" },
|
|
{ kind: "bogus", id: "raw" },
|
|
{ kind: "url", id: "https://secret" },
|
|
],
|
|
});
|
|
const item = mergeActivityEvents([], [raw], CONVERSATION, "chat").items[0];
|
|
const serialized = JSON.stringify(item);
|
|
assert.equal(item.runId, "run-safe");
|
|
assert.deepEqual(
|
|
item.evidence.map((entry) => entry.label),
|
|
["Tool result", "File", "Website"],
|
|
);
|
|
assert.doesNotMatch(
|
|
serialized,
|
|
/secret-value|raw secret result|secret-id|secret\/path|user:pass/,
|
|
);
|
|
assert.equal("detail" in item, false);
|
|
|
|
const invalidRun = mergeActivityEvents(
|
|
[],
|
|
[event(2, "tool.call", { run_id: { raw: true } })],
|
|
CONVERSATION,
|
|
"chat",
|
|
);
|
|
assert.equal(invalidRun.items[0].runId, undefined);
|
|
});
|
|
|
|
test("full or restricted redaction ignores even an otherwise safe summary", () => {
|
|
const result = mergeActivityEvents(
|
|
[],
|
|
[
|
|
event(1, "tool.call", {
|
|
summary: "do not show",
|
|
redaction: { level: "full" },
|
|
}),
|
|
event(2, "run.failed", {
|
|
summary: "health detail",
|
|
sensitivity: "restricted",
|
|
}),
|
|
event(3, "run.completed", { summary: {}, redaction: null }),
|
|
event(4, "citation.attached", {
|
|
summary: "",
|
|
redaction: { level: "partial" },
|
|
}),
|
|
],
|
|
CONVERSATION,
|
|
"chat",
|
|
);
|
|
assert.deepEqual(
|
|
result.items.map((item) => item.summary),
|
|
[
|
|
"Hermes used a tool",
|
|
"The run failed; sensitive detail is hidden",
|
|
"The run completed",
|
|
"Activity recorded; sensitive detail is hidden",
|
|
],
|
|
);
|
|
});
|
|
|
|
test("reconnect merge is ordered, idempotent, and fail-closed on conflicts", () => {
|
|
const first = mergeActivityEvents(
|
|
[],
|
|
[event(3), event(1)],
|
|
CONVERSATION,
|
|
"chat",
|
|
);
|
|
const replay = { ...event(3), summary: "attempted rewrite" };
|
|
const idConflict = { ...event(4), id: first.items[0].id };
|
|
const seqConflict = { ...event(1), id: "evt_conflict1" };
|
|
const result = mergeActivityEvents(
|
|
first.items,
|
|
[replay, idConflict, seqConflict, event(2)],
|
|
CONVERSATION,
|
|
"chat",
|
|
);
|
|
assert.deepEqual(
|
|
result.items.map((item) => item.seq),
|
|
[1, 2, 3],
|
|
);
|
|
assert.equal(result.items.at(-1).summary, "Safe update 3");
|
|
assert.equal(result.replays, 1);
|
|
assert.equal(result.conflicts, 2);
|
|
assert.equal(result.afterSeq, 3);
|
|
assert.equal(result.invalid, 0);
|
|
});
|
|
|
|
test("an initial same-sequence conflict resolves deterministically", () => {
|
|
const higher = { ...event(7), id: "evt_zulu1234" };
|
|
const lower = {
|
|
...event(7),
|
|
id: "evt_alpha1234",
|
|
summary: "Deterministic owner",
|
|
};
|
|
const forward = mergeActivityEvents(
|
|
[],
|
|
[higher, lower],
|
|
CONVERSATION,
|
|
"chat",
|
|
);
|
|
const reverse = mergeActivityEvents(
|
|
[],
|
|
[lower, higher],
|
|
CONVERSATION,
|
|
"chat",
|
|
);
|
|
assert.deepEqual(forward.items, reverse.items);
|
|
assert.equal(forward.items[0].id, "evt_alpha1234");
|
|
assert.equal(forward.conflicts, 1);
|
|
});
|
|
|
|
test("malformed and cross-conversation events are rejected", () => {
|
|
const bad = [
|
|
{ ...event(1), schema: "hux.event.v0" },
|
|
{ ...event(2), id: "bad" },
|
|
{ ...event(3), seq: -1 },
|
|
{ ...event(4), ts: "later" },
|
|
{ ...event(5), conversation_id: "conv_other1234" },
|
|
{ ...event(6), kind: "tool.raw" },
|
|
];
|
|
const result = mergeActivityEvents([], bad, CONVERSATION, "chat");
|
|
assert.deepEqual(result.items, []);
|
|
assert.equal(result.invalid, bad.length);
|
|
assert.equal(result.afterSeq, -1);
|
|
});
|
|
|
|
test("retention is bounded per surface while preserving the resume watermark", () => {
|
|
const records = Array.from({ length: 85 }, (_, seq) => event(seq));
|
|
const result = mergeActivityEvents([], records, CONVERSATION, "chat");
|
|
assert.equal(result.items.length, 80);
|
|
assert.equal(result.items[0].seq, 5);
|
|
assert.equal(result.afterSeq, 84);
|
|
assert.equal(result.truncated, 5);
|
|
const replayed = mergeActivityEvents(
|
|
result.items,
|
|
[event(84)],
|
|
CONVERSATION,
|
|
"chat",
|
|
);
|
|
assert.equal(replayed.replays, 1);
|
|
assert.equal(replayed.truncated, 0);
|
|
});
|
|
|
|
test("evidence display and cancellation receipts obey surface bounds", () => {
|
|
const evidence = Array.from({ length: 10 }, (_, index) => ({
|
|
kind: index % 2 ? "file" : "source",
|
|
id: `secret-${index}`,
|
|
uri: `https://token-${index}`,
|
|
}));
|
|
assert.equal(
|
|
mergeActivityEvents(
|
|
[],
|
|
[event(1, "citation.attached", { evidence })],
|
|
CONVERSATION,
|
|
"chat",
|
|
).items[0].evidence.length,
|
|
3,
|
|
);
|
|
assert.equal(
|
|
mergeActivityEvents(
|
|
[],
|
|
[event(1, "citation.attached", { evidence })],
|
|
CONVERSATION,
|
|
"worker",
|
|
).items[0].evidence.length,
|
|
8,
|
|
);
|
|
|
|
const receipt = {
|
|
schema: "hux.cancel_receipt.v1",
|
|
id: "rcpt_test1234",
|
|
run_id: "run_1",
|
|
requested_at: "2026-08-24T10:00:00Z",
|
|
acknowledged_at: "2026-08-24T10:00:01Z",
|
|
completed_at: "not-a-time",
|
|
outcome: "cancelled",
|
|
side_effects: Array.from({ length: 7 }, (_, index) => ({
|
|
description: index === 0 ? "token=do-not-show" : `Effect ${index}`,
|
|
reverted: index % 2 === 0,
|
|
evidence: { kind: "file", id: "secret" },
|
|
})),
|
|
};
|
|
const safe = normalizeCancellationReceipt(receipt, "chat");
|
|
assert.equal(safe.sideEffects.length, 4);
|
|
assert.equal(safe.omittedEffects, 3);
|
|
assert.equal(safe.completedAt, undefined);
|
|
assert.equal(safe.acknowledgedAt, receipt.acknowledged_at);
|
|
assert.doesNotMatch(JSON.stringify(safe), /do-not-show|"evidence"|"secret"/);
|
|
});
|
|
|
|
test("cancellation parsing rejects bad identity, time, run, and outcome", () => {
|
|
const base = {
|
|
schema: "hux.cancel_receipt.v1",
|
|
id: "rcpt_test1234",
|
|
run_id: "run_1",
|
|
requested_at: "2026-08-24T10:00:00Z",
|
|
outcome: "already_complete",
|
|
side_effects: [{ description: {}, reverted: false }],
|
|
};
|
|
const safe = normalizeCancellationReceipt(base, "worker");
|
|
assert.equal(safe.sideEffects[0].description, "A side effect was recorded");
|
|
assert.equal(safe.completedAt, undefined);
|
|
assert.equal(
|
|
normalizeCancellationReceipt({ ...base, schema: "old" }, "chat"),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
normalizeCancellationReceipt({ ...base, id: "bad" }, "chat"),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
normalizeCancellationReceipt({ ...base, run_id: 3 }, "chat"),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
normalizeCancellationReceipt({ ...base, requested_at: "bad" }, "chat"),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
normalizeCancellationReceipt({ ...base, outcome: "unknown" }, "chat"),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
normalizeCancellationReceipt({ ...base, side_effects: null }, "chat")
|
|
.sideEffects.length,
|
|
0,
|
|
);
|
|
});
|