@principles/host-runtime 0.7.7 → 0.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { type ActivatedPrinciple } from '@principles/core/runtime-v2';
1
2
  export interface ActivePrinciplePromptResult {
2
3
  additionalContext: string;
3
4
  principleIds: string[];
@@ -11,6 +12,25 @@ export interface ActivePrinciplePromptResult {
11
12
  exclusionReason?: 'host_principle_overlap';
12
13
  allValidatedPrinciplesExcluded: boolean;
13
14
  }
15
+ export interface PromptActivationCandidates {
16
+ principles: ActivatedPrinciple[];
17
+ excludedPrincipleIds: string[];
18
+ warnings: string[];
19
+ /** true = prompt flag off or state.db missing — candidates are necessarily empty */
20
+ aborted: boolean;
21
+ selfReportEnabled: boolean;
22
+ abstractionLayerEnabled: boolean;
23
+ }
24
+ /**
25
+ * PR #1844 follow-up: the activation READ half of buildActivePrinciplePromptContext,
26
+ * exported so the console budget projection can share the exact same input
27
+ * selection (FIFO activations → artifact resolve → exclude partition) without
28
+ * duplicating logic.
29
+ */
30
+ export declare function readPromptActivationCandidates(input: {
31
+ workspaceDir: string;
32
+ excludePrincipleIds?: ReadonlySet<string>;
33
+ }): Promise<PromptActivationCandidates>;
14
34
  export declare function buildActivePrinciplePromptContext(input: {
15
35
  workspaceDir: string;
16
36
  excludePrincipleIds?: ReadonlySet<string>;
@@ -3,7 +3,13 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { escapeXml } from '@principles/core/prompt-builder';
5
5
  import { loadPdConfigForPlugin } from './pd-config.js';
6
- export async function buildActivePrinciplePromptContext(input) {
6
+ /**
7
+ * PR #1844 follow-up: the activation READ half of buildActivePrinciplePromptContext,
8
+ * exported so the console budget projection can share the exact same input
9
+ * selection (FIFO activations → artifact resolve → exclude partition) without
10
+ * duplicating logic.
11
+ */
12
+ export async function readPromptActivationCandidates(input) {
7
13
  const warnings = [];
8
14
  const principles = [];
9
15
  const excludedPrincipleIds = [];
@@ -11,24 +17,21 @@ export async function buildActivePrinciplePromptContext(input) {
11
17
  if (!config.ok) {
12
18
  warnings.push(...config.errors.map((error) => `config_invalid: ${error.reason}; nextAction=${error.nextAction}`));
13
19
  }
14
- const promptFlag = computeFeatureFlagsFromConfig(config.effective).flags.prompt;
20
+ const { flags } = computeFeatureFlagsFromConfig(config.effective);
21
+ const { prompt: promptFlag } = flags;
22
+ const abstractionLayerEnabled = flags.abstraction_layer_v1?.enabled === true;
15
23
  if (!promptFlag?.enabled) {
16
24
  warnings.push('prompt_feature_disabled; nextAction=set features.prompt.enabled=true in .pd/config.yaml');
17
- return { additionalContext: '', principleIds: [], activationIds: [], artifactIds: [], warnings, budget: RUNTIME_V2_PRINCIPLE_BUDGET, truncated: false, excludedPrincipleIds, excludedCount: 0, allValidatedPrinciplesExcluded: false };
25
+ return { principles, excludedPrincipleIds, warnings, aborted: true, selfReportEnabled: false, abstractionLayerEnabled };
18
26
  }
19
27
  // PRI-532: agent self-report instruction rides the directive template when
20
28
  // the flag is on (flag-off keeps the template byte-identical).
21
- const selfReportEnabled = computeFeatureFlagsFromConfig(config.effective)
22
- .flags.principle_receipt_self_report?.enabled === true;
29
+ const selfReportEnabled = flags.principle_receipt_self_report?.enabled === true;
23
30
  let connection;
24
31
  const stateDbPath = path.join(input.workspaceDir, '.pd', 'state.db');
25
32
  if (!fs.existsSync(stateDbPath)) {
26
33
  warnings.push('activation_db_not_found; nextAction=initialize_workspace_runtime_state');
27
- return {
28
- additionalContext: '', principleIds: [], activationIds: [], artifactIds: [], warnings,
29
- budget: RUNTIME_V2_PRINCIPLE_BUDGET, truncated: false, excludedPrincipleIds,
30
- excludedCount: 0, allValidatedPrinciplesExcluded: false,
31
- };
34
+ return { principles, excludedPrincipleIds, warnings, aborted: true, selfReportEnabled, abstractionLayerEnabled };
32
35
  }
33
36
  try {
34
37
  connection = new SqliteConnection({ workspaceDir: input.workspaceDir, readonly: true, bootstrapIfMissing: false });
@@ -73,6 +76,17 @@ export async function buildActivePrinciplePromptContext(input) {
73
76
  finally {
74
77
  connection?.close();
75
78
  }
79
+ return { principles, excludedPrincipleIds, warnings, aborted: false, selfReportEnabled, abstractionLayerEnabled };
80
+ }
81
+ export async function buildActivePrinciplePromptContext(input) {
82
+ const { principles, excludedPrincipleIds, warnings, aborted, selfReportEnabled } = await readPromptActivationCandidates(input);
83
+ if (aborted) {
84
+ return {
85
+ additionalContext: '', principleIds: [], activationIds: [], artifactIds: [], warnings,
86
+ budget: RUNTIME_V2_PRINCIPLE_BUDGET, truncated: false, excludedPrincipleIds,
87
+ excludedCount: excludedPrincipleIds.length, allValidatedPrinciplesExcluded: false,
88
+ };
89
+ }
76
90
  const included = [];
77
91
  let additionalContext = '';
78
92
  let truncated = false;
@@ -0,0 +1,15 @@
1
+ export interface DegradedEnforcementRecord {
2
+ workspaceDir: string;
3
+ reason: string;
4
+ firstSeenAt: string;
5
+ lastSeenAt: string;
6
+ occurrences: number;
7
+ }
8
+ /** Stable per-workspace marker path (keyed by resolved workspace dir). */
9
+ export declare function markerPathFor(workspaceDir: string): string;
10
+ /**
11
+ * Record (or bump) the degraded-enforcement marker for a workspace.
12
+ * Overwrites atomically; repeated occurrences accumulate a counter and refresh
13
+ * `lastSeenAt` so a stale marker is distinguishable from an active one.
14
+ */
15
+ export declare function recordDegradedEnforcement(workspaceDir: string, reason: string, now?: Date): void;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Durable degraded-enforcement marker (security audit run-1,
3
+ * gate-failopen-allow-on-state-corruption).
4
+ *
5
+ * The RuleHost gate fails open when the governance store is unavailable
6
+ * (missing/corrupt `.pd/state.db`) — the documented availability posture. But
7
+ * the store lives in a space the governed agent can write, so "unavailable"
8
+ * is an attacker-choosable state, and the only in-source trace was a per-call
9
+ * warning plus an ambiguous console read model. This module records a durable
10
+ * marker OUTSIDE the agent-writable workspace (`~/.pd/enforcement-health/`)
11
+ * so an Owner can distinguish "enforcement degraded because the governance
12
+ * store was deleted/corrupt" from an ordinary never-initialized workspace.
13
+ *
14
+ * Best-effort by contract: every failure is swallowed. This is observability
15
+ * layered on top of the fail-open decision — it must never break gating.
16
+ */
17
+ import { createHash } from 'node:crypto';
18
+ import * as fs from 'node:fs';
19
+ import * as os from 'node:os';
20
+ import * as path from 'node:path';
21
+ function markerDir() {
22
+ return path.join(path.resolve(os.homedir()), '.pd', 'enforcement-health');
23
+ }
24
+ /** Stable per-workspace marker path (keyed by resolved workspace dir). */
25
+ export function markerPathFor(workspaceDir) {
26
+ const key = createHash('sha256').update(path.resolve(workspaceDir), 'utf8').digest('hex').slice(0, 16);
27
+ return path.join(markerDir(), `degraded-${key}.json`);
28
+ }
29
+ /**
30
+ * Record (or bump) the degraded-enforcement marker for a workspace.
31
+ * Overwrites atomically; repeated occurrences accumulate a counter and refresh
32
+ * `lastSeenAt` so a stale marker is distinguishable from an active one.
33
+ */
34
+ export function recordDegradedEnforcement(workspaceDir, reason, now = new Date()) {
35
+ try {
36
+ const dir = markerDir();
37
+ fs.mkdirSync(dir, { recursive: true });
38
+ const markerPath = markerPathFor(workspaceDir);
39
+ let previous = null;
40
+ try {
41
+ const parsed = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
42
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
43
+ previous = parsed;
44
+ }
45
+ }
46
+ catch {
47
+ // First occurrence, or the previous marker was unreadable — start fresh.
48
+ }
49
+ const timestamp = now.toISOString();
50
+ const record = {
51
+ workspaceDir: path.resolve(workspaceDir),
52
+ reason,
53
+ firstSeenAt: typeof previous?.firstSeenAt === 'string' ? previous.firstSeenAt : timestamp,
54
+ lastSeenAt: timestamp,
55
+ occurrences: (typeof previous?.occurrences === 'number' && Number.isSafeInteger(previous.occurrences)
56
+ ? previous.occurrences
57
+ : 0) + 1,
58
+ };
59
+ const tmpPath = `${markerPath}.tmp.${process.pid}.${Date.now()}`;
60
+ fs.writeFileSync(tmpPath, JSON.stringify(record, null, 2), { encoding: 'utf8', mode: 0o600 });
61
+ fs.renameSync(tmpPath, markerPath);
62
+ }
63
+ catch {
64
+ // Observability only — the gate's fail-open decision stands regardless.
65
+ }
66
+ }
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ import type { RuleImplementationRuntime } from './rule-implementation-runtime.js
5
5
  import { type PainDatabaseFactory, type PainEnrichmentProvider } from './production-pain-evidence.js';
6
6
  import type { GovernanceHostKind, ToolSemanticRegistry } from '@principles/core/runtime-v2';
7
7
  export * from './active-principle-prompt.js';
8
+ export * from './prompt-injection-projection.js';
8
9
  export * from './pd-config.js';
9
10
  export * from './production-rulehost-gate.js';
10
11
  export * from './rule-implementation-runtime.js';
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { buildActivePrinciplePromptContext } from './active-principle-prompt.js'
4
4
  import { createProductionRuleHostGate } from './production-rulehost-gate.js';
5
5
  import { createProductionPainEvidenceHandler } from './production-pain-evidence.js';
6
6
  export * from './active-principle-prompt.js';
7
+ export * from './prompt-injection-projection.js';
7
8
  export * from './pd-config.js';
8
9
  export * from './production-rulehost-gate.js';
9
10
  export * from './rule-implementation-runtime.js';
@@ -20,7 +20,7 @@
20
20
  * Hosts inject: a logger, a structured-event sink, their tool catalog, and an
21
21
  * env getter. Everything else is the existing production path.
22
22
  */
23
- import { createRuntimeStateHandle, InternalizationOrchestrator, DreamerRunner, PhilosopherRunner, ScribeRunner, ArtificerRunner, buildArtificerHostSemanticContext, EvaluatorRunner, RolloutReviewerRunner, DefaultDreamerValidator, DefaultPhilosopherValidator, DefaultScribeValidator, DefaultArtificerValidator, DefaultEvaluatorValidator, DefaultRolloutReviewerValidator, PiAiRuntimeAdapter, L2AgentLoopAdapter, ArtificerL2Adapter, buildL2PrincipleReaderFromLedger, OpenClawCliRuntimeAdapter, storeEmitter, createProductionGateDeps, resolveRuntimeConfigFromPdConfig, resolveRuntimeConfigForAgent, AGENT_NAME_FOR_TASK_KIND, isRuntimeConfigError, computeConsumerDecision, FULL_CHAIN_CONSUMER_RUNNER_KINDS, DEFAULT_CONSUMER_RUNNER_KINDS, InternalizationQueueReadModel, MVP_CORE_TASK_KINDS, SqliteConnection, SqliteReconciliationCursorStore, SUCCEEDED_TRANSITIONS_SCOPE,
23
+ import { createRuntimeStateHandle, InternalizationOrchestrator, DreamerRunner, PhilosopherRunner, ScribeRunner, ArtificerRunner, buildArtificerHostSemanticContext, EvaluatorRunner, RolloutReviewerRunner, DefaultDreamerValidator, DefaultPhilosopherValidator, DefaultScribeValidator, DefaultArtificerValidator, DefaultEvaluatorValidator, DefaultRolloutReviewerValidator, PiAiRuntimeAdapter, L2AgentLoopAdapter, ArtificerL2Adapter, buildL2PrincipleReaderFromLedger, OpenClawCliRuntimeAdapter, storeEmitter, createProductionGateDeps, resolveRuntimeConfigFromPdConfig, resolveRuntimeConfigForAgent, AGENT_NAME_FOR_TASK_KIND, isRuntimeConfigError, computeConsumerDecision, FULL_CHAIN_CONSUMER_RUNNER_KINDS, DEFAULT_CONSUMER_RUNNER_KINDS, InternalizationQueueReadModel, MVP_CORE_TASK_KINDS, SqliteConnection, SqliteReconciliationCursorStore, SUCCEEDED_TRANSITIONS_SCOPE, PrincipleTreeLedgerAdapter,
24
24
  // PRI-714: resolve outputLanguage from the same effective config the
25
25
  // runners already receive (EP-07: canonical resolved value, not raw input).
26
26
  resolveOutputLanguage, } from '@principles/core/runtime-v2';
@@ -459,7 +459,18 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
459
459
  runner = new PhilosopherRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultPhilosopherValidator() }, runnerOptions);
460
460
  break;
461
461
  case 'scribe':
462
- runner = new ScribeRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultScribeValidator() }, runnerOptions);
462
+ runner = new ScribeRunner({
463
+ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultScribeValidator(),
464
+ // I2 — CHAIN STAMPING (Owner review of PR #1856, P1): the scribe
465
+ // stamps the ledger principle UUID (minted at intake, keyed by the
466
+ // dreamer task seed's candidateId) onto the artifact it writes, so
467
+ // the dispatcher's ledger identity gate sees a resolvable
468
+ // identity on the live production chain. listForCandidate reads
469
+ // the ledger from disk per call — no cross-cycle staleness.
470
+ ledgerIdentity: {
471
+ listForCandidate: (candidateId) => new PrincipleTreeLedgerAdapter({ stateDir: `${workspaceDir}/.state` }).listForCandidate(candidateId),
472
+ },
473
+ }, runnerOptions);
463
474
  break;
464
475
  case 'artificer':
465
476
  runner = new ArtificerRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultArtificerValidator() },
@@ -483,6 +494,14 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
483
494
  runner = new EvaluatorRunner({
484
495
  stateManager, runtimeAdapter: adapter, eventEmitter: workspaceEmitter,
485
496
  artifactStore: stateManager.piArtifactStore, validator: new DefaultEvaluatorValidator(),
497
+ // PRI-911A (I1 writer boundary): the rule artifact's identity is a
498
+ // ledger UUID or nothing. hasPrinciple turns "UUID-shaped" into
499
+ // "UUID the ledger actually mints", so a shaped-but-unknown id is
500
+ // refused as invalid_identity instead of being carried forward.
501
+ // Same per-call disk read as the scribe wiring above.
502
+ ledgerIdentity: {
503
+ hasPrinciple: (principleId) => new PrincipleTreeLedgerAdapter({ stateDir: `${workspaceDir}/.state` }).hasPrinciple(principleId),
504
+ },
486
505
  ...createEvaluatorRepairDeps(workspaceDir, stateManager, logger),
487
506
  }, {
488
507
  ...runnerOptions,
@@ -14,7 +14,7 @@
14
14
  * dedupes by `${artifactId}::${channel}`; reopen idempotency is owned by
15
15
  * orchestrator.reopenTaskForRevision.
16
16
  */
17
- import { ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, SqliteConnection, SqliteActivationStateStore, SqliteApprovalQueueStore, SqlitePIArtifactStore, createProductionGateDeps, createPITaskDiagnosticJson, artificerRepairTaskId, computeFeatureFlagsFromConfig, isFeatureEnabled, } from '@principles/core/runtime-v2';
17
+ import { ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, SqliteConnection, SqliteActivationStateStore, SqliteApprovalQueueStore, SqlitePIArtifactStore, PrincipleTreeLedgerAdapter, createProductionGateDeps, createPITaskDiagnosticJson, artificerRepairTaskId, computeFeatureFlagsFromConfig, isFeatureEnabled, } from '@principles/core/runtime-v2';
18
18
  import { loadPdConfigForPlugin } from './pd-config.js';
19
19
  function normalizeDecision(decision) {
20
20
  if (decision.decision === 'activated') {
@@ -64,6 +64,27 @@ export async function dispatchRolloutActivation(workspaceDir, input, options = {
64
64
  const activationStateStore = new SqliteActivationStateStore(connection);
65
65
  const approvalQueueStore = new SqliteApprovalQueueStore(connection);
66
66
  const flagProbe = makeFlagProbe(workspaceDir);
67
+ // I3 upgrade (Owner review of PR #1856, P1): the production dispatch path
68
+ // verifies ledger membership BEFORE the approval record and the activation
69
+ // commit. The ledger SSOT lives in <workspace>/.state (same dir the intake
70
+ // used to mint the principle UUID); candidate lineage reuses the artifact
71
+ // read model plus raw diagnostic_json reads.
72
+ const ledgerIdentity = {
73
+ ledger: new PrincipleTreeLedgerAdapter({ stateDir: `${workspaceDir}/.state` }),
74
+ getArtifactById: artifactReadModel.getArtifactById,
75
+ getTaskDiagnosticJson: (taskId) => {
76
+ try {
77
+ const row = connection
78
+ .getDb()
79
+ .prepare('SELECT diagnostic_json FROM tasks WHERE task_id = ?')
80
+ .get(taskId);
81
+ return row?.diagnostic_json ?? null;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ },
87
+ };
67
88
  const dispatcher = new ActivationDispatcher(artifactReadModel, activationStateStore, {
68
89
  writers: [
69
90
  new PromptWriter(),
@@ -79,6 +100,7 @@ export async function dispatchRolloutActivation(workspaceDir, input, options = {
79
100
  new DeferArchiveWriter(),
80
101
  ],
81
102
  approvalQueueStore,
103
+ ledgerIdentity,
82
104
  });
83
105
  const decision = await dispatcher.dispatch({
84
106
  artifactId: input.artifactId,
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { buildRuleHostAction, buildToolSemanticRegistry, deriveToolHintsFromCanonicalKind, estimateLineChanges, mergeDecisions, SqliteConnection, UNAVAILABLE_RULE_CONTEXT, validateRuleContextV2, validateRuleHostResult, } from '@principles/core/runtime-v2';
3
+ import { buildRuleHostAction, buildToolSemanticRegistry, deriveToolHintsFromCanonicalKind, estimateLineChanges, mergeDecisions, SqliteConnection, UNAVAILABLE_RULE_CONTEXT, validateRuleContextV2, validateRuleHostResult, verifyPiArtifactRowDigest, } from '@principles/core/runtime-v2';
4
4
  import { scanRetiredContractSymbols } from './legacy-rule-contract-symbols.js';
5
+ import { recordDegradedEnforcement } from './degraded-enforcement-marker.js';
5
6
  import { createNodeRuleImplementationRuntime, MAX_ACTIVE_RULES, RULE_BATCH_SOURCE_BYTES, RULE_SOURCE_BYTES, } from './rule-implementation-runtime.js';
6
7
  const WARNING_LIMIT = 500;
7
8
  const MAX_WARNINGS = 16;
@@ -130,6 +131,12 @@ export function createProductionRuleHostGate(options = {}) {
130
131
  return { decision: 'allow', source: event.source, warnings: [boundedWarning('gate_deadline_exceeded', 'inspect host context providers and active RuleCode resource use')] };
131
132
  }
132
133
  if (!fs.existsSync(dbPath)) {
134
+ // Security audit run-1 (gate-failopen-allow-on-state-corruption):
135
+ // fail-open stays the availability contract, but a missing governance
136
+ // store is agent-choosable state — record a durable marker OUTSIDE the
137
+ // agent-writable workspace so the degradation is Owner-visible and
138
+ // distinguishable from a never-initialized workspace.
139
+ recordDegradedEnforcement(event.context.workspaceDir, 'activation_db_not_found');
133
140
  return { decision: 'allow', source: event.source, warnings: [boundedWarning('activation_db_not_found', 'initialize_workspace_runtime_state')] };
134
141
  }
135
142
  const connection = new SqliteConnection({ workspaceDir: event.context.workspaceDir, readonly: true, bootstrapIfMissing: false });
@@ -150,7 +157,10 @@ export function createProductionRuleHostGate(options = {}) {
150
157
  SELECT a.activation_id, a.artifact_id, a.target_ref, a.action,
151
158
  c.enforcement, c.isolation_decision_id,
152
159
  p.source_rule_id, p.source_principle_id,
153
- length(CAST(p.content_json AS BLOB)) AS content_bytes
160
+ length(CAST(p.content_json AS BLOB)) AS content_bytes,
161
+ (SELECT d.artifact_digest FROM activation_decisions d
162
+ WHERE d.subject_kind = 'activation' AND d.activation_id = a.activation_id
163
+ ORDER BY d.decided_at DESC, d.rowid DESC LIMIT 1) AS approved_artifact_digest
154
164
  FROM activations a
155
165
  JOIN pi_artifacts p ON a.artifact_id = p.artifact_id
156
166
  LEFT JOIN activation_control_states c ON a.activation_id = c.activation_id
@@ -220,7 +230,10 @@ export function createProductionRuleHostGate(options = {}) {
220
230
  continue;
221
231
  }
222
232
  const contentRow = connection.getDb().prepare(`
223
- SELECT content_json, length(CAST(content_json AS BLOB)) AS content_bytes
233
+ SELECT artifact_id, artifact_kind, source_task_id, source_principle_id, source_rule_id,
234
+ lineage_artifact_ids, validation_status, content_json,
235
+ created_at, updated_at,
236
+ length(CAST(content_json AS BLOB)) AS content_bytes
224
237
  FROM pi_artifacts WHERE artifact_id = ?
225
238
  `).get(artifactId);
226
239
  if (!isRecord(contentRow) || typeof contentRow.content_json !== 'string'
@@ -248,6 +261,34 @@ export function createProductionRuleHostGate(options = {}) {
248
261
  }
249
262
  return { decision: 'allow', source: event.source, warnings: [boundedWarning(`artifact_content_size_changed: activation=${activationId}`, 'retry after the active artifact update completes')], metadata: { evaluatedLiveRules: 0 } };
250
263
  }
264
+ // Enforcement-time content binding (security audit run-1,
265
+ // rulecode-approval-content-unbound): the Owner decision recorded
266
+ // activation_decisions.artifact_digest over the promotion-time
267
+ // artifact snapshot. Re-verify the CURRENT row against that digest
268
+ // before compiling — a workspace-local writer can UPDATE
269
+ // content_json, and executing content the Owner never approved
270
+ // silently defeats enforcement. A digest mismatch is an integrity
271
+ // failure, not an availability failure: skip this activation with a
272
+ // structured warning instead of failing open. Rows without a
273
+ // recorded activation-scoped decision (legacy activations from
274
+ // before the owner-decision lifecycle) keep their prior behavior.
275
+ // The check itself is core-owned (verifyPiArtifactRowDigest) so the
276
+ // gate and the plugin RuleHost cannot drift.
277
+ const approvedArtifactDigest = typeof row.approved_artifact_digest === 'string' ? row.approved_artifact_digest : null;
278
+ const digestCheck = verifyPiArtifactRowDigest(contentRow, contentJson, approvedArtifactDigest);
279
+ if (digestCheck.outcome === 'unverifiable') {
280
+ addWarning(warnings, `activation_artifact_invalid: ${activationId} (${digestCheck.error})`, 'inspect state.db artifact row integrity; the approved digest could not be re-verified');
281
+ continue;
282
+ }
283
+ if (digestCheck.outcome === 'tampered') {
284
+ const digestNextAction = 'the artifact content no longer matches the Owner-approved digest — deactivate and re-approve the rule; execution was skipped';
285
+ if (activationMode === 'shadow') {
286
+ addWarning(warnings, `artifact_digest_mismatch (shadow): activation=${activationId}`, `${digestNextAction}; live enforcement is unaffected`);
287
+ continue;
288
+ }
289
+ addWarning(warnings, `artifact_content_tampered: activation=${activationId} approved=${approvedArtifactDigest} actual=${digestCheck.actualDigest}`, digestNextAction);
290
+ continue;
291
+ }
251
292
  try {
252
293
  const content = JSON.parse(contentJson);
253
294
  if (!isRecord(content) || typeof content.implementationCode !== 'string' || content.implementationCode.length === 0) {
@@ -453,6 +494,14 @@ export function createProductionRuleHostGate(options = {}) {
453
494
  }
454
495
  catch (error) {
455
496
  addWarning(warnings, `activation_read_failed: ${error instanceof Error ? error.message : String(error)}`, 'inspect state.db schema and integrity');
497
+ // Same durable-marker contract as the missing-DB branch: a corrupt or
498
+ // unreadable governance store degrades enforcement to allow, so the
499
+ // degradation must leave a durable, Owner-visible trace outside the
500
+ // agent-writable workspace. Best-effort; never replaces the decision.
501
+ try {
502
+ recordDegradedEnforcement(event.context.workspaceDir, `activation_read_failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 300));
503
+ }
504
+ catch { /* observability only */ }
456
505
  return { decision: 'allow', source: event.source, warnings, metadata: { evaluatedLiveRules: 0 } };
457
506
  }
458
507
  finally {
@@ -0,0 +1,38 @@
1
+ export interface PromptInjectionProjection {
2
+ /** Which real injection route the workspace is on (abstraction_layer_v1 flag). */
3
+ route: 'legacy_trim' | 'shared_render';
4
+ budget: number;
5
+ /**
6
+ * Char count the REAL prompt path would consume: legacy route counts the
7
+ * trimToBudget lines joined as injected content (the render wrapper does
8
+ * NOT consume budget on that route); shared route counts the rendered
9
+ * directive context in full.
10
+ */
11
+ usedChars: number;
12
+ truncated: boolean;
13
+ injectedPrincipleIds: string[];
14
+ injectedActivationIds: string[];
15
+ warnings: string[];
16
+ }
17
+ /**
18
+ * PR #1844 follow-up: forecast the prompt-injection budget from the SAME
19
+ * projection the agent injection itself uses, routed by the same
20
+ * `abstraction_layer_v1` flag the OpenClaw plugin's runtime gate reads:
21
+ *
22
+ * - legacy route (live default, flag OFF): activation candidates from
23
+ * `readPromptActivationCandidates` (same FIFO query + artifact resolve as
24
+ * the plugin's `PromptActivationReader`) trimmed by the REAL
25
+ * `trimToBudget` — identical input × ordering × trim logic as
26
+ * `openclaw-plugin/src/hooks/prompt.ts`.
27
+ * - shared route (flag ON): delegate to `buildActivePrinciplePromptContext`,
28
+ * which IS the production shared path's projection source.
29
+ *
30
+ * Known conservative gap: the plugin's legacy route additionally dedups
31
+ * candidates against the legacy evolution ledger before trimming; the
32
+ * console cannot replay that reducer read-only, so this projection may
33
+ * forecast slightly MORE consumption than the real injection (never less).
34
+ */
35
+ export declare function buildLivePromptInjectionProjection(input: {
36
+ workspaceDir: string;
37
+ excludePrincipleIds?: ReadonlySet<string>;
38
+ }): Promise<PromptInjectionProjection>;
@@ -0,0 +1,61 @@
1
+ import { RUNTIME_V2_PRINCIPLE_BUDGET, computeFeatureFlagsFromConfig, trimToBudget, } from '@principles/core/runtime-v2';
2
+ import { escapeXml } from '@principles/core/prompt-builder';
3
+ import { loadPdConfigForPlugin } from './pd-config.js';
4
+ import { buildActivePrinciplePromptContext, readPromptActivationCandidates } from './active-principle-prompt.js';
5
+ /**
6
+ * PR #1844 follow-up: forecast the prompt-injection budget from the SAME
7
+ * projection the agent injection itself uses, routed by the same
8
+ * `abstraction_layer_v1` flag the OpenClaw plugin's runtime gate reads:
9
+ *
10
+ * - legacy route (live default, flag OFF): activation candidates from
11
+ * `readPromptActivationCandidates` (same FIFO query + artifact resolve as
12
+ * the plugin's `PromptActivationReader`) trimmed by the REAL
13
+ * `trimToBudget` — identical input × ordering × trim logic as
14
+ * `openclaw-plugin/src/hooks/prompt.ts`.
15
+ * - shared route (flag ON): delegate to `buildActivePrinciplePromptContext`,
16
+ * which IS the production shared path's projection source.
17
+ *
18
+ * Known conservative gap: the plugin's legacy route additionally dedups
19
+ * candidates against the legacy evolution ledger before trimming; the
20
+ * console cannot replay that reducer read-only, so this projection may
21
+ * forecast slightly MORE consumption than the real injection (never less).
22
+ */
23
+ export async function buildLivePromptInjectionProjection(input) {
24
+ const sharedRoute = computeFeatureFlagsFromConfig(loadPdConfigForPlugin(input.workspaceDir).effective).flags.abstraction_layer_v1?.enabled === true;
25
+ if (sharedRoute) {
26
+ const context = await buildActivePrinciplePromptContext(input);
27
+ return {
28
+ route: 'shared_render',
29
+ budget: context.budget,
30
+ usedChars: context.additionalContext.length,
31
+ truncated: context.truncated,
32
+ injectedPrincipleIds: context.principleIds,
33
+ injectedActivationIds: context.activationIds,
34
+ warnings: context.warnings,
35
+ };
36
+ }
37
+ const candidates = await readPromptActivationCandidates(input);
38
+ const { principles, warnings } = candidates;
39
+ if (candidates.aborted || principles.length === 0) {
40
+ return {
41
+ route: 'legacy_trim',
42
+ budget: RUNTIME_V2_PRINCIPLE_BUDGET,
43
+ usedChars: 0,
44
+ truncated: false,
45
+ injectedPrincipleIds: [],
46
+ injectedActivationIds: [],
47
+ warnings,
48
+ };
49
+ }
50
+ const trimmed = trimToBudget(principles, RUNTIME_V2_PRINCIPLE_BUDGET, escapeXml);
51
+ const injected = principles.filter((p) => trimmed.injectedIds.has(p.principleId));
52
+ return {
53
+ route: 'legacy_trim',
54
+ budget: RUNTIME_V2_PRINCIPLE_BUDGET,
55
+ usedChars: trimmed.lines.join('\n').length,
56
+ truncated: trimmed.truncated,
57
+ injectedPrincipleIds: [...trimmed.injectedIds],
58
+ injectedActivationIds: injected.map((p) => p.activationId),
59
+ warnings,
60
+ };
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/host-runtime",
3
- "version": "0.7.7",
3
+ "version": "0.7.10",
4
4
  "description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,8 +21,8 @@
21
21
  "lint": "eslint \"src/**/*.ts\""
22
22
  },
23
23
  "dependencies": {
24
- "@principles/core": "^1.74.1",
25
- "@principles/install-layout": "^0.2.0",
24
+ "@principles/core": "^1.287.5",
25
+ "@principles/install-layout": "^0.2.7",
26
26
  "better-sqlite3": "^13.0.3",
27
27
  "js-yaml": "^5.4.1"
28
28
  },