/* node:coverage disable */ (function (root, factory) { 'use strict'; const contract = typeof module === 'object' && module.exports ? require('./wave_a_contract.js') : root.HermesHuxWaveAContract; const shellApi = typeof module === 'object' && module.exports ? require('../shell.js') : root.HermesHuxShell; const api = factory(contract, shellApi); if (typeof module === 'object' && module.exports) module.exports = api; else root.HermesHuxAutonomyPrivacy = api; }(typeof globalThis === 'object' ? globalThis : this, function (contract, shellApi) { /* node:coverage enable */ 'use strict'; const ACCEPT = 'application/vnd.hermes.hux+json; version=1'; const ID = /^[a-z]{2,6}_[A-Za-z0-9._-]{4,80}$/; const SLOT = /^slot-[0-9]{1,3}$/; 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 AUTONOMY = new Set(['ask_first', 'safe', 'autonomous']); const DECISIONS = new Set(['allow', 'ask', 'deny']); const CHOICES = new Set(['once', 'session', 'always', 'deny']); const OUTCOMES = new Set(['cancelled', 'already_complete', 'failed_to_cancel']); const TOPICS = new Set(['health', 'finance', 'legal', 'relationships', 'credentials', 'minors', 'location', 'biometric']); const NOTICE_CONTROLS = new Set(['forget_this_conversation', 'switch_to_private', 'disable_memory_here', 'dismiss']); const CAPABILITIES = new Set(['read_files', 'write_files', 'shell', 'network', 'web_search', 'send_message', 'memory_write', 'artifact_write', 'spend_tokens', 'delegate', 'deploy', 'external_side_effect']); const BUDGETS = ['tokens_per_run', 'tool_calls_per_run', 'wall_clock_seconds', 'delegations_per_run', 'spend_units', 'subagents_per_run']; const GENERIC_NOTICE = 'Sensitive-topic protections are active. Details stay scoped to this conversation and are not remembered without approval.'; const UNVERIFIED_STOP = 'Model response stopped; tool/side-effect cancellation unverified.'; function record(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : null; } function exact(value, required, optional) { const allowed = new Set(required.concat(optional || [])); return required.every((key) => Object.prototype.hasOwnProperty.call(value, key)) && Object.keys(value).every((key) => allowed.has(key)); } function id(value, prefix) { return typeof value === 'string' && ID.test(value) && (!prefix || value.startsWith(`${prefix}_`)); } function timestamp(value) { return typeof value === 'string' && UTC.test(value) && !Number.isNaN(Date.parse(value)); } function text(value, max) { return typeof value === 'string' && value.length > 0 && value.length <= max && !CONTROL.test(value); } function integer(value, min, max) { return Number.isSafeInteger(value) && value >= min && value <= max; } function sameIdentity(owner, identity) { return owner === identity.userRef; } function safeRun(value) { return id(value, 'run'); } function normalizeBudgets(raw) { const value = record(raw); if (!value || !exact(value, [], BUDGETS.concat(['scope']))) return null; const result = {}; for (const key of BUDGETS) { if (value[key] !== undefined && !integer(value[key], 0, Number.MAX_SAFE_INTEGER)) return null; if (value[key] !== undefined) result[key] = value[key]; } if (value.scope !== undefined) { const scope = record(value.scope); if (!scope || !exact(scope, [], ['conversations', 'paths'])) return null; const conversations = scope.conversations || []; const paths = scope.paths || []; if (!Array.isArray(conversations) || conversations.length > 64 || conversations.some((item) => !id(item, 'conv')) || !Array.isArray(paths) || paths.length > 64 || paths.some((item) => !text(item, 300))) return null; result.scope = {conversations: conversations.slice(), paths: paths.slice()}; } return result; } function normalizePolicy(raw, identity, conversationId, etag) { const value = record(raw); if (!value || !exact(value, ['schema', 'id', 'owner', 'scope', 'autonomy', 'grants', 'budgets', 'provenance', 'updated_at', 'revision']) || value.schema !== 'hux.policy.v1' || !id(value.id, 'pol') || !sameIdentity(value.owner, identity) || !AUTONOMY.has(value.autonomy) || !timestamp(value.updated_at) || !integer(value.revision, 1, Number.MAX_SAFE_INTEGER) || String(etag || '').replaceAll('"', '') !== String(value.revision)) return null; const scope = record(value.scope); if (!scope || !exact(scope, ['level'], ['scope_id']) || !['global', 'project', 'conversation'].includes(scope.level) || (scope.level === 'global' ? scope.scope_id !== undefined : !id(scope.scope_id, scope.level === 'project' ? 'prj' : 'conv')) || (scope.level === 'global' ? value.id !== 'pol_global' : !value.id.startsWith(`pol_${scope.level}.`)) || (scope.level === 'conversation' && scope.scope_id !== conversationId) || !record(value.provenance) || !Array.isArray(value.grants) || value.grants.length > 64) return null; const grants = []; for (const rawGrant of value.grants) { const grant = record(rawGrant); if (!grant || !exact(grant, ['capability', 'decision'], ['expires_at', 'granted_by']) || !CAPABILITIES.has(grant.capability) || !DECISIONS.has(grant.decision) || (grant.expires_at !== undefined && !timestamp(grant.expires_at))) return null; grants.push({capability: grant.capability, decision: grant.decision, ...(grant.expires_at ? {expires_at: grant.expires_at} : {})}); } const budgets = normalizeBudgets(value.budgets); return budgets ? {id: value.id, owner: value.owner, effectiveScope: {...scope}, scope: {level: 'conversation', scope_id: conversationId}, autonomy: value.autonomy, grants, budgets, revision: value.revision, ifMatch: scope.level === 'conversation' ? value.revision : 0, updatedAt: value.updated_at} : null; } function normalizeApproval(raw, conversationId, statuses, expectedChoice, identity) { const value = record(raw); if (!value || !exact(value, ['schema', 'id', 'run_id', 'conversation_id', 'capability', 'request', 'status', 'requested_at', 'expires_at'], ['decision', 'idempotency_key']) || value.schema !== 'hux.approval.v1' || !id(value.id, 'apr') || !safeRun(value.run_id) || value.conversation_id !== conversationId || !CAPABILITIES.has(value.capability) || !statuses.has(value.status) || !timestamp(value.requested_at) || !timestamp(value.expires_at)) return null; const request = record(value.request); if (!request || !exact(request, ['summary', 'risk', 'external'], ['detail', 'evidence']) || !text(request.summary, 280) || !['low', 'medium', 'high'].includes(request.risk) || typeof request.external !== 'boolean') return null; if (expectedChoice !== undefined) { const decision = record(value.decision); const actor = decision && record(decision.by); if (!decision || !exact(decision, ['choice', 'by', 'at']) || decision.choice !== expectedChoice || !timestamp(decision.at) || !identity || !actor || !exact(actor, ['type', 'id'], ['display']) || actor.type !== 'user' || actor.id !== identity.userRef) return null; } else if (value.decision !== undefined) return null; return {id: value.id, runId: value.run_id, capability: value.capability, summary: request.summary, risk: request.risk, external: request.external, status: value.status, requestedAt: value.requested_at, expiresAt: value.expires_at}; } function normalizeApprovalPage(raw, conversationId) { const value = record(raw); if (!value || !exact(value, ['items', 'next']) || !Array.isArray(value.items) || value.items.length > 200 || value.next !== null) return null; const items = value.items.map((item) => normalizeApproval(item, conversationId, new Set(['pending']))); return items.some((item) => item === null) || new Set(items.map((item) => item.id)).size !== items.length ? null : items; } function normalizeReceipt(raw, runId, conversationId, identity) { const value = record(raw); const actor = value && record(value.requested_by); const human = identity && ['chat', 'telegram', 'voice'].includes(identity.surface); if (!value || !exact(value, ['schema', 'id', 'run_id', 'requested_by', 'requested_at', 'outcome', 'side_effects'], ['acknowledged_at', 'completed_at', 'conversation_id']) || value.schema !== 'hux.cancel_receipt.v1' || !id(value.id, 'rcpt') || value.run_id !== runId || !actor || !exact(actor, ['type', 'id'], ['display']) || !identity || (human ? actor.type !== 'user' || actor.id !== identity.userRef : actor.type !== 'system' || actor.id !== identity.surface) || !timestamp(value.requested_at) || !OUTCOMES.has(value.outcome) || (value.conversation_id !== undefined && value.conversation_id !== conversationId) || !Array.isArray(value.side_effects) || value.side_effects.length > 64) return null; const effects = value.side_effects.map((rawEffect) => { const effect = record(rawEffect); return effect && exact(effect, ['description', 'reverted'], ['evidence']) && text(effect.description, 280) && typeof effect.reverted === 'boolean' ? {description: effect.description, reverted: effect.reverted} : null; }); return effects.some((item) => item === null) ? null : {id: value.id, runId, outcome: value.outcome, sideEffects: effects}; } function normalizePrivacyPolicy(raw, auditStale) { const value = record(raw); if (!value || !exact(value, ['schema', 'version', 'topics', 'topic_scoping', 'retention_audit'], ['audit_stale']) || value.schema !== 'hux.privacy_policy.v1' || !integer(value.version, 1, Number.MAX_SAFE_INTEGER) || !Array.isArray(value.topics) || value.topics.length !== TOPICS.size) return null; const names = new Set(); let shortest = 366; let longest = 0; for (const rawTopic of value.topics) { const topic = record(rawTopic); if (!topic || !exact(topic, ['topic', 'sensitivity', 'memory_write', 'decay_days', 'notice']) || !TOPICS.has(topic.topic) || names.has(topic.topic) || !['sensitive', 'restricted'].includes(topic.sensitivity) || !['ask', 'deny'].includes(topic.memory_write) || !integer(topic.decay_days, 1, 365) || !text(topic.notice, 280)) return null; names.add(topic.topic); shortest = Math.min(shortest, topic.decay_days); longest = Math.max(longest, topic.decay_days); } const scoping = record(value.topic_scoping); const audit = record(value.retention_audit); if (!scoping || !exact(scoping, ['scope_to_conversation', 'cross_surface_sharing']) || scoping.scope_to_conversation !== true || !['never', 'same_owner_only'].includes(scoping.cross_surface_sharing) || !audit || !exact(audit, ['interval_days', 'actions']) || !integer(audit.interval_days, 1, 30) || !Array.isArray(audit.actions) || audit.actions.length < 1) return null; return {version: value.version, protectedCount: names.size, shortestDecay: shortest, longestDecay: longest, crossSurfaceSharing: scoping.cross_surface_sharing, auditIntervalDays: audit.interval_days, auditStale: auditStale === 'true'}; } function normalizeAuditPage(raw) { const value = record(raw); if (!value || !exact(value, ['items', 'next']) || !Array.isArray(value.items) || value.items.length > 200) return null; const rows = value.items.map((rawItem) => { const item = record(rawItem); if (!item || !exact(item, ['schema', 'id', 'ran_at', 'policy_version', 'results']) || item.schema !== 'hux.retention_audit.v1' || !id(item.id, 'aud') || !timestamp(item.ran_at) || !integer(item.policy_version, 1, Number.MAX_SAFE_INTEGER) || !Array.isArray(item.results)) return null; let actions = 0; let affected = 0; for (const rawResult of item.results) { const result = record(rawResult); if (!result || !exact(result, ['action', 'count']) || !['expire_memory', 'decay_topic_context', 'purge_forgotten_content', 'report'].includes(result.action) || !integer(result.count, 0, Number.MAX_SAFE_INTEGER)) return null; actions += 1; affected += result.action === 'report' ? 0 : result.count; } return {id: item.id, ranAt: item.ran_at, policyVersion: item.policy_version, actions, affected}; }); return rows.some((item) => item === null) ? null : rows; } function normalizeNotice(raw, conversationId) { const value = record(raw); if (!value || !exact(value, ['schema', 'topic', 'conversation_id', 'text', 'controls', 'shown_at']) || value.schema !== 'hux.privacy_notice.v1' || !TOPICS.has(value.topic) || value.conversation_id !== conversationId || !text(value.text, 280) || !timestamp(value.shown_at) || !Array.isArray(value.controls) || value.controls.length < 1 || value.controls.some((item) => !NOTICE_CONTROLS.has(item))) return null; return {text: GENERIC_NOTICE, controls: [...new Set(value.controls)], shownAt: value.shown_at}; } function normalizeForget(raw, conversationId) { const value = record(raw); return value && exact(value, ['conversation_id', 'forgotten', 'memory_forgotten', 'events_redacted']) && value.conversation_id === conversationId && value.forgotten === true && integer(value.memory_forgotten, 0, Number.MAX_SAFE_INTEGER) && integer(value.events_redacted, 0, Number.MAX_SAFE_INTEGER) ? {forgotten: true, memoryForgotten: value.memory_forgotten, eventsRedacted: value.events_redacted} : null; } function el(doc, tag, attrs, content) { const node = doc.createElement(tag); Object.entries(attrs || {}).forEach(([key, value]) => node.setAttribute(key, value)); if (content !== undefined) node.textContent = String(content); return node; } function status(doc, message, role) { return el(doc, 'p', {class: 'hux-runtime-status', role: role || 'status', 'aria-live': 'polite'}, message); } function safeError(doc) { return status(doc, 'This workspace control is temporarily unavailable.', 'alert'); } function responseHeader(response, name) { return response.headers && typeof response.headers.get === 'function' ? response.headers.get(name) : ''; } function createRuntime(options) { const settings = options || {}; const doc = settings.document || (typeof document === 'object' ? document : null); if (!doc || !id(settings.conversationId, 'conv')) throw new TypeError('Runtime needs a document and conversation id'); if (settings.runId !== undefined && !safeRun(settings.runId)) throw new TypeError('Runtime run id is invalid'); const fetcher = settings.fetcher || (typeof fetch === 'function' ? fetch.bind(globalThis) : null); if (!fetcher) throw new TypeError('Runtime needs fetch'); let client = null; let autonomyRoot = null; let privacyRoot = null; let policy = null; let approvals = []; let receipt = null; let privacyPolicy = null; let audits = []; let activeTopic = null; let privacyNotice = null; let noticeShown = false; let noticePending = false; let privacyMessage = ''; let stopMessage = ''; let inlineDock = null; let pollTimer = null; const issuedApprovals = new Set(); const pollMs = Number.isSafeInteger(settings.approvalPollMs) && settings.approvalPollMs >= 1 ? settings.approvalPollMs : 4000; function timers() { return settings.timers || globalThis; } function assertClient(candidate, flag) { if (!candidate || candidate.apiVersion !== 'hux.v1' || !candidate.identity || !SLOT.test(candidate.identity.tenantSlot || '') || !USER.test(candidate.identity.userRef || '') || !['chat', 'worker', 'telegram', 'voice', 'api'].includes(candidate.identity.surface) || !['router', 'relay', 'worker'].includes(candidate.identity.trust) || !candidate.enabled('hux.foundation') || !candidate.enabled(flag)) throw new Error('HUX capability unavailable'); return candidate; } async function request(path, init) { const customHeaders = (init && init.headers) || {}; const response = await fetcher(client.endpoint(path), {...(init || {}), cache: 'no-store', credentials: 'same-origin', headers: {Accept: ACCEPT, ...((init && init.body) ? {'Content-Type': 'application/json'} : {}), ...customHeaders}}); if (!response.ok) throw new Error(`HUX request failed (${response.status})`); return response; } function key(action, target) { return `hux:${action}:${target}`.slice(0, 120); } async function loadAutonomy() { const scope = encodeURIComponent(settings.conversationId); const [policyResponse, approvalResponse] = await Promise.all([ request(`/policy?scope=conversation&scope_id=${scope}`, {method: 'GET'}), request('/approvals?status=pending', {method: 'GET'}), ]); policy = normalizePolicy(await policyResponse.json(), client.identity, settings.conversationId, responseHeader(policyResponse, 'ETag')); approvals = normalizeApprovalPage(await approvalResponse.json(), settings.conversationId); if (!policy || !approvals) throw new Error('Invalid autonomy response'); approvals.forEach((item) => issuedApprovals.add(item.id)); renderAutonomy(); } async function savePolicy(level) { if (!policy || !AUTONOMY.has(level)) throw new TypeError('Policy choice is invalid'); const body = {scope: policy.scope, autonomy: level, grants: policy.grants, budgets: policy.budgets}; const response = await request('/policy', {method: 'PUT', headers: {'If-Match': String(policy.ifMatch)}, body: JSON.stringify(body)}); const updated = normalizePolicy(await response.json(), client.identity, settings.conversationId, responseHeader(response, 'ETag')); if (!updated || updated.effectiveScope.level !== 'conversation' || updated.revision <= policy.ifMatch) throw new Error('Policy revision did not advance'); policy = updated; renderAutonomy(); return policy; } async function decideApproval(approvalId, choice) { if (!issuedApprovals.has(approvalId) || !CHOICES.has(choice)) throw new TypeError('Approval decision was not presented here'); const response = await request(`/approvals/${encodeURIComponent(approvalId)}`, {method: 'POST', headers: {'Idempotency-Key': key(`approval-${choice}`, approvalId)}, body: JSON.stringify({choice})}); const resolved = normalizeApproval(await response.json(), settings.conversationId, new Set(['approved', 'denied']), choice, client.identity); if (!resolved || resolved.id !== approvalId) throw new Error('Approval result is invalid'); issuedApprovals.delete(approvalId); approvals = approvals.filter((item) => item.id !== approvalId); renderAutonomy(); return resolved; } async function refreshApprovals() { if (!client) return null; try { const response = await request('/approvals?status=pending', {method: 'GET'}); const page = normalizeApprovalPage(await response.json(), settings.conversationId); if (!page) return null; approvals = page; page.forEach((item) => issuedApprovals.add(item.id)); renderAutonomy(); return page; } catch (_) { return null; } } function startApprovalPolling() { if (pollTimer !== null) return; pollTimer = timers().setInterval(() => { if (typeof settings.canStopModelResponse === 'function' && !settings.canStopModelResponse()) return; void refreshApprovals(); }, pollMs); if (pollTimer && typeof pollTimer.unref === 'function') pollTimer.unref(); } function stopApprovalPolling() { if (pollTimer === null) return; timers().clearInterval(pollTimer); pollTimer = null; } function mountInlineDock() { const host = settings.inlineHost || doc.body || null; if (inlineDock || !host) return; inlineDock = el(doc, 'section', {class: 'hux-runtime-inline-dock', role: 'region', 'aria-label': 'Approvals needed', 'aria-live': 'polite'}); inlineDock.hidden = true; host.appendChild(inlineDock); } function approvalCard(item, choices, extraClass) { const card = el(doc, 'article', {class: `hux-runtime-approval${extraClass} risk-${item.risk}`}); card.appendChild(el(doc, 'strong', {}, item.summary)); card.appendChild(el(doc, 'p', {}, `${item.capability.replaceAll('_', ' ')} · ${item.risk} risk${item.external ? ' · external' : ''}`)); const actions = el(doc, 'div', {class: 'hux-runtime-actions'}); choices.forEach(([choice, label]) => { const button = el(doc, 'button', {type: 'button'}, label); button.addEventListener('click', () => { void decideApproval(item.id, choice).catch(() => card.appendChild(safeError(doc))); }); actions.appendChild(button); }); card.appendChild(actions); return card; } function renderInline() { if (!inlineDock) return; inlineDock.replaceChildren(); const pending = (approvals || []).slice().sort((a, b) => b.requestedAt.localeCompare(a.requestedAt)); inlineDock.hidden = pending.length === 0; if (!pending.length) return; inlineDock.appendChild(el(doc, 'h2', {class: 'hux-runtime-inline-title'}, 'Approval needed')); pending.slice(0, 3).forEach((item) => inlineDock.appendChild(approvalCard(item, [['once', 'Allow once'], ['always', 'Always allow'], ['deny', 'Deny']], ' hux-runtime-inline-card'))); if (pending.length > 3) inlineDock.appendChild(status(doc, `${pending.length - 3} more waiting in the workspace drawer.`)); } async function stopRun() { if (typeof settings.stopModelResponse !== 'function') throw new TypeError('No owned model response is bound to this view'); const model = await settings.stopModelResponse(); if (!record(model) || model.accepted !== true) { stopMessage = 'Model response stop could not be verified; tool/side-effect cancellation unverified.'; renderAutonomy(); return {modelStopped: false, verified: false}; } stopMessage = UNVERIFIED_STOP; receipt = null; renderAutonomy(); if (!settings.runId) return {modelStopped: true, verified: false}; try { const body = {conversation_id: settings.conversationId, process_registry_empty: false}; const response = await request(`/runs/${encodeURIComponent(settings.runId)}/stop`, {method: 'POST', headers: {'Idempotency-Key': key('stop', settings.runId)}, body: JSON.stringify(body)}); receipt = normalizeReceipt(await response.json(), settings.runId, settings.conversationId, client.identity); const verified = Boolean(receipt && receipt.outcome === 'cancelled' && receipt.sideEffects.every((effect) => effect.reverted)); stopMessage = verified ? 'Model response and recorded side effects stopped.' : UNVERIFIED_STOP; renderAutonomy(); return {modelStopped: true, verified, receipt}; } catch (_) { stopMessage = UNVERIFIED_STOP; renderAutonomy(); return {modelStopped: true, verified: false}; } } function renderAutonomy() { renderInline(); if (!autonomyRoot) return; autonomyRoot.replaceChildren(); if (!policy || !approvals) { autonomyRoot.appendChild(status(doc, 'Loading autonomy controls…')); return; } const title = el(doc, 'h2', {class: 'hux-runtime-title'}, 'Autonomy and approvals'); autonomyRoot.appendChild(title); const picker = el(doc, 'label', {class: 'hux-runtime-field'}, 'Autonomy level'); const select = el(doc, 'select', {'aria-label': 'Autonomy level'}); [['ask_first', 'Ask first'], ['safe', 'Safe actions'], ['autonomous', 'Autonomous']].forEach(([value, label]) => { const option = el(doc, 'option', {value}, label); option.selected = value === policy.autonomy; select.appendChild(option); }); picker.appendChild(select); const save = el(doc, 'button', {type: 'button'}, 'Save autonomy'); save.addEventListener('click', () => { void savePolicy(select.value).catch(() => autonomyRoot.appendChild(safeError(doc))); }); picker.appendChild(save); autonomyRoot.appendChild(picker); const queue = el(doc, 'section', {'aria-labelledby': 'hux-runtime-approvals'}); queue.appendChild(el(doc, 'h3', {id: 'hux-runtime-approvals'}, `Pending approvals (${approvals.length})`)); approvals.forEach((item) => queue.appendChild(approvalCard(item, [['once', 'Allow once'], ['session', 'Allow this session'], ['always', 'Always allow'], ['deny', 'Deny']], ''))); autonomyRoot.appendChild(queue); const canStop = typeof settings.canStopModelResponse === 'function' && settings.canStopModelResponse(); if (canStop) { const stop = el(doc, 'button', {type: 'button', class: 'hux-runtime-stop'}, 'Stop model response'); stop.addEventListener('click', () => { void stopRun(); }); autonomyRoot.appendChild(stop); } if (stopMessage) autonomyRoot.appendChild(status(doc, stopMessage, stopMessage === 'Model response and recorded side effects stopped.' ? 'status' : 'alert')); if (receipt) { const box = el(doc, 'section', {class: 'hux-runtime-receipt', role: 'status', 'aria-live': 'polite'}); box.appendChild(el(doc, 'h3', {}, 'Stop receipt')); box.appendChild(el(doc, 'p', {}, `Outcome: ${receipt.outcome.replaceAll('_', ' ')}`)); const list = el(doc, 'ul'); receipt.sideEffects.forEach((effect) => list.appendChild(el(doc, 'li', {}, `${effect.reverted ? 'Reverted' : 'Not reverted'}: ${effect.description}`))); box.appendChild(list); autonomyRoot.appendChild(box); } } async function loadPrivacy() { const [policyResponse, auditResponse] = await Promise.all([ request('/privacy/policy', {method: 'GET'}), request('/privacy/audit', {method: 'GET'}), ]); privacyPolicy = normalizePrivacyPolicy(await policyResponse.json(), responseHeader(policyResponse, 'HUX-Audit-Stale')); audits = normalizeAuditPage(await auditResponse.json()); if (!privacyPolicy || !audits) throw new Error('Invalid privacy response'); renderPrivacy(); } async function showPrivacyNotice(topic) { if (noticeShown || noticePending || !TOPICS.has(topic) || !client) return null; noticePending = true; activeTopic = topic; try { const response = await request('/privacy/notices', {method: 'POST', headers: {'Idempotency-Key': key('privacy-notice', settings.conversationId)}, body: JSON.stringify({topic, conversation_id: settings.conversationId})}); privacyNotice = normalizeNotice(await response.json(), settings.conversationId); if (!privacyNotice) throw new Error('Invalid privacy notice'); noticeShown = true; renderPrivacy(); return privacyNotice; } catch (error) { activeTopic = null; privacyNotice = null; renderPrivacy(); throw error; } finally { noticePending = false; } } async function privacyChoice(choice) { if (!privacyNotice || !privacyNotice.controls.includes(choice)) throw new TypeError('Privacy control was not presented here'); if (choice === 'dismiss') { activeTopic = null; privacyNotice = null; privacyMessage = 'Privacy notice dismissed.'; renderPrivacy(); return null; } if (choice === 'forget_this_conversation') { const response = await request(`/conversations/${encodeURIComponent(settings.conversationId)}/forget`, {method: 'POST', headers: {'Idempotency-Key': key('forget', settings.conversationId)}, body: JSON.stringify({})}); const result = normalizeForget(await response.json(), settings.conversationId); if (!result) throw new Error('Forget result is invalid'); privacyMessage = `Conversation forgotten; ${result.memoryForgotten} memory entries removed.`; } else { const response = await request('/privacy/notices', {method: 'POST', headers: {'Idempotency-Key': key(`privacy-${choice}`, settings.conversationId)}, body: JSON.stringify({topic: activeTopic, conversation_id: settings.conversationId, chosen: choice})}); if (!normalizeNotice(await response.json(), settings.conversationId)) throw new Error('Privacy choice is invalid'); if (choice === 'switch_to_private' && typeof settings.onPrivateMode === 'function') await settings.onPrivateMode(); privacyMessage = choice === 'disable_memory_here' ? 'Memory is disabled for this conversation.' : 'Private mode was requested.'; } activeTopic = null; privacyNotice = null; renderPrivacy(); return true; } function renderPrivacy() { if (!privacyRoot) return; privacyRoot.replaceChildren(); if (!privacyPolicy || !audits) { privacyRoot.appendChild(status(doc, 'Loading privacy controls…')); return; } privacyRoot.appendChild(el(doc, 'h2', {class: 'hux-runtime-title'}, 'Privacy and retention')); const summary = el(doc, 'dl', {class: 'hux-runtime-summary', 'aria-label': 'Privacy policy summary'}); [['Sensitive categories', `${privacyPolicy.protectedCount} protected`], ['Context minimization', `${privacyPolicy.shortestDecay}–${privacyPolicy.longestDecay} days`], ['Cross-surface sharing', privacyPolicy.crossSurfaceSharing === 'never' ? 'Never' : 'Same owner only'], ['Retention audit', privacyPolicy.auditStale ? 'Overdue' : `Every ${privacyPolicy.auditIntervalDays} days`]].forEach(([term, value]) => { const row = el(doc, 'div'); row.appendChild(el(doc, 'dt', {}, term)); row.appendChild(el(doc, 'dd', {}, value)); summary.appendChild(row); }); privacyRoot.appendChild(summary); privacyRoot.appendChild(el(doc, 'p', {class: 'hux-runtime-minimize'}, 'When the topic changes, prior sensitive details are not repeated.')); if (privacyNotice) { const notice = el(doc, 'aside', {class: 'hux-runtime-notice', 'aria-labelledby': 'hux-runtime-privacy-notice'}); notice.appendChild(el(doc, 'h3', {id: 'hux-runtime-privacy-notice'}, 'Sensitive-topic protection')); notice.appendChild(el(doc, 'p', {}, privacyNotice.text)); const actions = el(doc, 'div', {class: 'hux-runtime-actions'}); privacyNotice.controls.forEach((choice) => { const button = el(doc, 'button', {type: 'button'}, choice.replaceAll('_', ' ')); button.addEventListener('click', () => { void privacyChoice(choice).catch(() => notice.appendChild(safeError(doc))); }); actions.appendChild(button); }); notice.appendChild(actions); privacyRoot.appendChild(notice); } const latest = audits[0]; privacyRoot.appendChild(status(doc, latest ? `Last retention audit: ${latest.ranAt}; ${latest.affected} records affected.` : 'No retention audit has been recorded.')); if (privacyMessage) privacyRoot.appendChild(status(doc, privacyMessage)); } const autonomyExtension = {id: 'autonomy-controls', flag: 'hux.autonomy', label: 'Autonomy', order: 30, render(context) { client = assertClient(context.client, 'hux.autonomy'); autonomyRoot = context.container; mountInlineDock(); renderAutonomy(); const target = autonomyRoot; void loadAutonomy().catch(() => { if (autonomyRoot === target) target.replaceChildren(safeError(doc)); }); startApprovalPolling(); }}; const privacyExtension = {id: 'privacy-controls', flag: 'hux.privacy', label: 'Privacy', order: 40, render(context) { client = assertClient(context.client, 'hux.privacy'); privacyRoot = context.container; renderPrivacy(); const target = privacyRoot; void loadPrivacy().catch(() => { if (privacyRoot === target) target.replaceChildren(safeError(doc)); }); }}; function register(shell) { if (!shell || typeof shell.register !== 'function') throw new TypeError('HUX shell is required'); const removeAutonomy = shell.register(autonomyExtension); const removePrivacy = shell.register(privacyExtension); return () => { removePrivacy(); removeAutonomy(); }; } function destroy() { stopApprovalPolling(); if (inlineDock && inlineDock.parentNode) inlineDock.parentNode.removeChild(inlineDock); inlineDock = null; client = null; autonomyRoot = null; privacyRoot = null; policy = null; approvals = []; receipt = null; privacyPolicy = null; audits = []; activeTopic = null; privacyNotice = null; stopMessage = ''; issuedApprovals.clear(); } return Object.freeze({extensions: Object.freeze([autonomyExtension, privacyExtension]), register, destroy, showPrivacyNotice, clearPrivacyNotice() { activeTopic = null; privacyNotice = null; if (privacyRoot) renderPrivacy(); }, savePolicy, decideApproval, refreshApprovals, stopRun, privacyChoice}); } function createAutonomyPrivacyRuntime(options) { const settings = options || {}; const doc = settings.document || (typeof document === 'object' ? document : null); if (!doc) throw new TypeError('Autonomy/privacy runtime requires a document'); const client = contract.createCanonicalClient(settings); const shell = shellApi.createShell({client, document: doc, instanceId: settings.instanceId || 'governance'}); const runtime = createRuntime(settings); const removers = runtime.extensions.map((extension) => shell.register(extension)); let mounted = false; let destroyed = false; return Object.freeze({client, async mount(target) { if (destroyed) throw new Error('Autonomy/privacy runtime was destroyed'); if (mounted) throw new Error('Autonomy/privacy runtime is already mounted'); mounted = true; shell.mount(target); await client.negotiate(); }, destroy() { if (destroyed) return; destroyed = true; shell.destroy(); removers.slice().reverse().forEach((remove) => remove()); runtime.destroy(); mounted = false; }, }); } return Object.freeze({GENERIC_NOTICE, UNVERIFIED_STOP, createAutonomyPrivacyRuntime, createRuntime, normalizeApproval, normalizeApprovalPage, normalizeAuditPage, normalizeForget, normalizeNotice, normalizePolicy, normalizePrivacyPolicy, normalizeReceipt}); }));