Releases: rayspec-labs/rayspec
Release list
RaySpec v1.6.2
Added
- Static frontends can ship a custom
404.html. When a request to a mounted
static frontend misses (no file, nodir/index.html, and no SPA fallback) and
the mount's root contains a404.html, that file is served with HTTP status 404
andContent-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 anspa:true
mount the SPAindex.htmlfallback 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>_FILEmount 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 becauseRAYSPEC_API_KEY_PEPPERis the HMAC key for api-key, session, and
invite hashes. A deployment whose plainRAYSPEC_API_KEY_PEPPERcurrently 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_stepsgains acreated_atindex. Additive migration0009adds a
btree index (journal_steps_created_at_idx) onjournal_steps(created_at), so
time-range and day-bucket scans over the step journal are index-backed instead of
sequential — paralleling the existingruns_created_at_idxon the run header. The
change is non-destructive;drizzle-kit migratebuilds the index on the existing
table (a plain, non-CONCURRENTLYbuild that briefly locks writes for its duration).
Security
- Transitive
postcsspinned to 8.5.18 (GHSA-r28c-9q8g-f849). A pnpm override
raises the transitivepostcss— pulled only by thevite/vitestdevelopment
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
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
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>_FILEvariant
(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 (mode600) 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>_FILEwins outright (the plain
variable is not consulted); a blank<VAR>_FILEcounts as not set (the plain
variable is used); and a non-blank<VAR>_FILEpointing 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 afrontend(empty
stores,api,agents,tooling,triggers,handlers, and
extensions, and no durable worker) now boots with noDATABASE_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).
/healthis liveness-only (200 {"status":"ok"}, no database probe). The two
response security headers a reverse proxy would otherwise supply —
Content-Security-PolicyandPermissions-Policy— are read from the
environment (RAYSPEC_FRONTEND_CSPandRAYSPEC_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 staticfrontendalongside
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 aninstructions
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 pinnedinstructions_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
designatedinstructionsfield, and everywhere else — includingpurpose,
extraction_constraints, and the still-bannedprompt/system_promptkeys —
the guardrail stays fail-closed.
Changed
rayspec plan's read-only shadow guard resolves its target from
DATABASE_URL_FILEtoo. The guard that refuses to shadow-apply when
SHADOW_DATABASE_URLresolves to the same host and database as the real
DATABASE_URLnow resolves that comparison target from aDATABASE_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. Becauseplan
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
plainDATABASE_URL.
Security
- Dependency advisories patched. Six advisories are resolved by upgrade
(acrosshono,brace-expansion,fast-uri, andprotobufjs— direct
dependencies and their transitive copies), and the dependency SBOM is
refreshed. One remaining advisory — a Windows-only@hono/node-server
serve-staticpath 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
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
Added
- RaySpec is installable from npm.
npx rayspec initscaffolds a new project (a
minimal, valid backend spec you canrayspec planand deploy without provider
credentials), andnpm i -g rayspecputs therayspeccommand on your PATH — no
clone-and-build required. The scoped@rayspec/*packages are published alongside the
unscopedrayspeclauncher. - Declarative full-text search. A store can opt into Postgres full-text search with
fullTextSearch: true: the store gains a generatedtsvectorcolumn over its text
columns, a GIN index, and a ranked__searchquery 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_sourcecapability 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-scoperoute 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.jsmodules (a deterministic, Node-version-
independent boundary); a raw.tsmodule 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 planfails on a boot-fatal document. A document whose stores cannot be
derived now returns a non-ok plan verdict instead of reportingok: trueand 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
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-0700and 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
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-idis 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_fieldthat 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 mode0700; 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, thegitleaksdownload 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 whenRAYSPEC_REQUIRE_LIVE_TESTS=true, with the exercised backends selected
viaRAYSPEC_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 mode0700, 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 with0755
(or any group/other permissions), runchmod 0700on 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
Added
- Persist a validated agent output to a store (
persistTo). An agent action —
on both an api route and a trigger — may now declarepersistTo: <store>. On a
successful run the validatedoutputSchemaoutput 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 anenum, the property's enum must be a subset of the
column's whitelist). - Declarative record-input normalization (
input_normalize). Therecord_input
capability accepts an optionalinput_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.jsonconfig (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
listroutes (?search=/?<col>__contains=).
The declarativelistop gains an opt-in, additive, keyset-stable search:
?search=<term>is a case-insensitiveORmatch across the store's declared text
columns, and?<column>__contains=<term>matches one declared text column. User
terms are bound parameters with theLIKEwildcards%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.searchis a reserved query word — a store that declares a column named
searchfails lint. created_byon escape-hatch handler inserts. A handler-managed store insert now
stamps the injectedcreated_bycolumn from the authenticated caller
(server-derived and un-spoofable), matching the declarativestorecreate path. A
posture with no request principal is unaffected.
Changed
- Handler-facade input-validation rejections now return
400(previously500).
An unknown column, a server-controlled column, anenum-whitelist violation, an
injection attempt, an invalid timestamp, or a negative pagination value now surface
as a typed400error rather than an internal500. 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-columnsflag (an update-mode flag that requires--against). - Opt-in run-journal payload scrub during tenant erasure. Passing
journalScrub: trueto 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 anerrorpredecessor, 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) andinput_normalize(record-input
normalization) in the spec reference and the authoring skill, and the new server-side
substring search (?search=/?<column>__contains=) under thelist-route query
surface — correcting the earlier "noLIKE/full-text operators" note.
v1.3.2
[1.3.2] - 2026-07-14
Added
- Opt-in
readonlyroute handlers. A{ kind: route }handler may now declare
readonly: true. Ahandler-kind route is gated on the sensitivestore:write
permission by default (the platform cannot statically prove a handler only reads, so
it fail-closes to the stronger gate);readonly: trueis the author's assertion that
the handler only reads product stores, so its route is gated onstore:readinstead
— 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 orfalseflag 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 answers501, and a foreign
or absent session id returns404with zero enqueue. - Opt-in reuse of a machine
claudelogin for the Anthropic backend. Setting
RAYSPEC_ANTHROPIC_REUSE_LOGIN=truelets the Anthropic subscription backend boot
with noCLAUDE_CODE_OAUTH_TOKEN/ANTHROPIC_API_KEYin the server environment,
reusing aclaudelogin the operator has seeded into the per-tenant config directory
underRAYSPEC_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.mdand.env.example.
Fixed
- Store
enumwhitelists are now enforced on the low-level escape-hatch handler
write path too. Atextcolumn's declaredenumwhitelist was already enforced on
the HTTPcreate/updateroute and the workflowstore.writevalue path, but a
custom handler writing directly through theHandlerDbfacade 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 in1.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 itsREFERENCES <parent>before
that parent'sCREATE TABLE, failing at apply (42P01 relation does not exist) while
doctor/planstill reported ok. A stable topological sort now orders every
CREATE TABLEahead 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 blockingfk_cycleerror atdoctor/plantime (rather than a
throw at apply); a merely out-of-order forward reference is a non-blocking
fk_forward_referenceadvisory. - An unsatisfiable
Rangeon a staticfrontendmount now returns416. The
underlying static server mishandled an unsatisfiable byte range — a closed range
beyond end-of-file yielded a malformed 0-byte206, and an open one surfaced as a
500. An additive guard now returns RFC-7233416with
Content-Range: bytes */<size>for an unsatisfiable range (a start at/after EOF —
open or closed — or a reversed range), underGETand every write verb.HEAD/
OPTIONSstay200full-size (never416), every satisfiable/clamped206is
unchanged, and the fail-closed dotfile/traversal/symlink guard still returns404
under aRangerequest. This corrects the1.3.1note that said an unsatisfiable
range returns500. - 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 original201must
mint a new key) or gets a409while 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-stampeddismissed: falseon 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 ofpreserve_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
detailsstructurally for non-input-echo codes.
The "a bare401/404leaks 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 keepsdetailsonly 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 adetails
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-surfacesenum
enforcement (the1.3.1handler-facade residual is closed), the static-mount416
correction (superseding the1.3.1"returns500" note), and the
RAYSPEC_ANTHROPIC_REUSE_LOGINreuse-login option.
v1.3.1
Added
- Opt-in soft delete for a store. A store may now declare
softDelete: true.
When it does, adeletestamps the injecteddeleted_attombstone (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:
get→404,listomits it, a seconddelete→404,update/PATCH→
404. Tombstone-hiding is enforced on the richer read/write surface too
(declarative views, workflowstore_read/store_write, and tool/route/trigger
handlers), not just the CRUD routes. Without the field the default is unchanged —
adeleteis a hard physical delete with nodeleted_atfiltering. Documented
caveat: because a tombstoned row physically persists (holding its column values),
auniquevalue from a soft-deleted row still occupies the tenant-scoped unique
index, so re-creating that same value returns409 CONFLICTrather than reusing it. - Server-enforced
enumwhitelists on a text column. Atextcolumn may declare
anenumlist of allowed values, and the platform now enforces it server-side: an
out-of-whitelist value on acreate/updatestore route is a400 VALIDATION_ERROR
(az.enumderived at the write chokepoint), and the same whitelist is enforced on
the workflowstore.writevalue path.enumis valid only on atextcolumn and
its members must be distinct (rejected at validation otherwise). Honest residual: a
custom escape-hatch handler that writes directly through theHandlerDbfacade is
not enum-checked — a handler author owns its own value discipline. - Foreign keys to a
uniqueparent column (referencesColumn). A store
foreign key may setreferencesColumnto target aunique: truecolumn of the
parent store instead of its injectedid. It materializes as a tenant-scoped
compound foreign key —(tenant_id, <col>) REFERENCES parent(tenant_id, <refcol>)
— which structurally forbids a cross-tenant reference. Acreate/updatenaming a
non-existent parent value returns400; arestrict-blocked parent delete returns
409(both tenant-safe — the400names only the local column, the409names no
relationship at all, never a foreign value). The
local column's type must match the referenced column's, the referenced column must
beunique: true, andonDelete: 'set null'is rejected (a compound FK cannot null
tenant_id). The id-target FK path is unchanged. - A set (
IN) filter on the declarativelistop. Alistroute now accepts a
per-column set filter?<col>__in=v1,v2,…that maps to SQLIN, so a "status is
open OR in_progress" read is expressible in one query. The distinct__insuffix
keeps plain?<col>=vequality byte-identical and unambiguous on a comma-bearing
value (a real column literally named<x>__instill 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 return400. rayspec deploy --apply-migration <delta.sql>.deploycan 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-runrejects 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-requiredOPENAI_API_KEYup 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 shippedassembleOptsFromEnv, 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-servematerializes 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 explicitrayspec deploy --apply-migration <delta.sql>(with--allowlistfor 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-mintedRAYSPEC_API_KEY_PEPPERbreaks is the set of copied API keys:
their stored HMACs no longer match, so they return401— 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>__inset filter), and pinned Range and HEAD on
a staticfrontendmount as a supported feature with tests (byte-range206/
HEAD200). Honest edge: an unsatisfiable range currently returns500— the
underlying static server (@hono/node-serverserveStatic) has no RFC-7233416
path.