Skip to content

feat(body-field-contract): a shared body-field parser contract, a Groovy implementation, Git/SVN storage, and custom formats at setup - #1698

Merged
potiuk merged 9 commits into
mainfrom
feat/body-field-contract
Oct 18, 2026
Merged

potiuk merged 9 commits into
mainfrom
feat/body-field-contract

Conversation

@potiuk

@potiuk potiuk commented Oct 18, 2026

Copy link
Copy Markdown
Member

Follow-up to #1680 and #1686 (#1672, umbrella #1342).

What this adds

The security trackers' body fields are read and written through one parser since #1680.
This PR turns that parser's behaviour into a contract other implementations can follow, and puts it to work in three places.

  • A body-field parser contract — tools/body-field-contract/ (contract:body-field).
    contract.md states the rules normatively: every column-0 ### line ends a field, closed code blocks (CommonMark fences, Jira {code}/{noformat}) only decide which heading starts a field, a field quoted only inside a closed block is absent, names are whitespace-lenient and case-sensitive, and ambiguous bodies come back as needs_decision (duplicate, container-heading, macro-ambiguous) instead of a guess.
    conformance/cases.jsonl holds 98 cases; a runner checks every implementation against them, including the byte-identical Python copies in vetted-ops, local-tracker and the Jira bridge.
  • A Groovy implementation — tools/body-field-contract/groovy/BodyFieldParser.groovy, for JVM-side tooling such as the Jira bridge.
    It passes all 98 cases (Groovy 6.0.0). No CI workflow installs Groovy yet, so its pytest adapter skips there.
  • File storage (Git / SVN) on the parser — local-tracker keeps each file's own line endings when it rewrites a field, and a storage conformance suite runs the cases through real Git and SVN working copies.
  • Custom formats at setup — when the tracker uses Git/SVN storage and detect reports a field layout the stock parser does not cover, setup generates a parser for it in .apache-magpie-overrides/tools/local-tracker/parsers/ from the formats/ template, following the contract's format adapter interface, runs the conformance suite on it, and suggests upstreaming it to Magpie through setup-override-upstream.

How a custom parser is trusted

The parser is code from the tracker's repository, and vetted operations run it outside the sandbox.

  • It loads only when its path stays in the parser directory (no .., no symlink) and its SHA-256 matches both the committed descriptor and the user's own confirmation; anything else stops with exit 2, never falling back to the default parser.
  • The confirmation lives in ~/.config/apache-magpie/local-tracker-parsers.json, under the working copy's real path: outside every working copy, so nothing a commit carries can stand in for it (a committed .svn/ directory included).
  • trust-parser asks on the controlling terminal and refuses without one; setup hands the user the command to run in their own terminal and never runs it. It then loads the parser and prints parser_loads.
  • The floor gains Edit(~/.config/apache-magpie/local-tracker-parsers.json) in permissions.deny, since the agent's file tools are not sandboxed, and sandbox-lint requires it.
  • The secure setup's sandbox reads nothing in ~/.config/apache-magpie, so a custom-layout tracker is read through vetted operations; a sandboxed command says the parser cannot be verified there (and doctor marks the check unverifiable_here) instead of asking for a re-confirmation. Default-layout trackers never read the record.

Fixes from the security review

  • Report text carrying its own \r\n or lone \r was written as it came, leaving the tracker file mixing line endings, which every later write refuses. Untrusted text is now normalised before it is escaped, and every write normalises before putting the file's convention back.
  • The first version kept the confirmation in <git-common-dir> / .svn/apache-magpie/, and the agent ran trust-parser: a committed .svn/ record could confirm a parser, and the agent could confirm one itself. Both are closed as described above.

Also: the Jira bridge tests keep the JVM on IPv4 loopback (-Djava.net.preferIPv4Stack=true), so they pass inside the macOS sandbox, where the IPv6 loopback socket is denied.

Verification

  • prek run --all-files passes.
  • Conformance: 98/98 on every Python copy and on Groovy; storage conformance on Git and SVN.
  • local-tracker: 226 tests, including the line-ending, committed-record, no-terminal and unreadable-record cases.
  • Regression check over the 379 airflow-s tracker bodies: no difference from main beyond CR removal.

Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 5)

Generated-by: Claude Code (Opus 5) following the guidelines

🤖 Generated with Claude Code

@github-actions github-actions Bot added family:setup setup-* skills family:tools tools/* family:docs Docs, MISSION.md, READMEs family:ci .github workflows, prek, validators capability:platform Framework / agent substrate skills (install, verify, doctor, override, status, setup bootstrap) contract:tracker Tool capability: issue / board / label backend contract:source-control Tool capability: branch / commit / diff / push (VCS) contract:change-request Tool capability: proposed-change review + merge gate (PR / MR / Gerrit change) substrate:analytics Tool substrate: read-only metrics / dashboards / renderers substrate:sandbox Tool substrate: agent isolation, egress control, settings audit substrate:framework-dev Tool substrate: build / validate / eval the framework itself contract:people contract:security-tracker Tool capability: security-tracker backend for embargoed reports (GitHub / files / Jira) labels Oct 18, 2026
The bridge-write tests run bridge.groovy against a mock JIRA server on
127.0.0.1 with a clean environment: PATH, HOME, the tracker variables and
the token. That environment drops JAVA_TOOL_OPTIONS, so a shell that sets
-Djava.net.preferIPv4Stack=true loses it and the JVM opens an IPv6 socket
to the loopback address. Under the agent sandbox on macOS that connect is
refused (`SocketException: Operation not permitted`), and every test that
reaches the server fails, so the workspace pytest hook failed on every
commit that touched a Python tool.

The test environment now passes JAVA_TOOL_OPTIONS through with
-Djava.net.preferIPv4Stack=true added when it is not there already. IPv4
loopback is harmless everywhere else. All 28 bridge-write tests pass inside
the sandbox. No other test starts a JVM with a clean environment.

Part of #1672

Generated-by: Claude Opus 5
…h a shared conformance suite

How a tracker body's `### <Name>` fields are found was stated only in
prose spread over the GitHub tool's README, the issue-template doc and
the parser's docstring, so a second implementation (another language, a
local parser for a custom report format) had nothing to check itself
against.

tools/body-field-contract/ states the rules once, language-neutral, in
RFC 2119 terms, each with a `BF-*` identifier: line endings, field
boundaries (every column-0 `###` line ends a field, code block or not),
names, closed code blocks, which copy of a heading is the field (the one
outside every closed block; a field quoted only inside closed blocks is
absent), the container-heading and mid-line Jira macro
(`macro-ambiguous`) decisions, the four operations with their inputs,
outputs and errors, and what a writer of untrusted text must escape
(escape text, escape body).

conformance/cases.jsonl holds 98 cases, each naming the clause it tests;
a test checks every clause has one and every case names a real clause.
tools/github-body-field/tests/test_conformance.py runs every case
against the reference parser, the vetted-ops copy, the local-files
tracker and the Jira bridge (reads and writes through their own entry
points, lenient name lookup opting out of the exact-name cases), and the
writer cases against the local-files tracker, the Jira bridge and the
import's inert block. The byte-identical copy pins stay; the copies are
re-synced for the parser docstring's new pointer.

The taxonomy gains `contract:body-field` (validator capability set,
labels doc, generated labeler config and vendor-neutrality score), the
implementing tools declare it, and the issue-template doc, READMEs and
specs point at the contract.

Part of #1672

Generated-by: Claude Opus 5
…contract

The contract is meant to be language-neutral. A second implementation, in
the language the Jira bridge is written in, shows that it is and gives
Groovy scripts a parser of their own.

tools/body-field-contract/groovy/BodyFieldParser.groovy implements every
operation of the contract: list fields, extract, replace and normalise,
with the not-found, duplicate and ambiguous refusals, the container-heading
and macro-ambiguous decisions, whole-body preservation on replace, and the
two writer operations (escape text, escape body). It follows the
conventions of tools/jira/bridge.groovy and needs nothing beyond the Groovy
runtime; it pulls no dependency with @grab. conformance.groovy beside it
reads cases.jsonl and prints one {"id", "result"} line per case.

tools/github-body-field/tests/test_conformance.py runs it once per session
when `groovy` is on PATH and compares every case with its expected result.
Without groovy, the check is skipped and says why. It was executed here
with Groovy 6.0.0 on JVM 27: all 98 cases pass. No CI workflow installs
Groovy, so in CI this check is skipped; this commit adds no workflow for it.

The contract's implementations table, its README and the spec list the
Groovy implementation.

Part of #1672

Generated-by: Claude Opus 5
…Git and Subversion storage

The local-files tracker read report files in Python's universal-newline
mode and wrote them back with LF. On a Git checkout with core.autocrlf, or
an SVN one with svn:eol-style=native, a report that reached the working
copy with CRLF line endings was rewritten in full by any field-set: every
line of it changed, not just the field. A lone CR was turned into a line
break as well.

Reads now take the file as stored, remember its convention (LF, CRLF or
CR, and whether it ends with one) and hand the parser LF lines; writes put
that convention back. A field write therefore changes no byte outside the
field, whatever the storage did to the file. A file that mixes line endings
is refused (exit 4) until it is normalised to one. Expanded svn:keywords
($Id: ... $) are ordinary text to the parser. The store keeps the parser's
duplicate or ambiguous error as the cause of its refusal, so callers can
tell the two apart.

tests/test_storage_conformance.py runs the body-field conformance cases
(extract and replace) through the store. Each case body becomes a report in
a temporary Git repository with core.autocrlf=true and in a temporary SVN
working copy with svn:eol-style=native and svn:keywords. The cases are
committed, the field reads and writes run, and the result is committed and
read back from a fresh checkout. The field values must match the contract's
and the untouched content must be byte-identical. The SVN half skips with a
reason when svn or svnadmin is missing; both ran here. Unit tests cover CRLF,
CR and LF files, a missing final newline and a file with mixed endings.

Part of #1672

Generated-by: Claude Opus 5
…detected report format

A team whose existing reports keep their fields some other way than `###`
sections (`Name: value` lines, say) could adopt the local-files tracker,
but no field in those reports could be read: the default parser finds
none, and nothing said so.

detect now reports `field_layout`: `default` or `custom`, a one-line
description, and as evidence the field names that two or more reports
share. It never reports report text. A custom layout is noted, and the
descriptor records it.

The contract gains a format adapter interface (BF-FMT-1 to BF-FMT-3): the
contract's operations, refusals and decisions for one layout's own boundary
rule, standard library only, with synthetic-value fixtures beside the
adapter. tools/body-field-contract/formats/ is the upstream home for
adapters; its _template.py is a working `Name: value` adapter and the
skeleton to copy. The conformance runner runs every adapter there, and any
named by BODY_FIELD_FORMAT_ADAPTERS: the generic properties every layout
shares, and the adapter's own fixtures.

`setup config` gains a local-files sub-step, kept in
local-files/custom-parser.md so the skill's headings and surface hash stay
as they are. When detect says custom, the sub-step:
- writes a parser from the template to
  .apache-magpie-overrides/tools/local-tracker/parsers/<name>.py, with
  synthetic fixtures and never report text;
- runs the conformance suite on it;
- enables it only on the user's explicit confirmation of its SHA-256.

The new `trust-parser` records `body_parser: {path, sha256}` in the
descriptor, and the same hash in the working copy's personal layer, which
is never committed.

The parser is code that vetted operations run outside the sandbox, so every
command loads it only when all of these hold:
- the descriptor names it;
- its path is a .py file directly in the parser directory, with no `..`
  and no symlink on the way;
- its hash matches both the descriptor and the personal layer.
Otherwise the command stops with exit 2, saying to review the change and
re-confirm it. A custom layout is never read with the default parser.
field-get, field-set and body go through the parser; create and edit with
a whole `###` body are refused on a custom layout. A test covers every
refusal.

setup-override-upstream lists generated parsers and upstreams them as
format adapters (cases/parser-adapters.md). A new setup eval suite,
config-local-custom-parser, covers four cases: a custom layout,
the default layout, a declined parser, and a confirmed one.
Self-eval passed 4/4.

Part of #1672

Generated-by: Claude Opus 5
A write keeps the file's own line-ending convention and refuses a file
that mixes several. Report text comes from outside the team, though, and
a report or field value carrying its own \r\n or lone \r was written as
it came: the file then mixed conventions, and every later write to that
tracker was refused until someone fixed the file by hand.

Untrusted text now has its line endings turned into \n before it is
escaped, and every write normalises its text before putting the file's
convention back, so no write can leave a file mixing them. A file that
already mixed them before the tracker touched it is still refused.

Generated-by: Claude Opus 5
A custom body parser is code from the tracker's repository, and vetted
operations run it outside the sandbox, so it loads only when its hash
matches one the user confirmed. That confirmation was not the user's:

- On Subversion it lived in <top>/.svn/apache-magpie/. In a repository
  hosted on Git a .svn/ directory is ordinary committed content, and the
  working-copy probe takes the nearest .svn/, so one commit carrying a
  parser, its descriptor entry and a .svn/ trust record got the parser
  run.
- trust-parser only compared the hash it was handed. The confirmation
  lived in the skill's prose, and the agent ran the command itself,
  writing the record into the git directory, which a sandboxed command
  may write.

The record now lives in ~/.config/apache-magpie/local-tracker-parsers.json,
under the working copy's real path: outside every working copy, so
nothing a commit carries can stand in for it, and a path the secure setup
never lets a sandboxed command write. trust-parser asks on the
controlling terminal and refuses without one, and setup hands the user
the command to run in their own terminal instead of running it.

Generated-by: Claude Opus 5
The user's parser confirmations live in ~/.config/apache-magpie, beside
the Gmail token, and the secure setup's sandbox reads nothing there. A
sandboxed command on a custom-layout tracker therefore could not see the
record, and said the parser had to be reviewed and re-confirmed, which
cannot help: a new confirmation lands in the same unreadable place.

A record the sandbox cannot read now says the parser cannot be verified
here and to run the command through vetted operations, which run outside
the sandbox; doctor marks that check unverifiable_here. trust-parser,
which the user runs in their own terminal, loads the parser after
recording it and prints parser_loads, so setup checks that instead of a
sandboxed doctor. A default-layout tracker never reads the record.

The agent's own file tools are not sandboxed, so the floor now denies
Edit(~/.config/apache-magpie/local-tracker-parsers.json), and
sandbox-lint requires that deny.

Generated-by: Claude Opus 5
… cannot annotate

Generated-by: Claude Opus 5
@potiuk
potiuk force-pushed the feat/body-field-contract branch from d01234b to 20617cb Compare October 18, 2026 17:05
@potiuk
potiuk merged commit 9a8ec5f into main Oct 18, 2026
21 checks passed
@potiuk
potiuk deleted the feat/body-field-contract branch October 18, 2026 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

capability:platform Framework / agent substrate skills (install, verify, doctor, override, status, setup bootstrap) contract:body-field contract:change-request Tool capability: proposed-change review + merge gate (PR / MR / Gerrit change) contract:people contract:security-tracker Tool capability: security-tracker backend for embargoed reports (GitHub / files / Jira) contract:source-control Tool capability: branch / commit / diff / push (VCS) contract:tracker Tool capability: issue / board / label backend family:ci .github workflows, prek, validators family:docs Docs, MISSION.md, READMEs family:setup setup-* skills family:tools tools/* substrate:analytics Tool substrate: read-only metrics / dashboards / renderers substrate:framework-dev Tool substrate: build / validate / eval the framework itself substrate:sandbox Tool substrate: agent isolation, egress control, settings audit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant