Skip to content

feat(kits): If-Match preconditions and composition metadata for personal kits - #94

Merged
mrgoonie merged 2 commits into
mainfrom
feat/personal-kit-api-gaps-88
Oct 16, 2026
Merged

mrgoonie merged 2 commits into
mainfrom
feat/personal-kit-api-gaps-88

Conversation

@mrgoonie

Copy link
Copy Markdown
Contributor

Refs #88 (does not close it: items 3, 4 and 7 are decisions recorded on the issue).

Summary

Closes the personal-kit API gaps AgentKit hit in G2 (bestagentkits/agentkit#2278) that SkillX can fix in code. Everything stays behind SKILLX_PERSONAL_KITS_API. No migration, no deploy.

Item 2: If-Match on kit writes

  • Every generation-guarded write (revisions, rebase, conflicts/:id/resolve, activate, rollback, revisions/:id/reject, revisions/:id/review, proposals, proposals/validate) accepts If-Match: "g<n>" (the kit ETag) or a bare <n>.
  • The body expectedGeneration still works. Both may be sent when they agree; a disagreement, a list, * or a weak tag is 400 invalid_request.
  • A stale If-Match is 412 kit.generation_conflict and writes nothing. A stale body field keeps its 409.
  • One place: withIfMatch in the router's body reader, and the 409 → 412 mapping in serveKit.

Item 6: pins on older releases

  • The server already resolved any readable release by package, version and digest. A new test proves an older release pin stores, activates and locks with no warning, a yanked pin still stores with its warning, and an unknown version is kit.base_unavailable. The rule is now written in the docs.

Item 1: optional composition on revision documents

  • composition: { extends, include?, resolutions? } mirrors AgentKit's manifest extends/include and agentkit.skill-overlay-resolution/v1 records.
  • Strict validation (kit.invalid at the path), nothing normalized or reordered, so client and server digests agree. Limits: 1 to 16 unique extends, 256 unique include, 1,000 records unique by identity, path and category.
  • It is part of the canonical JSON and so of the revision digest. A document without it is byte-identical to before (the existing golden vector still passes). New vector: packages/contracts/src/fixtures/personal-kit-composition-golden.json, digest cross-checked with an independent canonical-JSON hash.
  • SkillX stores it verbatim and never interprets it. Rebase and conflict-resolution revisions drop it because they move the pins the records describe; a session-derived proposal keeps it (it never moves a pin). diff reports a changed composition.

Tests

  • pnpm typecheck: pass.
  • pnpm test: 97 files, 2065 passed, 2 skipped.
  • New: apps/web/tests/personal-kits/kit-preconditions.test.ts, apps/web/tests/personal-kits/kit-composition.test.ts, packages/contracts/src/personal-kit/kit-composition.test.ts.

Docs impact

docs/personal-kits-api.md: concurrency section (If-Match, 412), base pin rule, the composition section and the error table.

🤖 Generated with Claude Code

A write may send the expected generation as If-Match (the kit ETag "g<n>"
or a bare <n>) instead of, or agreeing with, the body expectedGeneration.
A stale If-Match answers 412 kit.generation_conflict; the body field keeps
its 409. Also proves a pin on an older or yanked release still stores.
Revision documents may carry AgentKit's extends, include and resolution
records under composition. It is strictly validated, never normalized and
part of the revision digest; documents without it keep their digests.
Rebase and conflict resolution revisions drop it because they move the
pins the records describe.
@mrgoonie

Copy link
Copy Markdown
Contributor Author

Controller review: no blockers.

Checked

  • If-Match: accepts "g<n>" (matching the strong ETag the GET returns), "<n>" and <n>. If it and expectedGeneration disagree, the request gets a 400. A stale generation returns 412 only when If-Match was sent and 409 otherwise, so existing clients behave as before.
  • composition: schema-checked with unknown-field refusal, uniqueness and bounds on every list. It is optional and stays out of the document when absent, so existing digests are unchanged. When present it is part of the canonical document and therefore of its digest. The diff now covers it. There is no D1 migration, since the field lives in document_json.
  • Item 6: already supported. The new test pins an older release.

Nits (non-blocking)

  • KIT_COMPOSITION_LIMITS.maxText is unused.
  • A server-built revision (rebase or conflict resolution) drops composition. A kit synced right after one of those cannot be rebased from its copy until the next manual revision. That is documented and acceptable; AgentKit will treat a missing composition as "re-derive locally".
  • If-Match: * is rejected with 400 rather than treated as "any". That is fine for this API.

@mrgoonie
mrgoonie merged commit f165547 into main Oct 16, 2026
1 check passed
@mrgoonie
mrgoonie deleted the feat/personal-kit-api-gaps-88 branch October 16, 2026 06:42
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