@shogo-ai/worker 1.7.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.
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/bin/shogo.mjs +38 -0
- package/package.json +56 -0
- package/src/cli.ts +127 -0
- package/src/commands/config.ts +29 -0
- package/src/commands/login.ts +126 -0
- package/src/commands/logs.ts +24 -0
- package/src/commands/runtime.ts +104 -0
- package/src/commands/start.ts +252 -0
- package/src/commands/status.ts +22 -0
- package/src/commands/stop.ts +13 -0
- package/src/lib/__tests__/cloud-login.test.ts +218 -0
- package/src/lib/__tests__/config.test.ts +136 -0
- package/src/lib/__tests__/runtime-resolver.test.ts +112 -0
- package/src/lib/api-discovery.ts +36 -0
- package/src/lib/cloud-login.ts +321 -0
- package/src/lib/config.ts +63 -0
- package/src/lib/device-id.ts +27 -0
- package/src/lib/paths.ts +35 -0
- package/src/lib/preflight.ts +158 -0
- package/src/lib/process-manager.ts +123 -0
- package/src/lib/runtime-install.ts +371 -0
- package/src/lib/runtime-manager.ts +645 -0
- package/src/lib/runtime-resolver.ts +136 -0
- package/src/lib/transport.ts +202 -0
- package/src/lib/tunnel.ts +664 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (C) 2026 Shogo Technologies, Inc.
|
|
3
|
+
/**
|
|
4
|
+
* Locates the agent-runtime binary on disk.
|
|
5
|
+
*
|
|
6
|
+
* License boundary: this file does NOT import `@shogo/agent-runtime` —
|
|
7
|
+
* the worker (MIT) discovers and spawns the AGPL runtime binary as a
|
|
8
|
+
* separate OS process. No library link, no dynamic-import, no embed.
|
|
9
|
+
*
|
|
10
|
+
* Resolution priority (first hit wins):
|
|
11
|
+
*
|
|
12
|
+
* 1. `--runtime-bin <path>` CLI flag (explicit override; dev/monorepo).
|
|
13
|
+
* 2. `SHOGO_AGENT_RUNTIME_BIN` env var (CI / deterministic deploys).
|
|
14
|
+
* 3. `~/.shogo/runtime/agent-runtime` (default; installed by
|
|
15
|
+
* `shogo runtime install`).
|
|
16
|
+
* 4. `which shogo-agent-runtime` on PATH (system-wide install via OS
|
|
17
|
+
* package manager — future).
|
|
18
|
+
*
|
|
19
|
+
* Each candidate is checked for existence + executability. The resolver
|
|
20
|
+
* returns a structured result that includes which strategy hit so the
|
|
21
|
+
* CLI can surface it in `shogo runtime where`.
|
|
22
|
+
*/
|
|
23
|
+
import { accessSync, constants as fsConstants, existsSync } from 'node:fs';
|
|
24
|
+
import { delimiter, join } from 'node:path';
|
|
25
|
+
import { RUNTIME_BIN } from './paths.ts';
|
|
26
|
+
|
|
27
|
+
export type RuntimeSource = 'flag' | 'env' | 'home' | 'path' | 'none';
|
|
28
|
+
|
|
29
|
+
export interface ResolvedRuntime {
|
|
30
|
+
/** Absolute path to the agent-runtime binary. */
|
|
31
|
+
path: string;
|
|
32
|
+
/** Which resolution strategy succeeded. */
|
|
33
|
+
source: RuntimeSource;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RuntimeResolveOptions {
|
|
37
|
+
/** Value of the `--runtime-bin` CLI flag, if any. */
|
|
38
|
+
flag?: string;
|
|
39
|
+
/** Override the env (for tests). Defaults to `process.env`. */
|
|
40
|
+
env?: NodeJS.ProcessEnv;
|
|
41
|
+
/** Override platform-derived bin name (for tests). */
|
|
42
|
+
systemBinName?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the runtime binary path. Returns null if no candidate exists.
|
|
47
|
+
*
|
|
48
|
+
* The caller (typically `shogo worker start` / `shogo runtime where`) is
|
|
49
|
+
* responsible for surfacing a friendly error when null — see
|
|
50
|
+
* `formatMissingRuntimeError()` below.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveRuntime(opts: RuntimeResolveOptions = {}): ResolvedRuntime | null {
|
|
53
|
+
const env = opts.env ?? process.env;
|
|
54
|
+
const candidates = enumerateCandidates(opts);
|
|
55
|
+
for (const c of candidates) {
|
|
56
|
+
if (isExecutableFile(c.path)) return c;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const systemBinName = opts.systemBinName ?? defaultSystemBinName();
|
|
60
|
+
const onPath = findOnPath(systemBinName, env.PATH);
|
|
61
|
+
if (onPath) return { path: onPath, source: 'path' };
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build the ordered list of explicit-path candidates (sources 1-3).
|
|
68
|
+
* Source 4 (PATH search) is handled separately because it requires
|
|
69
|
+
* scanning the PATH env.
|
|
70
|
+
*/
|
|
71
|
+
function enumerateCandidates(opts: RuntimeResolveOptions): ResolvedRuntime[] {
|
|
72
|
+
const env = opts.env ?? process.env;
|
|
73
|
+
const out: ResolvedRuntime[] = [];
|
|
74
|
+
|
|
75
|
+
if (opts.flag && opts.flag.trim()) {
|
|
76
|
+
out.push({ path: opts.flag.trim(), source: 'flag' });
|
|
77
|
+
}
|
|
78
|
+
const envPath = env.SHOGO_AGENT_RUNTIME_BIN?.trim();
|
|
79
|
+
if (envPath) {
|
|
80
|
+
out.push({ path: envPath, source: 'env' });
|
|
81
|
+
}
|
|
82
|
+
out.push({ path: RUNTIME_BIN, source: 'home' });
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function defaultSystemBinName(): string {
|
|
87
|
+
return process.platform === 'win32' ? 'shogo-agent-runtime.exe' : 'shogo-agent-runtime';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isExecutableFile(p: string): boolean {
|
|
91
|
+
if (!existsSync(p)) return false;
|
|
92
|
+
try {
|
|
93
|
+
if (process.platform === 'win32') {
|
|
94
|
+
// Windows: existence is sufficient — the OS resolves PATHEXT for us.
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
accessSync(p, fsConstants.X_OK);
|
|
98
|
+
return true;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function findOnPath(binName: string, pathEnv: string | undefined): string | null {
|
|
105
|
+
if (!pathEnv) return null;
|
|
106
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
107
|
+
if (!dir) continue;
|
|
108
|
+
const candidate = join(dir, binName);
|
|
109
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Format the friendly missing-binary message used by `shogo worker start`
|
|
116
|
+
* and `shogo runtime where` when no candidate resolves.
|
|
117
|
+
*
|
|
118
|
+
* Surfaces the full priority chain so the user can see exactly which
|
|
119
|
+
* paths were checked and pick the right next step.
|
|
120
|
+
*/
|
|
121
|
+
export function formatMissingRuntimeError(opts: RuntimeResolveOptions = {}): string {
|
|
122
|
+
const env = opts.env ?? process.env;
|
|
123
|
+
const lines: string[] = [];
|
|
124
|
+
lines.push('Error: agent-runtime binary not found.');
|
|
125
|
+
lines.push('');
|
|
126
|
+
lines.push('Looked in (priority order):');
|
|
127
|
+
if (opts.flag) lines.push(` --runtime-bin ${opts.flag}`);
|
|
128
|
+
if (env.SHOGO_AGENT_RUNTIME_BIN) lines.push(` $SHOGO_AGENT_RUNTIME_BIN = ${env.SHOGO_AGENT_RUNTIME_BIN}`);
|
|
129
|
+
lines.push(` ${RUNTIME_BIN} (default install location)`);
|
|
130
|
+
lines.push(` ${defaultSystemBinName()} on \$PATH`);
|
|
131
|
+
lines.push('');
|
|
132
|
+
lines.push('Fix:');
|
|
133
|
+
lines.push(' - Run `shogo runtime install` to download the latest binary, or');
|
|
134
|
+
lines.push(' - Pass `--runtime-bin <path>` to point at an existing build.');
|
|
135
|
+
return lines.join('\n');
|
|
136
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (C) 2026 Shogo Technologies, Inc.
|
|
3
|
+
/**
|
|
4
|
+
* Corporate-proxy support + allowlist host derivation for the Shogo Worker.
|
|
5
|
+
*
|
|
6
|
+
* Reads HTTPS_PROXY / https_proxy / HTTP_PROXY / http_proxy from the
|
|
7
|
+
* environment and returns a normalized proxy URL (or null if not set).
|
|
8
|
+
*
|
|
9
|
+
* The CLI does NOT install a global undici dispatcher on the CLI process
|
|
10
|
+
* itself — the CLI only spawns the worker subprocess. Instead we forward
|
|
11
|
+
* the env to the child; the child's Node runtime picks it up automatically
|
|
12
|
+
* (undici >= 5.29 honours HTTPS_PROXY via getGlobalDispatcher).
|
|
13
|
+
*
|
|
14
|
+
* We also offer a reachability probe so `--debug` preflight can verify the
|
|
15
|
+
* proxy before spinning anything up. The probe uses a real CONNECT request
|
|
16
|
+
* to the target host on :443, which is exactly what the worker's TLS
|
|
17
|
+
* traffic needs — a plain GET to the proxy root can pass even when the
|
|
18
|
+
* proxy is misconfigured for CONNECT.
|
|
19
|
+
*/
|
|
20
|
+
import { request as httpRequest } from "node:http";
|
|
21
|
+
|
|
22
|
+
export interface ProxyConfig {
|
|
23
|
+
/** Normalized proxy URL (scheme://host:port). */
|
|
24
|
+
url: string;
|
|
25
|
+
/** Which env var (or flag) it came from — for debug output. */
|
|
26
|
+
source:
|
|
27
|
+
| "flag"
|
|
28
|
+
| "HTTPS_PROXY"
|
|
29
|
+
| "https_proxy"
|
|
30
|
+
| "HTTP_PROXY"
|
|
31
|
+
| "http_proxy";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the effective proxy from a --proxy override + env.
|
|
36
|
+
* Precedence: flag > HTTPS_PROXY > https_proxy > HTTP_PROXY > http_proxy.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveProxy(flag?: string, env: NodeJS.ProcessEnv = process.env): ProxyConfig | null {
|
|
39
|
+
const trimmed = (s: string | undefined) => (s && s.trim().length > 0 ? s.trim() : undefined);
|
|
40
|
+
const viaFlag = trimmed(flag);
|
|
41
|
+
if (viaFlag) return { url: normalize(viaFlag), source: "flag" };
|
|
42
|
+
|
|
43
|
+
const candidates: [ProxyConfig["source"], string | undefined][] = [
|
|
44
|
+
["HTTPS_PROXY", env.HTTPS_PROXY],
|
|
45
|
+
["https_proxy", env.https_proxy],
|
|
46
|
+
["HTTP_PROXY", env.HTTP_PROXY],
|
|
47
|
+
["http_proxy", env.http_proxy],
|
|
48
|
+
];
|
|
49
|
+
for (const [source, value] of candidates) {
|
|
50
|
+
const v = trimmed(value);
|
|
51
|
+
if (v) return { url: normalize(v), source };
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Normalize bare host:port into scheme-prefixed URL. */
|
|
57
|
+
function normalize(raw: string): string {
|
|
58
|
+
if (/^https?:\/\//i.test(raw)) return raw;
|
|
59
|
+
return `http://${raw}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Inject the proxy into a child process env so the spawned worker uses it.
|
|
64
|
+
* Sets both upper- and lower-case variants for maximum compatibility.
|
|
65
|
+
*/
|
|
66
|
+
export function applyProxyToEnv(env: NodeJS.ProcessEnv, proxy: ProxyConfig | null): NodeJS.ProcessEnv {
|
|
67
|
+
if (!proxy) return env;
|
|
68
|
+
return {
|
|
69
|
+
...env,
|
|
70
|
+
HTTPS_PROXY: env.HTTPS_PROXY ?? proxy.url,
|
|
71
|
+
https_proxy: env.https_proxy ?? proxy.url,
|
|
72
|
+
HTTP_PROXY: env.HTTP_PROXY ?? proxy.url,
|
|
73
|
+
http_proxy: env.http_proxy ?? proxy.url,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Probe the proxy by issuing an HTTP CONNECT to `targetHost:443`.
|
|
79
|
+
* This mirrors what the worker's TLS traffic will actually do, so it catches
|
|
80
|
+
* proxies that answer :80 but reject CONNECT tunneling.
|
|
81
|
+
*
|
|
82
|
+
* Accepts 200/407 as "proxy is answering":
|
|
83
|
+
* - 200 → tunnel established.
|
|
84
|
+
* - 407 → proxy requires authentication (still proves reachability; surfaces
|
|
85
|
+
* an actionable error on stdout).
|
|
86
|
+
* Network errors fail the probe.
|
|
87
|
+
*/
|
|
88
|
+
export async function probeProxy(
|
|
89
|
+
proxy: ProxyConfig,
|
|
90
|
+
targetHost = "api.shogo.ai",
|
|
91
|
+
timeoutMs = 5000,
|
|
92
|
+
): Promise<{ ok: boolean; detail: string }> {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
let settled = false;
|
|
95
|
+
const settle = (result: { ok: boolean; detail: string }) => {
|
|
96
|
+
if (settled) return;
|
|
97
|
+
settled = true;
|
|
98
|
+
resolve(result);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const u = new URL(proxy.url);
|
|
103
|
+
const proxyHost = u.hostname;
|
|
104
|
+
const proxyPort = u.port ? parseInt(u.port, 10) : u.protocol === "https:" ? 443 : 80;
|
|
105
|
+
|
|
106
|
+
const req = httpRequest({
|
|
107
|
+
method: "CONNECT",
|
|
108
|
+
host: proxyHost,
|
|
109
|
+
port: proxyPort,
|
|
110
|
+
path: `${targetHost}:443`,
|
|
111
|
+
timeout: timeoutMs,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
req.on("connect", (res) => {
|
|
115
|
+
req.destroy();
|
|
116
|
+
const status = res.statusCode ?? 0;
|
|
117
|
+
if (status === 200) {
|
|
118
|
+
settle({ ok: true, detail: `CONNECT ${targetHost}:443 → 200 (via ${proxyHost}:${proxyPort})` });
|
|
119
|
+
} else if (status === 407) {
|
|
120
|
+
settle({
|
|
121
|
+
ok: false,
|
|
122
|
+
detail: `407 Proxy Authentication Required — check credentials in ${proxy.source}`,
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
settle({ ok: false, detail: `CONNECT → HTTP ${status}` });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
req.on("timeout", () => {
|
|
130
|
+
req.destroy();
|
|
131
|
+
settle({ ok: false, detail: `timeout after ${timeoutMs}ms` });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
req.on("error", (err: NodeJS.ErrnoException) => {
|
|
135
|
+
const code = err.code ? ` (${err.code})` : "";
|
|
136
|
+
settle({ ok: false, detail: `${err.message}${code}` });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
req.end();
|
|
140
|
+
} catch (err: any) {
|
|
141
|
+
settle({ ok: false, detail: err?.message ?? "unknown error" });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Derive the full outbound allowlist (3 hosts) from a single cloudUrl.
|
|
148
|
+
*
|
|
149
|
+
* Shogo Worker talks to three hosts, as documented in
|
|
150
|
+
* `docs/my-machines-networking.md`:
|
|
151
|
+
*
|
|
152
|
+
* 1. <cloud> — session control plane (FATAL if blocked)
|
|
153
|
+
* 2. <cloud>-direct (or api-direct.<rootDomain>) — WS tunnel fallback
|
|
154
|
+
* 3. artifacts.<rootDomain> — artifact uploads (graceful if blocked)
|
|
155
|
+
*
|
|
156
|
+
* The rule for #2/#3 is: take the rootDomain of the cloud URL and prefix it.
|
|
157
|
+
* This supports region-pinned deploys (EU, US) without hardcoding.
|
|
158
|
+
*/
|
|
159
|
+
export interface AllowlistHost {
|
|
160
|
+
url: string;
|
|
161
|
+
host: string;
|
|
162
|
+
purpose: "control" | "tunnel-direct" | "artifacts";
|
|
163
|
+
criticality: "fatal" | "graceful";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function deriveAllowlist(cloudUrl: string): AllowlistHost[] {
|
|
167
|
+
let u: URL;
|
|
168
|
+
try {
|
|
169
|
+
u = new URL(cloudUrl);
|
|
170
|
+
} catch {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const host = u.host; // e.g. "studio.shogo.ai"
|
|
175
|
+
const scheme = u.protocol.replace(":", "") || "https";
|
|
176
|
+
|
|
177
|
+
// rootDomain = last two dotted labels — "shogo.ai" from "studio.shogo.ai".
|
|
178
|
+
// If the host is already 2 labels (e.g. "shogo.ai"), keep it as-is.
|
|
179
|
+
const parts = u.hostname.split(".");
|
|
180
|
+
const rootDomain = parts.length >= 2 ? parts.slice(-2).join(".") : u.hostname;
|
|
181
|
+
|
|
182
|
+
return [
|
|
183
|
+
{
|
|
184
|
+
url: `${scheme}://${host}`,
|
|
185
|
+
host,
|
|
186
|
+
purpose: "control",
|
|
187
|
+
criticality: "fatal",
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
url: `${scheme}://api-direct.${rootDomain}`,
|
|
191
|
+
host: `api-direct.${rootDomain}`,
|
|
192
|
+
purpose: "tunnel-direct",
|
|
193
|
+
criticality: "graceful",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
url: `${scheme}://artifacts.${rootDomain}`,
|
|
197
|
+
host: `artifacts.${rootDomain}`,
|
|
198
|
+
purpose: "artifacts",
|
|
199
|
+
criticality: "graceful",
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
}
|