@mastra/factory 0.16.0-alpha.9 → 0.16.0
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 +37 -0
- package/dist/auth.js +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/boards/review.d.ts.map +1 -1
- package/dist/boards/review.js +19 -8
- package/dist/boards/review.js.map +1 -1
- package/dist/boards/work.d.ts.map +1 -1
- package/dist/boards/work.js +31 -3
- package/dist/boards/work.js.map +1 -1
- package/dist/capabilities/intake.d.ts +4 -0
- package/dist/capabilities/intake.d.ts.map +1 -1
- package/dist/capabilities/version-control.d.ts +15 -0
- package/dist/capabilities/version-control.d.ts.map +1 -1
- package/dist/factory.d.ts +7 -7
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +100 -40
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +3 -0
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +21 -0
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/routes.js +2 -1
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +41 -42
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +416 -227
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/webhook.d.ts +2 -5
- package/dist/integrations/github/webhook.d.ts.map +1 -1
- package/dist/integrations/github/webhook.js +8 -48
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/gitlab/agent-tools.d.ts +14 -0
- package/dist/integrations/gitlab/agent-tools.d.ts.map +1 -0
- package/dist/integrations/gitlab/agent-tools.js +42 -0
- package/dist/integrations/gitlab/agent-tools.js.map +1 -0
- package/dist/integrations/gitlab/api.d.ts +222 -0
- package/dist/integrations/gitlab/api.d.ts.map +1 -0
- package/dist/integrations/gitlab/api.js +274 -0
- package/dist/integrations/gitlab/api.js.map +1 -0
- package/dist/integrations/gitlab/default-rules.d.ts +125 -0
- package/dist/integrations/gitlab/default-rules.d.ts.map +1 -0
- package/dist/integrations/gitlab/default-rules.js +172 -0
- package/dist/integrations/gitlab/default-rules.js.map +1 -0
- package/dist/integrations/gitlab/integration.d.ts +138 -0
- package/dist/integrations/gitlab/integration.d.ts.map +1 -0
- package/dist/integrations/gitlab/integration.js +705 -0
- package/dist/integrations/gitlab/integration.js.map +1 -0
- package/dist/integrations/gitlab/issue-reconciler.d.ts +6 -0
- package/dist/integrations/gitlab/issue-reconciler.d.ts.map +1 -0
- package/dist/integrations/gitlab/issue-reconciler.js +123 -0
- package/dist/integrations/gitlab/issue-reconciler.js.map +1 -0
- package/dist/integrations/gitlab/merge-request-reconciler.d.ts +6 -0
- package/dist/integrations/gitlab/merge-request-reconciler.d.ts.map +1 -0
- package/dist/integrations/gitlab/merge-request-reconciler.js +267 -0
- package/dist/integrations/gitlab/merge-request-reconciler.js.map +1 -0
- package/dist/integrations/gitlab/reconciler.d.ts +5 -0
- package/dist/integrations/gitlab/reconciler.d.ts.map +1 -0
- package/dist/integrations/gitlab/reconciler.js +31 -0
- package/dist/integrations/gitlab/reconciler.js.map +1 -0
- package/dist/integrations/gitlab/reconciliation-config.d.ts +3 -0
- package/dist/integrations/gitlab/reconciliation-config.d.ts.map +1 -0
- package/dist/integrations/gitlab/reconciliation-config.js +17 -0
- package/dist/integrations/gitlab/reconciliation-config.js.map +1 -0
- package/dist/integrations/gitlab/routes.d.ts +20 -0
- package/dist/integrations/gitlab/routes.d.ts.map +1 -0
- package/dist/integrations/gitlab/routes.js +473 -0
- package/dist/integrations/gitlab/routes.js.map +1 -0
- package/dist/integrations/gitlab/rules.d.ts +35 -0
- package/dist/integrations/gitlab/rules.d.ts.map +1 -0
- package/dist/integrations/gitlab/rules.js +391 -0
- package/dist/integrations/gitlab/rules.js.map +1 -0
- package/dist/integrations/gitlab/session-subscriptions.d.ts +32 -0
- package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -0
- package/dist/integrations/gitlab/session-subscriptions.js +192 -0
- package/dist/integrations/gitlab/session-subscriptions.js.map +1 -0
- package/dist/integrations/gitlab/subscriptions.d.ts +70 -0
- package/dist/integrations/gitlab/subscriptions.d.ts.map +1 -0
- package/dist/integrations/gitlab/subscriptions.js +76 -0
- package/dist/integrations/gitlab/subscriptions.js.map +1 -0
- package/dist/integrations/gitlab/version-control.d.ts +16 -0
- package/dist/integrations/gitlab/version-control.d.ts.map +1 -0
- package/dist/integrations/gitlab/version-control.js +622 -0
- package/dist/integrations/gitlab/version-control.js.map +1 -0
- package/dist/integrations/gitlab/webhook-dispatch.d.ts +75 -0
- package/dist/integrations/gitlab/webhook-dispatch.d.ts.map +1 -0
- package/dist/integrations/gitlab/webhook-dispatch.js +235 -0
- package/dist/integrations/gitlab/webhook-dispatch.js.map +1 -0
- package/dist/integrations/gitlab/webhook.d.ts +72 -0
- package/dist/integrations/gitlab/webhook.d.ts.map +1 -0
- package/dist/integrations/gitlab/webhook.js +201 -0
- package/dist/integrations/gitlab/webhook.js.map +1 -0
- package/dist/integrations/incidentio/integration.js +1 -1
- package/dist/integrations/issue-reconciler.d.ts +3 -3
- package/dist/integrations/issue-reconciler.d.ts.map +1 -1
- package/dist/integrations/issue-reconciler.js +22 -2
- package/dist/integrations/issue-reconciler.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +21 -0
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/gitlab/event-worker.d.ts +64 -0
- package/dist/integrations/platform/gitlab/event-worker.d.ts.map +1 -0
- package/dist/integrations/platform/gitlab/event-worker.js +306 -0
- package/dist/integrations/platform/gitlab/event-worker.js.map +1 -0
- package/dist/integrations/platform/gitlab/integration.d.ts +57 -0
- package/dist/integrations/platform/gitlab/integration.d.ts.map +1 -0
- package/dist/integrations/platform/gitlab/integration.js +125 -0
- package/dist/integrations/platform/gitlab/integration.js.map +1 -0
- package/dist/integrations/platform/incidentio/integration.js +1 -1
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +3 -3
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts +2 -0
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +21 -7
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/subscription-session.d.ts +37 -0
- package/dist/integrations/subscription-session.d.ts.map +1 -0
- package/dist/integrations/subscription-session.js +83 -0
- package/dist/integrations/subscription-session.js.map +1 -0
- package/dist/packages/_internals/workspace/dist/index.js +1 -11
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/routes/intake.d.ts.map +1 -1
- package/dist/routes/intake.js +4 -2
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/oauth.js +1 -1
- package/dist/routes/projects.d.ts +1 -0
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +1 -0
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +20 -0
- package/dist/routes/source-control-sessions.d.ts.map +1 -0
- package/dist/routes/source-control-sessions.js +319 -0
- package/dist/routes/source-control-sessions.js.map +1 -0
- package/dist/routes/source-control-settings.d.ts +10 -0
- package/dist/routes/source-control-settings.d.ts.map +1 -0
- package/dist/routes/source-control-settings.js +87 -0
- package/dist/routes/source-control-settings.js.map +1 -0
- package/dist/routes/surface.d.ts +3 -3
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +55 -13
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.d.ts +8 -0
- package/dist/routes/tenant-credentials.d.ts.map +1 -1
- package/dist/routes/tenant-credentials.js +20 -3
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +1 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +7 -1
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts +1 -1
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +3 -2
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +6 -3
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +67 -3
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +23 -2
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.d.ts.map +1 -1
- package/dist/rules/validation.js +2 -0
- package/dist/rules/validation.js.map +1 -1
- package/dist/sandbox/git-ref.d.ts +7 -0
- package/dist/sandbox/git-ref.d.ts.map +1 -0
- package/dist/sandbox/git-ref.js +15 -0
- package/dist/sandbox/git-ref.js.map +1 -0
- package/dist/sandbox/session-retirement.js +1 -1
- package/dist/sandbox/workdir.d.ts.map +1 -1
- package/dist/sandbox/workdir.js +6 -6
- package/dist/sandbox/workdir.js.map +1 -1
- package/dist/session/factory-session.d.ts +31 -1
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +60 -1
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/source-control-tools.d.ts +16 -0
- package/dist/session/source-control-tools.d.ts.map +1 -0
- package/dist/session/source-control-tools.js +464 -0
- package/dist/session/source-control-tools.js.map +1 -0
- package/dist/storage/domains/intake/base.d.ts +18 -0
- package/dist/storage/domains/intake/base.d.ts.map +1 -1
- package/dist/storage/domains/intake/base.js +77 -0
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/work-item-branch.d.ts +3 -1
- package/dist/work-item-branch.d.ts.map +1 -1
- package/dist/work-item-branch.js +22 -4
- package/dist/work-item-branch.js.map +1 -1
- package/dist/workspace.d.ts +15 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +57 -23
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-gitlab-rereview/SKILL.md +19 -0
- package/factory-skills/factory-gitlab-review/SKILL.md +30 -0
- package/factory-skills/factory-rereview/SKILL.md +13 -6
- package/factory-skills/factory-review/SKILL.md +13 -6
- package/factory-skills/factory-review/references/archaeology.md +225 -0
- package/factory-skills/factory-review/references/categories/README.md +34 -0
- package/factory-skills/factory-review/references/categories/behavior-change.md +36 -0
- package/factory-skills/factory-review/references/categories/bug-fix.md +37 -0
- package/factory-skills/factory-review/references/categories/docs.md +32 -0
- package/factory-skills/factory-review/references/categories/experimental-flagged.md +34 -0
- package/factory-skills/factory-review/references/categories/infra-tooling.md +37 -0
- package/factory-skills/factory-review/references/categories/internal-capability.md +35 -0
- package/factory-skills/factory-review/references/categories/large-cross-cutting.md +35 -0
- package/factory-skills/factory-review/references/categories/mechanical.md +36 -0
- package/factory-skills/factory-review/references/categories/performance.md +33 -0
- package/factory-skills/factory-review/references/categories/public-api.md +38 -0
- package/factory-skills/factory-review/references/categories/refactor.md +35 -0
- package/factory-skills/factory-review/references/categories/revert.md +31 -0
- package/factory-skills/factory-review/references/categories/schema-storage.md +39 -0
- package/factory-skills/factory-review/references/categories/security.md +35 -0
- package/factory-skills/factory-review/references/categories/tests-only.md +38 -0
- package/package.json +9 -9
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import { timedPhase } from "../../timing.js";
|
|
2
|
-
import {
|
|
2
|
+
import { isValidGitRef } from "../../sandbox/git-ref.js";
|
|
3
3
|
//#region src/integrations/github/sandbox.ts
|
|
4
4
|
/**
|
|
5
|
-
* Repo materialization for GitHub-backed repositories.
|
|
6
|
-
*
|
|
7
|
-
* A GitHub repo is never cloned onto the server host. The repo is cloned
|
|
8
|
-
* *inside* the session's sandbox, so the agent's file tools and command tools
|
|
9
|
-
* operate entirely against the remote checkout.
|
|
10
|
-
*
|
|
11
|
-
* - `materializeRepo(row, token)` clones the repo inside the sandbox when no
|
|
12
|
-
* checkout exists yet (a base-image boot, a wiped disk), using a short-lived
|
|
13
|
-
* installation token that is scrubbed from the git remote afterwards so it
|
|
14
|
-
* never persists in the VM. A checkout that is already there, from a repo
|
|
15
|
-
* template image or an earlier start, is left exactly as it is.
|
|
16
|
-
*
|
|
17
|
-
* This module owns everything git/GitHub: clone, commit/push, setup/teardown commands,
|
|
18
|
-
* and `gh pr create`. Workdir layout lives in `../sandbox/workdir`.
|
|
19
|
-
*/
|
|
20
|
-
/**
|
|
21
5
|
* Single-quote a string for safe POSIX shell interpolation. Wraps the value in
|
|
22
6
|
* single quotes and escapes any embedded single quote using the canonical
|
|
23
7
|
* close-quote / escaped-quote / reopen-quote sequence (`'\''`). This is the
|
|
@@ -38,6 +22,7 @@ const DEFAULT_COMMAND_TIMEOUT_MS = 15 * 6e4;
|
|
|
38
22
|
const CHECKOUT_COMMAND_TIMEOUT_MS = 5 * 6e4;
|
|
39
23
|
/**
|
|
40
24
|
* A thrown transport-level failure that is worth retrying: remote sandbox
|
|
25
|
+
|
|
41
26
|
* providers (e.g. the platform workspace proxy) surface transient 5xx errors
|
|
42
27
|
* as exceptions carrying an HTTP `status` — typically while a freshly
|
|
43
28
|
* provisioned VM is still coming up. Command failures are NOT exceptions
|
|
@@ -51,18 +36,24 @@ function isTransientTransportError(error) {
|
|
|
51
36
|
const SH_RETRIES = 2;
|
|
52
37
|
const SH_RETRY_DELAY_MS = 2e3;
|
|
53
38
|
/**
|
|
54
|
-
* Run a shell script in the sandbox via `sh -c
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* to re-run. Hang-guard timeouts are NOT retried — the budget applies to the
|
|
58
|
-
* command as a whole.
|
|
39
|
+
* Run a shell script in the sandbox via `sh -c`. This is reserved for the
|
|
40
|
+
* explicitly configured repository lifecycle hooks; programmatic commands use
|
|
41
|
+
* {@link execute} with an argv array.
|
|
59
42
|
*/
|
|
60
43
|
async function sh(sandbox, script, options = {}) {
|
|
44
|
+
return execute(sandbox, "sh", ["-c", script], options);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Execute one program with an argv array, bounded by a hang guard. Transient
|
|
48
|
+
* sandbox transport failures are retried without ever interpolating arguments
|
|
49
|
+
* into a shell command.
|
|
50
|
+
*/
|
|
51
|
+
async function execute(sandbox, command, args, options = {}) {
|
|
61
52
|
const deadlineMs = Date.now() + (options.timeoutMs ?? 9e5);
|
|
62
53
|
for (let attempt = 0;; attempt++) {
|
|
63
54
|
const started = performance.now();
|
|
64
55
|
try {
|
|
65
|
-
const result = await
|
|
56
|
+
const result = await executeOnce(sandbox, command, args, {
|
|
66
57
|
...options,
|
|
67
58
|
timeoutMs: Math.max(deadlineMs - Date.now(), 1)
|
|
68
59
|
});
|
|
@@ -77,8 +68,8 @@ async function sh(sandbox, script, options = {}) {
|
|
|
77
68
|
}
|
|
78
69
|
}
|
|
79
70
|
}
|
|
80
|
-
/** Single
|
|
81
|
-
async function
|
|
71
|
+
/** Single structured command execution attempt, bounded by the hang guard. */
|
|
72
|
+
async function executeOnce(sandbox, command, args, options) {
|
|
82
73
|
const timeoutMs = options.timeoutMs ?? 9e5;
|
|
83
74
|
let timer;
|
|
84
75
|
const hangGuard = new Promise((_, reject) => {
|
|
@@ -89,7 +80,11 @@ async function shOnce(sandbox, script, options) {
|
|
|
89
80
|
timer.unref?.();
|
|
90
81
|
});
|
|
91
82
|
try {
|
|
92
|
-
return await Promise.race([sandbox.executeCommand(
|
|
83
|
+
return await Promise.race([sandbox.executeCommand(command, args, {
|
|
84
|
+
timeout: timeoutMs,
|
|
85
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
86
|
+
...options.env !== void 0 ? { env: options.env } : {}
|
|
87
|
+
}), hangGuard]);
|
|
93
88
|
} finally {
|
|
94
89
|
clearTimeout(timer);
|
|
95
90
|
}
|
|
@@ -123,16 +118,16 @@ function isTransientGitFailure(result) {
|
|
|
123
118
|
* `beforeRetry` lets a call site clear whatever the aborted attempt left
|
|
124
119
|
* behind — a half-written clone directory blocks the next `git clone` outright.
|
|
125
120
|
*/
|
|
126
|
-
async function gitTransfer(sandbox,
|
|
127
|
-
const { beforeRetry, ...
|
|
128
|
-
const deadlineMs = Date.now() + (
|
|
121
|
+
async function gitTransfer(sandbox, args, options = {}) {
|
|
122
|
+
const { beforeRetry, ...commandOptions } = options;
|
|
123
|
+
const deadlineMs = Date.now() + (commandOptions.timeoutMs ?? 9e5);
|
|
129
124
|
for (let attempt = 0;; attempt++) {
|
|
130
|
-
const result = await
|
|
131
|
-
...
|
|
125
|
+
const result = await execute(sandbox, "git", args, {
|
|
126
|
+
...commandOptions,
|
|
132
127
|
timeoutMs: Math.max(deadlineMs - Date.now(), 1)
|
|
133
128
|
});
|
|
134
129
|
if (result.exitCode === 0 || attempt >= GIT_TRANSFER_RETRIES || !isTransientGitFailure(result)) return result;
|
|
135
|
-
process.stderr.write(`[factory:timing] git ${
|
|
130
|
+
process.stderr.write(`[factory:timing] git ${commandOptions.phase ?? "transfer"} retrying after attempt ${attempt + 1}\n`);
|
|
136
131
|
const delayMs = GIT_TRANSFER_RETRY_DELAY_MS * (attempt + 1);
|
|
137
132
|
if (deadlineMs - Date.now() <= delayMs) return result;
|
|
138
133
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
@@ -148,21 +143,72 @@ var MaterializeError = class extends Error {
|
|
|
148
143
|
this.name = "MaterializeError";
|
|
149
144
|
}
|
|
150
145
|
};
|
|
146
|
+
function cleanUrl(repoFullName) {
|
|
147
|
+
return `https://github.com/${repoFullName}.git`;
|
|
148
|
+
}
|
|
149
|
+
function credentialScope(cloneUrl, code = "clone-failed") {
|
|
150
|
+
let url;
|
|
151
|
+
try {
|
|
152
|
+
url = new URL(cloneUrl);
|
|
153
|
+
} catch {
|
|
154
|
+
throw new MaterializeError("Refusing to configure credentials for an invalid repository clone URL.", code);
|
|
155
|
+
}
|
|
156
|
+
if (url.protocol !== "https:" || !url.hostname || url.username || url.password || url.search || url.hash) throw new MaterializeError("Refusing to configure credentials for an invalid repository clone URL.", code);
|
|
157
|
+
const repositoryPath = url.pathname.replace(/\/+$/, "");
|
|
158
|
+
if (!repositoryPath || repositoryPath === "/") throw new MaterializeError("Refusing to configure credentials for an invalid repository clone URL.", code);
|
|
159
|
+
return url.origin + repositoryPath;
|
|
160
|
+
}
|
|
151
161
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
162
|
+
* Provide HTTP Basic credentials to one Git process without putting a secret
|
|
163
|
+
* in argv, a remote URL, or persistent git config. Git reads the URL-scoped
|
|
164
|
+
* extra header from its process environment and discards it on exit.
|
|
154
165
|
*/
|
|
155
|
-
function
|
|
156
|
-
|
|
166
|
+
function gitAuthenticationEnvironment(cloneUrl, token, username, code = "clone-failed") {
|
|
167
|
+
const scope = credentialScope(cloneUrl, code);
|
|
168
|
+
if (!token || !username) throw new MaterializeError("Repository access did not include usable credentials.", code);
|
|
169
|
+
const authorization = Buffer.from(`${username}:${token}`, "utf8").toString("base64");
|
|
170
|
+
return {
|
|
171
|
+
GIT_CONFIG_COUNT: "1",
|
|
172
|
+
GIT_CONFIG_KEY_0: `http.${scope}.extraHeader`,
|
|
173
|
+
GIT_CONFIG_VALUE_0: `Authorization: Basic ${authorization}`,
|
|
174
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
175
|
+
};
|
|
157
176
|
}
|
|
158
|
-
function
|
|
159
|
-
|
|
177
|
+
function normalizedRemoteUrl(value) {
|
|
178
|
+
let url;
|
|
179
|
+
try {
|
|
180
|
+
url = new URL(value);
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
if (url.protocol !== "https:" || !url.hostname || url.search || url.hash) return null;
|
|
185
|
+
const pathname = url.pathname.replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
186
|
+
const repositoryPath = url.hostname.toLowerCase() === "github.com" ? pathname.toLowerCase() : pathname;
|
|
187
|
+
return "https://" + url.host.toLowerCase() + repositoryPath;
|
|
188
|
+
}
|
|
189
|
+
async function clearWorkdir(sandbox, workdir) {
|
|
190
|
+
const mkdir = await execute(sandbox, "mkdir", ["-p", workdir]);
|
|
191
|
+
if (mkdir.exitCode !== 0) throw classifyGitFailure(mkdir, "clone-failed");
|
|
192
|
+
const clear = await execute(sandbox, "find", [
|
|
193
|
+
workdir,
|
|
194
|
+
"-mindepth",
|
|
195
|
+
"1",
|
|
196
|
+
"-maxdepth",
|
|
197
|
+
"1",
|
|
198
|
+
"-exec",
|
|
199
|
+
"rm",
|
|
200
|
+
"-rf",
|
|
201
|
+
"--",
|
|
202
|
+
"{}",
|
|
203
|
+
"+"
|
|
204
|
+
]);
|
|
205
|
+
if (clear.exitCode !== 0) throw classifyGitFailure(clear, "clone-failed");
|
|
160
206
|
}
|
|
161
207
|
/**
|
|
162
208
|
* Materialize the repo inside the user's sandbox: clone when no checkout of
|
|
163
|
-
* this repo exists, otherwise nothing.
|
|
164
|
-
* remote
|
|
165
|
-
* binding row.
|
|
209
|
+
* this repo exists, otherwise nothing. Credentials are process-scoped and the
|
|
210
|
+
* stored remote remains credential-free. Sets `materialized_at` on the
|
|
211
|
+
* per-user sandbox binding row.
|
|
166
212
|
*/
|
|
167
213
|
async function materializeRepo(options) {
|
|
168
214
|
return timedPhase("workspace.materialize", () => materializeRepoImpl(options));
|
|
@@ -171,35 +217,33 @@ async function materializeRepoImpl(options) {
|
|
|
171
217
|
const { row: sandboxRow, repoInfo, sandbox, token, storage } = options;
|
|
172
218
|
const workdir = sandboxRow.sandboxWorkdir;
|
|
173
219
|
const repo = repoInfo.repoFullName;
|
|
174
|
-
if (!/^[\w.-]
|
|
220
|
+
if (!/^[\w.-]+(?:\/[\w.-]+)+$/.test(repo)) throw new MaterializeError(`Refusing to materialize: invalid repo full name '${repo}'.`, "clone-failed");
|
|
175
221
|
if (!/^[A-Za-z0-9_./-]+$/.test(repoInfo.defaultBranch)) throw new MaterializeError(`Refusing to materialize: invalid default branch '${repoInfo.defaultBranch}'.`, "clone-failed");
|
|
176
|
-
if ((await
|
|
177
|
-
const
|
|
222
|
+
if ((await execute(sandbox, "git", ["--version"])).exitCode !== 0) throw new MaterializeError("git is not installed in the sandbox. The sandbox template must include git.", "git-missing");
|
|
223
|
+
const cleanCloneUrl = repoInfo.cloneUrl ?? cleanUrl(repo);
|
|
224
|
+
const existing = await existingCheckoutRemote(sandbox, workdir, cleanCloneUrl);
|
|
178
225
|
if (existing !== null) {
|
|
179
|
-
if (/\/\/[^/]*@/.test(existing)) await scrubRemote(sandbox, workdir, repo,
|
|
226
|
+
if (/\/\/[^/]*@/.test(existing)) await scrubRemote(sandbox, workdir, repo, cleanCloneUrl);
|
|
180
227
|
} else {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
228
|
+
const authEnv = gitAuthenticationEnvironment(cleanCloneUrl, token, repoInfo.authUsername ?? "x-access-token", "clone-failed");
|
|
229
|
+
await clearWorkdir(sandbox, workdir);
|
|
230
|
+
const clone = await gitTransfer(sandbox, [
|
|
231
|
+
"clone",
|
|
232
|
+
"--depth=1",
|
|
233
|
+
"--single-branch",
|
|
234
|
+
"--branch",
|
|
235
|
+
repoInfo.defaultBranch,
|
|
236
|
+
"--",
|
|
237
|
+
cleanCloneUrl,
|
|
238
|
+
workdir
|
|
239
|
+
], {
|
|
240
|
+
env: authEnv,
|
|
241
|
+
phase: "repository clone",
|
|
242
|
+
beforeRetry: async () => {
|
|
243
|
+
await clearWorkdir(sandbox, workdir);
|
|
197
244
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
throw await scrubbedFailure(sandbox, workdir, repo, tokenInRemote, primary, "clone-failed");
|
|
201
|
-
}
|
|
202
|
-
await scrubRemote(sandbox, workdir, repo, tokenInRemote);
|
|
245
|
+
});
|
|
246
|
+
if (clone.exitCode !== 0) throw classifyGitFailure(clone, "clone-failed");
|
|
203
247
|
}
|
|
204
248
|
await storage.markMaterialized({ id: sandboxRow.id });
|
|
205
249
|
}
|
|
@@ -208,61 +252,222 @@ async function materializeRepoImpl(options) {
|
|
|
208
252
|
* answers from the session's `GH_TOKEN`, so no credential is ever written.
|
|
209
253
|
*/
|
|
210
254
|
const GH_CREDENTIAL_HELPER = "!gh auth git-credential";
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
255
|
+
async function fetchStartPoint(sandbox, workdir, { baseBranch, pullRequestNumber, mergeRequestNumber }, shallowClone, env) {
|
|
256
|
+
const changeRequestRef = pullRequestNumber !== void 0 ? `refs/pull/${pullRequestNumber}/head` : mergeRequestNumber !== void 0 ? `refs/merge-requests/${mergeRequestNumber}/head` : void 0;
|
|
257
|
+
const base = await gitTransfer(sandbox, [
|
|
258
|
+
"-C",
|
|
259
|
+
workdir,
|
|
260
|
+
"fetch",
|
|
261
|
+
...changeRequestRef && shallowClone ? ["--unshallow"] : [],
|
|
262
|
+
...pullRequestNumber !== void 0 ? ["--filter=blob:none"] : [],
|
|
263
|
+
"origin",
|
|
264
|
+
baseBranch
|
|
265
|
+
], {
|
|
266
|
+
env,
|
|
267
|
+
timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS,
|
|
268
|
+
phase: "branch checkout fetch"
|
|
269
|
+
});
|
|
270
|
+
if (base.exitCode !== 0 || !changeRequestRef) return base;
|
|
271
|
+
return gitTransfer(sandbox, [
|
|
272
|
+
"-C",
|
|
273
|
+
workdir,
|
|
274
|
+
"fetch",
|
|
275
|
+
...pullRequestNumber !== void 0 ? ["--filter=blob:none"] : [],
|
|
276
|
+
"origin",
|
|
277
|
+
changeRequestRef
|
|
278
|
+
], {
|
|
279
|
+
env,
|
|
280
|
+
timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS,
|
|
281
|
+
phase: "change request head fetch"
|
|
282
|
+
});
|
|
220
283
|
}
|
|
221
284
|
/** Check out a session's branch inside its isolated repository clone. */
|
|
222
285
|
async function checkoutSessionBranch(sandbox, workdir, options) {
|
|
223
286
|
return timedPhase("workspace.checkout", () => checkoutSessionBranchImpl(sandbox, workdir, options));
|
|
224
287
|
}
|
|
288
|
+
/** Refresh an existing GitLab review checkout without exposing its credential to the agent. */
|
|
289
|
+
async function refreshMergeRequestCheckout(sandbox, workdir, input) {
|
|
290
|
+
const { branch, mergeRequestNumber, expectedHeadSha, access } = input;
|
|
291
|
+
if (!isValidGitRef(branch) || !Number.isSafeInteger(mergeRequestNumber) || mergeRequestNumber <= 0 || !/^[0-9a-f]{40}$/i.test(expectedHeadSha)) throw new Error("Refusing to refresh a GitLab review with invalid session or head metadata.");
|
|
292
|
+
const token = access.authorization?.token;
|
|
293
|
+
if (!token) throw new Error("GitLab repository access did not include a bearer token.");
|
|
294
|
+
const env = gitAuthenticationEnvironment(access.cloneUrl, token, access.authorization?.username ?? "oauth2", "pull-failed");
|
|
295
|
+
const current = await execute(sandbox, "git", [
|
|
296
|
+
"-C",
|
|
297
|
+
workdir,
|
|
298
|
+
"branch",
|
|
299
|
+
"--show-current"
|
|
300
|
+
]);
|
|
301
|
+
if (current.exitCode !== 0 || current.stdout.trim() !== branch) throw new Error("The active checkout is not on its bound review branch.");
|
|
302
|
+
const status = await execute(sandbox, "git", [
|
|
303
|
+
"-C",
|
|
304
|
+
workdir,
|
|
305
|
+
"status",
|
|
306
|
+
"--porcelain",
|
|
307
|
+
"--untracked-files=all"
|
|
308
|
+
]);
|
|
309
|
+
if (status.exitCode !== 0 || status.stdout.trim()) throw new Error("The review checkout has local changes; refusing to replace them.");
|
|
310
|
+
const before = await execute(sandbox, "git", [
|
|
311
|
+
"-C",
|
|
312
|
+
workdir,
|
|
313
|
+
"rev-parse",
|
|
314
|
+
"HEAD"
|
|
315
|
+
]);
|
|
316
|
+
if (before.exitCode !== 0) throw new Error("Could not read the current review checkout head.");
|
|
317
|
+
const fetch = await gitTransfer(sandbox, [
|
|
318
|
+
"-C",
|
|
319
|
+
workdir,
|
|
320
|
+
"fetch",
|
|
321
|
+
"origin",
|
|
322
|
+
`refs/merge-requests/${mergeRequestNumber}/head`
|
|
323
|
+
], {
|
|
324
|
+
env,
|
|
325
|
+
timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS,
|
|
326
|
+
phase: "GitLab review head refresh"
|
|
327
|
+
});
|
|
328
|
+
if (fetch.exitCode !== 0) throw classifyGitFailure(fetch, "pull-failed");
|
|
329
|
+
const fetched = await execute(sandbox, "git", [
|
|
330
|
+
"-C",
|
|
331
|
+
workdir,
|
|
332
|
+
"rev-parse",
|
|
333
|
+
"FETCH_HEAD"
|
|
334
|
+
]);
|
|
335
|
+
if (fetched.exitCode !== 0 || fetched.stdout.trim().toLowerCase() !== expectedHeadSha.toLowerCase()) throw new Error("The fetched GitLab review head differs from the provider-reported head; retry after it settles.");
|
|
336
|
+
if (before.stdout.trim().toLowerCase() === expectedHeadSha.toLowerCase()) return {
|
|
337
|
+
headSha: expectedHeadSha,
|
|
338
|
+
changed: false
|
|
339
|
+
};
|
|
340
|
+
const updated = await execute(sandbox, "git", [
|
|
341
|
+
"-C",
|
|
342
|
+
workdir,
|
|
343
|
+
"checkout",
|
|
344
|
+
"-B",
|
|
345
|
+
branch,
|
|
346
|
+
"FETCH_HEAD"
|
|
347
|
+
], { env });
|
|
348
|
+
if (updated.exitCode !== 0) throw classifyGitFailure(updated, "pull-failed");
|
|
349
|
+
return {
|
|
350
|
+
headSha: expectedHeadSha,
|
|
351
|
+
changed: true
|
|
352
|
+
};
|
|
353
|
+
}
|
|
225
354
|
async function checkoutSessionBranchImpl(sandbox, workdir, options) {
|
|
226
|
-
const { branch, baseBranch, token, repoFullName, pullRequestNumber } = options;
|
|
227
|
-
if (!isValidGitRef(branch) || !isValidGitRef(baseBranch)) throw new MaterializeError("Refusing to create a session from an invalid branch name.", "clone-failed");
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
const
|
|
355
|
+
const { branch, baseBranch, token, repoFullName, pullRequestNumber, mergeRequestNumber, cloneUrl, authUsername } = options;
|
|
356
|
+
if (!isValidGitRef(branch) || !isValidGitRef(baseBranch) || pullRequestNumber !== void 0 && mergeRequestNumber !== void 0 || mergeRequestNumber !== void 0 && (!Number.isSafeInteger(mergeRequestNumber) || mergeRequestNumber <= 0)) throw new MaterializeError("Refusing to create a session from an invalid branch name.", "clone-failed");
|
|
357
|
+
const changeRequestSession = pullRequestNumber !== void 0 || mergeRequestNumber !== void 0;
|
|
358
|
+
const cleanCloneUrl = cloneUrl ?? cleanUrl(repoFullName);
|
|
359
|
+
const authEnv = gitAuthenticationEnvironment(cleanCloneUrl, token, authUsername ?? "x-access-token", "pull-failed");
|
|
360
|
+
const credentialKey = authUsername ? "credential." + credentialScope(cleanCloneUrl) + ".helper" : "credential.helper";
|
|
361
|
+
if (authUsername) await execute(sandbox, "git", [
|
|
362
|
+
"-C",
|
|
363
|
+
workdir,
|
|
364
|
+
"config",
|
|
365
|
+
"--unset-all",
|
|
366
|
+
credentialKey
|
|
367
|
+
]);
|
|
368
|
+
else {
|
|
369
|
+
const configured = await execute(sandbox, "git", [
|
|
370
|
+
"-C",
|
|
371
|
+
workdir,
|
|
372
|
+
"config",
|
|
373
|
+
credentialKey,
|
|
374
|
+
GH_CREDENTIAL_HELPER
|
|
375
|
+
]);
|
|
376
|
+
if (configured.exitCode !== 0) throw classifyGitFailure(configured, "pull-failed");
|
|
377
|
+
}
|
|
378
|
+
const current = await execute(sandbox, "git", [
|
|
379
|
+
"-C",
|
|
380
|
+
workdir,
|
|
381
|
+
"branch",
|
|
382
|
+
"--show-current"
|
|
383
|
+
]);
|
|
231
384
|
if (current.exitCode === 0 && current.stdout.trim() === branch) return;
|
|
232
|
-
if ((await
|
|
233
|
-
|
|
385
|
+
if ((await execute(sandbox, "git", [
|
|
386
|
+
"-C",
|
|
387
|
+
workdir,
|
|
388
|
+
"show-ref",
|
|
389
|
+
"--verify",
|
|
390
|
+
"--quiet",
|
|
391
|
+
`refs/heads/${branch}`
|
|
392
|
+
])).exitCode === 0) {
|
|
393
|
+
const checkout = await execute(sandbox, "git", [
|
|
394
|
+
"-C",
|
|
395
|
+
workdir,
|
|
396
|
+
"checkout",
|
|
397
|
+
branch
|
|
398
|
+
], { env: authEnv });
|
|
234
399
|
if (checkout.exitCode !== 0) {
|
|
235
400
|
if (isBlockedByLocalWork(checkout)) return;
|
|
236
401
|
throw classifyGitFailure(checkout, "clone-failed");
|
|
237
402
|
}
|
|
238
403
|
return;
|
|
239
404
|
}
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
405
|
+
const fetch = await fetchStartPoint(sandbox, workdir, options, changeRequestSession && (await execute(sandbox, "git", [
|
|
406
|
+
"-C",
|
|
407
|
+
workdir,
|
|
408
|
+
"rev-parse",
|
|
409
|
+
"--is-shallow-repository"
|
|
410
|
+
])).stdout.trim() === "true", authEnv);
|
|
411
|
+
if (fetch.exitCode !== 0) throw classifyGitFailure(fetch, "pull-failed");
|
|
412
|
+
const create = await execute(sandbox, "git", [
|
|
413
|
+
"-C",
|
|
414
|
+
workdir,
|
|
415
|
+
"checkout",
|
|
416
|
+
"-b",
|
|
417
|
+
branch,
|
|
418
|
+
"FETCH_HEAD"
|
|
419
|
+
], {
|
|
420
|
+
env: authEnv,
|
|
421
|
+
timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS,
|
|
422
|
+
phase: "branch checkout"
|
|
423
|
+
});
|
|
424
|
+
if (create.exitCode === 0 || isBlockedByLocalWork(create)) return;
|
|
425
|
+
if (!isBranchCollision(create)) throw classifyGitFailure(create, "clone-failed");
|
|
426
|
+
const adopt = await execute(sandbox, "git", [
|
|
427
|
+
"-C",
|
|
428
|
+
workdir,
|
|
429
|
+
"checkout",
|
|
430
|
+
branch
|
|
431
|
+
], { env: authEnv });
|
|
432
|
+
if (adopt.exitCode === 0 || isBlockedByLocalWork(adopt)) return;
|
|
433
|
+
let drop = await execute(sandbox, "git", [
|
|
434
|
+
"-C",
|
|
435
|
+
workdir,
|
|
436
|
+
"update-ref",
|
|
437
|
+
"--no-deref",
|
|
438
|
+
"-d",
|
|
439
|
+
`refs/heads/${branch}`
|
|
440
|
+
]);
|
|
441
|
+
if (drop.exitCode !== 0) {
|
|
442
|
+
const gitDir = await execute(sandbox, "git", [
|
|
443
|
+
"-C",
|
|
444
|
+
workdir,
|
|
445
|
+
"rev-parse",
|
|
446
|
+
"--absolute-git-dir"
|
|
447
|
+
]);
|
|
448
|
+
if (gitDir.exitCode !== 0 || !gitDir.stdout.trim()) throw classifyGitFailure(create, "clone-failed");
|
|
449
|
+
drop = await execute(sandbox, "rm", [
|
|
450
|
+
"-f",
|
|
451
|
+
"--",
|
|
452
|
+
`${gitDir.stdout.trim()}/refs/heads/${branch}`
|
|
453
|
+
]);
|
|
454
|
+
}
|
|
455
|
+
if (drop.exitCode !== 0) throw classifyGitFailure(create, "clone-failed");
|
|
456
|
+
const retry = await execute(sandbox, "git", [
|
|
457
|
+
"-C",
|
|
458
|
+
workdir,
|
|
459
|
+
"checkout",
|
|
460
|
+
"-b",
|
|
461
|
+
branch,
|
|
462
|
+
"FETCH_HEAD"
|
|
463
|
+
], {
|
|
464
|
+
env: authEnv,
|
|
465
|
+
timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS,
|
|
466
|
+
phase: "branch checkout retry"
|
|
467
|
+
});
|
|
468
|
+
if (retry.exitCode !== 0) {
|
|
469
|
+
if (isBlockedByLocalWork(retry)) return;
|
|
470
|
+
throw classifyGitFailure(retry, "clone-failed");
|
|
266
471
|
}
|
|
267
472
|
}
|
|
268
473
|
/**
|
|
@@ -285,50 +490,35 @@ function isBlockedByLocalWork(result) {
|
|
|
285
490
|
return /Your local changes to the following files would be overwritten by checkout|untracked working tree files would be overwritten by checkout/i.test(output);
|
|
286
491
|
}
|
|
287
492
|
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* other remote (or no git dir at all) sends materialize down the clone path.
|
|
493
|
+
* Return the origin URL when the workdir contains this repository, including a
|
|
494
|
+
* legacy checkout whose remote still embeds credentials.
|
|
291
495
|
*/
|
|
292
|
-
async function existingCheckoutRemote(sandbox, workdir,
|
|
293
|
-
const result = await
|
|
496
|
+
async function existingCheckoutRemote(sandbox, workdir, cloneUrl) {
|
|
497
|
+
const result = await execute(sandbox, "git", [
|
|
498
|
+
"-C",
|
|
499
|
+
workdir,
|
|
500
|
+
"remote",
|
|
501
|
+
"get-url",
|
|
502
|
+
"origin"
|
|
503
|
+
]);
|
|
294
504
|
if (result.exitCode !== 0) return null;
|
|
295
505
|
const url = result.stdout.trim();
|
|
296
|
-
|
|
506
|
+
const actual = normalizedRemoteUrl(url);
|
|
507
|
+
const expected = normalizedRemoteUrl(cloneUrl);
|
|
508
|
+
return actual !== null && expected !== null && actual === expected ? url : null;
|
|
297
509
|
}
|
|
298
|
-
/**
|
|
299
|
-
function
|
|
300
|
-
let parsed;
|
|
301
|
-
try {
|
|
302
|
-
parsed = new URL(url);
|
|
303
|
-
} catch {
|
|
304
|
-
return false;
|
|
305
|
-
}
|
|
306
|
-
if (parsed.protocol !== "https:" || parsed.hostname.toLowerCase() !== "github.com") return false;
|
|
307
|
-
if (parsed.port !== "" || parsed.search !== "" || parsed.hash !== "") return false;
|
|
308
|
-
return parsed.pathname.replace(/\.git$/, "").toLowerCase() === `/${repoFullName.toLowerCase()}`;
|
|
309
|
-
}
|
|
310
|
-
/** Probed without `git -C` so a missing workdir returns false instead of throwing. */
|
|
311
|
-
async function hasGitDir(sandbox, workdir) {
|
|
312
|
-
return (await sh(sandbox, `test -d ${shellQuote(`${workdir}/.git`)}`).catch(() => null))?.exitCode === 0;
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Reset the git remote back to the tokenless URL. Strict when the token
|
|
316
|
-
* reached the remote: any failure — a non-zero exit or a provider throw —
|
|
317
|
-
* means the token may still be persisted, so it is thrown for the caller to
|
|
318
|
-
* surface. Best-effort when it never did: the workdir may not exist (e.g. a
|
|
319
|
-
* failed clone), which makes providers that spawn with `cwd` throw rather
|
|
320
|
-
* than return a non-zero exit code; both outcomes are tolerated so neither
|
|
321
|
-
* masks the primary failure.
|
|
322
|
-
*/
|
|
323
|
-
async function scrubRemote(sandbox, workdir, repoFullName, tokenInRemote) {
|
|
324
|
-
const scrub = `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`;
|
|
325
|
-
if (!tokenInRemote) {
|
|
326
|
-
await sh(sandbox, scrub).catch(() => void 0);
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
510
|
+
/** Remove credentials persisted by an older implementation from origin. */
|
|
511
|
+
async function scrubRemote(sandbox, workdir, repoFullName, cloneUrl = cleanUrl(repoFullName)) {
|
|
329
512
|
let failure;
|
|
330
513
|
try {
|
|
331
|
-
const result = await
|
|
514
|
+
const result = await execute(sandbox, "git", [
|
|
515
|
+
"-C",
|
|
516
|
+
workdir,
|
|
517
|
+
"remote",
|
|
518
|
+
"set-url",
|
|
519
|
+
"origin",
|
|
520
|
+
cloneUrl
|
|
521
|
+
]);
|
|
332
522
|
if (result.exitCode === 0) return;
|
|
333
523
|
failure = result.stderr.trim() || result.stdout.trim();
|
|
334
524
|
} catch (error) {
|
|
@@ -337,23 +527,6 @@ async function scrubRemote(sandbox, workdir, repoFullName, tokenInRemote) {
|
|
|
337
527
|
throw new MaterializeError(`Failed to scrub installation token from git remote: ${failure}`, "pull-failed");
|
|
338
528
|
}
|
|
339
529
|
/**
|
|
340
|
-
* Scrub after `primary` already failed and return the error to throw. A failed
|
|
341
|
-
* scrub is appended to `primary` rather than replacing it, so the caller gets
|
|
342
|
-
* the error it would have had without the scrub — same class, same `code` —
|
|
343
|
-
* carrying the leaked-token warning in its message.
|
|
344
|
-
*/
|
|
345
|
-
async function scrubbedFailure(sandbox, workdir, repoFullName, tokenInRemote, primary, fallback) {
|
|
346
|
-
try {
|
|
347
|
-
await scrubRemote(sandbox, workdir, repoFullName, tokenInRemote);
|
|
348
|
-
return primary;
|
|
349
|
-
} catch (scrubError) {
|
|
350
|
-
const scrubMessage = scrubError instanceof Error ? scrubError.message : String(scrubError);
|
|
351
|
-
if (!(primary instanceof Error)) return new MaterializeError(`${String(primary)} — additionally: ${scrubMessage}`, fallback);
|
|
352
|
-
primary.message = `${primary.message} — additionally: ${scrubMessage}`;
|
|
353
|
-
return primary;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
530
|
* Turn a failed git command into an actionable error, detecting the common
|
|
358
531
|
* "cannot reach github.com" egress failure.
|
|
359
532
|
*/
|
|
@@ -363,14 +536,6 @@ function classifyGitFailure(result, fallback) {
|
|
|
363
536
|
return new MaterializeError(`git ${fallback === "clone-failed" ? "clone" : fallback === "pull-failed" ? "pull" : "push"} failed: ${stderr}`, fallback);
|
|
364
537
|
}
|
|
365
538
|
/**
|
|
366
|
-
* Validate a git ref (branch) name. Server-side defense-in-depth: only allow a
|
|
367
|
-
* conservative character set so a branch can never be built into a shell
|
|
368
|
-
* command in a way that escapes quoting. Mirrors the route-layer check.
|
|
369
|
-
*/
|
|
370
|
-
function isValidGitRef(value) {
|
|
371
|
-
return typeof value === "string" && value.length > 0 && value.length <= 255 && !value.startsWith("-") && /^[A-Za-z0-9_./-]+$/.test(value);
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
539
|
* Resolve a concrete `{ name, email }` for git authorship from a possibly-sparse
|
|
375
540
|
* identity. Falls back to a GitHub-style noreply identity so commits are never
|
|
376
541
|
* authored with an empty or host-derived identity.
|
|
@@ -382,57 +547,57 @@ function resolveGitIdentity(identity) {
|
|
|
382
547
|
email: (identity.email || "").trim() || (login ? `${login}@users.noreply.github.com` : "mastra-code@users.noreply.github.com")
|
|
383
548
|
};
|
|
384
549
|
}
|
|
385
|
-
/**
|
|
386
|
-
* Configure `user.name` / `user.email` for the given repo working tree inside
|
|
387
|
-
* the sandbox so commits are authored correctly. Values are shell-quoted.
|
|
388
|
-
*/
|
|
550
|
+
/** Configure repository-local commit identity through direct Git argv calls. */
|
|
389
551
|
async function configureGitIdentity(sandbox, workdir, identity) {
|
|
390
552
|
const { name, email } = resolveGitIdentity(identity);
|
|
391
|
-
const setName = await
|
|
553
|
+
const setName = await execute(sandbox, "git", [
|
|
554
|
+
"-C",
|
|
555
|
+
workdir,
|
|
556
|
+
"config",
|
|
557
|
+
"user.name",
|
|
558
|
+
name
|
|
559
|
+
]);
|
|
392
560
|
if (setName.exitCode !== 0) throw new MaterializeError(`Failed to set git user.name: ${setName.stderr.trim()}`, "commit-failed");
|
|
393
|
-
const setEmail = await
|
|
561
|
+
const setEmail = await execute(sandbox, "git", [
|
|
562
|
+
"-C",
|
|
563
|
+
workdir,
|
|
564
|
+
"config",
|
|
565
|
+
"user.email",
|
|
566
|
+
email
|
|
567
|
+
]);
|
|
394
568
|
if (setEmail.exitCode !== 0) throw new MaterializeError(`Failed to set git user.email: ${setEmail.stderr.trim()}`, "commit-failed");
|
|
395
569
|
}
|
|
570
|
+
async function pushAuthenticatedBranch(sandbox, workdir, branch, cloneUrl, token, username) {
|
|
571
|
+
const env = gitAuthenticationEnvironment(cloneUrl, token, username, "push-failed");
|
|
572
|
+
const push = await execute(sandbox, "git", [
|
|
573
|
+
"-C",
|
|
574
|
+
workdir,
|
|
575
|
+
"push",
|
|
576
|
+
"-u",
|
|
577
|
+
"origin",
|
|
578
|
+
branch
|
|
579
|
+
], { env });
|
|
580
|
+
if (push.exitCode !== 0) throw classifyGitFailure(push, "push-failed");
|
|
581
|
+
}
|
|
396
582
|
/**
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
* therefore only ever lives in the remote URL for the duration of the
|
|
400
|
-
* operation and is never left in the VM's git config.
|
|
401
|
-
*
|
|
402
|
-
* Once the tokenized URL is installed a failed scrub may leave the token
|
|
403
|
-
* persisted, so it is always surfaced: on its own after a successful `fn`,
|
|
404
|
-
* appended to `fn`'s own error otherwise — `fn`'s error is never replaced.
|
|
405
|
-
* Only a failed set-url (the token never reached the remote) downgrades the
|
|
406
|
-
* scrub to best-effort.
|
|
583
|
+
* Push a branch back to GitHub with credentials scoped to the Git process.
|
|
584
|
+
* The token never enters argv, a repository URL, or persistent configuration.
|
|
407
585
|
*/
|
|
408
|
-
async function
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
await scrubRemote(sandbox, workdir, repoFullName, false);
|
|
413
|
-
throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr.trim()}`, "push-failed");
|
|
414
|
-
}
|
|
415
|
-
let result;
|
|
416
|
-
try {
|
|
417
|
-
result = await fn();
|
|
418
|
-
} catch (primary) {
|
|
419
|
-
throw await scrubbedFailure(sandbox, workdir, repoFullName, true, primary, "push-failed");
|
|
420
|
-
}
|
|
421
|
-
await scrubRemote(sandbox, workdir, repoFullName, true);
|
|
422
|
-
return result;
|
|
586
|
+
async function pushBranch(sandbox, workdir, branch, token, repoFullName) {
|
|
587
|
+
if (!isValidGitRef(branch)) throw new MaterializeError(`Refusing to push: invalid branch name '${branch}'.`, "push-failed");
|
|
588
|
+
if (!/^[\w.-]+(?:\/[\w.-]+)+$/.test(repoFullName)) throw new MaterializeError(`Refusing to push: invalid repo full name '${repoFullName}'.`, "push-failed");
|
|
589
|
+
await pushAuthenticatedBranch(sandbox, workdir, branch, cleanUrl(repoFullName), token, "x-access-token");
|
|
423
590
|
}
|
|
424
591
|
/**
|
|
425
|
-
* Push
|
|
426
|
-
*
|
|
427
|
-
* into the remote URL via `withInstallToken`, and egress failures are
|
|
428
|
-
* classified into actionable errors.
|
|
592
|
+
* Push the active session branch through the provider-neutral repository
|
|
593
|
+
* access contract without persisting or exposing provider credentials.
|
|
429
594
|
*/
|
|
430
|
-
async function
|
|
595
|
+
async function pushRepositoryBranch(sandbox, workdir, branch, access, repoFullName) {
|
|
431
596
|
if (!isValidGitRef(branch)) throw new MaterializeError(`Refusing to push: invalid branch name '${branch}'.`, "push-failed");
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
597
|
+
if (!/^[\w.-]+(?:\/[\w.-]+)+$/.test(repoFullName)) throw new MaterializeError(`Refusing to push: invalid repo full name '${repoFullName}'.`, "push-failed");
|
|
598
|
+
const authorization = access.authorization;
|
|
599
|
+
if (!authorization?.token) throw new MaterializeError("Repository access did not include push credentials.", "push-failed");
|
|
600
|
+
await pushAuthenticatedBranch(sandbox, workdir, branch, access.cloneUrl, authorization.token, authorization.username ?? "x-access-token");
|
|
436
601
|
}
|
|
437
602
|
/**
|
|
438
603
|
* Stage every change in the working tree and create a commit inside the
|
|
@@ -442,15 +607,32 @@ async function pushBranch(sandbox, workdir, branch, token, repoFullName) {
|
|
|
442
607
|
*
|
|
443
608
|
* @param sandbox the live sandbox containing the checkout
|
|
444
609
|
* @param workdir the session workdir to commit in
|
|
445
|
-
* @param message the commit message (
|
|
610
|
+
* @param message the commit message (passed as one argv value)
|
|
446
611
|
* @param identity authorship identity for the commit
|
|
447
612
|
*/
|
|
448
613
|
async function commitAll(sandbox, workdir, message, identity) {
|
|
449
614
|
await configureGitIdentity(sandbox, workdir, identity);
|
|
450
|
-
const add = await
|
|
615
|
+
const add = await execute(sandbox, "git", [
|
|
616
|
+
"-C",
|
|
617
|
+
workdir,
|
|
618
|
+
"add",
|
|
619
|
+
"-A"
|
|
620
|
+
]);
|
|
451
621
|
if (add.exitCode !== 0) throw new MaterializeError(`git add failed: ${add.stderr.trim() || add.stdout.trim()}`, "commit-failed");
|
|
452
|
-
if ((await
|
|
453
|
-
|
|
622
|
+
if ((await execute(sandbox, "git", [
|
|
623
|
+
"-C",
|
|
624
|
+
workdir,
|
|
625
|
+
"diff",
|
|
626
|
+
"--cached",
|
|
627
|
+
"--quiet"
|
|
628
|
+
])).exitCode === 0) return { committed: false };
|
|
629
|
+
const commit = await execute(sandbox, "git", [
|
|
630
|
+
"-C",
|
|
631
|
+
workdir,
|
|
632
|
+
"commit",
|
|
633
|
+
"-m",
|
|
634
|
+
message
|
|
635
|
+
]);
|
|
454
636
|
if (commit.exitCode !== 0) throw new MaterializeError(`git commit failed: ${commit.stderr.trim() || commit.stdout.trim()}`, "commit-failed");
|
|
455
637
|
return { committed: true };
|
|
456
638
|
}
|
|
@@ -515,7 +697,7 @@ async function runTeardownCommand(sandbox, workdir, command, options = {}) {
|
|
|
515
697
|
* the sandbox template requirement.
|
|
516
698
|
*/
|
|
517
699
|
async function assertGhAvailable(sandbox) {
|
|
518
|
-
if ((await
|
|
700
|
+
if ((await execute(sandbox, "gh", ["--version"])).exitCode !== 0) throw new MaterializeError("The GitHub CLI (gh) is not installed in the sandbox. The sandbox template must include gh to open pull requests.", "gh-missing");
|
|
519
701
|
}
|
|
520
702
|
/** Match the first GitHub PR URL in `gh pr create` output. */
|
|
521
703
|
function parsePullRequestUrl(stdout) {
|
|
@@ -524,8 +706,8 @@ function parsePullRequestUrl(stdout) {
|
|
|
524
706
|
/**
|
|
525
707
|
* Open a pull request from inside the sandbox via `gh pr create`. The token is
|
|
526
708
|
* passed only through a per-invocation `GH_TOKEN` env scoped to the single `gh`
|
|
527
|
-
* process (never persisted), all arguments are
|
|
528
|
-
* PR URL is parsed from stdout.
|
|
709
|
+
* process (never persisted), all arguments are passed as structured argv, and
|
|
710
|
+
* the resulting PR URL is parsed from stdout.
|
|
529
711
|
*
|
|
530
712
|
* @param sandbox live sandbox containing the checkout
|
|
531
713
|
* @param workdir the worktree (or repo) path the PR head branch is checked out in
|
|
@@ -534,14 +716,21 @@ async function createPullRequest(sandbox, workdir, { token, base, head, title, b
|
|
|
534
716
|
if (!isValidGitRef(base)) throw new MaterializeError(`Refusing to open PR: invalid base branch '${base}'.`, "pr-failed");
|
|
535
717
|
if (!isValidGitRef(head)) throw new MaterializeError(`Refusing to open PR: invalid head branch '${head}'.`, "pr-failed");
|
|
536
718
|
await assertGhAvailable(sandbox);
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
719
|
+
const result = await execute(sandbox, "gh", [
|
|
720
|
+
"pr",
|
|
721
|
+
"create",
|
|
722
|
+
"--base",
|
|
723
|
+
base,
|
|
724
|
+
"--head",
|
|
725
|
+
head,
|
|
726
|
+
"--title",
|
|
727
|
+
title,
|
|
728
|
+
"--body",
|
|
729
|
+
body ?? ""
|
|
730
|
+
], {
|
|
731
|
+
cwd: workdir,
|
|
732
|
+
env: { GH_TOKEN: token }
|
|
733
|
+
});
|
|
545
734
|
if (result.exitCode !== 0) {
|
|
546
735
|
const classified = classifyGitFailure(result, "push-failed");
|
|
547
736
|
if (classified.code === "egress-blocked") throw classified;
|
|
@@ -552,6 +741,6 @@ async function createPullRequest(sandbox, workdir, { token, base, head, title, b
|
|
|
552
741
|
return { url };
|
|
553
742
|
}
|
|
554
743
|
//#endregion
|
|
555
|
-
export { CHECKOUT_COMMAND_TIMEOUT_MS, DEFAULT_COMMAND_TIMEOUT_MS, MaterializeError, SetupCommandError, checkoutSessionBranch, commitAll, configureGitIdentity, createPullRequest, isValidGitRef, materializeRepo, pushBranch, resolveGitIdentity, runSetupCommand, runTeardownCommand, sh, shellQuote
|
|
744
|
+
export { CHECKOUT_COMMAND_TIMEOUT_MS, DEFAULT_COMMAND_TIMEOUT_MS, MaterializeError, SetupCommandError, checkoutSessionBranch, commitAll, configureGitIdentity, createPullRequest, isValidGitRef, materializeRepo, pushBranch, pushRepositoryBranch, refreshMergeRequestCheckout, resolveGitIdentity, runSetupCommand, runTeardownCommand, sh, shellQuote };
|
|
556
745
|
|
|
557
746
|
//# sourceMappingURL=sandbox.js.map
|