Skip to content

Behaviours the Postgres port dropped: audit ledger #3142

Description

@Israeltheminer

The Convex-to-Postgres port (#3107) and the tree retirement (#3125) dropped behaviours that 0.4 had. A six-way audit found 30-odd; this is the ledger for the ones with no PR yet.

Method, because it decides what counts: services/platform/convex/ survived #3107 as dead-or-shimmed code and was deleted by #3125, so an identifier existing in the tree never proved it ran. Every item below was verified by reading the live 0.5 path, and each names the file that should own the fix. backend/MIGRATION.md's PENDING: notes are the authority on what was deferred on purpose; nothing labelled REGRESSION here has one.

In flight

Finding PR
Retrieval gate served unbound, superseded, orphaned and mid-index refs to any member #3141
Erasure cascade missed 5 more subject-data categories #3122
Chat entity legs compared a whole question as one substring #3123
Emailed attachments are never indexed or retrievable #3121 (no PR)

Also being worked, no PR yet: unrevoked LLM gateway keys, audit-log and conversation-write role gates, 2FA audit rows, the retention sweeps, the agent-comment automation loop, the dead chat search legs, and the docs that still describe a tale-convex service.

Security and cost

Sandbox sessions never revoke their LLM gateway key. revokeVirtualKey has one caller where 0.4 had four. The expiry sweep flips status only (domains/sandbox/watchdogs.ts:32), admin destroy revokes the session token but not the key (domains/sandbox/sessions.ts:459), and the task-agent lane never revokes (domains/tasks/agent-runs.ts:206). The mint body states the invariant twice: the gateway has no native TTL, and reset_duration: '1M' is safe because teardown revokes. It does not. Each abandoned key can spend one session's budget against the org's provider keys every month, indefinitely, one per expired or destroyed session.

An agent-authored task comment can start the task's owning automation. 0.4 called the trigger only from the user-comment path. 0.5 has one addTaskComment for both lanes and calls maybeTriggerOwningAutomation unconditionally (domains/tasks/comments.ts:144). The template is directly below it: dispatchMentionedProjectAgent gates on authorType !== 'user' at line 534. Reachable from the workflow task.comment native, the task-agent's task_comment tool, and the overdue cron. The live-run guard blocks a concurrent second start, not a sequential loop.

Audit-log reads are open to every non-disabled role (domains/audit_logs/routes.ts, six sites; helper at auth/access.ts:94). 0.4 was admin and owner only. The log names data subjects and the lawful grounds for erasing them.

A read-only member can write to conversations — reply, send outbound mail, close, bulk-act, delete (domains/conversations/service.ts:125, routes at 170/207/318/469/502). The assignment-privacy half is correctly ported; the write-role gate is what is gone. Outbound mail leaves the building under the org's name.

2FA and passkey lifecycle events no longer audit. Only failures do (domains/two_factor/service.ts:97, auth/auth.ts:321). An attacker who adds a passkey and disables 2FA leaves no successful-action trail.

Retention destruction emits no audit rows. Sixteen 0.4 action names exist nowhere in the tree (domains/retention/service.ts:849).

Correctness

Legal holds are checked once, then not again for the rest of the cascade. 0.4 re-read them inside all 19 erasure arms and named the reason: FRCP 37(e) spoliation. 0.5 gates once at domains/erasure/service.ts:540 and then runs ten passes that are not one transaction, two of which fan out per-thread and per-document deletes.

The erasure receipt cannot distinguish "found nothing" from "never looked", and the drawer's Full breakdown renders blank. counts is now Record<string, number>; the renderer skips non-objects (app/features/settings/governance/data-subject-requests/request-detail-drawer.tsx:530), so every entry is dropped before the zero-count branch. The headline numbers still render, so it looks populated.

Two retention categories never sweep. An admin sets an externalConversations window and nothing deletes; message bodies are kept forever (domains/retention/service.ts, no reference to app.conversations). chatFilterEvents is the same shape but latent — no production writer yet. Separately login_block_counters went from a 30-day window to 90 with no note, and two_factor_attempts lost its 30-day sweep entirely, so a user who fails 2FA once and never returns keeps that row forever.

Bulk task edits and moveTask emit no notifications, open no review gate and fire no automation triggers (domains/tasks/service.ts:2582, :1504).

updateTask never clears startNotifiedAt/slaLevel, so moving a due date never re-fires the ladder (domains/tasks/service.ts:891).

setTaskReviewer is a bare column write — no subscribe, no re-target of a pending request, no notify (domains/tasks/service.ts:903). Distinct from #3130's fix, which gates the respond door rather than the designation door.

Agent transcript merge became "keep the longer array", so a disjoint later ring segment overwrites history at parity (domains/tasks/agent-turn-shim.ts:537).

Comment edits never re-resolve mentions. Editing a comment to add @someone notifies nobody and the stored mention set goes stale. addedMentions survives at core/tasks/mentions.ts:127 with no caller.

Two chat search legs are stubbed empty under false comments. kind="website" returns [] "because there is no 0.5 table" — migration 0045 created app.websites and listWebsites exists. kind="mail-attachment" likewise, while migration 0037 shipped the columns and the binder writes them. Both are advertised to the model on every turn. The task and project legs also lost their zero-hit listing fallback, leaving listedSource unreachable.

A search after a replacement upload can answer from the superseded version. #3141 closes the read half; the indexer-side guard and the orphaned corpus row remain (domains/documents/replacement.ts:755 moves file_ref on and never purges).

Scheduling and hygiene

The three daily governance crons were packed into one 30-minute window (jobs/schedules.ts:29). 0.4 spaced them and said why: releases at 01:00, audit integrity at 02:00, retention at 04:00, so the verifier never walks the chain while retention deletes audit prefixes. 0.5 runs releases at 04:15 and integrity at 04:30, inside the retention window. This compounds with the documented anchor change, where the verifier trusts the oldest surviving row.

The expiry sweep can expire a sandbox session under a live turn — it guards on the admin pin, not on a running op (domains/sandbox/watchdogs.ts:32).

The admission-ticket reap runs last, behind up to 375s of spawner probes against a 300s job expiry, so it never runs when tickets are piling up (domains/sandbox/watchdogs.ts:46).

Sandbox drift reconcile only heals phantoms — a dropped pin is never re-asserted and a missing pinned container is never rebuilt (domains/sandbox/service.ts:175).

No expired-session reaper. Inherited from 0.4, not a regression, but the session table grows without bound; the idle sweep deliberately skips already-expired rows.

No exhaustiveness gate on the chat shim map. The sandbox bridge has exactly this walker as a CI gate; backend/domains/chat/ has no test file. That absence is how #3112's and #3128's missing handlers shipped. The map is complete today.

The 0.4 privacy suite for the conversations chat leg was deleted and not replaced — 26 cases, including "a plain member sees nothing" and "does not reach into another organization". What survives is a unit test on the predicate plus one integration assertion no workflow invokes.

Checked and not regressions

policyAcknowledgements and wfExecutions have no 0.5 table, so their missing erasure arms are moot — though the docs and locale catalogs still promise both, which is a docs gap. Budget enforcement was never wired in 0.4 either. The prompt-injection boundary is fully ported; the untrusted_content zero-hit signal was a rename to lib/chat/untrusted-content.ts with 12 live call sites. pg-boss schedules do fire — verified empirically against a throwaway database, since config.schedule defaults true independently of supervise.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions