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
99 lines
4.0 KiB
TypeScript
99 lines
4.0 KiB
TypeScript
/** Identity, scope, trigger and time guards for HUX-09. */
|
|
|
|
import type {
|
|
HuxSurface,
|
|
OnboardingScope,
|
|
RawIdentity,
|
|
ScopedIdentity,
|
|
SuggestionContext,
|
|
SuggestionTrigger,
|
|
} from "./types.ts";
|
|
|
|
const 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 CONTROL = /[\u0000-\u001f\u007f]/;
|
|
const SURFACES = new Set<HuxSurface>(["chat", "worker", "telegram", "voice", "api"]);
|
|
const CONTEXTS = new Set<SuggestionContext>([
|
|
"first_session", "empty_project", "after_artifact", "after_research",
|
|
"after_approval", "idle",
|
|
]);
|
|
|
|
export function exactKeys(
|
|
value: Record<string, unknown>,
|
|
required: readonly string[],
|
|
optional: readonly string[] = [],
|
|
): boolean {
|
|
const allowed = new Set([...required, ...optional]);
|
|
return required.every((key) => Object.prototype.hasOwnProperty.call(value, key)) &&
|
|
Object.keys(value).every((key) => allowed.has(key));
|
|
}
|
|
|
|
export function asRecord(value: unknown): Record<string, unknown> | null {
|
|
return value && typeof value === "object" && !Array.isArray(value) ?
|
|
value as Record<string, unknown> : null;
|
|
}
|
|
|
|
export function isId(value: unknown, prefix?: string): value is string {
|
|
return typeof value === "string" && 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 isSafeText(value: unknown, min: number, max: number): value is string {
|
|
return typeof value === "string" && value.length >= min && value.length <= max &&
|
|
!CONTROL.test(value);
|
|
}
|
|
|
|
export function normalizeIdentity(raw: unknown): ScopedIdentity | null {
|
|
const value = raw as RawIdentity | null;
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
if (!exactKeys(value as Record<string, unknown>, ["tenant_ref", "user_ref", "surface"]))
|
|
return null;
|
|
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 as HuxSurface))
|
|
return null;
|
|
return {tenantRef: value.tenant_ref, userRef: value.user_ref,
|
|
surface: value.surface as HuxSurface};
|
|
}
|
|
|
|
export function sameIdentity(left: ScopedIdentity, right: ScopedIdentity): boolean {
|
|
return left.tenantRef === right.tenantRef && left.userRef === right.userRef &&
|
|
left.surface === right.surface;
|
|
}
|
|
|
|
export function normalizeTrigger(raw: unknown): SuggestionTrigger | null {
|
|
const value = asRecord(raw);
|
|
if (!value || !exactKeys(value, ["surface", "context"]) ||
|
|
!SURFACES.has(value.surface as HuxSurface) ||
|
|
!CONTEXTS.has(value.context as SuggestionContext)) return null;
|
|
return {surface: value.surface as HuxSurface, context: value.context as SuggestionContext};
|
|
}
|
|
|
|
export function sameTrigger(left: SuggestionTrigger, right: SuggestionTrigger): boolean {
|
|
return left.surface === right.surface && left.context === right.context;
|
|
}
|
|
|
|
export function normalizeScope(raw: OnboardingScope, identity: ScopedIdentity): OnboardingScope | null {
|
|
const value = asRecord(raw);
|
|
if (!value || !exactKeys(value, ["sessionId", "conversationId", "trigger"]) ||
|
|
!isId(value.sessionId, "ses") || !isId(value.conversationId, "conv")) return null;
|
|
const trigger = normalizeTrigger(value.trigger);
|
|
if (!trigger || trigger.surface !== identity.surface) return null;
|
|
return {sessionId: value.sessionId, conversationId: value.conversationId, trigger};
|
|
}
|
|
|
|
export function scopedSuggestionPath(base: string, suggestionId: string): string {
|
|
if (!base.startsWith("/") || base.startsWith("//") || /[?#\\]/.test(base) ||
|
|
base.includes("..") || !isId(suggestionId, "sug")) {
|
|
throw new TypeError("Onboarding path is not safely scoped");
|
|
}
|
|
return `${base.replace(/\/$/, "")}/${encodeURIComponent(suggestionId)}/decisions`;
|
|
}
|