maka-agent 0.2.0-dev.3.20260830 → 0.2.0-dev.4.20260830

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 (25) hide show
  1. package/dist/cli-core.js +1 -0
  2. package/dist/runtime-host-cli.js +37 -3
  3. package/dist/runtime-host-peer-mesh-management-command.js +17 -0
  4. package/dist/runtime-host-setup-command.js +33 -15
  5. package/dist/runtime-host-systemd-service.js +9 -3
  6. package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
  7. package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
  8. package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
  9. package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
  10. package/node_modules/@maka/runtime/dist/sandbox/macos-seatbelt.js +68 -9
  11. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +22 -3
  12. package/node_modules/@maka/runtime-host/dist/operator/peer-mesh-management-frame.js +14 -0
  13. package/node_modules/@maka/runtime-host/dist/peer-mesh/display-name.js +30 -0
  14. package/node_modules/@maka/runtime-host/dist/peer-mesh/model.js +32 -21
  15. package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +207 -47
  16. package/node_modules/@maka/runtime-host/dist/peer-mesh/owner.js +2 -0
  17. package/node_modules/@maka/runtime-host/dist/peer-mesh/store.js +18 -5
  18. package/node_modules/@maka/runtime-host/dist/protocol/index.js +5 -1
  19. package/node_modules/@maka/runtime-host/dist/protocol/peer-mesh.js +59 -2
  20. package/node_modules/@maka/runtime-host/dist/server/access-authority.js +11 -7
  21. package/node_modules/@maka/runtime-host/dist/server/connection-session.js +3 -0
  22. package/node_modules/@maka/runtime-host/dist/server/execution-service.js +1 -0
  23. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +1 -1
  24. package/node_modules/@maka/runtime-host/dist/server/peer-mesh-authority.js +19 -0
  25. package/package.json +1 -1
package/dist/cli-core.js CHANGED
@@ -435,6 +435,7 @@ export async function runMakaCli(argv = process.argv.slice(2), options = RELEASE
435
435
  expectedTarget: command.expectedTarget,
436
436
  ...(command.meshId !== undefined ? { meshId: command.meshId } : {}),
437
437
  ...(command.peerId ? { peerId: command.peerId } : {}),
438
+ ...(command.displayName !== undefined ? { displayName: command.displayName } : {}),
438
439
  });
439
440
  }
440
441
  case 'runtime-host-service-update': {
@@ -752,17 +752,28 @@ function parseServicePeerMeshCommand(argv) {
752
752
  action !== 'leave' &&
753
753
  action !== 'close' &&
754
754
  action !== 'reconcile' &&
755
- action !== 'transit') {
755
+ action !== 'transit' &&
756
+ action !== 'rename' &&
757
+ action !== 'rename-mesh') {
756
758
  return error(action
757
759
  ? `Unexpected runtime-host service mesh command: ${action}`
758
- : 'runtime-host service mesh requires status, create, invite, join, remove, leave, close, reconcile, or transit');
760
+ : 'runtime-host service mesh requires status, create, invite, join, remove, leave, close, reconcile, transit, rename, or rename-mesh');
759
761
  }
760
762
  let meshId;
761
763
  let peerId;
764
+ let displayName;
765
+ let clientDataRoot;
762
766
  const options = parseManagedServiceOptions(argv.slice(1), {
763
767
  allowConfiguration: false,
764
768
  allowFramed: true,
765
769
  valueOptions: {
770
+ '--client-data-root': (value) => {
771
+ if (clientDataRoot !== undefined)
772
+ return error('Duplicate --client-data-root');
773
+ if (!isSafeAbsolutePath(value))
774
+ return error('--client-data-root must be an absolute path');
775
+ clientDataRoot = value;
776
+ },
766
777
  '--mesh': (value) => {
767
778
  if (meshId !== undefined)
768
779
  return error('Duplicate --mesh');
@@ -777,6 +788,14 @@ function parseServicePeerMeshCommand(argv) {
777
788
  return error('--peer requires a valid Peer ID');
778
789
  peerId = value;
779
790
  },
791
+ '--name': (value) => {
792
+ if (displayName !== undefined)
793
+ return error('Duplicate --name');
794
+ if (!value.trim() || value.trim().length > 80) {
795
+ return error('--name requires a display name of at most 80 characters');
796
+ }
797
+ displayName = value.trim();
798
+ },
780
799
  },
781
800
  flagOptions: {
782
801
  '--off': () => {
@@ -784,6 +803,11 @@ function parseServicePeerMeshCommand(argv) {
784
803
  return error('mesh transit accepts either --mesh or --off');
785
804
  meshId = null;
786
805
  },
806
+ '--clear-name': () => {
807
+ if (displayName !== undefined)
808
+ return error('mesh rename accepts --name or --clear-name');
809
+ displayName = null;
810
+ },
787
811
  },
788
812
  });
789
813
  if ('kind' in options)
@@ -791,7 +815,11 @@ function parseServicePeerMeshCommand(argv) {
791
815
  if (!options.managedRootId || !options.operatorDeploymentId || !options.expectedTarget) {
792
816
  return error('runtime-host service mesh requires --managed-root-id, --operator-deployment-id, and an expected target');
793
817
  }
794
- const needsMesh = action === 'invite' || action === 'remove' || action === 'leave' || action === 'close';
818
+ const needsMesh = action === 'invite' ||
819
+ action === 'remove' ||
820
+ action === 'leave' ||
821
+ action === 'close' ||
822
+ action === 'rename-mesh';
795
823
  if (needsMesh && typeof meshId !== 'string') {
796
824
  return error(`runtime-host service mesh ${action} requires --mesh`);
797
825
  }
@@ -809,6 +837,11 @@ function parseServicePeerMeshCommand(argv) {
809
837
  if (action === 'transit' && meshId === undefined) {
810
838
  return error('runtime-host service mesh transit requires --mesh or --off');
811
839
  }
840
+ if ((action === 'rename' || action === 'rename-mesh') !== (displayName !== undefined)) {
841
+ return error(action === 'rename'
842
+ ? 'runtime-host service mesh rename requires --name or --clear-name'
843
+ : '--name and --clear-name are only valid with mesh rename or rename-mesh');
844
+ }
812
845
  return {
813
846
  kind: 'runtime-host-service-peer-mesh',
814
847
  action,
@@ -819,6 +852,7 @@ function parseServicePeerMeshCommand(argv) {
819
852
  expectedTarget: options.expectedTarget,
820
853
  ...(meshId !== undefined ? { meshId } : {}),
821
854
  ...(peerId ? { peerId } : {}),
855
+ ...(displayName !== undefined ? { displayName } : {}),
822
856
  };
823
857
  }
824
858
  function parseUpdatePolicy(value) {
@@ -139,6 +139,23 @@ async function executePeerMeshAction(connection, options, invitation) {
139
139
  meshId: requiredOption(options.meshId, 'Mesh ID'),
140
140
  }),
141
141
  };
142
+ case 'rename':
143
+ return {
144
+ kind: 'result',
145
+ action: 'rename',
146
+ result: await request('peer.mesh.display-name.set', {
147
+ displayName: requiredOption(options.displayName, 'Display name'),
148
+ }),
149
+ };
150
+ case 'rename-mesh':
151
+ return {
152
+ kind: 'result',
153
+ action: 'rename-mesh',
154
+ result: await request('peer.mesh.rename', {
155
+ meshId: requiredMeshId(options.meshId),
156
+ displayName: requiredOption(options.displayName, 'Display name'),
157
+ }),
158
+ };
142
159
  }
143
160
  }
144
161
  function requiredOption(value, label) {
@@ -22,7 +22,7 @@ import { join, resolve } from 'node:path';
22
22
  import { isDeepStrictEqual } from 'node:util';
23
23
  import { truncateUtf8 } from '@maka/core/diagnostic-log';
24
24
  import { generalizedErrorMessage } from '@maka/core/redaction';
25
- import { activateRuntimeHostManagedDeployment, connectRemoteRuntimeHost, ensureRuntimeHostPeerIdentity, } from '@maka/runtime-host/client';
25
+ import { activateRuntimeHostManagedDeployment, connectRemoteRuntimeHost, ensureRuntimeHostPeerIdentity, RuntimeHostOperationError, } from '@maka/runtime-host/client';
26
26
  import { RuntimeHostManagedDeploymentError as RuntimeHostDeploymentAuthorityError, encodeRuntimeHostSetupFrame, isSha512PackageIntegrity, resolveRuntimeHostManagedDeployment, RUNTIME_HOST_SETUP_ERROR_CODE_MAX_BYTES, RUNTIME_HOST_SETUP_ERROR_MESSAGE_MAX_BYTES, } from '@maka/runtime-host/operator';
27
27
  import { INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID, RUNTIME_HOST_PROTOCOL_VERSION, } from '@maka/runtime-host/protocol';
28
28
  import { prepareRuntimeHostAccessCredential, replaceRuntimeHostAccessCredential, revokeRuntimeHostAccessCredential, RuntimeHostAccessUnavailableError, } from './runtime-host-access-command.js';
@@ -34,7 +34,7 @@ import { repairStorageRootAfterRemount, resolveStorageRoot } from '@maka/storage
34
34
  import { createPlatformRuntimeHostServiceBackend, discoverRuntimeHostLifecycleProvider, resolveRuntimeHostLifecycleProvider, } from './runtime-host-service-management-command.js';
35
35
  import { allocateRuntimeHostLoopbackPort, allocateRuntimeHostPeerPort, effectiveRuntimeHostProjectDirectoryRoots, manageRuntimeHostService, readRuntimeHostManagedServiceConfig, removeRuntimeHostServiceFile, resolveRuntimeHostManagedServiceConfigPath, resolveRuntimeHostManagedServiceId, resolveRuntimeHostManagedProjectDirectoryRoots, RuntimeHostServiceManagerError, withRuntimeHostManagedServiceDeploymentLock, withRuntimeHostManagedServiceLifecycleLock, } from './runtime-host-service-manager.js';
36
36
  import { expandWildcardListenAddresses } from './runtime-host-peer-management-command.js';
37
- import { canDiscardRuntimeHostLifecycleDesiredArtifacts, replaceRuntimeHostLifecycle, resolveRecoverableRuntimeHostManagedDeployment, RUNTIME_HOST_READY_TIMEOUT_MS, } from './runtime-host-lifecycle-transaction.js';
37
+ import { canDiscardRuntimeHostLifecycleDesiredArtifacts, replaceRuntimeHostLifecycle, resolveRecoverableRuntimeHostManagedDeployment, RUNTIME_HOST_READY_TIMEOUT_MS, RuntimeHostLifecycleTransactionError, } from './runtime-host-lifecycle-transaction.js';
38
38
  import { resolveRuntimeHostManagedPeerKeyPath, resolveRuntimeHostPeerNativePath, } from './runtime-host-peer-artifact.js';
39
39
  import { activateRuntimeHostManagedDeploymentWithReconciliation } from './runtime-host-activation-command.js';
40
40
  const SETUP_LOCK_TIMEOUT_MS = 5 * 60_000;
@@ -192,12 +192,13 @@ async function runRuntimeHostSupervisedSetupLocked(options, deps, emit) {
192
192
  await assertLegacyArtifactsAbsent(legacyBackend);
193
193
  if (legacyToMigrate)
194
194
  await assertCompatibleExistingVersion(legacyStatus, options.version);
195
- if (current && current.launch.package.version !== options.version) {
195
+ if (current && current.launch.package.version !== options.version && !options.updateExisting) {
196
196
  throw new RuntimeHostSetupError('version_change_requires_update', `Runtime Host ${current.launch.package.version} is already installed; changing to ${options.version} requires the update workflow`);
197
197
  }
198
198
  const resolvedPackage = await resolveRuntimeHostSetupPackage(options, deps);
199
199
  const { candidate } = resolvedPackage;
200
- if (current && !sameExactPackage(current, candidate)) {
200
+ const packageChanged = current !== undefined && !sameExactPackage(current, candidate);
201
+ if (current && packageChanged && !options.updateExisting) {
201
202
  throw new RuntimeHostSetupError('version_change_requires_update', `Runtime Host ${current.launch.package.version} is already installed; changing its exact package requires the update workflow`);
202
203
  }
203
204
  const lifecycleOffer = current?.lifecycle.mode === 'supervised'
@@ -214,14 +215,16 @@ async function runRuntimeHostSupervisedSetupLocked(options, deps, emit) {
214
215
  sourcePackageRoot: packageRoot,
215
216
  version: candidate.version,
216
217
  packageIntegrity: candidate.integrity,
218
+ ...(current ? { deploymentRoot: current.deploymentRoot } : {}),
217
219
  });
218
220
  let committed = false;
219
221
  try {
220
222
  const desired = await prepareSupervisedDeploymentConfig(options, deps, capability, deployment.cliPath, deployment.root, candidate, current, legacyToMigrate, lifecycleOffer);
221
- if (current && !sameDesiredManagedDeployment(current, desired)) {
222
- if (current.lifecycle.mode === 'supervised') {
223
- throw new RuntimeHostSetupError('configuration_changed', 'Change an existing supervised Runtime Host through its explicit configure or update workflow');
224
- }
223
+ if (current &&
224
+ !sameDesiredManagedDeployment(current, desired) &&
225
+ !options.updateExisting &&
226
+ current.lifecycle.mode === 'supervised') {
227
+ throw new RuntimeHostSetupError('configuration_changed', 'Change an existing supervised Runtime Host through its explicit configure or update workflow');
225
228
  }
226
229
  emit({ kind: 'progress', phase: 'installing_service' });
227
230
  if (legacyToMigrate) {
@@ -233,9 +236,11 @@ async function runRuntimeHostSupervisedSetupLocked(options, deps, emit) {
233
236
  operation: legacyToMigrate
234
237
  ? 'legacy_migration'
235
238
  : current
236
- ? isDeepStrictEqual(current.lifecycle, desired.lifecycle)
237
- ? 'configure'
238
- : 'lifecycle_change'
239
+ ? packageChanged
240
+ ? 'update'
241
+ : isDeepStrictEqual(current.lifecycle, desired.lifecycle)
242
+ ? 'configure'
243
+ : 'lifecycle_change'
239
244
  : 'install',
240
245
  ...(current ? { current } : {}),
241
246
  desired,
@@ -247,6 +252,7 @@ async function runRuntimeHostSupervisedSetupLocked(options, deps, emit) {
247
252
  .then(() => undefined),
248
253
  }
249
254
  : {}),
255
+ allowInterruptActiveTasks: Boolean(current && packageChanged && options.updateExisting),
250
256
  deps: lifecycleDeps,
251
257
  });
252
258
  if (replacement.kind === 'active_tasks') {
@@ -286,8 +292,13 @@ async function runRuntimeHostSupervisedSetupLocked(options, deps, emit) {
286
292
  };
287
293
  }
288
294
  catch (error) {
289
- if (!current && !committed && canDiscardRuntimeHostLifecycleDesiredArtifacts(error)) {
290
- await removeRuntimeHostManagedDeployment(deployment.root, capability.rootId).catch(() => undefined);
295
+ if (!committed && canDiscardRuntimeHostLifecycleDesiredArtifacts(error)) {
296
+ if (current && packageChanged) {
297
+ await deployment.rollback().catch(() => undefined);
298
+ }
299
+ else if (!current) {
300
+ await removeRuntimeHostManagedDeployment(deployment.root, capability.rootId).catch(() => undefined);
301
+ }
291
302
  }
292
303
  throw error;
293
304
  }
@@ -552,6 +563,7 @@ async function runRuntimeHostOnDemandSetupLocked(options, deps, emit) {
552
563
  activateDesired: async () => {
553
564
  await deps.activateDesired({ rootId: capability.rootId });
554
565
  },
566
+ allowInterruptActiveTasks: Boolean(current && packageChanged && options.updateExisting),
555
567
  deps: lifecycleDeps,
556
568
  });
557
569
  if (replacement.kind === 'active_tasks') {
@@ -700,7 +712,7 @@ async function pairAndVerifyRuntimeHostSetup(options, target, deps, emit) {
700
712
  break;
701
713
  }
702
714
  catch (error) {
703
- if (!(error instanceof RuntimeHostAccessUnavailableError) || Date.now() >= deadline) {
715
+ if (!isTransientPairingAvailabilityError(error) || Date.now() >= deadline) {
704
716
  throw error;
705
717
  }
706
718
  await new Promise((resolveWait) => setTimeout(resolveWait, Math.min(PAIRING_AVAILABILITY_POLL_MS, deadline - Date.now())));
@@ -757,6 +769,11 @@ async function pairAndVerifyRuntimeHostSetup(options, target, deps, emit) {
757
769
  throw error;
758
770
  }
759
771
  }
772
+ function isTransientPairingAvailabilityError(error) {
773
+ return (error instanceof RuntimeHostAccessUnavailableError ||
774
+ (error instanceof RuntimeHostOperationError &&
775
+ (error.code === 'host_not_ready' || error.code === 'host_draining')));
776
+ }
760
777
  async function assertCompatibleExistingVersion(status, version) {
761
778
  if (!status.service.config) {
762
779
  if (!status.service.installed)
@@ -828,7 +845,8 @@ function setupFailure(error) {
828
845
  error instanceof RuntimeHostManagedDeploymentError ||
829
846
  error instanceof RuntimeHostDeploymentAuthorityError ||
830
847
  error instanceof RuntimeHostUpdateDiscoveryError ||
831
- error instanceof RuntimeHostUpdatePackageError) {
848
+ error instanceof RuntimeHostUpdatePackageError ||
849
+ error instanceof RuntimeHostLifecycleTransactionError) {
832
850
  code = error.code;
833
851
  message = error.message;
834
852
  }
@@ -127,7 +127,7 @@ export function createSystemdUserRuntimeHostService(serviceId, options) {
127
127
  await runLifecycleAction(context, 'restart');
128
128
  await ensureSystemdUpdateSchedulerStartedIfInstalled(scheduler);
129
129
  },
130
- retire: () => runLifecycleAction(context, 'stop'),
130
+ retire: () => retireSystemdSupervisor(context),
131
131
  logs: async () => {
132
132
  const readJournal = async (unitName) => {
133
133
  const result = await runJournalctl([
@@ -220,7 +220,7 @@ export function createSystemdUserRuntimeHostLifecycleProvider(serviceId, options
220
220
  await context.runSystemctl(['reset-failed', context.unitName]);
221
221
  await runLifecycleAction(context, 'start');
222
222
  },
223
- retire: () => runLifecycleAction(context, 'stop'),
223
+ retire: () => retireSystemdSupervisor(context),
224
224
  logs: () => readJournal(context.unitName),
225
225
  uninstall: () => uninstallSystemdSupervisor(context),
226
226
  },
@@ -507,7 +507,7 @@ async function stopSystemdManagedDeployment(service, scheduler) {
507
507
  errors.push(error);
508
508
  }
509
509
  try {
510
- await runLifecycleAction(service, 'stop');
510
+ await retireSystemdSupervisor(service);
511
511
  }
512
512
  catch (error) {
513
513
  errors.push(error);
@@ -516,6 +516,12 @@ async function stopSystemdManagedDeployment(service, scheduler) {
516
516
  throw new RuntimeHostServiceManagerError('service_manager_operation_failed', 'Unable to stop the Runtime Host managed deployment', { cause: new AggregateError(errors) });
517
517
  }
518
518
  }
519
+ async function retireSystemdSupervisor(context) {
520
+ const status = await readSystemdStatus(context);
521
+ if (!isSystemdUnitRunning(status))
522
+ return;
523
+ await runLifecycleAction(context, 'stop');
524
+ }
519
525
  async function removeSystemdUpdateScheduler(context) {
520
526
  const timerStatus = await readSystemdStatus(context.timer);
521
527
  await stopSystemdUpdateScheduler(context);
@@ -16,6 +16,8 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
+ import { readlinkSync, realpathSync } from 'node:fs';
20
+ import { basename, dirname, resolve } from 'node:path';
19
21
  export const MACOS_SEATBELT_EXECUTABLE = '/usr/bin/sandbox-exec';
20
22
  export const MACOS_SEATBELT_BASE_POLICY = `(version 1)
21
23
  (deny default)
@@ -301,30 +303,87 @@ function resolveRoots(profile, pathContext) {
301
303
  writableRoots,
302
304
  deniedRoots,
303
305
  protectedWritableRoots: profile.fileSystem.protectedMetadata && writableRoots.length > 0
304
- ? uniqueRoots([...protectedWritableRoots, ...pathContext.workspaceRoots])
306
+ ? uniqueRoots([
307
+ ...protectedWritableRoots,
308
+ ...pathContext.workspaceRoots.map(resolveRootPath),
309
+ ])
305
310
  : [],
306
311
  protectedMetadataNames: profile.fileSystem.protectedMetadata?.names ?? [],
307
- runtimeReadableRoots: uniqueRoots(pathContext.runtimeReadableRoots ?? []),
308
- executableRoots: uniqueRoots(pathContext.executableRoots ?? []),
312
+ runtimeReadableRoots: uniqueRoots((pathContext.runtimeReadableRoots ?? []).map(resolveRootPath)),
313
+ executableRoots: uniqueRoots((pathContext.executableRoots ?? []).map(resolveRootPath)),
309
314
  };
310
315
  }
311
316
  function rootsForEntry(entry, pathContext) {
312
317
  if (entry.kind === 'path') {
313
- return [{ path: entry.path, match: entry.match ?? 'subtree' }];
318
+ return [{ path: resolveRootPath(entry.path), match: entry.match ?? 'subtree' }];
314
319
  }
315
320
  switch (entry.special) {
316
321
  case ':root':
317
- return [{ path: '/', match: 'subtree' }];
322
+ return [{ path: resolveRootPath('/'), match: 'subtree' }];
318
323
  case ':workspace_roots':
319
- return pathContext.workspaceRoots.map((path) => ({ path, match: 'subtree' }));
324
+ return pathContext.workspaceRoots.map((path) => ({
325
+ path: resolveRootPath(path),
326
+ match: 'subtree',
327
+ }));
320
328
  case ':tmpdir':
321
- return pathContext.tmpdir ? [{ path: pathContext.tmpdir, match: 'subtree' }] : [];
329
+ return pathContext.tmpdir
330
+ ? [{ path: resolveRootPath(pathContext.tmpdir), match: 'subtree' }]
331
+ : [];
322
332
  case ':slash_tmp':
323
- return [{ path: pathContext.slashTmp ?? '/tmp', match: 'subtree' }];
333
+ return [{ path: resolveRootPath(pathContext.slashTmp ?? '/tmp'), match: 'subtree' }];
324
334
  case ':minimal':
325
- return (pathContext.minimalRoots ?? []).map((path) => ({ path, match: 'subtree' }));
335
+ return (pathContext.minimalRoots ?? []).map((path) => ({
336
+ path: resolveRootPath(path),
337
+ match: 'subtree',
338
+ }));
326
339
  }
327
340
  }
341
+ const MAX_DANGLING_SYMLINK_HOPS = 40;
342
+ /**
343
+ * Seatbelt evaluates kernel-resolved paths, so every root must be emitted in
344
+ * canonical form. A root may not exist yet (a deny for a file that has not
345
+ * been created), so canonicalize the deepest existing ancestor and re-append
346
+ * the missing tail, mirroring `realpathAllowMissing`; an allow and its deny
347
+ * then stay in the same path space. Any other resolution failure (EACCES,
348
+ * ELOOP, ...) propagates and fails policy construction: emitting a root in
349
+ * lexical path space could split an allow from its deny across aliases.
350
+ */
351
+ function resolveRootPath(path) {
352
+ let cursor = resolve(path);
353
+ const missing = [];
354
+ let hops = 0;
355
+ while (true) {
356
+ try {
357
+ return resolve(realpathSync(cursor), ...missing.reverse());
358
+ }
359
+ catch (error) {
360
+ if (!isMissingPathError(error))
361
+ throw error;
362
+ let link = null;
363
+ try {
364
+ link = readlinkSync(cursor);
365
+ }
366
+ catch {
367
+ link = null;
368
+ }
369
+ if (link !== null) {
370
+ if (++hops > MAX_DANGLING_SYMLINK_HOPS)
371
+ throw new Error(`Root ${JSON.stringify(path)} traverses too many dangling symlinks.`);
372
+ cursor = resolve(dirname(cursor), link);
373
+ continue;
374
+ }
375
+ const parent = dirname(cursor);
376
+ if (parent === cursor)
377
+ throw error;
378
+ missing.push(basename(cursor));
379
+ cursor = parent;
380
+ }
381
+ }
382
+ }
383
+ function isMissingPathError(error) {
384
+ const code = error?.code;
385
+ return code === 'ENOENT' || code === 'ENOTDIR';
386
+ }
328
387
  function addUniqueResolvedRoots(target, roots) {
329
388
  for (const root of roots) {
330
389
  if (!target.some((existing) => existing.path === root.path && existing.match === root.match)) {
@@ -16,7 +16,7 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { RuntimeHostPeerError, signRuntimeHostPeerIdentity, startRuntimeHostPeerEndpoint, verifyRuntimeHostPeerIdentity, } from '../transport/peer-native.js';
19
+ import { normalizePeerError, RuntimeHostPeerError, signRuntimeHostPeerIdentity, startRuntimeHostPeerEndpoint, verifyRuntimeHostPeerIdentity, } from '../transport/peer-native.js';
20
20
  import { RuntimeHostPermanentReconnectError } from './reconnect-lifecycle.js';
21
21
  export function createRuntimeHostPeerClientFromEnvironment(environment = process.env, options = {}) {
22
22
  const nativePath = environment.MAKA_RUNTIME_HOST_PEER_NATIVE_PATH;
@@ -86,11 +86,30 @@ class RuntimeHostPeerClientImpl {
86
86
  return Object.freeze({ ...this.#requireEndpoint().transitSnapshot });
87
87
  }
88
88
  configureTransit(input) {
89
- return this.#requireEndpoint().configureTransit(input);
89
+ return this.#requireEndpoint()
90
+ .configureTransit(input)
91
+ .catch((error) => {
92
+ throw normalizePeerError(error);
93
+ });
90
94
  }
91
95
  async connect(input, signal) {
96
+ await this.#prepareRoutes(input, signal);
92
97
  return this.#connect(input, signal, 'application');
93
98
  }
99
+ async #prepareRoutes(input, signal) {
100
+ if (!this.#routeResolver?.prepareRoutes)
101
+ return;
102
+ const deadline = AbortSignal.timeout(Math.min(10_000, input.directDeadlineMs));
103
+ const operationSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
104
+ try {
105
+ await this.#routeResolver.prepareRoutes(input.peerId, operationSignal);
106
+ }
107
+ catch {
108
+ // Route preparation enriches an invitation/profile with fresher Mesh
109
+ // routes. It must not suppress explicit routes the caller already has.
110
+ signal?.throwIfAborted();
111
+ }
112
+ }
94
113
  async connectMeshControl(input, signal) {
95
114
  return this.#connect(input, signal, 'mesh-control');
96
115
  }
@@ -193,7 +212,7 @@ class RuntimeHostPeerClientImpl {
193
212
  }
194
213
  catch (error) {
195
214
  signal?.throwIfAborted();
196
- throw error;
215
+ throw normalizePeerError(error);
197
216
  }
198
217
  finally {
199
218
  settled = true;
@@ -30,6 +30,8 @@ const ACTION_SCHEMA = z.enum([
30
30
  'close',
31
31
  'reconcile',
32
32
  'transit',
33
+ 'rename',
34
+ 'rename-mesh',
33
35
  ]);
34
36
  const FRAME_SCHEMA = z.union([
35
37
  z.object({ kind: z.literal('input'), action: z.literal('join') }).strict(),
@@ -93,6 +95,18 @@ function decodeFrame(value) {
93
95
  action: 'transit',
94
96
  result: decodePeerMeshQueryResult(frame.result),
95
97
  };
98
+ case 'rename':
99
+ return {
100
+ kind: 'result',
101
+ action: 'rename',
102
+ result: decodePeerMeshQueryResult(frame.result),
103
+ };
104
+ case 'rename-mesh':
105
+ return {
106
+ kind: 'result',
107
+ action: 'rename-mesh',
108
+ result: decodePeerMeshQueryResult(frame.result),
109
+ };
96
110
  case 'invite':
97
111
  return {
98
112
  kind: 'result',
@@ -0,0 +1,30 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ export const PEER_MESH_DISPLAY_NAME_MAX_LENGTH = 80;
20
+ export function canonicalPeerMeshDisplayName(value) {
21
+ if (typeof value !== 'string')
22
+ throw new Error('Invalid Peer Mesh display name');
23
+ const displayName = value.trim();
24
+ if (displayName.length === 0 ||
25
+ displayName.length > PEER_MESH_DISPLAY_NAME_MAX_LENGTH ||
26
+ /[\u0000-\u001f\u007f]/u.test(displayName)) {
27
+ throw new Error('Invalid Peer Mesh display name');
28
+ }
29
+ return displayName;
30
+ }
@@ -19,6 +19,7 @@
19
19
  import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, sign, timingSafeEqual, verify, } from 'node:crypto';
20
20
  import { decodePeerMeshInvitation as decodePeerMeshInvitationWire, } from '../protocol/peer-mesh.js';
21
21
  import { PEER_MESH_MAX_MEMBERS, PEER_MESH_MAX_ROUTE_HINTS, PEER_MESH_ROUTE_RECORD_MAX_BYTES, } from './limits.js';
22
+ import { canonicalPeerMeshDisplayName } from './display-name.js';
22
23
  export { PEER_MESH_MAX_INVITATION_RECORDS, PEER_MESH_MAX_MEMBERS, PEER_MESH_MAX_MESHES, PEER_MESH_MAX_PENDING_INVITATIONS, PEER_MESH_MAX_ROUTE_HINTS, PEER_MESH_MAX_TRANSIT_ADDRESSES_PER_RELAY, PEER_MESH_MAX_TRANSIT_RELAY_ADDRESSES, PEER_MESH_ROUTE_RECORD_MAX_BYTES, } from './limits.js';
23
24
  export function generatePeerMeshAuthorityKeyPair() {
24
25
  const { publicKey, privateKey } = generateKeyPairSync('ed25519');
@@ -114,13 +115,12 @@ export function validatePeerMeshInvitation(value) {
114
115
  });
115
116
  }
116
117
  export function canonicalPeerMeshRoster(value) {
117
- const record = exactObject(value, 'Peer Mesh roster', [
118
- 'version',
119
- 'meshId',
120
- 'revision',
121
- 'members',
122
- 'closed',
123
- ]);
118
+ const keys = ['version', 'meshId', 'revision', 'members', 'closed'];
119
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
120
+ if (Object.hasOwn(value, 'displayName'))
121
+ keys.push('displayName');
122
+ }
123
+ const record = exactObject(value, 'Peer Mesh roster', keys);
124
124
  if (record.version !== 1)
125
125
  throw new Error('Unsupported Peer Mesh roster version');
126
126
  const members = stringArray(record.members, 'members', PEER_MESH_MAX_MEMBERS, 256)
@@ -137,6 +137,9 @@ export function canonicalPeerMeshRoster(value) {
137
137
  revision: integer(record.revision, 'revision', 1),
138
138
  members: Object.freeze(members),
139
139
  closed: record.closed,
140
+ ...(record.displayName === undefined
141
+ ? {}
142
+ : { displayName: canonicalPeerMeshDisplayName(record.displayName) }),
140
143
  });
141
144
  }
142
145
  export function decodeAuthorityTarget(value) {
@@ -152,22 +155,23 @@ export function decodeAuthorityTarget(value) {
152
155
  });
153
156
  }
154
157
  export function canonicalPeerMeshRouteRecord(value) {
155
- const baseKeys = [
156
- 'version',
157
- 'peerId',
158
- 'sequence',
159
- 'expiresAt',
160
- 'routeHints',
161
- 'coordinationRelays',
162
- ];
163
- const record = exactObject(value, 'Peer Mesh route record', value &&
164
- typeof value === 'object' &&
165
- !Array.isArray(value) &&
166
- Object.hasOwn(value, 'transitMeshId')
167
- ? [...baseKeys, 'transitMeshId']
168
- : baseKeys);
158
+ const keys = ['version', 'peerId', 'sequence', 'expiresAt', 'routeHints', 'coordinationRelays'];
159
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
160
+ if (Object.hasOwn(value, 'endpointKind'))
161
+ keys.push('endpointKind');
162
+ if (Object.hasOwn(value, 'displayName'))
163
+ keys.push('displayName');
164
+ if (Object.hasOwn(value, 'transitMeshId'))
165
+ keys.push('transitMeshId');
166
+ }
167
+ const record = exactObject(value, 'Peer Mesh route record', keys);
169
168
  if (record.version !== 1)
170
169
  throw new Error('Unsupported Peer Mesh route record version');
170
+ if (record.endpointKind !== undefined &&
171
+ record.endpointKind !== 'client' &&
172
+ record.endpointKind !== 'host') {
173
+ throw new Error('Invalid Peer Mesh endpoint kind');
174
+ }
171
175
  const route = Object.freeze({
172
176
  version: 1,
173
177
  peerId: token(record.peerId, 'peerId', 256),
@@ -175,6 +179,10 @@ export function canonicalPeerMeshRouteRecord(value) {
175
179
  expiresAt: integer(record.expiresAt, 'route expiry', 1),
176
180
  routeHints: Object.freeze(addressArray(record.routeHints, 'routeHints')),
177
181
  coordinationRelays: Object.freeze(addressArray(record.coordinationRelays, 'coordinationRelays')),
182
+ ...(record.endpointKind === undefined ? {} : { endpointKind: record.endpointKind }),
183
+ ...(record.displayName === undefined
184
+ ? {}
185
+ : { displayName: canonicalPeerMeshDisplayName(record.displayName) }),
178
186
  ...(record.transitMeshId === undefined
179
187
  ? {}
180
188
  : { transitMeshId: string(record.transitMeshId, 'transitMeshId', 128) }),
@@ -201,6 +209,8 @@ export function decodeSignedPeerMeshRouteRecord(value) {
201
209
  export function peerMeshRouteRecordSigningBytes(route) {
202
210
  return Buffer.from(`maka.peer-mesh.route.v1\n${JSON.stringify({
203
211
  coordinationRelays: route.coordinationRelays,
212
+ ...(route.displayName ? { displayName: route.displayName } : {}),
213
+ ...(route.endpointKind ? { endpointKind: route.endpointKind } : {}),
204
214
  expiresAt: route.expiresAt,
205
215
  peerId: route.peerId,
206
216
  routeHints: route.routeHints,
@@ -212,6 +222,7 @@ export function peerMeshRouteRecordSigningBytes(route) {
212
222
  function encodeRoster(roster) {
213
223
  return Buffer.from(`maka.peer-mesh.roster.v1\n${JSON.stringify({
214
224
  closed: roster.closed,
225
+ ...(roster.displayName ? { displayName: roster.displayName } : {}),
215
226
  members: roster.members,
216
227
  meshId: roster.meshId,
217
228
  revision: roster.revision,