fix(client): a broken store answer is not an absent skill - #57
Merged
XieX merged 2 commits intoSep 15, 2026
Merged
Conversation
Two untrusted-store answers were read as data rather than as failures.
`allRawObjects` collapsed a non-object listing to `{}` with no error, so
a store that served nothing usable was indistinguishable from one holding
no skills. `resolveFromStore` read identity off the object without
checking it against the key that was asked for, so an answer served under
a different key came back under the caller's key while carrying its own.
Both are now withheld and reported, alongside the version check that
already guarded the same way. `allRawObjects` now returns the listing
together with the reason it could not answer, with a throwing store
caught and worded in the same place, so `allSkills` and the `'*'`
reconcile path no longer each re-derive the log line and the message.
Ports launchdarkly/python-ai-sdk 5817d89.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Covers the reconcile side of the withheld-answer fix: a listing that is not an object leaves every managed file alone rather than reading as a full revocation, and an answer served under a different key writes nothing, is reported against the key that was asked for, and does not reach that other key's file. Each one previously deleted a file and reported a clean run. Ports launchdarkly/python-ai-sdk 4c6d965. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
XieX
force-pushed
the
xie/skills-08-review-closeout
branch
from
September 14, 2026 20:13
90b45a5 to
888a616
Compare
XieX
force-pushed
the
xie/skills-store-answer-hardening
branch
from
September 14, 2026 20:13
5657291 to
ee5d469
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ports two Agent Skills store-answer fixes from launchdarkly/python-ai-sdk (
split/skills-materialization, launchdarkly/python-ai-sdk#53) into the Node client.What changed
A non-object listing is a broken store, not an empty one (
packages/client/src/skills-core.ts).allRawObjectsused to collapse anythingstore.allObjects()returned that was not an object down to{}with no error. A store that served nothing usable was therefore indistinguishable from a store holding no skills, which the'*'reconcile read as "every skill was revoked" and pruned accordingly. It now returns{ objects, error }, logs an error naming the returned type (the skill store listed skills as null rather than an object), and puts that answer on the same footing as a store that threw. A throwing store is caught in the same place with the same wording (storeThrew), soallSkillsand the reconcile'sresolveAllno longer each re-derive the log line and the message.allSkillsstill returns[]on either failure;writeSkills('*')reports an incomplete run and prunes nothing. Mirrors Python'slist_raw_objectsreturning(objects, error).An answer under a different key is withheld (
resolveFromStore). Identity is read off the object itself, so a store answering under a different key used to hand the caller aSkillcarrying someone else's key. The reconcile then wrote that other key's path and, since prune keys off the request, deleted it in the same pass while reportingok. After verification,skill.key !== keyis now withheld withskill '<key>' is not available: the store answered under key '<skill.key>', using the same shape as the existing version-mismatch branch.Tests. Accessor side from Python
5817d89(aliased key withheld bygetSkill/getSkillResult; non-object listing reported viaallRawObjectswith the type named) and reconcile side from4c6d965(a non-object listing leaves every managed file and manifest entry alone; an aliased answer writes nothing, is reported against the requested key, and never reaches the other key's file). All six fail against the unpatched source.Commits mirror Python
5817d89and4c6d965.Notes for review
Resolutionhas no typed outcome, so there is nothing to port there. Here it reuseswrong_version, per the existing branch's shape: the store held an answer, but not the one that was asked for. If a dedicated token is wanted it is a cross-language vocabulary change and belongs in its own PR.e2b54fd(an unheld version pin is a miss, not an integrity failure) is deliberately not ported. Python'sInMemorySkillStorefiles several versions per key plus a version-less slot, and that commit changes which one a pinned miss falls back to. Node'sInMemorySkillStoreholds exactly one object per key and ignores the version argument, lettingresolveFromStorerefuse a wrong-version answer afterwards. There is no fallback slot to fix, so the change has no direct equivalent; whether Node should grow multi-version semantics is a separate decision.xie/skills-fdv2-transport) sits above this branch and will need a rebase once this lands, sinceallRawObjectschanged its return type andresolveAll/allSkillschanged accordingly. Not rebased here.Verification
vitest runinpackages/client: 652 passed, 2 skippedtsc --noEmit: cleanbiome check: clean🤖 Generated with Claude Code
Note
Overview
Hardens the Node client against untrusted skill store behavior (ported from the Python SDK): broken listings and key aliasing no longer look like “no skills” or succeed while corrupting disk state.
Non-object listings —
allRawObjectsnow returns{ objects, error }instead of silently{}. Throws and invalid list shapes (null, array, etc.) are logged with consistent wording viastoreThrew, andwriteSkills('*')treats a listing error as an incomplete run so prune does not delete managed files.allSkillsstill returns[]on failure but the error is available on the listing path.Key aliasing — After verification,
resolveFromStorewithholds answers whereskill.key !==the requested key, with the same outcome shape as version mismatch (wrong_version). That blocksgetSkillfrom returning the wrong identity and stopswriteSkillsfrom writing under one key and pruning under another in the same reconcile.Tests cover accessor and filesystem reconcile paths for both fixes.
Reviewed by Cursor Bugbot for commit ee5d469. Bugbot is set up for automated code reviews on this repo. Configure here.