Guide

The upload audit trail — who wrote what, and when

Every upload-link issuance and every claim lands in `upload_link` — a governed, forensic record of who touched the workspace. Read the trail from the dashboard, from `info`, and (for operators) directly from Postgres.

5/18/2026 · connect0 · 3 min

Because the core sees every write, "who put this file here?" is a lookup, not an investigation.

connect0 keeps a forensic record of every upload-link issued: who issued it, when it expires, whether the upload completed, and how many bytes R2 actually saw. The record exists for three reasons:

  1. Debug "did the upload work?" — the same question every user has after their first PUT.
  2. Operations — when something looks weird ("why is project X 50 GB?"), follow the rows.
  3. Compliance — for accounts on the audit plan, the trail is the answer to "who put this file here?"

From the dashboard

/a/<account>/projects/<project>/upload has a "Recent uploads" card showing the 10 most recent entries. Each row shows:

  • target_path — where the file landed under /workspace/.
  • sourcemcp (via upload.create_link), mcp-write (via upload.write), or web (dashboard drag-drop).
  • bytes_received / content_length — how big it actually was vs. how big the link promised.
  • Status — pending / claimed / expired / oversize (see the status guide).

From the agent's perspective

> What did I upload to this project recently?

Agent calls info with both slugs set. current_project.recent_uploads returns the same 10-row window. Useful when the agent itself is the consumer — it can confirm a file landed without you having to switch to the dashboard.

For operators: direct SQL

The upload_link table lives in connect0_controlplane. Schema:

upload_link (
  id              UUID,
  project_id      UUID REFERENCES project(id),
  ip_local_id     TEXT,        -- the Identity Platform user id that issued the link
  client_id       TEXT,        -- OAuth client id (when issued via API token)
  target_path     TEXT,
  content_type    TEXT,
  content_length  BIGINT,      -- declared by the agent
  expires_at      TIMESTAMPTZ,
  issued_at       TIMESTAMPTZ,
  claimed_at      TIMESTAMPTZ, -- null = never used
  bytes_received  BIGINT,      -- what R2 actually saw
  etag            TEXT,        -- from the R2 event
  oversize        BOOLEAN,     -- bytes_received > content_length
  source          TEXT
)

Common ops queries:

-- All uploads for a project in the last 24h.
SELECT issued_at, ip_local_id, target_path, status_label
FROM upload_link
WHERE project_id = '<uuid>'
  AND issued_at > now() - INTERVAL '24 hours'
ORDER BY issued_at DESC;

-- Oversize incidents — files bigger than declared.
SELECT * FROM upload_link WHERE oversize = true ORDER BY claimed_at DESC LIMIT 50;

-- Stale unclaimed links — issued but never used, useful for cleanup analytics.
SELECT count(*) FROM upload_link WHERE claimed_at IS NULL AND expires_at < now() - INTERVAL '7 days';

What the trail can't tell you

  • The file's contents. We never log stdout/stderr or upload bodies. The audit is about that a write happened, not what was written.
  • The user's identity beyond localId. Cross-reference to Identity Platform's directory to get a name/email.
  • Anything about files written by sh "tee" or sh "cat >". Those go through sandbox_invocation, not upload_link. The R2 event notification fires for them too, but it doesn't match an upload_link row and is silently dropped.

Retention

90 days. After that the rows are pruned by the daily housekeeping job. Audit-plan accounts get longer retention per their contract.

Next steps

Where this fits

This is the One system of record pillar of connect0 — the core that connects everything your company runs on. Because every upload flows through the core, it records who issued each link and what R2 actually received — so the audit trail is a byproduct of how the system works, not a bolt-on. Start building → · All guides →