Skip to content

feat(artifacts): versioning, edits, comments & per-artifact sharing platform#78

Open
ianu82 wants to merge 8 commits into
mainfrom
codex/artifact-platform-server
Open

feat(artifacts): versioning, edits, comments & per-artifact sharing platform#78
ianu82 wants to merge 8 commits into
mainfrom
codex/artifact-platform-server

Conversation

@ianu82

@ianu82 ianu82 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What this is

This is the backend platform behind the artifacts workspace: everything the server needs to version an artifact, edit it safely, comment on it, and share/publish it per-artifact. It's the server half of the feature — the UI is the companion PR in mindsdb/cowork.

It's a big diff, but about a third of it is tests. Here's the map.

What it provides

  • Versioned artifacts. Every save snapshots the artifact's files (content-addressed blobs + a manifest), with full history, restore, and diff.
  • Safe edits. A propose → accept edit flow with optimistic-concurrency checks, so two people editing the same artifact don't silently clobber each other.
  • Comments & suggestions. Anchored comments with open / resolved / accepted / rejected status, tied to versions.
  • Per-artifact sharing & publish. Public / password / restricted access resolution, wired into the publish pipeline.
  • Soft delete + recovery. Deleting an artifact frees its name and path for reuse but keeps the original recoverable.

How to review a ~19k-line diff

Area Where ~Lines What to do with it
Tests tests/test_artifact_*, tests/test_project_collaboration.py ~6k Evidence the feature works — not logic to scrutinize line-by-line.
Core service services/artifact_versions.py ~3.1k Snapshots, versions, comments, restore. The heart of it — start here.
HTTP layer api/v1/endpoints/artifact_versions.py, endpoints/artifacts.py ~2.2k The REST surface.
Collaboration & sharing services/project_collaboration.py, artifact_shares.py, publish.py ~1.5k Sharing, access resolution, publish.
Schema models/artifact.py + db/alembic/versions/d3a5c9e7b1f2_artifact_versions.py ~0.9k New tables + one migration.

How it's kept safe to merge

  • Additive. New tables and endpoints; it doesn't rewrite existing behavior.
  • One migration, chained after the current head.
  • 0 commits behind main — merges cleanly today.

Testing

Comprehensive endpoint + service test suites (the ~6k lines of tests above). Core modules py_compile clean and the server boots.

Heads-up for the reviewer

  • Lands together with the frontend companion PR below.
  • Deliberate boundaries worth knowing:
    • Access enforcement for restricted/password shares lives in the external viewer (anton-services Lambda), not here — this PR resolves access and builds the payload.
    • Commenter emails are returned to artifact viewers (this powers the email-on-hover identity in the UI).
    • A couple of perf/atomicity items (an event_type index; propose/accept transaction atomicity) are noted as deferred — none blocking.

Companion PR (frontend): mindsdb/cowork#213

🤖 Generated with Claude Code

ianu82 and others added 8 commits June 20, 2026 22:04
…ion-2)

- artifact_edits: POST /artifacts/edits/propose (non-mutating diff) and
  /accept with optimistic concurrency — base-version compare-and-swap,
  HTTP 409 (+currentVersionId) on a stale base — reusing the existing
  patch-apply + snapshot primitives (operation_type="ai_edit").
- identity + artifact_shares: User model, per-artifact ShareGrant, and a
  lightweight-signup guest accept flow (token hashed at rest, replay-safe);
  migration a1b2c3d4e5f6 chained on f1c2d3e4a5b6.
- Registered models + routers. TODO(M3): enforce per-artifact capability on
  the share endpoints (currently unguarded by design — scaffold).
  Full suite 241 passed / 1 skipped + 9 new; single alembic head.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… metadata

For shared-DB compatibility when running direction-2 against a copy of the live
database: include the direction-a `9b7c6d5e4f3a` artifact-metadata migration and
re-chain `a1b2c3d4e5f6` (identity + artifact_shares) onto it, giving a single
linear head identical to the DB's history (…f1c2d3e4a5b6 → 9b7c6d5e4f3a → a1b2c3d4e5f6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…to the snapshot

accept_edit takes operation_type (default "ai_edit"); a user's direct typed edit
passes "manual_edit" so versions distinguish human edits from AI edits, and the
actor (name/email/subject from the JWT principal) now flows into the version's
activity event (was previously accepted but dropped). Default behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rt fresh

Artifacts are keyed by folder path (artifacts.path UNIQUE). Deleting an artifact
soft-deletes it (folder removed, record + versions kept for recovery) but never
released the path — so creating a new artifact at the same folder (e.g. a deck
with the same title/slug) resolved straight back to the old record and inherited
its entire version history and comments.

- delete_artifact_endpoint: after recording the "deleted" event (which preserves
  the original path in its details), tombstone artifact.path
  (`<path>#deleted-<uuid>`). A new artifact at the original path now finds no
  match and is created fresh.
- restore_artifact: a deleted artifact's path is tombstoned, so when restoring
  one, resolve its ORIGINAL folder from the "deleted" event and heal the record's
  path — restoring to the original location, or a `-restored` sibling if that
  path is now taken. Recovery is preserved.
- Add _deleted_original_path() helper (mirrors _deleted_external_artifact_id).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on restore

The path-release fix freed uq_artifacts_path but not uq_artifacts_project_slug, so
re-creating an artifact at the same name still hit a UNIQUE(project_id, slug)
IntegrityError → 500 on the first edit/propose of the new artifact.

- delete: tombstone artifact.slug alongside artifact.path (same uuid).
- restore: when landing a deleted artifact back, pick a folder whose path AND slug
  (basename) are both free (—restored sibling if taken) and heal both fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the parallel backend review:
- _get_or_create_artifact: pre-uniquify the slug within the project (new
  _unique_project_slug) so a (project_id, slug) collision no longer throws an
  unhandled IntegrityError → opaque 500 on the first edit of a new artifact.
- restore_artifact landing-spot: the free-spot loop now checks path-taken (not
  just slug-taken) against other artifact rows, closing a uq_artifacts_path 500
  on the heal commit.
- _deleted_artifact_card: show the ORIGINAL slug/path (from the deleted event,
  else strip the #deleted-<uuid> tombstone) instead of leaking the tombstone
  string into the trash UI.
- Remove the unused `_delete_artifact` import; mark the dead hard-delete
  delete_artifact() DEPRECATED (kept: a test imports it).
- Log (warning + exc_info) the previously-silent except blocks in
  _mark_live_preview_ready / _record_live_preview_failure.
- Atomic metadata.json port write (temp file + os.replace) so a concurrent
  reader can't see truncated JSON (which made artifacts vanish from listings).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Delete tombstone could overflow slug (255) / path (2048) under
  validate_assignment, turning delete of a long-slug artifact into a 500.
  Truncate the base before suffixing (new _tombstone helper).
- _get_or_create_artifact only uniquified the slug on INSERT; the UPDATE
  branch could assign a colliding (project_id, slug) and 500 on a rename.
  Uniquify there too (excludes own path, so an unchanged slug stays stable).
- Centralize the "#deleted-" tombstone prefix as a constant; collapse the two
  byte-identical deleted-event queries into _latest_deleted_details; use
  _actor_kwargs() in the comment-status endpoints; comment the by-name dispatch
  aliases so they aren't mistaken for dead code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@ianu82

ianu82 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

CodeQL checks are failing but as far as I can tell, not because of changes in this PR: three independent pieces of evidence:

  1. They pre-exist on main. Every open alert I can enumerate (26 × py/path-injection, 1 × py/stack-trace-exposure) sits on refs/heads/main - they're already in the codebase, not introduced here.
  2. An unchanged file is flagged against the PR. cowork/services/projects.py isn't touched by this PR at all, yet its path-injection alerts are counted against feat(artifacts): versioning, edits, comments & per-artifact sharing platform #78. That's exactly the failure mode GitHub warns about in the check summary itself: "Alerts not introduced by this pull request might have been detected because the code changes were too large." An +18.7k diff blows past CodeQL's attribution window.
  3. The artifact filesystem code is correctly guarded. Every path built from input uses the canonical defense — build, .resolve(), then .relative_to(root) which raises on escape, e.g. artifacts.py:956:
target = (parent / rel_path).resolve()
 ...
 target.relative_to(parent)   # → 403 "Asset is outside the artifact directory"

This pattern appears at ~20 sites across services/artifacts.py, endpoints/artifacts.py, and artifact_versions.py (_safe_relative_path). py/path-injection routinely can't follow an exception-based guard, so it false-positives on exactly this.

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.

2 participants