Skip to content

feat(personal-kits): answer kit.api_disabled while off and add an enablement smoke - #109

Open
mrgoonie wants to merge 3 commits into
mainfrom
feat/personal-kits-enablement-signal
Open

mrgoonie wants to merge 3 commits into
mainfrom
feat/personal-kits-enablement-signal

Conversation

@mrgoonie

@mrgoonie mrgoonie commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Part of #88 (the "API enablement, rollback flag and smoke receipts" part left
open after #94). It covers the server side that AgentKit needs to run personal
kits against SkillX once an operator turns the API on. It does not turn it on.

  • Flag-off answer. While SKILLX_PERSONAL_KITS_API is off, every kit path
    still answers 404. It now carries the code kit.api_disabled, answered
    before any authentication or lookup, and names no kit, owner or state.
    AgentKit (bestagentkits/agentkit#2589) maps it to "unsupported" (exit 2) and
    says the deployment does not offer personal kits yet. A plain not_found
    stays "no such kit", so it is never taken as a deleted kit. The flag accepts
    "1" or "true" (trimmed, any case), like SKILLX_RELEASES_API; tests pin
    the on and off values, and the docs and the wrangler.jsonc comment say so.

  • CLI. packages/cli kitApi maps 404 kit.api_disabled to
    KitApiDisabledError ("this SkillX deployment does not offer personal kits
    yet ... your local kits are unaffected"). It is still an ApiError, and a
    plain 404 stays an ordinary ApiError. ApiError gains the raw body and a
    code getter (an additive change).

  • Enablement smoke. scripts/smoke-personal-kits.mjs checks:

    • an unknown kit gets 404 and anonymous gets 401;
    • an idempotent replay changes nothing;
    • a stale If-Match gets 412 kit.generation_conflict;
    • activation works and the lock reads back;
    • for another account:
      • read is 404;
      • write (store revision) is 404;
      • activate is 404;
      • creating a kit under the owner's handle is 403 kit.publisher_not_owned;
      • the owner's kit is unchanged afterwards.

    Behavior:

    • It prints a JSON receipt with no keys in it.
    • It refuses a non-loopback base unless --allow-remote is passed. A remote
      run also refuses a non-https base and refuses to start without SKILLX_SMOKE_OTHER_KEY, so the
      cross-account checks always run there.
    • Every request uses redirect: "manual", and a 3xx fails the check.
    • It exits 2 on kit.api_disabled.
  • Cleanup. The API cannot delete a kit: revisions and audit rows are
    immutable, and kits are ON DELETE RESTRICT (migration 0015). So, instead of
    a new random kit per run, the smoke reuses one fixed private kit,
    @handle/agentkit-smoke (--kit-name to change it). Each run appends and
    activates one revision there, and a per-run profile keeps the revision
    unique. The runbook adds a cleanup step: use a dedicated smoke account, and
    never point the smoke at a real user's handle. The second account's attempt
    to create @handle/smoke-squat-<random> is refused with 403 before any write, so it leaves no kit.

  • Runbook. docs/personal-kits-api.md "Enablement and rollback":

    • the flag is the only switch, with no migration;
    • how to run the smoke, with two accounts;
    • cleanup;
    • rolling back to "0" keeps every stored revision, lock and audit row.

    The error table lists kit.api_disabled.

Not in this PR (on purpose)

E2E staging receipt: BLOCKED

There is no SkillX staging Worker and no SKILLX_STAGING_TOKEN, so this PR has
no staging smoke receipt. What exists is a local receipt, and it is not a
production-readiness claim. apps/web/tests/personal-kits/kit-enablement-smoke.test.ts
runs the smoke against the real api.v1.kits and release-manifest route
modules over the in-memory D1/R2 test harness:

  • all 14 checks pass with the flag on, including the cross-account write
    checks; the foreign writes store nothing (1 kit, 1 revision);
  • a second run reuses the same kit (1 kit, 2 revisions);
  • a 302 fails the first check with code redirect and is never followed;
  • with the flag off it reports enabled: false and writes nothing;
  • a remote run without a second key or over plain http, and bad input, are
    refused before any request.

Tests

  • pnpm test at cc7f69a: 104 files, 2136 passed and 2 skipped.
  • pnpm -r --if-present run typecheck (skill-tree, cli, contracts, web)
    exits 0.
  • packages/cli tsup build succeeds.

Related

…blement smoke

While SKILLX_PERSONAL_KITS_API is off every kit path still answers 404, now
with the code kit.api_disabled, before any authentication or lookup. A client
can then say the deployment does not offer personal kits instead of reporting
a missing kit; the answer names no kit, owner or state.

scripts/smoke-personal-kits.mjs stores one throwaway private kit and checks
anonymous 401, idempotent replay, 412 on a stale If-Match, activation, the lock
read-back and 404 for another account, then prints a JSON receipt without
keys. It refuses a non-loopback base unless --allow-remote is passed and exits
2 when the deployment answers kit.api_disabled. A vitest runs it against the
real route modules over the in-memory D1/R2.

docs/personal-kits-api.md gains an enablement and rollback runbook. The
production flag value in wrangler.jsonc is unchanged.
@mrgoonie

Copy link
Copy Markdown
Contributor Author

AgentKit counterpart (draft): bestagentkits/agentkit#2647 maps 404 kit.api_disabled to source.unsupported (exit 2). A plain 404 stays source.not_found.

@mrgoonie mrgoonie left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review độc lập cho head 69dbb139e

Kết luận: 0 blocking, 4 non-blocking. Phần flag-off trả đúng 404 kit.api_disabled trước cả auth, lookup và rate limiter. Phản hồi có Cache-Control: no-store và không nhắc tới kit, owner hay state nào. apps/web/wrangler.jsonc vẫn để "SKILLX_PERSONAL_KITS_API": "0".

Bằng chứng đã chạy

  • Môi trường: worktree tạm, pnpm install --frozen-lockfile.
  • pnpm test: 104 files, 2130 passed và 2 skipped.
  • apps/web: react-router typegen rồi tsc -b, exit 0.
  • Probe scripts/smoke-personal-kits.mjs, dùng key giả và một fake loopback, không gửi gì tới skillx.sh:
    • --base https://example.invalid bị từ chối, không có request nào.
    • Các host localhost.example.com và 127.0.0.1@evil.example đều bị từ chối.
    • Fake trên 127.0.0.1 trả kit.api_disabled: exit 2, đúng 1 request GET, receipt có enabled:false.
    • Không có key nào xuất hiện trong stdout hay stderr.
  • Tenancy, xem trong code hiện có vì diff không đổi phần này:
    • serveKit: read đi qua resolveViewer + rate limit + 401 khi anonymous.
    • Write đòi actor.kind === "user", nếu không thì trả 404.
    • Admin secret không thấy kit nào, trừ các admin route.
    • Kit của người khác trả cùng 404 not_found như kit không tồn tại. Smoke harness test "other account is 404" đã chạy qua route thật.
  • Rate limit: phản hồi flag-off bỏ qua limiter. Điều này chấp nhận được vì nó là hằng, không chạm D1 hay R2.

Non-blocking

  1. Tenancy check của smoke là tùy chọn, và chỉ kiểm tra phía đọc.
    • Khi thiếu SKILLX_SMOKE_OTHER_KEY, receipt vẫn ok: true mà không có check cross-account. Với #88/#2589, "private permissions" là tiêu chí GO.
    • Đề xuất: bắt buộc có other key khi --allow-remote, hoặc ghi check skipped và để ok:false.
    • Nên thêm check cross-account write: other key gọi POST revisions lên kit của owner thì phải nhận 404 và không tạo row nào.
  2. Kit smoke tồn tại vĩnh viễn trên deployment. Mỗi lần chạy lại tạo thêm một kit @handle/smoke-<random>, mà API không có endpoint xóa. Runbook nên nói rõ điều này, và chỉ cách dọn: dùng tên cố định qua name, hoặc ghi SQL dọn kèm rollback note.
  3. Smoke vẫn follow redirect mặc định của fetch. Undici có strip Authorization khi redirect sang origin khác, nhưng nên đặt thêm redirect: "manual" để defense-in-depth: base loopback không thể chuyển owner key đi nơi khác, và một 3xx sẽ thành check fail rõ ràng.
  4. Doc chưa khớp code. Runbook ghi "1" bật API, nhưng personalKitsApiEnabled có sẵn từ trước và cũng nhận "true". Nên ghi đúng, hoặc thu hẹp code lại chỉ nhận "1".
    • Thêm một gợi ý (không bắt buộc): packages/cli/src/lib/kit-api.ts chưa map kit.api_disabled thành thông báo thân thiện như AgentKit đã làm.

Đối chiếu với AgentKit

  • bestagentkits/agentkit#2647 map 404 kit.api_disabled thành source.unsupported (exit 2). Tôi đã probe bằng binary thật với fake loopback và thấy khớp.
  • Các 404 not_found khác vẫn là source.not_found.
  • Contract giữa hai bên nhất quán.

Đây là COMMENT review, không approve.

…bled in the CLI

The smoke now checks cross-account writes as well as reads: a second account
gets 404 storing or activating a revision, 403 kit.publisher_not_owned creating
a kit under the owner's handle, and leaves the owner's kit unchanged. A remote
run refuses to start without SKILLX_SMOKE_OTHER_KEY, and no request follows a
redirect (a 3xx fails the check).

The API cannot delete a kit (immutable revisions and audit, ON DELETE
RESTRICT), so the smoke reuses one fixed private kit (--kit-name, default
agentkit-smoke) and appends one revision per run instead of a new kit each
time; the runbook gains the cleanup step. The docs state both accepted
enable values ("1" or "true").

The CLI kit client maps 404 kit.api_disabled to KitApiDisabledError, a clear
"this deployment does not offer personal kits yet" error, while a plain 404
stays an ordinary API error.

@mrgoonie mrgoonie left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-review các commit sửa (đến d8480db)

Kết luận: tất cả finding trước đó đã được sửa. Không có blocking. 3 điểm non-blocking ở cuối.

Từng finding

Finding Kết quả Bằng chứng
--allow-remote phải có SKILLX_SMOKE_OTHER_KEY, kiểm tra cross-account đọc và ghi Fixed main() trả 1 khi remote mà thiếu key thứ hai (test kiểm tra thông báo). Khi có key: đọc 404, ghi revision 404, activate 404, tạo kit dưới handle của owner 403 kit.publisher_not_owned, và kit của owner không đổi sau đó. Key sai hoặc trùng owner thì check fail (fail closed).
Kit smoke dùng lại, --kit-name, bước runbook (API không xóa được) Fixed Kit cố định @handle/agentkit-smoke, profile theo run để revision không trùng. docs/personal-kits-api.md có bước Clean up (tài khoản smoke riêng, không trỏ vào handle người dùng thật) và nêu lý do (ON DELETE RESTRICT, migration 0015).
redirect: "manual", 3xx là failure Fixed Mọi request dùng redirect: "manual"; opaqueredirect hoặc 3xx cho code redirect và check fail. Có test (302 làm check đầu fail, mọi request mang manual).
Flag nhận "1" hoặc "true", doc Fixed personalKitsApiEnabled trim, lowercase, so với 1 hoặc true, giống releasesApiEnabled; doc ghi đúng.
CLI map kit.api_disabled sang KitApiDisabledError Fixed kit-api.ts bọc apiRequest: 404 + code kit.api_disabled thì ném KitApiDisabledError extends ApiError; 404 not_found vẫn là ApiError thường. Có test hai chiều.
wrangler.jsonc flag vẫn "0" Confirmed apps/web/wrangler.jsonc:72 là "0", và PR không sửa file này.

Mọi đường kit (kể cả quarantine) đi qua serveKit, nên câu trả lời flag-off phủ toàn bộ và đến trước auth và lookup.

Kiểm tra đã chạy (worktree tạm tại commit d8480db, đã xóa sau khi xong)

  • vitest run cho apps/web/tests/personal-kits, packages/cli/src/lib, scripts: 15 file, 435 test, pass.
  • pnpm -r run typecheck: không có lỗi.
  • Không gọi production skillx.sh; chỉ test trong bộ nhớ.

Non-blocking

  1. Smoke remote không yêu cầu https. Với --allow-remote và http:// ở host remote, cả hai API key đi dạng cleartext. Nên từ chối non-loopback không phải https.
  2. Test chưa phủ giá trị "true" (và khoảng trắng) của flag; chỉ có "1" và "0". Một test nhỏ cho personalKitsApiEnabled là đủ.
  3. Comment trong apps/web/wrangler.jsonc vẫn ghi "0" = off (404 everywhere), chưa nhắc kit.api_disabled. Dòng "stores nothing" của squat probe chỉ đúng khi server đúng; nếu server hồi quy thì kit squat không xóa được. Nên ghi điều kiện đó trong runbook.

CLEAN: 0 blocking, 3 non-blocking.

… values

A remote enablement smoke now refuses a base URL that is not https, since
it sends account keys. Tests pin the accepted flag values: "1" or "true",
trimmed and in any case, and nothing else. The wrangler.jsonc comment and
the runbook state those values (the production value stays "0"), and the
runbook names the cross-account create probe precisely.
@mrgoonie
mrgoonie marked this pull request as ready for review October 19, 2026 19:54

This branch has not been deployed

No deployments
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