Skip to content

feat(fleet): module-owned publisher lineage; reject expressions in Dagger args - #50

Open
hseshadr wants to merge 5 commits into
mainfrom
feat/release-lineage-module
Open

hseshadr wants to merge 5 commits into
mainfrom
feat/release-lineage-module

Conversation

@hseshadr

@hseshadr hseshadr commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

TL;DR

Two fleet-policy security gaps from #49, closed in the central module instead of in consumer shell.

  1. Publisher lineage is now a module function. portfolio-foundation gains release-lineage (PyPI) and release-provenance (npm). Each fails unless GitHub's run records show all of the following: the candidate run is a successful release-candidate.yml dispatch for exactly HEAD_SHA; the publish run is this repo's in-progress publish.yml workflow_run on main; and main contains both commits (branches/main endpoint, so a tag named main can't stand in for the branch). release-provenance also returns npm's github-context.json, built from the publish run record instead of caller text. The fleet policy accepts exactly this leading step (publisher-lineage for anything weaker) and a consumer publisher loaded at @${{ github.sha }}. edgeproc-core and privacy-core can then drop their run: steps with no exemption.
  2. dagger-args-expression. The policy now rejects ${{ inputs.* }}, ${{ github.event.* }} and ${{ github.head_ref }} in every dagger-for-github input the action pastes into bash (args, call, shell, dagger-flags, workdir, cloud-token). module is exempt because the action passes it as INPUT_MODULE env.

Claim touched: "a publisher only ships bytes built from main" and "no caller-controlled text reaches script".

Fixes #49

Merge order

This PR is stacked on #47 (feat/fleet-minimum-pin) and #48 (feat/fleet-coverage). Merge it after both. Its base is main, so until they merge, the diff also shows their commits.

Contract reversal (read this)

NPM_BRIDGE and the planned-PyPI fixture in test_fleet_policy.py pasted --expected-sha=${{ github.event.workflow_run.head_sha }} into args and asserted that shape was compliant. They now pass it through env: as --expected-sha="$HEAD_SHA". The policy reports the old shape.

Fleet impact after merge (new dagger-args-expression findings on current mains)

Repo Where Fix
assay release-candidate.yml (inputs.tag), publish.yml x2 (workflow_run.head_sha) hseshadr/assay#74
almamesh deploy.yml (workflow_run.head_sha, .id, .run_attempt) needs env migration
edge-reco deploy.yml (same three) needs env migration
aml-filter deploy.yml, publish-watchlist.yml (github.event.workflow_run.head_sha) needs env migration
agentic-saga dagger.yml, dagger-security.yml (github.event.repository.private inside a conditional) needs env migration

These values are GitHub-generated, but the rule is literal by design. It flags every event expression, with no judgement about which ones are safe.

Evidence

Check Result
New policy tests, before the change red: 16 failed / 14 passed (every expression case, and both lineage shapes reported pypi-shape/npm-shape)
Root poe gate (lint, mypy, xenon A, pytest, branch floor) green: 275 passed, 97% line coverage
portfolio-foundation lineage tests 31 passed. lint, mypy strict, xenon A green
Mutation M1: expression guard disabled 14 failed
Mutation M2: lineage env binding (RUN_ID/HEAD_SHA from the event) dropped 2 failed
Mutation M3: ancestry (compare) check dropped 3 failed (both tag-named-main cases + publisher-not-on-main)
Mutation M4: candidate SHA binding dropped 3 failed
Live, through the engine from this branch (at 49b2716; later commit 3de1c4b only splits helpers): dagger -m .../portfolio-foundation@3de1c4b functions release-lineage, release-provenance listed
Live release-lineage on privacy-core run 35935478810 + completed publish run candidate accepted from the real payload; refused at "publish run is not this repository's running main publisher" (fails closed: the run is not in progress)
Live release-lineage on failed candidate run 35930137724 refused: "candidate run is not a successful release dispatch of this SHA"
Live compare via the module's REST adapter released SHA de1c0b8 contained in main; privacy-core#48 PR-head 8de4231 refused "not contained in main"

Not verified

  • The success path end to end. It needs a real in-progress publish run, which only a tag dispatch creates, and this PR dispatches nothing.
  • The full portfolio-foundation poe gate locally. On this machine it stops at 86% total coverage even on the base branch, before this change (247 passed, 1 skipped). ci's hosted CI does not run module gates.
  • Not enforced yet: the lineage step is accepted, not required. edge-proc and assay publishers still pass without it. Requiring it is a follow-up once they migrate.

Consumer drafts pinned at this PR's head: hseshadr/edgeproc-core#60 and hseshadr/privacy-core#49; assay injection fix: hseshadr/assay#74. They get re-pinned to the merge SHA after this merges.

🤖 Generated with Claude Code

https://claude.ai/code/session_015oBArfm762nN1r4F4Fst5a

hseshadr and others added 4 commits September 25, 2026 08:17
Consumers pin central Dagger modules at exact SHAs, and fleet policy
checked that each pin was well-formed but not that it was new enough.
Repos could stay pinned below a mandatory fix (dd19871, #46)
without anyone noticing, which blocked the aml-filter release.

REQUIRED_MINIMUM in fleet_policy.py holds a reviewed floor per central
module, starting with portfolio-foundation -> dd19871. The GitHub reader
compares every floored module revision in a consumer's resolved Dagger
graph against the floor (compare/<floor>...<pin>) and against central
main (compare/<pin>...main). Both must be ahead or identical. Otherwise,
including when there is no common history or the evidence is missing,
the scan reports pin-below-required-minimum and fails.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oBArfm762nN1r4F4Fst5a
The fleet scan only checked repositories listed by hand in
repository_expectations. agentic-saga and agentic-context-service pin
hseshadr/ci modules but were never listed, so no fleet rule ran on them.

- Add both to repository_expectations with the full consumer contract
  (sole Dagger check, conversation resolution, no rollout exception;
  both already declare shared foundation).
- New fleet_coverage module: list every public hseshadr repository, read
  its default-branch dagger.json, and report uncovered-consumer for any
  active repository that pins a github.com/hseshadr/ci module but is
  missing from the list. Listing or config read errors fail closed.
- scan_repository now turns FleetAccessError into an evidence-unreadable
  finding, so one unreadable repository (agentic-context-service has no
  branch protection on main) no longer hides every later result.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oBArfm762nN1r4F4Fst5a
…ger args

Publisher lineage moves out of consumer `run:` steps into two
portfolio-foundation functions. release-lineage and release-provenance fail
unless GitHub's run records show a successful release-candidate.yml dispatch
for exactly the expected SHA, run by this repository's in-progress main
publish.yml, with main containing both commits. That blocks a dispatch on a
tag named `main` from publishing its own bytes. release-provenance also
returns npm's provenance context, built from the publish run record.

The fleet policy accepts that exact leading step (publisher-lineage for
anything weaker) and a consumer publisher loaded at `@${{ github.sha }}`, so
edgeproc-core and privacy-core can publish with no shell step and no
exemption. It also reports dagger-args-expression for `${{ inputs.* }}`,
`${{ github.event.* }}` or `${{ github.head_ref }}` in any dagger-for-github
input the action pastes into bash.

Fixtures that pasted the workflow_run head SHA into args as compliant now
pass it through env; the policy reports the old shape.

Fixes #49

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oBArfm762nN1r4F4Fst5a
Split evidence construction and the repository part of the provenance context
into named helpers (python-quality function-length rule). The provenance JSON
keys are now emitted sorted. Behavior is unchanged; lineage.py stays at 100%
line and branch coverage.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oBArfm762nN1r4F4Fst5a
hseshadr added a commit to hseshadr/edgeproc-core that referenced this pull request Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oBArfm762nN1r4F4Fst5a
hseshadr added a commit to hseshadr/privacy-core that referenced this pull request Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oBArfm762nN1r4F4Fst5a
hseshadr added a commit to hseshadr/aml-filter that referenced this pull request Sep 25, 2026
dagger-for-github pastes `call` raw into a bash script, so the fleet rule
dagger-args-expression (hseshadr/ci#50) forbids ${{ inputs.* }},
${{ github.event.* }} and ${{ github.head_ref }} in every input it pastes
(args, call, shell, dagger-flags, workdir, cloud-token). Both delivery steps
now get RELEASE_SHA through env: and pass --release-id="$RELEASE_SHA:$GITHUB_RUN_ID";
the queue step passes --run-id="$GITHUB_RUN_ID". Checkout ref keeps the same
source expression (not Dagger args).

Contract tests in both suites fail while any such expression is in a Dagger
script input; a mutation re-adding one to publish-watchlist.yml turns both red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oBArfm762nN1r4F4Fst5a
hseshadr added a commit to hseshadr/almamesh that referenced this pull request Sep 25, 2026
…seshadr/ci#50)

dagger-for-github pastes `args` into bash, so `${{ github.event.* }}` there
is a shell-injection sink (fleet rule dagger-args-expression, hseshadr/ci#50).
deploy.yml now passes head_sha, run id, and attempt as HEAD_SHA,
WORKFLOW_RUN_ID, and RUN_ATTEMPT in the step env and references them quoted.

CONTRACT REVERSAL: the ingress and deploy-workflow contracts used to REQUIRE
`--expected-sha=${{ github.event.workflow_run.head_sha }}` (and run id and
attempt) in args. They are inverted, not deleted: they now forbid those forms
and require the env + quoted form. A new fleet-rule test scans every
workflow's dagger-for-github bash-pasted inputs, including live-probe.yml. It
was red on deploy.yml before the migration, and four mutants cover
inputs.*, github.event.*, head_ref, and spacing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oBArfm762nN1r4F4Fst5a
hseshadr added a commit that referenced this pull request Sep 26, 2026
Brings in #47 -> #48 -> #50 -> #52 so this PR merges last without
conflicts. Their README lines move to the new layout: the consumer list
(now with agentic-context-service and agentic-saga), the uncovered-consumer
failure and the required-minimum pin floor go to docs/ARCHITECTURE.md
"What dagger call fleet checks", with plain one-line versions in the README
intro. Publisher lineage and the dagger-args-expression rule are noted there too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oBArfm762nN1r4F4Fst5a
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.

Fleet policy: no compliant publisher-lineage shape, and it accepts ${{ inputs }} in Dagger args

1 participant