@ixo/decision-oracle 0.2.24 → 0.2.25
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.
- package/README.md +1 -1
- package/dist/define-oracle.d.ts.map +1 -1
- package/dist/define-oracle.js +4 -2
- package/dist/define-oracle.js.map +1 -1
- package/dist/lib/openhands/explain.d.ts +85 -0
- package/dist/lib/openhands/explain.d.ts.map +1 -0
- package/dist/lib/openhands/explain.js +499 -0
- package/dist/lib/openhands/explain.js.map +1 -0
- package/dist/lib/openhands/routes.d.ts +5 -0
- package/dist/lib/openhands/routes.d.ts.map +1 -1
- package/dist/lib/openhands/routes.js +4 -1
- package/dist/lib/openhands/routes.js.map +1 -1
- package/dist/lib/openhands/why.d.ts +74 -0
- package/dist/lib/openhands/why.d.ts.map +1 -0
- package/dist/lib/openhands/why.js +244 -0
- package/dist/lib/openhands/why.js.map +1 -0
- package/dist/routes/v1/info.d.ts.map +1 -1
- package/dist/routes/v1/info.js +4 -2
- package/dist/routes/v1/info.js.map +1 -1
- package/docs/api.md +9 -5
- package/docs/guide.md +7 -1
- package/package.json +1 -1
package/docs/api.md
CHANGED
|
@@ -466,8 +466,11 @@ collection gets at most three hire reruns a day from the backstop and three from
|
|
|
466
466
|
separate budgets. `POST /openhands/scan/<collectionId>` scans a watched collection at once (at
|
|
467
467
|
most once a minute; extra calls are scanned on the next minute tick). A scan that starts nothing —
|
|
468
468
|
usually because Blocksync has not indexed the claim yet — is looked at again on each of the next
|
|
469
|
-
three minute ticks, and no webhook scan moves the collection's place in the rotation.
|
|
470
|
-
|
|
469
|
+
three minute ticks, and no webhook scan moves the collection's place in the rotation. The third
|
|
470
|
+
route, `GET /openhands/claims/<claimId>/why`, tells the claim's own submitter why it was decided
|
|
471
|
+
the way it was — the per-check reasons the public receipt leaves out (§7 has its auth and body);
|
|
472
|
+
the OpenHands app shows it beside the receipt. `GET /v1/info` advertises the three paths under
|
|
473
|
+
`openhands` (`hire`, `scan`, `why`), which is how the OpenHands app tells a checker from any other oracle.
|
|
471
474
|
|
|
472
475
|
`OpenhandsOptions` (all optional):
|
|
473
476
|
|
|
@@ -479,7 +482,7 @@ both paths under `openhands`, which is how the OpenHands app tells a checker fro
|
|
|
479
482
|
|
|
480
483
|
Bindings, with `openhands` on: the `HIRE_WORKFLOW` workflow (class `OpenhandsHire`) and, outside
|
|
481
484
|
`ENVIRONMENT=development`, the `OPENHANDS_RATE_LIMITER` ratelimit — unbound there, both webhooks
|
|
482
|
-
answer `503` (fail closed). `decision-oracle init --openhands` writes both. State lives in two
|
|
485
|
+
answer `503` and the why route `429` (fail closed). `decision-oracle init --openhands` writes both. State lives in two
|
|
483
486
|
tables of your `DB` (`openhands_state`, `openhands_hires`, shipped migration `0025_openhands`).
|
|
484
487
|
|
|
485
488
|
### The whole-producer escape hatch
|
|
@@ -1604,7 +1607,7 @@ it("bands a vendor score against the rule's own thresholds", async () => {
|
|
|
1604
1607
|
| `CLAIM_WORKFLOW` | `Workflow` | `ClaimIngest` — one instance per claim |
|
|
1605
1608
|
| `REVIEW_WORKFLOW` | `Workflow` | `ReviewComplete` — the review fold |
|
|
1606
1609
|
| `HIRE_WORKFLOW` | `Workflow` (optional) | `OpenhandsHire` — one instance per hired OpenHands collection. Required with `openhands` on |
|
|
1607
|
-
| `OPENHANDS_RATE_LIMITER` | `RateLimit` (optional) | The OpenHands
|
|
1610
|
+
| `OPENHANDS_RATE_LIMITER` | `RateLimit` (optional) | The OpenHands routes' limit, spent per caller address (an IPv6 caller by its /64) and per collection (the webhooks) or per issuer (the why route). Required with `openhands` on outside `ENVIRONMENT=development` (unbound ⇒ `503`) |
|
|
1608
1611
|
| `SEQUENCE_MANAGER` | `DurableObjectNamespace` | The SDK's `SequenceManagerDO`: one instance per signer, allocating account sequence numbers so concurrent chain writes never race |
|
|
1609
1612
|
|
|
1610
1613
|
Your own bindings ride alongside — the type is open (`OracleEnv & YourEnv`); only the names above
|
|
@@ -1781,9 +1784,10 @@ check.**
|
|
|
1781
1784
|
| `GET /health/ready` | public, **rate-limited** (per IP) | `{ ok }` only — `200` when every critical check passes, `503` otherwise. Checks `d1`, `kv`, `engine` (its `/.well-known/did.json`) and, with `API_LANE` and `API_PAYMENTS` on, `facilitator` (`X402_FACILITATOR_URL/supported`), in parallel, 2 s each, plus `identity` (a config read: fails when `ORACLE_ENTITY_DID` is unset on a non-local `ORACLE_HOSTNAME`) and `owner-delegation` (fails when `OWNER_DELEGATION_CAR` is set but is not a UCAN delegation; unset passes); the settled answer is cached per isolate for 15 s. A failure before the route runs (a config that does not load) is also `503 { ok: false }`. Names no check — the detail is `/v1/admin/status`'s |
|
|
1782
1785
|
| `GET /.well-known/did.json` | public, rate-limit exempt | Needs `SERVICE_ED25519_MNEMONIC` |
|
|
1783
1786
|
| `GET /.well-known/oauth-protected-resource/v1/mcp` | public, rate-limit exempt, `Access-Control-Allow-Origin: *`, `cache-control: public, max-age=300` | RFC 9728 metadata for the MCP server: `{ resource: "https://<ORACLE_HOSTNAME>/v1/mcp", authorization_servers: [AUTH_HUB_URL], scopes_supported: ["claims:write", "claims:read"], bearer_methods_supported: ["header"] }`. `404` unless `API_LANE` is on. Outside the `/v1/api` contract |
|
|
1784
|
-
| `GET /v1/info` | **public** (the only public `/v1` path) | —. The marketplace listing: `name`, `description`, `did`, `address`, `entityDid` (the on-chain oracle entity from `ORACLE_ENTITY_DID`, `null` when unset), `serviceDid`, `supportedProtocols`, `protocols` (the same list readably: `[{ did, name, fee }]` — `name` the protocol's `#pro` profile name, else its claim form's title, cached 1 h, `null` when neither reads; `fee` the `{ denom, minAmount }` register charges for it), `pricing`, `policies`, `earnings: { address, baseWallet }` (the operator's `EARNINGS_ADDRESS` / `EARNINGS_BASE_WALLET`, `null` when unset — read-only, set in the oracle's config), `payouts: { decisionToPay, operatorAmount }` — how a payout amount is chosen, for a portal to show before a company opts in: `decisionToPay` is the config's `payouts` map verbatim (per protocol DID, `{ amount: <answer path>, currency: <ISO 4217 code> | { path } | { asset, decimals } }`; `{}` when none, and a protocol absent from it gets no decision to pay), `operatorAmount` is `{ summary }` from `policies.actSummary` when an `act` policy is configured, else `null` (`actSummary` is served only here, never under `policies`), and `reviewBilling: { mode, pricePerReview }` — how a collection pays for its reviews: `mode: "x402"` (per-review billing ACTIVE: IXO's plan for the oracle allows it — the engine's `GET /v1/oracles/me` `reviewBilling`, KV-cached 5 min — on an oracle running `API_LANE`: per review from the company's credits, an empty evaluation leg) with `pricePerReview: { denom: "upay", amount }` the published per-review price (the one the supported protocols share, else `pricing.api.amountMicroUsd` / `API_PRICE_USD_CENTS`; `null` when none is published), or `mode: "escrow"` (the collection's on-chain evaluation payment, `pricing`) with `pricePerReview: null`. `cache-control: public, max-age=300`. With `openhands` set, also `openhands: { hire: "/openhands/hire/:collectionId", scan: "/openhands/scan/:collectionId" }` — absent on any other oracle. A portal is built against these exact field names |
|
|
1787
|
+
| `GET /v1/info` | **public** (the only public `/v1` path) | —. The marketplace listing: `name`, `description`, `did`, `address`, `entityDid` (the on-chain oracle entity from `ORACLE_ENTITY_DID`, `null` when unset), `serviceDid`, `supportedProtocols`, `protocols` (the same list readably: `[{ did, name, fee }]` — `name` the protocol's `#pro` profile name, else its claim form's title, cached 1 h, `null` when neither reads; `fee` the `{ denom, minAmount }` register charges for it), `pricing`, `policies`, `earnings: { address, baseWallet }` (the operator's `EARNINGS_ADDRESS` / `EARNINGS_BASE_WALLET`, `null` when unset — read-only, set in the oracle's config), `payouts: { decisionToPay, operatorAmount }` — how a payout amount is chosen, for a portal to show before a company opts in: `decisionToPay` is the config's `payouts` map verbatim (per protocol DID, `{ amount: <answer path>, currency: <ISO 4217 code> | { path } | { asset, decimals } }`; `{}` when none, and a protocol absent from it gets no decision to pay), `operatorAmount` is `{ summary }` from `policies.actSummary` when an `act` policy is configured, else `null` (`actSummary` is served only here, never under `policies`), and `reviewBilling: { mode, pricePerReview }` — how a collection pays for its reviews: `mode: "x402"` (per-review billing ACTIVE: IXO's plan for the oracle allows it — the engine's `GET /v1/oracles/me` `reviewBilling`, KV-cached 5 min — on an oracle running `API_LANE`: per review from the company's credits, an empty evaluation leg) with `pricePerReview: { denom: "upay", amount }` the published per-review price (the one the supported protocols share, else `pricing.api.amountMicroUsd` / `API_PRICE_USD_CENTS`; `null` when none is published), or `mode: "escrow"` (the collection's on-chain evaluation payment, `pricing`) with `pricePerReview: null`. `cache-control: public, max-age=300`. With `openhands` set, also `openhands: { hire: "/openhands/hire/:collectionId", scan: "/openhands/scan/:collectionId", why: "/openhands/claims/:claimId/why" }` — absent on any other oracle. A portal is built against these exact field names |
|
|
1785
1788
|
| `POST /openhands/hire/:collectionId` | public, **`OPENHANDS_RATE_LIMITER`** (per caller and per collection; unbound outside development ⇒ `503`), `openhands` on only | Starts the collection's hire (`202 { started: true }`) after reading that it exists and its admin granted this oracle a live meta-grant: `404` no such collection (not cached), `409` no meta-grant (cached 10 min). A hire in flight, or an ended one the chain says is healthy, is `200 { existing: true }` (the healthy answer cached 1 min); an ended one the chain says is stale or low is rerun from the webhook's own budget (`200 { restarted: true, reason }`, three a day, then `{ throttled: true }`). `400` a malformed id, `405` not POST, `503` when the chain or Blocksync cannot answer |
|
|
1786
1789
|
| `POST /openhands/scan/:collectionId` | public, **`OPENHANDS_RATE_LIMITER`**, `openhands` on only | Scans one watched collection now: `200 { started }`, `202 { coalesced: true }` within a minute of the last (scanned on the next minute tick), `404` not watched, `409` paused. A `200 { started: 0 }` (or a failed scan) is retried on the next three minute ticks |
|
|
1790
|
+
| `GET /openhands/claims/:claimId/why` | **the claim's own submitter**: a UCAN invocation (`Authorization: Bearer <token>`, `X-Auth-Type: ucan`) carrying exactly `{ can: "claims/why", with: "ixo:claim:<claimId>" }`, addressed to this oracle's did:web, self-issued (no delegation), expiring at most 10 min out, whose issuer is the claim's on-chain `agentDid`. **`OPENHANDS_RATE_LIMITER`** per caller address before the token is checked and per issuer after (`429`; unbound outside development ⇒ `429`). `openhands` on only | Why the claim was decided: `200 { outcome, summary, reasons[], evidenceGrade?, reviewNote?, receiptCid? }` — `outcome` is `approved` (approve or partial) / `rejected` / `review` (held for a person) / `pending`; each reason is `{ code, label, result: pass \| fail \| error \| review, detail, value?, threshold?, unit? }`, one per gate or criterion, explained through the one fact it reads and labelled by the pinned rubric (`#rub`, read for its public sentences only, hash-checked, cached 7 days; unreadable ⇒ codes without labels). An `ext` fact's reason is never echoed (only the stock geo handlers' distances and evidence grade), an AI sentence only scrubbed of identifiers, and no AI instruction is ever read. A claim queued, or on a watched collection and not yet seen, is `pending`; a **failed ingest** is `pending` with a fixed sentence (`claimFailureReason`) — never the stored error. Anyone but the agent, or a claim this oracle never saw, gets the same `404 NOT_FOUND`; `401` no or a bad token, `400` a malformed id, `405` not GET, `503` when Blocksync or the DID resolver cannot answer |
|
|
1787
1791
|
| `POST /v1/collections/register` | UCAN `collections/register` | The four gates, in order: **who** (the collection entity's chain owner, or a holder of an active on-chain `EvaluateClaimAuthorization` — read uncached) · **hired** (this oracle's `ORACLE_ADDRESS` holds that grant too, else `409`) · **protocol** (in `supportedProtocols`, else `409` — checked before payment) · **paid** (`Payments.evaluation` matches `pricing`, else `402`; while per-review billing is active (IXO's plan), the collection's company — ONE engine read, `GET /v1/collections/:id/company`, its blip or a `disputed` company `503 company_unavailable` — holding a live review-billing delegation on its Decision API source, still signed by the organisation's current chain owner, with a published price and cap left, registers with an EMPTY evaluation leg (`billingSourceId` set, never billed twice) once the payer's credits cover one review: `GET /v1/balances` with the stored delegation in `X-Payment-Delegation`, both `available` and `delegationRemaining` ≥ the price, else `402 insufficient_credits` (`details.price`, `details.fundingUrl`), an unreadable answer `503 credits_unavailable`; no usable delegation ⇒ the escrow gate as before, its refusal `402 no_review_billing` naming both ways in). Body: `{ collectionId, delegationCar?, contactEmail? }`. No `contactEmail` sent and none stored ⇒ the row takes the `contactEmail` of the oldest-activated `active` Decision API source whose `companyEntityDid` is the collection's COMPANY as the engine derives it (`GET /v1/collections/:id/company`, UCAN `collection/company-read`: the collection's entity, or the org a deed is linked to both ways); a `disputed` company, a collection the engine does not know, or none such source ⇒ stays empty. Its `503` or any other failure never fails the register (logged `register.company-unavailable`, the row stays empty for a later register to fill). An explicit or stored contact is never overwritten. The SAME answer (one engine read per register) links the collection to every `active` Decision API source of that company, so its chain claims read with the source's (contracts 1.21.0: `LinkedCollection.linkedBy` = the company's entity DID, audit `source.collection_linked` by `system:company-link`, re-checked hourly against the engine and dropped `not_company` when it names another company or `disputed`; only such a positive finding hides a link — an unreachable engine keeps it in the reads, contracts 1.23.0); idempotent, and only the engine's `ok` links. The engine is asked only when no contact is sent or stored, or an `active` source is not yet linked to the collection. Every refusal carries a stable machine code in `details.reason` beside the human message: `404 collection_not_found`, `403 owner_unresolvable` · `not_owner_or_grantee` · `contact_change_forbidden` · `resume_forbidden`, `409 not_hired` · `unsupported_protocol`, `402 evaluation_underpaid` · `no_review_billing` · `insufficient_credits`, `503 company_unavailable` · `credits_unavailable`. |
|
|
1788
1792
|
| `GET /v1/collections` | UCAN `collections/read` | The rows the caller owns or enrolled |
|
|
1789
1793
|
| `POST /v1/collections/:id/pause` | UCAN `collections/pause` + read gate + owner, enroller (while it holds a live grant) or `ORACLE_ADMIN_DIDS` (a plain grant holder is `403`) | Sets `status: paused`, clears the reason — the cron enqueues nothing |
|
package/docs/guide.md
CHANGED
|
@@ -1043,9 +1043,15 @@ oracle the right to authorize itself on the new collection, and the app calls
|
|
|
1043
1043
|
own evaluate grant under it, registers the collection through the same four gates above, and scans
|
|
1044
1044
|
it. A grants feed every minute catches a call that never arrived (it follows the meta-grants
|
|
1045
1045
|
made to your oracle on Blocksync), and a backstop every five minutes keeps every hired ask
|
|
1046
|
-
topped up. `GET /v1/info` lists the two webhooks under `openhands` — that is how the app knows
|
|
1046
|
+
topped up. `GET /v1/info` lists the two webhooks (and the why route below) under `openhands` — that is how the app knows
|
|
1047
1047
|
you are an OpenHands checker. (api.md §2, "The OpenHands add-on", has every step and answer.)
|
|
1048
1048
|
|
|
1049
|
+
The add-on also answers the people who submit: `GET /openhands/claims/<claimId>/why` tells a
|
|
1050
|
+
claim's own submitter (a UCAN their app signs as the claim's on-chain agent) which checks passed,
|
|
1051
|
+
which failed and why, in the rubric's words — the reasons the public receipt leaves out — and the
|
|
1052
|
+
OpenHands app shows it on the claim. A claim still running, or one whose check could not run, reads
|
|
1053
|
+
"pending"; the second with a fixed sentence, never your logs' error text. Nothing to configure.
|
|
1054
|
+
|
|
1049
1055
|
Three things to decide:
|
|
1050
1056
|
|
|
1051
1057
|
- **Your protocols.** With `supportedProtocols` set, the app offers your oracle for asks on those
|
package/package.json
CHANGED