terramend 0.2.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/LICENSE +661 -0
- package/README.md +145 -0
- package/dist/agents/claude.d.ts +73 -0
- package/dist/agents/claudePretoolGate.d.ts +99 -0
- package/dist/agents/gateServer.d.ts +7 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/nativeFsDenies.d.ts +28 -0
- package/dist/agents/opencode.d.ts +231 -0
- package/dist/agents/opencodePlugin.d.ts +85 -0
- package/dist/agents/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +132 -0
- package/dist/agents/reviewer.d.ts +38 -0
- package/dist/agents/sessionLabeler.d.ts +97 -0
- package/dist/agents/shared.d.ts +189 -0
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/agents/subagentToolGates.d.ts +55 -0
- package/dist/cli.mjs +197426 -0
- package/dist/external.d.ts +227 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +196783 -0
- package/dist/internal/index.d.ts +18 -0
- package/dist/internal.js +1714 -0
- package/dist/lifecycle.d.ts +2 -0
- package/dist/main.d.ts +8 -0
- package/dist/mcp/arkConfig.d.ts +1 -0
- package/dist/mcp/checkSuite.d.ts +25 -0
- package/dist/mcp/checkout.d.ts +77 -0
- package/dist/mcp/comment.d.ts +119 -0
- package/dist/mcp/commitInfo.d.ts +9 -0
- package/dist/mcp/crosswalk.d.ts +105 -0
- package/dist/mcp/dependencies.d.ts +8 -0
- package/dist/mcp/geminiSanitizer.d.ts +28 -0
- package/dist/mcp/git.d.ts +46 -0
- package/dist/mcp/guardrails.d.ts +104 -0
- package/dist/mcp/issue.d.ts +18 -0
- package/dist/mcp/issueComments.d.ts +9 -0
- package/dist/mcp/issueEvents.d.ts +9 -0
- package/dist/mcp/issueInfo.d.ts +9 -0
- package/dist/mcp/labels.d.ts +12 -0
- package/dist/mcp/localContext.d.ts +19 -0
- package/dist/mcp/moduleExtraction.d.ts +71 -0
- package/dist/mcp/moduleTests.d.ts +104 -0
- package/dist/mcp/modules.d.ts +179 -0
- package/dist/mcp/output.d.ts +12 -0
- package/dist/mcp/pathSafety.d.ts +14 -0
- package/dist/mcp/policy.d.ts +48 -0
- package/dist/mcp/pr.d.ts +49 -0
- package/dist/mcp/prInfo.d.ts +9 -0
- package/dist/mcp/providerSchema.d.ts +50 -0
- package/dist/mcp/review.d.ts +199 -0
- package/dist/mcp/reviewComments.d.ts +178 -0
- package/dist/mcp/roots.d.ts +58 -0
- package/dist/mcp/scope.d.ts +15 -0
- package/dist/mcp/selectMode.d.ts +18 -0
- package/dist/mcp/server.d.ts +48 -0
- package/dist/mcp/shared.d.ts +47 -0
- package/dist/mcp/shell.d.ts +37 -0
- package/dist/mcp/staleFix.d.ts +51 -0
- package/dist/mcp/terraform/cost.d.ts +55 -0
- package/dist/mcp/terraform/currency.d.ts +94 -0
- package/dist/mcp/terraform/decisions.d.ts +178 -0
- package/dist/mcp/terraform/findings.d.ts +75 -0
- package/dist/mcp/terraform/plan.d.ts +157 -0
- package/dist/mcp/terraform/scanners.d.ts +131 -0
- package/dist/mcp/terraform/tools.d.ts +63 -0
- package/dist/mcp/terraform/types.d.ts +172 -0
- package/dist/mcp/terraform.d.ts +22 -0
- package/dist/mcp/terratest.d.ts +83 -0
- package/dist/mcp/upload.d.ts +6 -0
- package/dist/models.d.ts +171 -0
- package/dist/modes.d.ts +26 -0
- package/dist/prep/index.d.ts +7 -0
- package/dist/prep/installNodeDependencies.d.ts +2 -0
- package/dist/prep/installPythonDependencies.d.ts +2 -0
- package/dist/prep/types.d.ts +31 -0
- package/dist/reviewQuality.d.ts +64 -0
- package/dist/skills/terraform-best-practices/SKILL.md +369 -0
- package/dist/toolState.d.ts +135 -0
- package/dist/utils/activity.d.ts +40 -0
- package/dist/utils/agent.d.ts +20 -0
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/apiFetch.d.ts +19 -0
- package/dist/utils/apiKeys.d.ts +41 -0
- package/dist/utils/apiUrl.d.ts +20 -0
- package/dist/utils/assets.d.ts +8 -0
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/body.d.ts +34 -0
- package/dist/utils/buildTerramendFooter.d.ts +25 -0
- package/dist/utils/byokFallback.d.ts +85 -0
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/cli.d.ts +10 -0
- package/dist/utils/codexHome.d.ts +29 -0
- package/dist/utils/codexOAuth.d.ts +60 -0
- package/dist/utils/diffCoverage.d.ts +63 -0
- package/dist/utils/errorReport.d.ts +17 -0
- package/dist/utils/exitHandler.d.ts +8 -0
- package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
- package/dist/utils/gitAuth.d.ts +84 -0
- package/dist/utils/gitAuthServer.d.ts +24 -0
- package/dist/utils/github.d.ts +78 -0
- package/dist/utils/globals.d.ts +3 -0
- package/dist/utils/install.d.ts +60 -0
- package/dist/utils/instructions.d.ts +48 -0
- package/dist/utils/leapingComment.d.ts +11 -0
- package/dist/utils/learnings.d.ts +62 -0
- package/dist/utils/learningsTruncate.d.ts +25 -0
- package/dist/utils/lifecycle.d.ts +57 -0
- package/dist/utils/log.d.ts +111 -0
- package/dist/utils/normalizeEnv.d.ts +30 -0
- package/dist/utils/openCodeModels.d.ts +11 -0
- package/dist/utils/overrides.d.ts +40 -0
- package/dist/utils/packageManager.d.ts +49 -0
- package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
- package/dist/utils/payload.d.ts +105 -0
- package/dist/utils/prSummary.d.ts +61 -0
- package/dist/utils/progressComment.d.ts +146 -0
- package/dist/utils/providerErrors.d.ts +31 -0
- package/dist/utils/rangeDiff.d.ts +51 -0
- package/dist/utils/remediationCommand.d.ts +55 -0
- package/dist/utils/retry.d.ts +13 -0
- package/dist/utils/reviewCleanup.d.ts +14 -0
- package/dist/utils/run.d.ts +9 -0
- package/dist/utils/runContext.d.ts +60 -0
- package/dist/utils/runContextData.d.ts +23 -0
- package/dist/utils/runErrorRenderer.d.ts +64 -0
- package/dist/utils/runLifecycle.d.ts +86 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/secrets.d.ts +22 -0
- package/dist/utils/setup.d.ts +90 -0
- package/dist/utils/shell.d.ts +32 -0
- package/dist/utils/skills.d.ts +10 -0
- package/dist/utils/subprocess.d.ts +80 -0
- package/dist/utils/terraformMcp.d.ts +42 -0
- package/dist/utils/time.d.ts +15 -0
- package/dist/utils/timer.d.ts +23 -0
- package/dist/utils/todoTracking.d.ts +16 -0
- package/dist/utils/token.d.ts +39 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/versioning.d.ts +7 -0
- package/dist/utils/vertex.d.ts +16 -0
- package/dist/utils/workflow.d.ts +13 -0
- package/package.json +119 -0
- package/src/agents/claude.test.ts +1016 -0
- package/src/agents/claude.ts +1246 -0
- package/src/agents/claudePretoolGate.test.ts +28 -0
- package/src/agents/claudePretoolGate.ts +173 -0
- package/src/agents/gateServer.test.ts +204 -0
- package/src/agents/gateServer.ts +124 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/nativeFsDenies.ts +82 -0
- package/src/agents/opencode.test.ts +1440 -0
- package/src/agents/opencode.ts +1312 -0
- package/src/agents/opencodePlugin.ts +222 -0
- package/src/agents/opencodeShared.test.ts +34 -0
- package/src/agents/opencodeShared.ts +121 -0
- package/src/agents/postRun.test.ts +549 -0
- package/src/agents/postRun.ts +535 -0
- package/src/agents/reviewer.ts +104 -0
- package/src/agents/sessionLabeler.test.ts +247 -0
- package/src/agents/sessionLabeler.ts +178 -0
- package/src/agents/shared.test.ts +76 -0
- package/src/agents/shared.ts +292 -0
- package/src/agents/subagentModels.test.ts +113 -0
- package/src/agents/subagentModels.ts +40 -0
- package/src/agents/subagentRegistration.test.ts +41 -0
- package/src/agents/subagentToolGates.ts +114 -0
- package/src/cli.test.ts +129 -0
- package/src/cli.ts +105 -0
- package/src/commands/gha.test.ts +192 -0
- package/src/commands/gha.ts +188 -0
- package/src/commands/mcp.ts +122 -0
- package/src/config.ts +1 -0
- package/src/entry.ts +7 -0
- package/src/entryPost.stdlibOnly.test.ts +109 -0
- package/src/entryPost.ts +99 -0
- package/src/external.test.ts +16 -0
- package/src/external.ts +302 -0
- package/src/index.ts +11 -0
- package/src/internal/index.ts +71 -0
- package/src/lifecycle.ts +2 -0
- package/src/main.test.ts +873 -0
- package/src/main.ts +712 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
- package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
- package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
- package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
- package/src/mcp/arkConfig.ts +7 -0
- package/src/mcp/checkSuite.test.ts +245 -0
- package/src/mcp/checkSuite.ts +255 -0
- package/src/mcp/checkout.test.ts +752 -0
- package/src/mcp/checkout.ts +886 -0
- package/src/mcp/comment.test.ts +772 -0
- package/src/mcp/comment.ts +582 -0
- package/src/mcp/commitInfo.test.ts +127 -0
- package/src/mcp/commitInfo.ts +61 -0
- package/src/mcp/crosswalk.test.ts +106 -0
- package/src/mcp/crosswalk.ts +339 -0
- package/src/mcp/dependencies.test.ts +309 -0
- package/src/mcp/dependencies.ts +189 -0
- package/src/mcp/geminiSanitizer.test.ts +287 -0
- package/src/mcp/geminiSanitizer.ts +207 -0
- package/src/mcp/git.test.ts +1083 -0
- package/src/mcp/git.ts +890 -0
- package/src/mcp/guardrails.test.ts +705 -0
- package/src/mcp/guardrails.ts +465 -0
- package/src/mcp/issue.test.ts +113 -0
- package/src/mcp/issue.ts +73 -0
- package/src/mcp/issueComments.test.ts +69 -0
- package/src/mcp/issueComments.ts +48 -0
- package/src/mcp/issueEvents.test.ts +134 -0
- package/src/mcp/issueEvents.ts +100 -0
- package/src/mcp/issueInfo.test.ts +104 -0
- package/src/mcp/issueInfo.ts +72 -0
- package/src/mcp/labels.test.ts +52 -0
- package/src/mcp/labels.ts +34 -0
- package/src/mcp/localContext.ts +28 -0
- package/src/mcp/localServer.test.ts +75 -0
- package/src/mcp/localServer.ts +131 -0
- package/src/mcp/moduleExtraction.test.ts +261 -0
- package/src/mcp/moduleExtraction.ts +313 -0
- package/src/mcp/moduleTests.test.ts +269 -0
- package/src/mcp/moduleTests.ts +421 -0
- package/src/mcp/modules.test.ts +640 -0
- package/src/mcp/modules.ts +696 -0
- package/src/mcp/output.test.ts +96 -0
- package/src/mcp/output.ts +70 -0
- package/src/mcp/pathSafety.test.ts +44 -0
- package/src/mcp/pathSafety.ts +28 -0
- package/src/mcp/policy.test.ts +282 -0
- package/src/mcp/policy.ts +199 -0
- package/src/mcp/pr.test.ts +387 -0
- package/src/mcp/pr.ts +194 -0
- package/src/mcp/prInfo.test.ts +96 -0
- package/src/mcp/prInfo.ts +91 -0
- package/src/mcp/providerSchema.test.ts +85 -0
- package/src/mcp/providerSchema.ts +175 -0
- package/src/mcp/review.test.ts +936 -0
- package/src/mcp/review.ts +923 -0
- package/src/mcp/reviewComments.test.ts +549 -0
- package/src/mcp/reviewComments.ts +896 -0
- package/src/mcp/roots.test.ts +175 -0
- package/src/mcp/roots.ts +217 -0
- package/src/mcp/scope.test.ts +59 -0
- package/src/mcp/scope.ts +65 -0
- package/src/mcp/security.test.ts +720 -0
- package/src/mcp/selectMode.test.ts +210 -0
- package/src/mcp/selectMode.ts +181 -0
- package/src/mcp/server.test.ts +292 -0
- package/src/mcp/server.ts +403 -0
- package/src/mcp/shared.ts +100 -0
- package/src/mcp/shell.test.ts +520 -0
- package/src/mcp/shell.ts +505 -0
- package/src/mcp/staleFix.test.ts +237 -0
- package/src/mcp/staleFix.ts +277 -0
- package/src/mcp/terraform/cost.ts +163 -0
- package/src/mcp/terraform/currency.test.ts +338 -0
- package/src/mcp/terraform/currency.ts +336 -0
- package/src/mcp/terraform/decisions.ts +527 -0
- package/src/mcp/terraform/findings.ts +333 -0
- package/src/mcp/terraform/plan.ts +348 -0
- package/src/mcp/terraform/scanners.ts +809 -0
- package/src/mcp/terraform/tools.test.ts +1071 -0
- package/src/mcp/terraform/tools.ts +908 -0
- package/src/mcp/terraform/types.ts +305 -0
- package/src/mcp/terraform.test.ts +1957 -0
- package/src/mcp/terraform.ts +23 -0
- package/src/mcp/terratest.test.ts +105 -0
- package/src/mcp/terratest.ts +196 -0
- package/src/mcp/toolFiltering.test.ts +85 -0
- package/src/mcp/upload.test.ts +180 -0
- package/src/mcp/upload.ts +112 -0
- package/src/models.test.ts +300 -0
- package/src/models.ts +708 -0
- package/src/modes.test.ts +107 -0
- package/src/modes.ts +880 -0
- package/src/prep/index.ts +43 -0
- package/src/prep/installNodeDependencies.test.ts +298 -0
- package/src/prep/installNodeDependencies.ts +196 -0
- package/src/prep/installPythonDependencies.test.ts +268 -0
- package/src/prep/installPythonDependencies.ts +199 -0
- package/src/prep/types.ts +38 -0
- package/src/reviewQuality.test.ts +63 -0
- package/src/reviewQuality.ts +134 -0
- package/src/runCli.test.ts +214 -0
- package/src/runCli.ts +282 -0
- package/src/skills/terraform-best-practices/SKILL.md +369 -0
- package/src/toolState.test.ts +45 -0
- package/src/toolState.ts +252 -0
- package/src/utils/activity.test.ts +188 -0
- package/src/utils/activity.ts +210 -0
- package/src/utils/agent.test.ts +251 -0
- package/src/utils/agent.ts +139 -0
- package/src/utils/agentHangReport.test.ts +203 -0
- package/src/utils/agentHangReport.ts +170 -0
- package/src/utils/apiFetch.test.ts +115 -0
- package/src/utils/apiFetch.ts +62 -0
- package/src/utils/apiKeys.test.ts +344 -0
- package/src/utils/apiKeys.ts +206 -0
- package/src/utils/apiUrl.test.ts +30 -0
- package/src/utils/apiUrl.ts +59 -0
- package/src/utils/assets.test.ts +153 -0
- package/src/utils/assets.ts +107 -0
- package/src/utils/billingErrors.test.ts +121 -0
- package/src/utils/billingErrors.ts +189 -0
- package/src/utils/body.test.ts +217 -0
- package/src/utils/body.ts +168 -0
- package/src/utils/buildTerramendFooter.test.ts +38 -0
- package/src/utils/buildTerramendFooter.ts +82 -0
- package/src/utils/byokFallback.test.ts +205 -0
- package/src/utils/byokFallback.ts +128 -0
- package/src/utils/claudeSubscription.test.ts +179 -0
- package/src/utils/claudeSubscription.ts +93 -0
- package/src/utils/cli.ts +31 -0
- package/src/utils/codexHome.test.ts +190 -0
- package/src/utils/codexHome.ts +191 -0
- package/src/utils/codexOAuth.ts +147 -0
- package/src/utils/codexRefreshDetect.test.ts +85 -0
- package/src/utils/codexRefreshDetect.ts +35 -0
- package/src/utils/diffCoverage.test.ts +468 -0
- package/src/utils/diffCoverage.ts +404 -0
- package/src/utils/errorReport.test.ts +135 -0
- package/src/utils/errorReport.ts +83 -0
- package/src/utils/exitHandler.ts +35 -0
- package/src/utils/fixDoubleEscapedString.ts +9 -0
- package/src/utils/ghaCore.ts +13 -0
- package/src/utils/gitAuth.test.ts +322 -0
- package/src/utils/gitAuth.ts +263 -0
- package/src/utils/gitAuthServer.test.ts +260 -0
- package/src/utils/gitAuthServer.ts +182 -0
- package/src/utils/github.test.ts +615 -0
- package/src/utils/github.ts +538 -0
- package/src/utils/globals.ts +9 -0
- package/src/utils/humanEditCapture.test.ts +100 -0
- package/src/utils/humanEditCapture.ts +193 -0
- package/src/utils/install.test.ts +768 -0
- package/src/utils/install.ts +492 -0
- package/src/utils/instructions.test.ts +240 -0
- package/src/utils/instructions.ts +543 -0
- package/src/utils/leapingComment.test.ts +51 -0
- package/src/utils/leapingComment.ts +18 -0
- package/src/utils/learnings.test.ts +87 -0
- package/src/utils/learnings.ts +138 -0
- package/src/utils/learningsTocRender.test.ts +116 -0
- package/src/utils/learningsTruncate.test.ts +39 -0
- package/src/utils/learningsTruncate.ts +42 -0
- package/src/utils/lifecycle.test.ts +195 -0
- package/src/utils/lifecycle.ts +198 -0
- package/src/utils/log.test.ts +402 -0
- package/src/utils/log.ts +432 -0
- package/src/utils/normalizeEnv.test.ts +91 -0
- package/src/utils/normalizeEnv.ts +106 -0
- package/src/utils/openCodeModels.ts +82 -0
- package/src/utils/overrides.test.ts +89 -0
- package/src/utils/overrides.ts +98 -0
- package/src/utils/packageManager.test.ts +321 -0
- package/src/utils/packageManager.ts +257 -0
- package/src/utils/patchWorkflowRunFields.test.ts +92 -0
- package/src/utils/patchWorkflowRunFields.ts +150 -0
- package/src/utils/payload.test.ts +497 -0
- package/src/utils/payload.ts +371 -0
- package/src/utils/postApiFetch.ts +51 -0
- package/src/utils/prSummary.test.ts +224 -0
- package/src/utils/prSummary.ts +147 -0
- package/src/utils/progressComment.ts +261 -0
- package/src/utils/providerErrors.test.ts +315 -0
- package/src/utils/providerErrors.ts +172 -0
- package/src/utils/rangeDiff.test.ts +236 -0
- package/src/utils/rangeDiff.ts +182 -0
- package/src/utils/remediationCommand.test.ts +163 -0
- package/src/utils/remediationCommand.ts +119 -0
- package/src/utils/retry.test.ts +153 -0
- package/src/utils/retry.ts +58 -0
- package/src/utils/reviewCleanup.ts +106 -0
- package/src/utils/run.ts +99 -0
- package/src/utils/runContext.ts +145 -0
- package/src/utils/runContextData.ts +58 -0
- package/src/utils/runErrorRenderer.test.ts +95 -0
- package/src/utils/runErrorRenderer.ts +259 -0
- package/src/utils/runFixture.ts +76 -0
- package/src/utils/runLifecycle.ts +237 -0
- package/src/utils/runStartupLog.ts +60 -0
- package/src/utils/secrets.test.ts +103 -0
- package/src/utils/secrets.ts +177 -0
- package/src/utils/setup.test.ts +509 -0
- package/src/utils/setup.ts +352 -0
- package/src/utils/shell.ts +103 -0
- package/src/utils/skills.test.ts +46 -0
- package/src/utils/skills.ts +67 -0
- package/src/utils/subprocess.test.ts +170 -0
- package/src/utils/subprocess.ts +438 -0
- package/src/utils/terraformMcp.test.ts +63 -0
- package/src/utils/terraformMcp.ts +83 -0
- package/src/utils/time.test.ts +105 -0
- package/src/utils/time.ts +59 -0
- package/src/utils/timer.test.ts +91 -0
- package/src/utils/timer.ts +72 -0
- package/src/utils/todoTracking.test.ts +223 -0
- package/src/utils/todoTracking.ts +167 -0
- package/src/utils/token.test.ts +239 -0
- package/src/utils/token.ts +186 -0
- package/src/utils/version.ts +10 -0
- package/src/utils/versioning.test.ts +34 -0
- package/src/utils/versioning.ts +44 -0
- package/src/utils/vertex.ts +85 -0
- package/src/utils/workflow.ts +25 -0
package/src/mcp/shell.ts
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
// changes to shell security (filterEnv, spawnShell) should be reflected in wiki/security.md and docs/security.mdx
|
|
2
|
+
import { type ChildProcess, type StdioOptions, spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { closeSync, openSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { userInfo } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
8
|
+
import { type } from "arktype";
|
|
9
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
10
|
+
import { execute, tool } from "#app/mcp/shared";
|
|
11
|
+
import { log } from "#app/utils/log";
|
|
12
|
+
import { resolveEnv } from "#app/utils/secrets";
|
|
13
|
+
|
|
14
|
+
export const ShellParams = type({
|
|
15
|
+
command: "string",
|
|
16
|
+
description: "string",
|
|
17
|
+
"timeout?": type.number.describe(
|
|
18
|
+
"Timeout in MILLISECONDS (not seconds). Default 30000 (30s), max 120000 (2m). e.g. timeout: 180000 for 3 minutes; timeout: 180 means 180ms and will kill the process almost immediately.",
|
|
19
|
+
),
|
|
20
|
+
"working_directory?": "string",
|
|
21
|
+
"background?": "boolean",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
type SpawnParams = {
|
|
25
|
+
command: string;
|
|
26
|
+
env: Record<string, string | undefined>;
|
|
27
|
+
cwd: string;
|
|
28
|
+
stdio: StdioOptions;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type SandboxMethod = "unshare" | "sudo-unshare" | "none";
|
|
32
|
+
|
|
33
|
+
/** cached result of sandbox capability check */
|
|
34
|
+
let detectedSandboxMethod: SandboxMethod | undefined;
|
|
35
|
+
|
|
36
|
+
/** get the current sandbox method (for testing/diagnostics) */
|
|
37
|
+
export function getSandboxMethod(): SandboxMethod {
|
|
38
|
+
return detectSandboxMethod();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** detect which sandbox method is available on this system */
|
|
42
|
+
function detectSandboxMethod(): SandboxMethod {
|
|
43
|
+
if (detectedSandboxMethod !== undefined) {
|
|
44
|
+
return detectedSandboxMethod;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// only attempt in CI environments - sandbox has overhead and is primarily for untrusted code
|
|
48
|
+
if (process.env.CI !== "true") {
|
|
49
|
+
detectedSandboxMethod = "none";
|
|
50
|
+
log.debug("sandbox disabled (CI !== true)");
|
|
51
|
+
return "none";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// try unprivileged unshare first (works on some systems)
|
|
55
|
+
try {
|
|
56
|
+
const result = spawnSync("unshare", ["--pid", "--fork", "--mount-proc", "true"], {
|
|
57
|
+
timeout: 5000,
|
|
58
|
+
stdio: "ignore",
|
|
59
|
+
});
|
|
60
|
+
if (result.status === 0) {
|
|
61
|
+
detectedSandboxMethod = "unshare";
|
|
62
|
+
log.debug("PID namespace isolation enabled (unprivileged unshare)");
|
|
63
|
+
return "unshare";
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// continue to try sudo
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// sudo unshare (works on GHA runners)
|
|
70
|
+
try {
|
|
71
|
+
const result = spawnSync("sudo", ["unshare", "--pid", "--fork", "--mount-proc", "true"], {
|
|
72
|
+
timeout: 5000,
|
|
73
|
+
stdio: "ignore",
|
|
74
|
+
});
|
|
75
|
+
if (result.status === 0) {
|
|
76
|
+
detectedSandboxMethod = "sudo-unshare";
|
|
77
|
+
log.debug("PID namespace isolation enabled (sudo unshare)");
|
|
78
|
+
return "sudo-unshare";
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// no sandbox available
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
detectedSandboxMethod = "none";
|
|
85
|
+
log.info("PID namespace isolation not available");
|
|
86
|
+
return "none";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// strip inherited proc mount that sits underneath --mount-proc's overlay.
|
|
90
|
+
// --mount-proc mounts fresh proc on top, but `umount /proc` peels it off and exposes the
|
|
91
|
+
// host's proc with all host PIDs — allowing /proc/<pid>/environ exfiltration.
|
|
92
|
+
// double-umount removes both layers, then a clean mount gives only sandbox PIDs.
|
|
93
|
+
// on unprivileged systems where umount fails, --mount-proc still provides isolation
|
|
94
|
+
// (the agent also can't umount in that case).
|
|
95
|
+
const PROC_CLEANUP =
|
|
96
|
+
"umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;";
|
|
97
|
+
|
|
98
|
+
// block container-runtime sockets that would otherwise grant a PID-namespace
|
|
99
|
+
// escape: `docker run --pid=host --privileged busybox cat /proc/<pid>/environ`
|
|
100
|
+
// reads the parent action process's env (which contains user secrets) even
|
|
101
|
+
// though the sandbox itself is unsharing PIDs. GHA `ubuntu-latest` puts the
|
|
102
|
+
// `runner` user in the `docker` group by default, so the socket is reachable
|
|
103
|
+
// without sudo. bind-mounting /dev/null on top inside the sandbox's mount
|
|
104
|
+
// namespace makes the socket unreachable from sandboxed shells without
|
|
105
|
+
// touching the host runner (so it doesn't break user workflow steps that
|
|
106
|
+
// run before/after terramend and legitimately need docker). same trick for
|
|
107
|
+
// podman/containerd/cri-o sockets — all silent-fail if the path is missing.
|
|
108
|
+
const SOCKET_CLEANUP = [
|
|
109
|
+
"/var/run/docker.sock",
|
|
110
|
+
"/run/docker.sock",
|
|
111
|
+
"/var/run/podman/podman.sock",
|
|
112
|
+
"/run/podman/podman.sock",
|
|
113
|
+
"/run/containerd/containerd.sock",
|
|
114
|
+
"/var/run/crio/crio.sock",
|
|
115
|
+
]
|
|
116
|
+
.map((path) => `mount --bind /dev/null ${path} 2>/dev/null;`)
|
|
117
|
+
.join(" ");
|
|
118
|
+
|
|
119
|
+
// extend the mount-namespace isolation that PROC_CLEANUP and SOCKET_CLEANUP
|
|
120
|
+
// already establish. these mounts hide terramend-managed on-disk secrets,
|
|
121
|
+
// block env-injection into subsequent workflow steps, and make git's
|
|
122
|
+
// code-execution config read-only inside the bash subprocess.
|
|
123
|
+
//
|
|
124
|
+
// 1. tmpfs over /var/lib/terramend/ — codex auth.json and any future
|
|
125
|
+
// terramend-managed on-disk secret live here (see action/utils/codexHome.ts
|
|
126
|
+
// TERRAMEND_DATA_DIR). opencode's internal auth module runs in the agent
|
|
127
|
+
// process outside this namespace and reads the real file via bypass of
|
|
128
|
+
// external_directory; bash sees an empty tmpfs. mkdir -p the path
|
|
129
|
+
// first so the tmpfs always engages — without that, runs without
|
|
130
|
+
// CODEX_AUTH_JSON wouldn't have bootstrapped the dir, the mountpoint
|
|
131
|
+
// wouldn't exist, and `mount -t tmpfs` would silent-fail. precreate
|
|
132
|
+
// keeps the overlay active for any future on-disk secret that lands
|
|
133
|
+
// under /var/lib/terramend regardless of which install path created it.
|
|
134
|
+
// 2. tmpfs over $RUNNER_TEMP/_runner_file_commands/ — anything bash writes
|
|
135
|
+
// to $GITHUB_ENV / $GITHUB_PATH / $GITHUB_OUTPUT / $GITHUB_STATE lands in
|
|
136
|
+
// a per-namespace tmpfs that the GHA runner never sees. our own action
|
|
137
|
+
// process writes core.setOutput / core.saveState outside the namespace,
|
|
138
|
+
// so legitimate outputs are unaffected. requires RUNNER_TEMP to be set.
|
|
139
|
+
// 3. self-bind + remount-ro on the ENTIRE <repoRoot>/.git directory.
|
|
140
|
+
// A blanket ro-bind is free: nothing legitimately writes .git from
|
|
141
|
+
// bash (commits go through our $git(), whose binary runs OUTSIDE this
|
|
142
|
+
// namespace, so it's unaffected; bash `git` is already blocked). It
|
|
143
|
+
// robustly covers every code-exec surface an enumerated list would miss
|
|
144
|
+
// — .git/config, .git/config.worktree, .git/modules/*/config (all carry
|
|
145
|
+
// core.hooksPath / filter / alias / credential.helper exec vectors),
|
|
146
|
+
// plus .git/hooks/* and .git/info/attributes. Prevents agent-planted git
|
|
147
|
+
// filters / hooks from firing in downstream workflow steps (the threat
|
|
148
|
+
// survives ASKPASS because hooks fire after auth and our $git() uses -c
|
|
149
|
+
// core.hooksPath to override its own hooks, but downstream `git`
|
|
150
|
+
// invocations in later steps DON'T get that protection — see
|
|
151
|
+
// wiki/security.md "Filesystem Sandbox"). CONSEQUENCE: .git/info/exclude
|
|
152
|
+
// (a legit per-repo ignore file) is now read-only too — accepted, the
|
|
153
|
+
// narrow earlier bind list left it writable. Does not cover `~/.gitconfig`
|
|
154
|
+
// or `/etc/gitconfig` — see "Scope and Limitations" in wiki/security.md.
|
|
155
|
+
//
|
|
156
|
+
// these mounts run as root inside the namespace (before `exec su -p` drops
|
|
157
|
+
// to runner). after the drop, runner has no CAP_SYS_ADMIN in the host, so
|
|
158
|
+
// can't undo from outside. intra-namespace sudo undo is theoretically
|
|
159
|
+
// possible — same risk profile as SOCKET_CLEANUP, accepted per wiki/security.md
|
|
160
|
+
// "why sudo inside sandbox doesn't break security".
|
|
161
|
+
//
|
|
162
|
+
// in the unprivileged-unshare path (Docker --privileged test environments),
|
|
163
|
+
// the user retains CAP_SYS_ADMIN inside the user namespace and could
|
|
164
|
+
// `umount` these. production uses sudo-unshare where the drop seals them.
|
|
165
|
+
//
|
|
166
|
+
// repoDir is interpolated by the action process from resolveRepoRoot() —
|
|
167
|
+
// NOT $PWD — because spawnShell's cwd is agent-controllable via
|
|
168
|
+
// `working_directory` AND the action's process.cwd() may have been chdir'd
|
|
169
|
+
// to payload.cwd (monorepo subdir support in main.ts). using either would
|
|
170
|
+
// let the agent bypass the .git/* binds. resolveRepoRoot pins the actual
|
|
171
|
+
// repo root once at startup via $GITHUB_WORKSPACE or `git rev-parse`.
|
|
172
|
+
function buildFsMounts(repoDir: string): string {
|
|
173
|
+
// shell-escape via single-quote wrap; bash interprets \' as the escape
|
|
174
|
+
// for a single quote inside a single-quoted string by closing-and-reopening.
|
|
175
|
+
// repoDir paths in practice are GHA workspace paths (no quotes), but the
|
|
176
|
+
// escape keeps us correct against arbitrary user-configured workspaces.
|
|
177
|
+
const escaped = repoDir.replace(/'/g, "'\\''");
|
|
178
|
+
return [
|
|
179
|
+
`mkdir -p /var/lib/terramend 2>/dev/null;`,
|
|
180
|
+
`mount -t tmpfs tmpfs /var/lib/terramend 2>/dev/null;`,
|
|
181
|
+
`[ -n "$RUNNER_TEMP" ] && [ -d "$RUNNER_TEMP/_runner_file_commands" ] && mount -t tmpfs tmpfs "$RUNNER_TEMP/_runner_file_commands" 2>/dev/null;`,
|
|
182
|
+
`[ -e '${escaped}/.git' ] && mount --bind '${escaped}/.git' '${escaped}/.git' 2>/dev/null && mount -o remount,bind,ro '${escaped}/.git' 2>/dev/null;`,
|
|
183
|
+
].join(" ");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** locate the repo root once at action startup. process.cwd() is unreliable
|
|
187
|
+
* because main.ts may `process.chdir(payload.cwd)` for monorepo subdirs;
|
|
188
|
+
* the agent's `working_directory` shell param also moves spawn cwd. we need
|
|
189
|
+
* the actual git working tree root for the .git/* binds. memoized for the
|
|
190
|
+
* lifetime of the action process. */
|
|
191
|
+
let _repoRoot: string | undefined;
|
|
192
|
+
function resolveRepoRoot(): string {
|
|
193
|
+
if (_repoRoot) return _repoRoot;
|
|
194
|
+
const fromEnv = process.env.GITHUB_WORKSPACE;
|
|
195
|
+
if (fromEnv) {
|
|
196
|
+
_repoRoot = fromEnv;
|
|
197
|
+
return _repoRoot;
|
|
198
|
+
}
|
|
199
|
+
// fallback: `git rev-parse --show-toplevel` from process.cwd(). only used
|
|
200
|
+
// outside GHA (local dev, custom runners). swallow errors and fall back
|
|
201
|
+
// to process.cwd() so we never throw from the shell-tool init path.
|
|
202
|
+
try {
|
|
203
|
+
_repoRoot = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
204
|
+
cwd: process.cwd(),
|
|
205
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
206
|
+
encoding: "utf-8",
|
|
207
|
+
}).stdout?.trim();
|
|
208
|
+
} catch {
|
|
209
|
+
// intentionally empty — fall through to process.cwd()
|
|
210
|
+
}
|
|
211
|
+
if (!_repoRoot) _repoRoot = process.cwd();
|
|
212
|
+
return _repoRoot;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function spawnShell(params: SpawnParams): ChildProcess {
|
|
216
|
+
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
|
|
217
|
+
const sandboxMethod = detectSandboxMethod();
|
|
218
|
+
const ci = process.env.CI === "true";
|
|
219
|
+
|
|
220
|
+
if (ci && sandboxMethod === "none") {
|
|
221
|
+
throw new Error(
|
|
222
|
+
"pid namespace isolation is required in CI but unavailable (both unshare and sudo unshare failed)",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// resolve the actual git repo root (NOT params.cwd which is
|
|
227
|
+
// agent-controllable via `working_directory`, NOT process.cwd() which may
|
|
228
|
+
// have been chdir'd to payload.cwd in main.ts). resolveRepoRoot prefers
|
|
229
|
+
// $GITHUB_WORKSPACE in CI and falls back to `git rev-parse`.
|
|
230
|
+
const repoRoot = resolveRepoRoot();
|
|
231
|
+
const fsMounts = buildFsMounts(repoRoot);
|
|
232
|
+
|
|
233
|
+
if (sandboxMethod === "unshare") {
|
|
234
|
+
return spawn(
|
|
235
|
+
"unshare",
|
|
236
|
+
[
|
|
237
|
+
"--pid",
|
|
238
|
+
"--fork",
|
|
239
|
+
"--mount-proc",
|
|
240
|
+
"bash",
|
|
241
|
+
"-c",
|
|
242
|
+
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} ${params.command}`,
|
|
243
|
+
],
|
|
244
|
+
spawnOpts,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (sandboxMethod === "sudo-unshare") {
|
|
249
|
+
const envArgs: string[] = [];
|
|
250
|
+
for (const [k, v] of Object.entries(params.env)) {
|
|
251
|
+
if (v !== undefined) {
|
|
252
|
+
envArgs.push(`${k}=${v}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// drop back to original user after PROC_CLEANUP / FS_MOUNTS so files aren't
|
|
256
|
+
// owned by root. sudo is only needed for unshare + the mount setup; the
|
|
257
|
+
// actual command should run as the normal user to avoid ownership
|
|
258
|
+
// mismatches with files created by the Node.js parent process.
|
|
259
|
+
const username = userInfo().username;
|
|
260
|
+
// su -p resets PATH on many Linux systems (ALWAYS_SET_PATH in /etc/login.defs).
|
|
261
|
+
// restore it from the SANDBOX_PATH env var that survives the su transition.
|
|
262
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: we need to restore the PATH variable
|
|
263
|
+
const pathRestore = 'export PATH="${SANDBOX_PATH:-$PATH}"; ';
|
|
264
|
+
const escaped = (pathRestore + params.command).replace(/'/g, "'\\''");
|
|
265
|
+
envArgs.push(`SANDBOX_PATH=${params.env.PATH ?? ""}`);
|
|
266
|
+
return spawn(
|
|
267
|
+
"sudo",
|
|
268
|
+
[
|
|
269
|
+
"env",
|
|
270
|
+
...envArgs,
|
|
271
|
+
"unshare",
|
|
272
|
+
"--pid",
|
|
273
|
+
"--fork",
|
|
274
|
+
"--mount-proc",
|
|
275
|
+
"bash",
|
|
276
|
+
"-c",
|
|
277
|
+
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec su -p -s /bin/bash ${username} -c '${escaped}'`,
|
|
278
|
+
],
|
|
279
|
+
{ ...spawnOpts, env: {} },
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return spawn("bash", ["-c", params.command], spawnOpts);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** kill process and its entire process group */
|
|
287
|
+
async function killProcessGroup(proc: ChildProcess): Promise<void> {
|
|
288
|
+
if (!proc.pid) return;
|
|
289
|
+
try {
|
|
290
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
291
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
292
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
293
|
+
} catch {
|
|
294
|
+
try {
|
|
295
|
+
proc.kill("SIGKILL");
|
|
296
|
+
} catch {
|
|
297
|
+
/* already dead */
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getTempDir(): string {
|
|
303
|
+
const tempDir = process.env.TERRAMEND_TEMP_DIR;
|
|
304
|
+
if (!tempDir) {
|
|
305
|
+
throw new Error("TERRAMEND_TEMP_DIR not set");
|
|
306
|
+
}
|
|
307
|
+
return tempDir;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** chars of shell output kept inline in the agent reply. anything past this
|
|
311
|
+
* blows the agent's context budget on commands that dump big logs (test
|
|
312
|
+
* runners, build tools, grep on large trees), so the overflow is spilled
|
|
313
|
+
* to a tempfile the agent can re-read selectively (cat/tail/grep). */
|
|
314
|
+
export const MAX_OUTPUT_CHARS = 5000;
|
|
315
|
+
|
|
316
|
+
/** if `output` exceeds `MAX_OUTPUT_CHARS`, persist the full body to a
|
|
317
|
+
* tempfile and return the last `MAX_OUTPUT_CHARS` prefixed with a sentinel
|
|
318
|
+
* pointing at the saved path. otherwise return as-is. */
|
|
319
|
+
function capOutput(output: string): string {
|
|
320
|
+
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
321
|
+
const fullPath = join(getTempDir(), `shell-${randomUUID().slice(0, 8)}.log`);
|
|
322
|
+
writeFileSync(fullPath, output);
|
|
323
|
+
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
324
|
+
return `... [${elided} chars truncated; full output saved to ${fullPath}] ...\n${output.slice(-MAX_OUTPUT_CHARS)}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// detect `git` as a command invocation in any position a shell would start a new
|
|
328
|
+
// command: at the start, after a separator (`;`, `&`, `|`, newline, carriage
|
|
329
|
+
// return), or inside a subshell / command substitution / brace group (`(`,
|
|
330
|
+
// backtick, `$(`, `{`). Tolerates one or more launcher prefixes
|
|
331
|
+
// (`sudo`/`env`/`command`/`exec`/`nohup`). Matches `git` only when followed by
|
|
332
|
+
// whitespace or end-of-string, so it never fires on `.gitignore`, `digit`,
|
|
333
|
+
// `legit`, `git-lfs`, etc.
|
|
334
|
+
//
|
|
335
|
+
// NOTE: this is a UX redirect to the dedicated git tools, NOT a security
|
|
336
|
+
// boundary — in restricted mode the shell runs in a stripped, token-free sandbox
|
|
337
|
+
// with `.git` mounted read-only, so a `git` the agent slips past this still has
|
|
338
|
+
// no credentials and can't tamper with repo config. The detection is kept broad
|
|
339
|
+
// so the redirect isn't trivially defeated, but it is NOT relied on to contain a
|
|
340
|
+
// hostile `git` — that is the sandbox's job.
|
|
341
|
+
const GIT_INVOCATION =
|
|
342
|
+
/(?:^|\$\(|[\n\r;&|`({])\s*(?:(?:sudo|env|command|exec|nohup)\s+)*git(?:\s|$)/;
|
|
343
|
+
|
|
344
|
+
function isGitCommand(command: string): boolean {
|
|
345
|
+
return GIT_INVOCATION.test(command.trim());
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function ShellTool(ctx: ToolContext) {
|
|
349
|
+
return tool({
|
|
350
|
+
name: "shell",
|
|
351
|
+
timeoutMs: 120_000,
|
|
352
|
+
description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
|
|
353
|
+
|
|
354
|
+
Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
|
|
355
|
+
|
|
356
|
+
Use this tool to:
|
|
357
|
+
- Run shell commands (ls, cat, grep, find, etc.)
|
|
358
|
+
- Execute build tools (npm, pnpm, cargo, make, etc.)
|
|
359
|
+
- Run tests and linters
|
|
360
|
+
|
|
361
|
+
Output is capped at ${MAX_OUTPUT_CHARS} chars: if exceeded, only the tail is returned and the full body is saved to a tempfile (path included in the response). Re-read the tempfile with cat/tail/grep when you need more.
|
|
362
|
+
|
|
363
|
+
Do NOT use this tool for git commands — use the dedicated git tools instead.`,
|
|
364
|
+
parameters: ShellParams,
|
|
365
|
+
execute: execute(async (params) => {
|
|
366
|
+
if (isGitCommand(params.command)) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
"git commands are not allowed in the shell tool. use the dedicated git tools instead:\n" +
|
|
369
|
+
"- git: local operations (status, log, diff, add, commit, checkout, merge, rebase, etc.)\n" +
|
|
370
|
+
"- push_branch: push to remote (handles authentication)\n" +
|
|
371
|
+
"- git_fetch: fetch from remote (handles authentication)\n" +
|
|
372
|
+
"- checkout_pr: check out PR branches",
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const timeout = Math.min(params.timeout ?? 30000, 120000);
|
|
377
|
+
const cwd = params.working_directory ?? process.cwd();
|
|
378
|
+
const env = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
|
|
379
|
+
|
|
380
|
+
if (params.background) {
|
|
381
|
+
const tempDir = getTempDir();
|
|
382
|
+
const handle = `bg-${randomUUID().slice(0, 8)}`;
|
|
383
|
+
const outputPath = join(tempDir, `${handle}.log`);
|
|
384
|
+
const pidPath = join(tempDir, `${handle}.pid`);
|
|
385
|
+
const logFd = openSync(outputPath, "a");
|
|
386
|
+
let proc: ChildProcess;
|
|
387
|
+
try {
|
|
388
|
+
proc = spawnShell({
|
|
389
|
+
command: params.command,
|
|
390
|
+
env,
|
|
391
|
+
cwd,
|
|
392
|
+
stdio: ["ignore", logFd, logFd],
|
|
393
|
+
});
|
|
394
|
+
} finally {
|
|
395
|
+
closeSync(logFd);
|
|
396
|
+
}
|
|
397
|
+
if (!proc.pid) {
|
|
398
|
+
throw new Error("failed to start background process");
|
|
399
|
+
}
|
|
400
|
+
proc.unref();
|
|
401
|
+
writeFileSync(pidPath, `${proc.pid}\n`);
|
|
402
|
+
ctx.toolState.backgroundProcesses.set(handle, { pid: proc.pid, outputPath, pidPath });
|
|
403
|
+
return {
|
|
404
|
+
handle,
|
|
405
|
+
outputPath,
|
|
406
|
+
pidPath,
|
|
407
|
+
message: `started background process ${handle} (pid ${proc.pid})`,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const proc = spawnShell({
|
|
412
|
+
command: params.command,
|
|
413
|
+
env,
|
|
414
|
+
cwd,
|
|
415
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
let stdout = "",
|
|
419
|
+
stderr = "",
|
|
420
|
+
timedOut = false,
|
|
421
|
+
exited = false;
|
|
422
|
+
proc.stdout?.on("data", (chunk: Buffer) => {
|
|
423
|
+
stdout += chunk.toString();
|
|
424
|
+
});
|
|
425
|
+
proc.stderr?.on("data", (chunk: Buffer) => {
|
|
426
|
+
stderr += chunk.toString();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const timeoutId = setTimeout(async () => {
|
|
430
|
+
if (!exited) {
|
|
431
|
+
timedOut = true;
|
|
432
|
+
await killProcessGroup(proc);
|
|
433
|
+
}
|
|
434
|
+
}, timeout);
|
|
435
|
+
|
|
436
|
+
const exitCode = await new Promise<number | null>((resolve) => {
|
|
437
|
+
const done = (code: number | null) => {
|
|
438
|
+
exited = true;
|
|
439
|
+
clearTimeout(timeoutId);
|
|
440
|
+
resolve(code);
|
|
441
|
+
};
|
|
442
|
+
proc.on("exit", done);
|
|
443
|
+
proc.on("error", () => done(null));
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
let output = stderr ? (stdout ? `${stdout}\n${stderr}` : stderr) : stdout;
|
|
447
|
+
if (timedOut)
|
|
448
|
+
output = output
|
|
449
|
+
? `${output}\n[timed out after ${timeout}ms]`
|
|
450
|
+
: `[timed out after ${timeout}ms]`;
|
|
451
|
+
|
|
452
|
+
const finalExitCode = exitCode ?? (timedOut ? 124 : -1);
|
|
453
|
+
const trimmed = output.trim();
|
|
454
|
+
if (finalExitCode !== 0) {
|
|
455
|
+
log.info(`shell command failed with exit code ${finalExitCode}: ${params.command}`);
|
|
456
|
+
if (trimmed) log.info(`output: ${trimmed}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
output: capOutput(trimmed),
|
|
461
|
+
exit_code: finalExitCode,
|
|
462
|
+
timed_out: timedOut,
|
|
463
|
+
};
|
|
464
|
+
}),
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export const KillBackgroundParams = type({
|
|
469
|
+
handle: type.string.describe("The handle of the background process to kill (e.g., bg-a1b2c3d4)"),
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
export function KillBackgroundTool(ctx: ToolContext) {
|
|
473
|
+
return tool({
|
|
474
|
+
name: "kill_background",
|
|
475
|
+
description: `Kill a background process by its handle. Use this to stop dev servers or other long-running processes started with shell({ background: true }).`,
|
|
476
|
+
parameters: KillBackgroundParams,
|
|
477
|
+
execute: execute(async (params) => {
|
|
478
|
+
const proc = ctx.toolState.backgroundProcesses.get(params.handle);
|
|
479
|
+
if (!proc) {
|
|
480
|
+
return {
|
|
481
|
+
success: false,
|
|
482
|
+
message: `no background process with handle ${params.handle}`,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
488
|
+
} catch {
|
|
489
|
+
// already dead
|
|
490
|
+
}
|
|
491
|
+
await sleep(200);
|
|
492
|
+
try {
|
|
493
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
494
|
+
} catch {
|
|
495
|
+
// already dead
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
ctx.toolState.backgroundProcesses.delete(params.handle);
|
|
499
|
+
return {
|
|
500
|
+
success: true,
|
|
501
|
+
message: `killed background process ${params.handle} (pid ${proc.pid})`,
|
|
502
|
+
};
|
|
503
|
+
}),
|
|
504
|
+
});
|
|
505
|
+
}
|