@principles/host-runtime 0.7.4 → 0.7.6
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/host-liveness-contract.d.ts +57 -0
- package/dist/host-liveness-contract.js +126 -0
- package/dist/internalization-consumer-cycle.d.ts +22 -0
- package/dist/internalization-consumer-cycle.js +30 -15
- package/dist/internalization-consumer-governance.d.ts +3 -3
- package/dist/internalization-consumer-governance.js +5 -3
- package/dist/production-rulehost-gate.js +137 -17
- package/dist/workspace-telemetry-emitter.js +5 -1
- package/package.json +1 -1
|
@@ -1,3 +1,60 @@
|
|
|
1
1
|
import type { HostLivenessContract } from '@principles/core/runtime-v2';
|
|
2
2
|
/** Adapter-owned facts consumed by the non-bypassable RuleCode promotion gate. */
|
|
3
3
|
export declare const OPENCLAW_HOST_LIVENESS_CONTRACT: HostLivenessContract;
|
|
4
|
+
export type PromotionHostLivenessResolution = {
|
|
5
|
+
ok: true;
|
|
6
|
+
hostKind: 'openclaw';
|
|
7
|
+
/**
|
|
8
|
+
* The workspace host DECLARATIONS this resolution is based on.
|
|
9
|
+
* Empty array = nothing declared yet (the OpenClaw contract is the
|
|
10
|
+
* documented historical default; review S3: callers can distinguish an
|
|
11
|
+
* undeclared workspace from a declared-OpenClaw one);
|
|
12
|
+
* `['openclaw']` = the OpenClaw host actually declared itself.
|
|
13
|
+
*/
|
|
14
|
+
hostKinds: readonly string[];
|
|
15
|
+
hostContract: HostLivenessContract;
|
|
16
|
+
hostRuntimeVersion: string;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
/** Stable machine-readable fail-closed reason (also surfaced as the hostRuntimeVersion marker in failure snapshots / capability reporting). */
|
|
20
|
+
reason: 'host_declarations_unreadable' | 'workspace_provenance_unreadable' | 'promotion_host_unsupported' | 'multi_host_promotion_ambiguous' | 'host_declaration_missing_for_configured_host';
|
|
21
|
+
hostKinds: readonly string[];
|
|
22
|
+
hostContract: null;
|
|
23
|
+
hostRuntimeVersion: null;
|
|
24
|
+
nextAction: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* PRI-813: resolve the promotion host-liveness contract from the workspace's
|
|
28
|
+
* REAL host state instead of unconditionally inheriting the OpenClaw
|
|
29
|
+
* contract (which made Codex workspaces report runtime_compatibility /
|
|
30
|
+
* runtime_shadow_evidence as passed — a false capability claim; the promotion
|
|
31
|
+
* then only ever failed on sample count, hiding the unsupported truth).
|
|
32
|
+
*
|
|
33
|
+
* Two durable provenance sources, cross-checked (Owner review round-4):
|
|
34
|
+
* 1. host declarations — `.pd/host-tool-semantics/<hostKind>.json`;
|
|
35
|
+
* 2. trajectory.db `pain_events.host_kind` — which hosts actually produced
|
|
36
|
+
* governance evidence here (survives declaration deletion).
|
|
37
|
+
*
|
|
38
|
+
* Resolution rules (fail closed outside the one host with an approved
|
|
39
|
+
* capability authority, and fail closed whenever Codex evidence exists but
|
|
40
|
+
* its declaration is missing — never silently fall back to OpenClaw):
|
|
41
|
+
* - no declaration AND no Codex evidence → OpenClaw contract (the historical
|
|
42
|
+
* governance default; a workspace where only OpenClaw ever ran — or no
|
|
43
|
+
* host ran — is exactly the legacy state, and existing workspaces rely
|
|
44
|
+
* on it);
|
|
45
|
+
* - only openclaw declared and no Codex evidence → OPENCLAW_HOST_LIVENESS_CONTRACT;
|
|
46
|
+
* - codex declared (alone) → promotion_host_unsupported (Codex promotion
|
|
47
|
+
* stays explicitly unsupported until an evidence-backed capability
|
|
48
|
+
* authority decision exists — no second host contract here);
|
|
49
|
+
* - multiple hosts declared → multi_host_promotion_ambiguous (never guess a
|
|
50
|
+
* target host; the Owner decides host-selection policy separately);
|
|
51
|
+
* - Codex evidence present but codex.json missing (deleted/corrupted —
|
|
52
|
+
* alone or alongside openclaw.json) → host_declaration_missing_for_configured_host
|
|
53
|
+
* (re-persist the declaration before promoting);
|
|
54
|
+
* - declarations or provenance unreadable → fail closed with the reason.
|
|
55
|
+
*
|
|
56
|
+
* This function intentionally creates no Codex contract, no capability
|
|
57
|
+
* registry, and no new store — it only routes the existing single authority
|
|
58
|
+
* truthfully over state that already exists.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolvePromotionHostLiveness(workspaceDir: string): PromotionHostLivenessResolution;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import { loadHostToolDeclarations } from './host-tool-declaration.js';
|
|
1
5
|
/** Adapter-owned facts consumed by the non-bypassable RuleCode promotion gate. */
|
|
2
6
|
export const OPENCLAW_HOST_LIVENESS_CONTRACT = {
|
|
3
7
|
version: 'openclaw-legacy@1',
|
|
@@ -16,3 +20,125 @@ export const OPENCLAW_HOST_LIVENESS_CONTRACT = {
|
|
|
16
20
|
{ probeId: 'probe-owner-review', capabilityId: 'owner_review_access', toolName: 'owner_review_access', params: {}, expectedDecision: 'allow' },
|
|
17
21
|
],
|
|
18
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Existing durable provenance (Owner review round-4): `trajectory.db`
|
|
25
|
+
* `pain_events.host_kind` records which host actually produced governance
|
|
26
|
+
* evidence in this workspace ('codex' rows are written by the Codex
|
|
27
|
+
* ingestion/admission path). It survives deletion of
|
|
28
|
+
* `.pd/host-tool-semantics/<hostKind>.json`, which is exactly the
|
|
29
|
+
* declaration-lost state a silent OpenClaw fallback would misread.
|
|
30
|
+
* Absent file = no evidence (normal for quiet workspaces). A pain_events
|
|
31
|
+
* table predating the host_kind column carries no host evidence BY
|
|
32
|
+
* CONSTRUCTION (nothing could have been recorded before the column existed)
|
|
33
|
+
* — equivalent to empty, not unreadable. Read-only, never mutates state.
|
|
34
|
+
*/
|
|
35
|
+
function readWorkspaceHostProvenance(workspaceDir) {
|
|
36
|
+
const dbPath = path.join(workspaceDir, '.state', 'trajectory.db');
|
|
37
|
+
if (!fs.existsSync(dbPath))
|
|
38
|
+
return { ok: true, kinds: [] };
|
|
39
|
+
let db;
|
|
40
|
+
try {
|
|
41
|
+
db = new Database(dbPath, { readonly: true });
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return { ok: false, nextAction: `inspect the workspace trajectory database before promoting: ${error instanceof Error ? error.message : String(error)}` };
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const columns = db.prepare('PRAGMA table_info(pain_events)').all();
|
|
48
|
+
const hasHostKindColumn = Array.isArray(columns)
|
|
49
|
+
&& columns.some((column) => typeof column === 'object' && column !== null && column.name === 'host_kind');
|
|
50
|
+
if (!hasHostKindColumn)
|
|
51
|
+
return { ok: true, kinds: [] };
|
|
52
|
+
const rows = db.prepare('SELECT DISTINCT host_kind FROM pain_events WHERE host_kind IS NOT NULL').all();
|
|
53
|
+
if (!Array.isArray(rows)) {
|
|
54
|
+
return { ok: false, nextAction: 'inspect the workspace trajectory database pain_events integrity before promoting' };
|
|
55
|
+
}
|
|
56
|
+
const kinds = rows
|
|
57
|
+
.filter((row) => typeof row === 'object' && row !== null)
|
|
58
|
+
.map(row => row.host_kind)
|
|
59
|
+
.filter((kind) => typeof kind === 'string');
|
|
60
|
+
return { ok: true, kinds };
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return { ok: false, nextAction: `inspect the workspace trajectory database (locked, or damaged pain_events table) before promoting: ${error instanceof Error ? error.message : String(error)}` };
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
try {
|
|
67
|
+
db.close();
|
|
68
|
+
}
|
|
69
|
+
catch { /* best effort */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function promotionHostFailure(reason, hostKinds, nextAction) {
|
|
73
|
+
return { ok: false, reason, hostKinds, hostContract: null, hostRuntimeVersion: null, nextAction };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* PRI-813: resolve the promotion host-liveness contract from the workspace's
|
|
77
|
+
* REAL host state instead of unconditionally inheriting the OpenClaw
|
|
78
|
+
* contract (which made Codex workspaces report runtime_compatibility /
|
|
79
|
+
* runtime_shadow_evidence as passed — a false capability claim; the promotion
|
|
80
|
+
* then only ever failed on sample count, hiding the unsupported truth).
|
|
81
|
+
*
|
|
82
|
+
* Two durable provenance sources, cross-checked (Owner review round-4):
|
|
83
|
+
* 1. host declarations — `.pd/host-tool-semantics/<hostKind>.json`;
|
|
84
|
+
* 2. trajectory.db `pain_events.host_kind` — which hosts actually produced
|
|
85
|
+
* governance evidence here (survives declaration deletion).
|
|
86
|
+
*
|
|
87
|
+
* Resolution rules (fail closed outside the one host with an approved
|
|
88
|
+
* capability authority, and fail closed whenever Codex evidence exists but
|
|
89
|
+
* its declaration is missing — never silently fall back to OpenClaw):
|
|
90
|
+
* - no declaration AND no Codex evidence → OpenClaw contract (the historical
|
|
91
|
+
* governance default; a workspace where only OpenClaw ever ran — or no
|
|
92
|
+
* host ran — is exactly the legacy state, and existing workspaces rely
|
|
93
|
+
* on it);
|
|
94
|
+
* - only openclaw declared and no Codex evidence → OPENCLAW_HOST_LIVENESS_CONTRACT;
|
|
95
|
+
* - codex declared (alone) → promotion_host_unsupported (Codex promotion
|
|
96
|
+
* stays explicitly unsupported until an evidence-backed capability
|
|
97
|
+
* authority decision exists — no second host contract here);
|
|
98
|
+
* - multiple hosts declared → multi_host_promotion_ambiguous (never guess a
|
|
99
|
+
* target host; the Owner decides host-selection policy separately);
|
|
100
|
+
* - Codex evidence present but codex.json missing (deleted/corrupted —
|
|
101
|
+
* alone or alongside openclaw.json) → host_declaration_missing_for_configured_host
|
|
102
|
+
* (re-persist the declaration before promoting);
|
|
103
|
+
* - declarations or provenance unreadable → fail closed with the reason.
|
|
104
|
+
*
|
|
105
|
+
* This function intentionally creates no Codex contract, no capability
|
|
106
|
+
* registry, and no new store — it only routes the existing single authority
|
|
107
|
+
* truthfully over state that already exists.
|
|
108
|
+
*/
|
|
109
|
+
export function resolvePromotionHostLiveness(workspaceDir) {
|
|
110
|
+
const loaded = loadHostToolDeclarations(workspaceDir);
|
|
111
|
+
if (!loaded.ok && loaded.reason !== 'host_tool_declaration_missing') {
|
|
112
|
+
return promotionHostFailure('host_declarations_unreadable', [], `repair the host tool declarations before promoting: ${loaded.reason} (${loaded.nextAction})`);
|
|
113
|
+
}
|
|
114
|
+
const declaredKinds = loaded.ok
|
|
115
|
+
? [...new Set(loaded.declarations.map(declaration => declaration.hostKind))].sort()
|
|
116
|
+
: [];
|
|
117
|
+
const provenance = readWorkspaceHostProvenance(workspaceDir);
|
|
118
|
+
if (!provenance.ok) {
|
|
119
|
+
return promotionHostFailure('workspace_provenance_unreadable', declaredKinds, provenance.nextAction);
|
|
120
|
+
}
|
|
121
|
+
const provenanceKinds = provenance.kinds.filter(kind => kind === 'openclaw' || kind === 'codex');
|
|
122
|
+
const effectiveKinds = [...new Set([...declaredKinds, ...provenanceKinds])].sort();
|
|
123
|
+
// The promotion host can only be the OpenClaw contract when nothing on the
|
|
124
|
+
// workspace — neither a declaration nor behavioral evidence — says Codex.
|
|
125
|
+
if (!effectiveKinds.includes('codex')) {
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
hostKind: 'openclaw',
|
|
129
|
+
hostKinds: declaredKinds,
|
|
130
|
+
hostContract: OPENCLAW_HOST_LIVENESS_CONTRACT,
|
|
131
|
+
hostRuntimeVersion: OPENCLAW_HOST_LIVENESS_CONTRACT.version,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (!declaredKinds.includes('codex')) {
|
|
135
|
+
// Codex evidence exists (declaration gone, or only openclaw.json
|
|
136
|
+
// remains) — the exact state where a silent OpenClaw fallback would
|
|
137
|
+
// resurrect the false capability PASS (Owner review round-4).
|
|
138
|
+
return promotionHostFailure('host_declaration_missing_for_configured_host', effectiveKinds, 'Codex evidence exists in this workspace but .pd/host-tool-semantics/codex.json is missing; re-run the Codex worker so it re-persists its declaration (or deactivate the Codex activation) before promoting');
|
|
139
|
+
}
|
|
140
|
+
if (declaredKinds.length > 1) {
|
|
141
|
+
return promotionHostFailure('multi_host_promotion_ambiguous', effectiveKinds, `the workspace declares multiple hosts (${declaredKinds.join(', ')}); promotion requires an unambiguous governing host — decide the promotion target host first`);
|
|
142
|
+
}
|
|
143
|
+
return promotionHostFailure('promotion_host_unsupported', effectiveKinds, `host '${declaredKinds[0]}' has no approved promotion capability authority; promotion on this host is explicitly unsupported until an evidence-backed capability decision exists`);
|
|
144
|
+
}
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared host-neutral internalization consumer cycle (PRI-624 Slice C).
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from openclaw-plugin's
|
|
5
|
+
* `internalization-auto-consumer-service.ts` `runConsumerCycle` so that the
|
|
6
|
+
* OpenClaw auto-consumer scheduler and the Companion workspace worker call
|
|
7
|
+
* ONE downstream execution implementation (SPEC §13: "reusing existing
|
|
8
|
+
* downstream consumer logic" — the Companion must not copy it):
|
|
9
|
+
*
|
|
10
|
+
* flag gate → config/runtime config → state handle → orchestrator(dryRun)
|
|
11
|
+
* → queue read model → consumer decision → wakeOnce loop → adapter
|
|
12
|
+
* → runner construction (dreamer…rollout_reviewer) → runner.run
|
|
13
|
+
* → commit proposal → finally: recovery sweep + reconciliation budget
|
|
14
|
+
*
|
|
15
|
+
* The lease is acquired INSIDE runner.run by the Runtime V2 lease manager
|
|
16
|
+
* (persisted in `<workspace>/.pd/state.db`, cross-process safe) — this cycle
|
|
17
|
+
* never leases by itself, so two schedulers (OpenClaw + Companion) racing on
|
|
18
|
+
* one workspace converge to exactly-once execution via lease_conflict.
|
|
19
|
+
*
|
|
20
|
+
* Hosts inject: a logger, a structured-event sink, their tool catalog, and an
|
|
21
|
+
* env getter. Everything else is the existing production path.
|
|
22
|
+
*/
|
|
1
23
|
import { type ToolSemanticRegistry } from '@principles/core/runtime-v2';
|
|
2
24
|
import { type ConsumerGovernanceLogger } from './internalization-consumer-governance.js';
|
|
3
25
|
export declare const INTERNALIZATION_AUTO_CONSUMER_FLAG_ID = "internalization_auto_consumer";
|
|
@@ -20,7 +20,6 @@
|
|
|
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 { createHash } from 'node:crypto';
|
|
24
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,
|
|
25
24
|
// PRI-714: resolve outputLanguage from the same effective config the
|
|
26
25
|
// runners already receive (EP-07: canonical resolved value, not raw input).
|
|
@@ -30,9 +29,6 @@ import { loadPdConfigForPlugin, loadFeatureFlagFromConfig } from './pd-config.js
|
|
|
30
29
|
import { createEvaluatorRepairDeps, createRolloutGovernanceDeps } from './internalization-consumer-governance.js';
|
|
31
30
|
import { WorkspaceTelemetryEmitter } from './workspace-telemetry-emitter.js';
|
|
32
31
|
export const INTERNALIZATION_AUTO_CONSUMER_FLAG_ID = 'internalization_auto_consumer';
|
|
33
|
-
function contentHashFn(text) {
|
|
34
|
-
return createHash('sha256').update(text).digest('hex');
|
|
35
|
-
}
|
|
36
32
|
function formatRunOnceCommand(workspaceDir) {
|
|
37
33
|
return `pd runtime internalization run-once --workspace "${workspaceDir}" --runner dreamer --runtime config --json`;
|
|
38
34
|
}
|
|
@@ -311,6 +307,19 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
|
311
307
|
// PRI-719: everything downstream reads the LEASED TASK's resolved config —
|
|
312
308
|
// the kind-specific profile, not the shared gate resolution.
|
|
313
309
|
const taskRuntimeKind = taskRuntimeConfig.runtimeKind;
|
|
310
|
+
// PRI-634 A3: workspace-scoped telemetry sink. Constructed here (per-wake,
|
|
311
|
+
// workspaceDir in scope) so events from THIS workspace's runners are
|
|
312
|
+
// attributable to THIS workspace — a global subscriber on the storeEmitter
|
|
313
|
+
// singleton cannot (multi-workspace isolation, see
|
|
314
|
+
// workspace-telemetry-emitter.ts). Persist failures degrade through the
|
|
315
|
+
// host's structured event port, never break the runner.
|
|
316
|
+
// PRI-795 review P1: shared by the EvaluatorRunner AND the ArtificerL2
|
|
317
|
+
// adapter so `artificer_l2_complete` evidence (abortOwner/budget/elapsed/
|
|
318
|
+
// stopReason/tokenUsage) persists durably instead of vanishing with the
|
|
319
|
+
// process (the EP002-R3 "nothing recoverable after a failure" class).
|
|
320
|
+
const workspaceEmitter = new WorkspaceTelemetryEmitter(storeEmitter, workspaceDir, (detail) => {
|
|
321
|
+
emitEvent('WORKSPACE_TELEMETRY_PERSIST_FAILED', detail);
|
|
322
|
+
});
|
|
314
323
|
let adapter;
|
|
315
324
|
if (taskRuntimeKind === 'pi-ai') {
|
|
316
325
|
// PRI-419: when l2_dreamer flag is on AND this is a dreamer task, route
|
|
@@ -342,6 +351,13 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
|
342
351
|
// PRI-758 CodeRabbit P2: forward maxTokens + systemPrompt from profile.
|
|
343
352
|
...(taskRuntimeConfig.maxTokens !== undefined ? { maxTokens: taskRuntimeConfig.maxTokens } : {}),
|
|
344
353
|
...(taskRuntimeConfig.systemPrompt ? { systemPrompt: taskRuntimeConfig.systemPrompt } : {}),
|
|
354
|
+
// PRI-795: forward the profile reasoning level — the PiAiRuntimeAdapter
|
|
355
|
+
// branch below already receives it; the L2 loop was silently dropping
|
|
356
|
+
// it, leaving always-thinking models at their most expensive default.
|
|
357
|
+
...(taskRuntimeConfig.reasoning !== undefined ? { reasoning: taskRuntimeConfig.reasoning } : {}),
|
|
358
|
+
// PRI-795 review P1: the completion evidence must land in the
|
|
359
|
+
// workspace's durable telemetry sink, not the in-process singleton.
|
|
360
|
+
eventEmitter: workspaceEmitter,
|
|
345
361
|
});
|
|
346
362
|
}
|
|
347
363
|
else if (l2Flag.enabled && wakeResult.taskKind === 'dreamer') {
|
|
@@ -429,25 +445,24 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
|
429
445
|
// subscriber on the storeEmitter singleton cannot (multi-workspace
|
|
430
446
|
// isolation, see workspace-telemetry-emitter.ts). Persist failures
|
|
431
447
|
// degrade through the host's structured event port, never break the runner.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
});
|
|
448
|
+
// (PRI-795 review P1: the emitter itself is constructed above, before the
|
|
449
|
+
// adapter dispatch, so the ArtificerL2 adapter can share it.)
|
|
435
450
|
// Dispatch by leased task kind. Only kinds listed in
|
|
436
451
|
// FULL_CHAIN_CONSUMER_RUNNER_KINDS can be leased here; anything else
|
|
437
452
|
// (e.g. diagnostician) hits the default branch — fail loud if it does (EP-03).
|
|
438
453
|
let runner;
|
|
439
454
|
switch (taskKind) {
|
|
440
455
|
case 'dreamer':
|
|
441
|
-
runner = new DreamerRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDreamerValidator()
|
|
456
|
+
runner = new DreamerRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDreamerValidator() }, runnerOptions);
|
|
442
457
|
break;
|
|
443
458
|
case 'philosopher':
|
|
444
|
-
runner = new PhilosopherRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultPhilosopherValidator()
|
|
459
|
+
runner = new PhilosopherRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultPhilosopherValidator() }, runnerOptions);
|
|
445
460
|
break;
|
|
446
461
|
case 'scribe':
|
|
447
|
-
runner = new ScribeRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultScribeValidator()
|
|
462
|
+
runner = new ScribeRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultScribeValidator() }, runnerOptions);
|
|
448
463
|
break;
|
|
449
464
|
case 'artificer':
|
|
450
|
-
runner = new ArtificerRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultArtificerValidator()
|
|
465
|
+
runner = new ArtificerRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultArtificerValidator() },
|
|
451
466
|
// PRI-741: thread the host semantic projection into generation.
|
|
452
467
|
{ ...runnerOptions, ...(artificerHostSemanticContext !== undefined ? { hostSemanticContext: artificerHostSemanticContext } : {}) });
|
|
453
468
|
break;
|
|
@@ -466,7 +481,7 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
|
466
481
|
// 无论 OpenClaw 还是 Codex worker 执行, deterministic gate wiring
|
|
467
482
|
// 必须存在 (第二个 options 参数, 绝不放第一个 deps 参数)。
|
|
468
483
|
runner = new EvaluatorRunner({
|
|
469
|
-
stateManager, runtimeAdapter: adapter, eventEmitter:
|
|
484
|
+
stateManager, runtimeAdapter: adapter, eventEmitter: workspaceEmitter,
|
|
470
485
|
artifactStore: stateManager.piArtifactStore, validator: new DefaultEvaluatorValidator(),
|
|
471
486
|
...createEvaluatorRepairDeps(workspaceDir, stateManager, logger),
|
|
472
487
|
}, {
|
|
@@ -492,9 +507,9 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
|
492
507
|
// authoritative values before validation, so a bad echo no longer
|
|
493
508
|
// dead-ends the candidate before the approval queue.
|
|
494
509
|
//
|
|
495
|
-
// P0-E/F 治理接线: approve_rollout → 自动 ActivationDispatcher (
|
|
496
|
-
//
|
|
497
|
-
// scribe/artificer 修订 (绝不进入 approval 队列, INV-04)。
|
|
510
|
+
// P0-E/F 治理接线: approve_rollout → 自动 ActivationDispatcher (PRI-811
|
|
511
|
+
// Phase B: 一律 approvals.pending, Owner 批准后才激活); needs_revision →
|
|
512
|
+
// reopen scribe/artificer 修订 (绝不进入 approval 队列, INV-04)。
|
|
498
513
|
runner = new RolloutReviewerRunner({
|
|
499
514
|
stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter,
|
|
500
515
|
artifactStore: stateManager.piArtifactStore, validator: new DefaultRolloutReviewerValidator(),
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* (P0-D/E/F 生产接线) so the OpenClaw auto-consumer and the Companion
|
|
6
6
|
* workspace worker share ONE wiring implementation instead of copying it:
|
|
7
7
|
* - evaluator: isRepairLoopEnabled + seedArtificerRepairTask (bounded repair)
|
|
8
|
-
* - rollout_reviewer: dispatchActivation (approve_rollout → ActivationDispatcher
|
|
9
|
-
*
|
|
10
|
-
* (needs_revision → reopen scribe/artificer, 禁止入 approval)
|
|
8
|
+
* - rollout_reviewer: dispatchActivation (approve_rollout → ActivationDispatcher;
|
|
9
|
+
* PRI-811 Phase B: 一律入 approvals.pending,Owner 批准后才激活) +
|
|
10
|
+
* reopenRevisionTarget (needs_revision → reopen scribe/artificer, 禁止入 approval)
|
|
11
11
|
*
|
|
12
12
|
* Everything is assembled from @principles/core stores/dispatchers; the only
|
|
13
13
|
* host knowledge is a structural logger. Idempotency (INV-08): dispatcher
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* (P0-D/E/F 生产接线) so the OpenClaw auto-consumer and the Companion
|
|
6
6
|
* workspace worker share ONE wiring implementation instead of copying it:
|
|
7
7
|
* - evaluator: isRepairLoopEnabled + seedArtificerRepairTask (bounded repair)
|
|
8
|
-
* - rollout_reviewer: dispatchActivation (approve_rollout → ActivationDispatcher
|
|
9
|
-
*
|
|
10
|
-
* (needs_revision → reopen scribe/artificer, 禁止入 approval)
|
|
8
|
+
* - rollout_reviewer: dispatchActivation (approve_rollout → ActivationDispatcher;
|
|
9
|
+
* PRI-811 Phase B: 一律入 approvals.pending,Owner 批准后才激活) +
|
|
10
|
+
* reopenRevisionTarget (needs_revision → reopen scribe/artificer, 禁止入 approval)
|
|
11
11
|
*
|
|
12
12
|
* Everything is assembled from @principles/core stores/dispatchers; the only
|
|
13
13
|
* host knowledge is a structural logger. Idempotency (INV-08): dispatcher
|
|
@@ -83,6 +83,8 @@ export async function dispatchRolloutActivation(workspaceDir, input, options = {
|
|
|
83
83
|
const decision = await dispatcher.dispatch({
|
|
84
84
|
artifactId: input.artifactId,
|
|
85
85
|
channel: input.channel, // InternalizationChannel union; rollout 链的 channel 已由任务元数据校验
|
|
86
|
+
// 'auto_activate' = rollout reviewer 推荐激活。PRI-811 Phase B 起
|
|
87
|
+
// dispatcher 对一切推荐只入 approval 队列,不直接激活。
|
|
86
88
|
rolloutDecision: 'auto_activate',
|
|
87
89
|
actor: { kind: 'system', source: 'rollout_reviewer' },
|
|
88
90
|
now: new Date().toISOString(),
|
|
@@ -199,7 +199,18 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
199
199
|
continue;
|
|
200
200
|
}
|
|
201
201
|
const [row] = group;
|
|
202
|
-
if (!row
|
|
202
|
+
if (!row)
|
|
203
|
+
continue;
|
|
204
|
+
// PRI-813: shadow activations are no longer skipped here — they run
|
|
205
|
+
// through the SAME budget/content/context validation and
|
|
206
|
+
// evaluateBatch pipeline as live rows, but their results are
|
|
207
|
+
// collected as observations only and never join mergeDecisions
|
|
208
|
+
// (shadow observes, never enforces). The v2 gate below stays ahead
|
|
209
|
+
// of any evaluation, so Codex v2 shadow remains suspended.
|
|
210
|
+
const activationMode = row.action === 'code_tool_hook_live_activate'
|
|
211
|
+
? 'live'
|
|
212
|
+
: row.action === 'code_tool_hook_shadow_activate' ? 'shadow' : null;
|
|
213
|
+
if (activationMode === null)
|
|
203
214
|
continue;
|
|
204
215
|
const activationId = row.activation_id;
|
|
205
216
|
const artifactId = row.artifact_id;
|
|
@@ -220,9 +231,21 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
220
231
|
const contentJson = contentRow.content_json;
|
|
221
232
|
const returnedContentBytes = Buffer.byteLength(contentJson, 'utf8');
|
|
222
233
|
if (contentRow.content_bytes > ARTIFACT_CONTENT_BYTES || returnedContentBytes > ARTIFACT_CONTENT_BYTES) {
|
|
234
|
+
// CodeRabbit CR-3: a shadow row's envelope overrun must only skip
|
|
235
|
+
// the shadow observation — pre-PRI-813 this return was unreachable
|
|
236
|
+
// for shadow rows, and letting it fire now would suppress live
|
|
237
|
+
// enforcement (shadow may never influence the live outcome).
|
|
238
|
+
if (activationMode === 'shadow') {
|
|
239
|
+
addWarning(warnings, `artifact_content_budget_exceeded: activation=${activationId} bytes=${Math.max(contentRow.content_bytes, returnedContentBytes)} maximum=${ARTIFACT_CONTENT_BYTES}`, 'reduce the active shadow artifact envelope and reactivate the rule; live enforcement is unaffected');
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
223
242
|
return { decision: 'allow', source: event.source, warnings: [boundedWarning(`artifact_content_budget_exceeded: activation=${activationId} bytes=${Math.max(contentRow.content_bytes, returnedContentBytes)} maximum=${ARTIFACT_CONTENT_BYTES}`, 'reduce the active artifact envelope and reactivate the rule')], metadata: { evaluatedLiveRules: 0 } };
|
|
224
243
|
}
|
|
225
244
|
if (contentRow.content_bytes !== expectedContentBytes || returnedContentBytes !== contentRow.content_bytes) {
|
|
245
|
+
if (activationMode === 'shadow') {
|
|
246
|
+
addWarning(warnings, `artifact_content_size_changed: activation=${activationId}`, 'retry after the active shadow artifact update completes; live enforcement is unaffected');
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
226
249
|
return { decision: 'allow', source: event.source, warnings: [boundedWarning(`artifact_content_size_changed: activation=${activationId}`, 'retry after the active artifact update completes')], metadata: { evaluatedLiveRules: 0 } };
|
|
227
250
|
}
|
|
228
251
|
try {
|
|
@@ -233,6 +256,10 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
233
256
|
}
|
|
234
257
|
const sourceBytes = Buffer.byteLength(content.implementationCode, 'utf8');
|
|
235
258
|
if (sourceBytes > RULE_SOURCE_BYTES) {
|
|
259
|
+
if (activationMode === 'shadow') {
|
|
260
|
+
addWarning(warnings, `rule_source_budget_exceeded: activation=${activationId} bytes=${sourceBytes}`, `reduce the shadow RuleCode source below ${RULE_SOURCE_BYTES} bytes; live enforcement is unaffected`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
236
263
|
return { decision: 'allow', source: event.source, warnings: [boundedWarning(`rule_source_budget_exceeded: activation=${activationId} bytes=${sourceBytes}`, `reduce each RuleCode source below ${RULE_SOURCE_BYTES} bytes`)], metadata: { evaluatedLiveRules: 0 } };
|
|
237
264
|
}
|
|
238
265
|
if (Object.hasOwn(content, 'requiresContextVersion')) {
|
|
@@ -265,7 +292,7 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
265
292
|
continue;
|
|
266
293
|
}
|
|
267
294
|
const fallbackMeta = { name: activationId, version: '1', ruleId, coversCondition: 'all' };
|
|
268
|
-
candidates.push({ implId: activationId, ruleId, principleId, meta: isRuleMeta(content.meta) ? content.meta : fallbackMeta, source: content.implementationCode });
|
|
295
|
+
candidates.push({ implId: activationId, ruleId, principleId, meta: isRuleMeta(content.meta) ? content.meta : fallbackMeta, source: content.implementationCode, activationMode });
|
|
269
296
|
}
|
|
270
297
|
catch (error) {
|
|
271
298
|
addWarning(warnings, `implementation_unhealthy: ${error instanceof Error ? error.message : String(error)}`, 'fix the RuleCode and reactivate the rule');
|
|
@@ -280,27 +307,91 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
280
307
|
derived: { estimatedLineChanges: estimateLineChanges({ toolName: input.toolName, params: input.params }), bashRisk: enrichment.bashRisk },
|
|
281
308
|
...(context ? { context } : {}),
|
|
282
309
|
};
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
310
|
+
// CodeRabbit CR-3: live and shadow evaluate in SEPARATE batches so a
|
|
311
|
+
// shadow rule's timeout/budget failure can never suppress live
|
|
312
|
+
// enforcement (shadow observes; it must not influence the live
|
|
313
|
+
// outcome). Live keeps the exact pre-PRI-813 early-return semantics;
|
|
314
|
+
// every shadow-batch failure degrades to a warning and skips the
|
|
315
|
+
// shadow observation only.
|
|
316
|
+
const liveCandidates = candidates.filter((candidate) => candidate.activationMode === 'live');
|
|
317
|
+
const shadowCandidates = candidates.filter((candidate) => candidate.activationMode === 'shadow');
|
|
318
|
+
const liveBatchBytes = liveCandidates.reduce((sum, candidate) => sum + Buffer.byteLength(candidate.source, 'utf8'), 0);
|
|
319
|
+
if (liveBatchBytes > RULE_BATCH_SOURCE_BYTES) {
|
|
320
|
+
return { decision: 'allow', source: event.source, warnings: [boundedWarning(`rule_source_budget_exceeded: batchBytes=${liveBatchBytes}`, `reduce total active RuleCode below ${RULE_BATCH_SOURCE_BYTES} bytes`)], metadata: { evaluatedLiveRules: 0 } };
|
|
286
321
|
}
|
|
287
|
-
|
|
288
|
-
const remaining = remainingGateMs(startedAt);
|
|
322
|
+
let remaining = remainingGateMs(startedAt);
|
|
289
323
|
if (remaining <= 0) {
|
|
290
324
|
return { decision: 'allow', source: event.source, warnings: [boundedWarning('gate_deadline_exceeded', 'reduce active RuleCode count or source size and retry')], metadata: { evaluatedLiveRules: 0 } };
|
|
291
325
|
}
|
|
292
|
-
const
|
|
293
|
-
if (!
|
|
294
|
-
return { decision: 'allow', source: event.source, warnings: [boundedWarning(`${
|
|
326
|
+
const liveBatch = implementationRuntime.evaluateBatch(liveCandidates.map((candidate) => ({ source: candidate.source, filename: `activation-${candidate.implId}` })), hostInput, remaining);
|
|
327
|
+
if (!liveBatch.ok || !liveBatch.results) {
|
|
328
|
+
return { decision: 'allow', source: event.source, warnings: [boundedWarning(`${liveBatch.reason ?? 'rule_batch_failed'}: ${liveBatch.detail ?? 'unknown failure'}`, 'inspect active RuleCode resource use and repair or deactivate the unhealthy rule')], metadata: { evaluatedLiveRules: 0 } };
|
|
295
329
|
}
|
|
296
|
-
const timedOutChild =
|
|
330
|
+
const timedOutChild = liveBatch.results.find((candidate) => !candidate.ok && candidate.error?.includes('timed out'));
|
|
297
331
|
if (timedOutChild) {
|
|
298
332
|
return { decision: 'allow', source: event.source, warnings: [boundedWarning(`rule_batch_timeout: ${timedOutChild.error ?? 'unknown child timeout'}`, 'fix or deactivate the unhealthy RuleCode and retry')], metadata: { evaluatedLiveRules: 0 } };
|
|
299
333
|
}
|
|
334
|
+
// PRI-813: one canonical evaluation fact per shadow activation —
|
|
335
|
+
// observations only. Shadow results never join mergeDecisions, never
|
|
336
|
+
// block/modify/require approval: they record what a live rule WOULD
|
|
337
|
+
// have decided (ACTIVATION_CHANNELS §3.4), mirroring the legacy
|
|
338
|
+
// RuleHost report's shadowDecisions. Every failure mode below skips
|
|
339
|
+
// shadow observation without touching the live decision.
|
|
340
|
+
const shadowEvaluations = [];
|
|
341
|
+
if (shadowCandidates.length > 0) {
|
|
342
|
+
const shadowBatchBytes = shadowCandidates.reduce((sum, candidate) => sum + Buffer.byteLength(candidate.source, 'utf8'), 0);
|
|
343
|
+
remaining = remainingGateMs(startedAt);
|
|
344
|
+
if (shadowBatchBytes > RULE_BATCH_SOURCE_BYTES) {
|
|
345
|
+
addWarning(warnings, `rule_source_budget_exceeded: shadowBatchBytes=${shadowBatchBytes}`, `reduce total shadow RuleCode below ${RULE_BATCH_SOURCE_BYTES} bytes; shadow observation was skipped, live enforcement is unaffected`);
|
|
346
|
+
}
|
|
347
|
+
else if (remaining <= 0) {
|
|
348
|
+
addWarning(warnings, 'gate_deadline_exceeded', 'shadow observation was skipped after the live evaluation budget; live enforcement is unaffected');
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
const shadowBatch = implementationRuntime.evaluateBatch(shadowCandidates.map((candidate) => ({ source: candidate.source, filename: `activation-${candidate.implId}` })), hostInput, remaining);
|
|
352
|
+
const shadowTimedOut = shadowBatch.ok && shadowBatch.results
|
|
353
|
+
? shadowBatch.results.find((candidate) => !candidate.ok && candidate.error?.includes('timed out'))
|
|
354
|
+
: undefined;
|
|
355
|
+
if (!shadowBatch.ok || !shadowBatch.results) {
|
|
356
|
+
addWarning(warnings, `${shadowBatch.ok ? 'rule_batch_failed' : shadowBatch.reason ?? 'rule_batch_failed'}: ${shadowBatch.ok ? 'unknown failure' : shadowBatch.detail ?? 'unknown failure'}`, 'inspect active shadow RuleCode; the shadow observation was skipped, live enforcement is unaffected');
|
|
357
|
+
}
|
|
358
|
+
else if (shadowTimedOut) {
|
|
359
|
+
addWarning(warnings, `rule_batch_timeout: ${shadowTimedOut.error ?? 'unknown child timeout'}`, 'fix or deactivate the unhealthy shadow RuleCode; the shadow observation was skipped, live enforcement is unaffected');
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
for (let index = 0; index < shadowCandidates.length; index += 1) {
|
|
363
|
+
const candidate = shadowCandidates[index];
|
|
364
|
+
const batchResult = shadowBatch.results[index];
|
|
365
|
+
if (!candidate || !batchResult || !batchResult.ok) {
|
|
366
|
+
addWarning(warnings, `implementation_unhealthy: ${batchResult && !batchResult.ok ? batchResult.error ?? 'unknown child error' : 'rule_batch_result_missing'}`, 'fix the shadow RuleCode and reactivate the rule; live enforcement is unaffected');
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const validation = validateRuleHostResult(batchResult.result);
|
|
370
|
+
if (!isRuleResult(batchResult.result)) {
|
|
371
|
+
addWarning(warnings, `invalid RuleHostResult: ${validation.errors.join('; ')}`, 'fix the shadow RuleCode result and reactivate the rule; live enforcement is unaffected');
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const shadowResult = batchResult.result.matched
|
|
375
|
+
? { ...batchResult.result, ruleId: candidate.ruleId, principleId: candidate.principleId }
|
|
376
|
+
: batchResult.result;
|
|
377
|
+
shadowEvaluations.push({
|
|
378
|
+
toolName: input.toolName,
|
|
379
|
+
filePath: action.normalizedPath,
|
|
380
|
+
matched: shadowResult.matched,
|
|
381
|
+
decision: shadowResult.decision,
|
|
382
|
+
ruleId: candidate.ruleId,
|
|
383
|
+
activationId: candidate.implId,
|
|
384
|
+
activationMode: 'shadow',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
300
390
|
const implementations = [];
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
391
|
+
const liveEvaluatedResults = [];
|
|
392
|
+
for (let index = 0; index < liveCandidates.length; index += 1) {
|
|
393
|
+
const candidate = liveCandidates[index];
|
|
394
|
+
const batchResult = liveBatch.results[index];
|
|
304
395
|
if (!candidate || !batchResult) {
|
|
305
396
|
addWarning(warnings, 'rule_batch_result_missing', 'inspect the RuleCode runtime result contract');
|
|
306
397
|
continue;
|
|
@@ -318,18 +409,47 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
318
409
|
? { ...batchResult.result, ruleId: candidate.ruleId, principleId: candidate.principleId }
|
|
319
410
|
: batchResult.result;
|
|
320
411
|
implementations.push({ ...candidate, evaluate: () => validatedResult });
|
|
412
|
+
liveEvaluatedResults.push(validatedResult);
|
|
321
413
|
}
|
|
322
414
|
const result = mergeDecisions(implementations, hostInput, {
|
|
323
415
|
warn(message) { addWarning(warnings, message, 'inspect the unhealthy activation and RuleCode output'); },
|
|
324
416
|
});
|
|
417
|
+
// PRI-813 + CodeRabbit CR-4: attribute the winning live activation by
|
|
418
|
+
// REFERENCE IDENTITY — mergeDecisions returns the winning candidate's
|
|
419
|
+
// own result object for block/auto_correct, so indexOf is exact even
|
|
420
|
+
// when two live activations share a ruleId (the previous ruleId
|
|
421
|
+
// reverse-lookup could mis-attribute). Unattributable outcomes
|
|
422
|
+
// (requireApproval aggregate, no winner) carry no activationId.
|
|
423
|
+
const winnerIndex = result === undefined ? -1 : liveEvaluatedResults.indexOf(result);
|
|
424
|
+
const liveActivationId = winnerIndex >= 0 ? implementations[winnerIndex]?.implId : undefined;
|
|
425
|
+
// PRI-813: per-activation evaluation facts for host-side telemetry
|
|
426
|
+
// writers. HostEventResult.metadata is the designed channel for
|
|
427
|
+
// host-neutral evaluation facts; each entry maps losslessly onto the
|
|
428
|
+
// canonical RuleHostEvaluatedEventData contract.
|
|
429
|
+
const evaluations = [
|
|
430
|
+
{
|
|
431
|
+
toolName: input.toolName,
|
|
432
|
+
filePath: action.normalizedPath,
|
|
433
|
+
matched: result?.matched ?? false,
|
|
434
|
+
// CodeRabbit CR-5 / PRI-567: an EMPTY live set is 'no_rules_armed',
|
|
435
|
+
// not a live 'allow' — enforcement statistics must not read as if a
|
|
436
|
+
// live rule evaluated (the legacy writer already distinguishes
|
|
437
|
+
// this; the shared path now aligns).
|
|
438
|
+
decision: liveCandidates.length === 0 ? 'no_rules_armed' : result?.decision ?? 'allow',
|
|
439
|
+
...(result?.ruleId !== undefined ? { ruleId: result.ruleId } : {}),
|
|
440
|
+
...(liveActivationId !== undefined ? { activationId: liveActivationId } : {}),
|
|
441
|
+
activationMode: 'live',
|
|
442
|
+
},
|
|
443
|
+
...shadowEvaluations,
|
|
444
|
+
];
|
|
325
445
|
if (result?.decision === 'block') {
|
|
326
446
|
if (result.reason.trim().length === 0) {
|
|
327
447
|
addWarning(warnings, 'deny_reason_missing', 'fix the RuleCode to return a non-empty block reason');
|
|
328
|
-
return { decision: 'allow', source: event.source, warnings, metadata: { evaluatedLiveRules: implementations.length } };
|
|
448
|
+
return { decision: 'allow', source: event.source, warnings, metadata: { evaluatedLiveRules: implementations.length, evaluations } };
|
|
329
449
|
}
|
|
330
|
-
return { decision: 'deny', reason: result.reason, source: event.source, ...(warnings.length ? { warnings } : {}), metadata: { evaluatedLiveRules: implementations.length, ruleId: result.ruleId, principleId: result.principleId } };
|
|
450
|
+
return { decision: 'deny', reason: result.reason, source: event.source, ...(warnings.length ? { warnings } : {}), metadata: { evaluatedLiveRules: implementations.length, ruleId: result.ruleId, principleId: result.principleId, evaluations } };
|
|
331
451
|
}
|
|
332
|
-
return { decision: 'allow', source: event.source, ...(warnings.length ? { warnings } : {}), metadata: { evaluatedLiveRules: implementations.length, ruleDecision: result?.decision ?? 'allow' } };
|
|
452
|
+
return { decision: 'allow', source: event.source, ...(warnings.length ? { warnings } : {}), metadata: { evaluatedLiveRules: implementations.length, ruleDecision: result?.decision ?? 'allow', evaluations } };
|
|
333
453
|
}
|
|
334
454
|
catch (error) {
|
|
335
455
|
addWarning(warnings, `activation_read_failed: ${error instanceof Error ? error.message : String(error)}`, 'inspect state.db schema and integrity');
|
|
@@ -15,13 +15,16 @@ import { StoreEventEmitter } from '@principles/core/runtime-v2';
|
|
|
15
15
|
* (ERR-092)。正确粒度是 **workspace-scoped**:本 emitter 在 consumer
|
|
16
16
|
* cycle 的 per-wake 装配处构造,只经手本 workspace runner 发出的事件。
|
|
17
17
|
*
|
|
18
|
-
* 持久化策略:**allowlist**,只落盘
|
|
18
|
+
* 持久化策略:**allowlist**,只落盘 critical events(与 telemetry-event
|
|
19
19
|
* schema 枚举同源),不做全量 telemetry 无差别写盘(日志量 + 隐私审计面
|
|
20
20
|
* 无谓扩大):
|
|
21
21
|
* - evaluator_adversarial_replay_skipped
|
|
22
22
|
* - evaluator_adversarial_replay
|
|
23
23
|
* - evaluator_rule_assembled
|
|
24
24
|
* - evaluator_rule_assembly_failed
|
|
25
|
+
* - artificer_l2_complete(PRI-795 review P1:Artificer L2 的 completion
|
|
26
|
+
* 证据 — abortOwner/budgetMs/elapsedMs/stopReason/tokenUsage — 必须
|
|
27
|
+
* crash 前落盘,否则 EP002-R3 的「失败后什么都查不到」重演)
|
|
25
28
|
*
|
|
26
29
|
* 落点 `<workspaceDir>/.pd/telemetry/critical-events.jsonl`(JSONL,一行一
|
|
27
30
|
* 事件,含完整 TelemetryEvent)。同步 append:事件量小(allowlist 限流),
|
|
@@ -34,6 +37,7 @@ const CRITICAL_EVENT_ALLOWLIST = new Set([
|
|
|
34
37
|
'evaluator_adversarial_replay',
|
|
35
38
|
'evaluator_rule_assembled',
|
|
36
39
|
'evaluator_rule_assembly_failed',
|
|
40
|
+
'artificer_l2_complete',
|
|
37
41
|
]);
|
|
38
42
|
export class WorkspaceTelemetryEmitter extends StoreEventEmitter {
|
|
39
43
|
upstream;
|