@refleet-it/runner 0.1.199 → 0.1.200

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.
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { AgentExecutionError } from './types.js';
2
+ import { agentEnv, AgentExecutionError } from './types.js';
3
3
  /**
4
4
  * Builds the `claude` CLI argument list for one job kind. Ported 1:1 from
5
5
  * runner/claude/entrypoint.sh's build_claude_args/build_kind_claude_args: qualification
@@ -107,6 +107,7 @@ export class ClaudeBackend {
107
107
  let buffer = '';
108
108
  const child = this.spawnFn(this.executablePath, args, {
109
109
  cwd: opts.cwd,
110
+ env: agentEnv(this.env),
110
111
  stdio: ['ignore', 'pipe', 'inherit'],
111
112
  });
112
113
  child.stdout.setEncoding('utf8');
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { AgentExecutionError } from './types.js';
2
+ import { agentEnv, AgentExecutionError } from './types.js';
3
3
  /**
4
4
  * Pulls the ACP session id out of a `session/new` (or `session/load`) response.
5
5
  * Pure/testable in isolation from the actual JSON-RPC transport.
@@ -177,7 +177,7 @@ export class KiroBackend {
177
177
  // protocol-native session/request_permission (see AcpConnection.handlePeerRequest).
178
178
  const child = this.spawnFn(this.executablePath, ['acp'], {
179
179
  cwd: opts.cwd,
180
- env: this.env,
180
+ env: agentEnv(this.env),
181
181
  stdio: ['pipe', 'pipe', 'inherit'],
182
182
  });
183
183
  const chunks = [];
@@ -1,2 +1,11 @@
1
1
  export class AgentExecutionError extends Error {
2
2
  }
3
+ /**
4
+ * The agent runs untrusted code and prompts from a customer repository with tool
5
+ * permissions switched off, so anything it can read from its environment it can also be
6
+ * talked into exfiltrating. The runner's own credentials — the API key that fetches
7
+ * GitLab tokens — have no business there.
8
+ */
9
+ export function agentEnv(env) {
10
+ return Object.fromEntries(Object.entries(env).filter(([name]) => !name.startsWith('REFLEET_')));
11
+ }
package/dist/fleet/api.js CHANGED
@@ -92,8 +92,10 @@ export class FleetApi {
92
92
  }
93
93
  }
94
94
  /**
95
- * Fetched per job rather than once at startup so a reconnected/rotated GitLab
96
- * token is picked up without restarting the runner.
95
+ * Fetched right before every git/GitLab call rather than once per job or at startup:
96
+ * OAuth-backed connections hand out short-lived access tokens, and the backend refreshes
97
+ * one only when asked for it, so a token fetched before a long agent run may be dead by
98
+ * the time the branch is pushed.
97
99
  */
98
100
  async fetchGitLabCredentials() {
99
101
  const response = await this.request('GET', '/runner/gitlab-credentials');
package/dist/fleet/git.js CHANGED
@@ -11,18 +11,25 @@ export class GitError extends Error {
11
11
  }
12
12
  }
13
13
  /**
14
- * GitLab's git-http backend (unlike its REST API) does not accept the PRIVATE-TOKEN
14
+ * GitLab's git-http backend (unlike its REST API) does not accept a bearer/private token
15
15
  * header — it only honors HTTP Basic auth, so the token is sent as a Basic credential
16
- * (any non-empty username works). Passed per invocation as `-c http.extraHeader` so it
17
- * is never written into the cached checkout's git config.
16
+ * (the `oauth2` username works for both OAuth and personal/group access tokens).
17
+ * Handed to git through GIT_CONFIG_* environment variables rather than `-c` on the
18
+ * command line, so it is never written into the cached checkout's git config, never
19
+ * visible in `ps`, and never part of the argv a GitError echoes back into logs and job
20
+ * failure reports.
18
21
  */
19
- export function gitAuthConfig(accessToken) {
22
+ export function gitAuthEnv(accessToken) {
20
23
  const basic = Buffer.from(`oauth2:${accessToken}`).toString('base64');
21
- return ['-c', `http.extraHeader=Authorization: Basic ${basic}`];
24
+ return {
25
+ GIT_CONFIG_COUNT: '1',
26
+ GIT_CONFIG_KEY_0: 'http.extraHeader',
27
+ GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}`,
28
+ };
22
29
  }
23
30
  export function createGitRunner(execFileFn = execFile) {
24
- return (args, cwd) => new Promise((resolve, reject) => {
25
- execFileFn('git', args, { cwd, maxBuffer: 64 * 1024 * 1024, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } }, (error, stdout, stderr) => {
31
+ return (args, cwd, env = {}) => new Promise((resolve, reject) => {
32
+ execFileFn('git', args, { cwd, maxBuffer: 64 * 1024 * 1024, env: { ...process.env, GIT_TERMINAL_PROMPT: '0', ...env } }, (error, stdout, stderr) => {
26
33
  if (error) {
27
34
  const code = 'code' in error && 'number' === typeof error.code ? error.code : null;
28
35
  reject(new GitError(args, code, String(stderr)));
@@ -19,7 +19,7 @@ async function gitlabRequest(credentials, fetchFn, method, path, body) {
19
19
  return fetchFn(`${apiBase(credentials)}${path}`, {
20
20
  method,
21
21
  headers: {
22
- 'PRIVATE-TOKEN': credentials.accessToken,
22
+ Authorization: `Bearer ${credentials.accessToken}`,
23
23
  'Content-Type': 'application/json',
24
24
  },
25
25
  body: undefined === body ? undefined : JSON.stringify(body),
package/dist/fleet/job.js CHANGED
@@ -83,9 +83,16 @@ export async function claimAndRunJob(deps) {
83
83
  }
84
84
  return;
85
85
  }
86
- await publishAndReport(deps, job, credentials, repoDir, project, output, report);
86
+ await publishAndReport(deps, job, repoDir, project, output, report);
87
87
  }
88
- async function publishAndReport(deps, job, credentials, repoDir, project, output, report) {
88
+ async function publishAndReport(deps, job, repoDir, project, output, report) {
89
+ // The agent may have run for longer than the access token fetched for the clone lives.
90
+ const credentials = await deps.api.fetchGitLabCredentials();
91
+ if (!credentials) {
92
+ deps.log(`job ${job.jobId}: failed to fetch GitLab credentials from Slipway before publishing`);
93
+ await report('failure', 'Failed to fetch GitLab credentials from Slipway', { errorMessage: 'gitlab credentials fetch failed' });
94
+ return;
95
+ }
89
96
  let published;
90
97
  try {
91
98
  published = await (deps.publishChange ?? defaultPublishChange)(deps.workspace, credentials, repoDir, project, job.ownerTargetId ?? '', `Apply Slipway shift ${job.ownerId ?? ''}`, tail(output), deps.fetchFn);
@@ -1,6 +1,6 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { gitAuthConfig } from './git.js';
3
+ import { gitAuthEnv } from './git.js';
4
4
  import { createMergeRequest, ensureRefleetLabel, REFLEET_LABEL } from './gitlab.js';
5
5
  const LAST_USED_MARKER = '.slipway-last-used';
6
6
  function directorySizeBytes(dir) {
@@ -82,10 +82,10 @@ function excludeMarkerFromGit(repoDir) {
82
82
  /** Returns the ready-to-use working directory; throws (GitError) when any git step fails. */
83
83
  export async function syncRepo(workspace, credentials, project) {
84
84
  const repoDir = join(workspace.cacheDir, project.externalId);
85
- const auth = gitAuthConfig(credentials.accessToken);
85
+ const auth = gitAuthEnv(credentials.accessToken);
86
86
  if (existsSync(join(repoDir, '.git'))) {
87
87
  workspace.log(`reusing cached checkout for ${project.path}`);
88
- await workspace.git([...auth, 'fetch', '--prune', 'origin'], repoDir);
88
+ await workspace.git(['fetch', '--prune', 'origin'], repoDir, auth);
89
89
  await workspace.git(['checkout', '-f', project.defaultBranch], repoDir);
90
90
  await workspace.git(['reset', '--hard', `origin/${project.defaultBranch}`], repoDir);
91
91
  await workspace.git(['clean', '-fdx'], repoDir);
@@ -95,7 +95,7 @@ export async function syncRepo(workspace, credentials, project) {
95
95
  evictLruUntilUnderCap(workspace);
96
96
  workspace.log(`cloning ${project.path} (first use on this runner)`);
97
97
  const cloneUrl = `${credentials.baseUrl.replace(/\/+$/, '')}/${project.path}.git`;
98
- await workspace.git([...auth, 'clone', '--origin', 'origin', cloneUrl, repoDir]);
98
+ await workspace.git(['clone', '--origin', 'origin', cloneUrl, repoDir], undefined, auth);
99
99
  }
100
100
  touchMarker(repoDir);
101
101
  excludeMarkerFromGit(repoDir);
@@ -118,7 +118,7 @@ export async function publishChange(workspace, credentials, repoDir, project, sh
118
118
  return null;
119
119
  }
120
120
  await workspace.git(['-c', 'user.name=Slipway', '-c', 'user.email=slipway@localhost', 'commit', '-m', title], repoDir);
121
- await workspace.git([...gitAuthConfig(credentials.accessToken), 'push', '--force', 'origin', `HEAD:refs/heads/${branchName}`], repoDir);
121
+ await workspace.git(['push', '--force', 'origin', `HEAD:refs/heads/${branchName}`], repoDir, gitAuthEnv(credentials.accessToken));
122
122
  // Without the label in place GitLab would create it in a random colour, so the MR
123
123
  // rather goes out unlabelled than off-brand.
124
124
  const labelled = await ensureRefleetLabel(credentials, project.externalId, fetchFn);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@refleet-it/runner",
3
- "version": "0.1.199",
3
+ "version": "0.1.200",
4
4
  "description": "Refleet fleet runner: claims code-modernisation jobs from the Refleet API and runs them through Claude Code or Kiro on your own machine.",
5
5
  "keywords": [
6
6
  "refleet",