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

247 lines
10 KiB
TypeScript

/** Fail-closed suggestion.schema.json adapters for HUX-09. */
import {
asRecord,
exactKeys,
isId,
isSafeText,
isUtc,
normalizeIdentity,
normalizeTrigger,
sameIdentity,
sameTrigger,
} from "./security.ts";
import type {
OnboardingActionIntent,
OnboardingScope,
OnboardingSuggestion,
RawSuggestion,
RawSuggestionState,
ScopedIdentity,
SuggestionActionType,
SuggestionDecision,
SuggestionEvaluation,
SuggestionKind,
SuggestionState,
} from "./types.ts";
export const FOUNDATION_FLAG = "hux.foundation";
export const PROJECTS_FLAG = "hux.projects";
export const FRIENDLY_MODES_FLAG = "hux.friendly_modes";
export const ONBOARDING_FLAG = "hux.onboarding";
export const PRIVACY_FLAG = "hux.privacy";
export const REQUIRED_FLAGS = Object.freeze([
FOUNDATION_FLAG,
PROJECTS_FLAG,
FRIENDLY_MODES_FLAG,
ONBOARDING_FLAG,
]);
const KINDS = new Set<SuggestionKind>(["workflow", "project", "feature", "tip"]);
const ACTIONS = new Set<SuggestionActionType>([
"open_mode", "create_project", "open_memory", "open_artifacts",
"start_workflow", "none",
]);
const MODES = new Set(["fast", "thoughtful", "research", "create", "private"]);
const INTENTS = new Set(["schedule_weekly", "save_reusable"]);
const KIND_ACTIONS: Readonly<Record<SuggestionKind, ReadonlySet<SuggestionActionType>>> = {
workflow: new Set(["start_workflow"]),
project: new Set(["create_project"]),
feature: new Set(["open_mode", "open_memory", "open_artifacts"]),
tip: new Set(["none"]),
};
function finiteInteger(value: unknown, min: number, max: number): value is number {
return Number.isSafeInteger(value) && (value as number) >= min && (value as number) <= max;
}
function normalizePayload(
type: SuggestionActionType,
raw: unknown,
): Readonly<Record<string, string>> | null | undefined {
const value = raw === undefined ? {} : asRecord(raw);
if (!value) return null;
if (type === "start_workflow") {
return exactKeys(value, ["intent"]) && INTENTS.has(value.intent as string) ?
{intent: value.intent as string} : null;
}
if (type === "create_project") {
if (!exactKeys(value, [], ["prefill_title"])) return null;
return value.prefill_title === undefined ? {} :
isSafeText(value.prefill_title, 1, 120) ? {prefill_title: value.prefill_title} : null;
}
if (type === "open_mode") {
if (!exactKeys(value, [], ["mode"])) return null;
return value.mode === undefined ? {} : MODES.has(value.mode as string) ?
{mode: value.mode as string} : null;
}
if (type === "open_memory") {
return exactKeys(value, [], ["view"]) &&
(value.view === undefined || value.view === "suggestions") ?
(value.view ? {view: "suggestions"} : {}) : null;
}
if (type === "open_artifacts") {
return exactKeys(value, [], ["view"]) &&
(value.view === undefined || value.view === "current") ?
(value.view ? {view: "current"} : {}) : null;
}
return exactKeys(value, []) ? undefined : null;
}
export function onboardingEnabled(flags?: Iterable<string>): boolean {
if (!flags) return false;
const enabled = new Set(flags);
return REQUIRED_FLAGS.every((flag) => enabled.has(flag));
}
export function normalizeSuggestion(
raw: RawSuggestion,
expectedTrigger: OnboardingScope["trigger"],
): OnboardingSuggestion | null {
const value = asRecord(raw);
if (!value || !exactKeys(value, [
"schema", "id", "kind", "trigger", "title", "body", "priority", "suppression",
], ["action"]) || value.schema !== "hux.suggestion.v1" || !isId(value.id, "sug") ||
!KINDS.has(value.kind as SuggestionKind) || !isSafeText(value.title, 1, 80) ||
!isSafeText(value.body, 1, 280) || !finiteInteger(value.priority, 0, 100)) return null;
const trigger = normalizeTrigger(value.trigger);
const suppression = asRecord(value.suppression);
if (!trigger || !sameTrigger(trigger, expectedTrigger) || !suppression ||
!exactKeys(suppression, [
"dismissable", "max_shows", "cooldown_seconds", "never_again_supported",
]) || suppression.dismissable !== true || suppression.never_again_supported !== true ||
!finiteInteger(suppression.max_shows, 1, 5) ||
!finiteInteger(suppression.cooldown_seconds, 3600, Number.MAX_SAFE_INTEGER)) return null;
const action = value.action === undefined ? {type: "none"} : asRecord(value.action);
if (!action || !exactKeys(action, ["type"], ["payload"]) ||
!ACTIONS.has(action.type as SuggestionActionType)) return null;
const type = action.type as SuggestionActionType;
const payload = normalizePayload(type, action.payload);
const kind = value.kind as SuggestionKind;
if (payload === null || !KIND_ACTIONS[kind].has(type)) return null;
return {
schema: "hux.suggestion.v1", id: value.id as string, kind, trigger,
title: value.title as string, body: value.body as string,
action: {type, ...(payload === undefined ? {} : {payload})},
priority: value.priority as number,
suppression: {
dismissable: true, maxShows: suppression.max_shows as number,
cooldownSeconds: suppression.cooldown_seconds as number,
neverAgainSupported: true,
},
};
}
export function normalizeState(
raw: RawSuggestionState,
suggestion: OnboardingSuggestion,
owner: string,
): SuggestionState | null {
const value = asRecord(raw);
if (!value || !exactKeys(value, [
"schema", "owner", "suggestion_id", "shows", "last_shown_at", "never_again",
], ["dismissed_at", "acted_at"]) || value.schema !== "hux.suggestion_state.v1" ||
value.owner !== owner || value.suggestion_id !== suggestion.id ||
!finiteInteger(value.shows, 1, suggestion.suppression.maxShows) ||
!isUtc(value.last_shown_at) || value.never_again !== false ||
(value.dismissed_at !== undefined && !isUtc(value.dismissed_at)) ||
(value.acted_at !== undefined && !isUtc(value.acted_at))) return null;
return {
schema: "hux.suggestion_state.v1", owner, suggestionId: suggestion.id,
shows: value.shows as number, lastShownAt: value.last_shown_at as string,
...(value.dismissed_at ? {dismissedAt: value.dismissed_at as string} : {}),
...(value.acted_at ? {actedAt: value.acted_at as string} : {}),
neverAgain: false,
};
}
export function normalizeDecisionState(
raw: RawSuggestionState,
evaluation: SuggestionEvaluation,
decision: SuggestionDecision,
): SuggestionState | null {
const value = asRecord(raw);
const suggestion = evaluation.suggestion;
if (!value || !exactKeys(value, [
"schema", "owner", "suggestion_id", "shows", "last_shown_at", "never_again",
], ["dismissed_at", "acted_at"]) || value.schema !== "hux.suggestion_state.v1" ||
value.owner !== evaluation.identity.userRef || value.suggestion_id !== suggestion.id ||
value.shows !== evaluation.state.shows || !isUtc(value.last_shown_at) ||
value.last_shown_at !== evaluation.state.lastShownAt ||
typeof value.never_again !== "boolean") return null;
const dismissed = value.dismissed_at === undefined || isUtc(value.dismissed_at);
const acted = value.acted_at === undefined || isUtc(value.acted_at);
const outcomeMatches = decision === "acted" ?
Boolean(value.acted_at) && value.never_again === false :
Boolean(value.dismissed_at) &&
value.never_again === (decision === "never_again");
if (!dismissed || !acted || !outcomeMatches) return null;
return {
schema: "hux.suggestion_state.v1", owner: evaluation.identity.userRef,
suggestionId: suggestion.id, shows: value.shows as number,
lastShownAt: value.last_shown_at as string,
...(value.dismissed_at ? {dismissedAt: value.dismissed_at as string} : {}),
...(value.acted_at ? {actedAt: value.acted_at as string} : {}),
neverAgain: value.never_again,
};
}
export function normalizeEvaluation(
raw: unknown,
expectedIdentity: ScopedIdentity,
expectedScope: OnboardingScope,
privacyCapability: boolean,
): SuggestionEvaluation | null {
const value = asRecord(raw);
if (!value || !exactKeys(value, [
"schema", "api_version", "identity", "session_id", "conversation_id", "trigger",
"privacy", "claim_id", "issued_at", "suggestion", "state",
]) || value.schema !== "hux.suggestion_evaluation.v1" || value.api_version !== "hux.v1")
return null;
const identity = normalizeIdentity(value.identity);
const trigger = normalizeTrigger(value.trigger);
const privacy = asRecord(value.privacy);
if (!identity || !sameIdentity(identity, expectedIdentity) ||
value.session_id !== expectedScope.sessionId ||
value.conversation_id !== expectedScope.conversationId || !trigger ||
!sameTrigger(trigger, expectedScope.trigger) || !privacy ||
!exactKeys(privacy, ["schema", "no_store", "memory_suggestions_allowed"]) ||
privacy.schema !== "hux.onboarding_privacy.v1" ||
typeof privacy.no_store !== "boolean" ||
typeof privacy.memory_suggestions_allowed !== "boolean") return null;
if (privacy.no_store || value.suggestion === null || value.state === null ||
value.claim_id === null) return null;
if (!isId(value.claim_id, "clm") || !isUtc(value.issued_at)) return null;
const suggestion = normalizeSuggestion(value.suggestion as RawSuggestion, expectedScope.trigger);
if (!suggestion || (suggestion.action.type === "open_memory" &&
(!privacyCapability || privacy.memory_suggestions_allowed !== true))) return null;
const state = normalizeState(value.state as RawSuggestionState, suggestion, identity.userRef);
if (!state) return null;
return {
schema: "hux.suggestion_evaluation.v1", identity,
scope: expectedScope, claimId: value.claim_id, issuedAt: value.issued_at,
privacy: {
schema: "hux.onboarding_privacy.v1", noStore: false,
memorySuggestionsAllowed: privacy.memory_suggestions_allowed,
},
suggestion, state,
};
}
export function buildActionIntent(
evaluation: SuggestionEvaluation,
): OnboardingActionIntent | null {
const {action} = evaluation.suggestion;
const payload = action.payload || {};
if (action.type === "open_mode") return {type: action.type, ...(payload.mode ? {mode: payload.mode} : {})};
if (action.type === "create_project") return {type: action.type,
conversationId: evaluation.scope.conversationId,
...(payload.prefill_title ? {prefillTitle: payload.prefill_title} : {})};
if (action.type === "open_memory") return {type: action.type, view: "suggestions"};
if (action.type === "open_artifacts") return {type: action.type, view: "current"};
if (action.type === "start_workflow") return {type: action.type,
intent: payload.intent as "schedule_weekly" | "save_reusable"};
return null;
}