@snappedly-tools/shipyard 0.4.2 → 0.4.4-staging.35704989542
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -66
- package/dist/{chunk-VDHZO5PP.js → chunk-EGYPJMLV.js} +62 -4
- package/dist/chunk-EGYPJMLV.js.map +1 -0
- package/dist/{chunk-2GT3OPO5.js → chunk-FDYOTN55.js} +196 -6
- package/dist/chunk-FDYOTN55.js.map +1 -0
- package/dist/chunk-KL3ASRFR.js +925 -0
- package/dist/chunk-KL3ASRFR.js.map +1 -0
- package/dist/index.js +3 -59
- package/dist/index.js.map +1 -1
- package/dist/main.js +1739 -141
- package/dist/main.js.map +1 -1
- package/dist/sandboxes/docker.js +2 -2
- package/package.json +3 -2
- package/dist/chunk-2GT3OPO5.js.map +0 -1
- package/dist/chunk-OCJKONZ7.js +0 -475
- package/dist/chunk-OCJKONZ7.js.map +0 -1
- package/dist/chunk-VDHZO5PP.js.map +0 -1
package/README.md
CHANGED
|
@@ -34,10 +34,28 @@ cp .shipyard/.env.example .shipyard/.env
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
`init` asks which agent, sandbox provider, and starter template to use. Its
|
|
37
|
-
issue-based templates use GitHub Issues.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
issue-based templates use GitHub Issues. Init always attempts to create or
|
|
38
|
+
update the lowercase `shipyard` label, and built-in issue workflows select only
|
|
39
|
+
open issues carrying that label. When you select Codex, init also asks whether
|
|
40
|
+
to sign in with ChatGPT or use an API key. Start with the `blank` template. Put
|
|
41
|
+
any requested credentials in `.shipyard/.env`, then write one concrete task in
|
|
42
|
+
`.shipyard/prompt.md`.
|
|
43
|
+
|
|
44
|
+
Use a subscription first when the selected agent supports it. If subscription
|
|
45
|
+
authentication does not work in your environment, use the API-key fallback:
|
|
46
|
+
|
|
47
|
+
- Codex subscription authentication: choose **Sign in with ChatGPT** during
|
|
48
|
+
interactive init and complete the browser login. The generated configuration
|
|
49
|
+
mounts `~/.codex/auth.json` read-only. For non-interactive init, run
|
|
50
|
+
`codex login` on the host, then pass `--codex-auth chatgpt`.
|
|
51
|
+
- Codex API authentication: choose **OpenAI API key** during interactive init
|
|
52
|
+
and put `OPENAI_API_KEY` in `.shipyard/.env`. For non-interactive init, pass
|
|
53
|
+
`--codex-auth api-key`.
|
|
54
|
+
- Claude Code subscription authentication: run `claude setup-token` on the
|
|
55
|
+
host and put the result in `CLAUDE_CODE_OAUTH_TOKEN` in `.shipyard/.env`.
|
|
56
|
+
- Claude Code API authentication: uncomment `ANTHROPIC_API_KEY` in
|
|
57
|
+
`.shipyard/.env` and put your API key there.
|
|
58
|
+
- GitHub Issues authentication: set `GH_TOKEN` and `GH_REPO` in `.shipyard/.env`.
|
|
41
59
|
|
|
42
60
|
Run it:
|
|
43
61
|
|
|
@@ -53,44 +71,101 @@ image when its definition has not changed:
|
|
|
53
71
|
npx shipyard run --skip-build
|
|
54
72
|
```
|
|
55
73
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
## JavaScript API
|
|
74
|
+
Shipyard supports Codex and Claude Code agents, with Docker, Vercel Sandbox,
|
|
75
|
+
and no-sandbox providers. `shipyard init` scaffolds Docker; configure Vercel
|
|
76
|
+
Sandbox or no-sandbox mode through the JavaScript interface.
|
|
61
77
|
|
|
62
|
-
|
|
63
|
-
TypeScript:
|
|
78
|
+
## Repository runner for GitHub Issues
|
|
64
79
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
Shipyard can keep one repository ready for labelled GitHub Issues on a
|
|
81
|
+
user-managed Mac. This deployment currently supports GitHub.com repositories
|
|
82
|
+
on Apple Silicon macOS only. It uses GitHub's official self-hosted Actions
|
|
83
|
+
runner as a wake-up transport; the foreground Shipyard controller owns the
|
|
84
|
+
actual work outside the Actions job.
|
|
68
85
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
promptFile: ".shipyard/prompt.md",
|
|
73
|
-
});
|
|
86
|
+
Prerequisites are an initialized repository, Docker Desktop, `gh` authenticated
|
|
87
|
+
to GitHub.com with repository-administration access, and the credentials needed
|
|
88
|
+
by the selected agent in `.shipyard/.env`. Install from the repository root:
|
|
74
89
|
|
|
75
|
-
|
|
90
|
+
```sh
|
|
91
|
+
npx shipyard runner install
|
|
92
|
+
git add .github/workflows/shipyard-wake.yml .shipyard/.gitignore
|
|
93
|
+
git commit -m "Add Shipyard wake workflow"
|
|
94
|
+
git push
|
|
95
|
+
npx shipyard runner start
|
|
76
96
|
```
|
|
77
97
|
|
|
78
|
-
|
|
98
|
+
`--registration-token` can supply the one-time registration credential, but it
|
|
99
|
+
does not replace administrative `gh` access: Shipyard still uses that access to
|
|
100
|
+
verify that the repository has no conflicting `shipyard` runner.
|
|
101
|
+
|
|
102
|
+
Interactive `shipyard init` also offers installation after it creates a valid
|
|
103
|
+
scaffold, defaulting to Yes. Non-interactive init skips installation unless
|
|
104
|
+
`--install-runner true` is passed. Installation creates the workflow locally;
|
|
105
|
+
it never commits or pushes it. In a terminal, installation shows a progress
|
|
106
|
+
bar for validation, download, registration, and workflow setup. `runner start`
|
|
107
|
+
requires the exact generated workflow on the repository's default branch.
|
|
108
|
+
|
|
109
|
+
The activation label is the exact, lowercase label `shipyard`. Init attempts to
|
|
110
|
+
create it, and start creates it if it is missing. Adding that label or manually
|
|
111
|
+
dispatching the **Shipyard wake-up** workflow wakes the controller. Labels with
|
|
112
|
+
different casing do not match. Issue edits and comments do not wake it.
|
|
113
|
+
|
|
114
|
+
The controller first checks the current backlog, so restarting it recovers work
|
|
115
|
+
labelled while the Mac was off. It invokes exactly `npx shipyard run`, then
|
|
116
|
+
compares the eligible issue-number set. An empty set returns it to idle; a
|
|
117
|
+
changed nonempty set starts another finite invocation; an unchanged set records
|
|
118
|
+
no progress and returns it to idle instead of looping. A later label event,
|
|
119
|
+
manual dispatch, or restart retries that backlog. Wake-ups delivered while
|
|
120
|
+
Shipyard is busy are coalesced.
|
|
121
|
+
|
|
122
|
+
The Actions job only reports whether its wake-up reached the controller. It
|
|
123
|
+
does not report the later agent outcome and does not own the agent's runtime.
|
|
124
|
+
The controller stays available only while `runner start` remains open in its
|
|
125
|
+
foreground terminal. Ctrl-C, closing that terminal, or `runner stop` stops it
|
|
126
|
+
and cancels active work; it does not start at login. A nonzero Shipyard exit or
|
|
127
|
+
infrastructure failure records the error and takes the controller offline.
|
|
128
|
+
|
|
129
|
+
Use these commands from the same repository root:
|
|
79
130
|
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
promptFile: ".shipyard/prompt.md",
|
|
85
|
-
branchStrategy: { type: "branch", branch: "agent/my-task" },
|
|
86
|
-
maxIterations: 1,
|
|
87
|
-
logging: { type: "stdout" },
|
|
88
|
-
});
|
|
131
|
+
```sh
|
|
132
|
+
npx shipyard runner status
|
|
133
|
+
npx shipyard runner stop
|
|
134
|
+
npx shipyard runner remove
|
|
89
135
|
```
|
|
90
136
|
|
|
91
|
-
|
|
92
|
-
and
|
|
93
|
-
|
|
137
|
+
Status reports local process state, GitHub connectivity, repository identity,
|
|
138
|
+
and the last outcome without showing credentials. Runner binaries, credentials,
|
|
139
|
+
state, and diagnostics are protected and ignored under `.shipyard/runner/`;
|
|
140
|
+
normal removal unregisters the runner and deletes those files while preserving
|
|
141
|
+
the Shipyard config, workflow, issues, logs, and worktrees. If GitHub
|
|
142
|
+
unregistration is unavailable, retry after restoring access. As a last resort,
|
|
143
|
+
`runner remove --force` deletes local files and prints the GitHub registration
|
|
144
|
+
that must be removed manually.
|
|
145
|
+
|
|
146
|
+
There is no Mac sleep/wake detector. GitHub can discard a queued self-hosted job
|
|
147
|
+
after 24 hours, so after a long sleep use manual workflow dispatch or restart
|
|
148
|
+
the controller. GitHub removes self-hosted runner registrations after 14 days
|
|
149
|
+
offline; the next start automatically re-registers with the current
|
|
150
|
+
administrative `gh` login. A Mac rename does not rename an existing
|
|
151
|
+
registration. Remove and reinstall it explicitly.
|
|
152
|
+
|
|
153
|
+
GitHub does not charge Actions minutes for the self-hosted wake job. The brief
|
|
154
|
+
exact-case gate runs on `ubuntu-latest` and may consume hosted Actions minutes
|
|
155
|
+
for private repositories. Model costs apply only when a preflight finds eligible
|
|
156
|
+
issues and starts Shipyard; the host owner still pays for hardware, electricity,
|
|
157
|
+
network, Docker, and model usage. GitHub warns that self-hosted runners on public repositories can be
|
|
158
|
+
exposed to untrusted repository activity. Use this deployment only where label
|
|
159
|
+
authority, workflows, collaborators, and agent credentials have an acceptable
|
|
160
|
+
trust boundary. See GitHub's
|
|
161
|
+
[self-hosted runner security guidance](https://docs.github.com/en/actions/reference/security/secure-use#hardening-for-self-hosted-runners).
|
|
162
|
+
|
|
163
|
+
For setup failures, run `runner status`, inspect the foreground terminal and
|
|
164
|
+
`.shipyard/runner/.shipyard-last-failure.json`, then verify Docker, GitHub
|
|
165
|
+
authentication with `gh auth status --hostname github.com`, `.shipyard/.env`,
|
|
166
|
+
the lowercase label, and the published workflow. The
|
|
167
|
+
[Apple Silicon validation runbook](docs/runbooks/repository-runner-macos-validation.md)
|
|
168
|
+
covers the complete lifecycle.
|
|
94
169
|
|
|
95
170
|
To run directly on the host without isolation:
|
|
96
171
|
|
|
@@ -108,39 +183,6 @@ const result = await run({
|
|
|
108
183
|
No-sandbox mode grants the agent the permissions of the Shipyard process. Use
|
|
109
184
|
it only with trusted repositories and prompts.
|
|
110
185
|
|
|
111
|
-
## Branch behavior
|
|
112
|
-
|
|
113
|
-
Docker keeps the repository and Git metadata inside the sandbox. It starts
|
|
114
|
-
from committed history and syncs changes back. The generated blank
|
|
115
|
-
template omits `branchStrategy`, which defaults to `merge-to-head`: commits
|
|
116
|
-
are transferred to a temporary host branch and merged into your current
|
|
117
|
-
branch. Direct `head` mode is not supported by Docker or Vercel Sandbox.
|
|
118
|
-
|
|
119
|
-
The explicit `branch` strategy keeps commits on the named branch for review.
|
|
120
|
-
Commit input files before either strategy, or use `copyToWorktree` for specific
|
|
121
|
-
untracked or ignored files.
|
|
122
|
-
|
|
123
|
-
## Authentication
|
|
124
|
-
|
|
125
|
-
`shipyard init` generates the environment variables and mounts required by
|
|
126
|
-
the chosen agent. Keep `.shipyard/.env`, agent login files, logs, and recovery
|
|
127
|
-
patches private.
|
|
128
|
-
|
|
129
|
-
Use a subscription first when the selected agent supports it. If subscription
|
|
130
|
-
authentication does not work in your environment, use the API-key fallback:
|
|
131
|
-
|
|
132
|
-
- Codex subscription authentication: choose **Sign in with ChatGPT** during
|
|
133
|
-
interactive init and complete the browser login. The generated configuration
|
|
134
|
-
mounts `~/.codex/auth.json` read-only. For non-interactive init, run
|
|
135
|
-
`codex login` on the host, then pass `--codex-auth chatgpt`.
|
|
136
|
-
- Codex API authentication: choose **OpenAI API key** during interactive init
|
|
137
|
-
and put `OPENAI_API_KEY` in `.shipyard/.env`. For non-interactive init, pass
|
|
138
|
-
`--codex-auth api-key`.
|
|
139
|
-
- Claude Code subscription authentication: run `claude setup-token` on the
|
|
140
|
-
host and put the result in `CLAUDE_CODE_OAUTH_TOKEN` in `.shipyard/.env`.
|
|
141
|
-
- Claude Code API authentication: uncomment `ANTHROPIC_API_KEY` in
|
|
142
|
-
`.shipyard/.env` and put your API key there.
|
|
143
|
-
|
|
144
186
|
## Generated files
|
|
145
187
|
|
|
146
188
|
| Path | Purpose |
|
|
@@ -152,6 +194,7 @@ authentication does not work in your environment, use the API-key fallback:
|
|
|
152
194
|
| `.shipyard/logs/` | Run logs |
|
|
153
195
|
| `.shipyard/worktrees/` | Worktrees for separate-branch runs |
|
|
154
196
|
| `.shipyard/patches/` | Recovery artifacts preserved after some failures |
|
|
197
|
+
| `.shipyard/runner/` | Protected self-hosted runner files and diagnostics |
|
|
155
198
|
|
|
156
199
|
## Workflow templates
|
|
157
200
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { Display } from './chunk-
|
|
1
|
+
import { CONFIG_DIR, ConfigDirError, assertNoSymlinkComponents, Display } from './chunk-FDYOTN55.js';
|
|
2
2
|
import { Effect } from 'effect';
|
|
3
|
+
import { FileSystem } from '@effect/platform';
|
|
4
|
+
import { join } from 'path';
|
|
3
5
|
|
|
4
6
|
var formatErrorMessage = (error) => {
|
|
5
7
|
switch (error._tag) {
|
|
@@ -107,7 +109,63 @@ var CODEX_MODELS = {
|
|
|
107
109
|
effort: readEffort("strong")
|
|
108
110
|
}
|
|
109
111
|
};
|
|
112
|
+
var parseEnvFile = (filePath) => Effect.gen(function* () {
|
|
113
|
+
const fs = yield* FileSystem.FileSystem;
|
|
114
|
+
const symlink = yield* fs.readLink(filePath).pipe(
|
|
115
|
+
Effect.map(() => true),
|
|
116
|
+
Effect.catchAll(() => Effect.succeed(false))
|
|
117
|
+
);
|
|
118
|
+
if (symlink) return {};
|
|
119
|
+
const content = yield* fs.readFileString(filePath).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
120
|
+
if (content === null) return {};
|
|
121
|
+
const vars = {};
|
|
122
|
+
for (const line of content.split("\n")) {
|
|
123
|
+
const trimmed = line.trim();
|
|
124
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
125
|
+
const eqIndex = trimmed.indexOf("=");
|
|
126
|
+
if (eqIndex === -1) continue;
|
|
127
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
128
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
129
|
+
const isDoubleQuoted = value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"';
|
|
130
|
+
const isSingleQuoted = value.length >= 2 && value[0] === "'" && value[value.length - 1] === "'";
|
|
131
|
+
if (isDoubleQuoted || isSingleQuoted) {
|
|
132
|
+
value = value.slice(1, -1);
|
|
133
|
+
}
|
|
134
|
+
if (isDoubleQuoted) {
|
|
135
|
+
value = value.replace(/\\([nrt\\])/g, (_, ch) => {
|
|
136
|
+
const escapes = {
|
|
137
|
+
n: "\n",
|
|
138
|
+
r: "\r",
|
|
139
|
+
t: " ",
|
|
140
|
+
"\\": "\\"
|
|
141
|
+
};
|
|
142
|
+
return escapes[ch] ?? ch;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
vars[key] = value;
|
|
146
|
+
}
|
|
147
|
+
return vars;
|
|
148
|
+
});
|
|
149
|
+
var resolveEnv = (repoDir) => Effect.gen(function* () {
|
|
150
|
+
const configDir = join(repoDir, CONFIG_DIR);
|
|
151
|
+
const envPath = join(configDir, ".env");
|
|
152
|
+
yield* Effect.tryPromise({
|
|
153
|
+
try: () => assertNoSymlinkComponents(repoDir, envPath, "environment file"),
|
|
154
|
+
catch: (e) => new ConfigDirError({
|
|
155
|
+
message: `Refusing symlinked environment path: ${e instanceof Error ? e.message : String(e)}`
|
|
156
|
+
})
|
|
157
|
+
});
|
|
158
|
+
const shipyardEnv = yield* parseEnvFile(envPath);
|
|
159
|
+
const result = {};
|
|
160
|
+
for (const key of Object.keys(shipyardEnv)) {
|
|
161
|
+
const value = shipyardEnv[key] || process.env[key];
|
|
162
|
+
if (value) {
|
|
163
|
+
result[key] = value;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
});
|
|
110
168
|
|
|
111
|
-
export { CODEX_MODELS, CODEX_REASONING_EFFORTS, formatErrorMessage, withFriendlyErrors };
|
|
112
|
-
//# sourceMappingURL=chunk-
|
|
113
|
-
//# sourceMappingURL=chunk-
|
|
169
|
+
export { CODEX_MODELS, CODEX_REASONING_EFFORTS, formatErrorMessage, resolveEnv, withFriendlyErrors };
|
|
170
|
+
//# sourceMappingURL=chunk-EGYPJMLV.js.map
|
|
171
|
+
//# sourceMappingURL=chunk-EGYPJMLV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ErrorHandler.ts","../src/modelConfig.ts","../src/EnvResolver.ts"],"names":["Effect"],"mappings":";;;;;AAQO,IAAM,kBAAA,GAAqB,CAAC,KAAA,KAAgC;AACjE,EAAA,QAAQ,MAAM,IAAA;AAAM,IAClB,KAAK,WAAA;AACH,MAAA,OAAO,CAAA,2BAAA,EAA8B,KAAA,CAAM,OAAO,CAAA,GAAA,EAAM,MAAM,OAAO,CAAA,CAAA;AAAA,IACvE,KAAK,eAAA;AACH,MAAA,OAAO,CAAA,wBAAA,EAA2B,KAAA,CAAM,OAAO,CAAA,GAAA,EAAM,MAAM,OAAO,CAAA,CAAA;AAAA,IACpE,KAAK,WAAA;AACH,MAAA,OAAO,CAAA,kBAAA,EAAqB,MAAM,OAAO,CAAA,CAAA;AAAA,IAC3C,KAAK,aAAA;AACH,MAAA,OAAO,CAAA,yBAAA,EAA4B,MAAM,OAAO,CAAA,oBAAA,CAAA;AAAA,IAClD,KAAK,WAAA;AACH,MAAA,OAAO,CAAA,iBAAA,EAAoB,MAAM,OAAO,CAAA,CAAA;AAAA,IAC1C,KAAK,eAAA;AACH,MAAA,OAAO,CAAA,+BAAA,EAAkC,MAAM,OAAO,CAAA,CAAA;AAAA,IACxD,KAAK,aAAA;AACH,MAAA,OAAO,CAAA,0BAAA,EAA6B,MAAM,OAAO,CAAA,CAAA;AAAA,IACnD,KAAK,YAAA;AACH,MAAA,OAAO,CAAA,yBAAA,EAA4B,MAAM,OAAO,CAAA,CAAA;AAAA,IAClD,KAAK,gBAAA;AACH,MAAA,OAAO,CAAA,EAAG,MAAM,OAAO,CAAA,CAAA;AAAA,IACzB,KAAK,WAAA;AACH,MAAA,OAAO,CAAA,EAAG,MAAM,OAAO,CAAA,CAAA;AAAA,IACzB,KAAK,uBAAA;AAAA,IACL,KAAK,sBAAA;AAAA,IACL,KAAK,4BAAA;AAAA,IACL,KAAK,4BAAA;AAAA,IACL,KAAK,qBAAA;AAAA,IACL,KAAK,oBAAA;AAAA,IACL,KAAK,kBAAA;AAAA,IACL,KAAK,sBAAA;AAAA,IACL,KAAK,6BAAA;AAAA,IACL,KAAK,8BAAA;AAAA,IACL,KAAK,yBAAA;AAAA,IACL,KAAK,qBAAA;AAAA,IACL,KAAK,UAAA;AACH,MAAA,OAAO,KAAA,CAAM,OAAA;AAAA;AAEnB;AAEA,IAAM,gBAAA,GAAmB,CAAC,KAAA,KACxB,MAAA,CAAO,IAAI,aAAa;AACtB,EAAA,MAAM,IAAI,OAAO,OAAA;AACjB,EAAA,OAAO,CAAA,CAAE,MAAA,CAAO,kBAAA,CAAmB,KAAK,GAAG,OAAO,CAAA;AAClD,EAAA,OAAO,OAAO,MAAA,CAAO,IAAA,CAAK,MAAM,OAAA,CAAQ,IAAA,CAAK,CAAC,CAAU,CAAA;AAC1D,CAAC,CAAA;AAOI,IAAM,qBAAqB,CAChC,MAAA;AAAA;AAAA,EAGA,MAAA,CAAO,UAAU,MAAA,EAA6C;AAAA,IAC5D,SAAA,EAAW,gBAAA;AAAA,IACX,aAAA,EAAe,gBAAA;AAAA,IACf,SAAA,EAAW,gBAAA;AAAA,IACX,WAAA,EAAa,gBAAA;AAAA,IACb,SAAA,EAAW,gBAAA;AAAA,IACX,aAAA,EAAe,gBAAA;AAAA,IACf,WAAA,EAAa,gBAAA;AAAA,IACb,UAAA,EAAY,gBAAA;AAAA,IACZ,cAAA,EAAgB,gBAAA;AAAA,IAChB,SAAA,EAAW,gBAAA;AAAA,IACX,qBAAA,EAAuB,gBAAA;AAAA,IACvB,oBAAA,EAAsB,gBAAA;AAAA,IACtB,0BAAA,EAA4B,gBAAA;AAAA,IAC5B,0BAAA,EAA4B,gBAAA;AAAA,IAC5B,mBAAA,EAAqB,gBAAA;AAAA,IACrB,kBAAA,EAAoB,gBAAA;AAAA,IACpB,gBAAA,EAAkB,gBAAA;AAAA,IAClB,oBAAA,EAAsB,gBAAA;AAAA,IACtB,2BAAA,EAA6B,gBAAA;AAAA,IAC7B,4BAAA,EAA8B,gBAAA;AAAA,IAC9B,uBAAA,EAAyB,gBAAA;AAAA,IACzB,mBAAA,EAAqB,gBAAA;AAAA,IACrB,QAAA,EAAU;AAAA,GACX;AAAA;;;ACtFI,IAAM,uBAAA,GAA0B;AAAA,EACrC,KAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF;AAsBA,IAAM,oBAAA,GAAuB;AAAA,EAC3B,OAAA,EAAS,EAAE,KAAA,EAAO,cAAA,EAAgB,QAAQ,KAAA,EAAM;AAAA,EAChD,MAAA,EAAQ,EAAE,KAAA,EAAO,aAAA,EAAe,QAAQ,KAAA;AAC1C,CAAA;AAEA,IAAM,SAAA,GAAY,CAAC,IAAA,KACjB,OAAA,CAAQ,IAAI,CAAA,eAAA,EAAkB,IAAA,CAAK,WAAA,EAAa,QAAQ,CAAA,EAAG,IAAA,EAAK,IAChE,oBAAA,CAAqB,IAAI,CAAA,CAAE,KAAA;AAE7B,IAAM,UAAA,GAAa,CACjB,IAAA,KACyB;AACzB,EAAA,MAAM,OAAA,GAAU,CAAA,eAAA,EAAkB,IAAA,CAAK,WAAA,EAAa,CAAA,iBAAA,CAAA;AACpD,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,GAAA,CAAI,OAAO,GAAG,IAAA,EAAK;AACzC,EAAA,IAAI,CAAC,KAAA,EAAO,OAAO,oBAAA,CAAqB,IAAI,CAAA,CAAE,MAAA;AAC9C,EAAA,IAAK,uBAAA,CAA8C,QAAA,CAAS,KAAK,CAAA,EAAG;AAClE,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,MAAM,IAAI,KAAA;AAAA,IACR,CAAA,EAAG,OAAO,CAAA,gBAAA,EAAmB,uBAAA,CAAwB,KAAK,IAAI,CAAC,eAAe,KAAK,CAAA,CAAA;AAAA,GACrF;AACF,CAAA;AAEO,IAAM,YAAA,GAAe;AAAA,EAC1B,OAAA,EAAS;AAAA,IACP,KAAA,EAAO,UAAU,SAAS,CAAA;AAAA,IAC1B,MAAA,EAAQ,WAAW,SAAS;AAAA,GAC9B;AAAA,EACA,MAAA,EAAQ;AAAA,IACN,KAAA,EAAO,UAAU,QAAQ,CAAA;AAAA,IACzB,MAAA,EAAQ,WAAW,QAAQ;AAAA;AAE/B;ACrDA,IAAM,YAAA,GAAe,CACnB,QAAA,KAEAA,MAAAA,CAAO,IAAI,aAAa;AACtB,EAAA,MAAM,EAAA,GAAK,OAAO,UAAA,CAAW,UAAA;AAC7B,EAAA,MAAM,OAAA,GAAU,OAAO,EAAA,CAAG,QAAA,CAAS,QAAQ,CAAA,CAAE,IAAA;AAAA,IAC3CA,MAAAA,CAAO,GAAA,CAAI,MAAM,IAAI,CAAA;AAAA,IACrBA,OAAO,QAAA,CAAS,MAAMA,MAAAA,CAAO,OAAA,CAAQ,KAAK,CAAC;AAAA,GAC7C;AAGA,EAAA,IAAI,OAAA,SAAgB,EAAC;AACrB,EAAA,MAAM,OAAA,GAAU,OAAO,EAAA,CACpB,cAAA,CAAe,QAAQ,CAAA,CACvB,IAAA,CAAKA,MAAAA,CAAO,QAAA,CAAS,MAAMA,MAAAA,CAAO,OAAA,CAAQ,IAAI,CAAC,CAAC,CAAA;AACnD,EAAA,IAAI,OAAA,KAAY,IAAA,EAAM,OAAO,EAAC;AAC9B,EAAA,MAAM,OAA+B,EAAC;AACtC,EAAA,KAAA,MAAW,IAAA,IAAQ,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAA,EAAG;AACtC,IAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAC1B,IAAA,IAAI,CAAC,OAAA,IAAW,OAAA,CAAQ,UAAA,CAAW,GAAG,CAAA,EAAG;AACzC,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AACnC,IAAA,IAAI,YAAY,EAAA,EAAI;AACpB,IAAA,MAAM,MAAM,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,OAAO,EAAE,IAAA,EAAK;AAC3C,IAAA,IAAI,QAAQ,OAAA,CAAQ,KAAA,CAAM,OAAA,GAAU,CAAC,EAAE,IAAA,EAAK;AAC5C,IAAA,MAAM,cAAA,GACJ,KAAA,CAAM,MAAA,IAAU,CAAA,IAChB,KAAA,CAAM,CAAC,CAAA,KAAM,GAAA,IACb,KAAA,CAAM,KAAA,CAAM,MAAA,GAAS,CAAC,CAAA,KAAM,GAAA;AAC9B,IAAA,MAAM,cAAA,GACJ,KAAA,CAAM,MAAA,IAAU,CAAA,IAChB,KAAA,CAAM,CAAC,CAAA,KAAM,GAAA,IACb,KAAA,CAAM,KAAA,CAAM,MAAA,GAAS,CAAC,CAAA,KAAM,GAAA;AAC9B,IAAA,IAAI,kBAAkB,cAAA,EAAgB;AACpC,MAAA,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AAAA,IAC3B;AACA,IAAA,IAAI,cAAA,EAAgB;AAClB,MAAA,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,cAAA,EAAgB,CAAC,GAAG,EAAA,KAAe;AACvD,QAAA,MAAM,OAAA,GAAkC;AAAA,UACtC,CAAA,EAAG,IAAA;AAAA,UACH,CAAA,EAAG,IAAA;AAAA,UACH,CAAA,EAAG,GAAA;AAAA,UACH,IAAA,EAAM;AAAA,SACR;AACA,QAAA,OAAO,OAAA,CAAQ,EAAE,CAAA,IAAK,EAAA;AAAA,MACxB,CAAC,CAAA;AAAA,IACH;AACA,IAAA,IAAA,CAAK,GAAG,CAAA,GAAI,KAAA;AAAA,EACd;AACA,EAAA,OAAO,IAAA;AACT,CAAC,CAAA;AASI,IAAM,UAAA,GAAa,CACxB,OAAA,KAMAA,MAAAA,CAAO,IAAI,aAAa;AACtB,EAAA,MAAM,SAAA,GAAY,IAAA,CAAK,OAAA,EAAS,UAAU,CAAA;AAC1C,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,SAAA,EAAW,MAAM,CAAA;AACtC,EAAA,OAAOA,OAAO,UAAA,CAAW;AAAA,IACvB,GAAA,EAAK,MACH,yBAAA,CAA0B,OAAA,EAAS,SAAS,kBAAkB,CAAA;AAAA,IAChE,KAAA,EAAO,CAAC,CAAA,KACN,IAAI,cAAA,CAAe;AAAA,MACjB,OAAA,EAAS,wCAAwC,CAAA,YAAa,KAAA,GAAQ,EAAE,OAAA,GAAU,MAAA,CAAO,CAAC,CAAC,CAAA;AAAA,KAC5F;AAAA,GACJ,CAAA;AACD,EAAA,MAAM,WAAA,GAAc,OAAO,YAAA,CAAa,OAAO,CAAA;AAE/C,EAAA,MAAM,SAAiC,EAAC;AACxC,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,WAAW,CAAA,EAAG;AAK1C,IAAA,MAAM,QAAQ,WAAA,CAAY,GAAG,CAAA,IAAK,OAAA,CAAQ,IAAI,GAAG,CAAA;AACjD,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,KAAA;AAAA,IAChB;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT,CAAC","file":"chunk-EGYPJMLV.js","sourcesContent":["import { Effect } from \"effect\";\nimport { Display } from \"./Display.js\";\nimport type { SandboxError } from \"./errors.js\";\n\n/**\n * Formats a tagged SandboxError into a user-friendly message with\n * context-specific hints about what went wrong and how to recover.\n */\nexport const formatErrorMessage = (error: SandboxError): string => {\n switch (error._tag) {\n case \"ExecError\":\n return `Command failed in sandbox (${error.command}): ${error.message}`;\n case \"ExecHostError\":\n return `Command failed on host (${error.command}): ${error.message}`;\n case \"CopyError\":\n return `File copy failed: ${error.message}`;\n case \"DockerError\":\n return `Docker operation failed: ${error.message}. Is Docker running?`;\n case \"SyncError\":\n return `Git sync failed: ${error.message}`;\n case \"WorktreeError\":\n return `Git worktree operation failed: ${error.message}`;\n case \"PromptError\":\n return `Failed to resolve prompt: ${error.message}`;\n case \"AgentError\":\n return `Agent invocation failed: ${error.message}`;\n case \"ConfigDirError\":\n return `${error.message}`;\n case \"InitError\":\n return `${error.message}`;\n case \"AgentIdleTimeoutError\":\n case \"WorktreeTimeoutError\":\n case \"ContainerStartTimeoutError\":\n case \"CopyToWorktreeTimeoutError\":\n case \"CopyToWorktreeError\":\n case \"SyncInTimeoutError\":\n case \"HookTimeoutError\":\n case \"GitSetupTimeoutError\":\n case \"PromptExpansionTimeoutError\":\n case \"CommitCollectionTimeoutError\":\n case \"MergeToHostTimeoutError\":\n case \"SessionCaptureError\":\n case \"CwdError\":\n return error.message;\n }\n};\n\nconst showErrorAndExit = (error: SandboxError) =>\n Effect.gen(function* () {\n const d = yield* Display;\n yield* d.status(formatErrorMessage(error), \"error\");\n return yield* Effect.sync(() => process.exit(1) as never);\n });\n\n/**\n * Wraps an effect so that any SandboxError is caught, displayed via the\n * Display service as an error-severity status message, then exits the process\n * with code 1. Non-SandboxError errors pass through unchanged.\n */\nexport const withFriendlyErrors = <A, E, R>(\n effect: Effect.Effect<A, E, R>,\n): Effect.Effect<A, Exclude<E, SandboxError>, R | Display> =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Effect.catchTags(effect as Effect.Effect<A, SandboxError, R>, {\n ExecError: showErrorAndExit,\n ExecHostError: showErrorAndExit,\n CopyError: showErrorAndExit,\n DockerError: showErrorAndExit,\n SyncError: showErrorAndExit,\n WorktreeError: showErrorAndExit,\n PromptError: showErrorAndExit,\n AgentError: showErrorAndExit,\n ConfigDirError: showErrorAndExit,\n InitError: showErrorAndExit,\n AgentIdleTimeoutError: showErrorAndExit,\n WorktreeTimeoutError: showErrorAndExit,\n ContainerStartTimeoutError: showErrorAndExit,\n CopyToWorktreeTimeoutError: showErrorAndExit,\n CopyToWorktreeError: showErrorAndExit,\n SyncInTimeoutError: showErrorAndExit,\n HookTimeoutError: showErrorAndExit,\n GitSetupTimeoutError: showErrorAndExit,\n PromptExpansionTimeoutError: showErrorAndExit,\n CommitCollectionTimeoutError: showErrorAndExit,\n MergeToHostTimeoutError: showErrorAndExit,\n SessionCaptureError: showErrorAndExit,\n CwdError: showErrorAndExit,\n }) as Effect.Effect<A, Exclude<E, SandboxError>, R | Display>;\n","/** Reasoning efforts accepted by the Codex CLI. */\nexport const CODEX_REASONING_EFFORTS = [\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\",\n] as const;\n\nexport type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORTS)[number];\n\n/** A model identifier plus the reasoning policy used for that model role. */\nexport interface CodexModelConfig {\n readonly model: string;\n readonly effort: CodexReasoningEffort;\n}\n\n/**\n * Default values for Shipyard's built-in Codex roles.\n *\n * Runtime environment variables override these defaults:\n * SHIPYARD_CODEX_ROUTINE_MODEL\n * SHIPYARD_CODEX_ROUTINE_REASONING_EFFORT\n * SHIPYARD_CODEX_STRONG_MODEL\n * SHIPYARD_CODEX_STRONG_REASONING_EFFORT\n *\n * Keep the defaults here so runtime providers, generated templates, and the\n * repository workflows all use the same role definitions.\n */\nconst DEFAULT_CODEX_MODELS = {\n routine: { model: \"gpt-5.6-luna\", effort: \"max\" },\n strong: { model: \"gpt-5.6-sol\", effort: \"max\" },\n} as const satisfies Record<string, CodexModelConfig>;\n\nconst readModel = (role: keyof typeof DEFAULT_CODEX_MODELS): string =>\n process.env[`SHIPYARD_CODEX_${role.toUpperCase()}_MODEL`]?.trim() ||\n DEFAULT_CODEX_MODELS[role].model;\n\nconst readEffort = (\n role: keyof typeof DEFAULT_CODEX_MODELS,\n): CodexReasoningEffort => {\n const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;\n const value = process.env[envName]?.trim();\n if (!value) return DEFAULT_CODEX_MODELS[role].effort;\n if ((CODEX_REASONING_EFFORTS as readonly string[]).includes(value)) {\n return value as CodexReasoningEffort;\n }\n throw new Error(\n `${envName} must be one of ${CODEX_REASONING_EFFORTS.join(\", \")}; received \"${value}\"`,\n );\n};\n\nexport const CODEX_MODELS = {\n routine: {\n model: readModel(\"routine\"),\n effort: readEffort(\"routine\"),\n },\n strong: {\n model: readModel(\"strong\"),\n effort: readEffort(\"strong\"),\n },\n} as const satisfies Record<string, CodexModelConfig>;\n","import { FileSystem } from \"@effect/platform\";\nimport { Effect } from \"effect\";\nimport { join } from \"node:path\";\n\nimport { ConfigDirError } from \"./errors.js\";\nimport { assertNoSymlinkComponents } from \"./pathSecurity.js\";\nimport { CONFIG_DIR } from \"./runtimeNames.js\";\n\nconst parseEnvFile = (\n filePath: string,\n): Effect.Effect<Record<string, string>, never, FileSystem.FileSystem> =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const symlink = yield* fs.readLink(filePath).pipe(\n Effect.map(() => true),\n Effect.catchAll(() => Effect.succeed(false)),\n );\n // A repository-controlled symlink could redirect this read to a host\n // secret and then expose matching keys to the agent.\n if (symlink) return {};\n const content = yield* fs\n .readFileString(filePath)\n .pipe(Effect.catchAll(() => Effect.succeed(null)));\n if (content === null) return {};\n const vars: Record<string, string> = {};\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eqIndex = trimmed.indexOf(\"=\");\n if (eqIndex === -1) continue;\n const key = trimmed.slice(0, eqIndex).trim();\n let value = trimmed.slice(eqIndex + 1).trim();\n const isDoubleQuoted =\n value.length >= 2 &&\n value[0] === '\"' &&\n value[value.length - 1] === '\"';\n const isSingleQuoted =\n value.length >= 2 &&\n value[0] === \"'\" &&\n value[value.length - 1] === \"'\";\n if (isDoubleQuoted || isSingleQuoted) {\n value = value.slice(1, -1);\n }\n if (isDoubleQuoted) {\n value = value.replace(/\\\\([nrt\\\\])/g, (_, ch: string) => {\n const escapes: Record<string, string> = {\n n: \"\\n\",\n r: \"\\r\",\n t: \"\\t\",\n \"\\\\\": \"\\\\\",\n };\n return escapes[ch] ?? ch;\n });\n }\n vars[key] = value;\n }\n return vars;\n });\n\n/**\n * Resolve all env vars from .env files with process.env fallback.\n *\n * Precedence: .shipyard/.env > process.env\n * Only keys declared in .shipyard/.env are resolved from process.env.\n * Repo root .env is not part of the resolution chain.\n */\nexport const resolveEnv = (\n repoDir: string,\n): Effect.Effect<\n Record<string, string>,\n ConfigDirError,\n FileSystem.FileSystem\n> =>\n Effect.gen(function* () {\n const configDir = join(repoDir, CONFIG_DIR);\n const envPath = join(configDir, \".env\");\n yield* Effect.tryPromise({\n try: () =>\n assertNoSymlinkComponents(repoDir, envPath, \"environment file\"),\n catch: (e) =>\n new ConfigDirError({\n message: `Refusing symlinked environment path: ${e instanceof Error ? e.message : String(e)}`,\n }),\n });\n const shipyardEnv = yield* parseEnvFile(envPath);\n\n const result: Record<string, string> = {};\n for (const key of Object.keys(shipyardEnv)) {\n // A blank placeholder falls back to process.env, so callers can source\n // credentials from a host login for one invocation, e.g.\n // `GH_TOKEN=\"$(gh auth token)\" npx shipyard run`. Only keys declared in\n // .shipyard/.env are eligible for this fallback.\n const value = shipyardEnv[key] || process.env[key];\n if (value) {\n result[key] = value;\n }\n }\n\n return result;\n });\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toSessionTransferHandle, shellQuote } from './chunk-KMGNFXKN.js';
|
|
2
|
-
import { existsSync, constants, statSync } from 'fs';
|
|
2
|
+
import { existsSync, constants, lstatSync, realpathSync, statSync } from 'fs';
|
|
3
3
|
import { mkdtemp, writeFile, chmod, mkdir, stat, copyFile, rm, readdir, lstat, readFile, open } from 'fs/promises';
|
|
4
4
|
import * as clack from '@clack/prompts';
|
|
5
5
|
import { FileSystem } from '@effect/platform';
|
|
@@ -29,6 +29,15 @@ var SilentDisplay = {
|
|
|
29
29
|
]),
|
|
30
30
|
() => effect
|
|
31
31
|
),
|
|
32
|
+
progress: (title, effect) => Effect.gen(function* () {
|
|
33
|
+
const updates = [];
|
|
34
|
+
const result = yield* effect((update) => updates.push(update));
|
|
35
|
+
yield* Ref.update(ref, (entries) => [
|
|
36
|
+
...entries,
|
|
37
|
+
{ _tag: "progress", title, updates: [...updates] }
|
|
38
|
+
]);
|
|
39
|
+
return result;
|
|
40
|
+
}),
|
|
32
41
|
summary: (title, rows) => Ref.update(ref, (entries) => [
|
|
33
42
|
...entries,
|
|
34
43
|
{ _tag: "summary", title, rows }
|
|
@@ -127,6 +136,20 @@ var FileDisplay = {
|
|
|
127
136
|
yield* appendToLog(`${message} done (${elapsed}s)`);
|
|
128
137
|
return result;
|
|
129
138
|
}),
|
|
139
|
+
progress: (title, effect) => Effect.gen(function* () {
|
|
140
|
+
yield* appendToLog(`${title}...`);
|
|
141
|
+
const start = Date.now();
|
|
142
|
+
const updates = [];
|
|
143
|
+
const result = yield* effect((update) => updates.push(update));
|
|
144
|
+
for (const update of updates) {
|
|
145
|
+
yield* appendToLog(
|
|
146
|
+
` ${update.message} (${update.current}/${update.total})`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
150
|
+
yield* appendToLog(`${title} done (${elapsed}s)`);
|
|
151
|
+
return result;
|
|
152
|
+
}),
|
|
130
153
|
summary: (title, rows) => {
|
|
131
154
|
const lines = Object.entries(rows).map(([key, value]) => ` ${key}: ${value}`).join("\n");
|
|
132
155
|
return appendToLog(`${title}
|
|
@@ -186,6 +209,34 @@ var ClackDisplay = {
|
|
|
186
209
|
}
|
|
187
210
|
})
|
|
188
211
|
),
|
|
212
|
+
progress: (title, effect) => Effect.acquireUseRelease(
|
|
213
|
+
Effect.sync(() => {
|
|
214
|
+
const progress2 = clack.progress({ max: 100 });
|
|
215
|
+
let percent = 0;
|
|
216
|
+
progress2.start(title);
|
|
217
|
+
const report = (update) => {
|
|
218
|
+
const total = Math.max(1, update.total);
|
|
219
|
+
const current = Math.min(total, Math.max(0, update.current));
|
|
220
|
+
const nextPercent = Math.round(current / total * 100);
|
|
221
|
+
const message = `${update.message} (${current}/${total})`;
|
|
222
|
+
if (nextPercent > percent) {
|
|
223
|
+
progress2.advance(nextPercent - percent, message);
|
|
224
|
+
} else {
|
|
225
|
+
progress2.message(message);
|
|
226
|
+
}
|
|
227
|
+
percent = nextPercent;
|
|
228
|
+
};
|
|
229
|
+
return { progress: progress2, report };
|
|
230
|
+
}),
|
|
231
|
+
({ report }) => effect(report),
|
|
232
|
+
({ progress: progress2 }, exit) => Effect.sync(() => {
|
|
233
|
+
if (exit._tag === "Success") {
|
|
234
|
+
progress2.stop(`${title} complete`);
|
|
235
|
+
} else {
|
|
236
|
+
progress2.error(`${title} failed`);
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
),
|
|
189
240
|
summary: (title, rows) => Effect.sync(() => {
|
|
190
241
|
const lines = Object.entries(rows).map(([key, value]) => terminalStyle.summaryRow(key, value)).join("\n");
|
|
191
242
|
clack.note(lines, terminalStyle.summaryTitle(title));
|
|
@@ -306,11 +357,14 @@ var SessionCaptureError = class extends Data.TaggedError(
|
|
|
306
357
|
var PRODUCT_NAME = "Shipyard";
|
|
307
358
|
var CLI_NAME = "shipyard";
|
|
308
359
|
var RUNTIME_NAMESPACE = "shipyard";
|
|
360
|
+
var ACTIVATION_LABEL = "shipyard";
|
|
309
361
|
var CONFIG_DIR = ".shipyard";
|
|
310
362
|
var LOGS_DIR = "logs";
|
|
311
363
|
var WORKTREES_DIR = "worktrees";
|
|
312
364
|
var PATCHES_DIR = "patches";
|
|
313
365
|
var LOCKS_DIR = "locks";
|
|
366
|
+
var RUNNER_DIR = "runner";
|
|
367
|
+
var RUNNER_SANDBOX_MASK_DIR = "runner-sandbox-mask";
|
|
314
368
|
var PARENT_GIT_SANDBOX_DIR = "/.shipyard-parent-git";
|
|
315
369
|
var SYNC_BASE_REF = "refs/shipyard/sync-base";
|
|
316
370
|
var CONTAINER_NAME_PREFIX = "shipyard-";
|
|
@@ -815,6 +869,79 @@ var pruneStale = (repoDir) => Effect.gen(function* () {
|
|
|
815
869
|
})
|
|
816
870
|
)
|
|
817
871
|
);
|
|
872
|
+
var PROTECTED_RUNNER_PATH = `${CONFIG_DIR}/${RUNNER_DIR}`;
|
|
873
|
+
var pathKey = (value) => value.normalize("NFC").toLocaleLowerCase("en-US");
|
|
874
|
+
var SAFE_RUNNER_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
875
|
+
"HOME",
|
|
876
|
+
"LANG",
|
|
877
|
+
"LC_ALL",
|
|
878
|
+
"LOGNAME",
|
|
879
|
+
"PATH",
|
|
880
|
+
"SHELL",
|
|
881
|
+
"TEMP",
|
|
882
|
+
"TERM",
|
|
883
|
+
"TMP",
|
|
884
|
+
"TMPDIR",
|
|
885
|
+
"USER"
|
|
886
|
+
]);
|
|
887
|
+
var repositoryRunnerEnvironment = (source) => Object.fromEntries(
|
|
888
|
+
Object.entries(source).filter(
|
|
889
|
+
([key, value]) => SAFE_RUNNER_ENV_KEYS.has(key) && value !== void 0
|
|
890
|
+
)
|
|
891
|
+
);
|
|
892
|
+
var assertExcludesRepositoryRunner = (relativePath) => {
|
|
893
|
+
const normalized = pathKey(
|
|
894
|
+
posix.normalize(relativePath.replace(/\\/g, "/")).replace(/^\.\//, "").replace(/\/$/, "")
|
|
895
|
+
);
|
|
896
|
+
const protectedPath = pathKey(PROTECTED_RUNNER_PATH);
|
|
897
|
+
if (normalized === "." || normalized === protectedPath || normalized.startsWith(`${protectedPath}/`) || protectedPath.startsWith(`${normalized}/`)) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
`Copy path ${relativePath} would expose the protected repository runner to an agent sandbox.`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
var assertSafeRepositoryRunnerDirectories = (repoDir) => {
|
|
904
|
+
const runnerDir = join(repoDir, CONFIG_DIR, RUNNER_DIR);
|
|
905
|
+
const maskDir = join(repoDir, CONFIG_DIR, RUNNER_SANDBOX_MASK_DIR);
|
|
906
|
+
for (const [name, path] of [
|
|
907
|
+
["repository runner", runnerDir],
|
|
908
|
+
["repository runner sandbox mask", maskDir]
|
|
909
|
+
]) {
|
|
910
|
+
const stat2 = lstatSync(path);
|
|
911
|
+
if (stat2.isSymbolicLink() || !stat2.isDirectory()) {
|
|
912
|
+
throw new Error(
|
|
913
|
+
`The protected ${name} path must be a real directory: ${path}`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (pathKey(realpathSync(runnerDir)) === pathKey(realpathSync(maskDir))) {
|
|
918
|
+
throw new Error(
|
|
919
|
+
"The protected repository runner and sandbox mask paths must be distinct directories."
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
var assertProtectedDirectoryIdentities = async (runnerDir, maskDir, inspect) => {
|
|
924
|
+
if (!inspect) return;
|
|
925
|
+
const runner = await inspect(runnerDir);
|
|
926
|
+
const mask = await inspect(maskDir);
|
|
927
|
+
if (!runner.directory || runner.symbolicLink) {
|
|
928
|
+
throw new Error(
|
|
929
|
+
`The protected repository runner path must be a real directory: ${runnerDir}`
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
if (!mask.directory || mask.symbolicLink) {
|
|
933
|
+
throw new Error(
|
|
934
|
+
`The protected repository runner sandbox mask path must be a real directory: ${maskDir}`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
if (pathKey(runner.realPath) === pathKey(mask.realPath)) {
|
|
938
|
+
throw new Error(
|
|
939
|
+
"The protected repository runner and sandbox mask paths must be distinct directories."
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
// src/CopyToWorktree.ts
|
|
818
945
|
var COPY_TO_WORKTREE_TIMEOUT_MS = 6e4;
|
|
819
946
|
var getCopyOnWriteFlags = (platform) => platform === "darwin" ? ["-cR"] : ["-R", "--reflink=auto"];
|
|
820
947
|
var copyToWorktree = (paths, hostRepoDir, worktreePath, timeoutMs) => {
|
|
@@ -825,6 +952,7 @@ var copyToWorktree = (paths, hostRepoDir, worktreePath, timeoutMs) => {
|
|
|
825
952
|
let src;
|
|
826
953
|
let dest;
|
|
827
954
|
try {
|
|
955
|
+
assertExcludesRepositoryRunner(relativePath);
|
|
828
956
|
src = resolveSafeRelativePath(hostRepoDir, relativePath, "copy path");
|
|
829
957
|
dest = resolveSafeRelativePath(worktreePath, relativePath, "copy path");
|
|
830
958
|
} catch (error) {
|
|
@@ -1109,7 +1237,35 @@ var syncOut = (hostRepoDir, handle) => Effect.gen(function* () {
|
|
|
1109
1237
|
const hasUntracked = lsFilesResult.stdout.length > 0;
|
|
1110
1238
|
const untrackedFiles = hasUntracked ? lsFilesResult.stdout.split("\0").filter((f) => f.length > 0) : [];
|
|
1111
1239
|
yield* Effect.try({
|
|
1112
|
-
try: () => untrackedFiles.forEach(
|
|
1240
|
+
try: () => untrackedFiles.forEach((path) => {
|
|
1241
|
+
assertSafeGitWorktreePath(path);
|
|
1242
|
+
assertExcludesRepositoryRunner(path);
|
|
1243
|
+
}),
|
|
1244
|
+
catch: (error) => new SyncError({ message: String(error) })
|
|
1245
|
+
});
|
|
1246
|
+
const changedPaths = [];
|
|
1247
|
+
if (hasCommits) {
|
|
1248
|
+
const committedNames = yield* execOk(
|
|
1249
|
+
handle,
|
|
1250
|
+
`git diff --name-only -z ${shellQuote(`${base}..HEAD`)}`,
|
|
1251
|
+
{ cwd: worktreePath }
|
|
1252
|
+
);
|
|
1253
|
+
changedPaths.push(
|
|
1254
|
+
...committedNames.stdout.split("\0").filter((path) => path.length > 0)
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
if (hasDiff) {
|
|
1258
|
+
const uncommittedNames = yield* execOk(
|
|
1259
|
+
handle,
|
|
1260
|
+
"git diff --name-only -z HEAD",
|
|
1261
|
+
{ cwd: worktreePath }
|
|
1262
|
+
);
|
|
1263
|
+
changedPaths.push(
|
|
1264
|
+
...uncommittedNames.stdout.split("\0").filter((path) => path.length > 0)
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
yield* Effect.try({
|
|
1268
|
+
try: () => changedPaths.forEach(assertExcludesRepositoryRunner),
|
|
1113
1269
|
catch: (error) => new SyncError({ message: String(error) })
|
|
1114
1270
|
});
|
|
1115
1271
|
if (!hasCommits && !hasDiff && !hasUntracked) {
|
|
@@ -1727,6 +1883,18 @@ ${result.stderr}`
|
|
|
1727
1883
|
)
|
|
1728
1884
|
);
|
|
1729
1885
|
var syncIn = (hostRepoDir, handle) => Effect.gen(function* () {
|
|
1886
|
+
const protectedRunnerPath = `${CONFIG_DIR}/${RUNNER_DIR}`;
|
|
1887
|
+
const runnerObjects = yield* execHostGit3(
|
|
1888
|
+
["rev-list", "--objects", "--all", "--", protectedRunnerPath],
|
|
1889
|
+
hostRepoDir
|
|
1890
|
+
);
|
|
1891
|
+
if (runnerObjects.trim().length > 0) {
|
|
1892
|
+
return yield* Effect.fail(
|
|
1893
|
+
new SyncError({
|
|
1894
|
+
message: `Refusing to copy repository history into the sandbox because it contains protected repository runner files under ${protectedRunnerPath}. Remove those files from all refs before retrying.`
|
|
1895
|
+
})
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1730
1898
|
const branch = (yield* execHostGit3(
|
|
1731
1899
|
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
1732
1900
|
hostRepoDir
|
|
@@ -1823,12 +1991,33 @@ var startNoSandbox = (options) => Effect.tryPromise({
|
|
|
1823
1991
|
);
|
|
1824
1992
|
var startBindMountSandbox = (options) => Effect.tryPromise({
|
|
1825
1993
|
try: () => {
|
|
1994
|
+
const usesRunnerMask = options.worktreeOrRepoPath === options.hostRepoDir && existsSync(join(options.hostRepoDir, CONFIG_DIR, RUNNER_DIR)) && existsSync(
|
|
1995
|
+
join(options.hostRepoDir, CONFIG_DIR, RUNNER_SANDBOX_MASK_DIR)
|
|
1996
|
+
);
|
|
1997
|
+
if (usesRunnerMask) {
|
|
1998
|
+
assertSafeRepositoryRunnerDirectories(options.hostRepoDir);
|
|
1999
|
+
}
|
|
1826
2000
|
const rawMounts = [
|
|
1827
2001
|
{
|
|
1828
2002
|
hostPath: options.worktreeOrRepoPath,
|
|
1829
2003
|
sandboxPath: options.repoDir
|
|
1830
2004
|
},
|
|
1831
|
-
...options.gitMounts
|
|
2005
|
+
...options.gitMounts,
|
|
2006
|
+
...usesRunnerMask ? [
|
|
2007
|
+
{
|
|
2008
|
+
hostPath: join(
|
|
2009
|
+
options.hostRepoDir,
|
|
2010
|
+
CONFIG_DIR,
|
|
2011
|
+
RUNNER_SANDBOX_MASK_DIR
|
|
2012
|
+
),
|
|
2013
|
+
sandboxPath: posix.join(
|
|
2014
|
+
options.repoDir,
|
|
2015
|
+
CONFIG_DIR,
|
|
2016
|
+
RUNNER_DIR
|
|
2017
|
+
),
|
|
2018
|
+
readonly: true
|
|
2019
|
+
}
|
|
2020
|
+
] : []
|
|
1832
2021
|
];
|
|
1833
2022
|
const mounts = normalizeMounts(
|
|
1834
2023
|
rawMounts,
|
|
@@ -1895,6 +2084,7 @@ var startIsolatedSandbox = (options) => Effect.gen(function* () {
|
|
|
1895
2084
|
let hostPath;
|
|
1896
2085
|
let sandboxPath;
|
|
1897
2086
|
try {
|
|
2087
|
+
assertExcludesRepositoryRunner(relativePath);
|
|
1898
2088
|
hostPath = resolveSafeRelativePath(
|
|
1899
2089
|
options.sourceRepoDir ?? options.hostRepoDir,
|
|
1900
2090
|
relativePath,
|
|
@@ -2615,6 +2805,6 @@ var registerShutdown = (teardown) => {
|
|
|
2615
2805
|
};
|
|
2616
2806
|
};
|
|
2617
2807
|
|
|
2618
|
-
export { AgentError, AgentIdleTimeoutError, CLI_NAME, CONFIG_DIR, ClackDisplay, ConfigDirError, CwdError, Display, DockerError, ExecHostError, FileDisplay, InitError, LOCKS_DIR, LOGS_DIR, PRODUCT_NAME, PromptError, PromptExpansionTimeoutError, SANDBOX_REPO_DIR, SandboxConfig, SandboxFactory, SessionCaptureError, SilentDisplay, WorktreeDockerSandboxFactory, assertIsolatedWorkspaceMounts, assertNoSymlinkComponents, copyToWorktree, create, defaultImageName, formatVolumeMount, generateTempBranchName, getCurrentBranch, hasUncommittedChanges, makeSandboxFromHandle, patchGitMountsForWindows, processFileMountParents, pruneStale, registerShutdown, remove, resolveCwd, resolveGitMounts, resolveUserMounts, runHostHooks, startSandbox, syncOut, withSandboxLifecycle };
|
|
2619
|
-
//# sourceMappingURL=chunk-
|
|
2620
|
-
//# sourceMappingURL=chunk-
|
|
2808
|
+
export { ACTIVATION_LABEL, AgentError, AgentIdleTimeoutError, CLI_NAME, CONFIG_DIR, ClackDisplay, ConfigDirError, CwdError, Display, DockerError, ExecHostError, FileDisplay, InitError, LOCKS_DIR, LOGS_DIR, PRODUCT_NAME, PromptError, PromptExpansionTimeoutError, RUNNER_DIR, RUNNER_SANDBOX_MASK_DIR, SANDBOX_REPO_DIR, SandboxConfig, SandboxFactory, SessionCaptureError, SilentDisplay, WorktreeDockerSandboxFactory, assertIsolatedWorkspaceMounts, assertNoSymlinkComponents, assertProtectedDirectoryIdentities, copyToWorktree, create, defaultImageName, formatVolumeMount, generateTempBranchName, getCurrentBranch, hasUncommittedChanges, makeSandboxFromHandle, patchGitMountsForWindows, processFileMountParents, pruneStale, registerShutdown, remove, repositoryRunnerEnvironment, resolveCwd, resolveGitMounts, resolveUserMounts, runHostHooks, startSandbox, syncOut, withSandboxLifecycle };
|
|
2809
|
+
//# sourceMappingURL=chunk-FDYOTN55.js.map
|
|
2810
|
+
//# sourceMappingURL=chunk-FDYOTN55.js.map
|