@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 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. When you select Codex, it also asks
38
- whether to sign in with ChatGPT or use an API key. Start with the `blank`
39
- template. Put any requested credentials in `.shipyard/.env`, then write one
40
- concrete task in `.shipyard/prompt.md`.
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
- Inspect the generated branch strategy and start from a clean working tree. A
57
- completion marker means the agent stopped; inspect its commits and run the
58
- repository's checks before keeping the result.
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
- The generated `.shipyard/main.ts` or `.shipyard/main.mts` is ordinary
63
- TypeScript:
78
+ ## Repository runner for GitHub Issues
64
79
 
65
- ```ts
66
- import { CODEX_MODELS, codex, run } from "@snappedly-tools/shipyard";
67
- import { docker } from "@snappedly-tools/shipyard/sandboxes/docker";
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
- const result = await run({
70
- agent: codex(CODEX_MODELS.routine),
71
- sandbox: docker(),
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
- console.log(result.commits);
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
- Use a dedicated branch when the scaffold has already been committed:
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
- ```ts
81
- const result = await run({
82
- agent: codex(CODEX_MODELS.routine),
83
- sandbox: docker(),
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
- Shipyard supports Codex and Claude Code agents, with Docker, Vercel Sandbox,
92
- and no-sandbox providers. `shipyard init` scaffolds Docker; configure Vercel
93
- Sandbox or no-sandbox mode through the JavaScript interface.
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-2GT3OPO5.js';
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-VDHZO5PP.js.map
113
- //# sourceMappingURL=chunk-VDHZO5PP.js.map
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":"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","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(assertSafeGitWorktreePath),
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-2GT3OPO5.js.map
2620
- //# sourceMappingURL=chunk-2GT3OPO5.js.map
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