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
@@ -0,0 +1,571 @@
1
+ import { RelayFileClient } from '@relayfile/sdk';
2
+ import { execSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import * as tar from 'tar';
6
+
7
+ interface BulkWriteResponseShape {
8
+ written?: number;
9
+ errorCount?: number;
10
+ errors?: unknown;
11
+ }
12
+
13
+ interface SeedFile {
14
+ path: string;
15
+ content: string;
16
+ encoding?: 'utf-8' | 'base64';
17
+ }
18
+
19
+ interface SeedFileResult {
20
+ written: number;
21
+ errorCount: number;
22
+ errors: unknown;
23
+ }
24
+
25
+ const DEFAULT_EXCLUDED_DIRS = ['.relay', '.git', 'node_modules'];
26
+ const DEFAULT_EXCLUDED_FILES = new Set(['.relayfile-mount-state.json']);
27
+ const BATCH_SIZE = 50;
28
+ const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
29
+
30
+ interface WorkflowAclAgent {
31
+ name: string;
32
+ acl: Record<string, string[]>;
33
+ }
34
+
35
+ interface SeedWorkflowAclsOptions {
36
+ relayfileUrl: string;
37
+ adminToken: string;
38
+ workspace: string;
39
+ agents: WorkflowAclAgent[];
40
+ }
41
+
42
+ function normalizeBaseUrl(baseUrl: string): string {
43
+ const url = String(baseUrl ?? '').trim();
44
+ let end = url.length;
45
+ while (end > 0 && url.charCodeAt(end - 1) === 0x2f) {
46
+ end--;
47
+ }
48
+ return end === url.length ? url : url.slice(0, end);
49
+ }
50
+
51
+ function normalizeWorkspaceId(workspaceId: string): string {
52
+ const value = String(workspaceId ?? '').trim();
53
+ if (!value) {
54
+ throw new Error('workspaceId is required');
55
+ }
56
+ return value;
57
+ }
58
+
59
+ function normalizeExcludeDirs(excludeDirs: string[]): Set<string> {
60
+ const result = new Set<string>();
61
+ for (const dir of excludeDirs) {
62
+ const normalized = String(dir ?? '')
63
+ .trim()
64
+ .replace(/^[/\\]+|[/\\]+$/g, '');
65
+ if (!normalized) {
66
+ continue;
67
+ }
68
+ result.add(normalized);
69
+ }
70
+ return result;
71
+ }
72
+
73
+ function normalizeAclDirectory(dirPath: string): string {
74
+ const normalized = String(dirPath ?? '')
75
+ .trim()
76
+ .replace(/\\/gu, '/')
77
+ .replace(/\/+$/u, '');
78
+
79
+ if (!normalized || normalized === '/') {
80
+ return '/';
81
+ }
82
+
83
+ return normalized.startsWith('/') ? normalized : `/${normalized}`;
84
+ }
85
+
86
+ function isReviewerAgent(agentName: string): boolean {
87
+ return /reviewer/iu.test(String(agentName ?? '').trim());
88
+ }
89
+
90
+ function createClient(baseUrl: string, token: string): RelayFileClient {
91
+ return new RelayFileClient({
92
+ baseUrl: normalizeBaseUrl(baseUrl),
93
+ token,
94
+ retry: { maxRetries: 0 },
95
+ });
96
+ }
97
+
98
+ function isUtf8(raw: Buffer): boolean {
99
+ try {
100
+ utf8Decoder.decode(raw);
101
+ return true;
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+
107
+ function buildSeedFilePayload(filePath: string, rootDir: string): SeedFile {
108
+ const relative = path.relative(rootDir, filePath).split(path.sep).join('/');
109
+ const raw = fs.readFileSync(filePath);
110
+ if (isUtf8(raw)) {
111
+ return { path: `/${relative}`, content: raw.toString('utf8'), encoding: 'utf-8' };
112
+ }
113
+ return { path: `/${relative}`, content: raw.toString('base64'), encoding: 'base64' };
114
+ }
115
+
116
+ function collectSeedPaths(
117
+ rootDir: string,
118
+ currentRelative: string,
119
+ excludeDirs: Set<string>,
120
+ output: string[]
121
+ ): void {
122
+ const absoluteDir = path.join(rootDir, currentRelative);
123
+ const entries = fs.readdirSync(absoluteDir, { withFileTypes: true });
124
+
125
+ for (const entry of entries) {
126
+ if (excludeDirs.has(entry.name)) {
127
+ continue;
128
+ }
129
+ if (DEFAULT_EXCLUDED_FILES.has(entry.name)) {
130
+ continue;
131
+ }
132
+
133
+ const nextRelative = currentRelative ? `${currentRelative}/${entry.name}` : entry.name;
134
+ const absolutePath = path.join(rootDir, nextRelative);
135
+
136
+ if (excludeDirs.has(nextRelative)) {
137
+ continue;
138
+ }
139
+
140
+ if (entry.isDirectory()) {
141
+ collectSeedPaths(rootDir, nextRelative, excludeDirs, output);
142
+ continue;
143
+ }
144
+
145
+ if (entry.isFile()) {
146
+ output.push(absolutePath);
147
+ continue;
148
+ }
149
+
150
+ if (entry.isSymbolicLink()) {
151
+ try {
152
+ const resolved = fs.realpathSync(absolutePath);
153
+ if (!resolved.startsWith(rootDir + path.sep) && resolved !== rootDir) {
154
+ continue;
155
+ }
156
+ const stat = fs.statSync(resolved);
157
+ if (stat.isDirectory()) {
158
+ collectSeedPaths(rootDir, nextRelative, excludeDirs, output);
159
+ continue;
160
+ }
161
+ if (stat.isFile()) {
162
+ output.push(absolutePath);
163
+ }
164
+ } catch {
165
+ // Ignore symlinks that cannot be resolved.
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ function parseBulkWriteResponse(payload: unknown): SeedFileResult {
172
+ if (!payload || typeof payload !== 'object') {
173
+ return { written: 0, errorCount: 0, errors: [] };
174
+ }
175
+ const parsed = payload as BulkWriteResponseShape;
176
+ return {
177
+ written: typeof parsed.written === 'number' ? parsed.written : 0,
178
+ errorCount: typeof parsed.errorCount === 'number' ? parsed.errorCount : 0,
179
+ errors: parsed.errors ?? [],
180
+ };
181
+ }
182
+
183
+ async function postBulkWrite(
184
+ baseUrl: string,
185
+ token: string,
186
+ workspaceId: string,
187
+ files: SeedFile[],
188
+ correlationId: string
189
+ ): Promise<SeedFileResult> {
190
+ const response = await fetch(
191
+ `${normalizeBaseUrl(baseUrl)}/v1/workspaces/${encodeURIComponent(workspaceId)}/fs/bulk`,
192
+ {
193
+ method: 'POST',
194
+ headers: {
195
+ Authorization: `Bearer ${token}`,
196
+ 'Content-Type': 'application/json',
197
+ 'X-Correlation-Id': correlationId,
198
+ },
199
+ body: JSON.stringify({ files }),
200
+ }
201
+ );
202
+
203
+ const body = await response.text();
204
+ if (!response.ok) {
205
+ throw new Error(`failed to seed workspace ${workspaceId}: HTTP ${response.status} ${body}`.trim());
206
+ }
207
+
208
+ if (!body) {
209
+ return { written: files.length, errorCount: 0, errors: [] };
210
+ }
211
+ try {
212
+ return parseBulkWriteResponse(JSON.parse(body));
213
+ } catch {
214
+ return { written: files.length, errorCount: 0, errors: [] };
215
+ }
216
+ }
217
+
218
+ async function writeBulkWrite(
219
+ baseUrl: string,
220
+ token: string,
221
+ workspaceId: string,
222
+ files: SeedFile[],
223
+ correlationId: string
224
+ ): Promise<SeedFileResult> {
225
+ const client = createClient(baseUrl, token);
226
+ try {
227
+ const response = await client.bulkWrite({
228
+ workspaceId,
229
+ files,
230
+ correlationId,
231
+ });
232
+ return parseBulkWriteResponse(response);
233
+ } catch (error) {
234
+ if (typeof (error as { status?: number }).status === 'number') {
235
+ throw error;
236
+ }
237
+ }
238
+
239
+ return postBulkWrite(baseUrl, token, workspaceId, files, correlationId);
240
+ }
241
+
242
+ export async function createWorkspaceIfNeeded(
243
+ baseUrl: string,
244
+ token: string,
245
+ workspaceId: string
246
+ ): Promise<void> {
247
+ const workspace = normalizeWorkspaceId(workspaceId);
248
+ const client = createClient(baseUrl, token);
249
+
250
+ const maybeCreateWorkspace = client as unknown as {
251
+ createWorkspace?: (...input: unknown[]) => Promise<unknown>;
252
+ };
253
+ if (typeof maybeCreateWorkspace.createWorkspace === 'function') {
254
+ for (const arg of [workspace, { id: workspace }, { workspaceId: workspace }, { name: workspace }]) {
255
+ try {
256
+ await maybeCreateWorkspace.createWorkspace(arg);
257
+ return;
258
+ } catch {
259
+ // Continue to the next overload candidate, then fallback to HTTP.
260
+ }
261
+ }
262
+ }
263
+
264
+ const endpoint = `${normalizeBaseUrl(baseUrl)}/v1/workspaces`;
265
+ const bodyCandidates: Array<Record<string, string>> = [
266
+ { name: workspace },
267
+ { workspace: workspace },
268
+ { workspaceId: workspace },
269
+ { id: workspace },
270
+ ];
271
+ let lastFailure: string | null = null;
272
+
273
+ for (const body of bodyCandidates) {
274
+ try {
275
+ const response = await fetch(endpoint, {
276
+ method: 'POST',
277
+ headers: {
278
+ Authorization: `Bearer ${token}`,
279
+ 'Content-Type': 'application/json',
280
+ 'X-Correlation-Id': `create-workspace-${Date.now()}`,
281
+ },
282
+ body: JSON.stringify(body),
283
+ });
284
+
285
+ if (
286
+ response.status === 200 ||
287
+ response.status === 201 ||
288
+ response.status === 204 ||
289
+ response.status === 409
290
+ ) {
291
+ return;
292
+ }
293
+
294
+ const responseBody = await response.text().catch(() => '');
295
+ lastFailure = `HTTP ${response.status} ${responseBody}`.trim();
296
+ if (response.status < 500 && response.status !== 409) {
297
+ continue;
298
+ }
299
+ } catch (error) {
300
+ lastFailure = String(error);
301
+ }
302
+ }
303
+
304
+ if (lastFailure) {
305
+ throw new Error(`Failed to create workspace ${workspace}: ${lastFailure}`);
306
+ }
307
+ }
308
+
309
+ export async function seedAclRules(
310
+ baseUrl: string,
311
+ token: string,
312
+ workspaceId: string,
313
+ aclRules: Record<string, string[]>
314
+ ): Promise<void> {
315
+ const workspace = normalizeWorkspaceId(workspaceId);
316
+ const files = Object.entries(aclRules).map(([dirPath, rules]) => {
317
+ const normalizedDir = String(dirPath ?? '')
318
+ .trim()
319
+ .replace(/\/+$/, '');
320
+ const aclPath =
321
+ normalizedDir === '' || normalizedDir === '/' ? '/.relayfile.acl' : `${normalizedDir}/.relayfile.acl`;
322
+ return {
323
+ path: aclPath,
324
+ content: JSON.stringify({ semantics: { permissions: rules } }),
325
+ encoding: 'utf-8' as const,
326
+ };
327
+ });
328
+
329
+ if (files.length === 0) {
330
+ return;
331
+ }
332
+
333
+ const result = await writeBulkWrite(
334
+ baseUrl,
335
+ token,
336
+ workspace,
337
+ files,
338
+ `seed-acl-${workspace}-${Date.now()}`
339
+ );
340
+ if (result.errorCount > 0) {
341
+ const details = result.errors ? JSON.stringify(result.errors) : '[]';
342
+ throw new Error(`ACL seeding had ${result.errorCount} error(s) for workspace ${workspace}: ${details}`);
343
+ }
344
+ }
345
+
346
+ export async function seedWorkspace(
347
+ baseUrl: string,
348
+ token: string,
349
+ workspaceId: string,
350
+ projectDir: string,
351
+ excludeDirs: string[]
352
+ ): Promise<number> {
353
+ const workspace = normalizeWorkspaceId(workspaceId);
354
+ const rootDir = path.resolve(projectDir);
355
+ const excludes = normalizeExcludeDirs([...DEFAULT_EXCLUDED_DIRS, ...excludeDirs]);
356
+ const seedPaths: string[] = [];
357
+ collectSeedPaths(rootDir, '', excludes, seedPaths);
358
+ const allFiles = seedPaths
359
+ .sort((left, right) => left.localeCompare(right))
360
+ .map((filePath) => buildSeedFilePayload(filePath, rootDir));
361
+
362
+ let seededCount = 0;
363
+ for (let index = 0; index < allFiles.length; index += BATCH_SIZE) {
364
+ const batch = allFiles.slice(index, index + BATCH_SIZE);
365
+ const batchIndex = Math.floor(index / BATCH_SIZE);
366
+ const result = await writeBulkWrite(
367
+ baseUrl,
368
+ token,
369
+ workspace,
370
+ batch,
371
+ `seed-workspace-${workspace}-${Date.now()}-${batchIndex}`
372
+ );
373
+ seededCount += result.written;
374
+ }
375
+
376
+ return seededCount;
377
+ }
378
+
379
+ function buildWorkflowAclRules(agents: WorkflowAclAgent[]): Record<string, string[]> {
380
+ const directories = new Set<string>();
381
+ const normalizedAgents = agents.map((agent) => ({
382
+ name: String(agent.name ?? '').trim(),
383
+ acl: Object.fromEntries(
384
+ Object.entries(agent.acl ?? {}).map(([dirPath, rules]) => [
385
+ normalizeAclDirectory(dirPath),
386
+ Array.isArray(rules) ? rules : [],
387
+ ])
388
+ ),
389
+ }));
390
+ const reviewerNames = normalizedAgents
391
+ .map((agent) => agent.name)
392
+ .filter((name) => name !== '' && isReviewerAgent(name));
393
+
394
+ for (const agent of normalizedAgents) {
395
+ for (const dirPath of Object.keys(agent.acl)) {
396
+ directories.add(dirPath);
397
+ }
398
+ }
399
+
400
+ const merged = new Map<string, Set<string>>();
401
+
402
+ for (const dirPath of [...directories].sort((left, right) => left.localeCompare(right))) {
403
+ const rules = new Set<string>();
404
+
405
+ for (const reviewerName of reviewerNames) {
406
+ rules.add(`allow:agent:${reviewerName}:read`);
407
+ }
408
+
409
+ for (const agent of normalizedAgents) {
410
+ if (!agent.name) {
411
+ continue;
412
+ }
413
+
414
+ const agentRules = agent.acl[dirPath] ?? [];
415
+ const hasRead = agentRules.includes('read') || agentRules.includes('write');
416
+ const hasWrite = agentRules.includes('write');
417
+
418
+ if (hasRead) {
419
+ rules.add(`allow:agent:${agent.name}:read`);
420
+ } else if (!isReviewerAgent(agent.name)) {
421
+ rules.add(`deny:agent:${agent.name}`);
422
+ }
423
+
424
+ if (hasWrite) {
425
+ rules.add(`allow:agent:${agent.name}:write`);
426
+ }
427
+ }
428
+
429
+ if (rules.size > 0) {
430
+ merged.set(dirPath, rules);
431
+ }
432
+ }
433
+
434
+ return Object.fromEntries([...merged.entries()].map(([dirPath, rules]) => [dirPath, [...rules].sort()]));
435
+ }
436
+
437
+ export async function seedWorkflowAcls({
438
+ relayfileUrl,
439
+ adminToken,
440
+ workspace,
441
+ agents,
442
+ }: SeedWorkflowAclsOptions): Promise<void> {
443
+ const aclRules = buildWorkflowAclRules(agents);
444
+
445
+ if (Object.keys(aclRules).length === 0) {
446
+ return;
447
+ }
448
+
449
+ await seedAclRules(relayfileUrl, adminToken, workspace, aclRules);
450
+ }
451
+
452
+ // ── Tar-based bulk upload ───────────────────────────────────────────────────
453
+
454
+ interface ImportResponseShape {
455
+ imported?: number;
456
+ }
457
+
458
+ function getGitTrackedFiles(rootDir: string): string[] | null {
459
+ try {
460
+ const output = execSync('git ls-files -z --cached --others --exclude-standard', {
461
+ cwd: rootDir,
462
+ encoding: 'utf-8',
463
+ maxBuffer: 50 * 1024 * 1024,
464
+ });
465
+ const files = output.split('\0').filter(Boolean);
466
+ return files;
467
+ } catch {
468
+ return null;
469
+ }
470
+ }
471
+
472
+ function collectAllFiles(rootDir: string, excludeDirs: Set<string>): string[] {
473
+ const files: string[] = [];
474
+ const stack = [''];
475
+
476
+ while (stack.length > 0) {
477
+ const currentRelative = stack.pop()!;
478
+ const absoluteDir = path.join(rootDir, currentRelative);
479
+ let entries: fs.Dirent[];
480
+ try {
481
+ entries = fs.readdirSync(absoluteDir, { withFileTypes: true });
482
+ } catch {
483
+ continue;
484
+ }
485
+
486
+ for (const entry of entries) {
487
+ if (excludeDirs.has(entry.name)) continue;
488
+ if (DEFAULT_EXCLUDED_FILES.has(entry.name)) continue;
489
+ const nextRelative = currentRelative ? `${currentRelative}/${entry.name}` : entry.name;
490
+ if (excludeDirs.has(nextRelative)) continue;
491
+
492
+ if (entry.isDirectory()) {
493
+ stack.push(nextRelative);
494
+ } else if (entry.isFile()) {
495
+ files.push(nextRelative);
496
+ }
497
+ }
498
+ }
499
+
500
+ return files;
501
+ }
502
+
503
+ async function createTarBuffer(rootDir: string, files: string[]): Promise<Buffer> {
504
+ const tarStream = tar.create({ gzip: true, cwd: rootDir, portable: true, follow: true }, files);
505
+ const chunks: Buffer[] = [];
506
+ for await (const chunk of tarStream) {
507
+ chunks.push(Buffer.from(chunk as Uint8Array));
508
+ }
509
+ return Buffer.concat(chunks);
510
+ }
511
+
512
+ export async function seedWorkspaceTar(
513
+ baseUrl: string,
514
+ token: string,
515
+ workspaceId: string,
516
+ projectDir: string,
517
+ excludeDirs: string[]
518
+ ): Promise<number> {
519
+ const workspace = normalizeWorkspaceId(workspaceId);
520
+ const rootDir = path.resolve(projectDir);
521
+ const excludes = normalizeExcludeDirs([...DEFAULT_EXCLUDED_DIRS, ...excludeDirs]);
522
+
523
+ const gitFiles = getGitTrackedFiles(rootDir);
524
+ const rawFiles = gitFiles ?? collectAllFiles(rootDir, excludes);
525
+ const files = gitFiles
526
+ ? rawFiles.filter((f) => {
527
+ const segments = f.split('/');
528
+ if (DEFAULT_EXCLUDED_FILES.has(segments[segments.length - 1])) return false;
529
+ return !segments.some((seg) => excludes.has(seg));
530
+ })
531
+ : rawFiles;
532
+
533
+ if (files.length === 0) {
534
+ return 0;
535
+ }
536
+
537
+ const tarball = await createTarBuffer(rootDir, files);
538
+
539
+ const url = `${normalizeBaseUrl(baseUrl)}/v1/workspaces/${encodeURIComponent(workspace)}/fs/import`;
540
+ const response = await fetch(url, {
541
+ method: 'POST',
542
+ headers: {
543
+ Authorization: `Bearer ${token}`,
544
+ 'Content-Type': 'application/gzip',
545
+ 'X-Correlation-Id': `seed-tar-${workspace}-${Date.now()}`,
546
+ },
547
+ body: tarball.buffer.slice(tarball.byteOffset, tarball.byteOffset + tarball.byteLength) as ArrayBuffer,
548
+ });
549
+
550
+ if (response.status === 404) {
551
+ // Tar import not supported — fall back to batch upload
552
+ return seedWorkspace(baseUrl, token, workspaceId, projectDir, excludeDirs);
553
+ }
554
+
555
+ if (!response.ok) {
556
+ const body = await response.text().catch(() => '');
557
+ throw new Error(`tar import failed for workspace ${workspace}: HTTP ${response.status} ${body}`.trim());
558
+ }
559
+
560
+ const raw = await response.text();
561
+ if (!raw.trim()) {
562
+ return files.length;
563
+ }
564
+
565
+ try {
566
+ const parsed = JSON.parse(raw) as ImportResponseShape;
567
+ return typeof parsed.imported === 'number' ? parsed.imported : files.length;
568
+ } catch {
569
+ return files.length;
570
+ }
571
+ }
@@ -0,0 +1,112 @@
1
+ import { createHmac, randomUUID } from 'node:crypto';
2
+
3
+ export const DEFAULT_WORKFLOW_TOKEN_TTL_SECONDS = 2 * 60 * 60;
4
+ export const DEFAULT_ADMIN_AGENT_NAME = 'relay-admin';
5
+ export const DEFAULT_ADMIN_SCOPES = [
6
+ 'relayauth:*:manage:*',
7
+ 'relayauth:*:read:*',
8
+ 'relayfile:*:*:*',
9
+ 'fs:read',
10
+ 'fs:write',
11
+ 'sync:trigger',
12
+ 'ops:read',
13
+ 'admin:read',
14
+ ];
15
+
16
+ const JWT_HEADER = { alg: 'HS256', typ: 'JWT' } as const;
17
+
18
+ export interface TokenClaims {
19
+ sub: string;
20
+ org: string;
21
+ wks: string;
22
+ workspace_id: string;
23
+ agent_name: string;
24
+ scopes: string[];
25
+ sponsorId: string;
26
+ sponsorChain: string[];
27
+ token_type: string;
28
+ iss: string;
29
+ aud: string[];
30
+ iat: number;
31
+ exp: number;
32
+ jti: string;
33
+ }
34
+
35
+ export interface MintAgentTokenOptions {
36
+ secret: string;
37
+ agentName: string;
38
+ workspace: string;
39
+ scopes: string[];
40
+ ttlSeconds?: number;
41
+ }
42
+
43
+ function base64urlEncode(value: unknown): string {
44
+ return Buffer.from(JSON.stringify(value)).toString('base64url');
45
+ }
46
+
47
+ function normalizeTtlSeconds(ttlSeconds?: number): number {
48
+ if (ttlSeconds === undefined) {
49
+ return DEFAULT_WORKFLOW_TOKEN_TTL_SECONDS;
50
+ }
51
+
52
+ return Math.max(1, Math.floor(ttlSeconds));
53
+ }
54
+
55
+ export function mintAgentToken(opts: MintAgentTokenOptions): string {
56
+ const now = Math.floor(Date.now() / 1000);
57
+ const payload: TokenClaims = {
58
+ sub: `agent_${opts.agentName}`,
59
+ org: 'org_relay',
60
+ wks: opts.workspace,
61
+ workspace_id: opts.workspace,
62
+ agent_name: opts.agentName,
63
+ scopes: [...opts.scopes],
64
+ sponsorId: 'relay-local',
65
+ sponsorChain: ['relay-local'],
66
+ token_type: 'access',
67
+ iss: 'relayauth:local',
68
+ aud: ['relayauth', 'relayfile'],
69
+ iat: now,
70
+ exp: now + normalizeTtlSeconds(opts.ttlSeconds),
71
+ jti: `tok-${now}-${randomUUID()}`,
72
+ };
73
+
74
+ const unsigned = `${base64urlEncode(JWT_HEADER)}.${base64urlEncode(payload)}`;
75
+ const signature = createHmac('sha256', opts.secret).update(unsigned).digest('base64url');
76
+
77
+ return `${unsigned}.${signature}`;
78
+ }
79
+
80
+ export class WorkflowTokenFactory {
81
+ private readonly tokens = new Map<string, string>();
82
+ private readonly ttlSeconds: number;
83
+
84
+ constructor(
85
+ private readonly secret: string,
86
+ private readonly workspace: string,
87
+ ttlSeconds = DEFAULT_WORKFLOW_TOKEN_TTL_SECONDS
88
+ ) {
89
+ this.ttlSeconds = normalizeTtlSeconds(ttlSeconds);
90
+ }
91
+
92
+ mintForAgent(agentName: string, scopes: string[], ttlSeconds = this.ttlSeconds): string {
93
+ const token = mintAgentToken({
94
+ secret: this.secret,
95
+ workspace: this.workspace,
96
+ agentName,
97
+ scopes,
98
+ ttlSeconds,
99
+ });
100
+
101
+ this.tokens.set(agentName, token);
102
+ return token;
103
+ }
104
+
105
+ mintAdmin(ttlSeconds = this.ttlSeconds): string {
106
+ return this.mintForAgent(DEFAULT_ADMIN_AGENT_NAME, DEFAULT_ADMIN_SCOPES, ttlSeconds);
107
+ }
108
+
109
+ getToken(agentName: string): string | undefined {
110
+ return this.tokens.get(agentName);
111
+ }
112
+ }