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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{
"name": "agentic-engineering",
"description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md",
"version": "5.0.0",
"version": "5.0.1",
"source": "./plugins/agentic-engineering"
},
{
Expand Down
2 changes: 1 addition & 1 deletion .github/plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{
"name": "agentic-engineering",
"description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md",
"version": "5.0.0",
"version": "5.0.1",
"source": "./plugins/agentic-engineering"
},
{
Expand Down
2 changes: 1 addition & 1 deletion plugins/agentic-engineering/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "agentic-engineering",
"description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md",
"version": "5.0.0",
"version": "5.0.1",
"author": {
"name": "devantler-tech",
"url": "https://github.com/devantler-tech"
Expand Down
10 changes: 9 additions & 1 deletion plugins/agentic-engineering/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,15 @@ provider-neutral desired-state entry pins the plugin-relative path, reviewed SHA
requirement. The
guard accepts only the exact classifier beside itself and only `--repo`, `--branch`, and a full
`--head-sha`; it refuses the helper's offline `--input` mode. Resolve both scripts from the same
installed, reviewed plugin directory. The classifier captures its fixed paginated API GET in memory,
installed, reviewed plugin directory. Preflight may supply the literal absolute classifier path.
Otherwise, one bare `classify-default-branch-ci-runs.sh` probe through the active guard is a denied
discovery request: it executes nothing and returns `classifier-path-json:` with a JSON string naming
the guard's own executable sibling. The adapter preserves this record in its denial reason and
stderr. Decode it as data, quote the decoded path as one literal shell argument, and submit the
remote-mode command through the same guard. Never evaluate the record or use JSON double quotes as
shell quoting. Missing JSON tooling, a missing executable, or an absent, malformed, ambiguous, or
unusable hint leaves classification `QUERY-UNKNOWN`; directory searches and fallback roots are not
part of discovery. The classifier captures its fixed paginated API GET in memory,
so this exception neither writes an intermediate file nor permits an arbitrary local executable.

**Three residues the guard cannot close from argv alone — the calling runtime must.** They are stated
Expand Down
12 changes: 9 additions & 3 deletions plugins/agentic-engineering/agents/portfolio-surveyor.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,13 +472,19 @@ Judge the default branch by **its current head**, and only by runs that represen
health. Resolve the head first, using the **full-length sha** — a runs endpoint typically returns an
empty set for an abbreviated one, which reads exactly like "nothing failed". Then invoke the shipped
[`../scripts/classify-default-branch-ci-runs.sh`](../scripts/classify-default-branch-ci-runs.sh),
resolving it from the installed, reviewed plugin path. **Invoke the classifier only in its flag
form, by its resolved installed path:**
using the literal absolute path supplied by preflight. If preflight supplied no path, submit
**one** bare `classify-default-branch-ci-runs.sh` probe through the active read-only guard. The
probe stays denied and executes nothing; the refusal carries one `classifier-path-json:` record
encoding the executable sibling of that guard. Decode that JSON string as path data, quote the
decoded path as one literal shell argument, and submit the resulting call through the same guard.
Never evaluate the record or reuse JSON double quotes as shell quoting. A missing, malformed,
ambiguous, or unusable hint means `QUERY-UNKNOWN`; do not hunt directories or try other roots.
**Invoke the classifier only in its flag form, by its resolved installed path:**
`<installed plugin>/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 the first invocation must already carry the resolved path and all three
itself exits 2 on it, so every executable invocation must carry the resolved path and all three
flags. **Do not reimplement the helper** inline. It owns the
paginated API call in memory as well as classification, so a later-page API failure cannot be masked
by a successful consumer of partial output and the read-only role never writes an intermediate file.
Expand Down
2 changes: 1 addition & 1 deletion plugins/agentic-engineering/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "agentic-engineering",
"description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md",
"version": "5.0.0",
"version": "5.0.1",
"author": {
"name": "devantler-tech",
"url": "https://github.com/devantler-tech"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
{
"path": "scripts/forge-readonly-guard.sh",
"sha256": "9591835448efc0d8f08f83da48deacc9a50079f4df79e9db7ed91d9146dd3649",
"sha256": "80bd20ba36537dc33ca21c003e4708731a0e0ab9f52f4ac320be322cb348ca6b",
"executable": true
},
{
Expand Down Expand Up @@ -61,7 +61,7 @@
"portfolio-surveyor": {
"enabled": true,
"mode": "delegated-read-only",
"definitionSha256": "f671faa85d7749bc22ee29fde2fe737dbf409c9bc17c55a4dc324e3ee966f504"
"definitionSha256": "e3a88b11ceb111ffae86f250f9fd468441bad575db57c2329b191affbf756563"
},
"agent-improver": {
"enabledWhen": "Both optional consumer contract sections are present",
Expand Down
17 changes: 17 additions & 0 deletions plugins/agentic-engineering/scripts/forge-readonly-guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1579,6 +1579,9 @@ classify_consumer_classifier() {
return 0
}

# Classify one parsed pipeline segment. $1 is its command text and $2 its
# zero-based position. Return only for an admitted read; deny exits the guard.
# A classifier discovery hint supplies path data while keeping the probe denied.
classify_segment() {
local seg=$1
local index=$2
Expand All @@ -1595,6 +1598,20 @@ classify_segment() {
if [ "$index" -eq 0 ]; then
case "$prog" in
gh | git | "$DEFAULT_BRANCH_CLASSIFIER") ;;
classify-default-branch-ci-runs.sh | */classify-default-branch-ci-runs.sh)
# Discovery remains a denial. Only the guard's own executable sibling
# supplies the hint; caller text is never reflected into this record.
# JSON keeps path characters as data. Missing jq or encoding failure
# leaves discovery unknown without changing command admission.
local path_json=''
if [ -x "$DEFAULT_BRANCH_CLASSIFIER" ]; then
path_json=$(jq -cn --arg path "$DEFAULT_BRANCH_CLASSIFIER" '$path' 2>/dev/null) || path_json=''
fi
if [ -n "$path_json" ]; then
deny "$(printf 'default-branch classifier requires its installed absolute path\nclassifier-path-json: %s' "$path_json")"
fi
deny 'default-branch classifier path is unavailable; report QUERY-UNKNOWN'
;;
*) deny "a read must begin with a forge command, not '$prog'" ;;
esac
fi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,71 @@ else
fi
fi

# Discover through the real adapter, then admit and execute the returned literal
# path. Relocating the installation must not require knowing a versioned root.
# Only the external forge response is stubbed; neither guard nor classifier is.
mkdir -p "$TMP/forge-bin"
cat >"$TMP/forge-bin/gh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
[ "$*" = 'api --paginate --slurp --method GET repos/owner/repo/actions/runs -f head_sha=1111111111111111111111111111111111111111 -f branch=main -F per_page=100' ] || exit 1
printf '%s\n' '{"total_count":1,"workflow_runs":[{"id":10,"workflow_id":11,"event":"push","conclusion":"failure","created_at":"2026-07-14T09:00:00Z","html_url":"https://example.test/fail","name":"CI"}]}'
EOF
chmod +x "$TMP/forge-bin/gh"
for install_dir in "$TMP/install-v1" "$TMP/relocated plugin 'quoted' \$literal"; do
mkdir -p "$install_dir"
install_dir=$(CDPATH='' cd -- "$install_dir" && pwd -P)
cp "$GUARD" "$WRAPPER" "$HERE/classify-default-branch-ci-runs.sh" "$install_dir/"
for probe in 'classify-default-branch-ci-runs.sh' '/incorrect/install/classify-default-branch-ci-runs.sh'; do
st=0
out=$(run_wrapper "$(hook_stdin "$probe")" "$install_dir/surveyor-forge-readonly.sh" 2>"$TMP/discovery.err") || st=$?
reason=$(printf '%s' "$out" | jq -r '.hookSpecificOutput.permissionDecisionReason')
resolved=$(printf '%s' "$reason" | jq -Rse '
split("\n") | map(select(startswith("classifier-path-json: ")))
| if length == 1 then .[0] | ltrimstr("classifier-path-json: ") | fromjson
else error("missing or ambiguous classifier path") end' -r 2>/dev/null) || resolved=''
if [ "$st" -eq 2 ] && [ "$resolved" = "$install_dir/classify-default-branch-ci-runs.sh" ] &&
[ "$(cat "$TMP/discovery.err")" = "$reason" ]; then
pass
else
fail "discovery must deny and carry the exact relocated path in JSON and stderr (st=$st)"
continue
fi
# @sh produces one literal shell word, preserving quotes and dollar signs.
# This is not eval: only the complete command that passed the guard runs.
quoted=$(printf '%s' "$resolved" | jq -Rs '@sh' -r)
cmd="$quoted --repo owner/repo --branch main --head-sha 1111111111111111111111111111111111111111"
if run_wrapper "$(hook_stdin "$cmd")" "$install_dir/surveyor-forge-readonly.sh" >/dev/null 2>&1; then
st=0
result=$(PATH="$TMP/forge-bin:$PATH" bash -c "$cmd" 2>"$TMP/classifier.err") || st=$?
if [ "$st" -eq 0 ] && [ "$result" = $'11\tfailure\thttps://example.test/fail\tCI\tpush\t\t2026-07-14T09:00:00Z\t10' ]; then
pass
else
fail "discovered guarded classifier must return the named red workflow (st=$st)"
fi
else
fail "the discovered literal remote-mode command must be admitted"
fi
st=0
run_wrapper "$(hook_stdin "$quoted --input -")" "$install_dir/surveyor-forge-readonly.sh" >/dev/null 2>&1 || st=$?
if [ "$st" -eq 2 ]; then pass; else fail 'discovery must not admit offline input'; fi
done
for availability in nonexecutable missing; do
if [ "$availability" = nonexecutable ]; then
chmod -x "$install_dir/classify-default-branch-ci-runs.sh"
else
rm "$install_dir/classify-default-branch-ci-runs.sh"
fi
st=0
out=$(run_wrapper "$(hook_stdin 'classify-default-branch-ci-runs.sh')" "$install_dir/surveyor-forge-readonly.sh" 2>/dev/null) || st=$?
if [ "$st" -eq 2 ] && ! printf '%s' "$out" | grep -q 'classifier-path-json:'; then
pass
else
fail "a $availability classifier must deny without a usable path hint"
fi
done
done

# --- agent scoping (opt-in): SURVEYOR_FORGE_READONLY_SCOPE ---
#
# A PreToolUse `matcher` filters on tool name only, so a Bash matcher fires for
Expand Down
2 changes: 1 addition & 1 deletion scripts/validate-manifests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ validate_desired_state_resources() {
# merged-PR channel silently loses the field, since the two rows carry the same token.
local portfolio_survey_comment_disclosure_row="CANDIDATE-MAINTAINER-COMMENT <repo> #<n> (draft?, merged?) — disclosure=<routine|interactive|none>"
local portfolio_survey_call_shape_contract="**Every forge read is one command in one call.** The read-only guard refuses on shape before it ever inspects intent: output redirection, \`;\`, \`&\`, \`&&\`, a newline, command substitution, and any leading program that is neither a forge command nor a reviewed helper this definition names are all denied, so an ordinary shell idiom silently costs the read. Emit exactly one forge command per call and reduce it in-band with \`--paginate\` and \`--jq\`, or a pipe into the allowlisted read-only filters; never redirect to a scratch file. Sweep repositories with one call per repository or one org-wide search, never a \`for\` loop. Take every timestamp from a payload you already read, never from \`date\`. Select with \`--jq\` rather than \`grep -oE\` or \`xargs\`. A shape denial is a lost read that reads exactly like no evidence: mark the affected evidence \`QUERY-UNKNOWN\` and reissue in the admitted shape — never work around the guard."
local portfolio_survey_classifier_argv_contract="**Invoke the classifier only in its flag form, by its resolved installed path:** \`<installed plugin>/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 the first invocation must already carry the resolved path and all three flags."
local portfolio_survey_classifier_argv_contract="**Invoke the classifier only in its flag form, by its resolved installed path:** \`<installed plugin>/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
Expand Down
2 changes: 1 addition & 1 deletion scripts/validate-manifests.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@ An incomplete candidate can never be classified clean: no `CLEAR`, `MERGE-READY`

**Every forge read is one command in one call.** The read-only guard refuses on shape before it ever inspects intent: output redirection, `;`, `&`, `&&`, a newline, command substitution, and any leading program that is neither a forge command nor a reviewed helper this definition names are all denied, so an ordinary shell idiom silently costs the read. Emit exactly one forge command per call and reduce it in-band with `--paginate` and `--jq`, or a pipe into the allowlisted read-only filters; never redirect to a scratch file. Sweep repositories with one call per repository or one org-wide search, never a `for` loop. Take every timestamp from a payload you already read, never from `date`. Select with `--jq` rather than `grep -oE` or `xargs`. A shape denial is a lost read that reads exactly like no evidence: mark the affected evidence `QUERY-UNKNOWN` and reissue in the admitted shape — never work around the guard.

**Invoke the classifier only in its flag form, by its resolved installed path:** `<installed plugin>/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 the first invocation must already carry the resolved path and all three flags.
**Invoke the classifier only in its flag form, by its resolved installed path:** `<installed plugin>/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.
EOF
awk -v name="$name" '
index($0, "[`" name "`](plugins/" name "/)") {
Expand Down
Loading