gdkbox 0.1.19 → 0.1.20
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 +31 -2
- 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: 6a5d8a8a1fbb33083f8cfb0b306ee0d6fc4efaffad04a110ef9f1a55141baae5
|
|
4
|
+
data.tar.gz: f4ec5a9b60bf6ab209ced26b1c0ec9c6b95503ae6a65484844506553055ea900
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 596f76ed57ea61c0fc0a00a7e0cd72a917a076aa9fcf022127d8b3ac7e5303c6ccb2c60e30c24727dcedb402f14065da1776be58832d1d28446b070850b09145
|
|
7
|
+
data.tar.gz: 1e4e94bc489cfc712b5e7a7386cad211e5cdf26b826eddeb43fa5e2bf47cacfde8c945677d896c5a21e97deef8dae99458133ebb4a3253a2530142c66404ff77
|
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
|
@@ -66,6 +66,30 @@ Violating these is how "another agent stole my box" happens:
|
|
|
66
66
|
user) and never in host-side metadata. Check readiness with the
|
|
67
67
|
`api_key_set` field from `gdkbox ls --json` / `status --json`.
|
|
68
68
|
|
|
69
|
+
### Other harnesses (e.g. opencode with GitLab Duo)
|
|
70
|
+
|
|
71
|
+
The steps above assume the default Claude Code harness. If you are told to use
|
|
72
|
+
another agent, or GitLab Duo, the harness, key env var and model are fixed
|
|
73
|
+
**when the box is created**, so pass them to every `up`:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
export GITLAB_TOKEN=<token> # personal access token, api scope
|
|
77
|
+
gdkbox up "pool-$i" --owner "$OWNER" --ttl 7200 --json \
|
|
78
|
+
--harness opencode --model gitlab/duo-chat-sonnet-4-5
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- The opencode harness seeds `$GITLAB_TOKEN` (GitLab Duo) by default. For
|
|
82
|
+
another provider, pass `--key-env <VAR>` (e.g. `OPENAI_API_KEY`) and export
|
|
83
|
+
that variable instead.
|
|
84
|
+
- `--model` pins the model `dispatch` uses. Use a `gitlab/duo-chat-*` model;
|
|
85
|
+
`duo-workflow-*` models do not work with a token.
|
|
86
|
+
- An existing box cannot switch harness, key env or model. `gdkbox rm` it and
|
|
87
|
+
`up` it again.
|
|
88
|
+
- Verify with `gdkbox ls --json`: `"harness"`, `"key_env"` and `"model"` must
|
|
89
|
+
match what you asked for, and `"api_key_set"` must be `true`.
|
|
90
|
+
- `dispatch --json` output is harness-specific. For opencode it is plain text,
|
|
91
|
+
so rely on the exit status rather than parsing it.
|
|
92
|
+
|
|
69
93
|
In the examples below, `gdkbox` means "the gdkbox CLI, however it is invoked".
|
|
70
94
|
|
|
71
95
|
## Command reference (what the orchestrator uses)
|
|
@@ -73,7 +97,8 @@ In the examples below, `gdkbox` means "the gdkbox CLI, however it is invoked".
|
|
|
73
97
|
| Goal | Command |
|
|
74
98
|
| --- | --- |
|
|
75
99
|
| Create/start a box (claimed from birth) | `gdkbox up <name> --owner <id> --ttl <secs> [--json]` |
|
|
76
|
-
|
|
|
100
|
+
| Create a non-Claude box | add `--harness <id> [--key-env <VAR>] [--model <id>]` to `up` (see above) |
|
|
101
|
+
| Seed/rotate API key | `gdkbox set-key <name>` (uses the box's `key_env`, `$ANTHROPIC_API_KEY` by default) |
|
|
77
102
|
| List the fleet (parseable) | `gdkbox ls --json` |
|
|
78
103
|
| Inspect one box | `gdkbox status <name> --json` |
|
|
79
104
|
| **Claim a free box** | `gdkbox claim --owner <id> --ttl <secs> --json` (or `gdkbox claim <name> ...`) |
|
|
@@ -97,8 +122,11 @@ In the examples below, `gdkbox` means "the gdkbox CLI, however it is invoked".
|
|
|
97
122
|
"web_port": 3000,
|
|
98
123
|
"web_url": "http://127.0.0.1:3000",
|
|
99
124
|
"remote_path": "/home/gdk/gdk",
|
|
125
|
+
"harness": "claude",
|
|
100
126
|
"claude_installed": true,
|
|
101
127
|
"api_key_set": true,
|
|
128
|
+
"key_env": "ANTHROPIC_API_KEY",
|
|
129
|
+
"model": null,
|
|
102
130
|
"claimed_by": "agent-7",
|
|
103
131
|
"claim_expires_at": "2026-08-12T13:00:00Z"
|
|
104
132
|
}
|
|
@@ -108,7 +136,8 @@ In the examples below, `gdkbox` means "the gdkbox CLI, however it is invoked".
|
|
|
108
136
|
`claimed_by` is `null` for a free box (an expired lease counts as free).
|
|
109
137
|
|
|
110
138
|
`dispatch` exits with the agent's own exit status (0 = success). With `--json`
|
|
111
|
-
its stdout is
|
|
139
|
+
its stdout is the agent's structured result (Claude's by default), which you
|
|
140
|
+
can parse per task.
|
|
112
141
|
|
|
113
142
|
## Orchestration workflow
|
|
114
143
|
|