agent-relay 4.0.2 → 4.0.4

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 (175) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +7906 -2084
  6. package/dist/packages/sdk/src/provisioner/seeder.d.ts +17 -0
  7. package/dist/packages/sdk/src/provisioner/seeder.d.ts.map +1 -0
  8. package/dist/packages/sdk/src/provisioner/seeder.js +419 -0
  9. package/dist/packages/sdk/src/provisioner/seeder.js.map +1 -0
  10. package/dist/packages/sdk/src/provisioner/token.d.ts +38 -0
  11. package/dist/packages/sdk/src/provisioner/token.d.ts.map +1 -0
  12. package/dist/packages/sdk/src/provisioner/token.js +74 -0
  13. package/dist/packages/sdk/src/provisioner/token.js.map +1 -0
  14. package/dist/src/cli/commands/core.d.ts.map +1 -1
  15. package/dist/src/cli/commands/core.js +7 -3
  16. package/dist/src/cli/commands/core.js.map +1 -1
  17. package/dist/src/cli/commands/on/provision.d.ts.map +1 -1
  18. package/dist/src/cli/commands/on/provision.js +8 -3
  19. package/dist/src/cli/commands/on/provision.js.map +1 -1
  20. package/dist/src/cli/commands/on/start.d.ts +5 -0
  21. package/dist/src/cli/commands/on/start.d.ts.map +1 -1
  22. package/dist/src/cli/commands/on/start.js +126 -88
  23. package/dist/src/cli/commands/on/start.js.map +1 -1
  24. package/dist/src/cli/commands/on/symlink-mount.d.ts +12 -0
  25. package/dist/src/cli/commands/on/symlink-mount.d.ts.map +1 -0
  26. package/dist/src/cli/commands/on/symlink-mount.js +304 -0
  27. package/dist/src/cli/commands/on/symlink-mount.js.map +1 -0
  28. package/dist/src/cli/commands/on.d.ts.map +1 -1
  29. package/dist/src/cli/commands/on.js +3 -0
  30. package/dist/src/cli/commands/on.js.map +1 -1
  31. package/install.sh +4 -0
  32. package/package.json +9 -9
  33. package/packages/acp-bridge/package.json +2 -2
  34. package/packages/brand/package.json +1 -1
  35. package/packages/cloud/package.json +2 -2
  36. package/packages/config/package.json +1 -1
  37. package/packages/hooks/package.json +4 -4
  38. package/packages/memory/package.json +2 -2
  39. package/packages/openclaw/package.json +2 -2
  40. package/packages/policy/package.json +2 -2
  41. package/packages/sdk/dist/client.d.ts +3 -10
  42. package/packages/sdk/dist/client.d.ts.map +1 -1
  43. package/packages/sdk/dist/client.js +2 -0
  44. package/packages/sdk/dist/client.js.map +1 -1
  45. package/packages/sdk/dist/provisioner/__tests__/audit.test.d.ts +2 -0
  46. package/packages/sdk/dist/provisioner/__tests__/audit.test.d.ts.map +1 -0
  47. package/packages/sdk/dist/provisioner/__tests__/audit.test.js +45 -0
  48. package/packages/sdk/dist/provisioner/__tests__/audit.test.js.map +1 -0
  49. package/packages/sdk/dist/provisioner/__tests__/compiler.test.d.ts +2 -0
  50. package/packages/sdk/dist/provisioner/__tests__/compiler.test.d.ts.map +1 -0
  51. package/packages/sdk/dist/provisioner/__tests__/compiler.test.js +345 -0
  52. package/packages/sdk/dist/provisioner/__tests__/compiler.test.js.map +1 -0
  53. package/packages/sdk/dist/provisioner/__tests__/presets.test.d.ts +2 -0
  54. package/packages/sdk/dist/provisioner/__tests__/presets.test.d.ts.map +1 -0
  55. package/packages/sdk/dist/provisioner/__tests__/presets.test.js +23 -0
  56. package/packages/sdk/dist/provisioner/__tests__/presets.test.js.map +1 -0
  57. package/packages/sdk/dist/provisioner/__tests__/seeder.test.d.ts +2 -0
  58. package/packages/sdk/dist/provisioner/__tests__/seeder.test.d.ts.map +1 -0
  59. package/packages/sdk/dist/provisioner/__tests__/seeder.test.js +224 -0
  60. package/packages/sdk/dist/provisioner/__tests__/seeder.test.js.map +1 -0
  61. package/packages/sdk/dist/provisioner/__tests__/tar-seeder.test.d.ts +2 -0
  62. package/packages/sdk/dist/provisioner/__tests__/tar-seeder.test.d.ts.map +1 -0
  63. package/packages/sdk/dist/provisioner/__tests__/tar-seeder.test.js +191 -0
  64. package/packages/sdk/dist/provisioner/__tests__/tar-seeder.test.js.map +1 -0
  65. package/packages/sdk/dist/provisioner/__tests__/token-factory.test.d.ts +2 -0
  66. package/packages/sdk/dist/provisioner/__tests__/token-factory.test.d.ts.map +1 -0
  67. package/packages/sdk/dist/provisioner/__tests__/token-factory.test.js +127 -0
  68. package/packages/sdk/dist/provisioner/__tests__/token-factory.test.js.map +1 -0
  69. package/packages/sdk/dist/provisioner/__tests__/token.test.d.ts +2 -0
  70. package/packages/sdk/dist/provisioner/__tests__/token.test.d.ts.map +1 -0
  71. package/packages/sdk/dist/provisioner/__tests__/token.test.js +44 -0
  72. package/packages/sdk/dist/provisioner/__tests__/token.test.js.map +1 -0
  73. package/packages/sdk/dist/provisioner/audit.d.ts +19 -0
  74. package/packages/sdk/dist/provisioner/audit.d.ts.map +1 -0
  75. package/packages/sdk/dist/provisioner/audit.js +74 -0
  76. package/packages/sdk/dist/provisioner/audit.js.map +1 -0
  77. package/packages/sdk/dist/provisioner/compiler.d.ts +23 -0
  78. package/packages/sdk/dist/provisioner/compiler.d.ts.map +1 -0
  79. package/packages/sdk/dist/provisioner/compiler.js +355 -0
  80. package/packages/sdk/dist/provisioner/compiler.js.map +1 -0
  81. package/packages/sdk/dist/provisioner/index.d.ts +9 -0
  82. package/packages/sdk/dist/provisioner/index.d.ts.map +1 -0
  83. package/packages/sdk/dist/provisioner/index.js +266 -0
  84. package/packages/sdk/dist/provisioner/index.js.map +1 -0
  85. package/packages/sdk/dist/provisioner/mount.d.ts +14 -0
  86. package/packages/sdk/dist/provisioner/mount.d.ts.map +1 -0
  87. package/packages/sdk/dist/provisioner/mount.js +329 -0
  88. package/packages/sdk/dist/provisioner/mount.js.map +1 -0
  89. package/packages/sdk/dist/provisioner/seeder.d.ts +17 -0
  90. package/packages/sdk/dist/provisioner/seeder.d.ts.map +1 -0
  91. package/packages/sdk/dist/provisioner/seeder.js +419 -0
  92. package/packages/sdk/dist/provisioner/seeder.js.map +1 -0
  93. package/packages/sdk/dist/provisioner/token.d.ts +38 -0
  94. package/packages/sdk/dist/provisioner/token.d.ts.map +1 -0
  95. package/packages/sdk/dist/provisioner/token.js +74 -0
  96. package/packages/sdk/dist/provisioner/token.js.map +1 -0
  97. package/packages/sdk/dist/provisioner/types.d.ts +133 -0
  98. package/packages/sdk/dist/provisioner/types.d.ts.map +1 -0
  99. package/packages/sdk/dist/provisioner/types.js +2 -0
  100. package/packages/sdk/dist/provisioner/types.js.map +1 -0
  101. package/packages/sdk/dist/relay.d.ts +6 -0
  102. package/packages/sdk/dist/relay.d.ts.map +1 -1
  103. package/packages/sdk/dist/relay.js +17 -5
  104. package/packages/sdk/dist/relay.js.map +1 -1
  105. package/packages/sdk/dist/types.d.ts +9 -0
  106. package/packages/sdk/dist/types.d.ts.map +1 -1
  107. package/packages/sdk/dist/workflows/__tests__/e2e-permissions.test.d.ts +2 -0
  108. package/packages/sdk/dist/workflows/__tests__/e2e-permissions.test.d.ts.map +1 -0
  109. package/packages/sdk/dist/workflows/__tests__/e2e-permissions.test.js +331 -0
  110. package/packages/sdk/dist/workflows/__tests__/e2e-permissions.test.js.map +1 -0
  111. package/packages/sdk/dist/workflows/__tests__/permission-types.test.d.ts +2 -0
  112. package/packages/sdk/dist/workflows/__tests__/permission-types.test.d.ts.map +1 -0
  113. package/packages/sdk/dist/workflows/__tests__/permission-types.test.js +124 -0
  114. package/packages/sdk/dist/workflows/__tests__/permission-types.test.js.map +1 -0
  115. package/packages/sdk/dist/workflows/__tests__/permissions-integration.test.d.ts +2 -0
  116. package/packages/sdk/dist/workflows/__tests__/permissions-integration.test.d.ts.map +1 -0
  117. package/packages/sdk/dist/workflows/__tests__/permissions-integration.test.js +526 -0
  118. package/packages/sdk/dist/workflows/__tests__/permissions-integration.test.js.map +1 -0
  119. package/packages/sdk/dist/workflows/dry-run-format.d.ts.map +1 -1
  120. package/packages/sdk/dist/workflows/dry-run-format.js +8 -0
  121. package/packages/sdk/dist/workflows/dry-run-format.js.map +1 -1
  122. package/packages/sdk/dist/workflows/runner.d.ts +14 -0
  123. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  124. package/packages/sdk/dist/workflows/runner.js +455 -6
  125. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  126. package/packages/sdk/dist/workflows/types.d.ts +190 -0
  127. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  128. package/packages/sdk/dist/workflows/types.js +29 -0
  129. package/packages/sdk/dist/workflows/types.js.map +1 -1
  130. package/packages/sdk/package.json +6 -2
  131. package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +123 -1
  132. package/packages/sdk/src/__tests__/provisioner-mount.test.ts +126 -0
  133. package/packages/sdk/src/__tests__/spawn-token.test.ts +41 -0
  134. package/packages/sdk/src/__tests__/workflow-runner.test.ts +77 -45
  135. package/packages/sdk/src/client.ts +4 -8
  136. package/packages/sdk/src/provisioner/__tests__/audit.test.ts +62 -0
  137. package/packages/sdk/src/provisioner/__tests__/compiler.test.ts +369 -0
  138. package/packages/sdk/src/provisioner/__tests__/presets.test.ts +25 -0
  139. package/packages/sdk/src/provisioner/__tests__/seeder.test.ts +284 -0
  140. package/packages/sdk/src/provisioner/__tests__/tar-seeder.test.ts +249 -0
  141. package/packages/sdk/src/provisioner/__tests__/token-factory.test.ts +172 -0
  142. package/packages/sdk/src/provisioner/__tests__/token.test.ts +53 -0
  143. package/packages/sdk/src/provisioner/audit.ts +104 -0
  144. package/packages/sdk/src/provisioner/compiler.ts +498 -0
  145. package/packages/sdk/src/provisioner/index.ts +332 -0
  146. package/packages/sdk/src/provisioner/mount.ts +419 -0
  147. package/packages/sdk/src/provisioner/seeder.ts +571 -0
  148. package/packages/sdk/src/provisioner/token.ts +112 -0
  149. package/packages/sdk/src/provisioner/types.ts +188 -0
  150. package/packages/sdk/src/relay.ts +31 -9
  151. package/packages/sdk/src/types.ts +9 -0
  152. package/packages/sdk/src/workflows/__tests__/e2e-permissions.test.ts +407 -0
  153. package/packages/sdk/src/workflows/__tests__/fixtures/.agentignore +2 -0
  154. package/packages/sdk/src/workflows/__tests__/fixtures/.reader.agentreadonly +2 -0
  155. package/packages/sdk/src/workflows/__tests__/fixtures/permission-test.yaml +42 -0
  156. package/packages/sdk/src/workflows/__tests__/permission-types.test.ts +154 -0
  157. package/packages/sdk/src/workflows/__tests__/permissions-integration.test.ts +649 -0
  158. package/packages/sdk/src/workflows/builtin-templates/bug-fix.yaml +13 -9
  159. package/packages/sdk/src/workflows/builtin-templates/code-review.yaml +12 -8
  160. package/packages/sdk/src/workflows/builtin-templates/competitive.yaml +11 -7
  161. package/packages/sdk/src/workflows/builtin-templates/documentation.yaml +16 -8
  162. package/packages/sdk/src/workflows/builtin-templates/feature-dev.yaml +13 -9
  163. package/packages/sdk/src/workflows/builtin-templates/refactor.yaml +13 -9
  164. package/packages/sdk/src/workflows/builtin-templates/review-loop.yaml +14 -10
  165. package/packages/sdk/src/workflows/builtin-templates/security-audit.yaml +19 -9
  166. package/packages/sdk/src/workflows/dry-run-format.ts +14 -1
  167. package/packages/sdk/src/workflows/runner.ts +559 -6
  168. package/packages/sdk/src/workflows/schema.json +204 -114
  169. package/packages/sdk/src/workflows/types.ts +266 -1
  170. package/packages/sdk/vitest.config.ts +5 -1
  171. package/packages/sdk-py/pyproject.toml +1 -1
  172. package/packages/telemetry/package.json +1 -1
  173. package/packages/trajectory/package.json +2 -2
  174. package/packages/user-directory/package.json +2 -2
  175. package/packages/utils/package.json +2 -2
@@ -77,13 +77,7 @@ const mockHuman = {
77
77
  sendMessage: vi.fn().mockResolvedValue(undefined),
78
78
  };
79
79
 
80
- const defaultSpawnPtyImplementation = async ({
81
- name,
82
- task,
83
- }: {
84
- name: string;
85
- task?: string;
86
- }) => {
80
+ const defaultSpawnPtyImplementation = async ({ name, task }: { name: string; task?: string }) => {
87
81
  const queued = mockSpawnOutputs.shift();
88
82
  const stepComplete = task?.match(/STEP_COMPLETE:([^\n]+)/)?.[1]?.trim();
89
83
  const isReview = task?.includes('REVIEW_DECISION: APPROVE or REJECT');
@@ -578,10 +572,7 @@ agents:
578
572
  });
579
573
 
580
574
  it('should apply verification fallback for self-owned interactive steps', async () => {
581
- mockSpawnOutputs = [
582
- 'LEAD_DONE\n',
583
- 'REVIEW_DECISION: APPROVE\nREVIEW_REASON: verified\n',
584
- ];
575
+ mockSpawnOutputs = ['LEAD_DONE\n', 'REVIEW_DECISION: APPROVE\nREVIEW_REASON: verified\n'];
585
576
 
586
577
  const run = await runner.execute(
587
578
  makeConfig({
@@ -724,10 +715,7 @@ agents:
724
715
  });
725
716
 
726
717
  it('should pass canonical bypass args to interactive codex PTY spawns', async () => {
727
- mockSpawnOutputs = [
728
- 'LEAD_DONE\n',
729
- 'REVIEW_DECISION: APPROVE\nREVIEW_REASON: verified\n',
730
- ];
718
+ mockSpawnOutputs = ['LEAD_DONE\n', 'REVIEW_DECISION: APPROVE\nREVIEW_REASON: verified\n'];
731
719
 
732
720
  const run = await runner.execute(
733
721
  makeConfig({
@@ -877,44 +865,40 @@ agents:
877
865
  const workerRelease = vi.fn().mockResolvedValue(undefined);
878
866
  const ownerRelease = vi.fn().mockResolvedValue(undefined);
879
867
 
880
- mockRelayInstance.spawnPty.mockImplementation(async ({
881
- name,
882
- task,
883
- }: {
884
- name: string;
885
- task?: string;
886
- }) => {
887
- const isOwner = name.includes('-owner-');
888
- const output = isOwner ? 'owner checking\n' : 'worker finished\n';
889
-
890
- queueMicrotask(() => {
891
- if (typeof mockRelayInstance.onWorkerOutput === 'function') {
892
- mockRelayInstance.onWorkerOutput({ name, chunk: output });
868
+ mockRelayInstance.spawnPty.mockImplementation(
869
+ async ({ name, task }: { name: string; task?: string }) => {
870
+ const isOwner = name.includes('-owner-');
871
+ const output = isOwner ? 'owner checking\n' : 'worker finished\n';
872
+
873
+ queueMicrotask(() => {
874
+ if (typeof mockRelayInstance.onWorkerOutput === 'function') {
875
+ mockRelayInstance.onWorkerOutput({ name, chunk: output });
876
+ }
877
+ });
878
+
879
+ if (isOwner) {
880
+ return {
881
+ name,
882
+ waitForExit: vi.fn().mockImplementation(async () => {
883
+ await Promise.resolve();
884
+ return 'timeout';
885
+ }),
886
+ waitForIdle: vi.fn().mockResolvedValue('timeout'),
887
+ release: ownerRelease,
888
+ };
893
889
  }
894
- });
895
890
 
896
- if (isOwner) {
897
891
  return {
898
892
  name,
899
893
  waitForExit: vi.fn().mockImplementation(async () => {
900
- await Promise.resolve();
901
- return 'timeout';
894
+ await workerRelease();
895
+ return 'released';
902
896
  }),
903
- waitForIdle: vi.fn().mockResolvedValue('timeout'),
904
- release: ownerRelease,
897
+ waitForIdle: vi.fn().mockImplementation(() => never()),
898
+ release: workerRelease,
905
899
  };
906
900
  }
907
-
908
- return {
909
- name,
910
- waitForExit: vi.fn().mockImplementation(async () => {
911
- await workerRelease();
912
- return 'released';
913
- }),
914
- waitForIdle: vi.fn().mockImplementation(() => never()),
915
- release: workerRelease,
916
- };
917
- });
901
+ );
918
902
 
919
903
  const run = await runner.execute(makeSupervisedConfig(), 'default');
920
904
 
@@ -1150,6 +1134,54 @@ agents:
1150
1134
  expect(agentB?.stepCount).toBe(1);
1151
1135
  });
1152
1136
 
1137
+ it('should include resolved permissions without provisioning tokens', () => {
1138
+ const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'workflow-dry-run-perms-'));
1139
+ try {
1140
+ writeFileSync(path.join(tmpDir, 'readme.md'), '# readme\n');
1141
+ writeFileSync(path.join(tmpDir, 'notes.txt'), 'notes\n');
1142
+ writeFileSync(path.join(tmpDir, '.agentreadonly'), 'readme.md\n');
1143
+
1144
+ const dryRunRunner = new WorkflowRunner({ db, workspaceId: 'ws-test', cwd: tmpDir });
1145
+ const report = dryRunRunner.dryRun(
1146
+ makeConfig({
1147
+ agents: [
1148
+ {
1149
+ name: 'agent-a',
1150
+ cli: 'claude',
1151
+ permissions: {
1152
+ access: 'readonly',
1153
+ files: {
1154
+ write: ['notes.txt'],
1155
+ },
1156
+ scopes: ['relay:custom'],
1157
+ },
1158
+ },
1159
+ { name: 'agent-b', cli: 'claude' },
1160
+ ],
1161
+ })
1162
+ );
1163
+
1164
+ const agentA = report.permissions?.find((entry) => entry.agent === 'agent-a');
1165
+ const agentB = report.permissions?.find((entry) => entry.agent === 'agent-b');
1166
+
1167
+ expect(agentA?.agent).toBe('agent-a');
1168
+ expect(agentA?.access).toBe('readonly');
1169
+ expect(agentA?.writePaths).toBe(1);
1170
+ expect(agentA?.denyPaths).toBe(0);
1171
+ expect(agentA?.readPaths).toBeGreaterThanOrEqual(1);
1172
+ expect(agentA?.source).toBe('yaml');
1173
+ expect(agentA?.scopes).toBeGreaterThan(1);
1174
+
1175
+ expect(agentB).toMatchObject({
1176
+ agent: 'agent-b',
1177
+ access: 'readwrite',
1178
+ source: 'preset',
1179
+ });
1180
+ } finally {
1181
+ rmSync(tmpDir, { recursive: true, force: true });
1182
+ }
1183
+ });
1184
+
1153
1185
  it('should warn when step references unknown agent', () => {
1154
1186
  const config = makeConfig({
1155
1187
  workflows: [
@@ -26,6 +26,7 @@ import type {
26
26
  } from './protocol.js';
27
27
  import type {
28
28
  AgentTransport,
29
+ SpawnHeadlessInput,
29
30
  SpawnPtyInput,
30
31
  SpawnProviderInput,
31
32
  SendMessageInput,
@@ -313,6 +314,7 @@ export class AgentRelayClient {
313
314
  channels: input.channels ?? [],
314
315
  cwd: input.cwd,
315
316
  team: input.team,
317
+ agentToken: input.agentToken,
316
318
  shadowOf: input.shadowOf,
317
319
  shadowMode: input.shadowMode,
318
320
  continueFrom: input.continueFrom,
@@ -342,6 +344,7 @@ export class AgentRelayClient {
342
344
  channels: input.channels ?? [],
343
345
  cwd: input.cwd,
344
346
  team: input.team,
347
+ agentToken: input.agentToken,
345
348
  shadowOf: input.shadowOf,
346
349
  shadowMode: input.shadowMode,
347
350
  continueFrom: input.continueFrom,
@@ -353,14 +356,7 @@ export class AgentRelayClient {
353
356
  });
354
357
  }
355
358
 
356
- async spawnHeadless(input: {
357
- name: string;
358
- provider: HeadlessProvider;
359
- args?: string[];
360
- channels?: string[];
361
- task?: string;
362
- skipRelayPrompt?: boolean;
363
- }): Promise<{ name: string; runtime: AgentRuntime }> {
359
+ async spawnHeadless(input: SpawnHeadlessInput): Promise<{ name: string; runtime: AgentRuntime }> {
364
360
  return this.spawnProvider({ ...input, transport: 'headless' });
365
361
  }
366
362
 
@@ -0,0 +1,62 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { provisionWorkflowAgents } from '../index.js';
8
+
9
+ async function createWorkspace(): Promise<{ dir: string; cleanup: () => Promise<void> }> {
10
+ const dir = await mkdtemp(path.join(tmpdir(), 'relay-provisioner-audit-'));
11
+ await mkdir(path.join(dir, 'src'), { recursive: true });
12
+ await writeFile(path.join(dir, 'src', 'index.ts'), 'export const value = 1;\n');
13
+
14
+ return {
15
+ dir,
16
+ cleanup: () => rm(dir, { recursive: true, force: true }),
17
+ };
18
+ }
19
+
20
+ test('provisionWorkflowAgents writes a permission audit without token values', async () => {
21
+ const workspace = await createWorkspace();
22
+
23
+ try {
24
+ const result = await provisionWorkflowAgents({
25
+ secret: 'test-secret',
26
+ workspace: 'audit-workspace',
27
+ projectDir: workspace.dir,
28
+ relayfileBaseUrl: 'http://127.0.0.1:8080',
29
+ agents: {
30
+ worker: {
31
+ access: 'readonly',
32
+ },
33
+ },
34
+ skipSeeding: true,
35
+ skipMount: true,
36
+ });
37
+
38
+ const auditPath = path.join(workspace.dir, '.agent-relay', 'permission-audit.json');
39
+ const auditRaw = await readFile(auditPath, 'utf8');
40
+ const auditJson = JSON.parse(auditRaw) as {
41
+ entries: Array<{
42
+ agentName: string;
43
+ action: string;
44
+ details: Record<string, unknown>;
45
+ }>;
46
+ };
47
+
48
+ assert.ok(auditJson.entries.length >= 3);
49
+ assert.deepEqual(
50
+ auditJson.entries.map((entry) => `${entry.agentName}:${entry.action}`),
51
+ ['worker:resolve', 'worker:mint', 'relay-admin:mint']
52
+ );
53
+ assert.equal(
54
+ auditJson.entries[1]?.details.jwtPath,
55
+ path.join(workspace.dir, '.relay', 'tokens', 'worker.jwt')
56
+ );
57
+ assert.ok(!auditRaw.includes(result.agents.worker.token));
58
+ assert.ok(!auditRaw.includes(result.adminToken));
59
+ } finally {
60
+ await workspace.cleanup();
61
+ }
62
+ });
@@ -0,0 +1,369 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { compileAgentScopes, globToScopes } from '../compiler.js';
8
+
9
+ async function createWorkspace(
10
+ files: Record<string, string>
11
+ ): Promise<{ dir: string; cleanup: () => Promise<void> }> {
12
+ const dir = await mkdtemp(path.join(tmpdir(), 'relay-provisioner-compiler-'));
13
+
14
+ for (const [relativePath, content] of Object.entries(files)) {
15
+ const filePath = path.join(dir, relativePath);
16
+ await mkdir(path.dirname(filePath), { recursive: true });
17
+ await writeFile(filePath, content);
18
+ }
19
+
20
+ return {
21
+ dir,
22
+ cleanup: () => rm(dir, { recursive: true, force: true }),
23
+ };
24
+ }
25
+
26
+ test('compileAgentScopes applies explicit file permissions', async () => {
27
+ const workspace = await createWorkspace({
28
+ 'docs/guide.md': '# guide\n',
29
+ 'src/index.ts': 'export const value = 1;\n',
30
+ 'secrets.env': 'TOP_SECRET=1\n',
31
+ });
32
+
33
+ try {
34
+ const compiled = compileAgentScopes({
35
+ agentName: 'builder',
36
+ workspace: 'relay-test',
37
+ projectDir: workspace.dir,
38
+ permissions: {
39
+ access: 'restricted',
40
+ inherit: false,
41
+ files: {
42
+ read: ['docs/**'],
43
+ write: ['src/**'],
44
+ deny: ['secrets.env'],
45
+ },
46
+ },
47
+ });
48
+
49
+ assert.deepEqual(compiled.readonlyPaths, ['docs/guide.md']);
50
+ assert.deepEqual(compiled.readwritePaths, ['src/index.ts']);
51
+ assert.deepEqual(compiled.deniedPaths, ['secrets.env']);
52
+ assert.deepEqual(compiled.scopes, [
53
+ 'relayfile:fs:read:/docs/guide.md',
54
+ 'relayfile:fs:read:/src/index.ts',
55
+ 'relayfile:fs:write:/src/index.ts',
56
+ ]);
57
+ assert.deepEqual(compiled.sources, [
58
+ {
59
+ type: 'yaml',
60
+ label: 'permissions.files',
61
+ ruleCount: 3,
62
+ },
63
+ ]);
64
+ } finally {
65
+ await workspace.cleanup();
66
+ }
67
+ });
68
+
69
+ test('compileAgentScopes honors the readonly preset', async () => {
70
+ const workspace = await createWorkspace({
71
+ 'docs/guide.md': '# guide\n',
72
+ 'src/index.ts': 'export const value = 1;\n',
73
+ });
74
+
75
+ try {
76
+ const compiled = compileAgentScopes({
77
+ agentName: 'reader',
78
+ workspace: 'relay-test',
79
+ projectDir: workspace.dir,
80
+ permissions: {
81
+ access: 'readonly',
82
+ },
83
+ });
84
+
85
+ assert.equal(compiled.effectiveAccess, 'readonly');
86
+ assert.deepEqual(compiled.readonlyPaths, ['docs/guide.md', 'src/index.ts']);
87
+ assert.deepEqual(compiled.readwritePaths, []);
88
+ assert.deepEqual(compiled.deniedPaths, []);
89
+ assert.deepEqual(compiled.readonlyPatterns, ['**']);
90
+ assert.deepEqual(compiled.readwritePatterns, []);
91
+ assert.deepEqual(compiled.scopes, [
92
+ 'relayfile:fs:read:/docs/guide.md',
93
+ 'relayfile:fs:read:/src/index.ts',
94
+ ]);
95
+ } finally {
96
+ await workspace.cleanup();
97
+ }
98
+ });
99
+
100
+ test('compileAgentScopes honors the readwrite preset', async () => {
101
+ const workspace = await createWorkspace({
102
+ 'docs/guide.md': '# guide\n',
103
+ 'src/index.ts': 'export const value = 1;\n',
104
+ });
105
+
106
+ try {
107
+ const compiled = compileAgentScopes({
108
+ agentName: 'writer',
109
+ workspace: 'relay-test',
110
+ projectDir: workspace.dir,
111
+ permissions: {
112
+ access: 'readwrite',
113
+ },
114
+ });
115
+
116
+ assert.equal(compiled.effectiveAccess, 'readwrite');
117
+ assert.deepEqual(compiled.readonlyPaths, []);
118
+ assert.deepEqual(compiled.readwritePaths, ['docs/guide.md', 'src/index.ts']);
119
+ assert.deepEqual(compiled.deniedPaths, []);
120
+ assert.deepEqual(compiled.readwritePatterns, ['**']);
121
+ assert.deepEqual(compiled.scopes, [
122
+ 'relayfile:fs:read:/docs/guide.md',
123
+ 'relayfile:fs:read:/src/index.ts',
124
+ 'relayfile:fs:write:/docs/guide.md',
125
+ 'relayfile:fs:write:/src/index.ts',
126
+ ]);
127
+ } finally {
128
+ await workspace.cleanup();
129
+ }
130
+ });
131
+
132
+ test('compileAgentScopes applies deny patterns last', async () => {
133
+ const workspace = await createWorkspace({
134
+ 'docs/private.md': '# private\n',
135
+ 'docs/public.md': '# public\n',
136
+ 'secrets.env': 'TOP_SECRET=1\n',
137
+ });
138
+
139
+ try {
140
+ const compiled = compileAgentScopes({
141
+ agentName: 'writer',
142
+ workspace: 'relay-test',
143
+ projectDir: workspace.dir,
144
+ permissions: {
145
+ access: 'full',
146
+ files: {
147
+ deny: ['docs/private.md', 'secrets.env'],
148
+ },
149
+ },
150
+ });
151
+
152
+ assert.equal(compiled.inherited, false);
153
+ assert.deepEqual(compiled.readonlyPaths, []);
154
+ assert.deepEqual(compiled.readwritePaths, ['docs/public.md']);
155
+ assert.deepEqual(compiled.deniedPaths, ['docs/private.md', 'secrets.env']);
156
+ assert.deepEqual(compiled.deniedPatterns, ['docs/private.md', 'secrets.env']);
157
+ } finally {
158
+ await workspace.cleanup();
159
+ }
160
+ });
161
+
162
+ test('compileAgentScopes ignores dotfiles when inherit is false', async () => {
163
+ const workspace = await createWorkspace({
164
+ '.agentignore': 'blocked.txt\n',
165
+ '.agentreadonly': 'locked.txt\n',
166
+ 'blocked.txt': 'blocked\n',
167
+ 'locked.txt': 'locked\n',
168
+ 'open.txt': 'open\n',
169
+ });
170
+
171
+ try {
172
+ const compiled = compileAgentScopes({
173
+ agentName: 'writer',
174
+ workspace: 'relay-test',
175
+ projectDir: workspace.dir,
176
+ permissions: {
177
+ access: 'readwrite',
178
+ inherit: false,
179
+ },
180
+ });
181
+
182
+ assert.equal(compiled.inherited, false);
183
+ assert.deepEqual(compiled.sources, [
184
+ {
185
+ type: 'preset',
186
+ label: 'access: readwrite',
187
+ ruleCount: 2,
188
+ },
189
+ ]);
190
+ assert.deepEqual(compiled.readonlyPaths, []);
191
+ assert.deepEqual(compiled.readwritePaths, [
192
+ '.agentignore',
193
+ '.agentreadonly',
194
+ 'blocked.txt',
195
+ 'locked.txt',
196
+ 'open.txt',
197
+ ]);
198
+ assert.deepEqual(compiled.deniedPaths, []);
199
+ } finally {
200
+ await workspace.cleanup();
201
+ }
202
+ });
203
+
204
+ test('compileAgentScopes loads dotfiles when inherit is true', async () => {
205
+ const workspace = await createWorkspace({
206
+ '.agentignore': 'blocked.txt\n',
207
+ '.agentreadonly': 'locked.txt\n',
208
+ 'blocked.txt': 'blocked\n',
209
+ 'locked.txt': 'locked\n',
210
+ 'open.txt': 'open\n',
211
+ });
212
+
213
+ try {
214
+ const compiled = compileAgentScopes({
215
+ agentName: 'writer',
216
+ workspace: 'relay-test',
217
+ projectDir: workspace.dir,
218
+ permissions: {
219
+ access: 'readwrite',
220
+ },
221
+ });
222
+
223
+ assert.equal(compiled.inherited, true);
224
+ assert.deepEqual(compiled.sources, [
225
+ {
226
+ type: 'dotfile',
227
+ label: 'dotfiles',
228
+ ruleCount: 2,
229
+ },
230
+ {
231
+ type: 'preset',
232
+ label: 'access: readwrite',
233
+ ruleCount: 2,
234
+ },
235
+ ]);
236
+ assert.deepEqual(compiled.readonlyPaths, ['locked.txt']);
237
+ assert.deepEqual(compiled.readwritePaths, ['.agentignore', '.agentreadonly', 'open.txt']);
238
+ assert.deepEqual(compiled.deniedPaths, ['blocked.txt']);
239
+ assert.deepEqual(compiled.readonlyPatterns, ['locked.txt']);
240
+ assert.deepEqual(compiled.deniedPatterns, ['blocked.txt']);
241
+ } finally {
242
+ await workspace.cleanup();
243
+ }
244
+ });
245
+
246
+ test('compileAgentScopes appends raw scopes', async () => {
247
+ const workspace = await createWorkspace({
248
+ 'src/index.ts': 'export const value = 1;\n',
249
+ });
250
+
251
+ try {
252
+ const compiled = compileAgentScopes({
253
+ agentName: 'scoped',
254
+ workspace: 'relay-test',
255
+ projectDir: workspace.dir,
256
+ permissions: {
257
+ access: 'restricted',
258
+ inherit: false,
259
+ scopes: ['relay:custom:deploy', 'relay:custom:audit', 'relay:custom:deploy'],
260
+ },
261
+ });
262
+
263
+ assert.deepEqual(compiled.readonlyPaths, []);
264
+ assert.deepEqual(compiled.readwritePaths, []);
265
+ assert.deepEqual(compiled.deniedPaths, ['src/index.ts']);
266
+ assert.deepEqual(compiled.scopes, ['relay:custom:deploy', 'relay:custom:audit']);
267
+ assert.deepEqual(compiled.summary, {
268
+ readonly: 0,
269
+ readwrite: 0,
270
+ denied: 1,
271
+ customScopes: 2,
272
+ });
273
+ assert.deepEqual(compiled.sources, [
274
+ {
275
+ type: 'scope',
276
+ label: 'permissions.scopes',
277
+ ruleCount: 2,
278
+ },
279
+ ]);
280
+ } finally {
281
+ await workspace.cleanup();
282
+ }
283
+ });
284
+
285
+ test('compileAgentScopes lets YAML rules override dotfiles', async () => {
286
+ const workspace = await createWorkspace({
287
+ '.agentignore': 'blocked.txt\n',
288
+ '.agentreadonly': 'locked.txt\n',
289
+ 'blocked.txt': 'blocked\n',
290
+ 'locked.txt': 'locked\n',
291
+ 'plain.txt': 'plain\n',
292
+ });
293
+
294
+ try {
295
+ const compiled = compileAgentScopes({
296
+ agentName: 'override-agent',
297
+ workspace: 'relay-test',
298
+ projectDir: workspace.dir,
299
+ permissions: {
300
+ access: 'restricted',
301
+ files: {
302
+ read: ['blocked.txt'],
303
+ write: ['locked.txt'],
304
+ },
305
+ },
306
+ });
307
+
308
+ assert.deepEqual(compiled.readonlyPaths, ['blocked.txt']);
309
+ assert.deepEqual(compiled.readwritePaths, ['locked.txt']);
310
+ assert.deepEqual(compiled.deniedPaths, ['.agentignore', '.agentreadonly', 'plain.txt']);
311
+ assert.deepEqual(compiled.scopes, [
312
+ 'relayfile:fs:read:/blocked.txt',
313
+ 'relayfile:fs:read:/locked.txt',
314
+ 'relayfile:fs:write:/locked.txt',
315
+ ]);
316
+ assert.deepEqual(compiled.sources, [
317
+ {
318
+ type: 'dotfile',
319
+ label: 'dotfiles',
320
+ ruleCount: 2,
321
+ },
322
+ {
323
+ type: 'yaml',
324
+ label: 'permissions.files',
325
+ ruleCount: 2,
326
+ },
327
+ ]);
328
+ } finally {
329
+ await workspace.cleanup();
330
+ }
331
+ });
332
+
333
+ test('compileAgentScopes defaults empty permissions to inherited readwrite access', async () => {
334
+ const workspace = await createWorkspace({
335
+ 'docs/guide.md': '# guide\n',
336
+ 'src/index.ts': 'export const value = 1;\n',
337
+ });
338
+
339
+ try {
340
+ const compiled = compileAgentScopes({
341
+ agentName: 'defaulted',
342
+ workspace: 'relay-test',
343
+ projectDir: workspace.dir,
344
+ permissions: {},
345
+ });
346
+
347
+ assert.equal(compiled.effectiveAccess, 'readwrite');
348
+ assert.equal(compiled.inherited, true);
349
+ assert.deepEqual(compiled.readonlyPaths, []);
350
+ assert.deepEqual(compiled.readwritePaths, ['docs/guide.md', 'src/index.ts']);
351
+ assert.deepEqual(compiled.deniedPaths, []);
352
+ assert.deepEqual(compiled.sources, [
353
+ {
354
+ type: 'preset',
355
+ label: 'access: readwrite',
356
+ ruleCount: 2,
357
+ },
358
+ ]);
359
+ } finally {
360
+ await workspace.cleanup();
361
+ }
362
+ });
363
+
364
+ test('globToScopes normalizes and de-duplicates globs', () => {
365
+ assert.deepEqual(globToScopes(['src\\index.ts', './docs/**', '/docs/**', '', ' src/index.ts '], 'write'), [
366
+ 'relayfile:fs:write:/src/index.ts',
367
+ 'relayfile:fs:write:/docs/**',
368
+ ]);
369
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { defaultPermissionsForPreset, expandAccessPreset } from '../compiler.js';
4
+
5
+ describe('expandAccessPreset', () => {
6
+ it.each([
7
+ ['readonly', { read: ['**'], write: [], deny: [] }],
8
+ ['restricted', { read: [], write: [], deny: [] }],
9
+ ['full', { read: ['**'], write: ['**'], deny: [] }],
10
+ ['readwrite', { read: ['**'], write: ['**'], deny: [] }],
11
+ ] as const)('expands %s', (preset, expected) => {
12
+ expect(expandAccessPreset(preset)).toEqual(expected);
13
+ });
14
+ });
15
+
16
+ describe('defaultPermissionsForPreset', () => {
17
+ it.each([
18
+ ['lead', { access: 'full' }],
19
+ ['worker', { access: 'readwrite' }],
20
+ ['reviewer', { access: 'readonly' }],
21
+ ['analyst', { access: 'readonly' }],
22
+ ] as const)('maps %s to the expected default permissions', (preset, expected) => {
23
+ expect(defaultPermissionsForPreset(preset)).toEqual(expected);
24
+ });
25
+ });