Module: Portage::Ucp::Client

Defined in:
lib/portage/ucp/client.rb,
lib/portage/ucp/client/errors.rb,
lib/portage/ucp/client/session.rb,
lib/portage/ucp/client/version.rb,
lib/portage/ucp/client/tool_result.rb,
lib/portage/ucp/client/transports/http.rb,
lib/portage/ucp/client/transports/stdio.rb,
lib/portage/ucp/client/transports/loopback.rb,
lib/portage/ucp/client/transports/ucp_wire_shape.rb,
lib/portage/ucp/client/transports/local_arguments.rb,
lib/portage/ucp/client/transports/http/complete_checkout_wire_shape.rb

Overview

Client-side counterpart to the rest of portage-ucp: everything else in this repo lets a Ruby program expose a commerce backend to agents (server side). This gem lets a Ruby program act as the shopper's agent — discover somebody else's manifest, or drive an owner's own Adapter directly, and place an order.

Defined Under Namespace

Modules: ToolResult, Transports Classes: DiscoveryError, Error, ManifestShapeError, MissingAgentProfileError, PaymentPermissionError, ServerError, Session, UnsupportedWireShapeError

Constant Summary collapse

MANIFEST_PATH =
"/.well-known/ucp".freeze
USER_AGENT =

Sent on every request this gem makes to a store (the manifest GET and each Streamable HTTP call) unless the caller's own headers name one, so a merchant reading its logs can tell a Portage agent from Ruby's or Faraday's default and find out what it is.

"portage-ucp-client/#{VERSION} (+https://github.com/tomtom87/Portage)".freeze
VERSION =
"0.6.3".freeze

Class Method Summary collapse

Class Method Details

.connect(command: nil, args: [], env: nil, url: nil, headers: {}, capabilities: nil, proxy: nil) ⇒ Object

Connects over stdio (a subprocess) or Streamable HTTP (a URL) — pass exactly one of command: or url:.

Parameters:

  • proxy (String, Hash, nil) (defaults to: nil) —

    forwarded to Transports::Http (a url: connection only) — see its own doc comment.



45
46
47
48
49
50
51
52
53
54
# File 'lib/portage/ucp/client.rb', line 45

def self.connect(command: nil, args: [], env: nil, url: nil, headers: {}, capabilities: nil, proxy: nil)
  transport = if command
                Transports::Stdio.new(command: command, args: args, env: env)
              elsif url
                Transports::Http.new(url: url, headers: headers, proxy: proxy)
              else
                raise ArgumentError, "connect requires either command: or url:"
              end
  Session.new(transport: transport, capabilities: capabilities)
end

.discover(url, headers: {}, proxy: nil) ⇒ Session

GETs <url>/.well-known/ucp, parses the manifest, and connects to the mcp-transport endpoint it advertises in services (see Portage::Ucp::Manifest#services — the core-gem fix this client depends on to know where to connect).

Parameters:

  • headers (Hash{String => String}) (defaults to: {}) —

    sent with the manifest GET and every call after it — e.g. a "User-Agent" naming the app built on this gem.

  • proxy (String, Hash, nil) (defaults to: nil) —

    forwarded to the Streamable HTTP connection this discovers into — see Transports::Http.

Returns:

  • (Session) —

    scoped to the manifest's advertised capabilities.



66
67
68
69
# File 'lib/portage/ucp/client.rb', line 66

def self.discover(url, headers: {}, proxy: nil)
  manifest = fetch_manifest(url, headers)
  connect(url: mcp_endpoint(manifest), headers: headers, capabilities: capability_names(manifest), proxy: proxy)
end

.for_adapter(adapter, **server_opts) ⇒ Object

Wraps an already-built Adapter directly — no subprocess/socket. Still runs the real merchant-side contract (authenticator, rate limiter, Dispatcher, WireEnvelope, via Portage::Ucp::Mcp::Server), just in-process. This is the transport for the "your own store" case: you already have credentials for this Adapter, so there's no manifest to discover and no wire hop to make.



37
38
39
# File 'lib/portage/ucp/client.rb', line 37

def self.for_adapter(adapter, **server_opts)
  Session.new(transport: Transports::Loopback.new(adapter: adapter, **server_opts))
end

.with_user_agent(headers) ⇒ Object

USER_AGENT unless headers already names one, in any case.



87
88
89
90
91
# File 'lib/portage/ucp/client.rb', line 87

def self.with_user_agent(headers)
  return headers if headers.keys.any? { |name| name.to_s.casecmp?("user-agent") }

  { "User-Agent" => USER_AGENT }.merge(headers)
end