Skip to content

feat(context): nan context ls|show|create|push|pull|edit|rm (agent contexts C1) - #62

Merged
sre-helmcode merged 4 commits into
mainfrom
feat/contexts-c1
Oct 26, 2026
Merged

sre-helmcode merged 4 commits into
mainfrom
feat/contexts-c1

Conversation

@sre-helmcode

@sre-helmcode sre-helmcode commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

What

The CLI side of agent contexts (helmcode/nan-devops AGENT-CONTEXT.md §6, row C1), against the /v1/contexts API merged in cloud-api (#391 uploads, #395 CRUD/publish/download; still CONTEXTS_ENABLED=false in prod, so these commands answer "not available for your account yet" there).

nan context ls [--json]
nan context show <NAME|id> [--json]
nan context create NAME [--description TEXT] [--from DIR] [--json]
nan context push <NAME|id> DIR [--create] [--dry-run] [--force] [--allow-secrets] [--skip-invalid] [--allow-empty] [--json]
nan context pull <NAME|id> DIR [--force]
nan context edit <NAME|id> [--name NEW] [--description TEXT] [--json]
nan context rm <NAME|id> [--yes]
  • internal/contexts/path.go: verbatim copy of cloud-api's validator (cloud-api 30af3381), plus its test table (path_test.go, the middleware test left out) and a pins test.
  • internal/api/contexts.go: client for every route; ContextError keeps the top-level extras (current_version, sha256, files, findings, used/quota, automations) and Retry-After, and unwraps to APIError.
  • cmd/context.go:
    • push: NFC-normalises names (NFD from macOS), skips dotfiles with a notice, lists every refused file with its reason and exits 65 before any upload unless --skip-invalid (CLAUDE.md gets a rename hint; local reasons symlink, too_large, skill_invalid added), checks version limits, hashes, blobs/check, uploads only missing blobs (4 at a time; single PUT up to 16 MiB, multipart above with resume via upload status in a push and via a remembered upload id in the user cache dir across pushes), publishes with base_version + Idempotency-Key. blob_missing -> re-check and re-upload; version_conflict -> exit 1, or --force prints the diff and rebases; context_secret_detected -> findings + exit 65, --allow-secrets sends allow_secret_like; 429/upload_in_progress/blob_busy/503 wait out Retry-After up to 2 min (3 tries).
    • pull: refuses a non-empty DIR (--force overrides) before any request; validates every server path with the shared validator (.., absolute, case/file-folder collisions, reserved names, dotfiles, non-NFC) and writes nothing if one fails; writes via os.OpenRoot; downloads pinned to the version read; sha256 + size verified, bad files removed.
  • Exit codes: 0, 1 (changed while pushing), 64 usage, 65 content refused, 69 unavailable.

Tests (spec first, httptest fake of the real API: cmd/context_fake_test.go)

  • Layout checks + refused-file report golden, --skip-invalid, NFC from NFD input (and NFD/NFC collision on Linux), symlinks, AGENTS.md limit.
  • Dry-run goldens (text and JSON), push golden, only missing blobs uploaded (dedup across paths), up-to-date push, --create.
  • Multipart: resume after a failed part; resume across runs from a saved upload id; stale id starts over.
  • 409 version_conflict message and --force rebase; --allow-secrets; blob_missing re-upload; Retry-After honoured (and not slept past 2 min); server per-file refusals; quota.
  • pull: goldens, executable bit, pinned version, round trip push/pull, non-empty DIR, 11 hostile path sets (nothing written inside or outside DIR), corrupted bytes.
  • ls/show goldens, edit, rm (context_in_use), surface off, name validation, help.
  • API client unit tests; --json contract cases added to json_output_test.go.

Gates: go build ./... && go vet ./... && go test ./... green; go test -race ./... green; gofmt clean; govulncheck: no vulnerabilities; cross-compile linux/windows/darwin OK.

Review round 1 (QA + Security), fixed in 4cefc87

  • A DIR that is a symlink is resolved once and walked (it used to publish an empty tree). push refuses an empty tree over a non-empty version unless --allow-empty.
  • push hashes/uploads through os.OpenRoot. pull writes a temporary file, verifies it, sets the mode both ways and renames it. A failed download never removes the member's file, and an existing link or folder at a path is never replaced. Total is capped at 250 MiB. A partial pull says how many files were written.
  • NAME|id is validated locally. The resume cache is keyed by API host + credential hash. Windows keeps the remote executable bit for unchanged files. Dead ContextVersions removed.
  • New tests (cmd/context_hardening_test.go + blob_missing re-check assertions):
    • symlinked files dir and symlinked file on pull;
    • symlinked DIR on push and the empty-tree guard;
    • limits: 31 skills, 1001 files, 250 MiB, 100 MiB per file, SKILL.md 64 KiB;
    • resume cache: abort of an unusable remembered upload, forgetting upload_not_found/upload_expired mid-parts;
    • exit mappings, the inherited exec bit, pull --force modes, total cap, local ref checks.
  • The fake now follows cloud-api's publish order (tree, blobs, base_version), decodes strictly and answers 411.
  • A spot mutation check of the new guards (7 mutations) is caught by the suite.
  • QA blocker 5 (path.go drift check) is handled in cloud-api #397 (Lead).

Deviations / deploy notes

  • golang.org/x/text is v0.39.0 (cloud-api was on v0.33.0, which has GO-2026-5970 in unicode/norm, reached by the path rules). The tables differ between the two, so exact parity matters: depends on helmcode/nan-cloud-api#397 (x/text to v0.39.0, Go stays 1.25.0), merge it first.
  • The cross-repo parity check lives in cloud-api's CI (Lead): nan-cli is public and can be read without a token, cloud-api is private. No CI job or secret is added here. nan-cli keeps the unit test internal/contexts/pins_test.go, which pins x/text v0.39.0 and Unicode tables 15.0.0. Go 1.25 (cloud-api) and 1.26.10 (nan-cli, GO-2026-6809) both carry Unicode 15.0.0.
  • pull --version N is left out: the API has no route that lists the files of an older version.
  • Reads use the write credential order (session or platform token); sk- keys are not offered for contexts in the CLI.
  • No version bump or release (C2 nan run --context comes later).

🤖 Generated with Claude Code

barckcode and others added 4 commits October 26, 2026 19:59
…ntexts C1)

The CLI side of agent contexts (helmcode/nan-devops AGENT-CONTEXT.md §6, C1),
against the /v1/contexts API merged in cloud-api (#391, #395).

- internal/contexts/path.go is a verbatim copy of cloud-api's validator, with
  its parity table (path_test.go) and a test of the shared pins.
- push: walks DIR, NFC-normalises names (macOS gives NFD), skips dotfiles
  with a notice, prints every refused file with its reason and exits 65
  before uploading unless --skip-invalid (CLAUDE.md gets a rename hint),
  checks the version limits, hashes, asks blobs/check, uploads only missing
  blobs (4 at a time; single PUT up to 16 MiB, multipart above, resumed
  within a push via the upload status and across pushes via a remembered
  upload id), publishes with base_version and an Idempotency-Key. Handles
  blob_missing (re-check, re-upload), version_conflict (exit 1, or --force
  shows the diff and rebases), context_secret_detected (--allow-secrets),
  per-file refusals and Retry-After (up to 2 minutes). --dry-run prints the
  add/change/remove list and the bytes to upload.
- pull: refuses a non-empty DIR (--force overrides), validates every server
  path with the shared validator before writing anything, writes through
  os.OpenRoot, pins downloads to the version read and verifies sha256/size.
- golang.org/x/text becomes a direct dependency at v0.39.0. CI gains a job
  (scripts/check-cloud-api-pins.sh, secret CLOUD_API_READ_TOKEN) comparing
  x/text, the Go toolchain's Unicode tables and path.go with cloud-api.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nan-cli is public and cloud-api private: cloud-api's CI reads this
repository's go.mod and path.go without a token, so the check lives there
and no secret is added here. The unit test keeps pinning golang.org/x/text
v0.39.0 and the Unicode tables (15.0.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security:
- A DIR that is a symlink is resolved once and its target walked; WalkDir
  alone saw one non-directory and push published an empty tree.
- push refuses to publish no files over a version that has some unless
  --allow-empty (also after a --force rebase).
- Files are hashed and uploaded through os.OpenRoot on DIR and refused if
  they stopped being regular files.
- pull downloads into a temporary name, verifies, sets the mode both ways
  and renames into place: a failed download never deletes a member's file,
  and a link or folder at a file's path is never replaced. The total of a
  version is capped at 250 MiB; a partial pull says how many files were
  written and points at --force.
- NAME|id is checked locally (context name or id) before any request.
- Remembered multipart uploads are keyed by API host and credential hash.

QA:
- Tests for a symlinked folder inside DIR and a symlinked file on pull,
  the blob_missing re-check body and single PUT, the version limits (31
  skills, 1001 files, 250 MiB, 100 MiB per file, SKILL.md 64 KiB), aborting
  an unusable remembered upload, forgetting an upload dropped mid-parts,
  exit mappings (entitlement_required and others), the inherited
  executable bit, pull --force modes, the total cap and local ref checks.
- Unchanged files keep the remote executable bit where the file system has
  none (Windows).
- Dead ContextVersions/ContextVersionInfo removed.
- The fake follows cloud-api's publish check order, decodes strictly and
  answers 411 without Content-Length.
- Help says API keys are not used here; pins comment wrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sre-helmcode
sre-helmcode merged commit 9ee99eb into main Oct 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants