Skip to content

fix(server): audit instance-admin adapter and database-backup mutations to activity_log - #336

Open
claudegoogl-sudo wants to merge 1 commit into
masterfrom
feat/adapter-backup-activity-log
Open

fix(server): audit instance-admin adapter and database-backup mutations to activity_log#336
claudegoogl-sudo wants to merge 1 commit into
masterfrom
feat/adapter-backup-activity-log

Conversation

@claudegoogl-sudo

@claudegoogl-sudo claudegoogl-sudo commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • The server exposes instance-admin routes that manage adapter plugins and database backups
  • Adapter install runs npm install and then loads the package into the server process. A database backup reads the whole database. Both are high-privilege operations.
  • None of these mutations wrote a row to activity_log. A board token could install an arbitrary package and import it with no audit trail. boardMutationGuard exempts board-key writes from the trusted-origin check, so the token alone reaches these routes.
  • Existing instance-scoped mutations already solve the audit fan-out: instance settings and plugin routes write one activity_log row per company on the instance, because logActivity requires a companyId and instance mutations have no single company.
  • This pull request applies that same pattern to the adapter routes and the manual backup route. One small shared helper holds the fan-out, and the code-execution routes also write a row when the operation fails, because a failed install is still an attempted code execution.
  • The benefit is that every adapter and backup mutation now names its actor, and responders can see attempts that failed, not only ones that landed.

Linked Issues or Issue Description

Related (found in the duplicate search, different scope): paperclipai#9739 proposes a broader instance-scoped audit stream and privileged lifecycle auditing. This change only adds the missing activity_log call sites for the adapter and backup mutation routes, on the existing per-company fan-out pattern.

No public issue exists for this change. The underlying problem, in the bug-report shape:

What happened?

The seven mutating instance-admin routes wrote no activity_log rows:

  • POST /api/adapters/install — runs npm install, then dynamically imports the package in the server process
  • PATCH /api/adapters/:type — enable or disable an adapter
  • PATCH /api/adapters/:type/override — pause or resume an override of a builtin
  • DELETE /api/adapters/:type — uninstall an external adapter
  • POST /api/adapters/:type/reload — bust the module cache and re-import adapter code
  • POST /api/adapters/:type/reinstall — re-run npm install for an adapter
  • POST /api/instance/database-backups — run a manual database backup

Steps to reproduce

  1. Call any route above as an instance admin.
  2. Query the activity_log table.
  3. Before this change, no row appears. After this change, one row per company names the actor, the action, and the target.

Expected behavior

Every mutation on these routes writes at least one audit row naming the actor. Failed install, reload, and reinstall attempts also leave a row with the failure reason. A failed npm install is still an attempted code execution, and a responder needs it.

Paperclip version or commit

master at 14a4d4b59 (this branch is cut from it).

Deployment mode

Self-hosted. The routes require instance-admin access; the install route is additionally reachable with a board token alone, because boardMutationGuard exempts source === "board_key" from the trusted-origin check.

Follows CONTRIBUTING.md PR template — sections present: Thinking Path (7 steps), Linked Issues (in-PR description, bug-report labels), What Changed, Verification, Risks, Model Used, Checklist (every box ticked or marked N/A with reason).

What Changed

  • New shared helper server/src/routes/instance-activity.ts. logInstanceActivity(db, req, input) resolves the actor with getActorInfo, fans one logActivity row out per company through instanceSettingsService(db).listCompanyIds(), and emits a logger.warn when the company list is empty, so the row is no longer dropped silently.
  • server/src/routes/adapters.ts: all six mutating routes write audit rows. Success rows use actions instance.adapter.installed, instance.adapter.disabled, instance.adapter.overridden, instance.adapter.uninstalled, instance.adapter.reloaded, instance.adapter.reinstalled, with entityType: "adapter". Install, reload, and reinstall also write a row from their catch branch with outcome: "failed" and the failure reason. Failure-path writes are guarded with .catch so an audit-write failure cannot mask the original error response.
  • server/src/routes/instance-database-backups.ts: the route factory now takes db (instanceDatabaseBackupRoutes(service, db)), and the handler writes instance.database_backup.created with entityType: "instance_database_backup" on success and on rejection (the error is re-thrown, so response codes do not change).
  • details carry only bounded, relevant fields: package name, requested version, isLocalPath, resolved adapter type; backup rows carry the backup file basename, size, pruned count, and duration. The raw request body is never splatted in.
  • server/src/app.ts: threads db into both route factories. adapterRoutes(options) now requires db in its options object, so the audit fan-out always has a handle.
  • Actor provenance keeps flowing through the central actor-context (AsyncLocalStorage), so board-key writes keep the board_key_authenticated_write flag with no extra plumbing at the call sites.

Verification

  • pnpm vitest run src/__tests__/adapter-routes.test.ts src/__tests__/adapter-routes-authz.test.ts src/__tests__/instance-database-backups-routes.test.ts (in server/) — 39 passed (was 34 before; 5 new audit tests).
  • npx tsc --noEmit (in server/) — 0 errors.
  • Negative control: with the logActivity call deleted from the helper, all four new audit assertions fail. The tests prove the rows land.
  • The new install tests use isLocalPath: true with a mocked adapter loader, so no real npm runs in tests.

Risks

  • Each route now awaits audit writes before responding. This adds one listCompanyIds query plus N row inserts (N = companies on the instance) to these admin-only, low-frequency routes. If logActivity throws on a success path, the mutation succeeded but the request returns 500 — the same behavior the existing instance-settings audit fan-out has. Failure-path writes are guarded so they cannot mask the original error.
  • adapterRoutes callers must now pass db. Both in-repo test call sites are updated. External embedders of the router would see a type error, not a silent gap.
  • No schema changes. No new tables. companyId stays required and non-nullable.

For core feature work, check ROADMAP.md first and discuss it in #dev before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See CONTRIBUTING.md.

Model Used

Claude (Opus 4.6, model id claude-opus-4-7) via the Paperclip agent runtime, extended thinking + tool use, as the "Coder" software-engineer agent.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have not referenced internal/instance-local Paperclip issues or links (only public GitHub #NNN / github.com/paperclipai/paperclip URLs)
  • My branch name describes the change (e.g. docs/..., fix/...) and contains no internal Paperclip ticket id or instance-derived details
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • I have updated relevant documentation to reflect my changes
  • I have considered and documented any risks above
  • All Paperclip CI gates are green
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups — N/A on this fork: Greptile does not run on claudegoogl-sudo/paperclip
  • I will address all Greptile and reviewer comments before requesting merge — N/A on this fork for Greptile; reviewer comments will be addressed before merge

…ns to activity_log

Every mutating route in adapters.ts (install, disable, override, delete,
reload, reinstall) and POST /api/instance/database-backups now writes an
activity_log row naming the actor, on the success path and — for the
code-execution routes (install, reload, reinstall) and the backup attempt —
on the failure path with the failure reason.

Instance-scoped mutations have no single company, so rows fan out per
company via instanceSettingsService(db).listCompanyIds(), matching the
existing precedent in instance-settings.ts and plugins.ts. An empty
company list now emits a logger.warn instead of silently dropping the row.

- new shared helper routes/instance-activity.ts (logInstanceActivity)
- adapterRoutes(options) and instanceDatabaseBackupRoutes(service, db)
  now receive db from app wiring; no module-level singleton
- actions: instance.adapter.installed/.disabled/.overridden/.uninstalled/
  .reloaded/.reinstalled, instance.database_backup.created
- details carry packageName/version/isLocalPath/type (never the raw body);
  backup rows carry basename(backupFile) + size/prune/duration only
- provenance keeps flowing through the central actor-context
  (AsyncLocalStorage), so board_key writes are flagged as before

Verification: vitest adapter-routes.test.ts, adapter-routes-authz.test.ts,
instance-database-backups-routes.test.ts (39 passed); server tsc --noEmit
clean; negative control — deleting the logActivity call in the helper fails
all four new audit tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant