borgmcp 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/assimilate-cmd.d.ts +6 -0
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +31 -7
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +2 -0
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +10 -0
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +2 -0
- package/dist/cli-help.js.map +1 -1
- package/dist/cli-platform.d.ts +1 -0
- package/dist/cli-platform.d.ts.map +1 -1
- package/dist/cli-platform.js +5 -1
- package/dist/cli-platform.js.map +1 -1
- package/dist/console-prefix.d.ts +9 -10
- package/dist/console-prefix.d.ts.map +1 -1
- package/dist/console-prefix.js +23 -20
- package/dist/console-prefix.js.map +1 -1
- package/dist/cubes.d.ts +11 -0
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +10 -0
- package/dist/cubes.js.map +1 -1
- package/dist/display-identity.d.ts +34 -0
- package/dist/display-identity.d.ts.map +1 -0
- package/dist/display-identity.js +88 -0
- package/dist/display-identity.js.map +1 -0
- package/dist/docs-sections.d.ts.map +1 -1
- package/dist/docs-sections.js +9 -0
- package/dist/docs-sections.js.map +1 -1
- package/dist/drone-management.d.ts.map +1 -1
- package/dist/drone-management.js +6 -0
- package/dist/drone-management.js.map +1 -1
- package/dist/inbox-monitor.d.ts.map +1 -1
- package/dist/inbox-monitor.js +12 -0
- package/dist/inbox-monitor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -16
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +41 -15
- package/dist/log-stream.js.map +1 -1
- package/dist/parse-assimilate-args.d.ts +1 -1
- package/dist/parse-assimilate-args.d.ts.map +1 -1
- package/dist/parse-assimilate-args.js +5 -2
- package/dist/parse-assimilate-args.js.map +1 -1
- package/dist/regen-format.d.ts +2 -2
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +6 -5
- package/dist/regen-format.js.map +1 -1
- package/dist/regen.js +6 -2
- package/dist/regen.js.map +1 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +8 -1
- package/dist/remote-client.js.map +1 -1
- package/dist/reset-local-seat-cmd.d.ts +8 -0
- package/dist/reset-local-seat-cmd.d.ts.map +1 -1
- package/dist/reset-local-seat-cmd.js +21 -2
- package/dist/reset-local-seat-cmd.js.map +1 -1
- package/dist/seat-reattach-guard.d.ts +21 -0
- package/dist/seat-reattach-guard.d.ts.map +1 -0
- package/dist/seat-reattach-guard.js +83 -0
- package/dist/seat-reattach-guard.js.map +1 -0
- package/dist/seats.d.ts +6 -2
- package/dist/seats.d.ts.map +1 -1
- package/dist/seats.js +37 -14
- package/dist/seats.js.map +1 -1
- package/dist/update-cmd.d.ts.map +1 -1
- package/dist/update-cmd.js +1 -37
- package/dist/update-cmd.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +3 -3
- package/docs/LOCAL_SERVER.md +4 -0
- package/docs/RELEASING.md +23 -4
- package/docs/SEAT_LIFECYCLE.md +242 -0
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +45 -7
- package/src/assimilate-deps.ts +2 -0
- package/src/claude.ts +11 -0
- package/src/cli-help.ts +2 -0
- package/src/cli-platform.ts +5 -2
- package/src/console-prefix.ts +25 -18
- package/src/cubes.ts +16 -0
- package/src/display-identity.ts +130 -0
- package/src/docs-sections.ts +9 -0
- package/src/drone-management.ts +6 -0
- package/src/inbox-monitor.ts +19 -0
- package/src/index.ts +66 -20
- package/src/log-stream.ts +38 -14
- package/src/parse-assimilate-args.ts +4 -2
- package/src/regen-format.ts +6 -5
- package/src/regen.ts +11 -2
- package/src/remote-client.ts +8 -0
- package/src/reset-local-seat-cmd.ts +26 -1
- package/src/seat-reattach-guard.ts +113 -0
- package/src/seats.ts +36 -14
- package/src/update-cmd.ts +0 -34
|
@@ -25,6 +25,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
25
25
|
import {
|
|
26
26
|
snapshotLocalSeat as cubesSnapshotLocalSeat,
|
|
27
27
|
resetLocalSeatBinding as cubesResetLocalSeatBinding,
|
|
28
|
+
findRemainingActiveSeatForWorktree as cubesFindRemainingActiveSeatForWorktree,
|
|
28
29
|
findProjectRoot as cubesFindProjectRoot,
|
|
29
30
|
type LocalSeatSnapshot,
|
|
30
31
|
type ResetLocalSeatOutcome,
|
|
@@ -39,6 +40,10 @@ export interface ResetLocalSeatFlags {
|
|
|
39
40
|
export interface ResetLocalSeatDeps {
|
|
40
41
|
snapshotLocalSeat: () => Promise<LocalSeatSnapshot | null>;
|
|
41
42
|
resetLocalSeatBinding: (expected: LocalSeatSnapshot) => Promise<ResetLocalSeatOutcome>;
|
|
43
|
+
findRemainingActiveSeat: (worktree: string) => Promise<{
|
|
44
|
+
apiUrl: string;
|
|
45
|
+
operation: { projectRoot: string; kind: 'seat' | 'sibling'; operationKey: string };
|
|
46
|
+
} | null>;
|
|
42
47
|
findProjectRoot: (cwd: string) => string;
|
|
43
48
|
normalizeHost: (host: string) => string;
|
|
44
49
|
cwd: () => string;
|
|
@@ -52,6 +57,10 @@ function reenrollCommand(apiUrl: string): string {
|
|
|
52
57
|
return `\`borg assimilate --host ${apiUrl} --enroll\``;
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
function reattachCommand(apiUrl: string): string {
|
|
61
|
+
return `\`borg assimilate --host ${apiUrl} --here\``;
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
/**
|
|
56
65
|
* Recovery guidance shared by the success + honest-no-op copy. Makes NO
|
|
57
66
|
* server-revocation claim: it recommends the operator issue a LIVE scoped
|
|
@@ -149,7 +158,22 @@ export async function runResetLocalSeat(
|
|
|
149
158
|
`audit: this worktree's saved local seat for ${snapshot.apiUrl} (worktree ${worktree}) ` +
|
|
150
159
|
'was cleared; server, trust anchor, cube, and sibling worktrees unchanged.\n',
|
|
151
160
|
);
|
|
152
|
-
|
|
161
|
+
let remaining: Awaited<ReturnType<ResetLocalSeatDeps['findRemainingActiveSeat']>> = null;
|
|
162
|
+
try {
|
|
163
|
+
remaining = await deps.findRemainingActiveSeat(worktree);
|
|
164
|
+
} catch {
|
|
165
|
+
// The reset already committed. A follow-up read must not turn that success
|
|
166
|
+
// into a false failure; fall back to the universally valid enrollment path.
|
|
167
|
+
}
|
|
168
|
+
if (remaining?.apiUrl === snapshot.apiUrl) {
|
|
169
|
+
deps.stdout(
|
|
170
|
+
`Another saved active seat remains for this worktree on ${snapshot.apiUrl}. ` +
|
|
171
|
+
`Re-attach with ${reattachCommand(snapshot.apiUrl)}; Borg will revalidate that ` +
|
|
172
|
+
'seat with the server before launch.\n',
|
|
173
|
+
);
|
|
174
|
+
} else {
|
|
175
|
+
deps.stdout(recoveryGuidance(snapshot.apiUrl));
|
|
176
|
+
}
|
|
153
177
|
return 0;
|
|
154
178
|
}
|
|
155
179
|
|
|
@@ -175,6 +199,7 @@ export function buildDefaultResetLocalSeatDeps(): ResetLocalSeatDeps {
|
|
|
175
199
|
return {
|
|
176
200
|
snapshotLocalSeat: () => cubesSnapshotLocalSeat(),
|
|
177
201
|
resetLocalSeatBinding: (expected) => cubesResetLocalSeatBinding(expected),
|
|
202
|
+
findRemainingActiveSeat: (worktree) => cubesFindRemainingActiveSeatForWorktree(worktree),
|
|
178
203
|
findProjectRoot: (cwd) => cubesFindProjectRoot(cwd),
|
|
179
204
|
normalizeHost: (host) => normalizeServerEndpoint(host),
|
|
180
205
|
cwd: () => process.cwd(),
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import {
|
|
3
|
+
HEARTBEAT_STALE_MS,
|
|
4
|
+
heartbeatPathFor,
|
|
5
|
+
legacyHeartbeatPathFor,
|
|
6
|
+
legacyPidfilePathFor,
|
|
7
|
+
pidfilePathFor,
|
|
8
|
+
} from './inbox-monitor.js';
|
|
9
|
+
|
|
10
|
+
export type MonitorHeartbeatState = 'fresh' | 'stale' | 'missing';
|
|
11
|
+
|
|
12
|
+
export interface LiveInboxMonitor {
|
|
13
|
+
pid: number;
|
|
14
|
+
heartbeat: MonitorHeartbeatState;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SeatReattachGuardDeps {
|
|
18
|
+
readPidfile: (path: string) => string | null;
|
|
19
|
+
readHeartbeatMtimeMs: (path: string) => number | null;
|
|
20
|
+
isAlive: (pid: number) => boolean;
|
|
21
|
+
now: number;
|
|
22
|
+
heartbeatStaleMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseMonitorPid(raw: string | null): number | null {
|
|
26
|
+
if (raw === null) return null;
|
|
27
|
+
const match = /^([1-9]\d*)(?::[^:\s]+)?$/.exec(raw.trim());
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
const pid = Number(match[1]);
|
|
30
|
+
return Number.isSafeInteger(pid) ? pid : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Read-only preflight for reusing a worktree seat. A live PID always blocks:
|
|
35
|
+
* a stale heartbeat may mean the holder is wedged, but only an explicit
|
|
36
|
+
* `--force` decision may launch a second session onto that seat.
|
|
37
|
+
*/
|
|
38
|
+
export function inspectLiveInboxMonitor(
|
|
39
|
+
inboxPath: string,
|
|
40
|
+
monitorStateRoot: string,
|
|
41
|
+
deps: SeatReattachGuardDeps = defaultSeatReattachGuardDeps(),
|
|
42
|
+
): LiveInboxMonitor | null {
|
|
43
|
+
const candidates = [
|
|
44
|
+
{
|
|
45
|
+
pidfile: pidfilePathFor(inboxPath, monitorStateRoot),
|
|
46
|
+
heartbeat: heartbeatPathFor(inboxPath, monitorStateRoot),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
pidfile: legacyPidfilePathFor(inboxPath),
|
|
50
|
+
heartbeat: legacyHeartbeatPathFor(inboxPath),
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
const staleMs = deps.heartbeatStaleMs ?? HEARTBEAT_STALE_MS;
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
const pid = parseMonitorPid(deps.readPidfile(candidate.pidfile));
|
|
56
|
+
if (pid === null || !deps.isAlive(pid)) continue;
|
|
57
|
+
const heartbeatMtime = deps.readHeartbeatMtimeMs(candidate.heartbeat);
|
|
58
|
+
return {
|
|
59
|
+
pid,
|
|
60
|
+
heartbeat: heartbeatMtime === null
|
|
61
|
+
? 'missing'
|
|
62
|
+
: deps.now - heartbeatMtime < staleMs
|
|
63
|
+
? 'fresh'
|
|
64
|
+
: 'stale',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function formatSeatReattachRefusal(
|
|
71
|
+
holder: LiveInboxMonitor,
|
|
72
|
+
forcedCommand: string,
|
|
73
|
+
): string {
|
|
74
|
+
const heartbeat =
|
|
75
|
+
holder.heartbeat === 'fresh'
|
|
76
|
+
? ''
|
|
77
|
+
: ` Its heartbeat is ${holder.heartbeat}, so the process may be wedged.`;
|
|
78
|
+
return (
|
|
79
|
+
`This worktree's Borg seat already has a live session (inbox monitor pid ${holder.pid}).${heartbeat}\n` +
|
|
80
|
+
'No agent was launched. Stop the existing session or use a fresh worktree with `borg assimilate --worktree <name>`. ' +
|
|
81
|
+
`If the live monitor is wedged, override once with \`${forcedCommand}\`.\n`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function defaultSeatReattachGuardDeps(
|
|
86
|
+
now: number = Date.now(),
|
|
87
|
+
): SeatReattachGuardDeps {
|
|
88
|
+
return {
|
|
89
|
+
readPidfile: (path) => {
|
|
90
|
+
try {
|
|
91
|
+
return readFileSync(path, 'utf8');
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
readHeartbeatMtimeMs: (path) => {
|
|
97
|
+
try {
|
|
98
|
+
return statSync(path).mtimeMs;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
isAlive: (pid) => {
|
|
104
|
+
try {
|
|
105
|
+
process.kill(pid, 0);
|
|
106
|
+
return true;
|
|
107
|
+
} catch (error: any) {
|
|
108
|
+
return error?.code === 'EPERM';
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
now,
|
|
112
|
+
};
|
|
113
|
+
}
|
package/src/seats.ts
CHANGED
|
@@ -98,6 +98,8 @@ function emptyStore(): SeatsFile {
|
|
|
98
98
|
|
|
99
99
|
const ROLE_CLASSES = new Set(['queen', 'worker']);
|
|
100
100
|
const OPERATION_KINDS = new Set(['seat', 'sibling']);
|
|
101
|
+
const SEAT_BEARER_RE = /^[A-Za-z0-9_-]{43,1024}$/;
|
|
102
|
+
const rejectedSeatRefs = new Set<string>();
|
|
101
103
|
|
|
102
104
|
function isNonEmptyString(value: unknown): value is string {
|
|
103
105
|
return typeof value === 'string' && value.length > 0;
|
|
@@ -117,10 +119,10 @@ function isValidOperation(value: unknown): value is SeatOperation {
|
|
|
117
119
|
/**
|
|
118
120
|
* CR#2: FULL per-entry validation. Every key/value/invariant of a seat record is
|
|
119
121
|
* checked — the ref is well-formed and self-consistent (the map key equals the
|
|
120
|
-
* record's derived ref), state ∈ {pending,active}, the credential is
|
|
121
|
-
*
|
|
122
|
-
* server + binding fields, and a PENDING record carries NO
|
|
123
|
-
* id. A single invalid entry ⇒ the whole store is rejected
|
|
122
|
+
* record's derived ref), state ∈ {pending,active}, the credential is an exact
|
|
123
|
+
* 32-byte base64url bearer, the operation is well-shaped, an ACTIVE record carries
|
|
124
|
+
* ALL its required server + binding fields, and a PENDING record carries NO
|
|
125
|
+
* active-only session id. A single invalid entry ⇒ the whole store is rejected
|
|
124
126
|
* (fail closed at the caller, bytes preserved) — never a silent cast.
|
|
125
127
|
*/
|
|
126
128
|
function isValidSeatRecord(ref: string, value: unknown): value is SeatRecord {
|
|
@@ -134,7 +136,8 @@ function isValidSeatRecord(ref: string, value: unknown): value is SeatRecord {
|
|
|
134
136
|
!isNonEmptyString(r.cubeId) ||
|
|
135
137
|
!isNonEmptyString(r.roleId) ||
|
|
136
138
|
!isValidOperation(r.operation) ||
|
|
137
|
-
|
|
139
|
+
typeof r.credential !== 'string' ||
|
|
140
|
+
!SEAT_BEARER_RE.test(r.credential)
|
|
138
141
|
) {
|
|
139
142
|
return false;
|
|
140
143
|
}
|
|
@@ -204,7 +207,7 @@ function parseStore(raw: string): SeatsFile | null {
|
|
|
204
207
|
!Array.isArray(replacement) &&
|
|
205
208
|
Object.keys(replacement).length === 1 &&
|
|
206
209
|
typeof (replacement as { credential?: unknown }).credential === 'string' &&
|
|
207
|
-
|
|
210
|
+
SEAT_BEARER_RE.test((replacement as { credential: string }).credential);
|
|
208
211
|
let canonicalOrigin = false;
|
|
209
212
|
try {
|
|
210
213
|
const origin = new URL(String(withoutReplacement.origin));
|
|
@@ -338,6 +341,7 @@ export async function mintPendingSeat(input: {
|
|
|
338
341
|
};
|
|
339
342
|
txn.data.seats[ref] = record;
|
|
340
343
|
await txn.commit();
|
|
344
|
+
rejectedSeatRefs.delete(ref);
|
|
341
345
|
return record;
|
|
342
346
|
});
|
|
343
347
|
}
|
|
@@ -430,6 +434,7 @@ export async function prepareSeat(input: {
|
|
|
430
434
|
};
|
|
431
435
|
txn.data.seats[ref] = record;
|
|
432
436
|
await txn.commit();
|
|
437
|
+
rejectedSeatRefs.delete(ref);
|
|
433
438
|
return { ok: true as const, record };
|
|
434
439
|
});
|
|
435
440
|
}
|
|
@@ -498,6 +503,7 @@ export async function activateAndBindSeat(input: {
|
|
|
498
503
|
...(input.isHumanSeat !== undefined ? { isHumanSeat: input.isHumanSeat } : {}),
|
|
499
504
|
};
|
|
500
505
|
await txn.commit();
|
|
506
|
+
rejectedSeatRefs.delete(ref);
|
|
501
507
|
return 'activated';
|
|
502
508
|
});
|
|
503
509
|
}
|
|
@@ -567,16 +573,28 @@ export async function bindPendingSeatToWorktree(input: {
|
|
|
567
573
|
|
|
568
574
|
// ─── Hydration / enumeration (scan by worktree) ──────────────────────────────
|
|
569
575
|
|
|
570
|
-
/** The
|
|
571
|
-
* worktree, or non-active) is NEVER surfaced as a live binding.
|
|
576
|
+
/** The preferred ACTIVE seat bound to `worktree`, or null. A pending record (no
|
|
577
|
+
* worktree, or non-active) is NEVER surfaced as a live binding. Candidates use
|
|
578
|
+
* one total order across every process: unrejected before rejected, a sibling
|
|
579
|
+
* finalized into this worktree before an older in-place binding, then seat ref. */
|
|
572
580
|
export async function getActiveSeatForWorktree(worktree: string): Promise<SeatRecord | null> {
|
|
573
581
|
const store = await readStore();
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
582
|
+
const candidates = Object.entries(store.seats)
|
|
583
|
+
.filter(([ref, record]) =>
|
|
584
|
+
record.state === 'active' && record.worktree === worktree && seatRef(record) === ref)
|
|
585
|
+
.sort(([leftRef, left], [rightRef, right]) => {
|
|
586
|
+
const rejectionOrder = Number(rejectedSeatRefs.has(leftRef)) - Number(rejectedSeatRefs.has(rightRef));
|
|
587
|
+
if (rejectionOrder !== 0) return rejectionOrder;
|
|
588
|
+
const operationOrder = Number(left.operation.kind === 'seat') - Number(right.operation.kind === 'seat');
|
|
589
|
+
if (operationOrder !== 0) return operationOrder;
|
|
590
|
+
return leftRef < rightRef ? -1 : leftRef > rightRef ? 1 : 0;
|
|
591
|
+
});
|
|
592
|
+
return candidates[0]?.[1] ?? null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** Deprioritize an exact seat after a definitive server auth or eviction verdict. */
|
|
596
|
+
export function markSeatRejected(ref: string): void {
|
|
597
|
+
if (REF_RE.test(ref)) rejectedSeatRefs.add(ref);
|
|
580
598
|
}
|
|
581
599
|
|
|
582
600
|
/**
|
|
@@ -624,6 +642,8 @@ export async function findIncompleteSiblingAttempt(binding: {
|
|
|
624
642
|
record.state === 'pending' &&
|
|
625
643
|
record.worktree === undefined &&
|
|
626
644
|
record.operation.kind === 'sibling' &&
|
|
645
|
+
record.operation.operationKey.startsWith('implicit-sibling:') &&
|
|
646
|
+
record.operation.operationKey.length > 'implicit-sibling:'.length &&
|
|
627
647
|
record.origin === binding.origin &&
|
|
628
648
|
record.trustIdentity === binding.trustIdentity &&
|
|
629
649
|
record.cubeId === binding.cubeId &&
|
|
@@ -696,6 +716,7 @@ export async function resetSeatForWorktree(expected: {
|
|
|
696
716
|
}
|
|
697
717
|
delete txn.data.seats[expected.ref];
|
|
698
718
|
await txn.commit();
|
|
719
|
+
rejectedSeatRefs.delete(expected.ref);
|
|
699
720
|
return { outcome: 'reset' as const, ref: expected.ref };
|
|
700
721
|
});
|
|
701
722
|
}
|
|
@@ -726,6 +747,7 @@ export async function clearSeat(ref: string): Promise<void> {
|
|
|
726
747
|
if (txn.data.seats[ref] !== undefined) {
|
|
727
748
|
delete txn.data.seats[ref];
|
|
728
749
|
await txn.commit();
|
|
750
|
+
rejectedSeatRefs.delete(ref);
|
|
729
751
|
}
|
|
730
752
|
});
|
|
731
753
|
}
|
package/src/update-cmd.ts
CHANGED
|
@@ -308,23 +308,6 @@ function decodeServerStatus(value: unknown): ServerStatus {
|
|
|
308
308
|
throw new Error('server returned invalid JSON status');
|
|
309
309
|
}
|
|
310
310
|
const record = value as Record<string, unknown>;
|
|
311
|
-
const allowed = new Set([
|
|
312
|
-
'status',
|
|
313
|
-
'installed_controller',
|
|
314
|
-
'prepared_runtime',
|
|
315
|
-
'prepared_integrity',
|
|
316
|
-
'running_runtime',
|
|
317
|
-
'running_integrity',
|
|
318
|
-
'build_identity',
|
|
319
|
-
'endpoint',
|
|
320
|
-
'mode',
|
|
321
|
-
'service_adapter',
|
|
322
|
-
'data_identity',
|
|
323
|
-
'next_action',
|
|
324
|
-
]);
|
|
325
|
-
for (const key of Object.keys(record)) {
|
|
326
|
-
if (!allowed.has(key)) throw new Error(`server status contains unknown field ${key}`);
|
|
327
|
-
}
|
|
328
311
|
if (
|
|
329
312
|
(record.status !== 'running' && record.status !== 'stopped') ||
|
|
330
313
|
typeof record.installed_controller !== 'string' ||
|
|
@@ -382,10 +365,7 @@ function decodeServerUpdate(value: unknown): ServerUpdateResult {
|
|
|
382
365
|
}
|
|
383
366
|
const record = value as Record<string, unknown>;
|
|
384
367
|
if (record.status === 'failed') {
|
|
385
|
-
const failureKeys = Object.keys(record);
|
|
386
368
|
if (
|
|
387
|
-
failureKeys.length !== 4 ||
|
|
388
|
-
!failureKeys.every((key) => ['status', 'error_code', 'recovery', 'data_identity'].includes(key)) ||
|
|
389
369
|
(record.error_code !== 'ARTIFACT_VERIFICATION_FAILED' && record.error_code !== 'ACTIVATION_FAILED') ||
|
|
390
370
|
!['verification_failed', 'restored', 'stopped', 'recovery_failed'].includes(record.recovery as string) ||
|
|
391
371
|
record.data_identity !== 'preserved' ||
|
|
@@ -400,20 +380,6 @@ function decodeServerUpdate(value: unknown): ServerUpdateResult {
|
|
|
400
380
|
recovery: record.recovery as ServerUpdateFailure['recovery'],
|
|
401
381
|
};
|
|
402
382
|
}
|
|
403
|
-
const allowed = new Set([
|
|
404
|
-
'status',
|
|
405
|
-
'installed_controller',
|
|
406
|
-
'artifact',
|
|
407
|
-
'artifact_integrity',
|
|
408
|
-
'running_runtime',
|
|
409
|
-
'build_identity',
|
|
410
|
-
'mode',
|
|
411
|
-
'data_identity',
|
|
412
|
-
'next_action',
|
|
413
|
-
]);
|
|
414
|
-
for (const key of Object.keys(record)) {
|
|
415
|
-
if (!allowed.has(key)) throw new Error(`server update contains unknown field ${key}`);
|
|
416
|
-
}
|
|
417
383
|
if (
|
|
418
384
|
(record.status !== 'prepared' && record.status !== 'updated') ||
|
|
419
385
|
typeof record.installed_controller !== 'string' ||
|