Skip to content

fix(platform): close skill upload data-safety + UX gaps from review#1741

Merged
larryro merged 4 commits into
mainfrom
skills-bundle-upload
May 26, 2026
Merged

fix(platform): close skill upload data-safety + UX gaps from review#1741
larryro merged 4 commits into
mainfrom
skills-bundle-upload

Conversation

@larryro

@larryro larryro commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Multi-agent review of the upload-only skills flow surfaced 6 merge blockers and several correctness/leak risks. This PR closes them all in one bundled change. Builds on top of e3af6f2ae (the original upload-only-skills feature).

Data safety

  • Per-(orgId, slug) exclusion lock via new skillUploadClaim table + claim/release internal mutations. Concurrent force: true uploads to the same slug no longer race past the existence check and last-write-wins.
  • Storage ownership binding via new skillUploadIntent table + presign / record / verify / delete mutations. uploadSkillBundle now refuses any storageId not bound to the calling org — closes the cross-tenant _storage read / delete path.
  • _storage blob lifecycle wired through a single cleanupUploadResources helper. needs_confirm, dialog close, parse failure, write failure, and success-finally all delete both the blob and its intent row. No more orphans per replace-cycle / cancel.
  • tale deploy preserves UI uploads via new listContainerSkills + stageSkillsWithoutConflictingBundles (mirrors the providers branch). Any slug already in the container is stripped from the host sync.
  • duplicateSkill preserves binary assets via new readFileBufferSafe + existing atomicWriteBuffer, replacing the prior UTF-8 round-trip that corrupted PNGs / PDFs / fonts. Emits dedicated duplicate_skill audit action.
  • Scaffold copyTree skips @<slug>/ entries — creating a new org no longer recursively copies every existing org's tenant tree into the new org's namespace (skills/integrations/workflows). Agents/providers use raw slugs without the marker — follow-up issue.

Correctness

  • Six empty-catch sites (CLAUDE.md hard rule) now log non-ENOENT errors: walkSkillBundle readdir/stat, duplicateSkill readdir, dirHasFiles, copyTree stat, readSkillMd lstat. errnoCode promoted from scaffold.ts to shared convex/lib/file_io.ts.
  • macOS __MACOSX/ filter (plus .DS_Store / Thumbs.db) on both client + server parsers. The most common Mac packaging workflow no longer fails with a misleading "missing SKILL.md" error.
  • Upload audit row now carries skillMdHash, totalBytes, license, disableModelInvocation so rollback investigation can tie an audit row to a specific bundle.
  • audit_mutations.ALLOWED_ACTIONS adds duplicate_skill.

UX + i18n

  • Cache invalidation fixed: dialog routes through useUploadSkillBundle (was raw useAction), so the skills list query invalidates on success — table updates without manual refresh; replace flow's detail panel refetches. lib/config-watcher.ts typeMap + path parser learn skills so SSE-driven invalidation also covers them.
  • AbortController + isMountedRef on the dialog — mid-flight close cancels the blob POST and skips stale-state setState warnings.
  • ConvexError unwrap (extractErrorMessage) — user toasts show the human message instead of a serialized error object.
  • Drop UX: multi-file / folder / wrong-type drops produce specific errors; window-level dragover/drop preventDefault stops a missed drop from navigating away.
  • Preview step: renders full SKILL.md frontmatter (license, recommended Python/Node, disable-model-invocation, unknown-key count); ICU plural for file count.
  • "Replace bundle" button next to Duplicate / Delete in the detail panel — opens the upload dialog directly for the visible skill.
  • i18n: orphan skills.asset.loadNotFound renamed to skills.viewer.notFound; DE hard-fail Wird X… patterns fixed per TERMINOLOGY_DE.md (Wird hochgeladen…Lade hoch…, Bundle wird gelesen…Lese Bundle…); Zip loanword aligned with .zip-Datei; dropOrClick aligned with the integrations dialog metaphor.

Type widening

requireOrgMembershipById and requireOrgAdminOrDeveloper now accept ActionCtx | MutationCtx so the new V8 mutations can reuse the same auth + role gate. No behavior change to existing callers.

Test plan

  • bun run check green from repo root: lint (0 warnings), typecheck (0 errors), 272 test files / 70,878 tests passed
  • bunx convex dev --once regenerates _generated/api.d.ts with the new tables + mutations
  • Upload a new skill bundle → table row appears WITHOUT manual refresh
  • Replace an existing skill → detail panel shows NEW content WITHOUT closing/reopening
  • macOS Finder-zipped folder (right-click → Compress) → upload succeeds (was failing with "missing SKILL.md")
  • Drop a folder → "zip the folder first" error
  • Drop two zips → "single file only" error
  • Close dialog mid-upload → no console warnings; no orphaned _storage blob
  • Concurrent force-uploads to same slug from two tabs → second sees LOCK_HELD toast; on-disk bundle is the first writer's; exactly one audit row
  • needs_confirm → close confirm dialog → _storage row is gone (Convex dashboard)
  • Upload a skill with assets/logo.png, duplicate it, compare sha256 — bytes must match
  • Fresh tale init + tale deploy → container has examples/skills/pptx (first-time seed works)
  • tale deploy after UI upload of same-slug skill → "Preserved 1 existing container skill" log line, UI upload not clobbered

Follow-ups (deferred per scope minimization)

  • Same cross-org copyTree leak applies to agents/providers (raw <slug> shape vs @<slug> — needs curated examples/ source or domain-aware filter)
  • backfill_skill_scaffolding single-page pagination + per-org try/catch (manual migration, pre-existing pattern in migrate_org_creators.ts too)
  • Audit-vs-fs atomicity (fs commits before audit; deleteSkill writes audit before rm)
  • Zip symlink permission bits not rejected
  • Case-insensitive SKILL.md collision on case-insensitive filesystems
  • BOM-prefixed SKILL.md rejected as malformed
  • parse-skill-bundle test coverage (the parser is security-sensitive and has zero unit tests)

Summary by CodeRabbit

  • New Features

    • Added skill bundle upload functionality with ZIP file support, multi-step validation, and preview.
    • Added ability to replace existing skill bundles with confirmation.
  • Changes

    • Skill details view is now read-only; inline skill editing removed.
    • Individual skill creation replaced with bundle upload workflow.
    • Removed granular skill asset editing interface.
  • Translations

    • Updated UI text in German, English, and French to reflect upload-based skill management.

Review Change Stack

larryro added 4 commits May 26, 2026 14:18
Skills now ship as zip bundles uploaded through the UI, not edited in
place. The old in-container editor (create / update / write-asset /
delete-asset actions and dialogs) is removed; `uploadSkillBundle` reads
the zip from Convex `_storage`, validates SKILL.md + paths + sizes, and
atomically swaps it onto disk. This kills the deploy-time clobber risk
that motivated `.tale-backup/skills-<ts>` snapshots — now removed from
`tale deploy`, taking the noisy "invalid output path" warning with it.

Also fixes empty-skills-for-new-org: `skills` was missing from the
per-org filesystem scaffolder's DOMAINS list, so newly created
organizations had no `skills/@<slug>/` seeded from the default org. A
one-shot `backfill_skill_scaffolding` migration covers orgs created
before this change.
The left file tree in the skill detail panel was wrapped in a block-display
container that broke the flex height chain, so its overflow-y-auto never
activated and tall bundles (e.g. pptx, 58 files) got silently clipped.
Switch the wrapper to display: contents so the aside becomes a direct
flex-row child and gets a bounded height via stretch.
Multi-agent review of the upload-only skills flow surfaced six merge
blockers and several correctness/leak risks. This PR closes them in one
bundled change.

Data safety
- Per-(orgId, slug) exclusion lock (`skillUploadClaim` table + claim/
  release internal mutations) — concurrent `force: true` uploads to the
  same slug no longer race past the existence check and last-write-wins.
- Storage ownership binding (`skillUploadIntent` table + presign /
  record / verify / delete mutations) — `uploadSkillBundle` now refuses
  any storageId not bound to the calling org, closing the cross-tenant
  read / delete path on `_storage`.
- `_storage` blob lifecycle now wired through a single
  `cleanupUploadResources` helper — `needs_confirm`, dialog close, parse
  failure, and success-finally all delete both the blob and its intent
  row. No more orphans per replace-cycle / cancel.
- `tale deploy` now mirrors the providers branch: `listContainerSkills`
  + `stageSkillsWithoutConflictingBundles` strips any slug already in
  the container so UI uploads survive subsequent host syncs.
- `duplicateSkill` uses byte-preserving `readFileBufferSafe` +
  `atomicWriteBuffer` (new helpers in `convex/lib/file_io.ts`) instead of
  the prior UTF-8 round-trip that corrupted PNGs / PDFs / fonts. Emits a
  dedicated `duplicate_skill` audit action with the source slug.
- Scaffold `copyTree` skips `@<slug>/` entries so creating a new org no
  longer recursively copies every existing org's tenant tree under the
  new org's namespace (skills/integrations/workflows). Agents/providers
  use raw slugs without the marker — follow-up.

Correctness
- Six empty-catch sites (CLAUDE.md hard rule) now log non-ENOENT errors:
  `walkSkillBundle` readdir/stat, `duplicateSkill` readdir,
  `dirHasFiles`, `copyTree` stat, `readSkillMd` lstat. `errnoCode`
  promoted from scaffold to shared `file_io`.
- macOS Finder `__MACOSX/` (plus `.DS_Store` / `Thumbs.db`) is now
  filtered before `detectSingleTopLevelFolder` runs on both client and
  server — the most common Mac packaging workflow no longer fails with
  a misleading "missing SKILL.md" error.
- Upload action audit row now carries `skillMdHash`, `totalBytes`,
  `license`, and `disableModelInvocation` so rollback investigation can
  tie an audit row to a bundle.
- `audit_mutations.ALLOWED_ACTIONS` adds `duplicate_skill` so the new
  action audit fires through the validated enum.

UX + i18n
- Dialog now routes through `useUploadSkillBundle` (was raw `useAction`)
  so the skills list query invalidates on success — table updates
  without a manual refresh, replace flow's detail panel refetches.
- Companion: `lib/config-watcher.ts` typeMap + path parser learn
  `skills`, so SSE-driven invalidation also covers them.
- AbortController + isMountedRef on the dialog — mid-flight close
  cancels the blob POST and skips stale-state setState warnings.
- ConvexError JSON unwrap (`extractErrorMessage`) — user toasts show the
  human message instead of a serialized error object.
- Multi-file / folder / wrong-type drops now produce specific errors,
  and a window-level `dragover`/`drop` preventDefault stops a missed
  drop from navigating away from the page.
- Preview step renders the full SKILL.md frontmatter (license,
  recommended Python/Node, disable-model-invocation, unknown-key count)
  and uses ICU plural for the file count.
- "Replace bundle" button next to Duplicate / Delete in the detail
  panel — opens the upload dialog directly for the visible skill.
- `skills.asset.loadNotFound` was an orphan reference (key removed in
  the prior PR); renamed to `skills.viewer.notFound` and re-added.
- DE i18n hard-fail violations fixed per `TERMINOLOGY_DE.md`:
  `Wird hochgeladen…` → `Lade hoch…`, `Bundle wird gelesen…` →
  `Lese Bundle…`, `Zip` loanword aligned with `.zip-Datei`,
  `dropOrClick` aligned with the integrations dialog metaphor.

Type signature widening: `requireOrgMembershipById` and
`requireOrgAdminOrDeveloper` now accept `ActionCtx | MutationCtx` so the
new V8 mutations can reuse the same auth + role gate as the existing
actions. No behavior change to existing callers.

Verified: `bun run check` green (lint 0 warnings, typecheck clean, 272
test files / 70,878 tests passed). Convex codegen re-run against local
backend; new tables + mutations land in `_generated/api.d.ts`.

Deferred to follow-up issues per scope minimization: same cross-org
leak in agents/providers (raw-slug shape, needs different fix);
`backfill_skill_scaffolding` single-page pagination + per-org try/catch
(manual migration); audit-vs-fs atomicity; zip symlink permission bits;
case-insensitive SKILL.md collision; BOM rejection;
`parse-skill-bundle` test coverage.
…KILL.md parser

`parseSkillMd` is imported from both the Convex Node runtime (where
`Buffer` is a global) and the browser-side skill-upload parser (where
it isn't). The frontmatter-size guard threw `ReferenceError: Buffer is
not defined` the first time a user dropped a real bundle, surfacing as
"SKILL.md frontmatter rejected: Buffer is not defined" in the dialog.

`TextEncoder` is in lib.dom + lib.es2017 and is available on every
runtime we ship to, so byte-length the way the YAML reader wants. Same
behavior, isomorphic.

Latent since the original upload-only-skills commit (e3af6f2); the
existing skills.test.ts suite runs in Node so it didn't catch the
browser path.
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ec15aa20-c4a5-433d-8ab5-9ba3362c7936

📥 Commits

Reviewing files that changed from the base of the PR and between ccf4408 and 23485d8.

⛔ Files ignored due to path filters (2)
  • services/platform/convex/_generated/api.d.ts is excluded by !**/_generated/**
  • services/platform/convex/migrations/backfill_skill_scaffolding.ts is excluded by !**/migrations/**
📒 Files selected for processing (29)
  • services/platform/app/features/skills/components/skill-asset-editor-dialog.tsx
  • services/platform/app/features/skills/components/skill-asset-viewer.tsx
  • services/platform/app/features/skills/components/skill-assets-section.tsx
  • services/platform/app/features/skills/components/skill-create-dialog.tsx
  • services/platform/app/features/skills/components/skill-detail-panel.tsx
  • services/platform/app/features/skills/components/skill-upload/hooks/use-upload-skill.ts
  • services/platform/app/features/skills/components/skill-upload/skill-upload-dialog.tsx
  • services/platform/app/features/skills/components/skill-upload/steps/preview-step.tsx
  • services/platform/app/features/skills/components/skill-upload/steps/upload-step.tsx
  • services/platform/app/features/skills/components/skill-upload/utils/parse-skill-bundle.ts
  • services/platform/app/features/skills/components/skills-action-menu.tsx
  • services/platform/app/features/skills/components/skills-table.tsx
  • services/platform/app/features/skills/hooks/mutations.ts
  • services/platform/convex/lib/auth/require_org_admin_or_developer.ts
  • services/platform/convex/lib/auth/require_org_membership.ts
  • services/platform/convex/lib/file_io.ts
  • services/platform/convex/organizations/scaffold.ts
  • services/platform/convex/schema.ts
  • services/platform/convex/skills/audit_mutations.ts
  • services/platform/convex/skills/file_actions.ts
  • services/platform/convex/skills/file_utils.ts
  • services/platform/convex/skills/schema.ts
  • services/platform/convex/skills/upload_mutations.ts
  • services/platform/lib/config-watcher.ts
  • services/platform/lib/shared/schemas/skills.ts
  • services/platform/messages/de.json
  • services/platform/messages/en.json
  • services/platform/messages/fr.json
  • tools/cli/src/lib/actions/deploy.ts

📝 Walkthrough

Walkthrough

This PR migrates skill management from in-place editing to bundle-based uploads. Users now upload ZIP files containing SKILL.md and assets; the client validates locally while the server re-validates, atomically stages, and swaps bundles in place. Old edit dialogs (SkillAssetEditorDialog, SkillAssetsSection, CreateSkillDialog) are removed entirely. The skill detail panel shifts to read-only with a "Replace bundle" action wired to a new multi-step SkillUploadDialog. Backend changes include new schema tables (skillUploadClaims for locking, skillUploadIntents for storage tracking), new mutations for presigned URLs and slot management, and uploadSkillBundle action for atomic file staging and swap. Supporting changes widen auth contexts, add file I/O helpers, update audit action types, and preserve uploaded bundles during deploy.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch skills-bundle-upload

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Comment @coderabbitai help to get the list of available commands and usage tips.

@larryro larryro merged commit 71b3efa into main May 26, 2026
24 of 28 checks passed
@larryro larryro deleted the skills-bundle-upload branch May 26, 2026 07:30
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