Repository navigation
Tags
Tags
feat(magic-hour): add AI GIF connector (#56) * feat(magic-hour): add AI GIF connector * fix(magic-hour): settle credits only on completion * test(magic-hour): cover AI GIF lifecycle with fixtures * test(magic-hour): mark replay fixtures synthetic * test(magic-hour): record the happy chain, cover canceled/unknown status, input gate and live run - recorded-job-succeeded.json replaces the synthetic happy chain: a real `deno task record` run (2026-09-25), minimized to submit + one rendering tick + complete, signed download query stripped. - The synthetic chains now use the recorded body shapes (submit {id, frame_cost, credits_charged}; full project object; error object). - New chains: canceled -> 409 zero usage; an unrecognized status keeps polling until complete. - New tests: an omitted output_format goes on the wire as "gif"; the input schema rejects an over-500 prompt, an empty prompt, an unknown format and an unknown key before any upstream call; a gated live test. --------- Co-authored-by: Octo Dang <ooctoo@monid.ai>
Resource lifecycle, 2/2 — saperly connector (17 endpoints + phone-num… …ber resource) (#37) * feat(platform): resource lifecycle — schema, compiler, engine, local host loop (1/2) The RESOURCE family: first-class support for durable, billable things a provider can OWN on a workspace's behalf (openspec: add-resource-lifecycle-saperly D30–D37, refined by refine-resource-model D38–D47). Platform half of a stacked pair — 2/2 adds the saperly proving connector. - resource defs (slug, data, REQUIRED usage rate card: one period clock + fixed/estimated lines, reconcileUsage per estimated line, lifecycle verify/release/refresh, views, webhooks) compiled to zResourceDoc and executed via engine.loadResource - purpose-keyed endpoint bindings (provisions ≤1 / uses / updates / releases / reads): ownership pre-gate in canonical order, uniform vendor-shaped 404 on foreign ids, gated instances injected as data.resources[alias], settle marks by purpose, ensure seeds persist pre-run - mid-run metering = the estimate re-run (zEstimateData.elapsedMs?, usage.updateEstimateEveryMs); stop() outcomes (COMPLETED / UNRESOLVED / STOPPED_UNSETTLED); run.runId, bounded utils.sleep - webhooks: positional scope, declarative HMAC verify (payload template must contain ${rawBody}), ONE route(delivery) → {who, what} verdict - identity guard: connectors/ids.lock.json + deno task ids:check (endpoint ids stay derived ?? request.path — the lock turns silent drift into CI failure) - local host loop: Deno KV IResourceStore (.output/local.db) as engine:run's default ownership window; scripts/webhook.ts simulate|listen (sign/verify/route per compiled descriptors; cloudflared/tailscale/none tunnels, scripts-only) - engine 0.4.0 (resources_since 0.4.0); error taxonomy: NO_RESOURCE_READER, RESOURCES_UNDECLARED, PROVISION_CONSTRUCT, RESOURCE_OP_FAILED * feat(connectors): saperly — 17 endpoints + the phone-number resource (2/2) The resource lifecycle's proving connector (part 2/2; the platform is 1/2). Real US phone numbers with AI personas answering and placing calls. - resource saperly/phone-number: $2/month fixed rent (MONTH/1, CREATION_TIME anchor — flat, confirmed vs the live OpenAPI: no proration/refunds); lifecycle verify (404/releasedAt ⇒ inactive, flaky reads throw retriable) / release (idempotent, embedded connection dies with the number) / refresh (connection pointer AUTHORITATIVE — absence upstream clears it); live connection view sanitized through an allowlist - numbers: provision (consent-priced saga with ONE 409 PriceChanged re-quote retry; seed carries the quoted observedUsage.rent), get, list, update (persona edit), sync (webhook re-sync), release - calls: place (estimate 60s floor / elapsed-priced, updateEstimateEveryMs 30s; stop hangs up then drains), get, list, inbound (webhook-admitted, admit-overdraft), transcripts, recordings - messages: send, list, inbound (bill-only); catalogs: voices, languages - ONE webhook stream (number-events): HMAC verify descriptor + route verdict (message.received → run inbound-messages; call.received → run inbound-calls; call.completed/recording.saved → signal-run; number.* → refresh; receipts → ignore) - categories: agentic-phone + sms leaves; ids.lock +17 endpoints +1 resource - 21 recorded/synthetic fixtures; provider-level strip of internal keys on success AND error surfaces * review(platform): address CodeRabbit round 1 Bugs: - ensure() binds utils to the DERIVED input — utils.request() now sends post-toRequest body/query consistent with the derived URL (ctx.data keeps the validated input, the fn-facing contract) - reader window normalizes host failures: only RESOURCE_OP_FAILED rethrows; any other reader throw wraps retriable (a host reusing INVALID_INPUT must not mark a transient read non-retriable) - KvResourceStore transitions are atomic: provision/release are one kv.atomic() each (row + tombstone together); refresh checks the read versionstamp so a concurrent release wins - webhook --execute uses the same host ordering as engine:run (admit seeds pre-start, persist provisions/releases post-settle) Tightenings: - EngineCtx.admit — the seed ADMISSION port: run() hands ensure's seeds to the host's write before start() (absent + seeds ⇒ loud warn, never a silent drop); scripts pass a store-backed admit via shared admitInto/persistEffects helpers - verify.payload template must ALSO contain ${timestamp} — freshness bound to the HMAC (unbound, a captured body+signature replays forever inside rolling toleranceMs windows); saperly already complies - version-check CONTRACT_PATHS: dead billing.ts/accrue.ts out, resource/usage.ts + resource/def.ts in - resource ids.ts comment: identity is AUTHORED (slug) + asserted, not inferred; .coderabbit.yaml reviews stacked-base PRs (base_branches) Spec drift (both change folders now read the final API): - add-resource-lifecycle-saperly specs/tasks: OwnedResource, data.resource, verify/release/refresh/reconcileUsage/view, accrued(runInput, elapsedMs), host-driven ensure + admit port - run identity: SPEC aligned to code (host handle = stable; run() mints ONE UUID per loop; direct phase calls without a handle are per-call — by design, not persisted state) - refine-resource-model proposal/compiler spec: endpoint: stays optional (the revert's missed spots); 'four write methods' → the five verbs - minimax test files restored to main's bytes (pure fmt churn, out of the PR) Deferred: resource-scope webhook simulate/listen — no resource-scope hook exists in-tree yet (saperly defers subscribe); noted in the script header. Post-merge version reconcile: main took 0.4.0 (credential overhaul) — the resource family ships at 0.5.0 (resources_since 0.5.0). * review(saperly): address CodeRabbit round 1 on #37 Paid-number containment — never delete a maybe-bound connection: - provision-numbers: an AMBIGUOUS bind failure (5xx / thrown transport) now RECONCILES against the number record before any cleanup — our connection attached ⇒ treated as bound (the failure lied); shown definitively unbound ⇒ orphan deleted; inconclusive ⇒ connection KEPT (a leaked free connection beats a dangling pointer). Clean 4xx stays a definitive rejection. New fixture arm pins the reconciled-as-bound path; the degraded fixture gains the reconcile read. - update-numbers: same reconcile on the repair bind — the repair-bind Idempotency-Key makes RETRYING safe, not deleting; delete only when the number record proves the bind did not commit (new update-repair-ambiguous fixture: 500 that committed ⇒ bound, no delete). - a 2xx purchase with no readable id now deletes the pre-created connection before seed throws PROVISION_CONSTRUCT (the alarm stands, the orphan doesn't). Webhook route: - the generic `id` fallback is now scoped: number.* events correlate to the RESOURCE via numberId ?? id (who and what agree), call.* keeps the call-scoped fallback — a number.deleted carrying only `id` no longer misroutes as a run signal. Test pin added. Contract fidelity + hygiene: - zConnectionPatchInput: .refine at-least-one → z.union of .required() arms (the contactout pattern) — the published update contract now rejects {} post-compilation too - zCountry loses its shared-schema .default; the "US" default moves to the two binding sites (house rule: our tightenings live at bindings; compiled docs unchanged) - provider categories gain "sms" (the three message endpoints already declare it); rate card cited once in the provider header and once above the test rate pins (saperly.com/docs/pricing, confirmed 2026-09-16) * fix(webhooks): verify.signaturePrefix — a vendor may sign `<prefix><hex>` Some vendors put literal text before the hex digest in the signature header (saperly: `v1=<hex>`). The verify descriptor had no way to say so and checkDelivery compared bare hex, so such a delivery could only fail "signature mismatch" while simulate's own bare-hex signing kept the local loop green. - zWebhookVerify: optional `signaturePrefix` (absent = bare hex, unchanged for every existing hook) - scripts/webhook.ts: sign() emits it, checkDelivery() requires it * fix(saperly): number-events verify — signaturePrefix "v1=" Saperly's signature header is `v1=<hex>` (https://saperly.com/docs/guides/webhooks; v1's adaptors/saperly/webhooks.ts strips the same prefix). Without the prefix declared every real delivery fails "signature mismatch". Checked with `webhook listen saperly`: a vendor-style `v1=` delivery was 401 before, 200 after; bare hex is now 401. * test(webhook): pin the verify executor; signature-first, constant-time (v1 fidelity) The signaturePrefix fix (29af387/62dbab5) rested on a manual `listen` check — the executor had NO unit pins, which is exactly how the bare-hex port bug hid (simulate signed and verified with the same wrong assumption, a self-consistent loop). Now: - sign/checkDelivery exported (CLI behind import.meta.main so the test import doesn't parse the runner's args); scripts/webhook.test.ts pins the v1 semantics: prefixed round-trip verifies; bare-hex vs declared prefix ⇒ mismatch; prefixed vs undeclared ⇒ mismatch; tampered raw bytes ⇒ mismatch - checkDelivery now checks the SIGNATURE FIRST with a constant-time comparison, THEN the tolerance window — v1's documented ordering (adaptors/saperly/webhooks.ts): timing/reason must not distinguish a stale-but-valid signature from a fresh-but-invalid one. Both orderings pinned. - connector-schema spec: verify MAY declare signaturePrefix (absent = bare hex; a header missing a declared prefix is a mismatch); verification is signature-first, constant-time. * review(round-2): trust-boundary invariants, provider cadence, slot shapes Doc schemas now RE-ENFORCE what the authoring schemas enforced — a compiled doc crosses a trust boundary (disk, hosts), so a doctored artifact must fail LOAD, never silently degrade mid-run: - bundle: a resource doc's id <provider>/ prefix must equal doc.provider (execution fuses doc.provider's auth+origin while hosts select by id — a mismatch runs one vendor's resource under another's identity) - endpoint docs: the binding invariants (provisions ≤1, as requires key, alias uniqueness) extracted into ONE shared checker applied by BOTH the def and doc superRefines — link.ts can no longer silently run provisions[0] of a doctored two-provisions doc - resource docs: reconcile coherence refined on the doc (reconciler keys === estimated-line keys, both directions) — a fixed-line reconciler fails load instead of surfacing as NOT_ASYNC at call time usage.updateEstimateEveryMs no 8000 w resolves endpoint ?? provider (the layering every other usage leaf follows; the spec always promised it) — a provider-declared cadence previously VANISHED from compiled docs and the engine treated affected metered runs as static: holds never re-priced. Used in the coherence checks, the version floor, and the emitted doc. The inputs ⊇ slot check now lints SHAPES, not just names: a slot-required property whose declared scalar type mismatches fails compilation (id: number no longer satisfies id: string), one level of nested required recursed. Bounded lint, not JSON-Schema subsumption. Also: runner.ts loadResource comment → the real op names. Pins: six new tests (three trust-boundary, two cadence, one slot-type). * docs(review): round-2 drift — openspec to final API, synthetic fixtures - add-resource-lifecycle-saperly design.md + proposal.md carry the AMENDED-by-refine banner (the D30–D37 narrative stays; the vocabulary mapping is stated once) — EXCEPT the versioning claim, corrected outright: doc_format_since/fn_abi_since stay put, resources_since alone carries the family - its four spec deltas state the FINAL contracts: purpose-keyed binding arrays + shape lint (compiler), usage rate card + slug + doc-side coherence (schema), loadResource verify/view/reconcileUsage (testing), the shipped phone-number fields (saperly) - refine design: estimate(input, elapsedMs) — the real signature - ALL saperly fixtures renamed synthetic-* (every one is hand-built — no saperly key was ever used) + fixture() refs updated - narration comments dropped (schema/common ×3, catalog JSDoc, webhook.test ×3) - .coderabbit.yaml fixtures rule updated to the shipped allowlist mechanism: response headers only from RECORDED_RES_HEADERS (location) --------- Co-authored-by: ZuoyouDang <ooctoo@g.ucla.edu>
revert(identity): endpoint: optional again — derivation is the defaul… …t, ids.lock guards renames The required-endpoint: rule (D45's second arm) taxed every def and forced a 230-file mechanical sweep — including all 183 defs of main's new connectors, which this branch had no business touching. The default (request.path, trailing slashes stripped) is reasonable, and the rename protection it bought is already covered: connectors/ids.lock.json turns a derived-id drift (a vendor moving a route) into a CI failure. - endpoint: back to zEndpointPath.optional(); compiler derivation (?? request.path) restored - 230 sweep-inserted endpoint: lines removed (the 104 meaningful declarations and main's 3 deliberate redundant ones stay); contextdev and the other 8 merge connectors are byte-for-byte main again - 35 test-def insertions removed; .coderabbit.yaml + CI workflows + README restored to main's bytes (repo-wide fmt noise) - docs + openspec amended (D45, connector-schema spec, tasks 5.1, ids-check header); resource slug stays REQUIRED; ids.lock stays - all 338 compiled docs verified byte-identical
chore(publish): disable GitLab trigger until MR 258; record artifact-… …store rationale (#4) - publish-catalog.yml: GitLab trigger step commented out — until the monid-services catalog-publish job + catalog_publish_tag pipeline input land, the curl would fail after the release is already created. A publish now stops at the GitHub Release (the durable artifact); once re-enabled, re-run an already-published tag's workflow to ingest it (every step is idempotent). - DEVELOPMENT.md: note the disabled trigger + record why publishing is tag-triggered and why the release asset is the artifact store (vs release-triggered / Actions artifacts / direct-to-S3).