Lasagna-010726/seam-integration - #14
Merged
Arcoders merged 33 commits intoJul 1, 2026
Merged
Conversation
… (SEAM-1) Add a required tableLocation(tenant) to IsolationDriver returning a closed tagged union (schema | database | rowscope | connection), so an extension places per-tenant tables without hardcoding a tenant_<id> namespace and without branching on the driver. Implement it on all four shipped drivers, bump ISOLATION_CONTRACT_VERSION 1 -> 2 (the internal isolation-driver contract, not the package semver), and pair the bump with an UNCONDITIONAL registry presence gate that throws for any driver missing the method (drivers at contractVersion 1 and unversioned drivers included), so the requirement surfaces at boot rather than at first call. Also add the optional MigrateOptions.extraMigrationPaths? field (reserved for the per-tenant satellite-migration wiring, riding the v2 bump) and export the TableLocation types from the root entry, /services, and the isolation barrel. Update the custom-isolation-driver cookbook, regenerate the api-extractor golden, and record the seam in the curated [1.0.0] CHANGELOG. Red-first: isolation_table_location.spec.ts covers each driver's union, rowscope carrying no schema/rlsGuc key, fail-closed identifiers, an exhaustive never-default consumer, and the presence gate in four forms. Contract note (pre-1.0, no release yet): a custom IsolationDriver implements tableLocation() and sets contractVersion: 2. The four shipped drivers are updated; hosts using only the shipped drivers need no action.
…AM-3) Add reserve()/settle()/release() to QuotaService so an operation whose true cost is only known when it finishes (a streaming model response) can hold its worst-case cost up front and reconcile the actual usage as it arrives. Backs the AI-satellite streaming seam and is useful standalone for any metered streaming. The model is "derive, never store": there is NO scalar reserved-counter. Each live hold is a member of a per-(tenant,day,quota) sorted set scored by absolute expiry, with amounts in a companion hash; the outstanding reserved sum is recomputed from the live members on every reserve, and each reserve first evicts score<now members. That eviction is the reaper, so a process that crashes between reserve and settle has its budget reclaimed by Redis TTL alone, with no timer, no scheduler, and no counter left inflated. QuotaService stays stateless. - reserve(tenant, quota, worstCase): atomic single-EVAL hold against BOTH the per-tenant daily budget AND an optional operator-global ceiling (plans.operatorCeiling[quota], a denial-of-wallet cap), both-or-neither. Over-budget is a hard stop. FAIL-CLOSED BY CONSTRUCTION (not configurable): a Redis outage refuses rather than let an unbudgeted call through, the deliberate opposite of consume()'s fail-open. - settle(reservation, cumulativeUsed): monotonic-cumulative, clamped to [0, worstCase]; a repeated or smaller total is a no-op. Fail-open. - release(reservation): returns the unused remainder, idempotent under a double abort. Fail-open. New public type QuotaReservation (exported from the root, /services, and the services barrel). New config plans.operatorCeiling + plans.reservationTtlMs (additive, no contract bump; consume() is unchanged). A quota is metered by consume XOR reserve. Designed and adversarially reviewed via multi-agent passes. The review caught a real money bug the first cut missed: the operator holds/amt are shared across tenants, so one tenant's reserve reaps another's expired op member while the latter's own tenant hold survives, and the old settle then charged the shared op counter for a reaped hold, inflating the ceiling permanently. Fixed by gating each side's write on its own live member (settle) and giving the op cleanup its own guard (release). Also hardened: exclusive reap boundary (no self-reap at a reservationTtlMs of 0), non-finite settle coerced to 0 (no phantom-outage cost dodge), and malformed-record-tolerant parsing. Unit specs cover the TS orchestration and the fail-closed policy (with a mutation check proving the spec bites). Real-Redis integration specs cover single-EVAL atomicity, the over-budget hard stop, the both-or-neither ceiling, clamp and monotonic settle, idempotent release, orphan reclaim by TTL, and the two shared-ceiling regressions; the Lua was also validated against a live Redis. Targets standalone/Sentinel Redis; Cluster (hash-tags plus a second-EVAL ceiling) is a deferred follow-up.
Add operator tooling to install the PostgreSQL `vector` extension outside the app's request-serving role, so the app role stays least-privilege and never runs CREATE EXTENSION. Backs the AI-satellite vector store and is useful standalone for any pgvector host. - provisionVectorExtension(): CREATE EXTENSION IF NOT EXISTS vector under a privileged provisioning connection (isolation.provisionConnectionName, default centralConnectionName), dispatched by driver: once on the shared database for schema-pg/rowscope-pg, per tenant database for database-pg (cloning the privileged connection onto each), skipped for sqlite-memory and unknown drivers. Idempotent; doubles as the backfill for existing databases. - tenant:vector:provision --dry-run --tenant: the operator/backfill command. - pgvector_extension doctor check (opt-in): asserts the app role is not a superuser and the extension is present where embeddings live. - config isolation.provisionConnectionName; CI Postgres image -> pgvector; a docs/reference/commands.md section; the curated [1.0.0] CHANGELOG. Dispatched by driver.name (not tableLocation), so this PR is independent of the SEAM-1 branch. The database-pg tenant-creation hook and its "new tenant after backfill" ordering test are deferred to the SEAM-2 PR, where an embeddings migration finally exists to order against and to test. Additive, no contract bump. Targets standalone Postgres. Adversarially reviewed; the review hardened five real gaps. The backfill loop and the doctor check now isolate per-tenant failures (one unreachable tenant database no longer aborts the run or the whole check) and report a failed count; a warning fires when provisioning falls back to the app's central connection; unknown or custom drivers are skipped rather than assumed central; the database-pg driver's databaseName()/connect() are contract-guarded; and each throwaway provisioning connection gets a unique per-invocation name so concurrent runs cannot close each other's pool. Unit specs cover the dispatch, dry-run, per-tenant failure isolation, and the unknown-driver plus missing-contract paths; a real-Postgres integration spec self-skips when the image lacks pgvector.
… contract
pinnedFetch's custom `lookup` returned a single (address, family). Since Node 20,
autoSelectFamily (default true) invokes a custom lookup with `{ all: true }` and
requires an array result `[{ address, family }]`; a single-address callback there
throws ERR_INVALID_IP_ADDRESS at connect. On Node 24 (the package's engines floor)
this broke EVERY pinned outbound request — webhook delivery and OIDC token/discovery
— and the failure was wrapped as a retryable `network_error`, so it read as flaky
networking rather than a hard bug.
It went unnoticed because the completing pinned path was never exercised in tests:
the pinning specs only cover pinned mode REFUSING a private/loopback target (which
fails closed at validation, before the socket) and reaching a listener via
allowLoopback (plain fetch, not pinnedFetch). Pinned mode blocks every private
range by design, so the httpsRequest + custom lookup only ever runs against a
public address.
Extract the pin into `pinnedLookup(address, family)` and return the pinned address
in whichever shape the caller asked for. It is the same single validated address
either way, so the DNS-rebinding pin is unchanged. Add a deterministic unit
regression on the { all } contract and a fault spec that drives the real https
connect path under default Happy-Eyeballs.
Verified end to end: safeFetch('https://example.com/') now returns 200 on Node
24.15.0 where it previously threw "Invalid IP address: undefined".
…n (SEAM-4 core)
executeExtension gains an optional `signal`. It is composed with the internal
timeout controller BEFORE the no-timeout fast-path return, so an external abort
(a caller timeout, a tenant suspension, a client disconnect) reaches `fn` even
when no timeoutMs is set. Previously the fast path returned a promise bound only
to the internal controller, silently dropping every external abort; with a
timeout set, the deadline still wins its race deterministically. Callers that
pass no signal are byte-identical (fn still receives a live, non-aborted signal).
New `utils/signals.ts`, exposed at the `/signals` subpath:
- composeSignals(): the single AbortSignal.any composition point in the kernel,
now reused by safeFetch's combinedSignal so the two cannot drift.
- onRequestDisconnect(ctx): the ONLY sanctioned place that touches the raw
inbound request (ctx.request.request). It bridges a client hangup to an
AbortSignal, aborting only while the response has not finished (a
writableEnded latch, so a clean end-of-response 'close' does not false-abort),
and disposes its listener so nothing leaks. An @architecture guard
(inbound_request_seam.spec.ts) fences the raw-request access to this helper.
This is the core lever the AI streaming gateway (SEAM-4 satellite) builds on:
the composed signal cancels the provider call, onRequestDisconnect feeds it a
disconnect trigger. Additive, no contract bump; the three existing executeExtension
callers pass no signal.
# Conflicts: # packages/core/CHANGELOG.md
The isolation override `{ rowScopeRls: true }` omitted `driver`, which
`IsolationConfig` requires. It was latent on the SEAM-1 branch (surfaced only
when the full test tree typechecks together on the integration branch). Match the
established pattern (isolation_scoping.spec.ts) and name the rowscope-pg driver.
…ths (SEAM-2) A satellite can now ship migrations that run PER TENANT (embeddings, per-tenant memory) rather than only once in the shared backoffice schema. - The three real-migrate drivers (schema-pg, database-pg, sqlite-memory) delegate migrate() to a shared runTenantMigrations() that folds MigrateOptions .extraMigrationPaths into the run. Lucid's Migrator reads its source directories from the CONNECTION's migrations.paths (not from its options), so the helper clones the already-registered tenant connection into a throwaway registration whose migrations.paths is [...base, ...extra], runs the Migrator against it, and releases it. The shared connection is never mutated; the ledger and advisory lock live in the same physical database, so a fold is idempotent and the no-extra-paths path is byte-identical to the old inline code. rowscope-pg stays a no-op (row-scoped tenants use a central migration). - SatelliteManifest gains perTenantMigrations: a directory of RUNNABLE per-tenant migration files inside the package, validated by the same isSafeRelativePath as migrations. It runs straight from node_modules, with no host copy step. - tenant:migrate discovers installed satellites (manifests read as JSON, no satellite code imported) and folds each perTenantMigrations dir into every tenant's run. satelliteMigrationDirs() emits the dirs RELATIVE to the app root and forward-slashed: Lucid resolves each via new URL(dir, appRoot), which an absolute path breaks on Windows (the drive letter parses as a URL scheme), so a root-relative path is correct and ledger-stable on every OS. The database-pg tenant-creation pgvector hook (deferred here from SEAM-5) lands with the AI satellite's embeddings migration, where its ordering test has a real migration to run against. Additive: MigrateOptions.extraMigrationPaths rides the isolation contract v2 bump SEAM-1 already paid; no new break.
ci.yml only triggers on push/PR to master|main, so the merged seam work on this branch never gets the real-PG/Redis integration, billing, and e2e tiers. Add this branch to the push filter so the whole suite runs here for validation. Revert before merging into isolation-hardening.
The full ci.yml suite ran green on this branch (run 28525464822: real PG/Redis integration, billing, e2e, and the compose deploy stack). Remove the temporary branch entry so ci.yml is back to master|main only and the branch is clean to merge into isolation-hardening.
Re-add this branch to the push filter so the whole ci.yml suite (real PG/Redis integration, billing, e2e, compose deploy) runs here again for validation ahead of PR-6. Revert before merging into isolation-hardening.
…ruction A QuotaReservation handle is server-only in-process state, but it is a plain object a caller could forge or mutate. The safety against that is structural: settle()/release() key every Redis op by the handle's own (tenantId, day, quota) via the *Key builders, and address the hold by id, so a handle with a swapped tenantId lands in a different namespace, misses the hold, and charges nothing. Promote that implicit property into an enforced, tested invariant: - security_quota_handle_tamper.spec.ts (real Redis): forged tenantId / holdId / quota / day are all harmless no-ops; a control case proves the assertions are not vacuous. Tagged @ai (cross-cutting slice, runs via --tags @ai). - check-quota-key-tenant-scoped.mjs: fails if any file outside the key-builder module hand-builds a quota: key, so a refactor cannot drop the tenantId. Ships a --self-test and is wired into npm run check. - keys.ts: add tenantKeyPattern() so reset()'s wildcard also comes from a builder, keeping the "every key from a builder" invariant absolute. - Introduce the @ai Japa tag (AI_TAG constant) for the cross-cutting AI slice.
…vision Three fault-injection specs (non-gating tier) covering the new SEAM surfaces against real Postgres + Redis, all @ai-tagged: - redis_outage_during_reserve: the reserve EVAL is made to reject with a real ECONNRESET through the requireRedis() seam; the real ResilienceService classifies it and reserve fails closed (DependencyUnavailableException) leaving zero holds, then recovers cleanly once Redis is healthy. Complements the resilience-tier policy unit test with real-infra classification + state proof. - ceiling_flood_concurrent: a burst of reserves against a shared operator ceiling admits exactly floor(ceiling/worstCase) and never over-commits; a crashed winner's hold is reclaimed by TTL so a latecomer reserves again under contention. - migrate_during_vector_provision: running tenant:vector:provision and migrate concurrently corrupts neither the extension nor the adonis_schema ledger — disjoint objects, no advisory lock needed. Self-skips without pgvector. Document the ordering-not-locking rule in the commands reference.
reserve/settle/release and executeExtension routed errors through ResilienceService but emitted no telemetry of their own, so an operator could see "Redis degraded" but not "tenant over budget" or "ceiling 90% utilised". Instrument the existing primitives — no prom-client, no new infra — matched to call frequency: - TelemetryService gains addEvent/addLink/addEventOnActive helpers over the OTel Span API the withSpan wrapper hid. - reserve (once/stream) opens a quota.reserve span with hold_placed/refused events and an outcome of ok|over_budget|ceiling; release opens a quota.release span carrying the freed remainder; settle (per-fragment, hot) records a settle event on the ACTIVE span — never a span per fragment (no trace explosion). - executeExtension opens an extension.execute span whose outcome classifies the terminal state; the fast path is preserved (behavior unchanged, spans are OTel no-ops without an SDK). - /metrics gains bounded operator-ceiling gauges (committed/outstanding/ utilization), DERIVED at scrape from the shared Redis op keys and labelled by quota only — never tenant_id (a cardinality bomb). O(#quotas), no tenant scan. Names and attribute keys are single-sourced in services/observability/names.ts (the dashboard/alert contract). Two disciplines are enforced BEHAVIORALLY rather than by a fragile source regex: span names come from OBS_SPAN, and every emitted attribute key is in the non-PII allowlist (ids/counts/outcomes only) — the telemetry unit spec fails on a stray content attribute. All specs tagged @ai.
The WS3 fault spec imported SchemaPgDriver as a value but only uses it as a type annotation and cast, tripping @typescript-eslint/consistent-type-imports in CI's Lint job. Mark it `type` like every sibling driver spec.
…api report WS2 added addEvent/addEventOnActive/addLink to the public TelemetryService. The api-extractor golden report was stale, failing the API report gate, and the two event helpers referenced a module-local `SpanAttrs` alias that api-extractor flagged as a forgotten export. Match the existing `withSpan` convention in the same class: inline the `Record<string, string | number | boolean>` attribute type (no exported alias, no ABI expansion) and regenerate etc/saas-tenancy.api.md.
behavior_compliance_report's "oldest first" assertion relied on the three probe rows keeping insertion order, but exportStream orders by (created_at, id) and the three inserts can share a millisecond — then the tie breaks on the primary key, a random UUID, so b/c came back swapped intermittently (green on the two prior CI runs, red on this one). The production query is correct and deterministic; the test was the flake. Pin each probe row to a distinct ascending created_at so "oldest first" is decided by created_at alone, independent of insert timing.
…R-6 C1) C1 of PR-6: a valid, RC-labeled @adonisjs-lasagna/ai package with the config surface and provider skeleton the streaming spine builds on. No feature logic yet; mirrors packages/reporting. - AiConfig + defineAiConfig + assertAiConfig: eager boot validation, a per-tenant default-deny provider allow-list (G12), and named-constant defaults so nothing streaming-related is hardcoded. SatelliteConfigRegistry declaration-merge. - AiProvider: assertSatelliteApiCompatAtBoot + assertAiConfig + the compile-time SatelliteProviderConstructor ABI pin. - configure hook, the guarantee test tree, an RC .c8rc coverage gate, CHANGELOG, README (RC badge), the stability.md RC row, and docs/guides/satellites/ai.md. - the eight-place new-workspace wiring: build:ai + build:all, the lockfile, check-satellite-config-wiring, check-abi-boot-assertion, the per-satellite ci.yml + publish.yml steps, and the stability matrix. 17 unit specs green; typecheck, check (21 guards), lint, knip:deps clean.
…e, exceptions (PR-6 C2) C2 of PR-6: the provider abstraction the streaming spine calls through. - AIProviderContract + AI_CONTRACT_VERSION=1: closed StreamFragment / AIStreamRequest / AICapabilities shapes (readonly), model + maxTokens as per-request input. - AIProviderRegistry (Map-backed, container.singleton): mirrors BillingDriverRegistry plus the one divergence, an unconditional streaming-presence gate at registration (a provider not declaring capabilities.streaming fail-closes), and forTenant behind the per-tenant default-deny allow-list. - resolveTenantProviderSelection: the isolated selection seam WS-AI-2 swaps for per-tenant BYOK storage with no signature change (it already takes the tenant). - AIException: closed AIErrorCode union + FATAL_CODES + isRetryable() + the pinned 402/429/503 status map; never carries an upstream body, key or prompt. - assertNever exhaustiveness helper; MockAIProvider + checkAIProviderConformance shipped from ./testing. 39 unit specs green (presence gate, version compat, default-deny, exception codes, conformance); coverage 92.6% lines; typecheck, check (21 guards), lint clean.
…ket-error handling (PR-6 C3)
C3 of PR-6: the isolated SSE frame writer the streaming integrator pumps through,
unit-testable without an HttpContext (it takes a plain sink double).
- SseWriter: well-formed id/event/data frames, multi-line data split into one
data: line per line, monotonic ids continued from a Last-Event-ID resume cursor.
- Backpressure: write() === false awaits 'drain' before resolving, so a slow
consumer throttles the pull instead of forcing unbounded buffering.
- Socket write-error handling (the review gap): a sink 'error' (EPIPE / hang up),
captured out-of-band or during drain, rejects the write so the pump aborts
rather than looping into a dead socket; dispose() detaches the listener.
- writeHeartbeat (':\n\n' comment) and writeErrorEvent (a code-only in-band
'event: error' frame, never an upstream body). Named SSE constants, no inlines.
11 sse_writer specs green; coverage 94.8% lines; typecheck + lint clean.
C4 of PR-6: the orchestrator that pumps a provider's fragments to the client over SSE, metering cost per chunk and resolving a discriminated StreamResult. Kept thin over three unit-testable collaborators (SseWriter, FragmentPipeline, the pump loop), per the review's decomposition point. - StreamResult: completed | aborted(reason) | failed_preflight(error), closed unions. worstCase = the request maxTokens cap; settle is clamped to it (in the quota seam), so no fragment sequence can over-spend. - Lifecycle: breaker + reserve pre-flight can resolve failed_preflight (402/429/ 503) before any byte; the first fragment (or a clean empty end) is the commit point after which nothing throws; the finally always settles used + releases the remainder, FAIL-OPEN so a transient Redis blip can never break it (the I3 violation this guards against). - Four-way composed abort (liveness/tenant_suspended, client disconnect, budget early-stop at worstCase, timeout) attributed to the right reason; a mid-stream provider error writes a code-only in-band event:error frame, never an upstream body; a broken socket write is a client_disconnect. - executeExtension / ExtensionTimeoutError are INJECTED (not imported) so the module never pulls the core /services barrel (eager redis) into the unit runner; the provider supplies the real ones. Registered as a container singleton resolving QuotaService + CircuitBreakerService. 72 unit specs green (happy path, budget, pre-flight matrix, mid-stream aborts, socket error, fail-open finally, fragment validation); coverage 96% lines.
C5 of PR-6: the outbound half the AI providers need. An opt-in `streaming` on the shared `SafeFetchOptions` hands the pinned response body to the Response as an incremental web stream (Readable.toWeb) instead of buffering it. - The pin is a connect-time `lookup` property, so streaming the body off the same socket keeps the IP-pin (no second lookup, no rebind); cancelling the body destroys the socket. Buffered remains the default, byte-identical for every current caller (the whole core suite stays green). - Streaming is pinned-path only: combining it with trustedHost/allowLoopback (both buffered) throws `streaming_unsupported_mode` rather than silently no-op-ing. - Because `SafeFetchOptions` is exported from the main entry, this changes the public surface: the core api-extractor golden is regenerated in this commit (check-api-report back in sync). Additive, no contract-version bump. Red-first unit specs (the rejection guard + the Readable.toWeb incremental / cancel-destroys-socket mechanism); the existing pinning fault spec still proves the pin holds at connect. Core unit suite 1314 green; lint + api-report clean.
C6 of PR-6: the two pure functions where a vendor wire shape is known, split from the providers so they unit-test exhaustively without a transport. - parseSseFrames: the shared framing, correctly reassembling a frame split across chunk boundaries, dropping heartbeat comments, handling CRLF and LF. - anthropic_sse: content_block_delta text + incremental message_delta usage; a malformed frame is skipped, an error event becomes a sanitized AIException. - openai_sse: choices[].delta.content + final usage; the data: [DONE] sentinel; same skip-malformed / sanitized-error handling. DeepSeek and Kimi share it (both OpenAI-compatible), so one adapter serves both. - Error mapping emits only a classified code, never the upstream body or a key fragment (a canary-in-the-body test asserts it). 13 parser specs green (text/usage extraction, split frames, sentinels, malformed skip, sanitized errors); coverage 96% lines.
…ch (PR-6 C7)
C7 of PR-6: three real providers streaming through the kernel's SSRF-pinned fetch
with no vendor SDKs (an SDK's own transport would bypass the pin).
- HttpAiProvider base with an INJECTABLE transport (deps.fetch defaults to the
pinned safeFetch({streaming:true}); tests inject a fake fetch), so the full
adapter (request build to parse to error map) runs in CI without a live key.
Mirrors SsoService's injectable-deps pattern.
- ClaudeProvider (Anthropic Messages, x-api-key + anthropic-version, /v1/messages,
anthropic_sse) and OpenAICompatibleProvider (Bearer, /chat/completions,
openai_sse) with DeepSeek + Kimi as thin subclasses. One adapter serves both
OpenAI-compatible vendors.
- Nothing hardcoded: base URL, model, path suffixes and the anthropic-version are
named-constant defaults + config overrides; the model is per-request input
checked against the per-provider allow-list (G12). defaultModel is now optional
(falls back to the provider's built-in).
- SSRF-safe: never passes trustedHost/allowLoopback; a pin rejection of a BYOK
endpoint surfaces as byok_endpoint_blocked; a 429/5xx head throws before the
first byte so the gateway resolves failed_preflight. A missing key throws
config_missing (never a shared/env fallback); a canary-key test proves no key
reaches a fragment.
- The provider boot() registers the allow-listed, configured built-ins (claude
active); unconfigured providers are never registered. Real-API smokes per
provider self-skip without their key.
97 unit specs green (request shaping, dialect parse, model override, secret
negatives, SSRF boundary, HTTP head classification); coverage 96% lines; check
(21 guards) + lint clean.
…, docs (PR-6 C8) C8 of PR-6: the observability layer, the guarantee specs that need real composition, and the production docs. - Observability (injected, so the module stays unit-loadable): the streamed call is wrapped in an ai.stream span (tenant / provider / model attributes only, never content) and emits integer usage metrics (ai_requests, ai_tokens_total, ai_errors, and ai_stream_disconnects on the disconnect path). The provider wires the real MetricsService.emitMetric + TelemetryService.withSpan. A negative assertion proves no prompt/response content and no float ever reaches a metric or a span attribute (G3), and the attribute set is allow-listed. - First performance guarantee spec: the first fragment is flushed before the producer completes (incremental, not buffered), and backpressure throttles the producer pull instead of buffering unboundedly. - Integration: the AI provider's DI wiring resolves a StreamExtensionService and the provider registry against the real booted container (its quota / breaker / metrics seams are all makeable) through the shared kit Ignitor. - Docs: the production-checklist heartbeat-below-proxy-idle-timeout entry (OQ#12). minMergedCoverage stays at the graduation floor (60), ratcheted off the first CI merged-coverage run. 103 unit specs green; coverage 96% lines; check (21 guards) + lint clean.
The AI-seam work on seam-integration is validated (full ci.yml green on real PG/Redis + e2e + deploy), so the push trigger that ran the full suite on this branch is no longer needed. Restore the default: ci.yml runs on push to master|main and on PRs to master|main only.
Owner
Author
|
Fresh out of the oven. See you on the |
Arcoders
merged commit Jul 1, 2026
4b7eefa
into
LASAGNA-020626/isolation-hardening-and-benchmarks
1 check passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AI
Add a new layer to the Lasagna: the AI satellite. Your SaaS can now talk to AI models without getting burned (fail-closed), without making a mess (SSRF pinned), and without serving the same dish twice (tenant-reserved budgets). Bon appétit. 🧑🍳