@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.
- package/dist/active-principle-prompt.d.ts +20 -0
- package/dist/active-principle-prompt.js +24 -10
- package/dist/degraded-enforcement-marker.d.ts +15 -0
- package/dist/degraded-enforcement-marker.js +66 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/internalization-consumer-cycle.js +21 -2
- package/dist/internalization-consumer-governance.js +23 -1
- package/dist/production-rulehost-gate.js +52 -3
- package/dist/prompt-injection-projection.d.ts +38 -0
- package/dist/prompt-injection-projection.js +61 -0
- package/package.json +3 -3
|
@@ -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
|
-
|
|
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
|
|
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 {
|
|
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 =
|
|
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({
|
|
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
|
|
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.
|
|
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.
|
|
25
|
-
"@principles/install-layout": "^0.2.
|
|
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
|
},
|