agent-relay 3.2.22 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +6564 -2100
- package/dist/src/cli/bootstrap.d.ts.map +1 -1
- package/dist/src/cli/bootstrap.js +2 -0
- package/dist/src/cli/bootstrap.js.map +1 -1
- package/dist/src/cli/commands/agent-management.d.ts.map +1 -1
- package/dist/src/cli/commands/agent-management.js +14 -4
- package/dist/src/cli/commands/agent-management.js.map +1 -1
- package/dist/src/cli/commands/core.d.ts +2 -6
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +31 -12
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/commands/messaging.d.ts.map +1 -1
- package/dist/src/cli/commands/messaging.js +10 -3
- package/dist/src/cli/commands/messaging.js.map +1 -1
- package/dist/src/cli/commands/monitoring.d.ts +2 -2
- package/dist/src/cli/commands/monitoring.d.ts.map +1 -1
- package/dist/src/cli/commands/monitoring.js +15 -6
- package/dist/src/cli/commands/monitoring.js.map +1 -1
- package/dist/src/cli/commands/on/dotfiles.d.ts +35 -0
- package/dist/src/cli/commands/on/dotfiles.d.ts.map +1 -0
- package/dist/src/cli/commands/on/dotfiles.js +157 -0
- package/dist/src/cli/commands/on/dotfiles.js.map +1 -0
- package/dist/src/cli/commands/on/prereqs.d.ts +15 -0
- package/dist/src/cli/commands/on/prereqs.d.ts.map +1 -0
- package/dist/src/cli/commands/on/prereqs.js +103 -0
- package/dist/src/cli/commands/on/prereqs.js.map +1 -0
- package/dist/src/cli/commands/on/provision.d.ts +22 -0
- package/dist/src/cli/commands/on/provision.d.ts.map +1 -0
- package/dist/src/cli/commands/on/provision.js +157 -0
- package/dist/src/cli/commands/on/provision.js.map +1 -0
- package/dist/src/cli/commands/on/relayfile-binary.d.ts +2 -0
- package/dist/src/cli/commands/on/relayfile-binary.d.ts.map +1 -0
- package/dist/src/cli/commands/on/relayfile-binary.js +208 -0
- package/dist/src/cli/commands/on/relayfile-binary.js.map +1 -0
- package/dist/src/cli/commands/on/scan.d.ts +8 -0
- package/dist/src/cli/commands/on/scan.d.ts.map +1 -0
- package/dist/src/cli/commands/on/scan.js +59 -0
- package/dist/src/cli/commands/on/scan.js.map +1 -0
- package/dist/src/cli/commands/on/services.d.ts +17 -0
- package/dist/src/cli/commands/on/services.d.ts.map +1 -0
- package/dist/src/cli/commands/on/services.js +328 -0
- package/dist/src/cli/commands/on/services.js.map +1 -0
- package/dist/src/cli/commands/on/start.d.ts +61 -0
- package/dist/src/cli/commands/on/start.d.ts.map +1 -0
- package/dist/src/cli/commands/on/start.js +1107 -0
- package/dist/src/cli/commands/on/start.js.map +1 -0
- package/dist/src/cli/commands/on/stop.d.ts +4 -0
- package/dist/src/cli/commands/on/stop.d.ts.map +1 -0
- package/dist/src/cli/commands/on/stop.js +11 -0
- package/dist/src/cli/commands/on/stop.js.map +1 -0
- package/dist/src/cli/commands/on/token.d.ts +8 -0
- package/dist/src/cli/commands/on/token.d.ts.map +1 -0
- package/dist/src/cli/commands/on/token.js +26 -0
- package/dist/src/cli/commands/on/token.js.map +1 -0
- package/dist/src/cli/commands/on/workspace.d.ts +4 -0
- package/dist/src/cli/commands/on/workspace.d.ts.map +1 -0
- package/dist/src/cli/commands/on/workspace.js +245 -0
- package/dist/src/cli/commands/on/workspace.js.map +1 -0
- package/dist/src/cli/commands/on.d.ts +10 -0
- package/dist/src/cli/commands/on.d.ts.map +1 -0
- package/dist/src/cli/commands/on.js +52 -0
- package/dist/src/cli/commands/on.js.map +1 -0
- package/dist/src/cli/commands/setup.d.ts.map +1 -1
- package/dist/src/cli/commands/setup.js +10 -21
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/lib/bridge.js +1 -1
- package/dist/src/cli/lib/bridge.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts +14 -4
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +82 -120
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/src/cli/lib/client-factory.d.ts +4 -4
- package/dist/src/cli/lib/client-factory.d.ts.map +1 -1
- package/dist/src/cli/lib/client-factory.js +14 -11
- package/dist/src/cli/lib/client-factory.js.map +1 -1
- package/dist/src/cli/lib/core-maintenance.d.ts.map +1 -1
- package/dist/src/cli/lib/core-maintenance.js +11 -22
- package/dist/src/cli/lib/core-maintenance.js.map +1 -1
- package/dist/src/cost/pricing.d.ts +18 -0
- package/dist/src/cost/pricing.d.ts.map +1 -0
- package/dist/src/cost/pricing.js +111 -0
- package/dist/src/cost/pricing.js.map +1 -0
- package/dist/src/cost/tracker.d.ts +13 -0
- package/dist/src/cost/tracker.d.ts.map +1 -0
- package/dist/src/cost/tracker.js +152 -0
- package/dist/src/cost/tracker.js.map +1 -0
- package/dist/src/cost/types.d.ts +23 -0
- package/dist/src/cost/types.d.ts.map +1 -0
- package/dist/src/cost/types.js +2 -0
- package/dist/src/cost/types.js.map +1 -0
- package/package.json +15 -12
- package/packages/acp-bridge/package.json +2 -2
- package/packages/brand/package.json +1 -1
- package/packages/cloud/package.json +3 -3
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/README.md +10 -3
- package/packages/sdk/dist/broker-path.d.ts +3 -2
- package/packages/sdk/dist/broker-path.d.ts.map +1 -1
- package/packages/sdk/dist/broker-path.js +119 -32
- package/packages/sdk/dist/broker-path.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +119 -197
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +354 -823
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/examples/example.js +2 -5
- package/packages/sdk/dist/examples/example.js.map +1 -1
- package/packages/sdk/dist/index.d.ts +3 -1
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +3 -1
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/relay-adapter.d.ts +9 -26
- package/packages/sdk/dist/relay-adapter.d.ts.map +1 -1
- package/packages/sdk/dist/relay-adapter.js +75 -47
- package/packages/sdk/dist/relay-adapter.js.map +1 -1
- package/packages/sdk/dist/relay.d.ts +26 -6
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +213 -43
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/transport.d.ts +58 -0
- package/packages/sdk/dist/transport.d.ts.map +1 -0
- package/packages/sdk/dist/transport.js +184 -0
- package/packages/sdk/dist/transport.js.map +1 -0
- package/packages/sdk/dist/types.d.ts +69 -0
- package/packages/sdk/dist/types.d.ts.map +1 -0
- package/packages/sdk/dist/types.js +5 -0
- package/packages/sdk/dist/types.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js +117 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js +4 -3
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js.map +1 -1
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.js +378 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js +145 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.js +170 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.js.map +1 -0
- package/packages/sdk/dist/workflows/builder.d.ts +3 -2
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +1 -3
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/channel-messenger.d.ts +28 -0
- package/packages/sdk/dist/workflows/channel-messenger.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/channel-messenger.js +255 -0
- package/packages/sdk/dist/workflows/channel-messenger.js.map +1 -0
- package/packages/sdk/dist/workflows/index.d.ts +7 -0
- package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/index.js +7 -0
- package/packages/sdk/dist/workflows/index.js.map +1 -1
- package/packages/sdk/dist/workflows/process-spawner.d.ts +35 -0
- package/packages/sdk/dist/workflows/process-spawner.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/process-spawner.js +141 -0
- package/packages/sdk/dist/workflows/process-spawner.js.map +1 -0
- package/packages/sdk/dist/workflows/run.d.ts +2 -1
- package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/run.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +6 -6
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +443 -719
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/step-executor.d.ts +95 -0
- package/packages/sdk/dist/workflows/step-executor.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/step-executor.js +393 -0
- package/packages/sdk/dist/workflows/step-executor.js.map +1 -0
- package/packages/sdk/dist/workflows/template-resolver.d.ts +33 -0
- package/packages/sdk/dist/workflows/template-resolver.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/template-resolver.js +144 -0
- package/packages/sdk/dist/workflows/template-resolver.js.map +1 -0
- package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/validator.js +17 -2
- package/packages/sdk/dist/workflows/validator.js.map +1 -1
- package/packages/sdk/dist/workflows/verification.d.ts +33 -0
- package/packages/sdk/dist/workflows/verification.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/verification.js +122 -0
- package/packages/sdk/dist/workflows/verification.js.map +1 -0
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/unit.test.ts +100 -1
- package/packages/sdk/src/broker-path.ts +136 -30
- package/packages/sdk/src/client.ts +453 -1069
- package/packages/sdk/src/examples/example.ts +2 -5
- package/packages/sdk/src/index.ts +9 -1
- package/packages/sdk/src/relay-adapter.ts +75 -55
- package/packages/sdk/src/relay.ts +262 -55
- package/packages/sdk/src/transport.ts +216 -0
- package/packages/sdk/src/types.ts +75 -0
- package/packages/sdk/src/workflows/__tests__/channel-messenger.test.ts +137 -0
- package/packages/sdk/src/workflows/__tests__/run-summary-table.test.ts +4 -3
- package/packages/sdk/src/workflows/__tests__/step-executor.test.ts +444 -0
- package/packages/sdk/src/workflows/__tests__/template-resolver.test.ts +162 -0
- package/packages/sdk/src/workflows/__tests__/verification.test.ts +229 -0
- package/packages/sdk/src/workflows/builder.ts +6 -6
- package/packages/sdk/src/workflows/channel-messenger.ts +314 -0
- package/packages/sdk/src/workflows/index.ts +12 -0
- package/packages/sdk/src/workflows/process-spawner.ts +201 -0
- package/packages/sdk/src/workflows/run.ts +2 -1
- package/packages/sdk/src/workflows/runner.ts +636 -951
- package/packages/sdk/src/workflows/step-executor.ts +579 -0
- package/packages/sdk/src/workflows/template-resolver.ts +180 -0
- package/packages/sdk/src/workflows/validator.ts +20 -2
- package/packages/sdk/src/workflows/verification.ts +184 -0
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +0 -8
- package/packages/sdk-py/src/agent_relay/client.py +329 -522
- package/packages/sdk-py/src/agent_relay/protocol.py +2 -96
- package/packages/sdk-py/src/agent_relay/relay.py +1 -4
- package/packages/sdk-py/tests/test_wait_for_api_url.py +92 -0
- package/packages/sdk-py/uv.lock +5388 -0
- package/packages/telemetry/dist/client.d.ts.map +1 -1
- package/packages/telemetry/dist/client.js +1 -1
- package/packages/telemetry/dist/client.js.map +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/telemetry/src/client.ts +3 -10
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
- package/scripts/postinstall.js +121 -1
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { appendFileSync, accessSync, constants, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parse as parseYaml } from 'yaml';
|
|
6
|
+
import { compileDotfiles, hasDotfiles } from './dotfiles.js';
|
|
7
|
+
import { ensureRelayfileMountBinary } from './relayfile-binary.js';
|
|
8
|
+
import { mintToken } from './token.js';
|
|
9
|
+
import { seedWorkspace as seedWorkspaceFiles, seedAclRules } from './workspace.js';
|
|
10
|
+
import { ensureAuthenticated, readStoredAuth } from '@agent-relay/cloud';
|
|
11
|
+
const DEFAULT_SEED_EXCLUDES = ['.relay', '.git', 'node_modules'];
|
|
12
|
+
const DEFAULT_RELAYCAST_BASE_URL = 'https://api.relaycast.dev';
|
|
13
|
+
const WORKSPACE_ID_PREFIX = 'rw_';
|
|
14
|
+
const WORKSPACE_ID_ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
15
|
+
function normalizeLineList(value) {
|
|
16
|
+
if (!value)
|
|
17
|
+
return [];
|
|
18
|
+
return value
|
|
19
|
+
.split('\n')
|
|
20
|
+
.map((line) => line.trim())
|
|
21
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'));
|
|
22
|
+
}
|
|
23
|
+
function normalizeBaseUrl(input) {
|
|
24
|
+
if (!input)
|
|
25
|
+
return input;
|
|
26
|
+
return input.startsWith('http://') || input.startsWith('https://')
|
|
27
|
+
? input.replace(/\/$/, '')
|
|
28
|
+
: `http://127.0.0.1:${input}`;
|
|
29
|
+
}
|
|
30
|
+
function parseJsonConfig(raw) {
|
|
31
|
+
const trimmed = raw.trim();
|
|
32
|
+
if (!trimmed)
|
|
33
|
+
return null;
|
|
34
|
+
return JSON.parse(trimmed);
|
|
35
|
+
}
|
|
36
|
+
function parseYamlConfig(raw) {
|
|
37
|
+
const parsed = parseYaml(raw);
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
function toRecord(value) {
|
|
41
|
+
return value && typeof value === 'object' ? value : {};
|
|
42
|
+
}
|
|
43
|
+
function toStringArray(value, fallback = []) {
|
|
44
|
+
if (!Array.isArray(value))
|
|
45
|
+
return fallback;
|
|
46
|
+
const values = value
|
|
47
|
+
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
|
|
48
|
+
.filter((entry) => entry.length > 0);
|
|
49
|
+
return values.length > 0 ? values : fallback;
|
|
50
|
+
}
|
|
51
|
+
function toString(value, fallback = '') {
|
|
52
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : fallback;
|
|
53
|
+
}
|
|
54
|
+
function buildJoinCommand(workspaceId) {
|
|
55
|
+
return `agent-relay on <cli> --workspace ${workspaceId}`;
|
|
56
|
+
}
|
|
57
|
+
function normalizeWorkspaceId(value) {
|
|
58
|
+
const trimmed = value?.trim();
|
|
59
|
+
return trimmed ? trimmed : undefined;
|
|
60
|
+
}
|
|
61
|
+
function generateWorkspaceId() {
|
|
62
|
+
const alphabetLength = WORKSPACE_ID_ALPHABET.length;
|
|
63
|
+
const maxUnbiasedValue = Math.floor(256 / alphabetLength) * alphabetLength;
|
|
64
|
+
let suffix = '';
|
|
65
|
+
while (suffix.length < 8) {
|
|
66
|
+
const bytes = randomBytes(8 - suffix.length + 2);
|
|
67
|
+
for (const byte of bytes) {
|
|
68
|
+
if (byte >= maxUnbiasedValue)
|
|
69
|
+
continue;
|
|
70
|
+
suffix += WORKSPACE_ID_ALPHABET[byte % alphabetLength];
|
|
71
|
+
if (suffix.length === 8)
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return `${WORKSPACE_ID_PREFIX}${suffix}`;
|
|
76
|
+
}
|
|
77
|
+
function sanitizePathComponent(value) {
|
|
78
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
79
|
+
}
|
|
80
|
+
function toWorkspaceRegistryEntry(value) {
|
|
81
|
+
if (!value || typeof value !== 'object') {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
const entry = value;
|
|
85
|
+
const relaycastApiKey = typeof entry.relaycastApiKey === 'string' && entry.relaycastApiKey.trim()
|
|
86
|
+
? entry.relaycastApiKey.trim()
|
|
87
|
+
: undefined;
|
|
88
|
+
const relayfileUrl = typeof entry.relayfileUrl === 'string' && entry.relayfileUrl.trim()
|
|
89
|
+
? entry.relayfileUrl.trim()
|
|
90
|
+
: undefined;
|
|
91
|
+
const createdAt = typeof entry.createdAt === 'string' && entry.createdAt.trim() ? entry.createdAt.trim() : undefined;
|
|
92
|
+
const agents = Array.isArray(entry.agents)
|
|
93
|
+
? entry.agents
|
|
94
|
+
.filter((agent) => typeof agent === 'string')
|
|
95
|
+
.map((agent) => agent.trim())
|
|
96
|
+
.filter((agent) => agent.length > 0)
|
|
97
|
+
: undefined;
|
|
98
|
+
return {
|
|
99
|
+
...(relaycastApiKey ? { relaycastApiKey } : {}),
|
|
100
|
+
...(relayfileUrl ? { relayfileUrl } : {}),
|
|
101
|
+
...(createdAt ? { createdAt } : {}),
|
|
102
|
+
...(agents && agents.length > 0 ? { agents } : {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function getWorkspaceRegistryPath(relayDir) {
|
|
106
|
+
return path.join(relayDir, 'workspaces.json');
|
|
107
|
+
}
|
|
108
|
+
function readWorkspaceRegistry(relayDir) {
|
|
109
|
+
if (!relayDir) {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
const registryPath = getWorkspaceRegistryPath(relayDir);
|
|
113
|
+
if (!existsSync(registryPath)) {
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
const raw = readFileSync(registryPath, 'utf8').trim();
|
|
117
|
+
if (!raw) {
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
let parsed;
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(raw);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
const registry = {};
|
|
131
|
+
for (const [workspaceId, entry] of Object.entries(parsed)) {
|
|
132
|
+
const normalizedId = normalizeWorkspaceId(workspaceId);
|
|
133
|
+
if (!normalizedId)
|
|
134
|
+
continue;
|
|
135
|
+
registry[normalizedId] = toWorkspaceRegistryEntry(entry);
|
|
136
|
+
}
|
|
137
|
+
return registry;
|
|
138
|
+
}
|
|
139
|
+
function writeWorkspaceRegistry(relayDir, registry) {
|
|
140
|
+
ensureDirectory(relayDir);
|
|
141
|
+
writeFileSync(getWorkspaceRegistryPath(relayDir), `${JSON.stringify(registry, null, 2)}\n`, {
|
|
142
|
+
encoding: 'utf8',
|
|
143
|
+
mode: 0o600,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function updateWorkspaceRegistry(relayDir, workspaceId, update) {
|
|
147
|
+
const existingRegistry = readWorkspaceRegistry(relayDir);
|
|
148
|
+
const existing = existingRegistry[workspaceId] ?? {};
|
|
149
|
+
const agents = [...new Set([...(existing.agents ?? []), ...(update.agentName ? [update.agentName] : [])])];
|
|
150
|
+
const next = {
|
|
151
|
+
...existing,
|
|
152
|
+
...(update.relaycastApiKey ? { relaycastApiKey: update.relaycastApiKey } : {}),
|
|
153
|
+
relayfileUrl: update.relayfileUrl ?? existing.relayfileUrl,
|
|
154
|
+
createdAt: update.createdAt ?? existing.createdAt ?? new Date().toISOString(),
|
|
155
|
+
...(agents.length > 0 ? { agents } : {}),
|
|
156
|
+
};
|
|
157
|
+
if (relayDir) {
|
|
158
|
+
existingRegistry[workspaceId] = next;
|
|
159
|
+
writeWorkspaceRegistry(relayDir, existingRegistry);
|
|
160
|
+
}
|
|
161
|
+
return next;
|
|
162
|
+
}
|
|
163
|
+
async function postWorkspaceApi(fetchFn, url, body) {
|
|
164
|
+
const headers = {
|
|
165
|
+
'Content-Type': 'application/json',
|
|
166
|
+
'X-Correlation-Id': `agent-relay-on-${Date.now()}`,
|
|
167
|
+
};
|
|
168
|
+
// For remote endpoints, try anonymous first — attach existing auth if
|
|
169
|
+
// available but never force a browser login. If the server returns 401,
|
|
170
|
+
// fall back to interactive login and retry once.
|
|
171
|
+
if (!isLocalBaseUrl(url)) {
|
|
172
|
+
const stored = await readStoredAuth().catch(() => null);
|
|
173
|
+
const parsed = new URL(url);
|
|
174
|
+
const targetOrigin = `${parsed.protocol}//${parsed.host}`;
|
|
175
|
+
if (stored && stored.apiUrl === targetOrigin) {
|
|
176
|
+
headers['Authorization'] = `Bearer ${stored.accessToken}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
let response = await fetchFn(url, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers,
|
|
182
|
+
body: JSON.stringify(body),
|
|
183
|
+
});
|
|
184
|
+
// Retry with interactive login if the server requires auth
|
|
185
|
+
if (response.status === 401 && !isLocalBaseUrl(url)) {
|
|
186
|
+
const parsed = new URL(url);
|
|
187
|
+
const auth = await ensureAuthenticated(`${parsed.protocol}//${parsed.host}`);
|
|
188
|
+
headers['Authorization'] = `Bearer ${auth.accessToken}`;
|
|
189
|
+
response = await fetchFn(url, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers,
|
|
192
|
+
body: JSON.stringify(body),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const raw = await response.text();
|
|
196
|
+
if (!response.ok) {
|
|
197
|
+
throw new Error(`workspace API request failed (${response.status}): ${raw}`.trim());
|
|
198
|
+
}
|
|
199
|
+
if (!raw.trim()) {
|
|
200
|
+
return {};
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
return JSON.parse(raw);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
throw new Error(`workspace API returned invalid JSON: ${String(error)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function parseCreateWorkspaceResponse(payload, requestedWorkspaceId) {
|
|
210
|
+
const root = toRecord(payload);
|
|
211
|
+
const data = toRecord(root.data);
|
|
212
|
+
const workspaceId = toString(data.workspaceId, toString(data.id, toString(root.workspaceId, toString(root.id, requestedWorkspaceId))));
|
|
213
|
+
if (!workspaceId) {
|
|
214
|
+
throw new Error('workspace create response is missing workspaceId');
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
workspaceId,
|
|
218
|
+
token: toString(data.token, toString(root.token)),
|
|
219
|
+
relayfileUrl: normalizeBaseUrl(toString(data.relayfileUrl, toString(root.relayfileUrl))),
|
|
220
|
+
relaycastApiKey: toString(data.relaycastApiKey, toString(data.apiKey, toString(data.api_key, toString(root.relaycastApiKey, toString(root.apiKey, toString(root.api_key)))))) || undefined,
|
|
221
|
+
joinCommand: toString(data.joinCommand, toString(root.joinCommand, buildJoinCommand(workspaceId))),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function parseJoinWorkspaceResponse(payload, requestedWorkspaceId) {
|
|
225
|
+
const root = toRecord(payload);
|
|
226
|
+
const data = toRecord(root.data);
|
|
227
|
+
const workspaceId = toString(data.workspaceId, toString(data.id, toString(root.workspaceId, toString(root.id, requestedWorkspaceId))));
|
|
228
|
+
const token = toString(data.token, toString(root.token));
|
|
229
|
+
if (!workspaceId) {
|
|
230
|
+
throw new Error('workspace join response is missing workspaceId');
|
|
231
|
+
}
|
|
232
|
+
if (!token) {
|
|
233
|
+
throw new Error(`workspace join response for ${workspaceId} is missing token`);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
workspaceId,
|
|
237
|
+
token,
|
|
238
|
+
relayfileUrl: normalizeBaseUrl(toString(data.relayfileUrl, toString(root.relayfileUrl))),
|
|
239
|
+
relaycastApiKey: toString(data.relaycastApiKey, toString(data.apiKey, toString(data.api_key, toString(root.relaycastApiKey, toString(root.apiKey, toString(root.api_key)))))) || undefined,
|
|
240
|
+
joinCommand: toString(data.joinCommand, toString(root.joinCommand, buildJoinCommand(workspaceId))),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
async function joinWorkspaceSession(fetchFn, authBase, workspaceId, agentName, scopes) {
|
|
244
|
+
const body = { agentName };
|
|
245
|
+
if (scopes.length > 0) {
|
|
246
|
+
body.scopes = scopes;
|
|
247
|
+
}
|
|
248
|
+
const payload = await postWorkspaceApi(fetchFn, `${authBase}/api/v1/workspaces/${encodeURIComponent(workspaceId)}/join`, body);
|
|
249
|
+
return parseJoinWorkspaceResponse(payload, workspaceId);
|
|
250
|
+
}
|
|
251
|
+
async function createRelaycastWorkspace(fetchFn, baseUrl, workspaceName) {
|
|
252
|
+
const response = await fetchFn(`${normalizeBaseUrl(baseUrl)}/v1/workspaces`, {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: {
|
|
255
|
+
'Content-Type': 'application/json',
|
|
256
|
+
'X-Correlation-Id': `agent-relay-on-relaycast-${Date.now()}`,
|
|
257
|
+
},
|
|
258
|
+
body: JSON.stringify({ name: workspaceName }),
|
|
259
|
+
});
|
|
260
|
+
const raw = await response.text();
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
throw new Error(`relaycast workspace create failed (${response.status}): ${raw}`.trim());
|
|
263
|
+
}
|
|
264
|
+
const parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
265
|
+
const root = toRecord(parsed);
|
|
266
|
+
const data = toRecord(root.data);
|
|
267
|
+
const apiKey = toString(data.apiKey, toString(data.api_key, toString(root.apiKey, toString(root.api_key))));
|
|
268
|
+
if (!apiKey) {
|
|
269
|
+
throw new Error('relaycast workspace create response is missing apiKey');
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
apiKey,
|
|
273
|
+
createdAt: toString(data.createdAt, toString(data.created_at, toString(root.createdAt, toString(root.created_at)))) || undefined,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async function requestLocalWorkspaceSession(options) {
|
|
277
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
278
|
+
const relayDir = options.relayDir;
|
|
279
|
+
const signingSecret = toString(options.signingSecret);
|
|
280
|
+
const requestedWorkspaceId = normalizeWorkspaceId(options.requestedWorkspaceId);
|
|
281
|
+
if (!relayDir) {
|
|
282
|
+
throw new Error('relayDir is required for local workspace sessions');
|
|
283
|
+
}
|
|
284
|
+
if (!signingSecret) {
|
|
285
|
+
throw new Error('signingSecret is required for local workspace sessions');
|
|
286
|
+
}
|
|
287
|
+
const workspaceId = requestedWorkspaceId ?? generateWorkspaceId();
|
|
288
|
+
const existing = readWorkspaceRegistry(relayDir)[workspaceId];
|
|
289
|
+
if (requestedWorkspaceId && !existing) {
|
|
290
|
+
throw new Error(`workspace ${workspaceId} is not registered locally`);
|
|
291
|
+
}
|
|
292
|
+
let relaycastApiKey = toString(existing?.relaycastApiKey) || undefined;
|
|
293
|
+
let createdAt = toString(existing?.createdAt) || undefined;
|
|
294
|
+
if (!relaycastApiKey) {
|
|
295
|
+
const created = await createRelaycastWorkspace(fetchFn, options.relaycastBaseUrl ?? process.env.RELAYCAST_BASE_URL ?? DEFAULT_RELAYCAST_BASE_URL, toString(options.workspaceName, workspaceId));
|
|
296
|
+
relaycastApiKey = created.apiKey;
|
|
297
|
+
createdAt = created.createdAt ?? createdAt;
|
|
298
|
+
}
|
|
299
|
+
const registryEntry = updateWorkspaceRegistry(relayDir, workspaceId, {
|
|
300
|
+
relaycastApiKey,
|
|
301
|
+
relayfileUrl: existing?.relayfileUrl ?? options.fallbackRelayfileUrl,
|
|
302
|
+
createdAt,
|
|
303
|
+
agentName: options.agentName,
|
|
304
|
+
});
|
|
305
|
+
return {
|
|
306
|
+
created: !requestedWorkspaceId,
|
|
307
|
+
workspaceId,
|
|
308
|
+
token: mintToken({
|
|
309
|
+
secret: signingSecret,
|
|
310
|
+
agentName: options.agentName,
|
|
311
|
+
workspace: workspaceId,
|
|
312
|
+
scopes: options.scopes,
|
|
313
|
+
}),
|
|
314
|
+
relayfileUrl: registryEntry.relayfileUrl ?? options.fallbackRelayfileUrl,
|
|
315
|
+
relaycastApiKey: registryEntry.relaycastApiKey,
|
|
316
|
+
joinCommand: buildJoinCommand(workspaceId),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
export async function requestWorkspaceSession(options) {
|
|
320
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
321
|
+
const requestedWorkspaceId = normalizeWorkspaceId(options.requestedWorkspaceId);
|
|
322
|
+
if (isLocalBaseUrl(options.authBase) && options.relayDir && options.signingSecret) {
|
|
323
|
+
return requestLocalWorkspaceSession({ ...options, fetchFn, requestedWorkspaceId });
|
|
324
|
+
}
|
|
325
|
+
if (requestedWorkspaceId) {
|
|
326
|
+
const joined = await joinWorkspaceSession(fetchFn, options.authBase, requestedWorkspaceId, options.agentName, options.scopes);
|
|
327
|
+
const relaycastApiKey = joined.relaycastApiKey ?? readWorkspaceRegistry(options.relayDir)[joined.workspaceId]?.relaycastApiKey;
|
|
328
|
+
updateWorkspaceRegistry(options.relayDir, joined.workspaceId, {
|
|
329
|
+
relaycastApiKey,
|
|
330
|
+
relayfileUrl: joined.relayfileUrl || options.fallbackRelayfileUrl,
|
|
331
|
+
agentName: options.agentName,
|
|
332
|
+
});
|
|
333
|
+
return {
|
|
334
|
+
created: false,
|
|
335
|
+
workspaceId: joined.workspaceId,
|
|
336
|
+
token: joined.token,
|
|
337
|
+
relayfileUrl: joined.relayfileUrl || options.fallbackRelayfileUrl,
|
|
338
|
+
relaycastApiKey,
|
|
339
|
+
joinCommand: joined.joinCommand,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const generatedWorkspaceId = generateWorkspaceId();
|
|
343
|
+
const createBody = {
|
|
344
|
+
workspaceId: generatedWorkspaceId,
|
|
345
|
+
};
|
|
346
|
+
if (toString(options.workspaceName)) {
|
|
347
|
+
createBody.name = options.workspaceName;
|
|
348
|
+
}
|
|
349
|
+
const created = parseCreateWorkspaceResponse(await postWorkspaceApi(fetchFn, `${options.authBase}/api/v1/workspaces/create`, createBody), generatedWorkspaceId);
|
|
350
|
+
let token = created.token;
|
|
351
|
+
let relayfileUrl = created.relayfileUrl;
|
|
352
|
+
let relaycastApiKey = created.relaycastApiKey;
|
|
353
|
+
if (!token || !relayfileUrl) {
|
|
354
|
+
const joined = await joinWorkspaceSession(fetchFn, options.authBase, created.workspaceId, options.agentName, options.scopes);
|
|
355
|
+
token = joined.token;
|
|
356
|
+
relayfileUrl = joined.relayfileUrl || relayfileUrl;
|
|
357
|
+
relaycastApiKey = relaycastApiKey ?? joined.relaycastApiKey;
|
|
358
|
+
}
|
|
359
|
+
if (!token) {
|
|
360
|
+
throw new Error(`workspace ${created.workspaceId} did not return a token`);
|
|
361
|
+
}
|
|
362
|
+
updateWorkspaceRegistry(options.relayDir, created.workspaceId, {
|
|
363
|
+
relaycastApiKey,
|
|
364
|
+
relayfileUrl: relayfileUrl || options.fallbackRelayfileUrl,
|
|
365
|
+
agentName: options.agentName,
|
|
366
|
+
});
|
|
367
|
+
return {
|
|
368
|
+
created: true,
|
|
369
|
+
workspaceId: created.workspaceId,
|
|
370
|
+
token,
|
|
371
|
+
relayfileUrl: relayfileUrl || options.fallbackRelayfileUrl,
|
|
372
|
+
relaycastApiKey,
|
|
373
|
+
joinCommand: created.joinCommand,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function normalizeAgents(rawAgents) {
|
|
377
|
+
const fallbackScopes = [
|
|
378
|
+
'relayfile:*:*:*',
|
|
379
|
+
'relayauth:*:manage:*',
|
|
380
|
+
'relayauth:*:read:*',
|
|
381
|
+
'fs:read',
|
|
382
|
+
'fs:write',
|
|
383
|
+
];
|
|
384
|
+
if (!Array.isArray(rawAgents) || rawAgents.length === 0) {
|
|
385
|
+
return [{ name: 'default-agent', scopes: fallbackScopes }];
|
|
386
|
+
}
|
|
387
|
+
const parsed = rawAgents
|
|
388
|
+
.map((entry) => toRecord(entry))
|
|
389
|
+
.map((entry, index) => ({
|
|
390
|
+
name: toString(entry.name, `agent-${index + 1}`),
|
|
391
|
+
scopes: toStringArray(entry.scopes, fallbackScopes),
|
|
392
|
+
}))
|
|
393
|
+
.filter((entry) => entry.name.length > 0);
|
|
394
|
+
return parsed.length > 0 ? parsed : [{ name: 'default-agent', scopes: fallbackScopes }];
|
|
395
|
+
}
|
|
396
|
+
function loadConfigFromFile(configPath, projectDir) {
|
|
397
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
398
|
+
let parsed;
|
|
399
|
+
if (path.extname(configPath).toLowerCase() === '.json') {
|
|
400
|
+
parsed = parseJsonConfig(raw);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
parsed = parseYamlConfig(raw);
|
|
404
|
+
}
|
|
405
|
+
const root = toRecord(parsed);
|
|
406
|
+
const payload = toRecord(root.data);
|
|
407
|
+
const fallbackWorkspace = path.basename(projectDir);
|
|
408
|
+
const workspace = toString(payload.workspace, toString(root.workspace, fallbackWorkspace));
|
|
409
|
+
const signing_secret = toString(payload.signing_secret, toString(root.signing_secret, process.env.SIGNING_KEY ?? ''));
|
|
410
|
+
if (!signing_secret) {
|
|
411
|
+
throw new Error(`relay config at ${configPath} is missing signing_secret and SIGNING_KEY env var is not set. ` +
|
|
412
|
+
'Set signing_secret in your config or export SIGNING_KEY.');
|
|
413
|
+
}
|
|
414
|
+
const agents = normalizeAgents(payload.agents ?? root.agents);
|
|
415
|
+
return { workspace, signing_secret, agents };
|
|
416
|
+
}
|
|
417
|
+
function writeGeneratedZeroConfig(configPath, projectDir, overridesAgentName) {
|
|
418
|
+
const fallbackWorkspace = path.basename(projectDir);
|
|
419
|
+
const generatedSecret = randomBytes(32).toString('hex');
|
|
420
|
+
const signingSecret = process.env.SIGNING_KEY ?? generatedSecret;
|
|
421
|
+
const defaultAgent = overridesAgentName ?? 'default-agent';
|
|
422
|
+
const config = {
|
|
423
|
+
version: '1',
|
|
424
|
+
workspace: fallbackWorkspace,
|
|
425
|
+
signing_secret: signingSecret,
|
|
426
|
+
agents: [
|
|
427
|
+
{
|
|
428
|
+
name: defaultAgent,
|
|
429
|
+
scopes: ['relayfile:*:*:*', 'relayauth:*:manage:*', 'relayauth:*:read:*', 'fs:read', 'fs:write'],
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
};
|
|
433
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
434
|
+
return config;
|
|
435
|
+
}
|
|
436
|
+
function resolveConfig(projectDir, relayDir, requestedAgent) {
|
|
437
|
+
const explicitYaml = path.join(projectDir, 'relay.yaml');
|
|
438
|
+
if (existsSync(explicitYaml)) {
|
|
439
|
+
return loadConfigFromFile(explicitYaml, projectDir);
|
|
440
|
+
}
|
|
441
|
+
const cachedConfig = path.join(relayDir, 'config.json');
|
|
442
|
+
if (existsSync(cachedConfig)) {
|
|
443
|
+
return loadConfigFromFile(cachedConfig, projectDir);
|
|
444
|
+
}
|
|
445
|
+
const generatedPath = path.join(relayDir, 'generated', 'relay-zero-config.json');
|
|
446
|
+
return writeGeneratedZeroConfig(generatedPath, projectDir, requestedAgent);
|
|
447
|
+
}
|
|
448
|
+
function isCommandAvailable(command) {
|
|
449
|
+
const checker = process.platform === 'win32' ? 'where' : 'sh';
|
|
450
|
+
const args = process.platform === 'win32' ? [command] : ['-lc', `command -v "${command}" >/dev/null 2>&1`];
|
|
451
|
+
const proc = spawnSync(checker, args, { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
452
|
+
return proc.status === 0;
|
|
453
|
+
}
|
|
454
|
+
async function waitForHttpHealthy(url, attempts = 12) {
|
|
455
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
456
|
+
try {
|
|
457
|
+
const res = await fetch(`${url}/health`);
|
|
458
|
+
if (res.ok)
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
// Retry until timeout
|
|
463
|
+
}
|
|
464
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
465
|
+
}
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
async function runCommandCapture(command, args, env) {
|
|
469
|
+
return await new Promise((resolve, reject) => {
|
|
470
|
+
const proc = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], env });
|
|
471
|
+
let output = '';
|
|
472
|
+
proc.stdout.setEncoding('utf8');
|
|
473
|
+
proc.stderr.setEncoding('utf8');
|
|
474
|
+
proc.stdout.on('data', (chunk) => {
|
|
475
|
+
output += chunk;
|
|
476
|
+
});
|
|
477
|
+
proc.stderr.on('data', (chunk) => {
|
|
478
|
+
output += chunk;
|
|
479
|
+
});
|
|
480
|
+
proc.on('error', (error) => {
|
|
481
|
+
reject(error);
|
|
482
|
+
});
|
|
483
|
+
proc.on('close', (code, signal) => {
|
|
484
|
+
if (code === 0) {
|
|
485
|
+
resolve(output);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const reason = signal ? `signal ${signal}` : `exit code ${typeof code === 'number' ? code : 'unknown'}`;
|
|
489
|
+
const detail = output.trim();
|
|
490
|
+
reject(new Error(detail || `command failed with ${reason}`));
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
function normalizeRelativePosix(filePath) {
|
|
495
|
+
return filePath.split(path.sep).join('/');
|
|
496
|
+
}
|
|
497
|
+
function globMatch(filePath, rawPattern) {
|
|
498
|
+
const pattern = normalizeRelativePosix(rawPattern.trim());
|
|
499
|
+
const target = normalizeRelativePosix(filePath);
|
|
500
|
+
if (!pattern)
|
|
501
|
+
return false;
|
|
502
|
+
if (!/[\\*?\[]/.test(pattern)) {
|
|
503
|
+
if (pattern.endsWith('/')) {
|
|
504
|
+
return target === pattern.slice(0, -1) || target.startsWith(`${pattern}`);
|
|
505
|
+
}
|
|
506
|
+
return target === pattern || target.startsWith(`${pattern}/`);
|
|
507
|
+
}
|
|
508
|
+
// Escape regex special chars, then convert glob wildcards to regex.
|
|
509
|
+
// Handle ** (double-star) before single * to avoid incorrect conversion.
|
|
510
|
+
const escaped = pattern
|
|
511
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
512
|
+
.replace(/\\\*\\\*/g, '__DOUBLESTAR__')
|
|
513
|
+
.replace(/\\\*/g, '__STAR__')
|
|
514
|
+
.replace(/\\\?/g, '__QMARK__')
|
|
515
|
+
.replace(/__DOUBLESTAR__/g, '.*')
|
|
516
|
+
.replace(/__STAR__/g, '[^/]*')
|
|
517
|
+
.replace(/__QMARK__/g, '[^/]');
|
|
518
|
+
const withDirectory = `^${escaped}$`;
|
|
519
|
+
return new RegExp(withDirectory).test(target);
|
|
520
|
+
}
|
|
521
|
+
function isPathIgnored(relPath, patterns) {
|
|
522
|
+
const normalized = normalizeRelativePosix(relPath);
|
|
523
|
+
return patterns.some((pattern) => globMatch(normalized, pattern));
|
|
524
|
+
}
|
|
525
|
+
function extractPermissionPatternsFromCompiled(compiledPath, agentName) {
|
|
526
|
+
if (!existsSync(compiledPath)) {
|
|
527
|
+
return { readonlyPatterns: [], ignoredPatterns: [] };
|
|
528
|
+
}
|
|
529
|
+
const parsed = toRecord(JSON.parse(readFileSync(compiledPath, 'utf8')));
|
|
530
|
+
const agents = Array.isArray(parsed.agents) ? parsed.agents : [];
|
|
531
|
+
const entry = agents
|
|
532
|
+
.map((raw) => toRecord(raw))
|
|
533
|
+
.find((item) => String(item.name ?? '') === agentName);
|
|
534
|
+
return {
|
|
535
|
+
readonlyPatterns: toStringArray(entry?.readonlyPatterns, []),
|
|
536
|
+
ignoredPatterns: toStringArray(entry?.ignoredPatterns, []),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function collectPermissionPatternsFromDotfiles(projectDir) {
|
|
540
|
+
const readonlyFile = path.join(projectDir, '.agentreadonly');
|
|
541
|
+
const ignoreFile = path.join(projectDir, '.agentignore');
|
|
542
|
+
return {
|
|
543
|
+
readonlyPatterns: normalizeLineList(readIfExists(readonlyFile)),
|
|
544
|
+
ignoredPatterns: normalizeLineList(readIfExists(ignoreFile)),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function readIfExists(filePath, fallback = '') {
|
|
548
|
+
if (!existsSync(filePath))
|
|
549
|
+
return fallback;
|
|
550
|
+
return readFileSync(filePath, 'utf8');
|
|
551
|
+
}
|
|
552
|
+
function buildPermissionDoc(agentName, readonlyPatterns, ignoredPatterns) {
|
|
553
|
+
const readonlyList = readonlyPatterns.length > 0 ? readonlyPatterns.join('\n') : '(none)';
|
|
554
|
+
const ignoredList = ignoredPatterns.length > 0 ? ignoredPatterns.join('\n') : '(none)';
|
|
555
|
+
return `# Workspace Permissions
|
|
556
|
+
|
|
557
|
+
This workspace is managed by the relay.
|
|
558
|
+
File access is controlled by project-local .agentignore and .agentreadonly.
|
|
559
|
+
|
|
560
|
+
## Read-only files (cannot be modified)
|
|
561
|
+
${readonlyList || '(none)'}
|
|
562
|
+
|
|
563
|
+
## Hidden files (not available in this workspace)
|
|
564
|
+
${ignoredList || '(none)'}
|
|
565
|
+
|
|
566
|
+
## Writable files
|
|
567
|
+
All other files can be read and modified freely.
|
|
568
|
+
|
|
569
|
+
If you get "permission denied", the file is read-only.
|
|
570
|
+
Changes to read-only files will be automatically reverted.
|
|
571
|
+
Do not attempt to chmod files — permissions will be restored.
|
|
572
|
+
|
|
573
|
+
Agent: ${agentName}
|
|
574
|
+
`;
|
|
575
|
+
}
|
|
576
|
+
function ensureDirectory(pathValue) {
|
|
577
|
+
mkdirSync(pathValue, { recursive: true });
|
|
578
|
+
}
|
|
579
|
+
function ensureStateDirs(relayDir) {
|
|
580
|
+
ensureDirectory(path.join(relayDir, 'tokens'));
|
|
581
|
+
ensureDirectory(path.join(relayDir, 'logs'));
|
|
582
|
+
ensureDirectory(path.join(relayDir, 'generated'));
|
|
583
|
+
ensureDirectory(path.join(relayDir, 'mounts'));
|
|
584
|
+
}
|
|
585
|
+
function findAgentConfig(config, requestedAgent) {
|
|
586
|
+
if (requestedAgent) {
|
|
587
|
+
const match = config.agents.find((agent) => agent.name === requestedAgent);
|
|
588
|
+
if (match)
|
|
589
|
+
return match;
|
|
590
|
+
const fallback = config.agents[0];
|
|
591
|
+
if (fallback) {
|
|
592
|
+
return {
|
|
593
|
+
...fallback,
|
|
594
|
+
name: requestedAgent,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
return { name: requestedAgent, scopes: ['relayfile:*:*:*'] };
|
|
598
|
+
}
|
|
599
|
+
return config.agents[0] ?? { name: 'default-agent', scopes: ['relayfile:*:*:*'] };
|
|
600
|
+
}
|
|
601
|
+
function countFilesForSync(baseDir) {
|
|
602
|
+
let total = 0;
|
|
603
|
+
const stack = [baseDir];
|
|
604
|
+
while (stack.length > 0) {
|
|
605
|
+
const current = stack.pop();
|
|
606
|
+
if (!current)
|
|
607
|
+
continue;
|
|
608
|
+
const entries = readdirSync(current, { withFileTypes: true });
|
|
609
|
+
for (const entry of entries) {
|
|
610
|
+
if (entry.name === '.relay')
|
|
611
|
+
continue;
|
|
612
|
+
const entryPath = path.join(current, entry.name);
|
|
613
|
+
if (entry.isDirectory()) {
|
|
614
|
+
stack.push(entryPath);
|
|
615
|
+
}
|
|
616
|
+
else if (entry.isFile()) {
|
|
617
|
+
total += 1;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return total;
|
|
622
|
+
}
|
|
623
|
+
function listFiles(baseDir) {
|
|
624
|
+
const files = [];
|
|
625
|
+
const stack = [baseDir];
|
|
626
|
+
while (stack.length > 0) {
|
|
627
|
+
const current = stack.pop();
|
|
628
|
+
if (!current)
|
|
629
|
+
continue;
|
|
630
|
+
const entries = readdirSync(current, { withFileTypes: true });
|
|
631
|
+
for (const entry of entries) {
|
|
632
|
+
if (entry.name === '.relay')
|
|
633
|
+
continue;
|
|
634
|
+
const entryPath = path.join(current, entry.name);
|
|
635
|
+
if (entry.isDirectory()) {
|
|
636
|
+
stack.push(entryPath);
|
|
637
|
+
}
|
|
638
|
+
else if (entry.isFile()) {
|
|
639
|
+
files.push(entryPath);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return files;
|
|
644
|
+
}
|
|
645
|
+
function hasWriteAccess(filePath) {
|
|
646
|
+
try {
|
|
647
|
+
accessSync(filePath, constants.W_OK);
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function fileNeedsSync(sourcePath, readonlyPatterns, mountDir, projectDir) {
|
|
655
|
+
const relPath = normalizeRelativePosix(path.relative(mountDir, sourcePath));
|
|
656
|
+
const absTargetPath = path.resolve(projectDir, relPath);
|
|
657
|
+
if (relPath.startsWith('../') || relPath.startsWith('.agent-relay'))
|
|
658
|
+
return false;
|
|
659
|
+
if (!absTargetPath.startsWith(projectDir + path.sep) && absTargetPath !== projectDir)
|
|
660
|
+
return false;
|
|
661
|
+
if (isPathIgnored(relPath, readonlyPatterns))
|
|
662
|
+
return false;
|
|
663
|
+
if (existsSync(absTargetPath) && hasSameContent(sourcePath, absTargetPath))
|
|
664
|
+
return false;
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
function hasSameContent(left, right) {
|
|
668
|
+
try {
|
|
669
|
+
const leftContent = readFileSync(left);
|
|
670
|
+
const rightContent = readFileSync(right);
|
|
671
|
+
return leftContent.equals(rightContent);
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async function syncWritableFilesBack(mountDir, projectDir, readonlyPatterns, ignoredPatterns) {
|
|
678
|
+
let synced = 0;
|
|
679
|
+
const realProjectDir = realpathSync(projectDir);
|
|
680
|
+
const realMountDir = realpathSync(mountDir);
|
|
681
|
+
const files = listFiles(mountDir);
|
|
682
|
+
for (const sourceFile of files) {
|
|
683
|
+
const relative = path.relative(mountDir, sourceFile);
|
|
684
|
+
if (relative === '' || relative.startsWith('..'))
|
|
685
|
+
continue;
|
|
686
|
+
if (relative === '.agent-relay' || normalizeRelativePosix(relative) === '_PERMISSIONS.md')
|
|
687
|
+
continue;
|
|
688
|
+
if (isPathIgnored(relative, readonlyPatterns))
|
|
689
|
+
continue;
|
|
690
|
+
if (isPathIgnored(relative, ignoredPatterns))
|
|
691
|
+
continue;
|
|
692
|
+
if (!hasWriteAccess(sourceFile))
|
|
693
|
+
continue;
|
|
694
|
+
if (!fileNeedsSync(sourceFile, readonlyPatterns, mountDir, projectDir)) {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
const targetPath = path.resolve(realProjectDir, relative);
|
|
698
|
+
// Guard against path traversal via symlinks: resolved target must stay within projectDir
|
|
699
|
+
if (!targetPath.startsWith(realProjectDir + path.sep) && targetPath !== realProjectDir) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
// Also verify source file resolves within the mount directory (prevents symlink-based exfiltration)
|
|
703
|
+
try {
|
|
704
|
+
const realSource = realpathSync(sourceFile);
|
|
705
|
+
if (!realSource.startsWith(realMountDir + path.sep) && realSource !== realMountDir) {
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
continue; // Skip files whose realpath cannot be resolved (broken symlinks)
|
|
711
|
+
}
|
|
712
|
+
ensureDirectory(path.dirname(targetPath));
|
|
713
|
+
cpSync(sourceFile, targetPath, { force: true });
|
|
714
|
+
synced += 1;
|
|
715
|
+
}
|
|
716
|
+
return synced;
|
|
717
|
+
}
|
|
718
|
+
function pickDeniedCount(syncOutput) {
|
|
719
|
+
const match = syncOutput.match(/skipping denied file/gi);
|
|
720
|
+
return match ? match.length : 0;
|
|
721
|
+
}
|
|
722
|
+
function generateTokenFromScript(config, agent, log, error) {
|
|
723
|
+
try {
|
|
724
|
+
return mintToken({
|
|
725
|
+
secret: config.signing_secret,
|
|
726
|
+
agentName: agent.name,
|
|
727
|
+
workspace: config.workspace,
|
|
728
|
+
scopes: agent.scopes,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
catch (err) {
|
|
732
|
+
error('Failed to mint token:', err);
|
|
733
|
+
log('Set a valid token path or provision tokens manually if token minting fails.');
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function ensureProcessRunning(processRef) {
|
|
738
|
+
return processRef.exitCode === null && !processRef.killed;
|
|
739
|
+
}
|
|
740
|
+
function isLocalBaseUrl(url) {
|
|
741
|
+
try {
|
|
742
|
+
const parsed = new URL(url);
|
|
743
|
+
return parsed.hostname === '127.0.0.1' || parsed.hostname === 'localhost';
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function killMountProcess(processRef) {
|
|
750
|
+
if (processRef.exitCode !== null || !processRef.pid)
|
|
751
|
+
return Promise.resolve();
|
|
752
|
+
processRef.kill('SIGTERM');
|
|
753
|
+
return new Promise((resolve) => {
|
|
754
|
+
const timeout = setTimeout(() => {
|
|
755
|
+
if (processRef.exitCode === null && processRef.pid) {
|
|
756
|
+
processRef.kill('SIGKILL');
|
|
757
|
+
}
|
|
758
|
+
resolve();
|
|
759
|
+
}, 1200);
|
|
760
|
+
processRef.once('exit', () => {
|
|
761
|
+
clearTimeout(timeout);
|
|
762
|
+
resolve();
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
function getSandboxFlags(cli) {
|
|
767
|
+
const name = path.basename(cli);
|
|
768
|
+
switch (name) {
|
|
769
|
+
case 'claude':
|
|
770
|
+
return ['--dangerously-skip-permissions'];
|
|
771
|
+
case 'codex':
|
|
772
|
+
return ['--dangerously-bypass-approvals-and-sandbox'];
|
|
773
|
+
case 'gemini':
|
|
774
|
+
return ['--yolo'];
|
|
775
|
+
case 'aider':
|
|
776
|
+
return ['--yes'];
|
|
777
|
+
default:
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
async function ensureProvisioned(config, agent, relayfileRoot, projectDir, tokenPath, log, error, deps) {
|
|
782
|
+
try {
|
|
783
|
+
return readFileSync(tokenPath, 'utf8').trim();
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
// Token file does not exist yet — continue to provision
|
|
787
|
+
}
|
|
788
|
+
if (typeof deps?.provision === 'function') {
|
|
789
|
+
await deps.provision(config, { ...agent });
|
|
790
|
+
try {
|
|
791
|
+
return readFileSync(tokenPath, 'utf8').trim();
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
// Token still not written — continue
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (typeof deps?.provisionAgentToken === 'function') {
|
|
798
|
+
const generated = await deps.provisionAgentToken({ config, agent, tokenPath });
|
|
799
|
+
if (typeof generated === 'string' && generated.trim()) {
|
|
800
|
+
const generatedToken = generated.trim();
|
|
801
|
+
writeFileSync(tokenPath, `${generatedToken}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
802
|
+
return generatedToken;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const generatedToken = generateTokenFromScript(config, agent, log, error);
|
|
806
|
+
if (generatedToken) {
|
|
807
|
+
ensureDirectory(path.dirname(tokenPath));
|
|
808
|
+
writeFileSync(tokenPath, `${generatedToken}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
809
|
+
return generatedToken;
|
|
810
|
+
}
|
|
811
|
+
throw new Error(`missing token for ${agent.name}: ${tokenPath}. Run provisioning before launching relay.`);
|
|
812
|
+
}
|
|
813
|
+
async function ensureServices(authBase, fileBase, deps, log, error) {
|
|
814
|
+
const needsLocalAuth = isLocalBaseUrl(authBase);
|
|
815
|
+
const needsLocalFile = isLocalBaseUrl(fileBase);
|
|
816
|
+
if (!needsLocalAuth && !needsLocalFile) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const authHealthy = !needsLocalAuth || (await waitForHttpHealthy(authBase));
|
|
820
|
+
const fileHealthy = !needsLocalFile || (await waitForHttpHealthy(fileBase));
|
|
821
|
+
if (authHealthy && fileHealthy)
|
|
822
|
+
return;
|
|
823
|
+
if (typeof deps?.ensureServicesRunning === 'function') {
|
|
824
|
+
await deps.ensureServicesRunning(authBase, fileBase);
|
|
825
|
+
const postAuthHealthy = !needsLocalAuth || (await waitForHttpHealthy(authBase));
|
|
826
|
+
const postFileHealthy = !needsLocalFile || (await waitForHttpHealthy(fileBase));
|
|
827
|
+
if (postAuthHealthy && postFileHealthy)
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (typeof deps?.startServices === 'function') {
|
|
831
|
+
await deps.startServices({ authBase, fileBase });
|
|
832
|
+
const postAuthHealthy = !needsLocalAuth || (await waitForHttpHealthy(authBase));
|
|
833
|
+
const postFileHealthy = !needsLocalFile || (await waitForHttpHealthy(fileBase));
|
|
834
|
+
if (postAuthHealthy && postFileHealthy)
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
error('Relay services are not ready.');
|
|
838
|
+
if (needsLocalAuth) {
|
|
839
|
+
error(`- relayauth (${authBase}/health): ${authHealthy ? 'healthy' : 'unhealthy'}`);
|
|
840
|
+
}
|
|
841
|
+
if (needsLocalFile) {
|
|
842
|
+
error(`- relayfile (${fileBase}/health): ${fileHealthy ? 'healthy' : 'unhealthy'}`);
|
|
843
|
+
}
|
|
844
|
+
throw new Error('Start relay services before running relay on; or pass a dependency that can launch them.');
|
|
845
|
+
}
|
|
846
|
+
async function cleanupRun(state, agentName, log) {
|
|
847
|
+
if (!state.mountProc && !state.mountDir && !state.mountLogPath)
|
|
848
|
+
return;
|
|
849
|
+
const mountDir = state.mountDir;
|
|
850
|
+
if (state.mountProc) {
|
|
851
|
+
await killMountProcess(state.mountProc);
|
|
852
|
+
}
|
|
853
|
+
let synced = 0;
|
|
854
|
+
if (mountDir && existsSync(mountDir)) {
|
|
855
|
+
synced = await syncWritableFilesBack(mountDir, state.projectDir, state.readonlyPatterns, state.ignoredPatterns);
|
|
856
|
+
log(` ✓ ${synced} file(s) synced back`);
|
|
857
|
+
try {
|
|
858
|
+
rmSync(mountDir, { recursive: true, force: true });
|
|
859
|
+
}
|
|
860
|
+
catch {
|
|
861
|
+
// Workspace cleanup is best-effort.
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
log(`Cleaned relay mount for ${agentName}`);
|
|
865
|
+
}
|
|
866
|
+
export async function goOnTheRelay(cli, options, extraArgs, deps = {}) {
|
|
867
|
+
const log = deps.log ?? ((...args) => console.log(...args));
|
|
868
|
+
const error = deps.error ?? ((...args) => console.error(...args));
|
|
869
|
+
const exit = deps.exit ?? ((code) => process.exit(code));
|
|
870
|
+
const projectDir = process.cwd();
|
|
871
|
+
const relayDir = path.join(projectDir, '.relay');
|
|
872
|
+
if (!isCommandAvailable('node') || !isCommandAvailable('npx')) {
|
|
873
|
+
throw new Error('node and npx must be available in PATH to run relay.');
|
|
874
|
+
}
|
|
875
|
+
ensureStateDirs(relayDir);
|
|
876
|
+
const defaultAgentName = toString(options.agent, path.basename(cli));
|
|
877
|
+
const config = resolveConfig(projectDir, relayDir, defaultAgentName);
|
|
878
|
+
const agent = findAgentConfig(config, defaultAgentName);
|
|
879
|
+
const authBase = normalizeBaseUrl(options.portAuth);
|
|
880
|
+
const fileBase = normalizeBaseUrl(options.portFile);
|
|
881
|
+
const mountBin = process.env.RELAYFILE_ROOT
|
|
882
|
+
? path.join(process.env.RELAYFILE_ROOT, 'bin', 'relayfile-mount')
|
|
883
|
+
: await ensureRelayfileMountBinary();
|
|
884
|
+
if (!existsSync(mountBin)) {
|
|
885
|
+
throw new Error(`missing relayfile mount binary: ${mountBin}`);
|
|
886
|
+
}
|
|
887
|
+
await ensureServices(authBase, fileBase, deps, log, error);
|
|
888
|
+
const workspaceSession = await requestWorkspaceSession({
|
|
889
|
+
authBase,
|
|
890
|
+
fallbackRelayfileUrl: fileBase,
|
|
891
|
+
requestedWorkspaceId: options.workspace,
|
|
892
|
+
workspaceName: config.workspace,
|
|
893
|
+
agentName: agent.name,
|
|
894
|
+
scopes: agent.scopes,
|
|
895
|
+
signingSecret: config.signing_secret,
|
|
896
|
+
relayDir,
|
|
897
|
+
relaycastBaseUrl: process.env.RELAYCAST_BASE_URL,
|
|
898
|
+
fetchFn: deps.fetch,
|
|
899
|
+
});
|
|
900
|
+
// Compile dotfile permissions for this agent
|
|
901
|
+
const hasDots = hasDotfiles(projectDir);
|
|
902
|
+
const dotfileAcl = hasDots ? compileDotfiles(projectDir, agent.name, workspaceSession.workspaceId) : null;
|
|
903
|
+
if (workspaceSession.created) {
|
|
904
|
+
const seedExcludes = [...DEFAULT_SEED_EXCLUDES];
|
|
905
|
+
if (dotfileAcl) {
|
|
906
|
+
// Add ignored patterns so ignored files are never uploaded
|
|
907
|
+
for (const [dir, rules] of Object.entries(dotfileAcl.acl)) {
|
|
908
|
+
if (rules.some((r) => r.startsWith('deny:agent:'))) {
|
|
909
|
+
seedExcludes.push(dir.replace(/^\//, ''));
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
await seedWorkspaceFiles(workspaceSession.relayfileUrl, workspaceSession.token, workspaceSession.workspaceId, projectDir, seedExcludes);
|
|
914
|
+
if (dotfileAcl && Object.keys(dotfileAcl.acl).length > 0) {
|
|
915
|
+
await seedAclRules(workspaceSession.relayfileUrl, workspaceSession.token, workspaceSession.workspaceId, dotfileAcl.acl);
|
|
916
|
+
// Write compiled ACL for mount to read
|
|
917
|
+
const bundlePath = path.join(relayDir, 'compiled-acl.json');
|
|
918
|
+
writeFileSync(bundlePath, JSON.stringify({
|
|
919
|
+
workspace: workspaceSession.workspaceId,
|
|
920
|
+
acl: dotfileAcl.acl,
|
|
921
|
+
summary: dotfileAcl.summary,
|
|
922
|
+
agents: [{ name: agent.name, summary: dotfileAcl.summary }],
|
|
923
|
+
}, null, 2) + '\n', { encoding: 'utf8' });
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const mountDir = path.join(relayDir, `workspace-${sanitizePathComponent(workspaceSession.workspaceId)}-${sanitizePathComponent(agent.name)}`);
|
|
927
|
+
mkdirSync(mountDir, { recursive: true });
|
|
928
|
+
const mountLogPath = path.join(relayDir, 'logs', `${agent.name}-mount.log`);
|
|
929
|
+
writeFileSync(mountLogPath, '', 'utf8');
|
|
930
|
+
const mountBaseArgs = [
|
|
931
|
+
'--base-url',
|
|
932
|
+
workspaceSession.relayfileUrl,
|
|
933
|
+
'--workspace',
|
|
934
|
+
workspaceSession.workspaceId,
|
|
935
|
+
'--local-dir',
|
|
936
|
+
mountDir,
|
|
937
|
+
];
|
|
938
|
+
const onceArgs = [...mountBaseArgs, '--once'];
|
|
939
|
+
const mountEnv = { ...process.env, RELAYFILE_TOKEN: workspaceSession.token };
|
|
940
|
+
let initialSyncOutput = '';
|
|
941
|
+
log(`Mounting workspace at ${mountDir}...`);
|
|
942
|
+
try {
|
|
943
|
+
initialSyncOutput = await runCommandCapture(mountBin, onceArgs, mountEnv);
|
|
944
|
+
}
|
|
945
|
+
catch (err) {
|
|
946
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
947
|
+
throw new Error(`initial workspace sync failed for ${agent.name}: ${message}`);
|
|
948
|
+
}
|
|
949
|
+
const deniedCount = pickDeniedCount(initialSyncOutput);
|
|
950
|
+
const compiledPath = path.join(relayDir, 'compiled-acl.json');
|
|
951
|
+
const compiled = extractPermissionPatternsFromCompiled(compiledPath, agent.name);
|
|
952
|
+
const fallback = collectPermissionPatternsFromDotfiles(projectDir);
|
|
953
|
+
const readonlyPatterns = compiled.readonlyPatterns.length > 0 ? compiled.readonlyPatterns : fallback.readonlyPatterns;
|
|
954
|
+
const ignoredPatterns = compiled.ignoredPatterns.length > 0 ? compiled.ignoredPatterns : fallback.ignoredPatterns;
|
|
955
|
+
const permsDoc = buildPermissionDoc(agent.name, readonlyPatterns, ignoredPatterns);
|
|
956
|
+
writeFileSync(path.join(mountDir, '_PERMISSIONS.md'), permsDoc, 'utf8');
|
|
957
|
+
const projectDeny = path.join(projectDir, '.agentdeny');
|
|
958
|
+
if (existsSync(projectDeny)) {
|
|
959
|
+
cpSync(projectDeny, path.join(mountDir, '.agentdeny'), { force: true });
|
|
960
|
+
}
|
|
961
|
+
const mountedFiles = countFilesForSync(mountDir);
|
|
962
|
+
log(`On the relay as ${agent.name}`);
|
|
963
|
+
log(` Workspace: ${workspaceSession.workspaceId}`);
|
|
964
|
+
log(` Join: ${workspaceSession.joinCommand}`);
|
|
965
|
+
log(` Mounted files: ${mountedFiles}`);
|
|
966
|
+
log(` Permissions denied (initial sync): ${deniedCount}`);
|
|
967
|
+
const sandboxFlags = getSandboxFlags(cli);
|
|
968
|
+
if (sandboxFlags.length > 0) {
|
|
969
|
+
log(` Sandbox: relay-enforced (${sandboxFlags.join(' ')})`);
|
|
970
|
+
log(` ⚠ Agent CLI sandbox bypassed — relay file permissions are the only safety layer`);
|
|
971
|
+
}
|
|
972
|
+
const cleanupState = {
|
|
973
|
+
mountDir,
|
|
974
|
+
mountLogPath,
|
|
975
|
+
projectDir,
|
|
976
|
+
relayDir,
|
|
977
|
+
workspace: workspaceSession.workspaceId,
|
|
978
|
+
readonlyPatterns,
|
|
979
|
+
ignoredPatterns,
|
|
980
|
+
};
|
|
981
|
+
let mountProc;
|
|
982
|
+
let agentProc;
|
|
983
|
+
let cleanupDone = false;
|
|
984
|
+
const finalizeCleanup = async () => {
|
|
985
|
+
if (cleanupDone)
|
|
986
|
+
return;
|
|
987
|
+
cleanupDone = true;
|
|
988
|
+
cleanupState.mountProc = mountProc;
|
|
989
|
+
await cleanupRun(cleanupState, agent.name, log);
|
|
990
|
+
};
|
|
991
|
+
try {
|
|
992
|
+
const mountedProc = spawn(mountBin, mountBaseArgs, {
|
|
993
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
994
|
+
env: mountEnv,
|
|
995
|
+
});
|
|
996
|
+
mountProc = mountedProc;
|
|
997
|
+
mountedProc.stdout.on('data', (chunk) => {
|
|
998
|
+
appendFileSync(mountLogPath, chunk);
|
|
999
|
+
});
|
|
1000
|
+
mountedProc.stderr.on('data', (chunk) => {
|
|
1001
|
+
appendFileSync(mountLogPath, chunk);
|
|
1002
|
+
});
|
|
1003
|
+
await new Promise((resolve, reject) => {
|
|
1004
|
+
const timer = setTimeout(() => resolve(), 600);
|
|
1005
|
+
mountedProc.on('error', (spawnError) => {
|
|
1006
|
+
clearTimeout(timer);
|
|
1007
|
+
reject(spawnError);
|
|
1008
|
+
});
|
|
1009
|
+
mountedProc.on('spawn', () => {
|
|
1010
|
+
clearTimeout(timer);
|
|
1011
|
+
resolve();
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
if (!ensureProcessRunning(mountedProc)) {
|
|
1015
|
+
throw new Error(`mount process for ${agent.name} exited before continuing`);
|
|
1016
|
+
}
|
|
1017
|
+
cleanupState.mountProc = mountProc;
|
|
1018
|
+
let agentExitCode = 0;
|
|
1019
|
+
await new Promise((resolve, reject) => {
|
|
1020
|
+
const envVars = {
|
|
1021
|
+
...process.env,
|
|
1022
|
+
RELAYFILE_TOKEN: workspaceSession.token,
|
|
1023
|
+
RELAYFILE_BASE_URL: workspaceSession.relayfileUrl,
|
|
1024
|
+
RELAYFILE_WORKSPACE: workspaceSession.workspaceId,
|
|
1025
|
+
RELAY_WORKSPACE_ID: workspaceSession.workspaceId,
|
|
1026
|
+
RELAY_DEFAULT_WORKSPACE: workspaceSession.workspaceId,
|
|
1027
|
+
RELAY_WORKSPACE: mountDir,
|
|
1028
|
+
RELAY_AGENT_NAME: agent.name,
|
|
1029
|
+
...(workspaceSession.relaycastApiKey
|
|
1030
|
+
? {
|
|
1031
|
+
RELAY_API_KEY: workspaceSession.relaycastApiKey,
|
|
1032
|
+
RELAY_WORKSPACES_JSON: JSON.stringify([
|
|
1033
|
+
{
|
|
1034
|
+
workspace_id: workspaceSession.workspaceId,
|
|
1035
|
+
api_key: workspaceSession.relaycastApiKey,
|
|
1036
|
+
},
|
|
1037
|
+
]),
|
|
1038
|
+
}
|
|
1039
|
+
: {}),
|
|
1040
|
+
};
|
|
1041
|
+
agentProc = spawn(cli, [...sandboxFlags, ...extraArgs], {
|
|
1042
|
+
cwd: mountDir,
|
|
1043
|
+
stdio: 'inherit',
|
|
1044
|
+
env: envVars,
|
|
1045
|
+
});
|
|
1046
|
+
let cleanupInProgress;
|
|
1047
|
+
const cleanupHook = () => {
|
|
1048
|
+
if (agentProc && !agentProc.killed) {
|
|
1049
|
+
agentProc.kill('SIGTERM');
|
|
1050
|
+
}
|
|
1051
|
+
// Wait for the agent process to exit so agentExitCode is set by the close handler,
|
|
1052
|
+
// then ensure cleanup completes before resolving — avoids data loss from premature exit
|
|
1053
|
+
cleanupInProgress = new Promise((r) => {
|
|
1054
|
+
if (!agentProc || agentProc.exitCode !== null) {
|
|
1055
|
+
r();
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const t = setTimeout(r, 2000);
|
|
1059
|
+
agentProc.once('close', () => {
|
|
1060
|
+
clearTimeout(t);
|
|
1061
|
+
r();
|
|
1062
|
+
});
|
|
1063
|
+
})
|
|
1064
|
+
.then(() => finalizeCleanup())
|
|
1065
|
+
.then(() => resolve());
|
|
1066
|
+
};
|
|
1067
|
+
process.once('SIGINT', cleanupHook);
|
|
1068
|
+
process.once('SIGTERM', cleanupHook);
|
|
1069
|
+
agentProc.on('error', (err) => {
|
|
1070
|
+
process.removeListener('SIGINT', cleanupHook);
|
|
1071
|
+
process.removeListener('SIGTERM', cleanupHook);
|
|
1072
|
+
reject(err);
|
|
1073
|
+
});
|
|
1074
|
+
agentProc.on('close', (code, signal) => {
|
|
1075
|
+
process.removeListener('SIGINT', cleanupHook);
|
|
1076
|
+
process.removeListener('SIGTERM', cleanupHook);
|
|
1077
|
+
if (typeof code === 'number') {
|
|
1078
|
+
agentExitCode = code;
|
|
1079
|
+
}
|
|
1080
|
+
else if (signal === 'SIGINT') {
|
|
1081
|
+
agentExitCode = 130;
|
|
1082
|
+
}
|
|
1083
|
+
else if (signal === 'SIGTERM') {
|
|
1084
|
+
agentExitCode = 143;
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
agentExitCode = 1;
|
|
1088
|
+
}
|
|
1089
|
+
// If cleanup was triggered by a signal, wait for it to finish
|
|
1090
|
+
if (cleanupInProgress) {
|
|
1091
|
+
cleanupInProgress.then(() => resolve());
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
resolve();
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
// Finalization happens in outer finally.
|
|
1098
|
+
});
|
|
1099
|
+
await finalizeCleanup();
|
|
1100
|
+
log('Off the relay.');
|
|
1101
|
+
exit(agentExitCode);
|
|
1102
|
+
}
|
|
1103
|
+
finally {
|
|
1104
|
+
await finalizeCleanup();
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
//# sourceMappingURL=start.js.map
|