pi-microsandbox 0.2.0 → 0.3.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 +58 -45
- package/SECURITY.md +12 -8
- package/docs/commands.md +18 -15
- package/docs/configuration.md +15 -8
- package/docs/development.md +14 -10
- package/docs/getting-started.md +16 -4
- package/docs/images.md +7 -6
- package/docs/safety.md +34 -22
- package/docs/storage.md +54 -55
- package/docs/troubleshooting.md +32 -11
- package/extensions/pi-msb/command.ts +12 -189
- package/extensions/pi-msb/config.ts +126 -38
- package/extensions/pi-msb/control.ts +42 -291
- package/extensions/pi-msb/index.ts +2 -2
- package/extensions/pi-msb/labels.ts +46 -231
- package/extensions/pi-msb/locks.ts +4 -6
- package/extensions/pi-msb/prune.ts +16 -12
- package/extensions/pi-msb/sandbox-manager.ts +39 -138
- package/extensions/pi-msb/tools.ts +13 -26
- package/extensions/pi-msb/transport.ts +0 -18
- package/extensions/pi-msb/types.ts +24 -108
- package/extensions/pi-msb/workspace.ts +126 -0
- package/native/flock/prebuilds/darwin-arm64/flock.node +0 -0
- package/package.json +1 -1
- package/extensions/pi-msb/git.ts +0 -256
- package/extensions/pi-msb/storage.ts +0 -332
|
@@ -5,317 +5,132 @@ import {
|
|
|
5
5
|
type ManagedSandboxRecord,
|
|
6
6
|
type PersistedSandboxState,
|
|
7
7
|
type SandboxLabelInput,
|
|
8
|
-
type StorageMode,
|
|
9
8
|
type ValidatedManagedSandbox,
|
|
10
|
-
type VolumeLabelInput,
|
|
11
|
-
type VolumeRecord,
|
|
12
9
|
} from "./types.ts";
|
|
13
10
|
|
|
14
|
-
const STORAGE_MODES: readonly StorageMode[] = ["git", "direct", "none"];
|
|
15
|
-
|
|
16
11
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
17
12
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
13
|
}
|
|
19
|
-
|
|
20
|
-
function isStorageMode(value: unknown): value is StorageMode {
|
|
21
|
-
return typeof value === "string" && STORAGE_MODES.includes(value as StorageMode);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
14
|
function nonEmptyString(value: unknown): value is string {
|
|
25
15
|
return typeof value === "string" && value.length > 0;
|
|
26
16
|
}
|
|
27
|
-
|
|
28
17
|
function finiteNumber(value: unknown): value is number {
|
|
29
18
|
return typeof value === "number" && Number.isFinite(value);
|
|
30
19
|
}
|
|
31
20
|
|
|
32
|
-
/**
|
|
33
|
-
* SDK versions have returned labels both as maps and as entry/tuple arrays.
|
|
34
|
-
* Keep that tolerance in this module so callers never need to know the SDK shape.
|
|
35
|
-
*/
|
|
21
|
+
/** SDK versions have returned labels both as maps and as entry/tuple arrays. */
|
|
36
22
|
function normalizeLabels(value: unknown): Record<string, string> | null {
|
|
37
23
|
const labels: Record<string, string> = {};
|
|
38
|
-
|
|
39
24
|
if (isRecord(value)) {
|
|
40
|
-
for (const [key,
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
typeof labelValue === "string" ||
|
|
44
|
-
typeof labelValue === "number" ||
|
|
45
|
-
typeof labelValue === "boolean"
|
|
46
|
-
) {
|
|
47
|
-
labels[key] = String(labelValue);
|
|
25
|
+
for (const [key, item] of Object.entries(value)) {
|
|
26
|
+
if (key && (typeof item === "string" || typeof item === "number" || typeof item === "boolean")) {
|
|
27
|
+
labels[key] = String(item);
|
|
48
28
|
}
|
|
49
29
|
}
|
|
50
30
|
return labels;
|
|
51
31
|
}
|
|
52
|
-
|
|
53
32
|
if (Array.isArray(value)) {
|
|
54
33
|
for (const entry of value) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
[key, labelValue] = entry;
|
|
60
|
-
} else if (isRecord(entry)) {
|
|
61
|
-
key = entry.key ?? entry.name;
|
|
62
|
-
labelValue = entry.value;
|
|
34
|
+
const key = Array.isArray(entry) ? entry[0] : isRecord(entry) ? entry.key ?? entry.name : undefined;
|
|
35
|
+
const item = Array.isArray(entry) ? entry[1] : isRecord(entry) ? entry.value : undefined;
|
|
36
|
+
if (typeof key === "string" && key && (typeof item === "string" || typeof item === "number" || typeof item === "boolean")) {
|
|
37
|
+
labels[key] = String(item);
|
|
63
38
|
}
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
typeof key !== "string" ||
|
|
67
|
-
key.length === 0 ||
|
|
68
|
-
(typeof labelValue !== "string" &&
|
|
69
|
-
typeof labelValue !== "number" &&
|
|
70
|
-
typeof labelValue !== "boolean")
|
|
71
|
-
) {
|
|
72
|
-
// A bad label entry must not make otherwise useful SDK output unusable.
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
labels[key] = String(labelValue);
|
|
76
39
|
}
|
|
77
40
|
return labels;
|
|
78
41
|
}
|
|
79
|
-
|
|
80
42
|
return null;
|
|
81
43
|
}
|
|
82
44
|
|
|
83
45
|
function rawRecord(value: unknown): Record<string, unknown> | null {
|
|
84
46
|
if (isRecord(value)) return value;
|
|
85
|
-
|
|
86
|
-
// Some CLI/SDK adapters expose a record as [name, labels]. Accept this only
|
|
87
|
-
// at the normalization boundary; all public functions return object records.
|
|
88
|
-
if (
|
|
89
|
-
Array.isArray(value) &&
|
|
90
|
-
value.length >= 2 &&
|
|
91
|
-
typeof value[0] === "string"
|
|
92
|
-
) {
|
|
47
|
+
if (Array.isArray(value) && value.length >= 2 && typeof value[0] === "string") {
|
|
93
48
|
return { name: value[0], labels: value[1] };
|
|
94
49
|
}
|
|
95
|
-
|
|
96
50
|
return null;
|
|
97
51
|
}
|
|
98
52
|
|
|
99
53
|
function labelsFromRaw(raw: Record<string, unknown>): Record<string, string> | null {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const metadata = raw.metadata;
|
|
104
|
-
if (isRecord(metadata) && metadata.labels !== undefined) {
|
|
105
|
-
return normalizeLabels(metadata.labels);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function optionalString(
|
|
112
|
-
target: Record<string, unknown>,
|
|
113
|
-
key: string,
|
|
114
|
-
value: unknown,
|
|
115
|
-
): void {
|
|
116
|
-
if (typeof value === "string") target[key] = value;
|
|
117
|
-
else delete target[key];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function optionalNumber(
|
|
121
|
-
target: Record<string, unknown>,
|
|
122
|
-
key: string,
|
|
123
|
-
value: unknown,
|
|
124
|
-
): void {
|
|
125
|
-
if (finiteNumber(value)) target[key] = value;
|
|
126
|
-
else delete target[key];
|
|
54
|
+
if (raw.labels !== undefined) return normalizeLabels(raw.labels);
|
|
55
|
+
return isRecord(raw.metadata) ? normalizeLabels(raw.metadata.labels) : null;
|
|
127
56
|
}
|
|
128
57
|
|
|
129
58
|
export function buildSandboxLabels(input: SandboxLabelInput): Record<string, string> {
|
|
130
|
-
|
|
59
|
+
return {
|
|
131
60
|
[LABEL_KEYS.managed]: "true",
|
|
132
61
|
[LABEL_KEYS.schema]: String(STATE_SCHEMA_VERSION),
|
|
133
62
|
[LABEL_KEYS.session]: input.sessionId,
|
|
134
|
-
[LABEL_KEYS.mode]: input.mode,
|
|
135
63
|
[LABEL_KEYS.cwd]: input.cwd,
|
|
64
|
+
[LABEL_KEYS.root]: input.root,
|
|
65
|
+
[LABEL_KEYS.guestRoot]: input.guestRoot,
|
|
136
66
|
[LABEL_KEYS.pid]: String(input.pid),
|
|
137
67
|
[LABEL_KEYS.image]: input.image,
|
|
138
68
|
[LABEL_KEYS.keep]: "true",
|
|
139
69
|
};
|
|
140
|
-
|
|
141
|
-
if (input.mode === "git") {
|
|
142
|
-
if (!nonEmptyString(input.volumeName)) {
|
|
143
|
-
throw new Error("git sandbox labels require a volume name");
|
|
144
|
-
}
|
|
145
|
-
labels[LABEL_KEYS.volume] = input.volumeName;
|
|
146
|
-
if (input.seedBranch !== undefined && input.seedBranch !== null) {
|
|
147
|
-
labels[LABEL_KEYS.seedBranch] = input.seedBranch;
|
|
148
|
-
}
|
|
149
|
-
if (input.seedSha !== undefined && input.seedSha !== null) {
|
|
150
|
-
labels[LABEL_KEYS.seedSha] = input.seedSha;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return labels;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function buildVolumeLabels(input: VolumeLabelInput): Record<string, string> {
|
|
158
|
-
const labels: Record<string, string> = {
|
|
159
|
-
[LABEL_KEYS.managed]: "true",
|
|
160
|
-
[LABEL_KEYS.schema]: String(STATE_SCHEMA_VERSION),
|
|
161
|
-
[LABEL_KEYS.session]: input.sessionId,
|
|
162
|
-
[LABEL_KEYS.cwd]: input.cwd,
|
|
163
|
-
[LABEL_KEYS.keep]: "true",
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
if (input.seedBranch !== undefined && input.seedBranch !== null) {
|
|
167
|
-
labels[LABEL_KEYS.seedBranch] = input.seedBranch;
|
|
168
|
-
}
|
|
169
|
-
if (input.seedSha !== undefined && input.seedSha !== null) {
|
|
170
|
-
labels[LABEL_KEYS.seedSha] = input.seedSha;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return labels;
|
|
174
70
|
}
|
|
175
71
|
|
|
176
72
|
export function normalizeSandboxRecord(value: unknown): ManagedSandboxRecord | null {
|
|
177
73
|
const raw = rawRecord(value);
|
|
178
74
|
if (!raw) return null;
|
|
179
|
-
|
|
180
75
|
const labels = labelsFromRaw(raw);
|
|
181
76
|
const name = raw.name ?? raw.id ?? raw.sandboxName;
|
|
182
77
|
if (!labels || !nonEmptyString(name)) return null;
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const raw = rawRecord(value);
|
|
192
|
-
if (!raw) return null;
|
|
193
|
-
|
|
194
|
-
const labels = labelsFromRaw(raw);
|
|
195
|
-
const name = raw.name ?? raw.id ?? raw.volumeName;
|
|
196
|
-
const hostPath = raw.hostPath ?? raw.path ?? raw.host_path;
|
|
197
|
-
if (!labels || !nonEmptyString(name) || !nonEmptyString(hostPath)) return null;
|
|
198
|
-
|
|
199
|
-
const normalized: Record<string, unknown> = { ...raw, name, hostPath, labels };
|
|
200
|
-
optionalString(normalized, "kind", raw.kind ?? raw.type);
|
|
201
|
-
optionalNumber(normalized, "usedBytes", raw.usedBytes ?? raw.used_bytes);
|
|
202
|
-
optionalNumber(normalized, "createdAt", raw.createdAt ?? raw.created_at);
|
|
203
|
-
return normalized as unknown as VolumeRecord;
|
|
78
|
+
const status = raw.status ?? raw.state;
|
|
79
|
+
const createdAt = raw.createdAt ?? raw.created_at;
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
labels,
|
|
83
|
+
...(typeof status === "string" ? { status } : {}),
|
|
84
|
+
...(finiteNumber(createdAt) ? { createdAt } : {}),
|
|
85
|
+
};
|
|
204
86
|
}
|
|
205
87
|
|
|
206
88
|
export function parseCliSandboxList(json: string): ManagedSandboxRecord[] {
|
|
207
89
|
let parsed: unknown;
|
|
208
|
-
try {
|
|
209
|
-
parsed = JSON.parse(json);
|
|
210
|
-
} catch (error) {
|
|
90
|
+
try { parsed = JSON.parse(json); } catch (error) {
|
|
211
91
|
throw new Error("invalid sandbox list JSON", { cause: error });
|
|
212
92
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
throw new Error("invalid sandbox list shape: expected an array or {sandboxes: []}");
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return rows
|
|
224
|
-
.map((row) => normalizeSandboxRecord(row))
|
|
225
|
-
.filter((row): row is ManagedSandboxRecord => row !== null);
|
|
93
|
+
const rows = Array.isArray(parsed)
|
|
94
|
+
? parsed
|
|
95
|
+
: isRecord(parsed) && Array.isArray(parsed.sandboxes)
|
|
96
|
+
? parsed.sandboxes
|
|
97
|
+
: null;
|
|
98
|
+
if (!rows) throw new Error("invalid sandbox list shape: expected an array or {sandboxes: []}");
|
|
99
|
+
return rows.map(normalizeSandboxRecord).filter((row): row is ManagedSandboxRecord => row !== null);
|
|
226
100
|
}
|
|
227
101
|
|
|
228
|
-
export function validateManagedSandbox(
|
|
229
|
-
record: ManagedSandboxRecord,
|
|
230
|
-
): ValidatedManagedSandbox | null {
|
|
102
|
+
export function validateManagedSandbox(record: ManagedSandboxRecord): ValidatedManagedSandbox | null {
|
|
231
103
|
if (!isRecord(record) || !nonEmptyString(record.name)) return null;
|
|
232
|
-
|
|
233
104
|
const labels = normalizeLabels(record.labels);
|
|
234
105
|
if (!labels) return null;
|
|
235
|
-
|
|
236
106
|
const sessionId = labels[LABEL_KEYS.session];
|
|
237
|
-
const mode = labels[LABEL_KEYS.mode];
|
|
238
107
|
const cwd = labels[LABEL_KEYS.cwd];
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
108
|
+
const root = labels[LABEL_KEYS.root];
|
|
109
|
+
const guestRoot = labels[LABEL_KEYS.guestRoot];
|
|
110
|
+
const pid = labels[LABEL_KEYS.pid];
|
|
243
111
|
if (
|
|
244
112
|
labels[LABEL_KEYS.managed] !== "true" ||
|
|
245
113
|
labels[LABEL_KEYS.schema] !== String(STATE_SCHEMA_VERSION) ||
|
|
246
114
|
labels[LABEL_KEYS.keep] !== "true" ||
|
|
247
|
-
!nonEmptyString(sessionId) ||
|
|
248
|
-
!
|
|
249
|
-
!
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
!/^\d+$/.test(pidText) ||
|
|
253
|
-
!Number.isSafeInteger(Number(pidText))
|
|
254
|
-
) {
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (mode === "git" && !nonEmptyString(volumeName)) return null;
|
|
259
|
-
if (mode !== "git" && volumeName !== undefined) return null;
|
|
260
|
-
|
|
261
|
-
const normalized: Record<string, unknown> = {
|
|
262
|
-
...record,
|
|
263
|
-
labels,
|
|
264
|
-
name: record.name,
|
|
265
|
-
sessionId,
|
|
266
|
-
mode,
|
|
267
|
-
cwd,
|
|
268
|
-
};
|
|
269
|
-
if (volumeName !== undefined && volumeName.length > 0) {
|
|
270
|
-
normalized.volumeName = volumeName;
|
|
271
|
-
} else {
|
|
272
|
-
delete normalized.volumeName;
|
|
273
|
-
}
|
|
274
|
-
return normalized as unknown as ValidatedManagedSandbox;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function validOptionalString(value: unknown): boolean {
|
|
278
|
-
return value === undefined || typeof value === "string";
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function validOptionalNullableString(value: unknown): boolean {
|
|
282
|
-
return value === undefined || value === null || typeof value === "string";
|
|
115
|
+
!nonEmptyString(sessionId) || !nonEmptyString(cwd) || !nonEmptyString(root) ||
|
|
116
|
+
!nonEmptyString(guestRoot) || !nonEmptyString(labels[LABEL_KEYS.image]) || !pid || !/^\d+$/.test(pid) ||
|
|
117
|
+
!Number.isSafeInteger(Number(pid))
|
|
118
|
+
) return null;
|
|
119
|
+
return { ...record, labels, sessionId, cwd, root, guestRoot };
|
|
283
120
|
}
|
|
284
121
|
|
|
285
122
|
function validPersistedState(value: Record<string, unknown>): boolean {
|
|
286
|
-
return
|
|
287
|
-
value.
|
|
288
|
-
nonEmptyString(value.
|
|
289
|
-
nonEmptyString(value.
|
|
290
|
-
isStorageMode(value.mode) &&
|
|
291
|
-
nonEmptyString(value.cwd) &&
|
|
292
|
-
nonEmptyString(value.image) &&
|
|
293
|
-
typeof value.enabled === "boolean" &&
|
|
294
|
-
finiteNumber(value.createdAt) &&
|
|
295
|
-
validOptionalString(value.volumeName) &&
|
|
296
|
-
validOptionalString(value.volumeHostPath) &&
|
|
297
|
-
validOptionalNullableString(value.seedBranch) &&
|
|
298
|
-
validOptionalNullableString(value.seedSha)
|
|
299
|
-
);
|
|
123
|
+
return value.version === STATE_SCHEMA_VERSION &&
|
|
124
|
+
nonEmptyString(value.sessionId) && nonEmptyString(value.sandboxName) &&
|
|
125
|
+
nonEmptyString(value.cwd) && nonEmptyString(value.root) && nonEmptyString(value.guestRoot) &&
|
|
126
|
+
nonEmptyString(value.image) && typeof value.enabled === "boolean" && finiteNumber(value.createdAt);
|
|
300
127
|
}
|
|
301
128
|
|
|
302
|
-
export function encodeSessionState(
|
|
303
|
-
state: PersistedSandboxState,
|
|
304
|
-
): PersistedSandboxState {
|
|
305
|
-
// Do not mutate a caller-owned object. Encoding always stamps the current
|
|
306
|
-
// schema so a state written by this module can never be mistaken for an old
|
|
307
|
-
// version after a future schema change.
|
|
129
|
+
export function encodeSessionState(state: PersistedSandboxState): PersistedSandboxState {
|
|
308
130
|
return { ...state, version: STATE_SCHEMA_VERSION };
|
|
309
131
|
}
|
|
310
132
|
|
|
311
|
-
export function decodeSessionState(
|
|
312
|
-
value
|
|
313
|
-
currentSessionId: string,
|
|
314
|
-
): PersistedSandboxState | null {
|
|
315
|
-
if (!isRecord(value) || value.sessionId !== currentSessionId) return null;
|
|
316
|
-
if (!validPersistedState(value)) return null;
|
|
317
|
-
|
|
318
|
-
// Preserve forward-compatible, unknown JSON fields while replacing the
|
|
319
|
-
// validated known fields with their exact runtime values.
|
|
133
|
+
export function decodeSessionState(value: unknown, currentSessionId: string): PersistedSandboxState | null {
|
|
134
|
+
if (!isRecord(value) || value.sessionId !== currentSessionId || !validPersistedState(value)) return null;
|
|
320
135
|
return { ...value, version: STATE_SCHEMA_VERSION } as PersistedSandboxState;
|
|
321
136
|
}
|
|
@@ -47,18 +47,17 @@ async function lockOperations(opts: LocksOptions): Promise<LockOperations> {
|
|
|
47
47
|
function normalizeLockInfo(value: unknown): LockInfo | null {
|
|
48
48
|
if (typeof value !== "object" || value === null) return null;
|
|
49
49
|
const candidate = value as Partial<LockInfo>;
|
|
50
|
-
const { version, sessionId, sandboxName,
|
|
50
|
+
const { version, sessionId, sandboxName, cwd, root, pid, createdAt } = candidate;
|
|
51
51
|
if (version !== LOCKFILE_VERSION
|
|
52
52
|
|| typeof sessionId !== "string" || sessionId.length === 0
|
|
53
53
|
|| typeof sandboxName !== "string" || sandboxName.length === 0
|
|
54
|
-
|| (volumeName !== undefined && (typeof volumeName !== "string" || volumeName.length === 0))
|
|
55
|
-
|| (mode !== "git" && mode !== "direct" && mode !== "none")
|
|
56
54
|
|| typeof cwd !== "string" || !isAbsolute(cwd)
|
|
55
|
+
|| typeof root !== "string" || !isAbsolute(root)
|
|
57
56
|
|| typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0
|
|
58
57
|
|| typeof createdAt !== "number" || !Number.isFinite(createdAt)) {
|
|
59
58
|
return null;
|
|
60
59
|
}
|
|
61
|
-
return { version, sessionId, sandboxName,
|
|
60
|
+
return { version, sessionId, sandboxName, cwd, root, pid, createdAt };
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
function validLockInfo(value: unknown): value is LockInfo {
|
|
@@ -72,12 +71,11 @@ function lockInfoJson(info: LockInfo): string {
|
|
|
72
71
|
version: LOCKFILE_VERSION,
|
|
73
72
|
sessionId: info.sessionId,
|
|
74
73
|
sandboxName: info.sandboxName,
|
|
75
|
-
mode: info.mode,
|
|
76
74
|
cwd: info.cwd,
|
|
75
|
+
root: info.root,
|
|
77
76
|
pid: info.pid,
|
|
78
77
|
createdAt: info.createdAt,
|
|
79
78
|
};
|
|
80
|
-
if (info.volumeName !== undefined) value.volumeName = info.volumeName;
|
|
81
79
|
return `${JSON.stringify(value)}\n`;
|
|
82
80
|
}
|
|
83
81
|
|
|
@@ -26,12 +26,13 @@ const MANAGED_LABELS: Readonly<Record<string, string>> = Object.freeze({
|
|
|
26
26
|
[LABEL_KEYS.managed]: "true",
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
const
|
|
29
|
+
const CURRENT_REQUIRED_LABELS = [
|
|
30
30
|
LABEL_KEYS.managed,
|
|
31
31
|
LABEL_KEYS.schema,
|
|
32
32
|
LABEL_KEYS.session,
|
|
33
|
-
LABEL_KEYS.mode,
|
|
34
33
|
LABEL_KEYS.cwd,
|
|
34
|
+
LABEL_KEYS.root,
|
|
35
|
+
LABEL_KEYS.guestRoot,
|
|
35
36
|
LABEL_KEYS.pid,
|
|
36
37
|
LABEL_KEYS.image,
|
|
37
38
|
LABEL_KEYS.keep,
|
|
@@ -62,17 +63,20 @@ function validateRecord(value: unknown): ManagedSandboxRecord | null {
|
|
|
62
63
|
labels[key] = label;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
for (const key of REQUIRED_LABELS) {
|
|
66
|
-
if (typeof labels[key] !== "string" || labels[key].length === 0) return null;
|
|
67
|
-
}
|
|
68
66
|
if (labels[LABEL_KEYS.managed] !== "true") return null;
|
|
69
|
-
if (labels[LABEL_KEYS.schema]
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (labels[LABEL_KEYS.
|
|
67
|
+
if (!labels[LABEL_KEYS.session] || !/^\d+$/.test(labels[LABEL_KEYS.schema] ?? "")) return null;
|
|
68
|
+
if (Number(labels[LABEL_KEYS.schema]) > STATE_SCHEMA_VERSION) return null;
|
|
69
|
+
|
|
70
|
+
// Old managed schemas are removable after their session owner lock is
|
|
71
|
+
// acquired. Their storage labels are deliberately ignored: prune has no
|
|
72
|
+
// volume capability and legacy volumes must remain untouched.
|
|
73
|
+
if (labels[LABEL_KEYS.schema] === String(STATE_SCHEMA_VERSION)) {
|
|
74
|
+
for (const key of CURRENT_REQUIRED_LABELS) {
|
|
75
|
+
if (typeof labels[key] !== "string" || labels[key].length === 0) return null;
|
|
76
|
+
}
|
|
77
|
+
if (!/^\d+$/.test(labels[LABEL_KEYS.pid]) || !Number.isSafeInteger(Number(labels[LABEL_KEYS.pid]))) return null;
|
|
78
|
+
if (labels[LABEL_KEYS.keep] !== "true") return null;
|
|
79
|
+
}
|
|
76
80
|
|
|
77
81
|
return {
|
|
78
82
|
name: value.name,
|