create-principles-disciple 1.134.7 → 1.134.9
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/console/dist/server/routes/update-history.d.ts +37 -0
- package/console/dist/server/routes/update-history.js +42 -0
- package/console/dist/server/routes/update.js +67 -0
- package/console/dist/server/utils/installed-layout.d.ts +6 -4
- package/console/dist/server/utils/installed-layout.js +28 -3
- package/package.json +1 -1
- package/release-manager/package.json +1 -1
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* PRI-702 (ADR-0024 D-7): which authority actually performed the mutation.
|
|
4
|
+
*
|
|
5
|
+
* The vocabulary is REUSED from the MutationController — the one place that
|
|
6
|
+
* already names mutation authorities — so the history stream can never drift
|
|
7
|
+
* into a second authority vocabulary. `legacy-migration` is deliberately not
|
|
8
|
+
* listed: it writes the separate SPEC §12 stream (`<pdHome>/logs/history.jsonl`)
|
|
9
|
+
* and must not be conflated with this Owner-facing one.
|
|
10
|
+
*
|
|
11
|
+
* The field is additive and optional. Records written before PRI-702 have no
|
|
12
|
+
* `authority`; they are kept verbatim (no migration) and the reader never
|
|
13
|
+
* fabricates a value for them.
|
|
14
|
+
*/
|
|
15
|
+
export declare const UPDATE_HISTORY_AUTHORITIES: readonly ["release-manager", "legacy-console-updater"];
|
|
16
|
+
export type UpdateHistoryAuthority = (typeof UPDATE_HISTORY_AUTHORITIES)[number];
|
|
2
17
|
export declare const UPDATE_HISTORY_KINDS: readonly ['update', 'reinstall', 'legacy_migration', 'rollback', 'refusal', 'failure', 'recovery', 'unknown'];
|
|
3
18
|
export type UpdateHistoryKind = (typeof UPDATE_HISTORY_KINDS)[number];
|
|
4
19
|
interface UpdateHistoryEntry {
|
|
@@ -11,7 +26,29 @@ interface UpdateHistoryEntry {
|
|
|
11
26
|
backupPath?: string;
|
|
12
27
|
reason?: string;
|
|
13
28
|
nextAction?: string;
|
|
29
|
+
/** PRI-702: authority that performed the mutation. Absent on pre-PRI-702 records. */
|
|
30
|
+
authority?: UpdateHistoryAuthority;
|
|
31
|
+
/**
|
|
32
|
+
* PRI-702: the transaction journal id of the update, when one exists.
|
|
33
|
+
* Correlates the Owner-facing event with the machine-recovery stream
|
|
34
|
+
* (`~/.pd/transactions/<transactionId>.jsonl`) — the two stay separate
|
|
35
|
+
* structures (ADR-0024 §2.5-2); this is a pointer, not a copy.
|
|
36
|
+
*/
|
|
37
|
+
transactionId?: string;
|
|
14
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* PRI-702 (ADR-0024 D-7): the ONE Owner-facing update-history writer.
|
|
41
|
+
*
|
|
42
|
+
* Both mutation authorities append through this function — the legacy console
|
|
43
|
+
* updater (every existing call site in `routes/update.ts`) and the
|
|
44
|
+
* ReleaseManager-served apply-full dispatch. There is no second writer and no
|
|
45
|
+
* authority-specific schema: callers differ only in the `authority` they pass.
|
|
46
|
+
*
|
|
47
|
+
* `authority` defaults to `legacy-console-updater` because that is the writer
|
|
48
|
+
* that has owned this stream since before PRI-702; a caller that omits it is
|
|
49
|
+
* by definition the legacy path. Every entry written from now on carries the
|
|
50
|
+
* field explicitly, so PRI-701's legacy-usage census can read it directly.
|
|
51
|
+
*/
|
|
15
52
|
export declare function appendUpdateHistory(workspaceDir: string, entry: Omit<UpdateHistoryEntry, 'id' | 'timestamp'>): void;
|
|
16
53
|
export declare function handleUpdateHistoryRoute(req: IncomingMessage, res: ServerResponse, workspaceDir: string, _subPath: string): Promise<void>;
|
|
17
54
|
export {};
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { sendSuccess, sendMethodNotAllowed } from '../utils/response.js';
|
|
4
|
+
import { LEGACY_MUTATION_AUTHORITY, RELEASE_MANAGER_AUTHORITY, } from '../update/mutation-controller.js';
|
|
5
|
+
/**
|
|
6
|
+
* PRI-702 (ADR-0024 D-7): which authority actually performed the mutation.
|
|
7
|
+
*
|
|
8
|
+
* The vocabulary is REUSED from the MutationController — the one place that
|
|
9
|
+
* already names mutation authorities — so the history stream can never drift
|
|
10
|
+
* into a second authority vocabulary. `legacy-migration` is deliberately not
|
|
11
|
+
* listed: it writes the separate SPEC §12 stream (`<pdHome>/logs/history.jsonl`)
|
|
12
|
+
* and must not be conflated with this Owner-facing one.
|
|
13
|
+
*
|
|
14
|
+
* The field is additive and optional. Records written before PRI-702 have no
|
|
15
|
+
* `authority`; they are kept verbatim (no migration) and the reader never
|
|
16
|
+
* fabricates a value for them.
|
|
17
|
+
*/
|
|
18
|
+
export const UPDATE_HISTORY_AUTHORITIES = [
|
|
19
|
+
RELEASE_MANAGER_AUTHORITY,
|
|
20
|
+
LEGACY_MUTATION_AUTHORITY,
|
|
21
|
+
];
|
|
4
22
|
export const UPDATE_HISTORY_KINDS = [
|
|
5
23
|
'update',
|
|
6
24
|
'reinstall',
|
|
@@ -14,6 +32,9 @@ export const UPDATE_HISTORY_KINDS = [
|
|
|
14
32
|
function isHistoryKind(value) {
|
|
15
33
|
return typeof value === 'string' && UPDATE_HISTORY_KINDS.includes(value);
|
|
16
34
|
}
|
|
35
|
+
function isHistoryAuthority(value) {
|
|
36
|
+
return typeof value === 'string' && UPDATE_HISTORY_AUTHORITIES.includes(value);
|
|
37
|
+
}
|
|
17
38
|
function parseHistoryEntry(value) {
|
|
18
39
|
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
19
40
|
return undefined;
|
|
@@ -32,6 +53,8 @@ function parseHistoryEntry(value) {
|
|
|
32
53
|
return undefined;
|
|
33
54
|
if (raw.backupPath !== undefined && typeof raw.backupPath !== 'string')
|
|
34
55
|
return undefined;
|
|
56
|
+
if (raw.transactionId !== undefined && typeof raw.transactionId !== 'string')
|
|
57
|
+
return undefined;
|
|
35
58
|
return {
|
|
36
59
|
id: raw.id,
|
|
37
60
|
timestamp: raw.timestamp,
|
|
@@ -45,6 +68,11 @@ function parseHistoryEntry(value) {
|
|
|
45
68
|
...(typeof raw.backupPath === 'string' ? { backupPath: raw.backupPath } : {}),
|
|
46
69
|
...(typeof raw.reason === 'string' ? { reason: raw.reason } : {}),
|
|
47
70
|
...(typeof raw.nextAction === 'string' ? { nextAction: raw.nextAction } : {}),
|
|
71
|
+
// An authority outside the closed vocabulary is dropped rather than
|
|
72
|
+
// failing the whole entry: this stream is an Owner read model, and one
|
|
73
|
+
// unknown writer must not hide every other record (rc-3).
|
|
74
|
+
...(isHistoryAuthority(raw.authority) ? { authority: raw.authority } : {}),
|
|
75
|
+
...(typeof raw.transactionId === 'string' ? { transactionId: raw.transactionId } : {}),
|
|
48
76
|
};
|
|
49
77
|
}
|
|
50
78
|
function getHistoryPath(workspaceDir) {
|
|
@@ -68,11 +96,25 @@ function loadHistory(historyPath) {
|
|
|
68
96
|
}
|
|
69
97
|
return [];
|
|
70
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* PRI-702 (ADR-0024 D-7): the ONE Owner-facing update-history writer.
|
|
101
|
+
*
|
|
102
|
+
* Both mutation authorities append through this function — the legacy console
|
|
103
|
+
* updater (every existing call site in `routes/update.ts`) and the
|
|
104
|
+
* ReleaseManager-served apply-full dispatch. There is no second writer and no
|
|
105
|
+
* authority-specific schema: callers differ only in the `authority` they pass.
|
|
106
|
+
*
|
|
107
|
+
* `authority` defaults to `legacy-console-updater` because that is the writer
|
|
108
|
+
* that has owned this stream since before PRI-702; a caller that omits it is
|
|
109
|
+
* by definition the legacy path. Every entry written from now on carries the
|
|
110
|
+
* field explicitly, so PRI-701's legacy-usage census can read it directly.
|
|
111
|
+
*/
|
|
71
112
|
export function appendUpdateHistory(workspaceDir, entry) {
|
|
72
113
|
const historyPath = getHistoryPath(workspaceDir);
|
|
73
114
|
const history = loadHistory(historyPath);
|
|
74
115
|
history.push({
|
|
75
116
|
...entry,
|
|
117
|
+
authority: entry.authority ?? LEGACY_MUTATION_AUTHORITY,
|
|
76
118
|
id: `update-${Date.now()}`,
|
|
77
119
|
timestamp: new Date().toISOString(),
|
|
78
120
|
});
|
|
@@ -1921,7 +1921,31 @@ async function runReleaseManagerCheckDispatch(mod, req, res, ctx) {
|
|
|
1921
1921
|
* layer maps the outcome into the legacy doInlineFullUpdate response contract
|
|
1922
1922
|
* ({success, message, reason?, nextAction?, newVersion?, requiresRestart}) —
|
|
1923
1923
|
* wire contract unchanged. It performs NO runtime mutation itself.
|
|
1924
|
+
*
|
|
1925
|
+
* PRI-702 (ADR-0024 D-7): this boundary is also where the Owner-facing update
|
|
1926
|
+
* history is appended for an RM-served update, through the SAME
|
|
1927
|
+
* `appendUpdateHistory` writer the legacy updater uses. It is the only layer
|
|
1928
|
+
* that knows all three of the workspace (where the history lives), the
|
|
1929
|
+
* authority that served the mutation, and the final outcome — the installer
|
|
1930
|
+
* writes the transaction journal only, and a pre-transaction refusal writes
|
|
1931
|
+
* NO history event (the fallback's legacy event is the single record, see
|
|
1932
|
+
* `X-PD-Mutation-Fallback-Reason`).
|
|
1933
|
+
*/
|
|
1934
|
+
/**
|
|
1935
|
+
* A history-write failure must never misreport a mutation that already
|
|
1936
|
+
* happened: the response still carries the true outcome (same policy as
|
|
1937
|
+
* `logLegacyJournalGap` above — blocking the Owner's update would be worse
|
|
1938
|
+
* than an unaudited one) but the audit gap is logged loud (rc-9).
|
|
1924
1939
|
*/
|
|
1940
|
+
function appendGovernedUpdateHistory(workspaceDir, entry) {
|
|
1941
|
+
try {
|
|
1942
|
+
appendUpdateHistory(workspaceDir, entry);
|
|
1943
|
+
}
|
|
1944
|
+
catch (error) {
|
|
1945
|
+
console.error(`[update] ReleaseManager-served mutation completed WITHOUT an update-history record (${error instanceof Error ? error.message : String(error)}). `
|
|
1946
|
+
+ 'ADR-0024 D-7 requires every update to be auditable; the response still reports the real outcome.');
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1925
1949
|
async function runReleaseManagerApplyFullDispatch(mod, req, res, ctx) {
|
|
1926
1950
|
if (req.method !== 'POST') {
|
|
1927
1951
|
sendMethodNotAllowed(res);
|
|
@@ -1951,9 +1975,26 @@ async function runReleaseManagerApplyFullDispatch(mod, req, res, ctx) {
|
|
|
1951
1975
|
await legacyApplyFullMutation(req, res, ctx);
|
|
1952
1976
|
return;
|
|
1953
1977
|
}
|
|
1978
|
+
// PRI-702 (ADR-0024 D-7): the pre-apply version, read from the SAME install
|
|
1979
|
+
// state the ReleaseManager used to decide the update (active.json) — never
|
|
1980
|
+
// from a second version source.
|
|
1981
|
+
const fromVersion = authority.installStatus?.productVersion ?? 'unknown';
|
|
1954
1982
|
try {
|
|
1955
1983
|
const outcome = await authority.manager.apply({ workspaceDir: ctx.workspaceDir });
|
|
1956
1984
|
if (outcome.kind === 'applied') {
|
|
1985
|
+
// One update → one Owner-visible history event. The Console boundary is
|
|
1986
|
+
// the only layer that owns workspaceDir + authority + outcome together,
|
|
1987
|
+
// so the canonical writer is called here; the installer and the
|
|
1988
|
+
// ReleaseManager keep writing the transaction journal only (journal =
|
|
1989
|
+
// machine recovery, history = Owner audit, ADR-0024 §2.5-2).
|
|
1990
|
+
appendGovernedUpdateHistory(ctx.workspaceDir, {
|
|
1991
|
+
fromVersion,
|
|
1992
|
+
toVersion: outcome.productVersion,
|
|
1993
|
+
success: true,
|
|
1994
|
+
kind: 'update',
|
|
1995
|
+
authority: RELEASE_MANAGER_AUTHORITY,
|
|
1996
|
+
transactionId: outcome.transactionId,
|
|
1997
|
+
});
|
|
1957
1998
|
sendSuccess(res, {
|
|
1958
1999
|
success: true,
|
|
1959
2000
|
message: `Updated to ${outcome.productVersion}. Transaction ${outcome.transactionId} confirmed in the journal.`,
|
|
@@ -1963,6 +2004,19 @@ async function runReleaseManagerApplyFullDispatch(mod, req, res, ctx) {
|
|
|
1963
2004
|
});
|
|
1964
2005
|
}
|
|
1965
2006
|
else {
|
|
2007
|
+
// Legacy parity: the legacy updater records a `refusal` event when the
|
|
2008
|
+
// source does not advance the installation, so an RM-served "no update"
|
|
2009
|
+
// leaves the same trace instead of silence. No runtime byte changed —
|
|
2010
|
+
// toVersion stays at the installed version.
|
|
2011
|
+
appendGovernedUpdateHistory(ctx.workspaceDir, {
|
|
2012
|
+
fromVersion,
|
|
2013
|
+
toVersion: fromVersion,
|
|
2014
|
+
success: false,
|
|
2015
|
+
kind: 'refusal',
|
|
2016
|
+
reason: outcome.note,
|
|
2017
|
+
nextAction: 'No runtime change was made. Retry when a newer signed release is published.',
|
|
2018
|
+
authority: RELEASE_MANAGER_AUTHORITY,
|
|
2019
|
+
});
|
|
1966
2020
|
sendSuccess(res, {
|
|
1967
2021
|
success: true,
|
|
1968
2022
|
message: `No update applied: ${outcome.note}`,
|
|
@@ -1986,6 +2040,19 @@ async function runReleaseManagerApplyFullDispatch(mod, req, res, ctx) {
|
|
|
1986
2040
|
await legacyApplyFullMutation(req, res, ctx);
|
|
1987
2041
|
return;
|
|
1988
2042
|
}
|
|
2043
|
+
// PRI-702: a post-transaction failure is a real, terminal update attempt,
|
|
2044
|
+
// so it gets exactly one failure event under the ReleaseManager authority.
|
|
2045
|
+
// There is no legacy fallback on this path, hence no second event — the
|
|
2046
|
+
// runtime is left on the previous release (installer backup/restore).
|
|
2047
|
+
appendGovernedUpdateHistory(ctx.workspaceDir, {
|
|
2048
|
+
fromVersion,
|
|
2049
|
+
toVersion: 'failed',
|
|
2050
|
+
success: false,
|
|
2051
|
+
kind: 'failure',
|
|
2052
|
+
reason: mapped.reason,
|
|
2053
|
+
nextAction: mapped.nextAction ?? 'The runtime is unchanged. Retry the update after resolving the cause.',
|
|
2054
|
+
authority: RELEASE_MANAGER_AUTHORITY,
|
|
2055
|
+
});
|
|
1989
2056
|
sendSuccess(res, {
|
|
1990
2057
|
success: false,
|
|
1991
2058
|
message: mapped.message,
|
|
@@ -10,10 +10,12 @@ export interface UpdateLayout {
|
|
|
10
10
|
releaseManagerDir: string;
|
|
11
11
|
/**
|
|
12
12
|
* PRI-711: codex-adapter is a runtime-layout component pd-cli resolves
|
|
13
|
-
* eagerly. Undefined when the
|
|
14
|
-
* generation
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* eagerly. Undefined only when the canonical runtime root itself cannot be
|
|
14
|
+
* resolved: a one-generation-old deployed install-layout (this update runs
|
|
15
|
+
* inside the currently-running console, which resolves the layout helper
|
|
16
|
+
* installed by the PREVIOUS update) lacks the codexAdapterDir field, but
|
|
17
|
+
* PRI-724 derives the destination from the stable runtimeDir instead of
|
|
18
|
+
* degrading to a silent skip.
|
|
17
19
|
*/
|
|
18
20
|
codexAdapterDir: string | undefined;
|
|
19
21
|
hosts: InstallHost[];
|
|
@@ -18,6 +18,29 @@ import { resolveOpenClawHome } from './pd-backups.js';
|
|
|
18
18
|
export function resolveExtensionsDir() {
|
|
19
19
|
return path.join(resolveOpenClawHome(), 'extensions');
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve where the codex-adapter component lives in a CANONICAL layout.
|
|
23
|
+
*
|
|
24
|
+
* PRI-724: a one-generation-old deployed install-layout does not export the
|
|
25
|
+
* codexAdapterDir field (introduced 2026-09-09, PRI-711). The full update
|
|
26
|
+
* runs inside the currently-running console, which resolves the layout
|
|
27
|
+
* helper installed by the PREVIOUS update — so during exactly one update
|
|
28
|
+
* generation the field is always missing. Derive the destination from the
|
|
29
|
+
* stable runtimeDir (the adapter has always been installed at
|
|
30
|
+
* <runtimeDir>/codex-adapter) instead of degrading the update to skipping
|
|
31
|
+
* the adapter copy, which left the deployed adapter one release behind
|
|
32
|
+
* while the update reported success (observed 2026-09-10).
|
|
33
|
+
*/
|
|
34
|
+
function resolveCanonicalCodexAdapterDir(paths) {
|
|
35
|
+
if (typeof paths.codexAdapterDir === 'string' && paths.codexAdapterDir.length > 0) {
|
|
36
|
+
return paths.codexAdapterDir;
|
|
37
|
+
}
|
|
38
|
+
// rc-1/rc-2: the deployed module's shape is not trusted — only derive from
|
|
39
|
+
// a runtimeDir that is actually a non-empty string.
|
|
40
|
+
return typeof paths.runtimeDir === 'string' && paths.runtimeDir.length > 0
|
|
41
|
+
? path.join(paths.runtimeDir, 'codex-adapter')
|
|
42
|
+
: undefined;
|
|
43
|
+
}
|
|
21
44
|
export function resolveUpdateLayout() {
|
|
22
45
|
const homeDir = os.homedir();
|
|
23
46
|
const paths = getInstallLayoutPaths(homeDir);
|
|
@@ -46,9 +69,11 @@ export function resolveUpdateLayout() {
|
|
|
46
69
|
pdCliDir: paths.pdCliDir,
|
|
47
70
|
installLayoutDir: paths.installLayoutDir,
|
|
48
71
|
releaseManagerDir: paths.releaseManagerDir,
|
|
49
|
-
//
|
|
50
|
-
// field — guard instead of trusting the shape.
|
|
51
|
-
|
|
72
|
+
// PRI-711: the deployed install-layout may predate the codexAdapterDir
|
|
73
|
+
// field — guard instead of trusting the shape. PRI-724: when the field
|
|
74
|
+
// is missing, derive the destination from runtimeDir so the update
|
|
75
|
+
// still swaps the adapter (see resolveCanonicalCodexAdapterDir).
|
|
76
|
+
codexAdapterDir: resolveCanonicalCodexAdapterDir(paths),
|
|
52
77
|
hosts: resolution.manifest?.hosts ?? [],
|
|
53
78
|
};
|
|
54
79
|
}
|
package/package.json
CHANGED