/** * Rule Host — Constrained execution layer for active code implementations * * PURPOSE: Load active code implementations from the SQLite activations table * (code_tool_hook channel), execute them in a constrained node:vm context, * and merge their decisions. * * ARCHITECTURE (PRI-436): * - SQLite is the SOLE production source of active RuleCode * - No filesystem ledger or implementation asset reads occur during evaluation * - Constructor takes stateDir for API compatibility (no longer used for impl loading) * - workspaceDir enables reading code_tool_hook activations from SQLite * - evaluate(input) loads active code implementations and runs them * - Each implementation executes in an isolated vm context with minimal helpers * - Decision merge: block short-circuits, requireApproval collects, allow is implicit * * SECURITY CONSTRAINTS (T-12-01, T-12-04): * - Candidate code loads through a dedicated vm context, not the host realm * - No importModuleDynamically callback * - Helpers are a frozen object — implementations cannot modify the helper surface * * CONSERVATIVE DEGRADATION (T-12-02, D-08): * - On ANY host error (load failure, eval error, vm error): return undefined * - Never throw, never bypass downstream gates (Progressive Gate, Edit Verification) */ import type { RuleHostInput, RuleHostResult } from '@principles/core/runtime-v2'; import type { RuleHostLogger } from '@principles/core/runtime-v2'; export type { RuleHostLogger } from '@principles/core/runtime-v2'; export interface RuleHostOptions { /** Workspace directory for SQLite access. Required for RuleHost to load active code_tool_hook activations. */ workspaceDir?: string; } type RuleHostActivationMode = 'shadow' | 'live'; export interface RuleHostObservedDecision extends RuleHostResult { readonly activationId: string; } /** * PRI-491 — A structured record of an activation that was skipped at load * time (flag-off v2, unsupported action, unsupported context version, * missing target_ref, content_json not an object, no implementationCode, * or duplicate active activation for target_ref). * * Unlike compile/load failures (which emit rulehost_unhealthy), skipped * activations have a configuration/flag reason — the RuleCode itself may be * valid, but the runtime chose not to execute it. Duplicate activations are * a special case: they ALSO emit rulehost_unhealthy (for telemetry), but are * surfaced in skippedActivations so the owner can observe and act (rc-9). * * ERR-002 (rc-9): every skip carries a reason + nextAction, never silent. */ export interface SkippedActivation { readonly activationId: string; readonly ruleId: string; /** * The mode the activation WOULD have had if loaded. Optional for cases * where the action itself is unrecognized (neither shadow nor live). */ readonly mode?: RuleHostActivationMode; readonly reason: string; readonly nextAction: string; } export interface RuleHostEvaluationReport { readonly liveDecision: RuleHostResult | undefined; /** * P1 (ISSUE-023): live 聚合决策的溯源 — 贡献该决策的 live activation id * (经 ruleId 反查 implementationSources)。审计开放项: 414 次 live-mode * 评估无法对账到具体规则,因为 live 事件不带 activationId。 */ readonly liveDecisionActivationId?: string; readonly shadowDecisions: readonly RuleHostObservedDecision[]; /** * PRI-491 — Activations that were skipped at load time. Empty when all * active activations loaded successfully. Each entry carries a structured * reason + nextAction so the owner can act without reading SQLite rows. */ readonly skippedActivations: readonly SkippedActivation[]; /** * PRI-567 — Number of live-mode implementations loaded for this evaluation. * Lets the gate distinguish "live rule evaluated → allow" from * "no live rules armed at all" (which previously both logged decision='allow', * making enforcement statistics read as if rules were active when none were). */ readonly liveRulesLoaded: number; /** Distinguishes an empty live set from a failed evaluation. */ readonly evaluationStatus: 'ok' | 'failed'; } export declare class RuleHost { private readonly stateDir; private logger; private readonly workspaceDir; private readonly implementationSources; private activationFingerprint; private cachedImplementations; /** * PRI-491: Cached skipped activations from the last load. Returned alongside * cachedImplementations on fingerprint hit so evaluateDetailed can surface * them without re-scanning SQLite. */ private cachedSkipped; private sqliteConnection; /** * R2-RH-002: Guards the "armed but empty" warn so it fires at most once per * RuleHost instance. Without this, the 0-rules path (workspaceDir missing OR * zero active code_tool_hook activations) returns [] silently on every * evaluation — an observability gap (rc-9-no-silent-fallback). The warn is * NOT a degradation fallback (RuleHost correctly has no opinion when empty); * it makes the empty-armed state visible so operators can investigate why * no live rules are loaded. */ private emptyLoadWarnEmitted; constructor(stateDir: string, logger?: RuleHostLogger, options?: RuleHostOptions); /** * Update the logger sink on a cached RuleHost instance. * * WorkspaceContext caches the RuleHost singleton, but each gate call may * pass a request-level logger. Without this update, warn/unhealthy logs * would forever go to the first logger sink, making the new path hard to * debug. */ updateLogger(logger: RuleHostLogger): void; /** * Evaluate the input against all active code implementations. * * Returns: * - undefined when no active code implementations exist (no opinion) * - undefined when all implementations return allow or matched=false * - { decision: 'block', ... } when any implementation returns block (short-circuits) * - { decision: 'requireApproval', ... } when any implementation returns requireApproval */ evaluate(input: RuleHostInput): RuleHostResult | undefined; dispose(): void; evaluateDetailed(input: RuleHostInput): RuleHostEvaluationReport; /** * Load active code implementations from the SQLite activations table. * * PRI-436: SQLite is the SOLE production source. The filesystem ledger * (principle-tree-ledger) and implementation asset paths have been deleted. * No fallback, no dual-source, no deprecated adapter. * * Source: activations table (code_tool_hook channel, deactivated_at IS NULL) * → JOIN pi_artifacts for content_json → extract implementationCode → compile */ private _loadActiveCodeImplementations; /** * R2-RH-002: Emit the "armed but empty" warn at most once per RuleHost * instance. The warn is cached via `emptyLoadWarnEmitted` so repeated * evaluations (which all hit the empty path) do not spam the log. * * This is an observability signal, NOT a degradation fallback — RuleHost * correctly returns "no opinion" when there are no active rules. The warn * makes the empty state visible so operators can distinguish "RuleHost is * working but has no rules" from "RuleHost is broken" (rc-9-no-silent-fallback). */ private _emitEmptyLoadWarn; /** * Load active code implementations from the activations table (code_tool_hook channel). * * For each activation record: * 1. Query the pi_artifacts table for the artifact content * 2. Parse content_json to extract implementationCode (treated as unknown, EP-01) * 3. Compile via loadRuleImplementationModule (isolated vm context) * * PRI-436 invariant: at most one active activation per rule (target_ref). * Duplicate active activations for the same target_ref are ALL skipped * (zero executions) and emit structured unhealthy evidence via logger.warn * (Runtime Contract Rule 9: graceful degradation includes a reason). * * All data from SQLite is treated as unknown and validated before use. */ private _loadFromActivationsTable; /** * PRI-437: Record an unhealthy activation state to EventLog. * * This makes compile/load failures visible to CLI (pd runtime health) and * Console API — NOT just a logger.warn that's silently skipped. * * ERR-002: degradation includes a reason and nextAction (not silent). * Failures in EventLog recording are caught and logged (never throw). */ private _recordUnhealthy; /** * PRI-491: Record a skipped activation to EventLog. * * Unlike _recordUnhealthy (compile/load failures), skipped activations have * a configuration/flag reason — the RuleCode itself may be valid, but the * runtime chose not to execute it (flag-off v2, unsupported context version, * unsupported action, content_json not object, no implementationCode). * * ERR-002: degradation includes a reason and nextAction (rc-9-no-silent-fallback). * Failures in EventLog recording are caught and logged (never throw). */ private _recordSkipped; }