From f48d9adc7031590271b03b091feb47f8b19b3055 Mon Sep 17 00:00:00 2001 From: Zongqi Chen <137198879+zongqichen@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:22:08 +0200 Subject: [PATCH] feat: add optional cfs Agent Skill --- .agents/skills/cfs/SKILL.md | 17 ++++++++ CHANGELOG.md | 2 + Makefile | 5 ++- README.md | 11 +++++ docs/testing.md | 7 +++ scripts/smoke-agent-skill.sh | 81 +++++++++++++++++++++++++++++++++++ test/fixtures/agent-skill/cf | 6 +++ test/fixtures/agent-skill/cfs | 21 +++++++++ 8 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 .agents/skills/cfs/SKILL.md create mode 100755 scripts/smoke-agent-skill.sh create mode 100755 test/fixtures/agent-skill/cf create mode 100755 test/fixtures/agent-skill/cfs diff --git a/.agents/skills/cfs/SKILL.md b/.agents/skills/cfs/SKILL.md new file mode 100644 index 0000000..2fd0192 --- /dev/null +++ b/.agents/skills/cfs/SKILL.md @@ -0,0 +1,17 @@ +--- +name: cfs +description: Safely select an existing cfs Cloud Foundry context and route CF CLI commands in cfs-managed workspaces, including parallel terminal or coding-agent workflows. Use for workspace-default or named-context selection, not for general Cloud Foundry deployment guidance. +--- + +# Use cfs contexts + +Treat the cfs CLI as the source of truth. Do not inspect or edit CF configuration files. + +1. Run `cfs context list --json` to discover context names. +2. For an unqualified request, inspect the workspace default with `cfs status --json --redact`. For an explicitly named context, require an exact existing name and inspect it with `cfs context status --json --redact`. +3. If the requested context is absent or the request does not identify one context unambiguously, stop and ask the user which existing name to use. Do not guess, create, or retarget a context. +4. Run an authorized command through `cf ` for `default`, or exactly `cfs -c ` for a named context. + +Never select a context through `cf target`, a manual `CF_HOME`, `CFS_DISABLE`, or a direct official-CF binary path. Keep shared diagnostics both JSON-formatted and redacted. + +Selecting a context grants no authority to log in, deploy, import, create or remove contexts, change targets, or perform any other mutation. Run such commands only when the user has authorized that specific operation. If inspection shows the selected context is unavailable or not logged in, report that state and ask before changing it. diff --git a/CHANGELOG.md b/CHANGELOG.md index 929d32b..ea66a65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ contract may still change while cfs is pre-1.0. ### Added +- Added an optional Agent Skill for safe cfs context discovery and explicit + named-context routing in Codex, Claude Code, and other Agent Skills clients. - Added workspace-local named contexts for safely running multiple Cloud Foundry targets in one project through `cfs -c ...`. - Added `cfs context create|list|status|remove` and named-context import through diff --git a/Makefile b/Makefile index 21c35a9..b16ef7c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build format-check test test-race vet check security smoke e2e release-check +.PHONY: build format-check test test-race vet check security smoke agent-smoke e2e release-check build: go build -o bin/cfs ./cmd/cfs @@ -23,6 +23,9 @@ security: smoke: ./scripts/smoke-real-cf.sh +agent-smoke: + ./scripts/smoke-agent-skill.sh + e2e: go test -count=1 -race -tags=e2e -v ./test/e2e diff --git a/README.md b/README.md index b9d553e..8c7ca3d 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,16 @@ cfs status --json --redact cfs context status prod --json --redact ``` +The optional [cfs Agent Skill](.agents/skills/cfs/SKILL.md) teaches agents to +discover existing contexts, fail closed on ambiguity, and preserve user +authorization. Install that directory as `.agents/skills/cfs` for Codex. For +Claude Code, copy it to `.claude/skills/cfs` or symlink that path to the +canonical directory. No global agent settings are changed. + +To make invocation explicit, add `Use $cfs for Cloud Foundry context +selection.` to `AGENTS.md`, or `Use /cfs for Cloud Foundry context selection.` +to `CLAUDE.md`. + ## How it works ```text @@ -147,6 +157,7 @@ make check make security make release-check make smoke +make agent-smoke CFS_REAL_CF=/path/to/official/cf make e2e ``` diff --git a/docs/testing.md b/docs/testing.md index a31f37e..77fa30a 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -4,6 +4,13 @@ the test suite on Linux and macOS. `make smoke` runs the official CF CLI with isolated local configuration but makes no network requests. +`make agent-smoke` runs a real Codex session in a temporary repository. It +copies in the cfs Agent Skill, supplies credential-free `cfs` and `cf` fixtures, +and verifies that Codex discovers the skill, performs redacted inspection, and +routes the command through the exact named context. It uses the caller's Codex +authentication and provider configuration, runs with a temporary home and +workspace, and deletes the fixture repository afterward. + Run the full protocol test with an official CF CLI binary: ```sh diff --git a/scripts/smoke-agent-skill.sh b/scripts/smoke-agent-skill.sh new file mode 100755 index 0000000..9066ced --- /dev/null +++ b/scripts/smoke-agent-skill.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +codex_bin=${CFS_CODEX_BIN:-codex} +codex_home=${CODEX_HOME:-$HOME/.codex} + +if ! command -v "$codex_bin" >/dev/null 2>&1; then + echo "agent skill smoke: Codex CLI not found: $codex_bin" >&2 + exit 69 +fi + +test_root=$(mktemp -d "${TMPDIR:-/tmp}/cfs-agent-skill.XXXXXX") +trap 'rm -rf "$test_root"' EXIT + +workspace=$test_root/workspace +fixture_bin=$workspace/.smoke/bin +invocations=$workspace/.smoke/invocations +result=$workspace/.smoke/result.txt +isolated_home=$test_root/home +mkdir -p "$workspace/.agents/skills" "$fixture_bin" "$isolated_home" +cp -R "$repo_root/.agents/skills/cfs" "$workspace/.agents/skills/cfs" +cp "$repo_root/test/fixtures/agent-skill/cfs" "$fixture_bin/cfs" +cp "$repo_root/test/fixtures/agent-skill/cf" "$fixture_bin/cf" +git -C "$workspace" init --quiet +chmod +x "$fixture_bin/cfs" "$fixture_bin/cf" +: >"$invocations" + +prompt='Use the cfs skill to inspect and then list applications in the existing named context `qa-blue`. Execute the required read-only commands; do not merely describe them. Do not log in or change any context. Report the application name you observe.' + +env \ + -u CF_HOME \ + -u CF_PLUGIN_HOME \ + -u CFS_ACTIVE_CONTEXT \ + -u CFS_DISABLE \ + -u CFS_REAL_CF \ + -u CFS_STATE_HOME \ + -u CFS_WORKSPACE_ROOT \ + CFS_AGENT_SMOKE_LOG="$invocations" \ + CODEX_HOME="$codex_home" \ + HOME="$isolated_home" \ + PATH="$fixture_bin:$PATH" \ + "$codex_bin" exec \ + --ephemeral \ + --ignore-rules \ + --sandbox workspace-write \ + --cd "$workspace" \ + --output-last-message "$result" \ + "$prompt" + +required_commands=( + "context list --json" + "context status qa-blue --json --redact" + "-c qa-blue apps" +) +next_required=0 +unexpected=false +while IFS= read -r invocation; do + case "$invocation" in + "context list --json" | "context status qa-blue --json --redact" | "-c qa-blue apps") ;; + *) unexpected=true ;; + esac + if ((next_required < ${#required_commands[@]})) && + [[ "$invocation" == "${required_commands[$next_required]}" ]]; then + next_required=$((next_required + 1)) + fi +done <"$invocations" + +if [[ "$unexpected" == true ]] || ((next_required != ${#required_commands[@]})); then + echo "agent skill smoke: required command sequence was not followed" >&2 + sed 's/^/ /' "$invocations" >&2 + exit 1 +fi + +if [[ $(<"$result") != *sample-app* ]]; then + echo "agent skill smoke: Codex did not report the fixture result" >&2 + exit 1 +fi + +echo "agent skill smoke: passed" diff --git a/test/fixtures/agent-skill/cf b/test/fixtures/agent-skill/cf new file mode 100755 index 0000000..e93aa0d --- /dev/null +++ b/test/fixtures/agent-skill/cf @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -euo pipefail +printf 'cf %s\n' "$*" >>"${CFS_AGENT_SMOKE_LOG:?}" +printf 'unexpected direct cf invocation: %s\n' "$*" >&2 +exit 64 diff --git a/test/fixtures/agent-skill/cfs b/test/fixtures/agent-skill/cfs new file mode 100755 index 0000000..d4ad87f --- /dev/null +++ b/test/fixtures/agent-skill/cfs @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -euo pipefail + +printf '%s\n' "$*" >>"${CFS_AGENT_SMOKE_LOG:?}" + +case "$*" in + "context list --json") + printf '%s\n' '{"contexts":[{"name":"default","context":"11111111","default":true,"created":true},{"name":"qa-blue","context":"22222222","default":false,"created":true}]}' + ;; + "context status qa-blue --json --redact") + printf '%s\n' '{"mode":"managed","source":"git-worktree","context":"22222222","context_name":"qa-blue","target_exit_code":0,"redacted":true}' + ;; + "-c qa-blue apps") + printf '%s\n' 'name requested state' 'sample-app started' + ;; + *) + printf 'unexpected cfs invocation: %s\n' "$*" >&2 + exit 64 + ;; +esac