Repository navigation
Merge Sunrise 0.15.0 - #133
Merged
Merged
Conversation
…979) * Record where each webhook and event-hook delivery was sent, and keep it after the sender is deleted (t-739) A delivery row now carries its own destination: the URL reduced by loggableUrl (or the email address) plus a keyed HMAC fingerprint of the full destination, so the record holds no URL secret but an operator can still confirm an exact URL. Retries follow the parent's current destination and move the old one into previousDestinations rather than overwriting it. The parent FK on both delivery tables becomes nullable with ON DELETE SET NULL, so deleting a subscription or hook (or erasing the admin who created a subscription) no longer deletes its delivery history. Retries of an orphaned delivery stop and say why; the DLQ routes show orphans to every admin of the org; the DLQ table renders them without a link or a retry. Existing rows are backfilled from their current parent by the 022-delivery-destinations seed unit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Prove delivery destinations against a real database, and say what the export carries (t-739) scripts/smoke/delivery-destination.ts exercises what mocked tests cannot: the parent FK's ON DELETE SET NULL keeping a delivery (and its recorded destination) after its subscription or hook is deleted, or after the subscription's creator is erased; the org export carrying those rows without the URL secret; and retention still pruning them. Wired into the CI smoke job. The org-export manifest's descriptions now say a delivery carries where it was sent, reduced, with a keyed fingerprint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document where a delivery went, and what changed for code that reads one (t-739) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test delivery destination recording, orphaned deliveries and the backfill (t-739) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Answer code review round 1 on delivery destinations (t-739) - Erasure: eraseUser() redacts the erased person's own address (and its fingerprint) from the deliveries their subscriptions sent, which now survive them; previousDestinations entries too. - Orphaned webhook deliveries are a new 'webhookDelivery' kind in UNATTRIBUTED_READ_KINDS, read through lib/orchestration/access/webhook-delivery-access.ts, so the policy's 'unattributed' arm decides who sees them instead of a hard-coded every-admin rule; the DLQ routes compose it with AND. - Orphaned event-hook deliveries get a surface: GET /hooks/deliveries (?orphaned=true), since the per-hook list 404s once the hook is gone. - GET /webhooks/dlq stops returning the subscription url (it can carry a credential); the table shows the recorded destination only. - A retry the tick abandons keeps its last real error and appends '(not retried: ...)' instead of replacing it. - The delete audit entry names the reduced URL and never an email address. - Both retry routes answer 409 for an orphaned delivery. - The delete-subscription dialog and the erasure doc no longer say the delivery history is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the review-round-1 fixes on delivery destinations (t-739) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cover the delivery-subscription table's error paths it now has to clear the per-file gate on (t-739) The two it.todo entries described catch blocks the component has since gained; they are real tests now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Answer code review round 2 on delivery destinations (t-739) - A URL destination is recorded as its origin only. loggableUrl is a log-line heuristic that keeps short or slug-shaped path tokens, so the stored column could carry a credential; the full URL now lives only in the keyed fingerprint. - An email destination is stored trimmed and lower-cased, so subject access and erasure match it exactly. One matcher, webhookDeliveriesAddressedTo(), serves both: exact on the normalised address (never ILIKE, whose _ and % match strangers), JSON containment for previousDestinations, verified addresses only. - Erasure redacts the address on every delivery sent to it, not only on those reached through the person's live subscriptions: deliveries already orphaned by an earlier delete, and other admins' subscriptions, were missed. Only rows naming the address are read. - The subject-access export gains notificationsSentToYou: deliveries emailed to the subject, without their payloads. Before t-739 the address lived only on the (exported) subscription; now it can outlive it. - DeliveryLike moves beside RecordedDestination; stale 'four times' comment and the DLQ tab copy corrected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the review-round-2 fixes on delivery destinations (t-739) The subject-pinning guard in export-user.test.ts now accepts an OR whose every branch pins the subject, and an exact JSON containment of their address — the shape of the new notificationsSentToYou source — and pins the negative cases (one unpinned branch, empty OR, someone else's address) so the widening cannot read others' rows unnoticed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Answer code review round 3 on delivery destinations (t-739) - The subject-access export no longer returns a delivery's destination or previousDestinations: after a re-pointed retry one row can name two inboxes, so either column could hand a subject someone else's address. It states the subject's own address (sentTo) instead. - Erasure stops any of the subject's deliveries still retrying (the next attempt would email them again and write the address back), keeping each row's real error; redacts the rest in one updateMany; rewrites only the rare rows that name the address in their history. The redacted count is on the 'User erased' log line. - 20261011120000_delivery_destination_index: a btree on destination and a GIN (jsonb_path_ops) on previousDestinations, so neither half of the address match scans the table inside the erasure transaction. - GET /webhooks/deliveries reaches delivered and failed orphans, which neither the per-subscription list nor the DLQ can. - Bulk replay leaves out rows whose subscription was deleted, and says so when that is every row on the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the review-round-3 fixes, and build export rows field by field (t-739) notificationsSentToYou spread each selected row; a widened select would then have been enough to put a row's own address columns in front of the subject. The rows are built field by field, and the probe test fails if they are spread again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say that bulk replay skips rows whose subscription was deleted (t-739) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Anchor the DLQ table test's link-name pattern, which CodeQL read as a URL check (t-739) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Give each org its own storage prefix and token claim, and make agent uploads private by default at multi (t-736)
At TENANCY_MODE=multi, upload_to_storage writes under orgs/{orgId}/ through
getStorageClient({ orgId }), which refuses any key outside that org on
upload, read, delete and signing. A binding choosing neither public nor a
TTL now uploads privately with a 24h signed URL. Local access tokens carry
an org claim, refused on mint for a key outside the org and checked again
by the read route. eraseOrg deletes the org's prefix after its transaction
commits, best-effort, reporting storageSwept. single is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Say in the capability and recipe docs that uploads are private by default at multi (t-736)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test org storage prefixes, the token org claim, and private-by-default uploads at multi (t-736)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refuse a binding keyPrefix under the reserved orgs/ root at single, before uploading (t-736)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Document the reserved orgs/ storage root (t-736)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Answer code review round 1 on org storage prefixes (t-736)
- S3 and Vercel Blob deletePrefix follow every listing page, and S3
per-key DeleteObjects errors make the result false, so eraseOrg's
storageSwept no longer claims a sweep that stopped at 1000 objects.
- eraseOrg's sweep builds the provider inside its try, from client.ts,
so nothing can throw after the commit.
- A default binding at multi on a provider that cannot store privately
(S3 without S3_OBJECTS_PRIVATE_BY_DEFAULT / S3_USE_ACL) gets a remedy
naming the deployment setting or public: true, not settings it never had.
- keyPrefix refuses ., .. and empty segments, so a path-normalising
spelling cannot slip past the reserved orgs/ root.
- Mint and the read route share tokenOrgMatchesKey.
- The erasure doc names the in-flight-upload window and says to suspend first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Note the S3 private-objects requirement at multi and the deletePrefix pagination fix (t-736)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test the review-round-1 fixes on org storage prefixes (t-736)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Answer code review round 2 on org storage prefixes (t-736)
- The unbound getStorageClient() refuses every key under the reserved
orgs/ root (any case); the read route and eraseOrg use the owning org's
client. An org's files are reachable only through that org's client.
- A key under orgs/ that names no org (orgs//x, Orgs/x) matches no token
claim, so nothing can mint for it or read it.
- orgKeyPrefix accepts only [A-Za-z0-9_-] ids, so no id resolves to the
orgs/ root (orgs/./).
- Vercel Blob deletePrefix re-lists until empty instead of following a
cursor that deletion may invalidate, bounded by a round limit.
- sweepErasedOrgStorage(orgId) re-runs the sweep for an erased org;
the docs said a re-run was possible and nothing could do it.
- SignedUrlOptions docs: a provider minting local-style tokens must
forward the org.
- One S3 remedy string; getStorageClient({ orgId }) without the no-op
conditional.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Say in the storage tenancy doc that the unbound client refuses org keys (t-736)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test the review-round-2 fixes on org storage prefixes (t-736)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Answer code review round 3 on org storage prefixes (t-736)
- isUnderOrgRoot also covers prefixes of the root (orgs, org, o), so the
unbound client cannot prefix-delete every org's tree, and judges a full
blob URL by its path.
- validateStorageKey rejects . and empty segments (trailing / allowed;
URLs judged by their path): ./orgs/b/x resolved into org b's directory
on local past every string check.
- orgOfKey names only ids an org prefix could hold, so mint, match and
bind agree on what an org id is (no 500 from the read route).
- The read route refuses an org-claimed token whose org is erased or
suspended (ORG_UNAVAILABLE).
- upload_to_storage: case-insensitive reserved-root check; org prefix and
client binding inside error handling.
- Vercel Blob deletePrefix: no re-delete or recount of stale listings,
short wait between them, bounded stale run.
- Docs: getStorageClient() returns a guarded wrapper, not the instance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test the review-round-3 fixes on org storage prefixes (t-736)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Let a fork add its own tables to the org export (t-737) A fork registers its orgId models from lib/app/org-export.ts through registerAppOrgSources(), each source carrying its own fetch. exportOrgData reads them after core's, as the org, into a new app key with meta.app; both absent when nothing is registered. A throwing init is remembered and the export refuses. The org-manifest coverage test unions the registry in, and now also requires every orgId model's Org relation to cascade or set null on erasure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the org-export fork seam and its registry (t-737) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Answer code review round 1 on the org-export seam (t-737) Refuse section names that are built-in object members, drop wrongly typed fields instead of throwing, match core's description floor, accept a composite Org foreign key, use Object.hasOwn in the smoke check, and say plainly that SetNull is a retention choice the rule cannot judge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Answer code review round 2 on the org-export seam (t-737) A rejected declaration now makes exportOrgData refuse, as a throwing init does: the coverage guard cannot see a dropped source for a table without an orgId column. Null entries are dropped with a log instead of throwing. The shadow check is one helper the test exercises, the fork-seam tests run the seam's own init first, and the smoke check that could not fail is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Answer code review round 3 on the org-export seam (t-737) Refuse only when a rejected row leaves a model uncovered, not for a rejection another declaration already answers. Test the checkout's own seam declarations at build time. Export ownedBy for fork sources so a NULL-org row reaches the install org's export at single. Carry exactly model and reason into meta.excluded, label any model value without throwing, and read onDelete from the code, not a comment, keeping the unsafe policy when a model has two Org relations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Scope backup export and import to one org, and move the shared part only for the install org (t-738) Both routes take ?orgId= and ?shared=, resolved by resolveBackupScope(): the target defaults to the active org, an unbound admin key must name one at multi, and naming another org re-asks the policy about it. The shared part (capabilities, knowledge tags, settings) moves only for the install org or an unbound key; a customer org that asks is refused. Exporter and importer run inside runAsOrg(target), so provider warnings and version records are the target's. The import route drops writesSharedSettings and is listed as enforcing the rule itself, so a customer org can restore its own part. Cross-org workflow slugs take a free slug; tag grants fall back to existing tags. The backup panel offers the shared part only where it may move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test org-scoped backup: scope resolution, importer, exporter, routes, panel and a real-Postgres smoke (t-738) The isolation smoke gains [19b]: A's own part, exported in A and imported into B from inside A and from no org, changes only B's rows, leaves A's agent, workflows and every shared row untouched, lands A's workflow under a free slug, and skips a bundle's shared part when not asked for. The t-751 shared-settings test drops its backup-import row: a customer org's import is no longer refused at the guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Answer code review round 1 on org-scoped backup (t-738) The shared part now follows the caller's org, as the ruling and t-751's guard read it, not the org named in ?orgId=. A refusal is logged. An import naming a suspended org is refused. A renamed workflow is marked with metadata.importedFromSlug so a repeat import updates it; a new slug avoids the bundle's own slugs and this import's creations, and the probe is skipped at single. Audit entityId is the target org. The skip warning is worded for the mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the review-round-1 fixes on org-scoped backup (t-738) The smoke's repeat import now has to update B's one copy of A's workflow, found by its importedFromSlug marker, rather than add another. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Answer code review round 2 on org-scoped backup (t-738) Plan renamed workflow slugs before the transaction, so no probe waits on a second connection while it holds one; a slug lost to a race answers 409. The marker lookup is ordered. The shared rule reuses canChangeSharedSettings. Bundle metadata is parsed, not cast. The panel shows the API's message when an export is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the review-round-2 fixes on org-scoped backup (t-738) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Answer code review round 3 on org-scoped backup (t-738) Ask the policy before saying whether a named org exists or is suspended. Plan renamed slugs in two queries, skipping slugs the loop skips. Look up the renamed-copy marker only when another org holds the slug, and strip it on export. Count a v1 bundle's category tags in the shared-skip report. Name the org in the export filename. Reuse parseApiResponse in the panel and show sharedSkipped there. Correct slug-availability's comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the review-round-3 fixes on org-scoped backup (t-738) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
checkBudget() runs inside the guard's runAsOrg, and the global total is an unfiltered AiCostLog aggregate read in that scope. With org_isolation on it sees only the acting org's rows, so the global cap acts as the same cap per org. This test records the scope each aggregate runs in; the two multi cases fail until the total is read in the system scope. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e again (t-788) - Org.settings gains a 'quotas' slice holding monthlyBudgetUsd, with a typed read/load/write helper (readOrgQuotas / loadOrgQuotas / writeOrgQuotas) for forks' billing code. The read is per key and fails open to 'no org cap'; the global cap still applies. - PATCH /admin/orgs/[id] writes it as settings.quotas. At multi only the install org (or an unbound admin API key) may, so a platform admin switched into a customer org cannot raise that org's cap. - checkBudget() enforces the org cap beside the agent and global caps, on month-to-date spend filtered by orgId. No quotas set gives the same result as before. - The global total is summed in the system scope through the confined cross-org count module, so it covers every org and detached rows. The agents list and the costs alert read the same getGlobalCapState(). - The streaming handler emits budget_exceeded / org_budget_exceeded / global_budget_exceeded, and the budget_exceeded webhook carries 'cap'. - (orgId, createdAt) index on AiCostLog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Costs doc: the org cap, who may set it, fail-open reads, the per-agent lock bound, and the three chat codes. Multi-tenancy and identity docs list the quotas slice. The design doc's out-of-scope line now says per-tenant copies of the settings singleton are out, and a per-org cap in Org.settings is in. CHANGELOG and VERSIONING carry the helper, the quotas key, the new codes and the global-cap scope fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… smoke (t-788) Asked from org A, the global spend total counts org B's cost row; an org cap below A's spend stops A's agent and B's own cap does not count A's spend. Runs in CI's smoke-multi job; not run locally (no throwaway Postgres with the role split here). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ions (t-783, t-786) (#989) * Erase a person from delivery payloads and colleagues' email subscriptions (t-783, t-786) Deliveries whose payload names the subject - their id, a conversation or run of theirs, as the actor or an approver - are redacted on erasure and exported on subject access, matched by JSON containment through a new GIN index on each delivery table's payload. A colleague's email subscription to the subject's verified address is cleared and deactivated, with an audit entry. The subject export's notificationsSentToYou section becomes webhookDeliveries; event-hook deliveries join as eventHookDeliveries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test and document erasure of delivery payloads and colleagues' subscriptions (t-783, t-786) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Admit the new raw-SQL and ownerless-read sites to their guards (t-783, t-786) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep org configuration out of a subject's delivery export (t-783) An event about the subject's run can carry the org's own configuration: a notification step's webhookUrl (often holding its credential), approval links, the body an admin's template rendered. The export's activity view is now an allowlist - the record keys plus the free text about the subject - as the erasure side already was. Found by /security-review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address code-review round 1 on delivery erasure (t-783, t-786) - actorUserId names an admin only on the admin-action events; on any other event it is the run or conversation owner, so the row is activity and loses its free text even after the run is pruned - the retry stop is decided by the UPDATE against the row's live status - colleagues' subscriptions are matched with normaliseAddress in code, not re-implemented in SQL, and cleared before the delivery passes - the erasure transaction gets a 60s timeout - an approver-only export row says the subject was an approver - one shared createdAt-then-id comparator - documents the pruned-run limit (t-792) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the review-round-1 fixes; smoke the pruned-run and padded-address cases (t-783, t-786) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address code-review round 2 on delivery erasure (t-783, t-786) - in-process retry timers skip a row settled since they were armed, so an erasure's stop (and t-739's) holds - an admin action's free text is never exported to the run owner - the webhook export source reads one table at a time, as exportUserData does - the match selects only the columns it needs and orders in SQL - docs: subscription matching is normalised in code (the step-7 edit had not been written), the in-flight-delivery limit, the approver flag Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the review-round-2 fixes (t-783, t-786) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address code-review round 3 on delivery erasure (t-783, t-786) - the workflow.failed a force-fail emits carries the admin's note too; never exported to the run owner - one AWAITING_RETRY_STATUSES constant for the timers, the SQL stop and the address pass - erasedPayload narrows the payload once - docs and CHANGELOG no longer overclaim the stop; the open cases are t-793 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The route reads the global cap through getGlobalCapState now; the tests still mocked getMonthToDateGlobalSpend, so the global-cap case fell into the route's catch and returned null budgets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w's other findings (t-788)
Code review of t-788 found ten issues. All are fixed here.
- The org and global caps applied only to chat. The provider manager's
Proxy now runs a spend gate (assertWithinSpendCaps, new
lib/orchestration/llm/spend-caps.ts) before every vendor call, beside the
eligibility gate. Workflows, the orchestrator, evaluations, judges,
ingestion and jobs now stop at the caps. A refusal is
SpendCapExceededError, code org_budget_exceeded / global_budget_exceeded.
It counts as a request fault, so there is no failover and no breaker
failure, and ExecutorError keeps the code and never retries it.
- A cap of 0 refused local models too. The gate never refuses a local
provider. Chat holds a refusal while the turn is on a local primary and
makes it only if the turn would reach a paid provider.
- The agents list ignored the org cap. It now applies each agent's org cap,
one lookup per org, and reports orgCapExceeded.
- checkBudget ran its cap lookups one after another. The agent, org and
global reads now run in parallel.
- The global total was an unscoped month-to-date sum on every call. The
month-to-date totals are now cached for 10s, and logCost adds this
process's own rows at once. The cap values are cached for 30s, and the
orgs and settings PATCH routes clear them in their own process.
- Org cap changes left no audit entry. The PATCH now writes
org.quotas.update with the caps it replaced, read inside the write's
transaction (updateOrgReportingPrevious).
- { monthlyBudgetUsd: undefined } was stored as quotas: {}. A slice is now
empty when every key is undefined (definedEntries), in writeOrgQuotas,
applyOrgSettingsPatch and the lifecycle log line.
- Three copies of the UTC month-start helper are now one, utcMonthStart.
- The unused applyRetentionPatch wrapper is gone. Its tests now call
applyOrgSettingsPatch.
- The two-org smoke compared a 24-hour total with a month-to-date one.
Both totals now start at the start of the month, and the smoke also
proves the gate per org.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A chat turn that goes on, on a local model, past a reached cap now holds every paid call it makes (withPaidCallHold), so paid embeddings, the summarizer and tools stop at the agent's cap as the main stream does. - A cap that stops a turn part way persists the error marker, sends the budget webhook and wipes the partial answer, as any refusal does. - A paid primary refused by a cap fails over to a local fallback: provider selection passes over refused paid candidates, in getProviderWithFallbacks and in chat's mid-stream failover. - GET /costs/alerts shows the every-org spend only to the install org. - The agents list skips an unreadable global cap instead of dropping every budget; one pair of skip-on-failure readers serves all three callers. - isLocalProvider reads the provider row instead of building a client. - The global cap is read from the settings cache, which the backup importer now clears too; the second cache and forgetGlobalCap are gone. - Budget refusal text comes from the error registry, so the pre-turn and mid-turn refusals say the same thing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-budget-cap Per-org monthly AI budget caps, held on every paid call (t-788)
…s (t-789) An org's Org.settings.quotas.requestsPerMinute is a second counter in applyRateLimit: the matched per-caller rule runs first, then the org's window. The org comes only from a verified principal — the session's activeOrgId, or the org column of the key or token row the credential lookup found — never the x-sunrise-org header. Skipped at single, for an org with no cap, and under /api/v1/admin/**, where a full cap is raised. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Count a user only against an org they can enter: the session's user or a user key's owner must be an active member (the guards' rule, cached 30s, read only for orgs with a cap). A removed member's session, or anyone on a resolver-named hostname, can no longer hold an org at its cap. - Take a session's org in the guard's precedence: the fork's tenant resolver, computed from the request, else activeOrgId. - Stop counting embed-widget traffic; the token is public. - Never count POST /api/v1/orgs/switch, the way out of a full org. - Replace the timestamp-list window with a two-bucket sliding estimate, so a check is constant work whatever the cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Count a user API key on session-keyed routes against its org: with no session, look the key up for its org only; the bucket stays the IP's. - Give an admin-scoped key no org, as enterApiKeyOrg does. - Bound the cap cache with an LRU (a resolver can name arbitrary org ids). - Set Retry-After to when the sliding estimate has room, not the minute end. - Never count GET /api/v1/orgs, which the switcher needs. - Skip the tenant resolver at single; fix the stale schema comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A signed-in request carrying a user API key counts against the key's org, as withAuth reads the key before the cookie. - Only sk_ keys count on session-keyed routes; those routes refuse MCP keys. - Correct the docs: the org window is keyed by org id, not a mw:org token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-request-cap Cap each org's API requests per minute on top of the per-caller limits (t-789)
getOrgUsage() rolls AiCostLog up by org for one UTC calendar month, in the system scope at multi, with detached NULL-org rows in their own bucket and each org's quotas.monthlyBudgetUsd beside its spend. At single it is one install-org row. GET /api/v1/admin/orchestration/costs/orgs serves it, install org only at multi (the rule org caps are written under). The costs page shows a per-org table when there is more than one org; the org detail page shows month-to-date spend against the cap from the same endpoint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…777, t-778, t-780, t-785) (#993) * Reduce outbound URLs in every logged string, and collapse colon-joined secrets (t-778) The logger's sanitiser now passes every string it writes through loggableUrlsInText, so a tokened webhook URL under any key, in a message, or in an error's message or stack is logged as origin + collapsed path. A path segment containing ':' is judged part by part, so a Telegram bot URL no longer logs its secret. fetch-error.ts reuses the shared helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop a logger test that could not fail (t-778) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hide webhook secrets in served traces, notification events and audit entries (t-777, t-785) - withDisplayUrls shortens URL-bearing trace values where a trace is served (execution detail and live, report.md, the approver's status page, the supervisor review); the persisted trace keeps working URLs. - The workflow_notification event carries the step's reduced URL. - Webhook subscription audit entries name the URL origin or the id and reduce url/emailAddress in the changes diff; the create log line no longer carries an email subscriber's address. - The fetch-url audit entry records the reduced source URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the reduced source URL on the fetch-url audit entry (t-777) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the page address's secrets out of analytics (t-780) Page views, and the addresses providers attach by themselves, keep only origin, collapsed path and the attribution query parameters (owner ruling b): utm_*, gclid, fbclid. Applied at the React page() chokepoint, in each provider's page() and fallbacks, via gtag 'set' for GA4's later events, PostHog before_send for its own properties, and server-side tracking's Referer context. Plausible loads its manual script so it no longer records page views by itself with the full address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document URL reduction in logs, traces, audit entries and analytics (t-777, t-778, t-780, t-785) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Let the logger import only import-free modules, checked (t-778) The guard keeping the logger's static imports out of the browser bundle was absolute; the sanitiser now needs the URL scrubber. It allows an explicit list instead, and asserts each listed module reaches no other module at runtime, so nothing behind it can pull in a Node builtin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address code-review round 1 on URL secrets (t-777, t-778, t-780, t-785) - The executions list and the three run streams (execute, execute-stream, rerun) serve URL-bearing values reduced, as the trace routes do; inputData is reduced on the detail and review routes too. - withDisplayUrls also matches a provenance source's 'reference'. - The userinfo pattern stops at '"', backtick, '<>' and whitespace, so a URL in serialised JSON no longer reads across to a later field's '@'. - PostHog before_send reduces autocapture's link targets. - A same-origin URL change in a subscription audit diff is marked '(path changed)' instead of reading as a no-op. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address code-review round 2 on URL secrets (t-777, t-778, t-780) - runSupervisorAssessment reduces step outputs, input and output itself, covering the in-workflow supervisor step and the review route alike. - The userinfo pattern allows quotes again but stops at a quote beside a separator, so a double-quoted password is stripped and a JSON field or list item is not read across. - withDisplayUrls walks plain objects and arrays only, so a Date or Decimal in a live step output keeps its toJSON. - PostHog's $direct referrer sentinel is left as it is. - GA4's reduced address is set globally at init, not on the config, where it would outrank each page view's set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Name the round-2 trace surfaces in the CHANGELOG and logging doc (t-777) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address code-review round 3 on URL secrets (t-777, t-780) - The approver status route reduces step configs only: the chat approval cards post the last step's output to the agent as the workflow result, and a reduced URL there would be a dead link. - report.md reduces inputData and outputData, which its renderer prints. - A provenance reference is reduced only for an external_call source; a web_search citation stays a working link. - Docs name PostHog heatmap and web-vitals data as out of reach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Show the per-org table with one org when there is detached spend, so an erased org's spend, which still counts towards the global cap, is seen. - Require a year of 1000 or later in `period`: Date.UTC reads 0-99 as 1900-1999, so the report named one month and summed another. - Say "Paid calls off" for a cap of 0 on the org page, as the table does. - Drop 'use client' from the per-org table, which has no client state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-usage-metering Show each org's monthly AI usage to platform admins (t-790)
The bulk route's where excluded system agents but not deleted ones, so a deleted agent's id in the selection revived it and wrote it a version. Add deletedAt: null, and report the ids the action could not reach (system, deleted or unknown) as `skipped`. The candidates are now read once with isActive, and filtered in code to the ones the action changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The proxy already applies the admin section cap, keyed on the session, to every /api/v1/admin/** request. The DLQ list, stats, replay and delivery-delete handlers also called adminLimiter.check on the client IP, so each request counted twice and admins behind a shared NAT hit 429s early. Remove the four calls and the tests that mocked them. No per-flow sub-cap for bulk replay: a batch is already bounded at 500 rows and 5 concurrent retries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-847–t-849) (#1034) * Fail evaluation runs where no grader scored any case (t-847, #921) When the subject ran but every grader or judge threw, no case carried an errorCode, so the all-cases-failed check passed and the run ended `completed` with nothing scored. The cost estimator then sampled it as a good run. Such a run now ends `failed` with `summary.note = 'no_scores_recorded'`. A run with no cases, or where any metric scored at least once, is unchanged. The happy-path worker test only mocked one of the worker's two case-result reads, so its aggregation always ran on an empty list. It now mocks the resume-cursor read too, and aggregates the rows it declares. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Show failed evaluation runs without crashing the run detail page (t-848, #922) A run the worker fails before aggregation (dataset changed, invalid metric configs, preflight) stores a summary with only a note, but the page typed metricSlugs and stats as always present and mapped over metricSlugs, so opening one threw. They are now optional. The metrics table renders only when both exist; otherwise the card says no metrics were aggregated and shows the note, plus the preflight error when there is one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Show decisions on scheduled and inbound runs in approval history (t-849, #773) Approval history read executions with `userId: session.user.id`, so a run nobody owns (scheduled or inbound) never matched, and every decision on one was missing, including the caller's own. The policy was never asked either, so a fork that narrows unattributed reads had no say over this page. It now selects through `executionVisibilityWhere(session)`, the same rule as the executions list, with the status filter in its own AND arm. Recorded on the Approval queue feature: listing "decisions I made on any run" was rejected because it means discovering other admins' owned runs through the trace, the approver arm t-690 kept off the read surfaces. The route leaves the ownerless-surfaces roster's known gaps (now empty), and joins the policy-narrowing test beside the other execution read surfaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address review: approval-history scan window, run summaries (t-847–t-849) - Approval history scans only runs whose trace holds a human_approval step (jsonb containment). With ownerless runs admitted, a schedule running every minute would otherwise fill the 1000-run window with runs that have no approval, and older decisions would drop off the page. Checked against the dev database: the filter matches the 31 of 58 runs a hand count finds. - A no_scores_recorded run also records casesFailed and dominantErrorCode when some cases errored before grading. - The run detail page shows a worker crash's message beside its code, and its fallback no longer claims when the run stopped. - The CHANGELOG and docs say a grader that skips leaves a run unscored too, not only one that throws. Not changed: a named approver still does not see their decision on another admin's run (the t-690 ruling, recorded on the Approval queue feature), and the route's ownership declaration stays 'self', as on the executions list it now matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Answer a hidden agent the same as an unknown one on chat token validation (t-850) The validate-token route looked agents up without the consumer routes' visibility filter, so an internal agent answered "does not require an invite token" where an unknown slug answered "not found" — enough to probe whether a named internal agent exists. Filter to public and invite_only as the stream route does, so a hidden agent is not found. Fixes #894. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Treat an async start-up registration as a failure, not a success (t-851) createAppInitGate read a seam's returned promise as success: it latched 'ok' as soon as the call returned, kept what the init registered, and a later rejection never reached onFailure. Authorization selects safe mode and subject-sources refuses an Art. 15 bundle only from onFailure, so an async initAppAuthorizationPolicy that rejected left the install on the default policy with nothing reporting it. The gate now refuses a thenable return as if the init had threw: roll back, settle 'failed', log, run onFailure, and route any later rejection to the log. Refusing rather than awaiting because the registries' reads are synchronous. This breaks an async seam that happened to work; it was outside the guarantee already, and the CHANGELOG tells forks to drop the async. initApp (the boot hook) stays async and its docblock now says so. Fixes #739. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse keeping executions for ever while cost logs expire (t-852) The global settings schema, PATCH route and form skipped the retention pair check whenever either side was null. Null means "keep for ever", so executionRetentionDays: null with a finite cost-log window was accepted and the cost breakdown emptied out under executions still on file. All three now share costLogsOutliveExecutions(): null cost logs always cover; null executions need null cost logs. Same rule the org route already applied. Error messages name the fix. Closes #839. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Follow the device theme until a visitor picks one (t-853) The no-flash script, ThemeProvider's lazy initialiser and its first-mount effect all saved the device's light/dark setting to localStorage as if the visitor had picked it, so a device that later switched was never followed again. Only setTheme writes now. With nothing stored the provider listens for prefers-color-scheme changes. lib/theme.ts holds the resolution and the script text, so the layout and the hook cannot drift; a test runs both over the same inputs. Stored values are kept, so existing visitors see no change. Closes #756. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Make erasure see the cleanup hooks forks register at start-up (t-854) The erasure hook registry was a plain module Map with no lib/app seam. instrumentation.ts, and so initApp(), loads in a separate module graph from the route handlers that call eraseUser(), so a hook registered at start-up, as the erasure doc said to, was never run and nothing logged. The Map is now on globalThis, so hooks registered from initApp() are seen. A new fork-owned seam, lib/app/erasure.ts initAppErasureHooks(), runs once through createAppInitGate before the first read, with rollback on a throw. Docs, roster, VERSIONING and CUSTOMIZATION name it. Closes #691. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Assert the erasure seam ships empty alongside the other lib/app seams (t-854) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cover the theme provider's blocked-storage, server and no-provider paths (t-853) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Check the global retention pair only when a PATCH names it (t-852) A row saved before #839 tightened the rule must not 400 a PATCH that changes something unrelated, as on the org route. Also un-export a file-local message and fix a test path in the theme docblock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Note that a PATCH naming neither retention window is not checked (t-852) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Never hand out more heap than was asked for or the machine can afford (t-856)
resolveHeapMb floored at v8's heap_size_limit, which is only Node's
unaided default when nothing capped this process. Under an inherited
--max-old-space-size it reads the parent's cap plus V8 overhead, so a
6144 request came back as 6336, and a large parent cap on a small box
stepped over the 75%-of-RAM clamp the function exists to enforce.
unaidedLimitMb now reports no floor when NODE_OPTIONS or execArgv
carries a cap, and the clamp applies last so it holds whatever the
floor says. Fixes #711.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Make the heap-cap tests independent of the host's RAM (t-857)
Two tests asserted the applied cap against DEFAULT_HEAP_MB while running
the real resolver, which clamps to 75% of physical memory, so both went
red on the ~8GB private runner ci.md tells forks to size for.
main() now takes the resolver as an injectable dep, and its test pins
what main owns: the ask passed in, the answer passed on. The chunked-lint
child-env test compares against the resolver's own answer on the host
instead of a fixed number. Fixes #700.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cap the type-check's heap so a cold run does not run out of memory (t-855)
tsc was kept out of run-capped.mjs on a 1.64-1.75 GiB peak, but with
incremental on that was a warm run. Cold, base Sunrise peaks at 2.74 GiB
RSS and a ~1.9x fork aborted at 4.40 GB, and type-check runs first in
validate, so the abort stopped the gate before lint or format ran.
type-check now goes through the wrapper like lint. Its argv is static,
so the Windows shell-quoting caveat does not apply. CI is unchanged: the
workflow sets NODE_OPTIONS and the wrapper stands down. The docs restate
the measurement as cold. Fixes #693.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pin the emails seam in the shared test setup so fork templates leave core tests green (t-858)
tests/setup.ts pinned lib/app/brand but not lib/app/emails, so a fork
that filled emailOverrides turned registry.test.tsx red with nothing in
the fork it could change. The seam is now pinned to {} suite-wide, like
brand; defaults.test.ts reads the real scaffold through importActual so
'seams ship empty' can still fail in a fork; and registry.test.tsx
restores the pin with doMock instead of removing it with doUnmock.
Checked by filling the seam: without the pin the registry fallback test
fails; with it only the ships-empty row does. Fixes #895.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Stop the feature-flag delete test updating state after teardown (t-859)
The test mocked apiClient.delete with a real 100ms timer and returned as
soon as the call started, so the promise could resolve after happy-dom
was torn down and the component's state updates failed the run with
'window is not defined'.
The delete is now held open by hand and settled inside the test. The
test was also named for a disabled-button state it never asserted, and
which is never visible: the dialog closes on click. It now checks what
the component does: the row stays while the delete is pending and goes
once it completes. Test-only; React 19 already ignores state set after
unmount, so no mount guard in the component. Fixes #925.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address code review: keep a command-line heap cap, guard the emails pin
- unaidedLimitMb ignored execArgv for the floor only when NODE_OPTIONS is
capped. A cap on the wrapper's own command line is not inherited by the
child, so dropping the floor there cut an explicit 12288 back to 6144.
- The defaults.test.ts pin guard now covers lib/app/emails as well as
brand, so a future doUnmock of the emails pin fails a test rather than
relying on a comment.
- chunked-lint.mjs's withHeapCap comment states the new floor-then-clamp
order.
- CHANGELOG entry for type-check moving behind the heap cap, following
the precedent set when lint did.
Not changed: totalmem() vs a container's cgroup limit, and the uncapped
floor counting V8's non-old spaces, both predate this branch; the
deleting-state coverage, because the dialog closes on click and that
state is never visible; and the wiring tests that compare against the
resolver, since resolveHeapMb's own tests pin its values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…all-screen header nav (t-860, t-861, t-862) (#1038)
* Trim before checking in every Zod string chain (t-860, #713)
Zod runs a string chain's checks in the order written, and .trim() is
one of them. 88 chains trimmed after .min()/.max()/.regex(), so " "
passed .min(1) and was stored as "". Every chain now trims straight
after z.string().
The reorder was done by an AST codemod rather than by hand: one move per
chain, 116 lines each way. trim-order.test.ts parses every z.string()
chain under app/, lib/ and components/ and fails on a length, pattern or
refinement check before .trim(); it is in ALWAYS_RUN_TESTS because the
change it catches is a new schema no import chain reaches. Docs and the
form-builder template taught the old order and are corrected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Expect whitespace-only names to be rejected in the user schema tests (t-860)
Both tests pinned #713 as intended behaviour. The user-schema one said
a whitespace-only name was "simply omitted"; it was stored as "",
blanking the user's name on a profile PATCH.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Make "Try again" on error boundaries re-fetch the page (t-861)
Every boundary passed Next's reset(), which re-renders without
re-fetching, so a page whose server-side data load failed failed again
the same way. Next 16.3 hands boundaries a stable retry() that
re-fetches the segment first; all five boundaries now use it.
This is what makes the next commit's error path recoverable: an admin
page whose main data fails to load will throw to the admin boundary,
and its "Try again" has to actually try again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add a server-side loader that tells a failed load from an empty one (t-861, #518)
Admin pages wrapped each serverFetch in a try/catch returning [] or null,
so a 500, a timeout or a bad base URL rendered as "No agents yet", and a
detail page's 500 rendered as a 404.
lib/api/server-load.ts has two entry points:
- requireServerData() for a page's main data: returns it or throws to
the route's error boundary, and with { notFound: true } renders the
not-found page on a genuine 404 only.
- loadServerData() for a secondary panel: never throws, returns a typed
failure, and the page renders <LoadFailedNotice> in that panel's place.
Both log every failure, so pages lose their own catch-and-log.
Three pages move to it, one per shape: the agents list (main data), the
provider edit page (detail, 404 vs failure), and the orchestration
dashboard (independent panels; an empty quarantine panel or a missing
budget banner is the all-clear signal, so a failure must not look like
one). Their tests now fake only serverFetch, with real Responses, so the
real parser and loader run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Document the server-load recipe and the INTERNAL_API_URL case it surfaced (t-861)
.context/api/examples.md now says which loader to use for which data,
with the three reference pages. The dashboard doc describes what each
failed panel renders. .env.example said a local reverse proxy needs no
INTERNAL_API_URL in every mode; that holds under npm run dev only, and
a local production build behind one is the setup that hid #518.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Record the loader and the boundary's retry prop in docs and CHANGELOG (t-861)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show load failures on the user, org, member, logs, overview and support-access pages (t-861)
Users and orgs lists, logs and the user/org detail pages throw to the
admin boundary on a failed load. A malformed id (the API's 400) still
renders not-found, as before; only a 404 or 400 does. The overview's
stats show a notice instead of skeletons that never resolve. Members,
support access and the org detail's provider and usage panels keep the
'could not be loaded' messages they already had, now driven by the
loader and logged. Five of these pages logged nothing on failure.
Invitations on the users page fail the whole page for now: the tabs
component has no place to show a failure in that tab alone, and an
empty 'Pending invitations' tab reads as none pending.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show load failures on the agent, profile, capability and provider pages (t-861)
List rows and detail records are main data; a detail page 404s only on
a 404. The agent pages' quarantine banner and evaluation trend, and the
category list on the capability forms, show a notice when they fail to
load: an empty quarantine banner reads as nothing quarantined. A
capability's 'used by' list is main data because the cards that show it
would otherwise state 0 agents as fact. The agent form's existing
free-text fallback for a failed provider list is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show load failures on analytics, approvals, conversations, costs, executions and settings (t-861)
Panels whose empty state reads as an all-clear (no unanswered questions,
no budget alerts, nothing awaiting approval, 0 running) are main data
for now, because the view components that lay them out cannot show a
failure in one panel; the rest show a notice. The settings record is
main data, so its forms are never pre-filled with defaults an admin
could save over the real settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show load failures on the evaluation, experiment, knowledge and learning pages (t-861)
Lists, detail records and the option lists a form cannot work without
are main data. Option lists that only feed an optional control (case
generation, the agent filter, related patterns, the pairwise judges)
show a notice. A pattern number below 1 is answered without a fetch,
since the API's 400 would now reach the error boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show load failures on the event-subscription, MCP, trigger and workflow pages (t-861)
MCP settings that fail to load or parse now throw instead of filling the
form with defaults, and the MCP dashboard no longer shows a failed
settings load as 'Disabled'. The workflow builder's option lists show a
notice above the canvas. The workflows list keeps its existing error
banner, now driven by the loader.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Render not-found for a malformed id on every detail page (t-861)
The detail routes validate their id and answer a malformed one 400, which
the old pages showed as a 404. With requireServerData({ notFound: true })
it reached the error boundary instead, and was logged as an error.
notFound now covers a 400 as well as a 404, and the loader logs a 400 as
a warning. The user and org pages, which handled this themselves, use the
option.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let the users, new-dataset and MCP pages fail one panel at a time (t-861)
In the first pass these failed the whole page when a side panel's data
did not load, because the component laying the page out had nowhere to
show a failure in one tab. Pending invitations, the case-generation tab
and the MCP card counts now show a notice (or '—' on a count) where they
would be, and the rest of the page works.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let the providers, capability and event-subscription pages fail one panel at a time (t-861)
The model matrix, a capability's 'used by' list and the dead-letter
queue each show a notice in their own tab when they fail to load. With
the agent list unknown, the quarantine card says it applies to every
agent with the capability attached instead of stating a count; only the
capability load decides not-found.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let the costs, analytics and executions pages fail one panel at a time (t-861)
Budget alerts, each analytics panel and the live-engine snapshot show a
notice in their own place when they fail to load; no single analytics
endpoint can take its page down. The cost summary stays main data: it
feeds five panels and is what the page is for. A failed live-engine
snapshot does not mount the polling panel, which has no state between
'no data yet' and '0 running'.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Give the app header a labelled small-screen menu (t-862)
Below 640px the header navs hid each label with `hidden sm:inline`. The
icon is aria-hidden, so every link lost its accessible name, an item with
no icon rendered as an empty box, and nothing in the row could shrink.
PublicNav and ProtectedNav now resolve their items and hand them to a
shared HeaderNav: labelled inline links from `md` up, a named dropdown
menu below it. The menu lives inside the nav rather than in a second
AppHeader slot, so any caller passing `navigation` keeps its nav on every
screen size. AppHeader lets the brand truncate and holds the actions'
size.
Closes #862
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cover AppHeader's default brand (t-862)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Document HeaderNav and the header's small-screen layout (t-862)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Render no header nav when there are no items (t-862)
A fork whose nav items are all admin-only left non-admins below md with a
menu button that opened an empty menu. HeaderNav now renders nothing for an
empty list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address code-review findings on the server loaders (t-861)
- Trigger pages asked the list routes for 200 rows; the routes cap limit at
100 and answered 400, so both pages always hit the error boundary.
- The executions list dropped bad workflowId/status filters into a required
load; it now drops them, so a stale bookmark shows every execution.
- loadServerData logs a 404 (the panel notice says it was logged), takes an
expected-status option (the costs page's 403 is no longer logged), and
passes Next's control-flow errors through with unstable_rethrow.
- requireServerData's notFound covers only a 400 whose validation error
names the id, not every 400.
- Malformed graders/agents replies show the panel notice instead of
crashing the page; the agent page shares one cached agent fetch; the
dashboard's activity feed no longer waits on the first wave.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address second code-review round on the server loaders (t-861)
- loadServerData honours expected: [404]; the pattern-detail page passes
it, so a pattern with no chunks no longer logs a warning per view.
- The agent page's metadata fallback lets Next's control-flow errors
through with unstable_rethrow.
- requireAllServerData() reads every page of a list route, or throws; the
trigger pages use it, so past 100 rows no agent or workflow is dropped
(including the trigger's own agent).
- The executions list says when a filter from the URL was ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address third code-review round on the server loaders (t-861)
- Executions table ignores a URL status its menu doesn't offer
- Pattern page validates the number with the route schema
- Agent page and dashboard log list-less panel replies; a quarantines
reply without items shows the notice instead of "nothing quarantined"
- Org usage loads through loadServerData with 403 expected
- ProvidersTabs takes a non-null initialProviders
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… (t-863) (#1039) * Make the /pre-pr router-mock scan catch shorthand and alternate casts (t-863) /pre-pr step 4m was a Python block in pre-pr.md that matched text. In a fork it reported CLEAN over 16 complete router literals and 17 casts (#936): its member regex needed a colon, so shorthand members were never counted; it located literals by a `prefetch:` anchor, so a shorthand prefetch hid the whole literal; and its cast rule grepped one spelling while the offenders wrote `ReturnType<typeof vi.fn>`. The rules now parse with the TypeScript compiler (scripts/ci/router-mocks.ts, lifted from the downstream fix in resparkable#106) behind a CLI, `npm run check:router-mocks`, which `validate` and CI run and 4m now calls. Sunrise's own suite is clean under it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix router-mock checker misses and false positives from review (t-863) Code review round 1. Fixed, each with a test that fails without it: - a file naming 'next/router' anywhere was skipped even when its next/navigation mock used a template literal (a silent pass); - a cast returned from inside a block in a mockImplementation body was missed; - a namespaced `mocks.createMockRouter()` was not recognised as the factory; - all six spies passed to the factory as overrides were flagged, though that is what the CLI's own advice tells such a test to write; - `/* router-mocks-ignore:\n */` passed as a directive with no reason. Not changed: an aliased `const u = vi.mocked(useRouter)` is the stored-spy limit #936 already documents; a scan root other than tests/ matches where Sunrise keeps its tests and the output names that root; a read-back cast to MockRouter and a widened factory call are rare and have the ignore directive; the CI step's code-only gate matches every sibling check, and validate runs it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Catch router casts in conditionals; accept any ignore reason (t-863) Code review round 2, on the round-1 fixes: - a partial router cast returned as one branch of `c ? X : Y`, or as a `??`/`||`/`&&` operand, was still missed, though round 1's comment claimed "any return X"; - round 1's directive regex rejected genuine reasons starting with `/` or `*`; a block comment's closing `*/` is now stripped before testing instead; - the nearest-function walk is pinned by a nested-function-with-block test. Not changed: overrides passed to the factory through a variable need data flow tracking and have the ignore directive; any `x.createMockRouter()` counts as the factory; a pages-router file naming its module in a template literal is scanned, by design, so the check errs towards looking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…'s Hub bootstrap out of CLAUDE.md (t-864, t-866) (#1043) * Move Sunrise's own Hub bootstrap out of the CLAUDE.md forks inherit (t-866) CLAUDE.md carried Sunrise's Hub project identity and process tier between hce-hub:bootstrap markers, so every Hub-coordinated fork read two project identities after a sync, or had to delete the block on every upstream regeneration. The block now lives in a gitignored CLAUDE.local.md per checkout; CLAUDE.md keeps only a generic pointer, and CUSTOMIZATION.md's first steps tell a fork where its own bootstrap goes. Fixes #845 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse version-pinned experiment variants until a saved version can run (t-864) The run path only loads the live agent, so a variant pinned to an AiAgentVersion was scored against the live config and a version A/B named a winner between identical setups. Create now answers 400 for any pinned variant, and run answers 400 for an experiment stored with one before writing any evaluation run or session. Unpinned experiments are unchanged; the create form and experiments doc now say every variant runs the live agent (an A/A check). Refusing every identical-config experiment, as the issue's stop-gap proposed, was rejected: the UI never sends a pin, so it would have disabled experiments entirely. Fixes #857 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address code review: keep the gates in CLAUDE.md, name the one-time fork conflict, refuse empty pins - The gate order (/pre-pr, /security-review, /code-review, format) is a platform rule, not Hub identity, so it stays in the committed CLAUDE.md rather than only in a gitignored CLAUDE.local.md. - A fork that replaced the block between the old markers conflicts once on the sync that removes it; CLAUDE.md and CUSTOMIZATION.md now say so instead of claiming it merges cleanly. - Create refuses any agentVersionId that is present, so an empty string is refused instead of being silently dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ment, coverage note, MCP client docs, seam table (t-867 to t-872) (#1046) * Cap the CI test jobs at 30 minutes so a hung run is cancelled (t-867) A vitest worker that deadlocks during module evaluation (an import cycle closed through a vi.mock async factory) hangs before any test starts, so no vitest timeout fires and test-full / test-changed sat for GitHub's six-hour default (#843). Both now carry timeout-minutes, default 30, raisable through the CI_TEST_TIMEOUT_MINUTES repo variable so a fork with a slower suite need not edit ci.yml. Shards take 2-4 minutes on Sunrise. Corrects the docker job comment and ci.md's convention that every other job runs processes that end on their own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Reject always-run test entries that point outside the repository (t-869) Dropping the tests/ prefix check (#763) also dropped repo containment: the runner keeps an entry when existsSync(resolve(root, path)), and resolve leaves root freely, so ../other/x.test.ts or /tmp/x.test.ts passed every check and was then silently skipped (#765). validateAlwaysRun now rejects an absolute path (POSIX, Windows drive, backslash root) or one with a .. segment, with a regression test per form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Correct the coverage-widening note and the exemption reasons (t-870) v0.11.2's notes said only .mjs joined the per-file coverage floor, but coverageTargets admits .js, .jsx, .cjs and .mjs; a fork that checked as told missed its .jsx (#698). The shipped note stays as it was; the correction goes in Unreleased so the next release carries it. Two NOT_EXEMPT_DESPITE_COVERAGE_EXCLUSION reasons claimed "4f still asks" about files it cannot read: sourceStem handles only .ts/.tsx. lib/app/eslint.config.mjs's reason now says 4f never reports it today and why the entry stays here rather than in PATH_EXEMPTIONS; the spikes reason scopes the claim to the .ts spikes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Stop telling MCP users to paste keys into Claude Desktop (t-871) The MCP guide and the dashboard's Client Configuration card both said an smcp_ key goes into claude_desktop_config.json. Claude Desktop's remote connectors have no header field, so that connection can never work (#896, split from #820). The guide now names the clients that can send the Authorization header (Claude Code and Cursor, with examples), says sign-in-only clients cannot connect until OAuth lands (#820), and moves that case into the "When OAuth becomes necessary" table in place of the row claiming no client refuses bearer auth. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * List footer.ts and tenant-resolver.ts in the fork guide's seam table (t-872) CUSTOMIZATION.md §4's "Edit this file" table had rows for 38 of the 40 lib/app/ scaffolds. A fork looking there for the copyright line found no footer.ts and could edit the platform footer instead, carrying a conflict on every upgrade (#735). Both rows added. So it cannot drift again, versioning-seam-coverage.test.ts (#732's derived roster) now also checks the table against lib/app/ in both directions, reading only rows of the table, not paths in the prose around it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bound the CI Next.js build cache so it starts afresh past a limit (t-868) The Build job restores .next/cache through a restore-keys chain and saves it under a per-commit key, so each cache is the last one plus this build's output until package-lock.json changes. Sunrise's grew from ~0.5GB to ~2GB compressed between lockfile changes; a fork's reached 5.5GB and a 37-minute Build was cancelled (#856). A step before npm run build measures the restored cache on disk and deletes it past CI_NEXT_CACHE_MAX_MB (default 2048), logging the size either way. 2048 is about three times a fresh Sunrise cache, measured at 674MB on disk (501MB compressed) from a clean local build. The variable reaches the script through env, not interpolation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Name CUSTOMIZATION.md in the seam-coverage guard's always-run reason (t-872) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Let a re-run save a fresh build cache; say the cache limit is on-disk (t-868) Review round 1. The Next cache key ended in github.sha, so re-running a commit restored an exact primary-key hit, and actions/cache skips its save on one: a cache the bound step discarded was never replaced. The key now ends in github.run_attempt too. ci.md now says the limit is measured on disk while the cache list and the restore log report compressed sizes (Sunrise's ~2GB is ~2.7GB on disk), and why smoke / smoke-multi stay uncapped under the "cap anything that can wait forever" rule (their Postgres is a health-checked service container). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Stop naming Claude Desktop as a working MCP client anywhere (t-871) Review round 1. The MCP page, its resources and prompts screens, the admin MCP doc and the top of mcp.md still gave Claude Desktop as the example of a client that connects, contradicting the Client Configuration card on the same screen. Examples now name Claude Code or Cursor, and the admin doc says sign-in-only clients cannot connect until OAuth (#820). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give test-changed a 60-minute default; read the cache limit as base 10 Review round 2 (t-867, t-868). test-changed runs the whole suite unsharded when a lockfile or vitest-config change makes --changed select everything, so it now defaults to 60 minutes rather than a shard's 30; CI_TEST_TIMEOUT_MINUTES still sets both. The cache-bound comparison forces base 10, since bash read a leading-zero limit (08, 0100) as octal and either failed the step or silently used 64MB. ci.md no longer claims the smoke jobs are safe uncapped: their service start-up is health-checked, but nothing bounds the scripts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Suggest naming an MCP key "Cursor", not "Claude Desktop" (t-871) Review round 2: the key form's example named a client that cannot connect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Rewrap the CI_TEST_TIMEOUT_MINUTES CHANGELOG bullet Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…(#1047) * Record an account deletion durably and finish it on the tick (t-842) A deletion now records its intent on the User row and disables the account (sessions deleted, API keys revoked, sign-in refused) in one transaction before any erasure work starts. If the erasure fails or the request is killed, the pendingErasures platform job finishes it. erasureNextAttemptAt is both the lease and the backoff, so two attempts never run at once. Both delete routes go through requestUserErasure(); a repeat while another attempt holds the lease is a 409. An admin whose deletion is pending no longer counts towards the last-admin check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Prove pending erasures against Postgres: smoke:erasure-pending (t-842) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document pending erasures, the pendingErasures job and the 409 (t-842) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test pending erasures: the service, sign-in refusal, retrier job and both routes (t-842) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Take a lease-less pending erasure first, and correct the actor-fallback comment (t-842) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix code-review round 1 on pending erasures (t-842) - A retry whose requesting admin has since been erased names the person instead: the admin's id on an audit row failed the FK every attempt, so the person was never erased. Proved in smoke:erasure-pending. - The lease runs from the claim's own clock, not the retrier run's start. - The failure backoff is written only while this attempt's lease stands, so it cannot cut short a newer attempt's lease. - The retrier runs every tick, one erasure per run, and wakes the idle gate on a claim and while anything is pending. - resolveApiKey refuses a key whose owner is being deleted, closing the key a cached session could mint after revocation. - Index erasureRequestedAt, which the retrier filters on, instead of erasureNextAttemptAt (new migration; the first is already applied). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix code-review round 2 on pending erasures (t-842) - The subject-access export leaves out a pending erasure's requester (another person's id) and the retrier's attempt count and next attempt. - Self-delete answers a repeat with a 409 it returns rather than throws, so it is not logged as a failure, and clears the cookies: the sessions are gone. - The last-admin count always includes the caller, so a caller whose own deletion is pending is not told they are the last admin. - The "erasure requested" log names the actor and reason the erasure records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix code-review round 3: erasing an admin clears them as a requester (t-842) erasureRequestedBy has no FK, so no SetNull reached it, and an erased admin's id outlived them on every pending erasure they had asked for. eraseUser now clears it in its transaction. Proved in smoke:erasure-pending. Documents the export omission from round 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say what a 409 and a failing erasure cost, in the pending-erasures doc (t-842) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Compute the MCP key rotate test's expiry relative to now (t-873) The test sent a fixed 2027-01-01 expiry to a schema that requires expiresAt to be in the future, so it would start failing on that date. Checked by pinning the clock to 2027-06: the old test returns 400, the new one passes. Fixes #904 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Skip the useLocalStorage write when the value is unchanged (t-874) An updater returning prev still serialised, wrote localStorage and dispatched the same-tab event. The setter now returns early when the resolved value is Object.is-equal to the current one. The test pairs the no-op with a real change on the same spies, so it cannot pass because the spies see nothing (the first draft spied on Storage.prototype, which happy-dom does not use, and passed vacuously). Fixes #937 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Install tw-animate-css so the UI overlays animate (t-875) The components/ui/ overlays (dialog, alert dialog, dropdown, popover, select, tooltip) use animate-in / fade / zoom / slide-in-from-* classes that nothing defined, so they appeared instantly. Adds tw-animate-css 1.4.0 beside tailwindcss and imports it from globals.css. Checked by compiling globals.css through @tailwindcss/postcss: every class the overlays use is generated with the import, and none without it. Fixes #682 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Make the local lint out-of-memory remedies take effect (t-876) Three traps, each a silent no-op for someone following the docs: - chunked-lint read only LINT_CHUNKS, but every doc names CI_LINT_CHUNKS (only ci.yml bridged the two). It now falls back to CI_LINT_CHUNKS; LINT_CHUNKS still wins so CI's bridge is unchanged. - withHeapCap ignored NODE_HEAP_MB, so lint and lint:ci disagreed when it was set. It now resolves it through run-capped's own parser. - lint-memory-probe printed an arm with no measurement as "0.00 GB ok", so the arm the OS killed looked like the best. It now keeps the exit code and reports KILLED(rc) or NO-DATA, with "n/a" for the peak. The two script fixes have tests that fail without them. The workflow change was checked by simulating its verdict block; it runs only on manual dispatch. Fixes #699 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give the three lib/app guards one rule for what a scaffold is (t-877) versioning-seam-coverage classified lib/app/ with a deny-list that counts directories and any extension; defaults.test kept top-level .ts|.mjs files; fork-init-seams kept top-level .ts. A nested lib/app/<name>/ seam or a .mts file was caught by the first and silently skipped by the other two. lib/app/ is flat, so they agreed by accident. The classifier moves to tests/helpers/app-scaffolds.ts with a recursive scaffoldSourceFiles() for the export scans, and all three read through it. The helper's test drives a temp fixture with a nested seam and a .mts file. Checked end to end by dropping lib/app/zz-sub/seam.ts and lib/app/zz-theme.mts into the tree: all three guards now fail (before, only the versioning one did); the files were removed afterwards. Fixes #734 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document that useLocalStorage skips unchanged values (t-874) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Compare useLocalStorage's skip against storage, not state (t-874) The Object.is check against in-memory state dropped three writes the old code made: persisting a never-stored default (so another instance with a different default never heard), retrying after a failed write, and an updater that mutated prev in place. The setter now skips only when storage already holds the same serialised value, which still covers the updater-returning-prev case from #937. Each new test fails against the previous setter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pin the heap test's machine and name the chunk variable read (t-876) The NODE_HEAP_MB test guarded itself with a resolveHeapMb comparison that collapses on a box under 4GB or under a large inherited heap limit. withHeapCap now takes the machine facts as deps, and the test pins them. A bad chunk count is reported under the variable that supplied it, so a stale LINT_CHUNKS overriding CI_LINT_CHUNKS is visible. The probe's result column is widened for KILLED(137). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Use the shared APP_DIR in the seam guard, with a separator-safe prefix (t-877) The guard kept its own APP_DIR beside the helper's, and its startsWith exclusion would also have dropped a sibling such as lib/application/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Skip a useLocalStorage set only when state and storage both match (t-874) Comparing against storage alone stopped broadcasting a set that an out-of-step instance needed, and read storage on every keystroke. The skip now needs the value unchanged in memory and already stored, and reads storage only in that case. The hook doc says an in-place mutation persists but, like useState, does not re-render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Read and parse the chunk count in one call (t-876) chunksFromEnv now parses as well, so main destructures deps once, and the default name in parseChunks no longer blames LINT_CHUNKS for a value a caller took from CI_LINT_CHUNKS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Return a 400, not a 500, when an invitation delete has a malformed address (t-878) DELETE /api/v1/admin/invitations/:email decoded its segment unguarded, so a stray `%` threw URIError and became a 500. The decode is now caught as a ValidationError, and the result must be an email address. It is not trimmed or lowercased, so a valid address is looked up exactly as before. Fixes #954 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Leave built-in agents out of the agent export, so the file imports again (t-879) A built-in agent starts with an empty provider and model, which the bundle schema rejects, so an export that included one failed import as a whole. The import would have skipped those agents anyway, and the backup export already leaves them out. The agent export now does the same. A selection of only built-in agents is a 400, and the agents table shows that reason instead of its generic "try again" message. Fixes #721 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Let an admin reopen the PDF preview for a document left in Needs Review (t-880) Closing the PDF preview left the document in pending_review, and its Review button opened the chunks view, which is empty before confirmation. The only way back was for the original uploader to upload the same file again. Review now reads the document and reopens the preview from the extraction the upload already stored, so any admin can confirm or discard it. If the stored text is missing, the list says so. Fixes #807 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Stop the API response helpers inviting a public Cache-Control override (t-881) The successResponse() docblock and the API docs said a route could replace the private default with a public directive. That was never safe. checkConditional() sends the default on its 304, so revalidation reverts it. The proxy can also attach the per-visitor cookie to a first visit's API response, and a shared cache would replay it. Making public caching work needs both changed together, so the docs now withdraw the invitation: a route may tighten the default but not loosen it. Docs only. Fixes #751 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tell the admin when an agent export left built-in agents out (t-879 review) A mixed selection exported without its built-in agents and said nothing, so an admin could believe every selected agent was in the file. The export now sends the number it left out in X-Skipped-System-Agents, and the agents table shows a notice when it is above zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Make the reopened PDF preview safe to use from the document list (t-880 review) Three gaps the review found once Review could open the preview from the list: - Two quick Review clicks could let the slower response replace the preview just opened, so an admin could confirm the wrong document. Every Review button is now disabled while one loads. - Reopening the same document kept text edited and abandoned in the last session, and Confirm would send it. The modal now remounts on every open. - Discard left the deleted row in the list. The modal gains an optional onDiscarded callback, and the list refreshes on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Validate the invitation delete address with the shared email schema (t-878 review) The route had its own email schema that skipped trimming and lowercasing. Every invitation is created through emailSchema, which lowercases, so a delete for Alice@Example.com could never find the stored invitation. Reusing emailSchema fixes that and keeps the rule in one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop stale Review responses, and clear the export notice with its selection (review round 2) - Disabling the Review buttons did not stop a slow Review response from landing after an upload's preview opened, or after its row was deleted. A request counter shared by every way of opening the preview now drops any response a later open or a delete has overtaken. The buttons no longer need locking. - Discard refreshes the embedding status as well as the list, as confirm and delete already do. - The left-out notice on the agents table clears when the selection changes. - The admin endpoint docs list the invitation delete's 400 and its lowercased lookup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… (#1051) * Let operators set how long failed webhook deliveries are kept (t-884, #838) webhookDlqRetentionDays was read by the retention prune but nothing could write it globally: no field in the settings PATCH schema, no route branch, no form control, and backups dropped it. Only an org's retention slice could set it. Wire it through the settings schema, route, OrchestrationSettings, the Retention card (blank = use the webhook window) and backup round-trip. An older backup without the field imports it as null, as the other later-added windows do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bound the backup's failed-webhook window and keep it on an older restore (t-884) Review fixes. The backup schema accepted any number, so a backup with 0 or a negative window would prune every dead-lettered delivery on the next tick; it is now held to the PATCH bound (int, 1-365, or null). It is also no longer defaulted to null: a backup taken before the field existed restores without clearing a window set since. The hydrate and form types now require the field, like their neighbours. Not taken: a refine tying it to webhookRetentionDays (a shorter window for failures is a legitimate choice), and a way to keep failures forever while pruning the rest (what null means is unchanged, and documented as a gap). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…un (t-843) (#1050) * Keep a running workflow from writing an erased person back into its run (t-843) Every engine write of a run (checkpoint, approval pause, finalise) now runs in a transaction: the write locks the row, then the run checks for erasure receipts since it started. When there are any, the trace, input, step outputs and error message are redacted in memory by erasure's run rule and the row is written again before commit. Erasure makes one more pass over recently written runs after it commits, for a write that landed while its transaction was open on a row its pass had not locked. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Test the run-write erasure check and the pass after the commit (t-843) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Smoke a running workflow against an erasure in Postgres (t-843) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Document what a running workflow's writes do after an erasure, and assert the whole trace in the sends smoke (t-843) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Record the running-workflow erasure fix in the CHANGELOG (t-843) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Allowlist the run-write receipt query and count the erasure's run pass after the commit in the user-deletion tests (t-843) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Lock every recent run in erasure's pass after the commit, and take its subject from the transaction (t-843, review round 1) The pass after the commit narrowed by the person's text, which Postgres tests against a row's committed version before it waits for the row's lock, so a running workflow's write still open when the pass scanned was skipped. It now locks every run written since or not finished. Its subject now comes from the transaction, so a failed pre-pass lookup no longer skips it. An id that is only a JSON key no longer counts as named, since redaction cannot remove it, and the pause no longer redacts its trace entry twice. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Lock each run on its own in erasure's pass after the commit, and keep the terminal event and hook clear of the erased (t-843, review round 2) The pass after the commit lists the runs still pending or running, or written since, by id, then locks and redacts each in a short transaction of its own. That waits for a running workflow's open write without holding a page of locks, and leaves paused runs the transaction's pass already reached. The completion event and the workflow.failed hook now carry what the final write wrote. The follow-ups are filed as t-882 (supervisorReport, running-step turns) and t-883 (approval, rejection and the scheduler's failure write). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Count the user-deletion transactions again now the pass after the commit opens one per run it finds (t-843) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Keep erasure's pass after the commit going past a run it cannot lock, and give it the engine's connection wait (t-843, review round 3) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ry tests (t-889) (#1054) * Count only core's platform agents in the tenancy smoke and the reconcile test (t-889) A fork that registers its own platform agents through lib/app/platform-agents.ts failed two core checks that compared the live registry with core's counts. - Smoke [11]: pin the 12/4 split on CORE_PLATFORM_AGENTS and require each core slug in the registry's list for its audience; count the install org's rows by registered slug, so a fork's own seeded isSystem agent is not counted (#973). - Reconcile test: hold the fork seam empty, so the real-registry case measures core's registry and never runs a fork definition against the stand-in. Closes #974. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Say that core's platform-agent checks leave a fork's registrations green (t-889) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Let the smoke take a fork's replaced core agents, and require registered agents active (t-889 review) - Require every core slug registered, whatever its audience, instead of pinning core's 12/4 split by audience: a fork may replace a core slug and move it, and the split is already pinned by the unit tests. - Install org: every registered slug present AND active, one condition. - Docs: name defaults.test.ts as the row a fork pins, and the core slugs the smoke drives by name. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Hold the fork seam empty in the guard and clone tests, and drop a smoke check that could not fail (t-889 review round 2) - platform-agent-guard.test.ts and agents.id.clone.test.ts read the real seam through isReservedAgentSlug, so a fork registering the docs' own example failed them: the same #974 defect, missed by a grep for two function names. - Smoke [11]: the 'registry holds every core slug' check could never fail (nothing removes a slug); keep only the non-empty guard the absence checks need. Install org: presence by registered slug, without the isActive requirement round 1 added (a fork may ship a dormant agent). - Docs: claim only what holds, and name what still notices. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Take the seam mock back out of the clone test: no fork registration fails it (t-889) Measured: with a fork registering 'my-source-copy' the case moves past it and passes with or without the mock, so the mock guarded nothing a fork would hit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Say which core tests hold the platform-agent seam empty (t-889) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ewer Claude models from OpenRouter (t-890, t-891) (#1055) * Stop sending temperature to Claude models that refuse it (t-890) Claude Opus 4.7+, Sonnet 5+, Haiku 5+, Fable and Mythos 400 on any sampling temperature. The Anthropic provider sent the agent's temperature whenever thinking was off, so an agent on claude-haiku-5-5 failed every call (#986). acceptsSamplingTemperature() is an allow-list of the families that still honour one (Claude 2/3.x, Haiku 4.x, Opus/Sonnet 4.0-4.6), so an unrecognised model drops the temperature rather than failing. The caller's value is still recorded on the trace's requestParams. A family heuristic beside supportsReasoningEffort rather than a matrix column: it needs no migration, and every Claude release since 4.6 has gone the same way, so the default for an unknown id already covers the next one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Price newer Claude models from OpenRouter under their API ids (t-891) OpenRouter writes Anthropic versions with a dot (claude-haiku-5.5); Anthropic's API, and so every agent, uses a dash (claude-haiku-5-5). The registry filed OpenRouter's entry only under the dotted ids, so turns on the newer models had no known price and cost caps couldn't count them (#987). refreshFromOpenRouter now also files each Anthropic entry under its dashed API id. An entry OpenRouter lists under that exact id wins over the alias. A matrix row on the same id is treated as for any model OpenRouter lists: its descriptive fields stand and the published split price wins (t-774). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say which Claude models get no temperature, and how OpenRouter's dotted ids are filed (t-890, t-891) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give OpenRouter's Anthropic entries the API id as their own, and share the Claude id normaliser (t-890, t-891) Code review, round 1. The alias overwrote the static fallback's dashed entry with an object whose id was still dotted, so listings and the discovery merge (keyed on id) lost the API id and offered one Anthropic rejects. The entry now takes the dashed id as its own and keeps the dotted one as an alias. Bedrock cross-region ids (us./eu./global.anthropic.) never matched the Claude checks, so an old model through such a proxy lost its temperature. One normaliser now serves acceptsSamplingTemperature and supportsReasoningEffort, in place of two copies of the prefix strip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Leave OpenRouter's Claude variants on their own ids (t-891) Code review, round 2. anthropicApiId turned `claude-3.7-sonnet:thinking` into `claude-3-7-sonnet:thinking`, an id Anthropic's API does not have, and listed it under that name. A `:variant` id now keeps OpenRouter's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…use budgets (t-894) (#1056)
* Send adaptive thinking, not a token budget, to Claude models that refuse budgets (t-894)
An agent on Claude Opus 4.7+ or Sonnet 5+ with a reasoning effort set
failed every call: the Anthropic provider sent
thinking: { type: 'enabled', budget_tokens: N }, which those models
reject with a 400.
acceptsThinkingBudget() is an allow-list of the families that still
take a budget (Haiku 4.x, Opus/Sonnet 4.0-4.6), matching
acceptsSamplingTemperature, so an unrecognised model gets adaptive
thinking. On the rest, low/medium/high send thinking: { type: 'adaptive' }
and output_config.effort of the same name. minimal sends effort 'low'
with no thinking field: Opus 5.5 and Sonnet 5.5 400 on
{ type: 'disabled' }, and leaving thinking out runs Opus 4.7/4.8
without thinking while the always-on models run at their lowest
effort. Models that take budgets are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Narrow the reasoning effort through a local so the thinking branch type-checks (t-894)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Share the Claude 4.6 cutoff between the temperature and budget rules; document adaptive thinking on reasoningEffort (t-894 review)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…uest responds (t-846) (#1057) * Keep a scheduled, recovered or resumed run going after the response (t-846) The scheduler's four engine drains and the inbound and approval routes' started runs were bare void promises. On a serverless host work left running after the response can be frozen, stalling the run until recovery or the reaper finds it. Each is now handed to Next's after() through keepRunningAfterResponse, which falls back to running on the process where there is no request scope (the dev ticker, scripts, tests). The inbound and resuming approval routes declare maxDuration = 300, as the maintenance tick does, so the drain gets the same budget on every plan. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Prove each route hands its started run to after() (t-846) Inbound, chat/embed approve and admin approve: after() is handed a promise that stays pending until the run or resume settles. The legacy token approve route, which does not resume, hands it nothing. The helper logs a failure as '<label> failed', keeping the admin approve route's existing message. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Say that every engine drain started from a request outlives the response (t-846) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Let drainEngine keep itself alive, and hold a resume until its run settles (t-846) Review round 1: - drainEngine registers its own run with keepRunningAfterResponse, so a bare void drainEngine(...) is safe again and five hand-written wrappers go. - resumeApprovedExecution awaits the drain, so the route's one hold covers the lookup and the run; its docstring no longer says fire-and-forget. - The schedules tick route sets maxDuration = 300 like the other routes that start drains. - No request scope is recognised by Next's error code E468 first, then by the message. - The admin approve test asserts the handed promise stays pending until the resume settles. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Say that drainEngine keeps itself alive, and what it does not hold yet (t-846) The engine's detached webhooks, hook dispatch and lease events still end with the drain's hold; filed as t-903. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Answer review round 2 (t-846) - Route comments and docs say a cut-off run is recovered by the pending sweep or the orphan sweep, and that each orphan re-drive uses a recovery attempt. - The maintenance tick hands its chain to keepRunningAfterResponse too, so a refusing after() logs rather than turning a committed tick into a 500. - drainEngine forwards runDrain's parameters rather than restating them. - Two doc sentences garbled in round 1 rewritten. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the response (t-903) (#1059) * Keep the webhooks, hooks and lease events a run detaches going after the response (t-903) t-846 held each engine drain past the response, but not the work the drain starts without awaiting. On a serverless host that work could be frozen once the drain settled: an approver's approval_required notification, a crash notification, hook deliveries, lease-inspector rows. - dispatchWebhookEvent, emitHookEvent and recordLeaseEvent register their own work with keepRunningAfterResponse, so every caller that does not await them is covered, inside these modules and outside (streaming chat, the circuit breaker, escalation, the agents route). - emitHookEvent's dispatch now settles once each hook's first attempt has, so its one hold covers the deliveries. - The engine's approval_required IIFE, the scheduler's two execution_crashed IIFEs, the engine's parallel current-step writes and both manual retry attempts are wrapped in the helper. In-process retry timers are left as they are: the tick's retry sweeps pick them up if the process is gone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Say that what a run detaches is held past the response too (t-903) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Answer review round 1 (t-903) - logCost registers its own write too: the engine's llm-runner, agent-call and chat-turn steps, and every other caller, write it with void, and a lost cost row is spend the budget caps never see. - The parallel batch's markCurrentStep goes back to void: the drain's own hold covers it while the batch runs. - scheduling.md says the self-registering functions are held when called while a request or an enclosing after() task is open, not from a timer after everything has settled. Declined, with reasons in the PR: a shared display-name dispatch helper (the three payloads differ), skipping registration for awaited calls (negligible), and not awaiting hook first attempts (that wait is the point, bounded by DISPATCH_TIMEOUT_MS.hook). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Answer review round 2 (t-903) - The workflow_failed webhook (engine/events.ts) and the budget_exceeded webhook (chat streaming handler) are wrapped where they start: both read display names before dispatchWebhookEvent registers its own hold. - emitHookEvent's docblock says only first attempts are held, not retries. - Tests prove the workflow_failed dispatch and both manual retry attempts stay held until they settle. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ng when it needs longer (t-845) (#1061) * Answer an account deletion within a request budget, and show it pending (t-845) eraseUser() takes an optional deadline. It stops at the next safe point once the deadline passes (avatars, each hook, each pre-pass chunk or page, each transaction try) and throws ErasureDeferredError. Work already running is capped at the time left through statement_timeout, lock_timeout and the transaction timeout. Without a deadline nothing changes. requestUserErasure() erases within ERASURE_REQUEST_BUDGET_MS (8 s, overridable by env) and answers 'pending' when it runs out, leaving the next attempt due at once. Both DELETE user routes answer 202. The retrier gives each attempt ERASURE_RETRY_BUDGET_MS. The delete-account form shows the deletion in progress, and the admin users table shows a 'Deletion pending' badge with no delete action. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the erasure request budget and the 202 pending answer (t-845) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the delete-account dialog open while the deletion runs (t-845) The confirm button is Radix's Dialog.Close, so the dialog closed before the API answered: a 202 never showed 'Deletion in progress', and an error was set on a closed dialog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the erasure deadline, the 202 pending answer and its UI (t-845) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Allowlist the erasure deadline's set_config, and expect erasureRequestedAt on the users page (t-845) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Show a 409 on delete-account as a deletion in progress, not an error (t-845) The route signs the person out before answering 409, so leaving the error in an open dialog stranded them on a page with a dead session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the retrier's erasure finishing: time for the transaction, real failures, the tick's limit (t-845) Code-review findings: - The retrier's pre-passes stop 60 s before its deadline (prePassDeadline) and hand the rest to the transaction, so an erasure whose scans outlast the budget no longer defers at the same scan every attempt. - Only a timeout after the deadline is a deferral (isTimeoutError); any other error is a failure, logged and backed off. - A deferral no longer counts toward erasureAttempts. - An attempt that finds the account already erased answers with the receipt, deferral or not. - The retrier ends by the tick's own limit (stopBy) and claims nothing with too little of it left, so the host never kills it holding its lease. - boundLockWaits takes statementsToo in place of a second set_config helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the delete-account dialog open until the request answers (t-845) Closed mid-request, a later 202 or 409 left the person signed out on the settings page with no message and no redirect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Back off the retrier's deferrals, and hold the pass after the commit to the deadline (t-845) Code-review round 2: - A transaction timeout in the retrier, which had its 60 s set aside, is a failure, not a deferral. - A retrier deferral is counted and backed off like a failure and logged as a warning; only a request's deferral is due at once and uncounted. A stuck erasure no longer retries every tick unseen. - The run pass after the commit starts no run past the deadline, and caps each run's wait at the time left; what it leaves is logged as an error. - TICK_MAX_DURATION_MS replaces the private tick-limit copy; the route test holds it equal to maxDuration. - isTimeoutError reads Prisma's error meta through a Zod schema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Leave the page once when Continue closes the pending-deletion dialog (t-845) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the deadline to the request: the retrier and the pass after the commit run to the end (t-845) Code-review round 3 found the retrier's time limit was the source of most round 1 and 2 findings: on a long-running host it could keep a heavy erasure from ever finishing, and holding the run pass after the commit to the deadline could skip it for good, with nothing to redo it. - The retrier has no deadline again, as before t-845; a run killed by its host is retried when its lease runs out. prePassDeadline, stopBy, ERASURE_RETRY_BUDGET_MS, ERASURE_MIN_RETRY_MS, TICK_MAX_DURATION_MS and the deferred count are gone. - The run pass after the commit runs in full again. - A bounded transaction measures its statement cap once it has a connection, so the connection wait is not added on top; the same in waitForSubjectLocks. Kept from earlier rounds: only a timeout is a deferral, an already-erased account answers with its receipt, a deferral is uncounted, and the 409 and in-flight dialog handling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…(t-904) (#1063) * Take October's Dependabot bumps: Next 16.4, vitest 5.0.5, vite 8.3.5 and 43 more (t-904) Folds the three open npm Dependabot PRs into one branch off current main, because each rewrites package-lock.json and they overlap on vite: - #996 production-minor-patch (30 bumps); next taken to 16.4.2 rather than 16.4.0, to match eslint-config-next / @next/eslint-plugin-next 16.4.2 - #1040 development-minor-patch (16 bumps) - #1060 vite 8.3.5, which supersedes #1040's 8.3.4 @react-email/ui 6.11.3 now pins next 16.4.1, outside the RCE range its override was added for. The override stays, so the preview server keeps sharing the app's next instead of installing a second copy; its reason is restated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Keep Next's agent rules in a committed AGENTS.md, imported from CLAUDE.md (t-904) Next 16.4 writes its agent-rules block only to AGENTS.md; 16.3 also kept it in CLAUDE.md when it was already there. Without a committed AGENTS.md, the first next dev run under an agent leaves an untracked file in every checkout and every fork. AGENTS.md is generated by Next's own writer, so next dev finds it current and leaves it alone (checked with a real next dev run). CLAUDE.md's frozen copy is replaced with an @AGENTS.md import. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Say when the @react-email/ui next override needs revisiting (t-904) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Bumps the github-actions group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [trufflesecurity/trufflehog](https://github.com/trufflesecurity/trufflehog). Updates `github/codeql-action` from 4.38.2 to 4.38.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](github/codeql-action@v4.38.2...v4.38.3) Updates `trufflesecurity/trufflehog` from 3.97.9 to 3.99.2 - [Release notes](https://github.com/trufflesecurity/trufflehog/releases) - [Commits](trufflesecurity/trufflehog@v3.97.9...v3.99.2) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.38.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: trufflesecurity/trufflehog dependency-version: 3.99.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…katex alerts (t-914) (#1064) * Take Prisma 7.10.1 and mermaid 12.2.0, clearing the deepmerge-ts and katex alerts (t-914) Two of the four Dependabot alerts left open after #1063 have fixes inside their parents' current majors, released after Dependabot's October run: - deepmerge-ts (high, GHSA-ggr8-5vv4-36mx): Prisma 7.10.1 moves @prisma/config to deepmerge-ts 8.0.2. prisma, @prisma/client and @prisma/adapter-pg move together. - katex (low, GHSA-238p-pmpm-9mq7): mermaid 12.2.0 depends on katex ^0.18. Prisma 7.10.1 also pins mysql2 to 3.24.4, which meets the prisma override's own removal condition (3.23.1 or later), so the override and its reason go. mysql2 resolves to 3.24.4 rather than 3.24.5 as a result, still past both advisories the override was added for. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Tell forks the deepmerge-ts alert is fixed and the prisma override can go (t-914) The 0.14.0 Security entry told forks to merge the prisma -> mysql2 override, and the 0.13.0 notes said only Prisma 8 would fix deepmerge-ts; both are answered in [Unreleased]. The security overview's override count was stale (it named two of three), so it now points at overrideReasons instead of restating a list. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Cut Sunrise 0.15.0 (t-888) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Give the 0.15.0 notes the suite's measured size (t-888) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Correct the 0.15.0 notes against the code (t-888) Review of the release blockquote against the migrations, code and git: - eight migrations, not five, take locks that scale with table size: the audit-log support-grant indexes and FK, and the two indexes on user; - delivery_destination_index builds a B-tree and a GIN index on one table; - the delivery seed fills destination only where the parent still exists, and split prices reach only uncustomised seeded rows; - a fork that replaced canAdminister must gate on scope.tier; - <AdminTierGate> is new, so a fork's own admin layout must add it; - the owner layer's scope and its silent findFirst narrowing; - only a memberless user with no signup record loses the install-org heal; - braces / micromatch / fast-glob are still in the tree and accepted; - the Prisma 8 note was 0.12.0's, here and in the t-914 entry. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nsole, support access Merges v0.15.0 (135 commits, v0.14.0..v0.15.0) in one stage. 34 conflicts, resolved by taking upstream's change and keeping the fork's additions on top. - RESPARKABLE_VERSION is 0.15.0. - Platform jobs: upstream's new pendingErasures job sits after platformAgents; the fork's transferJobs and transferArchiveExpiry follow it, unchanged. The job tests, the tick route tests and scheduling.md list all three. The tick route test keeps the fork's count derived from PLATFORM_JOB_NAMES rather than upstream's literal 11. - Sunrise asks taken upstream, so the fork's local copies give way to upstream's: the router-mock scanner (ask #52, sunrise#936) and the host-coupled heap assertions in chunked-lint.test.ts (ask #45, sunrise#702). The duplicate check:router-mocks script is dropped. - The tenancy smoke's platform-agent count takes upstream's registered-slug check, which is the fork's #973 fix generalised. - Admin layout takes upstream's tier gate and sidebar props on the fork's lattice-field background. AppHeader keeps the fork's sticky lattice panel and takes upstream's small-screen truncation. - RouteErrorBoundary's reset is now retry: the /resparkable error page follows. - Erasure doc: upstream's steps 5-8 come first, the fork's transfer-archive cleanup is step 9; transfer jobs stay in the cascade/SetNull table. - CLAUDE.md: upstream's optional Hub section and @AGENTS.md ending, the fork's doc rows and .test troubleshooting kept. AGENTS.md committed. - Lockfile seeded from v0.15.0's and reconciled with --package-lock-only: upstream plus the fork-only packages. No version floated. - CHANGELOG: fork [Unreleased], then upstream's 0.15.0 verbatim. - model-graph.generated.ts regenerated for the merged schema. Not yet done: the owner layer's effect on the fork's reads (§115), routing the fork's account deletion through requestUserErasure(), erasure-hook idempotency, migrations, validate and the full test run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ransfer guards - Owner layer (§115): TransferJob is personal (not enforced); ResparkableGroupMember and ResparkableGroupAuditEntry are not personal. Registered from lib/app/owner-classification.ts through lib/framework/resparkable/owner-classification.ts. The billing sweep's raw read of ai_workflow_execution goes in the raw-SQL manifest: it runs from the tick, where nobody is narrowed. The fork-tier test leaves TransferJob out of its core half, since the model lives in platform.prisma. - Capability seed parity (#545, #646): the 24 capabilities are paired with their classes in lib/app/capability-seed-pairs.ts, derived from the dispatcher's registration list. Seeds 004 and 006 read capabilities, write none and throw by design when one is missing, so they are unchecked. - Account transfer: User.signupOrgId and the five erasure columns are regenerated, never imported, so a bundle cannot move the importing account into another org or schedule its deletion. OrgSupportGrant is excluded; AiAdminAuditLog.supportOrgId is a dismissed soft reference. - CUSTOMIZATION.md lists the fork's lib/app/robots.ts seam. - The erasure-hook comments no longer say a boot registration may be missing: 0.15.0 keeps the registry on globalThis (sunrise#691). - Each filled seam is pinned in defaults.test.ts under a FORK note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he CHANGELOG entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core runs initAppErasureHooks() itself before it first reads the hook registry, in whatever process is erasing somebody. Registering from initResparkable() only covered a process that had booted the app, so a script calling eraseUser() directly skipped the hook and left unaccepted invites and stored document originals behind with no error. Core's erasure tests assume the seam ships empty, so they mock it empty under FORK notes; defaults.test.ts pins the real registration. Also records ask #44 as landed (sunrise#691) and rewrites install.md section 2.13, which still said the hook was gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Next 16.4.2 fails the Docker build resolving Google font files now that Google serves them at extensionless URLs (vercel/next.js#100277). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Brings Resparkable up to Sunrise 0.15.0: owner isolation, account deletion that is recorded first and finished in the background, the org-admin console and vendor support access.
TENANCY_MODEstayssingle, so the org-admin console, per-org quotas and support access arrive switched off.What the fork had to change
pendingErasuresjob runs before the fork'stransferJobsandtransferArchiveExpiry. The fork's erasure hook and the transfer-archive cleanup ineraseUser()are both safe to run again on a retried attempt.TransferJobis personal (not enforced);ResparkableGroupMemberandResparkableGroupAuditEntryare not personal. Registered from the newlib/app/owner-classification.tsseam.lib/app/capability-seed-pairs.ts.User.signupOrgIdand the five erasure columns are regenerated, never imported, so a bundle cannot move the importing account into another org or schedule its deletion.OrgSupportGrantis excluded.lib/app/erasure.tsinstead ofinitResparkable(). Core runs that seam itself before it first reads the hook registry, so the hook is in place in any process that erases somebody, not only one that booted the app. A script callingeraseUser()directly used to skip it silently, leaving unaccepted invites and stored document originals behind. Core's erasure tests assume the seam ships empty, so they mock it empty underFORKnotes;defaults.test.tspins the real registration.install.md§2.13 rewritten: it still said the erasure hook was gone, and a host project now has to wire it. CHANGELOG entry for the merge.Checks run locally
npm run test:changed: 1,786 files, 36,744 tests passed.npm run validate, migrations against the dev database,db:seed, drift probes,/security-reviewand/code-review.Before deploying to a real database
npm run db:seed. Eight take locks that scale with table size (the delivery tables,ai_cost_log,ai_admin_audit_loganduser; upstream's 0.15.0 notes list them), so apply them off-peak.202 pendingor409. Both delete routes callrequestUserErasure().Known limits
TransferJobstays inplatform.prisma, a core file, with aFORKnote inowner-classification-fork.test.ts. That is deliberate: ask Account import/export (transfer) #36 proposes the whole portability engine for core, and if it lands, that file is the model's home.🤖 Generated with Claude Code