Skip to content

feat(docs): Add enactment pipeline - #359

Open
misonijnik wants to merge 5 commits into
misonijnik/skills-tagsfrom
misonijnik/enactment
Open

feat(docs): Add enactment pipeline#359
misonijnik wants to merge 5 commits into
misonijnik/skills-tagsfrom
misonijnik/enactment

Conversation

@misonijnik

Copy link
Copy Markdown
Member

No description provided.

@misonijnik
misonijnik requested a review from Gr-i-niy August 19, 2026 07:41
OpenTaint work now runs one of two pipelines over the same machine and the
same `.opentaint/` tree, differing only in where the source and sink rules
come from:

- assessment (`assessment-agent`, renamed from `appsec-agent`) — discovered
  from the project's dependency attack surface, as before
- enactment (`enactment-agent`) — generalized from a supplied finding set, so
  both rule sides exist before the first scan and that scan is rule-first

`appsec-agent` becomes a thin entry point: toolchain and nesting checks, then
it picks the pipeline and loads it in the same session (MAIN must keep the
build and scans). An existing `state.yaml` decides for it; `mode` is not
switchable, since neither pipeline's tracking means anything to the other.

The shared phase machine keys off `state.yaml`'s `mode`, so `get_status.py`
holds ASSESSMENT_PHASES and ENACTMENT_PHASES and `generate.py init` takes
`--mode`. Enactment adds `reference_set`, `boundaries`, and `crossref`; it
closes on the cross-reference, which judges each supplied finding by trace
identity — never by a rule-id match — and writes `.opentaint/enactment.md`.

One more piece both pipelines share:

- `discover-universal-boundaries`, the leaf that generalizes one finding
  family into a saturated source and sink boundary — widen, re-factor the
  whole family, split rather than listing one pattern branch per finding

New tracking: `reference/<id>.yaml` and `boundaries/<family>.yaml`.
Rule units keep their existing format and path, so
`create-rule` and `create-test-project` are untouched.
The two pipelines are not alternatives. One `.opentaint/` tree accumulates
the artifacts of every pass over it, in either order and repeatedly across
commits: enactment then assessment hunts with the boundaries the supplied
report produced, assessment then enactment measures that report against the
corpus the first pass built, and either re-run on a later HEAD is a
regression check. `init` refused a mode switch and told the user to start a
fresh tree, which made all of that impossible.

`mode` is now the pipeline of the current pass, not a property of the tree:

- `init` keeps every durable artifact across a switch and prints what
  carried over
- `history.yaml` records one entry per pass, keyed on (commit, mode,
  levels); re-running init with the same knobs is a resume, not a new pass.
  `get_status.py --full` prints the chain once there is more than one
- `findings` survives an assessment pass, so a later enactment pass inherits
  the set and `--findings` is required only the first time

One consequence: an assessment pass rescans a tree that may carry a
reference set, which invalidates the coverage manifest an enactment pass
wrote. So `crossref` is gated on the reference set existing rather than on
the mode — it closes an assessment pass too, re-judging the supplied
findings against the new results. Identical work; only the triggering pass
differs.

Both pipeline skills now open by saying the run is one pass over a tree that
outlives it, and their resumption sections forbid deleting or rewriting an
artifact just because this pass didn't produce it.
The enactment split landed `discover-universal-boundaries` authored against
Java: it takes `language` as an input and the boundaries stage passes it, but
its workflow told the leaf to lean on annotations, `@Valid`, `$MAP.get(...)`,
and a JVM descriptor for `candidate_patterns` — vocabulary that belongs to one
language, in a body that is supposed to hold the reasoning method.

Keep the reasoning, which is language-agnostic, and move the vocabulary into
`references/<language>.md` the way the leaf's siblings already do. The Java
reference is grounded in the bundled ruleset rather than in generic advice,
because what a family actually needs is decided by what the built-ins already
match: `spring/untrusted-data-source.yaml` taints every non-scalar parameter of
a mapping-annotated method and the servlet rule taints the whole
`HttpServletRequest` of an entry method, which puts a Java family's real gap on
non-HTTP transports, reactive accessors, and second-order re-entry. It carries
the per-class sink coverage with its two notable absences, Bean Validation as
the validation impostor, and the `Class#member` plus JVM descriptor shape the
spec's `candidate_patterns` take.

Also re-apply two skill doc fixes that an earlier rebase dropped: the
silently-skipped-file note in the pass-through Java reference, and the
`project.yaml` dependencies wording in triage-dependencies.

skills/ is regenerated from skills-templates/ via build.py.
The enactment split left two orchestrators — assessment-agent and
enactment-agent — behind an appsec-agent that only chose between them, and the
two ran the pipeline in different orders: enactment authored both rule sides
before its first scan, assessment authored sinks after the model round. Two
documents describing one machine drift, and the divergent order made the shared
stage references say "in enactment mode" about work that is not mode-specific
at all.

Collapse them back into `appsec-agent` and make the difference a `mode` knob
with three values, which is all the difference ever was — where the run's
boundary evidence comes from:

- onboarding — the external-method frontier: every dependency member the
  project's own code calls, taken as a trust boundary until a leaf verdicts it.
  This is the old dependency sweep, named for what it does and scoped to the
  once-per-project pass it is; init refuses a second one over a tree that
  already carries the ledger, rules, and models it produced
- discovery — the project, a diff, or an informal spec, resolved to the code it
  names
- enactment — the supplied finding set, normalized into the reference set

Universal rules are now every mode's output, not enactment's: intake ends with
a family list whatever it started from, the boundaries stage generalizes each
family into one universal source and one universal sink, and the seeded units
are what rule authoring works from. `discover-universal-boundaries` takes
`evidence` rather than `finding-ids`, and its reasoning is unchanged — a swept
frontier member and a reference finding factor through a boundary the same way.

One phase list, in the order the spine has always implied and every mode now
follows: build, intake, boundaries, source_rules, scan, approximations,
sink_rules, triage, then poc and — whenever the tree carries a reference
set, whichever pass left it — crossref. Both boundary sides are
decided before the scan that proves them; only the sink rules are authored
after it, against the frontier that scan names, so a model can never stand in
for a boundary nobody authored.

Tracking gains `tracking/scope.yaml` (the family list for the modes that don't
carry it on reference files), the boundary spec keys its factorization by
`evidence`, and `partition discover` becomes `partition frontier` so the
partitioner and the discovery mode no longer share a name for different things.

skills/ is regenerated from skills-templates/ via build.py; no engine, rule, or
model behaviour changes.
@misonijnik
misonijnik force-pushed the misonijnik/enactment branch from 57f3b62 to 20eaf2f Compare August 19, 2026 07:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant