Skip to content

v2.4: Agents & Teams — SDK extraction (pmcp-agent, pmcp-team-servers, pmcp-package + cargo-pmcp verbs)#302

Merged
guyernest merged 106 commits into
paiml:mainfrom
guyernest:pr/v2.4-agents-teams
Jul 19, 2026
Merged

v2.4: Agents & Teams — SDK extraction (pmcp-agent, pmcp-team-servers, pmcp-package + cargo-pmcp verbs)#302
guyernest merged 106 commits into
paiml:mainfrom
guyernest:pr/v2.4-agents-teams

Conversation

@guyernest

Copy link
Copy Markdown
Collaborator

Summary

Delivers the v2.4 "Agents & Teams" milestone — three new experimental (0.x) crates, cargo-pmcp verbs to drive them, and additive core-SDK plumbing to support agent/team host surfaces. All changes are additive; no breaking changes to the core pmcp API.

103 commits, 235 files. This branch is filtered to contain only code/doc changes — GSD .planning/ planning artifacts are excluded so the diff is pure code review.

New crates

Crate Version Purpose
pmcp-agent 0.1 (experimental) Agent-loop crate: WithTools real-loop, ClientToolInvoker, ConnectorClientFactory. openai-compat/anthropic/url-connector features are non-default, so the default publish build is reqwest-free and wasm-clean.
pmcp-team-servers 0.1 (experimental) Reference team-server implementations (4 servers) + in-process runtime + conformance harness. webhook/http features non-default.
pmcp-package 0.1 (experimental, workspace-excluded) AI-Package format crate; first in-repo consumer is pmcp-agent.

Highlights by phase

  • 106 — Client host surface: wire Client host dispatch + generic registration builders; relocate roots wire types; add a wasm-clean client::host module; Sampling & Hosting disambiguation docs.
  • 107 — pmcp-package: port the AI-Package crate tree into the SDK with publish-ready metadata, dual licenses, README/CHANGELOG; author contracts/team-servers-v1.yaml.
  • 108 — pmcp-agent: end-to-end WithTools sampling (client host + peer, additive); Transport Actor pump in Server::run (never-block) + stdio cancel-safety; bump core to pmcp 2.17.0.
  • 109 — pmcp-team-servers: additive _meta forwarding — extend RequestMeta with a namespaced _meta map propagated to handlers; scaffold the reference-team-server crate + conformance harness.
  • 110 — cargo-pmcp verbs: add agent / team / package command groups (cargo-pmcp → 0.18.0) with version-pin tripwires and --help coverage tests.

Testing

  • Unit, property, and conformance tests across all three crates (tests/, fuzz/).
  • Contracts under contracts/ updated/added for the new surfaces.
  • Core src/ changes are additive and covered by existing + new tests.

Notes for reviewers

  • The three new crates are 0.x/experimental — a failure in any of them must not gate the core SDK release; their default builds are reqwest-free and wasm-clean.
  • pmcp-package is workspace-excluded (own [workspace] table); publish via cargo publish --manifest-path crates/pmcp-package/Cargo.toml.

🤖 Generated with Claude Code

Guy Ernest and others added 30 commits July 19, 2026 11:35
… audience

The "Publish to MCP Registry" job has failed on every release since
v2.10.0 with:

  invalid audience: expected https://registry.modelcontextprotocol.io,
  got [mcp-registry]

mcp-publisher v1.5.0 mints its github-oidc token with the legacy
"mcp-registry" audience; the registry now requires the URL audience,
which newer publisher releases send. Non-blocking historically
(crates.io publish is a separate job), but the registry listing has
been stuck since 2.9.x.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit fb99bca)
… module

- Move Root + ListRootsResult to target-agnostic src/types/roots.rs
  (server::roots keeps a back-compat re-export; now compiles on wasm32)
- Add client::host module: HostSamplingHandler, HostElicitationHandler traits
- Add RootsProvider (Result-returning), ClientHostRegistry, and the
  preflight/result-review approval types (ApprovalDecision/PreflightApproval/
  SamplingResultReview) — invocation lands in the next task
- Add pure classify_host_request + HostRequestKind for dispatch routing
- Re-export the host surface from client::mod for pmcp::client::host::*

(cherry picked from commit d8985918ad636189ba9cf47738756e64d82a0b35)
- Add host_registry field to Client (all 3 constructors + Clone + Debug)
- Add ClientBuilder host_registry field + generic builder methods:
  on_sampling / on_elicitation / on_roots / on_sampling_approval /
  on_sampling_result_review (closures, no Arc-alias in signatures)
- Replace the ":2234" Unexpected-message error arm with dispatch_host_request:
  routes inbound sampling (both parse variants) / elicitation / roots to the
  host handlers, answers or returns -32601, then continues the receive loop
- Handler/provider errors log full context locally (tracing::error!) and return
  a sanitized -32603 (no internal text forwarded to the remote server)
- create_message rustdoc names the LLM-server pattern and cross-links the real
  pmcp::SamplingHandler + pmcp::client::host::HostSamplingHandler paths
- Unit tests: sampling client-alias routing, known-unhandled -> -32601,
  sanitized -32603, roots provider answer

(cherry picked from commit 6d0d0bd)
…ample

- tests/client_host_roundtrip.rs: raw-duplex-pump round-trips proving the client
  answers inbound sampling (HOST-01), roots (HOST-03), and elicitation (HOST-02)
  from its registered host handlers while a tools/list is in flight; plus
  prop_sampling_passthrough asserting params (tools/tool_choice/tool_use/
  tool_result) reach the handler unchanged
- examples/s49_sampling_host.rs: runnable sampling-host example (mock LLM host
  answers an inbound sampling/createMessage over an in-process duplex); registered
  in Cargo.toml + examples/README.md
- Uses a raw pump (not Server::run + PeerHandle): the high-level Server::run
  serializes request handling on one loop, so a tool blocking on
  extra.peer().sample() deadlocks (pre-existing server-side limitation, flagged
  for Phase 108); PeerHandle is also intentionally not extended with elicit

(cherry picked from commit 86d4d29)
RootsProvider/ApprovalDecision are re-exported into client scope, so the bare
intra-doc link form resolves — silences 3 rustdoc redundant-link warnings.

(cherry picked from commit 9025699da3c98142b4c8a8ca4a37f4bc499bc3bf)
- Contrast spec host sampling (server->client, HostSamplingHandler) vs
  the LLM-server pattern (client->server, pmcp::SamplingHandler)
- Document the preflight on_sampling_approval gate + result-review hook
- Note the nested-flow/idle-host + Server::run (D-106-A) limitations
- Reference the s49_sampling_host runnable example

(cherry picked from commit 8e066f0e342a911c8f0b602f61c872c2db2d5d79)
- Add ch17-04 sub-entry under Chapter 17, after Tool with Sampling
- mdbook build passes with no broken-link warning

(cherry picked from commit 85ba059)
… test

- Two-stage sampling approval in dispatch_host_sampling: preflight gate
  runs BEFORE the handler (Deny prevents the LLM call, no tokens billed),
  optional result-review runs AFTER (default pass-through)
- Sanitized -32603 'request denied by host policy'; raw deny reason logged
  locally via tracing::warn!, never forwarded
- Unit tests (a-e): default-allow, preflight-deny-skips-handler, allow,
  result-review-deny-after-handler, review-absent passthrough
- tests/client_host_approval.rs: duplex preflight-deny + connection survives;
  plus known-unhandled elicitation -> -32601 + connection survives (addendum)

(cherry picked from commit fc9873a34f0ce8c2fe6232385166e03dac69e845)
- initialize now applies the registry-authoritative rule per host field:
  handler absent => force None (anti-capability-lie); handler present +
  caller None => default(); handler present + caller detail => preserve
- tasks/experimental and all non-host fields left untouched; no independent
  public setter for the three host fields
- Unit tests: registered=>present, unregistered=>absent, anti-lie discards
  caller value, preservation of caller sub-field detail, elicitation+roots
  parallel coverage, tasks/experimental untouched

(cherry picked from commit 69ee638129ce18468d5be1154ace0167a6089bed)
…uest)

- New fuzz/fuzz_targets/client_host_routing.rs drives the REAL routing path:
  bytes -> JSONRPCRequest<Value> -> parse_request -> classify_host_request
  (plus the CreateMessageParams/ElicitRequestParams serde boundary)
- Expose classify_host_request + HostRequestKind as #[doc(hidden)] pub so the
  fuzz crate can reach them (not part of the stable public API)
- Register [[bin]] client_host_routing in fuzz/Cargo.toml
- Add empty [workspace] to fuzz/Cargo.toml so the crate resolves as its own
  workspace root under git-worktree checkouts (Rule 3 blocking fix)
- Smoke: cargo fuzz run client_host_routing -- -runs=2000 => Done, exit 0

(cherry picked from commit 62b0ffb)
- Root pmcp crate 2.15.0 -> 2.16.0 (additive host surface, minor bump)
- cargo-pmcp workbook_server.rs PMCP_VERSION pin 2.15.0 -> 2.16.0 in lockstep
  (the drift-guard tripwire emitted_pmcp_version_matches_workspace_pin that
  broke the 2.15.0 release)
- cargo-pmcp own version 0.17.3 -> 0.17.4 so the scaffold-pin change ships
- No unrelated downstream pin edits

(cherry picked from commit ffd5099)
- Capability tests: use struct-init with ..Default::default() instead of
  field-reassign-after-default (clippy::field_reassign_with_default)
- Reflow assert! macros to rustfmt width
- tests/client_host_roundtrip.rs (plan-01): backtick tool_choice/tool_use/
  tool_result in arb_params doc (clippy::doc_markdown, surfaced by the current
  toolchain; blocks the shared quality gate) [Rule 3 - blocking, pre-existing]

(cherry picked from commit ed5fdaa01de920a8e315cc4317146362ff75457c)
…ge works

Client::create_message unconditionally errored because assert_capability
had no "sampling" match arm — the check fell through to _ => false and
always returned a capability error, so the documented LLM-server pattern
was dead. Add the arm mirroring ServerCapabilities.sampling (which
ServerBuilder::sampling sets), plus positive/negative lib unit tests and
an end-to-end duplex regression proving create_message round-trips against
a Server::builder().sampling(..) server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 96987bb)
The host dispatch path classified an inbound server->client ping
(ClientRequest::Ping) as Unhandled and replied -32601, so keepalive pings
failed and connections that treat ping failure as death would drop. Add a
HostRequestKind::Ping arm that answers with an empty-object success result
independent of the registry, plus a classify unit test and a duplex test
proving the ping returns {} and the connection survives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit ee3415d503db9c1d27f63ae22318d61a856ce726)
…fails

The receive-loop Request arm propagated a transport send error via `?`
without removing request_id from active_requests, leaking the map entry
and its oneshot cancel sender (a regression vs the replaced code path and
inconsistent with the Response arm). Remove the pending entry before
returning the error, and add a unit test driving a transport whose host-
response send fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 5c95fec)
… optional)

The sampling approval docs claimed the hooks' invocation was deferred to a
follow-on plan and labelled PreflightApproval "Mandatory". Both are wrong:
dispatch_host_sampling invokes the preflight and result-review hooks in
this phase, and the posture is optional with default-allow. Rewrite the
type docs (sampling.rs) and both builder-method docs to state the hooks
are live as of this phase and describe the default-allow / pass-through
posture accurately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 606bbcf37e6a0fb50a18209330a17d039ea0e322)
… unchanged)

The HOST-05 anti-capability-lie rule overrides caller-set sampling/
elicitation/roots capability fields on initialize when no host handler is
registered — including for Client::new users who cannot register handlers.
This is the locked phase decision and is intentionally left unchanged;
document it instead. Add a rustdoc note on `initialize`, rewrite the
contradicting `send_roots_list_changed` doc example to register a roots
provider via `ClientBuilder::on_roots` (and preserve `list_changed`), and
add a CHANGELOG 2.16.0 entry flagging the silent-change risk for existing
Client::new callers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 9cea2f7)
Behavior-preserving cleanups from a 4-angle review of the Phase 106
client host surface (fix 8 additionally repairs pre-existing
active_requests leaks on error paths, approved):

1. Collapse host_method_not_found / host_policy_denied /
   host_internal_error into one host_error(id, code, message) built on
   JSONRPCResponse::error + JSONRPCError::new + ErrorCode constants
   (no more magic numbers / struct literals).
2. Extract sync_cap<C: Default> helper for the HOST-05 rule in
   derive_host_capabilities, replacing the three duplicated stanzas.
3. Borrow host_registry entries (&self.host_registry.X) instead of
   Arc-cloning in the four dispatch paths.
4. Make handler param clone conditional: clone for result-review only
   when a review callback is registered (zero clones otherwise).
5. Inline the single-use extract_elicitation_params via let-else;
   keep extract_sampling_params (two-variant ambiguity).
6. Extract duplicated pump helpers (recv_request_id, recv_response,
   send_success, init_result_value + shared inject_and_capture prefix)
   into tests/common/host_pump.rs, included per-crate.
7. Match response.payload directly (ResponsePayload::Result) instead of
   serialize-and-lookup in result_payload and the s49 example.
8. Install a single WR-04 exit-cleanup invariant in send_request: the
   post-registration body runs in an inner future; active_requests entry
   is removed at one point on every error exit (fixes the three prior
   leaks). Adds test_receive_failure_cleans_active_requests.
9. Make assert_capability's fall-through loud: tracing::error! +
   debug_assert! on an unknown capability string (still returns false in
   release).
10. Remove the redundant clone chain in the client_host_routing fuzz
    target; probe both param types from &Value via Deserialize.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit a255be8)
…oc wording

Code-review findings on the Phase 106 host surface:
- assert_capability unknown-arm diagnostic logged {method} while labeling it
  'unknown capability string'; now logs the actual unmatched {capability} token
  (plus method for context) so the 'add an arm' hint names the right string.
- dispatch_host_sampling doc called preflight 'a mandatory gate', contradicting
  the default-allow posture two lines down and the corrected sampling.rs wording
  (WR-02 missed this occurrence); reworded to 'optional gate (default-allow)'.

(cherry picked from commit 402db4e158e51d33901d446a1ea8e16e32ba00c2)
- 4 equations: fs/mem/approval/team_dispatch tool surfaces
- namespaced provisional extension; resolve_approval/get_approval unnamespaced legacy
- storage-agnostic team-mcp dispatch invariants (stable member ID, ToolOutput::Result)
- no lean_theorem, no binding.yaml (deferred to Phase 109)

(cherry picked from commit e8496d836539bc0e547f211565a707062bf7247a)
- Copy the full crate (Cargo.toml, src/ 23 files, tests/) verbatim from
  pmcp-run/crates/pmcp-package, excluding target/
- Keep the empty [workspace] table (standalone-isolation mechanism)
- Relax the repo-coupled deploy.toml fixture-coverage discovery floor
  (was >=15, a pmcp-run-specific count) to the environment-agnostic
  invariant: every tracked descriptor parses [Rule 1/3 - port fix]

Standalone: cd crates/pmcp-package && cargo test -> 118 passed.
(cherry picked from commit b7fa50b)
- Cargo.toml: repository -> paiml/rust-mcp-sdk, add authors/readme/
  documentation/keywords/categories, public description, and a
  [package.metadata.docs.rs] table (PKG-01); scrub internal refs from
  dependency comments; keep license and pinned deps unchanged
- LICENSE-MIT: copy repo-root MIT body verbatim
- LICENSE-APACHE: byte-identical canonical Apache-2.0 template (no
  injected copyright holder)
- NOTICE: ownership/attribution kept out of the license body
- README.md: public overview + explicit Wire-Freeze Policy (0.1.x
  digest-stable, shape change bumps 0.2.0)
- CHANGELOG.md: 0.1.0 entry recording adoption + wire-freeze policy

cargo publish --dry-run --allow-dirty exits 0; README/CHANGELOG/both
licenses present in cargo package --list.

(cherry picked from commit 8c2fe36)
- 13 fixtures across team-fs/mem-mcp/approval-mcp/team-mcp (versioned schema v1)
- positive per family + both dynamic families + related_task _meta surface
- 5 negative/security cases (invalid args, unknown member, malformed/excessive depth, self-call)
- tests/team_contracts_conformance.rs: schema-aware gate, contract cross-reference via CARGO_MANIFEST_DIR
- dependency-free (serde_json + std); no serde_yaml added

(cherry picked from commit 1f2bfc3)
- Remove internal ticket IDs (I-N, D-N, T-168, Phase N, Wave 0) and
  'Plan NN' refs from every shipped file's comments and test messages,
  preserving all behavioral/security rationale (path-traversal guard,
  trust-boundary, canonicalize-then-hash explanations retained)
- Rewrite the crate-level lib.rs rustdoc to a clean public description
- Disambiguate intra-doc links to crate::digest::verify (module vs fn)
  so docs build under -D rustdoc::broken_intra_doc_links
- Reword 'OCI-1.1' -> 'OCI 1.1' (the substring 'I-1' tripped the scrub
  gate as a false positive)

Zero files match the scrub regex; RUSTDOCFLAGS="-D
rustdoc::broken_intra_doc_links" cargo doc --no-deps exits 0; 118 tests
still pass.

(cherry picked from commit a16cff97d3771325e9d488503b1b5d5bdd17ab67)
…kinds

- Add agent + team golden fixtures (canonical-JSON input, round-trips)
- Add tests/golden_fixtures/canonical/<kind>.canonical.json snapshots
- Pin EXPECTED_<KIND>_DIGEST wire-freeze constants (server/workflow/agent/team)
- Assert canonicalize() byte-equals each snapshot (silent field add/remove gate)
- Retain per-kind >=100-recomputation determinism assertions

(cherry picked from commit 3aa09737ff981e7136128cd6f6ea9a241d8ce0ce)
…ygiene)

- Makefile: add pmcp-package-gate (fmt/clippy/test via --manifest-path), chain into quality-gate
- ci.yml: add standalone pmcp-package fmt/clippy/test step (closes workspace-exclusion blind spot)
- release.yml: add early-leaf publish step via --manifest-path with precise failure classification
- Cargo.toml: exclude tests/team_contracts_conformance.rs from published pmcp crate
- CLAUDE.md: document pmcp-package as workspace-excluded leaf, publish before cargo-pmcp
- normalize pmcp-package sources now that standalone fmt is gated

(cherry picked from commit ee21734)
pmcp-package is a standalone workspace-excluded leaf with no in-repo
consumers yet. Its release.yml publish step sat before core `pmcp` with
an `exit 1`-on-failure guard and a rationale claiming cargo-pmcp pins it —
but nothing in the repo depends on it. Moving it to the end of the
crates.io job so a failure in this experimental 0.x crate cannot gate the
core SDK release; corrected the ordering comments in release.yml and the
CLAUDE.md publish-order list. Re-pin earlier once a shipped crate actually
depends on it (planned: cargo-pmcp, Phase 110).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 0b37daabfab27f2a14b8db605b358fe977cb08c0)
The untracking of contracts/.pv/cache/... landed in the WR-01 commit;
this adds the .pv/ ignore rule so PMAT/pv lint-cache scratch is never
re-committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 12081f66d6eda1baba83c51c2819bbe9a42d435f)
pmcp-package (workspace-excluded) was fmt/clippy/test'd twice in CI: a
standalone step in the `test` job AND via the `quality-gate` job's
`make quality-gate` -> `pmcp-package-gate`. Both are full from-scratch
compiles of the crate tree on separate runners. Removed the test-job copy;
the Makefile gate is the single source of truth (reused locally and by the
quality-gate CI job, which feeds the merge-blocking `gate`). No coverage
lost. (/simplify: efficiency + altitude, deduped.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit d827dfa)
Guy Ernest and others added 29 commits July 19, 2026 11:36
LocalDirPackageResolver joined the raw ComponentRef name into a filesystem
path (root.join(format!("{}.json", r.name()))) with no containment check,
so a name containing '/', '\\', '..' or NUL (e.g. ../../../../etc/hosts)
escaped root and read a file outside it.

Add is_safe_component() and reject any name that is not a single safe path
component in resolve_agent BEFORE the join, returning the existing
ResolveError::NotFound. Adds a unit test proving a traversal name is rejected
(not read) plus a direct test of the guard predicate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 544891924ce2d135bc893840ed01aa51cb148dcf)
- pmcp-agent with openai-compat feature (agent dev source)
- pmcp-team-servers with runtime+http (http transitively enables member-llm)
- pmcp-package caret "0.1" pin (CLI-04 tripwire)
- version 0.17.4 -> 0.18.0

(cherry picked from commit b2dff8de31912c73a6ef21b21cfdb86c5f0bd959)
…lers

- agent group: AgentCommand{New,Dev}, async execute
- team group: TeamCommand{Dev}, async execute
- package group: PackageCommand{Show,Capture}, async execute
- all handler bodies actionable anyhow::bail! stubs (no panic)
- unused params underscore-prefixed for zero-warning gate
- capture carries capture-local --target (Codex MEDIUM, for 110-05)

(cherry picked from commit 7e732c8caf04fe0269f3e6f83783c678fa691d54)
- three enum Commands variants mirroring the Workbook arm
- three dispatch arms via execute_agent/team/package async wrappers
- each wrapper owns a tokio runtime + block_on (mirrors execute_landing)
- Package NOT added to is_target_consuming (capture resolves own target)
- verb_help.rs asserts agent/team/package --help list subcommands

(cherry picked from commit a238142)
- tests/scaffold_agent.rs invokes the real cargo-pmcp binary, asserts the
  four emitted files, round-trips agent.package.json through AgentPackage,
  then spawns cargo check + cargo test --test pin on the emitted crate
- covers the --force destination-overwrite policy
- extend shared append_crates_io_patch to also patch the unpublished
  pmcp-agent + pmcp-package 0.1.0 crates (transitive closure for the runner)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2052b7b)
…evel pin tripwire

- templates/agent.rs: generate() emits Cargo.toml (full dep set), src/main.rs
  (a runner that LOADS agent.package.json and drives AgentEngine), the
  AgentPackage manifest (built from the real struct → guaranteed round-trip),
  and an in-scaffold tests/pin.rs tripwire
- PMCP_AGENT_VERSION drift-guard test asserts the pin == crates/pmcp-agent
  [package] version (D-05); manifest round-trip + full-dep + manifest-driven
  unit tests
- register templates::agent in the bin tree and a templates_agent lib seam
  (mirrors templates_workbook_server) so the drift guard runs under --lib
- add semver dep for constructing the starter AgentPackage version

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit d981e2f)
…mote validate_crate_name (GREEN)

- promote validate_crate_name to pub(crate) (reuse, not re-implement — D-01a)
- agent/new.rs: validate the crate name before any fs write, resolve the
  target dir (default ./<name>), reject a symlinked destination and refuse a
  non-empty dir unless --force, create src/, then delegate to
  templates::agent::generate; gated colored next-steps
- ensure_destination_writable helper keeps the handler under cog 25
- the scaffold_agent integration test now passes GREEN (compile + pin + force)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit c22c4a2d2b949a28fbad46a67ae39c4f3beb20be)
…ss format!)

The pin.rs template interpolates nothing itself — its {version}/{EXPECTED_LINE}
are the scaffold's own assert! args. Return a plain literal instead of format!
with doubled braces (byte-identical output, clears clippy useless-format).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7609631817e38254df6ee3bdef71eccf88d43810)
…hosted agent dev tests

- fixed_source_runs_offline: drives the lib-safe run_fixed_source seam to
  RunOutcome::Completed + asserts the real binary agent dev --source fixed exits 0
- sampling_hosted_run_in_process: AgentServer + SamplingSourceFactory over an
  in-process DuplexTransport, scripted end-turn host, driven to a terminal task
- RED: cargo_pmcp::agent_run seam and the wired handler do not exist yet

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 800f0300678dc7a5215dd2233cd3e2f7bf917a26)
…source seam

- commands/agent/run.rs: lib-safe run_fixed_source(config) -> RunOutcome (leaf;
  no clap/GlobalFlags) + shared NoopInvoker; mounted in lib.rs as agent_run via
  a #[path] seam so the offline test + 110-06 example reach the same production path
- commands/agent/dev.rs: --source clap ValueEnum (OpenaiCompat|Sampling|Fixed);
  loads an AgentPackage (--package / ./agent.package.json / built-in demo) and
  resolves it via resolve_agent; openai-compat builds OpenAiCompatSource via
  with_options, maps a construction Decode error to a --allow-insecure-http bail
  and a non-Completed RunOutcome to an actionable --endpoint/--source fixed bail;
  sampling serves AgentServer over pmcp::StdioTransport; fixed delegates to the seam
- GREEN: fixed + sampling agent_dev tests pass; no PMAT cog>25 on new helpers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit cbff3b5)
- transcript_drives_seven_step_doc_review_offline: TeamRuntimeBuilder +
  fixed_override drives the 7-step doc-review flow, asserts step ordering
  and a clean rt.shutdown() count (fully offline)
- serve_exposes_team_mcp_over_http: the public team-mcp binary recipe
  (build_team_mcp_server + StreamableHttpServer) on an ephemeral loopback
  port; asserts a tools/list over HTTP lists the team_mcp__<member> tools
- llm_drives_against_mock_endpoint: OpenAiCompatSource wrapped in the
  exported FixedSourceFactory against a mockito endpoint; asserts the mock
  was hit and a terminal related-task pointer surfaced
- Tests characterize the composable primitives the CLI handler assembles
  (commands::* is bin-only, so the stub cannot be reached from an
  integration test)

(cherry picked from commit bf50b8f)
- default: compose the doc-review team via TeamRuntimeBuilder over
  in-memory transports on an offline FixedSource and print a labeled
  7-step transcript; TeamPackage from --package (+ --data-dir) or the
  built-in doc-review fixture (D-02 locked default). Composition is
  delegated entirely to TeamRuntime — no hand-rolled server spin-up.
- --serve: expose team-mcp over HTTP via the shipped team-mcp binary's
  PUBLIC serve recipe (member-wiring loop -> build_team_mcp_server ->
  serve_streamable_http) on 127.0.0.1:<--port>, running until Ctrl-C.
  Not TeamRuntime, no upstream API change.
- --llm <endpoint>: parse with url::Url, build+validate an
  OpenAiCompatSource once (Decode -> actionable --allow-insecure-http
  bail), wrap it in the exported FixedSourceFactory (correct
  sync/infallible shape), key from --llm-api-key-env as a SecretString.
- Replaces the 110-01 bail! stub; each branch is a small helper (cog <=25).

(cherry picked from commit 05b26983f414a7bcc06d28c6334f886bd9e5f446)
…pin tripwire

- pmcp_package_pin.rs asserts cargo-pmcp pins pmcp-package = "0.1" (CLI-04/D-04b), green immediately
- package_show.rs builds an agent fixture via pack_agent and drives the real binary offline
- edge cases: non-OCI-layout path + zero-manifest layout must error clearly (RED pending show handler)

(cherry picked from commit 361abb8d786832d6dda752f5154c1ef022cd8e24)
…line dispatch

- kind.rs: PURE detect_kind + artifact_type_from_manifest_json leaves (never panic on adversarial input) + table-driven units + proptest 'Some iff known constant'
- lib #[path] seam exposes kind.rs as cargo_pmcp::package_kind so --lib reaches it and 110-06 can fuzz the untrusted manifest-parse boundary
- show.rs: OciLayout::open + reject zero/multiple manifests + detect_kind over BOTH artifactType and config/layer media types + unpack_* dispatch + colored offline render; digest verification surfaces via unpack_*

(cherry picked from commit 3084f56)
…iry check, timeout + non-2xx upload

- capture.rs: resolve_target(--target) + TokenCacheV1.entries.get + .value (correct APIs); is_near_expiry bails before uploading an expired token; actionable configure/auth guidance when unconfigured; packs the OCI layout to an in-memory zip; never prints the token
- capture_upload.rs: lib-safe HTTP seam (Bearer header + request timeout + bounded non-2xx error), mounted into lib as cargo_pmcp::package_capture so --lib capture_upload + 110-06 reach it
- mockito unit tests prove Bearer/path/bytes + 2xx->Ok / 500->Err; child-process HOME-isolated unconfigured integration test

(cherry picked from commit 76d15a2)
…as a live candidate

- gives the pure untrusted-parse leaf a production caller over the RAW manifest bytes (the 110-06 fuzz boundary), removing the bin-target dead-code path

(cherry picked from commit d5f2228)
…w tests

Cross-plan fmt integration fix — 110-01's multi-line execute_agent
signature and 110-05's test formatting are collapsed by rustfmt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit fafb188801ece62841662206ab462377413e8e0d)
…seams

- Mark templates_agent / agent_run / package_kind seams #[doc(hidden)]
  (Codex 110-06 MEDIUM — internal support seams, not stable API)
- Add examples/agent_scaffold_and_run.rs: scaffold via templates_agent::generate
  then drive the PRODUCTION run_fixed_source runner offline (Codex 110-06 HIGH —
  no re-implemented AgentEngine loop)
- Satisfies the CLAUDE.md ALWAYS EXAMPLE requirement for CLI-01/CLI-02

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit ed0118c)
… boundary

- New libfuzzer target: raw bytes → artifact_type_from_manifest_json → detect_kind
- Exercises the real package show untrusted seam (Codex 110-06 MEDIUM), never
  panics on adversarial manifest bytes (threat T-110-05-03 / T-110-06-01)
- Reaches production via the #[doc(hidden)] package_kind lib seam (no new dep)
- Satisfies the CLAUDE.md ALWAYS FUZZ requirement for the package parse path

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 95edacdf8ffa6954778370d2784b2a1dbf41692e)
- New examples/team_dev_transcript.rs: composes the built-in doc-review team via
  TeamRuntimeBuilder over in-memory transports with a FixedSource override
- Drives the 7-step doc-review flow and rt.shutdown() fully offline (D-02 — no
  hand-rolled server spin-up), mirroring team dev's default path
- Satisfies the CLAUDE.md ALWAYS EXAMPLE requirement for CLI-03

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 169b299)
Cleanup pass over the Phase 110 diff (no behavior change):
- Extract shared resolve_api_key + build_openai_compat_source into
  commands/agent/sources.rs; agent dev and team dev --llm both call it
  instead of maintaining byte-identical copies.
- builtin_demo_package delegates to templates::agent::starter_package
  (the two were identical AgentPackage literals).
- Collapse agent_ref/server_ref into one component_ref(name, kind).
- Drop dead write-only EndTurnSource.calls field (now a unit struct).
- Drop redundant client-builder timeout in package capture (the
  capture_upload seam already applies the per-request timeout).
- Reuse pmcp_team_servers::transport::DuplexTransport and
  cargo_pmcp::agent_run::NoopInvoker in tests instead of re-implementing.
- Add missing #[doc(hidden)] to the package_capture lib seam for
  consistency with agent_run/package_kind.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 71fba19)
… guards

Code-review findings on the Phase 110 diff:
- finish_engine_outcome: map RunOutcome::LimitReached and RetryRequired to
  cause-specific messages instead of the misleading 'endpoint may be
  unreachable' — a hit iteration/token limit or requested retry is not a
  connectivity failure.
- agent new: reject a destination that already exists as a regular file with
  an actionable message, instead of failing opaquely in create_dir_all under
  --force.
- package capture: the expiry gate fires within the refresh window (before
  actual expiry), so say 'expired or about to expire', not 'is expired'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit f17e426)
Pre-existing rustfmt drift (rustfmt 1.9.0) in files outside Phase 110, surfaced
by make quality-gate's workspace-wide fmt --all --check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit c5a956170b0efeba9a39e6e31a6291783e092599)
Repo + CLI documentation for the Phase 110 verbs (book/course deferred):
- Clean plan-internal leakage from user-facing CLI help ('delivered by Plan
  110-0X', internal 'resolve_target' in --target help).
- New per-command reference pages: docs/commands/{agent,team,package}.md.
- cargo-pmcp/README.md: new 'Agents & Teams' section, Commands-table rows,
  and a Features bullet, all grounded in the real CLI help.
- Root README.md: new 'Agents & Teams' ecosystem bullet.
- Add README.md for pmcp-agent and pmcp-team-servers (they had none and are
  first-publish 0.1.0 crates — crates.io needs a landing page) + wire the
  readme field in both manifests, matching pmcp-package.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit f3e6732)
…ct, drop capture

pmcp.run platform review: our local package show|capture squatted the verb
names their remote capture service defines with opposite semantics, and
capture POSTed to a nonexistent endpoint. Per the review + owner decision:

- Rename package show → package inspect (honest local offline OCI-layout
  inspector). Frees show for the platform's remote manifest-fetch thin client.
- Drop package capture entirely (handler, capture_upload seam, tests,
  placeholder endpoint) — its endpoint 404s everywhere and the name belongs to
  the platform's remote dependency-graph capture. Frees capture too.
- Keep the pmcp-package caret 0.1 pin tripwire (platform conceded =0.1.0).

Two review DX papercuts fixed:
- --api-key-env with an unset/empty var now errors clearly instead of a silent
  empty key (which surfaced as a confusing 401).
- team dev's built-in fixture used max_team_total_tokens:1 + 1s wall clock,
  which would kill any real --llm run; raised to dev-harness limits.

Also gitignore the auto-discovered fuzz_package_kind corpus (was untracked).
Docs updated for inspect-only + the 'agent/team produce OCI layouts' overclaim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit a2af479)
pmcp.run platform review found the shipped loop demo-grade: it never told the
model its tools and never read provider token usage back, so it couldn't drive
real tool use and its budget could never trip. Per the review + owner decision,
wire both before the 0.1.0 tag (Gap 3 / opaque provider blocks deferred to 0.2
with a README note):

Gap 1 — tools via tools/list discovery:
- Add a defaulted list_tools() to the ConnectorClient seam (UrlConnectorClient
  delegates to pmcp::Client::list_tools) and to the ToolInvoker seam (Arc<dyn>
  forwards; ClientToolInvoker lists its connector). Defaults keep existing impls
  compiling — backward-compatible.
- Engine resolves tools once per run, filters by the package tool-selection
  allow-list (empty = advertise all), and passes them via CreateMessageParams
  .with_tools() so the model can actually request tool calls.

Gap 2 — provider usage → live budget:
- openai_compat + anthropic now parse provider usage (total_tokens; input+output)
  into the result _meta the loop already reads, so the token budget can trip.
- Split the overloaded max_tokens: it stays the per-turn output cap; a new
  optional token_budget carries the cumulative run budget (None = iterations
  bound the run). Prevents the per-turn cap from killing runs after one turn
  once usage is live.

Tests: +2 usage-mapping (both sources), +2 discovery (advertise-all + allow-list
filter). Full suite green (lib 74→78) on default and openai/anthropic/url features.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit f7982cb)
CI's PMAT complexity gate (cog <=25) flagged 4 functions in the new
Phase 107-109 crates. Behavior-preserving extract-method refactors;
all existing unit/property/conformance tests pass unchanged:

- pmcp-package validate (47): split into ordered check_target/
  check_tier/check_iam/check_statement helpers (first-violation-wins
  ordering preserved).
- pmcp-team-servers match_value (58): delegate the object/array/scalar
  arms to match_object/match_array/match_scalar.
- pmcp-team-servers select (24): extract per-segment descend().
- pmcp-team-servers copy_recursive (27): factor out an io_err() wrapper
  plus copy_dir/copy_file branch helpers.

`pmat quality-gate --checks complexity` now passes (0 violations).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te single-version

umya-spreadsheet 3.0.1 bumped its transitive quick-xml 0.37 -> 0.41. Because
Cargo.lock is gitignored (CI always cold-resolves), a caret `umya = "3.0"`
drifts onto 3.0.1, which:
  - forks Cargo.lock into two quick-xml versions (0.37.5 + 0.41.0) against the
    compiler's direct `quick-xml = "0.37"` pin, so `cargo tree -i quick-xml`
    errors "ambiguous" and the Phase-93 single-version purity gate fails closed;
  - regresses a data-validation-list ingest test under 3.0.1.

Exact-pin to =3.0.0 (the version every test + purity run has been validated
against): cold resolve keeps a single quick-xml 0.37.5, `make purity-check`
passes, all 375 compiler tests pass. No code changes. Revisit the 3.0.1 bump
deliberately (realign the quick-xml pin + port the 0.41 reader API + triage the
ingest delta) rather than via silent caret drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Makefile purity-check Layer 2 invokes cargo-deny with 0.18.3's CLI ordering
(`check --config deny.toml bans`). CI installed cargo-deny *latest* via
taiki-e/install-action, and newer cargo-deny relocated `--config` to a global
flag (before `check`), rejecting the old form with a usage error (exit 2) —
silently breaking the Purity Gate once the single-version guard passed. Pin both
install sites to 0.18.3 (same approach as the pinned PMAT_VERSION). Verified:
`make purity-check` passes locally with cargo-deny 0.18.3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@guyernest
guyernest merged commit 8c02a87 into paiml:main Jul 19, 2026
27 checks passed
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