gdkbox 0.1.19 → 0.1.21
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 +4 -4
- data/README.md +42 -7
- data/lib/gdkbox/box.rb +22 -8
- data/lib/gdkbox/cli.rb +12 -2
- data/lib/gdkbox/harness.rb +56 -10
- data/lib/gdkbox/version.rb +1 -1
- data/skills/gdkbox-fleet/SKILL.md +58 -10
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed8db8e33eb456eab81c33ec420fccbb86294d1569373da95d4eab9aebf1ee79
|
|
4
|
+
data.tar.gz: 23812d9d52938ddbdbd5613f412bdb5e35ed588ec177c5f1ad8acfbfa76fe214
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 588e9c4a5beaa9f7ddcfd0cdfe3c897de80b230646a24b1c580bdb1cf008ec9aa86531d5f5a03d2234bf128918dd821f02dba7b41a79470b172d5607b2abd8c2
|
|
7
|
+
data.tar.gz: d1369eb4dadb712a8e2cacef481c55475e4239f447daad5ef952bdb956d5651ca13ac7fe80a29e8390ac609228514ed3bbcac3cf659f1c050bcc22ce8d679ca5
|
data/README.md
CHANGED
|
@@ -122,6 +122,8 @@ Or run it straight from the checkout without installing:
|
|
|
122
122
|
| `--ttl SECS` | no expiry | Lease duration for `--owner`'s claim. |
|
|
123
123
|
| `--docker` | off | Mount the host Docker socket + per-box network so docker-executor CI jobs work. **Grants the box root-equivalent host access** — see "Docker inside a box". |
|
|
124
124
|
| `--publish PORT [PORT...]` | (none) | Publish extra container ports on `127.0.0.1`, same number on both sides (e.g. AI Gateway `5052`, Duo Workflow `50052`). |
|
|
125
|
+
| `--key-env NAME` | the harness's | Env var this box's API key is seeded under (e.g. `GITLAB_TOKEN`); see "opencode with GitLab Duo". |
|
|
126
|
+
| `--model ID` | the harness's own | Model the box's agent runs with on `dispatch` (claude, codex, opencode; e.g. `gitlab/duo-chat-sonnet-4-5`). |
|
|
125
127
|
|
|
126
128
|
## Typical workflow
|
|
127
129
|
|
|
@@ -373,8 +375,8 @@ harness the box was created with.
|
|
|
373
375
|
| --- | --- | --- | --- | --- |
|
|
374
376
|
| `claude` | Claude Code | `claude -p` | `ANTHROPIC_API_KEY` | `~/.claude/skills` |
|
|
375
377
|
| `codex` | OpenAI Codex | `codex exec` | `OPENAI_API_KEY` | `~/.codex/skills` |
|
|
376
|
-
| `opencode` | opencode (sst) | `opencode run` | `
|
|
377
|
-
| `pi` | pi (earendil-works) | `pi -p` | `
|
|
378
|
+
| `opencode` | opencode (sst) | `opencode run` | `GITLAB_TOKEN` | `~/.config/opencode/skills` |
|
|
379
|
+
| `pi` | pi (earendil-works) | `pi -p` | `GITLAB_TOKEN` | `~/.pi/skills` |
|
|
378
380
|
|
|
379
381
|
```sh
|
|
380
382
|
gdkbox up reviewer --harness codex # a Codex box
|
|
@@ -385,8 +387,39 @@ gdkbox harnesses # list available harnesses
|
|
|
385
387
|
```
|
|
386
388
|
|
|
387
389
|
Skills use the same `SKILL.md` format across all four, so a skill installed by
|
|
388
|
-
`add-skill`/`--skill` works regardless of harness. The key env
|
|
389
|
-
|
|
390
|
+
`add-skill`/`--skill` works regardless of harness. The key env is a per-harness
|
|
391
|
+
default (claude → `ANTHROPIC_API_KEY`, codex → `OPENAI_API_KEY`, opencode and
|
|
392
|
+
pi → `GITLAB_TOKEN`): point a box at another provider with
|
|
393
|
+
`gdkbox up --key-env NAME`.
|
|
394
|
+
`gdkbox up --model ID` pins the model a box's agent is dispatched with
|
|
395
|
+
(claude, codex, and opencode; not pi yet). Both are fixed when the box is
|
|
396
|
+
created.
|
|
397
|
+
|
|
398
|
+
### opencode with GitLab Duo
|
|
399
|
+
|
|
400
|
+
The `opencode` harness defaults to opencode's GitLab Duo provider, which reads
|
|
401
|
+
a personal access token from `GITLAB_TOKEN`. Pin a Duo model with `--model`:
|
|
402
|
+
|
|
403
|
+
```sh
|
|
404
|
+
GITLAB_TOKEN=<token> gdkbox up duo --harness opencode --model gitlab/duo-chat-sonnet-4-5
|
|
405
|
+
gdkbox dispatch duo --task "review the diff"
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
- The `duo-chat-*` models are GitLab Duo Agentic Chat and work with the token
|
|
409
|
+
alone. The `duo-workflow-*` models need a login stored with opencode's
|
|
410
|
+
`/connect`, so they are not available this way.
|
|
411
|
+
- GitLab Duo needs a Premium or Ultimate subscription with the Duo Agent
|
|
412
|
+
Platform turned on.
|
|
413
|
+
- Without `--model`, opencode picks its own default model.
|
|
414
|
+
- For another provider, seed its key instead, e.g.
|
|
415
|
+
`gdkbox up NAME --harness opencode --key-env OPENAI_API_KEY`.
|
|
416
|
+
|
|
417
|
+
> **The token acts as you.** opencode needs the `api` scope, which lets the
|
|
418
|
+
> agent read and write anything you can on GitLab. That is what lets it post
|
|
419
|
+
> MR review comments, but it also means a prompt injection in an issue, MR, or
|
|
420
|
+
> file the agent reads can act with your permissions. Use a dedicated,
|
|
421
|
+
> short-lived token. `gdkbox up --harness opencode` picks up `$GITLAB_TOKEN`
|
|
422
|
+
> from your shell, so check what that variable holds first.
|
|
390
423
|
|
|
391
424
|
## Shell completion
|
|
392
425
|
|
|
@@ -439,15 +472,17 @@ skills:
|
|
|
439
472
|
# - ./path/to/a/local/skill
|
|
440
473
|
|
|
441
474
|
# API keys seeded into boxes for unattended `gdkbox dispatch`, per provider.
|
|
442
|
-
# anthropic_api_key: sk-ant-... # used by the claude
|
|
443
|
-
# openai_api_key: sk-... # used by the codex
|
|
475
|
+
# anthropic_api_key: sk-ant-... # used by the claude harness
|
|
476
|
+
# openai_api_key: sk-... # used by the codex harness
|
|
477
|
+
# gitlab_token: <personal access token, api scope> # used by the opencode / pi harnesses
|
|
444
478
|
```
|
|
445
479
|
|
|
446
480
|
`gdkbox up` installs the skills on top of any `--skill` you pass; use
|
|
447
481
|
`--no-default-skills` to skip them for one run. Precedence for the image is
|
|
448
482
|
`--image` > `$GDKBOX_IMAGE` > `config.yml` > built-in default; for the API key
|
|
449
483
|
it is `--api-key` > the harness's provider env var (e.g. `$ANTHROPIC_API_KEY`,
|
|
450
|
-
`$
|
|
484
|
+
`$GITLAB_TOKEN`, or the `--key-env` name) > `config.yml` (the env var's name
|
|
485
|
+
lowercased, e.g. `gitlab_token:`).
|
|
451
486
|
|
|
452
487
|
> **API keys in `config.yml` are plaintext secrets on disk.** Prefer the
|
|
453
488
|
> provider env var; if you do put a key in `config.yml`, keep the file mode
|
data/lib/gdkbox/box.rb
CHANGED
|
@@ -73,9 +73,16 @@ module GDKBox
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
# The agent harness this box runs (defaults to Claude for boxes created
|
|
76
|
-
# before harness support existed)
|
|
76
|
+
# before harness support existed), with the box's own key env override
|
|
77
|
+
# (`up --key-env`) applied.
|
|
77
78
|
def harness
|
|
78
|
-
Harness[(data && data["harness"]) || Harness.default]
|
|
79
|
+
Harness[(data && data["harness"]) || Harness.default].with_key_env(data && data["key_env"])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The model the box's agent is pinned to (`up --model`), or nil to let
|
|
83
|
+
# the harness pick its own default.
|
|
84
|
+
def model
|
|
85
|
+
data && data["model"]
|
|
79
86
|
end
|
|
80
87
|
|
|
81
88
|
def state
|
|
@@ -91,10 +98,12 @@ module GDKBox
|
|
|
91
98
|
# box this one is still provisioning.
|
|
92
99
|
def create!(image: nil, ssh_port: nil, web_port: nil, vite_port: nil,
|
|
93
100
|
harness: Harness.default, install_agent: true, api_key: nil,
|
|
94
|
-
claim_owner: nil, claim_ttl: nil, docker_access: false, extra_ports: []
|
|
101
|
+
claim_owner: nil, claim_ttl: nil, docker_access: false, extra_ports: [],
|
|
102
|
+
key_env: nil, model: nil)
|
|
95
103
|
raise Error, "Box '#{name}' already exists" if exists?
|
|
96
104
|
|
|
97
|
-
harness = Harness[harness]
|
|
105
|
+
harness = Harness[harness].with_key_env(key_env)
|
|
106
|
+
model = harness.check_model!(model)
|
|
98
107
|
image ||= @config.default_image
|
|
99
108
|
public_key = @ssh_key.ensure!
|
|
100
109
|
cname = @config.container_name(name)
|
|
@@ -168,6 +177,8 @@ module GDKBox
|
|
|
168
177
|
"harness" => harness.id,
|
|
169
178
|
"created_at" => Time.now.utc.iso8601
|
|
170
179
|
)
|
|
180
|
+
@data["key_env"] = harness.key_env if key_env
|
|
181
|
+
@data["model"] = model if model
|
|
171
182
|
if docker_access
|
|
172
183
|
@data["docker_access"] = true
|
|
173
184
|
@data["docker_network"] = net
|
|
@@ -493,12 +504,12 @@ module GDKBox
|
|
|
493
504
|
|
|
494
505
|
# Run this box's agent harness non-interactively and capture its output.
|
|
495
506
|
# This is the primitive an orchestrator uses to dispatch a task to a box.
|
|
496
|
-
# The task text is passed through the
|
|
497
|
-
# cannot break out of the shell command.
|
|
507
|
+
# The task text (and the pinned model, if any) is passed through the
|
|
508
|
+
# environment so arbitrary values cannot break out of the shell command.
|
|
498
509
|
def run_agent(task:, json: false, yolo: true, timeout: nil)
|
|
499
510
|
raise Error, "Box '#{name}' does not exist" unless exists?
|
|
500
511
|
|
|
501
|
-
agent = harness.agent_command(json: json, yolo: yolo)
|
|
512
|
+
agent = harness.agent_command(json: json, yolo: yolo, model: model)
|
|
502
513
|
agent = "timeout #{Integer(timeout)} #{agent}" if timeout
|
|
503
514
|
# Source the seeded API key (if any) so the agent authenticates unattended.
|
|
504
515
|
command = %([ -f "$HOME/.gdkbox/env" ] && . "$HOME/.gdkbox/env"; #{agent})
|
|
@@ -507,7 +518,7 @@ module GDKBox
|
|
|
507
518
|
container_name, command,
|
|
508
519
|
user: @config.ssh_user,
|
|
509
520
|
workdir: remote_path,
|
|
510
|
-
env: { "GDKBOX_TASK" => task },
|
|
521
|
+
env: { "GDKBOX_TASK" => task, "GDKBOX_MODEL" => model }.compact,
|
|
511
522
|
check: false
|
|
512
523
|
)
|
|
513
524
|
AgentResult.new(result.stdout, result.stderr, result.status)
|
|
@@ -515,6 +526,7 @@ module GDKBox
|
|
|
515
526
|
|
|
516
527
|
# A machine-readable summary for orchestrators (`gdkbox ls --json`).
|
|
517
528
|
def summary(state: nil)
|
|
529
|
+
harness = self.harness
|
|
518
530
|
{
|
|
519
531
|
"name" => name,
|
|
520
532
|
"state" => (state || self.state).to_s,
|
|
@@ -529,6 +541,8 @@ module GDKBox
|
|
|
529
541
|
# Back-compat: orchestrators predating multi-harness check this field.
|
|
530
542
|
"claude_installed" => (harness.id == "claude" && (data && data["agent_installed"])) || false,
|
|
531
543
|
"api_key_set" => (data && data["api_key_set"]) || false,
|
|
544
|
+
"key_env" => harness.key_env,
|
|
545
|
+
"model" => model,
|
|
532
546
|
# Vite assets: the published port (nil on boxes created before vite
|
|
533
547
|
# support) and whether the in-box alignment step succeeded.
|
|
534
548
|
"vite_port" => vite_port,
|
data/lib/gdkbox/cli.rb
CHANGED
|
@@ -54,6 +54,12 @@ module GDKBox
|
|
|
54
54
|
option :publish, type: :array, default: [],
|
|
55
55
|
desc: "Extra container port(s) to publish on 127.0.0.1, same number on both sides " \
|
|
56
56
|
"(e.g. --publish 5052 50052)"
|
|
57
|
+
option :key_env, type: :string,
|
|
58
|
+
desc: "Env var to seed the harness API key under for this box " \
|
|
59
|
+
"(e.g. GITLAB_TOKEN for opencode + GitLab Duo; default: the harness's own)"
|
|
60
|
+
option :model, type: :string,
|
|
61
|
+
desc: "Model this box's agent runs with on dispatch " \
|
|
62
|
+
"(e.g. gitlab/duo-chat-sonnet-4-5 for opencode; default: the harness's own)"
|
|
57
63
|
def up(name)
|
|
58
64
|
if options[:ttl] && options[:owner].to_s.strip.empty?
|
|
59
65
|
raise Error, "--ttl only makes sense with --owner (it is the claim's lease)."
|
|
@@ -61,7 +67,8 @@ module GDKBox
|
|
|
61
67
|
ensure_docker!
|
|
62
68
|
box = build_box(name)
|
|
63
69
|
raise Error, "Box '#{name}' already exists. Use `gdkbox rm #{name}` first." if box.exists?
|
|
64
|
-
harness = Harness[options[:harness] || config.default_harness]
|
|
70
|
+
harness = Harness[options[:harness] || config.default_harness].with_key_env(options[:key_env])
|
|
71
|
+
model = harness.check_model!(options[:model])
|
|
65
72
|
api_key = resolve_api_key(harness)
|
|
66
73
|
|
|
67
74
|
say "Spinning up GDK box '#{name}' (#{harness.summary}; first run pulls a large image)...", :green unless options[:json]
|
|
@@ -76,7 +83,9 @@ module GDKBox
|
|
|
76
83
|
claim_owner: options[:owner],
|
|
77
84
|
claim_ttl: options[:ttl],
|
|
78
85
|
docker_access: options[:docker],
|
|
79
|
-
extra_ports: parse_extra_ports(options[:publish])
|
|
86
|
+
extra_ports: parse_extra_ports(options[:publish]),
|
|
87
|
+
key_env: options[:key_env],
|
|
88
|
+
model: model
|
|
80
89
|
)
|
|
81
90
|
rewrite_ssh_config
|
|
82
91
|
seed_skills(box)
|
|
@@ -754,6 +763,7 @@ module GDKBox
|
|
|
754
763
|
say " Web UI: #{box.web_url} (http://#{HostsFile::HOSTNAME}:#{box.web_port})", :cyan
|
|
755
764
|
say " Assets: vite on port #{box.vite_port}", :cyan if box.vite_port
|
|
756
765
|
say " Agent: ssh #{box.ssh_host_alias} -t #{box.harness.bin}", :cyan
|
|
766
|
+
say " Model: #{box.model} (on dispatch)", :cyan if box.model
|
|
757
767
|
end
|
|
758
768
|
end
|
|
759
769
|
end
|
data/lib/gdkbox/harness.rb
CHANGED
|
@@ -10,6 +10,10 @@ module GDKBox
|
|
|
10
10
|
# /usr/local/bin). A harness declares the env var its provider reads for
|
|
11
11
|
# credentials and the config.yml key gdkbox looks under for a default.
|
|
12
12
|
class Harness
|
|
13
|
+
# Env var names end up in an `export NAME=...` line inside the box, so
|
|
14
|
+
# only plain shell identifiers are accepted.
|
|
15
|
+
ENV_VAR_NAME = /\A[A-Z_][A-Z0-9_]*\z/
|
|
16
|
+
|
|
13
17
|
@registry = {}
|
|
14
18
|
|
|
15
19
|
class << self
|
|
@@ -43,8 +47,9 @@ module GDKBox
|
|
|
43
47
|
:summary, :skills_subdir
|
|
44
48
|
|
|
45
49
|
def initialize(id:, bin:, key_env:, config_key:, summary:, run:, skills_subdir:,
|
|
46
|
-
npm_package: nil, npm_flags: nil)
|
|
50
|
+
npm_package: nil, npm_flags: nil, model_flag: nil)
|
|
47
51
|
@id = id
|
|
52
|
+
@model_flag = model_flag
|
|
48
53
|
@npm_package = npm_package
|
|
49
54
|
@npm_flags = npm_flags
|
|
50
55
|
@bin = bin
|
|
@@ -55,15 +60,45 @@ module GDKBox
|
|
|
55
60
|
@skills_subdir = skills_subdir
|
|
56
61
|
end
|
|
57
62
|
|
|
63
|
+
# A copy of this harness that seeds its key under `key_env` instead
|
|
64
|
+
# (`up --key-env`), or self when blank. The config.yml fallback key
|
|
65
|
+
# follows the env var (GITLAB_TOKEN -> gitlab_token) so the override stays
|
|
66
|
+
# self-consistent.
|
|
67
|
+
def with_key_env(key_env)
|
|
68
|
+
key_env = key_env.to_s.strip
|
|
69
|
+
return self if key_env.empty?
|
|
70
|
+
unless ENV_VAR_NAME.match?(key_env)
|
|
71
|
+
raise Error, "Invalid --key-env '#{key_env}': not an env var name (use e.g. GITLAB_TOKEN)."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
dup.tap do |copy|
|
|
75
|
+
copy.key_env = key_env
|
|
76
|
+
copy.config_key = key_env.downcase
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
58
80
|
# True when gdkbox knows how to install this harness automatically.
|
|
59
81
|
def installable?
|
|
60
82
|
!@npm_package.nil?
|
|
61
83
|
end
|
|
62
84
|
|
|
85
|
+
# The model id (`up --model`) stripped, or nil when blank. Raises when
|
|
86
|
+
# this harness cannot pin a model. The id reaches the agent only through
|
|
87
|
+
# the environment, so it needs no further validation.
|
|
88
|
+
def check_model!(model)
|
|
89
|
+
model = model.to_s.strip
|
|
90
|
+
return nil if model.empty?
|
|
91
|
+
raise Error, "The #{id} harness does not support --model yet." unless @model_flag
|
|
92
|
+
|
|
93
|
+
model
|
|
94
|
+
end
|
|
95
|
+
|
|
63
96
|
# The in-box shell snippet that runs the agent against $GDKBOX_TASK. The
|
|
64
|
-
# caller wraps it with a timeout and sources the seeded API key.
|
|
65
|
-
|
|
66
|
-
|
|
97
|
+
# caller wraps it with a timeout and sources the seeded API key. With a
|
|
98
|
+
# model, the snippet reads it from $GDKBOX_MODEL, which the caller sets.
|
|
99
|
+
def agent_command(json:, yolo:, model: nil)
|
|
100
|
+
cmd = @run.call(json: json, yolo: yolo)
|
|
101
|
+
check_model!(model) ? %(#{cmd} #{@model_flag} "$GDKBOX_MODEL") : cmd
|
|
67
102
|
end
|
|
68
103
|
|
|
69
104
|
register(
|
|
@@ -74,6 +109,7 @@ module GDKBox
|
|
|
74
109
|
config_key: "anthropic_api_key",
|
|
75
110
|
summary: "Anthropic Claude Code",
|
|
76
111
|
skills_subdir: ".claude/skills",
|
|
112
|
+
model_flag: "--model",
|
|
77
113
|
run: lambda do |json:, yolo:|
|
|
78
114
|
cmd = +%(claude -p "$GDKBOX_TASK")
|
|
79
115
|
cmd << " --output-format json" if json
|
|
@@ -90,6 +126,7 @@ module GDKBox
|
|
|
90
126
|
config_key: "openai_api_key",
|
|
91
127
|
summary: "OpenAI Codex CLI",
|
|
92
128
|
skills_subdir: ".codex/skills",
|
|
129
|
+
model_flag: "-m",
|
|
93
130
|
run: lambda do |json:, yolo:|
|
|
94
131
|
cmd = +%(codex exec "$GDKBOX_TASK")
|
|
95
132
|
cmd << " --json" if json
|
|
@@ -103,11 +140,15 @@ module GDKBox
|
|
|
103
140
|
id: "opencode",
|
|
104
141
|
npm_package: "opencode-ai",
|
|
105
142
|
bin: "opencode",
|
|
106
|
-
# opencode reads provider keys from the environment; default to
|
|
107
|
-
|
|
108
|
-
|
|
143
|
+
# opencode reads provider keys from the environment; default to its
|
|
144
|
+
# GitLab Duo provider (a personal access token with the `api` scope).
|
|
145
|
+
# Point a box at another provider with `up --key-env` (e.g.
|
|
146
|
+
# OPENAI_API_KEY).
|
|
147
|
+
key_env: "GITLAB_TOKEN",
|
|
148
|
+
config_key: "gitlab_token",
|
|
109
149
|
summary: "opencode (sst)",
|
|
110
150
|
skills_subdir: ".config/opencode/skills",
|
|
151
|
+
model_flag: "-m",
|
|
111
152
|
run: lambda do |json:, yolo:|
|
|
112
153
|
# `opencode run` is non-interactive and has no skip-permissions flag.
|
|
113
154
|
+%(opencode run "$GDKBOX_TASK")
|
|
@@ -121,9 +162,10 @@ module GDKBox
|
|
|
121
162
|
# docs recommend installing with --ignore-scripts.
|
|
122
163
|
npm_flags: "--ignore-scripts",
|
|
123
164
|
bin: "pi",
|
|
124
|
-
# pi is multi-provider; default the seeded key to
|
|
125
|
-
|
|
126
|
-
|
|
165
|
+
# pi is multi-provider; default the seeded key to GitLab Duo
|
|
166
|
+
# (GITLAB_TOKEN). Point a box at another provider with `up --key-env`.
|
|
167
|
+
key_env: "GITLAB_TOKEN",
|
|
168
|
+
config_key: "gitlab_token",
|
|
127
169
|
summary: "pi (earendil-works)",
|
|
128
170
|
skills_subdir: ".pi/skills",
|
|
129
171
|
run: lambda do |json:, yolo:|
|
|
@@ -132,5 +174,9 @@ module GDKBox
|
|
|
132
174
|
cmd
|
|
133
175
|
end
|
|
134
176
|
)
|
|
177
|
+
|
|
178
|
+
protected
|
|
179
|
+
|
|
180
|
+
attr_writer :key_env, :config_key
|
|
135
181
|
end
|
|
136
182
|
end
|
data/lib/gdkbox/version.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gdkbox-fleet
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: >-
|
|
4
5
|
Orchestrate a fleet of GDK-in-a-box environments with the `gdkbox` CLI:
|
|
5
6
|
provision a reusable pool of GitLab Development Kit boxes, then dispatch
|
|
@@ -66,6 +67,30 @@ Violating these is how "another agent stole my box" happens:
|
|
|
66
67
|
user) and never in host-side metadata. Check readiness with the
|
|
67
68
|
`api_key_set` field from `gdkbox ls --json` / `status --json`.
|
|
68
69
|
|
|
70
|
+
### Other harnesses (e.g. opencode with GitLab Duo)
|
|
71
|
+
|
|
72
|
+
The steps above assume the default Claude Code harness. If you are told to use
|
|
73
|
+
another agent, or GitLab Duo, the harness, key env var and model are fixed
|
|
74
|
+
**when the box is created**, so pass them to every `up`:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
export GITLAB_TOKEN=<token> # personal access token, api scope
|
|
78
|
+
gdkbox up "pool-$i" --owner "$OWNER" --ttl 7200 --json \
|
|
79
|
+
--harness opencode --model gitlab/duo-chat-sonnet-4-5
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- The opencode harness seeds `$GITLAB_TOKEN` (GitLab Duo) by default. For
|
|
83
|
+
another provider, pass `--key-env <VAR>` (e.g. `OPENAI_API_KEY`) and export
|
|
84
|
+
that variable instead.
|
|
85
|
+
- `--model` pins the model `dispatch` uses. Use a `gitlab/duo-chat-*` model;
|
|
86
|
+
`duo-workflow-*` models do not work with a token.
|
|
87
|
+
- An existing box cannot switch harness, key env or model. `gdkbox rm` it and
|
|
88
|
+
`up` it again.
|
|
89
|
+
- Verify with `gdkbox ls --json`: `"harness"`, `"key_env"` and `"model"` must
|
|
90
|
+
match what you asked for, and `"api_key_set"` must be `true`.
|
|
91
|
+
- `dispatch --json` output is harness-specific. For opencode it is plain text,
|
|
92
|
+
so rely on the exit status rather than parsing it.
|
|
93
|
+
|
|
69
94
|
In the examples below, `gdkbox` means "the gdkbox CLI, however it is invoked".
|
|
70
95
|
|
|
71
96
|
## Command reference (what the orchestrator uses)
|
|
@@ -73,7 +98,8 @@ In the examples below, `gdkbox` means "the gdkbox CLI, however it is invoked".
|
|
|
73
98
|
| Goal | Command |
|
|
74
99
|
| --- | --- |
|
|
75
100
|
| Create/start a box (claimed from birth) | `gdkbox up <name> --owner <id> --ttl <secs> [--json]` |
|
|
76
|
-
|
|
|
101
|
+
| Create a non-Claude box | add `--harness <id> [--key-env <VAR>] [--model <id>]` to `up` (see above) |
|
|
102
|
+
| Seed/rotate API key | `gdkbox set-key <name>` (uses the box's `key_env`, `$ANTHROPIC_API_KEY` by default) |
|
|
77
103
|
| List the fleet (parseable) | `gdkbox ls --json` |
|
|
78
104
|
| Inspect one box | `gdkbox status <name> --json` |
|
|
79
105
|
| **Claim a free box** | `gdkbox claim --owner <id> --ttl <secs> --json` (or `gdkbox claim <name> ...`) |
|
|
@@ -97,8 +123,11 @@ In the examples below, `gdkbox` means "the gdkbox CLI, however it is invoked".
|
|
|
97
123
|
"web_port": 3000,
|
|
98
124
|
"web_url": "http://127.0.0.1:3000",
|
|
99
125
|
"remote_path": "/home/gdk/gdk",
|
|
126
|
+
"harness": "claude",
|
|
100
127
|
"claude_installed": true,
|
|
101
128
|
"api_key_set": true,
|
|
129
|
+
"key_env": "ANTHROPIC_API_KEY",
|
|
130
|
+
"model": null,
|
|
102
131
|
"claimed_by": "agent-7",
|
|
103
132
|
"claim_expires_at": "2026-08-12T13:00:00Z"
|
|
104
133
|
}
|
|
@@ -108,7 +137,8 @@ In the examples below, `gdkbox` means "the gdkbox CLI, however it is invoked".
|
|
|
108
137
|
`claimed_by` is `null` for a free box (an expired lease counts as free).
|
|
109
138
|
|
|
110
139
|
`dispatch` exits with the agent's own exit status (0 = success). With `--json`
|
|
111
|
-
its stdout is
|
|
140
|
+
its stdout is the agent's structured result (Claude's by default), which you
|
|
141
|
+
can parse per task.
|
|
112
142
|
|
|
113
143
|
## Orchestration workflow
|
|
114
144
|
|
|
@@ -161,8 +191,10 @@ its stdout is Claude's structured result, which you can parse per task.
|
|
|
161
191
|
```
|
|
162
192
|
|
|
163
193
|
When a dispatch returns, that box is idle again — assign it the next task
|
|
164
|
-
from the queue
|
|
165
|
-
|
|
194
|
+
from the queue, or **stop it immediately if nothing is queued for it**
|
|
195
|
+
(`gdkbox stop <box>`; see the "never leave a box running idle" guardrail).
|
|
196
|
+
Never run two dispatches against the same box at once, and never dispatch
|
|
197
|
+
into a box whose `claimed_by` is not your owner id.
|
|
166
198
|
|
|
167
199
|
**Delegating to subagents:** the renewal in `run_task` above is for when
|
|
168
200
|
*you* run the loop. If you hand a task to a subagent instead, do the
|
|
@@ -185,14 +217,20 @@ its stdout is Claude's structured result, which you can parse per task.
|
|
|
185
217
|
and summarize per task: success/failure (exit status), what the agent did,
|
|
186
218
|
and any follow-ups. Surface failures explicitly.
|
|
187
219
|
|
|
188
|
-
6. **
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
220
|
+
6. **Stop idle boxes as you go; release everything at wind-down.** A box with
|
|
221
|
+
no task running right now must not be left running — `gdkbox stop <box>`
|
|
222
|
+
it the moment it goes idle (stopping keeps the claim, license and seeded
|
|
223
|
+
data; `gdkbox start <box>` brings it straight back). Do this throughout the
|
|
224
|
+
batch, not only at the end — especially whenever you pause to wait on the
|
|
225
|
+
user, on a review, or on another box. See the guardrail below.
|
|
226
|
+
|
|
227
|
+
When the **whole batch is done**, stop every box, then release **all** of
|
|
228
|
+
them so the pool is usable by others (a lease would expire eventually, but
|
|
229
|
+
do not rely on it for normal completion), or `gdkbox rm --force` to discard:
|
|
193
230
|
|
|
194
231
|
```sh
|
|
195
232
|
for b in $(gdkbox ls --json | jq -r ".[] | select(.claimed_by==\"$OWNER\") | .name"); do
|
|
233
|
+
gdkbox stop "$b" # no box left running once its work is done
|
|
196
234
|
gdkbox release "$b" --owner "$OWNER"
|
|
197
235
|
done
|
|
198
236
|
```
|
|
@@ -216,7 +254,8 @@ for i in 1 2 3; do
|
|
|
216
254
|
gdkbox dispatch "pool-$i" --task-file "tasks/$i.md" --json > "out/$i.json" 2>&1 &
|
|
217
255
|
done
|
|
218
256
|
wait
|
|
219
|
-
|
|
257
|
+
# batch done: stop each box so none is left running, then release the pool
|
|
258
|
+
for i in 1 2 3; do gdkbox stop "pool-$i"; gdkbox release "pool-$i" --owner "$OWNER"; done
|
|
220
259
|
```
|
|
221
260
|
|
|
222
261
|
Adopt one more existing free box when the queue outgrows the pool (each call
|
|
@@ -274,6 +313,15 @@ rest of the pool accordingly, and give the claim a generous `--ttl`.
|
|
|
274
313
|
heavy. Prefer reusing the pool over creating more boxes.
|
|
275
314
|
- **One dispatch per box at a time.** Serialize tasks on a box; parallelize
|
|
276
315
|
*across* boxes.
|
|
316
|
+
- **Never leave a box running idle.** A box is expensive (a full GDK
|
|
317
|
+
container); it must be running *only* while a dispatch is actually using it.
|
|
318
|
+
The moment a box has no task queued for it — including whenever the
|
|
319
|
+
orchestration pauses to wait on the user, on a review, or on another box —
|
|
320
|
+
`gdkbox stop <box>` it. Stopping frees the host's memory and CPU but **keeps
|
|
321
|
+
the claim, license and seeded data**, so `gdkbox start <box>` resumes it in
|
|
322
|
+
seconds. Keep the claim while the box is yours and only *release* at session
|
|
323
|
+
wind-down; stopping is for idleness, releasing is for "done with it". If you
|
|
324
|
+
stopped boxes mid-session, `gdkbox start` them again before dispatching.
|
|
277
325
|
- **Own every box you touch, for the whole batch.** Provision with
|
|
278
326
|
`up --owner`, adopt with `claim`, and hold the claims until wind-down —
|
|
279
327
|
then release them all. Claims are advisory: `dispatch` will not stop you
|