Skip to content

plugins: a first-class kind, on the seven primitives, held to the laws through its interfaces (D29, supersedes #2) - #8

Draft
jithinAB wants to merge 5 commits into
masterfrom
pact-kernel/plugins-kind-on-primitives
Draft

jithinAB wants to merge 5 commits into
masterfrom
pact-kernel/plugins-kind-on-primitives

Conversation

@jithinAB

Copy link
Copy Markdown
Contributor

PR #2 (spec/plugins-kind, D29) rebased onto master 33b9800 (#3 and #4), plus the four laws of docs/97-PRIMITIVES.md §4 that a plugin has to answer now that the laws are checker rules. This supersedes #2: the same four commits, then one commit for the laws. #2 is left as it is for review history.

The rebase

Every conflict was two additions at the same place: spec/schema.yaml (agent.turns/agent.voice beside agent.plugins), Cargo.toml/Cargo.lock (minijinja beside nodejs-semver/pep440_rs), pact discover (the law-1 effect/moves-money beside plugins), the Python harness imports, the Node AGENT_SPEC_FIELDS list (boards beside plugins), the boundary test's skip list, CHANGELOG.md, and the counts in README and site-docs. The counts were recomputed from the schema and the suites (scripts/sync-counts.sh), not merged by hand.

The laws for a plugin (D29 consequence 7)

A plugin is code no checker reads, so nothing about what it does can be computed from the code. What can be held is the contract it stands behind. New line: plugin.implements: names the interfaces (interface: yes tools, procedures or sets of documents) whose code the plugin supplies.

Law What now holds Rule
4 closed world implements: names only interfaces; a plugin counts as one of an interface's implementations (no an-interface-nothing-implements for it) and may be a chosen-by: run-inputs.<name> choice; an agent using an interface only plugins it is not loaded with implement is warned loader/plugin-implements-what-is-not-an-interface, loader/an-interface-only-an-absent-plugin-implements
1 closure an agent's computed effect/moves-money includes the worst action of every interface its plugins implement, named on uses: or not, since the code is in its runtime either way effects::of_agent
3 exactly once an interface action a plugin implements that cannot be undone names same-request-key:; the interface is the only place a runtime retrying the call can read the claim loader/a-plugin-action-with-no-claim
8 one checker Learner.cycle holds a plugins proposal to pact check before holding it for a person, so nobody is asked to approve a tree that does not load checking.check_agent_field, learning.CHECKED_BEFORE_HELD

A plugin that implements nothing gives an agent nothing to ask (it is how a host loads hooks, for example), and D29 consequence 6 still holds: its own traffic is the runtime's to confine.

Found while writing the law-8 test: explode held every key in the document to the portable file-name alphabet, including the plugin's own config: keys (searchMaxResults in examples/plugin-desk), so the shipped plugin example could never be written out for the checker and every proposal about it went unchecked. config: is now an opaque value: written inline, never split into folders, keys not held to the alphabet (exploding.OPAQUE, pinned to the schema's map of anything by a test).

Evidence

  • cargo test --locked --workspace: green (new: a_plugin_is_held_to_the_laws_through_its_interfaces.rs, 7 tests; a_name_the_workspace_already_has.rs narrows the new skill-naming line in its rewritten specification).
  • cargo clippy --all-targets and cargo clippy --release -p pact-cli -- -D warnings: clean.
  • pact check --deny-warnings on every examples/**/workspace.yaml: clean.
  • adapters/typescript: tsc --noEmit clean.
  • adapters/python with --extra all --extra test: 3146 collected, all passing or skipped as on master (new: test_a_plugin_a_cycle_proposes_is_checked_then_held.py, 4 tests; two exploder tests).
  • scripts/sync-counts.sh run, so the headline count test holds.

Why (Bud Meta Harness)

BMH declares its runtime-loaded rows (decision hints, conversation capture, decision chains, serve defaults) through the plugins kind, and its PACT purpose is that nothing which changes an agent's behaviour lives only in BMH configuration. Holding plugins to the laws through declared interfaces keeps a plugin-backed agent portable and reviewable: the contract, the approvals and the computed effect are in the document, and the package is one way of doing it.

Review requested from the PACT maintainers.

🤖 Generated with Claude Code

jithinAB and others added 5 commits November 11, 2026 13:49
spec/schema.yaml:
- workspace.plugins (map of group:plugin, S-EXEC, expert): one file each under
  plugins/, the request for a package and never the package.
- agent.plugins (list of text, names: plugins, S-EXEC, expert): which of them
  this agent needs; one that applies-to every-agent need not be named.
- the plugin group: description, package (<registry>:<name>), version (a range
  in the registry's own grammar), if-it-cannot-be-loaded (refuse-to-start, the
  default, or run-without-it), config (the plugin's own settings, open), and
  applies-to (the-agents-that-name-it or every-agent).
- bundle.brings gains plugins (held equal to the workspace's collections by
  what_a_bundle_may_bring_is_what_a_workspace_can_hold); learning's
  needs-a-person-to-approve gains plugins.

tests/golden/schema-fields.txt blessed: the eight new rows, nothing moved.
README counts recomputed from the schema (57 kinds, 454 fields, 320 core; the
stated 54/384 was stale) with a capability row; site-docs kind, field and
named-field counts and the plugin row of reference/kinds.md.

cargo test -p pact-cli --test authoring_surface: 16 passed.
cargo test -p pact-loader --lib what_a_bundle_may_bring_is_what_a_workspace_can_hold: passed.
cargo test -p pact-cli --test nothing_that_already_works_changes: 3 passed.
pytest tests/test_the_documentation_site_tells_the_truth.py: 17 passed, 2 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/pact-loader/src/plugins.rs (new), wired into `pact check` beside the
bundle rule:
- loader/plugin-package-has-no-registry: `package:` is `<registry>:<name>`
  with a registry a runtime loads from (npm, pypi).
- loader/plugin-version-is-not-a-range: `version:` is a range in that
  registry's own grammar, npm by nodejs-semver and pypi by pep440_rs (two new
  workspace dependencies, default features off).
- loader/plugin-secret-written-in-the-file: a `config:` key whose words name a
  token, secret, password or API key carries `{by-reference: ...}`.
- loader/plugin-runs-only-where-loaded (note): the agents a refuse-to-start
  plugin keeps to the runtimes that can load it, so --deny-warnings stays
  usable on a tree that means it.
Nothing resolves, downloads or opens a package (R5, D17).

- unnamed.rs: a plugin nothing names and not for every agent is
  loader/nothing-points-at-it; policy.rs: `plugin` is a kind stem, so
  plugins/<name>/plugin.yaml is the folder form.
- `pact discover` lists each agent's plugins (name, package, version,
  if-it-cannot-be-loaded with the default written out) through the same
  `plugins::needed_by` the note reads.
- digests.rs needs no change: a plugin is in the document, so it moves the
  workspace digest and the doc-digest of a workflow that reaches its agent,
  and not the contract-digest; the module doc names it.

examples/plugin-desk (run-without-it, clean under --deny-warnings) with its
`pact show` golden; tests/trees/a-desk-that-needs-a-plugin (the note) and
tests/trees/a-plugin-with-no-registry (the error), each with a README.
site-docs/status/verified.md: 31 agents across 12 workspaces.

crates/pact-cli/tests/a_plugin_is_named_and_never_loaded.rs (11 tests): every
rule on a broken copy of plugin-desk, silence on the example itself, the
folder form, discover output, the digests, and the two trees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Python reference (adapters/python):
- ir.PluginSpec (name, package, version, if_cannot_load) and
  AgentSpec.plugins: the plugins an agent names plus every one that applies
  to every agent, the reading `pact discover` publishes.
- harness.run(..., loaded_plugins=): what the host loaded, by name, in the
  pattern of retrieved_by, and registered in SUPPLIED_BY_THE_HOST. Before
  the first model call a missing refuse-to-start plugin halts the run
  `plugins-not-loaded` with the sentence naming plugin, package, range and
  fix, and emits turn.run.failed; a missing run-without-it plugin is one
  line on `unenforced`. Anything but `run-without-it` refuses.
- Every transport's lattice (and the out-of-tree echo adapter's) gains
  `plugins: unsupported`, because the harness loads nothing, so the
  conformance report states it before execution.
- learning.HIGH_RISK_FIELDS gains `plugins` (D23); the Pydantic AI registry
  gains agent.plugins as a loss the host holds.

Node port (adapters/typescript): `plugins` joins AGENT_SPEC_FIELDS with the
same refusal and `unenforced` sentence, word for word, and the Vercel
lattice publishes `plugins: unsupported`.

docs/20-ARCHITECTURE-DRAFT.md §7.28 list A gains the `plugins:` row and the
README scope note names it (held by the_subset_the_second_port_runs.rs);
the golden set sends `plugins` to Node; README: 31 agents, 186 pairs.

New tests: test_a_plugin_nobody_loaded_stops_the_run_before_it_starts.py (7)
and test_a_plugin_the_agent_can_do_without_is_named_on_unenforced.py (4),
each driving the second port through run-trace.ts. Mutating the Python
refusal or the Node `unenforced` line turns them red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- docs/01-DECISIONS.md: D29 — runtime plugins, named, versioned, governed.
  Its consequences: the kind at S-EXEC (D23 holds it to a person); checking
  never resolves or runs a package (R5, D17); runtimes advertise a `plugins`
  lattice column, refuse an agent whose refuse-to-start plugin they did not
  load and name a run-without-it one on `unenforced`; D15 narrowed to the
  runtimes that load an agent's required registries; D27's claim covers agents
  with no plugins or only run-without-it ones; and what a plugin does once
  loaded is the loading runtime's to govern. T5's impact row is extended.
- docs/50-NOT-COPIED.md: R17 "narrowed — see its last paragraph" (the R16
  precedent) with §3.3's "Narrowed by D29"; a §4 row for packages a runtime
  loads; §8.6 says which half moved and which refusal stands; §8 and §8.2
  recomputed (51 / 22 / 28; Runtime 3/6/2, Developer experience 4/2/5), six
  split rows.
- research/notes/eve-capabilities.md: rows 84 and 99 are (a), split, citing
  workspace.plugins and plugin.package; the Count line follows.
- docs/20-ARCHITECTURE-DRAFT.md: the extension-path status names `plugins:`
  as the runtime path and that `x-` fields are still never emitted.
- site-docs/reference/adapters.md: the `plugins` lattice column. README: 29
  binding decisions. CHANGELOG: the 26 October entry. Test counts written by
  ./scripts/sync-counts.sh (1494 Rust + 2972 adapter).

cargo test -p pact-cli --test eve_inventory, --test deliberate_refusals,
--test every_example_loads_clean: green. mkdocs build --strict: clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…implements

spec/schema.yaml: plugin.implements (list of text, names: [tools, skills,
knowledge], S-CAP, expert) names the interfaces whose code the plugin supplies.

- Law 4: loader/plugin-implements-what-is-not-an-interface; a plugin counts as
  one of an interface's implementations (no an-interface-nothing-implements,
  and a chosen-by: run-inputs choice may name it); the warning
  loader/an-interface-only-an-absent-plugin-implements for an agent using an
  interface only plugins it is not loaded with implement.
- Law 1: effects::of_agent adds every interface the agent's plugins implement.
- Law 3: loader/a-plugin-action-with-no-claim for an interface action a plugin
  implements that cannot be undone and names no same-request-key.
- Law 8: Learner.cycle holds a plugins proposal to pact check before holding it
  for a person (checking.check_agent_field, learning.CHECKED_BEFORE_HELD).
- explode keeps a plugin's own config: whole (exploding.OPAQUE), so a package's
  camel-case setting names no longer make the document unexplodable.

D29 consequence 7 in docs/01-DECISIONS.md; CHANGELOG; schema and test counts
recomputed (scripts/sync-counts.sh).

cargo test --workspace, clippy (and -D warnings release), every example under
--deny-warnings, tsc and the Python suite with all extras pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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