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

146 lines
5.5 KiB
TypeScript

/** Same-origin, session-bound transport for contextual HUX-09 suggestions. */
import {ACCEPT, API_VERSION, DECISION_SCHEMA, EVALUATION_SCHEMA} from "./contracts.ts";
import {
FOUNDATION_FLAG,
FRIENDLY_MODES_FLAG,
ONBOARDING_FLAG,
PRIVACY_FLAG,
PROJECTS_FLAG,
normalizeDecisionState,
normalizeEvaluation,
} from "./model.ts";
import {
asRecord,
exactKeys,
isId,
normalizeIdentity,
normalizeScope,
sameIdentity,
scopedSuggestionPath,
} from "./security.ts";
import type {
FetchResponse,
HuxFoundationClient,
OnboardingScope,
RawSuggestionState,
SuggestionDecision,
SuggestionEvaluation,
SuggestionState,
} from "./types.ts";
interface ClientOptions {
client: HuxFoundationClient;
fetcher?: (url: string, init: Record<string, unknown>) => Promise<FetchResponse>;
}
export class OnboardingContractError extends Error {
constructor(message: string) {
super(message);
this.name = "OnboardingContractError";
}
}
function hasCoreFlags(client: HuxFoundationClient): boolean {
return client.apiVersion === API_VERSION && [
FOUNDATION_FLAG, PROJECTS_FLAG, FRIENDLY_MODES_FLAG, ONBOARDING_FLAG,
].every((flag) => client.enabled(flag));
}
export function createOnboardingClient(options: ClientOptions) {
const client = options?.client;
const fetcher = options?.fetcher ||
(typeof fetch === "function" ? fetch.bind(globalThis) : null);
if (!client || !fetcher) throw new TypeError("Onboarding requires HUX and fetch clients");
const identity = normalizeIdentity({
tenant_ref: client.identity?.tenantRef,
user_ref: client.identity?.userRef,
surface: client.identity?.surface,
});
if (!identity) throw new TypeError("Onboarding requires a scoped HUX identity");
const seenClaims = new Set<string>();
const activeClaims = new Map<string, SuggestionEvaluation>();
async function request(path: string, body: Record<string, unknown>): Promise<unknown> {
if (!hasCoreFlags(client)) throw new OnboardingContractError("Onboarding is not enabled");
const response = await fetcher(client.endpoint(path), {
method: "POST", cache: "no-store", credentials: "same-origin",
headers: {Accept: ACCEPT, "Content-Type": "application/json"},
body: JSON.stringify(body),
});
if (response.status === 404 || response.status === 204) return null;
if (!response.ok) throw new OnboardingContractError(
response.status === 409 ? "This suggestion is no longer current" :
`Onboarding request failed (${response.status})`,
);
return response.json();
}
async function evaluate(scope: OnboardingScope): Promise<SuggestionEvaluation | null> {
if (!hasCoreFlags(client)) throw new OnboardingContractError("Onboarding is not enabled");
const safeScope = normalizeScope(scope, identity);
if (!safeScope) throw new TypeError("Onboarding trigger is outside this session");
const raw = await request("/onboarding/evaluate", {
schema: "hux.suggestion_evaluation_request.v1",
session_id: safeScope.sessionId,
conversation_id: safeScope.conversationId,
trigger: safeScope.trigger,
});
if (raw === null) return null;
const result = normalizeEvaluation(raw, identity, safeScope, client.enabled(PRIVACY_FLAG));
if (!result || seenClaims.has(result.claimId)) return null;
seenClaims.add(result.claimId);
activeClaims.set(result.claimId, result);
return result;
}
function normalizeDecision(
raw: unknown,
evaluation: SuggestionEvaluation,
decision: SuggestionDecision,
): SuggestionState | null {
const value = asRecord(raw);
if (!value || !exactKeys(value, [
"schema", "api_version", "identity", "session_id", "conversation_id",
"claim_id", "suggestion_id", "state",
]) || value.schema !== DECISION_SCHEMA || value.api_version !== API_VERSION ||
value.session_id !== evaluation.scope.sessionId ||
value.conversation_id !== evaluation.scope.conversationId ||
value.claim_id !== evaluation.claimId ||
value.suggestion_id !== evaluation.suggestion.id ||
!isId(value.claim_id, "clm") || !isId(value.suggestion_id, "sug")) return null;
const actualIdentity = normalizeIdentity(value.identity);
if (!actualIdentity || !sameIdentity(actualIdentity, identity)) return null;
return normalizeDecisionState(value.state as RawSuggestionState, evaluation, decision);
}
async function decide(
evaluation: SuggestionEvaluation,
decision: SuggestionDecision,
): Promise<SuggestionState> {
if (!["dismiss", "never_again", "acted"].includes(decision) ||
activeClaims.get(evaluation?.claimId) !== evaluation) {
throw new TypeError("Suggestion decision is not bound to this session");
}
const path = scopedSuggestionPath("/onboarding/suggestions", evaluation.suggestion.id);
const raw = await request(path, {
schema: "hux.suggestion_decision_request.v1",
session_id: evaluation.scope.sessionId,
conversation_id: evaluation.scope.conversationId,
claim_id: evaluation.claimId,
suggestion_id: evaluation.suggestion.id,
expected_shows: evaluation.state.shows,
decision,
});
const state = normalizeDecision(raw, evaluation, decision);
if (!state) throw new OnboardingContractError("Suggestion decision crossed its scope");
activeClaims.delete(evaluation.claimId);
return state;
}
return Object.freeze({
enabled: () => hasCoreFlags(client), evaluate, decide,
schemas: Object.freeze({evaluation: EVALUATION_SCHEMA, decision: DECISION_SCHEMA}),
});
}