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:
- Version history is permanently destroyed — all
EnterpriseArtifactVersionrows are deleted viaON DELETE CASCADE - Deployment links orphaned —
EnterpriseProjectArtifactrows that referenced this artifact have their foreign key violated; cascading deletes remove deployment records and invalidate deployment tracking - Reconciliation proposals leak —
reconciliation_proposalsrows 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 - 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:
- Duplicate entries — You accidentally created two copies of the same artifact and want to remove the duplicate (keeping the primary).
- Wrong tenant scope — An artifact was provisioned in the wrong team or project and needs to be removed entirely.
- Explicit cleanup — You have a retention policy or audit requirement to hard-delete an artifact; reconciliation proposals have been reviewed and approved for deletion.
- 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}/historyto confirm you don't need the version and provenance history (and don't have downstream deployments depending on it). - Query
reconciliation_proposalsto 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_idis a texttype:namestring (e.g.skill:pdf-processor) — it is not a UUID and cannot be compared directly toenterprise_artifacts.id(a UUID primary key). Build the equivalenttype:namestring fromenterprise_artifacts'typecolumn (Python attributeartifact_type; the physical DDL column is namedtype) andnamefor the comparison. The proposal kind column iskind, notproposal_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.