Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .agents/skills/launch-openshell-gator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ If this fails, run the local Codex login flow outside the gator launch. If Codex

The launcher passes the non-secret host account ID as literal `CODEX_ACCOUNT_ID` for Codex's local workspace routing. The harness must not use the opaque `CODEX_AUTH_ACCOUNT_ID` credential placeholder for that comparison. Access tokens remain placeholders, and refresh material remains gateway-only.

The image contains task-independent tooling and a prompt template, not the run's PR assignment or test authorization. The launcher supplies those instructions at runtime through `OPENSHELL_AGENT_TASK` in the canonical process command. The supervisor captures the assembled prompt in memory and supplies it on stdin for every cycle. Task text is non-secret and may appear in runtime command metadata; never put credentials in the task. Task-only changes reuse the same tooling build inputs, although the launcher still builds/tags an image per invocation.

For Kubernetes gateways, select image delivery explicitly: `--k3d-cluster NAME`
builds with Docker and imports into a running local cluster with the matching
current kube context; `--publish-image REGISTRY/REPOSITORY:TAG` builds and pushes,
then launches by resolved digest; `--image REGISTRY/REPOSITORY@sha256:DIGEST`
reuses trusted compatible tooling without local image tools. Do not infer
locality from a loopback endpoint. The k3d selection asserts which cluster
serves the gateway; imported images require `if_not_present` or `never` pull
policy. Registry builds use the builder's platform and existing registry auth;
nodes need their own pull access. `--build-only` builds/delivers without
gateway mutations, provider credential reads, or a sandbox launch. Reused
images must match the local manifest's agent ID and payload version and support
the runtime task contract; legacy baked-task images fail startup. See
`scripts/agents/gator/README.md` for examples and option compatibility.

### Step 5: Verify Gateway Is Registered And Alive

Use the target gateway from the operator request or current session context. Do not assume a gateway name. If the operator did not specify one, list registered gateways and ask before launching when the correct target is ambiguous.
Expand Down Expand Up @@ -163,7 +179,7 @@ sandbox_name="gtr-pr-${pr_number}-watch"
"Review and monitor PR #${pr_number} through the gator-gate workflow. Scope this invocation only to PR #${pr_number}."
```

The launcher queries the gateway's selected compute driver, builds the gator image in the matching Docker or Podman image store, stages the immutable payload, imports provider profiles, configures provider credentials and refresh, and starts the agent supervisor as the sandbox's canonical main process. The detached main process survives loss of the host CLI connection and reconnects to a restarted gateway. Unless `--keep` is set, the sandbox is marked ephemeral so the gateway deletes it after the canonical main process exits and its terminal result is finalized. `CONTAINER_ENGINE`, when set, must match the gateway driver.
The launcher queries the gateway's selected compute driver and prepares the image through the selected build/import/publish or prebuilt path before importing provider profiles, configuring provider credentials and refresh, and starting the agent supervisor as the sandbox's canonical main process. The detached main process survives loss of the host CLI connection and reconnects to a restarted gateway. Unless `--keep` is set, the sandbox is marked ephemeral so the gateway deletes it after the canonical main process exits and its terminal result is finalized. `CONTAINER_ENGINE`, when set, must match Docker/Podman gateways or Docker for k3d imports; registry builds for Kubernetes may use either local engine.

The launcher streams image-build and provisioning output until the detached workload is ready, then exits. Use `openshell logs <sandbox-name>` or the TUI for runtime output.

Expand Down
84 changes: 78 additions & 6 deletions scripts/agents/gator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Launch a headless sandbox agent that runs the `gator-gate` skill against OpenShe
- `gh` is authenticated on the host and has access to `NVIDIA/OpenShell`.
- For `--harness codex`, `codex login` has created `$HOME/.codex/auth.json`.
- For `--harness codex`, local Codex auth must include an access token, refresh token, and account ID.
- A local gateway and either Docker or Podman are available to build the
default sandbox image.
- A Docker, Podman, or Kubernetes gateway is available. Local builds require
Docker or Podman; a prebuilt image does not require a local container engine.

## Usage

Expand All @@ -20,11 +20,79 @@ Launch a headless sandbox agent that runs the `gator-gate` skill against OpenShe
"Run gator on PR 1536 and keep watching until it closes or merges."
```

By default the launcher uses `scripts/agents/gator/Dockerfile` as the sandbox image source. It builds `scripts/agents/gator/` as the image context, so gator-specific image files such as `policy.yaml` and `bin/gh` stay with the gator agent. The launcher bakes rendered prompts, skills, subagents, and shared runtime files into `/etc/openshell/agent-payload`, then passes the resulting image reference to `openshell sandbox create`.
By default the launcher uses `scripts/agents/gator/Dockerfile` as the sandbox image source. It builds `scripts/agents/gator/` as the image context, so gator-specific image files such as `policy.yaml` and `bin/gh` stay with the gator agent. The launcher bakes a task-independent prompt template, skills, subagents, and shared runtime files into `/etc/openshell/agent-payload`, then passes the resulting image reference to `openshell sandbox create`.

The launcher queries the selected gateway and builds with its Docker or Podman
compute driver. If `CONTAINER_ENGINE` is set, it must match that driver. Other
gateway drivers cannot run this local-image launcher.
The PR assignment, operator test authorization, harness selection, watch mode, and polling interval are runtime inputs, not rendered image contents. The launcher supplies the non-secret assignment as `OPENSHELL_AGENT_TASK` in the canonical process command. The supervisor assembles the prompt once in memory and sends it to each harness cycle on stdin; cycles do not read task instructions from an agent-writable file. Assignment text is literal data, so shell syntax and template tokens inside it are not evaluated. Do not put credentials in assignments; use providers. Runtime command metadata can contain the assignment. This separates reusable tooling from per-run instructions; it does not make prompt instructions a security boundary.

Without `--image`, the launcher builds and tags an image for each invocation,
but task-only or supervision-setting changes reuse identical tooling build
inputs. Docker and Podman builds use the gateway's engine; an explicit
`CONTAINER_ENGINE` must match it.

## Kubernetes image delivery

The launcher discovers the compute driver from the selected gateway. It does
not infer a local cluster from a loopback gateway endpoint: that endpoint could
be a port-forward to a remote deployment. Select one of these image paths for
a Kubernetes gateway.

For local k3d, explicitly identify the cluster serving the gateway. The launcher
requires the matching current kube context and a running local cluster, builds
with Docker, and imports the image into that cluster before creating a sandbox:

```shell
OPENSHELL_WORKSPACE=gator mise exec k3d@5.9.0 -- ./scripts/agents/run.sh \
--agent gator --gateway openshell-dev-main \
--k3d-cluster openshell-dev-main --name gtr-pr-1234 --watch \
"Review and monitor PR #1234 through the gator-gate workflow."
```

The cluster selection is an operator assertion that the gateway uses that
cluster; it is not inferred from the gateway name. Its workload image pull
policy must allow imported images (`if_not_present` or `never`). The local
Skaffold configuration uses `if_not_present`.

For a registry-backed deployment, build and publish tooling once without
changing gateway settings, importing providers, reading provider credentials,
or launching a sandbox:

```shell
./scripts/agents/run.sh --agent gator --gateway remote-kube \
--publish-image registry.example.com/team/gator:payload-13 --build-only
```

The builder uses existing Docker/Podman registry authentication. Both local and
remote Kubernetes nodes must be able to pull from the chosen registry; private
registries require the deployment's image-pull credential configuration. Builds
target the local builder's platform, so build on a matching platform for the
target nodes. Multi-platform builds are not provided by this launcher.

The launcher prints `Tooling image: REGISTRY/REPOSITORY@sha256:DIGEST` after a
successful push. Reuse that full reference for subsequent tasks:

```shell
OPENSHELL_WORKSPACE=gator ./scripts/agents/run.sh \
--agent gator --gateway remote-kube --name gtr-pr-1234 --watch \
--image "$GATOR_IMAGE" \
"Review and monitor PR #1234 through the gator-gate workflow."
```

Set `GATOR_IMAGE` to the printed digest-pinned reference, not a mutable tag.
`--image` skips local builds and cluster imports and works with Docker/Podman
gateways too. It cannot be combined with `--from`, `--codex-bin`,
`--k3d-cluster`, `--publish-image`, or `--build-only`. The image must contain
tooling built by this launcher with the same agent ID and payload version as
the local manifest. Startup validates that contract before running the agent;
legacy images with baked assignments are incompatible. Only reuse trusted
tooling images: the contract check does not authenticate their contents.

Omit `--build-only` from `--publish-image` to build, push, and launch in one
invocation. A failed push, missing repository digest, or failed k3d import
stops the launcher before gateway settings, provider configuration, or sandbox
creation. Kubernetes launches without an explicit image-delivery choice fail
with guidance rather than guessing a cluster or registry.

## Runtime and providers

Use `--harness codex` to select Codex explicitly. Other harness names are rejected until their support is added to `agent.yaml` and `scripts/agents/runtime/harnesses/<name>/`. Agent directories do not carry their own harness implementations; they provide prompt templates and optional skills or subagents for the shared runtime to inject.

Expand Down Expand Up @@ -75,4 +143,8 @@ bash scripts/agents/gator/bin/gh_guard_test.sh
bash scripts/agents/gator/bin/review_feedback_ledger_test.sh
bash scripts/agents/gator/bin/resolve_gator_review_threads_test.sh
bash scripts/agents/runtime/harnesses/codex/exec_test.sh
bash scripts/agents/run_test.sh
bash scripts/agents/image_test.sh
bash scripts/agents/runtime/launch_test.sh
bash scripts/agents/runtime/supervisor_test.sh
```
2 changes: 1 addition & 1 deletion scripts/agents/gator/agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-License-Identifier: Apache-2.0

id: gator
payload_version: 12
payload_version: 13
display_name: Gator Gate Agent
description: Validate and monitor OpenShell GitHub issues and pull requests through the gator state machine.

Expand Down
2 changes: 1 addition & 1 deletion scripts/agents/gator/bin/review_feedback_ledger_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ rg -q 'COPY bin/validate-review-findings /usr/local/bin/validate-review-findings
"$GATOR_DIR/Dockerfile"
ruby -ryaml -e '
manifest = YAML.load_file(ARGV.fetch(0))
abort unless manifest.fetch("payload_version") == 12
abort unless manifest.fetch("payload_version") == 13
resource = manifest.fetch("resources").find {
|entry| entry.fetch("id") == "gator-review-findings-schema"
}
Expand Down
101 changes: 101 additions & 0 deletions scripts/agents/image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env bash

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Image transport for the host launcher. The caller supplies fail/log/require_cmd
# and selects the gateway; no locality inference is made from its endpoint.
prepare_agent_image_transport() {
local gateway_info
gateway_info="$("$OPENSHELL_BIN" --gateway "$GATEWAY" gateway info --output json)" || fail "failed to inspect gateway '$GATEWAY'"
GATEWAY_DRIVER="$(printf '%s' "$gateway_info" | ruby -rjson -e '
drivers = JSON.parse(STDIN.read).fetch("compute_drivers", []).map { |d| d.fetch("name").downcase }
abort "gateway must report exactly one compute driver" unless drivers.length == 1
puts drivers.first
')" || fail "gateway '$GATEWAY' did not report exactly one compute driver"
case "$GATEWAY_DRIVER" in
docker|podman|kubernetes) ;;
*) fail "unsupported agent compute driver: $GATEWAY_DRIVER" ;;
esac

if [[ -n "$AGENT_IMAGE" ]]; then
[[ "$AGENT_IMAGE" =~ ^[a-zA-Z0-9][a-zA-Z0-9._:/-]*@sha256:[a-f0-9]{64}$ ]] || fail "--image requires a digest-pinned image reference"
[[ -z "$K3D_CLUSTER$PUBLISH_IMAGE" ]] || fail "--image cannot be combined with --k3d-cluster or --publish-image"
IMAGE_MODE=prebuilt
return
fi

if [[ -n "$K3D_CLUSTER" ]]; then
[[ -z "$PUBLISH_IMAGE" ]] || fail "--k3d-cluster and --publish-image are mutually exclusive"
[[ "$GATEWAY_DRIVER" == kubernetes ]] || fail "--k3d-cluster requires a Kubernetes gateway"
[[ "$K3D_CLUSTER" =~ ^[a-z0-9]([a-z0-9-]{0,30}[a-z0-9])?$ ]] || fail "invalid k3d cluster name"
require_cmd k3d
require_cmd kubectl
local context
context="$(kubectl config current-context)" || fail "cannot determine Kubernetes context"
[[ "$context" == "k3d-$K3D_CLUSTER" ]] || fail "selected kube context '$context' does not match k3d cluster '$K3D_CLUSTER'"
k3d cluster list -o json | ruby -rjson -e '
cluster = JSON.parse(STDIN.read).find { |c| c.fetch("name") == ARGV.fetch(0) }
abort "selected local k3d cluster is not running" unless cluster && cluster.fetch("serversRunning", 0) > 0
' "$K3D_CLUSTER" || fail "cannot use local k3d cluster '$K3D_CLUSTER'"
IMAGE_MODE=k3d
set_agent_build_engine docker
elif [[ -n "$PUBLISH_IMAGE" ]]; then
# Require an explicit registry and tag. No implicit Docker Hub uploads.
[[ "$PUBLISH_IMAGE" =~ ^[a-z0-9][a-z0-9.:-]*/[a-z0-9][a-z0-9._/-]*:[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]] || fail "--publish-image requires REGISTRY/REPOSITORY:TAG"
local registry="${PUBLISH_IMAGE%%/*}"
[[ "$registry" == localhost || "$registry" == *.* || "$registry" == *:* ]] || fail "--publish-image requires an explicit registry hostname (use docker.io for Docker Hub)"
IMAGE_MODE=registry
if [[ "$GATEWAY_DRIVER" == kubernetes ]]; then
# Registry builds may use either local engine, independently of the
# engine used to host the remote Kubernetes cluster.
source "$ROOT_DIR/tasks/scripts/container-engine.sh"
else
set_agent_build_engine "$GATEWAY_DRIVER"
fi
elif [[ "$GATEWAY_DRIVER" == kubernetes ]]; then
fail "Kubernetes needs --image REGISTRY/REPOSITORY@sha256:DIGEST, --k3d-cluster NAME, or --publish-image REGISTRY/REPOSITORY:TAG"
else
IMAGE_MODE=engine
set_agent_build_engine "$GATEWAY_DRIVER"
fi
}

set_agent_build_engine() {
local engine="$1"
if [[ -n "${CONTAINER_ENGINE:-}" && "$(printf '%s' "$CONTAINER_ENGINE" | tr '[:upper:]' '[:lower:]')" != "$engine" ]]; then
fail "CONTAINER_ENGINE=$CONTAINER_ENGINE conflicts with required image build engine '$engine'"
fi
CONTAINER_ENGINE="$engine"
export CONTAINER_ENGINE
source "$ROOT_DIR/tasks/scripts/container-engine.sh"
}

deliver_agent_image() {
local image_tag="$1"
case "$IMAGE_MODE" in
k3d)
log "Importing '$image_tag' into k3d cluster '$K3D_CLUSTER'."
k3d image import --cluster "$K3D_CLUSTER" "$image_tag" || fail "k3d image import failed"
SANDBOX_FROM="$image_tag"
;;
registry)
log "Publishing '$image_tag'."
ce push "$image_tag" || fail "image push failed"
local repository="${image_tag%:*}"
SANDBOX_FROM="$(ce image inspect "$image_tag" --format '{{json .RepoDigests}}' | ruby -rjson -e '
digests = JSON.parse(STDIN.read) || []
normalize = lambda do |repository|
# Docker omits its default registry in RepoDigests.
repository = repository.sub(/\A(?:docker\.io|index\.docker\.io)\//, "")
repository.include?("/") ? repository : "library/#{repository}"
end
digest = digests.find { |d| normalize.call(d.split("@", 2).first) == normalize.call(ARGV.fetch(0)) && d.match?(/@sha256:[a-f0-9]{64}\z/) }
abort "push did not produce a matching repository digest" unless digest
puts "#{ARGV.fetch(0)}@#{digest.split("@", 2).last}"
' "$repository")" || fail "cannot resolve published image digest; refusing mutable-tag launch"
;;
engine) SANDBOX_FROM="$image_tag" ;;
*) fail "invalid image delivery mode: $IMAGE_MODE" ;;
esac
}
Loading
Loading