create-principles-disciple 1.133.13 → 1.134.1
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/codex-adapter/package.json +3 -2
- package/console/dist/server/routes/update.js +181 -34
- package/console/dist/server/update/legacy-mutation-journal.d.ts +118 -0
- package/console/dist/server/update/legacy-mutation-journal.js +220 -0
- package/console/dist/server/utils/cli-smoke.d.ts +19 -0
- package/console/dist/server/utils/cli-smoke.js +60 -0
- package/console/dist/server/utils/installed-layout.d.ts +8 -0
- package/console/dist/server/utils/installed-layout.js +4 -0
- package/console/dist/ui/i18n/en.json +2 -2
- package/console/dist/ui/i18n/zh-CN.json +2 -2
- package/console/dist/web/assets/app.js +85 -15
- package/console/package.json +2 -2
- package/core/dist/runtime-v2/__tests__/recovery-sweep-service.test.js +130 -0
- package/core/dist/runtime-v2/__tests__/recovery-sweep-service.test.js.map +1 -1
- package/core/dist/runtime-v2/recovery-sweep-service.d.ts +9 -0
- package/core/dist/runtime-v2/recovery-sweep-service.d.ts.map +1 -1
- package/core/dist/runtime-v2/recovery-sweep-service.js +7 -2
- package/core/dist/runtime-v2/recovery-sweep-service.js.map +1 -1
- package/core/package.json +1 -1
- package/dist/installer.d.ts +69 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +200 -14
- package/dist/installer.js.map +1 -1
- package/dist/uninstaller.d.ts.map +1 -1
- package/dist/uninstaller.js +7 -3
- package/dist/uninstaller.js.map +1 -1
- package/dist/update/install-layout.d.ts +39 -2
- package/dist/update/install-layout.d.ts.map +1 -1
- package/dist/update/install-layout.js +69 -3
- package/dist/update/install-layout.js.map +1 -1
- package/dist/update/release-manager-authority.d.ts +8 -0
- package/dist/update/release-manager-authority.d.ts.map +1 -1
- package/dist/update/release-manager-authority.js +26 -4
- package/dist/update/release-manager-authority.js.map +1 -1
- package/dist/update/release-metadata-source.d.ts +85 -0
- package/dist/update/release-metadata-source.d.ts.map +1 -0
- package/dist/update/release-metadata-source.js +102 -0
- package/dist/update/release-metadata-source.js.map +1 -0
- package/install-layout/dist/index.d.ts +8 -0
- package/install-layout/dist/index.js +1 -0
- package/install-layout/package.json +1 -1
- package/package.json +2 -2
- package/pd-cli/dist/commands/runtime-recovery-failed-tasks.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-recovery-failed-tasks.js +82 -6
- package/pd-cli/dist/commands/runtime-recovery-failed-tasks.js.map +1 -1
- package/pd-cli/dist/index.js +6 -3
- package/pd-cli/dist/index.js.map +1 -1
- package/pd-cli/package.json +1 -1
- package/plugin/dist/bundle.js +44 -44
- package/plugin/dist/governance-audit.js +139 -139
- package/plugin/dist/rulehost-evidence.js +144 -144
- package/release-manager/dist/installer.d.ts +69 -0
- package/release-manager/dist/installer.d.ts.map +1 -1
- package/release-manager/dist/installer.js +200 -14
- package/release-manager/dist/installer.js.map +1 -1
- package/release-manager/dist/uninstaller.d.ts.map +1 -1
- package/release-manager/dist/uninstaller.js +7 -3
- package/release-manager/dist/uninstaller.js.map +1 -1
- package/release-manager/dist/update/install-layout.d.ts +39 -2
- package/release-manager/dist/update/install-layout.d.ts.map +1 -1
- package/release-manager/dist/update/install-layout.js +69 -3
- package/release-manager/dist/update/install-layout.js.map +1 -1
- package/release-manager/dist/update/release-manager-authority.d.ts +8 -0
- package/release-manager/dist/update/release-manager-authority.d.ts.map +1 -1
- package/release-manager/dist/update/release-manager-authority.js +26 -4
- package/release-manager/dist/update/release-manager-authority.js.map +1 -1
- package/release-manager/dist/update/release-metadata-source.d.ts +85 -0
- package/release-manager/dist/update/release-metadata-source.d.ts.map +1 -0
- package/release-manager/dist/update/release-metadata-source.js +102 -0
- package/release-manager/dist/update/release-metadata-source.js.map +1 -0
- package/release-manager/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/codex-adapter",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Codex CLI host adapter for Principles Disciple — implements HostAdapter interface for OpenAI Codex CLI's stdin/stdout JSON hook model (ADR-0020).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@principles/core": "file:../core",
|
|
35
|
-
"@principles/host-runtime": "file:../host-runtime"
|
|
35
|
+
"@principles/host-runtime": "file:../host-runtime",
|
|
36
|
+
"@principles/install-layout": "file:../install-layout"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@types/node": "^26.4.1",
|
|
@@ -10,7 +10,9 @@ import { migrateLegacyExtensionBackups, reservePdBackupDestination, resolvePdBac
|
|
|
10
10
|
import { resolveExtensionsDir, resolveUpdateLayout, resolvePluginDir, resolveCanonicalRuntimeRoot, readCurrentVersion, } from '../utils/installed-layout.js';
|
|
11
11
|
import { ActivationCompatibilityReadModel, isFeatureEnabled } from '@principles/core/runtime-v2';
|
|
12
12
|
import { collectFileDepLinkSpecs } from '../utils/update-links.js';
|
|
13
|
+
import { runPostUpdateCliSmoke } from '../utils/cli-smoke.js';
|
|
13
14
|
import { updateMutationController, LEGACY_MUTATION_AUTHORITY, RELEASE_MANAGER_AUTHORITY, MUTATION_KINDS, } from '../update/mutation-controller.js';
|
|
15
|
+
import { runLegacyJournaledMutation } from '../update/legacy-mutation-journal.js';
|
|
14
16
|
import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
|
|
15
17
|
/**
|
|
16
18
|
* Legacy rule contract preflight (2026-08-19): refuse to swap the runtime
|
|
@@ -48,6 +50,10 @@ const WORKSPACE_FILES = ['AGENTS.md', 'SOUL.md', 'USER.md', 'CLAUDE.md'];
|
|
|
48
50
|
// .node addons locked by the gateway/console processes (EPERM on copyfile),
|
|
49
51
|
// npm symlinks/junctions, and thousands of regenerable files.
|
|
50
52
|
const SKIP_DIRS = new Set(['node_modules']);
|
|
53
|
+
// Full-update backups extend the existing plugin backup with sibling code
|
|
54
|
+
// trees. Dependency directories stay in place (native modules may be loaded).
|
|
55
|
+
const RUNTIME_BACKUP_DIR = '.runtime-components';
|
|
56
|
+
const BACKUP_SKIP_DIRS = new Set(['node_modules', RUNTIME_BACKUP_DIR]);
|
|
51
57
|
// ---------------------------------------------------------------------------
|
|
52
58
|
// Helpers
|
|
53
59
|
// ---------------------------------------------------------------------------
|
|
@@ -222,6 +228,58 @@ function copyFileTo(src, dest) {
|
|
|
222
228
|
fs.mkdirSync(dir, { recursive: true });
|
|
223
229
|
fs.copyFileSync(src, dest);
|
|
224
230
|
}
|
|
231
|
+
function runtimeComponents(layout) {
|
|
232
|
+
return {
|
|
233
|
+
console: layout.consoleDir,
|
|
234
|
+
'pd-cli': layout.pdCliDir,
|
|
235
|
+
'host-runtime': layout.hostRuntimeDir,
|
|
236
|
+
'install-layout': layout.installLayoutDir,
|
|
237
|
+
core: layout.coreDir,
|
|
238
|
+
plugin: layout.pluginDir,
|
|
239
|
+
'release-manager': layout.releaseManagerDir,
|
|
240
|
+
...(layout.codexAdapterDir ? { 'codex-adapter': layout.codexAdapterDir } : {}),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** Resolve a backup entry under the backup root, refusing escapes — the
|
|
244
|
+
* component names recorded in components.json are untrusted runtime data
|
|
245
|
+
* (rc-1/rc-2), so every join into the backup tree is boundary-checked. */
|
|
246
|
+
function backupEntryPath(root, name) {
|
|
247
|
+
const target = path.resolve(root, name);
|
|
248
|
+
if (target === root || !target.startsWith(root + path.sep)) {
|
|
249
|
+
throw new Error(`Runtime backup entry "${name}" escapes the backup root`);
|
|
250
|
+
}
|
|
251
|
+
return target;
|
|
252
|
+
}
|
|
253
|
+
function backupRuntimeComponents(layout, backupDir) {
|
|
254
|
+
const root = path.join(backupDir, RUNTIME_BACKUP_DIR);
|
|
255
|
+
fs.mkdirSync(root, { recursive: true });
|
|
256
|
+
const present = [];
|
|
257
|
+
for (const [name, dir] of Object.entries(runtimeComponents(layout))) {
|
|
258
|
+
if (name === 'plugin' || !fs.existsSync(dir))
|
|
259
|
+
continue;
|
|
260
|
+
copyDirRecursive(dir, backupEntryPath(root, name), BACKUP_SKIP_DIRS);
|
|
261
|
+
present.push(name);
|
|
262
|
+
}
|
|
263
|
+
// Written last: a partial backup must never be accepted as recoverable.
|
|
264
|
+
fs.writeFileSync(path.join(root, 'components.json'), JSON.stringify(present));
|
|
265
|
+
}
|
|
266
|
+
function restoreRuntimeComponents(layout, backupDir) {
|
|
267
|
+
const root = path.join(backupDir, RUNTIME_BACKUP_DIR);
|
|
268
|
+
if (!fs.existsSync(root))
|
|
269
|
+
return false; // Historical plugin-only backup.
|
|
270
|
+
const present = JSON.parse(fs.readFileSync(path.join(root, 'components.json'), 'utf8'));
|
|
271
|
+
const components = runtimeComponents(layout);
|
|
272
|
+
if (!Array.isArray(present) || present.some((name) => typeof name !== 'string' || name === 'plugin' || !Object.hasOwn(components, name)
|
|
273
|
+
|| !fs.existsSync(backupEntryPath(root, name)))) {
|
|
274
|
+
throw new Error('Runtime backup is incomplete or incompatible with this installation');
|
|
275
|
+
}
|
|
276
|
+
for (const [name, dir] of Object.entries(components)) {
|
|
277
|
+
if (name === 'plugin' || !present.includes(name))
|
|
278
|
+
continue;
|
|
279
|
+
copyDirRecursive(backupEntryPath(root, name), dir, BACKUP_SKIP_DIRS);
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
225
283
|
// --- Plugin skill-language preservation (PR #1332 companion) -----------------
|
|
226
284
|
// OpenClaw publishes plugin skills by directory name (first declaration
|
|
227
285
|
// wins, same-name roots only warn — no locale mechanism), so a manifest must
|
|
@@ -748,7 +806,11 @@ async function doRollbackUpdate(options, workspaceDir) {
|
|
|
748
806
|
// dependencies. Instead, overwrite from backup: modified files (dist/,
|
|
749
807
|
// package.json, etc.) get the backup version, while node_modules/console/
|
|
750
808
|
// core/ (not in the backup) are left untouched.
|
|
751
|
-
|
|
809
|
+
const layout = resolveUpdateLayout();
|
|
810
|
+
const fullBackup = layout && path.resolve(layout.pluginDir) === path.resolve(targetDir)
|
|
811
|
+
? restoreRuntimeComponents(layout, backupDir)
|
|
812
|
+
: false;
|
|
813
|
+
copyDirRecursive(backupDir, targetDir, BACKUP_SKIP_DIRS);
|
|
752
814
|
// Keep the OpenClaw extension copy in sync with the restored canonical
|
|
753
815
|
// plugin (CP-5 invariant from the 2026-09-05 investigation): OpenClaw
|
|
754
816
|
// loads ~/.openclaw/extensions/principles-disciple, so a rollback that
|
|
@@ -758,9 +820,29 @@ async function doRollbackUpdate(options, workspaceDir) {
|
|
|
758
820
|
const openClawPluginCopy = path.join(resolveExtensionsDir(), 'principles-disciple');
|
|
759
821
|
let extCopySynced = false;
|
|
760
822
|
if (path.resolve(openClawPluginCopy) !== path.resolve(targetDir) && fs.existsSync(openClawPluginCopy)) {
|
|
761
|
-
copyDirRecursive(backupDir, openClawPluginCopy);
|
|
823
|
+
copyDirRecursive(backupDir, openClawPluginCopy, BACKUP_SKIP_DIRS);
|
|
762
824
|
extCopySynced = true;
|
|
763
825
|
}
|
|
826
|
+
if (fullBackup && layout) {
|
|
827
|
+
const smoke = runPostUpdateCliSmoke(layout.pdCliDir);
|
|
828
|
+
if (!smoke.ok) {
|
|
829
|
+
// rc-9: a failed rollback must be observable in history, like every
|
|
830
|
+
// other failure path — not just in the HTTP response.
|
|
831
|
+
appendUpdateHistory(workspaceDir, {
|
|
832
|
+
fromVersion: 'rolled-back',
|
|
833
|
+
toVersion: readCurrentVersion(targetDir) ?? 'unknown',
|
|
834
|
+
success: false,
|
|
835
|
+
kind: 'failure',
|
|
836
|
+
backupPath: backupDir,
|
|
837
|
+
});
|
|
838
|
+
return {
|
|
839
|
+
success: false,
|
|
840
|
+
message: `Runtime backup restored but pd CLI still fails to start: ${smoke.error}`,
|
|
841
|
+
reason: 'rollback_cli_smoke_failed',
|
|
842
|
+
nextAction: 'Keep the backup and use the official installer pinned to the previous installer release to repair dependencies before restarting Console.',
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
}
|
|
764
846
|
// Record rollback history
|
|
765
847
|
appendUpdateHistory(workspaceDir, {
|
|
766
848
|
fromVersion: 'rolled-back',
|
|
@@ -771,9 +853,11 @@ async function doRollbackUpdate(options, workspaceDir) {
|
|
|
771
853
|
});
|
|
772
854
|
return {
|
|
773
855
|
success: true,
|
|
774
|
-
message:
|
|
775
|
-
? '
|
|
776
|
-
:
|
|
856
|
+
message: fullBackup
|
|
857
|
+
? 'Runtime components restored and pd CLI startup verified. Restart Console to load the restored code.'
|
|
858
|
+
: extCopySynced
|
|
859
|
+
? 'Rollback completed successfully (OpenClaw extension copy restored too). Note: only the plugin was rolled back; console/core/host-runtime/pd-cli keep the upgraded version. For a full step-back, re-run the official installer pinned to the older release.'
|
|
860
|
+
: 'Rollback completed successfully. Note: only the plugin was rolled back; console/core/host-runtime/pd-cli keep the upgraded version. For a full step-back, re-run the official installer pinned to the older release.',
|
|
777
861
|
};
|
|
778
862
|
}
|
|
779
863
|
catch (error) {
|
|
@@ -997,19 +1081,9 @@ function ensureRuntimeResolutionLinks(layout, tempDir) {
|
|
|
997
1081
|
];
|
|
998
1082
|
// Data-driven pass: derive links from the STAGED component manifests (see
|
|
999
1083
|
// the comment above — staged, never the deployed pre-update manifests).
|
|
1000
|
-
const stagedComponents = [
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
{ manifestDir: path.join(tempDir, 'host-runtime'), deployedDir: layout.hostRuntimeDir },
|
|
1004
|
-
{ manifestDir: path.join(tempDir, 'install-layout'), deployedDir: layout.installLayoutDir },
|
|
1005
|
-
{ manifestDir: path.join(tempDir, 'core'), deployedDir: layout.coreDir },
|
|
1006
|
-
{ manifestDir: path.join(tempDir, 'plugin'), deployedDir: layout.pluginDir },
|
|
1007
|
-
// PRI-672: staged release-manager component (npm name
|
|
1008
|
-
// create-principles-disciple). Its staged manifest carries the
|
|
1009
|
-
// file:../install-layout ref, and the staged console's manifest carries
|
|
1010
|
-
// file:../release-manager — both derive their resolution links here.
|
|
1011
|
-
{ manifestDir: path.join(tempDir, 'release-manager'), deployedDir: layout.releaseManagerDir },
|
|
1012
|
-
];
|
|
1084
|
+
const stagedComponents = Object.entries(runtimeComponents(layout)).map(([name, deployedDir]) => ({
|
|
1085
|
+
manifestDir: path.join(tempDir, name), deployedDir,
|
|
1086
|
+
}));
|
|
1013
1087
|
const readStagedDependencies = (manifestDir) => {
|
|
1014
1088
|
try {
|
|
1015
1089
|
const pkgPath = path.join(manifestDir, 'package.json');
|
|
@@ -1262,9 +1336,8 @@ async function doInlineFullUpdate(workspaceDir) {
|
|
|
1262
1336
|
// 2026-09-05 install/upgrade investigation). The backup excludes
|
|
1263
1337
|
// node_modules (same SKIP_DIRS contract as /apply), and the backup dir
|
|
1264
1338
|
// is recorded in update-history (success AND failure) so the rollback
|
|
1265
|
-
// handler can find it.
|
|
1266
|
-
//
|
|
1267
|
-
// next update, so only the version authority (plugin) is backed up.
|
|
1339
|
+
// handler can find it. Include every sibling code tree: a plugin-only
|
|
1340
|
+
// rollback cannot repair a CLI/adapter import failure after an update.
|
|
1268
1341
|
// R5 (review): reservePdBackupDestination creates the directory BEFORE
|
|
1269
1342
|
// the copy — a mid-copy failure must not leave backupPath pointing at a
|
|
1270
1343
|
// partial backup (a later /rollback would happily restore the fragment).
|
|
@@ -1275,6 +1348,7 @@ async function doInlineFullUpdate(workspaceDir) {
|
|
|
1275
1348
|
const backupDest = reservePdBackupDestination(path.basename(extDir));
|
|
1276
1349
|
try {
|
|
1277
1350
|
copyDirRecursive(extDir, backupDest, SKIP_DIRS);
|
|
1351
|
+
backupRuntimeComponents(layout, backupDest);
|
|
1278
1352
|
}
|
|
1279
1353
|
catch (backupError) {
|
|
1280
1354
|
try {
|
|
@@ -1306,6 +1380,23 @@ async function doInlineFullUpdate(workspaceDir) {
|
|
|
1306
1380
|
fs.existsSync(path.join(releaseManagerSrc, 'dist'))) {
|
|
1307
1381
|
copyDirRecursive(releaseManagerSrc, layout.releaseManagerDir, SKIP_DIRS);
|
|
1308
1382
|
}
|
|
1383
|
+
// PRI-711: same PRI-561 ordering for codex-adapter — pd-cli's eager
|
|
1384
|
+
// import graph statically resolves it, so an updated pd-cli paired with
|
|
1385
|
+
// a stale deployed adapter is exactly the generation drift that bricked
|
|
1386
|
+
// the 1.231.2 update (health-codex imported symbols the deployed
|
|
1387
|
+
// adapter happened to carry; the next API addition would not). The
|
|
1388
|
+
// copy keeps the deployed node_modules (SKIP_DIRS), so its resolution
|
|
1389
|
+
// links survive; the data-driven pass below re-derives them anyway.
|
|
1390
|
+
// Skipped when the running console resolves a one-generation-old
|
|
1391
|
+
// install-layout without codexAdapterDir — that generation also lacks
|
|
1392
|
+
// the derivation entry, mirroring the old no-op behavior (rc-9).
|
|
1393
|
+
const codexAdapterDest = layout.codexAdapterDir;
|
|
1394
|
+
if (codexAdapterDest !== undefined &&
|
|
1395
|
+
fs.existsSync(path.join(tempDir, 'codex-adapter')) &&
|
|
1396
|
+
fs.existsSync(path.join(tempDir, 'codex-adapter', 'package.json')) &&
|
|
1397
|
+
fs.existsSync(path.join(tempDir, 'codex-adapter', 'dist'))) {
|
|
1398
|
+
copyDirRecursive(path.join(tempDir, 'codex-adapter'), codexAdapterDest, SKIP_DIRS);
|
|
1399
|
+
}
|
|
1309
1400
|
const { error: linkError, quarantined } = ensureRuntimeResolutionLinks(layout, tempDir);
|
|
1310
1401
|
reconciledQuarantine = quarantined;
|
|
1311
1402
|
if (linkError) {
|
|
@@ -1382,12 +1473,32 @@ async function doInlineFullUpdate(workspaceDir) {
|
|
|
1382
1473
|
if (tempDir && fs.existsSync(tempDir)) {
|
|
1383
1474
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1384
1475
|
}
|
|
1385
|
-
// 6.
|
|
1386
|
-
//
|
|
1387
|
-
//
|
|
1388
|
-
//
|
|
1389
|
-
|
|
1390
|
-
|
|
1476
|
+
// 6.8 PRI-711: post-apply CLI smoke. The swap and link steps above are
|
|
1477
|
+
// data-driven but not self-proving — run the updated CLI once and refuse
|
|
1478
|
+
// to record a success if the eager import graph is broken (see
|
|
1479
|
+
// runPostUpdateCliSmoke for the three precedents this catches). The
|
|
1480
|
+
// backup recorded below keeps the failure recoverable via /rollback.
|
|
1481
|
+
const smoke = runPostUpdateCliSmoke(layout.pdCliDir);
|
|
1482
|
+
if (!smoke.ok) {
|
|
1483
|
+
restoreQuarantined(reconciledQuarantine);
|
|
1484
|
+
reconciledQuarantine = [];
|
|
1485
|
+
const failedVersion = readCurrentVersion(extDir) ?? toVersion ?? 'unknown';
|
|
1486
|
+
appendUpdateHistory(workspaceDir, {
|
|
1487
|
+
fromVersion,
|
|
1488
|
+
toVersion: failedVersion,
|
|
1489
|
+
success: false,
|
|
1490
|
+
kind: 'failure',
|
|
1491
|
+
...(pluginBackupDir ? { backupPath: pluginBackupDir } : {}),
|
|
1492
|
+
});
|
|
1493
|
+
return {
|
|
1494
|
+
success: false,
|
|
1495
|
+
message: `Update applied but the updated pd CLI fails to start: ${smoke.error}`,
|
|
1496
|
+
reason: 'post_update_cli_smoke_failed',
|
|
1497
|
+
nextAction: pluginBackupDir
|
|
1498
|
+
? 'Restore the backup recorded in update history (backupPath), then report the stderr above. The console must be restarted before further use.'
|
|
1499
|
+
: 'Report the stderr above; no backup was reserved for this update. The console must be restarted before further use.',
|
|
1500
|
+
requiresRestart: false,
|
|
1501
|
+
};
|
|
1391
1502
|
}
|
|
1392
1503
|
// 7. Version-advance check (drift guard). The full update installs the
|
|
1393
1504
|
// plugin bundled inside the installer. If the installer is stale (its
|
|
@@ -1399,6 +1510,8 @@ async function doInlineFullUpdate(workspaceDir) {
|
|
|
1399
1510
|
const newVersion = readCurrentVersion(extDir) ?? toVersion ?? 'unknown';
|
|
1400
1511
|
const installedExpectedRelease = newVersion === stagedVersion;
|
|
1401
1512
|
if (!installedExpectedRelease) {
|
|
1513
|
+
restoreQuarantined(reconciledQuarantine);
|
|
1514
|
+
reconciledQuarantine = [];
|
|
1402
1515
|
// Files were already rewritten to the same (or lower) version. Record a
|
|
1403
1516
|
// FAILED history entry so the operator sees why, and return a structured
|
|
1404
1517
|
// error with nextAction.
|
|
@@ -1425,6 +1538,10 @@ async function doInlineFullUpdate(workspaceDir) {
|
|
|
1425
1538
|
kind: 'update',
|
|
1426
1539
|
...(pluginBackupDir ? { backupPath: pluginBackupDir } : {}),
|
|
1427
1540
|
});
|
|
1541
|
+
// The quarantined pre-update copies are discarded on success; no reset of
|
|
1542
|
+
// the variable is needed here — the return below cannot re-enter the
|
|
1543
|
+
// catch (CodeQL js/useless-assignment-to-local, PR #1580 review).
|
|
1544
|
+
cleanupQuarantined(reconciledQuarantine);
|
|
1428
1545
|
return {
|
|
1429
1546
|
success: true,
|
|
1430
1547
|
message: depsChanged
|
|
@@ -1540,7 +1657,26 @@ function legacyCheckMutation(req, res, ctx) {
|
|
|
1540
1657
|
}
|
|
1541
1658
|
})();
|
|
1542
1659
|
}
|
|
1660
|
+
/**
|
|
1661
|
+
* PRI-709 P0-3 (ADR-0024 D-2): the shared installation root that owns the
|
|
1662
|
+
* transaction journal. Legacy console mutations journal to the SAME place as
|
|
1663
|
+
* the installer and the ReleaseManager — never a console-private ledger.
|
|
1664
|
+
*/
|
|
1665
|
+
function resolvePdHome() {
|
|
1666
|
+
return path.join(os.homedir(), '.pd');
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* A runtime mutation that ran without journal evidence is a governance gap,
|
|
1670
|
+
* not a mutation failure: it is reported loud (rc-9) and the mutation result
|
|
1671
|
+
* is still served. Blocking the Owner's update here would be worse than an
|
|
1672
|
+
* unaudited one — but the gap must never be silent.
|
|
1673
|
+
*/
|
|
1674
|
+
function logLegacyJournalGap(kind, reason) {
|
|
1675
|
+
console.warn(`[update] Runtime mutation "${kind}" completed WITHOUT journal evidence (${reason}). `
|
|
1676
|
+
+ 'ADR-0024 D-2 requires every runtime mutation to be auditable; this is usually a missing create-principles-disciple install surface.');
|
|
1677
|
+
}
|
|
1543
1678
|
function legacyApplyMutation(req, res, ctx) {
|
|
1679
|
+
const pdHome = resolvePdHome();
|
|
1544
1680
|
return (async () => {
|
|
1545
1681
|
const pluginDir = resolvePluginDir(ctx.workspaceDir);
|
|
1546
1682
|
if (req.method !== 'POST') {
|
|
@@ -1578,11 +1714,12 @@ function legacyApplyMutation(req, res, ctx) {
|
|
|
1578
1714
|
sendBadRequest(res, 'targetDir must be within workspace or extensions directory');
|
|
1579
1715
|
return;
|
|
1580
1716
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1717
|
+
// ADR-0024 D-2 (PRI-709 P0-3): every runtime mutation is journaled.
|
|
1718
|
+
// `planned` lands after all request validation, so a rejected request
|
|
1719
|
+
// leaves no transaction behind.
|
|
1720
|
+
const { result, journal } = await runLegacyJournaledMutation({ kind: 'apply', pdHome, pluginDir }, () => doApplyUpdate({ targetDir, mergeStrategy, createBackup }, ctx.workspaceDir));
|
|
1721
|
+
if (!journal.journaled)
|
|
1722
|
+
logLegacyJournalGap('apply', journal.reason);
|
|
1586
1723
|
sendSuccess(res, result);
|
|
1587
1724
|
}
|
|
1588
1725
|
catch (err) {
|
|
@@ -1595,6 +1732,7 @@ function legacyApplyMutation(req, res, ctx) {
|
|
|
1595
1732
|
})();
|
|
1596
1733
|
}
|
|
1597
1734
|
function legacyRollbackMutation(req, res, ctx) {
|
|
1735
|
+
const pdHome = resolvePdHome();
|
|
1598
1736
|
return (async () => {
|
|
1599
1737
|
const pluginDir = resolvePluginDir(ctx.workspaceDir);
|
|
1600
1738
|
if (req.method !== 'POST') {
|
|
@@ -1626,7 +1764,12 @@ function legacyRollbackMutation(req, res, ctx) {
|
|
|
1626
1764
|
sendBadRequest(res, 'backupDir must be within the workspace, extensions directory, or PD backups directory');
|
|
1627
1765
|
return;
|
|
1628
1766
|
}
|
|
1629
|
-
|
|
1767
|
+
// ADR-0024 D-2 (PRI-709 P0-3): a rollback is a forward transition to a
|
|
1768
|
+
// known-good deployment, not an undo of this transaction — that is why
|
|
1769
|
+
// the terminal state is `confirmed`, not `rolled_back`.
|
|
1770
|
+
const { result, journal } = await runLegacyJournaledMutation({ kind: 'rollback', pdHome, pluginDir }, () => doRollbackUpdate({ targetDir, backupDir }, ctx.workspaceDir));
|
|
1771
|
+
if (!journal.journaled)
|
|
1772
|
+
logLegacyJournalGap('rollback', journal.reason);
|
|
1630
1773
|
sendSuccess(res, result);
|
|
1631
1774
|
}
|
|
1632
1775
|
catch (err) {
|
|
@@ -1639,13 +1782,17 @@ function legacyRollbackMutation(req, res, ctx) {
|
|
|
1639
1782
|
})();
|
|
1640
1783
|
}
|
|
1641
1784
|
function legacyApplyFullMutation(req, res, ctx) {
|
|
1785
|
+
const pdHome = resolvePdHome();
|
|
1642
1786
|
return (async () => {
|
|
1643
1787
|
if (req.method !== 'POST') {
|
|
1644
1788
|
sendMethodNotAllowed(res);
|
|
1645
1789
|
return;
|
|
1646
1790
|
}
|
|
1791
|
+
const pluginDir = resolvePluginDir(ctx.workspaceDir);
|
|
1647
1792
|
try {
|
|
1648
|
-
const result = await doInlineFullUpdate(ctx.workspaceDir);
|
|
1793
|
+
const { result, journal } = await runLegacyJournaledMutation({ kind: 'apply-full', pdHome, pluginDir }, () => doInlineFullUpdate(ctx.workspaceDir));
|
|
1794
|
+
if (!journal.journaled)
|
|
1795
|
+
logLegacyJournalGap('apply-full', journal.reason);
|
|
1649
1796
|
sendSuccess(res, result);
|
|
1650
1797
|
}
|
|
1651
1798
|
catch (err) {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-709 P0-3 — journal coverage for the legacy console updater (ADR-0024 D-2).
|
|
3
|
+
*
|
|
4
|
+
* PRI-698 Phase 0 Audit finding F-3: the console updater performed runtime
|
|
5
|
+
* mutations with **zero** journal writes. Under ADR-0024 D-2 an unjournaled
|
|
6
|
+
* runtime mutation is the one thing that must never happen, so the legacy
|
|
7
|
+
* updater had to be brought under the SAME journal as the installer and the
|
|
8
|
+
* ReleaseManager before the legacy path can be retired.
|
|
9
|
+
*
|
|
10
|
+
* Design rules (all load-bearing):
|
|
11
|
+
*
|
|
12
|
+
* - **No new journal implementation.** This module reuses
|
|
13
|
+
* `create-principles-disciple`'s `transaction-journal` — one JSONL file per
|
|
14
|
+
* transaction under `~/.pd/transactions/`, append + fsync, strict reader.
|
|
15
|
+
* Nothing here parses, formats or rotates journals.
|
|
16
|
+
* - **The ReleaseManager path is never double-journaled.** ReleaseManager
|
|
17
|
+
* `apply-full` orchestrates the installer, which journals the whole
|
|
18
|
+
* lifecycle; the console dispatch for that path never reaches the legacy
|
|
19
|
+
* handlers this module wraps. When the ReleaseManager explicitly falls back
|
|
20
|
+
* to legacy, the legacy handler is the ONLY writer — one transaction per
|
|
21
|
+
* mutation, whichever authority served it.
|
|
22
|
+
* - **Explicit degradation, never a blocked mutation.** The journal module is
|
|
23
|
+
* loaded dynamically: the console runs in installations where the
|
|
24
|
+
* create-principles-disciple dist may be absent (the same delivery-surface
|
|
25
|
+
* gap the ReleaseManager authority loader already handles). A missing module
|
|
26
|
+
* is reported and the mutation proceeds unjournaled — refusing the Owner's
|
|
27
|
+
* update would be worse than an unaudited one, and the gap is observable.
|
|
28
|
+
* - **No fabricated digests.** The legacy updater does not verify signed
|
|
29
|
+
* release metadata, so its digest provenance is `fallback`: a synthetic
|
|
30
|
+
* sha256 over a literal marker, readable but explicitly NOT verifiable. It
|
|
31
|
+
* never claims `manifest` / `signed_channel` it cannot back.
|
|
32
|
+
*
|
|
33
|
+
* Transition strategy for legacy kinds:
|
|
34
|
+
*
|
|
35
|
+
* apply / apply-full / rollback: `planned` → `confirmed` | `failed`
|
|
36
|
+
*
|
|
37
|
+
* `planned` is appended immediately before the mutation (after all request
|
|
38
|
+
* validation, so rejected requests leave no transaction); `confirmed` or
|
|
39
|
+
* `failed` after it. `rolled_back` is NOT used for the rollback kind — a
|
|
40
|
+
* rollback restores a previous deployment and is itself a forward transition
|
|
41
|
+
* to a known-good state, whereas `rolled_back` means "this transaction was
|
|
42
|
+
* undone".
|
|
43
|
+
*
|
|
44
|
+
* Two documented trade-offs (PRI-709 review, deliberate):
|
|
45
|
+
*
|
|
46
|
+
* - **`generation` stays at the standalone default `1`.** Legacy console
|
|
47
|
+
* transactions do not participate in the dual-slot generation lineage — they
|
|
48
|
+
* never move the active record — so recording a real `active.json.generation
|
|
49
|
+
* + 1` would CLAIM a lineage the console does not actually advance. Recovery
|
|
50
|
+
* is unaffected either way: `recoverUnfinishedTransaction` keys on
|
|
51
|
+
* `activeRecord.transactionId`, which a console transaction never matches, so
|
|
52
|
+
* it resolves to "the previously confirmed release stands".
|
|
53
|
+
* - **The legacy path leaves `active.json` untouched.** The legacy updater
|
|
54
|
+
* replaces the runtime without the installer's dual-slot swap, so after a
|
|
55
|
+
* legacy `apply-full` the active record describes the PREVIOUS deployment.
|
|
56
|
+
* Making the console a second writer of the deployment identity would be a
|
|
57
|
+
* worse violation (one source of truth); the honest fix is retiring the
|
|
58
|
+
* legacy path (ADR-0024 D-1), which is exactly what this journal coverage
|
|
59
|
+
* unblocks.
|
|
60
|
+
*/
|
|
61
|
+
import type { ReleaseMetadataDigestSource, TransactionState } from 'create-principles-disciple/dist/update/transaction-journal.js';
|
|
62
|
+
/** Journal kinds the legacy console updater performs. */
|
|
63
|
+
export type LegacyMutationKind = 'apply' | 'apply-full' | 'rollback';
|
|
64
|
+
export interface LegacyMutationJournal {
|
|
65
|
+
readonly transactionId: string;
|
|
66
|
+
readonly journalPath: string;
|
|
67
|
+
readonly releaseId: string;
|
|
68
|
+
readonly productVersion: string;
|
|
69
|
+
readonly releaseMetadataDigest: string;
|
|
70
|
+
}
|
|
71
|
+
export type LegacyJournalStatus = {
|
|
72
|
+
readonly journaled: true;
|
|
73
|
+
readonly journal: LegacyMutationJournal;
|
|
74
|
+
} | {
|
|
75
|
+
readonly journaled: false;
|
|
76
|
+
readonly reason: string;
|
|
77
|
+
};
|
|
78
|
+
/** Journal port — the real implementation is the shared transaction journal. */
|
|
79
|
+
export interface LegacyJournalPort {
|
|
80
|
+
appendJournalTransition(journalPath: string, transition: {
|
|
81
|
+
readonly at: string;
|
|
82
|
+
readonly from: TransactionState | null;
|
|
83
|
+
readonly to: TransactionState;
|
|
84
|
+
readonly transactionId: string;
|
|
85
|
+
readonly releaseId: string;
|
|
86
|
+
readonly productVersion: string;
|
|
87
|
+
readonly releaseMetadataDigest: string;
|
|
88
|
+
readonly releaseMetadataDigestSource: ReleaseMetadataDigestSource;
|
|
89
|
+
readonly generation: number;
|
|
90
|
+
readonly detail?: string;
|
|
91
|
+
}): void;
|
|
92
|
+
}
|
|
93
|
+
export interface LegacyJournalOptions {
|
|
94
|
+
/** `~/.pd` — the shared installation root (ADR-0023). */
|
|
95
|
+
readonly pdHome: string;
|
|
96
|
+
/** Installed runtime plugin dir; identity source of what is deployed. */
|
|
97
|
+
readonly pluginDir: string;
|
|
98
|
+
readonly kind: LegacyMutationKind;
|
|
99
|
+
readonly now?: () => Date;
|
|
100
|
+
/** Test seam; defaults to the dynamically imported shared journal. */
|
|
101
|
+
readonly journal?: LegacyJournalPort;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Open one legacy transaction and append `planned` before the mutation runs.
|
|
105
|
+
*/
|
|
106
|
+
export declare function openLegacyMutationJournal(options: LegacyJournalOptions): Promise<LegacyJournalStatus>;
|
|
107
|
+
/**
|
|
108
|
+
* Runs a legacy mutation under one journal transaction.
|
|
109
|
+
*
|
|
110
|
+
* `planned` is written before `run()`; `confirmed` / `failed` after — decided
|
|
111
|
+
* by BOTH the thrown/not-thrown boundary and the updater's `success` result
|
|
112
|
+
* shape (see `toLegacyMutationOutcome`). A journal failure never changes the
|
|
113
|
+
* mutation's outcome — it is reported alongside it.
|
|
114
|
+
*/
|
|
115
|
+
export declare function runLegacyJournaledMutation<T>(options: LegacyJournalOptions, run: () => Promise<T>): Promise<{
|
|
116
|
+
readonly result: T;
|
|
117
|
+
readonly journal: LegacyJournalStatus;
|
|
118
|
+
}>;
|