Skip to content

Provider surface: advertise a capability, see jobs, quote - #101

Merged
ALLiDoizCode merged 2 commits into
mainfrom
sandcastle/issue-84
Aug 3, 2026
Merged

Provider surface: advertise a capability, see jobs, quote#101
ALLiDoizCode merged 2 commits into
mainfrom
sandcastle/issue-84

Conversation

@toon-backlog-bot

Copy link
Copy Markdown

Adds a provider-surface freshness invariant to the desktop app so providers can advertise a capability, see jobs, and quote based on current (not stale) availability data.

Part of #84

This PR was produced by the sandcastle agent:implement runner and is awaiting human review.

🤖 Generated with Claude Code

claude added 2 commits August 3, 2026 23:23
Task: #84, part of the agent-fleet-money epic
(toon-meta#262 decision 12 / connector#698). Scope is deliberately narrow:
this ticket's four asks (advertise a capability, inbound job feed, quote,
availability) each depend on protocol pieces that are not yet decided or
not yet publishable from this repo, so this first slice ships the one
piece that is fully specified and buildable now — the cross-plane
freshness invariant ("the socket is the lease") every other piece must
honour once it lands.

Key decisions:
- New `desktop/src/features/providers/lib/providerAvailability.ts`
  (+test, RGR): `deriveProviderAvailability` derives
  `unadvertised | pending | available | stale` from advertising-enabled,
  local session-connected, last-advertised-at, and the connector's
  session lease TTL. A dropped session reads `stale` immediately —
  never waiting out the lease — because offline must cost reachability
  only, and the asymmetry is deliberate: reading a live provider as
  unavailable costs a little missed work, reading an unreachable one as
  available costs a buyer a rejected payment. `canQuoteJobs` gates the
  one action that costs money (a quote) on `available` only, per the
  issue's "do not auto-quote broadly" gotcha.
- `sessionLeaseTtlMs` is a caller-supplied parameter, not hardcoded.
  connector#698 exports it as `SESSION_LEASE_BACKSTOP_TTL` and its own
  doc comment says to read it from there rather than duplicate the
  value — but that export is not present in any `@toon-protocol/client`
  (pinned 0.25.1; 0.26.0 adds `getClaimState` but still not the TTL) or
  `@toon-protocol/connector` release published as of this writing, so
  there is no live caller yet. Mirrors the `agentNetworkFlow.ts`
  precedent (buzz#86): pure, fully unit-tested domain logic shipped
  ahead of its data source, ready for a caller once the value exists.
- `refreshIntervalForLease` scales the mesh's own reference ratio
  (`mesh_llm/discovery.rs` 120s freshness / `coordinator.rs` 45s
  republish, ~0.375) to whatever lease the connector actually grants,
  rather than hardcoding either side of that ratio.

Files changed: desktop/src/features/providers/lib/providerAvailability.ts
(new, +test).

Full local gate green: fmt-check, desktop-tauri-fmt-check, clippy,
test-unit (864 tests), desktop-check, desktop-test (4220 passed),
desktop-build, web-check, web-build.

Blockers for next iteration (all real, not scoping choices):
- Advertise a capability (item 1) needs a decided wire format — no NIP-90
  kind or tag shape for "this agent serves job type X" exists anywhere in
  this repo or the epic's decisions; NIP-89 kind:31990 advertising is
  explicitly parked in toon-meta#262, and no in-relay substitute is
  specified. This is protocol design, not implementation, and should not
  be invented unreviewed in a single ticket.
- Inbound job feed (item 2) needs a job-list UI reading kind:5097 from
  g.toon.relay — the desktop has no open-relay job feed subscription
  today (only the free NIP-01 pattern huddle audio uses on the member
  relay, `toonRelayReader.ts`), and NIP-59 gift-wrapped briefs need a
  generalized unwrap (today's only precedent,
  `channelKeyDelivery.ts::unwrapChannelKey`, hardcodes its rumor kind).
- Quote (item 3) needs `getClaimState`
  (toon-client#494, requires bumping the pinned `@toon-protocol/client`
  to >=0.26.0) plus a pricing/increment-schedule policy the epic doc
  does not specify.
- `SESSION_LEASE_BACKSTOP_TTL` (connector#698) is still unpublished —
  confirmed against the live npm registry for both
  `@toon-protocol/client@0.26.0` and `@toon-protocol/connector@4.0.0`.
  `deriveProviderAvailability` is ready to consume it the moment it is.

Signed-off-by: Claude Sonnet 5 <noreply@anthropic.com>
…atio

Fix a dangling-clause sentence in the module doc, and pull the mesh's
45s/120s refresh ratio into named constants (matching the
agentNetworkFlow.ts/huddleFeeQuote.ts precedent) instead of an inline
magic-number fraction, removing the duplicate explanation of the same
ratio that lived in both the module doc and the function doc.

Signed-off-by: Claude Sonnet 5 <noreply@anthropic.com>
@ALLiDoizCode
ALLiDoizCode merged commit 9b67c45 into main Aug 3, 2026
22 checks passed
@ALLiDoizCode
ALLiDoizCode deleted the sandcastle/issue-84 branch August 3, 2026 23:45
ALLiDoizCode pushed a commit that referenced this pull request Aug 4, 2026
* RALPH: provider surface — advertise, see jobs, quote (buzz#84)

Task: #84, part of the agent-fleet-money epic
(toon-meta#262 decision 12 / Wave 5). PR #101 shipped this ticket's
freshness invariant (providerAvailability.ts) but stopped there, citing
three blockers on the "advertise"/"see jobs"/"quote" pieces. Re-checked
every one of those blockers against the current repo and live npm before
writing any code — two no longer hold, and this ships against what's left.

Key decisions:
- **Advertise (item 1) needs no wire event.** The prior blocker was "no
  NIP-89 kind:31990 allocation" — true, and toon-meta#263 (the closed spec
  ticket this whole epic builds against) still only specifies
  request/quote/offer/result/narration, nothing for discovery. But the
  pull model this ticket also needs (item 2) makes a discovery broadcast
  unnecessary: a provider finds jobs by reading the open kind:5097 feed
  directly, so "advertise" is a local, permissionless toggle
  (providerCapabilitySettings.ts) — consistent with decision 14's "no
  allowlist, no approval step."
- **The inbound job feed (item 2) was not actually blocked.** The prior
  note said "no open-relay subscription path exists" — but buzz#85's
  buyer surface (merged before PR #101, in the same epic) already built
  exactly that (ToonEventTransport.fetchEvents/subscribeLive against
  g.toon.relay). useInboundFactoryJobs.ts reuses it unscoped by author,
  filtered client-side by matchesProviderCapability.
- **Gift-wrapped briefs (also item 2) were not actually blocked either.**
  The prior note said the only unwrap precedent (channelKeyDelivery.ts)
  hardcodes its rumor kind — true, but nostr-tools/nip59 exports a fully
  generic unwrapEvent primitive underneath it. Verified that helper is
  insecure to use directly (it decrypts both layers and discards the
  seal, so a caller never learns whether the seal's signature verifies or
  whether rumor.pubkey matches it — channelKeyDelivery.ts's own stated
  reason for not using it). unwrapFactoryJobRequest.ts duplicates that
  ~10-line authenticity check for the factory-job rumor kind rather than
  sharing code with the channel-membership trust path, a different and
  more sensitive blast radius.
- **Quote (item 3) needed no getClaimState.** The prior note conflated
  "bump the client for getClaimState" (item 3's real ask was a schedule
  builder) with reading earned balance, which this ticket never needed —
  quoting is just signing and publishing a paid write, the same
  transport.publish path postFactoryJob.ts already uses.
  factoryJobQuote.ts builds the kind:7000 status:"quote" template to the
  exact shape factoryJobFeedback.ts's parser already reads (that module's
  own doc says "it never builds a request" — this is the missing
  builder), round-trip tested against that parser directly.
- **What is still genuinely blocked, re-confirmed today**:
  sessionLeaseTtlMs (providerAvailability.ts's freshness gate) has no
  live source — pulled @toon-protocol/client@0.26.1 from npm directly and
  diffed its .d.ts; ToonChannelAccept/ParsedX402Challenge still have no
  `extra` bag, so session_lease_ttl_ms is still silently dropped during
  parsing, exactly as the last two agents on this ticket found. Rather
  than block the whole surface on that, ProviderJobsPanel gates quoting
  on FactoryJobAvailability (the coarse "is the TOON transport ready"
  signal buzz#85 already uses) instead of the full time-windowed
  freshness model, which stays ready for its caller once block#722's TTL
  reaches the client library.

Files changed: desktop/src/features/providers/lib/{providerCapabilitySettings,
providerJobMatch,factoryJobQuote,postFactoryJobQuote,
unwrapFactoryJobRequest,useInboundFactoryJobs}.ts (new, +4 test files),
desktop/src/features/providers/ui/{ProviderCapabilityToggle,
InboundJobsList,QuoteForm,ProviderJobsPanel}.tsx (new),
desktop/src/features/factory-jobs/ui/FactoryJobsScreen.tsx (Buying/Providing
tab switch).

Verified: fmt-check, desktop-tauri-fmt-check, clippy (workspace,
-D warnings), test-unit (864 tests), desktop-check, desktop-test
(4345 passed, 3 pre-existing skips), desktop-build, web-check,
web-build — all green.

Blockers/notes for next iteration: sessionLeaseTtlMs remains unwireable
(see above) — no scope change needed here once it lands, just swap
ProviderJobsPanel's availability gate to canQuoteJobs(deriveProviderAvailability(...)).
Increment scheduling in QuoteForm is manual (owner types milestone names
and prices) rather than derived from any pricing policy — toon-meta#262
explicitly leaves that undecided ("a rate card cannot price 'understand
my weird codebase'"), so this is a deliberate scope stop, not a gap.
No increment-offer/narration/result publishing is built — out of scope
per the issue's own "What" list (delivery is rig#52's job). Gate-pass
rate and reputation display for a provider's own history are not
surfaced here (buyer-side only, from buzz#85).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Claude Sonnet 5 <noreply@anthropic.com>

* Fix unsafe verifyEvent cast in unwrapFactoryJobRequest

asLayer's validator and SealedLayer type never checked for id/sig, so
the seal-verification call papered over the gap with a blind
`as unknown as Parameters<typeof verifyEvent>[0]` cast instead of
validating what verifyEvent actually requires. Split into asRumor
(rumor fields only) and asSealedLayer (adds the id/sig fields the
seal must carry), so verifyEvent's argument is now honestly typed
and validated rather than force-cast.

Signed-off-by: Claude Sonnet 5 <noreply@anthropic.com>

---------

Signed-off-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
toon-backlog-bot Bot pushed a commit that referenced this pull request Aug 4, 2026
Task: #84, part of the agent-fleet-money epic
(toon-meta#262 decision 12). PR #101 shipped the freshness invariant
(providerAvailability.ts) and #101/1a593344 shipped advertise/see-jobs/
quote, but both left providerAvailability.ts's sessionLeaseTtlMs
unwireable — no published @toon-protocol/client exposed the connector's
session_lease_ttl_ms on the path this app's channel bootstrap actually
negotiates through (toon-client#509, re-checked and re-blocked twice
more in this issue's thread). toon-client#509 shipped in
@toon-protocol/client@0.28.0 via ToonClient.getLastConnectorRouteTerms(),
populated by ordinary publishEvent()/openChannel() bootstrap with no
extra probe — the one remaining blocker this ticket closes.

Key decisions:
- Bumped @toon-protocol/client to ^0.28.0 (pnpm-workspace.yaml's
  minimumReleaseAgeExclude updated accordingly, lockfile regenerated).
- ToonPaidWriter.getSessionLease() reads session_lease_ttl_ms off
  getLastConnectorRouteTerms()?.extra after every successful write, not
  just the first — ToonClient caches the greeting negotiation per peer,
  but a repeat write still proves the SESSION (not just the cached
  negotiation) was live at that moment, which is what the freshness
  window needs to bound. Undefined stays undefined (never a substituted
  120_000 default) until a connector with connector#722 answers.
- useProviderAvailability.ts wires that into
  providerAvailability.ts's deriveProviderAvailability, replacing
  ProviderJobsPanel's former coarse "is the transport ready" gate.
- Found and fixed a real chicken-and-egg bug while wiring this: the
  lease is learned FROM a successful write, so gating the quote button
  on canQuoteJobs (kind === "available") would permanently block a
  provider agent's first-ever quote, since nothing else would produce
  the write the freshness state is waiting on. The quote action now
  gates on `availability.kind !== "stale"` instead — pending (unknown)
  proceeds, only a confirmed-dead session blocks — matching the
  freshness invariant's own asymmetry (a wrongly-allowed quote costs
  one rejected 1 µUSDC write; a wrongly-blocked one costs a provider
  its first job).
- The @toon-protocol/client bump surfaced 5 pre-existing SVG
  accessibility lint failures (harness-logos icons, agent-outline.svg)
  and one theme.css formatting issue, unrelated to this ticket's code
  but now enforced by desktop-check against the new dependency
  resolution. Fixed via aria-hidden="true" (these are all consumed as
  <img src> with external alt text, matching the existing
  public/sounds/*.svg convention) and biome's auto-format.

Files changed: desktop/package.json, pnpm-lock.yaml, pnpm-workspace.yaml,
desktop/src/shared/api/toonPaidWriter.ts (+test),
desktop/src/features/providers/lib/useProviderAvailability.ts (new),
desktop/src/features/providers/ui/{ProviderJobsPanel,InboundJobsList,
QuoteForm}.tsx, desktop/public/harness-logos/{grok,omp,openclaw,
opencode}.svg, desktop/src/features/agents/assets/agent-outline.svg,
desktop/src/shared/styles/globals/theme.css.

Verified: fmt-check, desktop-tauri-fmt-check, clippy (workspace,
-D warnings), test-unit (864 tests), desktop-check, desktop-test
(4350 passed, 3 pre-existing skips), desktop-build, web-check,
web-build — all green.

No blockers remaining for this ticket. The provider surface (advertise,
see jobs, quote, availability) is now fully wired end to end: quoting
gates on a real, connector-sourced freshness window once one is known,
and degrades to the coarse-but-safe "session confirmed reachable" read
before the first write establishes it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Claude Sonnet 5 <noreply@anthropic.com>
ALLiDoizCode pushed a commit that referenced this pull request Aug 4, 2026
* RALPH: provider surface — wire the session lease TTL (buzz#84)

Task: #84, part of the agent-fleet-money epic
(toon-meta#262 decision 12). PR #101 shipped the freshness invariant
(providerAvailability.ts) and #101/1a593344 shipped advertise/see-jobs/
quote, but both left providerAvailability.ts's sessionLeaseTtlMs
unwireable — no published @toon-protocol/client exposed the connector's
session_lease_ttl_ms on the path this app's channel bootstrap actually
negotiates through (toon-client#509, re-checked and re-blocked twice
more in this issue's thread). toon-client#509 shipped in
@toon-protocol/client@0.28.0 via ToonClient.getLastConnectorRouteTerms(),
populated by ordinary publishEvent()/openChannel() bootstrap with no
extra probe — the one remaining blocker this ticket closes.

Key decisions:
- Bumped @toon-protocol/client to ^0.28.0 (pnpm-workspace.yaml's
  minimumReleaseAgeExclude updated accordingly, lockfile regenerated).
- ToonPaidWriter.getSessionLease() reads session_lease_ttl_ms off
  getLastConnectorRouteTerms()?.extra after every successful write, not
  just the first — ToonClient caches the greeting negotiation per peer,
  but a repeat write still proves the SESSION (not just the cached
  negotiation) was live at that moment, which is what the freshness
  window needs to bound. Undefined stays undefined (never a substituted
  120_000 default) until a connector with connector#722 answers.
- useProviderAvailability.ts wires that into
  providerAvailability.ts's deriveProviderAvailability, replacing
  ProviderJobsPanel's former coarse "is the transport ready" gate.
- Found and fixed a real chicken-and-egg bug while wiring this: the
  lease is learned FROM a successful write, so gating the quote button
  on canQuoteJobs (kind === "available") would permanently block a
  provider agent's first-ever quote, since nothing else would produce
  the write the freshness state is waiting on. The quote action now
  gates on `availability.kind !== "stale"` instead — pending (unknown)
  proceeds, only a confirmed-dead session blocks — matching the
  freshness invariant's own asymmetry (a wrongly-allowed quote costs
  one rejected 1 µUSDC write; a wrongly-blocked one costs a provider
  its first job).
- The @toon-protocol/client bump surfaced 5 pre-existing SVG
  accessibility lint failures (harness-logos icons, agent-outline.svg)
  and one theme.css formatting issue, unrelated to this ticket's code
  but now enforced by desktop-check against the new dependency
  resolution. Fixed via aria-hidden="true" (these are all consumed as
  <img src> with external alt text, matching the existing
  public/sounds/*.svg convention) and biome's auto-format.

Files changed: desktop/package.json, pnpm-lock.yaml, pnpm-workspace.yaml,
desktop/src/shared/api/toonPaidWriter.ts (+test),
desktop/src/features/providers/lib/useProviderAvailability.ts (new),
desktop/src/features/providers/ui/{ProviderJobsPanel,InboundJobsList,
QuoteForm}.tsx, desktop/public/harness-logos/{grok,omp,openclaw,
opencode}.svg, desktop/src/features/agents/assets/agent-outline.svg,
desktop/src/shared/styles/globals/theme.css.

Verified: fmt-check, desktop-tauri-fmt-check, clippy (workspace,
-D warnings), test-unit (864 tests), desktop-check, desktop-test
(4350 passed, 3 pre-existing skips), desktop-build, web-check,
web-build — all green.

No blockers remaining for this ticket. The provider surface (advertise,
see jobs, quote, availability) is now fully wired end to end: quoting
gates on a real, connector-sourced freshness window once one is known,
and degrades to the coarse-but-safe "session confirmed reachable" read
before the first write establishes it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 1bd97b2)
Signed-off-by: Claude Sonnet 5 <noreply@anthropic.com>

* Extract nested ternary in FactoryJobsScreen into an if/else render helper

The buy/provide body was a three-way nested ternary
(availability -> mode -> buy content), which CODING_STANDARDS.md calls
out to avoid. Pulled it into renderJobsScreenBody() with straight-line
if/else returns — same output, easier to follow.

Signed-off-by: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit cc4c818)
Signed-off-by: Claude Sonnet 5 <noreply@anthropic.com>

* Fix stale module doc in providerAvailability.ts after wiring the live caller

The module comment still claimed there was "no live caller yet" for the
session lease TTL, but this branch's useProviderAvailability.ts is exactly
that caller — reading the TTL from ToonPaidWriter.getSessionLease() rather
than the still-unpublished connector#698 constant the comment described.

Signed-off-by: Claude Sonnet 5 <noreply@anthropic.com>

---------

Signed-off-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants