portage-cli 0.7.5 → 0.8.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 +559 -0
- data/README.md +266 -5
- data/known-stores/categories.yml +1263 -0
- data/lib/portage/cli/agent_profile_url.rb +30 -0
- data/lib/portage/cli/browser_import/categorize.rb +59 -0
- data/lib/portage/cli/browser_import/confirm.rb +35 -0
- data/lib/portage/cli/browser_import/domains.rb +47 -0
- data/lib/portage/cli/browser_import/filter.rb +91 -0
- data/lib/portage/cli/browser_import/importer.rb +248 -0
- data/lib/portage/cli/browser_import/plist_xml.rb +72 -0
- data/lib/portage/cli/browser_import/prober.rb +60 -0
- data/lib/portage/cli/browser_import/profiles.rb +114 -0
- data/lib/portage/cli/browser_import/readers.rb +179 -0
- data/lib/portage/cli/browser_import/saver.rb +62 -0
- data/lib/portage/cli/browser_import/sqlite.rb +68 -0
- data/lib/portage/cli/browser_import.rb +23 -0
- data/lib/portage/cli/browser_profile/allowlist.rb +40 -0
- data/lib/portage/cli/browser_profile/bridge.rb +120 -0
- data/lib/portage/cli/browser_profile/browsers.rb +69 -0
- data/lib/portage/cli/browser_profile/cdp.rb +67 -0
- data/lib/portage/cli/browser_profile/cdp_socket.rb +186 -0
- data/lib/portage/cli/browser_profile/errors.rb +26 -0
- data/lib/portage/cli/browser_profile/launcher.rb +34 -0
- data/lib/portage/cli/browser_profile/profile.rb +93 -0
- data/lib/portage/cli/browser_profile.rb +25 -0
- data/lib/portage/cli/buy.rb +521 -29
- data/lib/portage/cli/classifier.rb +158 -0
- data/lib/portage/cli/compare.rb +3 -0
- data/lib/portage/cli/doctor.rb +155 -1
- data/lib/portage/cli/dot_env.rb +55 -0
- data/lib/portage/cli/find.rb +96 -12
- data/lib/portage/cli/handoff_agents.rb +186 -0
- data/lib/portage/cli/handoff_only.rb +94 -0
- data/lib/portage/cli/handoff_reconciler.rb +15 -1
- data/lib/portage/cli/handoff_target.rb +61 -0
- data/lib/portage/cli/index/builder.rb +335 -0
- data/lib/portage/cli/index/exporter.rb +91 -0
- data/lib/portage/cli/index/known_cache.rb +155 -0
- data/lib/portage/cli/index/product_store.rb +101 -0
- data/lib/portage/cli/index/sources/browser.rb +31 -0
- data/lib/portage/cli/index/sources/shopify_catalog.rb +82 -0
- data/lib/portage/cli/index/sources/stores_file.rb +58 -0
- data/lib/portage/cli/index/sources/webmcp_sweep.rb +29 -0
- data/lib/portage/cli/index/sources/wikidata.rb +95 -0
- data/lib/portage/cli/index/sources.rb +44 -0
- data/lib/portage/cli/index/store.rb +109 -0
- data/lib/portage/cli/index.rb +20 -0
- data/lib/portage/cli/known_stores_url.rb +15 -0
- data/lib/portage/cli/offer_sources.rb +460 -0
- data/lib/portage/cli/payment_methods.rb +24 -3
- data/lib/portage/cli/search_backends.rb +337 -12
- data/lib/portage/cli/setup_wizard/prompt.rb +67 -0
- data/lib/portage/cli/setup_wizard/steps/agent_profile.rb +60 -0
- data/lib/portage/cli/setup_wizard/steps/browser_import.rb +29 -0
- data/lib/portage/cli/setup_wizard/steps/handoff.rb +100 -0
- data/lib/portage/cli/setup_wizard/steps/index_build.rb +31 -0
- data/lib/portage/cli/setup_wizard/steps/policy.rb +60 -0
- data/lib/portage/cli/setup_wizard/steps/retailer_keys.rb +55 -0
- data/lib/portage/cli/setup_wizard/steps/search_keys.rb +54 -0
- data/lib/portage/cli/setup_wizard/steps/shipping.rb +51 -0
- data/lib/portage/cli/setup_wizard.rb +74 -0
- data/lib/portage/cli/version.rb +1 -1
- data/lib/portage/cli/webmcp.rb +10 -3
- data/lib/portage/cli/webmcp_autofill_confirm.rb +38 -0
- data/lib/portage/cli/webmcp_autofill_fields.rb +60 -0
- data/lib/portage/cli/webmcp_autofill_mode.rb +39 -0
- data/lib/portage/cli/webmcp_mapping_confirm.rb +68 -0
- data/lib/portage/cli/webmcp_mappings.rb +84 -0
- data/lib/portage/cli.rb +525 -5
- metadata +58 -2
data/README.md
CHANGED
|
@@ -123,8 +123,9 @@ use the gem's. Or reorder `PATH` so the one you want comes first.
|
|
|
123
123
|
```bash
|
|
124
124
|
portage buy <url> --query "..." [--qty N] [--payment-token TOKEN] [--product-id ID]
|
|
125
125
|
[--yes] [--dry-run] [--auto-open|--no-auto-open]
|
|
126
|
-
[--notify-webhook URL]
|
|
127
|
-
[--
|
|
126
|
+
[--notify-webhook URL]
|
|
127
|
+
[--handoff-target default|print|profile|agent:NAME]
|
|
128
|
+
[--decision-backend jev|laya] [--min-confidence N] [--json]
|
|
128
129
|
[--wait [--wait-timeout DURATION|off]]
|
|
129
130
|
portage buy --query "..." [--store URL] [--max-price N] [--limit N] ...
|
|
130
131
|
portage find --query "..." [--max-price N] [--limit N] [--json]
|
|
@@ -145,7 +146,19 @@ portage policy set [--per-transaction-cap N --currency CUR]
|
|
|
145
146
|
[--velocity-count N --velocity-window-seconds N]
|
|
146
147
|
[--allow HOST ...] [--clear-allowlist]
|
|
147
148
|
portage orders reconcile [--checkout ID] [--json]
|
|
148
|
-
portage
|
|
149
|
+
portage index build [--sources a,b] [--queries FILE] [--dry-run] [--export DIR] [--json]
|
|
150
|
+
portage index refresh [--sources a,b] [--queries FILE] [--dry-run] [--export DIR] [--json]
|
|
151
|
+
portage index show [--stores|--products] [--json]
|
|
152
|
+
portage index add <url> [--json]
|
|
153
|
+
portage index remove <host> [--json]
|
|
154
|
+
portage index sources [--json]
|
|
155
|
+
portage browser import [--browser chrome|edge|brave|arc|firefox|safari] [--profile-root DIR]
|
|
156
|
+
[--history-days 90] [--include-product-pages] [--max-probes 200]
|
|
157
|
+
[--exclude HOST,HOST] [--dry-run] [--yes] [--json]
|
|
158
|
+
portage browser profile init|open|status [--browser chrome|edge|brave|arc] [--port N]
|
|
159
|
+
[--url URL (open only)] [--json]
|
|
160
|
+
portage doctor [--require FILE] [--adapter CLASS_NAME] [--json] # alias: configure
|
|
161
|
+
portage setup [--json] # interactive wizard on a TTY; --json/no TTY: today's doctor report
|
|
149
162
|
portage generate adapter NAME [--dir DIR]
|
|
150
163
|
portage generate agent-profile [--out FILE] [--key-out FILE] [--rotate]
|
|
151
164
|
portage --version
|
|
@@ -188,6 +201,8 @@ See "Proxy" below.
|
|
|
188
201
|
`PORTAGE_MIN_CONFIDENCE`, then `0.8`. The flag is always checked; the env
|
|
189
202
|
var is read (and checked) only while a backend is selected, so a stale
|
|
190
203
|
value can't block a buy that doesn't use the gate.
|
|
204
|
+
- `--handoff-target default|print|profile|agent:NAME` — where a dead-end
|
|
205
|
+
checkout's link goes. See "Hand-off targets and hand-off-only hosts" below.
|
|
191
206
|
- `--json` — machine-readable report instead of the human-readable summary.
|
|
192
207
|
|
|
193
208
|
Exits `0` when a checkout completed (or a dry-run/browse/search resolved
|
|
@@ -346,6 +361,207 @@ opt-in guardrail, not a default-deny one. Per-token scopes (merchant/amount
|
|
|
346
361
|
limits bound to one enrolled card) are set via `portage payment enroll
|
|
347
362
|
--scope-*` above, not here.
|
|
348
363
|
|
|
364
|
+
### Tiers: how a purchase actually finishes
|
|
365
|
+
|
|
366
|
+
Most stores don't let a third-party agent complete payment. `portage buy`
|
|
367
|
+
never pretends otherwise — it builds the cart/checkout it can, then hands
|
|
368
|
+
off through one of three tiers, from least to most involved:
|
|
369
|
+
|
|
370
|
+
| Tier | What | Default | Guardrail |
|
|
371
|
+
| --- | --- | --- | --- |
|
|
372
|
+
| A | Hand off to your own default browser; optionally seed the store index from your bookmarks/history (`portage browser import`) | Hand-off on; import opt-in | Domains only; you see and approve the imported list; nothing leaves the machine |
|
|
373
|
+
| B | A dedicated Portage browser profile drives the cart via WebMCP, then hands off there for you to pay (`portage browser profile`) | Off | Never your default profile; a domain allowlist (the store plus its checkout host); stops at payment — you click pay, your browser's own card autofill fills it |
|
|
374
|
+
| C | Hand-off-only hosts (Amazon, and any host you add) — Portage opens the page or a search/cart-add URL and you buy | On by default, host list is yours to edit | No scraping, no page reads, no UCP probe — just a URL, built not fetched |
|
|
375
|
+
|
|
376
|
+
**Never, in any tier:** Portage reading your browser's password, cookie or
|
|
377
|
+
autofill store; Portage attaching to your default browser profile; Portage
|
|
378
|
+
solving or bypassing a CAPTCHA; card data passing through Portage.
|
|
379
|
+
|
|
380
|
+
### Hand-off targets and hand-off-only hosts
|
|
381
|
+
|
|
382
|
+
Most checkouts end in a hand-off, not a `purchased` outcome — see the next
|
|
383
|
+
section. `--handoff-target default|print|profile|agent:<name>`
|
|
384
|
+
(`PORTAGE_HANDOFF_TARGET`, or `~/.portage/config.json`'s `"handoff_target"`)
|
|
385
|
+
decides where that link goes:
|
|
386
|
+
|
|
387
|
+
- `default` (Tier A) — opens it in your own browser. Today's behaviour:
|
|
388
|
+
`--auto-open`/`--no-auto-open`, `PORTAGE_AUTO_OPEN_CHECKOUT`.
|
|
389
|
+
- `print` — just reports the URL.
|
|
390
|
+
- `profile` (Tier B) — drives the dedicated Portage browser profile (see
|
|
391
|
+
"Portage browser profile" below) instead of your own. With no profile
|
|
392
|
+
attached (not opened yet, or `portage-ucp-webmcp` isn't installed), it
|
|
393
|
+
reports that and falls back to reporting the link.
|
|
394
|
+
- `agent:<name>` — hands the checkout URL and cart summary (items, qty,
|
|
395
|
+
total, store — the same JSON `--notify-webhook` sends) to an external
|
|
396
|
+
agent you've approved once in `~/.portage/config.json`'s
|
|
397
|
+
`"handoff_agents"` (a command, run with a scrubbed environment and the
|
|
398
|
+
payload on stdin, or an `https` webhook — never invoked unless
|
|
399
|
+
`"approved": true`, and never given credentials, payment tokens or
|
|
400
|
+
shipping details beyond what the checkout URL already holds).
|
|
401
|
+
|
|
402
|
+
An unrecognized value is a usage error (`invalid_option` under `--json`),
|
|
403
|
+
checked before the buy starts.
|
|
404
|
+
|
|
405
|
+
Amazon (every marketplace TLD), walmart.com, ebay.com and bestbuy.com
|
|
406
|
+
(unconditionally — no adapter, no UCP for any of them to opt back into), and
|
|
407
|
+
any host in `~/.portage/config.json`'s `"handoff_only_hosts"` are **Tier C,
|
|
408
|
+
hand-off only**: `portage buy` never sends that host a request at all — no
|
|
409
|
+
UCP probe, no page fetch, no cart — it opens the page (or a cart-add/search
|
|
410
|
+
URL when a product id is known) and you buy it yourself. Absent that config
|
|
411
|
+
key, the default list is every Amazon marketplace; once present, your list
|
|
412
|
+
*is* the list — drop Amazon or add another host, and removing one only
|
|
413
|
+
changes the message, since there's no code here that automates a site
|
|
414
|
+
without UCP or WebMCP. `find`, `index build` and `browser import` all skip
|
|
415
|
+
probing a hand-off-only host too, though they may still list it as a
|
|
416
|
+
candidate you already know about. `portage doctor` reports the current
|
|
417
|
+
target and host list. **Portage is open-source software provided as-is,
|
|
418
|
+
without warranty of any kind (MIT)** — how it's used on any site, and
|
|
419
|
+
compliance with that site's terms, is your own responsibility.
|
|
420
|
+
|
|
421
|
+
### Categories and routing
|
|
422
|
+
|
|
423
|
+
`portage find` classifies your query and a store's title/description/URL
|
|
424
|
+
slug against `known-stores/categories.yml` (the top two levels of Google's
|
|
425
|
+
published product taxonomy, ~200 nodes shipped in the gem;
|
|
426
|
+
`~/.portage/categories.yml` overrides or extends it). A tagged
|
|
427
|
+
`stores.yml`/index entry (`{url:, categories: [...]}`) only spends one of a
|
|
428
|
+
query's probe slots when its categories actually match — capped at 3 stores
|
|
429
|
+
per category and 12 total — so a large personal allowlist or a big local
|
|
430
|
+
index doesn't crowd out the store that actually sells what you asked for. An
|
|
431
|
+
entry with no matching category is still reached when you name it by host or
|
|
432
|
+
brand.
|
|
433
|
+
|
|
434
|
+
### Local store index (`portage index`)
|
|
435
|
+
|
|
436
|
+
A fresh install only knows the stores you type into `stores.yml` or that a
|
|
437
|
+
search backend returns for one query. `portage index` gives `find` a
|
|
438
|
+
standing, local list of stores and products to route queries to instead,
|
|
439
|
+
built from sources you can read (`portage index sources`):
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
portage index build # every default source
|
|
443
|
+
portage index build --sources shopify_catalog,stores_file
|
|
444
|
+
portage index build --queries queries.txt # one query per line, instead of the built-in taxonomy sweep
|
|
445
|
+
portage index refresh # re-verify entries older than 7 days, add new ones
|
|
446
|
+
portage index show --stores --json
|
|
447
|
+
portage index show --products --json
|
|
448
|
+
portage index add https://some-shop.example
|
|
449
|
+
portage index remove some-shop.example
|
|
450
|
+
portage index sources # name, what each fetches, source file path
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Stored at `~/.portage/index/{stores,products}.json`, **never in git** and
|
|
454
|
+
**never containing a price or stock field** — those are always fetched live.
|
|
455
|
+
Each new origin gets exactly one `/.well-known/ucp` probe (capped at 500 new
|
|
456
|
+
probes per run), throttled, with progress output. Sources:
|
|
457
|
+
|
|
458
|
+
| Source | Fetches | Default |
|
|
459
|
+
| --- | --- | --- |
|
|
460
|
+
| `shopify_catalog` | Merchant origins and product identities, one query per top-level taxonomy node, from `catalog.shopify.com`'s open catalog | on |
|
|
461
|
+
| `stores_file` | Your own `~/.portage/stores.yml` | on |
|
|
462
|
+
| `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` |
|
|
463
|
+
| `wikidata` | Retailers'/brands' official sites via a public SPARQL query | opt-in (`--sources wikidata`) |
|
|
464
|
+
| `webmcp_sweep` | Which WebMCP preset an origin matches, when a bridge is attached | opt-in, needs a bridge |
|
|
465
|
+
|
|
466
|
+
**The index is untrusted data, on the same footing as any other `find`
|
|
467
|
+
candidate.** It never feeds `Policy#merchant_allowlist` and never counts as
|
|
468
|
+
"you picked a store" for `--yes` — a search ranker (which an index entry
|
|
469
|
+
still is) never gets to complete a purchase on its own.
|
|
470
|
+
|
|
471
|
+
**Known-stores, fetched, not built by you.** The repo itself publishes
|
|
472
|
+
`known-stores/{stores,products}.json` — built the same way, just by the
|
|
473
|
+
maintainer — over jsdelivr's `@main` CDN. `find` uses it automatically
|
|
474
|
+
(cached at `~/.portage/index/known-*.json`, refreshed on `index
|
|
475
|
+
build`/`refresh` or when `doctor` sees it's more than 7 days stale) even
|
|
476
|
+
before you ever run `index build` yourself; your own entries always win over
|
|
477
|
+
it on a conflict. `index build --export DIR` writes a PR-ready copy with
|
|
478
|
+
personal (browser-derived) entries stripped, for anyone who wants to
|
|
479
|
+
contribute a store they found to the shared list.
|
|
480
|
+
|
|
481
|
+
### Browser import (Tier A)
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
portage browser import --dry-run --json
|
|
485
|
+
portage browser import --browser chrome --history-days 90 --max-probes 200
|
|
486
|
+
portage browser import --yes --exclude some-domain-you-declined.example
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Reads your browser's bookmarks and history — Chromium family's `History`
|
|
490
|
+
(SQLite)/`Bookmarks` (JSON), Firefox's `places.sqlite`, Safari's
|
|
491
|
+
`History.db`/`Bookmarks.plist` (needs Full Disk Access on macOS; the command
|
|
492
|
+
explains the prompt and never works around it) — reduces every row to a bare
|
|
493
|
+
domain, and decides each one locally first against the hand-off-only list,
|
|
494
|
+
the local index and the known-stores cache before spending one of at most
|
|
495
|
+
`--max-probes` (default 200) `/.well-known/ucp` probes on an unknown one.
|
|
496
|
+
Kept domains are classified (page titles, bookmark folder names, URL slugs)
|
|
497
|
+
into the same categories `find` routes by, weighted by visit count.
|
|
498
|
+
**Nothing is saved without your approval:** a dry run (or any non-interactive
|
|
499
|
+
run without `--yes`) only shows what it *would* keep; `--yes` (after you've
|
|
500
|
+
reviewed the list, optionally with `--exclude host,host` for ones you don't
|
|
501
|
+
want) writes them to the local index as `sources: ["history"]`/`["bookmark"]`
|
|
502
|
+
entries. `--include-product-pages` (off by default) also keeps product page
|
|
503
|
+
titles/URLs as product entries.
|
|
504
|
+
|
|
505
|
+
**Never reads cookies, saved passwords, or autofill data**, on any browser —
|
|
506
|
+
only the two files named above, verified by a spec that opens a fixture
|
|
507
|
+
profile full of `Login Data`/`Cookies`/`Web Data` decoys and asserts none of
|
|
508
|
+
them were touched.
|
|
509
|
+
|
|
510
|
+
### Portage browser profile (Tier B)
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
portage browser profile init # create the dedicated profile directory
|
|
514
|
+
portage browser profile open # launch it, remote debugging on
|
|
515
|
+
portage browser profile status
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
A dedicated Chromium-family profile (Chrome, Edge, Brave or Arc — Firefox
|
|
519
|
+
and Safari aren't supported for driving) under `~/.portage/browser/`,
|
|
520
|
+
launched with remote debugging scoped to *that* profile only — never your
|
|
521
|
+
default one; Chrome 136+ refuses remote debugging on the default profile
|
|
522
|
+
anyway. Sign into your shopping sites there once. With
|
|
523
|
+
`portage buy ... --handoff-target profile`, the cart is built in this same
|
|
524
|
+
browser via WebMCP (when the store supports it) and the checkout opens there
|
|
525
|
+
for you to pay — driving is limited to a domain allowlist (the store being
|
|
526
|
+
bought from, plus its checkout host); navigating anywhere else stops the
|
|
527
|
+
run. Payment is filled by the browser's own saved-card autofill, triggered
|
|
528
|
+
by your own gesture — Portage never touches a payment field and never
|
|
529
|
+
clicks pay. Requires `gem install portage-ucp-webmcp`.
|
|
530
|
+
|
|
531
|
+
### Retailer offer sources
|
|
532
|
+
|
|
533
|
+
Official, opt-in buyer-side APIs that add more real offers to `portage
|
|
534
|
+
find`, each gated on its own key set in `~/.portage/.env` (or via `portage
|
|
535
|
+
setup`, below):
|
|
536
|
+
|
|
537
|
+
| Retailer | Env var |
|
|
538
|
+
| --- | --- |
|
|
539
|
+
| Walmart Affiliate API | `WALMART_AFFILIATE_API_KEY` |
|
|
540
|
+
| eBay Browse API (Buy It Now only) | `EBAY_BROWSE_ACCESS_TOKEN` (optional `EBAY_MARKETPLACE_ID`) |
|
|
541
|
+
| Best Buy Products API | `BESTBUY_API_KEY` |
|
|
542
|
+
| Etsy Open API v3 (buyer-side `findAllListingsActive`) | `ETSY_LISTINGS_API_KEY` |
|
|
543
|
+
| Amazon Creators API | `AMAZON_CREATORS_ACCESS_TOKEN` (optional `AMAZON_CREATORS_MARKETPLACE`) |
|
|
544
|
+
|
|
545
|
+
With none set, `find` behaves exactly as it did before these existed. Every
|
|
546
|
+
offer from any of these five still ends in hand-off — none of them has a
|
|
547
|
+
checkout `portage buy` can drive, so walmart.com/ebay.com/bestbuy.com are
|
|
548
|
+
hand-off only unconditionally and Amazon/Etsy follow the rules in "Hand-off
|
|
549
|
+
targets and hand-off-only hosts" above (Etsy only when *your own* seller
|
|
550
|
+
credentials aren't already configured for `portage-ucp-etsy`). `portage
|
|
551
|
+
doctor` reports which of the five are active.
|
|
552
|
+
|
|
553
|
+
### The `portage setup` wizard
|
|
554
|
+
|
|
555
|
+
On a TTY, `portage setup` (or `portage doctor`/`portage configure` when
|
|
556
|
+
nothing is configured yet) walks through setup interactively, one skippable
|
|
557
|
+
step at a time, never echoing a secret back: shipping address, search API
|
|
558
|
+
keys, retailer offer source keys, the agent profile, browser import, index
|
|
559
|
+
build, spending policy caps, and hand-off target/hand-off-only hosts. Each
|
|
560
|
+
step delegates to the real command it configures — nothing is
|
|
561
|
+
reimplemented — so it behaves exactly like running that command yourself.
|
|
562
|
+
Under `--json`, or with no TTY on stdin (piped, CI, or a tool call), `setup`
|
|
563
|
+
never prompts: it prints exactly `doctor --json`'s read-only report.
|
|
564
|
+
|
|
349
565
|
### Orders reconcile
|
|
350
566
|
|
|
351
567
|
Nearly every real checkout `portage buy` can't finish itself hands the
|
|
@@ -405,8 +621,9 @@ that).
|
|
|
405
621
|
### Doctor
|
|
406
622
|
|
|
407
623
|
```bash
|
|
408
|
-
portage doctor #
|
|
624
|
+
portage doctor # alias: portage configure
|
|
409
625
|
portage doctor --json
|
|
626
|
+
portage setup # same report, but interactive on a TTY — see below
|
|
410
627
|
```
|
|
411
628
|
|
|
412
629
|
Checks this machine's setup without touching the network (apart from
|
|
@@ -427,6 +644,12 @@ installed, then lists anything that needs fixing:
|
|
|
427
644
|
- `env_file`: which env file was loaded (see "Environment file" below).
|
|
428
645
|
Warns when other users can read it.
|
|
429
646
|
- The confidence gate's backend, the User-Agent, and proxy settings.
|
|
647
|
+
- `index`: local and known-stores index counts and staleness (see "Local
|
|
648
|
+
store index" below).
|
|
649
|
+
- `handoff`: the current `--handoff-target` default and the hand-off-only
|
|
650
|
+
host list, plus the as-is/no-warranty disclaimer.
|
|
651
|
+
- `retailer_offer_sources`: which of the five retailer offer source keys are
|
|
652
|
+
set, and a reminder that none of them can complete a purchase.
|
|
430
653
|
- Seller-side checks against `Portage::Ucp.configuration` (authenticator,
|
|
431
654
|
rate limiter, signing keys, payment handlers). These only run when you
|
|
432
655
|
pass `--require` with your app's initializer (Rails:
|
|
@@ -511,6 +734,35 @@ hand-off. `token` isn't implemented yet; it reports
|
|
|
511
734
|
`webmcp_token_unsupported` rather than attempting completion. Requires
|
|
512
735
|
`gem install portage-ucp-webmcp` — not a hard dependency of `portage-cli`.
|
|
513
736
|
|
|
737
|
+
Against a page whose tools aren't a known platform preset, `Buy` falls back
|
|
738
|
+
to a schema-matched, shopper-confirmed mapping instead of giving up (see
|
|
739
|
+
`portage-ucp-webmcp`'s README, "Stores that don't run Portage"). A mutating
|
|
740
|
+
match prompts on a real TTY with `--json` off; under `--json` or with no
|
|
741
|
+
TTY it stops instead (outcome `webmcp_mapping_unconfirmed`) and returns the
|
|
742
|
+
proposed mapping for the caller to pass back.
|
|
743
|
+
|
|
744
|
+
`dry_run: true` against a page whose preset hands off through its own
|
|
745
|
+
checkout tool (Shopify's `proceed_to_checkout`) stops after the read-only
|
|
746
|
+
product search: nothing is added to the store's cart, the tab isn't sent to
|
|
747
|
+
checkout and nothing is autofilled. The `dry_run` report carries a `would:`
|
|
748
|
+
key with the line item, the hand-off tool and whether autofill would run.
|
|
749
|
+
|
|
750
|
+
Once the flow hands off to the store's own checkout page, the shopper can
|
|
751
|
+
opt into having it pre-filled: `--autofill`, or
|
|
752
|
+
`PORTAGE_WEBMCP_AUTOFILL=approve` / config.json's `"webmcp_autofill":
|
|
753
|
+
"approve"` (only that literal string turns it on — a generic truthy value
|
|
754
|
+
doesn't). Even opted in, nothing is typed until a second prompt shows the
|
|
755
|
+
shopper exactly which fields and values are about to be entered and they
|
|
756
|
+
approve it — refused outright under `--json` or no TTY. It only ever
|
|
757
|
+
touches contact email and shipping address (from `PORTAGE_SHIP_*`/the new
|
|
758
|
+
`PORTAGE_SHIP_EMAIL`) plus the cheapest shipping rate it can find; it never
|
|
759
|
+
touches a payment field and never clicks submit/pay, and the run still
|
|
760
|
+
always ends in the same `express_stop` hand-off. A headless browser (or one
|
|
761
|
+
that never says) reports `autofill_needs_headed_browser`; a CAPTCHA/
|
|
762
|
+
challenge on the page reports `autofill_blocked` — see
|
|
763
|
+
`portage-ucp-webmcp`'s README, "Approved autofill of the store's checkout",
|
|
764
|
+
for the full field/outcome list.
|
|
765
|
+
|
|
514
766
|
### Decisions
|
|
515
767
|
|
|
516
768
|
`portage buy` and `portage find` make their judgment calls
|
|
@@ -542,6 +794,7 @@ with the same value, as `[outcome]`.
|
|
|
542
794
|
| `policy_blocked` | Your spend policy denied it; `decisions.policy.reason` says why. | yes |
|
|
543
795
|
| `low_confidence` | The confidence gate held it. | yes |
|
|
544
796
|
| `permission_denied` | The store doesn't let this agent complete checkout. | yes |
|
|
797
|
+
| `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) |
|
|
545
798
|
| `store_refused` | The store refused a cart/checkout call (e.g. sold out). | when the store gave one |
|
|
546
799
|
| `no_match` | Nothing in the store's results matched. | no |
|
|
547
800
|
| `browse_only` | The store has a catalog but no UCP checkout. | when an adapter offers a link |
|
|
@@ -708,7 +961,8 @@ already refuses to commit against a merchant.
|
|
|
708
961
|
|
|
709
962
|
| Backend | Credentials | Notes |
|
|
710
963
|
| --- | --- | --- |
|
|
711
|
-
| Allowlist | `~/.portage/stores.yml` (YAML array of URLs) or `PORTAGE_STORES` (comma-separated) | Stores you already trust. Checked first, costs no network call. |
|
|
964
|
+
| Allowlist | `~/.portage/stores.yml` (YAML array of URLs, optionally tagged `{url:, categories: [...]}`) or `PORTAGE_STORES` (comma-separated, untagged) | Stores you already trust. Checked first, costs no network call. Tagged entries are routed by category (see "Categories and routing" above); untagged ones are always a candidate. |
|
|
965
|
+
| Index | `~/.portage/index/` (your own `portage index build`) plus the repo's published known-stores list | Ranked between Allowlist and DuckDuckGo. Sits out entirely until an index actually exists — a fresh install's behavior is unchanged. Also matches a query against an indexed *product* by name or GTIN, not just a store. |
|
|
712
966
|
| DuckDuckGo | none | The [Instant Answer API](https://api.duckduckgo.com/api). Answers *entity* queries, not web queries: `burton snowboards` resolves to burton.com, `snowboard` resolves to nothing. |
|
|
713
967
|
| Brave | `BRAVE_SEARCH_API_KEY` | Real web results. Set this up if you want open-ended queries to work. |
|
|
714
968
|
| Google | `GOOGLE_CSE_KEY` + `GOOGLE_CSE_CX` | Programmable Search JSON API. |
|
|
@@ -717,6 +971,13 @@ Backends that have no credentials sit out; DuckDuckGo is the keyless default
|
|
|
717
971
|
because it's the only no-key engine with a real API, and its narrowness is the
|
|
718
972
|
price of not scraping.
|
|
719
973
|
|
|
974
|
+
Separate from all of the above, `portage find`/`buy` also merge in offers
|
|
975
|
+
directly from `OfferSources` — `ShopifyCatalog` (no key,
|
|
976
|
+
`catalog.shopify.com`'s open catalog, always on) and the retailer offer
|
|
977
|
+
sources (opt-in, keyed — see "Retailer offer sources" above). These skip the
|
|
978
|
+
origin-probe step entirely, since a catalog result already names the
|
|
979
|
+
merchant's own product page.
|
|
980
|
+
|
|
720
981
|
### Console
|
|
721
982
|
|
|
722
983
|
`portage-console` is a separate executable — a read-only IRB REPL over the
|