Skip to content

Enterprise Artifact Safe Updates

Overview

In enterprise deployments, artifacts are mutable resources that evolve over time. Content changes create a new entry in EnterpriseArtifactVersion, forming a queryable version history — but not every safe update path touches content, and not every path creates a new version row (see the paths below). However, deleting an artifact and re-adding it destroys existing lineage and cascades destructively through related tables.

Why Delete-Then-Re-Add Is Dangerous

When you DELETE an artifact in enterprise mode, the following happens:

  1. Version history is permanently destroyed — all EnterpriseArtifactVersion rows are deleted via ON DELETE CASCADE
  2. Deployment links orphanedEnterpriseProjectArtifact rows that referenced this artifact have their foreign key violated; cascading deletes remove deployment records and invalidate deployment tracking
  3. Reconciliation proposals leakreconciliation_proposals rows tied to the deleted artifact are orphaned (they may not have a direct FK, but they reference the now-deleted artifact_id), corrupting the reconciliation audit trail
  4. Audit trail broken — if you relied on version history for compliance or debugging, that record is gone forever

The Safe Alternative

Instead of delete-then-re-add, use in-place update paths that preserve version history and all downstream links.


Three Safe Paths

Path 1: Update Metadata (Metadata-Only)

Use PUT /api/v1/artifacts/{artifact_id} to update an artifact's metadata — aliases, tags, description/title/author/license (via the nested metadata object), owner_type, and visibility — while preserving its identity and version history.

This endpoint is metadata-only. It does not accept name, artifact_type, or file content, and it does not create a new EnterpriseArtifactVersion row (a version is only recorded when content changes — see Path 2). It is still the safe path for metadata changes because it never touches content, identity, or collection membership.

Silent no-op warning: ArtifactUpdateRequest does not set extra="forbid", so unknown/unsupported keys (e.g. content, files, name, artifact_type) are silently ignored by Pydantic rather than rejected — the request still returns 200 OK as if it succeeded, but nothing you intended to change actually changed. There is no validation error to warn you. If you're trying to update file content, sending it here will look successful and do nothing — use Path 2 instead.

When to use: You want to update the artifact's tags, aliases, description, author, license, owner type, or visibility without changing any file content.

Example:

curl -X PUT http://localhost:8080/api/v1/artifacts/skill:pdf-processor \
  -H "Authorization: Bearer $SKILLMEAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tags": ["document", "pdf", "productivity"],
    "aliases": ["pdf-processor", "doc-reader"],
    "metadata": {
      "description": "New description with updated details",
      "author": "Anthropic",
      "license": "MIT"
    },
    "visibility": "team"
  }'

Result: No new EnterpriseArtifactVersion is created (metadata changes are not versioned content); existing deployment links (EnterpriseProjectArtifact) remain valid; reconciliation history is preserved. To also change file content, use Path 2.


Path 2: Update Single File

Use PUT /api/v1/artifacts/{artifact_id}/files/{file_path} to update a single file's content without touching other files or metadata.

When to use: You want to fix a typo, update a section, or replace one file while keeping everything else unchanged.

The request body is JSON: {"content": "<new file content>"}.

Example:

curl -X PUT http://localhost:8080/api/v1/artifacts/skill:pdf-processor/files/prompt.md \
  -H "Authorization: Bearer $SKILLMEAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"content": "# Updated Prompt\n\nThis is the new content."}'

Or from a file, using jq to build the JSON payload safely:

curl -X PUT http://localhost:8080/api/v1/artifacts/skill:pdf-processor/files/prompt.md \
  -H "Authorization: Bearer $SKILLMEAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -Rs '{content: .}' < updated-prompt.md)"

Result: Only the specified file is updated; a new EnterpriseArtifactVersion is created (this is the path that actually creates version rows); metadata unchanged; all links preserved.


Path 3: Bulk Re-Publish with Overwrite

Use POST /api/v1/bundles/import-pack with conflict=overwrite to ingest a .skillmeat-pack archive, updating existing artifacts (matched by identity) in-place instead of creating duplicates.

When to use: You're re-publishing a bundle or collection from an upstream source or backup and want existing artifacts to be updated in-place rather than creating duplicates.

This endpoint is a multipart form upload, not a JSON body — it takes the pack archive as a file plus a handful of form fields.

Example:

# pack_file: the built .skillmeat-pack archive (e.g., produced by `skillmeat bundle export`)
curl -X POST http://localhost:8080/api/v1/bundles/import-pack \
  -H "Authorization: Bearer $SKILLMEAT_TOKEN" \
  -F "pack_file=@my-bundle.skillmeat-pack" \
  -F "bundle_name=my-bundle" \
  -F "conflict=overwrite"

conflict accepts skip (default), reuse, or overwrite. Optional form fields: bundle_name (defaults to the pack manifest's name), version (bundle version override), register_template (register a project_starter scaffold template).

Result: Artifacts in the pack are matched by identity and their content ingested in-place; conflict=overwrite refreshes existing artifact content without creating duplicates or losing identity; new EnterpriseArtifactVersion rows are created for changed content; deployment links and reconciliation history remain intact.


Decision Table

Intent Safe Endpoint When to Use
Update artifact metadata (description, tags, aliases, visibility) — no content change PUT /api/v1/artifacts/{id} Metadata-only; all file content stays the same; does not create a new version
Fix a typo or update one file's content PUT /api/v1/artifacts/{id}/files/{path} Surgical content update; creates a new version; other files untouched
Re-publish a bundle or collection POST /api/v1/bundles/import-pack with conflict=overwrite Bulk content refresh from upstream or backup; multipart pack upload
Create a new artifact with the same logical purpose POST /api/v1/artifacts (new artifact) Starting fresh; intentional new identity

When Delete IS Correct

The DELETE /api/v1/artifacts/{artifact_id} endpoint should only be used in these cases:

  1. Duplicate entries — You accidentally created two copies of the same artifact and want to remove the duplicate (keeping the primary).
  2. Wrong tenant scope — An artifact was provisioned in the wrong team or project and needs to be removed entirely.
  3. Explicit cleanup — You have a retention policy or audit requirement to hard-delete an artifact; reconciliation proposals have been reviewed and approved for deletion.
  4. Data migration — You're moving data between instances or schema versions and need to clear the old artifact as part of a planned cutover.

Before deleting, always:

  • Check GET /api/v1/artifacts/{artifact_id}/history to confirm you don't need the version and provenance history (and don't have downstream deployments depending on it).
  • Query reconciliation_proposals to understand any pending or in-flight reconciliation tied to this artifact.
  • Document the deletion reason in your audit log or team notes.

Remediation: Recovering from Delete-Then-Re-Add

If you've already deleted and re-added an artifact, orphaned reconciliation_proposals rows may exist. Use this query to identify them:

Note: reconciliation_proposals.artifact_id is a text type:name string (e.g. skill:pdf-processor) — it is not a UUID and cannot be compared directly to enterprise_artifacts.id (a UUID primary key). Build the equivalent type:name string from enterprise_artifacts' type column (Python attribute artifact_type; the physical DDL column is named type) and name for the comparison. The proposal kind column is kind, not proposal_type.

-- Find orphaned reconciliation_proposals (artifact_id references a deleted artifact)
SELECT
  rp.id,
  rp.artifact_id,
  rp.kind,
  rp.status,
  rp.created_at
FROM reconciliation_proposals rp
WHERE rp.artifact_id NOT IN (
  SELECT type || ':' || name FROM enterprise_artifacts WHERE tenant_id = 'YOUR_TENANT_ID'
)
  AND rp.tenant_id = 'YOUR_TENANT_ID'
ORDER BY rp.created_at DESC;

To clean up:

-- Delete orphaned reconciliation_proposals
DELETE FROM reconciliation_proposals
WHERE artifact_id NOT IN (
  SELECT type || ':' || name FROM enterprise_artifacts WHERE tenant_id = 'YOUR_TENANT_ID'
)
  AND tenant_id = 'YOUR_TENANT_ID';

Going forward: Use the safe update paths above to avoid this situation.


Summary

Don't Do
❌ DELETE artifact → POST new artifact ✅ PUT /artifacts/{id} (metadata) or PUT /artifacts/{id}/files/{path} (content)
❌ Hard-delete to "start fresh" ✅ Update metadata and/or file content in-place; content changes create a new version automatically
❌ Ignore reconciliation_proposals after delete ✅ Check for orphaned proposals; query and clean up if needed

By using in-place updates, you preserve version history, maintain deployment links, and keep your reconciliation audit trail clean and queryable.