portage-cli 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +188 -0
- data/README.md +117 -6
- data/lib/portage/cli/buy.rb +467 -48
- data/lib/portage/cli/buyer_context.rb +42 -0
- data/lib/portage/cli/checkout_handoff.rb +70 -0
- data/lib/portage/cli/confidence_check.rb +127 -0
- data/lib/portage/cli/config.rb +52 -0
- data/lib/portage/cli/decisions.rb +72 -0
- data/lib/portage/cli/doctor.rb +17 -0
- data/lib/portage/cli/find.rb +10 -9
- data/lib/portage/cli/generate/agent_profile.rb +66 -6
- data/lib/portage/cli/history.rb +23 -6
- data/lib/portage/cli/notifier.rb +73 -0
- data/lib/portage/cli/setting.rb +44 -0
- data/lib/portage/cli/version.rb +1 -1
- data/lib/portage/cli.rb +145 -19
- metadata +27 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c2f71cd7ac7290b314bc824678fc8a9fbc31b6e43cdea4c4e78077440c555c9
|
|
4
|
+
data.tar.gz: f63510ff1b5d5d945e6e9753b0cf1a521d8da6b084d562c230c4d6b5a98ad6ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 802fb90a17c93986ff7f9101e09e845af467e11607985ddd62cefcd4704f25c5f05292f871c168adeebf386d92620471d6ea6bd8273a74fabb11a3c765324f7d
|
|
7
|
+
data.tar.gz: '021008e8975eb3002986f0e9f27d5d776b1da067c3fbc6083909abbecde99a616680d3569c609fae0d87ae402e4002577c8cca6bd62ca4811d95c580d8ba8d10'
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,194 @@ All notable changes to this project are documented here. Format loosely follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/); this project is
|
|
5
5
|
pre-1.0, so APIs may still shift between minor versions.
|
|
6
6
|
|
|
7
|
+
## [0.7.0] - 2026-09-23
|
|
8
|
+
|
|
9
|
+
- **The decision layer is wired in.** `buy`/`find` make their judgment
|
|
10
|
+
calls through the same rules `portage-ucp-decision` wraps. Ranking,
|
|
11
|
+
escalation and the policy check live in `portage-ucp` core
|
|
12
|
+
(`Support::OfferRanking`, `Support::Escalation`, `PolicyGuard`), so they
|
|
13
|
+
run the same with or without that gem. `portage-ucp-decision` stays an
|
|
14
|
+
optional plugin that only the confidence gate needs. Every checkout
|
|
15
|
+
report carries the verdicts under `decisions:` (`escalation`, `policy`,
|
|
16
|
+
and `confidence` when enabled). Requires `portage-ucp ~> 0.9`.
|
|
17
|
+
- `find` ranks offers buyable first, then cheapest, then unpriced. The
|
|
18
|
+
order is unchanged.
|
|
19
|
+
- When a checkout both requires escalation and mismatches the request
|
|
20
|
+
under `PORTAGE_ABORT_ON_CHECKOUT_MISMATCH`, `buy` now reports the
|
|
21
|
+
store's escalation, not the mismatch. Both hand off the checkout.
|
|
22
|
+
- `buy --yes` now checks your spend policy before completing, on remote
|
|
23
|
+
native-UCP stores too. Before, only the own-store flow's in-process
|
|
24
|
+
`Dispatcher` enforced it, so a remote store never saw your caps,
|
|
25
|
+
allowlist or token scopes. A blocked purchase hands off the checkout.
|
|
26
|
+
- New opt-in confidence gate: `--decision-backend jev|laya` /
|
|
27
|
+
`PORTAGE_DECISION_BACKEND`, with a threshold from `--min-confidence` /
|
|
28
|
+
`PORTAGE_MIN_CONFIDENCE` (default `0.8`). Needs `portage-ucp-decision`.
|
|
29
|
+
A low score, a backend that can't answer, or a missing gem holds the
|
|
30
|
+
purchase and hands off the checkout.
|
|
31
|
+
- Every verdict carries a `reason`: a string, or null when the gate
|
|
32
|
+
passed, the same in-process as in `--json`. `confidence.reason` is
|
|
33
|
+
`below_threshold`, `backend_error` or `not_installed`, and
|
|
34
|
+
`confidence.error` holds the detail behind the last two.
|
|
35
|
+
- **`buy` checked out sold-out items when in-stock matches were right
|
|
36
|
+
there.** It took the top search hit and its first variant unconditionally,
|
|
37
|
+
so a sold-out top hit dead-ended on the store's "Sold out" refusal
|
|
38
|
+
(allbirds.com and billabong.com, 2026-09-23). It now takes the first hit
|
|
39
|
+
with stock, and that product's first in-stock variant. When nothing
|
|
40
|
+
reports stock it still falls back to the top hit, and `--product-id`
|
|
41
|
+
still buys exactly that product.
|
|
42
|
+
- `doctor` accepts `TYPESAFE_API_KEY` in place of `JEV_API_KEY`, matching
|
|
43
|
+
`portage-ucp-decision`'s Jev backend.
|
|
44
|
+
- **An agent couldn't tell a held purchase from a bought one without reading
|
|
45
|
+
prose.** `decisions:` only covers the three gates, so a missing payment
|
|
46
|
+
token, a permission-denied store, a dry run or a missing `--yes` all
|
|
47
|
+
showed `escalate: false, allowed: true`. Every `buy` report now carries
|
|
48
|
+
`outcome` (`purchased`, `no_payment_token`, `policy_blocked`, ... — the
|
|
49
|
+
full list is in the README), and the text output leads with it as
|
|
50
|
+
`[outcome]`. Checkout reports also carry `items`, what the checkout
|
|
51
|
+
actually holds, alongside `products`, the search results.
|
|
52
|
+
- **History recorded the wrong things.**
|
|
53
|
+
- A purchase entry listed all ten search results, not what was bought,
|
|
54
|
+
and had no way to tell a completed purchase from a dry run or a held
|
|
55
|
+
one. Entries now carry `outcome`, `items`, `total`/`currency`,
|
|
56
|
+
`checkout_id`, `source`, and the `checkout_url` for anything not
|
|
57
|
+
completed.
|
|
58
|
+
- A `buy` whose search matched nothing was logged as a purchase. It's now
|
|
59
|
+
a search at that store, as are browse-only and dead-end buys, which
|
|
60
|
+
weren't logged anywhere.
|
|
61
|
+
- The search behind a `buy` with no URL wasn't logged at all.
|
|
62
|
+
- **Checkout hand-off webhooks.** The body now includes the report's
|
|
63
|
+
`message`, the `store` and the shopper's `query`, so a Slack/Zapier relay
|
|
64
|
+
can post it without a lookup. A plain-text 2xx (Slack's `ok`) was
|
|
65
|
+
reported as a failed delivery, because the reply was parsed as JSON; any
|
|
66
|
+
2xx now counts. The POST times out after 5s instead of stalling the buy
|
|
67
|
+
for up to two minutes.
|
|
68
|
+
- **The confidence gate could crash a buy after the checkout existed.** Only
|
|
69
|
+
`Decision::Error` was rescued, so a backend's timeout, parse error or
|
|
70
|
+
missing binary escaped as a backtrace with no report, hand-off or history
|
|
71
|
+
entry. Any failure now holds the purchase like a low score does.
|
|
72
|
+
- **The spend policy failed open on a checkout with no total.** PolicyGuard
|
|
73
|
+
skips both caps without an amount, so `buy --yes` completed past a
|
|
74
|
+
configured cap while reporting `allowed: true`. It's now denied as
|
|
75
|
+
`total_unknown` whenever a cap is set.
|
|
76
|
+
- `doctor` checks the confidence backend you selected
|
|
77
|
+
(`PORTAGE_DECISION_BACKEND`): the gem missing, an unknown name, a missing
|
|
78
|
+
`JEV_API_KEY` or Laya bridge, or a bad `PORTAGE_MIN_CONFIDENCE`. It no
|
|
79
|
+
longer asks for `JEV_API_KEY` when no backend is selected.
|
|
80
|
+
- The no-buyer-context warning names every variable it reads and says to
|
|
81
|
+
set at least `PORTAGE_SHIP_COUNTRY`.
|
|
82
|
+
- **Settings resolve one way everywhere.** The auto-open toggle, the
|
|
83
|
+
notify webhook, `PORTAGE_ABORT_ON_CHECKOUT_MISMATCH` and the confidence
|
|
84
|
+
gate's backend/threshold each parsed their own env var and repeated the
|
|
85
|
+
"flag, then env var, then `config.json`" order. They now share
|
|
86
|
+
`Portage::Cli::Setting`. Two edge cases change:
|
|
87
|
+
- A blank flag or env var (`PORTAGE_AUTO_OPEN_CHECKOUT=`,
|
|
88
|
+
`--notify-webhook ""`) is unset and falls through to the next level.
|
|
89
|
+
Before, an empty auto-open env var turned auto-open off, and an empty
|
|
90
|
+
`--notify-webhook` turned the webhook off.
|
|
91
|
+
- A `config.json` `"auto_open_checkout"` string is read like the env var,
|
|
92
|
+
so `"no"` means off. Before, any non-empty value meant on.
|
|
93
|
+
- Report totals and the policy check's amount come from core's
|
|
94
|
+
`Support::Totals.amount` instead of their own lookups.
|
|
95
|
+
- **Remote purchases didn't count toward the rolling cap or velocity
|
|
96
|
+
limit.** Both count the transaction log (`~/.portage/transactions.json`),
|
|
97
|
+
and only the own-store `Dispatcher` wrote to it, so `buy --yes` against a
|
|
98
|
+
remote native-UCP store never added to either. `buy` now records a remote
|
|
99
|
+
completion there under the merchant host, with its amount, currency and
|
|
100
|
+
token ref: reserved before the store is asked, settled `complete` only
|
|
101
|
+
when it comes back purchased, `failed` otherwise. The own-store loopback
|
|
102
|
+
path is still left to `Dispatcher`, so nothing is counted twice. If the
|
|
103
|
+
log can't be written after the purchase, the report says so in
|
|
104
|
+
`warnings` rather than losing the purchase to an exception.
|
|
105
|
+
- **A garbage `PORTAGE_MIN_CONFIDENCE` broke every `buy`**, even with no
|
|
106
|
+
decision backend selected, because the threshold was parsed up front
|
|
107
|
+
either way. The env var is now read, and validated, only once a backend
|
|
108
|
+
is selected. An explicit `--min-confidence` is still always validated.
|
|
109
|
+
- **`buy --json` could refuse a flag without any JSON.** An out-of-range
|
|
110
|
+
threshold went to stderr as a bare line, and a flag OptionParser couldn't
|
|
111
|
+
read (`--min-confidence high`, an unknown flag) escaped as a backtrace.
|
|
112
|
+
Under `--json` both now print a report with `outcome: "invalid_option"`
|
|
113
|
+
and exit 1; without it, both are one line on stderr. The threshold is
|
|
114
|
+
also checked before the search when `buy` has no URL, not after it.
|
|
115
|
+
|
|
116
|
+
## [0.6.4] - 2026-09-22
|
|
117
|
+
|
|
118
|
+
- **Every dead-end hand-off pointed the shopper at the store's refund
|
|
119
|
+
policy.** `#checkout_url_of` took the first entry in the checkout's
|
|
120
|
+
`links` with a url, on the reasoning that not every backend types its
|
|
121
|
+
link entries. Live UCP stores put nothing *but* policy links there:
|
|
122
|
+
five third-party Shopify stores checked 2026-09-22 returned
|
|
123
|
+
`refund_policy`, `privacy_policy`, `terms_of_service`, `shipping_policy`
|
|
124
|
+
and `contact_information`, and never a checkout link — the checkout is
|
|
125
|
+
always at `continue_url`. So `requires_escalation`, permission-denied and
|
|
126
|
+
no-payment-token reports all handed over a policy page, `--auto-open`
|
|
127
|
+
opened it and `--notify-webhook` posted it. Now reads `continue_url`
|
|
128
|
+
first, and the `links` fallback skips policy/contact entries rather than
|
|
129
|
+
handing over a wrong URL.
|
|
130
|
+
- `adapter_flow` rescued `LoadError` and `StandardError` identically,
|
|
131
|
+
returning `nil` either way — correct once the adapter gem genuinely isn't
|
|
132
|
+
installed, wrong once it's live and its own call actually failed. A live
|
|
133
|
+
adapter's `StandardError` (e.g. "no payment_method configured on this
|
|
134
|
+
Adapter") now comes back as its own report instead of the generic "no
|
|
135
|
+
automated path" dead end, distinguishable from "no adapter for this
|
|
136
|
+
platform" by `source`.
|
|
137
|
+
|
|
138
|
+
## [0.6.3] - 2026-09-22
|
|
139
|
+
|
|
140
|
+
- A store refusing a cart or checkout call on its own terms — out of stock,
|
|
141
|
+
a line it won't take, an expired cart — is reported as a normal outcome
|
|
142
|
+
carrying the store's own sentence and its `continue_url`, instead of
|
|
143
|
+
escaping `Buy#call` as an unhandled `Client::ServerError`. It printed a
|
|
144
|
+
Ruby backtrace whose "message" was the store's entire several-kilobyte
|
|
145
|
+
`ucp` error envelope; confirmed live 2026-09-22 against a genuinely
|
|
146
|
+
sold-out variant. Same posture `requires_escalation` and
|
|
147
|
+
`PaymentPermissionError` already had.
|
|
148
|
+
- Requires `portage-ucp-client ~> 0.6` (was `~> 0.5`), which is what
|
|
149
|
+
`Buy#complete`'s `rescue Client::PaymentPermissionError` has actually
|
|
150
|
+
needed since 0.6.2 — that constant landed in client 0.6.0, and a `rescue`
|
|
151
|
+
naming a missing constant raises `NameError` over the top of whatever
|
|
152
|
+
error it was meant to catch.
|
|
153
|
+
|
|
154
|
+
## [0.6.2] - 2026-09-22
|
|
155
|
+
|
|
156
|
+
- `Buy#complete` treats a `Client::PaymentPermissionError` from
|
|
157
|
+
`complete_checkout` (this agent not yet granted checkout-completion on the
|
|
158
|
+
store) as a normal outcome rather than a failure — same posture as
|
|
159
|
+
`requires_escalation`: the report carries the checkout's `continue_url`
|
|
160
|
+
so the shopper can finish on the merchant's own checkout page.
|
|
161
|
+
- Every dead-end `buy` outcome that hands a shopper a `checkout_url`
|
|
162
|
+
(`requires_escalation`, permission denied, no `--payment-token`) can now
|
|
163
|
+
auto-open that link in the shopper's browser and/or POST it to a webhook,
|
|
164
|
+
instead of leaving it as inert text/JSON. Both off by default; opt in with
|
|
165
|
+
`--auto-open`/`--notify-webhook <url>`, `PORTAGE_AUTO_OPEN_CHECKOUT`/
|
|
166
|
+
`PORTAGE_NOTIFY_WEBHOOK_URL`, or `~/.portage/config.json`
|
|
167
|
+
(`auto_open_checkout`/`notify_webhook_url`), in that precedence order.
|
|
168
|
+
Never fires on `--dry-run`. Best-effort throughout: a failed open or POST
|
|
169
|
+
never fails the buy, and surfaces instead as `handoff: {opened:,
|
|
170
|
+
notified:, notify_error:}` on the report. New `CheckoutHandoff`, `Notifier`,
|
|
171
|
+
and `Config` classes; `portage-ucp` core is untouched.
|
|
172
|
+
|
|
173
|
+
## [0.6.1] - 2026-09-22
|
|
174
|
+
|
|
175
|
+
- `portage generate agent-profile` emits the capability identifiers a UCP
|
|
176
|
+
server actually resolves an agent's tool registry from. It was emitting
|
|
177
|
+
`dev.ucp.shopping.catalog` — reusing `Portage::Ucp::Capabilities::CATALOG
|
|
178
|
+
.name`, which is correct for a business's own manifest, where one Capability
|
|
179
|
+
owns all three catalog actions, and wrong here: the registry is per action
|
|
180
|
+
(`dev.ucp.shopping.catalog.search`, `dev.ucp.shopping.catalog.lookup`). A
|
|
181
|
+
profile declaring the coarse name resolved to zero catalog tools, and stores
|
|
182
|
+
reported that as `-32602 Tool not found: search_catalog` seconds after
|
|
183
|
+
`tools/list` advertised it. Versions are now spec revisions (`2026-08-25`)
|
|
184
|
+
rather than `"1"`, and `ucp.services` declares the shopping service instead
|
|
185
|
+
of being left `{}`. The checked-in
|
|
186
|
+
`agent-profile/agent-profile.json` is regenerated, existing signing keys
|
|
187
|
+
kept. See `docs/ucp-tool-gating-investigation.md`.
|
|
188
|
+
- New `Portage::Cli::BuyerContext` builds the UCP `context` object from
|
|
189
|
+
`PORTAGE_SHIP_COUNTRY`/`PORTAGE_SHIP_REGION`/`PORTAGE_SHIP_POSTAL_CODE`,
|
|
190
|
+
`PORTAGE_CURRENCY` and `PORTAGE_LANGUAGE`. `buy` and `find` send it on every
|
|
191
|
+
catalog and checkout call — without it a real store builds an empty cart and
|
|
192
|
+
calls it sold out. Partial by design, unlike `ShippingProfile`, which stays
|
|
193
|
+
all-or-nothing because a half-filled address can't be submitted.
|
|
194
|
+
|
|
7
195
|
## [0.6.0] - 2026-09-17
|
|
8
196
|
|
|
9
197
|
- Fixed `buy`/`find` crashing with a raw `Faraday::UnprocessableContentError`
|
data/README.md
CHANGED
|
@@ -68,7 +68,8 @@ gem install portage-cli
|
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
portage buy <url> --query "..." [--qty N] [--payment-token TOKEN] [--product-id ID]
|
|
71
|
-
[--yes] [--dry-run] [--
|
|
71
|
+
[--yes] [--dry-run] [--decision-backend jev|laya]
|
|
72
|
+
[--min-confidence N] [--json]
|
|
72
73
|
portage buy --query "..." [--store URL] [--max-price N] [--limit N] ...
|
|
73
74
|
portage find --query "..." [--max-price N] [--limit N] [--json]
|
|
74
75
|
portage compare <url> --product-id ID [--id VALUE ...] [--results N]
|
|
@@ -105,11 +106,20 @@ portage policy set [--per-transaction-cap N --currency CUR]
|
|
|
105
106
|
- `--limit` — how many candidate stores to probe, capped at 12.
|
|
106
107
|
- `--yes` — skip the confirmation prompt before completing checkout.
|
|
107
108
|
- `--dry-run` — resolve and price the order without completing checkout.
|
|
109
|
+
- `--decision-backend` — opt into the confidence gate (see "Decisions" below):
|
|
110
|
+
`jev` or `laya`. Defaults to `PORTAGE_DECISION_BACKEND`; unset means off.
|
|
111
|
+
- `--min-confidence` — the gate's threshold, `0.0`–`1.0`. Defaults to
|
|
112
|
+
`PORTAGE_MIN_CONFIDENCE`, then `0.8`. The flag is always checked; the env
|
|
113
|
+
var is read (and checked) only while a backend is selected, so a stale
|
|
114
|
+
value can't block a buy that doesn't use the gate.
|
|
108
115
|
- `--json` — machine-readable report instead of the human-readable summary.
|
|
109
116
|
|
|
110
117
|
Exits `0` when a checkout completed (or a dry-run/browse/search resolved
|
|
111
118
|
successfully), `1` otherwise — including the "no native manifest, no adapter
|
|
112
|
-
credentials" dead-end case, so it's scriptable in CI.
|
|
119
|
+
credentials" dead-end case, so it's scriptable in CI. A flag that can't be
|
|
120
|
+
used (an unreadable value, an out-of-range threshold) stops the buy before
|
|
121
|
+
anything runs: on stderr normally, or as a JSON report with `outcome:
|
|
122
|
+
"invalid_option"` under `--json`.
|
|
113
123
|
|
|
114
124
|
### Compare
|
|
115
125
|
|
|
@@ -162,10 +172,15 @@ abandoned carts on someone else's site; left out of scope for now.
|
|
|
162
172
|
|
|
163
173
|
### History
|
|
164
174
|
|
|
165
|
-
Every `
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
175
|
+
Every `portage buy` that creates a checkout is logged locally to
|
|
176
|
+
`~/.portage/history.json` as a purchase, whatever came of it. Each entry
|
|
177
|
+
carries the report's `outcome` (`purchased`, `dry_run`, `policy_blocked`,
|
|
178
|
+
...), what the checkout held (`items`), its `total`, and, when it wasn't
|
|
179
|
+
completed, the `checkout_url` to finish it. "What did I already buy" is
|
|
180
|
+
the entries whose `outcome` is `purchased`. A `buy` that never reached a
|
|
181
|
+
checkout (no match, browse-only, a dead end) is logged as a search at that
|
|
182
|
+
store instead, as is every `find`, `compare`, and the search behind a
|
|
183
|
+
`buy` with no URL. The most recent 200 entries of each are kept.
|
|
169
184
|
|
|
170
185
|
```bash
|
|
171
186
|
portage history # both lists, most recent last
|
|
@@ -255,6 +270,102 @@ opt-in guardrail, not a default-deny one. Per-token scopes (merchant/amount
|
|
|
255
270
|
limits bound to one enrolled card) are set via `portage payment enroll
|
|
256
271
|
--scope-*` above, not here.
|
|
257
272
|
|
|
273
|
+
### Decisions
|
|
274
|
+
|
|
275
|
+
`portage buy` and `portage find` make their judgment calls
|
|
276
|
+
(`docs/plans/system-one-decision-layer.md`) through rules that live in
|
|
277
|
+
`portage-ucp` core: `Support::OfferRanking`, `Support::Escalation` and
|
|
278
|
+
`PolicyGuard`. `portage-ucp-decision` wraps the same modules as typed
|
|
279
|
+
verdicts, so ranking, escalation and the policy check answer the same way
|
|
280
|
+
whether or not it's installed. It's an optional plugin, not a dependency:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
gem install portage-ucp-decision # only needed for the confidence gate
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The confidence gate is the one feature that needs the gem, because the
|
|
287
|
+
model backends live there.
|
|
288
|
+
|
|
289
|
+
Every `portage buy` report carries an `outcome`, so a script or agent loop
|
|
290
|
+
can branch on data rather than on the message text. The text output leads
|
|
291
|
+
with the same value, as `[outcome]`.
|
|
292
|
+
|
|
293
|
+
| `outcome` | Meaning | `checkout_url`? |
|
|
294
|
+
| --- | --- | --- |
|
|
295
|
+
| `purchased` | Completed. | no |
|
|
296
|
+
| `needs_confirmation` | Checkout ready; rerun with `--yes`. | no |
|
|
297
|
+
| `dry_run` | Checkout created, `--dry-run` stopped it. | no |
|
|
298
|
+
| `requires_escalation` | The store wants the shopper to finish. | yes |
|
|
299
|
+
| `checkout_mismatch` | Checkout differs from the request (`PORTAGE_ABORT_ON_CHECKOUT_MISMATCH`). | yes |
|
|
300
|
+
| `no_payment_token` | No `--payment-token` and no default payment method. | yes |
|
|
301
|
+
| `policy_blocked` | Your spend policy denied it; `decisions.policy.reason` says why. | yes |
|
|
302
|
+
| `low_confidence` | The confidence gate held it. | yes |
|
|
303
|
+
| `permission_denied` | The store doesn't let this agent complete checkout. | yes |
|
|
304
|
+
| `store_refused` | The store refused a cart/checkout call (e.g. sold out). | when the store gave one |
|
|
305
|
+
| `no_match` | Nothing in the store's results matched. | no |
|
|
306
|
+
| `browse_only` | The store has a catalog but no UCP checkout. | when an adapter offers a link |
|
|
307
|
+
| `agent_profile_missing`, `request_rejected`, `unsupported_wire_shape` | Native UCP setup problems; the message names the fix. | no |
|
|
308
|
+
| `adapter_error`, `adapter_misconfigured` | Your own-store adapter failed; the message quotes it. | no |
|
|
309
|
+
| `dead_end` | No UCP and no adapter for this store. | no |
|
|
310
|
+
| `invalid_option` | A flag was refused before the buy started (`--json` only); `message` says which. | no |
|
|
311
|
+
|
|
312
|
+
Checkout reports also carry `items` (what the checkout holds, as opposed to
|
|
313
|
+
`products`, the search results) and the verdicts under `decisions:`:
|
|
314
|
+
|
|
315
|
+
```json
|
|
316
|
+
"decisions": {
|
|
317
|
+
"escalation": { "escalate": false, "reason": null },
|
|
318
|
+
"policy": { "allowed": true, "reason": null },
|
|
319
|
+
"confidence": { "proceed": false, "reason": "below_threshold", "confidence": 0.41,
|
|
320
|
+
"threshold": 0.8, "backend": "jev", "error": null }
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Every verdict has a `reason`: `null` when the gate let the purchase through,
|
|
325
|
+
otherwise a string naming why it stopped it.
|
|
326
|
+
|
|
327
|
+
- **escalation** — `Support::Escalation`. A `requires_escalation` checkout
|
|
328
|
+
always escalates (`reason: "requires_escalation"`). A checkout that doesn't
|
|
329
|
+
match the request escalates (`reason: "mismatch"`) only under
|
|
330
|
+
`PORTAGE_ABORT_ON_CHECKOUT_MISMATCH`; otherwise it's reported as `warnings`.
|
|
331
|
+
- **policy** — `PolicyGuard`, run on your policy file (see "Policy" above)
|
|
332
|
+
before any `--yes` completion. `reason` is the guard's own
|
|
333
|
+
(`per_transaction_cap_exceeded`, `rolling_spend_cap_exceeded`,
|
|
334
|
+
`velocity_exceeded`, `merchant_not_allowlisted`, `token_scope_merchant`,
|
|
335
|
+
`token_scope_amount`, `currency_mismatch`, or `total_unknown`, below).
|
|
336
|
+
It applies to remote native-UCP stores too. Before this check, only the
|
|
337
|
+
own-store adapter flow's in-process `Dispatcher` enforced the policy.
|
|
338
|
+
Rolling caps and velocity limits count the completed purchases at that
|
|
339
|
+
merchant in `~/.portage/transactions.json`. `Dispatcher` records
|
|
340
|
+
own-store purchases there, and `portage buy` records remote ones: reserved
|
|
341
|
+
before the store is asked to complete, then settled as `complete` only
|
|
342
|
+
when the store answers that it's purchased.
|
|
343
|
+
- **confidence** — `ConfidenceGate`, off unless `--decision-backend` or
|
|
344
|
+
`PORTAGE_DECISION_BACKEND` names a backend. Right before a `--yes`
|
|
345
|
+
completion it asks the backend whether the checkout matches the request
|
|
346
|
+
and is safe to complete unattended. It sends the query, merchant,
|
|
347
|
+
quantity, line items, totals and warnings, never the payment token. The
|
|
348
|
+
gate fails closed, so three things hold the purchase: a score below the
|
|
349
|
+
threshold (`reason: "below_threshold"`), a backend that can't answer
|
|
350
|
+
(`"backend_error"`), and naming a backend without `portage-ucp-decision`
|
|
351
|
+
installed (`"not_installed"`). For the last two, `error` says what went
|
|
352
|
+
wrong. `jev` needs `JEV_API_KEY`; `laya` needs `LAYA_BRIDGE_SCRIPT` (see
|
|
353
|
+
`portage-ucp-decision`'s README). `portage doctor` flags whichever of
|
|
354
|
+
these is missing for the selected backend.
|
|
355
|
+
- **policy** also denies a checkout with no `total` line as
|
|
356
|
+
`total_unknown` whenever a spend cap is set, rather than skipping the cap.
|
|
357
|
+
|
|
358
|
+
A blocked or held purchase hands the checkout off the same way an escalation
|
|
359
|
+
does (`checkout_url`, plus `--auto-open`/`--notify-webhook` if set). The
|
|
360
|
+
shopper can finish it themselves. The webhook body is JSON: `event`
|
|
361
|
+
(`checkout_handoff`), `reason` (the report's `outcome`), `message` (the
|
|
362
|
+
report's own sentence), `store`, `query`, `checkout_url`, `checkout_id`,
|
|
363
|
+
`source`, `totals` and `warnings`. Any 2xx counts as delivered; the POST
|
|
364
|
+
gives up after 5 seconds and reports `notify_error` instead.
|
|
365
|
+
|
|
366
|
+
`portage find`'s offer order comes from `Support::OfferRanking`: buyable
|
|
367
|
+
first, then cheapest, then unpriced.
|
|
368
|
+
|
|
258
369
|
### Shipping address (own-store checkouts only)
|
|
259
370
|
|
|
260
371
|
When buying against your own store (`portage buy`'s step 2 adapter-credentials
|