2617 Commits
Author SHA1 Message Date
SToneX f3e26b2921 test: resolve repo files from the repo root, not the working directory 2026-09-27 21:03:17 +08:00
SToneX 6e632d2a3c test: stop tests leaking cwd, logging config and env to later tests
- test_cmd_run_linux_hands_off_to_treg_run_user let the isolated
  hand-off chdir to /tmp with exec stubbed out; record the cwd with
  monkeypatch.chdir so it is restored.
- test_call_response_limits started uvicorn with its default log
  config, leaving uvicorn.error at CRITICAL and hiding the fault the
  analytics uvicorn test expects; pass log_config=None.
- scripts/dump_surface.py rewrote TREG_DATABASE_URL when imported by
  the surface snapshot test; skip its env setup once treg is loaded.
- Import verified_signup from conftest, not tests.conftest, which ran
  conftest a second time.
- A teardown hook fails any test that leaves the working directory
  changed, so the next leak points at its cause.
2026-09-27 21:03:17 +08:00
SToneX 0abafa1677 fix(cli): scope --json and --org to one main() invocation
main() kept both flags in module globals and never reset them, so a
second invocation in the same process (tests, embedders) inherited the
previous one's JSON output mode or team override. Reset them on entry
and exit.
2026-09-27 21:03:17 +08:00
SToneX 9293ab17e8 perf(dashboard): load the tutorial scripts with Help and defer the tracking scripts
/tutorial.js and /dashboard-tour/tour.js were blocking scripts in every Dashboard head, but only
the Help view reads them. Its chunk now injects both before it renders, and idle prefetch skips
Help so members do not pay for them either. /adtrack.js and /sitetrack.js are deferred in the
head, ahead of the module entry: they no longer block the first paint and still run, in document
order, before the app can redirect or look for analytics.
2026-09-27 19:55:28 +08:00
SToneX b6f51f9ed5 perf(dashboard): load pages and dialogs on demand
Every page and dialog becomes its own chunk (frontend/src/views.ts), so the entry carries only
Vue, the shell and state; Matter.js now ships only with /search. The chunk for the URL being
opened starts before mount, next to /meta and /auth/me, and the rest of a visitor's reachable
screens load in idle time after boot through the async wrapper's own loader, so a prefetched
screen renders synchronously. v-dialog acts when a dialog mounts, so a dialog whose code arrives
late still takes its first field and returns focus to its opener. A *.vue declaration lets plain
tsserver resolve the dynamic page imports.
2026-09-27 19:55:28 +08:00
SToneX 83c999a943 perf(dashboard): compile the shared onboarding widgets and drop Vue's runtime template compiler
The agent picker, setup instructions and Try it out widgets were runtime template strings in
src/treg/web/agent-setup.js, which forced the Dashboard to alias vue to its compiler build.
Their one source is now frontend/src/agent-setup/ (typed data plus three SFCs): the Dashboard
imports it, and a second Vite build compiles the same modules into the classic /agent-setup.js
script that Enrich Arena loads on its global Vue build.
2026-09-27 19:55:28 +08:00
SToneX 67a59ec0b4 style(dashboard): drop the em dash from the agent cap hint 2026-09-27 19:52:23 +08:00
SToneX 1d93bf5176 fix(openapi): give each /call method its own operation id
FastAPI computes one unique_id per route from the first method its set
yields, so the multi-method /call relay published seven operations under
one hash-order-dependent id and warned on every schema build. Build the
schema from per-method views of multi-method routes (and GET-only views
of HEAD-widened routes) without touching the live routing table, drop
the snapshot script's workaround, and fail tests on the warning.
2026-09-27 19:43:00 +08:00
SToneX 0957ba47dd fix(dashboard): give every dialog an accessible name
Most dialogs (Request a tool, top-up, list as vendor, new team, join by
code...) carried role=dialog with no aria-labelledby or aria-label, so a
screen reader announced an unnamed dialog. Each is now labelled by its
title, and the first-run welcome, whose heading changes by step, by an
aria-label. The browser tests assert a name on every dialog they open and
that focus does not start on a dialog's close button.
2026-09-27 19:41:10 +08:00
SToneX 239b0a5fd8 fix(dashboard): show an uncapped member's daily cap as no limit
A daily cap of -1 means unlimited on the wire, and the Team page printed it
raw: every member row and the new-agent form showed "-1", explained only by
a hint. The fields now show an empty "No limit" placeholder, clearing a
field sends -1, and anything but a whole number is refused with a message.

A browser test checks the member row and the agent form read as no limit,
then sets a cap of 25 and clears it again, each surviving a reload.
2026-09-27 19:41:10 +08:00
SToneX d7b779bc9b test(dashboard): run the browser tests' server on a free port
The e2e server always took 127.0.0.1:18791, which is also the default port
of a `treg serve` proxy, so two runs in parallel worktrees (or a run beside
a proxy) collided: the second found the port taken, or its tests talked to
the other run's server with the other build. playwright.config.ts now picks
a port the OS reports free when the run starts and hands it to the server
script and its workers through TREG_E2E_PORT, which also pins one by hand.
2026-09-27 19:40:57 +08:00
SToneX 175b36e2ca style(dashboard): use a plain dash in the loadAll invites comment 2026-09-27 19:34:51 +08:00
SToneX f76548ad36 fix(dashboard): resume the agent check-in poll when the Team page comes back
Stopping the poll when the Team view was left meant a new agent that
checked in afterwards was never shown as connected on return. The poll now
belongs to the Team page: it stops when the page unmounts and starts again
when the page mounts while the new agent has not checked in yet.

A browser test creates an agent, leaves and returns to the Team page, and
only then marks the agent connected; the card flips without a reload. It
failed with the resume disabled.
2026-09-27 19:34:51 +08:00
SToneX adde47ed6a fix(dashboard): ask the hub nothing when the server has it off
With TREG_HUB_ENABLED off every hub route answers 404, yet the dashboard
probed /hub/tools/mine on every load and team switch, and the admin page
asked for /admin/hub/listings?state=requested. /meta now carries `hub`
(the flag), and the dashboard skips both when it is false. With the hub on
but not open to a team, the probe still decides the entry per team.

A browser test walks the signed-in pages and the public catalog and fails
on any 404 (before: the hub probe and the ai-judge logo); the hub test now
also checks that no hub route is asked while the hub is off.
2026-09-27 19:27:50 +08:00
SToneX 6b0d45aed1 fix(web): draw the ai-judge and lazada platform logos
Both platforms appear in the catalog, and every tile asks for
/logos/platforms/<slug>.svg, so each catalog view logged two 404s before
falling back to the initial tile. ai-judge is a capability, not a brand, so
it gets a drawn generic mark like image-gen and voice-gen (a balance
scale); Lazada has no Simple Icons mark, so it gets the documented
brand-colour lettermark, like JD.
2026-09-27 19:27:50 +08:00
SToneX 1f307caeb5 fix(dashboard): drop late answers for a team or page no longer shown
Only the catalog finder cancelled stale requests. A team switch started
loadAll() without regard for the one still running, so a slow /tools (or
roster, billing, Activity...) answer for the team just left could land
after the new team's and replace it. Opening another platform or detail
page had the same race.

Loaders now take a ticket (state/tickets.js) and check it after every
await: a newer call of the same loader, or for team-scoped data a switch to
another team, makes the late answer stale and it is dropped. The Team
roster's seven requests run in parallel instead of one after another. The
hub probe runs once the active team is settled.

Top-up read billing.autotopup from whatever billing was loaded, which after
a switch could be the previous team's: a switch now clears billing and
closes the dialog, payTopup refuses billing for another team, and it stops
before Checkout if the team changes between the mandate and the payment.

A browser test switches to a team whose /tools answers 1.5s late and
straight back; before, that team's tool appeared under the other team.
2026-09-27 19:27:50 +08:00
SToneX c19bbf911d fix(dashboard): stop the agent poll and the search pile's drop timers with their page
The agent check-in poll kept calling loadOrgAdmin every 3s after the Team
view (or the whole app) was gone; it now stops when the view changes and
the app clears it on unmount.

/search drops its tiles into the pile with one staggered setTimeout each.
They were not tracked, so a page left mid-drop kept adding tiles to the
destroyed pile, and add() restarted its animation frame loop. The timers
are tracked and cleared on rebuild and unmount, and a destroyed pile no
longer starts at all (unit test: failed before).
2026-09-27 19:27:50 +08:00
SToneX 57ac8479b5 fix(dashboard): give every dialog focus, a Tab trap and Escape
Escape went through closeOverlays(), which reset nine overlays and left the
rest (top-up, request a tool, list as vendor, try an endpoint, the access
question, recipes, call details, the resource picker, the agent guide, sign
in) open. Focus moved in for nine of them only, Tab walked out into the page
behind every one, and closing left focus on the page body.

One directive, v-dialog on each role="dialog" element, now does it for all:
focus goes to the first field (else the dialog), Tab and Shift+Tab stay
inside, Escape closes the topmost dialog through the close function it was
given, and focus returns to the control that opened it. The first-run
welcome and invite choice pass no close function, so Escape leaves them.
The per-dialog focus watchers go, and closeOverlays() now only closes the
page's menus. The sign-in dialog mounts with v-if instead of a class, so the
directive sees it open and close.

Browser tests open the top-up, list-as-vendor, request-a-tool, new-team and
sign-in dialogs, press Tab and Shift+Tab twenty times each, and press
Escape; every one failed before.
2026-09-27 19:27:50 +08:00
SToneX 85277b473d fix(dashboard): fit the top-up amounts, hub tool page and ledger rows on phones
At 390px:
- the top-up dialog's inline five-column grid overrode the phone rule, so
  $200 and Other were cut off; the grid is a class now, 3 + 2 on a phone;
- a hub tool's unbreakable `uses` identifiers pushed the page to 794px
  wide; inline code in the main column may now break anywhere;
- a merged ledger row's price column took its full nowrap width and the
  provider chips painted over it; the chips now take their own line.

A browser test at phone width checks that the page never scrolls sideways
on those views, that every amount is on screen and that no ledger text
overlaps; all three failed before.
2026-09-27 19:27:50 +08:00
SToneX 3ebe3cc6f0 fix(dashboard): keep working when browser storage is blocked
Safari with site data blocked throws on the first touch of localStorage.
data() read it unguarded, so the whole app failed to mount and the page
stayed blank; several writes (team switch, theme, logout) threw too.

Every storage access in frontend/src now goes through state/storage.js,
whose reads fall back to null and whose writes are dropped when storage is
unavailable. A browser test blocks localStorage and walks sign-in, page
switches and a reload, and the public catalog; both failed before.
2026-09-27 19:27:49 +08:00
SToneX 082e7b4535 test(dashboard): share the browser tests' sign-in and route helpers
Each spec carried its own copy of the sign-in flow and the hub route stubs.
The next fixes add several specs that need the same flow, plus stubs for a
billing-enabled server and the top-up dialog, so they live in one module.
2026-09-27 19:27:49 +08:00
SToneX eef7b34b4d fix(web): stop /tutorial and /fable scrolling sideways on phones
At 390px the tutorial's header kept every button on one row (the page was
570px wide) and the row of source logos in /fable's terminal ran 16px past
the edge. The header now wraps under the title and the logos wrap with
their line; desktop layouts are unchanged.
2026-09-27 19:19:28 +08:00
SToneX 669f038a91 feat(web): give the share-less landing pages the treg social card
/grokbot, /people-search and the five use-case pages declared a
summary_large_image card but no image, so a shared link rendered without
one. They now name the same /media/og.png card the homepage uses, through
{BASE} so a self-hosted registry points at itself; the use-case generator
emits the same tags.
2026-09-27 19:15:18 +08:00
SToneX e0c20961bb perf(web): give /media an explicit cache policy
/media was a plain StaticFiles mount with no Cache-Control, unlike /logos
and /vendor. Its names are unversioned, and without the header a browser
applies a heuristic lifetime and never revalidates, so an edited landing
script could keep running against new HTML. Scripts, stylesheets and text
now answer no-cache (the existing ETag makes that a 304); images, video
and fonts, which a page only swaps by renaming, keep a day's cache like
the logos.
2026-09-27 19:15:18 +08:00
SToneX 5be8442de6 fix(web): read the catalog size on every page instead of typing it
The launch pages, /resources and two use-case pages typed the catalog's
size by hand, and the copies disagreed with each other and with the
catalog. Every bundled HTML page now goes through one _static_page helper
(the old _legal_page, generalized) that fills {BASE}, {ENDPOINTS} and
{PROVIDERS}, so the pages quote the same live numbers as the landing. The
helper sends an ETag so a no-cache revalidation stays a 304, as it was
when these were FileResponses.

Copy that is not templated states no count: tutorial.js and its two
mirrors, README, integrate.md's old figures, the CLI help, and the
use-case generator now emits the placeholders instead of baking a number.

Tests: every sitemap page, use-case page and agent file is fetched for an
unfilled placeholder (the old list checked a 404 for /.well-known/skill.md),
and a scan of the front-door files fails on a typed catalog size.
2026-09-27 19:15:18 +08:00
SToneX d5075e2765 perf(web): serve grokbot icons as files instead of inline base64
The page inlined every provider icon as a base64 data URI (about 116 KB,
most of it in one JS map filled in at load). The same logos already ship
under media/grokbot/, so the images now reference those files directly,
with width/height matching their rendered size and loading=lazy below the
fold. The one icon with no file (the 32px Exa mark) is added; the treg
mark is /favicon.svg. The HTML drops from 256 KB to 134 KB.
2026-09-27 19:15:18 +08:00
SToneX 45a5dd2088 Merge pull request #695 from superdesigndev/fix/catalog-maintenance-audit
fix(catalog): make the validator and drift check green, merge proposed capabilities, remove dead weight
2026-09-27 19:12:58 +08:00
SToneX 94214fff49 chore(catalog): drop the two douyin.live capabilities the TikHub retirements emptied
douyin.live.danmaku and douyin.live.products had one member each, both TikHub
routes retired in the previous commit (douyin_live_room and
fetch_live_room_product_result). No live route in the catalog does either job:
fetch_live_im_fetch returns a live room's websocket parameters, not its danmaku,
and TikHub's current OpenAPI has no live-room product route. The retired rows
keep their status and note and only lose the capability line.
2026-09-27 18:59:52 +08:00
SToneX e5a5ccdb9c feat(catalog): promote shared proposed capabilities, unify synonyms, gate proposals
Proposed capabilities are live the moment they load (the loader merges them into
the taxonomy, first file wins the description), so unmerged shared proposals and
synonym ids were splitting comparison rows and making titles depend on filename
order.

- Promote the 22 proposals endpoints of two or more providers use into
  capabilities.yaml with one description each, and delete them from every
  provider file.
- One job, one id: companies.lookalike -> companies.similar,
  people.count -> people.search.count, limadata companies.count ->
  companies.search.count, people.email.personal.find ->
  people.email.find.personal. Crustdata's field-value autocompletes move to
  companies.search.filters / people.search.filters instead of sharing
  companies.autocomplete with a name-to-domain resolver.
- Delete proposals that repeat a taxonomy id (account.usage,
  companies.jobs.search, web.search, web.extract).
- catalog_validate: error on a proposal that repeats a taxonomy id or carries
  different descriptions across files; warn when two providers use a proposal
  (promote it) and when two ids of one platform share a description.
- catalog.md: the Rules describe the enforced proposal lifecycle, and the AI
  generation rule now matches capabilities.yaml (job-level generation
  capabilities are memberless; rows carry per-model keys).
2026-09-27 18:59:24 +08:00
SToneX 89456fcd26 chore(catalog): remove orphan examples, memberless capabilities and an empty platform
- Delete 15 example files no row references. They are leftovers from
  extended rows renamed on promotion to core; every successor core row
  (same upstream route) already carries its own example.
- Map anyapi.tiktok.trending_hashtags (live, verified) to the memberless
  tiktok.trends.hashtags instead of deleting the capability.
- Delete 18 taxonomy rows (17 linkedin.*, tiktok-shop.shop.detail) whose
  only members were TikHub rows retired when TikHub removed the routes. No
  live row in any provider does those jobs. The retired rows keep their
  status and status_note but drop the capability line, so they no longer
  point at a job nothing serves.
- Remove the netease-music platform and its TIKHUB_PLATFORM mapping in
  catalog_ingest.py. TikHub's current OpenAPI publishes no NetEase routes,
  so no ingest can recreate rows on it; keeping an empty platform would
  only advertise a shelf with nothing on it. The catalog-review-proposal
  fragment no longer lists it among the music platforms.
- Drop three openmart proposed capabilities (companies.email.find,
  companies.technologies, people.find) no row uses.
- Rename the example of openrouter.video-gen.wan-3-0.from_text after its
  own id: the extended id it was named for no longer exists. Same route,
  same model, same captured response.
2026-09-27 18:59:24 +08:00
SToneX 295fa9efa7 fix(catalog): retire TikHub routes removed upstream, including the core youtube video detail row
The daily catalog-drift check reported 28 catalogued TikHub paths absent from
TikHub's public OpenAPI. TikHub replaced weibo/web with weibo/web_v2 and moved
YouTube video routes to youtube/web_v2.

- Retire the 27 dead extended rows with status: retired and a status_note.
  Where the catalog has a live equivalent, the row points to it with
  superseded_by (weibo web_v2, tiktok shop reviews v2, youtube web_v2
  video info/captions, and the core tikhub.youtube.channel.videos row).
- Retire the core row tikhub.youtube.video.detail on its dead path with
  superseded_by: tikhub.x.youtube-web-v2-get-video-info-v2. That extended row
  serves the same video on the live web_v2 route and already sits in the
  youtube.video.detail contract, so callers of the old id get a 410 that
  points to it.
- Drop the adapters of the two retired contract members
  (tikhub.youtube.video.detail and tikhub.x.weibo-web-fetch-post-detail);
  retired rows are not router candidates.

Evidence:
- OpenAPI spec https://api.tikhub.io/openapi.json fetched 2026-09-27
  (1050 paths): all 28 paths absent, with no case or prefix near-miss; the
  daily drift job has flagged them since 2026-08-29.
- Live via treg 2026-09-27: tikhub.youtube.video.detail
  (GET /api/v1/youtube/web/get_video_info_v2) -> 404 {"detail":"Not Found"}.
  tikhub.x.weibo-web-fetch-user-info -> 404.
- Live via treg 2026-09-27:
  tikhub.x.youtube-web-v2-get-video-info-v2 (GET
  /api/v1/youtube/web_v2/get_video_info_v2?video_id=dQw4w9WgXcQ) -> 200 with
  full data, billed $0.001.
- catalog_drift.py tikhub: 0 dead-path, 0 restored.
2026-09-27 18:59:24 +08:00
SToneX a4445e39a4 ci: run the catalog validator
scripts/catalog_validate.py failed on main unnoticed because CI never ran it; only the unit tests
exercised pieces of it. Run it after the import-boundary check, under the same docs-only skip, so
a catalog data PR that breaks a rule fails the build.
2026-09-27 18:59:24 +08:00
SToneX 04adf88229 fix(catalog): accept synchronous usage settlement and give Jev an example
The catalog validator failed on main for openrouter.ai-judge.decide: it rejected `settle: usage`
on a flat per-call price and on a synchronous row, and the row was stamped verified with no
example_response.

The runtime already honours this shape: 8fccbad6 made `_platform_settle` hand a metered
synchronous call's buffered body to a `usage` basis, and `settlement.derive_basis` reserves a
non-table usage cost at its `fallback`. The validator was behind, so it now accepts
`settle: usage` without a table (requiring the same explicit fallback the runtime dereferences and
the same dotted usage.path and supported unit), rejects a stray usage block under any other
settle, and no longer requires an async descriptor for usage settlement.

The example response is a live call with the row's test_request: status 200, and the call settled
at the reply's usage.cost ($0.000014) rather than the $0.0005 reserve, so the verified stamp moves
to the date of that observation.

Fragments updated: architecture/catalog (the settle: usage paragraph).
2026-09-27 18:59:24 +08:00
UncleCode e579dc4e04 feat(table): "Sign in with treg" for the Google Sheets add-on
The add-on signs in with treg's own OAuth server (authorization code + PKCE S256, public client).

- Client `treg-sheets`, scope `treg:table`, resource `<public_url>/table`. Not a table row: built
  from TREG_SHEETS_REDIRECT_URIS (exact match, one per Apps Script project), and only while the
  table flag is on. It gets that resource and nothing else, and no other client may ask for it, so
  an MCP token never works on the table routes and this token never works on MCP.
- The token works only on /table/*, /table-columns/* and the new GET /table-account. table_caller
  checks the audience and presents the person to require_member as a two-minute identity, the way
  MCP exchanges its own token. Every other route still ignores a Bearer header. The Authorization
  header is removed before the call, so treg's token never reaches a provider.
- The grant belongs to the person, not one team (owner decision): the consent page has no team
  picker, the token carries a default team, and each request picks one with X-Treg-Org, checked for
  membership and role every time. A refresh whose default team the person left moves the default to
  another of their teams; only leaving every team ends the grant.
- GET /table-account: email, active team, and the teams with role and balance (every team with this
  token; the key's one team with a team key).
- Consent page text from the owner's mockup.

Also: a one-item list under a wrapper name (`data`, `results`...) was opened as a wrapper even when
it held no tables, so a search that found one record showed one JSON cell. It is now one row.

Tests: tests/test_table_oauth.py (consent page, one sign-in reaches every team, a team the person is
not in is refused, the token works nowhere else, an MCP token does not work here, only this client
gets the resource, no setting means no client, refresh and sign-out) and one converter test.
Updates docs/context/architecture/table.md and mcp-oauth.md.
2026-09-26 21:06:10 +08:00
UncleCode 8728369f61 fix(table): map every people-search provider's own names to the fixed columns
A real treg.people.search run served by quickenrich left linkedin_url empty: the link sat in its own
column `employee_linkedin`. Reading the saved example of each people-search provider found more
names the map missed, and one wrong reading: for aiark, lusha and leadsforge `company` or `location`
is an object, and the map took the whole object as a JSON cell.

- Aliases added: first_name + profile.first_name; last_name + profile.last_name; title + jobTitle,
  jobTitle.title, position, profile.title, basic_profile.current_title; company + companyName,
  organization_name, company.name, job_company_name; linkedin_url + employee_linkedin, URLs.linkedin,
  socials.linkedin_url, link.linkedin, socialLinks.linkedin; location + location_name,
  basic_profile.location.full_location, location.linkedinText, location.address, location.city.
- `headline` moves to the end of title: a profile tagline, used only when no job title field exists
  (icypeas now gives lastJobTitle).
- A mapped column takes text only; an object falls through to the next path.

Tests: the saved examples of 8 providers fill the fixed columns with the right field, once, and no
fixed column holds a JSON object. Updates docs/context/architecture/table.md.
2026-09-26 19:23:55 +08:00
UncleCode 109f91daf0 feat(table): the column preview says how many providers fill each field
GET /table-columns/<routed job> gains `coverage`: for each contract output field (a list job: the
list field only), {filled_by, providers}. `providers` counts the routed children with an adapter;
`filled_by` those whose adapter's `out` maps the field. The cheapest provider answers first, so a
field only some providers give can come back empty: in a real 9-row fill of email verify, trykitt
served every row and has no score. The add-on ticks only fields every provider fills.

Other tools get no `coverage` key. Tests in tests/test_table.py; updates
docs/context/architecture/table.md.
2026-09-26 17:37:34 +08:00
UncleCode f3c5eb7432 feat(table): GET /table-columns/<tool id>, the columns before anyone pays
The Sheets add-on's tool card and fill mode show a tool's columns before a paid run. Same key, flag
and team lists as /table/, but no provider call, no money and no audit row. A separate path, not
/table/...?preview=1: /table/ passes every query parameter to the provider, as /call/ does.

- routed job: its contract; a list job converts a sample taken from the first child with a saved
  example, so the mapped columns come first, then that provider's own fields;
- hub tool: its manifest's output fields;
- any other catalog endpoint: its saved example through the same converter, rows removed;
- none of these: 404 no_preview.

domain.table gains sample_items and columns_only. The file name of a saved example comes from the
loaded catalog row, never from the request. The route is on the control plane.

Tests: 3 more in tests/test_table.py (every kind, no provider call, balance unchanged, no audit row,
the flag, a hub tool). Updates docs/context/architecture/table.md.
2026-09-26 17:18:40 +08:00
UncleCode cd04b2bdd9 chore(dev): pass the /table/ flags through dev-local.sh
TREG_TABLE_ENABLED, TREG_TABLE_TEAMS and TREG_TABLE_USERS join TREG_HUB_ENABLED in the list the
script bakes into the server command: a tmux session does not inherit the caller's shell, so
without this the flag never reaches the server and every /table/ call answers 404.
2026-09-26 14:56:21 +08:00
UncleCode a6dbaee88f feat(table): /table/<tool id>, one call answered as rows and columns
For the Google Sheets add-on. /table/ takes exactly the request /call/ takes and runs the same call
on the same road (gates, key injection, hold and settle, audit row, Idempotency-Key); only the
ending differs. /call/ is unchanged: it passes every query parameter to the provider and must
return the provider's answer as is (non-negotiable 4), so this is a sibling route, not an option.

- routers.call.run_call_surface: the body of call_tool, shared by /call/, /catalog/call/ and
  /table/; each passes its own `finish`. /call/ passes _relay_answer (stream unchanged, async
  descriptor, review invitation), so its behavior is the same.
- domain.table: a pure, stdlib-only converter with its own import-linter contract. Routed jobs take
  their columns from the contract (flat: output fields then served_by, a miss is 0 rows; a required
  list: one row per item, people-shaped lists mapped to fixed columns first, the map is data). Hub
  tools take the manifest's output fields. Anything else is flat, list or nested (tables + summary),
  found from the lists of objects in the answer, with one-item wrappers opened. Never raises: an
  answer it cannot shape is raw.
- application.table: reads the answer (8 MiB cap, raw and truncated beyond), asks the provider for
  uncompressed bytes, keeps the call's X-Treg-* headers, maps an upstream non-2xx to a JSON error
  with the same status. No money moves here: the call is settled before the answer returns.
- Behind TREG_TABLE_ENABLED (+ TREG_TABLE_TEAMS / TREG_TABLE_USERS), off by default. With it off
  the route answers a plain 404 that leaves no audit row.

Tests: tests/test_table.py (converter on saved answers; end to end: flag, four shapes, same charge
as /call/, upstream error releases the hold, Idempotency-Key replay costs 0, a hub tool).

Adds docs/context/architecture/table.md; updates composition.md, proxy-model.md, api.md,
import-boundaries.md and the AGENTS.md non-negotiable 4 line.
2026-09-26 14:52:34 +08:00
b4ed9253d7 feat(catalog): serve herus13 Apify actors on the platform key with capped billing (#657)
* fix(catalog): keep Apify actor starts off treg's shared key

apify.web.scrape.job.start is priced free because the run bills later by the
actor's own pricing, and nothing on the shared-key path meters that run. On
treg's key it let any caller run any actor on treg's Apify account at no
charge. It now needs the team's own Apify key; job.status and job.results
stay open because per-team ownership already scopes them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(catalog): add Weibo, TikTok Shop, Lazada and Google Maps Apify actors

Combines #641-#644 into one catalog change: nine run-sync entries over four
herus13 actors, plus the lazada platform. The six Weibo entries serve on
treg's key at a price observed on it. Lazada, Google Maps and TikTok Shop are
own-key only: their actors bill run compute or a per-GB start fee that a
per-result price cannot meter.

Co-authored-by: Herus13 <bootforge.ai@gmail.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(catalog): require own keys for Weibo actor runs

* test(frontend): isolate landing interactions from continuous WebGL rendering

* feat(call): let platform_request pin query parameters

Some upstreams take their spend bounds as query options rather than body
fields (Apify's maxTotalChargeUsd, memory and timeout run options). A
queryParams pin must be sent exactly once and is compared as the pinned
value's type; own credentials keep the upstream contract.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(settle): bill Apify platform calls by returned dataset rows

run-sync-get-dataset-items answers a bare array, so every Apify per_result
call settled at its estimate whatever it returned. Count the rows, add an
optional per-row-independent call_fee for the actor's start or compute
charge, and require maxItems (1-200) on the platform key so the hold is the
worst case. Apify's usageTotalUsd lags a finished run by minutes, so the
response body is the settlement evidence.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(catalog): serve the herus13 Apify actors on treg's key

Weibo, Google Maps, TikTok Shop and Lazada now settle on the platform key by
counted dataset rows plus a flat call_fee (actor start, or Lazada's run
compute). memory and timeout are pinned so the fee is fixed and a run ends
before Apify's synchronous wait; TikTok Shop is held to keyword search.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(settle): bound Apify platform calls by maxTotalChargeUsd

maxItems does not bind pay-per-event actors whose own input sets the row
count, so a one-row hold could settle thousands of rows. Require the
maxTotalChargeUsd run option Apify enforces (at most $1), hold it plus
call_fee, accept only the run options each once in plain ASCII, and bill the
hold when a run reaches its cap. Pin meta-ads enrichment off and bill the
LinkedIn actor-start event on treg's key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(catalog): cap herus13 Apify runs by maxTotalChargeUsd on treg's key

Row notes name maxTotalChargeUsd as the enforced spend limit; Lazada's
compute fee is 0.015 under a 180-second timeout pin.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(catalog): name maxTotalChargeUsd as the Apify spend cap

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(settle): treat an Apify run within two rows of its hold as capped

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(call): require a short timeout and a three-row cap on Apify platform runs

Past Apify's 300-second synchronous wait a run answers 408 and keeps billing,
so every Apify per_result platform call now names timeout <= 280. A cap under
call_fee plus three rows would bill an empty answer in full, so it is refused.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(call): keep Apify platform runs inside every wait

treg's upstream read timeout (call_timeout_s, 180 s) and the MCP client's
120 s end the call before a 280 s run finishes, releasing the hold unbilled
while the run keeps billing. Bound timeout to 90 s and 30 s under
call_timeout_s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(catalog): pin herus13 Apify runs to a 90-second timeout on treg's key

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(settle): bill a timed-out Apify platform run at its hold

A run that outlives its own timeout answers 400 run-failed with no rows, but
Apify billed its events up to the caller's cap and the run id in that body
reads the dataset. The caller chose the run's size and timeout, so the hold
settles instead of releasing. The minimum-cap check compares micro-USD.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(catalog): tell Lazada callers to keep platform runs small

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(settle): release timed-out Apify runs; the account must stay Restricted

Billing the whole cap on a TIMED-OUT 400 overcharged callers: a run that
timed out after its start event cost Apify $0.00005 and would have billed the
full cap. The loss treg absorbs stays bounded by the $1 cap, and keeping the
Apify account's resource access Restricted stops anyone reading the unbilled
run's rows by id. Examples now show the 90-second platform timeout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(money): name disconnects as a bounded Apify loss; call_fee wording

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(catalog): record Lazada's 0.05 minimum cap and 10-product floor

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(asynctasks): keep the Bright Data and CompanyEnrich platform keys the merge dropped

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(catalog): verify every Apify per-result price on the platform key

Each row's test_request ran on 2026-09-26 and Apify's chargedEventCounts
matched its rate card. The TikTok ad library actor returns rows again, so it
is verified with a captured example.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(catalog): route Google Maps and Weibo post detail through Apify

Adapters let treg.google.serp.maps and treg.weibo.post.detail choose the
Apify actors, with the run's spend cap, timeout and memory fixed so the
platform guard accepts the child call.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(catalog): drop Lazada's compute fee and keep account details out of notes

The actor's 2026-09-25 pricing no longer bills run compute to the caller (a
79-second run showed no platform usage), so its call_fee over-charged every
call. Notes describe observations by price tier, not by the account that
made them, and carry no run ids.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(catalog): state tiered Apify observations without the account's tier

Notes for plan-tiered actors record the events billed and that they matched
the rate card for the key's tier, not the dollar figure that would name it;
the repeated Weibo observation and the owner-meter notes go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(settle): bill LinkedIn's actor-start once per query it runs

The LinkedIn jobs actor bills its actor-start event for every job title x
location searched, so a flat call_fee under-billed any multi-query call.
cost.call_fee_per names the body arrays whose lengths multiply the fee. The
apify.yaml header no longer names a plan or calls the TikTok actor broken.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(money): describe call_fee_per

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(catalog): accept only declared fields on LinkedIn job search

The actor bills an actor-start for every geo id it searches, and geoIds was
undeclared, so it passed through unbilled. The row now takes its declared
filters only (salary, easyApply, under10Applicants and industryIds added);
places go in locations, which call_fee_per counts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(catalog): leave headroom above the Apify spend cap

A run whose charges land exactly on maxTotalChargeUsd can be aborted by
Apify and answer 400 with no rows, which releases the hold. Lazada's
10-product floor costs exactly its 0.05 minimum cap, so its example now uses
0.06 and the notes say to set the cap above the expected spend.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Herus13 <bootforge.ai@gmail.com>
2026-09-26 15:04:26 +10:00
UncleCode 62035512c9 docs(context): sync 17 fragments to the code on main
The docs board showed 24 fragments whose sources changed since they were written. 16 of them
describe code that moved: the tool hub (call resolution, routes, MCP tools, CLI, reserved team
names, the no-relay base_url guard, review rules), per-row settlement adapters, the dev-local
env-flag trap and the hub worker check. 8 needed no change. data-model.md now lists
application/evidence_retention.py in its sources.

Two stale source entries fixed: api.md pointed at application/onboard.py, now the package
application/onboard/; multi-tenancy.md named tests/test_router_dependencies.py, deleted in
65f50889.

No document moved, renamed or deleted. docs/context/README.md and MAP.md regenerated.
2026-09-26 12:32:28 +08:00
UncleCode e003e29a9f docs: keep the plugin/submission docs at their top-level paths
The docs board proposed moving CLAUDE-PLUGIN.md, MINIMAX-PLUGIN.md, DSH-PLUGIN.md and
PLUGIN-SUBMISSION.md into docs/context/interface/. Jason and SToneX are co-authors on these
files, so the move is reverted to avoid breaking their bookmarks and any in-flight submission
references; they stay at their original docs/ paths, byte-identical to before.

Kept from the same pass:
- PLUGIN-TEST-CASES.md and PLUGIN-TOOL-JUSTIFICATIONS.md moved into docs/context/interface/
  (unclecode is the sole author on both).
- CATALOG-HUNTER-PLAN.md archived to docs/archive/ (unclecode is the sole author).
- catalog-review-proposal.md trimmed to its open decisions; the shipped mechanism it described
  now lives in architecture/catalog.md's new "Domain sections" section.
- dashboard.md gains frontend/README.md as a source.
- skill.md links the two kept worksheets and PLUGIN-SUBMISSION.md at its real path.
- docs/context/README.md and MAP.md regenerated.
2026-09-26 12:02:09 +08:00
UncleCode 085a17a7bf Merge pull request #691 from superdesigndev/release/0.22.0
chore(release): 0.22.0
2026-09-26 03:37:21 +02:00
UncleCode ac5ed26e57 release: 0.22.0 2026-09-26 09:30:58 +08:00
UncleCode 18a02f3145 fix(hub): keep the caller's key out of the maker's log; remember who a key is for a minute
Found reviewing main's CI after the hub merge.

- The maker's own-tool step ran on a snapshot that renamed the caller (`hub-caller:<org>`) but
  kept the caller's key id, name and prefix, so they reached the maker's call log. The snapshot
  now drops them. The privacy test checks it, and names the whole record if it fails again (one
  CI run found the caller's email there and cut the record from its output; not reproduced in
  14 local runs).
- While TREG_HUB_TEAMS or TREG_HUB_USERS is set, every open request that carries a key asked who
  it is: 3 to 5 database reads per catalog search, where search had cost none. The answer is now
  remembered for 60 s per key (HTTP and MCP). Only visibility rides on it, never access.
- test_a_catalog_search_storm_cannot_starve_calls_of_the_pool waited 5 s for the refresh to
  start; 100 searches rank the catalog on the loop first (1.4 s on a laptop), and it failed on 7
  CI runs, before the hub merge too. It now waits 30 s; what it checks is unchanged.

Updates docs/context/architecture/hub.md.
2026-09-26 09:29:00 +08:00
UncleCode 70f285148b feat(hub): TREG_HUB_USERS, a person list beside the team list
The owner's colleagues should try the hub from their own accounts, in whatever team they work
in, without a shared team. `TREG_HUB_USERS` takes sign-in emails; a reader in either list sees
the hub, both empty means every team.

- `enabled_for(slug, email)` and `visible_to(slug, email)`: the team OR the person.
- The caller's email reaches every gate: /call/ of a hub id, the hub routes, and on the open
  surfaces `hub_gate.reader` (catalog search, catalog get, the share page, the agent files) and
  MCP (`_hub_reader`: catalog_search and the hub tools in tools/list).
- A listed person's teammates who are not listed see nothing.

Updates docs/context/architecture/hub.md.
2026-09-26 08:22:41 +08:00
UncleCode 2ff15db042 fix(hub): with TREG_HUB_TEAMS set, a team outside the list sees no trace of the hub
Before, the team list gated only the hub routes and /call/ of a hub id. The open surfaces kept the
plain flag, so with the hub on for one team every other user still read the hub sections of the
agent files, found approved hub tools in catalog search, could open catalog get and the share page,
and saw hub_create, hub_update and hub_mine in the MCP tool list (listed even with the flag off).

- `hub_app.visible_to(slug)`: a reader with a team is judged by `enabled_for`; a reader with no team
  sees the hub only when the list is empty.
- `routers/hub_gate.reader_team` resolves the key or session a request carries, only while a list
  is set. Catalog search, catalog get and its sibling rows, the share page, /skill.md and /llms.txt
  use it; MCP catalog_search resolves the bearer's team the same way.
- `mcp._HubToolsGate` drops the three hub tools from `tools/list` for anyone who cannot see the hub.
- `treg skill bootstrap` sends the key, so a listed team's agents get the hub sections.
- The static plugin files no longer carry the hub sections (owner, 2026-09-26; replaces the
  2026-09-16 decision to always ship them).

Updates docs/context/architecture/hub.md.
2026-09-26 07:52:48 +08:00
UncleCode 3ee1948c64 Merge origin/main into dev/hub: the tool hub, behind TREG_HUB_ENABLED
Brings main's 86 commits (dashboard boot and loading, legacy dashboard removal, test pruning,
overflow and routing fixes) together with the tool hub branch.

Conflicts, both sides kept unless noted:
- the legacy dashboard stays deleted, as on main;
- App.vue and the dashboard state: main's search page and loading states plus the hub pages;
- ci.yml: main's Postgres job, with the hub tests added to its list;
- dev-local.sh: main's server environment plus the hub flag passthrough;
- test_call_application_contract.py, test_marketplace_call.py: main's pruned files plus the
  hub branch's sync `settle: usage` test.

Not conflicts: main and the hub branch fixed the same CompanyEnrich empty-page billing; main's
rule runs first, so the hub branch's copy and its test are dropped. The Listing-tab test reads
the Vue source instead of the deleted legacy page.
2026-09-26 07:32:53 +08:00
Taus a5513dac17 Merge pull request #688 from superdesigndev/codex/fix-akta-fallback-verification
fix(overflow): make route verification actionable
2026-09-25 23:30:40 +06:00
shehjad-dev d6a9516add fix(overflow): explain redacted-only shape differences 2026-09-25 23:24:25 +06:00