portage-cli 0.10.0 → 0.12.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 +129 -0
- data/README.md +107 -21
- data/known-stores/categories.yml +6841 -18
- data/known-stores/category-stoplist.yml +40 -0
- data/known-stores/category-synonyms.yml +15 -0
- data/lib/portage/cli/browser_import/categorize.rb +5 -2
- data/lib/portage/cli/buy.rb +217 -39
- data/lib/portage/cli/check.rb +11 -1
- data/lib/portage/cli/classifier/ranking.rb +135 -0
- data/lib/portage/cli/classifier/table.rb +63 -0
- data/lib/portage/cli/classifier.rb +23 -44
- data/lib/portage/cli/confidence_check.rb +27 -7
- data/lib/portage/cli/confidence_state.rb +109 -0
- data/lib/portage/cli/doctor.rb +16 -6
- data/lib/portage/cli/find.rb +69 -4
- data/lib/portage/cli/index/builder.rb +49 -12
- data/lib/portage/cli/index/database.rb +150 -0
- data/lib/portage/cli/index/entry_product.rb +37 -0
- data/lib/portage/cli/index/legacy_import.rb +54 -0
- data/lib/portage/cli/index/product_store.rb +73 -35
- data/lib/portage/cli/index/schema.rb +70 -0
- data/lib/portage/cli/index/search.rb +73 -0
- data/lib/portage/cli/index/sources/storefront_products/mapper.rb +127 -0
- data/lib/portage/cli/index/sources/storefront_products/pages.rb +114 -0
- data/lib/portage/cli/index/sources/storefront_products/robots.rb +70 -0
- data/lib/portage/cli/index/sources/storefront_products.rb +147 -0
- data/lib/portage/cli/index/sources.rb +5 -2
- data/lib/portage/cli/index/store.rb +23 -47
- data/lib/portage/cli/index.rb +1 -0
- data/lib/portage/cli/offer_sources.rb +17 -2
- data/lib/portage/cli/version.rb +1 -1
- data/lib/portage/cli.rb +111 -12
- metadata +30 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4e0a17085649f21644b4e373afc590ab69b92cfdbf23c4b83fbf7ed33b1cc1c8
|
|
4
|
+
data.tar.gz: 75e4435e369e70f529af29ce9891208615d2f0d458df616dbf11a032f8276ed3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d79a48ad6cf39bad956cafbb2e9f3317485af02a14fa3305306a4185839ce3add20326025e9eb4cfa0e0e1709ecbade149b5b2e9109627fb9b3b6112f11e91ed
|
|
7
|
+
data.tar.gz: 5bbf4e713f496fef30925cb4f59de1ad929e1a51b960c804f6bb60bbce39c8dd0856d3efcd5641f175a25bebfd7868358d63a827d0e6123a3d5b9763a7d27ead
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,135 @@ pre-1.0, so APIs may still shift between minor versions.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.12.0] - 2026-10-01
|
|
10
|
+
|
|
11
|
+
- **Release note: the confidence check wants `portage-ucp-decision` 0.1.2.** `portage-ucp-decision` stays an optional install, not a dependency, but 0.1.2 is the version that rejects a malformed backend answer (see its changelog); the README says so.
|
|
12
|
+
|
|
13
|
+
- **`find --store URL --query Q [--max-price N]` searches one store, live.** The index-search hint, the `buy` skill and the docs already told agents to re-check an index hit this way, but `find` had no `--store`. It now skips the search backends and retailer offer sources and probes and searches only that store's catalogue (read-only: never a cart or checkout), with offers, `offer_ref`, `search_id` and the history entry shaped like any find, so `pick` and `buy --offer` work on them. A hand-off-only host is never fetched and is reported as such; a store without UCP is reported as having no catalogue; a non-http(s) URL is a usage error. The index-search hint now reads `find --store URL --query ...`.
|
|
14
|
+
|
|
15
|
+
- **Security: a priced line nobody asked for is a checkout mismatch.** `buy` requests exactly one
|
|
16
|
+
line, but only checked that line, so a store that added an upsell, a "shipping protection"
|
|
17
|
+
add-on or a second copy of the item (or, on a WebMCP cart, whatever was already in the store's
|
|
18
|
+
cart) still went through. Any other line now stops a real run with `checkout_mismatch` ("Store
|
|
19
|
+
added ... to checkout, which wasn't requested."), and a dry run flags it with
|
|
20
|
+
`checkout_mismatch: true`. An extra line that costs nothing (its own total, or its unit price
|
|
21
|
+
times quantity, is 0) is allowed, so a free gift or $0 sample doesn't block a purchase; one
|
|
22
|
+
whose cost can't be read counts as priced.
|
|
23
|
+
|
|
24
|
+
- **Security: the confidence check sends an allowlisted summary, and compares against the
|
|
25
|
+
approved quote.** The state sent to the decision backend (`PORTAGE_DECISION_BACKEND`, e.g.
|
|
26
|
+
`jev`, TypeSafe's hosted API) used to be a slice of the raw checkout hash, so whatever a store
|
|
27
|
+
nested under `line_items` or `totals` went along with it. It is now built field by field by the
|
|
28
|
+
new `Portage::Cli::ConfidenceState`: the request (query, store host, quantity, picked item id
|
|
29
|
+
and title), on a `buy --quote` run the approved quote (store, product id, title, quantity,
|
|
30
|
+
total, currency), and the checkout's status, currency, per-line item id, title, unit price,
|
|
31
|
+
quantity, totals and whether it's the requested line, the totals (shipping, tax, fees included),
|
|
32
|
+
applied discounts' titles and amounts and the selected shipping option's title and price, plus
|
|
33
|
+
`warnings`. Never the payment token, the address, the buyer's name, phone or email, ids, links,
|
|
34
|
+
discount codes or environment values; store strings are cut to 200 characters. The question
|
|
35
|
+
now asks the model to say yes only when the checkout matches the request and the approved
|
|
36
|
+
quote. `Buy.new` takes `quote_store:` and `quote_title:`, which `buy --quote` passes from the
|
|
37
|
+
saved quote. The check stays additive: it only sees a checkout the mismatch check, the quote
|
|
38
|
+
cap and the spend policy let through, and can hold it but never let through one they stop.
|
|
39
|
+
|
|
40
|
+
- **Security: the non-preset WebMCP hand-off runs the confidence check too.** A page whose WebMCP
|
|
41
|
+
tools build a checkout (no preset, so `buy` ends in `express_stop` through `finish_checkout`)
|
|
42
|
+
applied the quote cap and the mismatch stop but not the opt-in confidence check. With a decision
|
|
43
|
+
backend enabled it now runs before the hand-off, after those two. A hold reports `low_confidence`
|
|
44
|
+
with the store's `/cart` page as `checkout_url`, as the preset flow's does, and nothing is handed
|
|
45
|
+
to the checkout (a `profile` target is not navigated there). A backend error holds the same way.
|
|
46
|
+
No backend enabled: unchanged.
|
|
47
|
+
|
|
48
|
+
- **Security: the WebMCP hand-off flow runs the quote cap and the confidence check.** Against a
|
|
49
|
+
page whose preset opens checkout through its own tool (Shopify's `proceed_to_checkout`), `buy`
|
|
50
|
+
now checks, before that tool runs and before autofill: a `--quote` run's cap (`quote_changed`,
|
|
51
|
+
never handed off; this flow never checked the quote before), the mismatch check, then, when a
|
|
52
|
+
decision backend is enabled, the confidence check. A hold reports `low_confidence` with the
|
|
53
|
+
store's `/cart` page as `checkout_url`; checkout isn't opened and nothing is autofilled. A
|
|
54
|
+
backend error holds the same way.
|
|
55
|
+
|
|
56
|
+
- **Security: a quote with no total refuses instead of buying uncapped.** `buy --quote` capped the
|
|
57
|
+
checkout at the quote's total only when the quote had one. A quote saved from a dry run with no
|
|
58
|
+
priced total (a WebMCP preset dry run never has one) set no cap at all, so its `--yes` run
|
|
59
|
+
bought at whatever the store asked. It now ends in `quote_changed`, saying the quote has no
|
|
60
|
+
total, and nothing is bought or handed off.
|
|
61
|
+
|
|
62
|
+
- **Security: a checkout mismatch always stops the purchase.** `buy` used to stop on a checkout
|
|
63
|
+
that didn't match the request (the item dropped, another quantity, another unit price) only under
|
|
64
|
+
`PORTAGE_ABORT_ON_CHECKOUT_MISMATCH`. Without it the mismatch was a `warnings` entry and the
|
|
65
|
+
purchase went ahead, so a real `--yes` run could pay for the wrong checkout and report
|
|
66
|
+
`purchased` (reported by ClawHub's security audit of the `portage-buy` skill). Now every
|
|
67
|
+
mismatch on a run that isn't `--dry-run` escalates (`decisions.escalation.reason: "mismatch"`)
|
|
68
|
+
and ends in `checkout_mismatch` before the payment token, policy and completion are reached. The
|
|
69
|
+
quote it ran under is spent, so the person dry-runs again for a new one. There is no opt-out:
|
|
70
|
+
`PORTAGE_ABORT_ON_CHECKOUT_MISMATCH` is deprecated and ignored, whatever its value. A
|
|
71
|
+
`--dry-run` keeps its `dry_run` outcome and `warnings`, and now adds `checkout_mismatch: true`
|
|
72
|
+
and says in `message` that a real run would stop. The check also compares currency: a checkout
|
|
73
|
+
in another currency than the catalog's price is a mismatch. The quoted total and currency are
|
|
74
|
+
still `--quote`'s `quote_changed` check, unchanged.
|
|
75
|
+
|
|
76
|
+
- **Security: the WebMCP hand-off flow stops on a mismatched cart too.** Against a WebMCP page
|
|
77
|
+
whose preset opens checkout through its own tool (Shopify's `proceed_to_checkout`), `buy` read
|
|
78
|
+
the cart back and checked it, but only put a mismatch in `warnings`: it still called the hand-off
|
|
79
|
+
tool, sending the browser tab to the store's checkout, and ran autofill there when it was
|
|
80
|
+
approved. Now any mismatch stops the run before that tool, with outcome `checkout_mismatch` and
|
|
81
|
+
`decisions.escalation.reason: "mismatch"`. Nothing is autofilled, and `checkout_url` is the
|
|
82
|
+
store's `/cart` page, so the shopper can look at the cart that was built. A matching cart hands
|
|
83
|
+
off as `express_stop`, as before. The dry run of this flow builds no cart, so it can't check one
|
|
84
|
+
and never carries `checkout_mismatch: true`.
|
|
85
|
+
|
|
86
|
+
## [0.11.0] - 2026-10-01
|
|
87
|
+
|
|
88
|
+
- **Category classification uses the whole taxonomy.** `known-stores/categories.yml` is now generated
|
|
89
|
+
by `script/categories` (stdlib only) from Google's product taxonomy, edition 2021-09-21, instead of
|
|
90
|
+
coming from a script that was never committed. It keeps the same ids and the same top-two-level keys
|
|
91
|
+
(so `~/.portage/categories.yml` overrides still work), but a level-2 node's `keywords` now include
|
|
92
|
+
its descendants' names ("Chandeliers" counts for Lighting), and its parent's words sit in a separate
|
|
93
|
+
`parent_keywords` list. The file grows from 20KB to 101KB. A shipped stoplist
|
|
94
|
+
(`known-stores/category-stoplist.yml`, every word with its reason) removes merchandising and url words
|
|
95
|
+
("new", "collection", "sale", "gift", "accessories", "products", ...) from both the data and the input,
|
|
96
|
+
and a small synonyms file (`category-synonyms.yml`, each word tied to a golden case) adds the few
|
|
97
|
+
words the taxonomy lacks ("pendant", "sconce"). `Classifier.categories_for` keeps its signature (plus
|
|
98
|
+
an optional `stoplist_path:`) and returns at most three ids, best first. Scoring: a keyword counts
|
|
99
|
+
2 and a `parent_keyword` 1, times how often the input repeats the word (1 + ln count) and, for the
|
|
100
|
+
cut, how rare it is. A word counts once per node, and an id scoring under half the strongest is
|
|
101
|
+
dropped. Ties go to the node whose own name the input covers most, then to one whose name has no
|
|
102
|
+
stoplisted word, then to the smaller node. On a 100-case golden set (Light Yard, JB Hi-Fi, shopper
|
|
103
|
+
queries, browser-import titles and urls) top-1 accuracy goes from 12% to 70%. "Pendant Light" is
|
|
104
|
+
Lighting, "New Collection" is no longer Toll Collection Devices, and 160 of Light Yard's 164 products
|
|
105
|
+
classify as Lighting (1 before). Products already in the index keep the category they were crawled
|
|
106
|
+
with: crawl the store again (`portage index build --sources storefront_products`) to re-classify them.
|
|
107
|
+
- **The local index is now a SQLite file.** `~/.portage/index/index.sqlite3` (mode 0600, WAL)
|
|
108
|
+
replaces `stores.json` and `products.json`, through the new `sqlite3` gem dependency (`~> 2.9`,
|
|
109
|
+
precompiled for macOS and Linux). An existing `stores.json`/`products.json` is imported once on
|
|
110
|
+
first use and renamed to `*.json.migrated`; it is never deleted. `Index::Store` and
|
|
111
|
+
`Index::ProductStore` keep their APIs, and `ProductStore#upsert_many` writes a batch in one
|
|
112
|
+
transaction. Index write failures now raise instead of being dropped. `portage doctor` reports the
|
|
113
|
+
database path, row counts and whether FTS5 is available. The known-stores cache and `index export`
|
|
114
|
+
output stay JSON.
|
|
115
|
+
- **Storefront catalogue crawl and `portage index search`.** A new opt-in index source,
|
|
116
|
+
`storefront_products`, reads a Shopify store's public `/products.json` into the local index. Run it
|
|
117
|
+
with `portage index add URL --crawl` for one store, or `portage index build --sources
|
|
118
|
+
storefront_products` for stores already indexed (25 a run, least recently crawled first). Each
|
|
119
|
+
product is mapped through the UCP `Product` shape and stored with its handle, URL, first image,
|
|
120
|
+
options and variant ids. Price and availability are dropped before anything is written. A crawl
|
|
121
|
+
reads at most 20 pages of 250 a store, 1s apart. It waits out one 429's `Retry-After` (capped at 60s)
|
|
122
|
+
and stops on a second, and it obeys `robots.txt` for every page URL. It never contacts a hand-off-only
|
|
123
|
+
host, and it skips a 404, a redirect or a non-JSON answer (a bot wall). What happened is kept on the
|
|
124
|
+
store entry as `crawl`. `portage index search QUERY [--category ID] [--store HOST] [--limit N]
|
|
125
|
+
[--json]` searches the index locally with SQLite FTS5, ranked by bm25, and sends no request. Without
|
|
126
|
+
FTS5 it falls back to an unranked text match and reports `engine: "like"`. `index show --products` now
|
|
127
|
+
pages (`--page N`, `--per-page N`, default 50). `portage check` suggests the `index add ... --crawl`
|
|
128
|
+
command for a Shopify or native-UCP store (`index_hint`) but never runs it. `find` and `buy` never
|
|
129
|
+
crawl.
|
|
130
|
+
- **Product cards for agents.** `find` offers (and `shopify_catalog` offers) gain a `product` field:
|
|
131
|
+
the store's UCP `Product` wire hash as served, with `media` cut to the first image, so an agent or
|
|
132
|
+
UI can draw a card without another request. The flat fields (`title`, `amount`, `currency`, `url`,
|
|
133
|
+
`product_id`, `store`) are unchanged, the retailer API sources leave `product` out, and `history`
|
|
134
|
+
does not save it. `portage index search` results are marked `live: false` and each hit gains
|
|
135
|
+
`product`, built from the fields the index keeps (title, handle, URL, first image, options, variant
|
|
136
|
+
ids, category), never a price. Text output says the hits are not live.
|
|
137
|
+
|
|
9
138
|
## [0.10.0] - 2026-09-29
|
|
10
139
|
|
|
11
140
|
- **`portage check <url> [--json]`.** Reports whether Portage can buy from a store and
|
data/README.md
CHANGED
|
@@ -29,6 +29,20 @@ portage find --query "burton snowboard" --max-price 400
|
|
|
29
29
|
|
|
30
30
|
`portage buy` with no URL runs that search and then buys the offer you pick.
|
|
31
31
|
|
|
32
|
+
Already know the store? `portage find --store URL --query "..."` skips the search
|
|
33
|
+
backends and offer sources and searches only that store's catalogue, live and
|
|
34
|
+
read-only (catalogue search only, never a cart or checkout). Use it to re-check
|
|
35
|
+
an index hit or an earlier offer before quoting its price or stock. The report
|
|
36
|
+
has the same shape as a normal find (`offer_ref`, `search_id`, a history entry),
|
|
37
|
+
so `pick` and `buy --offer` work on its offers. A hand-off-only host is never
|
|
38
|
+
fetched and is reported as such; a store that doesn't speak UCP is reported as
|
|
39
|
+
having no catalogue, with exit code 1 (as for any find with no offers). `--store`
|
|
40
|
+
must be an http(s) URL (a bare host is read as https).
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
portage find --store https://shop.example --query "cold brew" --json
|
|
44
|
+
```
|
|
45
|
+
|
|
32
46
|
Already have the item and want to know where else it's sold? `portage compare`
|
|
33
47
|
resolves a product you name by URL + product id, then runs the same
|
|
34
48
|
find pipeline against its title and ranks the results by how confident the
|
|
@@ -131,6 +145,7 @@ portage buy --offer REF [--qty N] [--yes] [--dry-run] ...
|
|
|
131
145
|
portage buy --quote QUOTE_ID --yes [--json] ...
|
|
132
146
|
portage buy --query "..." [--store URL] [--max-price N] [--limit N] ...
|
|
133
147
|
portage find --query "..." [--max-price N] [--limit N] [--json]
|
|
148
|
+
portage find --store URL --query "..." [--max-price N] [--json]
|
|
134
149
|
portage compare <url> --product-id ID [--id VALUE ...] [--results N]
|
|
135
150
|
[--max-price N] [--json]
|
|
136
151
|
portage check <url> [--json]
|
|
@@ -155,8 +170,9 @@ portage policy set [--per-transaction-cap N --currency CUR]
|
|
|
155
170
|
portage orders reconcile [--checkout ID] [--json]
|
|
156
171
|
portage index build [--sources a,b] [--queries FILE] [--dry-run] [--export DIR] [--json]
|
|
157
172
|
portage index refresh [--sources a,b] [--queries FILE] [--dry-run] [--export DIR] [--json]
|
|
158
|
-
portage index show [--stores|--products] [--json]
|
|
159
|
-
portage index
|
|
173
|
+
portage index show [--stores|--products [--page N] [--per-page N]] [--json]
|
|
174
|
+
portage index search QUERY [--category ID] [--store HOST] [--limit N] [--json]
|
|
175
|
+
portage index add <url> [--crawl] [--json]
|
|
160
176
|
portage index remove <host> [--json]
|
|
161
177
|
portage index sources [--json]
|
|
162
178
|
portage browser import [--browser chrome|edge|brave|arc|firefox|safari] [--profile-root DIR]
|
|
@@ -478,13 +494,16 @@ portage index build --sources shopify_catalog,stores_file
|
|
|
478
494
|
portage index build --queries queries.txt # one query per line, instead of the built-in taxonomy sweep
|
|
479
495
|
portage index refresh # re-verify entries older than 7 days, add new ones
|
|
480
496
|
portage index show --stores --json
|
|
481
|
-
portage index show --products --json
|
|
497
|
+
portage index show --products --page 2 --json # 50 a page; --per-page N
|
|
498
|
+
portage index search "wall light" --store some-shop.example --json
|
|
482
499
|
portage index add https://some-shop.example
|
|
500
|
+
portage index add https://some-shop.example --crawl # also read its /products.json catalogue
|
|
501
|
+
portage index build --sources storefront_products # crawl the catalogues of stores already indexed
|
|
483
502
|
portage index remove some-shop.example
|
|
484
503
|
portage index sources # name, what each fetches, source file path
|
|
485
504
|
```
|
|
486
505
|
|
|
487
|
-
Stored
|
|
506
|
+
Stored in `~/.portage/index/index.sqlite3` (mode 0600), **never in git** and
|
|
488
507
|
**never containing a price or stock field** — those are always fetched live.
|
|
489
508
|
Each new origin gets exactly one `/.well-known/ucp` probe (capped at 500 new
|
|
490
509
|
probes per run), throttled, with progress output. Sources:
|
|
@@ -496,6 +515,18 @@ probes per run), throttled, with progress output. Sources:
|
|
|
496
515
|
| `browser` | Whatever `portage browser import` (below) already saved — this source itself never reads a browser | on, but yields nothing unless you've run `browser import` |
|
|
497
516
|
| `wikidata` | Retailers'/brands' official sites via a public SPARQL query | opt-in (`--sources wikidata`) |
|
|
498
517
|
| `webmcp_sweep` | Which WebMCP preset an origin matches, when a bridge is attached | opt-in, needs a bridge |
|
|
518
|
+
| `storefront_products` | Each indexed Shopify store's own `/products.json`: title, brand, handle, URL, first image, options and variant ids, mapped through the UCP `Product` shape with price and availability dropped | opt-in (`--sources storefront_products`, or `index add URL --crawl`) |
|
|
519
|
+
|
|
520
|
+
**Catalogue crawls are polite and opt-in.** `storefront_products` never runs
|
|
521
|
+
from `find` or `buy` (`portage check` only prints the `index add ... --crawl`
|
|
522
|
+
command). It crawls at most 20 pages of 250 products a store and 25 stores a
|
|
523
|
+
run, least recently crawled first, 1s apart. It waits out one 429's
|
|
524
|
+
`Retry-After` (capped at 60s) and stops that store on a second. It obeys
|
|
525
|
+
`robots.txt` for every page URL, never contacts a hand-off-only host, and
|
|
526
|
+
skips a store that answers 404, a redirect or anything but products JSON (a
|
|
527
|
+
bot wall). What happened is kept on the store entry as `crawl`.
|
|
528
|
+
`portage index search` then searches those products locally (SQLite FTS5,
|
|
529
|
+
or a plain text match if FTS5 is missing), with no request.
|
|
499
530
|
|
|
500
531
|
**The index is untrusted data, on the same footing as any other `find`
|
|
501
532
|
candidate.** It never feeds `Policy#merchant_allowlist` and never counts as
|
|
@@ -795,6 +826,30 @@ checkout tool (Shopify's `proceed_to_checkout`) stops after the read-only
|
|
|
795
826
|
product search: nothing is added to the store's cart, the tab isn't sent to
|
|
796
827
|
checkout and nothing is autofilled. The `dry_run` report carries a `would:`
|
|
797
828
|
key with the line item, the hand-off tool and whether autofill would run.
|
|
829
|
+
Because it builds no cart, it can't check one, so it never carries
|
|
830
|
+
`checkout_mismatch: true`.
|
|
831
|
+
|
|
832
|
+
A real run against that kind of page reads the cart back after adding to it
|
|
833
|
+
and checks it the same way `buy` checks any checkout (item, quantity, unit
|
|
834
|
+
price, currency, extra lines). Any mismatch stops the run there, with outcome
|
|
835
|
+
`checkout_mismatch` and `decisions.escalation.reason: "mismatch"`: the
|
|
836
|
+
hand-off tool isn't called, so the tab never goes to checkout, and nothing is
|
|
837
|
+
autofilled. The cart is already on the store, so `checkout_url` is the
|
|
838
|
+
store's `/cart` page, for the shopper to look at. Items already sitting in
|
|
839
|
+
the store's cart count as extra lines, so they stop the run too.
|
|
840
|
+
|
|
841
|
+
The same run then applies the other gates a checkout gets before anything
|
|
842
|
+
leaves the cart: a `--quote` run's cap (`quote_changed`), and, when a
|
|
843
|
+
decision backend is enabled, the confidence check (see "Decisions"). A hold
|
|
844
|
+
from the confidence check reports `low_confidence` with the `/cart` page as
|
|
845
|
+
`checkout_url`; again the tab isn't sent to checkout and nothing is
|
|
846
|
+
autofilled.
|
|
847
|
+
|
|
848
|
+
A page whose WebMCP tools build a checkout (no preset) gets the same confidence
|
|
849
|
+
check before its `express_stop` hand-off, after the quote cap and the mismatch
|
|
850
|
+
stop. A hold reports `low_confidence` with the `/cart` page as `checkout_url`
|
|
851
|
+
and no navigation to the checkout, so a `profile` hand-off target is never sent
|
|
852
|
+
there. With no decision backend enabled, nothing changes.
|
|
798
853
|
|
|
799
854
|
Once the flow hands off to the store's own checkout page, the shopper can
|
|
800
855
|
opt into having it pre-filled: `--autofill`, or
|
|
@@ -826,7 +881,9 @@ gem install portage-ucp-decision # only needed for the confidence gate
|
|
|
826
881
|
```
|
|
827
882
|
|
|
828
883
|
The confidence gate is the one feature that needs the gem, because the
|
|
829
|
-
model backends live there.
|
|
884
|
+
model backends live there. Use `portage-ucp-decision` 0.1.2 or newer: it
|
|
885
|
+
rejects a backend answer that isn't a probability between 0 and 1, where
|
|
886
|
+
0.1.1 let an answer above 1 clear any threshold.
|
|
830
887
|
|
|
831
888
|
Every `portage buy` report carries an `outcome`, so a script or agent loop
|
|
832
889
|
can branch on data rather than on the message text. The text output leads
|
|
@@ -836,12 +893,12 @@ with the same value, as `[outcome]`.
|
|
|
836
893
|
| --- | --- | --- |
|
|
837
894
|
| `purchased` | Completed. | no |
|
|
838
895
|
| `needs_confirmation` | Checkout ready; rerun with `--yes`. | no |
|
|
839
|
-
| `dry_run` | Checkout created, `--dry-run` stopped it. | no |
|
|
896
|
+
| `dry_run` | Checkout created, `--dry-run` stopped it. `checkout_mismatch: true` when a real run would stop on a mismatch. | no |
|
|
840
897
|
| `requires_escalation` | The store wants the shopper to finish. | yes |
|
|
841
|
-
| `checkout_mismatch` | Checkout differs from the request (
|
|
898
|
+
| `checkout_mismatch` | Checkout differs from the request (item, quantity, unit price, currency, or a priced line nobody asked for); stopped before payment. | yes |
|
|
842
899
|
| `no_payment_token` | No `--payment-token` and no default payment method. | yes |
|
|
843
900
|
| `policy_blocked` | Your spend policy denied it; `decisions.policy.reason` says why. | yes |
|
|
844
|
-
| `low_confidence` | The confidence gate held it. | yes |
|
|
901
|
+
| `low_confidence` | The confidence gate held it (before a `--yes` completion, or before a WebMCP hand-off to checkout, preset or not). | yes |
|
|
845
902
|
| `permission_denied` | The store doesn't let this agent complete checkout. | yes |
|
|
846
903
|
| `handoff_only` | Tier C: Amazon or another hand-off-only host. `legal_notice` explains why; see "Hand-off targets and hand-off-only hosts". | yes (built, never fetched) |
|
|
847
904
|
| `store_refused` | The store refused a cart/checkout call (e.g. sold out). | when the store gave one |
|
|
@@ -868,9 +925,15 @@ Every verdict has a `reason`: `null` when the gate let the purchase through,
|
|
|
868
925
|
otherwise a string naming why it stopped it.
|
|
869
926
|
|
|
870
927
|
- **escalation** — `Support::Escalation`. A `requires_escalation` checkout
|
|
871
|
-
always escalates (`reason: "requires_escalation"`).
|
|
872
|
-
match the request
|
|
873
|
-
|
|
928
|
+
always escalates (`reason: "requires_escalation"`). So does a checkout that
|
|
929
|
+
doesn't match the request (`reason: "mismatch"`), on every run but
|
|
930
|
+
`--dry-run`, where the mismatch is reported in `warnings` and flagged with
|
|
931
|
+
`checkout_mismatch: true` instead. Nothing turns this off:
|
|
932
|
+
`PORTAGE_ABORT_ON_CHECKOUT_MISMATCH` is deprecated and ignored. A mismatch
|
|
933
|
+
is the requested line missing, a different quantity, a different unit
|
|
934
|
+
price or currency than the store's own catalog, or any extra line the
|
|
935
|
+
request didn't include. An extra line that costs nothing (a free gift, a
|
|
936
|
+
$0 sample) is allowed; one whose cost can't be read counts as priced.
|
|
874
937
|
- **policy** — `PolicyGuard`, run on your policy file (see "Policy" above)
|
|
875
938
|
before any `--yes` completion. `reason` is the guard's own
|
|
876
939
|
(`per_transaction_cap_exceeded`, `rolling_spend_cap_exceeded`,
|
|
@@ -885,16 +948,39 @@ otherwise a string naming why it stopped it.
|
|
|
885
948
|
when the store answers that it's purchased.
|
|
886
949
|
- **confidence** — `ConfidenceGate`, off unless `--decision-backend` or
|
|
887
950
|
`PORTAGE_DECISION_BACKEND` names a backend. Right before a `--yes`
|
|
888
|
-
completion
|
|
889
|
-
and
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
951
|
+
completion, and before a WebMCP preset flow sends the browser to the
|
|
952
|
+
store's checkout (and autofills it), it asks the backend whether the
|
|
953
|
+
checkout matches the request, and the approved quote on a `--quote` run,
|
|
954
|
+
and is safe to complete unattended. It is additive only: it runs after
|
|
955
|
+
the mismatch check, the quote cap and the spend policy, on a checkout all
|
|
956
|
+
of them let through, so it can hold a purchase but never let through one
|
|
957
|
+
they would stop. The gate fails closed, so three things hold the
|
|
958
|
+
purchase: a score below the threshold (`reason: "below_threshold"`), a
|
|
959
|
+
backend that can't answer or answers with something that isn't a
|
|
960
|
+
probability (`"backend_error"`), and naming a backend without
|
|
961
|
+
`portage-ucp-decision` installed (`"not_installed"`). For the last two,
|
|
962
|
+
`error` says what went wrong. `jev` needs `JEV_API_KEY`; `laya` needs
|
|
963
|
+
`LAYA_BRIDGE_SCRIPT` (see `portage-ucp-decision`'s README). `portage
|
|
964
|
+
doctor` flags whichever of these is missing for the selected backend.
|
|
965
|
+
|
|
966
|
+
`jev` is TypeSafe's hosted API, so what it's sent is kept to a minimal,
|
|
967
|
+
allowlisted summary (`Portage::Cli::ConfidenceState`); nothing else on
|
|
968
|
+
the checkout is read:
|
|
969
|
+
|
|
970
|
+
- `request`: the search query, the store's host, the quantity, and the
|
|
971
|
+
picked item's id and title.
|
|
972
|
+
- `approved_quote` (`--quote` runs only): the quote's store, product id,
|
|
973
|
+
title, quantity, total and currency.
|
|
974
|
+
- `checkout`: status, currency; per line, item id, title, unit price,
|
|
975
|
+
quantity, line totals and whether it's the requested line; the totals
|
|
976
|
+
(subtotal, shipping, tax, fees, total — whatever the store lists);
|
|
977
|
+
applied discounts' titles and amounts; the selected shipping option's
|
|
978
|
+
title and price.
|
|
979
|
+
- `warnings`.
|
|
980
|
+
|
|
981
|
+
Never sent: the payment token, the shipping address, the buyer's name,
|
|
982
|
+
phone or email, checkout ids, links and URLs, discount codes, or anything
|
|
983
|
+
from your environment. Store-supplied strings are cut to 200 characters.
|
|
898
984
|
- **policy** also denies a checkout with no `total` line as
|
|
899
985
|
`total_unknown` whenever a spend cap is set, rather than skipping the cap.
|
|
900
986
|
|