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
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,11 @@ optional. Filesystem inventories preserve complete filenames with NUL-delimited

Agent identity and installed-source checks share a minimum frontmatter observer. It requires a
closed header, unique top-level keys, text-valued agent identity, and a usable GitHub repository at
the direct `metadata.github-repo` key. Unsupported header syntax is refused; this observer does not
replace the skill specification validator. The bundled-edit guard uses the same source observation
the direct `metadata.github-repo` key. Plain identity values reject reserved YAML indicators and
mapping separators. Literal and folded identity blocks require a valid single indentation/chomping
declaration and content meeting the declared or inferred indentation. Unsupported header syntax is
refused; this observer does not replace the skill specification validator. The bundled-edit guard uses
the same source observation
at the base commit and preserves whole path components, including embedded and trailing newlines. Body examples
never supply provenance, and malformed base provenance remains UNKNOWN.

Expand Down
52 changes: 44 additions & 8 deletions scripts/frontmatter.awk
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Observe the supported scalar/mapping header shape, never claim full YAML validation.
function trim(s) { sub(/^[[:space:]]+/,"",s); sub(/[[:space:]]+$/,"",s); return s }
# YAML separation uses ASCII spaces and tabs; Unicode spaces are scalar content.
function yaml_trim(s) { sub(/^[ \t]+/,"",s); sub(/[ \t]+$/,"",s); return s }
# Normalize the same Unicode whitespace observed in escaped scalars for presence
# only. Preserve the original scalar for provenance; use complete UTF-8 strings
# so the C and UTF-8 locales agree without partial-byte regex ranges.
Expand Down Expand Up @@ -56,15 +58,15 @@ function quoted_text(s,q, i,c,n,hex,j,d,code,out) {
quoted_ok=1; return out
}
function scalar(s, q,i,c,escaped,tail) {
s=trim(s); scalar_ok=0
s=yaml_trim(s); scalar_ok=0
q=substr(s,1,1)
if (q == "\"" || q == "\047") {
for (i=2;i<=length(s);i++) {
c=substr(s,i,1)
if (q == "\"" && !escaped && c == "\\") { escaped=1; continue }
if (!escaped && c == q) {
if (q == "\047" && substr(s,i+1,1) == q) { i++; continue }
tail=trim(substr(s,i+1))
tail=yaml_trim(substr(s,i+1))
if (tail != "" && substr(tail,1,1) != "#") return ""
s=quoted_text(substr(s,2,i-2),q)
scalar_ok=(quoted_ok && nonblank_text(s)); return s
Expand All @@ -73,8 +75,12 @@ function scalar(s, q,i,c,escaped,tail) {
}
return ""
}
sub(/[[:space:]]+#.*$/,"",s); s=trim(s)
if (s == "" || substr(s,1,1) == "#" || s ~ /^[\[\{&*!|>]/ ||
sub(/[ \t]+#.*$/,"",s); s=yaml_trim(s)
# A plain scalar cannot start with a reserved indicator, contain a mapping
# separator, or turn its first token into a sequence/mapping declaration.
# The same punctuation remains valid inside supported quoted scalars.
if (s == "" || s ~ /^[\[\]\{\},#&*!|>%@`]/ ||
s ~ /^[-?:]([ \t]|$)/ || s ~ /:([ \t]|$)/ ||
s ~ /^(~|null|Null|NULL|true|True|TRUE|false|False|FALSE)$/ ||
s ~ /^[-+]?([0-9][0-9_]*(\.[0-9_]*)?|\.[0-9_]+)([eE][-+]?[0-9_]+)?$/ ||
s ~ /^[-+]?0([xX][0-9a-fA-F_]+|[oO][0-7_]+|[bB][01_]+)$/ ||
Expand All @@ -88,16 +94,46 @@ function scalar(s, q,i,c,escaped,tail) {
if (NR == 1) { if ($0 !~ /^---[[:space:]]*$/) bad=1; next }
if ($0 ~ /^---[[:space:]]*$/) { closed=1; exit }
if (bad) next
if (mode == "text" && block && $0 ~ /^[[:space:]]/ && nonblank_text($0)) found=1
if (mode == "text" && block) {
indent=length($0)
if (match($0,/[^ ]/)) indent=RSTART-1
rest=substr($0,indent+1)
blank_line=($0 ~ /^[ \t]*$/)
# A dedented comment ends the scalar. Later indented text cannot resume it
# before a new top-level key supplies another mapping or scalar context.
if ((!blank_line && indent == 0) ||
(rest ~ /^#/ && indent < (block_indent ? block_indent : 1))) {
block=0; after_block=1
} else if (blank_line && rest == "") {
if (!block_indent && indent > block_blank_indent) block_blank_indent=indent
} else {
# Tabs may be scalar content only after an explicit or inferred indentation
# is established; they cannot establish the first line's indentation.
if (!block_indent && rest ~ /^\t/) bad=1
if (!block_indent) {
block_indent=indent
if (block_blank_indent > indent) bad=1
}
if (indent < 1 || indent < block_indent) bad=1
else if (nonblank_text($0)) found=1
}
}
if (after_block && $0 !~ /^[ \t]*(#.*)?$/ && $0 !~ /^[A-Za-z_][A-Za-z0-9_-]*:/) bad=1
if ($0 ~ /^[[:space:]]*(#.*)?$/) next
if ($0 ~ /^[A-Za-z_][A-Za-z0-9_-]*:/) {
key=$0; sub(/:.*/,"",key)
if (++keys[key] > 1) bad=1
block=0; in_metadata=(key == "metadata"); depth=0
block=0; block_indent=0; block_blank_indent=0; after_block=0
in_metadata=(key == "metadata"); depth=0
raw=substr($0,length(key)+2)
if (mode == "text" && key == field) {
v=trim(raw); sub(/[[:space:]]+#.*$/,"",v)
if (v ~ /^[|>][0-9+-]*$/) block=1
v=yaml_trim(raw); sub(/[ \t]+#.*$/,"",v)
# YAML permits one nonzero indentation digit and one chomping indicator
# in either order. An omitted digit is inferred from the first text line.
if (v ~ /^[|>]([1-9][+-]?|[+-][1-9]?)?$/) {
block=1
if (match(v,/[1-9]/)) block_indent=substr(v,RSTART,1)+0
}
else { v=scalar(raw); found=scalar_ok }
}
if (mode == "repository" && in_metadata) {
Expand Down
81 changes: 81 additions & 0 deletions scripts/package-boundaries.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,87 @@ for scenario in valid number missing outside directory empty invalid-frontmatter
expected=reject; case $scenario in valid|empty) expected=pass ;; esac
gate "agent-$scenario" "$expected" "$root"
done
# Identity text must be usable YAML, rather than merely a nonempty encoded value.
# Each case runs through the complete gate with both supported identity fields.
for field in name description; do
for scenario in plain quoted-colon plain-colon leading-dash leading-question leading-colon \
leading-percent leading-at leading-backtick leading-bracket leading-brace leading-comma \
trailing-colon safe-colon safe-dash safe-question safe-leading-colon unicode-colon unicode-dash \
unicode-question unicode-leading-indicator unicode-trailing-colon valid-strip valid-keep valid-strip-first valid-inferred valid-empty-lines \
valid-leading-blank valid-explicit-tab-blank valid-explicit-tab-content valid-later-tab \
valid-explicit-more-indent valid-large-indent block-double-plus block-double-minus block-zero \
block-double-digit block-plus-minus block-minus-plus block-small-indent block-dedent \
block-oversized-leading-blank block-leading-tab block-leading-tab-content block-small-tab-indent \
block-continued-after-comment; do
root="$work/yaml-$field-$scenario"; fixture "$root"
cp "$root/plugins/alpha/plugin.json" "$root/plugins/alpha/.claude-plugin/plugin.json"
mkdir -p "$root/plugins/alpha/agents"
value='' body='' expected=reject
case "$scenario" in
plain) value=sample; expected=pass ;;
quoted-colon) value='"alpha: beta"'; expected=pass ;;
plain-colon) value='alpha: beta' ;;
leading-dash) value='- alpha' ;;
leading-question) value='? alpha' ;;
leading-colon) value=': alpha' ;;
leading-percent) value='%alpha' ;;
leading-at) value='@alpha' ;;
leading-backtick) value='`alpha' ;;
leading-bracket) value=']alpha' ;;
leading-brace) value='}alpha' ;;
leading-comma) value=',alpha' ;;
trailing-colon) value='alpha:' ;;
safe-colon) value='alpha:beta'; expected=pass ;;
safe-dash) value='-alpha'; expected=pass ;;
safe-question) value='?alpha'; expected=pass ;;
safe-leading-colon) value=':alpha'; expected=pass ;;
unicode-colon) value=$'alpha:\302\240beta'; expected=pass ;;
unicode-dash) value=$'-\302\240alpha'; expected=pass ;;
unicode-question) value=$'?\302\240alpha'; expected=pass ;;
unicode-leading-indicator) value=$'\302\240@alpha'; expected=pass ;;
unicode-trailing-colon) value=$'alpha:\302\240'; expected=pass ;;
valid-strip) value='|2-'; body=' alpha'; expected=pass ;;
valid-keep) value='>+2'; body=' alpha'; expected=pass ;;
valid-strip-first) value='|-2'; body=' alpha'; expected=pass ;;
valid-inferred) value='>'; body=$' alpha\n beta'; expected=pass ;;
valid-empty-lines) value='|'; body=$'\n\n alpha\n\n beta'; expected=pass ;;
valid-leading-blank) value='|'; body=$' \n alpha'; expected=pass ;;
valid-explicit-tab-blank) value='|2'; body=$' \t\n alpha'; expected=pass ;;
valid-explicit-tab-content) value='|2'; body=$' \talpha'; expected=pass ;;
valid-later-tab) value='|'; body=$' alpha\n \tbeta'; expected=pass ;;
valid-explicit-more-indent) value='|1'; body=$' alpha\n beta'; expected=pass ;;
valid-large-indent) value='|9'; body=' alpha'; expected=pass ;;
block-double-plus) value='|++'; body=' alpha' ;;
block-double-minus) value='|--'; body=' alpha' ;;
block-zero) value='|0'; body=' alpha' ;;
block-double-digit) value='|99'; body=' alpha' ;;
block-plus-minus) value='|+-'; body=' alpha' ;;
block-minus-plus) value='|-+'; body=' alpha' ;;
block-small-indent) value='|9'; body=' alpha' ;;
block-dedent) value='|'; body=$' alpha\n beta' ;;
block-oversized-leading-blank) value='|'; body=$' \n alpha' ;;
block-leading-tab) value='|'; body=$'\t\n alpha' ;;
block-leading-tab-content) value='|'; body=$' \talpha' ;;
block-small-tab-indent) value='|2'; body=$' \t\n alpha' ;;
block-continued-after-comment) value='|'; body=$' alpha\n# comment\n beta' ;;
esac
header="$root/plugins/alpha/agents/sample.agent.md"
printf '%s\n' --- > "$header"
if [ "$field" = description ]; then printf 'name: sample\n' >> "$header"; fi
printf '%s: %s\n' "$field" "$value" >> "$header"
if [ -n "$body" ]; then printf '%s\n' "$body" >> "$header"; fi
if [ "$field" = name ]; then printf 'description: Example agent.\n' >> "$header"; fi
printf '%s\n' --- >> "$header"
# shellcheck disable=SC2016 # Backticks are literal catalogue markup.
sed 's/`example` |/`example`, `sample` |/' "$root/docs/plugins.md" > "$root/new"
mv "$root/new" "$root/docs/plugins.md"
gate "yaml-$field-$scenario" "$expected" "$root"
if [ "$expected" = reject ] && ! grep -Fq "must declare a non-empty '$field'" "$root/out"; then
printf 'FAIL yaml-%s-%s did not identify the malformed identity\n' "$field" "$scenario"
fail=$((fail+1))
fi
done
done
# The real desired-state fixture must be valid before ambiguity is introduced;
# otherwise another schema failure could mask the repeated protected declaration.
for scenario in healthy duplicate; do
Expand Down
Loading