mtech-ai-local-runner 0.1.76 → 0.1.77
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 +20 -0
- package/dist/src/cli.js +26 -5
- package/dist/src/commands/fix.js +6 -1
- package/dist/src/daemon/execution-coordinator.js +3 -0
- package/dist/src/workspace/git-client.js +5 -1
- package/dist/src/workspace/repository-access.js +95 -0
- package/dist/src/workspace/workspace-manager.js +17 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -937,6 +937,26 @@ recorded job/generation and deterministic binding reference before restart.
|
|
|
937
937
|
It neither repeats the binding request nor assumes the server did not create it.
|
|
938
938
|
Do not delete that state to force another execution.
|
|
939
939
|
|
|
940
|
+
The workspace stage clones the registered https URL with the employee's own
|
|
941
|
+
`git` and `GIT_TERMINAL_PROMPT=0`. A clone or fetch that Git refuses for a
|
|
942
|
+
missing, wrong or unauthorized login is reported as `REPOSITORY_ACCESS_DENIED`,
|
|
943
|
+
and one that cannot reach the host as `REPOSITORY_UNREACHABLE`; any other
|
|
944
|
+
failure keeps `PREPARATION_FAILED`. Only the code leaves the Mac, never Git's
|
|
945
|
+
output. The Gateway's Fix result message turns these codes into setup advice
|
|
946
|
+
with the `git ls-remote <url> HEAD` check. A Mac that reaches Bitbucket only
|
|
947
|
+
over SSH may map the https URL with `url.<ssh>.insteadOf`: a cached mirror is
|
|
948
|
+
matched by the URL it stores (`git config --get remote.origin.url`), which stays
|
|
949
|
+
the registered https URL, so later jobs reuse it (Work Decision 2ae1acc1).
|
|
950
|
+
|
|
951
|
+
`doctor` and `fix enable` run that check before a job needs it: `git ls-remote
|
|
952
|
+
-- <url> HEAD` for each consented repository, with the same environment as the
|
|
953
|
+
clone, in its own session so neither Git nor ssh can prompt, and ended after 30
|
|
954
|
+
seconds. The clone URLs come from `local_agent_runner_status`
|
|
955
|
+
(`fix_repositories`); an older Gateway without that list skips the check. Both
|
|
956
|
+
report `repository_access` with an action for each repository that is not
|
|
957
|
+
readable. `doctor` exits 1 when one is not readable; `fix enable` still writes
|
|
958
|
+
the consent.
|
|
959
|
+
|
|
940
960
|
These preparation controls end at binding and do not impose time, turn, token
|
|
941
961
|
or cost termination limits on an approved unbounded employee-local Claude run.
|
|
942
962
|
Existing generation fencing, scope restrictions, cancellation and audit remain
|
package/dist/src/cli.js
CHANGED
|
@@ -37,6 +37,7 @@ import { asksDoctorReport } from "./asks/doctor.js";
|
|
|
37
37
|
import { guideDoctorReport, guideReportShown } from "./guide/doctor.js";
|
|
38
38
|
import { runClaudeReleaseCommand } from "./commands/claude-release.js";
|
|
39
39
|
import { runFixCommand } from "./commands/fix.js";
|
|
40
|
+
import { checkRepositoryAccess, fixRepositoryList } from "./workspace/repository-access.js";
|
|
40
41
|
function hookAction(result) {
|
|
41
42
|
return result.reason === "invalid_command"
|
|
42
43
|
? "Run the runner with a Node executable and package path outside the workspace, without quotes, $, backquotes or backslashes, then rerun doctor."
|
|
@@ -184,12 +185,24 @@ export async function main(argv, env, io = {}) {
|
|
|
184
185
|
// Offline Claude release review (Work Item 1c8026f1, P2c): no runner config, daemon or Gateway call.
|
|
185
186
|
if (argv[0] === "claude-release")
|
|
186
187
|
return runClaudeReleaseCommand(argv.slice(1), { stdout, stderr, fetchFn: io.fetchFn ?? fetch, ...io.claudeRelease });
|
|
187
|
-
// Fix consent (Gateway decision c5ed3a14)
|
|
188
|
+
// Fix consent (Gateway decision c5ed3a14) is written on this Mac alone; it restarts the runner only when idle. Afterwards
|
|
189
|
+
// one Gateway read names the repositories' clone URLs, so this Mac's git can be checked against them (Work Item 6f9e64ae).
|
|
188
190
|
if (argv[0] === "fix") {
|
|
189
191
|
const stateRoot = defaultStateRootFromEnv(env);
|
|
190
192
|
const launchd = io.launchdService ?? new LaunchdService({ home: env.HOME ?? "", uid: process.getuid?.() ?? 0, path: env.PATH });
|
|
193
|
+
const fetchFn = io.fetchFn ?? fetch;
|
|
191
194
|
return runFixCommand(argv.slice(1), { stdout, stderr, stateRoot, loadLocalConfig: () => new FileLocalConfigStore(stateRoot).load(),
|
|
192
|
-
restartService: () => launchd.start(), serviceLoaded: async () => (await launchd.status()).loaded
|
|
195
|
+
restartService: () => launchd.start(), serviceLoaded: async () => (await launchd.status()).loaded,
|
|
196
|
+
fixRepositories: async () => {
|
|
197
|
+
const local = await new FileLocalConfigStore(stateRoot).load();
|
|
198
|
+
if (!local)
|
|
199
|
+
return undefined;
|
|
200
|
+
const tokenProvider = new OAuthTokenProvider({ store: new FileOAuthCredentialStore(stateRoot), oauthClient: new OAuthClient({ resource: local.gatewayMcpUrl, fetchFn }),
|
|
201
|
+
resource: local.gatewayMcpUrl, tokenOverride: env.AI_GATEWAY_LOCAL_RUNNER_TOKEN, clock: () => Date.now() });
|
|
202
|
+
const status = await new McpGatewayClient({ mcpUrl: local.gatewayMcpUrl, tokenProvider, runnerId: local.runnerId, fetchFn }).localRunnerStatus();
|
|
203
|
+
return fixRepositoryList(status.runner);
|
|
204
|
+
},
|
|
205
|
+
...(io.probeRepository ? { probeRepository: io.probeRepository } : {}) });
|
|
193
206
|
}
|
|
194
207
|
let config;
|
|
195
208
|
try {
|
|
@@ -341,15 +354,23 @@ export async function main(argv, env, io = {}) {
|
|
|
341
354
|
const [gateway] = oauth.status === "fulfilled"
|
|
342
355
|
? await Promise.allSettled([new McpGatewayClient({ mcpUrl: local.gatewayMcpUrl, tokenProvider, runnerId: local.runnerId, fetchFn }).localRunnerStatus()])
|
|
343
356
|
: [{ status: "rejected", reason: "Authentication is unavailable" }];
|
|
357
|
+
// A Fix job checks out with this Mac's own git and no prompts: read each consented repository the same way now
|
|
358
|
+
// (Work Item 6f9e64ae). Only for a Fix host, and only when the Gateway lists the clone URLs; an older one leaves it out.
|
|
359
|
+
const runnerStatus = gateway.status === "fulfilled" ? gateway.value.runner : undefined;
|
|
360
|
+
const listed = fixRepositoryList(runnerStatus);
|
|
361
|
+
const consented = Array.isArray(runnerStatus?.local_fix?.repository_keys) ? runnerStatus.local_fix.repository_keys.filter((key) => typeof key === "string") : [];
|
|
362
|
+
const repositoryAccessPromise = listed === undefined || consented.length === 0 ? Promise.resolve(undefined) : checkRepositoryAccess(consented, listed, io.probeRepository);
|
|
344
363
|
const [service, workspace] = await localChecks;
|
|
345
364
|
const hook = await hookCheck;
|
|
346
365
|
const asks = await asksCheck;
|
|
347
366
|
const guide = await guideCheck;
|
|
367
|
+
const repositoryAccess = await repositoryAccessPromise;
|
|
368
|
+
const readable = repositoryAccess === undefined || repositoryAccess.every(check => check.status === "readable");
|
|
348
369
|
const checks = [oauth, gateway, service, workspace];
|
|
349
370
|
const loaded = service.status === "fulfilled" && service.value.loaded;
|
|
350
|
-
const ok = checks.every(check => check.status === "fulfilled") && loaded && (hook === null || (hasUsableAdapter(adapters) && hook.enforcing));
|
|
351
|
-
stdout(JSON.stringify({ ok, configured: true, adapters, checks: { runner_registration: true, oauth: oauth.status === "fulfilled", gateway: gateway.status === "fulfilled", workspace: workspace.status === "fulfilled", launchd: loaded, ...(hook === null ? {} : { hook: hook.enforcing }) },
|
|
352
|
-
hook: hook === null ? { status: "not_required", reason: "messenger_mode" } : hookReport(hook), ...asks, ...(guideReportShown(guide) ? { guide } : {}), ...(gateway.status === "fulfilled" ? { runner: gateway.value } : {}), ...(service.status === "fulfilled" ? { launchd: service.value } : {}) }));
|
|
371
|
+
const ok = checks.every(check => check.status === "fulfilled") && loaded && readable && (hook === null || (hasUsableAdapter(adapters) && hook.enforcing));
|
|
372
|
+
stdout(JSON.stringify({ ok, configured: true, adapters, checks: { runner_registration: true, oauth: oauth.status === "fulfilled", gateway: gateway.status === "fulfilled", workspace: workspace.status === "fulfilled", launchd: loaded, ...(hook === null ? {} : { hook: hook.enforcing }), ...(repositoryAccess === undefined ? {} : { repository_access: readable }) },
|
|
373
|
+
hook: hook === null ? { status: "not_required", reason: "messenger_mode" } : hookReport(hook), ...asks, ...(guideReportShown(guide) ? { guide } : {}), ...(repositoryAccess === undefined ? {} : { repository_access: repositoryAccess }), ...(gateway.status === "fulfilled" ? { runner: gateway.value } : {}), ...(service.status === "fulfilled" ? { launchd: service.value } : {}) }));
|
|
353
374
|
return ok ? 0 : 1;
|
|
354
375
|
}
|
|
355
376
|
if (config.mode === 'manual-codex') {
|
package/dist/src/commands/fix.js
CHANGED
|
@@ -7,6 +7,7 @@ import { measureCommandFile } from '../safety/registered-command-integrity.js';
|
|
|
7
7
|
import { assertPinnedGit } from '../publication/git-credential-broker.js';
|
|
8
8
|
import { assertLocalFixConsent } from '../publication/installed-host.js';
|
|
9
9
|
import { runnerMode } from '../registration/local-config.js';
|
|
10
|
+
import { checkRepositoryAccess } from '../workspace/repository-access.js';
|
|
10
11
|
export const FIX_USAGE = 'Usage: mtec-ai-local-runner fix enable --projects <key[,key...]> [--modes gateway_publish_v1[,gateway_ci_v1]] [--git <path>]\n'
|
|
11
12
|
+ ' mtec-ai-local-runner fix disable\n'
|
|
12
13
|
+ ' mtec-ai-local-runner fix status';
|
|
@@ -202,5 +203,9 @@ export async function runFixCommand(argv, io) {
|
|
|
202
203
|
io.stderr('FIX_CONSENT_INVALID: give registered repository keys (lowercase, comma separated, no repeats) and modes gateway_publish_v1 or gateway_ci_v1.');
|
|
203
204
|
return 1;
|
|
204
205
|
}
|
|
205
|
-
|
|
206
|
+
// A job checks out with this Mac's own git and no prompts: say now when it cannot read a repository (Work Item 6f9e64ae).
|
|
207
|
+
// The consent stands either way; the employee may set the login up afterwards.
|
|
208
|
+
const listed = await io.fixRepositories?.().catch(() => undefined);
|
|
209
|
+
const access = listed === undefined ? undefined : await checkRepositoryAccess(applied.consent.repository_keys, listed, io.probeRepository);
|
|
210
|
+
return restart(io, { ok: true, repository_keys: applied.consent.repository_keys, verification_modes: applied.consent.verification_modes, ...(access ? { repository_access: access } : {}) });
|
|
206
211
|
}
|
|
@@ -13,6 +13,7 @@ import { completionReportDiagnostic } from "../adapters/completion-report.js";
|
|
|
13
13
|
import { PROTOCOL_VERSION, isTerminalEventType, validateEvent, validateAttemptEvidence, validateAttemptHandoff, MAX_EVENT_SUMMARY_CHARS } from "../protocol.js";
|
|
14
14
|
import { DAEMON_BLOCKED_REASONS, DAEMON_CANCELLED_REASONS } from "../state-store.js";
|
|
15
15
|
import { PreparationDeadlineError, PreparationLease, validateJobLease } from './preparation-lease.js';
|
|
16
|
+
import { RepositoryAccessError } from '../workspace/repository-access.js';
|
|
16
17
|
function canonical(value) {
|
|
17
18
|
if (Array.isArray(value))
|
|
18
19
|
return value.map(canonical);
|
|
@@ -196,6 +197,8 @@ export class ExecutionCoordinator {
|
|
|
196
197
|
diagnostic.code = "LOCAL_FIX_HOST_REQUIRED";
|
|
197
198
|
if (error instanceof PreparationDeadlineError)
|
|
198
199
|
diagnostic.code = "PREPARATION_DEADLINE_EXCEEDED";
|
|
200
|
+
if (error instanceof RepositoryAccessError)
|
|
201
|
+
diagnostic.code = error.code;
|
|
199
202
|
if (error instanceof GatewayToolError) {
|
|
200
203
|
diagnostic.tool = error.toolName;
|
|
201
204
|
diagnostic.code = error.code;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
const execFileAsync = promisify(execFile);
|
|
4
|
+
/** The employee's own Git environment with prompts off: a job's Git never waits for a person. */
|
|
5
|
+
export function gitEnvironment() {
|
|
6
|
+
return { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
7
|
+
}
|
|
4
8
|
export class GitClient {
|
|
5
9
|
async run(args, options = {}) {
|
|
6
10
|
const result = await execFileAsync("git", args, {
|
|
7
11
|
cwd: options.cwd,
|
|
8
12
|
encoding: "utf8",
|
|
9
13
|
maxBuffer: 10 * 1024 * 1024,
|
|
10
|
-
env:
|
|
14
|
+
env: gitEnvironment(),
|
|
11
15
|
});
|
|
12
16
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
13
17
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { gitEnvironment } from "./git-client.js";
|
|
3
|
+
export class RepositoryAccessError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code) {
|
|
6
|
+
super(code === "REPOSITORY_ACCESS_DENIED" ? "Git could not read the registered repository without a credential prompt" : "Git could not reach the registered repository");
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
// No credential (GIT_TERMINAL_PROMPT=0 refused to ask), a refused one, or an account without access to the repository.
|
|
11
|
+
const DENIED = [
|
|
12
|
+
/terminal prompts disabled/i, /could not read (?:username|password)/i, /authentication failed/i, /invalid credentials/i,
|
|
13
|
+
/permission denied \(publickey/i, /host key verification failed/i, /returned error: 40[13]\b/i,
|
|
14
|
+
/you may not have access to this repository/i, /repository '[^'\n]*' not found/i, /repository not found/i,
|
|
15
|
+
];
|
|
16
|
+
// Name resolution, routing or a connection that never completed; ssh and https word these differently.
|
|
17
|
+
const UNREACHABLE = [
|
|
18
|
+
/could not resolve host/i, /failed to connect to/i, /couldn't connect to server/i, /connection timed out/i,
|
|
19
|
+
/operation timed out/i, /connection refused/i, /network is unreachable/i,
|
|
20
|
+
];
|
|
21
|
+
/** The access code a failed Git command's stderr shows, or undefined for any other failure. */
|
|
22
|
+
export function repositoryAccessFailure(error) {
|
|
23
|
+
const stderr = typeof error === "object" && error !== null ? error.stderr : undefined;
|
|
24
|
+
if (typeof stderr !== "string" || stderr === "")
|
|
25
|
+
return undefined;
|
|
26
|
+
if (DENIED.some(pattern => pattern.test(stderr)))
|
|
27
|
+
return "REPOSITORY_ACCESS_DENIED";
|
|
28
|
+
if (UNREACHABLE.some(pattern => pattern.test(stderr)))
|
|
29
|
+
return "REPOSITORY_UNREACHABLE";
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `git ls-remote <url> HEAD` with the environment a job's clone uses, so a missing credential shows before a job runs.
|
|
34
|
+
* It runs in its own session, so neither Git nor ssh can open the terminal to ask, and the whole group ends at the
|
|
35
|
+
* deadline. It reads refs only and writes nothing.
|
|
36
|
+
*/
|
|
37
|
+
export function probeRepositoryAccess(providerUrl, options = {}) {
|
|
38
|
+
return new Promise(resolve => {
|
|
39
|
+
// "--" keeps a URL from ever reading as an option such as --upload-pack.
|
|
40
|
+
const child = spawn("git", ["ls-remote", "--exit-code", "--", providerUrl, "HEAD"], { detached: true, stdio: ["ignore", "ignore", "pipe"], env: gitEnvironment() });
|
|
41
|
+
let stderr = "", settled = false;
|
|
42
|
+
const finish = (result) => { if (settled)
|
|
43
|
+
return; settled = true; clearTimeout(deadline); resolve(result); };
|
|
44
|
+
const deadline = setTimeout(() => {
|
|
45
|
+
try {
|
|
46
|
+
process.kill(-child.pid, "SIGKILL");
|
|
47
|
+
}
|
|
48
|
+
catch { /* The group already ended. */ }
|
|
49
|
+
finish({ status: "unreachable", code: "REPOSITORY_UNREACHABLE" });
|
|
50
|
+
}, options.timeoutMs ?? 30_000);
|
|
51
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => { if (stderr.length < 64 * 1024)
|
|
52
|
+
stderr += chunk; });
|
|
53
|
+
child.on("error", () => finish({ status: "failed" }));
|
|
54
|
+
child.on("close", code => {
|
|
55
|
+
// Exit 2 is --exit-code's "no HEAD": the credential worked.
|
|
56
|
+
if (code === 0 || code === 2)
|
|
57
|
+
return finish({ status: "readable" });
|
|
58
|
+
const access = repositoryAccessFailure({ stderr });
|
|
59
|
+
finish(access === "REPOSITORY_ACCESS_DENIED" ? { status: "denied", code: access } : access === "REPOSITORY_UNREACHABLE" ? { status: "unreachable", code: access } : { status: "failed" });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const KEY = /^[a-z0-9][a-z0-9._-]{0,99}$/;
|
|
64
|
+
// The Gateway's registered clone URLs: https, no account in the URL, nothing a shell or Git option parser could read.
|
|
65
|
+
const URL_PATTERN = /^https:\/\/[A-Za-z0-9.-]+\/[A-Za-z0-9._\/-]+$/;
|
|
66
|
+
/** The Gateway's list of Fix repositories from local_agent_runner_status, or undefined when this Gateway has none. */
|
|
67
|
+
export function fixRepositoryList(runner) {
|
|
68
|
+
const list = typeof runner === "object" && runner !== null ? runner.fix_repositories : undefined;
|
|
69
|
+
if (!Array.isArray(list))
|
|
70
|
+
return undefined;
|
|
71
|
+
return list.slice(0, 100).filter((entry) => typeof entry === "object" && entry !== null
|
|
72
|
+
&& typeof entry.repository_key === "string" && KEY.test(entry.repository_key)
|
|
73
|
+
&& typeof entry.provider_url === "string" && URL_PATTERN.test(entry.provider_url))
|
|
74
|
+
.map(entry => ({ repository_key: entry.repository_key, provider_url: entry.provider_url }));
|
|
75
|
+
}
|
|
76
|
+
/** Probe each asked repository the Gateway lists, at most four at a time, and say what to do about each failure. */
|
|
77
|
+
export async function checkRepositoryAccess(keys, listed, probe = probeRepositoryAccess) {
|
|
78
|
+
const checks = [];
|
|
79
|
+
for (let start = 0; start < keys.length; start += 4) {
|
|
80
|
+
checks.push(...await Promise.all(keys.slice(start, start + 4).map(async (key) => {
|
|
81
|
+
const url = listed.find(entry => entry.repository_key === key)?.provider_url;
|
|
82
|
+
if (url === undefined)
|
|
83
|
+
return { repository_key: key, status: "not_registered", action: `${key} has no reviewed Fix profile on the Gateway; check the repository key.` };
|
|
84
|
+
const result = await probe(url);
|
|
85
|
+
const check = `git ls-remote ${url} HEAD`;
|
|
86
|
+
switch (result.status) {
|
|
87
|
+
case "readable": return { repository_key: key, status: "readable" };
|
|
88
|
+
case "denied": return { repository_key: key, status: "denied", action: `Set up a Bitbucket login for git on this Mac so that this prints a commit without a prompt: ${check}` };
|
|
89
|
+
case "unreachable": return { repository_key: key, status: "unreachable", action: `Check the network or VPN, then run: ${check}` };
|
|
90
|
+
default: return { repository_key: key, status: "failed", action: `Run ${check} to see why git cannot read it.` };
|
|
91
|
+
}
|
|
92
|
+
})));
|
|
93
|
+
}
|
|
94
|
+
return checks;
|
|
95
|
+
}
|
|
@@ -5,6 +5,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
5
5
|
import { resolve, join, sep, dirname } from "node:path";
|
|
6
6
|
import { assertOwnedGitObjectStorage, RegisteredSubmoduleMaterializer } from './registered-submodules.js';
|
|
7
7
|
import { isP1WorkingBranch } from '../publication/git-snapshot.js';
|
|
8
|
+
import { repositoryAccessFailure, RepositoryAccessError } from './repository-access.js';
|
|
8
9
|
export class WorkspaceManager {
|
|
9
10
|
deps;
|
|
10
11
|
submodules;
|
|
@@ -84,19 +85,26 @@ export class WorkspaceManager {
|
|
|
84
85
|
return this.deps.git.run([...(attempt ? ['--no-replace-objects'] : []), ...args]);
|
|
85
86
|
};
|
|
86
87
|
if (await this.exists(mirrorPath)) {
|
|
87
|
-
|
|
88
|
+
// The stored URL, not `remote get-url`: that expands the employee's url.<x>.insteadOf, so a Mac that reaches the
|
|
89
|
+
// registered https URL over SSH would fail every job after its first clone (Work Decision 2ae1acc1).
|
|
90
|
+
const remote = (await managedGit(["--git-dir", mirrorPath, "config", "--get", "remote.origin.url"])).stdout.trim();
|
|
88
91
|
if (remote !== input.providerUrl)
|
|
89
92
|
throw new Error("Managed repository remote does not match the registered provider");
|
|
90
93
|
}
|
|
91
94
|
else {
|
|
92
|
-
|
|
95
|
+
try {
|
|
96
|
+
await managedGit(["clone", "--mirror", input.providerUrl, mirrorPath]);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw this.accessError(error) ?? error;
|
|
100
|
+
}
|
|
93
101
|
}
|
|
94
102
|
try {
|
|
95
103
|
await managedGit(["--git-dir", mirrorPath, "fetch", "--prune", "origin", input.baseCommitSha]);
|
|
96
104
|
await managedGit(["--git-dir", mirrorPath, "cat-file", "-e", `${input.baseCommitSha}^{commit}`]);
|
|
97
105
|
}
|
|
98
|
-
catch {
|
|
99
|
-
throw new Error(`Repository revision ${input.baseCommitSha} is unavailable`);
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw this.accessError(error) ?? new Error(`Repository revision ${input.baseCommitSha} is unavailable`);
|
|
100
108
|
}
|
|
101
109
|
if (!await this.exists(workspacePath)) {
|
|
102
110
|
await managedGit(["--git-dir", mirrorPath, "worktree", "add", "--detach", workspacePath, input.baseCommitSha]);
|
|
@@ -190,6 +198,11 @@ export class WorkspaceManager {
|
|
|
190
198
|
await handle.close();
|
|
191
199
|
}
|
|
192
200
|
}
|
|
201
|
+
/** A credential or network failure keeps its code (Work Item 6f9e64ae); anything else stays the caller's error. */
|
|
202
|
+
accessError(error) {
|
|
203
|
+
const code = repositoryAccessFailure(error);
|
|
204
|
+
return code === undefined ? undefined : new RepositoryAccessError(code);
|
|
205
|
+
}
|
|
193
206
|
assertContained(root, candidate) {
|
|
194
207
|
const resolved = resolve(candidate);
|
|
195
208
|
if (resolved !== root && !resolved.startsWith(`${root}${sep}`))
|