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
362 lines
18 KiB
JavaScript
362 lines
18 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
import { createArtifactEndpointContract } from "../../dockerfiles/hermes-webui-hux/artifacts/endpoints.ts";
|
|
import {
|
|
ARTIFACTS_FLAG,
|
|
FOUNDATION_FLAG,
|
|
PROJECTS_FLAG,
|
|
artifactsEnabled,
|
|
normalizeArtifact,
|
|
normalizeArtifactWorkspace,
|
|
normalizePreview,
|
|
preservesImmutableHistory,
|
|
} from "../../dockerfiles/hermes-webui-hux/artifacts/model.ts";
|
|
import {
|
|
ArtifactContractError,
|
|
isMime,
|
|
isOpaqueId,
|
|
isSha256,
|
|
isUtc,
|
|
normalizeIdentity,
|
|
normalizeScope,
|
|
safeBlobUrl,
|
|
safePreviewText,
|
|
safeText,
|
|
sameIdentity,
|
|
sameScope,
|
|
scopedPath,
|
|
} from "../../dockerfiles/hermes-webui-hux/artifacts/security.ts";
|
|
|
|
const IDENTITY = {
|
|
tenantRef: "tnt_0123456789abcdef",
|
|
userRef: "usr_0123456789abcdef",
|
|
surface: "chat",
|
|
};
|
|
const RAW_IDENTITY = {
|
|
tenant_ref: IDENTITY.tenantRef,
|
|
user_ref: IDENTITY.userRef,
|
|
surface: IDENTITY.surface,
|
|
};
|
|
const SCOPE = {projectId: "prj_test1234", conversationId: "conv_test1234"};
|
|
const RAW_SCOPE = {project_id: SCOPE.projectId, conversation_id: SCOPE.conversationId};
|
|
const FLAGS = [FOUNDATION_FLAG, PROJECTS_FLAG, ARTIFACTS_FLAG];
|
|
const HASH_A = `sha256:${"a".repeat(64)}`;
|
|
const HASH_B = `sha256:${"b".repeat(64)}`;
|
|
const CONTRACT_TYPES = JSON.parse(
|
|
readFileSync("services/hermes/contracts/hux/artifact.schema.json"),
|
|
).properties.type.enum;
|
|
|
|
function version(number = 1, extra = {}) {
|
|
return {
|
|
version: number,
|
|
created_at: `2026-08-24T10:00:0${number}Z`,
|
|
created_by: {type: "assistant", id: "hermes", display: "Hermes"},
|
|
content_ref: {hash: number === 1 ? HASH_A : HASH_B, bytes: 120 * number, mime: "text/markdown"},
|
|
...(number > 1 ? {diff_from: number - 1} : {}),
|
|
note: `Draft ${number}`,
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
function artifact(extra = {}) {
|
|
return {
|
|
schema: "hux.artifact.v1",
|
|
id: "art_test1234",
|
|
owner: IDENTITY.userRef,
|
|
conversation_id: SCOPE.conversationId,
|
|
project_id: SCOPE.projectId,
|
|
type: "markdown",
|
|
language: "report",
|
|
title: "Release report",
|
|
current_version: 2,
|
|
versions: [version(1), version(2)],
|
|
sensitivity: "personal",
|
|
created_at: "2026-08-24T10:00:01Z",
|
|
updated_at: "2026-08-24T10:00:02Z",
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
function page(artifacts = [artifact()], extra = {}) {
|
|
return {
|
|
schema: "hux.artifact_workspace.v1",
|
|
api_version: "hux.v1",
|
|
identity: RAW_IDENTITY,
|
|
binding: RAW_SCOPE,
|
|
artifacts,
|
|
attachments: [{artifact_id: "art_test1234", source_ids: ["src_test1234"],
|
|
citation_ids: ["cit_test1234"]}],
|
|
authorizations: [{artifact_id: "art_test1234", download: "authorized",
|
|
share: "requires_approval"}],
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
test("artifacts require the foundation and projects", () => {
|
|
assert.equal(artifactsEnabled(), false);
|
|
assert.equal(artifactsEnabled([]), false);
|
|
assert.equal(artifactsEnabled([ARTIFACTS_FLAG]), false);
|
|
assert.equal(artifactsEnabled([FOUNDATION_FLAG, ARTIFACTS_FLAG]), false);
|
|
assert.equal(artifactsEnabled([PROJECTS_FLAG, ARTIFACTS_FLAG]), false);
|
|
assert.equal(artifactsEnabled(FLAGS), true);
|
|
});
|
|
|
|
test("security primitives reject malformed scope and unsafe display data", () => {
|
|
assert.equal(isOpaqueId("art_test1234", "art"), true);
|
|
assert.equal(isOpaqueId("prj_test1234", "art"), false);
|
|
assert.equal(isOpaqueId("bad"), false);
|
|
assert.equal(isOpaqueId(4), false);
|
|
assert.equal(isUtc("2026-08-24T10:00:00.123Z"), true);
|
|
assert.equal(isUtc("2026-99-24T10:00:00Z"), false);
|
|
assert.equal(isUtc(4), false);
|
|
assert.equal(isSha256(HASH_A), true);
|
|
assert.equal(isSha256(`sha256:${"z".repeat(64)}`), false);
|
|
assert.equal(isSha256(4), false);
|
|
assert.equal(isMime("text/plain"), true);
|
|
assert.equal(isMime("bad"), false);
|
|
assert.equal(isMime("a/" + "b".repeat(121)), false);
|
|
assert.equal(isMime(4), false);
|
|
assert.equal(safeText({}, "fallback", 20), "fallback");
|
|
assert.equal(safeText(" \n ", "fallback", 20), "fallback");
|
|
assert.equal(safeText("abc\u0000def", "fallback", 20), "abc def");
|
|
assert.equal(safeText("abcdef", "fallback", 4), "abc…");
|
|
assert.equal(safeText("token=secret Bearer abc.DEF", "fallback", 200), "[redacted] [redacted]");
|
|
assert.match(safeText("-----BEGIN PRIVATE KEY----- secret -----END PRIVATE KEY-----", "x", 200), /redacted/);
|
|
assert.equal(safePreviewText("line one\nline two"), "line one\nline two");
|
|
assert.equal(safePreviewText("bad\u0000content"), null);
|
|
assert.equal(safePreviewText("abc", 2), null);
|
|
assert.equal(safePreviewText({}), null);
|
|
assert.equal(safeBlobUrl("blob:https://chat.example/abc"), "blob:https://chat.example/abc");
|
|
for (const unsafe of ["https://example.test/image.png", "data:image/png;base64,x", "blob:has space", 4]) {
|
|
assert.equal(safeBlobUrl(unsafe), null);
|
|
}
|
|
});
|
|
|
|
test("identity and binding normalization fail closed", () => {
|
|
assert.deepEqual(normalizeIdentity(RAW_IDENTITY), IDENTITY);
|
|
assert.deepEqual(normalizeScope(RAW_SCOPE), SCOPE);
|
|
assert.equal(sameIdentity(IDENTITY, {...IDENTITY}), true);
|
|
assert.equal(sameIdentity(IDENTITY, {...IDENTITY, surface: "worker"}), false);
|
|
assert.equal(sameScope(SCOPE, {...SCOPE}), true);
|
|
assert.equal(sameScope(SCOPE, {...SCOPE, projectId: "prj_other1234"}), false);
|
|
for (const bad of [null, [], {}, {...RAW_IDENTITY, tenant_ref: "tenant"},
|
|
{...RAW_IDENTITY, user_ref: "user"}, {...RAW_IDENTITY, surface: "browser"}]) {
|
|
assert.throws(() => normalizeIdentity(bad), ArtifactContractError);
|
|
}
|
|
for (const bad of [null, [], {}, {...RAW_SCOPE, project_id: "bad"},
|
|
{...RAW_SCOPE, conversation_id: "bad"}]) {
|
|
assert.throws(() => normalizeScope(bad), ArtifactContractError);
|
|
}
|
|
});
|
|
|
|
test("schema types select safe renderers without live HTML or SVG", () => {
|
|
const cases = [
|
|
["markdown", "text/markdown", "", "document"],
|
|
["markdown", "text/plain", "research", "report"],
|
|
["document", "application/pdf", "analysis", "report"],
|
|
["document", "text/plain", "", "document"],
|
|
["code", "text/x-python", "python", "code"],
|
|
["html", "text/html", "html", "code"],
|
|
["svg", "image/svg+xml", "svg", "code"],
|
|
["json", "application/json", "json", "data"],
|
|
["json", "text/plain", "json", "data"],
|
|
["csv", "text/csv", "csv", "data"],
|
|
["csv", "text/plain", "csv", "data"],
|
|
["image", "image/png", "", "image"],
|
|
["image", "image/jpeg", "", "image"],
|
|
["image", "image/webp", "", "image"],
|
|
["image", "image/gif", "", "image"],
|
|
["audio", "audio/wav", "", "audio"],
|
|
["audio", "audio/mpeg", "", "audio"],
|
|
["audio", "audio/ogg", "", "audio"],
|
|
["audio", "audio/webm", "", "audio"],
|
|
];
|
|
assert.deepEqual(new Set(cases.map(([type]) => type)), new Set(CONTRACT_TYPES));
|
|
for (const [type, mime, language, renderer] of cases) {
|
|
const raw = artifact({type, language, current_version: 1,
|
|
versions: [version(1, {content_ref: {hash: HASH_A, bytes: 1, mime}})]});
|
|
assert.equal(normalizeArtifact(raw, IDENTITY.userRef, SCOPE)?.renderer, renderer);
|
|
}
|
|
});
|
|
|
|
test("valid artifact preserves immutable content metadata and promotion", () => {
|
|
const raw = artifact({
|
|
promotion: {project_id: SCOPE.projectId, version: 1, at: "2026-08-24T10:00:03Z"},
|
|
versions: [version(1), version(2, {
|
|
lineage: {artifact_id: "art_source1234", version: 3},
|
|
created_by: {type: "user", id: "private-user"},
|
|
})],
|
|
});
|
|
const item = normalizeArtifact(raw, IDENTITY.userRef, SCOPE);
|
|
assert.equal(item.promotedVersion, 1);
|
|
assert.equal(item.renderer, "report");
|
|
assert.equal(item.versions[1].createdBy, "user");
|
|
assert.deepEqual(item.versions[1].lineage, {artifactId: "art_source1234", version: 3});
|
|
assert.doesNotMatch(JSON.stringify(item), /private-user|Hermes/);
|
|
});
|
|
|
|
test("artifact records reject malformed, rewritten, and cross-scope history", () => {
|
|
const bad = [
|
|
artifact({schema: "hux.artifact.v0"}), artifact({id: "bad"}),
|
|
artifact({owner: "usr_ffffffffffffffff"}), artifact({project_id: "prj_other1234"}),
|
|
artifact({conversation_id: "conv_other1234"}), artifact({type: "binary"}),
|
|
artifact({current_version: 0}), artifact({current_version: 3}), artifact({versions: []}),
|
|
artifact({versions: "no"}), artifact({versions: Array.from({length: 201}, () => version(1))}),
|
|
artifact({sensitivity: "secret"}), artifact({created_at: "today"}),
|
|
artifact({updated_at: "today"}), artifact({updated_at: "2026-08-24T09:00:00Z"}),
|
|
artifact({title: ""}), artifact({title: {private: true}}),
|
|
artifact({versions: [version(1), version(1)]}),
|
|
artifact({versions: [version(1), version(2, {created_at: "2026-08-24T09:00:00Z"})]}),
|
|
artifact({versions: [version(1), version(2, {diff_from: 2})]}),
|
|
artifact({versions: [version(1), version(2, {lineage: {artifact_id: "art_test1234", version: 2}})]}),
|
|
artifact({promotion: null}),
|
|
artifact({promotion: {project_id: "prj_other1234", version: 1, at: "2026-08-24T10:00:03Z"}}),
|
|
artifact({promotion: {project_id: SCOPE.projectId, version: 3, at: "2026-08-24T10:00:03Z"}}),
|
|
artifact({promotion: {project_id: SCOPE.projectId, version: 1, at: "today"}}),
|
|
artifact({type: "image", current_version: 1, versions: [version(1)]}),
|
|
];
|
|
for (const raw of bad) assert.equal(normalizeArtifact(raw, IDENTITY.userRef, SCOPE), null);
|
|
|
|
const badVersions = [
|
|
version(1, {version: 0}), version(1, {created_at: "today"}), version(1, {created_by: null}),
|
|
version(1, {created_by: {type: "intruder", id: "x"}}),
|
|
version(1, {created_by: {type: "user", id: ""}}),
|
|
version(1, {content_ref: null}), version(1, {content_ref: {hash: "bad", bytes: 1, mime: "text/markdown"}}),
|
|
version(1, {content_ref: {hash: HASH_A, bytes: -1, mime: "text/markdown"}}),
|
|
version(1, {content_ref: {hash: HASH_A, bytes: 1, mime: "bad"}}),
|
|
version(1, {diff_from: 0}), version(1, {lineage: null}),
|
|
version(1, {lineage: {artifact_id: "bad", version: 1}}),
|
|
version(1, {lineage: {artifact_id: "art_source1234", version: 0}}),
|
|
];
|
|
for (const invalid of badVersions) {
|
|
assert.equal(normalizeArtifact(artifact({current_version: 1, versions: [invalid]}),
|
|
IDENTITY.userRef, SCOPE), null);
|
|
}
|
|
});
|
|
|
|
test("workspace envelope enforces tenant, user, project, and conversation binding", () => {
|
|
const result = normalizeArtifactWorkspace(page(), IDENTITY, SCOPE);
|
|
assert.equal(result.rejected, 0);
|
|
assert.equal(result.artifacts.length, 1);
|
|
assert.deepEqual(result.attachments, [{artifactId: "art_test1234", sourceCount: 1, citationCount: 1}]);
|
|
assert.deepEqual(result.authorizations, [{artifactId: "art_test1234", download: "authorized",
|
|
share: "requires_approval"}]);
|
|
for (const bad of [null, {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: "worker"}}),
|
|
page([], {binding: {...RAW_SCOPE, project_id: "prj_other1234"}}),
|
|
page([], {binding: {...RAW_SCOPE, conversation_id: "conv_other1234"}})]) {
|
|
assert.throws(() => normalizeArtifactWorkspace(bad, IDENTITY, SCOPE), /scope boundary|identity/);
|
|
}
|
|
});
|
|
|
|
test("workspace rejects bad entries and ignores unsafe attachment metadata", () => {
|
|
const duplicate = artifact();
|
|
const result = normalizeArtifactWorkspace(page([artifact(), duplicate, artifact({id: "bad"})], {
|
|
attachments: [
|
|
{artifact_id: "art_test1234", source_ids: ["src_test1234"], citation_ids: []},
|
|
{artifact_id: "art_test1234", source_ids: [], citation_ids: []},
|
|
{artifact_id: "art_unknown1", source_ids: [], citation_ids: []},
|
|
{artifact_id: "art_other1234", source_ids: ["bad"], citation_ids: []},
|
|
null,
|
|
],
|
|
authorizations: [
|
|
{artifact_id: "art_test1234", download: "authorized", share: "unavailable"},
|
|
{artifact_id: "art_test1234", download: "authorized", share: "authorized"},
|
|
{artifact_id: "art_unknown1", download: "authorized", share: "authorized"},
|
|
{artifact_id: "art_other1234", download: "yes", share: "authorized"},
|
|
null,
|
|
],
|
|
}), IDENTITY, SCOPE);
|
|
assert.equal(result.artifacts.length, 1);
|
|
assert.equal(result.rejected, 2);
|
|
assert.equal(result.attachments.length, 1);
|
|
assert.equal(result.authorizations.length, 1);
|
|
assert.deepEqual(normalizeArtifactWorkspace(page([], {artifacts: null, attachments: {},
|
|
authorizations: {}}), IDENTITY, SCOPE).artifacts, []);
|
|
});
|
|
|
|
test("previews require matching identity, binding, hash, MIME, and local object URLs", () => {
|
|
const item = normalizeArtifact(artifact(), IDENTITY.userRef, SCOPE);
|
|
const base = {schema: "hux.artifact_preview.v1", identity: RAW_IDENTITY, binding: RAW_SCOPE,
|
|
artifact_id: item.id, version: 2, hash: HASH_B, mime: "text/markdown", text: "# Safe report"};
|
|
assert.equal(normalizePreview(base, item, IDENTITY, SCOPE)?.text, "# Safe report");
|
|
const invalid = [null, {...base, schema: "old"}, {...base, identity: {...RAW_IDENTITY, surface: "worker"}},
|
|
{...base, binding: {...RAW_SCOPE, project_id: "prj_other1234"}}, {...base, artifact_id: "art_other1234"},
|
|
{...base, version: 3}, {...base, hash: HASH_A}, {...base, mime: "text/plain"},
|
|
{...base, text: "bad\u0000text"}, {...base, blob_url: "blob:local"}];
|
|
for (const raw of invalid) assert.equal(normalizePreview(raw, item, IDENTITY, SCOPE), null);
|
|
assert.equal(normalizePreview(base, item, {...IDENTITY, userRef: "bad"}, SCOPE), null);
|
|
|
|
const image = normalizeArtifact(artifact({type: "image", language: "", current_version: 1,
|
|
versions: [version(1, {content_ref: {hash: HASH_A, bytes: 5, mime: "image/png"}})]}),
|
|
IDENTITY.userRef, SCOPE);
|
|
const imagePreview = {...base, artifact_id: image.id, version: 1, hash: HASH_A, mime: "image/png",
|
|
text: undefined, blob_url: "blob:https://chat.example/image"};
|
|
assert.equal(normalizePreview(imagePreview, image, IDENTITY, SCOPE)?.blobUrl,
|
|
"blob:https://chat.example/image");
|
|
assert.equal(normalizePreview({...imagePreview, blob_url: "https://remote/image"}, image,
|
|
IDENTITY, SCOPE), null);
|
|
assert.equal(normalizePreview({...imagePreview, text: "inline"}, image, IDENTITY, SCOPE), null);
|
|
});
|
|
|
|
test("immutable history permits append-only versions but not rewrites or orphaning", () => {
|
|
const before = normalizeArtifact(artifact(), IDENTITY.userRef, SCOPE);
|
|
const appendedRaw = artifact({current_version: 3, updated_at: "2026-08-24T10:00:03Z",
|
|
versions: [...artifact().versions, version(3, {content_ref: {hash: HASH_A, bytes: 360,
|
|
mime: "text/markdown"}})]});
|
|
const appended = normalizeArtifact(appendedRaw, IDENTITY.userRef, SCOPE);
|
|
assert.equal(preservesImmutableHistory(before, before), true);
|
|
assert.equal(preservesImmutableHistory(before, appended), true);
|
|
const changes = [
|
|
{...before, id: "art_other1234"}, {...before, owner: "usr_ffffffffffffffff"},
|
|
{...before, projectId: "prj_other1234"}, {...before, conversationId: "conv_other1234"},
|
|
{...before, type: "document"}, {...before, versions: before.versions.slice(1), currentVersion: 1},
|
|
{...before, versions: [{...before.versions[0], note: "rewritten"}, before.versions[1]]},
|
|
{...before, currentVersion: 1},
|
|
];
|
|
for (const changed of changes) assert.equal(preservesImmutableHistory(before, changed), false);
|
|
});
|
|
|
|
test("endpoint contract is default-off, scoped, immutable, and optimistic", () => {
|
|
const client = {apiVersion: "hux.v1", identity: IDENTITY,
|
|
endpoint(path) { assert.match(path, /^\/projects\/prj_[^/]+\/conversations\/conv_[^/]+\/artifacts/);
|
|
return `/hux/v1${path}`; }};
|
|
assert.equal(createArtifactEndpointContract(client, SCOPE), null);
|
|
assert.equal(createArtifactEndpointContract(client, SCOPE, [FOUNDATION_FLAG, ARTIFACTS_FLAG]), null);
|
|
const contract = createArtifactEndpointContract(client, SCOPE, FLAGS);
|
|
assert.equal(contract.list.path, "/hux/v1/projects/prj_test1234/conversations/conv_test1234/artifacts");
|
|
assert.equal(contract.item("art_test1234").method, "GET");
|
|
assert.match(contract.preview("art_test1234", 2).path, /versions\/2\/preview$/);
|
|
assert.deepEqual(contract.diff("art_test1234", 1, 2).requiredBody,
|
|
["from_version", "to_version", "expected_current_version"]);
|
|
assert.match(contract.appendVersion("art_test1234").path, /versions$/);
|
|
assert.match(contract.continueEdit("art_test1234").path, /continue$/);
|
|
assert.match(contract.authorize("art_test1234", "download").path, /authorizations\/download$/);
|
|
assert.match(contract.download("art_test1234", 1).path, /versions\/1\/download$/);
|
|
assert.match(contract.share("art_test1234", 1).path, /versions\/1\/shares$/);
|
|
assert.match(contract.promote("art_test1234").path, /promotions$/);
|
|
assert.match(contract.sources("art_test1234").path, /sources$/);
|
|
for (const call of [() => contract.item("bad"), () => contract.preview("art_test1234", 0),
|
|
() => contract.diff("art_test1234", 2, 1), () => contract.authorize("art_test1234", "execute")]) {
|
|
assert.throws(call, TypeError);
|
|
}
|
|
assert.throws(() => createArtifactEndpointContract({...client, apiVersion: "hux.v2"}, SCOPE, FLAGS),
|
|
/HUX v1/);
|
|
assert.throws(() => createArtifactEndpointContract({...client,
|
|
identity: {...IDENTITY, tenantRef: "bad"}}, SCOPE, FLAGS), /identity/);
|
|
});
|
|
|
|
test("scoped path rejects traversal and malformed identifiers", () => {
|
|
assert.equal(scopedPath("/projects", SCOPE, "art_test1234"),
|
|
"/projects/prj_test1234/conversations/conv_test1234/artifacts/art_test1234");
|
|
for (const call of [() => scopedPath("https://remote", SCOPE), () => scopedPath("//remote", SCOPE),
|
|
() => scopedPath("/../projects", SCOPE), () => scopedPath("/projects?x", SCOPE),
|
|
() => scopedPath("/projects", {...SCOPE, projectId: "bad"}),
|
|
() => scopedPath("/projects", {...SCOPE, conversationId: "bad"}),
|
|
() => scopedPath("/projects", SCOPE, "bad")]) assert.throws(call, TypeError);
|
|
});
|