create-principles-disciple 1.118.1 → 1.119.0

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.
Files changed (133) hide show
  1. package/console/dist/server/index.js +17 -1
  2. package/console/dist/server/models/ActivationsConsoleModel.d.ts +77 -0
  3. package/console/dist/server/models/ActivationsConsoleModel.js +322 -4
  4. package/console/dist/server/models/ReceiptsConsoleModel.d.ts +42 -0
  5. package/console/dist/server/models/ReceiptsConsoleModel.js +160 -0
  6. package/console/dist/server/routes/activations.d.ts +6 -1
  7. package/console/dist/server/routes/activations.js +142 -1
  8. package/console/dist/server/routes/receipts.d.ts +8 -0
  9. package/console/dist/server/routes/receipts.js +63 -0
  10. package/console/dist/ui/App.js +6 -3
  11. package/console/dist/ui/api.d.ts +40 -3
  12. package/console/dist/ui/api.js +94 -3
  13. package/console/dist/ui/i18n/en.json +75 -0
  14. package/console/dist/ui/i18n/zh-CN.json +75 -0
  15. package/console/dist/ui/pages/activation/ActivationPage.js +114 -5
  16. package/console/dist/ui/pages/activation/ActivationValidators.js +3 -0
  17. package/console/dist/ui/pages/focus/FocusPage.js +16 -3
  18. package/console/dist/ui/pages/principles/PrincipleDetailPage.js +17 -3
  19. package/console/dist/ui/utils/validators.d.ts +100 -0
  20. package/console/dist/ui/utils/validators.js +133 -0
  21. package/console/dist/web/assets/app.css +34 -0
  22. package/console/dist/web/assets/app.js +704 -14
  23. package/console/package.json +2 -0
  24. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +8 -2
  25. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
  26. package/core/dist/runtime-v2/activation/__tests__/openclaw-promotion-checks.test.d.ts +2 -0
  27. package/core/dist/runtime-v2/activation/__tests__/openclaw-promotion-checks.test.d.ts.map +1 -0
  28. package/core/dist/runtime-v2/activation/__tests__/openclaw-promotion-checks.test.js +91 -0
  29. package/core/dist/runtime-v2/activation/__tests__/openclaw-promotion-checks.test.js.map +1 -0
  30. package/core/dist/runtime-v2/activation/__tests__/promotion-readiness-evaluator.test.d.ts +2 -0
  31. package/core/dist/runtime-v2/activation/__tests__/promotion-readiness-evaluator.test.d.ts.map +1 -0
  32. package/core/dist/runtime-v2/activation/__tests__/promotion-readiness-evaluator.test.js +83 -0
  33. package/core/dist/runtime-v2/activation/__tests__/promotion-readiness-evaluator.test.js.map +1 -0
  34. package/core/dist/runtime-v2/activation/__tests__/promotion-readiness-reader.test.d.ts +2 -0
  35. package/core/dist/runtime-v2/activation/__tests__/promotion-readiness-reader.test.d.ts.map +1 -0
  36. package/core/dist/runtime-v2/activation/__tests__/promotion-readiness-reader.test.js +48 -0
  37. package/core/dist/runtime-v2/activation/__tests__/promotion-readiness-reader.test.js.map +1 -0
  38. package/core/dist/runtime-v2/activation/__tests__/rulecode-owner-decision-service.test.d.ts +2 -0
  39. package/core/dist/runtime-v2/activation/__tests__/rulecode-owner-decision-service.test.d.ts.map +1 -0
  40. package/core/dist/runtime-v2/activation/__tests__/rulecode-owner-decision-service.test.js +141 -0
  41. package/core/dist/runtime-v2/activation/__tests__/rulecode-owner-decision-service.test.js.map +1 -0
  42. package/core/dist/runtime-v2/activation/__tests__/rulecode-safety-circuit-breaker.test.d.ts +2 -0
  43. package/core/dist/runtime-v2/activation/__tests__/rulecode-safety-circuit-breaker.test.d.ts.map +1 -0
  44. package/core/dist/runtime-v2/activation/__tests__/rulecode-safety-circuit-breaker.test.js +31 -0
  45. package/core/dist/runtime-v2/activation/__tests__/rulecode-safety-circuit-breaker.test.js.map +1 -0
  46. package/core/dist/runtime-v2/activation/__tests__/sqlite-activation-safety-store.test.d.ts +2 -0
  47. package/core/dist/runtime-v2/activation/__tests__/sqlite-activation-safety-store.test.d.ts.map +1 -0
  48. package/core/dist/runtime-v2/activation/__tests__/sqlite-activation-safety-store.test.js +311 -0
  49. package/core/dist/runtime-v2/activation/__tests__/sqlite-activation-safety-store.test.js.map +1 -0
  50. package/core/dist/runtime-v2/activation/activation-control-types.d.ts +61 -0
  51. package/core/dist/runtime-v2/activation/activation-control-types.d.ts.map +1 -0
  52. package/core/dist/runtime-v2/activation/activation-control-types.js +2 -0
  53. package/core/dist/runtime-v2/activation/activation-control-types.js.map +1 -0
  54. package/core/dist/runtime-v2/activation/index.d.ts +14 -0
  55. package/core/dist/runtime-v2/activation/index.d.ts.map +1 -1
  56. package/core/dist/runtime-v2/activation/index.js +7 -0
  57. package/core/dist/runtime-v2/activation/index.js.map +1 -1
  58. package/core/dist/runtime-v2/activation/openclaw-promotion-checks.d.ts +30 -0
  59. package/core/dist/runtime-v2/activation/openclaw-promotion-checks.d.ts.map +1 -0
  60. package/core/dist/runtime-v2/activation/openclaw-promotion-checks.js +124 -0
  61. package/core/dist/runtime-v2/activation/openclaw-promotion-checks.js.map +1 -0
  62. package/core/dist/runtime-v2/activation/promotion-readiness-evaluator.d.ts +17 -0
  63. package/core/dist/runtime-v2/activation/promotion-readiness-evaluator.d.ts.map +1 -0
  64. package/core/dist/runtime-v2/activation/promotion-readiness-evaluator.js +55 -0
  65. package/core/dist/runtime-v2/activation/promotion-readiness-evaluator.js.map +1 -0
  66. package/core/dist/runtime-v2/activation/promotion-readiness-reader.d.ts +21 -0
  67. package/core/dist/runtime-v2/activation/promotion-readiness-reader.d.ts.map +1 -0
  68. package/core/dist/runtime-v2/activation/promotion-readiness-reader.js +44 -0
  69. package/core/dist/runtime-v2/activation/promotion-readiness-reader.js.map +1 -0
  70. package/core/dist/runtime-v2/activation/rulecode-owner-decision-service.d.ts +104 -0
  71. package/core/dist/runtime-v2/activation/rulecode-owner-decision-service.d.ts.map +1 -0
  72. package/core/dist/runtime-v2/activation/rulecode-owner-decision-service.js +86 -0
  73. package/core/dist/runtime-v2/activation/rulecode-owner-decision-service.js.map +1 -0
  74. package/core/dist/runtime-v2/activation/rulecode-safety-circuit-breaker.d.ts +25 -0
  75. package/core/dist/runtime-v2/activation/rulecode-safety-circuit-breaker.d.ts.map +1 -0
  76. package/core/dist/runtime-v2/activation/rulecode-safety-circuit-breaker.js +23 -0
  77. package/core/dist/runtime-v2/activation/rulecode-safety-circuit-breaker.js.map +1 -0
  78. package/core/dist/runtime-v2/activation/rulecode-shadow-summary.d.ts +14 -0
  79. package/core/dist/runtime-v2/activation/rulecode-shadow-summary.d.ts.map +1 -0
  80. package/core/dist/runtime-v2/activation/rulecode-shadow-summary.js +57 -0
  81. package/core/dist/runtime-v2/activation/rulecode-shadow-summary.js.map +1 -0
  82. package/core/dist/runtime-v2/activation/sqlite-activation-safety-store.d.ts +40 -0
  83. package/core/dist/runtime-v2/activation/sqlite-activation-safety-store.d.ts.map +1 -0
  84. package/core/dist/runtime-v2/activation/sqlite-activation-safety-store.js +461 -0
  85. package/core/dist/runtime-v2/activation/sqlite-activation-safety-store.js.map +1 -0
  86. package/core/dist/runtime-v2/activation/sqlite-activation-state-store.d.ts.map +1 -1
  87. package/core/dist/runtime-v2/activation/sqlite-activation-state-store.js +24 -6
  88. package/core/dist/runtime-v2/activation/sqlite-activation-state-store.js.map +1 -1
  89. package/core/dist/runtime-v2/activation/writers/__tests__/rule-host-writer.test.js +19 -4
  90. package/core/dist/runtime-v2/activation/writers/__tests__/rule-host-writer.test.js.map +1 -1
  91. package/core/dist/runtime-v2/activation/writers/rule-host-writer.d.ts.map +1 -1
  92. package/core/dist/runtime-v2/activation/writers/rule-host-writer.js +10 -0
  93. package/core/dist/runtime-v2/activation/writers/rule-host-writer.js.map +1 -1
  94. package/core/dist/runtime-v2/config/__tests__/pd-config-contract.test.js +13 -0
  95. package/core/dist/runtime-v2/config/__tests__/pd-config-contract.test.js.map +1 -1
  96. package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js +15 -2
  97. package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js.map +1 -1
  98. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
  99. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +2 -0
  100. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  101. package/core/dist/runtime-v2/index.d.ts +2 -2
  102. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  103. package/core/dist/runtime-v2/index.js +1 -1
  104. package/core/dist/runtime-v2/index.js.map +1 -1
  105. package/core/dist/runtime-v2/internalization/__tests__/legacy-rule-contract-scanner.test.js +20 -1
  106. package/core/dist/runtime-v2/internalization/__tests__/legacy-rule-contract-scanner.test.js.map +1 -1
  107. package/core/dist/runtime-v2/internalization/legacy-rule-contract-scanner.d.ts +4 -4
  108. package/core/dist/runtime-v2/internalization/legacy-rule-contract-scanner.d.ts.map +1 -1
  109. package/core/dist/runtime-v2/internalization/legacy-rule-contract-scanner.js +92 -5
  110. package/core/dist/runtime-v2/internalization/legacy-rule-contract-scanner.js.map +1 -1
  111. package/core/dist/runtime-v2/store/sqlite-connection-schema-version.test.js +11 -11
  112. package/core/dist/runtime-v2/store/sqlite-connection-schema-version.test.js.map +1 -1
  113. package/core/dist/runtime-v2/store/sqlite-connection.d.ts.map +1 -1
  114. package/core/dist/runtime-v2/store/sqlite-connection.js +111 -0
  115. package/core/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
  116. package/host-runtime/dist/host-liveness-contract.d.ts +3 -0
  117. package/host-runtime/dist/host-liveness-contract.js +18 -0
  118. package/host-runtime/dist/index.d.ts +1 -0
  119. package/host-runtime/dist/index.js +1 -0
  120. package/host-runtime/dist/legacy-rule-contract-symbols.js +88 -1
  121. package/host-runtime/dist/production-rulehost-gate.js +33 -16
  122. package/package.json +2 -2
  123. package/pd-cli/dist/commands/__tests__/runtime-activation-nextaction.test.js +3 -3
  124. package/pd-cli/dist/commands/__tests__/runtime-activation-nextaction.test.js.map +1 -1
  125. package/pd-cli/dist/commands/runtime-activation.d.ts +15 -3
  126. package/pd-cli/dist/commands/runtime-activation.d.ts.map +1 -1
  127. package/pd-cli/dist/commands/runtime-activation.js +160 -49
  128. package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
  129. package/plugin/dist/bundle.js +730 -565
  130. package/plugin/dist/core/rule-host.d.ts +0 -19
  131. package/plugin/dist/core/rulecode-safety-circuit.d.ts +13 -0
  132. package/plugin/openclaw.plugin.json +1 -1
  133. package/plugin/package.json +1 -1
@@ -13,6 +13,7 @@ import { handleHealthRoute, disposeHealthModels } from './routes/health.js';
13
13
  import { handlePrinciplesRoute, disposePrinciplesModels } from './routes/principles.js';
14
14
  import { handleLifecycleRoute, disposeLifecycleModels } from './routes/lifecycle.js';
15
15
  import { handleActivationsRoute, disposeActivationsModels } from './routes/activations.js';
16
+ import { handleReceiptsRoute, disposeReceiptsModels } from './routes/receipts.js';
16
17
  import { handleApprovalsGroupedRoute, disposeApprovalsGroupedModels } from './routes/approvals-grouped.js';
17
18
  import { handleGovernanceRoute, disposeGovernanceModels } from './routes/governance.js';
18
19
  import { handleEvidenceChainRoute, disposeEvidenceChainModels } from './routes/evidence-chain.js';
@@ -239,6 +240,7 @@ async function initServices(workspaceDir, authConfig) {
239
240
  async function closeServices() {
240
241
  disposeFeedbackReportModels();
241
242
  disposeFailedTasksModels();
243
+ disposeReceiptsModels();
242
244
  disposeApprovalsModels();
243
245
  disposeApprovalsGroupedModels();
244
246
  disposeHealthModels();
@@ -307,6 +309,12 @@ function handleRequest(services) {
307
309
  asyncHandler(() => handleFailedTasksRoute(req, res, { workspaceDir: services.workspaceDir, subPath, featureFlags: services.feedbackFlags }))(req, res);
308
310
  return;
309
311
  }
312
+ // PRI-533: GET /api/v1/receipts/counts, /api/v1/receipts/principles/:id
313
+ if (urlPath === '/api/v1/receipts' || urlPath.startsWith('/api/v1/receipts/')) {
314
+ const subPath = urlPath.slice('/api/v1/receipts'.length);
315
+ asyncHandler(() => handleReceiptsRoute(req, res, services.workspaceDir, subPath))(req, res);
316
+ return;
317
+ }
310
318
  // GET /api/v1/approvals/grouped (MUST be before approvals catch-all)
311
319
  if (urlPath === '/api/v1/approvals/grouped') {
312
320
  asyncHandler(() => handleApprovalsGroupedRoute(req, res, services.workspaceDir))(req, res);
@@ -368,7 +376,15 @@ function handleRequest(services) {
368
376
  // CR8: GET /api/v1/activations
369
377
  if (urlPath === '/api/v1/activations' || urlPath.startsWith('/api/v1/activations/')) {
370
378
  const subPath = urlPath.slice('/api/v1/activations'.length);
371
- asyncHandler(() => handleActivationsRoute(req, res, services.workspaceDir, subPath))(req, res);
379
+ const ownerId = process.env.PD_OWNER_ID?.trim();
380
+ const credentialId = process.env.PD_OWNER_CREDENTIAL_ID?.trim();
381
+ const ownerActor = services.authConfig.isEnabled() && ownerId && credentialId
382
+ ? { principal: { kind: 'configured_owner', ownerId }, authentication: { method: 'console_token', credentialId } }
383
+ : null;
384
+ asyncHandler(() => handleActivationsRoute(req, res, services.workspaceDir, subPath, {
385
+ ownerActor,
386
+ breakGlassActor: { principal: { kind: 'break_glass', reason: 'local_no_auth_emergency' }, authentication: { method: 'local_break_glass' } },
387
+ }))(req, res);
372
388
  return;
373
389
  }
374
390
  // CR8: GET /api/v1/governance/queue
@@ -1,3 +1,4 @@
1
+ import type { ActivationStatusRecord, PromotionReadinessResult, ActivationControlState, ActivationDecisionRecord, GlobalRuleCodePause, OwnerPromotionActor, OwnerPromotionResult } from '@principles/core/runtime-v2';
1
2
  /**
2
3
  * PRI-491 — owner-observable activation record.
3
4
  *
@@ -43,6 +44,9 @@ export interface ActivationRecord {
43
44
  nextAction?: string;
44
45
  /** Present when the activation references a non-existent artifact. */
45
46
  warning?: string;
47
+ enforcement?: 'eligible' | 'safety_isolated';
48
+ legacyDecisionUnknown?: boolean;
49
+ ownerReviewDueAt?: string;
46
50
  }
47
51
  export interface ActivationsResponse {
48
52
  activations: ActivationRecord[];
@@ -53,10 +57,82 @@ export interface ActivationsResponse {
53
57
  /** Present when status is 'degraded' — next operator action. */
54
58
  nextAction?: string;
55
59
  }
60
+ export interface RuleCodeOwnerReview {
61
+ activation: ActivationStatusRecord;
62
+ artifact: {
63
+ artifactId: string;
64
+ digest: string;
65
+ sourceTaskId: string;
66
+ lineageArtifactIds: string[];
67
+ content: Record<string, unknown> | null;
68
+ };
69
+ readiness: PromotionReadinessResult;
70
+ controlState: ActivationControlState | null;
71
+ decisions: ActivationDecisionRecord[];
72
+ globalPause: GlobalRuleCodePause | null;
73
+ ownerDecisionEnabled: boolean;
74
+ runtimeCapability: {
75
+ hostRuntimeVersion: string;
76
+ shadowEvidence: boolean;
77
+ };
78
+ liveMetrics: RuleCodeTelemetryMetrics;
79
+ behaviorDrift: {
80
+ approvedBlockRate: number | null;
81
+ liveBlockRate: number | null;
82
+ delta: number | null;
83
+ };
84
+ }
85
+ interface RuleCodeTelemetryWindow {
86
+ eligible: number | null;
87
+ matched: number | null;
88
+ blocked: number | null;
89
+ unhealthy: number | null;
90
+ circuitTrips: number;
91
+ toolDistribution: Record<string, number> | null;
92
+ }
93
+ interface RuleCodeTelemetryMetrics {
94
+ last24Hours: RuleCodeTelemetryWindow;
95
+ last7Days: RuleCodeTelemetryWindow;
96
+ representativeSamples: {
97
+ toolName: string;
98
+ decision: string;
99
+ pathCategory: string;
100
+ }[];
101
+ }
102
+ export interface OwnerMutationInput {
103
+ actor: OwnerPromotionActor;
104
+ idempotencyKey: string;
105
+ reasonCode: string;
106
+ note?: string;
107
+ }
56
108
  export declare class ActivationsConsoleModel {
57
109
  private readonly workspaceDir;
58
110
  constructor(workspaceDir: string);
59
111
  getActivations(): Promise<ActivationsResponse>;
112
+ getOwnerReview(activationId: string, ownerIdentityConfigured?: boolean): Promise<RuleCodeOwnerReview>;
113
+ continueObserving(activationId: string, input: OwnerMutationInput): Promise<{
114
+ decisionId: string;
115
+ }>;
116
+ deactivateRuleCode(activationId: string, decision: 'reject_after_shadow' | 'emergency_deactivate', input: OwnerMutationInput): Promise<{
117
+ activationId: string;
118
+ decisionId: string;
119
+ deactivatedAt: string;
120
+ }>;
121
+ recoverRuleCodeToShadow(activationId: string, expectedControlVersion: number, input: OwnerMutationInput): Promise<{
122
+ sourceActivationId: string;
123
+ shadowActivationId: string;
124
+ decisionId: string;
125
+ }>;
126
+ pauseAllRuleCode(input: OwnerMutationInput): Promise<GlobalRuleCodePause>;
127
+ releaseRuleCodePause(pauseId: string, expectedVersion: number, input: OwnerMutationInput): Promise<GlobalRuleCodePause>;
128
+ promoteRuleCode(activationId: string, expected: {
129
+ artifactId: string;
130
+ artifactDigest: string;
131
+ controlVersion: number;
132
+ confirmed: boolean;
133
+ }, input: OwnerMutationInput): Promise<OwnerPromotionResult>;
134
+ private withMutableRuleCode;
135
+ private requireOwnerDecisionFeature;
60
136
  deactivateActivation(activationId: string): Promise<{
61
137
  ok: true;
62
138
  } | {
@@ -66,3 +142,4 @@ export declare class ActivationsConsoleModel {
66
142
  }>;
67
143
  dispose(): void;
68
144
  }
145
+ export {};
@@ -1,7 +1,9 @@
1
- import { SqliteConnection, SqliteActivationStateStore, SqlitePIArtifactStore, extractPrincipleId, extractEvidenceRefs, } from '@principles/core/runtime-v2';
1
+ import { SqliteConnection, SqliteActivationStateStore, SqlitePIArtifactStore, extractPrincipleId, extractEvidenceRefs, PromotionReadinessReader, RuleHostWriter, createProductionGateDeps, SqliteActivationSafetyStore, isFeatureEnabled, RuleCodeOwnerDecisionService, collectOpenClawPromotionChecks, summarizeRuleCodeShadowEvents, } from '@principles/core/runtime-v2';
2
+ import { OPENCLAW_HOST_LIVENESS_CONTRACT } from '@principles/host-runtime';
2
3
  import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
3
4
  import * as fs from 'node:fs';
4
5
  import * as path from 'node:path';
6
+ import { createHash, randomUUID } from 'node:crypto';
5
7
  /**
6
8
  * Type guard for parsed JSON objects (rc-2-no-as-bypass).
7
9
  * Replaces `as Record<string, unknown>` casts on untrusted contentJson.
@@ -34,6 +36,80 @@ function toSnapshot(record) {
34
36
  updatedAt: record.updatedAt,
35
37
  };
36
38
  }
39
+ function makeActivationDecision(value) {
40
+ const { activation, artifactDigest, decision, input } = value;
41
+ return {
42
+ decisionId: `decision-${randomUUID()}`,
43
+ subject: { kind: 'activation', activationId: activation.activationId, artifactId: activation.artifactId, artifactDigest },
44
+ decision, principal: input.actor.principal, authentication: input.actor.authentication,
45
+ ...(input.actor.operator ? { operator: input.actor.operator } : {}), reasonCode: input.reasonCode,
46
+ note: input.note?.trim() || null, evidenceSnapshotId: null, decidedAt: new Date().toISOString(),
47
+ };
48
+ }
49
+ function unavailableShadowSummary() {
50
+ return {
51
+ observed: null, matched: null, wouldBlock: null, wouldAllow: null,
52
+ requireApproval: null, autoCorrect: null, errors: null, neutralControl: null,
53
+ firstObservedAt: null, lastObservedAt: null,
54
+ };
55
+ }
56
+ function readRuleCodeTelemetry(workspaceDir, activationId, decisions) {
57
+ const logsDir = path.join(workspaceDir, '.pd', 'logs');
58
+ const countCircuitTrips = (minimumTime) => decisions.filter(decision => decision.decision === 'safety_isolate' && Date.parse(decision.decidedAt) >= minimumTime).length;
59
+ const now = Date.now();
60
+ const unavailableWindow = (minimumTime) => ({ eligible: null, matched: null, blocked: null, unhealthy: null, circuitTrips: countCircuitTrips(minimumTime), toolDistribution: null });
61
+ if (!fs.existsSync(logsDir))
62
+ return { shadowSummary: unavailableShadowSummary(), metrics: { last24Hours: unavailableWindow(now - 24 * 60 * 60 * 1000), last7Days: unavailableWindow(now - 7 * 24 * 60 * 60 * 1000), representativeSamples: [] } };
63
+ const entries = [];
64
+ try {
65
+ for (const file of fs.readdirSync(logsDir).filter(name => /^events_.*\.jsonl$/.test(name)).sort().slice(-7)) {
66
+ for (const line of fs.readFileSync(path.join(logsDir, file), 'utf8').split('\n').filter(Boolean)) {
67
+ try {
68
+ entries.push(JSON.parse(line));
69
+ }
70
+ catch { /* malformed telemetry is excluded, readiness remains bounded */ }
71
+ }
72
+ }
73
+ }
74
+ catch {
75
+ return { shadowSummary: unavailableShadowSummary(), metrics: { last24Hours: unavailableWindow(now - 24 * 60 * 60 * 1000), last7Days: unavailableWindow(now - 7 * 24 * 60 * 60 * 1000), representativeSamples: [] } };
76
+ }
77
+ const summarizeWindow = (minimumTime) => {
78
+ let eligible = 0;
79
+ let matched = 0;
80
+ let blocked = 0;
81
+ let unhealthy = 0;
82
+ const tools = {};
83
+ for (const entry of entries) {
84
+ if (!isRecord(entry) || typeof entry.ts !== 'string' || Date.parse(entry.ts) < minimumTime || !isRecord(entry.data) || entry.data.activationId !== activationId)
85
+ continue;
86
+ if (entry.type === 'rulehost_unhealthy') {
87
+ unhealthy += 1;
88
+ continue;
89
+ }
90
+ if (entry.type !== 'rulehost_evaluated' || entry.data.activationMode !== 'live')
91
+ continue;
92
+ eligible += 1;
93
+ if (entry.data.matched === true)
94
+ matched += 1;
95
+ if (entry.data.decision === 'block')
96
+ blocked += 1;
97
+ if (typeof entry.data.toolName === 'string')
98
+ tools[entry.data.toolName] = (tools[entry.data.toolName] ?? 0) + 1;
99
+ }
100
+ return { eligible, matched, blocked, unhealthy, circuitTrips: countCircuitTrips(minimumTime), toolDistribution: tools };
101
+ };
102
+ const representativeSamples = [];
103
+ for (const entry of entries) {
104
+ if (representativeSamples.length >= 6)
105
+ break;
106
+ if (!isRecord(entry) || entry.type !== 'rulehost_evaluated' || !isRecord(entry.data) || entry.data.activationId !== activationId || typeof entry.data.toolName !== 'string' || typeof entry.data.decision !== 'string')
107
+ continue;
108
+ const filePath = typeof entry.data.filePath === 'string' ? entry.data.filePath : '';
109
+ representativeSamples.push({ toolName: entry.data.toolName, decision: entry.data.decision, pathCategory: path.extname(filePath) || (filePath ? 'workspace_path' : 'no_path') });
110
+ }
111
+ return { shadowSummary: summarizeRuleCodeShadowEvents(entries, activationId), metrics: { last24Hours: summarizeWindow(now - 24 * 60 * 60 * 1000), last7Days: summarizeWindow(now - 7 * 24 * 60 * 60 * 1000), representativeSamples } };
112
+ }
37
113
  export class ActivationsConsoleModel {
38
114
  workspaceDir;
39
115
  constructor(workspaceDir) {
@@ -119,6 +195,17 @@ export class ActivationsConsoleModel {
119
195
  // suspended (not executing) even though they remain active in the DB.
120
196
  const featureFlags = computeFlagsFromLoadResult(loadPdConfig(this.workspaceDir));
121
197
  const v2FlagEnabled = featureFlags.flags.rulecode_context_v2?.enabled === true;
198
+ const safetyStore = new SqliteActivationSafetyStore(conn);
199
+ const controlByActivation = new Map();
200
+ const hasPromotionDecision = new Set();
201
+ for (const activation of allActivations) {
202
+ const control = await safetyStore.getControlState(activation.activationId);
203
+ if (control)
204
+ controlByActivation.set(activation.activationId, control);
205
+ const decisions = await safetyStore.listDecisions(activation.activationId);
206
+ if (decisions.some(decision => decision.decision === 'promote_live'))
207
+ hasPromotionDecision.add(activation.activationId);
208
+ }
122
209
  const facts = allActivations.map((record) => {
123
210
  const meta = artifactMetadata.get(record.artifactId);
124
211
  const contextVersion = meta?.contextVersion;
@@ -141,15 +228,15 @@ export class ActivationsConsoleModel {
141
228
  }
142
229
  else if (contextVersion === 'v2' && !v2FlagEnabled) {
143
230
  status = 'suspended_by_flag';
144
- nextAction = `Enable rulecode_context_v2 flag or deactivate: pd activation deactivate --activation-id ${record.activationId} --confirm`;
231
+ nextAction = `Enable rulecode_context_v2 flag or deactivate: pd activation deactivate --activation-id ${record.activationId}`;
145
232
  }
146
233
  else {
147
234
  status = 'active';
148
235
  if (mode === 'shadow') {
149
- nextAction = `pd activation promote --activation-id ${record.activationId} --confirm`;
236
+ nextAction = 'Keep shadow; promotion requires an authenticated Owner decision, immutable evidence bindings, and a passing Promotion Readiness result.';
150
237
  }
151
238
  else if (mode === 'live') {
152
- nextAction = `pd activation deactivate --activation-id ${record.activationId} --confirm`;
239
+ nextAction = `pd activation deactivate --activation-id ${record.activationId}`;
153
240
  }
154
241
  else {
155
242
  nextAction = undefined;
@@ -171,6 +258,11 @@ export class ActivationsConsoleModel {
171
258
  evidenceRefs,
172
259
  evidenceSummary,
173
260
  nextAction,
261
+ enforcement: controlByActivation.get(record.activationId)?.enforcement,
262
+ ...(mode === 'live' && !hasPromotionDecision.has(record.activationId) ? {
263
+ legacyDecisionUnknown: true,
264
+ ownerReviewDueAt: new Date(new Date(record.promotedAt ?? record.activatedAt ?? Date.now()).getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
265
+ } : {}),
174
266
  };
175
267
  if (danglingArtifactIds.has(record.artifactId)) {
176
268
  enriched.warning = `artifact_id "${record.artifactId}" does not exist in pi_artifacts - activation is orphaned`;
@@ -198,6 +290,232 @@ export class ActivationsConsoleModel {
198
290
  catch { /* best-effort */ }
199
291
  }
200
292
  }
293
+ async getOwnerReview(activationId, ownerIdentityConfigured = false) {
294
+ const conn = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: true });
295
+ try {
296
+ const activationStore = new SqliteActivationStateStore(conn);
297
+ const artifactStore = new SqlitePIArtifactStore(conn);
298
+ const safetyStore = new SqliteActivationSafetyStore(conn);
299
+ const codeActivations = await activationStore.listCodeToolHookActivations(true);
300
+ const matches = codeActivations.filter(record => record.activationId === activationId);
301
+ if (matches.length !== 1)
302
+ throw new Error(`Owner review requires exactly one RuleCode activation: ${activationId}`);
303
+ const [activation] = matches;
304
+ if (!activation)
305
+ throw new Error(`Activation not found: ${activationId}`);
306
+ const artifact = await artifactStore.getArtifactById(activation.artifactId);
307
+ if (!artifact)
308
+ throw new Error(`Artifact not found for activation: ${activation.artifactId}`);
309
+ const digest = `sha256:${createHash('sha256').update(JSON.stringify(artifact), 'utf8').digest('hex')}`;
310
+ const decisions = await safetyStore.listDecisions(activationId);
311
+ const telemetry = readRuleCodeTelemetry(this.workspaceDir, activationId, decisions);
312
+ const flags = computeFlagsFromLoadResult(loadPdConfig(this.workspaceDir));
313
+ const writer = new RuleHostWriter({ gateDeps: createProductionGateDeps(), featureFlagProbe: id => isFeatureEnabled(flags, id) });
314
+ const reader = new PromotionReadinessReader({
315
+ listCodeToolHookActivations: () => activationStore.listCodeToolHookActivations(true),
316
+ getArtifactById: id => artifactStore.getArtifactById(id),
317
+ computeArtifactDigest: value => `sha256:${createHash('sha256').update(JSON.stringify(value), 'utf8').digest('hex')}`,
318
+ validateProductionArtifact: value => writer.canActivate(value),
319
+ collectHostChecks: async (value) => {
320
+ const liveArtifacts = [];
321
+ for (const active of codeActivations) {
322
+ if (active.action !== 'code_tool_hook_live_activate' || active.deactivatedAt !== null)
323
+ continue;
324
+ const liveArtifact = await artifactStore.getArtifactById(active.artifactId);
325
+ if (liveArtifact)
326
+ liveArtifacts.push(liveArtifact);
327
+ }
328
+ return collectOpenClawPromotionChecks(value, {
329
+ ownerIdentityConfigured,
330
+ safetyControlsEnabled: isFeatureEnabled(flags, 'rulecode_safety_controls'),
331
+ hostContract: OPENCLAW_HOST_LIVENESS_CONTRACT,
332
+ existingLiveArtifacts: liveArtifacts,
333
+ validateProductionArtifact: candidate => writer.canActivate(candidate),
334
+ });
335
+ },
336
+ buildEvidenceSnapshot: (checks, value) => {
337
+ const createdAt = new Date().toISOString();
338
+ const artifactDigest = value
339
+ ? `sha256:${createHash('sha256').update(JSON.stringify(value), 'utf8').digest('hex')}` : digest;
340
+ const body = JSON.stringify({ artifactDigest, checks, createdAt });
341
+ return {
342
+ snapshotId: `snapshot-${randomUUID()}`,
343
+ snapshotDigest: `sha256:${createHash('sha256').update(body, 'utf8').digest('hex')}`,
344
+ artifactDigest, lineageRefs: value ? [value.sourceTaskId, ...value.lineageArtifactIds] : [],
345
+ hostRuntimeVersion: 'openclaw-legacy@1', safetyGateResults: checks,
346
+ shadowSummary: telemetry.shadowSummary,
347
+ configurationVersion: 'pd-config-current', redaction: { version: 'v1', rawParametersStored: false }, createdAt,
348
+ };
349
+ },
350
+ newEvaluationId: () => `readiness-${randomUUID()}`,
351
+ });
352
+ let content = null;
353
+ try {
354
+ const parsed = JSON.parse(artifact.contentJson);
355
+ if (isRecord(parsed))
356
+ content = parsed;
357
+ }
358
+ catch { /* readiness reports validation failure */ }
359
+ let approvedBlockRate = null;
360
+ let promotion;
361
+ for (let index = decisions.length - 1; index >= 0; index -= 1) {
362
+ const candidate = decisions[index];
363
+ if (candidate?.decision === 'promote_live' && candidate.evidenceSnapshotId !== null) {
364
+ promotion = candidate;
365
+ break;
366
+ }
367
+ }
368
+ if (promotion?.evidenceSnapshotId) {
369
+ const row = conn.getDb().prepare('SELECT shadow_summary FROM activation_evidence_snapshots WHERE snapshot_id = ?').get(promotion.evidenceSnapshotId);
370
+ if (isRecord(row) && typeof row.shadow_summary === 'string') {
371
+ try {
372
+ const summary = JSON.parse(row.shadow_summary);
373
+ if (isRecord(summary) && typeof summary.observed === 'number' && summary.observed > 0 && typeof summary.wouldBlock === 'number') {
374
+ approvedBlockRate = summary.wouldBlock / summary.observed;
375
+ }
376
+ }
377
+ catch { /* malformed immutable evidence leaves drift unavailable */ }
378
+ }
379
+ }
380
+ const live24 = telemetry.metrics.last24Hours;
381
+ const liveBlockRate = live24.eligible !== null && live24.eligible > 0 && live24.blocked !== null
382
+ ? live24.blocked / live24.eligible
383
+ : null;
384
+ return {
385
+ activation,
386
+ artifact: { artifactId: artifact.artifactId, digest, sourceTaskId: artifact.sourceTaskId, lineageArtifactIds: [...artifact.lineageArtifactIds], content },
387
+ readiness: await reader.evaluate({ activationId, expectedArtifactId: artifact.artifactId, expectedArtifactDigest: digest }),
388
+ controlState: await safetyStore.getControlState(activationId),
389
+ decisions,
390
+ globalPause: await safetyStore.getActiveGlobalPause(),
391
+ ownerDecisionEnabled: isFeatureEnabled(flags, 'rulecode_owner_live_decision'),
392
+ runtimeCapability: {
393
+ hostRuntimeVersion: OPENCLAW_HOST_LIVENESS_CONTRACT.version,
394
+ shadowEvidence: OPENCLAW_HOST_LIVENESS_CONTRACT.supportsShadowEvidence,
395
+ },
396
+ liveMetrics: telemetry.metrics,
397
+ behaviorDrift: {
398
+ approvedBlockRate,
399
+ liveBlockRate,
400
+ delta: approvedBlockRate === null || liveBlockRate === null ? null : liveBlockRate - approvedBlockRate,
401
+ },
402
+ };
403
+ }
404
+ finally {
405
+ try {
406
+ conn.close();
407
+ }
408
+ catch { /* best effort */ }
409
+ }
410
+ }
411
+ async continueObserving(activationId, input) {
412
+ this.requireOwnerDecisionFeature();
413
+ return this.withMutableRuleCode(activationId, async (store, activation, artifactDigest) => store.recordOwnerDecision(makeActivationDecision({ activation, artifactDigest, decision: 'continue_observing', input }), input.idempotencyKey));
414
+ }
415
+ async deactivateRuleCode(activationId, decision, input) {
416
+ if (decision === 'reject_after_shadow')
417
+ this.requireOwnerDecisionFeature();
418
+ return this.withMutableRuleCode(activationId, async (store, activation, artifactDigest) => store.deactivateWithDecision(makeActivationDecision({ activation, artifactDigest, decision, input }), input.idempotencyKey));
419
+ }
420
+ async recoverRuleCodeToShadow(activationId, expectedControlVersion, input) {
421
+ this.requireOwnerDecisionFeature();
422
+ return this.withMutableRuleCode(activationId, async (store, activation, artifactDigest) => store.recoverToShadow({
423
+ ...makeActivationDecision({ activation, artifactDigest, decision: 'continue_observing', input }),
424
+ decision: 'recover_to_shadow',
425
+ }, { expectedControlVersion, newActivationId: `activation-recovery-${randomUUID()}`, idempotencyKey: input.idempotencyKey }));
426
+ }
427
+ async pauseAllRuleCode(input) {
428
+ const conn = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: false });
429
+ try {
430
+ const decision = {
431
+ decisionId: `decision-${randomUUID()}`, subject: { kind: 'all_live_rulecode' }, decision: 'global_emergency_pause',
432
+ principal: input.actor.principal, authentication: input.actor.authentication,
433
+ ...(input.actor.operator ? { operator: input.actor.operator } : {}), reasonCode: input.reasonCode,
434
+ note: input.note ?? null, evidenceSnapshotId: null, decidedAt: new Date().toISOString(),
435
+ };
436
+ return await new SqliteActivationSafetyStore(conn).pauseAllLive(decision, `pause-${randomUUID()}`, input.idempotencyKey);
437
+ }
438
+ finally {
439
+ try {
440
+ conn.close();
441
+ }
442
+ catch { /* best effort */ }
443
+ }
444
+ }
445
+ async releaseRuleCodePause(pauseId, expectedVersion, input) {
446
+ const conn = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: false });
447
+ try {
448
+ const decision = {
449
+ decisionId: `decision-${randomUUID()}`, subject: { kind: 'all_live_rulecode' }, decision: 'global_emergency_pause_release',
450
+ principal: input.actor.principal, authentication: input.actor.authentication,
451
+ ...(input.actor.operator ? { operator: input.actor.operator } : {}), reasonCode: input.reasonCode,
452
+ note: input.note ?? null, evidenceSnapshotId: null, decidedAt: new Date().toISOString(),
453
+ };
454
+ return await new SqliteActivationSafetyStore(conn).releaseGlobalPause(decision, { pauseId, expectedVersion, idempotencyKey: input.idempotencyKey });
455
+ }
456
+ finally {
457
+ try {
458
+ conn.close();
459
+ }
460
+ catch { /* best effort */ }
461
+ }
462
+ }
463
+ async promoteRuleCode(activationId, expected, input) {
464
+ const review = await this.getOwnerReview(activationId, input.actor.principal.kind === 'configured_owner' && input.actor.authentication.method === 'console_token');
465
+ const flags = computeFlagsFromLoadResult(loadPdConfig(this.workspaceDir));
466
+ const conn = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: false });
467
+ try {
468
+ const store = new SqliteActivationSafetyStore(conn);
469
+ const service = new RuleCodeOwnerDecisionService({
470
+ ownerLiveDecisionEnabled: () => isFeatureEnabled(flags, 'rulecode_owner_live_decision'),
471
+ safetyControlsEnabled: () => isFeatureEnabled(flags, 'rulecode_safety_controls'),
472
+ evaluateReadiness: async () => review.readiness,
473
+ commitPromotion: value => store.commitPromotion(value),
474
+ newDecisionId: () => `decision-${randomUUID()}`,
475
+ now: () => new Date().toISOString(),
476
+ });
477
+ return await service.promote({
478
+ activationId, expectedArtifactId: expected.artifactId, expectedArtifactDigest: expected.artifactDigest,
479
+ expectedControlVersion: expected.controlVersion, idempotencyKey: input.idempotencyKey,
480
+ reasonCode: input.reasonCode, note: input.note, confirmed: expected.confirmed,
481
+ }, input.actor);
482
+ }
483
+ finally {
484
+ try {
485
+ conn.close();
486
+ }
487
+ catch { /* best effort */ }
488
+ }
489
+ }
490
+ async withMutableRuleCode(activationId, action) {
491
+ const conn = new SqliteConnection({ workspaceDir: this.workspaceDir, readonly: false });
492
+ try {
493
+ const activationStore = new SqliteActivationStateStore(conn);
494
+ const artifactStore = new SqlitePIArtifactStore(conn);
495
+ const matches = (await activationStore.listCodeToolHookActivations(true)).filter(value => value.activationId === activationId);
496
+ if (matches.length !== 1)
497
+ throw new Error(`RuleCode mutation requires exactly one activation: ${activationId}`);
498
+ const [activation] = matches;
499
+ if (!activation)
500
+ throw new Error(`Activation not found: ${activationId}`);
501
+ const artifact = await artifactStore.getArtifactById(activation.artifactId);
502
+ if (!artifact)
503
+ throw new Error(`Artifact not found for activation: ${activation.artifactId}`);
504
+ const artifactDigest = `sha256:${createHash('sha256').update(JSON.stringify(artifact), 'utf8').digest('hex')}`;
505
+ return await action(new SqliteActivationSafetyStore(conn), activation, artifactDigest);
506
+ }
507
+ finally {
508
+ try {
509
+ conn.close();
510
+ }
511
+ catch { /* best effort */ }
512
+ }
513
+ }
514
+ requireOwnerDecisionFeature() {
515
+ const flags = computeFlagsFromLoadResult(loadPdConfig(this.workspaceDir));
516
+ if (!isFeatureEnabled(flags, 'rulecode_owner_live_decision'))
517
+ throw new Error('feature_not_enabled: enable rulecode_owner_live_decision only after the rollout gate passes');
518
+ }
201
519
  async deactivateActivation(activationId) {
202
520
  const stateDbPath = path.join(this.workspaceDir, '.pd', 'state.db');
203
521
  if (!fs.existsSync(stateDbPath)) {
@@ -0,0 +1,42 @@
1
+ export interface ReceiptEvent {
2
+ kind: 'rule_blocked' | 'auto_correct_applied' | 'self_reported' | 'prompt_injected';
3
+ level: 'effect' | 'presence';
4
+ sessionId: string | null;
5
+ toolName: string | null;
6
+ filePath: string | null;
7
+ digest: string | null;
8
+ createdAt: string;
9
+ }
10
+ export interface PrincipleReceipts {
11
+ status: 'ok' | 'degraded';
12
+ reason?: string;
13
+ nextAction?: string;
14
+ principleId: string;
15
+ effectCount: number;
16
+ presenceCount: number;
17
+ lastEffectAt: string | null;
18
+ events: ReceiptEvent[];
19
+ }
20
+ export interface ReceiptCountEntry {
21
+ principleId: string;
22
+ effectCount: number;
23
+ presenceCount: number;
24
+ lastEffectAt: string | null;
25
+ }
26
+ export interface ReceiptCounts {
27
+ status: 'ok' | 'degraded';
28
+ reason?: string;
29
+ nextAction?: string;
30
+ counts: ReceiptCountEntry[];
31
+ }
32
+ export declare class ReceiptsConsoleModel {
33
+ private readonly workspaceDir;
34
+ constructor(workspaceDir: string);
35
+ getPrincipleReceipts(principleId: string): Promise<PrincipleReceipts>;
36
+ getReceiptCounts(): Promise<ReceiptCounts>;
37
+ /**
38
+ * Shared preconditions: state.db exists + ledger flag enabled. Returns a
39
+ * degraded partial response when a precondition fails (rc-9), else null.
40
+ */
41
+ private precheck;
42
+ }