Skip to content

Releases: rayspec-labs/rayspec

RaySpec v1.6.2

Choose a tag to compare

@iloveautomation iloveautomation released this 24 Jul 20:30
05d3d20

Added

  • Static frontends can ship a custom 404.html. When a request to a mounted
    static frontend misses (no file, no dir/index.html, and no SPA fallback) and
    the mount's root contains a 404.html, that file is served with HTTP status 404
    and Content-Type: text/html — the GitHub Pages / Netlify / Cloudflare Pages
    convention. Fully backward compatible: without the file, the platform's uniform
    404 is unchanged. The custom page is served only on a genuine content miss —
    reserved platform prefixes (/v1, /health, /oidc) and refused paths
    (traversal, dotfiles, symlink escapes) keep the uniform 404, and on an spa:true
    mount the SPA index.html fallback still wins.

Changed

  • Boot secrets are whitespace-trimmed uniformly, whatever their source. Leading
    and trailing whitespace — a trailing newline and a leading byte-order mark
    included — is now stripped from every resolved boot secret (DATABASE_URL,
    RAYSPEC_JWT_SIGNING_KEY, RAYSPEC_API_KEY_PEPPER) whether it is read from a
    <VAR>_FILE mount or from the plain variable, giving the two sources one
    documented contract instead of source-dependent behavior. Interior bytes are never
    touched, so a multi-line PEM keeps its internal newlines and its header at offset 0.

    ⚠ Migration note. The one behavioral change is on the plain variable, and it
    matters because RAYSPEC_API_KEY_PEPPER is the HMAC key for api-key, session, and
    invite hashes. A deployment whose plain RAYSPEC_API_KEY_PEPPER currently carries
    significant edge whitespace must strip it before upgrading — already-issued keys
    and sessions were hashed under the untrimmed value and would otherwise stop
    verifying. For every well-formed secret (a base64 pepper, an RS256 PEM) this is a
    no-op. The one contract limit: a secret whose real bytes must begin or end with
    whitespace cannot be expressed through a boot variable — encode such a value (for
    example, base64).

  • journal_steps gains a created_at index. Additive migration 0009 adds a
    btree index (journal_steps_created_at_idx) on journal_steps(created_at), so
    time-range and day-bucket scans over the step journal are index-backed instead of
    sequential — paralleling the existing runs_created_at_idx on the run header. The
    change is non-destructive; drizzle-kit migrate builds the index on the existing
    table (a plain, non-CONCURRENTLY build that briefly locks writes for its duration).

Security

  • Transitive postcss pinned to 8.5.18 (GHSA-r28c-9q8g-f849). A pnpm override
    raises the transitive postcss — pulled only by the vite/vitest development
    toolchain, never a runtime dependency — to the patched 8.5.18, clearing a source-map
    (sourceMappingURL) path-traversal advisory flagged by the dependency audit. Build-
    and test-time only: no runtime code path is affected and no published package's
    contents change.

Licensing. This release is distributed under FSL-1.1-ALv2 and converts to the
Apache License 2.0 on 2028-07-24 (the second anniversary of this release).

npm. The 1.6.2 npm packages follow shortly.

RaySpec v1.6.1

Choose a tag to compare

@iloveautomation iloveautomation released this 22 Jul 10:01
059bd29

Patch: @rayspec/db now ships its drizzle/ platform migration chain in the npm tarball, so a backend booted from the npm packages applies its migrations at startup. No API or runtime changes. Previously the chain was excluded by files:["dist"], so an npm-consumed boot failed in applyMigrations().

RaySpec v1.6.0

Choose a tag to compare

@iloveautomation iloveautomation released this 22 Jul 07:35
bcb9491

Added

  • Boot secrets can be read from a file mount. Each of the three boot
    secrets — DATABASE_URL, RAYSPEC_JWT_SIGNING_KEY, and
    RAYSPEC_API_KEY_PEPPER — now also accepts a <VAR>_FILE variant
    (DATABASE_URL_FILE, RAYSPEC_JWT_SIGNING_KEY_FILE,
    RAYSPEC_API_KEY_PEPPER_FILE) naming a file to read the value from, so a
    mounted secret (mode 600) stays out of the container's declared environment
    (docker inspect) and out of the process's own environment. Precedence is
    unambiguous and fail-closed: a set <VAR>_FILE wins outright (the plain
    variable is not consulted); a blank <VAR>_FILE counts as not set (the plain
    variable is used); and a non-blank <VAR>_FILE pointing at a missing,
    unreadable, or empty file aborts the boot rather than silently downgrading
    to the plain variable. Documented in the CLI reference and .env.example.
  • A frontend-only spec boots as a static profile — no database, no auth
    surface.
    A backend-profile document that declares only a frontend (empty
    stores, api, agents, tooling, triggers, handlers, and
    extensions, and no durable worker) now boots with no DATABASE_URL, JWT
    signing key, or API-key pepper, and mounts no auth / OIDC / run route — the
    database-and-auth composition is never reached (not merely left empty).
    /health is liveness-only (200 {"status":"ok"}, no database probe). The two
    response security headers a reverse proxy would otherwise supply —
    Content-Security-Policy and Permissions-Policy — are read from the
    environment (RAYSPEC_FRONTEND_CSP and RAYSPEC_PERMISSIONS_POLICY), each with
    a secure default when unset, so the app can serve a static UI directly with no
    proxy in front. This is distinct from serving a static frontend alongside
    a full API, which is unchanged.
  • Inline and hash-pinned extraction prompts. A product-profile extractors[]
    entry may now carry its extraction system prompt inline as an instructions
    block scalar, or pin an external prompt file by hash with
    instructions_ref: { file, sha256 } — the file is read spec-relative
    (traversal-jailed) and sha256-verified at boot, fail-closed on a missing
    file or a hash mismatch. Exactly one prompt source is allowed (inline
    instructions, a pinned instructions_ref, or the existing sidecar
    prompt_file); declaring more than one fails closed. The no-code guardrail is
    narrowed, not removed: free-form prompt text is admitted only at the
    designated instructions field, and everywhere else — including purpose,
    extraction_constraints, and the still-banned prompt / system_prompt keys —
    the guardrail stays fail-closed.

Changed

  • rayspec plan's read-only shadow guard resolves its target from
    DATABASE_URL_FILE too.
    The guard that refuses to shadow-apply when
    SHADOW_DATABASE_URL resolves to the same host and database as the real
    DATABASE_URL now resolves that comparison target from a DATABASE_URL_FILE
    file mount as well (with precedence over the plain variable), so it still fires
    when the connection string is supplied only through the mount. Because plan
    is read-only and never connects to the real database, a broken mount is not
    fatal here (unlike a server boot): it emits one stderr warning — naming the
    variable, the path, and the OS error code, never the file content — and
    proceeds with no comparison target rather than falling back to a possibly-stale
    plain DATABASE_URL.

Security

  • Dependency advisories patched. Six advisories are resolved by upgrade
    (across hono, brace-expansion, fast-uri, and protobufjs — direct
    dependencies and their transitive copies), and the dependency SBOM is
    refreshed. One remaining advisory — a Windows-only @hono/node-server
    serve-static path traversal, reached only transitively (the fixed 2.x line is
    used directly; the older copy is pulled in solely for a child-process JSON-RPC
    transport, never for static file serving) and not exercised on this project's
    Linux code paths — is not silently ignored: it is a single, tightly scoped,
    documented suppression in the vulnerability-scan allowlist, so a new advisory on
    any other package still fails the dependency audit.

v1.5.1

Choose a tag to compare

@iloveautomation iloveautomation released this 19 Jul 13:12
76e5c17

Docs-only release: every published package now ships a README.md so its npm page renders a purpose description, quickstart pointers, and the license summary. No runtime, API, or dependency changes. See CHANGELOG.

RaySpec v1.5.0

Choose a tag to compare

@iloveautomation iloveautomation released this 18 Jul 11:25
8e3e103

Added

  • RaySpec is installable from npm. npx rayspec init scaffolds a new project (a
    minimal, valid backend spec you can rayspec plan and deploy without provider
    credentials), and npm i -g rayspec puts the rayspec command on your PATH — no
    clone-and-build required. The scoped @rayspec/* packages are published alongside the
    unscoped rayspec launcher.
  • Declarative full-text search. A store can opt into Postgres full-text search with
    fullTextSearch: true: the store gains a generated tsvector column over its text
    columns, a GIN index, and a ranked __search query that orders results by relevance.
    Stores that do not opt in keep the existing substring search unchanged.
  • Out-of-band organization invites. Invite a member by email with a single-use,
    expiring, organization-scoped invite token; the invitee redeems it to join (setting
    their own password for a new account, or authenticating as an existing one). This
    closes the account-existence signal the direct member-add response carried.
  • Read-only, path-jailed file source. A new fs_source capability gives handlers a
    deployer-configured, read-only reader over local files, contained by a symlink-safe
    path jail (no traversal or absolute-path escape).
  • Cron catch-up. A cron trigger can opt into missed-interval catch-up with
    catchUp: true: on startup the worker replays each interval it missed while it was
    down (bounded look-back). Default behaviour is unchanged (no catch-up).
  • Manual trigger firing. Fire a manual trigger on demand through an auth-guarded,
    rate-limited, tenant-scoped control route (POST /v1/triggers/:name/fire).
  • Live-executor readiness probe. A public GET /recovery-scope route reports the
    live durable-executor identity ({ executorId, applicationVersion }), failing closed
    (503) until the engine has finished launching.

Changed

  • Handler and extension-pack modules load compiled JavaScript in production. The
    production loader accepts only compiled .js modules (a deterministic, Node-version-
    independent boundary); a raw .ts module is refused fail-closed. The shipped example
    backends now include a build step, and the docs state the .js-only contract.
  • Durable cron exactly-once is hardened. The run-level exactly-once guard on the
    durable cron path is strengthened, with a test that asserts the durable invariant
    directly rather than counting raw invocations.
  • A first upload can no longer reset a sealed row. A conditional upsert closes the
    race where a first upload could reset an already-sealed row.

Fixed

  • rayspec plan fails on a boot-fatal document. A document whose stores cannot be
    derived now returns a non-ok plan verdict instead of reporting ok: true and crashing
    at boot.

Documentation

  • Tenant-table registration guidance corrected. The engine deploy() and the
    server composition root now describe the real mechanism — a product table joins the
    deny-by-default chokepoint set at boot through the sanctioned registration hook, and
    deploy() verifies rather than registers — replacing an out-of-date committed-source
    description.

v1.4.1

Choose a tag to compare

@iloveautomation iloveautomation released this 17 Jul 09:00
e80a0c8

Security

  • The per-tenant Anthropic credential directory is hardened further. The
    directory is now created in a single atomic step (create-or-validate, with no
    check-then-create window), the credential root's ownership and permissions are
    asserted at startup, and the tenant identifier is validated — an empty, absolute,
    separator-, traversal-, or NUL-bearing value is rejected — before it is ever used
    to build a path. This builds on the mode-0700 and containment checks from the
    previous release.
  • All static-analysis findings are resolved. The code-, path-, and label-parsing
    and file-I/O findings surfaced by static analysis are fixed, or dismissed with a
    documented rationale, leaving zero open alerts.
  • CI supply-chain integrity is tightened further. Container images used in CI
    are pinned by content digest, and repository secret-scanning with push-protection
    is enabled.

Changed

  • Clearer startup and developer diagnostics. Boot now emits a progress line
    before the ready banner and fails with an explicit timeout message if it stalls; a
    failed development-database connection reports its underlying cause; and a second
    local instance can run alongside the first through container, volume, and port
    overrides. The getting-started guide is polished to match.
  • Source comments and test descriptions are rewritten in self-carrying product
    language
    , and the repository check that keeps the shipped source product-neutral
    is stricter. These are non-functional text and tooling changes; runtime behaviour
    is unchanged.

Documentation

  • The v1 posture now states its honest edges. A new "what v1 does not do yet"
    section documents that request cancellation is bounded to the request rather than
    propagated into in-flight work, that the hard-delete purge is operator-gated and
    off by default, and that the federation and residency columns are shape-only with
    enforcement deferred to the separate hardening layer.

v1.4.0

Choose a tag to compare

@iloveautomation iloveautomation released this 16 Jul 13:44
deb55a7

Security

  • The local server now binds to loopback (127.0.0.1) by default. A freshly
    started instance no longer listens on all network interfaces; it is not reachable
    from the network until a host is explicitly configured, closing an
    accidental-exposure default.
  • Request bodies are size-bounded on every ingress path. Both the JSON and the
    audio-upload routes now reject an oversized payload before it is buffered, bounding
    the memory a single request can consume.
  • Rate-limit identity is derived from a trusted peer, and the limiter store is
    bounded.
    A spoofed client identifier can no longer evade the limit, and the
    limiter's memory footprint is capped so a flood of distinct identifiers cannot grow
    it without bound.
  • The session-reprocess affordance is now rate-limited and recorded, so repeated
    reprocessing of a session is bounded and observable.
  • An incoming x-request-id is constrained to a short, printable allow-list
    before it is echoed or logged, so an untrusted header value cannot inject control
    characters downstream.
  • A declared quote_field that carries no quote is now rejected under the
    unquoted-claim policy, rather than being silently accepted as an unquoted claim.
  • The per-tenant Anthropic credential directory is hardened against loose or hostile
    paths.
    It is created with mode 0700; a resolved path that is not a direct child
    of the configured root, an existing symlink or non-directory, or a group- or
    world-accessible directory is refused at startup (fail-closed), with containment
    re-checked against the real path. The adapter's interface and behaviour are
    otherwise unchanged.
  • Supply-chain integrity of the build is strengthened. CI actions are pinned to
    verified commit SHAs, the gitleaks download is verified against a pinned SHA-256,
    and a CodeQL static-analysis workflow now runs over the codebase.

Changed

  • The live provider parity smoke suites are now behind an explicit opt-in. They
    run only when RAYSPEC_REQUIRE_LIVE_TESTS=true, with the exercised backends selected
    via RAYSPEC_LIVE_BACKENDS; without the opt-in an ordinary test run never reaches a
    live provider or spends against a real credential.
  • Build and gate tooling resolve repository roots portably and fail closed on an
    empty scan, so a seam gate cannot pass vacuously.

Fixed

  • An authentication test asserting that a password is never leaked is now
    deterministic
    , removing a source of intermittent test failures.

Upgrade notes

  • Anthropic credential directory permissions — one-time action may be required.
    Anthropic credential directories are now created with mode 0700, and the adapter
    refuses to start when a credential directory is group- or world-accessible. If you
    upgrade an existing installation whose credential directory still exists with 0755
    (or any group/other permissions), run chmod 0700 on that directory once after
    upgrading — otherwise the Anthropic adapter will not start.

Minor hardening follow-ups are tracked for v1.4.1.

v1.3.3

Choose a tag to compare

@iloveautomation iloveautomation released this 16 Jul 02:25
e942c33

Added

  • Persist a validated agent output to a store (persistTo). An agent action —
    on both an api route and a trigger — may now declare persistTo: <store>. On a
    successful run the validated outputSchema output is written as one row into that
    store, exactly once, atomically with the run header's completing transition, across
    both the synchronous (in-request) and durable (off-request / recovery) execution
    paths. Safety is enforced at deploy, not runtime: the doctor validates the
    mapping in both directions and fails closed at boot on any mismatch — forward
    (every output property maps to a writable business column of a compatible type) and
    reverse (every NOT-NULL, no-default business column is reliably produced by a
    present, required, non-nullable output property; where a column and its mapped
    property both declare an enum, the property's enum must be a subset of the
    column's whitelist).
  • Declarative record-input normalization (input_normalize). The record_input
    capability accepts an optional input_normalize: { agent, output_contract } that
    runs a declared agent over a submitted record before it is persisted: the record is
    transformed, re-validated, then stored — the stored and emitted value is the
    normalized one. It runs synchronously through the neutral agent path; a failure is
    fail-closed (nothing is persisted) and never leaks raw provider or database text to
    the client. It is idempotent, keyed on the canonical payload hash, so a retry
    converges while a corrected resubmission re-normalizes. It is wired via a
    record/<agent>.normalizer.json config (path-jailed and validated); declaring it
    without a wired normalizer fails closed at deploy. A record capability without it is
    byte-identical to before.
  • Server-side substring search on list routes (?search= / ?<col>__contains=).
    The declarative list op gains an opt-in, additive, keyset-stable search:
    ?search=<term> is a case-insensitive OR match across the store's declared text
    columns, and ?<column>__contains=<term> matches one declared text column. User
    terms are bound parameters with the LIKE wildcards % and _ escaped (ESCAPE),
    so they match literally rather than as wildcards. Search folds into the same
    AND-chain as the equality and set filters and composes with ordering and the keyset
    cursor. search is a reserved query word — a store that declares a column named
    search fails lint.
  • created_by on escape-hatch handler inserts. A handler-managed store insert now
    stamps the injected created_by column from the authenticated caller
    (server-derived and un-spoofable), matching the declarative store create path. A
    posture with no request principal is unaffected.

Changed

  • Handler-facade input-validation rejections now return 400 (previously 500).
    An unknown column, a server-controlled column, an enum-whitelist violation, an
    injection attempt, an invalid timestamp, or a negative pagination value now surface
    as a typed 400 error rather than an internal 500. The client-facing message
    stays generic and no internal detail ever leaves the server.
  • A spec-vs-spec plan no longer emits phantom platform-column deltas. Running
    rayspec plan <spec> --against <copy> on two identical specs now produces no diff.
    The real-database injected-column reconcile stays reachable behind a new opt-in
    --reconcile-injected-columns flag (an update-mode flag that requires --against).
  • Opt-in run-journal payload scrub during tenant erasure. Passing
    journalScrub: true to a tenant erasure NULLs the raw journal payload columns while
    keeping the journal rows and their idempotency and cost columns intact — closing the
    content-erasure gap where a per-subject purge left raw payloads behind. The default
    behaviour is byte-identical (no scrub).

Fixed

  • A journaled error step-row no longer bricks an agent re-run. The run-journal
    writer now upserts a step on its unique key — replacing an error predecessor, but
    never overwriting a successful row — and reconciles the run header to the healed
    terminal outcome without ever downgrading an already-completed run. A re-run of a
    previously-failed step now succeeds, and both run observability and the
    double-charge guard see the true result.

Documentation

  • Documented persistTo (agent output persistence) and input_normalize (record-input
    normalization) in the spec reference and the authoring skill, and the new server-side
    substring search (?search= / ?<column>__contains=) under the list-route query
    surface — correcting the earlier "no LIKE/full-text operators" note.

v1.3.2

Choose a tag to compare

@iloveautomation iloveautomation released this 14 Jul 10:09
3f114dd

[1.3.2] - 2026-07-14

Added

  • Opt-in readonly route handlers. A { kind: route } handler may now declare
    readonly: true. A handler-kind route is gated on the sensitive store:write
    permission by default (the platform cannot statically prove a handler only reads, so
    it fail-closes to the stronger gate); readonly: true is the author's assertion that
    the handler only reads product stores, so its route is gated on store:read instead
    — letting a read-scoped credential (for example an ingest-only API key) reach a
    read-only route. It is an authorization gate / author assertion, not a runtime
    write restriction. An absent or false flag parses byte-identically to before, so
    every existing spec, fixture, and golden is unchanged.
  • A tenant-scoped session reprocess endpoint. POST /v1/sessions/{id}/reprocess
    (store:write, strictly tenant-scoped) re-drives a session's declared
    finalized-session workflow as a fresh durable run under a distinct idempotency
    key
    — the operational recovery path for re-running extraction after a fix or
    unsticking a stuck session, without manual database surgery (simply re-emitting the
    finalized event deduplicates to the original run and does nothing). It is wired for
    audio products; a deployment with no reprocessor wired answers 501, and a foreign
    or absent session id returns 404 with zero enqueue.
  • Opt-in reuse of a machine claude login for the Anthropic backend. Setting
    RAYSPEC_ANTHROPIC_REUSE_LOGIN=true lets the Anthropic subscription backend boot
    with no CLAUDE_CODE_OAUTH_TOKEN/ANTHROPIC_API_KEY in the server environment,
    reusing a claude login the operator has seeded into the per-tenant config directory
    under RAYSPEC_ANTHROPIC_CONFIG_ROOT (still required). A loud boot banner announces
    the mode. Honest caveats: the boot cannot verify any per-tenant directory is actually
    seeded, so an unseeded tenant boots clean and fails only at first run; seeding the
    login is a manual operator step; and if a token or key is also present in the
    environment it wins over the seeded login (the boot warns). Without the flag, boot
    behaviour is byte-identical (fail-closed when no credential is present). Documented in
    docs/concepts.md and .env.example.

Fixed

  • Store enum whitelists are now enforced on the low-level escape-hatch handler
    write path too.
    A text column's declared enum whitelist was already enforced on
    the HTTP create/update route and the workflow store.write value path, but a
    custom handler writing directly through the HandlerDb facade was not checked. It is
    now rejected fail-closed against a table-identity whitelist registry (a non-member
    value — including a non-string scalar — is refused; the failure names the store and
    column only, never the offending value), so all three write surfaces agree. This
    closes the "the facade is not enum-checked" residual noted in 1.3.1.
  • Every sealed track of a multi-track audio session is transcribed. The
    session-finalized event fires as soon as one track seals, but a sibling track could
    still be uploading at that instant; the transcribe node re-read only the completed
    tracks and finished, permanently dropping any track that sealed afterward. The
    durable transcribe node now waits (bounded, with real retry backoff) for all tracks
    to seal before transcribing, so every sealed track is transcribed under a staggered
    or concurrent finalize. The finalize emit stays unconditional, so a session-scoped
    idempotency key still deduplicates to exactly one durable run (never zero). Honest
    bound: once the wait elapses the run proceeds with whatever sealed and logs loudly, so
    an abandoned upload can never stall the run forever.
  • The migration generator emits foreign keys after the tables they reference. A
    store that referenced a later-declared store emitted its REFERENCES <parent> before
    that parent's CREATE TABLE, failing at apply (42P01 relation does not exist) while
    doctor/plan still reported ok. A stable topological sort now orders every
    CREATE TABLE ahead of the foreign keys that reference it (an already-ordered spec
    stays byte-identical, so committed goldens are unchanged). A genuine foreign-key
    cycle is now a blocking fk_cycle error at doctor/plan time (rather than a
    throw at apply); a merely out-of-order forward reference is a non-blocking
    fk_forward_reference advisory.
  • An unsatisfiable Range on a static frontend mount now returns 416. The
    underlying static server mishandled an unsatisfiable byte range — a closed range
    beyond end-of-file yielded a malformed 0-byte 206, and an open one surfaced as a
    500. An additive guard now returns RFC-7233 416 with
    Content-Range: bytes */<size> for an unsatisfiable range (a start at/after EOF —
    open or closed — or a reversed range), under GET and every write verb. HEAD/
    OPTIONS stay 200 full-size (never 416), every satisfiable/clamped 206 is
    unchanged, and the fail-closed dotfile/traversal/symlink guard still returns 404
    under a Range request. This corrects the 1.3.1 note that said an unsatisfiable
    range returns 500.
  • API-key minting is exactly-once under a concurrent Idempotency-Key. The mint
    applied idempotency as a non-atomic find-then-act, so two concurrent requests with the
    same key could each mint a distinct usable key with the loser's key left stranded
    (usable but never replayable). The mint is now retrofitted onto the atomic
    reserve-before-execute primitive: a concurrent loser replays the winner's redacted
    mint metadata (200, plaintext omitted — a caller that lost the original 201 must
    mint a new key) or gets a 409 while the mint is in progress, and exactly one key is
    ever minted. The
    no-idempotency-key path is behaviourally unchanged, and the plaintext secret is still
    never stored (the kill-trigger closure is preserved). Honest residual (documented in
    code): exactly-once except a rare ambiguous mint-commit window.
  • A user-dismissed collection row is preserved across a rebuild. The collections
    materializer re-stamped dismissed: false on every rebuild, so re-extracting (or a
    reprocess) would resurrect a user-dismissed artifact. A dismissed row is now spared
    unconditionally — reconciliation never deletes it and the upsert loop skips it —
    mirroring the existing human-edit preservation, independent of preserve_human_edits.
  • An extension-pack agent that selects a backend no base agent uses now boots. The
    env-driven backend factory derived its backend set from the pre-merge base document,
    so a backend introduced only by a pack agent was never built and the boot failed
    closed on it — including a backend spec whose only agents come from a pack (zero base
    agents:). The composition root now builds any backend a merged agent selects (via the
    same fail-closed path), while a base-only deploy stays byte-identical.

Security

  • The API error envelope strips details structurally for non-input-echo codes.
    The "a bare 401/404 leaks no details" invariant moved from a per-call-site
    convention to a structural guard at the single envelope chokepoint every non-2xx
    response flows through: an allowlist keeps details only for the codes whose details
    echo caller-supplied context (VALIDATION_ERROR, FORBIDDEN, RATE_LIMITED,
    GATEWAY_TIMEOUT) and drops it for every other code regardless of what a caller
    passes. Behaviour-preserving — no code outside the allowlist carries a details
    payload today, so no current response changes — but the guarantee is now enforced by
    construction rather than by convention.

Documentation

  • Updated the spec reference, concepts, and the authoring skill for the readonly
    route-handler flag, the session reprocess endpoint, the all-three-surfaces enum
    enforcement (the 1.3.1 handler-facade residual is closed), the static-mount 416
    correction (superseding the 1.3.1 "returns 500" note), and the
    RAYSPEC_ANTHROPIC_REUSE_LOGIN reuse-login option.

v1.3.1

Choose a tag to compare

@iloveautomation iloveautomation released this 13 Jul 18:12
757fece

Added

  • Opt-in soft delete for a store. A store may now declare softDelete: true.
    When it does, a delete stamps the injected deleted_at tombstone (through the
    tenant-scoped update chokepoint) instead of physically removing the row, and every
    read/write hides tombstoned rows — so a soft-deleted row is uniformly invisible:
    get404, list omits it, a second delete404, update/PATCH
    404. Tombstone-hiding is enforced on the richer read/write surface too
    (declarative views, workflow store_read/store_write, and tool/route/trigger
    handlers), not just the CRUD routes. Without the field the default is unchanged —
    a delete is a hard physical delete with no deleted_at filtering. Documented
    caveat: because a tombstoned row physically persists (holding its column values),
    a unique value from a soft-deleted row still occupies the tenant-scoped unique
    index, so re-creating that same value returns 409 CONFLICT rather than reusing it.
  • Server-enforced enum whitelists on a text column. A text column may declare
    an enum list of allowed values, and the platform now enforces it server-side: an
    out-of-whitelist value on a create/update store route is a 400 VALIDATION_ERROR
    (a z.enum derived at the write chokepoint), and the same whitelist is enforced on
    the workflow store.write value path. enum is valid only on a text column and
    its members must be distinct (rejected at validation otherwise). Honest residual: a
    custom escape-hatch handler that writes directly through the HandlerDb facade is
    not enum-checked — a handler author owns its own value discipline.
  • Foreign keys to a unique parent column (referencesColumn). A store
    foreign key may set referencesColumn to target a unique: true column of the
    parent store instead of its injected id. It materializes as a tenant-scoped
    compound
    foreign key — (tenant_id, <col>) REFERENCES parent(tenant_id, <refcol>)
    — which structurally forbids a cross-tenant reference. A create/update naming a
    non-existent parent value returns 400; a restrict-blocked parent delete returns
    409 (both tenant-safe — the 400 names only the local column, the 409 names no
    relationship at all, never a foreign value). The
    local column's type must match the referenced column's, the referenced column must
    be unique: true, and onDelete: 'set null' is rejected (a compound FK cannot null
    tenant_id). The id-target FK path is unchanged.
  • A set (IN) filter on the declarative list op. A list route now accepts a
    per-column set filter ?<col>__in=v1,v2,… that maps to SQL IN, so a "status is
    open OR in_progress" read is expressible in one query. The distinct __in suffix
    keeps plain ?<col>=v equality byte-identical and unambiguous on a comma-bearing
    value (a real column literally named <x>__in still routes as plain equality). It
    folds into the same AND-chain as equality filters, keyset pagination, and the tenant
    predicate. Fail-closed: an empty/blank element, an oversized set (> 100 values), a
    non-filterable (jsonb) column, or an unknown prefix column each return 400.
  • rayspec deploy --apply-migration <delta.sql>. deploy can now apply a reviewed
    forward migration in place, reaching the existing gated migration engine — an
    operator with a brownfield schema change no longer has to drop to the dev harness.
    --allowlist <file.json> supplies the reviewed cover for a destructive statement (a
    destructive statement without a covering entry is still blocked by the deploy gate);
    both paths are jailed through the same path check as the spec. It is reboot-safe:
    the boot classifies the live schema first and mounts a present-matching schema
    instead of re-applying a non-idempotent delta, so leaving the flag in a
    process-managed unit applies once and mounts thereafter. --dry-run rejects the flag
    (it touches no database), and a bare --allowlist (without --apply-migration) is
    refused. Reachable from both profiles.

Fixed

  • An agent-free spec boots and updates with no provider key. The local dev-boot
    wrapper hard-required OPENAI_API_KEY up front (an unconditional check before the
    spec was parsed, plus an always-on OpenAI factory), so applying an additive delta to
    an agent-free spec failed closed on a credential it never uses. The wrapper now
    routes through the shipped assembleOptsFromEnv, which returns an agent-backends
    factory only when the spec declares at least one agent — so a stores/api-only backend
    (or a product-profile document) boots and updates with no provider key, while an
    agent-bearing spec still fail-closes naming the missing per-agent credential.

Documentation

  • Corrected the "deploy applies the migration" overstatement to the mount-only
    truth.
    rayspec deploy/rayspec-serve materializes a store on a clean database
    and mounts a present-matching one, but against an existing deployment it is
    mount-only: it fail-closes on a drifted schema rather than altering it on its
    own. A schema change is applied by the explicit rayspec deploy --apply-migration <delta.sql> (with --allowlist for a reviewed destructive statement). The
    diff/gate and from-clean-database guarantees are unchanged. Corrected across
    getting-started, the CLI reference, concepts, ARCHITECTURE, and the README.
  • New "Restore and key rotation" operational note (ARCHITECTURE → security model):
    a restored database dump survives whole at the row level — orgs, users, the argon2id
    password hashes, and all tenant data come back reachable. The only thing a
    freshly-minted RAYSPEC_API_KEY_PEPPER breaks is the set of copied API keys:
    their stored HMACs no longer match, so they return 401 — mint new ones. User
    passwords are argon2id (pepper-independent), so an org owner just logs in again and a
    fresh JWT under the current signing key reaches the data. (The JWT signing key is the
    same class and self-heals on that same re-login; an org whose sole credential was an
    API key needs a fresh key established out of band.)
  • Documented the new store features in the spec reference (enum, softDelete,
    referencesColumn, and the <col>__in set filter), and pinned Range and HEAD on
    a static frontend mount
    as a supported feature with tests (byte-range 206 /
    HEAD 200). Honest edge: an unsatisfiable range currently returns 500 — the
    underlying static server (@hono/node-server serveStatic) has no RFC-7233 416
    path.