Skip to content
Merged
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
20 changes: 17 additions & 3 deletions .github/workflows/ai-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,31 @@ name: AI review

on:
pull_request_target:
types: [labeled, unlabeled, synchronize, reopened, ready_for_review, converted_to_draft, closed]
types: [opened, labeled, unlabeled, synchronize, reopened, ready_for_review, converted_to_draft, closed]

permissions:
contents: read
pull-requests: write

jobs:
ensure-label:
if: github.event.action == 'opened'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Add opt-in review label
env:
GH_TOKEN: ${{ github.token }}
run: gh pr edit "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/${{ github.event.pull_request.number }}" --add-label ai-review
Comment thread
coderabbitai[bot] marked this conversation as resolved.

review:
needs: ensure-label
if: >-
((github.event.action == 'labeled' || github.event.action == 'unlabeled') && github.event.label.name == 'ai-review') ||
(github.event.action != 'labeled' && github.event.action != 'unlabeled' && contains(github.event.pull_request.labels.*.name, 'ai-review'))
always() &&
(github.event.action != 'opened' || needs.ensure-label.result == 'success') &&
(((github.event.action == 'labeled' || github.event.action == 'unlabeled') && github.event.label.name == 'ai-review') ||
(github.event.action != 'labeled' && github.event.action != 'unlabeled' && contains(github.event.pull_request.labels.*.name, 'ai-review')))
concurrency:
group: ai-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
Expand Down
103 changes: 103 additions & 0 deletions .github/workflows/pr-review-reusable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: Harness PR review

on:
workflow_call:
inputs:
harness-ref:
description: Immutable 40-character Harness commit SHA to execute
required: true
type: string
skill-path:
description: Path to the trusted review skill in the caller repository
required: false
type: string
default: .github/skills/pr-review/SKILL.md
secrets:
VERTEX_AI_SERVICE_ACCOUNT_KEY:
required: true

permissions:
contents: read
pull-requests: write

jobs:
review:
if: >-
((github.event.action == 'labeled' || github.event.action == 'unlabeled') && github.event.label.name == 'ai-review') ||
(github.event.action != 'labeled' && github.event.action != 'unlabeled' && contains(github.event.pull_request.labels.*.name, 'ai-review'))
runs-on: ubuntu-latest
timeout-minutes: 15
concurrency:
group: ai-review-${{ github.repository }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
steps:
- name: Validate immutable Harness revision
env:
HARNESS_REF: ${{ inputs.harness-ref }}
run: '[[ "$HARNESS_REF" =~ ^[0-9a-f]{40}$ ]]'
# Checkout only default-branch content from the caller. The PR head is data.
- uses: actions/checkout@v7
with:
ref: ${{ github.event.repository.default_branch }}
path: caller
persist-credentials: false
- uses: actions/checkout@v7
with:
repository: stackrox/harness-openshell
ref: ${{ inputs.harness-ref }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
path: harness
persist-credentials: false
- name: Install caller skill
env:
SKILL_PATH: ${{ inputs.skill-path }}
run: |
[[ "$SKILL_PATH" != /* && "$SKILL_PATH" != *..* ]] || exit 1
test -f "caller/$SKILL_PATH"
install -D -m 0644 "caller/$SKILL_PATH" harness/skills/pr-review/SKILL.md
- uses: actions/setup-go@v7
with:
go-version-file: harness/go.mod
- name: Build trusted harness
working-directory: harness
run: echo "REVIEW_DIR=$RUNNER_TEMP/ai-review" >> "$GITHUB_ENV"; make cli
- name: Check label and fetch exact diff as data
id: prepare
working-directory: harness
env:
GH_TOKEN: ${{ github.token }}
REVIEW_REPOSITORY: ${{ github.repository }}
REVIEW_PR: ${{ github.event.pull_request.number }}
REVIEW_HEAD: ${{ github.event.pull_request.head.sha }}
run: bash scripts/pr-review.sh prepare
- uses: ./harness/.github/actions/setup-openshell
if: steps.prepare.outputs.eligible == 'true'
- name: Authenticate to Google Cloud
if: steps.prepare.outputs.eligible == 'true'
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
project_id: ${{ vars.VERTEX_AI_PROJECT_ID }}
credentials_json: ${{ secrets.VERTEX_AI_SERVICE_ACCOUNT_KEY }}
- uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
if: steps.prepare.outputs.eligible == 'true'
- name: Review in isolated OpenShell workspace
if: steps.prepare.outputs.eligible == 'true'
working-directory: harness
env:
GH_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ github.token }}
REVIEW_REPOSITORY: ${{ github.repository }}
REVIEW_PR: ${{ github.event.pull_request.number }}
REVIEW_HEAD: ${{ github.event.pull_request.head.sha }}
VERTEX_AI_PROJECT_ID: ${{ vars.VERTEX_AI_PROJECT_ID }}
VERTEX_AI_REGION: ${{ vars.VERTEX_AI_REGION }}
run: |
token="$(gcloud auth print-access-token)"
echo "::add-mask::$token"
export GOOGLE_VERTEX_AI_TOKEN="$token"
bash scripts/pr-review.sh run
- uses: actions/upload-artifact@v7
if: always() && steps.prepare.outputs.eligible == 'true'
with:
name: ai-review-pr-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/ai-review/
retention-days: 7
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ harness.yaml
test.yaml
reviewer.yaml
skills-lock.json

# Local audit working notes
docs/audit-*.md
docs/code-audit.md
Comment thread
coderabbitai[bot] marked this conversation as resolved.
45 changes: 31 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

> **Experimental.** Built on [OpenShell](https://github.com/NVIDIA/OpenShell), which is itself alpha software. Expect breaking changes in both.

Declarative workflow layer for OpenShell AI agent sandboxes.
Run workflows in OpenShell AI agent sandboxes. The current focus is portable
PR review with trusted, repository-controlled skills.

## Quick Start

Expand Down Expand Up @@ -79,11 +80,22 @@ The command writes results to stdout. For retained sandboxes, use

## Why this exists

[OpenShell](https://github.com/NVIDIA/OpenShell) provides a strict, secure sandbox runtime — deny-by-default L7 network policy, credential proxying, Landlock filesystem isolation, and inference routing. It also provisions the gateway itself (the local installer, or `helm install openshell` on a cluster). What it doesn't provide is the developer workflow layer on top: the config that wires up providers, the declarative reconciliation that makes a gateway match your intent, or the CI harness that catches breakage before developers hit it.
[OpenShell](https://github.com/NVIDIA/OpenShell) owns gateway provisioning,
sandbox isolation, credential proxying, provider lifecycle, and network policy.
Harness prepares workflow inputs, stages source and skills, runs an agent, and
reports the result while cleaning up its sandbox.

Without a shared harness layer, every team building on OpenShell independently solves the same problems — writing shell scripts to register providers, hand-rolling container images, re-deriving inference routing. The configs diverge, the security posture varies, and nobody catches regressions until something breaks in production.
The next portability milestone is running the same PR-review package in a
second repository with that repository's trusted skill. Users should customize
review behavior through skills; maintained integrations and platform setup
should supply provider credentials and native OpenShell policy. The current
review example still has repository-local orchestration and requires credential
wiring improvements before it meets that goal.

**The design boundary**: managing a gateway is OpenShell's problem; the harness is a declarative setup/run layer with zero compute-backend opinion. It never provisions or tears down a gateway — it declares providers, inference, and policy against one OpenShell already stood up, and runs agents in it. The workflow remains portable because its target can be overridden by standard gateway and workspace flags or environment variables.
Workflows target a local OpenShell gateway or a configured HyperShell gateway.
Provider references are read-only. Inference reconciliation remains supported
while existing callers migrate to platform-configured routes. See the
[code audit](docs/code-audit.md) for the dependency inventory and remaining cuts.

**The core design constraint**: if the developer harness isn't running and live-tested in CI, the developer experience can't be maintained. OpenShell, agent CLIs, and provider APIs all change frequently — often multiple times per week. A harness that works today and isn't continuously validated will silently break. CI exercises the workflow against local and Kind gateways on Linux. OpenShift remains a manually credentialed integration target.

Expand Down Expand Up @@ -130,14 +142,14 @@ spec:
`plan` is read-only and may render desired state while the gateway is offline.
`apply` requires the effective gateway to be reachable, verifies referenced
providers before sandbox creation, and disables OpenShell provider auto-discovery.
Managed providers may be updated or explicitly adopted, but apply does not create
credentialed providers; platform bootstrap owns their creation. Relative payload
Providers are read-only references; OpenShell/platform bootstrap owns their
creation, updates, and deletion. Relative payload
and policy paths resolve from the workflow file's directory.

Workflow schema essentials:

- `spec.providers` declares provider resources; `spec.sandbox.providers` attaches provider capabilities to the sandbox runtime.
- `management: referenced` requires an already-registered provider; managed providers can set `adopt: true` to take ownership of a pre-existing provider.
- `spec.providers` verifies existing provider references; `spec.sandbox.providers` attaches provider capabilities to the sandbox runtime.
- `management: referenced` is optional and is the only supported management mode. Provider configuration belongs in OpenShell/platform bootstrap.
- `spec.inference.verify: true` enforces inference-route endpoint checks during inference route writes.
- `spec.source.repo` is cloned outside the sandbox and uploaded; `spec.payloads[*].source` and `spec.sandbox.policy.file` resolve relative to the workflow file.
- Pin `spec.source.ref` to a full commit SHA for repeatable source inputs. Branches and tags resolve at preparation time; an omitted ref uses remote HEAD. Apply reports the actual prepared commit from the host checkout, including the commit behind an annotated tag. Missing refs fail instead of falling back to HEAD. This identifies the initial checkout, not later agent edits or payload overlays, and is not yet a structured run-result artifact.
Expand Down Expand Up @@ -189,7 +201,7 @@ review artifact bundle.
(OpenShell has already provisioned the gateway; you selected it)
harness apply -f config.yaml
|
+-> Verify/reconcile declared providers and inference
+-> Verify provider references and configure declared inference
+-> Create sandbox (isolated container, deny-by-default network)
+-> Upload payloads (CLAUDE.md, MCP config, skills)
+-> Run task (agent executes, outputs results)
Expand All @@ -212,7 +224,7 @@ openshell term # interactive policy terminal

- OpenShell CLI and gateway service at the repo-pinned version (see `make openshell` and `.openshell-version`).
- An active OpenShell gateway registration (`openshell gateway add ...`, `openshell gateway select ...`).
- Provider credentials already reconciled on the gateway for any referenced providers.
- Providers already configured on the gateway for any references.

## Install

Expand Down Expand Up @@ -263,8 +275,13 @@ harness apply -f harness.yaml # same YAML, cluster gateway

Tear the gateway down with `helm uninstall openshell` and
`openshell gateway remove my-cluster`. The harness `delete` command removes
sandboxes; add `--providers` (or `--all`) to remove providers too. It never
removes the gateway.
sandboxes only. Use `openshell provider delete` to remove providers and upstream
tools to remove the gateway.

Provider-management migration: `management: managed`, provider `adopt`/`config`,
and `harness delete --providers`/`--all` are removed. Configure providers with
OpenShell and reference their names in workflows. Use `delete --sandboxes` only
for a dedicated workspace, or delete individual sandbox names.

> **Migration:** `harness deploy`, `harness teardown`, `harness status`, and
> `delete --k8s` are removed. Provision the gateway with OpenShell (the
Expand All @@ -281,13 +298,13 @@ removes the gateway.
| `harness doctor` | Validate gateway reachability and referenced providers |
| `harness apply -f FILE` | Deploy a sandbox from config |
| `harness apply -f FILE --attach` | Interactive TTY mode |
| `harness apply -f FILE --setup-only` | Reconcile providers and inference only (skip sandbox run) |
| `harness apply -f FILE --setup-only` | Verify provider references and configure inference (skip sandbox run) |
| `harness apply -f FILE --dry-run` | Render the v1alpha1 action plan without mutating |
| `harness apply -f FILE -o yaml` | Output resolved config with interpolated and credential-bearing map values redacted |
| `harness get gateways` | Show active gateway only (name, endpoint, status, version) |
| `harness get agents\|providers` | List resources |
| `harness describe <name>` | Sandbox details |
| `harness delete <name> [--all\|--sandboxes\|--providers]` | Delete targeted or bulk resources |
| `harness delete <name>` / `harness delete --sandboxes` | Delete named sandboxes or all sandboxes in the selected workspace |
| `harness plan -f FILE` | Read-only reconciliation plan (mutates nothing) |

### Credentials
Expand Down
2 changes: 1 addition & 1 deletion cmd/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ host-interpolated and credential-bearing map values redacted.`,
cmd.Flags().StringVar(&entrypoint, "entrypoint", "", "Override the agent executable")
cmd.Flags().BoolVar(&attach, "attach", false, "Attach a TTY for interactive execution")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Render the action plan without mutating anything")
cmd.Flags().BoolVar(&setupOnly, "setup-only", false, "Reconcile providers and inference without running a sandbox")
cmd.Flags().BoolVar(&setupOnly, "setup-only", false, "Verify provider references and configure inference without running a sandbox")
cmd.Flags().StringVarP(&output, "output", "o", "", "Output format: yaml or json (dry-run also supports table)")
cmd.Flags().StringVar(&resultFile, "result-file", "", "Write host-derived execution result JSON to a new file")
gatewayName, workspace = registerTargetFlags(cmd)
Expand Down
5 changes: 1 addition & 4 deletions cmd/apply_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func executeResolvedWorkflow(ctx context.Context, workflow *resolvedWorkflow, p
if client == nil || !current.Reachable {
return fmt.Errorf("%s is not reachable or authenticated", targetDescription(workflow.Target))
}
if err := preflightPlan(workflow.Desired, p); err != nil {
if err := preflightPlan(p); err != nil {
return err
}
if err := verifySandboxProviders(ctx, client, workflow.Desired); err != nil {
Expand All @@ -149,9 +149,6 @@ func executeResolvedWorkflow(ctx context.Context, workflow *resolvedWorkflow, p
}

opts.Result.setPhase("reconcile")
if err := reconcileProviders(ctx, client, workflow.Desired.Spec.Providers); err != nil {
return err
}
if inferenceConfigured(workflow.Desired.Spec.Inference) {
result, err := reconcile.ReconcileInference(ctx, client, workflow.Desired.Spec.Inference)
if err != nil {
Expand Down
Loading
Loading