Skip to content

feat: unify profile contract, fnish the 0.9.0 profile and transport contracts - #55

Merged
igrigorik merged 6 commits into
mainfrom
feat/unify-profile-contract
Sep 6, 2026
Merged

feat: unify profile contract, fnish the 0.9.0 profile and transport contracts#55
igrigorik merged 6 commits into
mainfrom
feat/unify-profile-contract

Conversation

@igrigorik

Copy link
Copy Markdown
Contributor

#53 made UCP release negotiation profile-driven. This finishes that contract before 0.9.0 ships:

  • Treats local profile.json as the negotiation source, with ucp doctor checking that the served document agrees.
  • Keeps MCP routing independent of active.yaml and limits the server to commerce operations.
  • Upgrades incur and undici, raises the Node.js floor to 22.19.0, and refreshes the development toolchain.
  • Centrally refuses redirects as required by UCP, with actionable transport and doctor diagnostics.
  • Rewrites the 0.9.0 changeset around observable upgrade behavior.

Supersedes Dependabot #44, #45, and #49.

incur 0.5 publishes a tool for any command group that declares a root
handler. No group in this CLI declares one, so the tool surface is
unchanged — verified against a live tools/list, which returns the same
14 commerce tools before and after.

0.5 still defaults MCP tool discovery to 'progressive'
(Mcp.js: `options.tools?.discovery ?? 'progressive'`), so the
`discovery: 'direct'` pin remains load-bearing rather than
belt-and-braces.

tokenx stays at 1.3.0: incur declares ^1.3.0 and resolves to it. The
--token-count/--token-limit/--token-offset builtins it backs have no
references in src/, so nothing here changes behaviour with it.
Two faces of one defect: the CLI decided things on the user's behalf,
then handed them remedies they could not perform.

A named profile's profile.json is now what the CLI negotiates from at
every URL, whoever owns it. The release-default path used to negotiate
from a bundled snapshot and ignore the local file, so a hand-edit was
silently inert. Doctor's severity follows consequence instead of hosting:
anything that makes our requests wrong is fatal, because warn and pass
produce the same top-level `ok: true` and a consumer writing
`if (result.ok)` never enumerates checks.

Accepted trade-off: upgrading the CLI does not refresh an existing
profile. `profile init --force` is the only thing that rewrites
profile.json, and doctor reports the divergence. Auto-refresh would
reinstate exactly the silent mutation this removes.

The MCP server now publishes 14 commerce tools. Profile management,
`use`, and `doctor` carry `mcp: false`: one stdio server multiplexes
unrelated agent conversations, and none may repoint the operator's active
profile out from under the others. For the same reason MCP mode does not
read ~/.ucp/active.yaml at all — hiding the writer while leaving the
reader would make shared routing state invisible as well as shared. The
integration test asserts an exact sorted tool-name set; the previous
`>= 20` let additions pass silently, which is how CLI-management commands
became agent-callable to begin with.

Three remedies that could not be performed, removed:

  - SERVICE_VERSION_INCOMPATIBLE told the reader the declaration came
    from a document they cannot edit. It comes from their profile.json.
  - The MCP BUSINESS_NOT_RESOLVED CTA offered `ucp use` and active.yaml,
    the two things that cannot work in the context that raises it.
  - profile-hint derived a profile's version from its URL string, so it
    could suggest a profile that speaks a different one. It reads the
    document body.

Also corrects three comments the incur 0.5 bump falsified. `--verbose`
still cannot become a registered global option: incur reserves the name
and throws at construction while implementing nothing behind it.
undici 8 requires node >=22.19.0, so engines moves off >=22. That floor
was compiled into the binary by taking the first integer out of the
engines range, which silently yielded 22: doctor reported Node 22.10.0 as
supported on exactly the machines the new floor excludes, and since npm
only warns on an engines mismatch at install time, that check is the
user's only signal. The floor is compared across major, minor and patch,
and defines.mjs throws at build time rather than baking a value it could
not parse.

undici 8 forwards plain http through a proxy in absolute form instead of
tunnelling it with CONNECT. Every request this CLI initiates is https and
still tunnels, so a proxy ACL must permit CONNECT; plain http is reached
only through the loopback test escape hatch or a merchant that redirects
https to http. The fixture tests pin both wire forms per scheme, and the
no_proxy test asserts both so it cannot go vacuous when a default changes
underneath it again.
   UCP requires profile, identity, and schema documents to be served without
   redirects and requires implementations not to follow them. Enforce that
   contract at ucpFetch with redirect: manual, retaining the status and
   Location for an actionable error.

   Apply the same policy to negotiated service endpoints so the URL called
   stays equal to the endpoint declared in the business profile. Preserve
   TRANSPORT_REDIRECT_REFUSED through cache and MCP error mapping, and report
   agent-profile redirects as a failing profile-redirect doctor check.

   Cancel refused response bodies so connections remain reusable. Cover the
   redirect statuses, 304 handling, error propagation, doctor output, and the
   no-second-request guarantee over real sockets.
   Resolve the latest development dependency group ahead of the 0.9.0
   release.

   Migrate the Biome configuration to 2.5.10, fix its new unsafe optional
   chaining finding, and accept the formatter changes required by the
   upgraded toolchain.
@igrigorik
igrigorik merged commit 1cc02a2 into main Sep 6, 2026
13 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