hermes(webui): track the ignored HUX-04 artifacts card models

The artifacts/ directory matched a repo-wide ignore rule and was left
out of the UI card commit; its suites only passed locally because the
files existed on disk. Force-track the complete card so the source
push carries every module the tests import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
This commit is contained in:
jenkins 2026-08-24 04:19:03 -03:00
parent 13359769dc
commit b2cfbc94e8
7 changed files with 1140 additions and 0 deletions

View File

@ -0,0 +1,221 @@
/** Accessible HUX-04 artifact UI. This module never fetches or executes content. */
import {
Code2,
Download,
FileText,
GitCompare,
History,
Image as ImageIcon,
Pencil,
Share2,
Sparkles,
} from "lucide-react";
import { useMemo, useState } from "react";
import { artifactsEnabled, normalizeArtifactWorkspace, normalizePreview } from "./model.ts";
import type {
Artifact,
ArtifactAuthorization,
ArtifactMutationIntent,
ArtifactPreview,
ArtifactScope,
DiffIntent,
HuxIdentity,
PromoteIntent,
RawArtifactEnvelope,
RawPreviewPayload,
} from "./types.ts";
interface ArtifactWorkspaceProps {
flags?: Iterable<string>;
identity: HuxIdentity;
scope: ArtifactScope;
page: RawArtifactEnvelope;
previews?: readonly RawPreviewPayload[];
busy?: boolean;
error?: string | null;
onLoadPreview?: (intent: ArtifactMutationIntent & {version: number}) => void;
onContinueEdit?: (intent: ArtifactMutationIntent) => void;
onDiff?: (intent: DiffIntent) => void;
onAuthorize?: (action: "download" | "share", intent: ArtifactMutationIntent) => void;
onDownload?: (intent: ArtifactMutationIntent & {version: number}) => void;
onShare?: (intent: ArtifactMutationIntent & {version: number}) => void;
onPromote?: (intent: PromoteIntent) => void;
}
function intent(artifact: Artifact, scope: ArtifactScope): ArtifactMutationIntent {
return {artifactId: artifact.id, expectedVersion: artifact.currentVersion, scope};
}
function RendererIcon({artifact}: {artifact: Artifact}) {
if (artifact.renderer === "image") return <ImageIcon aria-hidden />;
if (artifact.renderer === "code" || artifact.renderer === "data") return <Code2 aria-hidden />;
return <FileText aria-hidden />;
}
function Preview({artifact, preview}: {artifact: Artifact; preview: ArtifactPreview | null}) {
if (!preview) return <div className="hux-artifact-preview-empty">
<RendererIcon artifact={artifact} />
<p>Preview not loaded. Content stays behind its verified content reference.</p>
</div>;
if (preview.renderer === "image") {
return <figure className="hux-artifact-image">
<img src={preview.blobUrl!} alt={`Preview of ${artifact.title}`} />
<figcaption>{preview.mime}; local authorized preview</figcaption>
</figure>;
}
if (preview.renderer === "audio") {
return <div className="hux-artifact-audio">
<audio src={preview.blobUrl!} controls preload="metadata">
Audio preview is unavailable in this browser.
</audio>
<span>{preview.mime}; local authorized preview</span>
</div>;
}
const className = `hux-artifact-${preview.renderer}`;
return <div className={className} aria-label={`${preview.renderer} preview`}>
{preview.renderer === "report" && <h3>Report preview</h3>}
<pre tabIndex={0}><code>{preview.text}</code></pre>
</div>;
}
function AuthorizationAction({action, state, busy, onAuthorize, onAction}: {
action: "download" | "share";
state: ArtifactAuthorization["download"];
busy: boolean;
onAuthorize: () => void;
onAction: () => void;
}) {
const label = action === "download" ? "Download" : "Share";
const Icon = action === "download" ? Download : Share2;
if (state === "authorized") return <button type="button" disabled={busy} onClick={onAction}>
<Icon aria-hidden /> {label}
</button>;
if (state === "requires_approval") return <button type="button" disabled={busy} onClick={onAuthorize}>
<Icon aria-hidden /> Request {label.toLowerCase()} approval
</button>;
return <button type="button" disabled aria-label={`${label} unavailable`}>
<Icon aria-hidden /> {label} unavailable
</button>;
}
function VersionHistory({artifact, selectedVersion, setSelectedVersion, busy, scope, onDiff}: {
artifact: Artifact;
selectedVersion: number;
setSelectedVersion: (version: number) => void;
busy: boolean;
scope: ArtifactScope;
onDiff?: ArtifactWorkspaceProps["onDiff"];
}) {
return <section className="hux-artifact-history" aria-labelledby="hux-artifact-history-title">
<h3 id="hux-artifact-history-title"><History aria-hidden /> Immutable version history</h3>
<ol>{[...artifact.versions].reverse().map((version) => <li key={version.version}
data-current={version.version === artifact.currentVersion}>
<button type="button" aria-pressed={selectedVersion === version.version}
onClick={() => setSelectedVersion(version.version)}>
<strong>Version {version.version}</strong>
<span>{version.note || `Created by ${version.createdBy}`}</span>
<time dateTime={version.createdAt}>{new Date(version.createdAt).toLocaleString()}</time>
</button>
{version.version > 1 && onDiff && <button className="hux-artifact-diff" type="button"
disabled={busy} onClick={() => onDiff({
...intent(artifact, scope), fromVersion: version.diffFrom || version.version - 1,
toVersion: version.version,
})} aria-label={`Compare version ${version.version}`}><GitCompare aria-hidden /> Compare</button>}
</li>)}</ol>
</section>;
}
/** Render nothing unless all negotiated HUX-04 dependencies are active. */
export function ArtifactWorkspace(props: ArtifactWorkspaceProps) {
const enabled = artifactsEnabled(props.flags);
const page = useMemo(() => enabled ?
normalizeArtifactWorkspace(props.page, props.identity, props.scope) : null,
[enabled, props.identity, props.page, props.scope]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [selectedVersion, setSelectedVersion] = useState<number | null>(null);
if (!enabled || !page) return null;
const artifact = page.artifacts.find((item) => item.id === selectedId) || page.artifacts[0] || null;
const version = artifact ? Math.min(selectedVersion || artifact.currentVersion, artifact.currentVersion) : null;
const rawPreview = artifact && version ? props.previews?.find((item) =>
item.artifact_id === artifact.id && item.version === version) : undefined;
const preview = artifact && rawPreview ?
normalizePreview(rawPreview, artifact, props.identity, props.scope) : null;
const attachments = artifact ? page.attachments.find((item) => item.artifactId === artifact.id) : null;
const authorization = artifact ? page.authorizations.find((item) => item.artifactId === artifact.id) : null;
function selectArtifact(item: Artifact) {
setSelectedId(item.id);
setSelectedVersion(item.currentVersion);
props.onLoadPreview?.({...intent(item, props.scope), version: item.currentVersion});
}
function selectVersion(next: number) {
if (!artifact) return;
setSelectedVersion(next);
props.onLoadPreview?.({...intent(artifact, props.scope), version: next});
}
return <section className="hux-artifacts" aria-label="Artifact workspace" aria-busy={props.busy}>
<nav className="hux-artifact-list" aria-label="Artifacts">
<header><h2><Sparkles aria-hidden /> Artifacts</h2>
<p>Durable outputs from this conversation.</p></header>
<ul>{page.artifacts.map((item) => <li key={item.id}>
<button type="button" aria-current={artifact?.id === item.id ? "page" : undefined}
onClick={() => selectArtifact(item)}>
<RendererIcon artifact={item} />
<span><strong>{item.title}</strong><small>{item.renderer} · v{item.currentVersion}</small></span>
</button>
</li>)}</ul>
{!page.artifacts.length && <p className="hux-artifact-empty">No artifacts are attached.</p>}
</nav>
<main className="hux-artifact-main">
{props.error && <p className="hux-artifact-error" role="alert">{props.error}</p>}
{page.rejected > 0 && <p className="hux-artifact-warning" role="status">
Some artifacts were withheld because their owner or workspace binding was invalid.
</p>}
{artifact && version ? <>
<header className="hux-artifact-heading">
<div><span>{artifact.renderer}</span><h2>{artifact.title}</h2>
<p>Version {version} · {artifact.sensitivity} · {artifact.language || artifact.type}</p></div>
<div className="hux-artifact-actions">
<button type="button" disabled={props.busy}
onClick={() => props.onContinueEdit?.(intent(artifact, props.scope))}>
<Pencil aria-hidden /> Continue editing
</button>
<AuthorizationAction action="download" state={authorization?.download || "unavailable"}
busy={Boolean(props.busy)}
onAuthorize={() => props.onAuthorize?.("download", intent(artifact, props.scope))}
onAction={() => props.onDownload?.({...intent(artifact, props.scope), version})} />
<AuthorizationAction action="share" state={authorization?.share || "unavailable"}
busy={Boolean(props.busy)}
onAuthorize={() => props.onAuthorize?.("share", intent(artifact, props.scope))}
onAction={() => props.onShare?.({...intent(artifact, props.scope), version})} />
</div>
</header>
<Preview artifact={artifact} preview={preview} />
<section className="hux-artifact-evidence" aria-label="Attached research evidence">
<div><strong>{attachments?.sourceCount || 0}</strong><span>Sources attached</span></div>
<div><strong>{attachments?.citationCount || 0}</strong><span>Citations attached</span></div>
</section>
<button className="hux-artifact-promote" type="button" disabled={props.busy ||
artifact.promotedVersion === version} onClick={() => props.onPromote?.({
...intent(artifact, props.scope), targetProjectId: props.scope.projectId, version,
})}>
{artifact.promotedVersion === version ? "Promoted to this project" : "Promote version to project"}
</button>
</> : <p className="hux-artifact-empty">Select an artifact to inspect it.</p>}
</main>
<aside className="hux-artifact-side" aria-label="Artifact versions">
{artifact && version ? <VersionHistory artifact={artifact} selectedVersion={version}
setSelectedVersion={selectVersion} busy={Boolean(props.busy)} scope={props.scope}
onDiff={props.onDiff} /> : <p>Version history will appear here.</p>}
</aside>
<p className="sr-only" role="status" aria-live="polite">
{props.busy ? "Artifact workspace is updating" : `${page.artifacts.length} artifacts available`}
</p>
</section>;
}

View File

@ -0,0 +1,94 @@
/** Same-origin endpoint descriptions for a future HUX-04 backend adapter. */
import { artifactsEnabled } from "./model.ts";
import { isOpaqueId, normalizeIdentity, scopedPath } from "./security.ts";
import type {
ArtifactEndpoint,
ArtifactEndpointContract,
ArtifactScope,
HuxIdentity,
} from "./types.ts";
interface FoundationClient {
apiVersion: string;
identity: HuxIdentity;
endpoint(path: string): string;
}
function endpoint(method: ArtifactEndpoint["method"], path: string, responseSchema: string,
requiredBody?: readonly string[]): ArtifactEndpoint {
return Object.freeze({method, path, responseSchema, ...(requiredBody ? {requiredBody} : {})});
}
function versionNumber(value: number): string {
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("Artifact version is invalid");
return String(value);
}
/**
* The server derives identity from the authenticated session and verifies the
* project/conversation path binding. Mutation bodies MUST carry
* expected_current_version and return 409 on stale state. Existing versions
* are never PATCHed or deleted; edits append a new content_ref-backed version.
*/
export function createArtifactEndpointContract(client: FoundationClient, scope: ArtifactScope,
flags?: Iterable<string>): ArtifactEndpointContract | null {
if (!artifactsEnabled(flags)) return null;
if (client.apiVersion !== "hux.v1") throw new TypeError("HUX v1 client required");
normalizeIdentity({tenant_ref: client.identity.tenantRef, user_ref: client.identity.userRef,
surface: client.identity.surface});
const collection = scopedPath("/projects", scope);
function itemPath(artifactId: string): string {
if (!isOpaqueId(artifactId, "art")) throw new TypeError("Opaque artifact id required");
return scopedPath("/projects", scope, artifactId);
}
function itemEndpoint(artifactId: string, suffix = ""): string {
return client.endpoint(`${itemPath(artifactId)}${suffix}`);
}
return Object.freeze({
list: endpoint("GET", client.endpoint(collection), "hux.artifact_workspace.v1"),
item(artifactId) {
return endpoint("GET", itemEndpoint(artifactId), "hux.artifact_response.v1");
},
preview(artifactId, version) {
return endpoint("GET", itemEndpoint(artifactId, `/versions/${versionNumber(version)}/preview`),
"hux.artifact_preview.v1");
},
diff(artifactId, fromVersion, toVersion) {
if (fromVersion >= toVersion) throw new TypeError("Artifact diff range is invalid");
return endpoint("POST", itemEndpoint(artifactId, "/diffs"), "hux.artifact_diff.v1",
Object.freeze(["from_version", "to_version", "expected_current_version"]));
},
appendVersion(artifactId) {
return endpoint("POST", itemEndpoint(artifactId, "/versions"), "hux.artifact_response.v1",
Object.freeze(["content_ref", "expected_current_version"]));
},
continueEdit(artifactId) {
return endpoint("POST", itemEndpoint(artifactId, "/continue"), "hux.artifact_edit_intent.v1",
Object.freeze(["version", "expected_current_version"]));
},
authorize(artifactId, action) {
if (action !== "download" && action !== "share") throw new TypeError("Artifact action is invalid");
return endpoint("POST", itemEndpoint(artifactId, `/authorizations/${action}`),
"hux.artifact_authorization.v1", Object.freeze(["expected_current_version"]));
},
download(artifactId, version) {
return endpoint("GET", itemEndpoint(artifactId, `/versions/${versionNumber(version)}/download`),
"hux.artifact_content.v1");
},
share(artifactId, version) {
return endpoint("POST", itemEndpoint(artifactId, `/versions/${versionNumber(version)}/shares`),
"hux.artifact_share.v1", Object.freeze(["expected_current_version"]));
},
promote(artifactId) {
return endpoint("POST", itemEndpoint(artifactId, "/promotions"), "hux.artifact_response.v1",
Object.freeze(["target_project_id", "version", "expected_current_version"]));
},
sources(artifactId) {
return endpoint("GET", itemEndpoint(artifactId, "/sources"), "hux.artifact_sources.v1");
},
});
}

View File

@ -0,0 +1,14 @@
export { ArtifactWorkspace } from "./ArtifactWorkspace.tsx";
export { createArtifactEndpointContract } from "./endpoints.ts";
export {
ARTIFACTS_FLAG,
FOUNDATION_FLAG,
PROJECTS_FLAG,
artifactsEnabled,
normalizeArtifact,
normalizeArtifactWorkspace,
normalizePreview,
preservesImmutableHistory,
} from "./model.ts";
export { ArtifactContractError } from "./security.ts";
export type * from "./types.ts";

View File

@ -0,0 +1,239 @@
/** Fail-closed normalization and immutable-history invariants for HUX-04. */
import {
ArtifactContractError,
isMime,
isOpaqueId,
isSha256,
isUtc,
normalizeIdentity,
normalizeScope,
safeBlobUrl,
safePreviewText,
safeText,
sameIdentity,
sameScope,
} from "./security.ts";
import type {
Artifact,
ArtifactAttachments,
ArtifactAuthorization,
ArtifactPreview,
ArtifactRenderer,
ArtifactScope,
ArtifactType,
ArtifactVersion,
AuthorizationStatus,
HuxIdentity,
RawArtifact,
RawArtifactEnvelope,
RawArtifactVersion,
RawPreviewPayload,
Sensitivity,
} from "./types.ts";
export const FOUNDATION_FLAG = "hux.foundation";
export const PROJECTS_FLAG = "hux.projects";
export const ARTIFACTS_FLAG = "hux.artifacts";
const TYPES = new Set<ArtifactType>([
"markdown", "code", "html", "svg", "image", "json", "csv", "document", "audio",
]);
const SENSITIVITIES = new Set<Sensitivity>(["public", "personal", "sensitive", "restricted"]);
const AUTHORIZATION = new Set<AuthorizationStatus>(["unavailable", "requires_approval", "authorized"]);
const ACTORS = new Set(["user", "assistant", "tool", "system", "operator"]);
const MAX_ARTIFACTS = 200;
const MAX_VERSIONS = 200;
export function artifactsEnabled(flags?: Iterable<string>): boolean {
const active = new Set(flags || []);
return active.has(FOUNDATION_FLAG) && active.has(PROJECTS_FLAG) && active.has(ARTIFACTS_FLAG);
}
function rendererFor(type: ArtifactType, language: string): ArtifactRenderer {
if (type === "image") return "image";
if (type === "audio") return "audio";
if (["code", "html", "svg"].includes(type)) return "code";
if (["json", "csv"].includes(type)) return "data";
if (/^(report|research|analysis)$/i.test(language)) return "report";
return "document";
}
function mimeAllowed(type: ArtifactType, mime: string): boolean {
if (type === "image") return ["image/png", "image/jpeg", "image/webp", "image/gif"].includes(mime);
if (type === "audio") return ["audio/mpeg", "audio/ogg", "audio/wav", "audio/webm"].includes(mime);
if (type === "html") return mime === "text/html";
if (type === "svg") return mime === "image/svg+xml";
if (type === "json") return mime === "application/json" || mime === "text/plain";
if (type === "csv") return mime === "text/csv" || mime === "text/plain";
if (type === "markdown") return mime === "text/markdown" || mime === "text/plain";
if (type === "code") return mime.startsWith("text/") || mime === "application/json";
return mime === "application/pdf" || mime === "text/plain" || mime === "text/markdown";
}
function normalizeVersion(raw: RawArtifactVersion, type: ArtifactType): ArtifactVersion | null {
const actor = raw.created_by as Record<string, unknown> | null;
const content = raw.content_ref as Record<string, unknown> | null;
if (!Number.isInteger(raw.version) || (raw.version as number) < 1 || !isUtc(raw.created_at) ||
!actor || !ACTORS.has(String(actor.type)) || typeof actor.id !== "string" ||
actor.id.length < 1 || actor.id.length > 120 || !content ||
!isSha256(content.hash) || !Number.isSafeInteger(content.bytes) ||
(content.bytes as number) < 0 || !isMime(content.mime) ||
!mimeAllowed(type, content.mime) || (raw.diff_from !== undefined &&
(!Number.isInteger(raw.diff_from) || (raw.diff_from as number) < 1))) return null;
let lineage: ArtifactVersion["lineage"] = null;
if (raw.lineage !== undefined) {
const value = raw.lineage as Record<string, unknown> | null;
if (!value || !isOpaqueId(value.artifact_id, "art") ||
!Number.isInteger(value.version) || (value.version as number) < 1) return null;
lineage = {artifactId: value.artifact_id, version: value.version as number};
}
return {
version: raw.version as number,
createdAt: raw.created_at,
createdBy: String(actor.type),
contentRef: {hash: content.hash, bytes: content.bytes as number, mime: content.mime},
diffFrom: raw.diff_from as number | undefined || null,
lineage,
note: safeText(raw.note, "", 200),
};
}
export function normalizeArtifact(raw: RawArtifact, expectedOwner: string, scope: ArtifactScope): Artifact | null {
if (raw.schema !== "hux.artifact.v1" || !isOpaqueId(raw.id, "art") ||
raw.owner !== expectedOwner || raw.project_id !== scope.projectId ||
raw.conversation_id !== scope.conversationId || !TYPES.has(raw.type as ArtifactType) ||
!Number.isInteger(raw.current_version) || (raw.current_version as number) < 1 ||
!Array.isArray(raw.versions) || raw.versions.length < 1 || raw.versions.length > MAX_VERSIONS ||
!SENSITIVITIES.has(raw.sensitivity as Sensitivity) || !isUtc(raw.created_at) ||
!isUtc(raw.updated_at) || raw.updated_at < raw.created_at) return null;
const type = raw.type as ArtifactType;
const language = safeText(raw.language, "", 40);
const title = safeText(raw.title, "", 200);
if (!title) return null;
const versions = raw.versions.map((item) => normalizeVersion(item as RawArtifactVersion, type));
if (versions.some((item) => item === null)) return null;
const ordered = versions as ArtifactVersion[];
if (ordered.some((item, index) => item.version !== index + 1 ||
(index > 0 && item.createdAt < ordered[index - 1].createdAt) ||
(item.diffFrom !== null && item.diffFrom >= item.version) ||
(item.lineage?.artifactId === raw.id && item.lineage.version >= item.version)) ||
raw.current_version !== ordered.length) return null;
let promotedVersion: number | null = null;
if (raw.promotion !== undefined) {
const promotion = raw.promotion as Record<string, unknown> | null;
if (!promotion || promotion.project_id !== scope.projectId ||
!Number.isInteger(promotion.version) || (promotion.version as number) < 1 ||
(promotion.version as number) > ordered.length || !isUtc(promotion.at)) return null;
promotedVersion = promotion.version as number;
}
return {
id: raw.id, owner: expectedOwner, projectId: scope.projectId,
conversationId: scope.conversationId, type, renderer: rendererFor(type, language), language,
title, currentVersion: raw.current_version as number, versions: ordered,
sensitivity: raw.sensitivity as Sensitivity, promotedVersion, updatedAt: raw.updated_at,
};
}
function normalizeAttachments(raw: unknown, allowed: ReadonlySet<string>): ArtifactAttachments[] {
if (!Array.isArray(raw)) return [];
const result: ArtifactAttachments[] = [];
const seen = new Set<string>();
for (const item of raw.slice(0, MAX_ARTIFACTS)) {
const value = item as Record<string, unknown> | null;
const sources = value?.source_ids;
const citations = value?.citation_ids;
if (!value || !isOpaqueId(value.artifact_id, "art") || !allowed.has(value.artifact_id) ||
seen.has(value.artifact_id) || !Array.isArray(sources) || !Array.isArray(citations) ||
sources.length > 500 || citations.length > 500 ||
sources.some((id) => !isOpaqueId(id, "src")) ||
citations.some((id) => !isOpaqueId(id, "cit")) ||
new Set(sources).size !== sources.length || new Set(citations).size !== citations.length) continue;
seen.add(value.artifact_id);
result.push({artifactId: value.artifact_id, sourceCount: sources.length, citationCount: citations.length});
}
return result;
}
function normalizeAuthorizations(raw: unknown, allowed: ReadonlySet<string>): ArtifactAuthorization[] {
if (!Array.isArray(raw)) return [];
const result: ArtifactAuthorization[] = [];
const seen = new Set<string>();
for (const item of raw.slice(0, MAX_ARTIFACTS)) {
const value = item as Record<string, unknown> | null;
if (!value || !isOpaqueId(value.artifact_id, "art") || !allowed.has(value.artifact_id) ||
seen.has(value.artifact_id) || !AUTHORIZATION.has(value.download as AuthorizationStatus) ||
!AUTHORIZATION.has(value.share as AuthorizationStatus)) continue;
seen.add(value.artifact_id);
result.push({artifactId: value.artifact_id, download: value.download as AuthorizationStatus,
share: value.share as AuthorizationStatus});
}
return result;
}
export function normalizeArtifactWorkspace(raw: RawArtifactEnvelope, expected: HuxIdentity,
scope: ArtifactScope) {
const trustedIdentity = normalizeIdentity({tenant_ref: expected.tenantRef,
user_ref: expected.userRef, surface: expected.surface});
if (!raw || typeof raw !== "object" || raw.schema !== "hux.artifact_workspace.v1" ||
raw.api_version !== "hux.v1" || !sameIdentity(normalizeIdentity(raw.identity), trustedIdentity) ||
!sameScope(normalizeScope(raw.binding), scope)) {
throw new ArtifactContractError("Artifact workspace crossed its scope boundary");
}
const records = Array.isArray(raw.artifacts) ? raw.artifacts : [];
const normalized = records.slice(0, MAX_ARTIFACTS)
.map((item) => normalizeArtifact(item as RawArtifact, trustedIdentity.userRef, scope))
.filter((item): item is Artifact => item !== null);
const ids = new Set<string>();
const artifacts = normalized.filter((item) => !ids.has(item.id) && Boolean(ids.add(item.id)));
const allowed = new Set(artifacts.map((item) => item.id));
return {
artifacts,
attachments: normalizeAttachments(raw.attachments, allowed),
authorizations: normalizeAuthorizations(raw.authorizations, allowed),
rejected: records.length - artifacts.length,
};
}
export function normalizePreview(raw: RawPreviewPayload, artifact: Artifact, expected: HuxIdentity,
scope: ArtifactScope): ArtifactPreview | null {
try {
const identity = normalizeIdentity(raw.identity);
const trustedIdentity = normalizeIdentity({tenant_ref: expected.tenantRef,
user_ref: expected.userRef, surface: expected.surface});
const binding = normalizeScope(raw.binding);
const version = artifact.versions.find((item) => item.version === raw.version);
if (raw.schema !== "hux.artifact_preview.v1" || !sameIdentity(identity, trustedIdentity) ||
!sameScope(binding, scope) || raw.artifact_id !== artifact.id || !version ||
raw.hash !== version.contentRef.hash || raw.mime !== version.contentRef.mime) return null;
if (artifact.renderer === "image" || artifact.renderer === "audio") {
const blobUrl = safeBlobUrl(raw.blob_url);
if (!blobUrl || raw.text !== undefined) return null;
return {artifactId: artifact.id, version: version.version, renderer: artifact.renderer,
mime: version.contentRef.mime, text: null, blobUrl};
}
if (raw.blob_url !== undefined) return null;
const text = safePreviewText(raw.text);
if (text === null) return null;
return {artifactId: artifact.id, version: version.version, renderer: artifact.renderer,
mime: version.contentRef.mime, text, blobUrl: null};
} catch {
return null;
}
}
function sameVersion(left: ArtifactVersion, right: ArtifactVersion): boolean {
return left.version === right.version && left.createdAt === right.createdAt &&
left.createdBy === right.createdBy && left.contentRef.hash === right.contentRef.hash &&
left.contentRef.bytes === right.contentRef.bytes && left.contentRef.mime === right.contentRef.mime &&
left.diffFrom === right.diffFrom && left.lineage?.artifactId === right.lineage?.artifactId &&
left.lineage?.version === right.lineage?.version && left.note === right.note;
}
/** A server mutation may append versions but never rewrite history or detach scope. */
export function preservesImmutableHistory(before: Artifact, after: Artifact): boolean {
return before.id === after.id && before.owner === after.owner &&
before.projectId === after.projectId && before.conversationId === after.conversationId &&
before.type === after.type && before.versions.length <= after.versions.length &&
before.versions.every((version, index) => sameVersion(version, after.versions[index])) &&
after.currentVersion === after.versions.length;
}

View File

@ -0,0 +1,104 @@
/** Identity, content-reference, URL, and display guards for HUX-04. */
import type { ArtifactScope, HuxIdentity } from "./types.ts";
const OPAQUE_ID = /^[a-z]{2,6}_[A-Za-z0-9._-]{4,80}$/;
const TENANT = /^tnt_[0-9a-f]{16,64}$/;
const USER = /^usr_[0-9a-f]{16,64}$/;
const UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/;
const HASH = /^sha256:[0-9a-f]{64}$/;
const MIME = /^[a-z0-9][a-z0-9.+-]{0,63}\/[a-z0-9][a-z0-9.+-]{0,63}$/i;
const CONTROL = /[\u0000-\u001f\u007f]/g;
const PREVIEW_CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
const SURFACES = new Set(["chat", "worker", "telegram", "voice", "api"]);
const CREDENTIALS = [
/\b(?:password|passwd|token|secret|api[_-]?key)\s*[:=]\s*\S+/gi,
/\bBearer\s+[A-Za-z0-9._~-]+/gi,
/-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/gi,
];
export class ArtifactContractError extends Error {
constructor(message: string) {
super(message);
this.name = "ArtifactContractError";
}
}
export function isOpaqueId(value: unknown, prefix?: string): value is string {
return typeof value === "string" && OPAQUE_ID.test(value) &&
(!prefix || value.startsWith(`${prefix}_`));
}
export function isUtc(value: unknown): value is string {
return typeof value === "string" && UTC.test(value) && !Number.isNaN(Date.parse(value));
}
export function isSha256(value: unknown): value is string {
return typeof value === "string" && HASH.test(value);
}
export function isMime(value: unknown): value is string {
return typeof value === "string" && value.length <= 120 && MIME.test(value);
}
export function safeText(value: unknown, fallback: string, limit: number): string {
if (typeof value !== "string") return fallback;
let clean = value.replace(CONTROL, " ").replace(/\s+/g, " ").trim();
for (const pattern of CREDENTIALS) clean = clean.replace(pattern, "[redacted]");
if (!clean) return fallback;
return clean.length <= limit ? clean : `${clean.slice(0, Math.max(0, limit - 1)).trimEnd()}`;
}
export function safePreviewText(value: unknown, limit = 200_000): string | null {
return typeof value === "string" && value.length <= limit && !PREVIEW_CONTROL.test(value) ? value : null;
}
export function normalizeIdentity(raw: unknown): HuxIdentity {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
throw new ArtifactContractError("Artifact identity is invalid");
}
const value = raw as Record<string, unknown>;
if (typeof value.tenant_ref !== "string" || !TENANT.test(value.tenant_ref) ||
typeof value.user_ref !== "string" || !USER.test(value.user_ref) ||
typeof value.surface !== "string" || !SURFACES.has(value.surface)) {
throw new ArtifactContractError("Artifact identity is invalid");
}
return {tenantRef: value.tenant_ref, userRef: value.user_ref,
surface: value.surface as HuxIdentity["surface"]};
}
export function sameIdentity(left: HuxIdentity, right: HuxIdentity): boolean {
return left.tenantRef === right.tenantRef && left.userRef === right.userRef &&
left.surface === right.surface;
}
export function normalizeScope(raw: unknown): ArtifactScope {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
throw new ArtifactContractError("Artifact binding is invalid");
}
const value = raw as Record<string, unknown>;
if (!isOpaqueId(value.project_id, "prj") || !isOpaqueId(value.conversation_id, "conv")) {
throw new ArtifactContractError("Artifact binding is invalid");
}
return {projectId: value.project_id, conversationId: value.conversation_id};
}
export function sameScope(left: ArtifactScope, right: ArtifactScope): boolean {
return left.projectId === right.projectId && left.conversationId === right.conversationId;
}
/** Only local object URLs created from an authorized response may reach img/audio. */
export function safeBlobUrl(value: unknown): string | null {
return typeof value === "string" && /^blob:[^\s]{1,2000}$/.test(value) ? value : null;
}
export function scopedPath(base: string, scope: ArtifactScope, artifactId?: string): string {
if (!base.startsWith("/") || base.startsWith("//") || /[?#\\]/.test(base) || base.includes("..") ||
!isOpaqueId(scope.projectId, "prj") || !isOpaqueId(scope.conversationId, "conv") ||
(artifactId !== undefined && !isOpaqueId(artifactId, "art"))) {
throw new TypeError("Artifact endpoint scope is invalid");
}
const parts = [base.replace(/\/$/, ""), scope.projectId, "conversations", scope.conversationId, "artifacts"];
if (artifactId) parts.push(artifactId);
return parts.map((part, index) => index ? encodeURIComponent(part) : part).join("/");
}

View File

@ -0,0 +1,294 @@
.hux-artifacts {
color: var(--hux-text, #dce8f4);
display: grid;
gap: 0.8rem;
grid-template-columns: minmax(12rem, 0.75fr) minmax(20rem, 2fr) minmax(13rem, 0.85fr);
min-height: 30rem;
}
.hux-artifact-list,
.hux-artifact-main,
.hux-artifact-side {
background: color-mix(in srgb, currentColor 2.5%, transparent);
border: 1px solid color-mix(in srgb, currentColor 13%, transparent);
border-radius: 0.9rem;
min-width: 0;
padding: 0.8rem;
}
.hux-artifact-list header h2,
.hux-artifact-history h3,
.hux-artifact-list button,
.hux-artifact-actions,
.hux-artifact-actions button,
.hux-artifact-heading,
.hux-artifact-preview-empty,
.hux-artifact-evidence,
.hux-artifact-promote,
.hux-artifact-diff {
align-items: center;
display: flex;
}
.hux-artifact-list header h2,
.hux-artifact-history h3 {
font-size: 0.9rem;
gap: 0.4rem;
margin: 0;
}
.hux-artifact-list header p,
.hux-artifact-heading p,
.hux-artifact-empty {
color: color-mix(in srgb, currentColor 58%, transparent);
font-size: 0.7rem;
margin: 0.25rem 0 0;
}
.hux-artifact-list ul,
.hux-artifact-history ol {
display: grid;
gap: 0.4rem;
list-style: none;
margin: 0.8rem 0 0;
padding: 0;
}
.hux-artifact-list button,
.hux-artifact-history button,
.hux-artifact-actions button,
.hux-artifact-promote,
.hux-artifact-diff {
background: color-mix(in srgb, currentColor 4%, transparent);
border: 1px solid color-mix(in srgb, currentColor 13%, transparent);
border-radius: 0.6rem;
color: inherit;
cursor: pointer;
font: inherit;
}
.hux-artifact-list button {
gap: 0.55rem;
padding: 0.55rem;
text-align: left;
width: 100%;
}
.hux-artifact-list button[aria-current="page"] {
background: color-mix(in srgb, var(--hux-accent, #25d4c3) 11%, transparent);
border-color: color-mix(in srgb, var(--hux-accent, #25d4c3) 45%, transparent);
}
.hux-artifact-list button > span {
display: grid;
gap: 0.15rem;
min-width: 0;
}
.hux-artifact-list strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hux-artifact-list small {
color: color-mix(in srgb, currentColor 55%, transparent);
text-transform: capitalize;
}
.hux-artifact-list svg,
.hux-artifact-actions svg,
.hux-artifact-history svg,
.hux-artifact-preview-empty svg,
.hux-artifact-promote svg {
flex: 0 0 auto;
height: 0.95rem;
width: 0.95rem;
}
.hux-artifact-heading {
align-items: start;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: space-between;
}
.hux-artifact-heading > div:first-child > span {
color: var(--hux-accent, #25d4c3);
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.hux-artifact-heading h2 {
font-size: 1rem;
margin: 0.15rem 0;
}
.hux-artifact-actions {
flex-wrap: wrap;
gap: 0.4rem;
justify-content: flex-end;
}
.hux-artifact-actions button,
.hux-artifact-promote,
.hux-artifact-diff {
gap: 0.35rem;
padding: 0.4rem 0.55rem;
}
.hux-artifacts button:focus-visible,
.hux-artifacts pre:focus-visible {
outline: 2px solid var(--hux-accent, #25d4c3);
outline-offset: 2px;
}
.hux-artifacts button:disabled {
cursor: not-allowed;
opacity: 0.48;
}
.hux-artifact-document,
.hux-artifact-code,
.hux-artifact-report,
.hux-artifact-data,
.hux-artifact-image,
.hux-artifact-audio,
.hux-artifact-preview-empty {
background: color-mix(in srgb, #050a12 70%, transparent);
border: 1px solid color-mix(in srgb, currentColor 12%, transparent);
border-radius: 0.75rem;
margin: 0.85rem 0;
min-height: 15rem;
overflow: auto;
padding: 0.8rem;
}
.hux-artifact-report h3 {
color: var(--hux-accent, #25d4c3);
font-size: 0.7rem;
letter-spacing: 0.08em;
margin: 0 0 0.65rem;
text-transform: uppercase;
}
.hux-artifact-document pre,
.hux-artifact-code pre,
.hux-artifact-report pre,
.hux-artifact-data pre {
font: 0.76rem/1.6 ui-monospace, SFMono-Regular, Consolas, monospace;
margin: 0;
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.hux-artifact-preview-empty {
align-content: center;
color: color-mix(in srgb, currentColor 55%, transparent);
flex-direction: column;
justify-content: center;
text-align: center;
}
.hux-artifact-image {
display: grid;
place-items: center;
}
.hux-artifact-image img {
max-height: 32rem;
max-width: 100%;
object-fit: contain;
}
.hux-artifact-image figcaption,
.hux-artifact-audio span {
color: color-mix(in srgb, currentColor 55%, transparent);
font-size: 0.65rem;
margin-top: 0.5rem;
}
.hux-artifact-audio {
align-content: center;
display: grid;
}
.hux-artifact-audio audio { width: 100%; }
.hux-artifact-evidence {
gap: 0.55rem;
margin: 0.8rem 0;
}
.hux-artifact-evidence div {
background: color-mix(in srgb, currentColor 4%, transparent);
border-radius: 0.55rem;
display: grid;
flex: 1;
padding: 0.55rem;
}
.hux-artifact-evidence strong { color: var(--hux-accent, #25d4c3); }
.hux-artifact-evidence span { font-size: 0.65rem; }
.hux-artifact-promote { margin-left: auto; }
.hux-artifact-history ol > li {
border-bottom: 1px solid color-mix(in srgb, currentColor 10%, transparent);
display: grid;
gap: 0.35rem;
padding-bottom: 0.45rem;
}
.hux-artifact-history ol > li[data-current="true"] { border-color: var(--hux-accent, #25d4c3); }
.hux-artifact-history li > button:first-child {
display: grid;
gap: 0.15rem;
padding: 0.48rem;
text-align: left;
width: 100%;
}
.hux-artifact-history li > button[aria-pressed="true"] {
background: color-mix(in srgb, var(--hux-accent, #25d4c3) 9%, transparent);
}
.hux-artifact-history span,
.hux-artifact-history time {
color: color-mix(in srgb, currentColor 56%, transparent);
font-size: 0.63rem;
}
.hux-artifact-diff { font-size: 0.68rem; justify-self: end; }
.hux-artifact-error,
.hux-artifact-warning {
border-left: 2px solid #efb756;
color: #efcf96;
font-size: 0.72rem;
margin: 0 0 0.7rem;
padding-left: 0.55rem;
}
.hux-artifact-error { border-color: #e36b7c; color: #f0a4ae; }
@media (max-width: 66rem) {
.hux-artifacts { grid-template-columns: minmax(12rem, 0.7fr) minmax(20rem, 2fr); }
.hux-artifact-side { grid-column: 1 / -1; }
.hux-artifact-history ol { grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); }
}
@media (max-width: 42rem) {
.hux-artifacts { grid-template-columns: 1fr; }
.hux-artifact-side { grid-column: auto; }
.hux-artifact-heading { align-items: stretch; }
.hux-artifact-actions { justify-content: stretch; }
.hux-artifact-actions button { flex: 1; }
}
@media (prefers-reduced-motion: reduce) {
.hux-artifacts * { scroll-behavior: auto; transition: none; }
}

View File

@ -0,0 +1,174 @@
/** Safe frontend contracts for the default-off HUX-04 artifact workspace. */
export type HuxSurface = "chat" | "worker" | "telegram" | "voice" | "api";
export type ArtifactType =
| "markdown"
| "code"
| "html"
| "svg"
| "image"
| "json"
| "csv"
| "document"
| "audio";
export type ArtifactRenderer = "document" | "code" | "image" | "report" | "data" | "audio";
export type Sensitivity = "public" | "personal" | "sensitive" | "restricted";
export type AuthorizationStatus = "unavailable" | "requires_approval" | "authorized";
export interface HuxIdentity {
tenantRef: string;
userRef: string;
surface: HuxSurface;
}
export interface ArtifactScope {
projectId: string;
conversationId: string;
}
export interface RawArtifactVersion {
version?: unknown;
created_at?: unknown;
created_by?: unknown;
message_id?: unknown;
content_ref?: unknown;
diff_from?: unknown;
lineage?: unknown;
note?: unknown;
}
export interface RawArtifact {
schema?: unknown;
id?: unknown;
owner?: unknown;
conversation_id?: unknown;
project_id?: unknown;
type?: unknown;
language?: unknown;
title?: unknown;
current_version?: unknown;
versions?: unknown;
promotion?: unknown;
sensitivity?: unknown;
created_at?: unknown;
updated_at?: unknown;
}
export interface RawArtifactEnvelope {
schema?: unknown;
api_version?: unknown;
identity?: unknown;
binding?: unknown;
artifacts?: unknown;
attachments?: unknown;
authorizations?: unknown;
}
export interface ContentReference {
hash: string;
bytes: number;
mime: string;
}
export interface ArtifactVersion {
version: number;
createdAt: string;
createdBy: string;
contentRef: ContentReference;
diffFrom: number | null;
lineage: {artifactId: string; version: number} | null;
note: string;
}
export interface Artifact {
id: string;
owner: string;
projectId: string;
conversationId: string;
type: ArtifactType;
renderer: ArtifactRenderer;
language: string;
title: string;
currentVersion: number;
versions: ArtifactVersion[];
sensitivity: Sensitivity;
promotedVersion: number | null;
updatedAt: string;
}
export interface ArtifactAttachments {
artifactId: string;
sourceCount: number;
citationCount: number;
}
export interface ArtifactAuthorization {
artifactId: string;
download: AuthorizationStatus;
share: AuthorizationStatus;
}
export interface ArtifactWorkspacePage {
artifacts: Artifact[];
attachments: ArtifactAttachments[];
authorizations: ArtifactAuthorization[];
rejected: number;
}
export interface RawPreviewPayload {
schema?: unknown;
identity?: unknown;
binding?: unknown;
artifact_id?: unknown;
version?: unknown;
hash?: unknown;
mime?: unknown;
text?: unknown;
blob_url?: unknown;
}
export interface ArtifactPreview {
artifactId: string;
version: number;
renderer: ArtifactRenderer;
mime: string;
text: string | null;
blobUrl: string | null;
}
export interface ArtifactMutationIntent {
artifactId: string;
expectedVersion: number;
scope: ArtifactScope;
}
export interface PromoteIntent extends ArtifactMutationIntent {
targetProjectId: string;
version: number;
}
export interface DiffIntent extends ArtifactMutationIntent {
fromVersion: number;
toVersion: number;
}
export interface ArtifactEndpoint {
method: "GET" | "POST";
path: string;
responseSchema: string;
requiredBody?: readonly string[];
}
export interface ArtifactEndpointContract {
list: ArtifactEndpoint;
item(artifactId: string): ArtifactEndpoint;
preview(artifactId: string, version: number): ArtifactEndpoint;
diff(artifactId: string, fromVersion: number, toVersion: number): ArtifactEndpoint;
appendVersion(artifactId: string): ArtifactEndpoint;
continueEdit(artifactId: string): ArtifactEndpoint;
authorize(artifactId: string, action: "download" | "share"): ArtifactEndpoint;
download(artifactId: string, version: number): ArtifactEndpoint;
share(artifactId: string, version: number): ArtifactEndpoint;
promote(artifactId: string): ArtifactEndpoint;
sources(artifactId: string): ArtifactEndpoint;
}