agl 21.0.2 → 22.0.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.
Files changed (73) hide show
  1. package/changelog.md +19 -0
  2. package/dist_serve/bundle.js +520 -509
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.aglhome.d.ts +25 -0
  5. package/dist_ts/classes.aglhome.js +65 -0
  6. package/dist_ts/classes.authmodels.js +7 -12
  7. package/dist_ts/classes.authstore.d.ts +1 -2
  8. package/dist_ts/classes.authstore.js +2 -34
  9. package/dist_ts/classes.cli.js +93 -35
  10. package/dist_ts/classes.config.d.ts +2 -11
  11. package/dist_ts/classes.config.js +14 -80
  12. package/dist_ts/classes.controller.d.ts +4 -1
  13. package/dist_ts/classes.controller.js +109 -46
  14. package/dist_ts/classes.embeddeddb.js +6 -5
  15. package/dist_ts/classes.gitreversion.js +3 -2
  16. package/dist_ts/classes.upgradecoordinator.d.ts +6 -1
  17. package/dist_ts/classes.upgradecoordinator.js +588 -177
  18. package/dist_ts/classes.upgradetransaction.js +46 -12
  19. package/dist_ts/classes.uploadmanager.d.ts +12 -0
  20. package/dist_ts/classes.uploadmanager.js +204 -2
  21. package/dist_ts/constants.upgradeenvironment.d.ts +2 -0
  22. package/dist_ts/constants.upgradeenvironment.js +3 -0
  23. package/dist_ts/functions.controllerdataroot.d.ts +4 -5
  24. package/dist_ts/functions.controllerdataroot.js +4 -29
  25. package/dist_ts/functions.embeddeddb.d.ts +1 -1
  26. package/dist_ts/functions.embeddeddb.js +8 -3
  27. package/dist_ts/functions.runtimeenvironment.js +2 -1
  28. package/dist_ts/index.d.ts +1 -0
  29. package/dist_ts/index.js +2 -1
  30. package/dist_ts/interfaces.config.d.ts +5 -7
  31. package/dist_ts_migration/classes.documentmigrationrunner.js +5 -1
  32. package/dist_ts_migration/index.d.ts +4 -0
  33. package/dist_ts_migration/index.js +5 -1
  34. package/dist_ts_migration/v23_aglhome.d.ts +91 -0
  35. package/dist_ts_migration/v23_aglhome.js +1775 -0
  36. package/dist_ts_migration/v23_runtimeconfig.d.ts +11 -0
  37. package/dist_ts_migration/v23_runtimeconfig.js +87 -0
  38. package/dist_ts_migration/v24_legacyterminallayout.d.ts +18 -0
  39. package/dist_ts_migration/v24_legacyterminallayout.js +79 -0
  40. package/dist_ts_migration/v24_legacyterminallayoutmigration.d.ts +6 -0
  41. package/dist_ts_migration/v24_legacyterminallayoutmigration.js +67 -0
  42. package/dist_ts_migration/v2_controllerdataroot.d.ts +5 -0
  43. package/dist_ts_migration/v2_controllerdataroot.js +176 -15
  44. package/package.json +1 -1
  45. package/readme.md +116 -35
  46. package/readme.plan.md +24 -9
  47. package/ts/00_commitinfo_data.ts +1 -1
  48. package/ts/classes.aglhome.ts +116 -0
  49. package/ts/classes.authmodels.ts +5 -13
  50. package/ts/classes.authstore.ts +1 -54
  51. package/ts/classes.cli.ts +92 -34
  52. package/ts/classes.config.ts +20 -117
  53. package/ts/classes.controller.ts +146 -52
  54. package/ts/classes.embeddeddb.ts +5 -4
  55. package/ts/classes.gitreversion.ts +3 -2
  56. package/ts/classes.upgradecoordinator.ts +668 -191
  57. package/ts/classes.upgradetransaction.ts +46 -11
  58. package/ts/classes.uploadmanager.ts +231 -1
  59. package/ts/constants.upgradeenvironment.ts +2 -0
  60. package/ts/functions.controllerdataroot.ts +11 -47
  61. package/ts/functions.embeddeddb.ts +13 -2
  62. package/ts/functions.runtimeenvironment.ts +1 -0
  63. package/ts/index.ts +1 -0
  64. package/ts/interfaces.config.ts +5 -7
  65. package/ts_migration/classes.documentmigrationrunner.ts +4 -0
  66. package/ts_migration/index.ts +4 -0
  67. package/ts_migration/v23_aglhome.ts +2101 -0
  68. package/ts_migration/v23_runtimeconfig.ts +112 -0
  69. package/ts_migration/v24_legacyterminallayout.ts +131 -0
  70. package/ts_migration/v24_legacyterminallayoutmigration.ts +88 -0
  71. package/ts_migration/v2_controllerdataroot.ts +174 -16
  72. package/ts_web/00_commitinfo_data.ts +1 -1
  73. package/ts_web/elements.harnesscontrollerapp.ts +32 -2
@@ -6,7 +6,7 @@ import {
6
6
  controllerUpgradeManagementVersion,
7
7
  type IControllerStatus,
8
8
  } from '../ts_interfaces/index.js';
9
- import { controllerDataDirectory } from './classes.config.js';
9
+ import { resolveAGLHomePaths } from './classes.aglhome.js';
10
10
  import {
11
11
  isLoopbackPortListening,
12
12
  queryControllerStatus,
@@ -33,9 +33,12 @@ import {
33
33
  upgradePackageTransitionTransactionVersion,
34
34
  upgradePackageTransitionSource,
35
35
  upgradePackageTransitionTarget,
36
- upgradeTokenEnvironmentVariable,
37
36
  serializeUpgradeWorkerPayload,
38
37
  } from './classes.upgradecoordinator.js';
38
+ import {
39
+ upgradeCoordinationRootEnvironmentVariable,
40
+ upgradeTokenEnvironmentVariable,
41
+ } from './constants.upgradeenvironment.js';
39
42
 
40
43
  const npmRegistryUrl = 'https://registry.npmjs.org/';
41
44
  const commandOutputByteLimit = 1024 * 1024;
@@ -50,6 +53,11 @@ const upgradePreparationPollMs = 250;
50
53
  const upgradePreparationSourceCheckMs = 500;
51
54
  const upgradePreparationAcceptanceReconciliationMs = 5_000;
52
55
  const upgradePreparationCleanupAllowanceMs = 60_000;
56
+
57
+ const scrubUpgradeCoordinationEnvironment = (): void => {
58
+ delete process.env[upgradeTokenEnvironmentVariable];
59
+ delete process.env[upgradeCoordinationRootEnvironmentVariable];
60
+ };
53
61
  const upgradeLogPattern = /^upgrade-[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{6}\.[0-9]{3}Z-[A-Za-z0-9_-]{10}\.log$/;
54
62
  const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
55
63
  let upgradeWorkerCliPath: string | undefined;
@@ -1110,7 +1118,7 @@ const openUpgradeLog = async (tokenArg: string): Promise<{
1110
1118
  handle: plugins.fs.promises.FileHandle;
1111
1119
  path: string;
1112
1120
  }> => {
1113
- const directory = controllerDataDirectory();
1121
+ const directory = resolveAGLHomePaths().logs;
1114
1122
  await plugins.fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
1115
1123
  const directoryStats = await plugins.fs.promises.lstat(directory);
1116
1124
  if (
@@ -1290,6 +1298,17 @@ const launchDetachedUpgradeWorkerProcess = async (optionsArg: {
1290
1298
  let child: plugins.childProcess.ChildProcess | undefined;
1291
1299
  let logClosed = false;
1292
1300
  try {
1301
+ const workerEnvironment = { ...(optionsArg.environment ?? process.env) };
1302
+ const inheritedToken = workerEnvironment[upgradeTokenEnvironmentVariable];
1303
+ const useLegacyCoordination = workerEnvironment[upgradeCoordinationRootEnvironmentVariable] === undefined
1304
+ && inheritedToken === optionsArg.payload.token;
1305
+ if (useLegacyCoordination) {
1306
+ delete workerEnvironment[upgradeCoordinationRootEnvironmentVariable];
1307
+ } else if (workerEnvironment[upgradeCoordinationRootEnvironmentVariable] === undefined) {
1308
+ workerEnvironment[upgradeCoordinationRootEnvironmentVariable] = new UpgradeCoordinator(
1309
+ optionsArg.installation.globalRoot,
1310
+ ).baseDirectory;
1311
+ }
1293
1312
  const launchedChild = plugins.childProcess.spawn(
1294
1313
  process.execPath,
1295
1314
  [
@@ -1302,7 +1321,7 @@ const launchDetachedUpgradeWorkerProcess = async (optionsArg: {
1302
1321
  cwd: plugins.os.homedir(),
1303
1322
  detached: true,
1304
1323
  env: {
1305
- ...(optionsArg.environment ?? process.env),
1324
+ ...workerEnvironment,
1306
1325
  [upgradeTokenEnvironmentVariable]: optionsArg.payload.token,
1307
1326
  },
1308
1327
  shell: false,
@@ -2185,8 +2204,14 @@ const runRecoveryUpgradeWorker = async (
2185
2204
  payloadArg: IUpgradeWorkerPayload,
2186
2205
  internalOptionsArg: IUpgradeWorkerInternalOptions,
2187
2206
  ): Promise<void> => {
2188
- const context = parseUpgradeRecoveryWorkerContext(internalOptionsArg.recoveryContext);
2189
- const coordinator = new UpgradeCoordinator(context.canonicalGlobalRoot);
2207
+ const [context, coordinator] = (() => {
2208
+ try {
2209
+ const parsedContext = parseUpgradeRecoveryWorkerContext(internalOptionsArg.recoveryContext);
2210
+ return [parsedContext, new UpgradeCoordinator(parsedContext.canonicalGlobalRoot)] as const;
2211
+ } finally {
2212
+ scrubUpgradeCoordinationEnvironment();
2213
+ }
2214
+ })();
2190
2215
  let lock: Awaited<ReturnType<UpgradeCoordinator['acquireUpgradeLock']>> | undefined;
2191
2216
  let releaseLock = true;
2192
2217
  try {
@@ -2252,16 +2277,26 @@ export async function runUpgradeWorker(
2252
2277
  ): Promise<void> {
2253
2278
  const environmentToken = process.env[upgradeTokenEnvironmentVariable];
2254
2279
  if (environmentToken !== payloadArg.token) {
2280
+ scrubUpgradeCoordinationEnvironment();
2255
2281
  throw new Error(`The detached ${currentCliName} upgrade worker token binding is invalid.`);
2256
2282
  }
2257
- delete process.env[upgradeTokenEnvironmentVariable];
2258
2283
  if (internalOptionsArg.recoveryContext !== undefined) {
2259
- await runRecoveryUpgradeWorker(payloadArg, internalOptionsArg);
2284
+ try {
2285
+ await runRecoveryUpgradeWorker(payloadArg, internalOptionsArg);
2286
+ } finally {
2287
+ scrubUpgradeCoordinationEnvironment();
2288
+ }
2260
2289
  return;
2261
2290
  }
2262
- const installation = await resolveCurrentPnpmGlobalInstallation();
2263
- upgradeWorkerCliPath = installation.cliPath;
2264
- const coordinator = new UpgradeCoordinator(installation.globalRoot);
2291
+ let installation: IPnpmGlobalInstallation;
2292
+ let coordinator: UpgradeCoordinator;
2293
+ try {
2294
+ installation = await resolveCurrentPnpmGlobalInstallation();
2295
+ upgradeWorkerCliPath = installation.cliPath;
2296
+ coordinator = new UpgradeCoordinator(installation.globalRoot);
2297
+ } finally {
2298
+ scrubUpgradeCoordinationEnvironment();
2299
+ }
2265
2300
  let lock: Awaited<ReturnType<UpgradeCoordinator['acquireUpgradeLock']>> | undefined;
2266
2301
  let releaseLock = true;
2267
2302
  try {
@@ -1,4 +1,6 @@
1
1
  import * as plugins from './plugins.js';
2
+ import { resolveAGLHomePaths } from './classes.aglhome.js';
3
+ import { readControllerProcessIdentity } from './classes.processinspection.js';
2
4
  import {
3
5
  controllerMaxAttachmentPromptSuffixBytes,
4
6
  controllerMaxDraftAttachmentBytes,
@@ -9,6 +11,40 @@ import {
9
11
 
10
12
  const maximumActiveUploads = 64;
11
13
  const maximumActiveUploadBytes = 64 * 1024 * 1024;
14
+ const uploadOwnerFileName = '.controller-upload-owner.json';
15
+ const uploadOwnerMaximumBytes = 8 * 1024;
16
+ const uploadOrphanRetentionMs = 24 * 60 * 60 * 1000;
17
+ const maximumStaleUploadNodes = 8_192;
18
+ const maximumStaleUploadDepth = 32;
19
+
20
+ interface IControllerUploadOwner {
21
+ version: 1;
22
+ pid: number;
23
+ fingerprint: string;
24
+ nonce: string;
25
+ createdAt: number;
26
+ }
27
+
28
+ const parseUploadOwner = (valueArg: unknown): IControllerUploadOwner => {
29
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
30
+ throw new Error('Controller upload owner metadata is malformed.');
31
+ }
32
+ const value = valueArg as Record<string, unknown>;
33
+ if (
34
+ Object.keys(value).sort().join('\0')
35
+ !== ['createdAt', 'fingerprint', 'nonce', 'pid', 'version'].sort().join('\0')
36
+ || value.version !== 1
37
+ || !Number.isSafeInteger(value.pid)
38
+ || (value.pid as number) < 2
39
+ || typeof value.fingerprint !== 'string'
40
+ || value.fingerprint.length === 0
41
+ || value.fingerprint.length > 512
42
+ || typeof value.nonce !== 'string'
43
+ || !/^[a-f0-9]{32}$/.test(value.nonce)
44
+ || !Number.isSafeInteger(value.createdAt)
45
+ ) throw new Error('Controller upload owner metadata is invalid.');
46
+ return value as unknown as IControllerUploadOwner;
47
+ };
12
48
 
13
49
  export interface IControllerUploadHandle {
14
50
  id: string;
@@ -24,7 +60,13 @@ interface IActiveUpload {
24
60
  cleanupPromise?: Promise<void>;
25
61
  }
26
62
 
63
+ export interface IControllerUploadManagerOptions {
64
+ rootDirectory?: string;
65
+ readProcessIdentity?: typeof readControllerProcessIdentity;
66
+ }
67
+
27
68
  export class ControllerUploadManager {
69
+ private readonly readProcessIdentity: typeof readControllerProcessIdentity;
28
70
  private rootPromise?: Promise<string>;
29
71
  private readonly activeUploads = new Map<string, IActiveUpload>();
30
72
  private readonly pendingCreates = new Set<Promise<IControllerUploadHandle>>();
@@ -33,6 +75,10 @@ export class ControllerUploadManager {
33
75
  private closed = false;
34
76
  private closePromise?: Promise<void>;
35
77
 
78
+ constructor(private readonly options: IControllerUploadManagerOptions = {}) {
79
+ this.readProcessIdentity = options.readProcessIdentity ?? readControllerProcessIdentity;
80
+ }
81
+
36
82
  public create(
37
83
  attachmentsArg: readonly IControllerDraftAttachment[],
38
84
  ): Promise<IControllerUploadHandle | undefined> {
@@ -193,10 +239,23 @@ export class ControllerUploadManager {
193
239
  this.rootPromise = (async () => {
194
240
  let root: string | undefined;
195
241
  try {
242
+ const baseRoot = this.options.rootDirectory ?? resolveAGLHomePaths().uploads;
243
+ await plugins.fs.promises.mkdir(baseRoot, { recursive: true, mode: 0o700 });
244
+ await plugins.fs.promises.chmod(baseRoot, 0o700);
245
+ await this.cleanupStaleRoots(baseRoot);
196
246
  root = await plugins.fs.promises.mkdtemp(
197
- plugins.path.join(plugins.os.tmpdir(), 'harness-controller-uploads-'),
247
+ plugins.path.join(baseRoot, 'controller-'),
198
248
  );
199
249
  await plugins.fs.promises.chmod(root, 0o700);
250
+ const identity = await readControllerProcessIdentity(process.pid);
251
+ if (!identity) throw new Error('Unable to establish controller upload ownership.');
252
+ await this.writeOwnerMarker(root, {
253
+ version: 1,
254
+ pid: identity.pid,
255
+ fingerprint: identity.fingerprint,
256
+ nonce: plugins.crypto.randomBytes(16).toString('hex'),
257
+ createdAt: Date.now(),
258
+ });
200
259
  return await plugins.fs.promises.realpath(root);
201
260
  } catch (errorArg) {
202
261
  if (root) {
@@ -216,6 +275,177 @@ export class ControllerUploadManager {
216
275
  }
217
276
  }
218
277
 
278
+ private async writeOwnerMarker(
279
+ rootArg: string,
280
+ ownerArg: IControllerUploadOwner,
281
+ ): Promise<void> {
282
+ const markerPath = plugins.path.join(rootArg, uploadOwnerFileName);
283
+ const temporaryPath = `${markerPath}.tmp-${ownerArg.pid}-${ownerArg.nonce.slice(0, 16)}`;
284
+ let handle: plugins.fs.promises.FileHandle | undefined;
285
+ let temporaryCreated = false;
286
+ try {
287
+ handle = await plugins.fs.promises.open(
288
+ temporaryPath,
289
+ plugins.fs.constants.O_WRONLY
290
+ | plugins.fs.constants.O_CREAT
291
+ | plugins.fs.constants.O_EXCL
292
+ | (plugins.fs.constants.O_NOFOLLOW ?? 0),
293
+ 0o600,
294
+ );
295
+ temporaryCreated = true;
296
+ await handle.writeFile(`${JSON.stringify(ownerArg)}\n`, 'utf8');
297
+ await handle.chmod(0o600);
298
+ await handle.sync();
299
+ await handle.close();
300
+ handle = undefined;
301
+ await plugins.fs.promises.rename(temporaryPath, markerPath);
302
+ await plugins.fs.promises.open(rootArg, plugins.fs.constants.O_RDONLY)
303
+ .then(async (directoryHandle) => {
304
+ try {
305
+ await directoryHandle.sync();
306
+ } finally {
307
+ await directoryHandle.close();
308
+ }
309
+ });
310
+ temporaryCreated = false;
311
+ } catch (errorArg) {
312
+ const cleanupErrors: unknown[] = [];
313
+ if (handle) {
314
+ try {
315
+ await handle.close();
316
+ } catch (cleanupErrorArg) {
317
+ cleanupErrors.push(cleanupErrorArg);
318
+ }
319
+ }
320
+ if (temporaryCreated) {
321
+ try {
322
+ await plugins.fs.promises.unlink(temporaryPath);
323
+ } catch (cleanupErrorArg) {
324
+ if ((cleanupErrorArg as NodeJS.ErrnoException).code !== 'ENOENT') {
325
+ cleanupErrors.push(cleanupErrorArg);
326
+ }
327
+ }
328
+ }
329
+ if (cleanupErrors.length > 0) {
330
+ throw new AggregateError(
331
+ [errorArg, ...cleanupErrors],
332
+ 'Controller upload owner publication failed and cleanup was incomplete.',
333
+ { cause: errorArg },
334
+ );
335
+ }
336
+ throw errorArg;
337
+ }
338
+ }
339
+
340
+ private async cleanupStaleRoots(baseRootArg: string): Promise<void> {
341
+ const entries = await plugins.fs.promises.readdir(baseRootArg, { withFileTypes: true });
342
+ if (entries.length > 256) throw new Error('The controller upload root contains too many entries.');
343
+ for (const entry of entries) {
344
+ if (!entry.name.startsWith('controller-')) {
345
+ throw new Error(`Unexpected controller upload root entry: ${entry.name}`);
346
+ }
347
+ const root = plugins.path.join(baseRootArg, entry.name);
348
+ const stats = await plugins.fs.promises.lstat(root);
349
+ if (
350
+ !stats.isDirectory()
351
+ || stats.isSymbolicLink()
352
+ || (typeof process.getuid === 'function' && stats.uid !== process.getuid())
353
+ || (stats.mode & 0o077) !== 0
354
+ ) throw new Error(`Controller upload root is unsafe: ${root}`);
355
+ const markerPath = plugins.path.join(root, uploadOwnerFileName);
356
+ const markerStats = await plugins.fs.promises.lstat(markerPath).catch((errorArg) => {
357
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
358
+ throw errorArg;
359
+ });
360
+ let stale = false;
361
+ if (!markerStats) {
362
+ stale = Date.now() - stats.mtimeMs > uploadOrphanRetentionMs;
363
+ } else {
364
+ if (
365
+ !markerStats.isFile()
366
+ || markerStats.isSymbolicLink()
367
+ || (typeof process.getuid === 'function' && markerStats.uid !== process.getuid())
368
+ || markerStats.nlink !== 1
369
+ || (markerStats.mode & 0o077) !== 0
370
+ || markerStats.size > uploadOwnerMaximumBytes
371
+ ) throw new Error(`Controller upload owner marker is unsafe: ${markerPath}`);
372
+ const owner = parseUploadOwner(JSON.parse(
373
+ await plugins.fs.promises.readFile(markerPath, 'utf8'),
374
+ ) as unknown);
375
+ let identity: Awaited<ReturnType<typeof readControllerProcessIdentity>>;
376
+ try {
377
+ identity = await this.readProcessIdentity(owner.pid);
378
+ } catch (errorArg) {
379
+ throw new Error(`Controller upload owner cannot be verified safely: ${root}`, {
380
+ cause: errorArg,
381
+ });
382
+ }
383
+ stale = !identity || identity.fingerprint !== owner.fingerprint;
384
+ }
385
+ if (!stale) continue;
386
+ await this.removeStaleRoot(root, stats);
387
+ }
388
+ }
389
+
390
+ private async removeStaleRoot(
391
+ rootArg: string,
392
+ identityArg: plugins.fs.Stats,
393
+ ): Promise<void> {
394
+ const current = await plugins.fs.promises.lstat(rootArg);
395
+ if (
396
+ !current.isDirectory()
397
+ || current.isSymbolicLink()
398
+ || current.dev !== identityArg.dev
399
+ || current.ino !== identityArg.ino
400
+ ) throw new Error(`Controller upload root changed before stale cleanup: ${rootArg}`);
401
+ const tombstone = `${rootArg}.stale-${process.pid}-${plugins.crypto.randomBytes(8).toString('hex')}`;
402
+ await plugins.fs.promises.rename(rootArg, tombstone);
403
+ try {
404
+ const moved = await plugins.fs.promises.lstat(tombstone);
405
+ if (moved.dev !== identityArg.dev || moved.ino !== identityArg.ino) {
406
+ throw new Error(`Controller upload stale root identity changed: ${rootArg}`);
407
+ }
408
+ await this.validateStaleTree(tombstone, identityArg.dev);
409
+ await plugins.fs.promises.rm(tombstone, { recursive: true, force: true });
410
+ } finally {
411
+ const parentHandle = await plugins.fs.promises.open(
412
+ plugins.path.dirname(rootArg),
413
+ plugins.fs.constants.O_RDONLY,
414
+ );
415
+ try {
416
+ await parentHandle.sync();
417
+ } finally {
418
+ await parentHandle.close();
419
+ }
420
+ }
421
+ }
422
+
423
+ private async validateStaleTree(rootArg: string, deviceArg: number): Promise<void> {
424
+ const pending = [{ path: rootArg, depth: 0 }];
425
+ let inspected = 0;
426
+ while (pending.length > 0) {
427
+ const current = pending.pop()!;
428
+ const entries = await plugins.fs.promises.readdir(current.path, { withFileTypes: true });
429
+ inspected += entries.length;
430
+ if (inspected > maximumStaleUploadNodes) {
431
+ throw new Error(`Controller upload stale tree contains too many entries: ${rootArg}`);
432
+ }
433
+ for (const entry of entries) {
434
+ const path = plugins.path.join(current.path, entry.name);
435
+ const stats = await plugins.fs.promises.lstat(path);
436
+ if (
437
+ stats.dev !== deviceArg
438
+ || (typeof process.getuid === 'function' && stats.uid !== process.getuid())
439
+ ) throw new Error(`Controller upload stale tree contains a foreign node: ${path}`);
440
+ if (!stats.isDirectory() || stats.isSymbolicLink()) continue;
441
+ if (current.depth >= maximumStaleUploadDepth) {
442
+ throw new Error(`Controller upload stale tree is too deep: ${path}`);
443
+ }
444
+ pending.push({ path, depth: current.depth + 1 });
445
+ }
446
+ }
447
+ }
448
+
219
449
  private async cleanup(idArg: string): Promise<void> {
220
450
  const active = this.activeUploads.get(idArg);
221
451
  if (!active) return;
@@ -0,0 +1,2 @@
1
+ export const upgradeTokenEnvironmentVariable = 'HARNESS_CONTROLLER_UPGRADE_TOKEN';
2
+ export const upgradeCoordinationRootEnvironmentVariable = 'AGL_UPGRADE_COORDINATION_ROOT';
@@ -1,55 +1,19 @@
1
- import * as plugins from './plugins.js';
2
1
  import {
3
- controllerDataRootPaths,
4
- readDatabaseConfig,
5
- type IControllerDataDirectoryOptions,
6
- } from './classes.config.js';
7
- import {
8
- listControllerDataWriterProcessesForCliPaths,
9
- } from './classes.processinspection.js';
10
- import { embeddedDatabaseSocketPath } from './functions.embeddeddb.js';
11
- import {
12
- ControllerDataRootMigrationRunner,
13
- type IControllerDataRootMigrationResult,
14
- type IControllerDataWriterProcessRecord,
15
- } from '../ts_migration/v2_controllerdataroot.js';
2
+ ensureAGLHomeDataRoot,
3
+ preflightAGLHomeDataRootMigration,
4
+ type IAGLHomeDataRootMigrationOptions,
5
+ } from '../ts_migration/v23_aglhome.js';
6
+ import type { IControllerDataRootMigrationResult } from '../ts_migration/v2_controllerdataroot.js';
16
7
 
17
- export interface IEnsureControllerDataRootOptions extends IControllerDataDirectoryOptions {
18
- listDataWriterProcesses?: () => Promise<readonly IControllerDataWriterProcessRecord[]>;
19
- signal?: AbortSignal;
20
- }
8
+ export interface IEnsureControllerDataRootOptions extends IAGLHomeDataRootMigrationOptions {}
9
+
10
+ export const preflightAGLHomeMigration = async (
11
+ optionsArg: IEnsureControllerDataRootOptions = {},
12
+ ): Promise<void> => await preflightAGLHomeDataRootMigration(optionsArg);
21
13
 
22
14
  export const ensureControllerDataRoot = async (
23
15
  optionsArg: IEnsureControllerDataRootOptions = {},
24
- ): Promise<IControllerDataRootMigrationResult> => {
25
- const roots = controllerDataRootPaths(optionsArg);
26
- const databaseConfig = readDatabaseConfig(optionsArg);
27
- if (!roots.migrationRequired) {
28
- return { directoryPath: roots.targetRoot, databaseConfig };
29
- }
30
- const listDataWriterProcesses = optionsArg.listDataWriterProcesses ?? (async () => {
31
- const cliPath = await plugins.fs.promises.realpath(
32
- plugins.url.fileURLToPath(new URL('../cli.js', import.meta.url)),
33
- );
34
- return await listControllerDataWriterProcessesForCliPaths([cliPath], {
35
- includeInstalledPackagePaths: true,
36
- });
37
- });
38
- return await new ControllerDataRootMigrationRunner({
39
- oldRoot: roots.legacyRoot,
40
- newRoot: roots.targetRoot,
41
- databaseConfig,
42
- oldEmbeddedSocketPath: embeddedDatabaseSocketPath(
43
- plugins.path.join(roots.legacyRoot, 'smartdb'),
44
- ),
45
- newEmbeddedSocketPath: embeddedDatabaseSocketPath(
46
- plugins.path.join(roots.targetRoot, 'smartdb'),
47
- ),
48
- invokerPid: process.pid,
49
- listDataWriterProcesses,
50
- ...(optionsArg.signal ? { signal: optionsArg.signal } : {}),
51
- }).run();
52
- };
16
+ ): Promise<IControllerDataRootMigrationResult> => await ensureAGLHomeDataRoot(optionsArg);
53
17
 
54
18
  export interface IEnsureControllerDataRootForStartupOptions
55
19
  extends IEnsureControllerDataRootOptions {
@@ -1,12 +1,23 @@
1
1
  import * as plugins from './plugins.js';
2
+ import { resolveAGLHomePaths } from './classes.aglhome.js';
2
3
 
3
- export const embeddedDatabaseSocketPath = (dataDirectoryArg: string): string => {
4
+ export const embeddedDatabaseSocketPath = (
5
+ dataDirectoryArg: string,
6
+ socketDirectoryArg = resolveAGLHomePaths().sockets,
7
+ ): string => {
4
8
  const directoryHash = plugins.crypto
5
9
  .createHash('sha256')
6
10
  .update(dataDirectoryArg)
7
11
  .digest('hex')
8
12
  .slice(0, 16);
9
- return plugins.path.join(plugins.os.tmpdir(), `harness-controller-${directoryHash}.sock`);
13
+ const socketPath = plugins.path.join(
14
+ socketDirectoryArg,
15
+ `smartdb-${directoryHash}.sock`,
16
+ );
17
+ if (Buffer.byteLength(socketPath, 'utf8') > 100) {
18
+ throw new Error('AGL_HOME is too long for the embedded database Unix socket path.');
19
+ }
20
+ return socketPath;
10
21
  };
11
22
 
12
23
  export const isSocketListening = (socketPathArg: string): Promise<boolean> => new Promise((resolve) => {
@@ -1,6 +1,7 @@
1
1
  const controllerEnvironmentPrefix = 'HARNESS_CONTROLLER_';
2
2
  const forwardedEnvironmentVariable = 'HARNESS_CONTROLLER_FORWARD_ENV';
3
3
  const runtimeEnvironmentAllowlist = new Set([
4
+ 'AGL_HOME',
4
5
  'APPDATA', 'COLORTERM', 'COMSPEC', 'FORCE_COLOR', 'HOME', 'HOMEDRIVE',
5
6
  'HOMEPATH', 'HTTP_PROXY', 'HTTPS_PROXY', 'LANG', 'LOCALAPPDATA', 'LOGNAME',
6
7
  'NODE_EXTRA_CA_CERTS', 'NO_PROXY', 'PATH', 'PATHEXT', 'SHELL', 'SSL_CERT_DIR',
package/ts/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from './00_commitinfo_data.js';
4
4
  export * from './interfaces.auth.js';
5
5
  export * from './interfaces.config.js';
6
6
  export * from './interfaces.lifecycle.js';
7
+ export * from './classes.aglhome.js';
7
8
  export {
8
9
  openCodeExpectedVersion,
9
10
  openCodeUsername,
@@ -4,14 +4,11 @@ export interface IControllerRuntimeConfig {
4
4
  controllerPort: number;
5
5
  publicOrigin: string;
6
6
  rpId: string;
7
- workspaceDirectory: string;
8
7
  /**
9
- * Root directory that bounds all registered projects. Absent only in
10
- * documents persisted before v2; config resolution adopts a value exactly
11
- * once (from --projects-root, else the workspace directory), after which it
12
- * is immutable like every other boundary.
8
+ * Default base for relative project paths and suggestions. Absolute projects
9
+ * remain independently identity-bound and may live outside this directory.
13
10
  */
14
- projectsRoot?: string;
11
+ projectsRoot: string;
15
12
  opencodePort: number;
16
13
  tlsMode: TControllerTlsMode;
17
14
  }
@@ -20,7 +17,8 @@ export interface IControllerRuntimeOverrides {
20
17
  controllerPort: number;
21
18
  publicOrigin?: string;
22
19
  rpId?: string;
23
- workspaceDirectory?: string;
20
+ /** Explicit one-time project registration; never persisted as runtime config. */
21
+ initialProjectDirectory?: string;
24
22
  projectsRoot?: string;
25
23
  opencodePort?: number;
26
24
  behindTlsProxy?: boolean;
@@ -2,6 +2,8 @@ import * as plugins from '../ts/plugins.js';
2
2
  import { ProtocolV6DocumentMigration } from './v6_protocoldocumentmigration.js';
3
3
  import { ProtocolV18ResourceLayoutMigration } from './v18_resourcelayoutmigration.js';
4
4
  import { ProtocolV22ProjectFilesystemIdentityMigration } from './v22_projectfilesystemidentitymigration.js';
5
+ import { RuntimeConfigV23Migration } from './v23_runtimeconfig.js';
6
+ import { LegacyTerminalLayoutV24Migration } from './v24_legacyterminallayoutmigration.js';
5
7
 
6
8
  interface IDocumentMigration {
7
9
  run(): Promise<void>;
@@ -12,6 +14,8 @@ export class PostDatabaseDocumentMigrationRunner {
12
14
 
13
15
  constructor(private readonly database: plugins.smartdata.SmartdataDb | undefined) {
14
16
  this.migrations = [
17
+ new RuntimeConfigV23Migration(this.database),
18
+ new LegacyTerminalLayoutV24Migration(this.database),
15
19
  new ProtocolV6DocumentMigration(this.database),
16
20
  new ProtocolV18ResourceLayoutMigration(this.database),
17
21
  ];
@@ -10,3 +10,7 @@ export * from './v18_resourcelayoutmigration.js';
10
10
  export * from './classes.flexpostharnessmigrationrunner.js';
11
11
  export * from './v19_flexprojectmanagement.js';
12
12
  export * from './v22_projectfilesystemidentitymigration.js';
13
+ export * from './v23_runtimeconfig.js';
14
+ export * from './v23_aglhome.js';
15
+ export * from './v24_legacyterminallayout.js';
16
+ export * from './v24_legacyterminallayoutmigration.js';