gdkbox 0.1.8 → 0.1.12

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1c7b818ffd457d8f32f3d80073585222be0140ba3ae5c32a76b587bae5a98f5
4
- data.tar.gz: 35b56df278015bbf0ad44cf175665c481cddd3939bc2130a5b72d193aec44c90
3
+ metadata.gz: b71b4f7e72836332b32feca0a6e3e0c485f8255fcfb0f1dc93c7792c3f6fc5ab
4
+ data.tar.gz: 66e0331f211fd1df2b67694e42e65d8971502615b42c61093f86a3a465e06425
5
5
  SHA512:
6
- metadata.gz: d0fd71a1460e658cd975298666fc7afaa1110a2b154a0da005bc80c7c4e9bdb21545ab544091c5a2f2773eab48e2e9520311e24b39c16962a7ae2416b9024bcf
7
- data.tar.gz: 44638a681108d58ddafdb1a9d4703f5a1f84cd6da764dae608d25ead66a3ebba8f4bb9057d57c38176ecd4f3c49e5c6809636c159c5c8ebbd0803c4b0ff2415d
6
+ metadata.gz: b70d9d914b6ce31f0a644cae9260a98adb3540b0d76555a1ed4e72fc423b9602deb84b338b1b91cf29bf455546b248e875dbfa2e7fdc5dafc9b37322cb2d58be
7
+ data.tar.gz: 73ebbfbfb11732b3d5fb9f40ea0d795a846c61ccb24d513c51684542424d2d78a7eef919e03d5d2007f64df3c3f0704cb75f07dbc3e7b494f90f9ae892daec43
data/README.md CHANGED
@@ -90,6 +90,9 @@ Or run it straight from the checkout without installing:
90
90
  | `gdkbox claude NAME` | Install Claude Code inside the box. |
91
91
  | `gdkbox set-key NAME` | Seed/rotate the Anthropic API key in the box for unattended dispatch. |
92
92
  | `gdkbox set-host` | Add the `gdk.local` entry to your `/etc/hosts` so box web UIs resolve (`--remove` to undo). Also runs during `up`. |
93
+ | `gdkbox set-git NAME` | Seed your git identity (`user.name`/`user.email`) into the box so `git commit` works. Also runs during `up`. |
94
+ | `gdkbox set-remote NAME [REMOTE]` | Point the box's GitLab checkout at a different remote (URL or `namespace/project`, e.g. `gitlab-org/gitlab`); default from config.yml. |
95
+ | `gdkbox hydrate NAME` | Backfill the box's treeless GitLab clone so deep rebases/blame/bisect work (`--trees` for a much smaller trees-only fetch). |
93
96
  | `gdkbox start NAME` | Start a stopped box (and re-enable SSH). |
94
97
  | `gdkbox stop NAME` | Stop a running box. |
95
98
  | `gdkbox rm NAME` | Remove a box: container, metadata, and SSH entry. |
@@ -110,6 +113,7 @@ Or run it straight from the checkout without installing:
110
113
  | `--harness ID` | `claude` (or config.yml) | Agent harness to install: claude, codex, opencode, pi. |
111
114
  | `--no-agent` | (agent installed) | Skip installing the agent harness. |
112
115
  | `--skill NAME [NAME...]` | (none) | Seed skill(s) into the box at creation for dispatched agents. |
116
+ | `--gitlab-remote REMOTE` | config.yml, else image default | Point the GitLab checkout at this remote (URL or `namespace/project`); the image ships pointing at the community mirror. |
113
117
 
114
118
  ## Typical workflow
115
119
 
@@ -133,6 +137,23 @@ gdkbox stop demo
133
137
  gdkbox rm demo
134
138
  ```
135
139
 
140
+ ### Working with git history in a box
141
+
142
+ The image's GitLab checkout is a **treeless clone** (`--filter=tree:0`, ~550 MB
143
+ instead of many GB). Day-to-day work near `master` is fine, but operations that
144
+ span a large history gap — rebasing a branch hundreds of commits behind,
145
+ `git blame` or `git bisect` across old ranges — trigger storms of lazy object
146
+ fetches and fail with errors like `could not fetch <sha> from promisor remote`
147
+ (not merge conflicts, even when a rebase reports failure). Either keep branches
148
+ current and do deep-history work elsewhere, or hydrate the box once:
149
+
150
+ ```sh
151
+ gdkbox hydrate demo --trees # fetch missing trees only: much smaller, usually
152
+ # enough for deep rebases (blobs stay lazy)
153
+ gdkbox hydrate demo # full backfill: several GB, then the clone
154
+ # behaves like a normal full clone
155
+ ```
156
+
136
157
  ## Orchestrating a fleet of agents
137
158
 
138
159
  The end goal of `gdkbox` is to back an **orchestrator** that runs a pool of
data/lib/gdkbox/box.rb CHANGED
@@ -137,6 +137,15 @@ module GDKBox
137
137
  )
138
138
  @store.save(@data)
139
139
 
140
+ # Best-effort: a host without a git identity (or a transient exec
141
+ # failure) should not abort the box; `gdkbox set-git` can seed it later.
142
+ begin
143
+ git_name, git_email = host_git_identity
144
+ provisioner.setup_git_identity(cname, name: git_name, email: git_email) if git_name || git_email
145
+ rescue StandardError
146
+ nil
147
+ end
148
+
140
149
  provisioner.setup_agent(cname, harness) if install_agent
141
150
  @data["agent_installed"] = install_agent
142
151
 
@@ -166,6 +175,75 @@ module GDKBox
166
175
  @store.delete(name)
167
176
  end
168
177
 
178
+ # The full git URL for a remote given as a URL or a "namespace/project"
179
+ # shorthand (expanded against gitlab.com over SSH, so pushes ride the
180
+ # forwarded agent).
181
+ def self.expand_remote(token)
182
+ return token if token.include?("://") || token.include?(":")
183
+
184
+ "git@gitlab.com:#{token}.git"
185
+ end
186
+
187
+ # The host an SSH-style git URL connects to, or nil for non-SSH remotes
188
+ # (https needs no SSH client preparation).
189
+ def self.remote_ssh_host(url)
190
+ if (m = url.match(%r{\Assh://(?:[^@/]+@)?([^:/]+)}))
191
+ m[1]
192
+ elsif (m = url.match(/\A(?:[^@:\/]+@)([^:]+):/))
193
+ m[1]
194
+ end
195
+ end
196
+
197
+ # Point the box's GitLab checkout at a different remote (URL or
198
+ # "namespace/project"). SSH remotes get the box's SSH client prepared
199
+ # first (host key acceptance + connection multiplexing) so the follow-up
200
+ # fetch — and later lazy fetches from the treeless clone — succeed.
201
+ # Returns the URL that origin now uses.
202
+ def set_gitlab_remote!(remote)
203
+ url = self.class.expand_remote(remote)
204
+ provisioner = Provisioner.new(docker: @docker, config: @config)
205
+ if (host = self.class.remote_ssh_host(url))
206
+ provisioner.setup_git_ssh(container_name, host)
207
+ end
208
+ provisioner.setup_gitlab_remote(container_name, url)
209
+ url
210
+ end
211
+
212
+ # Backfill the box's treeless GitLab clone so deep-history operations
213
+ # (old rebases, blame, bisect) work. trees: true fetches only the missing
214
+ # trees (much smaller; file contents stay lazy). Prepares the SSH client
215
+ # first when origin is an SSH remote — a refetch without multiplexing
216
+ # would hit the same throttling the lazy fetches do. Streams git's
217
+ # progress lines to the block.
218
+ def hydrate!(trees: false, &progress)
219
+ provisioner = Provisioner.new(docker: @docker, config: @config)
220
+ origin = @docker.exec(
221
+ container_name, "git remote get-url origin",
222
+ user: @config.ssh_user, workdir: @config.gitlab_checkout_path
223
+ ).stdout.strip
224
+ if (host = self.class.remote_ssh_host(origin))
225
+ provisioner.setup_git_ssh(container_name, host)
226
+ end
227
+ provisioner.hydrate(container_name, trees: trees, &progress)
228
+ end
229
+
230
+ # Seed a git identity into the box so `git commit` works there. Falls back
231
+ # to the host's `git config` when name/email are not given; raises when
232
+ # neither source has anything to seed. Returns the [name, email] seeded.
233
+ def set_git_identity!(git_name: nil, git_email: nil)
234
+ host_name, host_email = host_git_identity
235
+ git_name ||= host_name
236
+ git_email ||= host_email
237
+ if git_name.nil? && git_email.nil?
238
+ raise Error, "No git identity found. Pass --name/--email or set " \
239
+ "`git config --global user.name/user.email` on the host."
240
+ end
241
+
242
+ Provisioner.new(docker: @docker, config: @config)
243
+ .setup_git_identity(container_name, name: git_name, email: git_email)
244
+ [git_name, git_email]
245
+ end
246
+
169
247
  # (Re)install this box's agent harness inside the container.
170
248
  def install_agent!
171
249
  Provisioner.new(docker: @docker, config: @config).setup_agent(container_name, harness)
@@ -267,6 +345,15 @@ module GDKBox
267
345
 
268
346
  private
269
347
 
348
+ # The host's git identity as [name, email], each nil when unset.
349
+ def host_git_identity
350
+ read = lambda do |key|
351
+ value = @shell.run("git", "config", "--get", key).stdout.strip
352
+ value.empty? ? nil : value
353
+ end
354
+ [read.call("user.name"), read.call("user.email")]
355
+ end
356
+
270
357
  # Choose free host ports and persist a preliminary record claiming them,
271
358
  # all while holding an exclusive lock so concurrent `create!` calls in
272
359
  # separate processes serialize and cannot pick the same ports. Returns the
data/lib/gdkbox/cli.rb CHANGED
@@ -37,6 +37,9 @@ module GDKBox
37
37
  desc: "Skill name(s) or path(s) to install into the box for dispatched agents"
38
38
  option :default_skills, type: :boolean, default: true,
39
39
  desc: "Also install the default skills from ~/.gdkbox/config.yml"
40
+ option :gitlab_remote, type: :string,
41
+ desc: "Point the GitLab checkout at this remote (URL or namespace/project; " \
42
+ "default from config.yml, else the image's community mirror)"
40
43
  def up(name)
41
44
  ensure_docker!
42
45
  box = build_box(name)
@@ -55,6 +58,7 @@ module GDKBox
55
58
  )
56
59
  rewrite_ssh_config
57
60
  seed_skills(box)
61
+ repoint_gitlab_remote(box)
58
62
 
59
63
  if options[:json]
60
64
  puts JSON.generate(box.summary)
@@ -236,6 +240,90 @@ module GDKBox
236
240
  end
237
241
  map "set-key" => :set_key
238
242
 
243
+ desc "set-git NAME", "Seed your git identity (user.name/email) into the box"
244
+ long_desc <<~DESC
245
+ Copies your git identity into the box's global git config so `git
246
+ commit` works there. Defaults to the host's `git config user.name` and
247
+ `user.email`; override either with --name/--email. Runs automatically
248
+ during `gdkbox up`, so this is mainly for boxes created before that or
249
+ for changing the identity later.
250
+
251
+ Pushing uses your host's ssh-agent, forwarded into interactive sessions
252
+ (`gdkbox ssh` / VS Code) by the generated SSH config — no keys or tokens
253
+ are stored in the box.
254
+ DESC
255
+ option :name, type: :string, desc: "git user.name to seed (defaults to the host's)"
256
+ option :email, type: :string, desc: "git user.email to seed (defaults to the host's)"
257
+ def set_git(name)
258
+ box = load_box!(name)
259
+ git_name, git_email = box.set_git_identity!(
260
+ git_name: options[:name], git_email: options[:email]
261
+ )
262
+ say "Seeded git identity into '#{name}': #{[git_name, git_email].compact.join(' <')}#{'>' if git_email}", :green
263
+ end
264
+ map "set-git" => :set_git
265
+
266
+ desc "set-remote NAME [REMOTE]", "Point the box's GitLab checkout at a different remote"
267
+ long_desc <<~DESC
268
+ The GDK-in-a-box image clones GitLab from the community mirror
269
+ (gitlab-community/gitlab-org/gitlab). This repoints the checkout's
270
+ origin — pass a full git URL or a namespace/project shorthand (expanded
271
+ to git@gitlab.com:namespace/project.git), or set `gitlab_remote:` in
272
+ ~/.gdkbox/config.yml and omit REMOTE. Runs `git remote set-url` plus a
273
+ fetch inside the box; `gdkbox up` does the same automatically when a
274
+ remote is configured. Pushing uses your forwarded ssh-agent and your own
275
+ permissions — no credentials are stored in the box.
276
+ DESC
277
+ def set_remote(name, remote = nil)
278
+ box = load_box!(name)
279
+ remote ||= config.default_gitlab_remote
280
+ unless remote
281
+ raise Error, "No remote. Pass REMOTE (URL or namespace/project) " \
282
+ "or set `gitlab_remote:` in ~/.gdkbox/config.yml."
283
+ end
284
+
285
+ say "Pointing '#{name}' GitLab checkout at #{Box.expand_remote(remote)} (fetching)...", :green
286
+ url = box.set_gitlab_remote!(remote)
287
+ say "origin now #{url}.", :green
288
+ end
289
+ map "set-remote" => :set_remote
290
+
291
+ desc "hydrate NAME", "Backfill the box's treeless GitLab clone (deep rebases, blame, bisect)"
292
+ long_desc <<~DESC
293
+ The GDK-in-a-box image clones GitLab treeless (--filter=tree:0), so
294
+ operations spanning a large history gap — rebasing a branch far behind
295
+ master, blame or bisect across old ranges — storm the network with lazy
296
+ fetches and fail. Hydrating backfills the missing objects once.
297
+
298
+ By default everything is fetched and the clone becomes a normal full
299
+ clone (a large one-time download, likely several GB). With --trees only
300
+ the missing trees are fetched — a fraction of the size, and usually
301
+ enough for deep rebases; file contents are still fetched lazily as
302
+ needed. Progress is streamed live.
303
+ DESC
304
+ option :trees, type: :boolean, default: false,
305
+ desc: "Fetch missing trees only (much smaller; blobs stay lazy)"
306
+ option :force, type: :boolean, default: false, desc: "Skip confirmation"
307
+ def hydrate(name)
308
+ box = load_box!(name)
309
+ unless options[:force]
310
+ estimate = options[:trees] ? "a sizeable download" : "a large download, likely several GB"
311
+ return unless yes?("Hydrate '#{name}' (#{estimate})? [y/N]")
312
+ end
313
+
314
+ say "Hydrating '#{name}' (#{options[:trees] ? 'trees only' : 'full'})...", :green
315
+ tty = $stderr.tty?
316
+ box.hydrate!(trees: options[:trees]) do |line|
317
+ if tty
318
+ $stderr.print("\r\e[K#{line}")
319
+ elsif !line.include?("%") # skip per-percent spam when piped
320
+ $stderr.puts(line)
321
+ end
322
+ end
323
+ $stderr.print("\n") if tty
324
+ say "Done. '#{name}' can now work across the full history.", :green
325
+ end
326
+
239
327
  desc "set-host", "Add (default) or remove the gdk.local entry in /etc/hosts"
240
328
  long_desc <<~DESC
241
329
  GDK generates URLs and redirects that use the `gdk.local` hostname, so
@@ -412,6 +500,24 @@ module GDKBox
412
500
  end
413
501
  end
414
502
 
503
+ # Point the new box's GitLab checkout at the configured remote, when one
504
+ # is set (--gitlab-remote wins over config.yml; no setting keeps the
505
+ # image's community mirror). Best-effort like seed_skills: the box is
506
+ # already up, so a bad remote should warn, not abort. Muted in JSON mode.
507
+ def repoint_gitlab_remote(box)
508
+ remote = options[:gitlab_remote] || config.default_gitlab_remote
509
+ return unless remote
510
+
511
+ url = box.set_gitlab_remote!(remote)
512
+ say "Pointed the GitLab checkout at #{url}.", :green unless options[:json]
513
+ rescue StandardError => e
514
+ # Warn on stderr even in --json mode: stdout stays clean JSON, but a
515
+ # silently failed repoint costs the user a debugging session later
516
+ # (the box looks fine until the first fetch).
517
+ warn "gdkbox: could not repoint the GitLab checkout: #{e.message}"
518
+ warn "gdkbox: retry later with: gdkbox set-remote #{box.name} #{remote}"
519
+ end
520
+
415
521
  # Ensure gdk.local resolves on the host so the box's web UI is reachable
416
522
  # (GDK redirects to that hostname). Adding the entry needs sudo, so warn
417
523
  # before any password prompt appears. Failures are non-fatal: the box is
@@ -11,7 +11,8 @@ module GDKBox
11
11
  SHELLS = %w[bash zsh].freeze
12
12
 
13
13
  # Subcommands whose first positional argument is an existing box.
14
- BOX_COMMANDS = %w[status dispatch ssh code install-agent set-key start stop rm add-skill].freeze
14
+ BOX_COMMANDS = %w[status dispatch ssh code install-agent set-key set-git set-remote hydrate
15
+ start stop rm add-skill].freeze
15
16
 
16
17
  def initialize(cli_class = CLI)
17
18
  @cli = cli_class
data/lib/gdkbox/config.rb CHANGED
@@ -14,6 +14,7 @@ module GDKBox
14
14
  #
15
15
  # # ~/.gdkbox/config.yml
16
16
  # image: registry.example.com/my-gdk:latest # default image for `up`
17
+ # gitlab_remote: gitlab-org/gitlab # repoint the GitLab checkout
17
18
  # skills: # transferred into every box
18
19
  # - gdkbox-fleet
19
20
  # - code-history
@@ -102,6 +103,19 @@ module GDKBox
102
103
  key.empty? ? Harness.default : key
103
104
  end
104
105
 
106
+ # The git remote to point each box's GitLab checkout at, from config.yml
107
+ # (`gitlab_remote:`), or nil to keep the image's default (the community
108
+ # mirror). A URL or a "namespace/project" shorthand.
109
+ def default_gitlab_remote
110
+ remote = file_settings["gitlab_remote"].to_s.strip
111
+ remote.empty? ? nil : remote
112
+ end
113
+
114
+ # Where the GitLab repository lives inside the box.
115
+ def gitlab_checkout_path
116
+ File.join(remote_path, "gitlab")
117
+ end
118
+
105
119
  # An API key read from config.yml under `config_key` (e.g.
106
120
  # "anthropic_api_key", "openai_api_key"), or nil. This is a plaintext
107
121
  # secret on disk, so it is the lowest-priority source (a flag or the
data/lib/gdkbox/docker.rb CHANGED
@@ -223,6 +223,21 @@ module GDKBox
223
223
  check ? @shell.run!(*cmd, input: input) : @shell.run(*cmd, input: input)
224
224
  end
225
225
 
226
+ # Like #exec, but for long-running scripts whose output should be seen
227
+ # live (e.g. a multi-GB git fetch): runs under a PTY and yields each
228
+ # output line as it appears. Raises CommandError on a non-zero exit.
229
+ def exec_stream(name, script, user: nil, workdir: nil, env: {}, &block)
230
+ cmd = ["docker", "exec"]
231
+ cmd.push("-u", user) if user
232
+ cmd.push("-w", workdir) if workdir
233
+ env.each { |key, value| cmd.push("-e", "#{key}=#{value}") }
234
+ cmd.push(name, "bash", "-lc", script)
235
+ result = @shell.stream_tty(*cmd, &block)
236
+ raise CommandError.new(cmd, result.status, "") unless result.success?
237
+
238
+ result
239
+ end
240
+
226
241
  # Copy a host path into a container at dest (a full destination path).
227
242
  def cp_into(name, src, dest)
228
243
  @shell.run!("docker", "cp", src, "#{name}:#{dest}")
@@ -131,6 +131,79 @@ module GDKBox
131
131
  done
132
132
  BASH
133
133
 
134
+ # Seeds the GDK user's global git identity so `git commit` works inside
135
+ # the box. Values arrive via the environment (dodging shell quoting) and
136
+ # blanks are skipped, so a partial host config seeds what it can.
137
+ GIT_IDENTITY_SETUP = <<~'BASH'
138
+ set -e
139
+ [ -n "$GDKBOX_GIT_NAME" ] && git config --global user.name "$GDKBOX_GIT_NAME"
140
+ [ -n "$GDKBOX_GIT_EMAIL" ] && git config --global user.email "$GDKBOX_GIT_EMAIL"
141
+ true
142
+ BASH
143
+
144
+ # Prepares the box's SSH client for real git traffic against a remote
145
+ # host (issue #6, hit when a gitlab_remote is configured):
146
+ #
147
+ # - `StrictHostKeyChecking accept-new`: a fresh box has an empty
148
+ # known_hosts, so the first fetch would die on host key verification.
149
+ # - Connection multiplexing: lazy fetches from the treeless clone open
150
+ # SSH connections in bursts; gitlab.com throttles that, which surfaces
151
+ # as a misleading "Permission denied (publickey)". One shared control
152
+ # connection absorbs the burst.
153
+ #
154
+ # The stanza is tagged per host, so re-runs are no-ops and different
155
+ # remotes each get their own block.
156
+ GIT_SSH_SETUP = <<~'BASH'
157
+ set -e
158
+ mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
159
+ config="$HOME/.ssh/config"
160
+ touch "$config" && chmod 600 "$config"
161
+ if ! grep -qF "gdkbox:git-ssh $GDKBOX_GIT_HOST" "$config"; then
162
+ cat >> "$config" <<EOF
163
+
164
+ # gdkbox:git-ssh $GDKBOX_GIT_HOST
165
+ Host $GDKBOX_GIT_HOST
166
+ StrictHostKeyChecking accept-new
167
+ ControlMaster auto
168
+ ControlPath ~/.ssh/cm-%r@%h-%p
169
+ ControlPersist 20m
170
+ ServerAliveInterval 30
171
+ EOF
172
+ fi
173
+ BASH
174
+
175
+ # Points the GitLab checkout's origin at a different repository. The image
176
+ # hardcodes the community mirror (gitlab-community/gitlab-org/gitlab);
177
+ # team members typically want the canonical gitlab-org/gitlab. The mirror
178
+ # shares its history, so a set-url plus fetch is all it takes (the
179
+ # checkout is treeless, keeping the fetch cheap).
180
+ GITLAB_REMOTE_SETUP = <<~'BASH'
181
+ set -e
182
+ git remote set-url origin "$GDKBOX_GITLAB_REMOTE"
183
+ git fetch --quiet origin
184
+ BASH
185
+
186
+ # Backfills the treeless GitLab clone (the image clones with
187
+ # --filter=tree:0, so operations spanning a large history gap storm the
188
+ # network with lazy fetches and fail; see #7). Two levels:
189
+ #
190
+ # - GDKBOX_HYDRATE_FILTER=blob:none — fetch all missing trees, keep file
191
+ # contents lazy. A fraction of the download; enough for deep rebases,
192
+ # which mostly storm tree fetches.
193
+ # - no filter (full) — fetch everything and drop the promisor config;
194
+ # the repository behaves like a normal full clone afterwards.
195
+ HYDRATE = <<~'BASH'
196
+ set -e
197
+ if [ -n "$GDKBOX_HYDRATE_FILTER" ]; then
198
+ git config remote.origin.partialclonefilter "$GDKBOX_HYDRATE_FILTER"
199
+ git fetch --refetch --progress origin
200
+ else
201
+ git config --unset-all remote.origin.partialclonefilter || true
202
+ git fetch --refetch --progress origin
203
+ git config --unset remote.origin.promisor || true
204
+ fi
205
+ BASH
206
+
134
207
  def initialize(docker:, config:)
135
208
  @docker = docker
136
209
  @config = config
@@ -168,6 +241,46 @@ module GDKBox
168
241
  )
169
242
  end
170
243
 
244
+ def setup_git_identity(container_name, name:, email:)
245
+ @docker.exec(
246
+ container_name, GIT_IDENTITY_SETUP,
247
+ user: @config.ssh_user,
248
+ env: {
249
+ "GDKBOX_GIT_NAME" => name.to_s,
250
+ "GDKBOX_GIT_EMAIL" => email.to_s
251
+ }
252
+ )
253
+ end
254
+
255
+ # Hydrate the GitLab checkout, streaming git's progress lines to the
256
+ # block. trees: true fetches trees only (blobs stay lazy).
257
+ def hydrate(container_name, trees: false, &block)
258
+ @docker.exec_stream(
259
+ container_name, HYDRATE,
260
+ user: @config.ssh_user,
261
+ workdir: @config.gitlab_checkout_path,
262
+ env: { "GDKBOX_HYDRATE_FILTER" => (trees ? "blob:none" : "") },
263
+ &block
264
+ )
265
+ end
266
+
267
+ def setup_git_ssh(container_name, host)
268
+ @docker.exec(
269
+ container_name, GIT_SSH_SETUP,
270
+ user: @config.ssh_user,
271
+ env: { "GDKBOX_GIT_HOST" => host }
272
+ )
273
+ end
274
+
275
+ def setup_gitlab_remote(container_name, url)
276
+ @docker.exec(
277
+ container_name, GITLAB_REMOTE_SETUP,
278
+ user: @config.ssh_user,
279
+ workdir: @config.gitlab_checkout_path,
280
+ env: { "GDKBOX_GITLAB_REMOTE" => url }
281
+ )
282
+ end
283
+
171
284
  def setup_api_key(container_name, api_key, key_env)
172
285
  @docker.exec(
173
286
  container_name, API_KEY_SETUP,
@@ -32,6 +32,11 @@ module GDKBox
32
32
  lines << " User #{box['ssh_user']}"
33
33
  lines << " IdentityFile #{@config.private_key_path}"
34
34
  lines << " IdentitiesOnly yes"
35
+ # Forward the host's ssh-agent so `git push` inside the box uses the
36
+ # user's own keys — which never enter the container, and are only
37
+ # reachable while a human session is connected (dispatched agents run
38
+ # via `docker exec` and never see the agent).
39
+ lines << " ForwardAgent yes"
35
40
  lines << " StrictHostKeyChecking no"
36
41
  lines << " UserKnownHostsFile /dev/null"
37
42
  lines << ""
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GDKBox
4
- VERSION = "0.1.8"
4
+ VERSION = "0.1.12"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gdkbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - jotolo