agent-relay 5.0.0 → 6.0.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.
Files changed (217) hide show
  1. package/dist/index.cjs +599 -197
  2. package/dist/packages/sdk/src/provisioner/local-jwks.d.ts +25 -0
  3. package/dist/packages/sdk/src/provisioner/local-jwks.d.ts.map +1 -0
  4. package/dist/packages/sdk/src/provisioner/local-jwks.js +70 -0
  5. package/dist/packages/sdk/src/provisioner/local-jwks.js.map +1 -0
  6. package/dist/packages/sdk/src/provisioner/token.d.ts +6 -3
  7. package/dist/packages/sdk/src/provisioner/token.d.ts.map +1 -1
  8. package/dist/packages/sdk/src/provisioner/token.js +11 -8
  9. package/dist/packages/sdk/src/provisioner/token.js.map +1 -1
  10. package/dist/src/cli/commands/on/provision.d.ts +2 -1
  11. package/dist/src/cli/commands/on/provision.d.ts.map +1 -1
  12. package/dist/src/cli/commands/on/provision.js +5 -4
  13. package/dist/src/cli/commands/on/provision.js.map +1 -1
  14. package/dist/src/cli/commands/on/services.d.ts +1 -0
  15. package/dist/src/cli/commands/on/services.d.ts.map +1 -1
  16. package/dist/src/cli/commands/on/services.js +9 -0
  17. package/dist/src/cli/commands/on/services.js.map +1 -1
  18. package/dist/src/cli/commands/on/start.d.ts +6 -3
  19. package/dist/src/cli/commands/on/start.d.ts.map +1 -1
  20. package/dist/src/cli/commands/on/start.js +310 -297
  21. package/dist/src/cli/commands/on/start.js.map +1 -1
  22. package/dist/src/cli/commands/on/token.d.ts +3 -1
  23. package/dist/src/cli/commands/on/token.d.ts.map +1 -1
  24. package/dist/src/cli/commands/on/token.js +3 -3
  25. package/dist/src/cli/commands/on/token.js.map +1 -1
  26. package/node_modules/@agent-relay/broker-darwin-arm64/README.md +11 -0
  27. package/node_modules/@agent-relay/broker-darwin-arm64/package.json +17 -0
  28. package/node_modules/@agent-relay/broker-darwin-x64/README.md +11 -0
  29. package/node_modules/@agent-relay/broker-darwin-x64/bin/.gitkeep +0 -0
  30. package/node_modules/@agent-relay/broker-darwin-x64/package.json +17 -0
  31. package/node_modules/@agent-relay/broker-linux-arm64/README.md +12 -0
  32. package/node_modules/@agent-relay/broker-linux-arm64/bin/.gitkeep +0 -0
  33. package/node_modules/@agent-relay/broker-linux-arm64/package.json +17 -0
  34. package/node_modules/@agent-relay/broker-linux-x64/README.md +12 -0
  35. package/node_modules/@agent-relay/broker-linux-x64/bin/.gitkeep +0 -0
  36. package/node_modules/@agent-relay/broker-linux-x64/package.json +17 -0
  37. package/node_modules/@agent-relay/broker-win32-x64/README.md +11 -0
  38. package/node_modules/@agent-relay/broker-win32-x64/bin/.gitkeep +0 -0
  39. package/node_modules/@agent-relay/broker-win32-x64/package.json +17 -0
  40. package/node_modules/@agent-relay/cloud/package.json +2 -2
  41. package/node_modules/@agent-relay/config/dist/cli-registry.generated.d.ts +353 -157
  42. package/node_modules/@agent-relay/config/dist/cli-registry.generated.d.ts.map +1 -1
  43. package/node_modules/@agent-relay/config/dist/cli-registry.generated.js +356 -160
  44. package/node_modules/@agent-relay/config/dist/cli-registry.generated.js.map +1 -1
  45. package/node_modules/@agent-relay/config/package.json +1 -1
  46. package/node_modules/@agent-relay/hooks/package.json +4 -4
  47. package/node_modules/@agent-relay/sdk/dist/broker-path.d.ts +18 -7
  48. package/node_modules/@agent-relay/sdk/dist/broker-path.d.ts.map +1 -1
  49. package/node_modules/@agent-relay/sdk/dist/broker-path.js +92 -20
  50. package/node_modules/@agent-relay/sdk/dist/broker-path.js.map +1 -1
  51. package/node_modules/@agent-relay/sdk/dist/client.d.ts.map +1 -1
  52. package/node_modules/@agent-relay/sdk/dist/client.js +9 -2
  53. package/node_modules/@agent-relay/sdk/dist/client.js.map +1 -1
  54. package/node_modules/@agent-relay/sdk/dist/provisioner/__tests__/audit.test.js +2 -2
  55. package/node_modules/@agent-relay/sdk/dist/provisioner/__tests__/audit.test.js.map +1 -1
  56. package/node_modules/@agent-relay/sdk/dist/provisioner/__tests__/token-factory.test.js +29 -17
  57. package/node_modules/@agent-relay/sdk/dist/provisioner/__tests__/token-factory.test.js.map +1 -1
  58. package/node_modules/@agent-relay/sdk/dist/provisioner/__tests__/token.test.js +8 -3
  59. package/node_modules/@agent-relay/sdk/dist/provisioner/__tests__/token.test.js.map +1 -1
  60. package/node_modules/@agent-relay/sdk/dist/provisioner/index.d.ts +1 -0
  61. package/node_modules/@agent-relay/sdk/dist/provisioner/index.d.ts.map +1 -1
  62. package/node_modules/@agent-relay/sdk/dist/provisioner/index.js +5 -2
  63. package/node_modules/@agent-relay/sdk/dist/provisioner/index.js.map +1 -1
  64. package/node_modules/@agent-relay/sdk/dist/provisioner/local-jwks.d.ts +25 -0
  65. package/node_modules/@agent-relay/sdk/dist/provisioner/local-jwks.d.ts.map +1 -0
  66. package/node_modules/@agent-relay/sdk/dist/provisioner/local-jwks.js +70 -0
  67. package/node_modules/@agent-relay/sdk/dist/provisioner/local-jwks.js.map +1 -0
  68. package/node_modules/@agent-relay/sdk/dist/provisioner/token.d.ts +6 -3
  69. package/node_modules/@agent-relay/sdk/dist/provisioner/token.d.ts.map +1 -1
  70. package/node_modules/@agent-relay/sdk/dist/provisioner/token.js +11 -8
  71. package/node_modules/@agent-relay/sdk/dist/provisioner/token.js.map +1 -1
  72. package/node_modules/@agent-relay/sdk/dist/provisioner/types.d.ts +3 -2
  73. package/node_modules/@agent-relay/sdk/dist/provisioner/types.d.ts.map +1 -1
  74. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/sibling-links.test.d.ts +2 -0
  75. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/sibling-links.test.d.ts.map +1 -0
  76. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/sibling-links.test.js +166 -0
  77. package/node_modules/@agent-relay/sdk/dist/workflows/__tests__/sibling-links.test.js.map +1 -0
  78. package/node_modules/@agent-relay/sdk/dist/workflows/index.d.ts +2 -0
  79. package/node_modules/@agent-relay/sdk/dist/workflows/index.d.ts.map +1 -1
  80. package/node_modules/@agent-relay/sdk/dist/workflows/index.js +1 -0
  81. package/node_modules/@agent-relay/sdk/dist/workflows/index.js.map +1 -1
  82. package/node_modules/@agent-relay/sdk/dist/workflows/runner.d.ts.map +1 -1
  83. package/node_modules/@agent-relay/sdk/dist/workflows/runner.js +18 -1
  84. package/node_modules/@agent-relay/sdk/dist/workflows/runner.js.map +1 -1
  85. package/node_modules/@agent-relay/sdk/dist/workflows/sibling-links.d.ts +100 -0
  86. package/node_modules/@agent-relay/sdk/dist/workflows/sibling-links.d.ts.map +1 -0
  87. package/node_modules/@agent-relay/sdk/dist/workflows/sibling-links.js +205 -0
  88. package/node_modules/@agent-relay/sdk/dist/workflows/sibling-links.js.map +1 -0
  89. package/node_modules/@agent-relay/sdk/package.json +10 -3
  90. package/node_modules/@agent-relay/telemetry/package.json +1 -1
  91. package/node_modules/@agent-relay/trajectory/package.json +2 -2
  92. package/node_modules/@agent-relay/user-directory/package.json +2 -2
  93. package/node_modules/@agent-relay/utils/package.json +2 -2
  94. package/node_modules/@aws-sdk/core/package.json +5 -5
  95. package/node_modules/@aws-sdk/credential-provider-env/package.json +2 -2
  96. package/node_modules/@aws-sdk/credential-provider-http/package.json +5 -5
  97. package/node_modules/@aws-sdk/credential-provider-ini/package.json +9 -9
  98. package/node_modules/@aws-sdk/credential-provider-login/package.json +3 -3
  99. package/node_modules/@aws-sdk/credential-provider-node/package.json +7 -7
  100. package/node_modules/@aws-sdk/credential-provider-process/package.json +2 -2
  101. package/node_modules/@aws-sdk/credential-provider-sso/package.json +4 -4
  102. package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +3 -3
  103. package/node_modules/@aws-sdk/middleware-flexible-checksums/package.json +4 -4
  104. package/node_modules/@aws-sdk/middleware-sdk-s3/package.json +5 -5
  105. package/node_modules/@aws-sdk/middleware-user-agent/package.json +4 -4
  106. package/node_modules/@aws-sdk/nested-clients/package.json +14 -14
  107. package/node_modules/@aws-sdk/signature-v4-multi-region/package.json +2 -2
  108. package/node_modules/@aws-sdk/token-providers/package.json +3 -3
  109. package/node_modules/@aws-sdk/util-user-agent-node/package.json +2 -2
  110. package/node_modules/@aws-sdk/xml-builder/dist-cjs/xml-parser.js +0 -2
  111. package/node_modules/@aws-sdk/xml-builder/dist-es/xml-parser.js +0 -2
  112. package/node_modules/@aws-sdk/xml-builder/package.json +2 -2
  113. package/node_modules/@nodable/entities/README.md +41 -0
  114. package/node_modules/@nodable/entities/package.json +54 -0
  115. package/node_modules/@nodable/entities/src/EntityDecoder.js +543 -0
  116. package/node_modules/@nodable/entities/src/EntityEncoder.js +194 -0
  117. package/node_modules/@nodable/entities/src/entities.js +1177 -0
  118. package/node_modules/@nodable/entities/src/entityTries.js +49 -0
  119. package/node_modules/@nodable/entities/src/index.d.ts +264 -0
  120. package/node_modules/@nodable/entities/src/index.js +29 -0
  121. package/node_modules/@smithy/core/package.json +2 -2
  122. package/node_modules/@smithy/middleware-endpoint/package.json +3 -3
  123. package/node_modules/@smithy/middleware-retry/package.json +4 -4
  124. package/node_modules/@smithy/middleware-serde/package.json +2 -2
  125. package/node_modules/@smithy/node-http-handler/dist-cjs/index.js +27 -16
  126. package/node_modules/@smithy/node-http-handler/dist-es/http2/ClientHttp2SessionRef.js +5 -0
  127. package/node_modules/@smithy/node-http-handler/dist-es/node-http2-connection-manager.js +22 -16
  128. package/node_modules/@smithy/node-http-handler/dist-types/http2/ClientHttp2SessionRef.d.ts +4 -0
  129. package/node_modules/@smithy/node-http-handler/dist-types/node-http2-connection-manager.d.ts +2 -4
  130. package/node_modules/@smithy/node-http-handler/package.json +1 -1
  131. package/node_modules/@smithy/smithy-client/package.json +4 -4
  132. package/node_modules/@smithy/util-defaults-mode-browser/package.json +2 -2
  133. package/node_modules/@smithy/util-defaults-mode-node/package.json +2 -2
  134. package/node_modules/@smithy/util-retry/dist-cjs/index.js +20 -10
  135. package/node_modules/@smithy/util-retry/dist-es/StandardRetryStrategy.js +20 -10
  136. package/node_modules/@smithy/util-retry/dist-types/StandardRetryStrategy.d.ts +12 -4
  137. package/node_modules/@smithy/util-retry/package.json +1 -1
  138. package/node_modules/@smithy/util-stream/package.json +2 -2
  139. package/node_modules/fast-xml-parser/CHANGELOG.md +53 -0
  140. package/node_modules/fast-xml-parser/README.md +8 -28
  141. package/node_modules/fast-xml-parser/lib/fxbuilder.min.js +1 -1
  142. package/node_modules/fast-xml-parser/lib/fxbuilder.min.js.map +1 -1
  143. package/node_modules/fast-xml-parser/lib/fxp.cjs +1 -1
  144. package/node_modules/fast-xml-parser/lib/fxp.d.cts +172 -6
  145. package/node_modules/fast-xml-parser/lib/fxp.min.js +1 -1
  146. package/node_modules/fast-xml-parser/lib/fxp.min.js.map +1 -1
  147. package/node_modules/fast-xml-parser/lib/fxparser.min.js +1 -1
  148. package/node_modules/fast-xml-parser/lib/fxparser.min.js.map +1 -1
  149. package/node_modules/fast-xml-parser/package.json +5 -4
  150. package/node_modules/fast-xml-parser/src/fxp.d.ts +162 -3
  151. package/node_modules/fast-xml-parser/src/xmlparser/DocTypeReader.js +2 -5
  152. package/node_modules/fast-xml-parser/src/xmlparser/OptionsBuilder.js +15 -11
  153. package/node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js +168 -244
  154. package/node_modules/fast-xml-parser/src/xmlparser/XMLParser.js +1 -1
  155. package/package.json +9 -10
  156. package/packages/cloud/package.json +2 -2
  157. package/packages/config/dist/cli-registry.generated.d.ts +353 -157
  158. package/packages/config/dist/cli-registry.generated.d.ts.map +1 -1
  159. package/packages/config/dist/cli-registry.generated.js +356 -160
  160. package/packages/config/dist/cli-registry.generated.js.map +1 -1
  161. package/packages/config/package.json +1 -1
  162. package/packages/hooks/package.json +4 -4
  163. package/packages/sdk/dist/broker-path.d.ts +18 -7
  164. package/packages/sdk/dist/broker-path.d.ts.map +1 -1
  165. package/packages/sdk/dist/broker-path.js +92 -20
  166. package/packages/sdk/dist/broker-path.js.map +1 -1
  167. package/packages/sdk/dist/client.d.ts.map +1 -1
  168. package/packages/sdk/dist/client.js +9 -2
  169. package/packages/sdk/dist/client.js.map +1 -1
  170. package/packages/sdk/dist/provisioner/__tests__/audit.test.js +2 -2
  171. package/packages/sdk/dist/provisioner/__tests__/audit.test.js.map +1 -1
  172. package/packages/sdk/dist/provisioner/__tests__/token-factory.test.js +29 -17
  173. package/packages/sdk/dist/provisioner/__tests__/token-factory.test.js.map +1 -1
  174. package/packages/sdk/dist/provisioner/__tests__/token.test.js +8 -3
  175. package/packages/sdk/dist/provisioner/__tests__/token.test.js.map +1 -1
  176. package/packages/sdk/dist/provisioner/index.d.ts +1 -0
  177. package/packages/sdk/dist/provisioner/index.d.ts.map +1 -1
  178. package/packages/sdk/dist/provisioner/index.js +5 -2
  179. package/packages/sdk/dist/provisioner/index.js.map +1 -1
  180. package/packages/sdk/dist/provisioner/local-jwks.d.ts +25 -0
  181. package/packages/sdk/dist/provisioner/local-jwks.d.ts.map +1 -0
  182. package/packages/sdk/dist/provisioner/local-jwks.js +70 -0
  183. package/packages/sdk/dist/provisioner/local-jwks.js.map +1 -0
  184. package/packages/sdk/dist/provisioner/token.d.ts +6 -3
  185. package/packages/sdk/dist/provisioner/token.d.ts.map +1 -1
  186. package/packages/sdk/dist/provisioner/token.js +11 -8
  187. package/packages/sdk/dist/provisioner/token.js.map +1 -1
  188. package/packages/sdk/dist/provisioner/types.d.ts +3 -2
  189. package/packages/sdk/dist/provisioner/types.d.ts.map +1 -1
  190. package/packages/sdk/dist/workflows/__tests__/sibling-links.test.d.ts +2 -0
  191. package/packages/sdk/dist/workflows/__tests__/sibling-links.test.d.ts.map +1 -0
  192. package/packages/sdk/dist/workflows/__tests__/sibling-links.test.js +166 -0
  193. package/packages/sdk/dist/workflows/__tests__/sibling-links.test.js.map +1 -0
  194. package/packages/sdk/dist/workflows/index.d.ts +2 -0
  195. package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
  196. package/packages/sdk/dist/workflows/index.js +1 -0
  197. package/packages/sdk/dist/workflows/index.js.map +1 -1
  198. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  199. package/packages/sdk/dist/workflows/runner.js +18 -1
  200. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  201. package/packages/sdk/dist/workflows/sibling-links.d.ts +100 -0
  202. package/packages/sdk/dist/workflows/sibling-links.d.ts.map +1 -0
  203. package/packages/sdk/dist/workflows/sibling-links.js +205 -0
  204. package/packages/sdk/dist/workflows/sibling-links.js.map +1 -0
  205. package/packages/sdk/package.json +10 -3
  206. package/packages/telemetry/package.json +1 -1
  207. package/packages/trajectory/package.json +2 -2
  208. package/packages/user-directory/package.json +2 -2
  209. package/packages/utils/package.json +2 -2
  210. package/scripts/postinstall.js +9 -146
  211. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  212. package/bin/agent-relay-broker-darwin-x64 +0 -0
  213. package/bin/agent-relay-broker-linux-arm64 +0 -0
  214. package/bin/agent-relay-broker-linux-x64 +0 -0
  215. package/node_modules/fast-xml-parser/lib/pem.d.cts +0 -148
  216. package/node_modules/fast-xml-parser/src/pem.d.ts +0 -135
  217. /package/{bin → node_modules/@agent-relay/broker-darwin-arm64/bin}/.gitkeep +0 -0
@@ -10,6 +10,7 @@ import { launchOnMount } from '@relayfile/local-mount';
10
10
  import { mintToken } from './token.js';
11
11
  import { seedAclRules } from './workspace.js';
12
12
  import { seedWorkspace } from '../../../../packages/sdk/src/provisioner/seeder.js';
13
+ import { createLocalJwks, exportPrivateKeyPem, RELAYAUTH_JWKS_URL_ENV, RELAYAUTH_JWT_KID_ENV, RELAYAUTH_JWT_PRIVATE_KEY_PEM_ENV, } from '../../../../packages/sdk/src/provisioner/local-jwks.js';
13
14
  import { ensureAuthenticated, readStoredAuth } from '@agent-relay/cloud';
14
15
  const DEFAULT_SEED_EXCLUDES = ['.relay', '.git', 'node_modules'];
15
16
  const DEFAULT_RELAYCAST_BASE_URL = 'https://api.relaycast.dev';
@@ -279,13 +280,13 @@ async function createRelaycastWorkspace(fetchFn, baseUrl, workspaceName) {
279
280
  async function requestLocalWorkspaceSession(options) {
280
281
  const fetchFn = options.fetchFn ?? fetch;
281
282
  const relayDir = options.relayDir;
282
- const signingSecret = toString(options.signingSecret);
283
+ const tokenSigningKey = options.tokenSigningKey;
283
284
  const requestedWorkspaceId = normalizeWorkspaceId(options.requestedWorkspaceId);
284
285
  if (!relayDir) {
285
286
  throw new Error('relayDir is required for local workspace sessions');
286
287
  }
287
- if (!signingSecret) {
288
- throw new Error('signingSecret is required for local workspace sessions');
288
+ if (!tokenSigningKey) {
289
+ throw new Error('tokenSigningKey is required for local workspace sessions');
289
290
  }
290
291
  const workspaceId = requestedWorkspaceId ?? generateWorkspaceId();
291
292
  const existing = readWorkspaceRegistry(relayDir)[workspaceId];
@@ -315,7 +316,8 @@ async function requestLocalWorkspaceSession(options) {
315
316
  created: !requestedWorkspaceId,
316
317
  workspaceId,
317
318
  token: mintToken({
318
- secret: signingSecret,
319
+ privateKey: tokenSigningKey.privateKey,
320
+ kid: tokenSigningKey.kid,
319
321
  agentName: options.agentName,
320
322
  workspace: workspaceId,
321
323
  scopes: options.scopes,
@@ -330,7 +332,7 @@ export async function requestWorkspaceSession(options) {
330
332
  const requestedWorkspaceId = normalizeWorkspaceId(options.requestedWorkspaceId);
331
333
  if ((isLocalBaseUrl(options.authBase) || options.preferLocalSession) &&
332
334
  options.relayDir &&
333
- options.signingSecret) {
335
+ options.tokenSigningKey) {
334
336
  return requestLocalWorkspaceSession({ ...options, fetchFn, requestedWorkspaceId });
335
337
  }
336
338
  if (requestedWorkspaceId) {
@@ -418,10 +420,6 @@ function loadConfigFromFile(configPath, projectDir) {
418
420
  const fallbackWorkspace = path.basename(projectDir);
419
421
  const workspace = toString(payload.workspace, toString(root.workspace, fallbackWorkspace));
420
422
  const signing_secret = toString(payload.signing_secret, toString(root.signing_secret, process.env.SIGNING_KEY ?? ''));
421
- if (!signing_secret) {
422
- throw new Error(`relay config at ${configPath} is missing signing_secret and SIGNING_KEY env var is not set. ` +
423
- 'Set signing_secret in your config or export SIGNING_KEY.');
424
- }
425
423
  const agents = normalizeAgents(payload.agents ?? root.agents);
426
424
  return { workspace, signing_secret, agents };
427
425
  }
@@ -730,10 +728,11 @@ function pickDeniedCount(syncOutput) {
730
728
  const match = syncOutput.match(/skipping denied file/gi);
731
729
  return match ? match.length : 0;
732
730
  }
733
- function generateTokenFromScript(config, agent, log, error) {
731
+ function generateTokenFromScript(config, agent, tokenSigningKey, log, error) {
734
732
  try {
735
733
  return mintToken({
736
- secret: config.signing_secret,
734
+ privateKey: tokenSigningKey.privateKey,
735
+ kid: tokenSigningKey.kid,
737
736
  agentName: agent.name,
738
737
  workspace: config.workspace,
739
738
  scopes: agent.scopes,
@@ -789,7 +788,7 @@ function getSandboxFlags(cli) {
789
788
  return [];
790
789
  }
791
790
  }
792
- async function ensureProvisioned(config, agent, relayfileRoot, projectDir, tokenPath, log, error, deps) {
791
+ async function ensureProvisioned(config, agent, tokenSigningKey, relayfileRoot, projectDir, tokenPath, log, error, deps) {
793
792
  try {
794
793
  return readFileSync(tokenPath, 'utf8').trim();
795
794
  }
@@ -797,7 +796,7 @@ async function ensureProvisioned(config, agent, relayfileRoot, projectDir, token
797
796
  // Token file does not exist yet — continue to provision
798
797
  }
799
798
  if (typeof deps?.provision === 'function') {
800
- await deps.provision(config, { ...agent });
799
+ await deps.provision(config, { ...agent }, tokenSigningKey);
801
800
  try {
802
801
  return readFileSync(tokenPath, 'utf8').trim();
803
802
  }
@@ -806,14 +805,14 @@ async function ensureProvisioned(config, agent, relayfileRoot, projectDir, token
806
805
  }
807
806
  }
808
807
  if (typeof deps?.provisionAgentToken === 'function') {
809
- const generated = await deps.provisionAgentToken({ config, agent, tokenPath });
808
+ const generated = await deps.provisionAgentToken({ config, agent, tokenPath, tokenSigningKey });
810
809
  if (typeof generated === 'string' && generated.trim()) {
811
810
  const generatedToken = generated.trim();
812
811
  writeFileSync(tokenPath, `${generatedToken}\n`, { encoding: 'utf8', mode: 0o600 });
813
812
  return generatedToken;
814
813
  }
815
814
  }
816
- const generatedToken = generateTokenFromScript(config, agent, log, error);
815
+ const generatedToken = generateTokenFromScript(config, agent, tokenSigningKey, log, error);
817
816
  if (generatedToken) {
818
817
  ensureDirectory(path.dirname(tokenPath));
819
818
  writeFileSync(tokenPath, `${generatedToken}\n`, { encoding: 'utf8', mode: 0o600 });
@@ -821,7 +820,7 @@ async function ensureProvisioned(config, agent, relayfileRoot, projectDir, token
821
820
  }
822
821
  throw new Error(`missing token for ${agent.name}: ${tokenPath}. Run provisioning before launching relay.`);
823
822
  }
824
- async function ensureServices(authBase, fileBase, deps, log, error) {
823
+ async function ensureServices(authBase, fileBase, jwksUrl, deps, log, error) {
825
824
  const needsLocalAuth = isLocalBaseUrl(authBase);
826
825
  const needsLocalFile = isLocalBaseUrl(fileBase);
827
826
  if (!needsLocalAuth && !needsLocalFile) {
@@ -832,14 +831,14 @@ async function ensureServices(authBase, fileBase, deps, log, error) {
832
831
  if (authHealthy && fileHealthy)
833
832
  return;
834
833
  if (typeof deps?.ensureServicesRunning === 'function') {
835
- await deps.ensureServicesRunning(authBase, fileBase);
834
+ await deps.ensureServicesRunning(authBase, fileBase, jwksUrl);
836
835
  const postAuthHealthy = !needsLocalAuth || (await waitForHttpHealthy(authBase));
837
836
  const postFileHealthy = !needsLocalFile || (await waitForHttpHealthy(fileBase));
838
837
  if (postAuthHealthy && postFileHealthy)
839
838
  return;
840
839
  }
841
840
  if (typeof deps?.startServices === 'function') {
842
- await deps.startServices({ authBase, fileBase });
841
+ await deps.startServices({ authBase, fileBase, jwksUrl });
843
842
  const postAuthHealthy = !needsLocalAuth || (await waitForHttpHealthy(authBase));
844
843
  const postFileHealthy = !needsLocalFile || (await waitForHttpHealthy(fileBase));
845
844
  if (postAuthHealthy && postFileHealthy)
@@ -887,297 +886,311 @@ export async function goOnTheRelay(cli, options, extraArgs, deps = {}) {
887
886
  if (!isCommandAvailable('node') || !isCommandAvailable('npx')) {
888
887
  throw new Error('node and npx must be available in PATH to run relay.');
889
888
  }
890
- ensureStateDirs(relayDir);
891
- const defaultAgentName = toString(options.agent, path.basename(cli));
892
- const config = resolveConfig(projectDir, relayDir, defaultAgentName);
893
- const agent = findAgentConfig(config, defaultAgentName);
894
- const authBase = normalizeBaseUrl(options.cloud && options.url ? options.url : options.portAuth);
895
- const fileBase = normalizeBaseUrl(options.portFile);
896
- // Default: solo local (symlink mount). --shared or --cloud: use relayfile.
897
- const useSymlinkMount = !options.shared && !options.cloud;
898
- await ensureServices(authBase, fileBase, deps, log, error);
899
- const workspaceSession = await requestWorkspaceSession({
900
- authBase,
901
- fallbackRelayfileUrl: fileBase,
902
- requestedWorkspaceId: options.workspace,
903
- workspaceName: config.workspace,
904
- agentName: agent.name,
905
- scopes: agent.scopes,
906
- signingSecret: config.signing_secret,
907
- relayDir,
908
- relaycastBaseUrl: process.env.RELAYCAST_BASE_URL,
909
- fetchFn: deps.fetch,
910
- preferLocalSession: useSymlinkMount,
911
- });
912
- // Compile dotfile permissions for this agent
913
- const hasDots = hasDotfiles(projectDir);
914
- const dotfileAcl = hasDots ? compileDotfiles(projectDir, agent.name, workspaceSession.workspaceId) : null;
915
- const compiledPath = path.join(relayDir, 'compiled-acl.json');
916
- const compiled = extractPermissionPatternsFromCompiled(compiledPath, agent.name);
917
- const fallback = collectPermissionPatternsFromDotfiles(projectDir);
918
- const readonlyPatterns = compiled.readonlyPatterns.length > 0 ? compiled.readonlyPatterns : fallback.readonlyPatterns;
919
- const ignoredPatterns = compiled.ignoredPatterns.length > 0 ? compiled.ignoredPatterns : fallback.ignoredPatterns;
920
- const permsDoc = buildPermissionDoc(agent.name, readonlyPatterns, ignoredPatterns);
921
- const seedExcludes = [...DEFAULT_SEED_EXCLUDES];
922
- if (dotfileAcl) {
923
- for (const [dir, rules] of Object.entries(dotfileAcl.acl)) {
924
- if (rules.some((r) => r.startsWith('deny:agent:'))) {
925
- seedExcludes.push(dir.replace(/^\//, ''));
926
- }
927
- }
928
- }
929
- const mountDirName = `workspace-${sanitizePathComponent(workspaceSession.workspaceId)}-${sanitizePathComponent(agent.name)}`;
930
- // Symlink mounts live under ~/.agent-relay/mounts/ (outside the project tree).
931
- // @relayfile/local-mount refuses any mountDir that overlaps projectDir as a
932
- // safety check against destroying the project on cleanup, and putting mounts
933
- // in $HOME keeps them durable across reboots (unlike $TMPDIR) and scoped to
934
- // the user, consistent with ~/.agent-workforce/. The FUSE path keeps the
935
- // historical in-project location under .relay/ — it's managed by the
936
- // relayfile-mount Go binary, not @relayfile/local-mount, so the overlap
937
- // guard doesn't apply.
938
- const mountDir = useSymlinkMount
939
- ? path.join(os.homedir(), '.agent-relay', 'mounts', mountDirName)
940
- : path.join(relayDir, mountDirName);
941
- const sandboxFlags = getSandboxFlags(cli);
942
- const buildAgentEnv = () => ({
943
- RELAY_AGENT_TOKEN: workspaceSession.token,
944
- RELAYFILE_TOKEN: workspaceSession.token,
945
- RELAYFILE_BASE_URL: workspaceSession.relayfileUrl,
946
- RELAYFILE_WORKSPACE: workspaceSession.workspaceId,
947
- RELAY_WORKSPACE_ID: workspaceSession.workspaceId,
948
- RELAY_DEFAULT_WORKSPACE: workspaceSession.workspaceId,
949
- RELAY_WORKSPACE: mountDir,
950
- RELAY_AGENT_NAME: agent.name,
951
- ...(workspaceSession.relaycastApiKey
952
- ? {
953
- RELAY_API_KEY: workspaceSession.relaycastApiKey,
954
- RELAY_WORKSPACES_JSON: JSON.stringify([
955
- {
956
- workspace_id: workspaceSession.workspaceId,
957
- api_key: workspaceSession.relaycastApiKey,
958
- },
959
- ]),
960
- }
961
- : {}),
962
- });
963
- if (useSymlinkMount) {
964
- log(`Preparing local workspace at ${mountDir}...`);
965
- const agentArgs = [...sandboxFlags, ...extraArgs];
966
- // Extend ignoredPatterns with `_PERMISSIONS.md` so @relayfile/local-mount's
967
- // syncBack() does not copy the permissions doc we write in onBeforeLaunch
968
- // into the user's project directory (the library only hides its own
969
- // _MOUNT_README.md / .relayfile-local-mount marker from sync-back).
970
- const launchIgnoredPatterns = [...ignoredPatterns, '_PERMISSIONS.md'];
971
- // Ensure `.relay` is excluded from the mount — @relayfile/local-mount no
972
- // longer has it in the default excludeDirs list, and seedExcludes already
973
- // includes it for symlink + cloud paths.
974
- const launchResult = await launchOnMount({
975
- cli,
976
- projectDir,
977
- mountDir,
978
- args: agentArgs,
979
- ignoredPatterns: launchIgnoredPatterns,
980
- readonlyPatterns,
981
- excludeDirs: seedExcludes,
982
- env: { ...process.env, ...buildAgentEnv() },
889
+ const localJwks = await createLocalJwks();
890
+ try {
891
+ const privateKeyPem = exportPrivateKeyPem(localJwks.privateKey);
892
+ ensureStateDirs(relayDir);
893
+ const defaultAgentName = toString(options.agent, path.basename(cli));
894
+ const config = resolveConfig(projectDir, relayDir, defaultAgentName);
895
+ const agent = findAgentConfig(config, defaultAgentName);
896
+ const authBase = normalizeBaseUrl(options.cloud && options.url ? options.url : options.portAuth);
897
+ const fileBase = normalizeBaseUrl(options.portFile);
898
+ // Default: solo local (symlink mount). --shared or --cloud: use relayfile.
899
+ const useSymlinkMount = !options.shared && !options.cloud;
900
+ await ensureServices(authBase, fileBase, localJwks.jwksUrl, deps, log, error);
901
+ const workspaceSession = await requestWorkspaceSession({
902
+ authBase,
903
+ fallbackRelayfileUrl: fileBase,
904
+ requestedWorkspaceId: options.workspace,
905
+ workspaceName: config.workspace,
983
906
  agentName: agent.name,
984
- onBeforeLaunch: (realMountDir) => {
985
- // Write the richer agent-relay permissions doc. This coexists with the
986
- // generic _MOUNT_README.md that @relayfile/local-mount writes itself.
987
- writeFileSync(path.join(realMountDir, '_PERMISSIONS.md'), permsDoc, 'utf8');
988
- const mountedFiles = countFilesForSync(realMountDir);
989
- log(`On the relay as ${agent.name}`);
990
- log(` Workspace: ${workspaceSession.workspaceId}`);
991
- log(` Join: ${workspaceSession.joinCommand}`);
992
- log(` Mounted files: ${mountedFiles}`);
993
- log(` Permissions denied (initial sync): 0`);
994
- if (sandboxFlags.length > 0) {
995
- log(` Sandbox: relay-enforced (${sandboxFlags.join(' ')})`);
996
- log(` ⚠ Agent CLI sandbox bypassed — relay file permissions are the only safety layer`);
997
- }
998
- },
999
- onAfterSync: (synced) => {
1000
- log(` ✓ ${synced} file(s) synced back`);
1001
- log(`Cleaned relay mount for ${agent.name}`);
1002
- },
907
+ scopes: agent.scopes,
908
+ tokenSigningKey: localJwks,
909
+ relayDir,
910
+ relaycastBaseUrl: process.env.RELAYCAST_BASE_URL,
911
+ fetchFn: deps.fetch,
912
+ preferLocalSession: useSymlinkMount,
1003
913
  });
1004
- log('Off the relay.');
1005
- exit(launchResult.exitCode);
1006
- return;
1007
- }
1008
- // Cloud / --shared path: use the relayfile-mount FUSE binary.
1009
- const mountBin = process.env.RELAYFILE_ROOT
1010
- ? path.join(process.env.RELAYFILE_ROOT, 'bin', 'relayfile-mount')
1011
- : await ensureRelayfileMountBinary();
1012
- if (!existsSync(mountBin)) {
1013
- throw new Error(`missing relayfile mount binary: ${mountBin}`);
1014
- }
1015
- if (workspaceSession.created) {
1016
- await seedWorkspace(workspaceSession.relayfileUrl, workspaceSession.token, workspaceSession.workspaceId, projectDir, seedExcludes);
1017
- if (dotfileAcl && Object.keys(dotfileAcl.acl).length > 0) {
1018
- await seedAclRules(workspaceSession.relayfileUrl, workspaceSession.token, workspaceSession.workspaceId, dotfileAcl.acl);
1019
- writeFileSync(compiledPath, JSON.stringify({
1020
- workspace: workspaceSession.workspaceId,
1021
- acl: dotfileAcl.acl,
1022
- summary: dotfileAcl.summary,
1023
- agents: [{ name: agent.name, summary: dotfileAcl.summary }],
1024
- }, null, 2) + '\n', { encoding: 'utf8' });
914
+ // Compile dotfile permissions for this agent
915
+ const hasDots = hasDotfiles(projectDir);
916
+ const dotfileAcl = hasDots ? compileDotfiles(projectDir, agent.name, workspaceSession.workspaceId) : null;
917
+ const compiledPath = path.join(relayDir, 'compiled-acl.json');
918
+ const compiled = extractPermissionPatternsFromCompiled(compiledPath, agent.name);
919
+ const fallback = collectPermissionPatternsFromDotfiles(projectDir);
920
+ const readonlyPatterns = compiled.readonlyPatterns.length > 0 ? compiled.readonlyPatterns : fallback.readonlyPatterns;
921
+ const ignoredPatterns = compiled.ignoredPatterns.length > 0 ? compiled.ignoredPatterns : fallback.ignoredPatterns;
922
+ const permsDoc = buildPermissionDoc(agent.name, readonlyPatterns, ignoredPatterns);
923
+ const seedExcludes = [...DEFAULT_SEED_EXCLUDES];
924
+ if (dotfileAcl) {
925
+ for (const [dir, rules] of Object.entries(dotfileAcl.acl)) {
926
+ if (rules.some((r) => r.startsWith('deny:agent:'))) {
927
+ seedExcludes.push(dir.replace(/^\//, ''));
928
+ }
929
+ }
1025
930
  }
1026
- }
1027
- mkdirSync(mountDir, { recursive: true });
1028
- const mountLogPath = path.join(relayDir, 'logs', `${agent.name}-mount.log`);
1029
- writeFileSync(mountLogPath, '', 'utf8');
1030
- const mountBaseArgs = [
1031
- '--base-url',
1032
- workspaceSession.relayfileUrl,
1033
- '--workspace',
1034
- workspaceSession.workspaceId,
1035
- '--local-dir',
1036
- mountDir,
1037
- ];
1038
- const onceArgs = [...mountBaseArgs, '--once'];
1039
- const mountEnv = { ...process.env, RELAYFILE_TOKEN: workspaceSession.token };
1040
- log(`Mounting workspace at ${mountDir}...`);
1041
- let initialSyncOutput = '';
1042
- try {
1043
- initialSyncOutput = await runCommandCapture(mountBin, onceArgs, mountEnv);
1044
- }
1045
- catch (err) {
1046
- const message = err instanceof Error ? err.message : String(err);
1047
- throw new Error(`initial workspace sync failed for ${agent.name}: ${message}`);
1048
- }
1049
- const deniedCount = pickDeniedCount(initialSyncOutput);
1050
- writeFileSync(path.join(mountDir, '_PERMISSIONS.md'), permsDoc, 'utf8');
1051
- const projectDeny = path.join(projectDir, '.agentdeny');
1052
- if (existsSync(projectDeny)) {
1053
- cpSync(projectDeny, path.join(mountDir, '.agentdeny'), { force: true });
1054
- }
1055
- const mountedFiles = countFilesForSync(mountDir);
1056
- log(`On the relay as ${agent.name}`);
1057
- log(` Workspace: ${workspaceSession.workspaceId}`);
1058
- log(` Join: ${workspaceSession.joinCommand}`);
1059
- log(` Mounted files: ${mountedFiles}`);
1060
- log(` Permissions denied (initial sync): ${deniedCount}`);
1061
- if (sandboxFlags.length > 0) {
1062
- log(` Sandbox: relay-enforced (${sandboxFlags.join(' ')})`);
1063
- log(` ⚠ Agent CLI sandbox bypassed — relay file permissions are the only safety layer`);
1064
- }
1065
- const cleanupState = {
1066
- mountDir,
1067
- mountLogPath,
1068
- projectDir,
1069
- relayDir,
1070
- workspace: workspaceSession.workspaceId,
1071
- readonlyPatterns,
1072
- ignoredPatterns,
1073
- };
1074
- let mountProc;
1075
- let agentProc;
1076
- let cleanupDone = false;
1077
- const finalizeCleanup = async () => {
1078
- if (cleanupDone)
1079
- return;
1080
- cleanupDone = true;
1081
- cleanupState.mountProc = mountProc;
1082
- await cleanupRun(cleanupState, agent.name, log);
1083
- };
1084
- try {
1085
- const mountedProc = spawn(mountBin, mountBaseArgs, {
1086
- stdio: ['pipe', 'pipe', 'pipe'],
1087
- env: mountEnv,
1088
- });
1089
- mountProc = mountedProc;
1090
- mountedProc.stdout.on('data', (chunk) => {
1091
- appendFileSync(mountLogPath, chunk);
1092
- });
1093
- mountedProc.stderr.on('data', (chunk) => {
1094
- appendFileSync(mountLogPath, chunk);
931
+ const mountDirName = `workspace-${sanitizePathComponent(workspaceSession.workspaceId)}-${sanitizePathComponent(agent.name)}`;
932
+ // Symlink mounts live under ~/.agent-relay/mounts/ (outside the project tree).
933
+ // @relayfile/local-mount refuses any mountDir that overlaps projectDir as a
934
+ // safety check against destroying the project on cleanup, and putting mounts
935
+ // in $HOME keeps them durable across reboots (unlike $TMPDIR) and scoped to
936
+ // the user, consistent with ~/.agent-workforce/. The FUSE path keeps the
937
+ // historical in-project location under .relay/ — it's managed by the
938
+ // relayfile-mount Go binary, not @relayfile/local-mount, so the overlap
939
+ // guard doesn't apply.
940
+ const mountDir = useSymlinkMount
941
+ ? path.join(os.homedir(), '.agent-relay', 'mounts', mountDirName)
942
+ : path.join(relayDir, mountDirName);
943
+ const sandboxFlags = getSandboxFlags(cli);
944
+ const buildAgentEnv = () => ({
945
+ RELAY_AGENT_TOKEN: workspaceSession.token,
946
+ RELAYFILE_TOKEN: workspaceSession.token,
947
+ RELAYFILE_BASE_URL: workspaceSession.relayfileUrl,
948
+ RELAYFILE_WORKSPACE: workspaceSession.workspaceId,
949
+ [RELAYAUTH_JWKS_URL_ENV]: localJwks.jwksUrl,
950
+ [RELAYAUTH_JWT_PRIVATE_KEY_PEM_ENV]: privateKeyPem,
951
+ [RELAYAUTH_JWT_KID_ENV]: localJwks.kid,
952
+ RELAY_WORKSPACE_ID: workspaceSession.workspaceId,
953
+ RELAY_DEFAULT_WORKSPACE: workspaceSession.workspaceId,
954
+ RELAY_WORKSPACE: mountDir,
955
+ RELAY_AGENT_NAME: agent.name,
956
+ ...(workspaceSession.relaycastApiKey
957
+ ? {
958
+ RELAY_API_KEY: workspaceSession.relaycastApiKey,
959
+ RELAY_WORKSPACES_JSON: JSON.stringify([
960
+ {
961
+ workspace_id: workspaceSession.workspaceId,
962
+ api_key: workspaceSession.relaycastApiKey,
963
+ },
964
+ ]),
965
+ }
966
+ : {}),
1095
967
  });
1096
- await new Promise((resolve, reject) => {
1097
- const timer = setTimeout(() => resolve(), 600);
1098
- mountedProc.on('error', (spawnError) => {
1099
- clearTimeout(timer);
1100
- reject(spawnError);
1101
- });
1102
- mountedProc.on('spawn', () => {
1103
- clearTimeout(timer);
1104
- resolve();
968
+ if (useSymlinkMount) {
969
+ log(`Preparing local workspace at ${mountDir}...`);
970
+ const agentArgs = [...sandboxFlags, ...extraArgs];
971
+ // Extend ignoredPatterns with `_PERMISSIONS.md` so @relayfile/local-mount's
972
+ // syncBack() does not copy the permissions doc we write in onBeforeLaunch
973
+ // into the user's project directory (the library only hides its own
974
+ // _MOUNT_README.md / .relayfile-local-mount marker from sync-back).
975
+ const launchIgnoredPatterns = [...ignoredPatterns, '_PERMISSIONS.md'];
976
+ // Ensure `.relay` is excluded from the mount — @relayfile/local-mount no
977
+ // longer has it in the default excludeDirs list, and seedExcludes already
978
+ // includes it for symlink + cloud paths.
979
+ const launchResult = await launchOnMount({
980
+ cli,
981
+ projectDir,
982
+ mountDir,
983
+ args: agentArgs,
984
+ ignoredPatterns: launchIgnoredPatterns,
985
+ readonlyPatterns,
986
+ excludeDirs: seedExcludes,
987
+ env: { ...process.env, ...buildAgentEnv() },
988
+ agentName: agent.name,
989
+ onBeforeLaunch: (realMountDir) => {
990
+ // Write the richer agent-relay permissions doc. This coexists with the
991
+ // generic _MOUNT_README.md that @relayfile/local-mount writes itself.
992
+ writeFileSync(path.join(realMountDir, '_PERMISSIONS.md'), permsDoc, 'utf8');
993
+ const mountedFiles = countFilesForSync(realMountDir);
994
+ log(`On the relay as ${agent.name}`);
995
+ log(` Workspace: ${workspaceSession.workspaceId}`);
996
+ log(` Join: ${workspaceSession.joinCommand}`);
997
+ log(` Mounted files: ${mountedFiles}`);
998
+ log(` Permissions denied (initial sync): 0`);
999
+ if (sandboxFlags.length > 0) {
1000
+ log(` Sandbox: relay-enforced (${sandboxFlags.join(' ')})`);
1001
+ log(` ⚠ Agent CLI sandbox bypassed — relay file permissions are the only safety layer`);
1002
+ }
1003
+ },
1004
+ onAfterSync: (synced) => {
1005
+ log(` ✓ ${synced} file(s) synced back`);
1006
+ log(`Cleaned relay mount for ${agent.name}`);
1007
+ },
1105
1008
  });
1106
- });
1107
- if (!ensureProcessRunning(mountedProc)) {
1108
- throw new Error(`mount process for ${agent.name} exited before continuing`);
1009
+ log('Off the relay.');
1010
+ exit(launchResult.exitCode);
1011
+ return;
1109
1012
  }
1110
- cleanupState.mountProc = mountProc;
1111
- let agentExitCode = 0;
1112
- await new Promise((resolve, reject) => {
1113
- const envVars = {
1114
- ...process.env,
1115
- ...buildAgentEnv(),
1116
- };
1117
- agentProc = spawn(cli, [...sandboxFlags, ...extraArgs], {
1118
- cwd: mountDir,
1119
- stdio: 'inherit',
1120
- env: envVars,
1013
+ // Cloud / --shared path: use the relayfile-mount FUSE binary.
1014
+ const mountBin = process.env.RELAYFILE_ROOT
1015
+ ? path.join(process.env.RELAYFILE_ROOT, 'bin', 'relayfile-mount')
1016
+ : await ensureRelayfileMountBinary();
1017
+ if (!existsSync(mountBin)) {
1018
+ throw new Error(`missing relayfile mount binary: ${mountBin}`);
1019
+ }
1020
+ if (workspaceSession.created) {
1021
+ await seedWorkspace(workspaceSession.relayfileUrl, workspaceSession.token, workspaceSession.workspaceId, projectDir, seedExcludes);
1022
+ if (dotfileAcl && Object.keys(dotfileAcl.acl).length > 0) {
1023
+ await seedAclRules(workspaceSession.relayfileUrl, workspaceSession.token, workspaceSession.workspaceId, dotfileAcl.acl);
1024
+ writeFileSync(compiledPath, JSON.stringify({
1025
+ workspace: workspaceSession.workspaceId,
1026
+ acl: dotfileAcl.acl,
1027
+ summary: dotfileAcl.summary,
1028
+ agents: [{ name: agent.name, summary: dotfileAcl.summary }],
1029
+ }, null, 2) + '\n', { encoding: 'utf8' });
1030
+ }
1031
+ }
1032
+ mkdirSync(mountDir, { recursive: true });
1033
+ const mountLogPath = path.join(relayDir, 'logs', `${agent.name}-mount.log`);
1034
+ writeFileSync(mountLogPath, '', 'utf8');
1035
+ const mountBaseArgs = [
1036
+ '--base-url',
1037
+ workspaceSession.relayfileUrl,
1038
+ '--workspace',
1039
+ workspaceSession.workspaceId,
1040
+ '--local-dir',
1041
+ mountDir,
1042
+ ];
1043
+ const onceArgs = [...mountBaseArgs, '--once'];
1044
+ const mountEnv = {
1045
+ ...process.env,
1046
+ RELAYFILE_TOKEN: workspaceSession.token,
1047
+ [RELAYAUTH_JWKS_URL_ENV]: localJwks.jwksUrl,
1048
+ };
1049
+ log(`Mounting workspace at ${mountDir}...`);
1050
+ let initialSyncOutput = '';
1051
+ try {
1052
+ initialSyncOutput = await runCommandCapture(mountBin, onceArgs, mountEnv);
1053
+ }
1054
+ catch (err) {
1055
+ const message = err instanceof Error ? err.message : String(err);
1056
+ throw new Error(`initial workspace sync failed for ${agent.name}: ${message}`);
1057
+ }
1058
+ const deniedCount = pickDeniedCount(initialSyncOutput);
1059
+ writeFileSync(path.join(mountDir, '_PERMISSIONS.md'), permsDoc, 'utf8');
1060
+ const projectDeny = path.join(projectDir, '.agentdeny');
1061
+ if (existsSync(projectDeny)) {
1062
+ cpSync(projectDeny, path.join(mountDir, '.agentdeny'), { force: true });
1063
+ }
1064
+ const mountedFiles = countFilesForSync(mountDir);
1065
+ log(`On the relay as ${agent.name}`);
1066
+ log(` Workspace: ${workspaceSession.workspaceId}`);
1067
+ log(` Join: ${workspaceSession.joinCommand}`);
1068
+ log(` Mounted files: ${mountedFiles}`);
1069
+ log(` Permissions denied (initial sync): ${deniedCount}`);
1070
+ if (sandboxFlags.length > 0) {
1071
+ log(` Sandbox: relay-enforced (${sandboxFlags.join(' ')})`);
1072
+ log(` ⚠ Agent CLI sandbox bypassed — relay file permissions are the only safety layer`);
1073
+ }
1074
+ const cleanupState = {
1075
+ mountDir,
1076
+ mountLogPath,
1077
+ projectDir,
1078
+ relayDir,
1079
+ workspace: workspaceSession.workspaceId,
1080
+ readonlyPatterns,
1081
+ ignoredPatterns,
1082
+ };
1083
+ let mountProc;
1084
+ let agentProc;
1085
+ let cleanupDone = false;
1086
+ const finalizeCleanup = async () => {
1087
+ if (cleanupDone)
1088
+ return;
1089
+ cleanupDone = true;
1090
+ cleanupState.mountProc = mountProc;
1091
+ await cleanupRun(cleanupState, agent.name, log);
1092
+ };
1093
+ try {
1094
+ const mountedProc = spawn(mountBin, mountBaseArgs, {
1095
+ stdio: ['pipe', 'pipe', 'pipe'],
1096
+ env: mountEnv,
1121
1097
  });
1122
- let cleanupInProgress;
1123
- const cleanupHook = () => {
1124
- if (agentProc && !agentProc.killed) {
1125
- agentProc.kill('SIGTERM');
1126
- }
1127
- // Wait for the agent process to exit so agentExitCode is set by the close handler,
1128
- // then ensure cleanup completes before resolving — avoids data loss from premature exit
1129
- cleanupInProgress = new Promise((r) => {
1130
- if (!agentProc || agentProc.exitCode !== null) {
1131
- r();
1132
- return;
1133
- }
1134
- const t = setTimeout(r, 2000);
1135
- agentProc.once('close', () => {
1136
- clearTimeout(t);
1137
- r();
1138
- });
1139
- })
1140
- .then(() => finalizeCleanup())
1141
- .then(() => resolve());
1142
- };
1143
- process.once('SIGINT', cleanupHook);
1144
- process.once('SIGTERM', cleanupHook);
1145
- agentProc.on('error', (err) => {
1146
- process.removeListener('SIGINT', cleanupHook);
1147
- process.removeListener('SIGTERM', cleanupHook);
1148
- reject(err);
1098
+ mountProc = mountedProc;
1099
+ mountedProc.stdout.on('data', (chunk) => {
1100
+ appendFileSync(mountLogPath, chunk);
1149
1101
  });
1150
- agentProc.on('close', (code, signal) => {
1151
- process.removeListener('SIGINT', cleanupHook);
1152
- process.removeListener('SIGTERM', cleanupHook);
1153
- if (typeof code === 'number') {
1154
- agentExitCode = code;
1155
- }
1156
- else if (signal === 'SIGINT') {
1157
- agentExitCode = 130;
1158
- }
1159
- else if (signal === 'SIGTERM') {
1160
- agentExitCode = 143;
1161
- }
1162
- else {
1163
- agentExitCode = 1;
1164
- }
1165
- // If cleanup was triggered by a signal, wait for it to finish
1166
- if (cleanupInProgress) {
1167
- cleanupInProgress.then(() => resolve());
1168
- }
1169
- else {
1102
+ mountedProc.stderr.on('data', (chunk) => {
1103
+ appendFileSync(mountLogPath, chunk);
1104
+ });
1105
+ await new Promise((resolve, reject) => {
1106
+ const timer = setTimeout(() => resolve(), 600);
1107
+ mountedProc.on('error', (spawnError) => {
1108
+ clearTimeout(timer);
1109
+ reject(spawnError);
1110
+ });
1111
+ mountedProc.on('spawn', () => {
1112
+ clearTimeout(timer);
1170
1113
  resolve();
1171
- }
1114
+ });
1172
1115
  });
1173
- // Finalization happens in outer finally.
1174
- });
1175
- await finalizeCleanup();
1176
- log('Off the relay.');
1177
- exit(agentExitCode);
1116
+ if (!ensureProcessRunning(mountedProc)) {
1117
+ throw new Error(`mount process for ${agent.name} exited before continuing`);
1118
+ }
1119
+ cleanupState.mountProc = mountProc;
1120
+ let agentExitCode = 0;
1121
+ await new Promise((resolve, reject) => {
1122
+ const envVars = {
1123
+ ...process.env,
1124
+ ...buildAgentEnv(),
1125
+ };
1126
+ agentProc = spawn(cli, [...sandboxFlags, ...extraArgs], {
1127
+ cwd: mountDir,
1128
+ stdio: 'inherit',
1129
+ env: envVars,
1130
+ });
1131
+ let cleanupInProgress;
1132
+ const cleanupHook = () => {
1133
+ if (agentProc && !agentProc.killed) {
1134
+ agentProc.kill('SIGTERM');
1135
+ }
1136
+ // Wait for the agent process to exit so agentExitCode is set by the close handler,
1137
+ // then ensure cleanup completes before resolving — avoids data loss from premature exit
1138
+ cleanupInProgress = new Promise((r) => {
1139
+ if (!agentProc || agentProc.exitCode !== null) {
1140
+ r();
1141
+ return;
1142
+ }
1143
+ const t = setTimeout(r, 2000);
1144
+ agentProc.once('close', () => {
1145
+ clearTimeout(t);
1146
+ r();
1147
+ });
1148
+ })
1149
+ .then(() => finalizeCleanup())
1150
+ .then(() => resolve());
1151
+ };
1152
+ process.once('SIGINT', cleanupHook);
1153
+ process.once('SIGTERM', cleanupHook);
1154
+ agentProc.on('error', (err) => {
1155
+ process.removeListener('SIGINT', cleanupHook);
1156
+ process.removeListener('SIGTERM', cleanupHook);
1157
+ reject(err);
1158
+ });
1159
+ agentProc.on('close', (code, signal) => {
1160
+ process.removeListener('SIGINT', cleanupHook);
1161
+ process.removeListener('SIGTERM', cleanupHook);
1162
+ if (typeof code === 'number') {
1163
+ agentExitCode = code;
1164
+ }
1165
+ else if (signal === 'SIGINT') {
1166
+ agentExitCode = 130;
1167
+ }
1168
+ else if (signal === 'SIGTERM') {
1169
+ agentExitCode = 143;
1170
+ }
1171
+ else {
1172
+ agentExitCode = 1;
1173
+ }
1174
+ // If cleanup was triggered by a signal, wait for it to finish
1175
+ if (cleanupInProgress) {
1176
+ cleanupInProgress.then(() => resolve());
1177
+ }
1178
+ else {
1179
+ resolve();
1180
+ }
1181
+ });
1182
+ // Finalization happens in outer finally.
1183
+ });
1184
+ await finalizeCleanup();
1185
+ log('Off the relay.');
1186
+ exit(agentExitCode);
1187
+ }
1188
+ finally {
1189
+ await finalizeCleanup();
1190
+ }
1178
1191
  }
1179
1192
  finally {
1180
- await finalizeCleanup();
1193
+ await localJwks.shutdown();
1181
1194
  }
1182
1195
  }
1183
1196
  //# sourceMappingURL=start.js.map