create-principles-disciple 1.146.1 → 1.146.3

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 (122) hide show
  1. package/_release/product-identity.json +2 -2
  2. package/console/dist/server/models/ActivationsConsoleModel.d.ts +3 -0
  3. package/console/dist/server/models/ActivationsConsoleModel.js +25 -1
  4. package/console/dist/server/models/ApprovalsConsoleModel.d.ts +5 -0
  5. package/console/dist/server/models/ApprovalsConsoleModel.js +40 -2
  6. package/console/dist/server/routes/activations.js +47 -0
  7. package/console/dist/server/routes/approvals.js +1 -1
  8. package/console/dist/server/routes/health.js +17 -0
  9. package/console/dist/server/routes/update-transaction.js +63 -3
  10. package/console/dist/shared/approved-rule-revision-contract.d.ts +8 -0
  11. package/console/dist/shared/approved-rule-revision-contract.js +48 -0
  12. package/console/dist/ui/api.d.ts +3 -0
  13. package/console/dist/ui/api.js +9 -0
  14. package/console/dist/ui/i18n/en.json +9 -0
  15. package/console/dist/ui/i18n/zh-CN.json +9 -0
  16. package/console/dist/ui/pages/activation/ActivationPage.js +69 -2
  17. package/console/dist/ui/pages/settings/UpdatePage.js +1 -1
  18. package/console/dist/ui/utils/validators.d.ts +10 -0
  19. package/console/dist/ui/utils/validators.js +11 -1
  20. package/console/dist/web/assets/app.css +3 -0
  21. package/console/dist/web/assets/app.js +31 -31
  22. package/core/dist/runtime-v2/activation/activation-dispatcher.d.ts +2 -0
  23. package/core/dist/runtime-v2/activation/activation-dispatcher.d.ts.map +1 -1
  24. package/core/dist/runtime-v2/activation/activation-dispatcher.js +60 -22
  25. package/core/dist/runtime-v2/activation/activation-dispatcher.js.map +1 -1
  26. package/core/dist/runtime-v2/activation/activation-types.d.ts +12 -2
  27. package/core/dist/runtime-v2/activation/activation-types.d.ts.map +1 -1
  28. package/core/dist/runtime-v2/activation/activation-types.js.map +1 -1
  29. package/core/dist/runtime-v2/activation/index.d.ts +2 -0
  30. package/core/dist/runtime-v2/activation/index.d.ts.map +1 -1
  31. package/core/dist/runtime-v2/activation/index.js +2 -0
  32. package/core/dist/runtime-v2/activation/index.js.map +1 -1
  33. package/core/dist/runtime-v2/activation/rule-replacement.d.ts +38 -0
  34. package/core/dist/runtime-v2/activation/rule-replacement.d.ts.map +1 -0
  35. package/core/dist/runtime-v2/activation/rule-replacement.js +44 -0
  36. package/core/dist/runtime-v2/activation/rule-replacement.js.map +1 -0
  37. package/core/dist/runtime-v2/activation/sqlite-activation-state-store.d.ts +9 -1
  38. package/core/dist/runtime-v2/activation/sqlite-activation-state-store.d.ts.map +1 -1
  39. package/core/dist/runtime-v2/activation/sqlite-activation-state-store.js +53 -23
  40. package/core/dist/runtime-v2/activation/sqlite-activation-state-store.js.map +1 -1
  41. package/core/dist/runtime-v2/activation/writers/rule-host-writer.d.ts.map +1 -1
  42. package/core/dist/runtime-v2/activation/writers/rule-host-writer.js +15 -2
  43. package/core/dist/runtime-v2/activation/writers/rule-host-writer.js.map +1 -1
  44. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.d.ts +2 -0
  45. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.d.ts.map +1 -1
  46. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.js +5 -1
  47. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.js.map +1 -1
  48. package/core/dist/runtime-v2/index.d.ts +3 -1
  49. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  50. package/core/dist/runtime-v2/index.js +2 -1
  51. package/core/dist/runtime-v2/index.js.map +1 -1
  52. package/core/dist/runtime-v2/internalization/approved-rule-revision.d.ts +51 -0
  53. package/core/dist/runtime-v2/internalization/approved-rule-revision.d.ts.map +1 -0
  54. package/core/dist/runtime-v2/internalization/approved-rule-revision.js +211 -0
  55. package/core/dist/runtime-v2/internalization/approved-rule-revision.js.map +1 -0
  56. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts +1 -1
  57. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts.map +1 -1
  58. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js +2 -1
  59. package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js.map +1 -1
  60. package/core/dist/runtime-v2/internalization/bash-risk.d.ts +6 -0
  61. package/core/dist/runtime-v2/internalization/bash-risk.d.ts.map +1 -1
  62. package/core/dist/runtime-v2/internalization/bash-risk.js +120 -0
  63. package/core/dist/runtime-v2/internalization/bash-risk.js.map +1 -1
  64. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
  65. package/core/dist/runtime-v2/internalization/evaluator-runner.js +4 -1
  66. package/core/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
  67. package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts +2 -1
  68. package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts.map +1 -1
  69. package/core/dist/runtime-v2/internalization/peer-runner-contracts.js.map +1 -1
  70. package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts +21 -0
  71. package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts.map +1 -1
  72. package/core/dist/runtime-v2/internalization/pitask-metadata.js +30 -0
  73. package/core/dist/runtime-v2/internalization/pitask-metadata.js.map +1 -1
  74. package/core/dist/runtime-v2/internalization/rule-host-contracts.d.ts +2 -0
  75. package/core/dist/runtime-v2/internalization/rule-host-contracts.d.ts.map +1 -1
  76. package/core/dist/runtime-v2/internalization/rule-host-input-builder.d.ts.map +1 -1
  77. package/core/dist/runtime-v2/internalization/rule-host-input-builder.js +6 -0
  78. package/core/dist/runtime-v2/internalization/rule-host-input-builder.js.map +1 -1
  79. package/core/dist/runtime-v2/internalization/rulecode-verification-obligations.d.ts.map +1 -1
  80. package/core/dist/runtime-v2/internalization/rulecode-verification-obligations.js +32 -0
  81. package/core/dist/runtime-v2/internalization/rulecode-verification-obligations.js.map +1 -1
  82. package/core/dist/runtime-v2/proven-channel-baseline.js +1 -1
  83. package/core/dist/runtime-v2/proven-channel-baseline.js.map +1 -1
  84. package/core/dist/runtime-v2/store/task/sqlite-approved-rule-revision.d.ts +15 -0
  85. package/core/dist/runtime-v2/store/task/sqlite-approved-rule-revision.d.ts.map +1 -0
  86. package/core/dist/runtime-v2/store/task/sqlite-approved-rule-revision.js +39 -0
  87. package/core/dist/runtime-v2/store/task/sqlite-approved-rule-revision.js.map +1 -0
  88. package/core/package.json +1 -1
  89. package/dist/installer.d.ts +81 -0
  90. package/dist/installer.d.ts.map +1 -1
  91. package/dist/installer.js +480 -117
  92. package/dist/installer.js.map +1 -1
  93. package/dist/update/console-surface.d.ts +1 -1
  94. package/dist/update/console-surface.d.ts.map +1 -1
  95. package/dist/update/console-surface.js +1 -1
  96. package/dist/update/console-surface.js.map +1 -1
  97. package/dist/update/transaction-journal.d.ts +47 -0
  98. package/dist/update/transaction-journal.d.ts.map +1 -1
  99. package/dist/update/transaction-journal.js +87 -2
  100. package/dist/update/transaction-journal.js.map +1 -1
  101. package/host-runtime/dist/production-rulehost-gate.js +11 -2
  102. package/host-runtime/package.json +1 -1
  103. package/package.json +2 -2
  104. package/pd-cli/package.json +1 -1
  105. package/plugin/dist/bundle.js +477 -472
  106. package/plugin/dist/openclaw.plugin.json +1 -1
  107. package/plugin/dist/rulehost-evidence.js +52 -49
  108. package/plugin/openclaw.plugin.json +1 -1
  109. package/plugin/package.json +1 -1
  110. package/release-manager/dist/installer.d.ts +81 -0
  111. package/release-manager/dist/installer.d.ts.map +1 -1
  112. package/release-manager/dist/installer.js +480 -117
  113. package/release-manager/dist/installer.js.map +1 -1
  114. package/release-manager/dist/update/console-surface.d.ts +1 -1
  115. package/release-manager/dist/update/console-surface.d.ts.map +1 -1
  116. package/release-manager/dist/update/console-surface.js +1 -1
  117. package/release-manager/dist/update/console-surface.js.map +1 -1
  118. package/release-manager/dist/update/transaction-journal.d.ts +47 -0
  119. package/release-manager/dist/update/transaction-journal.d.ts.map +1 -1
  120. package/release-manager/dist/update/transaction-journal.js +87 -2
  121. package/release-manager/dist/update/transaction-journal.js.map +1 -1
  122. package/release-manager/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "productVersion": "2.7.0",
4
- "sourceCommit": "028bd1d8197cd5e6cd3187d3e6c0944848a95b9b"
3
+ "productVersion": "2.8.0",
4
+ "sourceCommit": "fd8fe301fb9ea4223b153e022022eefb0938ae21"
5
5
  }
@@ -2,6 +2,7 @@ import type { ActivationStatusRecord, PromotionReadinessResult, ActivationContro
2
2
  import { type PromptInjectionTargetHost } from '@principles/host-runtime';
3
3
  import type { PromptActivationInjectionStatusWire, PromptInjectionRouteSummaryWire } from '../../shared/prompt-injection-contract.js';
4
4
  import { type GovernanceAuditWriter } from 'principles-disciple/governance-audit';
5
+ import type { ApprovedRuleRevisionRequest } from '../../shared/approved-rule-revision-contract.js';
5
6
  /**
6
7
  * PRI-491 — owner-observable activation record.
7
8
  *
@@ -151,6 +152,8 @@ export declare class ActivationsConsoleModel {
151
152
  private readonly workspaceDir;
152
153
  private readonly governanceAuditWriter;
153
154
  constructor(workspaceDir: string, governanceAuditWriter?: GovernanceAuditWriter);
155
+ previewApprovedRuleRevision(activationId: string): Promise<import("@principles/core/runtime-v2").ApprovedRuleRevisionPreview>;
156
+ reviseApprovedRuleOnce(activationId: string, input: ApprovedRuleRevisionRequest, actor: OwnerPromotionActor): Promise<import("@principles/core/runtime-v2").ApprovedRuleRevisionResult>;
154
157
  getActivations(targetHost?: PromptInjectionTargetHost): Promise<ActivationsResponse>;
155
158
  getOwnerReview(activationId: string, ownerIdentityConfigured?: boolean, options?: {
156
159
  ownerActor?: OwnerPromotionActor;
@@ -1,4 +1,4 @@
1
- import { SqliteConnection, SqliteActivationStateStore, SqlitePIArtifactStore, extractPrincipleId, extractEvidenceRefs, PromotionReadinessReader, RuleHostWriter, createProductionGateDeps, SqliteActivationSafetyStore, isFeatureEnabled, RuleCodeOwnerDecisionService, collectOpenClawPromotionChecks, summarizeRuleCodeShadowEvents, buildPromotionEvidenceSnapshot, } from '@principles/core/runtime-v2';
1
+ import { SqliteConnection, SqliteActivationStateStore, SqlitePIArtifactStore, extractPrincipleId, extractEvidenceRefs, PromotionReadinessReader, RuleHostWriter, createProductionGateDeps, SqliteActivationSafetyStore, isFeatureEnabled, RuleCodeOwnerDecisionService, collectOpenClawPromotionChecks, summarizeRuleCodeShadowEvents, buildPromotionEvidenceSnapshot, createRuntimeStateHandle, PrincipleTreeLedgerAdapter, previewApprovedRuleRevision, reviseApprovedRuleOnce, } from '@principles/core/runtime-v2';
2
2
  import { resolvePromotionHostLiveness, resolveWorkspaceHostToolSemantics, readPromptActivationInjectionStatuses, operatorDeclaredBudgetResolution, runtimeBudgetObservationToResolution, selectConsoleRuntimeBudgetFacts, RUNTIME_BUDGET_OBSERVATION_STALE_REASON, RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS } from '@principles/host-runtime';
3
3
  import { PROMPT_BUDGET_HOST_KINDS } from '@principles/core/runtime-v2';
4
4
  import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
@@ -168,6 +168,30 @@ export class ActivationsConsoleModel {
168
168
  this.workspaceDir = workspaceDir;
169
169
  this.governanceAuditWriter = governanceAuditWriter;
170
170
  }
171
+ async previewApprovedRuleRevision(activationId) {
172
+ const handle = await createRuntimeStateHandle({ workspaceDir: this.workspaceDir, readonly: true, bootstrapIfMissing: false });
173
+ try {
174
+ const ledger = new PrincipleTreeLedgerAdapter({ stateDir: path.join(this.workspaceDir, '.state') });
175
+ return await previewApprovedRuleRevision({ stateManager: handle.stateManager,
176
+ ledger: { resolveForCandidate: async (id) => ledger.resolveCandidatePrincipleIdentity(id, 'rule'),
177
+ withLockedRuleCandidateIdentity: (id, operation) => ledger.withLockedRuleCandidateIdentity(id, operation) }, activationId });
178
+ }
179
+ finally {
180
+ await handle.close();
181
+ }
182
+ }
183
+ async reviseApprovedRuleOnce(activationId, input, actor) {
184
+ const handle = await createRuntimeStateHandle({ workspaceDir: this.workspaceDir, bootstrapIfMissing: false });
185
+ try {
186
+ const ledger = new PrincipleTreeLedgerAdapter({ stateDir: path.join(this.workspaceDir, '.state') });
187
+ return await reviseApprovedRuleOnce({ stateManager: handle.stateManager,
188
+ ledger: { resolveForCandidate: async (id) => ledger.resolveCandidatePrincipleIdentity(id, 'rule'),
189
+ withLockedRuleCandidateIdentity: (id, operation) => ledger.withLockedRuleCandidateIdentity(id, operation) }, activationId, ...input, actor });
190
+ }
191
+ finally {
192
+ await handle.close();
193
+ }
194
+ }
171
195
  async getActivations(targetHost) {
172
196
  const stateDbPath = path.join(this.workspaceDir, '.pd', 'state.db');
173
197
  if (!fs.existsSync(stateDbPath)) {
@@ -71,6 +71,11 @@ export type ApproveWithActivationResult = {
71
71
  error: 'prompt_replacement_refused';
72
72
  reason: string;
73
73
  nextAction: string;
74
+ } | {
75
+ ok: false;
76
+ error: 'rule_replacement_refused';
77
+ reason: string;
78
+ nextAction: string;
74
79
  }
75
80
  /**
76
81
  * PD_PROMPT_CAPACITY_V1 R-B2: the prompt-channel write gate refused BEFORE
@@ -2,7 +2,7 @@ import { Value } from '@sinclair/typebox/value';
2
2
  import { approvalReviewMaterial } from './approval-review-material.js';
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
- import { SqliteConnection, SqliteApprovalQueueStore, SqliteActivationStateStore, SqlitePIArtifactStore, ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, createProductionGateDeps, ApprovalCompletionService, ApprovalQueue, mapConfidenceToLabel, isArtifactRevisionOf, detectPromptReplacementTarget, buildOwnerRevisionArtifact, PrincipleTreeLedgerAdapter, MVP_CHANNELS, computeArtifactDigest, } from '@principles/core/runtime-v2';
5
+ import { SqliteConnection, SqliteApprovalQueueStore, SqliteActivationStateStore, SqlitePIArtifactStore, ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, createProductionGateDeps, ApprovalCompletionService, ApprovalQueue, mapConfidenceToLabel, isArtifactRevisionOf, detectPromptReplacementTarget, detectRuleReplacementTarget, buildOwnerRevisionArtifact, PrincipleTreeLedgerAdapter, MVP_CHANNELS, computeArtifactDigest, } from '@principles/core/runtime-v2';
6
6
  import { resolveLedgerPrincipleId } from './principle-id-resolution.js';
7
7
  import { createArtifactPrincipleResolutionDeps } from './artifact-principle-resolver.js';
8
8
  import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
@@ -191,6 +191,39 @@ export class ApprovalsConsoleModel {
191
191
  catch { /* best-effort */ }
192
192
  }
193
193
  }
194
+ // SPEC-RULECODE-APPROVED-REVISION-001 §B: approving a revised RuleCode
195
+ // artifact replaces its superseded shadow in the SAME transaction as the
196
+ // new activation (no stop-old-then-start-new window). Detection is
197
+ // fail-closed on ambiguity and refuses to shadow an already-live rule.
198
+ if (existing.channel === 'code_tool_hook' && (existing.status === 'pending' || (existing.status === 'approved' && options?.retryActivation === true))) {
199
+ const { connection } = this.createReadContext();
200
+ try {
201
+ const ruleArtifactStore = new SqlitePIArtifactStore(connection);
202
+ const detection = await detectRuleReplacementTarget({
203
+ approvalArtifactId: existing.artifactId,
204
+ getArtifactById: async (id) => ruleArtifactStore.getArtifactById(id),
205
+ listRuleActivations: async () => new SqliteActivationStateStore(connection).listCodeToolHookActivations(),
206
+ });
207
+ if (!detection.ok) {
208
+ // An unreadable NEW artifact is not a replacement decision: the
209
+ // activation path already reports it with a precise reason and the
210
+ // established status code. Only replacement-specific refusals stop here.
211
+ if (!detection.error.startsWith('rule_replacement_artifact_unreadable')) {
212
+ return { ok: false, error: 'rule_replacement_refused', reason: detection.error, nextAction: detection.nextAction };
213
+ }
214
+ }
215
+ else {
216
+ supersedeActivationId = detection.target?.supersededActivationId;
217
+ supersedeArtifactId = detection.target?.supersededArtifactId;
218
+ }
219
+ }
220
+ finally {
221
+ try {
222
+ connection.close();
223
+ }
224
+ catch { /* best-effort */ }
225
+ }
226
+ }
194
227
  const { queue: writeQueue, connection: writeConnection } = this.createWriteContext();
195
228
  let approvalResult;
196
229
  try {
@@ -236,7 +269,12 @@ export class ApprovalsConsoleModel {
236
269
  }
237
270
  }
238
271
  catch { /* best-effort rollback */ }
239
- return { ok: false, error: 'activation_failed', reason: detail, approvalRolledBack };
272
+ // Carry the dispatcher's nextAction (it names the concrete cause for a
273
+ // replacement failure); without it the Owner only sees a stable code.
274
+ const activationNextAction = 'nextAction' in activation && typeof activation.nextAction === 'string'
275
+ ? activation.nextAction : undefined;
276
+ return { ok: false, error: 'activation_failed', reason: detail, approvalRolledBack,
277
+ ...(activationNextAction !== undefined ? { nextAction: activationNextAction } : {}) };
240
278
  }
241
279
  // P1 #4 fix: when activation is skipped (feature flag disabled), surface
242
280
  // a clear warning so the owner knows behavior did NOT change. The approval
@@ -1,6 +1,7 @@
1
1
  import { ActivationsConsoleModel } from '../models/ActivationsConsoleModel.js';
2
2
  import { ApprovalsConsoleModel } from '../models/ApprovalsConsoleModel.js';
3
3
  import { sendSuccess, sendError, sendNotFound } from '../utils/response.js';
4
+ import { validateApprovedRuleRevisionRequest } from '../../shared/approved-rule-revision-contract.js';
4
5
  const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
5
6
  const models = new Map();
6
7
  const approvalsModels = new Map();
@@ -116,6 +117,52 @@ export async function handleActivationsRoute(req, res, workspaceDir, subPath, au
116
117
  }
117
118
  return;
118
119
  }
120
+ const revisionMatch = /^\/([^/]+)\/(rule-revision-preview|revise-rule-once)$/.exec(subPath);
121
+ if (revisionMatch && ((req.method === 'GET' && revisionMatch[2] === 'rule-revision-preview')
122
+ || (req.method === 'POST' && revisionMatch[2] === 'revise-rule-once'))) {
123
+ const actor = authority?.ownerActor;
124
+ if (!actor) {
125
+ sendError(res, 403, 'owner_authentication_required', 'This revision requires the configured Owner identity.', { nextAction: 'Authenticate as the configured Owner in the Console.' });
126
+ return;
127
+ }
128
+ const [, rawId] = revisionMatch;
129
+ if (!rawId) {
130
+ sendError(res, 400, 'invalid_id', 'Activation ID is missing');
131
+ return;
132
+ }
133
+ let activationId;
134
+ try {
135
+ activationId = decodeURIComponent(rawId);
136
+ }
137
+ catch {
138
+ sendError(res, 400, 'invalid_id', 'Activation ID contains invalid URI encoding');
139
+ return;
140
+ }
141
+ let input;
142
+ if (req.method === 'POST') {
143
+ try {
144
+ input = validateApprovedRuleRevisionRequest(await readJson(req));
145
+ }
146
+ catch (err) {
147
+ const message = err instanceof Error ? err.message : String(err);
148
+ sendError(res, message === 'payload_too_large' ? 413 : 400, 'validation_error', message);
149
+ return;
150
+ }
151
+ if (!input) {
152
+ sendError(res, 400, 'validation_error', 'Revision requires the reviewed artifact, digest, binding/control/task versions, requestId and 1–4000 characters of directed feedback.', { nextAction: 'Refresh the revision preview and describe the required correction.' });
153
+ return;
154
+ }
155
+ }
156
+ try {
157
+ const model = getModel(workspaceDir);
158
+ sendSuccess(res, input ? await model.reviseApprovedRuleOnce(activationId, input, actor) : await model.previewApprovedRuleRevision(activationId));
159
+ }
160
+ catch (err) {
161
+ const message = err instanceof Error ? err.message : String(err);
162
+ sendError(res, 500, 'rule_revision_unavailable', message, { nextAction: 'Refresh the revision preview; keep the current rule in shadow.' });
163
+ }
164
+ return;
165
+ }
119
166
  const mutationMatch = /^\/([^/]+)\/(continue-observing|promote|reject-after-shadow|emergency-deactivate|recover-to-shadow)$/.exec(subPath);
120
167
  const pauseMatch = subPath === '/emergency-pause';
121
168
  const releaseMatch = /^\/emergency-pause\/([^/]+)\/release$/.exec(subPath);
@@ -175,7 +175,7 @@ export async function handleApprovalsRoute(req, res, workspaceDir, subPath) {
175
175
  else if (result.error === 'unsupported_channel') {
176
176
  sendError(res, 403, 'unsupported_channel', `Cannot approve unsupported channel. Only MVP proven channels (${MVP_CHANNEL_LIST}) can be approved.`);
177
177
  }
178
- else if (result.error === 'prompt_replacement_refused') {
178
+ else if (result.error === 'prompt_replacement_refused' || result.error === 'rule_replacement_refused') {
179
179
  sendError(res, 409, result.error, result.reason, { nextAction: result.nextAction });
180
180
  }
181
181
  else if (result.error === 'activation_failed') {
@@ -2,6 +2,16 @@ import { HealthCheckModel } from '../models/HealthCheckModel.js';
2
2
  import { CodexGovernanceHealthModel } from '../models/CodexGovernanceHealthModel.js';
3
3
  import { sendSuccess, sendError } from '../utils/response.js';
4
4
  const MODEL_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
5
+ /**
6
+ * PRI-1069/G1 P0-1 (SPEC v1.2 §14): one-shot launcher nonce, read once at
7
+ * process start. The installer spawns this server with
8
+ * PD_CONSOLE_VERIFY_NONCE in the child env; the health response echoes it so
9
+ * the probe can bind the answer to THIS child instance. Absent on normal
10
+ * (user-launched) consoles. Never logged.
11
+ */
12
+ const VERIFY_NONCE = process.env.PD_CONSOLE_VERIFY_NONCE && process.env.PD_CONSOLE_VERIFY_NONCE.length > 0
13
+ ? process.env.PD_CONSOLE_VERIFY_NONCE
14
+ : null;
5
15
  const models = new Map();
6
16
  function getModel(workspaceDir) {
7
17
  const cached = models.get(workspaceDir);
@@ -69,6 +79,13 @@ export async function handleHealthRoute(req, res, options) {
69
79
  ...health,
70
80
  authenticationMode: options.authenticationMode,
71
81
  codexGovernance,
82
+ // PRI-1069/G1 P0-1: installer identity binding. When the launching
83
+ // installer passes a one-shot nonce via env, echo it back so the probe
84
+ // can prove THIS child answered — an old console, another local service,
85
+ // or a proxy 502 cannot know the value. The nonce is short-lived,
86
+ // machine-local, never logged here, and absent entirely for consoles
87
+ // launched without it (normal user launches), keeping the legacy shape.
88
+ ...(VERIFY_NONCE !== null ? { verifyNonce: VERIFY_NONCE } : {}),
72
89
  });
73
90
  }
74
91
  catch (error) {
@@ -90,6 +90,10 @@ async function recoveryStatus(res) {
90
90
  const unfinished = [];
91
91
  const superseded = [];
92
92
  const broken = [];
93
+ // P0-4: terminal `failed` transactions whose detail carries a restore
94
+ // inconsistency marker — the install on disk is NOT what the terminal state
95
+ // implies. They must stay visible in the recovery list.
96
+ const recoveryRequired = [];
93
97
  // PRI-896: a non-terminal journal is only an ALARM if the product's own
94
98
  // recovery decision cannot explain it away. The live release record is the
95
99
  // evidence for that, so a missing/corrupt active record means NO evidence —
@@ -117,8 +121,26 @@ async function recoveryStatus(res) {
117
121
  try {
118
122
  const read = journal.readTransactionJournalForRecovery(path.join(paths.transactionsDir, entry.name));
119
123
  const fact = factFromTransitions(journal, transactionId, read);
120
- if (fact.terminal)
124
+ if (fact.terminal) {
125
+ // P0-4: terminal facts are normally closed history — EXCEPT a
126
+ // `failed` whose detail says the physical restore did not (or could
127
+ // not) complete. That combination is a live recovery need hiding
128
+ // behind a terminal state; skipping it is how PRI-1069 stayed
129
+ // invisible. rc-9: reported with its own bucket, never dropped.
130
+ const lastDetail = fact.transitions.length > 0
131
+ ? (fact.transitions[fact.transitions.length - 1]?.detail ?? '')
132
+ : '';
133
+ if (fact.lastState === 'failed' && journal.RESTORE_INCONSISTENCY_MARKER.test(lastDetail)) {
134
+ // A later completed install records a `<txid>.recovered.json`
135
+ // resolution (journal module authority); until then the alarm
136
+ // stays. Skip only on real resolution evidence (review 2026-11-02:
137
+ // never clear the alarm just because time passed).
138
+ if (!journal.hasRecoveryResolution(paths.transactionsDir, transactionId)) {
139
+ recoveryRequired.push({ transactionId, reason: lastDetail.slice(0, 400) });
140
+ }
141
+ }
121
142
  continue;
143
+ }
122
144
  const outcome = journal.recoverUnfinishedTransaction({
123
145
  transitions: read.transitions,
124
146
  activeRecord,
@@ -139,10 +161,11 @@ async function recoveryStatus(res) {
139
161
  }
140
162
  unfinished.sort((a, b) => (b.lastAt ?? '').localeCompare(a.lastAt ?? ''));
141
163
  sendSuccess(res, {
142
- needsRecovery: unfinished.length > 0 || broken.length > 0,
164
+ needsRecovery: unfinished.length > 0 || broken.length > 0 || recoveryRequired.length > 0,
143
165
  unfinished,
144
166
  superseded,
145
167
  broken,
168
+ recoveryRequired,
146
169
  // rc-9: when the active record could not be read, the recovery decision had
147
170
  // no live-release evidence, so every non-terminal journal is conservatively
148
171
  // treated as an alarm. Surfacing this explains WHY the alarm persists and
@@ -153,7 +176,9 @@ async function recoveryStatus(res) {
153
176
  ? activeRecordUnreadable
154
177
  ? 'The active release record (active.json) could not be read, so no transaction can be proven superseded and every unfinished one is reported. Repair active.json (re-run the official installer recovery), then re-check recovery before starting a new update.'
155
178
  : 'An update transaction did not reach a terminal state. Review its phase, then continue, roll back, or repair through the official installer before starting a new update.'
156
- : undefined,
179
+ : recoveryRequired.length > 0
180
+ ? 'A completed-but-failed update left the physical installation inconsistent (restore incomplete or deferred). Close any running PD processes, then re-run the official installer to retry recovery before starting a new update.'
181
+ : undefined,
157
182
  });
158
183
  }
159
184
  /**
@@ -192,6 +217,41 @@ async function recoveryResolve(res, req) {
192
217
  }
193
218
  try {
194
219
  const read = journal.readTransactionJournalForRecovery(journalPath);
220
+ // Review 2026-11-02 (consistency): a terminal `failed` carrying a restore
221
+ // inconsistency marker must get the SAME verdict from both recovery
222
+ // surfaces. The generic decision below would answer `old_confirmed`
223
+ // ("previously confirmed release remains active") with no nextAction —
224
+ // read as "nothing to do" while the status endpoint demands repair. The
225
+ // generic decision cannot see physical state; refuse explicitly instead.
226
+ const last = read.transitions[read.transitions.length - 1];
227
+ if (last !== undefined && last.to === 'failed'
228
+ && typeof last.detail === 'string' && journal.RESTORE_INCONSISTENCY_MARKER.test(last.detail)) {
229
+ if (journal.hasRecoveryResolution(paths.transactionsDir, transactionId)) {
230
+ // Resolved by a later completed install — report that instead of the
231
+ // stale refusal.
232
+ sendSuccess(res, {
233
+ ok: true,
234
+ transactionId,
235
+ outcome: {
236
+ kind: 'old_confirmed',
237
+ releaseId: null,
238
+ generation: null,
239
+ reason: `transaction ended in failed with a restore-inconsistency marker but a later install recorded its recovery; the previously confirmed release remains active`,
240
+ },
241
+ });
242
+ return;
243
+ }
244
+ sendSuccess(res, {
245
+ ok: true,
246
+ transactionId,
247
+ outcome: {
248
+ kind: 'explicit_refusal',
249
+ reason: last.detail.slice(0, 400),
250
+ nextAction: 'A completed-but-failed update left the physical installation inconsistent (restore incomplete or deferred). Close any running PD processes, then re-run the official installer to retry recovery before starting a new update.',
251
+ },
252
+ });
253
+ return;
254
+ }
195
255
  const activeRecord = journal.readActiveRecord(paths.activeRecordPath);
196
256
  const outcome = journal.recoverUnfinishedTransaction({
197
257
  transitions: read.transitions,
@@ -0,0 +1,8 @@
1
+ import type { previewApprovedRuleRevision, reviseApprovedRuleOnce } from '@principles/core/runtime-v2';
2
+ /** Wire projections of the core authority; no independent revision state. */
3
+ export type ApprovedRuleRevisionPreview = Awaited<ReturnType<typeof previewApprovedRuleRevision>>;
4
+ export type ApprovedRuleRevisionResult = Awaited<ReturnType<typeof reviseApprovedRuleOnce>>;
5
+ export type ApprovedRuleRevisionRequest = Pick<Parameters<typeof reviseApprovedRuleOnce>[0], 'expectedArtifactId' | 'expectedArtifactDigest' | 'expectedBindingRevision' | 'expectedControlVersion' | 'expectedTaskRevisionEpoch' | 'requestId' | 'feedback'>;
6
+ export declare function validateApprovedRuleRevisionRequest(value: unknown): ApprovedRuleRevisionRequest | null;
7
+ export declare function validateApprovedRuleRevisionPreview(value: unknown): ApprovedRuleRevisionPreview | null;
8
+ export declare function validateApprovedRuleRevisionResult(value: unknown): ApprovedRuleRevisionResult | null;
@@ -0,0 +1,48 @@
1
+ function record(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }
4
+ function text(value) { return typeof value === 'string' && value.trim().length > 0; }
5
+ function version(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; }
6
+ export function validateApprovedRuleRevisionRequest(value) {
7
+ if (!record(value) || !text(value.expectedArtifactId) || !text(value.expectedArtifactDigest)
8
+ || !/^sha256:[a-f0-9]{64}$/.test(value.expectedArtifactDigest)
9
+ || !version(value.expectedBindingRevision) || !version(value.expectedControlVersion)
10
+ || !version(value.expectedTaskRevisionEpoch) || !text(value.requestId) || value.requestId.length > 200
11
+ || !text(value.feedback) || value.feedback.length > 4000)
12
+ return null;
13
+ return { expectedArtifactId: value.expectedArtifactId, expectedArtifactDigest: value.expectedArtifactDigest,
14
+ expectedBindingRevision: value.expectedBindingRevision, expectedControlVersion: value.expectedControlVersion,
15
+ expectedTaskRevisionEpoch: value.expectedTaskRevisionEpoch, requestId: value.requestId, feedback: value.feedback.trim() };
16
+ }
17
+ function refused(value) {
18
+ return value.status === 'refused' && text(value.reason) && text(value.nextAction)
19
+ ? { status: 'refused', reason: value.reason, nextAction: value.nextAction } : null;
20
+ }
21
+ export function validateApprovedRuleRevisionPreview(value) {
22
+ if (!record(value))
23
+ return null;
24
+ if (value.status === 'refused')
25
+ return refused(value);
26
+ if (value.status !== 'ready' || !text(value.activationId) || !text(value.candidateId)
27
+ || !text(value.principleId) || !text(value.artificerTaskId) || !text(value.expectedArtifactId)
28
+ || !text(value.expectedArtifactDigest) || !/^sha256:[a-f0-9]{64}$/.test(value.expectedArtifactDigest)
29
+ || !version(value.expectedBindingRevision) || !version(value.expectedControlVersion)
30
+ || !version(value.expectedTaskRevisionEpoch))
31
+ return null;
32
+ return { status: 'ready', activationId: value.activationId, candidateId: value.candidateId,
33
+ principleId: value.principleId, artificerTaskId: value.artificerTaskId,
34
+ expectedArtifactId: value.expectedArtifactId, expectedArtifactDigest: value.expectedArtifactDigest,
35
+ expectedBindingRevision: value.expectedBindingRevision, expectedControlVersion: value.expectedControlVersion,
36
+ expectedTaskRevisionEpoch: value.expectedTaskRevisionEpoch };
37
+ }
38
+ export function validateApprovedRuleRevisionResult(value) {
39
+ if (!record(value))
40
+ return null;
41
+ if (value.status === 'refused')
42
+ return refused(value);
43
+ if ((value.status !== 'started' && value.status !== 'already_started') || !text(value.artificerTaskId)
44
+ || !text(value.causeId) || !version(value.taskRevisionEpoch))
45
+ return null;
46
+ return { status: value.status, artificerTaskId: value.artificerTaskId,
47
+ causeId: value.causeId, taskRevisionEpoch: value.taskRevisionEpoch };
48
+ }
@@ -4,6 +4,7 @@ import type { OwnerDecisionsData, OwnerResolutionResultData, OwnerDecisionInboxD
4
4
  import type { DesignGateAction, DesignGateDecision, DesignGateDeclaration, DesignGateScope } from "@principles/core/runtime-v2/internalization/design-gate-facts";
5
5
  import type { RuleBindingReviewView, RuleBindingQueueView } from '../shared/rule-candidate-binding-contract.js';
6
6
  import type { RuleCandidateBindingRequest } from '@principles/core/runtime-v2';
7
+ import type { ApprovedRuleRevisionPreview, ApprovedRuleRevisionRequest, ApprovedRuleRevisionResult } from '../shared/approved-rule-revision-contract.js';
7
8
  import type { GovernanceExperienceSnapshot, OwnerGovernanceView, OwnerDecisionViewCore } from '@principles/core/runtime-v2';
8
9
  import type { OwnerIdentityViewData, OwnerIdentityRegisterData, OwnerIdentityUnregisterData } from "./utils/validators.js";
9
10
  import type { FeedbackReportData, FeedbackDraftSummaryData, FeedbackDraftEnvelopeData, FeedbackChannelsData, FeedbackSubmitResultData, DeleteEnvelopeData, WorkspaceEntryData, RemovedEnvelopeData, SyncResultData, ConfigSummaryData, ConfigCatalogData, AgentBindingUpdateData, DefaultRuntimeUpdateData, RuntimeProfileMutationData, ReadinessCheckData, FeatureFlagUpdateData, OutputLanguageData, GovernanceQueueData, RecoveryResultData, FailedTaskDetailData, ActivationsData, DisableActivationData, LifecycleMetricsData, UpdateStatusData, UpdateHistoryData, ApplyUpdateResultData, UpdateRecoveryData, UpdateTransactionData, ApprovalRecordData, PrinciplesListData, CorePrinciplesData, ApprovalsGroupedData, EvidenceChainData, TrajectoryData, IntentSummaryData, IntentDecisionRecordData, IntentDecisionResultData, IntentDecisionSummaryData, FollowUpResponseData, IntentInitResultData, IntentSaveResultData, IntentRawContentData, IntentVersionData, RuleCodeOwnerReviewData, RuleCodeMutationData } from "./utils/validators.js";
@@ -311,6 +312,8 @@ declare function fetchApprovalsGrouped(targetHost?: 'openclaw' | 'codex' | 'work
311
312
  declare function fetchAllActivations(targetHost?: 'openclaw' | 'codex' | 'workbuddy'): Promise<ApiResponse<ActivationsData>>;
312
313
  declare function disableActivation(activationId: string): Promise<ApiResponse<DisableActivationData>>;
313
314
  declare function fetchRuleCodeOwnerReview(activationId: string): Promise<ApiResponse<RuleCodeOwnerReviewData>>;
315
+ export declare function fetchApprovedRuleRevisionPreview(activationId: string): Promise<ApiResponse<ApprovedRuleRevisionPreview>>;
316
+ export declare function reviseApprovedRuleOnce(activationId: string, input: ApprovedRuleRevisionRequest): Promise<ApiResponse<ApprovedRuleRevisionResult>>;
314
317
  export interface ProposeRevisionData {
315
318
  ok: boolean;
316
319
  alreadyPending?: boolean;
@@ -1,5 +1,6 @@
1
1
  import { validateApprovalReview } from './utils/approval-review.js';
2
2
  import { clearStoredToken, loadStoredToken, storeToken } from "./utils/token-storage.js";
3
+ import { validateApprovedRuleRevisionPreview, validateApprovedRuleRevisionResult } from '../shared/approved-rule-revision-contract.js';
3
4
  import { validateBindingReview, validateBindingQueue, validateBindingMutation, validateBindingRevise } from './utils/rule-binding-validators.js';
4
5
  import { validateErrorResponse, validateHeaders, validateFeedbackReport, validateFeedbackDraftsList, validateFeedbackDraftEnvelope, validateFeedbackChannels, validateFeedbackSubmitResult, validateDeleteEnvelope, validateWorkspaceEntry, validateWorkspaceList, validateRemovedEnvelope, validateSyncResult, validateConfigSummary, validateConfigCatalog, validateAgentBindingUpdate, validateDefaultRuntimeUpdate, validateRuntimeProfileMutation, validateReadinessCheck, validateFeatureFlagUpdate, validateOutputLanguage, validateGovernanceQueue, validateRecoveryResult, validateFailedTaskDetail, validateOwnerDecisionsData, validateOwnerResolutionResult, validateActivations, validateDisableActivation, validateLifecycleMetrics, validateUpdateStatus, validateUpdateHistory, validateApplyUpdateResult, validateUpdateRecovery, validateUpdateTransaction, validateApprovalRecordDirect, validatePrinciplesList, validateCorePrinciples, validateApprovalsGrouped, validateEvidenceChain, validateTrajectoryData, validateIntentSummary, validateIntentDecisionList, validateIntentDecisionResult, validateIntentDecisionSummary, validateFollowUpResponse, validateIntentInitResult, validateIntentSaveResult, validateIntentRawContent, validateIntentVersions, validateOwnerGovernanceView, validateOwnerDecisionViewCore, validateOwnerDecisionInbox, validateOwnerDecisionLibrary, validateGovernanceExperienceSnapshot, validateRuleCodeOwnerReview, validateRuleCodeMutation, validateOwnerIdentityView, validateOwnerIdentityRegister, validateOwnerIdentityUnregister, } from "./utils/validators.js";
5
6
  // ── Auth ──────────────────────────────────────────────────────────────────────
@@ -793,6 +794,14 @@ async function disableActivation(activationId) {
793
794
  async function fetchRuleCodeOwnerReview(activationId) {
794
795
  return request(`/api/v1/activations/${encodeURIComponent(activationId)}/owner-review`, undefined, validateRuleCodeOwnerReview);
795
796
  }
797
+ export async function fetchApprovedRuleRevisionPreview(activationId) {
798
+ return request(`/api/v1/activations/${encodeURIComponent(activationId)}/rule-revision-preview`, undefined, validateApprovedRuleRevisionPreview);
799
+ }
800
+ export async function reviseApprovedRuleOnce(activationId, input) {
801
+ return request(`/api/v1/activations/${encodeURIComponent(activationId)}/revise-rule-once`, {
802
+ method: 'POST', body: JSON.stringify(input),
803
+ }, validateApprovedRuleRevisionResult);
804
+ }
796
805
  async function proposePromptRevision(activationId, statement, targetHost) {
797
806
  return request(`/api/v1/activations/${encodeURIComponent(activationId)}/propose-revision`, {
798
807
  method: 'POST',
@@ -1371,6 +1371,13 @@
1371
1371
  "evaluationErrors": "Errors",
1372
1372
  "notCollected": "Not collected",
1373
1373
  "scopeArtifactCode": "Scope, artifact and implementation",
1374
+ "ruleRevisionTitle": "Revise this rule for a new review",
1375
+ "ruleRevisionReviewQueue": "View review queue",
1376
+ "ruleRevisionBoundary": "Revise once using this feedback. Existing code and approvals are preserved. New code requires fresh approval and will not activate automatically.",
1377
+ "ruleRevisionFeedback": "Required correction (up to 4000 characters)",
1378
+ "ruleRevisionOnce": "Revise once with feedback",
1379
+ "ruleRevisionStarted": "Revision queued. The result will appear for review; the current rule stays in shadow.",
1380
+ "ruleRevisionAlreadyExists": "This artifact already has a revision; this request did not start a new one.",
1374
1381
  "ownerReviewNote": "Owner review note",
1375
1382
  "continueObserving": "Continue observing",
1376
1383
  "rejectAfterShadow": "Reject and deactivate",
@@ -1819,6 +1826,8 @@
1819
1826
  "reasonLabel": "Reason: ",
1820
1827
  "recoveryBanner": "An unfinished update transaction was found",
1821
1828
  "recoveryItem": "Transaction {{id}} stopped at phase \"{{phase}}\"",
1829
+ "recoveryRequiredBanner": "A failed update left the installation inconsistent (recovery required)",
1830
+ "recoveryRequiredItem": "Failed transaction {{id}} needs recovery — re-run the official installer",
1822
1831
  "inProgressToast": "Update transaction accepted and running in the background; do not click again.",
1823
1832
  "inProgressPanel": "Update in progress",
1824
1833
  "inProgressPhase": "Current phase: {{phase}}",
@@ -1371,6 +1371,13 @@
1371
1371
  "evaluationErrors": "异常",
1372
1372
  "notCollected": "未采集",
1373
1373
  "scopeArtifactCode": "范围、产物与实现代码",
1374
+ "ruleRevisionTitle": "修正规则后重新审阅",
1375
+ "ruleRevisionReviewQueue": "查看待审列表",
1376
+ "ruleRevisionBoundary": "只按本次反馈修订一次,保留旧规则和审批记录。新代码需重新审阅批准,不会自动启用。",
1377
+ "ruleRevisionFeedback": "需要修正什么(最多4000字)",
1378
+ "ruleRevisionOnce": "按反馈修订一次",
1379
+ "ruleRevisionStarted": "修订已排队。完成后将在待审列表出现;当前规则保持影子状态。",
1380
+ "ruleRevisionAlreadyExists": "该工件已有修订,本次请求未启动新的修订。",
1374
1381
  "ownerReviewNote": "拥有者评审备注",
1375
1382
  "continueObserving": "继续观察",
1376
1383
  "rejectAfterShadow": "拒绝并停用",
@@ -1819,6 +1826,8 @@
1819
1826
  "reasonLabel": "原因:",
1820
1827
  "recoveryBanner": "检测到未完成的更新事务",
1821
1828
  "recoveryItem": "事务 {{id}} 停留在阶段「{{phase}}」",
1829
+ "recoveryRequiredBanner": "一次失败的更新使安装处于不一致状态(需要恢复)",
1830
+ "recoveryRequiredItem": "失败事务 {{id}} 需要恢复——请重新运行官方安装器",
1822
1831
  "inProgressToast": "更新事务已受理,正在后台执行;请勿重复点击。",
1823
1832
  "inProgressPanel": "更新进行中",
1824
1833
  "inProgressPhase": "当前阶段:{{phase}}",