@almyty/runner 1.5.3 → 1.5.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.
@@ -0,0 +1,91 @@
1
+ import { RunnerConfig, RunnerRuntimeInfo } from './types.js';
2
+ /** Where a pod finds the token: the Secret, through envFrom. */
3
+ export declare const ENROLLMENT_TOKEN_ENV = "ALMYTY_ENROLLMENT_TOKEN";
4
+ /** Or a file, for a pod that mounts the Secret instead of injecting it. */
5
+ export declare const ENROLLMENT_TOKEN_FILE_ENV = "ALMYTY_ENROLLMENT_TOKEN_FILE";
6
+ export declare const DEFAULT_ENROLL_PATH = "/runners/enroll";
7
+ export declare const DEFAULT_HOSTED_STREAM_PATH = "/runners/hosted/stream";
8
+ export declare const DEFAULT_HOSTED_RENEW_PATH = "/runners/hosted/credential";
9
+ /** Enrollment could not happen; the process should exit non-zero. */
10
+ export declare class EnrollmentError extends Error {
11
+ readonly status?: number | undefined;
12
+ constructor(message: string, status?: number | undefined);
13
+ }
14
+ export interface EnrollSettings {
15
+ backendUrl: string;
16
+ enrollPath: string;
17
+ /** Kept off every log line and error message. */
18
+ token: string;
19
+ }
20
+ type Env = Record<string, string | undefined>;
21
+ /**
22
+ * The token and where to send it, from the environment the Deployment
23
+ * sets. Removes the token variables from `env` once read, so nothing the
24
+ * runner spawns later inherits them.
25
+ */
26
+ export declare function readEnrollSettings(env?: Env, opts?: {
27
+ url?: string;
28
+ readFile?: (path: string) => string;
29
+ }): EnrollSettings;
30
+ export interface EnrollResult {
31
+ runnerId: string;
32
+ credential: string;
33
+ expiresAt: Date;
34
+ effectiveConfig: RunnerConfig;
35
+ streamPath: string;
36
+ renewPath: string;
37
+ }
38
+ /**
39
+ * Trade the token for a runner credential, once. Throws EnrollmentError
40
+ * on any refusal or malformed answer; the message never carries the token
41
+ * or a credential.
42
+ */
43
+ export declare function enroll(settings: EnrollSettings, body: {
44
+ runtimeInfo: RunnerRuntimeInfo;
45
+ config?: Partial<RunnerConfig>;
46
+ }, fetchImpl?: typeof globalThis.fetch): Promise<EnrollResult>;
47
+ export interface RunnerCredentialOptions {
48
+ backendUrl: string;
49
+ renewPath: string;
50
+ credential: string;
51
+ expiresAt: Date;
52
+ /** Renewal is impossible: the process should stop. */
53
+ onFatal: (reason: string) => void;
54
+ log?: (line: string) => void;
55
+ fetch?: typeof globalThis.fetch;
56
+ setTimeoutFn?: typeof setTimeout;
57
+ clearTimeoutFn?: typeof clearTimeout;
58
+ now?: () => number;
59
+ }
60
+ /**
61
+ * Holds the runner credential and renews it before it expires. `current()`
62
+ * is what every request reads, so a renewed credential is used from the
63
+ * next request on without reconnecting.
64
+ */
65
+ export declare class RunnerCredential {
66
+ private readonly opts;
67
+ private credential;
68
+ private expiresAt;
69
+ private timer;
70
+ private stopped;
71
+ private readonly fetchImpl;
72
+ private readonly setTimeoutFn;
73
+ private readonly clearTimeoutFn;
74
+ private readonly now;
75
+ constructor(opts: RunnerCredentialOptions);
76
+ current(): string;
77
+ expiry(): Date;
78
+ /** Arm the renewal timer for the current credential. */
79
+ start(): void;
80
+ stop(): void;
81
+ /** When the current credential should be renewed, from now. */
82
+ renewDelay(): number;
83
+ /**
84
+ * One renewal attempt. Resolves true when a new credential is in place;
85
+ * otherwise schedules a retry, or reports fatal when the server refused
86
+ * or the credential will expire before another try.
87
+ */
88
+ renew(): Promise<boolean>;
89
+ private schedule;
90
+ }
91
+ export {};
package/dist/enroll.js ADDED
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Enroll mode: how a runner inside a hosted pod gets its identity.
3
+ *
4
+ * A hosted pod never holds anybody's login (docs/hosted-runners.md). The
5
+ * reconcile loop writes a single-use enrollment token into the pod's
6
+ * Secret, which the Deployment hands to the container as
7
+ * ALMYTY_ENROLLMENT_TOKEN (envFrom). This module trades that token once,
8
+ * at POST /runners/enroll, for a runner credential that only works on the
9
+ * runner's own stream and its renewal, and then keeps the credential fresh.
10
+ *
11
+ * The rules this file keeps:
12
+ *
13
+ * - The credential lives in memory only. It is never written to disk,
14
+ * never put in process.env (where every spawned command would inherit
15
+ * it) and never printed: errors name the status and the server's
16
+ * words, not the request.
17
+ * - The enrollment token is removed from process.env as soon as it is
18
+ * read, for the same reason. It is spent after one use anyway.
19
+ * - Enrollment failing is fatal (EnrollmentError). The CLI exits
20
+ * non-zero, Kubernetes restarts the container, and the reconcile loop
21
+ * writes a fresh token if the pod does not enroll in time.
22
+ * - The credential is renewed well before it expires. A renewal the
23
+ * server refuses (401, 404: the hosted runner is gone) or one that
24
+ * cannot succeed before expiry is fatal too.
25
+ */
26
+ import { readFileSync } from 'node:fs';
27
+ import { assertSecureBackendUrl } from './config.js';
28
+ /** Where a pod finds the token: the Secret, through envFrom. */
29
+ export const ENROLLMENT_TOKEN_ENV = 'ALMYTY_ENROLLMENT_TOKEN';
30
+ /** Or a file, for a pod that mounts the Secret instead of injecting it. */
31
+ export const ENROLLMENT_TOKEN_FILE_ENV = 'ALMYTY_ENROLLMENT_TOKEN_FILE';
32
+ export const DEFAULT_ENROLL_PATH = '/runners/enroll';
33
+ export const DEFAULT_HOSTED_STREAM_PATH = '/runners/hosted/stream';
34
+ export const DEFAULT_HOSTED_RENEW_PATH = '/runners/hosted/credential';
35
+ /** Renew once this share of the credential's lifetime has passed. */
36
+ const RENEW_AT_FRACTION = 0.75;
37
+ /** Delay between renewal attempts that failed for a reason worth retrying. */
38
+ const RENEW_RETRY_MS = 30_000;
39
+ /** Enrollment could not happen; the process should exit non-zero. */
40
+ export class EnrollmentError extends Error {
41
+ status;
42
+ constructor(message, status) {
43
+ super(message);
44
+ this.status = status;
45
+ this.name = 'EnrollmentError';
46
+ }
47
+ }
48
+ /**
49
+ * The token and where to send it, from the environment the Deployment
50
+ * sets. Removes the token variables from `env` once read, so nothing the
51
+ * runner spawns later inherits them.
52
+ */
53
+ export function readEnrollSettings(env = process.env, opts = {}) {
54
+ const readFile = opts.readFile ?? ((p) => readFileSync(p, 'utf-8'));
55
+ let token = (env[ENROLLMENT_TOKEN_ENV] ?? '').trim();
56
+ const file = (env[ENROLLMENT_TOKEN_FILE_ENV] ?? '').trim();
57
+ if (!token && file) {
58
+ try {
59
+ token = readFile(file).trim();
60
+ }
61
+ catch (err) {
62
+ throw new EnrollmentError(`cannot read ${ENROLLMENT_TOKEN_FILE_ENV} (${file}): ${err?.code ?? err?.message ?? err}`);
63
+ }
64
+ }
65
+ delete env[ENROLLMENT_TOKEN_ENV];
66
+ delete env[ENROLLMENT_TOKEN_FILE_ENV];
67
+ if (!token) {
68
+ throw new EnrollmentError(`no enrollment token: set ${ENROLLMENT_TOKEN_ENV} or ${ENROLLMENT_TOKEN_FILE_ENV}`);
69
+ }
70
+ const backendUrl = (opts.url ?? env.ALMYTY_API_URL ?? env.ALMYTY_URL ?? '').trim().replace(/\/+$/, '');
71
+ if (!backendUrl)
72
+ throw new EnrollmentError('no API URL: set ALMYTY_API_URL (or pass --url)');
73
+ try {
74
+ assertSecureBackendUrl(backendUrl);
75
+ }
76
+ catch (err) {
77
+ throw new EnrollmentError(err.message);
78
+ }
79
+ return { backendUrl, enrollPath: pathOr(env.ALMYTY_ENROLL_PATH, DEFAULT_ENROLL_PATH), token };
80
+ }
81
+ function pathOr(value, fallback) {
82
+ const v = (value ?? '').trim();
83
+ return v.startsWith('/') ? v : fallback;
84
+ }
85
+ /**
86
+ * Trade the token for a runner credential, once. Throws EnrollmentError
87
+ * on any refusal or malformed answer; the message never carries the token
88
+ * or a credential.
89
+ */
90
+ export async function enroll(settings, body, fetchImpl = globalThis.fetch) {
91
+ let res;
92
+ try {
93
+ res = await fetchImpl(`${settings.backendUrl}${settings.enrollPath}`, {
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body: JSON.stringify({ token: settings.token, runtimeInfo: body.runtimeInfo, ...(body.config ? { config: body.config } : {}) }),
97
+ });
98
+ }
99
+ catch (err) {
100
+ throw new EnrollmentError(`enrollment request failed: ${networkReason(err)}`);
101
+ }
102
+ if (!res.ok) {
103
+ throw new EnrollmentError(`enrollment refused: ${res.status} ${serverMessage(await safeText(res), settings.token)}`, res.status);
104
+ }
105
+ const json = (await res.json().catch(() => null));
106
+ const d = json?.data;
107
+ const expiresAt = new Date(d?.expiresAt);
108
+ if (!d || typeof d.runnerId !== 'string' || typeof d.credential !== 'string' || !d.credential ||
109
+ Number.isNaN(expiresAt.getTime()) || !d.effectiveConfig || typeof d.effectiveConfig !== 'object') {
110
+ throw new EnrollmentError('enrollment answered without a runner id, credential, expiry and config');
111
+ }
112
+ return {
113
+ runnerId: d.runnerId,
114
+ credential: d.credential,
115
+ expiresAt,
116
+ effectiveConfig: d.effectiveConfig,
117
+ streamPath: pathOr(d.streamPath, DEFAULT_HOSTED_STREAM_PATH),
118
+ renewPath: pathOr(d.renewPath, DEFAULT_HOSTED_RENEW_PATH),
119
+ };
120
+ }
121
+ /**
122
+ * Holds the runner credential and renews it before it expires. `current()`
123
+ * is what every request reads, so a renewed credential is used from the
124
+ * next request on without reconnecting.
125
+ */
126
+ export class RunnerCredential {
127
+ opts;
128
+ credential;
129
+ expiresAt;
130
+ timer = null;
131
+ stopped = false;
132
+ fetchImpl;
133
+ setTimeoutFn;
134
+ clearTimeoutFn;
135
+ now;
136
+ constructor(opts) {
137
+ this.opts = opts;
138
+ this.credential = opts.credential;
139
+ this.expiresAt = opts.expiresAt;
140
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
141
+ this.setTimeoutFn = opts.setTimeoutFn ?? setTimeout;
142
+ this.clearTimeoutFn = opts.clearTimeoutFn ?? clearTimeout;
143
+ this.now = opts.now ?? Date.now;
144
+ }
145
+ current() {
146
+ return this.credential;
147
+ }
148
+ expiry() {
149
+ return this.expiresAt;
150
+ }
151
+ /** Arm the renewal timer for the current credential. */
152
+ start() {
153
+ this.schedule(this.renewDelay());
154
+ }
155
+ stop() {
156
+ this.stopped = true;
157
+ if (this.timer)
158
+ this.clearTimeoutFn(this.timer);
159
+ this.timer = null;
160
+ }
161
+ /** When the current credential should be renewed, from now. */
162
+ renewDelay() {
163
+ const left = this.expiresAt.getTime() - this.now();
164
+ return Math.max(0, Math.floor(left * RENEW_AT_FRACTION));
165
+ }
166
+ /**
167
+ * One renewal attempt. Resolves true when a new credential is in place;
168
+ * otherwise schedules a retry, or reports fatal when the server refused
169
+ * or the credential will expire before another try.
170
+ */
171
+ async renew() {
172
+ if (this.stopped)
173
+ return false;
174
+ let status = 0;
175
+ let detail = '';
176
+ try {
177
+ const res = await this.fetchImpl(`${this.opts.backendUrl}${this.opts.renewPath}`, {
178
+ method: 'POST',
179
+ headers: { Authorization: `Bearer ${this.credential}` },
180
+ });
181
+ status = res.status;
182
+ if (res.ok) {
183
+ const json = (await res.json().catch(() => null));
184
+ const next = json?.data?.credential;
185
+ const expiresAt = new Date(json?.data?.expiresAt);
186
+ if (typeof next === 'string' && next && !Number.isNaN(expiresAt.getTime())) {
187
+ this.credential = next;
188
+ this.expiresAt = expiresAt;
189
+ this.opts.log?.(`runner credential renewed; next renewal in ${Math.round(this.renewDelay() / 1000)}s`);
190
+ this.schedule(this.renewDelay());
191
+ return true;
192
+ }
193
+ detail = 'answer carried no credential';
194
+ }
195
+ else {
196
+ detail = serverMessage(await safeText(res), this.credential);
197
+ }
198
+ }
199
+ catch (err) {
200
+ detail = networkReason(err);
201
+ }
202
+ if (this.stopped)
203
+ return false;
204
+ // 401: the credential is no longer accepted. 404: the hosted runner is
205
+ // gone or being torn down. Neither gets better by asking again.
206
+ if (status === 401 || status === 403 || status === 404) {
207
+ this.opts.onFatal(`credential renewal refused: ${status} ${detail}`.trim());
208
+ return false;
209
+ }
210
+ const left = this.expiresAt.getTime() - this.now();
211
+ if (left <= RENEW_RETRY_MS) {
212
+ this.opts.onFatal(`credential renewal failed and the credential expires before another try: ${status || ''} ${detail}`.trim());
213
+ return false;
214
+ }
215
+ this.opts.log?.(`credential renewal failed (${status || 'no answer'} ${detail}); retrying in ${RENEW_RETRY_MS / 1000}s`);
216
+ this.schedule(RENEW_RETRY_MS);
217
+ return false;
218
+ }
219
+ schedule(delay) {
220
+ if (this.stopped)
221
+ return;
222
+ if (this.timer)
223
+ this.clearTimeoutFn(this.timer);
224
+ this.timer = this.setTimeoutFn(() => {
225
+ this.timer = null;
226
+ void this.renew();
227
+ }, delay);
228
+ // Not the keep-alive: the heartbeat timer holds the process up.
229
+ this.timer?.unref?.();
230
+ }
231
+ }
232
+ /** The server's words, short, with anything secret-shaped removed. */
233
+ function serverMessage(text, ...secrets) {
234
+ let out = text;
235
+ try {
236
+ const parsed = JSON.parse(text);
237
+ if (typeof parsed?.message === 'string')
238
+ out = parsed.message;
239
+ else if (Array.isArray(parsed?.message))
240
+ out = parsed.message.join('; ');
241
+ }
242
+ catch { /* plain text */ }
243
+ for (const s of secrets)
244
+ if (s)
245
+ out = out.split(s).join('[redacted]');
246
+ return out.replace(/\s+/g, ' ').slice(0, 200);
247
+ }
248
+ /** fetch's own "fetch failed" says nothing; the cause names the socket error. */
249
+ function networkReason(err) {
250
+ const code = err?.cause?.code ?? err?.cause?.errors?.[0]?.code;
251
+ return [err?.message ?? String(err), code].filter(Boolean).join(': ');
252
+ }
253
+ async function safeText(res) {
254
+ try {
255
+ return await res.text();
256
+ }
257
+ catch {
258
+ return '';
259
+ }
260
+ }
261
+ //# sourceMappingURL=enroll.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enroll.js","sourceRoot":"","sources":["../src/enroll.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /** Where the volume is mounted when the Deployment does not say. */
2
+ export declare const DEFAULT_WORKSPACE_ROOT = "/workspace";
3
+ /** The checkout's folder inside the workspace. */
4
+ export declare const REPO_DIR_NAME = "repo";
5
+ export interface CommandResult {
6
+ code: number | null;
7
+ timedOut?: boolean;
8
+ }
9
+ /** Runs one command with its output going to the runner's log. */
10
+ export type RunCommand = (command: string, args: string[], opts: {
11
+ cwd: string;
12
+ env: NodeJS.ProcessEnv;
13
+ timeoutMs?: number;
14
+ }) => Promise<CommandResult>;
15
+ export interface PrepareInputs {
16
+ env?: Record<string, string | undefined>;
17
+ run?: RunCommand;
18
+ log?: (line: string) => void;
19
+ warn?: (line: string) => void;
20
+ /** Where the script file is written before it runs. */
21
+ tmpDir?: string;
22
+ }
23
+ export interface PrepareOutcome {
24
+ /** Nothing to do: this volume already carries this environment version. */
25
+ upToDate: boolean;
26
+ cloned: boolean;
27
+ setupRan: boolean;
28
+ /** Clone and setup both succeeded (or were not needed) and the version was recorded. */
29
+ ok: boolean;
30
+ repoDir: string | null;
31
+ }
32
+ export declare function workspaceRoot(env: Record<string, string | undefined>): string;
33
+ export declare function prepareHostedWorkspace(inputs?: PrepareInputs): Promise<PrepareOutcome>;
34
+ /**
35
+ * The token as an HTTP Authorization header for the repository's origin,
36
+ * passed through git's GIT_CONFIG_COUNT/KEY/VALUE variables (git 2.31+).
37
+ * Empty for no token or a URL that is not https.
38
+ */
39
+ export declare function gitAuthEnv(url: string, token: string | undefined, base?: Record<string, string | undefined>): Record<string, string>;
40
+ /** A URL with any user:password part removed, for log lines. */
41
+ export declare function redactUrl(url: string): string;
42
+ /** The real runner: inherits stdout/stderr, so setup output lands in the pod log. */
43
+ export declare const runCommand: RunCommand;
@@ -0,0 +1,196 @@
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
+ const recorded = existsSync(versionFile) ? readFileSync(versionFile, 'utf-8').trim() : null;
49
+ if (recorded === version) {
50
+ log(`workspace: environment version ${version} already set up`);
51
+ return { upToDate: true, cloned: false, setupRan: false, ok: true, repoDir: repoUrl ? repoDir : null };
52
+ }
53
+ mkdirSync(stateDir, { recursive: true, mode: 0o700 });
54
+ // A spawned step gets the runner's own variables (the environment's
55
+ // bindings among them); the enrollment token is already gone (enroll.ts).
56
+ const childEnv = { ...env, GIT_TERMINAL_PROMPT: '0' };
57
+ let ok = true;
58
+ let cloned = false;
59
+ if (repoUrl) {
60
+ if (existsSync(join(repoDir, '.git'))) {
61
+ // An existing checkout is the person's work; a new version does not
62
+ // overwrite it. Only the setup script runs again.
63
+ log(`workspace: ${repoDir} already holds a checkout; leaving it as it is`);
64
+ }
65
+ else {
66
+ 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 });
67
+ cloned = result;
68
+ if (!result) {
69
+ ok = false;
70
+ warn(`workspace: cloning ${redactUrl(repoUrl)} failed; it is tried again on the next start`);
71
+ }
72
+ }
73
+ }
74
+ const script = env.ALMYTY_SETUP_SCRIPT ?? '';
75
+ let setupRan = false;
76
+ if (script.trim() && ok) {
77
+ setupRan = true;
78
+ const tmp = inputs.tmpDir ?? '/tmp';
79
+ const file = join(tmp, `almyty-setup-${process.pid}-${Date.now()}.sh`);
80
+ writeFileSync(file, script, { mode: 0o700 });
81
+ const cwd = repoUrl && existsSync(repoDir) ? repoDir : root;
82
+ const timeoutS = positiveInt(env.ALMYTY_SETUP_TIMEOUT_SECONDS) ?? DEFAULT_SETUP_TIMEOUT_SECONDS;
83
+ log(`workspace: running the setup script in ${cwd}`);
84
+ try {
85
+ const res = await run('bash', [file], { cwd, env: childEnv, timeoutMs: timeoutS * 1000 });
86
+ if (res.code !== 0) {
87
+ ok = false;
88
+ warn(res.timedOut
89
+ ? `workspace: the setup script ran past ${timeoutS}s and was stopped; it runs again on the next start`
90
+ : `workspace: the setup script exited ${res.code}; it runs again on the next start`);
91
+ }
92
+ }
93
+ finally {
94
+ rmSync(file, { force: true });
95
+ }
96
+ }
97
+ if (ok) {
98
+ writeFileSync(versionFile, `${version}\n`, { mode: 0o600 });
99
+ log(`workspace: environment version ${version} set up`);
100
+ }
101
+ return { upToDate: false, cloned, setupRan, ok, repoDir: repoUrl ? repoDir : null };
102
+ }
103
+ async function cloneRepo(o) {
104
+ const env = { ...o.env, ...gitAuthEnv(o.url, o.token, o.env) };
105
+ o.log(`workspace: cloning ${redactUrl(o.url)}${o.ref ? ` at ${o.ref}` : ''} into ${o.dir}`);
106
+ const clone = await o.run('git', ['clone', '--', o.url, o.dir], { cwd: o.cwd, env });
107
+ if (clone.code !== 0) {
108
+ rmSync(o.dir, { recursive: true, force: true });
109
+ return false;
110
+ }
111
+ if (!o.ref)
112
+ return true;
113
+ // A branch or tag checks out directly; a commit or another ref may need
114
+ // fetching first.
115
+ const checkout = await o.run('git', ['-C', o.dir, 'checkout', o.ref, '--'], { cwd: o.cwd, env });
116
+ if (checkout.code === 0)
117
+ return true;
118
+ const fetch = await o.run('git', ['-C', o.dir, 'fetch', 'origin', o.ref], { cwd: o.cwd, env });
119
+ if (fetch.code !== 0)
120
+ return false;
121
+ const detached = await o.run('git', ['-C', o.dir, 'checkout', 'FETCH_HEAD', '--'], { cwd: o.cwd, env });
122
+ return detached.code === 0;
123
+ }
124
+ /**
125
+ * The token as an HTTP Authorization header for the repository's origin,
126
+ * passed through git's GIT_CONFIG_COUNT/KEY/VALUE variables (git 2.31+).
127
+ * Empty for no token or a URL that is not https.
128
+ */
129
+ export function gitAuthEnv(url, token, base = {}) {
130
+ if (!token)
131
+ return {};
132
+ let origin;
133
+ try {
134
+ const u = new URL(url);
135
+ if (u.protocol !== 'https:')
136
+ return {};
137
+ origin = `${u.protocol}//${u.host}/`;
138
+ }
139
+ catch {
140
+ return {};
141
+ }
142
+ const n = positiveInt(base.GIT_CONFIG_COUNT) ?? 0;
143
+ const basic = Buffer.from(`x-access-token:${token}`).toString('base64');
144
+ return {
145
+ GIT_CONFIG_COUNT: String(n + 1),
146
+ [`GIT_CONFIG_KEY_${n}`]: `http.${origin}.extraHeader`,
147
+ [`GIT_CONFIG_VALUE_${n}`]: `Authorization: Basic ${basic}`,
148
+ };
149
+ }
150
+ /** A URL with any user:password part removed, for log lines. */
151
+ export function redactUrl(url) {
152
+ try {
153
+ const u = new URL(url);
154
+ if (u.username || u.password) {
155
+ u.username = '';
156
+ u.password = '';
157
+ }
158
+ return u.toString();
159
+ }
160
+ catch {
161
+ return url.replace(/\/\/[^@/]*@/, '//');
162
+ }
163
+ }
164
+ function positiveInt(value) {
165
+ const n = Number((value ?? '').trim());
166
+ return Number.isInteger(n) && n > 0 ? n : undefined;
167
+ }
168
+ /** The real runner: inherits stdout/stderr, so setup output lands in the pod log. */
169
+ export const runCommand = (command, args, opts) => new Promise((resolve) => {
170
+ // Its own process group, so a timeout stops everything it started,
171
+ // not only the shell.
172
+ const child = spawn(command, args, { cwd: opts.cwd, env: opts.env, stdio: ['ignore', 'inherit', 'inherit'], detached: true });
173
+ let timedOut = false;
174
+ const timer = opts.timeoutMs
175
+ ? setTimeout(() => {
176
+ timedOut = true;
177
+ try {
178
+ process.kill(-child.pid, 'SIGKILL');
179
+ }
180
+ catch {
181
+ child.kill('SIGKILL');
182
+ }
183
+ }, opts.timeoutMs)
184
+ : null;
185
+ child.on('error', () => {
186
+ if (timer)
187
+ clearTimeout(timer);
188
+ resolve({ code: -1, timedOut });
189
+ });
190
+ child.on('close', (code) => {
191
+ if (timer)
192
+ clearTimeout(timer);
193
+ resolve({ code, timedOut });
194
+ });
195
+ });
196
+ //# 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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"}
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;