Skip to content

docs(client): document Agent Skills - #35

Open
XieX wants to merge 1 commit into
xie/agent-skills-feature-ac9ac7from
xie/skills-07-docs
Open

XieX wants to merge 1 commit into
xie/agent-skills-feature-ac9ac7from
xie/skills-07-docs

Conversation

@XieX

@XieX XieX commented Aug 25, 2026

Copy link
Copy Markdown

Stacked PR 7 of 7 — merge bottom-up into xie/agent-skills-feature-ac9ac7.

  1. feat(client): Agent Skills value types #29 — value types
  2. feat(client): symlink-refusing filesystem primitives #30 — symlink-refusing filesystem primitives
  3. feat(client): Agent Skills store seam, verification, and accessors #31 — store seam, verification, accessors
  4. test(client): integrity verification and the accessor telemetry sweep #32 — integrity + telemetry tests
  5. feat(client): writeSkills materialization and the abuse matrix #33 — writeSkills + abuse matrix
  6. test(client): the rest of the writeSkills suite #34 — remaining writeSkills tests
  7. docs(client): document Agent Skills #35 — docs ← you are here

Each PR targets the one below it, so GitHub already shows only this PR's own diff.


Last of seven. Documents the feature now that all of it exists. The README section and the contributor notes both span every layer in the stack, so they land once here rather than as six partial edits to the same two files.

README.md

The public API — the accessors, writeSkills, the store seam, the exported constants, and the types — plus three things stated plainly rather than left for a reader to discover:

Content delivery is not wired up in this release. Everything runs against SkillStore, the shipped default is absent, and the accessors throw an actionable error until one is configured.

Skill content is opaque to the SDK. A verified Skill carries content as a Uint8Array — the exact bytes that were hashed — and the SDK never parses, decodes, or interprets it. There is deliberately no frontmatter accessor and no YAML dependency; the docs show the one-line TextDecoder step for consumers who want a string, and leave frontmatter parsing to them.

The security posture, including its limitation. Node exposes no renameat/unlinkat, so the final rename cannot be performed relative to a pinned directory descriptor. An attacker who already has write permission on the managed root can still win a race to redirect a write or a delete outside it. Keep the managed root writable only by the process running the SDK.

The one narrow exception to the reconcile's conservatism, stated where the conservatism itself is described: a colliding file whose bytes already are the resolved content is adopted rather than refused, so a reconcile killed between the content writes and the final manifest write heals itself instead of wedging those skills forever. Nothing is overwritten, because nothing needs to be — the only file ever adopted is already byte-identical to what LaunchDarkly resolved. Differing bytes, and bytes that cannot be read at all, are still refused and left untouched.

The two key constraints the filesystem layer imposes that the data model does not — the 255-byte path-component bound and the 22 Windows reserved device names (con, prn, aux, nul, com1com9, lpt1lpt9) — including that a key like aux stays valid everywhere else, so an AI Config referencing it still parses and its other skills still materialize. Plus the residual the SDK cannot check for a caller: the bound is per component, not on the total path, so a deep root and a long key can still exceed Windows' 260-character MAX_PATH. One sentence, deliberately not a check — the root is the customer's.

agents.md

What a future change has to preserve:

  • Module ownership rows for the new modules.
  • The one-way dependency direction — typesskills-coreskills, and types + safe-fs + skills-coreskills-fs — and why closing a cycle there would let the accessor and filesystem layers disagree about whether a store is configured.
  • A Dependencies table giving a reason for every runtime dependency and optional peer.
  • Skill content documented as what it is: Uint8Array of the verified verbatim bytes, opaque to the SDK, with the single wire-string encode living inside integrity verification and nowhere else.

Four new pitfalls, each a mistake that would otherwise pass review: interpreting skill content anywhere in the SDK (a parser, a convenience accessor, an encoding assumption), reading writeSkills's timeout as milliseconds, relaxing a path or manifest check in skills-fs.ts, and bypassing fsOps for a destructive call — which makes the operation invisible to the assertions that prove no operation was attempted, so they then pass vacuously.

The path-check pitfall now carries the three specifics a later contributor is most likely to widen without reading the reasoning first:

  • Why the reserved-name rejection is unconditional rather than platform-gated, and why it lives in keyRejectionReason rather than in the key grammar: a grammar-level rejection would invalidate the whole AI Config for a customer who never touches Windows, and a dropped skillRefs entry lets prune delete the on-disk copy — turning "fails to write on Windows" into "gets deleted on Linux".
  • That adoption is a narrowing of the clobber refusal and requires an exact hash match; anything weaker and the guarantee is gone. skipped_current is reused deliberately, since a new ReconcileActionKind member would break consumers with an exhaustive switch.
  • That every flag in readRegularFile is load-bearing, O_NONBLOCK most of all, because opening a FIFO with no writer blocks forever — and that the sweep derives its filename pattern from tempNamePattern rather than copying it.

Also here

One pre-existing test renamed to describe its behaviour directly instead of citing a specification section.

getSkillResult (security review LA-2)

README.md gains the accessor in the Agent Skills table, SkillOutcome/SkillOutcomeReason in Shared Types, and a Fail closed on tampering subsection under the observability material — a worked example exiting on integrity_failure while treating absent as ordinary, a table of the five tokens, and an explicit statement that getSkill is unchanged and that the two accessors differ only in what they report.

agents.md records the five-token vocabulary, why it is coarser than IntegrityReasonCode, and the full Resolution → reason mapping including the two skills-fs.ts rows — so a later contributor adding a sixth internal outcome has to decide which public token it maps to rather than defaulting to absent. Plus the rule that the version goes into getObject while the equality check stays a defense, and that getSkill's contract is frozen.

Verification

Workspace 1172 passing (+2 skipped). typecheck, biome, and sherif clean.

🤖 Generated with Claude Code


Note

Overview
Documentation-only PR that publishes the Agent Skills stack for @launchdarkly/ai-server: a large Agent Skills section in README.md and matching contributor guidance in agents.md, after the feature landed in earlier stacked PRs.

The README now documents the public surface (skillRefs, getSkill / getSkillResult, writeSkills, SkillStore / InMemorySkillStore), states that remote content delivery is not wired yet (store must be configured), and spells out security/reconcile behavior (hash verification, manifest reconcile, ld.skills.integrity_failure SIEM logging, fail-closed tampering handling). It also updates initClient(client, options?) (optional skillStore on BYOC) and adds Agent Skills rows to the shared types table.

agents.md adds the skills module layout, one-way dependency rules, a Dependencies table, skillStore lifecycle notes, new pitfalls (opaque Uint8Array content, writeSkills timeout in seconds, non-relaxable fs checks, fsOps), and extended invariants (telemetry allowlist, vocabularies, frozen getSkill contract).

A single test in client.test.ts is renamed to drop a spec section reference; no runtime behavior changes in this diff.

Reviewed by Cursor Bugbot for commit bfb40f6. Bugbot is set up for automated code reviews on this repo. Configure here.

@XieX
XieX requested a review from andrewklatzke September 17, 2026 19:35
Base automatically changed from xie/skills-06-write-fs-tests to xie/agent-skills-feature-ac9ac7 September 18, 2026 19:19
Documents the feature now that all of it exists. The README section and the
contributor notes both span every layer in the stack, so they land once rather
than as six partial edits to the same two files.

`README.md` gains the public API — the accessors, `writeSkills`, the store seam,
the exported constants, and the types — and states two things plainly rather
than leaving a reader to discover them. Content delivery is not wired up in this
release: everything runs against `SkillStore`, the shipped default is absent,
and the accessors throw an actionable error until one is configured. And the
security posture, including the limitation: Node exposes no
`renameat`/`unlinkat`, so the final rename cannot be performed relative to a
pinned directory descriptor, and an attacker who already has write permission on
the managed root can still win a race to redirect a write or a delete outside
it. Keep the managed root writable only by the process running the SDK.

Two behaviours a reader would otherwise be surprised by are stated where the
conservatism itself is described. First, the one narrow exception to "a file you
placed yourself is never overwritten": a colliding file whose bytes *already are*
the resolved content is adopted rather than refused, which is what lets a
reconcile killed between the content writes and the final manifest write heal
itself instead of wedging those skills forever. Nothing is overwritten, because
nothing needs to be. Differing bytes, and bytes that cannot be read at all, are
still refused and left untouched. Second, the two key constraints this layer
imposes that the data model does not — the 255-byte path-component bound and the
22 Windows reserved device names — including why a key like `aux` still parses
everywhere else, and the residual the SDK cannot check: the bound is per
component, so a deep root plus a long key can still exceed Windows' `MAX_PATH`.

`agents.md` records what a future change has to preserve: the module ownership
rows, the one-way dependency direction (`types` <- `skills-core` <- `skills`,
and `types` + `safe-fs` + `skills-core` <- `skills-fs`) and why closing a cycle
there would let the accessor and filesystem layers disagree about whether a
store is configured, and a Dependencies table giving a reason for every runtime
dependency and optional peer. Skill content is documented as what it now is:
`Skill.content` is a `Uint8Array` of the verified verbatim bytes, opaque to the
SDK — no frontmatter accessor, no YAML dependency, and consumers who want
structure parse the bytes themselves.

Four new pitfalls, each one a mistake that would pass review: interpreting
skill content anywhere in the SDK, reading `writeSkills`'s `timeout` as milliseconds,
relaxing a path or manifest check in `skills-fs.ts`, and bypassing `fsOps` for a
destructive call — which makes the operation invisible to the assertions that
prove no operation was attempted, so they then pass vacuously.

The path-check pitfall carries the three specifics most likely to be widened by
someone who has not read the reasoning: why the reserved-name rejection is
unconditional rather than platform-gated and why it lives in `keyRejectionReason`
rather than in the key grammar (a grammar-level rejection would invalidate the
whole AI Config, and a dropped `skillRefs` entry lets prune delete the on-disk
copy — "fails to write on Windows" becoming "gets deleted on Linux"); that
adoption is a narrowing of the clobber refusal and requires an exact hash match;
and that every flag in `readRegularFile` is load-bearing, `O_NONBLOCK` most of
all, since opening a FIFO with no writer blocks forever.

Also renames one pre-existing test to describe its behaviour directly instead of
citing a specification section.

Workspace 1148 passing (+2 skipped). typecheck, biome, and sherif clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
XieX added a commit that referenced this pull request Sep 18, 2026
> **Stacked PR 6 of 7 — merge bottom-up into
`xie/agent-skills-feature-ac9ac7`.**
> 1. #29 — value types
> 2. #30 — symlink-refusing filesystem primitives
> 3. #31 — store seam, verification, accessors
> 4. #32 — integrity + telemetry tests
> 5. #33 — writeSkills + abuse matrix
> 6. **#34 — remaining writeSkills tests ← you are here**
> 7. #35 — docs
>
> Each PR targets the one below it, so GitHub already shows only this
PR's own diff.

---

Sixth of seven. Test-only — the functional half of the materialization
suite, split out so it isn't read alongside the implementation and the
abuse matrix in one sitting.

This closes the coverage gap the previous PR flagged: until now
`writeSkills` had its defenses proven but not its ordinary behaviour.

## What's covered

- **Basic writes and the returned report** — what lands on disk, at what
path, and what each `ReconcileAction` says about it.
- **Manifest** — the serialization is asserted byte for byte, since it's
a cross-language on-disk contract. Written literally rather than by
importing the implementation's own constants: a test that imported them
could not detect a change to them.
- **Reconcile semantics** — `written`, `updated`, `skipped_current`,
`removed`, and the `prune` and `onUnavailable` behaviours, including
that a retrieval failure under `keep` leaves existing content alone.
- **Root handling**, and the bare-string guard on the argument surface.
- **Atomicity and permissions** — the temp file is created in the
target's own directory so the rename is same-filesystem rather than
cross-device, the mode is `0644`, and a failure injected between write
and rename leaves no partial file and produces an `error` action.
- **Resilience** — an unreadable directory, a failing unlink, a manifest
that cannot be rewritten. One skill failing never takes the run down,
and every failure appears in the report.
- **Verify-then-write** — content is re-verified immediately before the
write, so a `Skill` a caller built by hand gets the same treatment as
one an accessor produced. Directly constructed `Skill`s carry
`Uint8Array` content, and the pass hashes those bytes as-is; the
`content_bytes` telemetry property is asserted to be the encoded byte
count.
- **Orphaned temp files** — the sweep added in #33 removes an orphan a
killed reconcile left behind, and does so ahead of the prune, so the
`rmdir` that empties a skill directory is no longer blocked by one. Most
of the block is about what the sweep must *not* touch: a customer file,
a temp-shaped name that fails the anchored pattern at either end, a temp
for another target, a symlink wearing a temp name, anything in the
managed root itself, and everything at all when the manifest is corrupt.
- **The write half of the telemetry seam**, with the same allowlist
sweep the accessor half applies: three signal names, no content, no
paths.

## Verification

Client package 555 → 619 tests (+2 skipped). Workspace 1148 passing.
`typecheck`, `biome`, and `sherif` clean.

The 2 skips are expected: the TOCTOU swap-race pair, gated off
`SUPPORTS_DIR_FD`, which is `false` on Node because it exposes no
`*at()` syscall family.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> **Test-only expansion** of `skills-fs.test.ts`: closes the gap where
`writeSkills` had security/abuse coverage but not ordinary
materialization behavior.
>
> Adds large new Vitest blocks for **basic writes** (Skill vs store
refs, `"*"`, reconcile actions), **manifest** byte-level contract
(sorted keys, no trailing newline, `\uXXXX` escaping, unknown field
preservation), **reconcile/prune** semantics, **root and argument
validation**, **atomic rename/unlink** (including `interceptUnlink` and
`assertAtomicRenameOf`), **resilience** (`onUnavailable`, timeouts, no
prune on store outage, no retries), **verify-then-write**, **orphaned
temp sweep** (narrow pattern, corrupt manifest suppresses sweep), and
the **write-side telemetry** allowlist (Materialized, Revoked,
Integrity; no paths or bodies).
>
> Shared harness additions: `RecordingEmitter`, `_setStore` /
`_setEmitterForTesting`, telemetry signal constants, and a **test
hygiene** check that `fsOps` mocks are restored between tests.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1c2c602. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@XieX
XieX force-pushed the xie/skills-07-docs branch from 50721ca to bfb40f6 Compare September 18, 2026 19:19
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.

2 participants