Commit Graph

8394 Commits

Author SHA1 Message Date
Katia Bulatova c0a93b015f merge: propagate review-comment fixes from feat/dashboard-agent-flows
📦 Preview packages (pkg.pr.new) / Build and publish previews (push) Has been cancelled
2026-08-10 19:10:19 +00:00
Katia Bulatova 83b7f072bd merge: openChat chat-pointer transient 5xx review-comment fixes 2026-08-10 19:10:03 +00:00
Katia Bulatova 2932b88455 merge: newUIMessages finalize + softDeleteChat org-scope review-comment fixes
📦 Preview packages (pkg.pr.new) / Build and publish previews (push) Has been cancelled
2026-08-10 19:10:02 +00:00
Katia Bulatova bd5f8fd67f fix(dashboard-agent-db): scope softDeleteChat by organizationId 2026-08-10 18:54:21 +00:00
Katia Bulatova 3977c2dbeb fix(dashboard-agent): finalize the turn's own message ids from newUIMessages 2026-08-10 18:54:12 +00:00
Katia Bulatova 70c10923c9 fix(webapp): keep the stored chat pointer when opening a chat fails transiently 2026-08-10 18:52:55 +00:00
Katia Bulatova 2bcd7eb16d merge: propagate org-purge best-effort from feat/dashboard-agent-flows 2026-08-10 16:00:43 +00:00
Katia Bulatova 41fdffedf0 merge: org-delete agent-purge enqueue is best-effort 2026-08-10 16:00:42 +00:00
Katia Bulatova 18379a40c9 fix(webapp): don't let a failed agent-purge enqueue break org deletion 2026-08-10 15:59:38 +00:00
Katia Bulatova 8506877708 merge: propagate wave-2 review fixes from feat/dashboard-agent-flows 2026-08-10 15:37:32 +00:00
Katia Bulatova 876ed564da feat(webapp,dashboard-agent-db): retain and purge deleted agent chats
Soft-deleted chats are hard-deleted with their children after a 30d
window by the maintenance sweep, and organization deletion enqueues a
job that soft-deletes the org's chats so the same sweep removes them.
The FK-free cascade lives in a single reusable deleteChatsByIds helper.
2026-08-10 15:36:04 +00:00
Katia Bulatova d6f67d4161 merge: H3 PAT-bound token revocation + 7d ceiling, H9 stable rate-limit key 2026-08-10 15:35:09 +00:00
Katia Bulatova b14c8f3480 merge: B3 prompt-injection boundary on tool results 2026-08-10 15:35:08 +00:00
Katia Bulatova a97ada590f fix(webapp): recheck the source PAT against the token's own claim, not the plugin's
At the apiBuilder verify site the user-actor claims can come from the RBAC plugin; a plugin image predating the pat claim would deliver pat-less claims and no-op revocation there. Re-verify the bearer locally so the recheck reads the token's authoritative pat. Add recheck tests for the apiBuilder and UAT-preamble sites.
2026-08-10 15:34:03 +00:00
Katia Bulatova 1e97712304 test(webapp): cover the jwt-actor rate-limit branch, not just the helper
Extract the limiter override into an exported function so the actor?.sub decision is testable without redis, and assert both branches (act present -> jwt-actor key; no act -> hashed-token fallback).
2026-08-10 15:33:52 +00:00
Katia Bulatova c18a0737bf fix(webapp): give the agent's env JWT a stable rate-limit key
A delegated env JWT rotates its token value every turn, so keying the limiter on the token handed each turn a fresh bucket. Key on env+acting-user instead, namespaced so it can't collide with private-key buckets.
2026-08-10 15:18:57 +00:00
Katia Bulatova 1acb6b8d20 fix(webapp): bind user-actor tokens to their source PAT
Stamp the source PAT id on a minted user-actor token and recheck it is still live at every verify site, so revoking the PAT invalidates any token created from it. Tokens with no source PAT (e.g. the dashboard agent's) skip the recheck.
2026-08-10 15:18:48 +00:00
Katia Bulatova 2e58b360cb fix(webapp,cli): cap user-actor token lifetime at 7 days
The mint route rejected TTLs only above 365 days; shorten the ceiling to 7 days so a delegated token can't be minted long-lived. Update the CLI --ttl help to match.
2026-08-10 15:18:31 +00:00
Katia Bulatova bf6872d3d5 fix(dashboard-agent): neutralize fence delimiters in payload and fence ignoredReason/commitRef 2026-08-10 15:13:49 +00:00
Katia Bulatova 5a83bcb426 fix(dashboard-agent): tell the agent fenced tool text is data, not instructions 2026-08-10 15:08:36 +00:00
Katia Bulatova 39ba7097ba fix(dashboard-agent): fence and cap untrusted free-text in tool results 2026-08-10 15:05:43 +00:00
Katia Bulatova 99bbc07c88 merge: propagate review fixes from feat/dashboard-agent-flows 2026-08-10 14:34:38 +00:00
Katia Bulatova 94c3a47d02 merge: CSP review fixes (B2 block remote model images, B4 reject CSP delimiters) 2026-08-10 14:32:04 +00:00
Katia Bulatova b962949205 test(webapp): assert StreamdownRenderer drops remote image src end-to-end 2026-08-10 14:31:33 +00:00
Katia Bulatova ca2bacd960 merge: base-misc review fixes (M2, M10a, H8, H13, M10r, ai-help, sweep-cron gate) 2026-08-10 14:29:45 +00:00
Katia Bulatova 237f01a6a5 test(webapp): assert report cache TTL stays within the liveness fresh window 2026-08-10 14:21:19 +00:00
Katia Bulatova 65c60dfbba fix(webapp): reject CSP-delimiter characters in the img-src allowlist
A CSP_IMG_SRC_ALLOWLIST entry containing ';' passed URL parsing and landed verbatim in the space-joined img-src directive, injecting or truncating a directive. Reject entries containing ';' or ',' alongside the existing wildcard and whitespace checks.
2026-08-10 14:21:10 +00:00
Katia Bulatova 502e1bde07 fix(webapp): block remote images in model-authored markdown
Model-authored markdown could emit a remote image whose URL the browser fetches on render — a zero-click data beacon. Constrain the shared markdown renderer with a urlTransform that strips the src of any non-local image and limits links to safe schemes.
2026-08-10 14:20:58 +00:00
Katia Bulatova 3f1eec6546 docs(errors-api-e2e): mint the e2e token with an explicit write cap
A capless user-actor token is now read-only, so the errors API smoke test
must declare read:errors,write:errors on the token to exercise writes.
2026-08-10 14:19:24 +00:00
Katia Bulatova e367280d9c fix(webapp): gate the agent maintenance cron on the agent being configured 2026-08-10 14:19:18 +00:00
Katia Bulatova 6cb06314f5 fix(webapp): clamp an env-JWT's lifetime to the delegated token's expiry
A user-actor token could be exchanged for an env JWT that outlived it by
requesting a longer expirationTime. Surface the token's exp as expiresAt
and clamp the minted JWT to it. Non-UAT exchanges are unchanged.
2026-08-10 14:19:12 +00:00
Katia Bulatova 35ab0c51fc fix(webapp): ceiling a capless user-actor token to read-only on env-JWT exchange
A delegated user-actor token with no scope cap could exchange for an env
JWT carrying any requested scopes (admin included). Ceiling every UAT
exchange by its cap, or read-only when it has none, projecting requested
scopes through the scope grammar so read:all still admits reads.
2026-08-10 14:18:24 +00:00
Katia Bulatova 64eaf2f4a5 fix(webapp): return 400 for ai-help with a missing query param 2026-08-10 14:16:57 +00:00
Katia Bulatova aef86b34e7 fix(webapp): reword the healthy-execution report line to not imply failure 2026-08-10 14:16:56 +00:00
Katia Bulatova 28c28e6935 fix(webapp): cap report cache TTL to the liveness fresh window 2026-08-10 14:15:44 +00:00
Katia Bulatova bb077d4b69 fix(dashboard-agent): time out stalled agent HTTP calls 2026-08-10 14:13:36 +00:00
Katia Bulatova fb4f0b3a22 fix(webapp): stop the agent body cap matching a task named dashboard-agent 2026-08-10 14:12:15 +00:00
Katia Bulatova f93f8d6c26 fix(webapp): detect tables inside a JOIN…ON subquery for query-scope auth 2026-08-10 14:11:46 +00:00
Katia Bulatova a6ae4199ae merge: origin/main into dashboard-agent base (resolve report-health)
Resolve conflicts in report-health: keep our richer fallback/trust model
(telemetry_absent trust, snapshot fallback, finished/completed throughput)
and adopt main's start-latency-unknown behavior so an absent measurement
reports as unknown instead of a confident 0ms. renderMarkdown keeps our
layout rewrite; main's compactFact change is covered by report-layout's
metricValue. Adapt main's two render assertions to our current output.
2026-08-10 14:04:30 +00:00
Eric Allam ce368dd8e0 perf(database): index EnvironmentVariableValue.valueReferenceId so secret deletes stop seq-scanning (#4555)
## Why this change

`EnvironmentVariableValue.valueReference` is an `onDelete: SetNull`
foreign key. Deleting a `SecretReference` (the env var edit/delete path
for secret values) fires the cascade `UPDATE ONLY
"EnvironmentVariableValue" SET "valueReferenceId" = NULL WHERE $1 =
"valueReferenceId"`. That cascade is scan-shaped: with no index on
`valueReferenceId`, it reads the entire table to find the rows
referencing the deleted secret. The parent `SecretReference` delete does
almost no work itself; its latency is dominated by this cascade.

## Diagnosis

`EnvironmentVariableValue` was indexed on `environmentId` and
`(variableId, environmentId)`, but not on `valueReferenceId`. The SET
NULL cascade therefore did a full sequential scan of the whole table.
Two sibling SET NULL cascades on the same delete
(`OrganizationIntegration.tokenReferenceId`,
`User.mfaSecretReferenceId`) are index-backed and stay fast, which
isolates the missing index as the cause.

## Change

Add `@@index([valueReferenceId])` on `EnvironmentVariableValue`, created
with `CREATE INDEX CONCURRENTLY IF NOT EXISTS` so `prisma migrate
deploy` stays safe on a live table.

## Benchmark (local, seeded)

Local Postgres seeded with 1,000,000 `EnvironmentVariableValue` rows,
`EXPLAIN (ANALYZE, BUFFERS)` on the SET NULL cascade with zero matching
rows (the worst case: reads the whole table, affects nothing):

| | before | after |
|---|---|---|
| plan | Seq Scan (1M rows) | Bitmap Index Scan |
| execution | 183 ms | 2.8 ms |

In a variant where the secret matched several thousand rows, the parent
`SecretReference` delete's
`EnvironmentVariableValue_valueReferenceId_fkey` trigger dropped from
216 ms to 88 ms (the residual is the heap work of nulling those rows).

## Expected impact

The cascade drops from a full-table sequential scan to a targeted index
lookup. The win grows with the table, so the benefit is larger than the
seeded numbers above.

## Risks

- One extra btree to maintain on `EnvironmentVariableValue` writes;
small, single-column, and it should be pre-created before the migration
deploys (per the repo index rules).
- No behavior change: same rows nulled, no ordering or result-set
change, read paths untouched.

Companion to the same fix on `ProjectAlert.channelId`.
2026-08-10 13:54:18 +01:00
Eric Allam 4c58091973 perf(database): index ProjectAlert.channelId so alert-channel deletes stop seq-scanning (#4554)
## Why this change

Deleting a `ProjectAlertChannel` fires the FK cascade `DELETE FROM ONLY
"ProjectAlert" WHERE $1 = "channelId"`. That cascade is scan-shaped:
with no index on `channelId`, it reads the entire `ProjectAlert` table
to find the few child rows belonging to the deleted channel. The parent
`DELETE ProjectAlertChannel` does almost no work itself; its latency is
dominated by this cascade. `ProjectAlert` is append-heavy and grows over
time, so the scan cost only increases.

## Diagnosis

`ProjectAlert` had no index on `channelId` (only `pkey` + a `friendlyId`
unique). The cascade therefore did a full sequential scan of the whole
table. The sibling `ProjectAlertStorage` cascade on the same delete is
index-backed and stays fast, which isolates the missing index as the
cause.

## Change

Add `@@index([channelId])` on `ProjectAlert`, created with `CREATE INDEX
CONCURRENTLY IF NOT EXISTS` so `prisma migrate deploy` stays safe on a
live table.

## Benchmark (local, seeded)

Local Postgres seeded with 1,000,000 `ProjectAlert` rows across 50
channels (~20k rows per channel), `EXPLAIN (ANALYZE, BUFFERS)` on the
cascade delete:

| | before | after |
|---|---|---|
| plan | Seq Scan (1M rows) | Bitmap Index Scan |
| direct child delete | 740 ms | 22 ms |
| parent delete `ProjectAlert_channelId_fkey` trigger | 77.7 ms | 23.8
ms |

## Expected impact

The cascade drops from a full-table sequential scan to a targeted index
lookup. The win grows with the table: the more rows in `ProjectAlert`,
the more a scan costs and the more the index saves, so the benefit is
larger than the seeded numbers above.

## Risks

- One extra btree to maintain on every `ProjectAlert` insert; acceptable
for a single-column index on a high-insert table, and it should be
pre-created before the migration deploys (per the repo index rules).
- No behavior change: no rows orphaned, no ordering or result-set
change, read paths untouched.

## Follow-up

`ProjectAlert`'s other cascade FK columns (`projectId`, `environmentId`,
`workerDeploymentId`) are also unindexed, but their parents are
soft-deleted rather than physically removed, so those cascades do not
currently fire. Lower priority unless a hard-delete path is introduced.
2026-08-10 13:54:15 +01:00
Eric Allam 951d8e8d7b feat(webapp): per-client database pool metrics that survive the driver adapter (#4541)
## What

Follow-up to #4539. The driver-adapter work is inert until a client
flips to the pg driver adapter, but the moment one does, our database
observability degrades: the OTel metrics pipeline reads pool stats from
Prisma's `$metrics`, which is owned by the Rust engine's `quaint` pool.
Under the adapter, `pg.Pool` owns the pool, so those gauges read zero.
The pipeline also only ever scraped a single client (the control-plane
writer singleton).

This PR makes database metrics driver-agnostic and per-client:

- Every configured client registers a metrics source: control-plane
writer/replica, run-ops writer/replica, legacy writer/replica.
Previously only the control-plane writer singleton was scraped.
- Each OTel instrument is observed per client with `db_client` and
`db_driver` (`quaint` | `pg-adapter`) attributes. `db_client` uses our
canonical datasource-role labels (`control-plane-writer`,
`control-plane-replica`, `run-ops-writer`, `run-ops-replica`,
`legacy-run-ops-writer`, `legacy-run-ops-replica`) — the same strings
used for the `db.datasource` span attribute, so a metric and a trace
point at the same pool.
- Pool figures come from the authoritative source per driver:
- **pg-adapter**: `pg.Pool` (`totalCount`/`idleCount`/`waitingCount`,
plus cumulative opened/closed from `connect`/`remove` events).
- **quaint**: the Rust engine's `$metrics` pool gauges/counters, exactly
as before.
- Query counters and duration histograms still come from `$metrics` for
both drivers (the Rust engine executes queries in both cases).
- New `db.pool.connections.waiting` gauge (pg.Pool exposes this; quaint
reports 0).
- Stops exporting Prisma metrics from the Prometheus `/metrics` route.
Pool observability now lives entirely in the OTel pipeline, per driver,
per client.

## Why

So we can flip any client (including the control-plane writer, the
primary desync-fix target) to the driver adapter without losing pool
visibility. Existing dashboards keyed on the same metric names keep
working; they gain a per-client dimension.

## Testing

Unit (`apps/webapp/app/utils/databaseMetrics.server.test.ts`): the pure
normalizer — quaint reads pool from `$metrics`; adapter reads pool from
`pg.Pool` and keeps engine query metrics; `busy` never goes negative;
graceful zeroing when `$metrics` is unavailable (adapter still reports
live pool figures).

Live smoke test against a prod-shaped local stack: three
physically-distinct Postgres DBs (control-plane, run-ops, legacy) behind
dual PgBouncers, split mode on, with a mix of adapter and quaint
clients. Reading the actual emitted OTel metrics, every pool shows up as
its own series:

```
db.pool.connections.total{db_client="control-plane-writer",  db_driver="pg-adapter"} = 1
db.pool.connections.total{db_client="control-plane-replica", db_driver="quaint"}     = 1
db.pool.connections.total{db_client="run-ops-writer",        db_driver="pg-adapter"} = 1
db.pool.connections.total{db_client="run-ops-replica",       db_driver="quaint"}     = 1
db.pool.connections.total{db_client="legacy-run-ops-writer", db_driver="quaint"}     = 1
db.pool.connections.total{db_client="legacy-run-ops-replica",db_driver="quaint"}     = 1
db.client.queries.total{db_client="control-plane-writer",db_driver="pg-adapter"} = incrementing
db.client.queries.duration.count{db_client="control-plane-writer",db_driver="pg-adapter"} = incrementing
```

Confirms: metrics are attributed per pool with the correct driver;
adapter pools' figures come from `pg.Pool`; and query counters/duration
histograms keep incrementing under the pg adapter. Also verified
`/metrics` (Prometheus) now returns zero `prisma_*` series while still
serving the app's own metrics.

`pnpm run typecheck --filter webapp` passes.

## Notes

- `/metrics` (Prometheus) no longer includes `prisma_*` series. Anything
scraping that endpoint for Prisma metrics should read the equivalent
`db.*` metrics from the OTel exporter instead.
- **PgBouncer + `?schema=` gotcha (separate from this PR, worth flagging
for rollout):** since #4539 parses `?schema=` from the DSN and passes `{
schema }` to the adapter, node-postgres sends `search_path` as a startup
parameter. A transaction-mode PgBouncer rejects that with `FATAL:
unsupported startup parameter: search_path`. Our prod control-plane DSNs
use the default `public` schema with no `?schema=` param, so this is
latent, but any client we flip to the adapter must not carry `?schema=`
in its DSN (or the pooler needs `ignore_startup_parameters =
search_path`).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-10 13:54:06 +01:00
Saadi Myftija bd8ce4a50f feat(deployments): split project dependencies and code into separate layers (#4551)
Deploy images previously shipped node_modules and the bundled task code
in a single layer, so every deploy re-pushed and re-pulled the full
dependency tree even when nothing in it changed. The generated
Containerfile now copies `/app/node_modules` as its own layer and the
app files separately. With unchanged dependencies the dependency layer
is identical across deploys, so registries and workers already have it
and only the code layer moves.
2026-08-10 14:44:11 +02:00
Katia Bulatova c00fb9c36c fix(webapp): report start latency as unknown when there is no data (#4544)
When the health report had no start-latency measurement for the window,
it printed a confident "p95 0ms" and graded it healthy. It now shows
"unknown" for that metric and skips grading it, so an absent measurement
can't read as a green signal.

A genuinely measured 0ms is still shown as 0ms: the loader keeps "no
measurement" distinct from a measured zero instead of coercing both to
0.
2026-08-10 13:53:23 +02:00
nicktrn 6e00aaf92b chore(deps): bump transitive mermaid to 11.16.1 (#4553)
## Summary

Bumps the transitive `mermaid` in the lockfile from `11.14.0` to
`11.16.1`.

`mermaid` has no direct dependents here. It arrives through
`streamdown`,
which declares it as a hard dependency even though diagram rendering is
gated
behind the optional `@streamdown/mermaid` plugin, which we don't
install.
`streamdown@2.5.0` is its latest release, and its declared range
(`^11.12.2`)
already permits `11.16.1`, so this was a stale lockfile pin rather than
a
range conflict.

Done as a scoped override rather than a bare lockfile refresh, so the
floor
survives a lockfile regenerated from an older base:

```json
"mermaid@>=11 <11.16.1": "^11.16.1"
```

Net effect is 96 fewer lockfile lines, contained to mermaid's own
subtree.
`11.16.1` swapped out its parser, so the `langium` / `chevrotain@12` /
`vscode-languageserver-*` chain drops in favour of a single
`@chevrotain/types`, and `lodash-es` and `uuid@11` are no longer pulled
at
all.

The override goes away once `streamdown` makes `mermaid` an optional
peer of
its diagram plugin instead of a hard dependency.
2026-08-10 12:52:49 +01:00
Katia Bulatova 69c7b02c2f test(webapp): report parity matches the de-duplicated untrustworthy layout 2026-08-10 10:29:47 +00:00
Katia Bulatova 489869e889 Merge remote-tracking branch 'origin/feat/dashboard-agent-flows' into feat/dashboard-agent-ui 2026-08-10 09:31:38 +00:00
Katia Bulatova 4d9af86b4b fix(dashboard-agent): navigate_to teaches the run-status vocabulary and drops the dead search filter 2026-08-10 09:29:08 +00:00
Katia Bulatova 01ef8fddbf fix(webapp): navigation filters speak the run page's language 2026-08-10 09:21:37 +00:00
Katia Bulatova b73599b2ce fix(webapp): action rows render at the end of the turn 2026-08-10 09:05:27 +00:00