jenkins b66c762f5d hermes(webui): add HUX card UI models with contract-locked suites
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
2026-08-24 04:12:03 -03:00

307 lines
8.5 KiB
TypeScript

/** Normalization, replay safety, and surface policy for HUX-01. */
import {
isOpaqueId,
isRfc3339Utc,
safeEvidenceLabel,
safeText,
} from "./security.ts";
import type {
ActivityItem,
ActivityPhase,
ActivityTone,
CancellationReceipt,
HuxSurface,
MergeResult,
RawActivityEvent,
RawCancellationReceipt,
SurfacePolicy,
} from "./types.ts";
export const ACTIVITY_TIMELINE_FLAG = "hux.activity_timeline";
export const FOUNDATION_FLAG = "hux.foundation";
const EVENT_KINDS = new Set([
"message.user",
"message.assistant",
"decision.route",
"decision.plan",
"tool.call",
"tool.result",
"approval.requested",
"approval.resolved",
"memory.proposed",
"memory.committed",
"memory.forgotten",
"artifact.created",
"artifact.version",
"artifact.promoted",
"citation.attached",
"mode.changed",
"run.started",
"run.cancelled",
"run.completed",
"run.failed",
"privacy.notice",
"suggestion.shown",
"suggestion.dismissed",
"release.transition",
"delegation.started",
"delegation.completed",
"delegation.failed",
"memory.suppressed",
"memory.retrieval_removed",
"budget.exhausted",
"side_effect.blocked",
"side_effect.released",
]);
const SURFACE_POLICIES: Readonly<Record<HuxSurface, SurfacePolicy>> = {
chat: {
maxItems: 80,
initialItems: 12,
evidenceLimit: 3,
cancellationEffectLimit: 4,
expandRoutineEvents: false,
},
worker: {
maxItems: 250,
initialItems: 40,
evidenceLimit: 8,
cancellationEffectLimit: 12,
expandRoutineEvents: true,
},
telegram: {
maxItems: 40,
initialItems: 8,
evidenceLimit: 2,
cancellationEffectLimit: 3,
expandRoutineEvents: false,
},
voice: {
maxItems: 24,
initialItems: 6,
evidenceLimit: 1,
cancellationEffectLimit: 2,
expandRoutineEvents: false,
},
api: {
maxItems: 250,
initialItems: 40,
evidenceLimit: 8,
cancellationEffectLimit: 12,
expandRoutineEvents: true,
},
};
export function surfacePolicy(surface: HuxSurface): SurfacePolicy {
return { ...SURFACE_POLICIES[surface] };
}
export function activityTimelineEnabled(flags?: Iterable<string>): boolean {
if (!flags) return false;
const enabled = new Set(flags);
return enabled.has(FOUNDATION_FLAG) && enabled.has(ACTIVITY_TIMELINE_FLAG);
}
function phaseFor(kind: string, hasParent: boolean): ActivityPhase {
if (kind === "message.user") return "intent";
if (
kind === "run.failed" ||
kind === "delegation.failed" ||
kind === "budget.exhausted"
) {
return "failure";
}
if (
kind === "run.completed" ||
kind === "run.cancelled" ||
kind === "message.assistant" ||
kind === "delegation.completed"
) {
return "completion";
}
if (kind === "delegation.started") return "delegation";
if (
kind.startsWith("decision.") ||
kind.startsWith("approval.") ||
kind === "mode.changed" ||
kind === "privacy.notice" ||
kind === "memory.suppressed" ||
kind === "memory.retrieval_removed" ||
kind === "side_effect.blocked"
)
return "decision";
if (
kind === "tool.result" ||
kind === "citation.attached" ||
kind === "artifact.version"
) {
return "evidence";
}
if (kind === "run.started" && hasParent) return "delegation";
return "action";
}
function toneFor(phase: ActivityPhase, kind: string): ActivityTone {
if (phase === "failure") return "warning";
if (phase === "completion") return "success";
if (kind === "approval.requested") return "warning";
if (phase === "action" || phase === "delegation") return "active";
return "neutral";
}
function hiddenSummary(kind: string): string {
if (kind === "tool.call") return "Hermes used a tool";
if (kind === "tool.result") return "Tool evidence was recorded";
if (kind === "run.failed")
return "The run failed; sensitive detail is hidden";
if (kind === "run.completed") return "The run completed";
return "Activity recorded; sensitive detail is hidden";
}
function normalizeEvent(
raw: RawActivityEvent,
conversationId: string,
policy: SurfacePolicy,
): ActivityItem | null {
if (raw.schema !== "hux.event.v1" || !isOpaqueId(raw.id, "evt")) return null;
if (!Number.isInteger(raw.seq) || (raw.seq as number) < 0) return null;
if (!isRfc3339Utc(raw.ts) || raw.conversation_id !== conversationId)
return null;
if (typeof raw.kind !== "string" || !EVENT_KINDS.has(raw.kind)) return null;
const redaction = raw.redaction as { level?: unknown } | null;
const fullyHidden =
raw.sensitivity === "restricted" || redaction?.level === "full";
const phase = phaseFor(raw.kind, typeof raw.parent_event_id === "string");
const references = Array.isArray(raw.evidence) ? raw.evidence : [];
const evidence = references
.map((item) => safeEvidenceLabel((item as { kind?: unknown } | null)?.kind))
.filter((item): item is NonNullable<typeof item> => item !== null)
.slice(0, policy.evidenceLimit);
return {
id: raw.id,
seq: raw.seq as number,
timestamp: raw.ts,
conversationId,
runId:
typeof raw.run_id === "string"
? safeText(raw.run_id, "", 120) || undefined
: undefined,
kind: raw.kind,
phase,
tone: toneFor(phase, raw.kind),
summary: fullyHidden
? hiddenSummary(raw.kind)
: safeText(raw.summary, hiddenSummary(raw.kind)),
evidence,
routine: phase === "action" || phase === "evidence",
};
}
/**
* Merge reconnect pages without duplicates. Existing sequence owners win;
* conflicting replay entries are rejected instead of rewriting visible history.
*/
export function mergeActivityEvents(
existing: readonly ActivityItem[],
incoming: readonly RawActivityEvent[],
conversationId: string,
surface: HuxSurface,
): MergeResult {
const policy = surfacePolicy(surface);
const byId = new Map(existing.map((item) => [item.id, item]));
const bySeq = new Map(existing.map((item) => [item.seq, item]));
let invalid = 0;
let replays = 0;
let conflicts = 0;
const normalized: ActivityItem[] = [];
for (const raw of incoming) {
const item = normalizeEvent(raw, conversationId, policy);
if (!item) {
invalid += 1;
continue;
}
normalized.push(item);
}
// A reconnect page is not required to preserve transport order. Sorting
// before conflict resolution makes an initial replay deterministic.
normalized.sort(
(left, right) => left.seq - right.seq || left.id.localeCompare(right.id),
);
for (const item of normalized) {
const priorId = byId.get(item.id);
if (priorId) {
if (priorId.seq === item.seq) replays += 1;
else conflicts += 1;
continue;
}
if (bySeq.has(item.seq)) {
conflicts += 1;
continue;
}
byId.set(item.id, item);
bySeq.set(item.seq, item);
}
const ordered = [...byId.values()].sort(
(left, right) => left.seq - right.seq || left.id.localeCompare(right.id),
);
const afterSeq = ordered.length ? ordered[ordered.length - 1].seq : -1;
const truncated = Math.max(0, ordered.length - policy.maxItems);
return {
items: truncated ? ordered.slice(-policy.maxItems) : ordered,
afterSeq,
invalid,
replays,
conflicts,
truncated,
};
}
export function normalizeCancellationReceipt(
raw: RawCancellationReceipt,
surface: HuxSurface,
): CancellationReceipt | null {
const outcomes = new Set([
"cancelled",
"already_complete",
"failed_to_cancel",
]);
if (raw.schema !== "hux.cancel_receipt.v1" || !isOpaqueId(raw.id, "rcpt"))
return null;
if (typeof raw.run_id !== "string" || !isRfc3339Utc(raw.requested_at))
return null;
if (typeof raw.outcome !== "string" || !outcomes.has(raw.outcome))
return null;
const policy = surfacePolicy(surface);
const effects = Array.isArray(raw.side_effects) ? raw.side_effects : [];
const sideEffects = effects
.slice(0, policy.cancellationEffectLimit)
.map((value) => {
const effect = value as {
description?: unknown;
reverted?: unknown;
} | null;
return {
description: safeText(
effect?.description,
"A side effect was recorded",
280,
),
reverted: effect?.reverted === true,
};
});
return {
id: raw.id,
runId: safeText(raw.run_id, "unknown run", 120),
requestedAt: raw.requested_at,
acknowledgedAt: isRfc3339Utc(raw.acknowledged_at)
? raw.acknowledged_at
: undefined,
completedAt: isRfc3339Utc(raw.completed_at) ? raw.completed_at : undefined,
outcome: raw.outcome as CancellationReceipt["outcome"],
sideEffects,
omittedEffects: Math.max(0, effects.length - sideEffects.length),
};
}