@ixo/decision-oracle 0.2.18 → 0.2.20
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 +3 -1
- package/dist/cli/commands/redeliver.d.ts +12 -0
- package/dist/cli/commands/redeliver.d.ts.map +1 -0
- package/dist/cli/commands/redeliver.js +45 -0
- package/dist/cli/commands/redeliver.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/lib/usage.d.ts +1 -1
- package/dist/cli/lib/usage.d.ts.map +1 -1
- package/dist/cli/lib/usage.js +2 -0
- package/dist/cli/lib/usage.js.map +1 -1
- package/dist/cron/collection.d.ts +8 -1
- package/dist/cron/collection.d.ts.map +1 -1
- package/dist/cron/collection.js +52 -9
- package/dist/cron/collection.js.map +1 -1
- package/dist/cron/feed.d.ts.map +1 -1
- package/dist/cron/feed.js +17 -6
- package/dist/cron/feed.js.map +1 -1
- package/dist/cron/scan.js +4 -3
- package/dist/cron/scan.js.map +1 -1
- package/dist/db/schema.d.ts +17 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +8 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/lib/ixo/blocksync/queries.d.ts +1 -1
- package/dist/lib/ixo/blocksync/queries.d.ts.map +1 -1
- package/dist/lib/ixo/blocksync/queries.js +1 -1
- package/dist/lib/ixo/blocksync/types.d.ts +2 -0
- package/dist/lib/ixo/blocksync/types.d.ts.map +1 -1
- package/dist/lib/ixo/blocksync/types.js +3 -0
- package/dist/lib/ixo/blocksync/types.js.map +1 -1
- package/dist/lib/ixo/chain-write.d.ts.map +1 -1
- package/dist/lib/ixo/chain-write.js +5 -1
- package/dist/lib/ixo/chain-write.js.map +1 -1
- package/dist/lib/openhands/backstop.d.ts +14 -5
- package/dist/lib/openhands/backstop.d.ts.map +1 -1
- package/dist/lib/openhands/backstop.js +22 -10
- package/dist/lib/openhands/backstop.js.map +1 -1
- package/dist/middleware/capability.d.ts +1 -1
- package/dist/middleware/capability.d.ts.map +1 -1
- package/dist/middleware/capability.js +4 -0
- package/dist/middleware/capability.js.map +1 -1
- package/dist/routes/v1/admin.d.ts +3 -0
- package/dist/routes/v1/admin.d.ts.map +1 -1
- package/dist/routes/v1/admin.js +102 -0
- package/dist/routes/v1/admin.js.map +1 -1
- package/dist/routes/v1/shared.d.ts +18 -0
- package/dist/routes/v1/shared.d.ts.map +1 -1
- package/dist/workflows/claim-ingest.d.ts.map +1 -1
- package/dist/workflows/claim-ingest.js +21 -6
- package/dist/workflows/claim-ingest.js.map +1 -1
- package/docs/api.md +10 -4
- package/docs/guide.md +7 -3
- package/migrations/0028_next_scan_at.sql +1 -0
- package/migrations/meta/0028_snapshot.json +3014 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
package/docs/api.md
CHANGED
|
@@ -258,7 +258,7 @@ and the database handle from it exactly as the cron does (`cron/scan.ts`'s `cron
|
|
|
258
258
|
| `broadcastAsOracle` | function | `(env, msgs, memo?) => Promise<{ code, transactionHash, rawLog? }>` — signs and broadcasts arbitrary messages as THIS oracle's account, the same sequence-managed signer the act lane uses (`getOracleSigner`, `lib/ixo/chain.ts`). Memo defaults to your oracle's own `name`. Does **not** inspect `res.code` — unlike `signEvaluation` this is a raw broadcast primitive, not a decision lane, so a non-zero code is yours to interpret. Throws `AppError.unavailable` when `ORACLE_SECP_MNEMONIC` is unset, before the signer is ever built | Composing your own `MsgExec` around kit-issued messages |
|
|
259
259
|
| `rehireOpenhands` | function | `(env, collectionId) => Promise<"started" \| "existing" \| "restarted" \| "throttled" \| "failed">` — starts the OpenHands hire for a collection, or reruns its ended instance (spending the collection's backstop restart budget: three a day). A rerun mints only when the chain says the grant is stale or below 50 verdicts | `openhands` on, and your own `act` policy counts verdicts and sees an ask running low before the backstop's next visit |
|
|
260
260
|
| `AppError` | class | The kit's typed error — `.notFound()`, `.forbidden()`, `.conflict()`, `.unavailable()`, `.internal()`, … (§13) | Catching/rethrowing what the four functions above throw, in your own error handling |
|
|
261
|
-
| `ChainWriteOutcome` | type | `"submitted" \| "skipped" \| "blocked" \| "held" \| "owner_submitted"` — what `signEvaluation` resolves to, and (alongside `"pending"`, the row's initial state before any write is attempted) the full `decisions.chainStatus` enum | Branching on the result, or reading `chainStatus` off `GET /v1/claims/:claimId` |
|
|
261
|
+
| `ChainWriteOutcome` | type | `"submitted" \| "skipped" \| "blocked" \| "held" \| "owner_submitted"` — what `signEvaluation` resolves to, and (alongside `"pending"`, the row's initial state before any write is attempted) the full `decisions.chainStatus` enum. A decision the act lane can never sign (an engine `unusable`, a `decided` review verdict) starts `"skipped"` instead; a `pending_review` starts `"pending"` and its fold acts | Branching on the result, or reading `chainStatus` off `GET /v1/claims/:claimId` |
|
|
262
262
|
| `RegisterInput` | type | `{ collectionId, principal: { did, address }, contactEmail?, delegationCar?, cursor? }`. 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) also links the collection to every `active` source of that company (`api_source_collections`, `linkedBy` = the company's entity DID, audit `source.collection_linked` by `system:company-link`; idempotent — an existing link is left alone); 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. | Building the input to `registerCollection` |
|
|
263
263
|
| `WatchedCollection` | type | The `watched_collections` row `registerCollection` and `scanCollection` read and write | Typing a helper that takes one |
|
|
264
264
|
|
|
@@ -451,7 +451,10 @@ id = the collection id):
|
|
|
451
451
|
A **backstop** runs every five minutes so a lost webhook never strands an ask: it finds meta-grants
|
|
452
452
|
whose collections are not watched yet and hires them, re-mints a watched collection whose poster
|
|
453
453
|
raised the ceiling or whose quota fell below 50, and lifts an `engine-no-grant` pause once the
|
|
454
|
-
chain shows quota again (at least 5 min after the last read — the engine's grant cache).
|
|
454
|
+
chain shows quota again (at least 5 min after the last read — the engine's grant cache). Its
|
|
455
|
+
window reads only the watched collections that can need it — never read, hired or claimed in the
|
|
456
|
+
last day, quota below a top-up, or not read for a week — so a quiet ask costs one read a week (a
|
|
457
|
+
raised ceiling on one arrives by the hire webhook, or within that week). Each
|
|
455
458
|
collection gets at most three hire reruns a day from the backstop and three from the webhook, on
|
|
456
459
|
separate budgets. `POST /openhands/scan/<collectionId>` scans a watched collection at once (at
|
|
457
460
|
most once a minute; extra calls are scanned on the next minute tick). A scan that starts nothing —
|
|
@@ -1608,7 +1611,7 @@ once deployed. Every value is validated by Zod at isolate start, so a bad env fa
|
|
|
1608
1611
|
| `LOG_PRETTY` | `.dev.vars` only | `false` | Colorized local logs. Deployed envs must emit JSON — that is what Workers Logs ingests |
|
|
1609
1612
|
| `SERVICE_NAME` / `SERVICE_VERSION` | vars | `eval-oracle` / `0.0.0` | Log and `/health` identity |
|
|
1610
1613
|
| `CORS_ORIGINS` | vars | `*` | Comma-separated origins, or `*` |
|
|
1611
|
-
| `CRON_COLLECTIONS_PER_TICK` | vars | `25` | Watched collections visited per cron tick. A fleet larger than this rotates in across ticks, least-recently-scanned first
|
|
1614
|
+
| `CRON_COLLECTIONS_PER_TICK` | vars | `25` | Watched collections visited per cron tick. Only DUE rows rotate (`nextScanAt`): each visit parks a collection by what Blocksync says — closed, or ended more than a day ago, for a week; never claimed for a day; otherwise an hour (a full page stays due) — and a webhook scan or a claims-feed hit makes it due again. A fleet larger than this rotates in across ticks, least-recently-scanned first |
|
|
1612
1615
|
| `IXO_NETWORK` | vars | `devnet` | `devnet` \| `testnet` \| `mainnet` — supplies eight URLs below |
|
|
1613
1616
|
| `EVALUATION_DENOM` / `EVALUATION_MIN_AMOUNT` | vars | unset — **required when `IXO_NETWORK` is `mainnet`** | The evaluation fee for this network: never `upay` on an oracle IXO does not run (register refuses it `409 pay_requires_ixo_managed`; mainnet third parties charge USDC.inj `ibc/77C12480BD8C29BF0ACEC2143736C15BAF8CD232CA27BAE09336677B828955E6` or `uixo`). the denom a collection's `Payments.evaluation` must be in (matched exactly) and its minimum, an integer base-unit string. Set both or neither (one alone fails at isolate start). When set they replace `pricing.evaluation` at register and on `/v1/info`; a `pricing.byProtocol` entry still wins for its protocol. No default in code: on mainnet an unset pair fails config validation at isolate start, so a stand-in coin written in `defineOracle` can never be charged there. The template sets `uixo` / `1000000` on devnet and testnet and Noble USDC (`ibc/6BBE9BD4246F8E04948D5A4EEE7164B2630263B9EBB5E7DC5F0A46C62A2FF97B`, 6 decimals) / `1000000` on production |
|
|
1614
1617
|
| `BLOCKSYNC_GRAPHQL_URL` | vars | per network | Override only for a self-hosted indexer |
|
|
@@ -1786,6 +1789,7 @@ check.**
|
|
|
1786
1789
|
| `GET /v1/admin/status` | UCAN `admin/status` + `ORACLE_ADMIN_DIDS` or `STATUS_READER_DIDS` (`403` otherwise — everyone, while both are unset) | `{ readiness: { ok, checks: { <name>: { ok, ms, critical, detail? } } }, backlog: { review: { pending }, collections: { paused: { total, <reason>: n } }, sources?: { paused }, webhooks?: { due, oldestAgeS } }, engine: { billingStatus, manageSubscriptionUrl } \| null }` — `sources`/`webhooks` only with `API_LANE` on; `engine` is the engine's `GET /v1/oracles/me`, `null` when it did not answer within 2 s; `detail` is set only on a failed `identity` or `owner-delegation` check and names the command to run. Outside the `/v1/api` contract |
|
|
1787
1790
|
| `POST /v1/admin/collections/relink` | UCAN `admin/collections-relink` (strict: never `*`) + `ORACLE_ADMIN_DIDS` (`403` otherwise) | The operator's backfill of register's company links. Body `{ collectionIds?: string[] }` (1–100 ids); absent ⇒ every watched collection, lowest id first, up to 100 (`truncated: true` says more exist — name them). Per collection, the same engine read and the same link register makes (no `active` source left unlinked ⇒ `unchanged`, no engine call) → `{ items: [{ collectionId, outcome: "linked" \| "unchanged" \| "no-company" \| "disputed" \| "unavailable" \| "not-watched", linkedSources }], truncated }`. Idempotent. CLI: `decision-oracle relink [--collection <id>]…`. Outside the `/v1/api` contract |
|
|
1788
1791
|
| `POST /v1/admin/claims/:claimId/reviewer-note` | UCAN `admin/reviewer-note` (strict: never `*`) + `ORACLE_ADMIN_DIDS` (`403` otherwise) | (Re)makes the reviewer note for a claim held for a person — one decided before the oracle made notes, or one whose note failed. Re-pulls the body through the claim's own lane, spends one model call, stores the note. `200 { claimId, written: true }`, `503 { written: false }` when the model gave none, `404` no decision, `409` not `pending_review`. The note itself is read on `GET /v1/reviews/:claimId`. CLI: `decision-oracle reviewer-note <claimId>`. Outside the `/v1/api` contract |
|
|
1792
|
+
| `POST /v1/admin/claims/:claimId/redeliver` | UCAN `admin/redeliver` (strict: never `*`) + `ORACLE_ADMIN_DIDS` (`403` otherwise) | Releases a decided approve/reject with a receipt whose delivery is `pending`/`blocked` — made for the row the evaluate step's recovery parked without facts. With `facts` null it re-pulls the body through the claim's own lane, rebuilds the facts (`buildFacts`, the collection's protocol) and stores their `ext`/`ai` on the row with `chainStatus: pending` in one statement — the act policy's inputs only, never sent to the engine, whose decision and receipt stay as recorded — then runs the idempotent `deliverChainEvaluation`. `200 { claimId, outcome, factsDigestMatches, chainStatus, chainError, txHash }` (`factsDigestMatches` null when the row had facts already, `false` when the rebuild differs from the engine's `factsDigest`, e.g. an AI fact answering differently — stored anyway); `404` no decision; `409` not a receipted approve/reject waiting for the chain, a rubric-unusable / invalid-facts rebuild (`details.reason`, `details.issues`; nothing written), or a protocol now pinned to another rubric than the decision's; transient trouble is a `503` to run again. CLI: `decision-oracle redeliver <claimId> --yes`. Outside the `/v1/api` contract |
|
|
1789
1793
|
| `GET /v1/oauth/approvals/:approvalId` | UCAN `oauth/approval/read`, invoker AuthHub's `did:web` only (`403` otherwise; `*` is never accepted here); no `X-Auth-Type` needed | What AuthHub's approve page shows: `{ did, grantId, clientName, protocol: { did, name \| null }, price: { display, maxAmountUpay }, billingMode: "x402" \| "none", status, expiresAt }` (`expiresAt` in unix seconds). `404` unless the approval exists, is `pending` and has not expired. Never the delegation. `404` without `API_LANE`. Outside the `/v1/api` contract |
|
|
1790
1794
|
| `POST /v1/oauth/approvals/:approvalId` | UCAN `oauth/approval/approve`, same invoker rule | `{ decision: "approve", did, delegation: string \| null }` or `{ decision: "decline", did }` → `{ status: "approved" \| "declined" }`. `404` unless pending and `did` is the approval's. For billing `x402` the delegation must be a payment delegation (`parsePaymentDelegation`) whose root issuer is `did`, whose `oracles` name this oracle's did:web, whose audience is this oracle's facilitator (`X402_FACILITATOR_URL`'s did.json), whose `maxAmount` is exactly `maxAmountUpay` (a total across rails: more would be standing authority), and which has not expired — else `400 VALIDATION` with `details.reason` one of `invalid_delegation`, `issuer_mismatch`, `oracle_mismatch`, `facilitator_mismatch` (also logged `oauth.approval.facilitator_mismatch` with both DIDs: AuthHub and this oracle name different facilitators), `amount_mismatch`, `expired`, and the approval stays pending. `price.display` is informational; AuthHub computes what it shows from `maxAmountUpay`. For `none` nothing is kept. Approving gives the app 15 minutes to use it. Audited as `oauth.approval_approved` / `oauth.approval_declined` (actor kind `oauth`). Outside the `/v1/api` contract |
|
|
1791
1795
|
| `GET /v1/oauth/standing/:grantId` | UCAN `oauth/standing/read`, AuthHub's `did:web` only, never `*` | A grant's auto-approve: `{ perClaimUpay, totalUpay, spentUpay, until }` (upay strings, unix seconds), never the delegation. `404` when there is none, it is revoked, `until` has passed, or `policies.allowStandingApprovals` is off |
|
|
@@ -2430,7 +2434,9 @@ Sentry cron monitors: `oracle-scan`, `oracle-claims-feed` and (with `API_LANE`)
|
|
|
2430
2434
|
inside the window the feed read; a NULL cursor is never moved. No active collection ⇒ no read.
|
|
2431
2435
|
`claims-replay` (hourly) re-reads the slice that turned 24 h old (`claims-feed-replay`), so a
|
|
2432
2436
|
claim indexed later than the overlap is still found within a day. The rotation (`scan`) keeps
|
|
2433
|
-
running beside it for first sweeps and as the backstop
|
|
2437
|
+
running beside it for first sweeps and as the backstop, over due rows only (`nextScanAt`,
|
|
2438
|
+
`CRON_COLLECTIONS_PER_TICK` in §11); a feed hit makes its collection due again, a paused one too,
|
|
2439
|
+
so the claims a pause held back are scanned as soon as it resumes.
|
|
2434
2440
|
3. `review-rooms` — hourly: the chain review rooms (20 a pass, least recently synced first) are
|
|
2435
2441
|
brought in line with their reviewer grants — an expired grant's reviewer kicked, a failed invite
|
|
2436
2442
|
retried.
|
package/docs/guide.md
CHANGED
|
@@ -1163,7 +1163,9 @@ watch. An hourly replay re-reads what turned 24 h old, for a claim Blocksync ind
|
|
|
1163
1163
|
|
|
1164
1164
|
**The cron tick.** `CRON_COLLECTIONS_PER_TICK` (default `25`, see [api.md §11](./api.md#11-configuration))
|
|
1165
1165
|
bounds how many watched collections one tick visits; a fleet larger than that rotates in over later
|
|
1166
|
-
ticks, least-recently-scanned first.
|
|
1166
|
+
ticks, least-recently-scanned first. The rotation visits a collection only when it is due: a closed
|
|
1167
|
+
or long-ended one weekly, one that never had a claim daily, the rest hourly — the claims feed is
|
|
1168
|
+
what finds new claims within a minute. Raise it for a large fleet. To act on one collection sooner
|
|
1167
1169
|
than the next tick — e.g. the instant your own webhook sees a submission — call
|
|
1168
1170
|
`scanCollection(env, collectionId)` from `@ixo/decision-oracle/runtime`, or hit
|
|
1169
1171
|
`POST /v1/collections/:collectionId/scan`; both run the exact same discover-and-start pass the tick
|
|
@@ -1178,7 +1180,9 @@ human review. Both are UCAN-guarded and gated to the collection's current chain
|
|
|
1178
1180
|
evaluate-grant holder — the registrant only while it still holds its grant — and the single-claim
|
|
1179
1181
|
read to the collection's live reviewers too, and to the claim's own submitter (its on-chain agent DID or
|
|
1180
1182
|
address, who sees the decision and its public receipt — which names who reviewed — but never the
|
|
1181
|
-
review's answers). `chainStatus` is `pending` / `submitted` / `owner_submitted` / `blocked` / `skipped
|
|
1183
|
+
review's answers). `chainStatus` is `pending` / `submitted` / `owner_submitted` / `blocked` / `skipped`
|
|
1184
|
+
(a decision nothing will ever sign: an engine `unusable`, or a `decided` review verdict with no review
|
|
1185
|
+
held for a person), or
|
|
1182
1186
|
**`held`** when your own `policies.act` hook chose to keep a decided verdict back instead of signing
|
|
1183
1187
|
it immediately — a payout you mean to pool across several claims, say (`ActContext`/`ActDecision`,
|
|
1184
1188
|
[api.md](./api.md#1-import-map)). `signEvaluation(env, claimId, amount?)` from
|
|
@@ -1559,7 +1563,7 @@ own retries, and a terminal failure clears it.
|
|
|
1559
1563
|
| A wasm / bundle error on `deploy` or `dev` | Usually a stale `worker-configuration.d.ts`, or the package's wasm not copied after an install | `bun run cf-typegen`, then reinstall. `wrangler deploy --dry-run` (`bun run build`) reproduces a bundle failure without shipping |
|
|
1560
1564
|
| `facts run` says a handler is `handler-unused` | The name in your table is not a name the manifest declares | Copy the name from `protocol review`, exactly |
|
|
1561
1565
|
| `facts run --preview` says to start `bun run dev` | The engine resolves your `did:web` from `ORACLE_HOSTNAME` and found nothing there | Start the dev server (or point `ORACLE_HOSTNAME` at a reachable host) |
|
|
1562
|
-
| A decision is `decided` but never lands on chain | `policies.autoSign` or `AUTO_SIGN` is off · `ORACLE_SECP_MNEMONIC` is unset · `chainStatus` is `blocked` (no grant, no receipt CID, or an unfunded admin account) · `chainStatus` is `held` (your own `policies.act` hook parked it on purpose) | Read `chainStatus` / `chainError` on `GET /v1/claims/:claimId`. An unfunded admin account resumes and re-delivers by itself once topped up; a `held` row waits for your own `signEvaluation(env, claimId, amount?)` call |
|
|
1566
|
+
| A decision is `decided` but never lands on chain | `policies.autoSign` or `AUTO_SIGN` is off · `ORACLE_SECP_MNEMONIC` is unset · `chainStatus` is `blocked` (no grant, no receipt CID, or an unfunded admin account) · `chainStatus` is `held` (your own `policies.act` hook parked it on purpose) | Read `chainStatus` / `chainError` on `GET /v1/claims/:claimId`. An unfunded admin account resumes and re-delivers by itself once topped up; a `held` row waits for your own `signEvaluation(env, claimId, amount?)` call; a `blocked` row whose `chainError` says its facts could not be rebuilt (a decision recovered after a crash) is released by `decision-oracle redeliver <claimId> --yes`, which rebuilds the facts your act policy reads and signs — the engine's decision and receipt stay as recorded |
|
|
1563
1567
|
|
|
1564
1568
|
---
|
|
1565
1569
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `watched_collections` ADD `next_scan_at` integer;
|