@almyty/runner 1.5.3 → 1.5.5

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.
@@ -0,0 +1,202 @@
1
+ /**
2
+ * First start of a hosted workspace: clone the environment's repository
3
+ * and run its setup script, once per environment version.
4
+ *
5
+ * Runs in enroll mode after the runner has its credential and before it
6
+ * opens its stream, so the machine reports ready only once the checkout
7
+ * and setup are in place (docs/hosted-runners.md, "Images and enroll
8
+ * mode"). The design's rule: on the first start of a volume, or when the
9
+ * environment version changed, clone `repo` at `ref`, run `setupScript`,
10
+ * and write `<workspace>/.almyty/env-version`.
11
+ *
12
+ * The inputs are the plain variables the Deployment sets
13
+ * (ALMYTY_WORKSPACE_ROOT, ALMYTY_ENVIRONMENT_VERSION, ALMYTY_REPO_URL,
14
+ * ALMYTY_REPO_REF, ALMYTY_SETUP_SCRIPT) and one from the Secret,
15
+ * ALMYTY_GIT_TOKEN. The git token reaches git through GIT_CONFIG_*
16
+ * variables of that one child process: never on its command line, never
17
+ * in a file, never in the clone's remote URL.
18
+ *
19
+ * A failed clone or setup is reported and the version is not recorded, so
20
+ * the next start tries again; the runner still comes online, so the
21
+ * person can look at what went wrong from a shell instead of watching a
22
+ * pod restart in a loop.
23
+ */
24
+ import { spawn } from 'node:child_process';
25
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
26
+ import { join } from 'node:path';
27
+ /** Where the volume is mounted when the Deployment does not say. */
28
+ export const DEFAULT_WORKSPACE_ROOT = '/workspace';
29
+ /** The checkout's folder inside the workspace. */
30
+ export const REPO_DIR_NAME = 'repo';
31
+ /** A setup script running longer than this is stopped. Override: ALMYTY_SETUP_TIMEOUT_SECONDS. */
32
+ const DEFAULT_SETUP_TIMEOUT_SECONDS = 1800;
33
+ export function workspaceRoot(env) {
34
+ const root = (env.ALMYTY_WORKSPACE_ROOT ?? '').trim();
35
+ return root.startsWith('/') ? root : DEFAULT_WORKSPACE_ROOT;
36
+ }
37
+ export async function prepareHostedWorkspace(inputs = {}) {
38
+ const env = inputs.env ?? process.env;
39
+ const run = inputs.run ?? runCommand;
40
+ const log = inputs.log ?? ((line) => process.stdout.write(`${line}\n`));
41
+ const warn = inputs.warn ?? ((line) => process.stderr.write(`${line}\n`));
42
+ const root = workspaceRoot(env);
43
+ const stateDir = join(root, '.almyty');
44
+ const versionFile = join(stateDir, 'env-version');
45
+ const version = (env.ALMYTY_ENVIRONMENT_VERSION ?? '').trim() || 'unversioned';
46
+ const repoUrl = (env.ALMYTY_REPO_URL ?? '').trim();
47
+ const repoDir = join(root, REPO_DIR_NAME);
48
+ // An inherited workspace is kept read-only for its new owner to copy
49
+ // from (the volume is mounted read-only): nothing is set up in it.
50
+ if ((env.ALMYTY_WORKSPACE_READ_ONLY ?? '').trim() === 'true') {
51
+ log('workspace: read-only (inherited); nothing is set up');
52
+ return { upToDate: true, cloned: false, setupRan: false, ok: true, repoDir: repoUrl && existsSync(repoDir) ? repoDir : null };
53
+ }
54
+ const recorded = existsSync(versionFile) ? readFileSync(versionFile, 'utf-8').trim() : null;
55
+ if (recorded === version) {
56
+ log(`workspace: environment version ${version} already set up`);
57
+ return { upToDate: true, cloned: false, setupRan: false, ok: true, repoDir: repoUrl ? repoDir : null };
58
+ }
59
+ mkdirSync(stateDir, { recursive: true, mode: 0o700 });
60
+ // A spawned step gets the runner's own variables (the environment's
61
+ // bindings among them); the enrollment token is already gone (enroll.ts).
62
+ const childEnv = { ...env, GIT_TERMINAL_PROMPT: '0' };
63
+ let ok = true;
64
+ let cloned = false;
65
+ if (repoUrl) {
66
+ if (existsSync(join(repoDir, '.git'))) {
67
+ // An existing checkout is the person's work; a new version does not
68
+ // overwrite it. Only the setup script runs again.
69
+ log(`workspace: ${repoDir} already holds a checkout; leaving it as it is`);
70
+ }
71
+ else {
72
+ const result = await cloneRepo({ url: repoUrl, ref: (env.ALMYTY_REPO_REF ?? '').trim(), dir: repoDir, token: env.ALMYTY_GIT_TOKEN, env: childEnv, cwd: root, run, log });
73
+ cloned = result;
74
+ if (!result) {
75
+ ok = false;
76
+ warn(`workspace: cloning ${redactUrl(repoUrl)} failed; it is tried again on the next start`);
77
+ }
78
+ }
79
+ }
80
+ const script = env.ALMYTY_SETUP_SCRIPT ?? '';
81
+ let setupRan = false;
82
+ if (script.trim() && ok) {
83
+ setupRan = true;
84
+ const tmp = inputs.tmpDir ?? '/tmp';
85
+ const file = join(tmp, `almyty-setup-${process.pid}-${Date.now()}.sh`);
86
+ writeFileSync(file, script, { mode: 0o700 });
87
+ const cwd = repoUrl && existsSync(repoDir) ? repoDir : root;
88
+ const timeoutS = positiveInt(env.ALMYTY_SETUP_TIMEOUT_SECONDS) ?? DEFAULT_SETUP_TIMEOUT_SECONDS;
89
+ log(`workspace: running the setup script in ${cwd}`);
90
+ try {
91
+ const res = await run('bash', [file], { cwd, env: childEnv, timeoutMs: timeoutS * 1000 });
92
+ if (res.code !== 0) {
93
+ ok = false;
94
+ warn(res.timedOut
95
+ ? `workspace: the setup script ran past ${timeoutS}s and was stopped; it runs again on the next start`
96
+ : `workspace: the setup script exited ${res.code}; it runs again on the next start`);
97
+ }
98
+ }
99
+ finally {
100
+ rmSync(file, { force: true });
101
+ }
102
+ }
103
+ if (ok) {
104
+ writeFileSync(versionFile, `${version}\n`, { mode: 0o600 });
105
+ log(`workspace: environment version ${version} set up`);
106
+ }
107
+ return { upToDate: false, cloned, setupRan, ok, repoDir: repoUrl ? repoDir : null };
108
+ }
109
+ async function cloneRepo(o) {
110
+ const env = { ...o.env, ...gitAuthEnv(o.url, o.token, o.env) };
111
+ o.log(`workspace: cloning ${redactUrl(o.url)}${o.ref ? ` at ${o.ref}` : ''} into ${o.dir}`);
112
+ const clone = await o.run('git', ['clone', '--', o.url, o.dir], { cwd: o.cwd, env });
113
+ if (clone.code !== 0) {
114
+ rmSync(o.dir, { recursive: true, force: true });
115
+ return false;
116
+ }
117
+ if (!o.ref)
118
+ return true;
119
+ // A branch or tag checks out directly; a commit or another ref may need
120
+ // fetching first.
121
+ const checkout = await o.run('git', ['-C', o.dir, 'checkout', o.ref, '--'], { cwd: o.cwd, env });
122
+ if (checkout.code === 0)
123
+ return true;
124
+ const fetch = await o.run('git', ['-C', o.dir, 'fetch', 'origin', o.ref], { cwd: o.cwd, env });
125
+ if (fetch.code !== 0)
126
+ return false;
127
+ const detached = await o.run('git', ['-C', o.dir, 'checkout', 'FETCH_HEAD', '--'], { cwd: o.cwd, env });
128
+ return detached.code === 0;
129
+ }
130
+ /**
131
+ * The token as an HTTP Authorization header for the repository's origin,
132
+ * passed through git's GIT_CONFIG_COUNT/KEY/VALUE variables (git 2.31+).
133
+ * Empty for no token or a URL that is not https.
134
+ */
135
+ export function gitAuthEnv(url, token, base = {}) {
136
+ if (!token)
137
+ return {};
138
+ let origin;
139
+ try {
140
+ const u = new URL(url);
141
+ if (u.protocol !== 'https:')
142
+ return {};
143
+ origin = `${u.protocol}//${u.host}/`;
144
+ }
145
+ catch {
146
+ return {};
147
+ }
148
+ const n = positiveInt(base.GIT_CONFIG_COUNT) ?? 0;
149
+ const basic = Buffer.from(`x-access-token:${token}`).toString('base64');
150
+ return {
151
+ GIT_CONFIG_COUNT: String(n + 1),
152
+ [`GIT_CONFIG_KEY_${n}`]: `http.${origin}.extraHeader`,
153
+ [`GIT_CONFIG_VALUE_${n}`]: `Authorization: Basic ${basic}`,
154
+ };
155
+ }
156
+ /** A URL with any user:password part removed, for log lines. */
157
+ export function redactUrl(url) {
158
+ try {
159
+ const u = new URL(url);
160
+ if (u.username || u.password) {
161
+ u.username = '';
162
+ u.password = '';
163
+ }
164
+ return u.toString();
165
+ }
166
+ catch {
167
+ return url.replace(/\/\/[^@/]*@/, '//');
168
+ }
169
+ }
170
+ function positiveInt(value) {
171
+ const n = Number((value ?? '').trim());
172
+ return Number.isInteger(n) && n > 0 ? n : undefined;
173
+ }
174
+ /** The real runner: inherits stdout/stderr, so setup output lands in the pod log. */
175
+ export const runCommand = (command, args, opts) => new Promise((resolve) => {
176
+ // Its own process group, so a timeout stops everything it started,
177
+ // not only the shell.
178
+ const child = spawn(command, args, { cwd: opts.cwd, env: opts.env, stdio: ['ignore', 'inherit', 'inherit'], detached: true });
179
+ let timedOut = false;
180
+ const timer = opts.timeoutMs
181
+ ? setTimeout(() => {
182
+ timedOut = true;
183
+ try {
184
+ process.kill(-child.pid, 'SIGKILL');
185
+ }
186
+ catch {
187
+ child.kill('SIGKILL');
188
+ }
189
+ }, opts.timeoutMs)
190
+ : null;
191
+ child.on('error', () => {
192
+ if (timer)
193
+ clearTimeout(timer);
194
+ resolve({ code: -1, timedOut });
195
+ });
196
+ child.on('close', (code) => {
197
+ if (timer)
198
+ clearTimeout(timer);
199
+ resolve({ code, timedOut });
200
+ });
201
+ });
202
+ //# sourceMappingURL=hosted-setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosted-setup.js","sourceRoot":"","sources":["../src/hosted-setup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The model proxy of a hosted pod: how the coding CLIs in it reach models
3
+ * while the pod model token stays out of their hands.
4
+ *
5
+ * The reconcile loop puts a short-lived pod model token into the pod's
6
+ * Secret (ALMYTY_MODEL_TOKEN, with its expiry in
7
+ * ALMYTY_MODEL_TOKEN_EXPIRES_AT) and tells the runner where to listen
8
+ * (ALMYTY_MODEL_PROXY_PORT) and where to renew (ALMYTY_MODEL_RENEW_PATH).
9
+ * The image's entrypoint points every CLI at http://127.0.0.1:<port> with a
10
+ * placeholder key (images/runner-env/entrypoint.sh). This module:
11
+ *
12
+ * - takes the token out of the runner's environment as soon as it is
13
+ * read, so nothing the runner starts inherits it;
14
+ * - listens on the loopback port and forwards each /v1/... call to the
15
+ * almyty API, replacing whatever credential the CLI sent with the
16
+ * current token, streaming both ways;
17
+ * - renews the token with itself when three quarters of its life have
18
+ * passed, so a CLI keeps working past the token's lifetime without
19
+ * ever seeing it change.
20
+ *
21
+ * A renewal the API refuses (the pod is being stopped) is not fatal to the
22
+ * runner: model calls answer 401 from then on and the next start brings a
23
+ * fresh token. The token is never logged or written to disk.
24
+ */
25
+ import { Server } from 'node:http';
26
+ export declare const MODEL_TOKEN_ENV = "ALMYTY_MODEL_TOKEN";
27
+ export declare const MODEL_TOKEN_EXPIRES_ENV = "ALMYTY_MODEL_TOKEN_EXPIRES_AT";
28
+ export declare const MODEL_PROXY_PORT_ENV = "ALMYTY_MODEL_PROXY_PORT";
29
+ export declare const MODEL_RENEW_PATH_ENV = "ALMYTY_MODEL_RENEW_PATH";
30
+ export declare const DEFAULT_MODEL_RENEW_PATH = "/runners/hosted/model-token";
31
+ type Env = Record<string, string | undefined>;
32
+ export interface ModelAccessSettings {
33
+ token: string;
34
+ /** Unknown when the pod was started without it; the token is then renewed at once. */
35
+ expiresAt: Date | null;
36
+ port: number;
37
+ renewPath: string;
38
+ }
39
+ /**
40
+ * The pod model token and the proxy's settings, or null when the pod has
41
+ * none (a runner outside a hosted pod, or an older backend). Removes the
42
+ * token variables from `env` once read.
43
+ */
44
+ export declare function readModelAccess(env?: Env): ModelAccessSettings | null;
45
+ export interface ModelTokenOptions {
46
+ backendUrl: string;
47
+ renewPath: string;
48
+ token: string;
49
+ expiresAt: Date | null;
50
+ log?: (line: string) => void;
51
+ fetch?: typeof globalThis.fetch;
52
+ setTimeoutFn?: typeof setTimeout;
53
+ clearTimeoutFn?: typeof clearTimeout;
54
+ now?: () => number;
55
+ }
56
+ /** Holds the pod model token and renews it before it expires. */
57
+ export declare class ModelToken {
58
+ private readonly opts;
59
+ private token;
60
+ private expiresAt;
61
+ private timer;
62
+ private stopped;
63
+ private readonly fetchImpl;
64
+ private readonly setTimeoutFn;
65
+ private readonly clearTimeoutFn;
66
+ private readonly now;
67
+ constructor(opts: ModelTokenOptions);
68
+ current(): string;
69
+ expiry(): Date | null;
70
+ start(): void;
71
+ stop(): void;
72
+ /** When to renew, from now; at once when the expiry is unknown. */
73
+ renewDelay(): number;
74
+ /** One renewal. True when a new token is in place. Never throws. */
75
+ renew(): Promise<boolean>;
76
+ private schedule;
77
+ }
78
+ export interface ModelProxyOptions {
79
+ backendUrl: string;
80
+ token: () => string;
81
+ /** 0 picks a free port (tests). */
82
+ port: number;
83
+ log?: (line: string) => void;
84
+ }
85
+ /**
86
+ * The loopback model proxy. Only /v1/... is forwarded; the CLI's own
87
+ * credential is replaced with the current pod token. Resolves once it
88
+ * listens, with the port it got.
89
+ */
90
+ export declare function startModelProxy(opts: ModelProxyOptions): Promise<{
91
+ server: Server;
92
+ port: number;
93
+ close: () => Promise<void>;
94
+ }>;
95
+ export {};
@@ -0,0 +1,210 @@
1
+ /**
2
+ * The model proxy of a hosted pod: how the coding CLIs in it reach models
3
+ * while the pod model token stays out of their hands.
4
+ *
5
+ * The reconcile loop puts a short-lived pod model token into the pod's
6
+ * Secret (ALMYTY_MODEL_TOKEN, with its expiry in
7
+ * ALMYTY_MODEL_TOKEN_EXPIRES_AT) and tells the runner where to listen
8
+ * (ALMYTY_MODEL_PROXY_PORT) and where to renew (ALMYTY_MODEL_RENEW_PATH).
9
+ * The image's entrypoint points every CLI at http://127.0.0.1:<port> with a
10
+ * placeholder key (images/runner-env/entrypoint.sh). This module:
11
+ *
12
+ * - takes the token out of the runner's environment as soon as it is
13
+ * read, so nothing the runner starts inherits it;
14
+ * - listens on the loopback port and forwards each /v1/... call to the
15
+ * almyty API, replacing whatever credential the CLI sent with the
16
+ * current token, streaming both ways;
17
+ * - renews the token with itself when three quarters of its life have
18
+ * passed, so a CLI keeps working past the token's lifetime without
19
+ * ever seeing it change.
20
+ *
21
+ * A renewal the API refuses (the pod is being stopped) is not fatal to the
22
+ * runner: model calls answer 401 from then on and the next start brings a
23
+ * fresh token. The token is never logged or written to disk.
24
+ */
25
+ import { createServer, request as httpRequest } from 'node:http';
26
+ import { request as httpsRequest } from 'node:https';
27
+ export const MODEL_TOKEN_ENV = 'ALMYTY_MODEL_TOKEN';
28
+ export const MODEL_TOKEN_EXPIRES_ENV = 'ALMYTY_MODEL_TOKEN_EXPIRES_AT';
29
+ export const MODEL_PROXY_PORT_ENV = 'ALMYTY_MODEL_PROXY_PORT';
30
+ export const MODEL_RENEW_PATH_ENV = 'ALMYTY_MODEL_RENEW_PATH';
31
+ export const DEFAULT_MODEL_RENEW_PATH = '/runners/hosted/model-token';
32
+ /** Renew once this share of the token's lifetime has passed. */
33
+ const RENEW_AT_FRACTION = 0.75;
34
+ /** Delay between renewal attempts that failed for a reason worth retrying. */
35
+ const RENEW_RETRY_MS = 30_000;
36
+ /** Headers the proxy never passes on: hop-by-hop, and every credential the CLI sent. */
37
+ const DROPPED_REQUEST_HEADERS = new Set(['host', 'connection', 'keep-alive', 'transfer-encoding', 'authorization', 'x-api-key', 'proxy-authorization']);
38
+ const DROPPED_RESPONSE_HEADERS = new Set(['connection', 'keep-alive', 'transfer-encoding']);
39
+ /**
40
+ * The pod model token and the proxy's settings, or null when the pod has
41
+ * none (a runner outside a hosted pod, or an older backend). Removes the
42
+ * token variables from `env` once read.
43
+ */
44
+ export function readModelAccess(env = process.env) {
45
+ const token = (env[MODEL_TOKEN_ENV] ?? '').trim();
46
+ const expires = (env[MODEL_TOKEN_EXPIRES_ENV] ?? '').trim();
47
+ delete env[MODEL_TOKEN_ENV];
48
+ delete env[MODEL_TOKEN_EXPIRES_ENV];
49
+ const port = Number((env[MODEL_PROXY_PORT_ENV] ?? '').trim());
50
+ if (!token || !Number.isInteger(port) || port <= 0 || port > 65535)
51
+ return null;
52
+ const expiresAt = expires ? new Date(expires) : null;
53
+ const renewPath = (env[MODEL_RENEW_PATH_ENV] ?? '').trim();
54
+ return {
55
+ token,
56
+ expiresAt: expiresAt && !Number.isNaN(expiresAt.getTime()) ? expiresAt : null,
57
+ port,
58
+ renewPath: renewPath.startsWith('/') ? renewPath : DEFAULT_MODEL_RENEW_PATH,
59
+ };
60
+ }
61
+ /** Holds the pod model token and renews it before it expires. */
62
+ export class ModelToken {
63
+ opts;
64
+ token;
65
+ expiresAt;
66
+ timer = null;
67
+ stopped = false;
68
+ fetchImpl;
69
+ setTimeoutFn;
70
+ clearTimeoutFn;
71
+ now;
72
+ constructor(opts) {
73
+ this.opts = opts;
74
+ this.token = opts.token;
75
+ this.expiresAt = opts.expiresAt;
76
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
77
+ this.setTimeoutFn = opts.setTimeoutFn ?? setTimeout;
78
+ this.clearTimeoutFn = opts.clearTimeoutFn ?? clearTimeout;
79
+ this.now = opts.now ?? Date.now;
80
+ }
81
+ current() {
82
+ return this.token;
83
+ }
84
+ expiry() {
85
+ return this.expiresAt;
86
+ }
87
+ start() {
88
+ this.schedule(this.renewDelay());
89
+ }
90
+ stop() {
91
+ this.stopped = true;
92
+ if (this.timer)
93
+ this.clearTimeoutFn(this.timer);
94
+ this.timer = null;
95
+ }
96
+ /** When to renew, from now; at once when the expiry is unknown. */
97
+ renewDelay() {
98
+ if (!this.expiresAt)
99
+ return 0;
100
+ return Math.max(0, Math.floor((this.expiresAt.getTime() - this.now()) * RENEW_AT_FRACTION));
101
+ }
102
+ /** One renewal. True when a new token is in place. Never throws. */
103
+ async renew() {
104
+ if (this.stopped)
105
+ return false;
106
+ let status = 0;
107
+ try {
108
+ const res = await this.fetchImpl(`${this.opts.backendUrl}${this.opts.renewPath}`, {
109
+ method: 'POST',
110
+ headers: { Authorization: `Bearer ${this.token}` },
111
+ });
112
+ status = res.status;
113
+ if (res.ok) {
114
+ const json = (await res.json().catch(() => null));
115
+ const next = json?.data?.token;
116
+ const expiresAt = new Date(json?.data?.expiresAt);
117
+ if (typeof next === 'string' && next && !Number.isNaN(expiresAt.getTime())) {
118
+ this.token = next;
119
+ this.expiresAt = expiresAt;
120
+ this.opts.log?.(`model token renewed; next renewal in ${Math.round(this.renewDelay() / 1000)}s`);
121
+ this.schedule(this.renewDelay());
122
+ return true;
123
+ }
124
+ }
125
+ }
126
+ catch {
127
+ status = 0;
128
+ }
129
+ if (this.stopped)
130
+ return false;
131
+ // Refused: the pod is being stopped or the token is gone. Model calls
132
+ // answer 401 from here on; the next start brings a fresh token.
133
+ if (status === 401 || status === 403 || status === 404) {
134
+ this.opts.log?.(`model token renewal refused (${status}); coding CLIs cannot reach models until the next start`);
135
+ this.stop();
136
+ return false;
137
+ }
138
+ const left = this.expiresAt ? this.expiresAt.getTime() - this.now() : 0;
139
+ if (this.expiresAt && left <= RENEW_RETRY_MS) {
140
+ this.opts.log?.(`model token renewal failed (${status || 'no answer'}) and it expires before another try`);
141
+ return false;
142
+ }
143
+ this.opts.log?.(`model token renewal failed (${status || 'no answer'}); retrying in ${RENEW_RETRY_MS / 1000}s`);
144
+ this.schedule(RENEW_RETRY_MS);
145
+ return false;
146
+ }
147
+ schedule(delay) {
148
+ if (this.stopped)
149
+ return;
150
+ if (this.timer)
151
+ this.clearTimeoutFn(this.timer);
152
+ this.timer = this.setTimeoutFn(() => {
153
+ this.timer = null;
154
+ void this.renew();
155
+ }, delay);
156
+ this.timer?.unref?.();
157
+ }
158
+ }
159
+ /**
160
+ * The loopback model proxy. Only /v1/... is forwarded; the CLI's own
161
+ * credential is replaced with the current pod token. Resolves once it
162
+ * listens, with the port it got.
163
+ */
164
+ export async function startModelProxy(opts) {
165
+ const base = new URL(opts.backendUrl);
166
+ const server = createServer((req, res) => forward(req, res, base, opts));
167
+ await new Promise((resolve, reject) => {
168
+ server.once('error', reject);
169
+ server.listen(opts.port, '127.0.0.1', () => resolve());
170
+ });
171
+ const port = server.address().port;
172
+ return { server, port, close: () => new Promise((r) => server.close(() => r())) };
173
+ }
174
+ function forward(req, res, base, opts) {
175
+ const path = req.url ?? '/';
176
+ if (!path.startsWith('/v1/')) {
177
+ res.writeHead(404, { 'Content-Type': 'application/json' }).end(JSON.stringify({ error: { message: 'Only the model endpoints (/v1/...) are served here' } }));
178
+ return;
179
+ }
180
+ const target = new URL(`${base.pathname.replace(/(?<!\/)\/+$/, '')}${path}`, base);
181
+ const headers = {};
182
+ for (const [name, value] of Object.entries(req.headers)) {
183
+ if (value !== undefined && !DROPPED_REQUEST_HEADERS.has(name.toLowerCase()))
184
+ headers[name] = value;
185
+ }
186
+ headers.authorization = `Bearer ${opts.token()}`;
187
+ const send = target.protocol === 'https:' ? httpsRequest : httpRequest;
188
+ const upstream = send(target, { method: req.method, headers }, (answer) => {
189
+ const out = {};
190
+ for (const [name, value] of Object.entries(answer.headers)) {
191
+ if (value !== undefined && !DROPPED_RESPONSE_HEADERS.has(name.toLowerCase()))
192
+ out[name] = value;
193
+ }
194
+ res.writeHead(answer.statusCode ?? 502, out);
195
+ answer.pipe(res);
196
+ });
197
+ upstream.on('error', (err) => {
198
+ opts.log?.(`model proxy: almyty could not be reached (${err.message})`);
199
+ if (!res.headersSent)
200
+ res.writeHead(502, { 'Content-Type': 'application/json' });
201
+ res.end(JSON.stringify({ error: { message: 'almyty could not be reached' } }));
202
+ });
203
+ // A client that goes away stops the upstream call too.
204
+ res.on('close', () => {
205
+ if (!res.writableFinished)
206
+ upstream.destroy();
207
+ });
208
+ req.pipe(upstream);
209
+ }
210
+ //# sourceMappingURL=model-proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-proxy.js","sourceRoot":"","sources":["../src/model-proxy.ts"],"names":[],"mappings":"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"}
package/dist/policy.d.ts CHANGED
@@ -1,6 +1,32 @@
1
1
  import { RunnerConfig } from './types.js';
2
2
  /** Drop env keys a payload must not be allowed to set. */
3
3
  export declare function sanitizeEnv(env?: Record<string, string>): Record<string, string> | undefined;
4
+ /**
5
+ * Parse an allowBinaries list from its environment form: a JSON array of
6
+ * strings (what the backend writes). Anything else throws, so a runner
7
+ * with a list it cannot read refuses to start rather than running
8
+ * unrestricted.
9
+ */
10
+ export declare function parseAllowBinaries(raw: string | undefined): string[] | undefined;
11
+ /**
12
+ * The config with an allowBinaries list applied. Only ever narrows: when
13
+ * the config already has a list, the result is the intersection, and an
14
+ * empty intersection throws rather than lifting the restriction.
15
+ */
16
+ export declare function withAllowBinaries(config: RunnerConfig, allow: string[] | undefined): RunnerConfig;
17
+ /**
18
+ * The command names a shell command line would start: the first word of
19
+ * every simple command, including those inside $( ), backticks, ( ), <( )
20
+ * and >( ), after variable assignments, redirections and keywords. Quotes
21
+ * and backslashes are removed from the word (`"rm"` is rm). A word that is
22
+ * built at run time ($VAR, a glob) is returned as is and never matches an
23
+ * entry, so it is refused.
24
+ *
25
+ * Best effort by design: separators inside quotes split too, which can
26
+ * refuse a harmless line (`echo "a; b"` checks `b`) but never lets a
27
+ * command through unseen.
28
+ */
29
+ export declare function commandHeads(cmd: string): string[];
4
30
  /** Enforce policy for a process.spawn; returns the sanitized env to use. */
5
31
  export declare function enforceSpawnPolicy(config: RunnerConfig, opts: {
6
32
  binary: string;