Skip to content

feat(scripts): sync fetcher entry scripts to Paramify + associate to evidence sets#3

Open
21tmccauley wants to merge 3 commits into
mainfrom
feat/paramify-scripts-sync
Open

feat(scripts): sync fetcher entry scripts to Paramify + associate to evidence sets#3
21tmccauley wants to merge 3 commits into
mainfrom
feat/paramify-scripts-sync

Conversation

@21tmccauley

Copy link
Copy Markdown
Collaborator

What

Adds a paramify scripts sync command (backed by a new uploaders/paramify_scripts/ uploader) that uses the new v0.6.0 /scripts API to push each fetcher's entry file to Paramify and CONNECT it to that fetcher's evidence set — so the tenant shows how each piece of evidence was generated.

Provisioning step, separate from paramify upload: run it when fetchers/** change, not on every collection.

Design (why it's a reconcile, not a create)

The /scripts API gives us {id, name, description, code} with no stable external key (only a server UUID) and no server-side versioning. So the tool reconciles the tenant to the repo, GitOps-style:

  • identity — a marker in the script description:
    paramify-fetcher: <fetcher name>
    version: <fetcher.yaml version>
    sha256: <sha256 of entry file>
    
    Indexed client-side from GET /scripts (there is no server-side name/marker filter).
  • versioning — the fetcher.yaml version is the update signal; git is history-of-record; the app holds "current".
  • drift guard — the sha256 catches a code edit that forgot to bump the version: warn + skip by default, --force to push.
  • association — get-or-create the evidence set by reference_id (same identity paramify_evidence uses, honoring the same overrides config), then POST /evidence/{id}/associate CONNECT SCRIPT. Idempotent-tolerant; --reassociate re-drives every fetcher to heal a partial failure.

Decisions baked in (per discussion)

  • Entry file onlyfetcher.py/fetcher.sh; shared modules ignored.
  • Script display name = evidence_set.name.
  • Only SCRIPT associations are automated; solution-capability / control-implementation / validator linkage stays Paramify-side.

Actions per fetcher

create (new) · update (version bumped) · drift (code changed, version didn't — warn/skip, --force) · noop.

Files

  • uploaders/paramify_scripts/{uploader.py,uploader.yaml,README.md}
  • framework/api.pyscripts_sync + scripts_sync_preflight facade (mirrors the evidence uploader loader)
  • framework/cli.pyparamify scripts sync (--dry-run/--force/--reassociate/--json)
  • tests/test_scripts_uploader.py — 8 tests (marker roundtrip, action decisions, drift gate, association-on-change, --reassociate, https guard, per-fetcher error isolation)
  • CLAUDE.md — documents the command + uploader

Testing

  • Full suite green: 91 passed (83 existing + 8 new).
  • Offline --dry-run plans all 107 fetchers as would_create (discovery + entry-file read + marker planning exercised without touching the API).

⚠️ Before merge

Not yet run against a live tenant (needs PARAMIFY_UPLOAD_API_TOKEN). Please run paramify scripts sync --dry-run (read-only) against your tenant to confirm the plan, then a real sync on a couple of fetchers. One assumption to verify on that first real call: that POST /evidence/{id}/associate CONNECT is idempotent / tolerant of an already-connected script (the API has no pre-check and GET /evidence/{id} doesn't expose associations, so the client treats an already/exists/connected 400/409 as success).

Follow-ups (out of scope)

  • Extract a shared Paramify HTTP client from paramify_evidence + paramify_scripts (kept self-contained here to avoid destabilizing the tested evidence uploader).
  • Validator association now has an API — separate decision.

🤖 Generated with Claude Code

Tate McCauley and others added 2 commits July 9, 2026 11:37
…evidence sets

Adds the paramify_scripts uploader and a `paramify scripts sync` command that
pushes each fetcher's entry file (fetcher.py/fetcher.sh) to the new Paramify
/scripts API and CONNECTs it to that fetcher's evidence set, so the tenant shows
how the evidence was generated.

The /scripts API has no stable external key and no server-side versioning, so
the tool reconciles the tenant to the repo (GitOps):
- identity: a marker in the script `description` (`paramify-fetcher: <name>`),
  indexed client-side from GET /scripts (no server name filter exists).
- versioning: the fetcher.yaml `version` is the update signal; a sha256 of the
  entry file guards undeclared drift (warn+skip by default, --force to push).
- association: get-or-create the evidence set by reference_id (shared identity
  with paramify_evidence), then POST /evidence/{id}/associate CONNECT SCRIPT.
  Only SCRIPT is automated; solcap/control/validator linkage stays manual.

Separate from evidence upload (provisioning, run on fetcher change). Shared
modules are intentionally ignored — only the entry file is pushed.

- uploaders/paramify_scripts/{uploader.py,uploader.yaml,README.md}
- framework/api.py: scripts_sync + scripts_sync_preflight facade
- framework/cli.py: `paramify scripts sync` (--dry-run/--force/--reassociate/--json)
- tests/test_scripts_uploader.py: marker roundtrip, create/update/drift/noop,
  drift gate, association, --reassociate, https guard, error isolation
- CLAUDE.md: document the command + uploader

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Surfaces `paramify scripts sync` in the TUI by folding it into the (renamed)
Paramify tab alongside evidence upload — the two share token, base_url,
overrides config, and the event-stream shape, so they share the log/worker/
preflight machinery.

- rename tab 5 "Upload" -> "Paramify" (id unchanged)
- UploadPage: "Sync Scripts" button + dry-run/force/reassociate checkboxes,
  a ScriptsSyncEvent worker, and sync_start/sync_item/sync_complete rendering
- the Scripts action is repo-scoped: gated on scripts_sync_preflight
  (token + fetchers), independent of the run picker; dry-run runs read-only
  without a confirm, a real sync confirms first and requires the token
- rebuild() now shows EVIDENCE and SCRIPTS readiness sections side by side
- extend the CLI/TUI parity harness to know the `scripts` command group,
  symmetric with `manifest` (EXPECTED_SCRIPTS + API_TO_CLI entries)

Verified via a Textual pilot: Paramify tab composes, widgets resolve, Sync
Scripts enables on 107 fetchers with no token (dry-run path). Full suite: 91 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@21tmccauley

Copy link
Copy Markdown
Collaborator Author

Added TUI support (commit 5fe7e0b): folded scripts sync into the renamed Paramify tab (was "Upload") alongside evidence upload, since both share token/config/event-stream plumbing.

  • "Sync Scripts" button + dry-run / force / reassociate checkboxes
  • repo-scoped: enabled on token+fetchers via scripts_sync_preflight, independent of the run picker; dry-run is read-only (no confirm), a real sync confirms + requires the token
  • the CLI/TUI parity harness now understands the scripts command group (symmetric with manifest)

Verified with a Textual pilot (tab composes, widgets resolve, button enables on 107 fetchers in the no-token dry-run path). Full suite: 91 passed.

…squat)

`paramify tui` told users to run `pip install 'paramify[tui]'`, but this project
is not published to PyPI and its distribution name is `paramify-fetchers`, not
`paramify`. The bare name resolves to an unrelated PyPI package (`paramify`),
which pip installs instead — clobbering the editable install and still leaving
textual absent. Point at the editable local install from the repo root:
`pip install -e '.[tui]'`, and warn off the PyPI form.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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