@mastra/factory 0.3.0-alpha.2 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,109 @@
1
1
  # @mastra/factory
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Added a `channel-identity` storage domain so a factory can link a chat-platform sender to one of its own users, and a `channels()` slot so an integration can supply the chat platform itself. ([#20060](https://github.com/mastra-ai/mastra/pull/20060))
8
+
9
+ `ChannelIdentityStorage` persists account links keyed by platform, external team id, and external user id, and records an optional default factory project per link. A link is written only after the chat platform itself asserts the account through OpenID Connect, with the existing `createStateSigner` binding the round trip to the tenant that started it.
10
+
11
+ `FactoryIntegration` gains an optional `channels(ctx)` returning an `AgentControllerChannels`, which the factory attaches to the mounted agent controller during `prepare()`. Inbound platform messages then reach the same agents the web UI drives, without the deploy entry reaching into the prepared controller to wire them by hand. `IntegrationContext` gains `storage.channelIdentity` for integrations that use the slot. Providing `channels()` adds the `channel-identity` domain to the integration's readiness requirements, so an integration whose reverse index is not migrated reports not-ready and its channels never attach. Only one integration may provide channels; a second fails the boot, because attaching replaces rather than merges.
12
+
13
+ `StateTenant` — what `StateSigner.verify` returns — gains a `nonce` field carrying the per-`state` random value. A signed `state` stays valid for its whole lifetime, so a flow that must not run twice off one `state` can key single-use bookkeeping on the nonce; the Slack account-link callback burns it before spending the authorization code. `verify` now rejects a `state` carrying no nonce.
14
+
15
+ The integration seam itself — `FactoryIntegration`, `IntegrationContext`, `IntegrationHooks`, and `IntegrationTools` — is now exported from the package entry point. Implementing an integration outside this package was already the documented path for third parties, but the types to do it were unreachable. `ChannelIdentityStorage` and `createFactoryRouteAuth` are exported too, alongside the existing projects and work-items storage domains.
16
+
17
+ Fixed sign-in returning to the root path instead of the page the visitor started from. The OAuth `state` carrying that destination was encoded as Base64URL JSON, but `MastraAuthStudio` reads the `uuid|encodedPath` shape, so it never found a destination and every sign-in landed on `/`. The state now uses that shape, and the destination is also stashed in a short-lived `HttpOnly` cookie for providers that do not echo `state` back to the callback.
18
+
19
+ - Added a per-Factory Slack work-item setting so a new Slack thread only opens a Work-board card when that Factory opts in, and Slack OAuth now returns to the Factory the flow started from. ([#20395](https://github.com/mastra-ai/mastra/pull/20395))
20
+
21
+ ### Patch Changes
22
+
23
+ - Fixed workspace re-opening failing when the session's agent switched branches and left uncommitted work in the tree. The workspace now keeps the checkout on its current branch instead of returning an error — the session's work in progress always wins over the recorded branch. ([#20372](https://github.com/mastra-ai/mastra/pull/20372))
24
+
25
+ - Move Github log to debug instead of info in factory ([#20331](https://github.com/mastra-ai/mastra/pull/20331))
26
+
27
+ - Opening a workspace no longer fails when the repository checkout holds uncommitted or untracked files that block `git pull` (for example residue from a changeset-version run or a build). Materialization now keeps the checkout as-is — the same treatment diverged session branches already receive — instead of surfacing "git pull failed: Your local changes would be overwritten by merge" and refusing to open the thread. Local state is never discarded to force the pull through. ([#20372](https://github.com/mastra-ai/mastra/pull/20372))
28
+
29
+ - Stop long-running Factory dispatches from starving the decision queue. The dispatcher poll loop previously awaited every dispatch to completion before claiming again, so a single slow effect (a skill kickoff consuming a full agent run, or binding preparation cloning a repository) froze the whole queue and left every other rule effect stuck in "pending" — sometimes for the 15-minute sandbox clone timeout times five retry attempts. Dispatches now run detached from the poll loop under a bounded in-flight cap while lease renewal keeps them protected from re-claim, so new decisions keep flowing while slow ones finish. ([#20356](https://github.com/mastra-ai/mastra/pull/20356))
30
+
31
+ - Added model switching to Factory review sessions so work can continue during a model outage. ([#20423](https://github.com/mastra-ai/mastra/pull/20423))
32
+
33
+ - Fixed a boot-time provisioning storm where several concurrent requests for the same cold session (for example multiple open browser tabs polling right after a server restart) each provisioned their own sandbox. Concurrent sandbox opens for the same session now share one in-flight provision, so only a single sandbox is created per session. ([#20380](https://github.com/mastra-ai/mastra/pull/20380))
34
+
35
+ - Fixed manual issue triage in platform deployments. The triage runner is now automatically derived from the mounted controller, so manual triage no longer returns 503 when no explicit runner is configured. The manual triage endpoint now shares the same wrapper as webhook-triggered triage, ensuring labels and default model resolution are handled consistently. ([#20362](https://github.com/mastra-ai/mastra/pull/20362))
36
+
37
+ - Improved contributor guidance for Factory backend development. ([#20327](https://github.com/mastra-ai/mastra/pull/20327))
38
+
39
+ - Fixed Factory losing repository access after a GitHub App is reinstalled with a new installation ID. ([#20348](https://github.com/mastra-ai/mastra/pull/20348))
40
+
41
+ - Review sessions now load project AGENTS.md/CLAUDE.md from the pull request's trusted base branch instead of skipping them entirely. The working-tree copies on an untrusted checkout remain excluded from the system prompt and reminder injection; content is served from the base ref via git, and sessions without a known base ref still skip project instruction files. ([#20372](https://github.com/mastra-ai/mastra/pull/20372))
42
+
43
+ - Factory review verdicts are stricter and grounded in the full review record: ([#20372](https://github.com/mastra-ai/mastra/pull/20372))
44
+
45
+ - The reviewer waits for pending review bots to finish on the head commit (polling up to 10 minutes) before forming a verdict, then reads existing reviews — bot and human — and every substantive prior finding is confirmed, addressed, or refuted with evidence. Confirmed unaddressed major findings block approval.
46
+ - Approval is earned through explicit gates: verification executed, all prior findings dispositioned, no bot still pending, behavior covered by tests, adversarial self-check survived. Any concrete change the author should make before merge means "request changes", borderline calls tie-break toward "request changes", and real defects can't be relabeled non-blocking to protect an approval.
47
+ - Non-blocking findings with mechanical fixes ship as a follow-up PR opened by the reviewer against the reviewed PR's branch, instead of landing as homework for the author.
48
+ - The reviewer is hardened against prompt injection: PR content can never direct the review, steering attempts become blocking security findings, bot identity is verified by account login, the PR's install/test-time code is inspected before anything is executed, and follow-up PRs only ever contain code the reviewer authored.
49
+ - The reviewer runs the changed packages' tests and typecheck itself instead of trusting green CI, and every approval must survive an adversarial self-check.
50
+ - PRs with merge conflicts still get a full review but are never approved and never have their conflicts resolved by the reviewer.
51
+
52
+ Reviews arrive on the pull request itself, published via `gh pr review --approve` or `gh pr review --request-changes` before the review pass completes.
53
+
54
+ - Fix Factory workspaces not being available to HTTP routes immediately after creation. Sessions now consistently reuse the same workspace across requests. ([#20421](https://github.com/mastra-ai/mastra/pull/20421))
55
+
56
+ - Fixed Factory rules treating a work item from a non-GitHub, non-Linear source as a GitHub issue. A Slack thread card moved into Triage ran the GitHub issue rule and handed the triage agent a Slack permalink labeled as a GitHub issue; those cards now resolve the plain work-item rules instead. ([#20395](https://github.com/mastra-ai/mastra/pull/20395))
57
+
58
+ - Review sessions no longer ingest AGENTS.md or CLAUDE.md from the checked-out pull request branch. A PR branch is third-party content, so its instruction files are treated as content under review instead of trusted configuration — closing a prompt-injection path into the reviewer agent. The reviewer also runs the PR's install/build/test commands with GitHub tokens stripped from the environment. ([#20372](https://github.com/mastra-ai/mastra/pull/20372))
59
+
60
+ - Fixed Factory provisioning a fresh Platform sandbox for every new session. When a work item finishes or a session is deleted, its sandbox is scrubbed back to the repository's default branch (including gitignored files) and returned to a per-repository reuse pool, so new sessions for the same repository reuse a pooled sandbox instead of spinning up another VM. ([#20328](https://github.com/mastra-ai/mastra/pull/20328))
61
+
62
+ GitHub tokens are injected per command and are no longer stored in the sandbox environment, so a reused sandbox never carries a previous session's credentials.
63
+
64
+ - Added an option to the instruction-file reminder processor that lets hosts disable injection entirely for a request, so instruction files from untrusted checkouts are never surfaced as reminders. ([#20372](https://github.com/mastra-ai/mastra/pull/20372))
65
+
66
+ - Updated dependencies [[`3f472b4`](https://github.com/mastra-ai/mastra/commit/3f472b468892a1ff14ccb43cc0343b86f7d8fd7d), [`ba369f2`](https://github.com/mastra-ai/mastra/commit/ba369f2a0aaf998da0d6aa033d26f64f96bef8ac), [`7457af7`](https://github.com/mastra-ai/mastra/commit/7457af7d309fa4ba4d975904249c0d05ec32e6b7), [`35b929b`](https://github.com/mastra-ai/mastra/commit/35b929b7abc3d20d85c7985880960ac2d04a6c86), [`55c9e24`](https://github.com/mastra-ai/mastra/commit/55c9e248c27c1d72b5bb7e94ea6b8a3999eee49f), [`dcfed93`](https://github.com/mastra-ai/mastra/commit/dcfed93e1e256c6abfa792cbb7ca836f5d0e8638), [`2876e15`](https://github.com/mastra-ai/mastra/commit/2876e15b4d2f616a3bc1ed3af57d546c268384ce), [`35b929b`](https://github.com/mastra-ai/mastra/commit/35b929b7abc3d20d85c7985880960ac2d04a6c86), [`9b3626a`](https://github.com/mastra-ai/mastra/commit/9b3626aeb1d16fcd34b0a8e94c114ddb80a3b240), [`6936517`](https://github.com/mastra-ai/mastra/commit/6936517137090304b735a32aca8f8694f91cb927), [`4696963`](https://github.com/mastra-ai/mastra/commit/469696312ac4c618bc8475b0c5ed7949b8a3455e), [`723aa54`](https://github.com/mastra-ai/mastra/commit/723aa5437106bdb708ae03c0ef6b77aa11291e73), [`4137863`](https://github.com/mastra-ai/mastra/commit/4137863eaa35f430117d21d5dc1bf2f534e64339), [`4137863`](https://github.com/mastra-ai/mastra/commit/4137863eaa35f430117d21d5dc1bf2f534e64339), [`07f5b4b`](https://github.com/mastra-ai/mastra/commit/07f5b4ba9d608d88865030732e580298296adf99), [`723aa54`](https://github.com/mastra-ai/mastra/commit/723aa5437106bdb708ae03c0ef6b77aa11291e73), [`723aa54`](https://github.com/mastra-ai/mastra/commit/723aa5437106bdb708ae03c0ef6b77aa11291e73), [`598080f`](https://github.com/mastra-ai/mastra/commit/598080f224edb3f0f5b801035b067fac50a56a03)]:
67
+ - @mastra/core@1.55.0
68
+ - @mastra/code-sdk@1.1.1
69
+
70
+ ## 0.3.0-alpha.3
71
+
72
+ ### Minor Changes
73
+
74
+ - Added a per-Factory Slack work-item setting so a new Slack thread only opens a Work-board card when that Factory opts in, and Slack OAuth now returns to the Factory the flow started from. ([#20395](https://github.com/mastra-ai/mastra/pull/20395))
75
+
76
+ ### Patch Changes
77
+
78
+ - Fixed workspace re-opening failing when the session's agent switched branches and left uncommitted work in the tree. The workspace now keeps the checkout on its current branch instead of returning an error — the session's work in progress always wins over the recorded branch. ([#20372](https://github.com/mastra-ai/mastra/pull/20372))
79
+
80
+ - Opening a workspace no longer fails when the repository checkout holds uncommitted or untracked files that block `git pull` (for example residue from a changeset-version run or a build). Materialization now keeps the checkout as-is — the same treatment diverged session branches already receive — instead of surfacing "git pull failed: Your local changes would be overwritten by merge" and refusing to open the thread. Local state is never discarded to force the pull through. ([#20372](https://github.com/mastra-ai/mastra/pull/20372))
81
+
82
+ - Review sessions now load project AGENTS.md/CLAUDE.md from the pull request's trusted base branch instead of skipping them entirely. The working-tree copies on an untrusted checkout remain excluded from the system prompt and reminder injection; content is served from the base ref via git, and sessions without a known base ref still skip project instruction files. ([#20372](https://github.com/mastra-ai/mastra/pull/20372))
83
+
84
+ - Factory review verdicts are stricter and grounded in the full review record: ([#20372](https://github.com/mastra-ai/mastra/pull/20372))
85
+
86
+ - The reviewer waits for pending review bots to finish on the head commit (polling up to 10 minutes) before forming a verdict, then reads existing reviews — bot and human — and every substantive prior finding is confirmed, addressed, or refuted with evidence. Confirmed unaddressed major findings block approval.
87
+ - Approval is earned through explicit gates: verification executed, all prior findings dispositioned, no bot still pending, behavior covered by tests, adversarial self-check survived. Any concrete change the author should make before merge means "request changes", borderline calls tie-break toward "request changes", and real defects can't be relabeled non-blocking to protect an approval.
88
+ - Non-blocking findings with mechanical fixes ship as a follow-up PR opened by the reviewer against the reviewed PR's branch, instead of landing as homework for the author.
89
+ - The reviewer is hardened against prompt injection: PR content can never direct the review, steering attempts become blocking security findings, bot identity is verified by account login, the PR's install/test-time code is inspected before anything is executed, and follow-up PRs only ever contain code the reviewer authored.
90
+ - The reviewer runs the changed packages' tests and typecheck itself instead of trusting green CI, and every approval must survive an adversarial self-check.
91
+ - PRs with merge conflicts still get a full review but are never approved and never have their conflicts resolved by the reviewer.
92
+
93
+ Reviews arrive on the pull request itself, published via `gh pr review --approve` or `gh pr review --request-changes` before the review pass completes.
94
+
95
+ - Fix Factory workspaces not being available to HTTP routes immediately after creation. Sessions now consistently reuse the same workspace across requests. ([#20421](https://github.com/mastra-ai/mastra/pull/20421))
96
+
97
+ - Fixed Factory rules treating a work item from a non-GitHub, non-Linear source as a GitHub issue. A Slack thread card moved into Triage ran the GitHub issue rule and handed the triage agent a Slack permalink labeled as a GitHub issue; those cards now resolve the plain work-item rules instead. ([#20395](https://github.com/mastra-ai/mastra/pull/20395))
98
+
99
+ - Review sessions no longer ingest AGENTS.md or CLAUDE.md from the checked-out pull request branch. A PR branch is third-party content, so its instruction files are treated as content under review instead of trusted configuration — closing a prompt-injection path into the reviewer agent. The reviewer also runs the PR's install/build/test commands with GitHub tokens stripped from the environment. ([#20372](https://github.com/mastra-ai/mastra/pull/20372))
100
+
101
+ - Added an option to the instruction-file reminder processor that lets hosts disable injection entirely for a request, so instruction files from untrusted checkouts are never surfaced as reminders. ([#20372](https://github.com/mastra-ai/mastra/pull/20372))
102
+
103
+ - Updated dependencies [[`723aa54`](https://github.com/mastra-ai/mastra/commit/723aa5437106bdb708ae03c0ef6b77aa11291e73), [`723aa54`](https://github.com/mastra-ai/mastra/commit/723aa5437106bdb708ae03c0ef6b77aa11291e73), [`723aa54`](https://github.com/mastra-ai/mastra/commit/723aa5437106bdb708ae03c0ef6b77aa11291e73)]:
104
+ - @mastra/code-sdk@1.1.1-alpha.3
105
+ - @mastra/core@1.55.0-alpha.3
106
+
3
107
  ## 0.3.0-alpha.2
4
108
 
5
109
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/sandbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EACV,sBAAsB,EACtB,UAAU,EAGV,YAAY,EACb,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EACV,wBAAwB,EACxB,0BAA0B,EAC3B,MAAM,8CAA8C,CAAC;AAEtD,KAAK,2BAA2B,GAAG,0BAA0B,CAAC,WAAW,CAAC,CAAC;AAC3E,KAAK,oBAAoB,GAAG,IAAI,CAAC,2BAA2B,EAAE,kBAAkB,CAAC,CAAC;AAElF,UAAU,0BAA0B;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;CAC7B;AAYD;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,KAAK,EAAE,YAAY,CAAC;IACpB,GAAG,EAAE,wBAAwB,CAAC;IAC9B,OAAO,EAAE,2BAA2B,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAGlC;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,KAAK,EAAE,YAAY,CAAC;IACpB,GAAG,EAAE,wBAAwB,CAAC;IAC9B,OAAO,EAAE,2BAA2B,CAAC;IACrC,OAAO,CAAC,EAAE,sBAAsB,CAAC;CAClC,GAAG,OAAO,CAAC,IAAI,CAAC,CAGhB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGhD;AASD,4EAA4E;AAC5E,eAAO,MAAM,2BAA2B,QAAa,CAAC;AA4EtD,8EAA8E;AAC9E,qBAAa,gBAAiB,SAAQ,KAAK;IAGvC,QAAQ,CAAC,IAAI,EACT,aAAa,GACb,gBAAgB,GAChB,cAAc,GACd,aAAa,GACb,aAAa,GACb,eAAe,GACf,YAAY,GACZ,WAAW;gBATf,OAAO,EAAE,MAAM,EACN,IAAI,EACT,aAAa,GACb,gBAAgB,GAChB,cAAc,GACd,aAAa,GACb,aAAa,GACb,eAAe,GACf,YAAY,GACZ,WAAW;CAKlB;AAcD,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE;IAC7C,uFAAuF;IACvF,GAAG,EAAE,0BAA0B,CAAC;IAChC,oDAAoD;IACpD,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,0CAA0C;IAC1C,OAAO,EAAE,sBAAsB,CAAC;IAChC,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,oBAAoB,CAAC;IAC9B,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6FhB;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CAmBf;AAED,yEAAyE;AACzE,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,MAAM,EACf,EACE,MAAM,EACN,UAAU,EACV,KAAK,EACL,YAAY,GACb,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC7E,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAwFD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAU7D;AAED,0DAA0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,yFAAyF;IACzF,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAOzF;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAuBZ;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAWf;AAED,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,YAAY,CAAC,CAqBvB;AAUD,gFAAgF;AAChF,qBAAa,aAAc,SAAQ,KAAK;IAGpC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,iBAAiB,GAAG,cAAc;gBADpE,OAAO,EAAE,MAAM,EACN,IAAI,EAAE,gBAAgB,GAAG,iBAAiB,GAAG,cAAc;CAKvE;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAUpD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAG/E;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,sBAAsB,EAC/B,WAAW,EAAE,MAAM,EACnB,EACE,MAAM,EACN,UAAU,EACV,KAAK,EACL,YAAY,GACb,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC7E,OAAO,CAAC,oBAAoB,CAAC,CAgD/B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,sBAAsB,EAC/B,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,sBAAsB,EAC/B,WAAW,EAAE,MAAM,EACnB,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACjE,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAYD,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,0BAA0B;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;CACb;AAuBD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,MAAM,EACf,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,qBAAqB,GACxD,OAAO,CAAC,uBAAuB,CAAC,CAwClC"}
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/sandbox.ts"],"names":[],"mappings":"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"}
@@ -214,7 +214,10 @@ async function checkoutSessionBranch(sandbox, workdir, { branch, baseBranch, tok
214
214
  if (current.exitCode === 0 && current.stdout.trim() === branch) return;
215
215
  if ((await sh(sandbox, `git -C ${shellQuote(workdir)} show-ref --verify --quiet refs/heads/${shellQuote(branch)}`)).exitCode === 0) {
216
216
  const checkout = await sh(sandbox, `git -C ${shellQuote(workdir)} checkout ${shellQuote(branch)}`);
217
- if (checkout.exitCode !== 0) throw classifyGitFailure(checkout, "clone-failed");
217
+ if (checkout.exitCode !== 0) {
218
+ if (isBlockedByLocalWork(checkout)) return;
219
+ throw classifyGitFailure(checkout, "clone-failed");
220
+ }
218
221
  return;
219
222
  }
220
223
  const authUrl = tokenUrl(repoFullName, token);
@@ -225,12 +228,25 @@ async function checkoutSessionBranch(sandbox, workdir, { branch, baseBranch, tok
225
228
  timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS,
226
229
  phase: "branch checkout"
227
230
  });
228
- if (fetch.exitCode !== 0) throw classifyGitFailure(fetch, "clone-failed");
231
+ if (fetch.exitCode !== 0) {
232
+ if (isBlockedByLocalWork(fetch)) return;
233
+ throw classifyGitFailure(fetch, "clone-failed");
234
+ }
229
235
  } finally {
230
236
  await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`);
231
237
  }
232
238
  }
233
239
  /**
240
+ * True when a failed `git checkout` just means uncommitted or untracked files
241
+ * in the working tree would be clobbered by the branch switch. Those files are
242
+ * a session's work in progress — the switch must yield to them, never the
243
+ * other way around.
244
+ */
245
+ function isBlockedByLocalWork(result) {
246
+ const output = `${result.stderr || ""}\n${result.stdout || ""}`;
247
+ 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);
248
+ }
249
+ /**
234
250
  * True when the workdir already holds a git checkout whose `origin` points at
235
251
  * this exact repo. Matches both the clean and token-auth URL forms; any other
236
252
  * remote (or no git dir at all) falls back to the clone path.
@@ -258,13 +274,17 @@ async function scrubRemote(sandbox, workdir, repoFullName, expectGitDir) {
258
274
  * branch can't be fast-forwarded — not that anything is broken. The shared
259
275
  * workdir is routinely left on a session's working branch, which may have
260
276
  * local commits diverging from upstream, no upstream at all (session branches
261
- * are created from `FETCH_HEAD`), or a detached HEAD. In all of those cases
262
- * the checkout is intact and holds the session's work; materialization must
263
- * keep it rather than fail the workspace open.
277
+ * are created from `FETCH_HEAD`), or a detached HEAD. A checkout can also
278
+ * hold uncommitted or untracked files (a build or script run left residue,
279
+ * or an older session worked directly in the shared checkout), which makes
280
+ * git refuse the merge outright. In all of these cases the checkout is
281
+ * intact and may hold real work; materialization must keep it as-is rather
282
+ * than fail the workspace open — and must never discard the local state to
283
+ * force the pull through.
264
284
  */
265
285
  function isBenignNonFastForward(result) {
266
286
  const output = `${result.stderr || ""}\n${result.stdout || ""}`;
267
- return /Not possible to fast-forward|Diverging branches can't be fast-forwarded|no tracking information for the current branch|You are not currently on a branch/i.test(output);
287
+ return /Not possible to fast-forward|Diverging branches can't be fast-forwarded|no tracking information for the current branch|You are not currently on a branch|Your local changes to the following files would be overwritten by merge|untracked working tree files would be overwritten by merge/i.test(output);
268
288
  }
269
289
  /**
270
290
  * Turn a failed git command into an actionable error, detecting the common
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.js","names":[],"sources":["../../../src/integrations/github/sandbox.ts"],"sourcesContent":["/**\n * Repo materialization for GitHub-backed repositories.\n *\n * A GitHub repo is never cloned onto the server host. Instead each project gets\n * its own isolated sandbox (provisioned by the fleet in `../sandbox/fleet`) and\n * the repo is cloned *inside* that sandbox. The agent's file tools and command\n * tools then operate entirely against the remote checkout.\n *\n * - `ensureProjectSandbox(row)` / `teardownProjectSandbox(row)` bind the fleet's\n * provision/reattach/teardown lifecycle to the per-(project,user) sandbox row.\n * - `materializeRepo(row, token)` runs `git clone` (first open) or `git pull`\n * (re-open) inside the sandbox, using a short-lived installation token that is\n * scrubbed from the git remote afterwards so it never persists in the VM.\n *\n * This module owns everything git/GitHub: clone/pull, commit/push, worktrees,\n * and `gh pr create`. Sandbox provisioning, budgets, and workdir layout live in\n * the fleet module.\n */\n\nimport { createHash } from 'node:crypto';\nimport { reportProgress } from '../../sandbox/fleet.js';\nimport type {\n MaterializationSandbox,\n ProgressFn,\n SandboxBindingStore,\n SandboxCommandResult,\n SandboxFleet,\n} from '../../sandbox/fleet.js';\nimport type {\n ProjectRepositorySandbox,\n SourceControlStorageHandle,\n} from '../../storage/domains/source-control/base.js';\n\ntype SourceControlSandboxStorage = SourceControlStorageHandle['sandboxes'];\ntype MaterializationStore = Pick<SourceControlSandboxStorage, 'markMaterialized'>;\n\ninterface RepoMaterializationBinding {\n id: string;\n sandboxWorkdir: string;\n materializedAt: Date | null;\n}\n\n/** Adapt a per-(project,user) sandbox binding row to the fleet's persistence seam. */\nfunction bindingStore(row: ProjectRepositorySandbox, storage: SourceControlSandboxStorage): SandboxBindingStore {\n return {\n sandboxId: row.sandboxId,\n setSandboxId: id =>\n id === null ? storage.clearBinding({ id: row.id }) : storage.setSandboxId({ id: row.id, sandboxId: id }),\n clear: () => storage.clearBinding({ id: row.id }),\n };\n}\n\n/**\n * Provision a new sandbox (persisting its provider id on first open) or\n * reattach to the stored one. Returns a started, live sandbox.\n */\nexport async function ensureProjectSandbox(options: {\n fleet: SandboxFleet;\n row: ProjectRepositorySandbox;\n storage: SourceControlSandboxStorage;\n token: string;\n onProgress?: ProgressFn;\n}): Promise<MaterializationSandbox> {\n const { fleet, row, storage, token, onProgress } = options;\n return fleet.ensureSandbox(bindingStore(row, storage), { GH_TOKEN: token }, onProgress);\n}\n\n/**\n * Tear down a user's sandbox for a project: stop the live VM (best-effort) and\n * clear the persisted `sandboxId`/`materializedAt` on the per-(project,user)\n * binding row so the next open re-provisions cleanly.\n *\n * @param row the per-(project,user) sandbox binding to tear down\n * @param sandbox an already-reattached live sandbox to stop, when available\n */\nexport async function teardownProjectSandbox(options: {\n fleet: SandboxFleet;\n row: ProjectRepositorySandbox;\n storage: SourceControlSandboxStorage;\n sandbox?: MaterializationSandbox;\n}): Promise<void> {\n const { fleet, row, storage, sandbox } = options;\n return fleet.teardownSandbox(bindingStore(row, storage), sandbox);\n}\n\n/**\n * Single-quote a string for safe POSIX shell interpolation. Wraps the value in\n * single quotes and escapes any embedded single quote using the canonical\n * close-quote / escaped-quote / reopen-quote sequence (`'\\''`). This is the\n * standard POSIX-safe construction and prevents the quoted string from being\n * terminated early.\n */\nexport function shellQuote(value: string): string {\n // Replace each ' with the four-character sequence: ' \\ ' '\n return `'` + value.split(`'`).join(`'\\\\''`) + `'`;\n}\n\n/**\n * Default hang guard for sandbox shell commands. Generous by design — large\n * clones and dependency installs legitimately take minutes; the guard exists\n * so a wedged sandbox surfaces a failure instead of hanging the request that\n * triggered materialization forever.\n */\nconst DEFAULT_COMMAND_TIMEOUT_MS = 15 * 60_000;\n/** Branch checkout only fetches one ref — a much tighter budget applies. */\nexport const CHECKOUT_COMMAND_TIMEOUT_MS = 5 * 60_000;\n\ninterface ShOptions {\n /** Override the hang-guard budget for this command. */\n timeoutMs?: number;\n /** Human-readable phase name included in the timeout error. */\n phase?: string;\n}\n\n/**\n * A thrown transport-level failure that is worth retrying: remote sandbox\n * providers (e.g. the platform workspace proxy) surface transient 5xx errors\n * as exceptions carrying an HTTP `status` — typically while a freshly\n * provisioned VM is still coming up. Command failures are NOT exceptions\n * (they resolve with a non-zero exit code), so retrying here never re-runs a\n * command that the sandbox already executed and rejected.\n */\nfunction isTransientTransportError(error: unknown): boolean {\n const status = (error as { status?: unknown })?.status;\n return typeof status === 'number' && status >= 500;\n}\n\nconst SH_RETRIES = 2;\nconst SH_RETRY_DELAY_MS = 2000;\n\n/**\n * Run a shell script in the sandbox via `sh -c`, bounded by a hang guard.\n * Transient transport-level 5xx failures (proxy hiccups while the VM boots)\n * are retried with a short backoff; every script routed through here is safe\n * to re-run. Hang-guard timeouts are NOT retried — the budget applies to the\n * command as a whole.\n */\nasync function sh(\n sandbox: MaterializationSandbox,\n script: string,\n options: ShOptions = {},\n): Promise<SandboxCommandResult> {\n // One budget for the command as a whole: each attempt only gets the time\n // remaining, so transport retries can never multiply the hang guard.\n const deadlineMs = Date.now() + (options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);\n for (let attempt = 0; ; attempt++) {\n try {\n return await shOnce(sandbox, script, { ...options, timeoutMs: Math.max(deadlineMs - Date.now(), 1) });\n } catch (error) {\n if (attempt >= SH_RETRIES || !isTransientTransportError(error)) throw error;\n const delayMs = SH_RETRY_DELAY_MS * (attempt + 1);\n if (deadlineMs - Date.now() <= delayMs) throw error;\n await new Promise(resolve => setTimeout(resolve, delayMs));\n }\n }\n}\n\n/** Single `sh -c` execution attempt, bounded by the hang guard. */\nasync function shOnce(\n sandbox: MaterializationSandbox,\n script: string,\n options: ShOptions,\n): Promise<SandboxCommandResult> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const hangGuard = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n const phase = options.phase ? ` during ${options.phase}` : '';\n reject(new Error(`Sandbox command timed out after ${Math.round(timeoutMs / 1000)}s${phase}.`));\n }, timeoutMs);\n timer.unref?.();\n });\n try {\n // Forward the budget to the provider too so it can terminate the wedged\n // process; the race stays as the outer guard for providers that ignore it.\n return await Promise.race([sandbox.executeCommand('sh', ['-c', script], { timeout: timeoutMs }), hangGuard]);\n } finally {\n clearTimeout(timer);\n }\n}\n\n/** Error raised when the sandbox cannot materialize the repo (actionable). */\nexport class MaterializeError extends Error {\n constructor(\n message: string,\n readonly code:\n | 'git-missing'\n | 'egress-blocked'\n | 'clone-failed'\n | 'pull-failed'\n | 'push-failed'\n | 'commit-failed'\n | 'gh-missing'\n | 'pr-failed',\n ) {\n super(message);\n this.name = 'MaterializeError';\n }\n}\n\n/**\n * Build the token-auth clone/pull URL for a repo. The token lives only inside\n * this URL and is scrubbed from the remote after the operation.\n */\nfunction tokenUrl(repoFullName: string, token: string): string {\n return `https://x-access-token:${token}@github.com/${repoFullName}.git`;\n}\n\nfunction cleanUrl(repoFullName: string): string {\n return `https://github.com/${repoFullName}.git`;\n}\n\n/** Repo metadata needed to materialize, read from the org-owned project row. */\nexport interface RepoMaterializeInfo {\n repoFullName: string;\n defaultBranch: string;\n}\n\n/**\n * Materialize the repo inside the user's sandbox. Clones on first open, pulls on\n * re-open. Always scrubs the install token from the remote afterwards and sets\n * `materialized_at` on the per-user sandbox binding row.\n *\n */\nexport async function materializeRepo(options: {\n /** The per-(project,user) sandbox binding (provisioned via `ensureProjectSandbox`). */\n row: RepoMaterializationBinding;\n /** Repo metadata from the org-owned project row. */\n repoInfo: RepoMaterializeInfo;\n /** The live sandbox to run git inside. */\n sandbox: MaterializationSandbox;\n /** A freshly minted, short-lived installation access token. */\n token: string;\n storage: MaterializationStore;\n onProgress?: ProgressFn;\n}): Promise<void> {\n const { row: sandboxRow, repoInfo, sandbox, token, storage, onProgress } = options;\n const workdir = sandboxRow.sandboxWorkdir;\n const repo = repoInfo.repoFullName;\n\n // 0. Defense in depth: never build a git command from values that aren't\n // strictly shaped, even if a malformed row reached the DB. Inputs are also\n // validated at the route boundary before storage.\n if (!/^[\\w.-]+\\/[\\w.-]+$/.test(repo)) {\n throw new MaterializeError(`Refusing to materialize: invalid repo full name '${repo}'.`, 'clone-failed');\n }\n if (!/^[A-Za-z0-9_./-]+$/.test(repoInfo.defaultBranch)) {\n throw new MaterializeError(\n `Refusing to materialize: invalid default branch '${repoInfo.defaultBranch}'.`,\n 'clone-failed',\n );\n }\n\n // 1. Preflight: git must be installed in the sandbox template.\n const gitVersion = await sh(sandbox, 'git --version');\n if (gitVersion.exitCode !== 0) {\n throw new MaterializeError(\n 'git is not installed in the sandbox. The sandbox template must include git.',\n 'git-missing',\n );\n }\n\n const authUrl = tokenUrl(repo, token);\n\n // The DB's `materializedAt` can drift from disk in both directions: a fresh\n // binding row over an already-populated workdir (local dev DB resets,\n // repaired rows, earlier flows) must pull instead of failing `git clone` on\n // the non-empty directory, and a stale `materializedAt` over an empty\n // sandbox (an expired/recreated VM whose disk was wiped) must re-clone\n // instead of running `git -C <workdir>` against a directory that no longer\n // exists. Disk is the source of truth: detect the checkout instead of\n // trusting the row.\n const alreadyMaterialized = await hasExistingCheckout(sandbox, workdir, repo);\n\n let succeeded = false;\n try {\n if (!alreadyMaterialized) {\n // 2a. First open: shallow-clone the default branch into the workdir. A\n // shallow single-branch clone is dramatically faster for large repos; the\n // later re-open uses `git pull --ff-only`, which works on shallow clones.\n reportProgress(onProgress, {\n phase: 'cloning',\n message: `Cloning ${repo} (first open can take a minute)…`,\n });\n const clone = await sh(\n sandbox,\n `git clone --depth=1 --single-branch --branch ${shellQuote(repoInfo.defaultBranch)} ${shellQuote(authUrl)} ${shellQuote(workdir)}`,\n { phase: 'repository clone' },\n );\n if (clone.exitCode !== 0) {\n throw classifyGitFailure(clone, 'clone-failed');\n }\n } else {\n // 2b. Re-open: refresh remote to the token URL and fast-forward pull.\n reportProgress(onProgress, { phase: 'pulling', message: `Updating ${repo} to the latest changes…` });\n const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);\n if (setUrl.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr}`, 'pull-failed');\n }\n const pull = await sh(sandbox, `git -C ${shellQuote(workdir)} pull --ff-only`, { phase: 'repository pull' });\n if (pull.exitCode !== 0) {\n if (!isBenignNonFastForward(pull)) {\n throw classifyGitFailure(pull, 'pull-failed');\n }\n // The workdir was left on a session's working branch that can't be\n // fast-forwarded (diverged from upstream, no upstream, or detached\n // HEAD). That branch holds the session's local work — never rebase or\n // reset it here. The checkout is still perfectly usable; leave it\n // as-is and let the session reconcile with the remote itself.\n reportProgress(onProgress, {\n phase: 'pulling',\n message: 'Workspace has local changes that diverge from the remote — keeping them as-is.',\n });\n }\n }\n succeeded = true;\n } finally {\n // 3. Always scrub the token from the remote so it isn't left in the VM's\n // git config, even when the clone/pull above failed partway through. This\n // is best-effort on the failure path (the workdir may not even exist, and\n // a scrub error must never mask the primary failure); on the success path\n // the scrub must succeed or we surface it.\n await scrubRemote(sandbox, workdir, repo, succeeded);\n }\n\n // 4. Mark materialized.\n reportProgress(onProgress, { phase: 'finalizing', message: 'Finalizing workspace…' });\n await storage.markMaterialized({ id: sandboxRow.id });\n}\n\n/**\n * Reset a pooled workdir that a new session just claimed: the previous\n * session's branch and dirty state must not leak into the new session, so\n * force-checkout the default branch and drop all local modifications. When\n * the claimed VM was reaped and re-provisioned there is no checkout yet and\n * this is a no-op (the clone path handles it). A wedged checkout falls back\n * to wiping the workdir so `materializeRepo` re-clones inside the same VM\n * instead of permanently failing the session.\n */\nexport async function recycleClaimedWorkdir(\n sandbox: MaterializationSandbox,\n workdir: string,\n defaultBranch: string,\n): Promise<void> {\n if (!/^[A-Za-z0-9_./-]+$/.test(defaultBranch)) {\n throw new MaterializeError(`Refusing to recycle: invalid default branch '${defaultBranch}'.`, 'clone-failed');\n }\n const w = shellQuote(workdir);\n const inspect = await sh(sandbox, `git -C ${w} rev-parse --is-inside-work-tree`);\n if (inspect.exitCode !== 0) return;\n const recycle = await sh(\n sandbox,\n // `-x` also drops gitignored files (.env, build caches) so no session\n // state survives into the next claim.\n `git -C ${w} checkout -f ${shellQuote(defaultBranch)} && git -C ${w} reset --hard && git -C ${w} clean -fdx`,\n { phase: 'claimed workdir recycle' },\n );\n if (recycle.exitCode === 0) return;\n const wipe = await sh(sandbox, `rm -rf ${w}`);\n if (wipe.exitCode !== 0) {\n throw new MaterializeError(`Failed to recycle claimed sandbox workdir: ${recycle.stderr}`, 'clone-failed');\n }\n}\n\n/** Check out a session's branch inside its isolated repository clone. */\nexport async function checkoutSessionBranch(\n sandbox: MaterializationSandbox,\n workdir: string,\n {\n branch,\n baseBranch,\n token,\n repoFullName,\n }: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<void> {\n if (!isValidGitRef(branch) || !isValidGitRef(baseBranch)) {\n throw new MaterializeError('Refusing to create a session from an invalid branch name.', 'clone-failed');\n }\n\n const current = await sh(sandbox, `git -C ${shellQuote(workdir)} branch --show-current`);\n if (current.exitCode === 0 && current.stdout.trim() === branch) return;\n\n const local = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} show-ref --verify --quiet refs/heads/${shellQuote(branch)}`,\n );\n if (local.exitCode === 0) {\n const checkout = await sh(sandbox, `git -C ${shellQuote(workdir)} checkout ${shellQuote(branch)}`);\n if (checkout.exitCode !== 0) throw classifyGitFailure(checkout, 'clone-failed');\n return;\n }\n\n const authUrl = tokenUrl(repoFullName, token);\n try {\n const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);\n if (setUrl.exitCode !== 0) throw classifyGitFailure(setUrl, 'pull-failed');\n const fetch = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} fetch origin ${shellQuote(baseBranch)} && git -C ${shellQuote(workdir)} checkout -b ${shellQuote(branch)} FETCH_HEAD`,\n { timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS, phase: 'branch checkout' },\n );\n if (fetch.exitCode !== 0) throw classifyGitFailure(fetch, 'clone-failed');\n } finally {\n await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`);\n }\n}\n\n/**\n * True when the workdir already holds a git checkout whose `origin` points at\n * this exact repo. Matches both the clean and token-auth URL forms; any other\n * remote (or no git dir at all) falls back to the clone path.\n */\nasync function hasExistingCheckout(\n sandbox: MaterializationSandbox,\n workdir: string,\n repoFullName: string,\n): Promise<boolean> {\n const result = await sh(sandbox, `git -C ${shellQuote(workdir)} remote get-url origin`);\n if (result.exitCode !== 0) return false;\n const url = result.stdout.trim().toLowerCase();\n const suffix = `github.com/${repoFullName.toLowerCase()}`;\n return url.endsWith(`${suffix}.git`) || url.endsWith(suffix);\n}\n\n/**\n * Reset the git remote back to the tokenless URL. On a successful clone/pull the\n * workdir always has a `.git`, so a non-zero exit code here means the token may\n * still be persisted — surface it. On the failure path the workdir may not exist\n * (e.g. a failed clone), so a non-zero exit is tolerated — and never masks the\n * primary failure being thrown through the `finally`.\n */\nasync function scrubRemote(\n sandbox: MaterializationSandbox,\n workdir: string,\n repoFullName: string,\n expectGitDir: boolean,\n): Promise<void> {\n const result = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`,\n );\n if (result.exitCode !== 0 && expectGitDir) {\n throw new MaterializeError(\n `Failed to scrub installation token from git remote: ${result.stderr.trim() || result.stdout.trim()}`,\n 'pull-failed',\n );\n }\n}\n\n/**\n * True when a failed `git pull --ff-only` on re-open just means the current\n * branch can't be fast-forwarded — not that anything is broken. The shared\n * workdir is routinely left on a session's working branch, which may have\n * local commits diverging from upstream, no upstream at all (session branches\n * are created from `FETCH_HEAD`), or a detached HEAD. In all of those cases\n * the checkout is intact and holds the session's work; materialization must\n * keep it rather than fail the workspace open.\n */\nfunction isBenignNonFastForward(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /Not possible to fast-forward|Diverging branches can't be fast-forwarded|no tracking information for the current branch|You are not currently on a branch/i.test(\n output,\n );\n}\n\n/**\n * Turn a failed git command into an actionable error, detecting the common\n * \"cannot reach github.com\" egress failure.\n */\nfunction classifyGitFailure(\n result: SandboxCommandResult,\n fallback: 'clone-failed' | 'pull-failed' | 'push-failed',\n): MaterializeError {\n const stderr = result.stderr || '';\n if (/could not resolve host|failed to connect|network is unreachable|Connection timed out/i.test(stderr)) {\n return new MaterializeError(\n 'The sandbox could not reach github.com. The sandbox network must allow outbound egress to github.com.',\n 'egress-blocked',\n );\n }\n const verb = fallback === 'clone-failed' ? 'clone' : fallback === 'pull-failed' ? 'pull' : 'push';\n return new MaterializeError(`git ${verb} failed: ${stderr}`, fallback);\n}\n\n// ---------------------------------------------------------------------------\n// Phase 1 — git identity + token-scoped push primitive\n//\n// These helpers let the sandbox author and push commits safely. The install\n// token is short-lived, minted per-operation server-side, injected only into\n// the temporary remote URL inside the sandbox, and always scrubbed in a\n// `finally` so it never persists in `.git/config`.\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a git ref (branch) name. Server-side defense-in-depth: only allow a\n * conservative character set so a branch can never be built into a shell\n * command in a way that escapes quoting. Mirrors the route-layer check.\n */\nexport function isValidGitRef(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length > 0 &&\n value.length <= 255 &&\n // Reject leading-dash refs (e.g. `--mirror`) so the value can never be\n // parsed as a git option when interpolated into a command.\n !value.startsWith('-') &&\n /^[A-Za-z0-9_./-]+$/.test(value)\n );\n}\n\n/** Identity used to author commits inside the sandbox. */\nexport interface GitIdentity {\n name?: string | null;\n email?: string | null;\n /** GitHub login, used to derive a stable noreply identity when name/email are absent. */\n login?: string | null;\n}\n\n/**\n * Resolve a concrete `{ name, email }` for git authorship from a possibly-sparse\n * identity. Falls back to a GitHub-style noreply identity so commits are never\n * authored with an empty or host-derived identity.\n */\nexport function resolveGitIdentity(identity: GitIdentity): { name: string; email: string } {\n const login = (identity.login || '').trim();\n const name = (identity.name || '').trim() || login || 'Mastra Code';\n const email =\n (identity.email || '').trim() ||\n (login ? `${login}@users.noreply.github.com` : 'mastra-code@users.noreply.github.com');\n return { name, email };\n}\n\n/**\n * Configure `user.name` / `user.email` for the given repo working tree inside\n * the sandbox so commits are authored correctly. Values are shell-quoted.\n */\nexport async function configureGitIdentity(\n sandbox: MaterializationSandbox,\n workdir: string,\n identity: GitIdentity,\n): Promise<void> {\n const { name, email } = resolveGitIdentity(identity);\n const setName = await sh(sandbox, `git -C ${shellQuote(workdir)} config user.name ${shellQuote(name)}`);\n if (setName.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git user.name: ${setName.stderr.trim()}`, 'commit-failed');\n }\n const setEmail = await sh(sandbox, `git -C ${shellQuote(workdir)} config user.email ${shellQuote(email)}`);\n if (setEmail.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git user.email: ${setEmail.stderr.trim()}`, 'commit-failed');\n }\n}\n\n/**\n * Temporarily rewrite `origin` to a tokenized URL, run `fn` (e.g. a push), and\n * **always** scrub the remote back to the tokenless URL in a `finally`. The\n * token therefore only ever lives in the remote URL for the duration of the\n * operation and is never left in the VM's git config.\n *\n * On the success path the scrub must succeed (a leaked token is a hard error);\n * if it fails we surface it. On the failure path the scrub is best-effort but\n * still attempted, and the original operation error is rethrown.\n */\nexport async function withInstallToken<T>(\n sandbox: MaterializationSandbox,\n workdir: string,\n repoFullName: string,\n token: string,\n fn: () => Promise<T>,\n): Promise<T> {\n if (!/^[\\w.-]+\\/[\\w.-]+$/.test(repoFullName)) {\n throw new MaterializeError(`Refusing to push: invalid repo full name '${repoFullName}'.`, 'push-failed');\n }\n\n const setUrl = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(tokenUrl(repoFullName, token))}`,\n );\n if (setUrl.exitCode !== 0) {\n // Best-effort scrub even though set-url failed, then surface the failure.\n await scrubRemote(sandbox, workdir, repoFullName, false);\n throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr.trim()}`, 'push-failed');\n }\n\n try {\n return await fn();\n } finally {\n // Always restore the tokenless remote. The workdir has a `.git` (we just\n // rewrote its remote) so a scrub failure means the token may still persist\n // — surface it.\n await scrubRemote(sandbox, workdir, repoFullName, true);\n }\n}\n\n/**\n * Push a branch back to GitHub from inside the sandbox using a short-lived\n * installation token. The branch is ref-validated, the token is injected only\n * into the remote URL via `withInstallToken`, and egress failures are\n * classified into actionable errors.\n */\nexport async function pushBranch(\n sandbox: MaterializationSandbox,\n workdir: string,\n branch: string,\n token: string,\n repoFullName: string,\n): Promise<void> {\n if (!isValidGitRef(branch)) {\n throw new MaterializeError(`Refusing to push: invalid branch name '${branch}'.`, 'push-failed');\n }\n\n await withInstallToken(sandbox, workdir, repoFullName, token, async () => {\n const push = await sh(sandbox, `git -C ${shellQuote(workdir)} push -u origin ${shellQuote(branch)}`);\n if (push.exitCode !== 0) {\n throw classifyGitFailure(push, 'push-failed');\n }\n });\n}\n\nexport interface CommitResult {\n /** True when a commit was created; false when there was nothing to commit. */\n committed: boolean;\n}\n\n/**\n * Stage every change in the working tree and create a commit inside the\n * sandbox. The git identity is configured first so authorship is correct. When\n * there is nothing to commit this is a no-op (`committed: false`) rather than an\n * error, so callers can safely commit-then-push without first diffing.\n *\n * @param sandbox the live sandbox containing the checkout\n * @param workdir the worktree (or repo) path to commit in\n * @param message the commit message (quoted; arbitrary text is safe)\n * @param identity authorship identity for the commit\n */\nexport async function commitAll(\n sandbox: MaterializationSandbox,\n workdir: string,\n message: string,\n identity: GitIdentity,\n): Promise<CommitResult> {\n await configureGitIdentity(sandbox, workdir, identity);\n\n const add = await sh(sandbox, `git -C ${shellQuote(workdir)} add -A`);\n if (add.exitCode !== 0) {\n throw new MaterializeError(`git add failed: ${add.stderr.trim() || add.stdout.trim()}`, 'commit-failed');\n }\n\n // Nothing staged → nothing to commit. `git diff --cached --quiet` exits 1 when\n // there are staged changes, 0 when the index is clean.\n const staged = await sh(sandbox, `git -C ${shellQuote(workdir)} diff --cached --quiet`);\n if (staged.exitCode === 0) {\n return { committed: false };\n }\n\n const commit = await sh(sandbox, `git -C ${shellQuote(workdir)} commit -m ${shellQuote(message)}`);\n if (commit.exitCode !== 0) {\n throw new MaterializeError(`git commit failed: ${commit.stderr.trim() || commit.stdout.trim()}`, 'commit-failed');\n }\n\n return { committed: true };\n}\n\n// ---------------------------------------------------------------------------\n// Phase 2 — worktree / branch lifecycle\n//\n// Each unit of work gets its own branch + working tree inside the same sandbox\n// as the base checkout. The worktree path is always computed server-side from a\n// sanitized branch name; client input never reaches a filesystem path.\n// ---------------------------------------------------------------------------\n\n/** Error raised when a worktree cannot be created/reused inside the sandbox. */\nexport class WorktreeError extends Error {\n constructor(\n message: string,\n readonly code: 'invalid-branch' | 'worktree-failed' | 'setup-failed',\n ) {\n super(message);\n this.name = 'WorktreeError';\n }\n}\n\n/**\n * Reduce a (already ref-validated) branch name to a filesystem-safe directory\n * segment for the worktree path: slashes/dots/unsafe chars collapsed to `-`.\n * This only affects the *directory name*, never the git branch itself.\n *\n * Sanitization is lossy (e.g. `feat/a` and `feat-a` both reduce to `feat-a`),\n * so an 8-char hash of the original branch is appended whenever the sanitized\n * form differs from the input. That keeps clean names (`main`) readable while\n * guaranteeing distinct branches never share a worktree directory.\n */\nexport function safeBranchDir(branch: string): string {\n const sanitized =\n branch\n .replace(/[^A-Za-z0-9._-]+/g, '-')\n .replace(/\\/+/g, '-')\n .replace(/^[-.]+|[-.]+$/g, '')\n .slice(0, 100) || 'work';\n if (sanitized === branch) return sanitized;\n const hash = createHash('sha256').update(branch).digest('hex').slice(0, 8);\n return `${sanitized}-${hash}`;\n}\n\n/**\n * Compute the absolute worktree path for a branch, server-side only. Worktrees\n * live alongside the repo checkout under a sibling `worktrees/` directory so the\n * repo's `.git` is shared. Never derived from client-supplied paths.\n */\nexport function computeWorktreePath(repoWorkdir: string, branch: string): string {\n const parent = repoWorkdir.replace(/\\/+$/, '').split('/').slice(0, -1).join('/') || '';\n return `${parent}/worktrees/${safeBranchDir(branch)}`;\n}\n\nexport interface EnsureWorktreeResult {\n worktreePath: string;\n branch: string;\n baseBranch: string;\n /** True when an existing worktree was reused rather than freshly created. */\n reused: boolean;\n}\n\n/**\n * Create (or reuse) a git worktree + branch inside the sandbox for a unit of\n * work. Idempotent: if a worktree already exists at the computed path it is\n * reused. The branch is created from the freshly fetched `origin/<baseBranch>`\n * — never the sandbox's possibly stale local ref — so new worktrees always\n * start from the latest remote state.\n *\n * @param sandbox live sandbox containing the base checkout\n * @param repoWorkdir the base repo checkout path inside the sandbox\n * @param branch the feature branch (ref-validated server-side)\n * @param baseBranch the branch to fork from (ref-validated; defaults to the repo's default branch)\n * @param token short-lived installation token used only for the base-branch fetch\n * @param repoFullName `owner/repo` used to build the tokenized remote URL\n */\nexport async function ensureWorktree(\n sandbox: MaterializationSandbox,\n repoWorkdir: string,\n {\n branch,\n baseBranch,\n token,\n repoFullName,\n }: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<EnsureWorktreeResult> {\n if (!isValidGitRef(branch)) {\n throw new WorktreeError(`Invalid branch name '${branch}'.`, 'invalid-branch');\n }\n if (!isValidGitRef(baseBranch)) {\n throw new WorktreeError(`Invalid base branch name '${baseBranch}'.`, 'invalid-branch');\n }\n\n const worktreePath = computeWorktreePath(repoWorkdir, branch);\n\n // Idempotent reuse: a worktree already checked out at this path has a `.git`\n // file (worktrees use a gitfile, not a directory). Reuse it as-is.\n const exists = await sh(sandbox, `test -e ${shellQuote(`${worktreePath}/.git`)}`);\n if (exists.exitCode === 0) {\n return { worktreePath, branch, baseBranch, reused: true };\n }\n\n // Fetch the latest base ref from origin before forking. The explicit refspec\n // updates `refs/remotes/origin/<base>` even when the checkout was created as\n // a single-branch clone. The fetch needs the install token (the resting\n // remote is tokenless), and a failure is a hard error — silently forking a\n // stale local ref is worse than failing the request.\n const baseRef = `origin/${baseBranch}`;\n await withInstallToken(sandbox, repoWorkdir, repoFullName, token, async () => {\n const fetch = await sh(\n sandbox,\n `git -C ${shellQuote(repoWorkdir)} fetch origin ${shellQuote(`+refs/heads/${baseBranch}:refs/remotes/${baseRef}`)}`,\n );\n if (fetch.exitCode !== 0) {\n throw classifyGitFailure(fetch, 'pull-failed');\n }\n });\n\n // Create the worktree. If the branch already exists, check it out into the\n // worktree; otherwise create it from the fetched base. `git worktree add -B`\n // creates-or-resets the branch to the base, which keeps this idempotent for a\n // fresh worktree while still working when the branch already exists remotely.\n // `--no-track` keeps the feature branch from tracking origin/<base>; pushes\n // set their own upstream via `push -u`.\n const add = await sh(\n sandbox,\n `git -C ${shellQuote(repoWorkdir)} worktree add --no-track -B ${shellQuote(branch)} ${shellQuote(worktreePath)} ${shellQuote(baseRef)}`,\n );\n if (add.exitCode !== 0) {\n throw new WorktreeError(`git worktree add failed: ${add.stderr.trim() || add.stdout.trim()}`, 'worktree-failed');\n }\n\n return { worktreePath, branch, baseBranch, reused: false };\n}\n\n/**\n * Run the project's setup command (e.g. `pnpm i && pnpm build`) inside a\n * freshly created worktree. Called before the worktree is handed to any agent\n * run so the checkout is ready to build/test. A non-zero exit is a hard error —\n * starting agent work in a half-set-up tree is worse than failing the request.\n *\n * Security model: the command is intentionally arbitrary shell — that is the\n * feature (install deps, build, seed fixtures). It is only configurable by\n * authenticated org members (the settings route is gated by\n * `resolveOrgTenant` + org-scoped project lookup, with length and\n * control-character validation), and it executes exclusively inside the\n * project's isolated sandbox — the same environment where org members already\n * run arbitrary shell via the agent's command tool. It never runs on the web\n * server host, so it grants no privilege beyond what sandbox access already\n * provides.\n *\n * @param sandbox live sandbox containing the worktree\n * @param worktreePath the server-computed worktree path the command runs in\n * @param command the org-configured setup shell command\n */\nexport async function runWorktreeSetup(\n sandbox: MaterializationSandbox,\n worktreePath: string,\n command: string,\n): Promise<void> {\n const result = await sh(sandbox, `cd ${shellQuote(worktreePath)} && { ${command}\\n}`, { phase: 'worktree setup' });\n if (result.exitCode !== 0) {\n const detail = (result.stderr.trim() || result.stdout.trim()).slice(-2000);\n throw new WorktreeError(`Setup command failed (exit ${result.exitCode}): ${detail}`, 'setup-failed');\n }\n}\n\n/**\n * Remove a worktree (and its local feature branch) from the sandbox. The\n * checkout is removed with `--force` — the caller owns confirming that any\n * uncommitted work in it can be discarded. Idempotent: a worktree whose\n * directory is already gone only has its metadata pruned.\n *\n * @param sandbox live sandbox containing the base checkout\n * @param repoWorkdir the base repo checkout path inside the sandbox\n * @param branch the worktree's feature branch (ref-validated)\n * @param worktreePath the persisted, server-computed worktree path\n */\nexport async function removeWorktree(\n sandbox: MaterializationSandbox,\n repoWorkdir: string,\n { branch, worktreePath }: { branch: string; worktreePath: string },\n): Promise<void> {\n if (!isValidGitRef(branch)) {\n throw new WorktreeError(`Invalid branch name '${branch}'.`, 'invalid-branch');\n }\n\n const remove = await sh(\n sandbox,\n `git -C ${shellQuote(repoWorkdir)} worktree remove --force ${shellQuote(worktreePath)}`,\n );\n if (remove.exitCode !== 0) {\n // Tolerate a checkout that's already gone (e.g. a fresh sandbox after\n // re-provisioning): prune stale metadata and only fail when the directory\n // still exists, meaning git genuinely refused to remove it.\n await sh(sandbox, `git -C ${shellQuote(repoWorkdir)} worktree prune`);\n const exists = await sh(sandbox, `test -e ${shellQuote(worktreePath)}`);\n if (exists.exitCode === 0) {\n throw new WorktreeError(\n `git worktree remove failed: ${remove.stderr.trim() || remove.stdout.trim()}`,\n 'worktree-failed',\n );\n }\n }\n\n // Best-effort local branch cleanup; the branch may not exist locally anymore\n // or may still be pushed remotely — neither should fail the removal.\n await sh(sandbox, `git -C ${shellQuote(repoWorkdir)} branch -D ${shellQuote(branch)}`);\n}\n\n// ---------------------------------------------------------------------------\n// Phase 3 — `gh` CLI pull-request creation primitive\n//\n// PRs are opened from inside the sandbox with the GitHub CLI. `gh` must be\n// present in the sandbox template (preflighted only on the PR path so clone /\n// open still work when it is absent). The token is passed to `gh` via a\n// per-invocation `GH_TOKEN` env that is scoped to the single `gh` process and\n// never written to git config, a shell rc, or the VM's environment.\n// ---------------------------------------------------------------------------\n\nexport interface CreatePullRequestArgs {\n /** Short-lived installation token, injected only into the `gh` process env. */\n token: string;\n /** Base branch the PR merges into. Ref-validated. */\n base: string;\n /** Head branch the PR is opened from. Ref-validated. */\n head: string;\n /** PR title. */\n title: string;\n /** PR body (optional). */\n body?: string;\n}\n\nexport interface CreatePullRequestResult {\n /** The PR URL parsed from `gh pr create` stdout. */\n url: string;\n}\n\n/**\n * Preflight that `gh` is installed in the sandbox. Only called on the PR path so\n * a missing `gh` never blocks clone/open. Surfaces an actionable error naming\n * the sandbox template requirement.\n */\nasync function assertGhAvailable(sandbox: MaterializationSandbox): Promise<void> {\n const version = await sh(sandbox, 'gh --version');\n if (version.exitCode !== 0) {\n throw new MaterializeError(\n 'The GitHub CLI (gh) is not installed in the sandbox. The sandbox template must include gh to open pull requests.',\n 'gh-missing',\n );\n }\n}\n\n/** Match the first GitHub PR URL in `gh pr create` output. */\nfunction parsePullRequestUrl(stdout: string): string | undefined {\n const match = stdout.match(/https:\\/\\/github\\.com\\/[^\\s]+\\/pull\\/\\d+/);\n return match?.[0];\n}\n\n/**\n * Open a pull request from inside the sandbox via `gh pr create`. The token is\n * passed only through a per-invocation `GH_TOKEN` env scoped to the single `gh`\n * process (never persisted), all arguments are shell-quoted, and the resulting\n * PR URL is parsed from stdout.\n *\n * @param sandbox live sandbox containing the checkout\n * @param workdir the worktree (or repo) path the PR head branch is checked out in\n */\nexport async function createPullRequest(\n sandbox: MaterializationSandbox,\n workdir: string,\n { token, base, head, title, body }: CreatePullRequestArgs,\n): Promise<CreatePullRequestResult> {\n if (!isValidGitRef(base)) {\n throw new MaterializeError(`Refusing to open PR: invalid base branch '${base}'.`, 'pr-failed');\n }\n if (!isValidGitRef(head)) {\n throw new MaterializeError(`Refusing to open PR: invalid head branch '${head}'.`, 'pr-failed');\n }\n\n await assertGhAvailable(sandbox);\n\n // GH_TOKEN is prefixed inline so it is exported only to the single `gh`\n // process and never to the wider shell session, git config, or VM env. `gh`\n // is run from inside the checkout so it targets the correct repo/head branch.\n const ghCommand = [\n `GH_TOKEN=${shellQuote(token)} gh pr create`,\n `--base ${shellQuote(base)}`,\n `--head ${shellQuote(head)}`,\n `--title ${shellQuote(title)}`,\n `--body ${shellQuote(body ?? '')}`,\n ].join(' ');\n const script = `cd ${shellQuote(workdir)} && ${ghCommand}`;\n\n const result = await sh(sandbox, script);\n if (result.exitCode !== 0) {\n const classified = classifyGitFailure(result, 'push-failed');\n if (classified.code === 'egress-blocked') {\n throw classified;\n }\n throw new MaterializeError(`gh pr create failed: ${result.stderr.trim() || result.stdout.trim()}`, 'pr-failed');\n }\n\n const url = parsePullRequestUrl(result.stdout);\n if (!url) {\n throw new MaterializeError(\n `gh pr create succeeded but no PR URL was found in its output: ${result.stdout.trim()}`,\n 'pr-failed',\n );\n }\n\n return { url };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sandbox.js","names":[],"sources":["../../../src/integrations/github/sandbox.ts"],"sourcesContent":["/**\n * Repo materialization for GitHub-backed repositories.\n *\n * A GitHub repo is never cloned onto the server host. Instead each project gets\n * its own isolated sandbox (provisioned by the fleet in `../sandbox/fleet`) and\n * the repo is cloned *inside* that sandbox. The agent's file tools and command\n * tools then operate entirely against the remote checkout.\n *\n * - `ensureProjectSandbox(row)` / `teardownProjectSandbox(row)` bind the fleet's\n * provision/reattach/teardown lifecycle to the per-(project,user) sandbox row.\n * - `materializeRepo(row, token)` runs `git clone` (first open) or `git pull`\n * (re-open) inside the sandbox, using a short-lived installation token that is\n * scrubbed from the git remote afterwards so it never persists in the VM.\n *\n * This module owns everything git/GitHub: clone/pull, commit/push, worktrees,\n * and `gh pr create`. Sandbox provisioning, budgets, and workdir layout live in\n * the fleet module.\n */\n\nimport { createHash } from 'node:crypto';\nimport { reportProgress } from '../../sandbox/fleet.js';\nimport type {\n MaterializationSandbox,\n ProgressFn,\n SandboxBindingStore,\n SandboxCommandResult,\n SandboxFleet,\n} from '../../sandbox/fleet.js';\nimport type {\n ProjectRepositorySandbox,\n SourceControlStorageHandle,\n} from '../../storage/domains/source-control/base.js';\n\ntype SourceControlSandboxStorage = SourceControlStorageHandle['sandboxes'];\ntype MaterializationStore = Pick<SourceControlSandboxStorage, 'markMaterialized'>;\n\ninterface RepoMaterializationBinding {\n id: string;\n sandboxWorkdir: string;\n materializedAt: Date | null;\n}\n\n/** Adapt a per-(project,user) sandbox binding row to the fleet's persistence seam. */\nfunction bindingStore(row: ProjectRepositorySandbox, storage: SourceControlSandboxStorage): SandboxBindingStore {\n return {\n sandboxId: row.sandboxId,\n setSandboxId: id =>\n id === null ? storage.clearBinding({ id: row.id }) : storage.setSandboxId({ id: row.id, sandboxId: id }),\n clear: () => storage.clearBinding({ id: row.id }),\n };\n}\n\n/**\n * Provision a new sandbox (persisting its provider id on first open) or\n * reattach to the stored one. Returns a started, live sandbox.\n */\nexport async function ensureProjectSandbox(options: {\n fleet: SandboxFleet;\n row: ProjectRepositorySandbox;\n storage: SourceControlSandboxStorage;\n token: string;\n onProgress?: ProgressFn;\n}): Promise<MaterializationSandbox> {\n const { fleet, row, storage, token, onProgress } = options;\n return fleet.ensureSandbox(bindingStore(row, storage), { GH_TOKEN: token }, onProgress);\n}\n\n/**\n * Tear down a user's sandbox for a project: stop the live VM (best-effort) and\n * clear the persisted `sandboxId`/`materializedAt` on the per-(project,user)\n * binding row so the next open re-provisions cleanly.\n *\n * @param row the per-(project,user) sandbox binding to tear down\n * @param sandbox an already-reattached live sandbox to stop, when available\n */\nexport async function teardownProjectSandbox(options: {\n fleet: SandboxFleet;\n row: ProjectRepositorySandbox;\n storage: SourceControlSandboxStorage;\n sandbox?: MaterializationSandbox;\n}): Promise<void> {\n const { fleet, row, storage, sandbox } = options;\n return fleet.teardownSandbox(bindingStore(row, storage), sandbox);\n}\n\n/**\n * Single-quote a string for safe POSIX shell interpolation. Wraps the value in\n * single quotes and escapes any embedded single quote using the canonical\n * close-quote / escaped-quote / reopen-quote sequence (`'\\''`). This is the\n * standard POSIX-safe construction and prevents the quoted string from being\n * terminated early.\n */\nexport function shellQuote(value: string): string {\n // Replace each ' with the four-character sequence: ' \\ ' '\n return `'` + value.split(`'`).join(`'\\\\''`) + `'`;\n}\n\n/**\n * Default hang guard for sandbox shell commands. Generous by design — large\n * clones and dependency installs legitimately take minutes; the guard exists\n * so a wedged sandbox surfaces a failure instead of hanging the request that\n * triggered materialization forever.\n */\nconst DEFAULT_COMMAND_TIMEOUT_MS = 15 * 60_000;\n/** Branch checkout only fetches one ref — a much tighter budget applies. */\nexport const CHECKOUT_COMMAND_TIMEOUT_MS = 5 * 60_000;\n\ninterface ShOptions {\n /** Override the hang-guard budget for this command. */\n timeoutMs?: number;\n /** Human-readable phase name included in the timeout error. */\n phase?: string;\n}\n\n/**\n * A thrown transport-level failure that is worth retrying: remote sandbox\n * providers (e.g. the platform workspace proxy) surface transient 5xx errors\n * as exceptions carrying an HTTP `status` — typically while a freshly\n * provisioned VM is still coming up. Command failures are NOT exceptions\n * (they resolve with a non-zero exit code), so retrying here never re-runs a\n * command that the sandbox already executed and rejected.\n */\nfunction isTransientTransportError(error: unknown): boolean {\n const status = (error as { status?: unknown })?.status;\n return typeof status === 'number' && status >= 500;\n}\n\nconst SH_RETRIES = 2;\nconst SH_RETRY_DELAY_MS = 2000;\n\n/**\n * Run a shell script in the sandbox via `sh -c`, bounded by a hang guard.\n * Transient transport-level 5xx failures (proxy hiccups while the VM boots)\n * are retried with a short backoff; every script routed through here is safe\n * to re-run. Hang-guard timeouts are NOT retried — the budget applies to the\n * command as a whole.\n */\nasync function sh(\n sandbox: MaterializationSandbox,\n script: string,\n options: ShOptions = {},\n): Promise<SandboxCommandResult> {\n // One budget for the command as a whole: each attempt only gets the time\n // remaining, so transport retries can never multiply the hang guard.\n const deadlineMs = Date.now() + (options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);\n for (let attempt = 0; ; attempt++) {\n try {\n return await shOnce(sandbox, script, { ...options, timeoutMs: Math.max(deadlineMs - Date.now(), 1) });\n } catch (error) {\n if (attempt >= SH_RETRIES || !isTransientTransportError(error)) throw error;\n const delayMs = SH_RETRY_DELAY_MS * (attempt + 1);\n if (deadlineMs - Date.now() <= delayMs) throw error;\n await new Promise(resolve => setTimeout(resolve, delayMs));\n }\n }\n}\n\n/** Single `sh -c` execution attempt, bounded by the hang guard. */\nasync function shOnce(\n sandbox: MaterializationSandbox,\n script: string,\n options: ShOptions,\n): Promise<SandboxCommandResult> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const hangGuard = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n const phase = options.phase ? ` during ${options.phase}` : '';\n reject(new Error(`Sandbox command timed out after ${Math.round(timeoutMs / 1000)}s${phase}.`));\n }, timeoutMs);\n timer.unref?.();\n });\n try {\n // Forward the budget to the provider too so it can terminate the wedged\n // process; the race stays as the outer guard for providers that ignore it.\n return await Promise.race([sandbox.executeCommand('sh', ['-c', script], { timeout: timeoutMs }), hangGuard]);\n } finally {\n clearTimeout(timer);\n }\n}\n\n/** Error raised when the sandbox cannot materialize the repo (actionable). */\nexport class MaterializeError extends Error {\n constructor(\n message: string,\n readonly code:\n | 'git-missing'\n | 'egress-blocked'\n | 'clone-failed'\n | 'pull-failed'\n | 'push-failed'\n | 'commit-failed'\n | 'gh-missing'\n | 'pr-failed',\n ) {\n super(message);\n this.name = 'MaterializeError';\n }\n}\n\n/**\n * Build the token-auth clone/pull URL for a repo. The token lives only inside\n * this URL and is scrubbed from the remote after the operation.\n */\nfunction tokenUrl(repoFullName: string, token: string): string {\n return `https://x-access-token:${token}@github.com/${repoFullName}.git`;\n}\n\nfunction cleanUrl(repoFullName: string): string {\n return `https://github.com/${repoFullName}.git`;\n}\n\n/** Repo metadata needed to materialize, read from the org-owned project row. */\nexport interface RepoMaterializeInfo {\n repoFullName: string;\n defaultBranch: string;\n}\n\n/**\n * Materialize the repo inside the user's sandbox. Clones on first open, pulls on\n * re-open. Always scrubs the install token from the remote afterwards and sets\n * `materialized_at` on the per-user sandbox binding row.\n *\n */\nexport async function materializeRepo(options: {\n /** The per-(project,user) sandbox binding (provisioned via `ensureProjectSandbox`). */\n row: RepoMaterializationBinding;\n /** Repo metadata from the org-owned project row. */\n repoInfo: RepoMaterializeInfo;\n /** The live sandbox to run git inside. */\n sandbox: MaterializationSandbox;\n /** A freshly minted, short-lived installation access token. */\n token: string;\n storage: MaterializationStore;\n onProgress?: ProgressFn;\n}): Promise<void> {\n const { row: sandboxRow, repoInfo, sandbox, token, storage, onProgress } = options;\n const workdir = sandboxRow.sandboxWorkdir;\n const repo = repoInfo.repoFullName;\n\n // 0. Defense in depth: never build a git command from values that aren't\n // strictly shaped, even if a malformed row reached the DB. Inputs are also\n // validated at the route boundary before storage.\n if (!/^[\\w.-]+\\/[\\w.-]+$/.test(repo)) {\n throw new MaterializeError(`Refusing to materialize: invalid repo full name '${repo}'.`, 'clone-failed');\n }\n if (!/^[A-Za-z0-9_./-]+$/.test(repoInfo.defaultBranch)) {\n throw new MaterializeError(\n `Refusing to materialize: invalid default branch '${repoInfo.defaultBranch}'.`,\n 'clone-failed',\n );\n }\n\n // 1. Preflight: git must be installed in the sandbox template.\n const gitVersion = await sh(sandbox, 'git --version');\n if (gitVersion.exitCode !== 0) {\n throw new MaterializeError(\n 'git is not installed in the sandbox. The sandbox template must include git.',\n 'git-missing',\n );\n }\n\n const authUrl = tokenUrl(repo, token);\n\n // The DB's `materializedAt` can drift from disk in both directions: a fresh\n // binding row over an already-populated workdir (local dev DB resets,\n // repaired rows, earlier flows) must pull instead of failing `git clone` on\n // the non-empty directory, and a stale `materializedAt` over an empty\n // sandbox (an expired/recreated VM whose disk was wiped) must re-clone\n // instead of running `git -C <workdir>` against a directory that no longer\n // exists. Disk is the source of truth: detect the checkout instead of\n // trusting the row.\n const alreadyMaterialized = await hasExistingCheckout(sandbox, workdir, repo);\n\n let succeeded = false;\n try {\n if (!alreadyMaterialized) {\n // 2a. First open: shallow-clone the default branch into the workdir. A\n // shallow single-branch clone is dramatically faster for large repos; the\n // later re-open uses `git pull --ff-only`, which works on shallow clones.\n reportProgress(onProgress, {\n phase: 'cloning',\n message: `Cloning ${repo} (first open can take a minute)…`,\n });\n const clone = await sh(\n sandbox,\n `git clone --depth=1 --single-branch --branch ${shellQuote(repoInfo.defaultBranch)} ${shellQuote(authUrl)} ${shellQuote(workdir)}`,\n { phase: 'repository clone' },\n );\n if (clone.exitCode !== 0) {\n throw classifyGitFailure(clone, 'clone-failed');\n }\n } else {\n // 2b. Re-open: refresh remote to the token URL and fast-forward pull.\n reportProgress(onProgress, { phase: 'pulling', message: `Updating ${repo} to the latest changes…` });\n const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);\n if (setUrl.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr}`, 'pull-failed');\n }\n const pull = await sh(sandbox, `git -C ${shellQuote(workdir)} pull --ff-only`, { phase: 'repository pull' });\n if (pull.exitCode !== 0) {\n if (!isBenignNonFastForward(pull)) {\n throw classifyGitFailure(pull, 'pull-failed');\n }\n // The workdir was left on a session's working branch that can't be\n // fast-forwarded (diverged from upstream, no upstream, or detached\n // HEAD). That branch holds the session's local work — never rebase or\n // reset it here. The checkout is still perfectly usable; leave it\n // as-is and let the session reconcile with the remote itself.\n reportProgress(onProgress, {\n phase: 'pulling',\n message: 'Workspace has local changes that diverge from the remote — keeping them as-is.',\n });\n }\n }\n succeeded = true;\n } finally {\n // 3. Always scrub the token from the remote so it isn't left in the VM's\n // git config, even when the clone/pull above failed partway through. This\n // is best-effort on the failure path (the workdir may not even exist, and\n // a scrub error must never mask the primary failure); on the success path\n // the scrub must succeed or we surface it.\n await scrubRemote(sandbox, workdir, repo, succeeded);\n }\n\n // 4. Mark materialized.\n reportProgress(onProgress, { phase: 'finalizing', message: 'Finalizing workspace…' });\n await storage.markMaterialized({ id: sandboxRow.id });\n}\n\n/**\n * Reset a pooled workdir that a new session just claimed: the previous\n * session's branch and dirty state must not leak into the new session, so\n * force-checkout the default branch and drop all local modifications. When\n * the claimed VM was reaped and re-provisioned there is no checkout yet and\n * this is a no-op (the clone path handles it). A wedged checkout falls back\n * to wiping the workdir so `materializeRepo` re-clones inside the same VM\n * instead of permanently failing the session.\n */\nexport async function recycleClaimedWorkdir(\n sandbox: MaterializationSandbox,\n workdir: string,\n defaultBranch: string,\n): Promise<void> {\n if (!/^[A-Za-z0-9_./-]+$/.test(defaultBranch)) {\n throw new MaterializeError(`Refusing to recycle: invalid default branch '${defaultBranch}'.`, 'clone-failed');\n }\n const w = shellQuote(workdir);\n const inspect = await sh(sandbox, `git -C ${w} rev-parse --is-inside-work-tree`);\n if (inspect.exitCode !== 0) return;\n const recycle = await sh(\n sandbox,\n // `-x` also drops gitignored files (.env, build caches) so no session\n // state survives into the next claim.\n `git -C ${w} checkout -f ${shellQuote(defaultBranch)} && git -C ${w} reset --hard && git -C ${w} clean -fdx`,\n { phase: 'claimed workdir recycle' },\n );\n if (recycle.exitCode === 0) return;\n const wipe = await sh(sandbox, `rm -rf ${w}`);\n if (wipe.exitCode !== 0) {\n throw new MaterializeError(`Failed to recycle claimed sandbox workdir: ${recycle.stderr}`, 'clone-failed');\n }\n}\n\n/** Check out a session's branch inside its isolated repository clone. */\nexport async function checkoutSessionBranch(\n sandbox: MaterializationSandbox,\n workdir: string,\n {\n branch,\n baseBranch,\n token,\n repoFullName,\n }: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<void> {\n if (!isValidGitRef(branch) || !isValidGitRef(baseBranch)) {\n throw new MaterializeError('Refusing to create a session from an invalid branch name.', 'clone-failed');\n }\n\n const current = await sh(sandbox, `git -C ${shellQuote(workdir)} branch --show-current`);\n if (current.exitCode === 0 && current.stdout.trim() === branch) return;\n\n const local = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} show-ref --verify --quiet refs/heads/${shellQuote(branch)}`,\n );\n if (local.exitCode === 0) {\n const checkout = await sh(sandbox, `git -C ${shellQuote(workdir)} checkout ${shellQuote(branch)}`);\n if (checkout.exitCode !== 0) {\n // The session's agent may have switched branches itself (e.g. `gh pr\n // checkout`) and left uncommitted work in the tree. Git refuses to\n // switch back over those files — that work must win. The checkout is\n // intact and usable on its current branch; keep it as-is rather than\n // fail the workspace open, and never reset or stash to force the\n // switch through.\n if (isBlockedByLocalWork(checkout)) return;\n throw classifyGitFailure(checkout, 'clone-failed');\n }\n return;\n }\n\n const authUrl = tokenUrl(repoFullName, token);\n try {\n const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);\n if (setUrl.exitCode !== 0) throw classifyGitFailure(setUrl, 'pull-failed');\n const fetch = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} fetch origin ${shellQuote(baseBranch)} && git -C ${shellQuote(workdir)} checkout -b ${shellQuote(branch)} FETCH_HEAD`,\n { timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS, phase: 'branch checkout' },\n );\n if (fetch.exitCode !== 0) {\n // Same rule as above: uncommitted work in the tree blocks the switch\n // to the new branch. Leave the checkout on its current branch.\n if (isBlockedByLocalWork(fetch)) return;\n throw classifyGitFailure(fetch, 'clone-failed');\n }\n } finally {\n await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`);\n }\n}\n\n/**\n * True when a failed `git checkout` just means uncommitted or untracked files\n * in the working tree would be clobbered by the branch switch. Those files are\n * a session's work in progress — the switch must yield to them, never the\n * other way around.\n */\nfunction isBlockedByLocalWork(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /Your local changes to the following files would be overwritten by checkout|untracked working tree files would be overwritten by checkout/i.test(\n output,\n );\n}\n\n/**\n * True when the workdir already holds a git checkout whose `origin` points at\n * this exact repo. Matches both the clean and token-auth URL forms; any other\n * remote (or no git dir at all) falls back to the clone path.\n */\nasync function hasExistingCheckout(\n sandbox: MaterializationSandbox,\n workdir: string,\n repoFullName: string,\n): Promise<boolean> {\n const result = await sh(sandbox, `git -C ${shellQuote(workdir)} remote get-url origin`);\n if (result.exitCode !== 0) return false;\n const url = result.stdout.trim().toLowerCase();\n const suffix = `github.com/${repoFullName.toLowerCase()}`;\n return url.endsWith(`${suffix}.git`) || url.endsWith(suffix);\n}\n\n/**\n * Reset the git remote back to the tokenless URL. On a successful clone/pull the\n * workdir always has a `.git`, so a non-zero exit code here means the token may\n * still be persisted — surface it. On the failure path the workdir may not exist\n * (e.g. a failed clone), so a non-zero exit is tolerated — and never masks the\n * primary failure being thrown through the `finally`.\n */\nasync function scrubRemote(\n sandbox: MaterializationSandbox,\n workdir: string,\n repoFullName: string,\n expectGitDir: boolean,\n): Promise<void> {\n const result = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`,\n );\n if (result.exitCode !== 0 && expectGitDir) {\n throw new MaterializeError(\n `Failed to scrub installation token from git remote: ${result.stderr.trim() || result.stdout.trim()}`,\n 'pull-failed',\n );\n }\n}\n\n/**\n * True when a failed `git pull --ff-only` on re-open just means the current\n * branch can't be fast-forwarded — not that anything is broken. The shared\n * workdir is routinely left on a session's working branch, which may have\n * local commits diverging from upstream, no upstream at all (session branches\n * are created from `FETCH_HEAD`), or a detached HEAD. A checkout can also\n * hold uncommitted or untracked files (a build or script run left residue,\n * or an older session worked directly in the shared checkout), which makes\n * git refuse the merge outright. In all of these cases the checkout is\n * intact and may hold real work; materialization must keep it as-is rather\n * than fail the workspace open — and must never discard the local state to\n * force the pull through.\n */\nfunction isBenignNonFastForward(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /Not possible to fast-forward|Diverging branches can't be fast-forwarded|no tracking information for the current branch|You are not currently on a branch|Your local changes to the following files would be overwritten by merge|untracked working tree files would be overwritten by merge/i.test(\n output,\n );\n}\n\n/**\n * Turn a failed git command into an actionable error, detecting the common\n * \"cannot reach github.com\" egress failure.\n */\nfunction classifyGitFailure(\n result: SandboxCommandResult,\n fallback: 'clone-failed' | 'pull-failed' | 'push-failed',\n): MaterializeError {\n const stderr = result.stderr || '';\n if (/could not resolve host|failed to connect|network is unreachable|Connection timed out/i.test(stderr)) {\n return new MaterializeError(\n 'The sandbox could not reach github.com. The sandbox network must allow outbound egress to github.com.',\n 'egress-blocked',\n );\n }\n const verb = fallback === 'clone-failed' ? 'clone' : fallback === 'pull-failed' ? 'pull' : 'push';\n return new MaterializeError(`git ${verb} failed: ${stderr}`, fallback);\n}\n\n// ---------------------------------------------------------------------------\n// Phase 1 — git identity + token-scoped push primitive\n//\n// These helpers let the sandbox author and push commits safely. The install\n// token is short-lived, minted per-operation server-side, injected only into\n// the temporary remote URL inside the sandbox, and always scrubbed in a\n// `finally` so it never persists in `.git/config`.\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a git ref (branch) name. Server-side defense-in-depth: only allow a\n * conservative character set so a branch can never be built into a shell\n * command in a way that escapes quoting. Mirrors the route-layer check.\n */\nexport function isValidGitRef(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length > 0 &&\n value.length <= 255 &&\n // Reject leading-dash refs (e.g. `--mirror`) so the value can never be\n // parsed as a git option when interpolated into a command.\n !value.startsWith('-') &&\n /^[A-Za-z0-9_./-]+$/.test(value)\n );\n}\n\n/** Identity used to author commits inside the sandbox. */\nexport interface GitIdentity {\n name?: string | null;\n email?: string | null;\n /** GitHub login, used to derive a stable noreply identity when name/email are absent. */\n login?: string | null;\n}\n\n/**\n * Resolve a concrete `{ name, email }` for git authorship from a possibly-sparse\n * identity. Falls back to a GitHub-style noreply identity so commits are never\n * authored with an empty or host-derived identity.\n */\nexport function resolveGitIdentity(identity: GitIdentity): { name: string; email: string } {\n const login = (identity.login || '').trim();\n const name = (identity.name || '').trim() || login || 'Mastra Code';\n const email =\n (identity.email || '').trim() ||\n (login ? `${login}@users.noreply.github.com` : 'mastra-code@users.noreply.github.com');\n return { name, email };\n}\n\n/**\n * Configure `user.name` / `user.email` for the given repo working tree inside\n * the sandbox so commits are authored correctly. Values are shell-quoted.\n */\nexport async function configureGitIdentity(\n sandbox: MaterializationSandbox,\n workdir: string,\n identity: GitIdentity,\n): Promise<void> {\n const { name, email } = resolveGitIdentity(identity);\n const setName = await sh(sandbox, `git -C ${shellQuote(workdir)} config user.name ${shellQuote(name)}`);\n if (setName.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git user.name: ${setName.stderr.trim()}`, 'commit-failed');\n }\n const setEmail = await sh(sandbox, `git -C ${shellQuote(workdir)} config user.email ${shellQuote(email)}`);\n if (setEmail.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git user.email: ${setEmail.stderr.trim()}`, 'commit-failed');\n }\n}\n\n/**\n * Temporarily rewrite `origin` to a tokenized URL, run `fn` (e.g. a push), and\n * **always** scrub the remote back to the tokenless URL in a `finally`. The\n * token therefore only ever lives in the remote URL for the duration of the\n * operation and is never left in the VM's git config.\n *\n * On the success path the scrub must succeed (a leaked token is a hard error);\n * if it fails we surface it. On the failure path the scrub is best-effort but\n * still attempted, and the original operation error is rethrown.\n */\nexport async function withInstallToken<T>(\n sandbox: MaterializationSandbox,\n workdir: string,\n repoFullName: string,\n token: string,\n fn: () => Promise<T>,\n): Promise<T> {\n if (!/^[\\w.-]+\\/[\\w.-]+$/.test(repoFullName)) {\n throw new MaterializeError(`Refusing to push: invalid repo full name '${repoFullName}'.`, 'push-failed');\n }\n\n const setUrl = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(tokenUrl(repoFullName, token))}`,\n );\n if (setUrl.exitCode !== 0) {\n // Best-effort scrub even though set-url failed, then surface the failure.\n await scrubRemote(sandbox, workdir, repoFullName, false);\n throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr.trim()}`, 'push-failed');\n }\n\n try {\n return await fn();\n } finally {\n // Always restore the tokenless remote. The workdir has a `.git` (we just\n // rewrote its remote) so a scrub failure means the token may still persist\n // — surface it.\n await scrubRemote(sandbox, workdir, repoFullName, true);\n }\n}\n\n/**\n * Push a branch back to GitHub from inside the sandbox using a short-lived\n * installation token. The branch is ref-validated, the token is injected only\n * into the remote URL via `withInstallToken`, and egress failures are\n * classified into actionable errors.\n */\nexport async function pushBranch(\n sandbox: MaterializationSandbox,\n workdir: string,\n branch: string,\n token: string,\n repoFullName: string,\n): Promise<void> {\n if (!isValidGitRef(branch)) {\n throw new MaterializeError(`Refusing to push: invalid branch name '${branch}'.`, 'push-failed');\n }\n\n await withInstallToken(sandbox, workdir, repoFullName, token, async () => {\n const push = await sh(sandbox, `git -C ${shellQuote(workdir)} push -u origin ${shellQuote(branch)}`);\n if (push.exitCode !== 0) {\n throw classifyGitFailure(push, 'push-failed');\n }\n });\n}\n\nexport interface CommitResult {\n /** True when a commit was created; false when there was nothing to commit. */\n committed: boolean;\n}\n\n/**\n * Stage every change in the working tree and create a commit inside the\n * sandbox. The git identity is configured first so authorship is correct. When\n * there is nothing to commit this is a no-op (`committed: false`) rather than an\n * error, so callers can safely commit-then-push without first diffing.\n *\n * @param sandbox the live sandbox containing the checkout\n * @param workdir the worktree (or repo) path to commit in\n * @param message the commit message (quoted; arbitrary text is safe)\n * @param identity authorship identity for the commit\n */\nexport async function commitAll(\n sandbox: MaterializationSandbox,\n workdir: string,\n message: string,\n identity: GitIdentity,\n): Promise<CommitResult> {\n await configureGitIdentity(sandbox, workdir, identity);\n\n const add = await sh(sandbox, `git -C ${shellQuote(workdir)} add -A`);\n if (add.exitCode !== 0) {\n throw new MaterializeError(`git add failed: ${add.stderr.trim() || add.stdout.trim()}`, 'commit-failed');\n }\n\n // Nothing staged → nothing to commit. `git diff --cached --quiet` exits 1 when\n // there are staged changes, 0 when the index is clean.\n const staged = await sh(sandbox, `git -C ${shellQuote(workdir)} diff --cached --quiet`);\n if (staged.exitCode === 0) {\n return { committed: false };\n }\n\n const commit = await sh(sandbox, `git -C ${shellQuote(workdir)} commit -m ${shellQuote(message)}`);\n if (commit.exitCode !== 0) {\n throw new MaterializeError(`git commit failed: ${commit.stderr.trim() || commit.stdout.trim()}`, 'commit-failed');\n }\n\n return { committed: true };\n}\n\n// ---------------------------------------------------------------------------\n// Phase 2 — worktree / branch lifecycle\n//\n// Each unit of work gets its own branch + working tree inside the same sandbox\n// as the base checkout. The worktree path is always computed server-side from a\n// sanitized branch name; client input never reaches a filesystem path.\n// ---------------------------------------------------------------------------\n\n/** Error raised when a worktree cannot be created/reused inside the sandbox. */\nexport class WorktreeError extends Error {\n constructor(\n message: string,\n readonly code: 'invalid-branch' | 'worktree-failed' | 'setup-failed',\n ) {\n super(message);\n this.name = 'WorktreeError';\n }\n}\n\n/**\n * Reduce a (already ref-validated) branch name to a filesystem-safe directory\n * segment for the worktree path: slashes/dots/unsafe chars collapsed to `-`.\n * This only affects the *directory name*, never the git branch itself.\n *\n * Sanitization is lossy (e.g. `feat/a` and `feat-a` both reduce to `feat-a`),\n * so an 8-char hash of the original branch is appended whenever the sanitized\n * form differs from the input. That keeps clean names (`main`) readable while\n * guaranteeing distinct branches never share a worktree directory.\n */\nexport function safeBranchDir(branch: string): string {\n const sanitized =\n branch\n .replace(/[^A-Za-z0-9._-]+/g, '-')\n .replace(/\\/+/g, '-')\n .replace(/^[-.]+|[-.]+$/g, '')\n .slice(0, 100) || 'work';\n if (sanitized === branch) return sanitized;\n const hash = createHash('sha256').update(branch).digest('hex').slice(0, 8);\n return `${sanitized}-${hash}`;\n}\n\n/**\n * Compute the absolute worktree path for a branch, server-side only. Worktrees\n * live alongside the repo checkout under a sibling `worktrees/` directory so the\n * repo's `.git` is shared. Never derived from client-supplied paths.\n */\nexport function computeWorktreePath(repoWorkdir: string, branch: string): string {\n const parent = repoWorkdir.replace(/\\/+$/, '').split('/').slice(0, -1).join('/') || '';\n return `${parent}/worktrees/${safeBranchDir(branch)}`;\n}\n\nexport interface EnsureWorktreeResult {\n worktreePath: string;\n branch: string;\n baseBranch: string;\n /** True when an existing worktree was reused rather than freshly created. */\n reused: boolean;\n}\n\n/**\n * Create (or reuse) a git worktree + branch inside the sandbox for a unit of\n * work. Idempotent: if a worktree already exists at the computed path it is\n * reused. The branch is created from the freshly fetched `origin/<baseBranch>`\n * — never the sandbox's possibly stale local ref — so new worktrees always\n * start from the latest remote state.\n *\n * @param sandbox live sandbox containing the base checkout\n * @param repoWorkdir the base repo checkout path inside the sandbox\n * @param branch the feature branch (ref-validated server-side)\n * @param baseBranch the branch to fork from (ref-validated; defaults to the repo's default branch)\n * @param token short-lived installation token used only for the base-branch fetch\n * @param repoFullName `owner/repo` used to build the tokenized remote URL\n */\nexport async function ensureWorktree(\n sandbox: MaterializationSandbox,\n repoWorkdir: string,\n {\n branch,\n baseBranch,\n token,\n repoFullName,\n }: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<EnsureWorktreeResult> {\n if (!isValidGitRef(branch)) {\n throw new WorktreeError(`Invalid branch name '${branch}'.`, 'invalid-branch');\n }\n if (!isValidGitRef(baseBranch)) {\n throw new WorktreeError(`Invalid base branch name '${baseBranch}'.`, 'invalid-branch');\n }\n\n const worktreePath = computeWorktreePath(repoWorkdir, branch);\n\n // Idempotent reuse: a worktree already checked out at this path has a `.git`\n // file (worktrees use a gitfile, not a directory). Reuse it as-is.\n const exists = await sh(sandbox, `test -e ${shellQuote(`${worktreePath}/.git`)}`);\n if (exists.exitCode === 0) {\n return { worktreePath, branch, baseBranch, reused: true };\n }\n\n // Fetch the latest base ref from origin before forking. The explicit refspec\n // updates `refs/remotes/origin/<base>` even when the checkout was created as\n // a single-branch clone. The fetch needs the install token (the resting\n // remote is tokenless), and a failure is a hard error — silently forking a\n // stale local ref is worse than failing the request.\n const baseRef = `origin/${baseBranch}`;\n await withInstallToken(sandbox, repoWorkdir, repoFullName, token, async () => {\n const fetch = await sh(\n sandbox,\n `git -C ${shellQuote(repoWorkdir)} fetch origin ${shellQuote(`+refs/heads/${baseBranch}:refs/remotes/${baseRef}`)}`,\n );\n if (fetch.exitCode !== 0) {\n throw classifyGitFailure(fetch, 'pull-failed');\n }\n });\n\n // Create the worktree. If the branch already exists, check it out into the\n // worktree; otherwise create it from the fetched base. `git worktree add -B`\n // creates-or-resets the branch to the base, which keeps this idempotent for a\n // fresh worktree while still working when the branch already exists remotely.\n // `--no-track` keeps the feature branch from tracking origin/<base>; pushes\n // set their own upstream via `push -u`.\n const add = await sh(\n sandbox,\n `git -C ${shellQuote(repoWorkdir)} worktree add --no-track -B ${shellQuote(branch)} ${shellQuote(worktreePath)} ${shellQuote(baseRef)}`,\n );\n if (add.exitCode !== 0) {\n throw new WorktreeError(`git worktree add failed: ${add.stderr.trim() || add.stdout.trim()}`, 'worktree-failed');\n }\n\n return { worktreePath, branch, baseBranch, reused: false };\n}\n\n/**\n * Run the project's setup command (e.g. `pnpm i && pnpm build`) inside a\n * freshly created worktree. Called before the worktree is handed to any agent\n * run so the checkout is ready to build/test. A non-zero exit is a hard error —\n * starting agent work in a half-set-up tree is worse than failing the request.\n *\n * Security model: the command is intentionally arbitrary shell — that is the\n * feature (install deps, build, seed fixtures). It is only configurable by\n * authenticated org members (the settings route is gated by\n * `resolveOrgTenant` + org-scoped project lookup, with length and\n * control-character validation), and it executes exclusively inside the\n * project's isolated sandbox — the same environment where org members already\n * run arbitrary shell via the agent's command tool. It never runs on the web\n * server host, so it grants no privilege beyond what sandbox access already\n * provides.\n *\n * @param sandbox live sandbox containing the worktree\n * @param worktreePath the server-computed worktree path the command runs in\n * @param command the org-configured setup shell command\n */\nexport async function runWorktreeSetup(\n sandbox: MaterializationSandbox,\n worktreePath: string,\n command: string,\n): Promise<void> {\n const result = await sh(sandbox, `cd ${shellQuote(worktreePath)} && { ${command}\\n}`, { phase: 'worktree setup' });\n if (result.exitCode !== 0) {\n const detail = (result.stderr.trim() || result.stdout.trim()).slice(-2000);\n throw new WorktreeError(`Setup command failed (exit ${result.exitCode}): ${detail}`, 'setup-failed');\n }\n}\n\n/**\n * Remove a worktree (and its local feature branch) from the sandbox. The\n * checkout is removed with `--force` — the caller owns confirming that any\n * uncommitted work in it can be discarded. Idempotent: a worktree whose\n * directory is already gone only has its metadata pruned.\n *\n * @param sandbox live sandbox containing the base checkout\n * @param repoWorkdir the base repo checkout path inside the sandbox\n * @param branch the worktree's feature branch (ref-validated)\n * @param worktreePath the persisted, server-computed worktree path\n */\nexport async function removeWorktree(\n sandbox: MaterializationSandbox,\n repoWorkdir: string,\n { branch, worktreePath }: { branch: string; worktreePath: string },\n): Promise<void> {\n if (!isValidGitRef(branch)) {\n throw new WorktreeError(`Invalid branch name '${branch}'.`, 'invalid-branch');\n }\n\n const remove = await sh(\n sandbox,\n `git -C ${shellQuote(repoWorkdir)} worktree remove --force ${shellQuote(worktreePath)}`,\n );\n if (remove.exitCode !== 0) {\n // Tolerate a checkout that's already gone (e.g. a fresh sandbox after\n // re-provisioning): prune stale metadata and only fail when the directory\n // still exists, meaning git genuinely refused to remove it.\n await sh(sandbox, `git -C ${shellQuote(repoWorkdir)} worktree prune`);\n const exists = await sh(sandbox, `test -e ${shellQuote(worktreePath)}`);\n if (exists.exitCode === 0) {\n throw new WorktreeError(\n `git worktree remove failed: ${remove.stderr.trim() || remove.stdout.trim()}`,\n 'worktree-failed',\n );\n }\n }\n\n // Best-effort local branch cleanup; the branch may not exist locally anymore\n // or may still be pushed remotely — neither should fail the removal.\n await sh(sandbox, `git -C ${shellQuote(repoWorkdir)} branch -D ${shellQuote(branch)}`);\n}\n\n// ---------------------------------------------------------------------------\n// Phase 3 — `gh` CLI pull-request creation primitive\n//\n// PRs are opened from inside the sandbox with the GitHub CLI. `gh` must be\n// present in the sandbox template (preflighted only on the PR path so clone /\n// open still work when it is absent). The token is passed to `gh` via a\n// per-invocation `GH_TOKEN` env that is scoped to the single `gh` process and\n// never written to git config, a shell rc, or the VM's environment.\n// ---------------------------------------------------------------------------\n\nexport interface CreatePullRequestArgs {\n /** Short-lived installation token, injected only into the `gh` process env. */\n token: string;\n /** Base branch the PR merges into. Ref-validated. */\n base: string;\n /** Head branch the PR is opened from. Ref-validated. */\n head: string;\n /** PR title. */\n title: string;\n /** PR body (optional). */\n body?: string;\n}\n\nexport interface CreatePullRequestResult {\n /** The PR URL parsed from `gh pr create` stdout. */\n url: string;\n}\n\n/**\n * Preflight that `gh` is installed in the sandbox. Only called on the PR path so\n * a missing `gh` never blocks clone/open. Surfaces an actionable error naming\n * the sandbox template requirement.\n */\nasync function assertGhAvailable(sandbox: MaterializationSandbox): Promise<void> {\n const version = await sh(sandbox, 'gh --version');\n if (version.exitCode !== 0) {\n throw new MaterializeError(\n 'The GitHub CLI (gh) is not installed in the sandbox. The sandbox template must include gh to open pull requests.',\n 'gh-missing',\n );\n }\n}\n\n/** Match the first GitHub PR URL in `gh pr create` output. */\nfunction parsePullRequestUrl(stdout: string): string | undefined {\n const match = stdout.match(/https:\\/\\/github\\.com\\/[^\\s]+\\/pull\\/\\d+/);\n return match?.[0];\n}\n\n/**\n * Open a pull request from inside the sandbox via `gh pr create`. The token is\n * passed only through a per-invocation `GH_TOKEN` env scoped to the single `gh`\n * process (never persisted), all arguments are shell-quoted, and the resulting\n * PR URL is parsed from stdout.\n *\n * @param sandbox live sandbox containing the checkout\n * @param workdir the worktree (or repo) path the PR head branch is checked out in\n */\nexport async function createPullRequest(\n sandbox: MaterializationSandbox,\n workdir: string,\n { token, base, head, title, body }: CreatePullRequestArgs,\n): Promise<CreatePullRequestResult> {\n if (!isValidGitRef(base)) {\n throw new MaterializeError(`Refusing to open PR: invalid base branch '${base}'.`, 'pr-failed');\n }\n if (!isValidGitRef(head)) {\n throw new MaterializeError(`Refusing to open PR: invalid head branch '${head}'.`, 'pr-failed');\n }\n\n await assertGhAvailable(sandbox);\n\n // GH_TOKEN is prefixed inline so it is exported only to the single `gh`\n // process and never to the wider shell session, git config, or VM env. `gh`\n // is run from inside the checkout so it targets the correct repo/head branch.\n const ghCommand = [\n `GH_TOKEN=${shellQuote(token)} gh pr create`,\n `--base ${shellQuote(base)}`,\n `--head ${shellQuote(head)}`,\n `--title ${shellQuote(title)}`,\n `--body ${shellQuote(body ?? '')}`,\n ].join(' ');\n const script = `cd ${shellQuote(workdir)} && ${ghCommand}`;\n\n const result = await sh(sandbox, script);\n if (result.exitCode !== 0) {\n const classified = classifyGitFailure(result, 'push-failed');\n if (classified.code === 'egress-blocked') {\n throw classified;\n }\n throw new MaterializeError(`gh pr create failed: ${result.stderr.trim() || result.stdout.trim()}`, 'pr-failed');\n }\n\n const url = parsePullRequestUrl(result.stdout);\n if (!url) {\n throw new MaterializeError(\n `gh pr create succeeded but no PR URL was found in its output: ${result.stdout.trim()}`,\n 'pr-failed',\n );\n }\n\n return { url };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/routes/projects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EAEV,oBAAoB,EAGrB,MAAM,2CAA2C,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAuInC,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D,wDAAwD;IACxD,QAAQ,EAAE,sBAAsB,CAAC;IACjC,2EAA2E;IAC3E,aAAa,EAAE,oBAAoB,CAAC;IACpC,oEAAoE;IACpE,4BAA4B,CAAC,EAAE,MAAM,EAAE,CAAC;CACzC;AAED,qBAAa,aAAc,SAAQ,KAAK,CAAC,iBAAiB,CAAC;;gBAG7C,IAAI,EAAE,iBAAiB;IA8DnC,MAAM,IAAI,QAAQ,EAAE;CAoOrB"}
1
+ {"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/routes/projects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EAEV,oBAAoB,EAGrB,MAAM,2CAA2C,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AA2InC,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D,wDAAwD;IACxD,QAAQ,EAAE,sBAAsB,CAAC;IACjC,2EAA2E;IAC3E,aAAa,EAAE,oBAAoB,CAAC;IACpC,oEAAoE;IACpE,4BAA4B,CAAC,EAAE,MAAM,EAAE,CAAC;CACzC;AAED,qBAAa,aAAc,SAAQ,KAAK,CAAC,iBAAiB,CAAC;;gBAG7C,IAAI,EAAE,iBAAiB;IA8DnC,MAAM,IAAI,QAAQ,EAAE;CAoOrB"}
@@ -57,6 +57,10 @@ function parseUpdateInput(value) {
57
57
  if (defaultModelId === false) return null;
58
58
  patch.defaultModelId = defaultModelId ?? null;
59
59
  }
60
+ if (input.slackWorkItemsEnabled !== void 0) {
61
+ if (typeof input.slackWorkItemsEnabled !== "boolean") return null;
62
+ patch.slackWorkItemsEnabled = input.slackWorkItemsEnabled;
63
+ }
60
64
  return Object.keys(patch).length > 0 ? patch : null;
61
65
  }
62
66
  function parseConnectionInput(value) {