diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e771d4c8..15f7b705 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -223,6 +223,9 @@ jobs: - name: ๐Ÿงช Verify complete packaged resource discovery run: bash scripts/package-discovery.test.sh + - name: ๐Ÿงช Verify packaged desired-state file boundaries + run: bash scripts/desired-state-boundaries.test.sh + - name: ๐Ÿงช Self-test the desired-state digest generator # Proves the generator writes exactly what validate-manifests.sh demands โ€” a # generator that disagreed with the gate would leave a sync branch just as diff --git a/AGENTS.md b/AGENTS.md index 4e2bc661..3e34b754 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -237,6 +237,9 @@ plugin membership) is authored here. A plugin may additionally carry ancillary `resources/*.desired-state.json` documents for human copy-paste onboarding. They do not satisfy the minimum auto-discovered-resource requirement and must be linked from the plugin README; `validate-manifests.sh` enforces their provider-neutral contract. + Desired-state files and their `resources/` parents must be regular packaged files and directories, + without symlinks. Run `bash scripts/desired-state-boundaries.test.sh` for complete-package refusal + cases, including ancillary files outside the canonical onboarding document. 3. **agentskills.io spec.** Every bundled `SKILL.md` must validate against the [`agentskills.io`](https://agentskills.io) spec โ€” CI validates each discovered skill in a matrix. 4. **Tool-neutral.** Keep names, descriptions, and README framing cross-tool (VS Code / Copilot CLI / diff --git a/scripts/desired-state-boundaries.test.sh b/scripts/desired-state-boundaries.test.sh new file mode 100644 index 00000000..8a36c345 --- /dev/null +++ b/scripts/desired-state-boundaries.test.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Exercise the complete package gate before any linked onboarding bytes are read. +set -euo pipefail +here=${PACKAGE_SOURCE:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)} +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +git -C "$here/.." archive HEAD > "$work/package.tar" +fail=0 +for scenario in healthy empty-resources canonical-internal canonical-external canonical-malformed \ + canonical-dangling canonical-directory canonical-fifo canonical-parent \ + optional-internal optional-external optional-dangling optional-directory optional-fifo \ + optional-parent optional-parent-empty resources-file; do + root="$work/$scenario"; mkdir -p "$root" + tar -xf "$work/package.tar" -C "$root" + canonical="$root/plugins/agentic-engineering/resources/provider-neutral.desired-state.json" + candidate="$root/plugins/agentic-engineering/resources/other.desired-state.json" + diagnostic='regular packaged desired-state file' + expected=reject + case "$scenario" in + healthy) expected=pass ;; + empty-resources) mkdir -p "$root/plugins/go/resources"; expected=pass ;; + canonical-internal) + mv "$canonical" "$root/plugins/agentic-engineering/original.json" + ln -s ../original.json "$canonical" ;; + canonical-external|canonical-malformed) + cp "$canonical" "$work/$scenario.json" + if [ "$scenario" = canonical-malformed ]; then + printf '%s\n' '{"kind":"AgenticEngineeringDesiredState","spec":null}' > "$work/$scenario.json" + fi + rm "$canonical"; ln -s "$work/$scenario.json" "$canonical" ;; + canonical-dangling) rm "$canonical"; ln -s absent.json "$canonical" ;; + canonical-directory) rm "$canonical"; mkdir "$canonical" ;; + canonical-fifo) rm "$canonical"; mkfifo "$canonical" ;; + canonical-parent) + mv "$root/plugins/agentic-engineering/resources" "$root/original-resources" + ln -s "$root/original-resources" "$root/plugins/agentic-engineering/resources" + diagnostic='regular packaged resources directory' ;; + optional-internal) ln -s provider-neutral.desired-state.json "$candidate" ;; + optional-external) cp "$canonical" "$work/$scenario.json"; ln -s "$work/$scenario.json" "$candidate" ;; + optional-dangling) ln -s absent.json "$candidate" ;; + optional-directory) mkdir "$candidate" ;; + optional-fifo) mkfifo "$candidate" ;; + optional-parent|optional-parent-empty) + mkdir -p "$root/original-resources" + if [ "$scenario" = optional-parent ]; then cp "$canonical" "$root/original-resources/other.desired-state.json"; fi + ln -s "$root/original-resources" "$root/plugins/go/resources" + diagnostic='regular packaged resources directory' ;; + resources-file) + printf 'not a directory\n' > "$root/plugins/go/resources" + diagnostic='regular packaged resources directory' ;; + esac + rc=0 + (cd "$root" && bash "$here/validate-manifests.sh") > "$root/out" 2>&1 || rc=$? + if { [ "$expected" = pass ] && [ "$rc" -eq 0 ] && grep -Fq 'โœ“ desired state plugins/agentic-engineering/resources/provider-neutral.desired-state.json' "$root/out"; } || + { [ "$expected" = reject ] && [ "$rc" -ne 0 ] && grep -Fq "$diagnostic" "$root/out"; }; then + printf 'PASS desired-state-%s\n' "$scenario" + else + printf 'FAIL desired-state-%s exit=%s\n' "$scenario" "$rc" + cat "$root/out"; fail=$((fail+1)) + fi +done +# Producer completion is part of the direct resources-parent census. Plausible +# first records cannot hide a second parent or an unterminated final record. +real_find=$(command -v find) +mkdir -p "$work/bin" +cat > "$work/bin/find" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +if [ "$*" = "plugins -path */resources/*.desired-state.json -print0" ]; then + case "$RESOURCE_FIND_MODE" in + files-empty) exit 0 ;; + files-omitted) printf 'plugins/agentic-engineering/resources/other.desired-state.json\0'; exit 0 ;; + esac +fi +if [ "$*" != 'plugins -mindepth 2 -maxdepth 2 -name resources -print0' ]; then exec "$RESOURCE_FIND_REAL" "$@"; fi +case "$RESOURCE_FIND_MODE" in files-*) exec "$RESOURCE_FIND_REAL" "$@" ;; esac +"$RESOURCE_FIND_REAL" "$@" > "$RESOURCE_FIND_WORK/parents" +case "$RESOURCE_FIND_MODE" in + partial-failure) + IFS= read -r -d '' first < "$RESOURCE_FIND_WORK/parents" + printf '%s\0' "$first"; exit 1 ;; + truncated) + while IFS= read -r -d '' record; do printf '%s\0' "$record"; done < "$RESOURCE_FIND_WORK/parents" + printf 'plugins/absent/resources' ;; + empty-record) cat "$RESOURCE_FIND_WORK/parents"; printf '\0' ;; +esac +STUB +chmod +x "$work/bin/find" +for mode in partial-failure truncated empty-record; do + root="$work/parents-$mode"; mkdir -p "$root" + tar -xf "$work/package.tar" -C "$root" + mkdir -p "$root/plugins/go/resources" + case "$mode" in + partial-failure) diagnostic='Could not enumerate desired-state resource parents' ;; + truncated) diagnostic='Incomplete record in desired-state resource parents' ;; + empty-record) diagnostic='Empty record in desired-state resource parents' ;; + esac + rc=0 + (cd "$root" && env PATH="$work/bin:$PATH" RESOURCE_FIND_REAL="$real_find" \ + RESOURCE_FIND_WORK="$work" RESOURCE_FIND_MODE="$mode" bash "$here/validate-manifests.sh") > "$root/out" 2>&1 || rc=$? + if [ "$rc" -ne 0 ] && grep -Fq "$diagnostic" "$root/out"; then printf 'PASS desired-state-parents-%s\n' "$mode" + else printf 'FAIL desired-state-parents-%s exit=%s\n' "$mode" "$rc"; cat "$root/out"; fail=$((fail+1)); fi +done +for mode in files-empty files-omitted; do + root="$work/$mode"; mkdir -p "$root" + tar -xf "$work/package.tar" -C "$root" + canonical="$root/plugins/agentic-engineering/resources/provider-neutral.desired-state.json" + if [ "$mode" = files-empty ]; then + printf '%s\n' '{"kind":"AgenticEngineeringDesiredState","spec":null}' > "$canonical" + else + cp "$canonical" "$root/plugins/agentic-engineering/resources/other.desired-state.json" + printf '\n[Other desired state](resources/other.desired-state.json)\n' >> "$root/plugins/agentic-engineering/README.md" + fi + rc=0 + (cd "$root" && env PATH="$work/bin:$PATH" RESOURCE_FIND_REAL="$real_find" \ + RESOURCE_FIND_WORK="$work" RESOURCE_FIND_MODE="$mode" bash "$here/validate-manifests.sh") > "$root/out" 2>&1 || rc=$? + if [ "$rc" -ne 0 ] && grep -Fq 'canonical desired-state resource was not enumerated' "$root/out"; then printf 'PASS desired-state-%s\n' "$mode" + else printf 'FAIL desired-state-%s exit=%s\n' "$mode" "$rc"; cat "$root/out"; fail=$((fail+1)); fi +done +printf 'desired-state boundaries: %s failure(s)\n' "$fail" +test "$fail" -eq 0 diff --git a/scripts/validate-manifests.sh b/scripts/validate-manifests.sh index 8bdea563..7db7d63d 100755 --- a/scripts/validate-manifests.sh +++ b/scripts/validate-manifests.sh @@ -638,9 +638,20 @@ validate_desired_state_resources() { local -a asset_components local entrypoint_sha256 actual_entrypoint_sha256 local portfolio_surveyor_sha256 actual_portfolio_surveyor_sha256 + # A linked resources parent is invisible to find's non-following traversal. + # Retain the direct parent census independently, including non-directories. + capture_nul_inventory resource-parents 'desired-state resource parents' \ + find plugins -mindepth 2 -maxdepth 2 -name resources -print0 || return 1 + while IFS= read -r -d '' resource; do + if [ ! -d "$resource" ] || [ -L "$resource" ]; then + echo "::error::$resource: must be a regular packaged resources directory" + return 1 + fi + done < "$inventory_dir/resource-parents" capture_nul_inventory desired-state 'desired-state resources' \ - find plugins -type f -path '*/resources/*.desired-state.json' -print0 || return 1 + find plugins -path '*/resources/*.desired-state.json' -print0 || return 1 local canonical_resource="plugins/agentic-engineering/resources/provider-neutral.desired-state.json" + local canonical_seen=0 local delivery_guardrail="Write-capable roles own selected engineering work from claim through exact-head review and merge; issue-only handoff is allowed only for a named external blocker or missing authority." local version_controlled_delivery="Version-controlled definition surfaces are delivered by draft pull request and owned through exact-head review and merge." local runtime_local_delivery="Runtime-local definition surfaces are delivered in place: back up the current state, apply the change, validate it, and record the reversible before/after evidence." @@ -665,9 +676,12 @@ validate_desired_state_resources() { local portfolio_survey_classifier_argv_contract="**Invoke the classifier only in its flag form, by its resolved installed path:** \`/scripts/classify-default-branch-ci-runs.sh --repo OWNER/REPO --branch BRANCH --head-sha FULL_SHA\`. The helper and the read-only guard accept nothing else: the guard admits only that exact installed sibling path โ€” never a bare basename, a \`PATH\` lookup, or a relative \`../scripts/\` form โ€” and a positional \`OWNER/REPO BRANCH SHA\` is denied as \`not the guarded remote-mode shape\` while the helper itself exits 2 on it, so every executable invocation must carry the resolved path and all three flags." if [ -d plugins/agentic-engineering ]; then - if [ ! -f "$canonical_resource" ]; then + if [ ! -e "$canonical_resource" ] && [ ! -L "$canonical_resource" ]; then echo "::error::$canonical_resource: missing canonical agentic desired-state resource" failed=1 + elif ! regular_packaged_file "$canonical_resource"; then + echo "::error::$canonical_resource: must be a regular packaged desired-state file" + failed=1 elif jq -e . "$canonical_resource" > /dev/null 2>&1 \ && ! jq -e '.kind == "AgenticEngineeringDesiredState"' "$canonical_resource" > /dev/null; then echo "::error::$canonical_resource: canonical agentic desired-state resource must use kind AgenticEngineeringDesiredState" @@ -677,6 +691,12 @@ validate_desired_state_resources() { while IFS= read -r -d '' resource; do resource_failed=0 + if [ "$resource" = "$canonical_resource" ]; then canonical_seen=1; fi + if ! regular_packaged_file "$resource"; then + echo "::error::$resource: must be a regular packaged desired-state file" + failed=1 + continue + fi if ! json_object_unique "$resource"; then echo "::error::$resource: not valid JSON" failed=1 @@ -1522,6 +1542,10 @@ validate_desired_state_resources() { echo "โœ“ desired state $resource" fi done < "$inventory_dir/desired-state" + if [ -d plugins/agentic-engineering ] && [ "$canonical_seen" -ne 1 ]; then + echo "::error::$canonical_resource: canonical desired-state resource was not enumerated" + failed=1 + fi return "$failed" } diff --git a/scripts/validate-manifests.test.sh b/scripts/validate-manifests.test.sh index 37d83b12..e8602d92 100755 --- a/scripts/validate-manifests.test.sh +++ b/scripts/validate-manifests.test.sh @@ -1943,8 +1943,8 @@ exec "$REAL_GREP" "$@" EOF cat > "$WORK/inventory-bin/find" <<'EOF' #!/usr/bin/env bash -case "$INVENTORY_SCOPE:${5:-}" in - desired-state:'*/resources/*.desired-state.json'|provenance:'*/skills/*/SKILL.md') +case "$INVENTORY_SCOPE:${3:-}:${5:-}" in + desired-state:'*/resources/*.desired-state.json':*|desired-state:*:'*/resources/*.desired-state.json'|provenance:*:'*/skills/*/SKILL.md') "$REAL_FIND" "$@" > "$INVENTORY_CAPTURE.full" || exit 8 if [ "$INVENTORY_OUTPUT" = partial ]; then # The current guard requests NUL records; preserve the original guard's