Skip to content

docs(client): Agent Skills — close out the security review's SDK-side remainder - #66

Open
XieX wants to merge 1 commit into
split/skills-typed-outcomefrom
split/skills-review-closeout
Open

docs(client): Agent Skills — close out the security review's SDK-side remainder#66
XieX wants to merge 1 commit into
split/skills-typed-outcomefrom
split/skills-review-closeout

Conversation

@XieX

@XieX XieX commented Sep 4, 2026

Copy link
Copy Markdown

Stacked on #58. Closes the four items left open in our response to the Agent Skills security design review.

Three documentation items and one test item. No behavior changes — the code halves of rows 9 and 2 were already done and are deliberately untouched; the only change to safe_fs.py is its module docstring.

1. Privilege separation (row 9 docs half + counter-proposal for row 26)

The proposed mitigation for AZ-1, the review's most serious finding.

The README now documents reconcile-identity ≠ agent-identity as the recommended deployment, which is the whole reason the 0644/0755 modes deny anything: the agent reads its instructions and cannot rewrite them, or the manifest. Write access to the manifest is the worse half — it is what tells the next reconcile which paths the SDK may delete — which is why _prune re-validates every entry from scratch rather than trusting it.

The operator gets an explicit check to run, because the SDK cannot run it.

ReconcileReport deliberately grows no writability field. Row 26 asked for one; we declined. The SDK knows only its own identity, which trivially has write access, having just written there. Any check it could make would answer a different question than the one asked and manufacture false confidence exactly where the review wants caution. agents.md records that reasoning so the field is not added later by someone reading its absence as an oversight.

2. Three hostile-manifest prune tests (row 12 remainder)

A well-formed manifest listing /etc/passwd, ../../../etc/passwd, and a path under a parent that has since become a symlink. _prune already refuses all three; these turn asserted into verified.

Two things make them worth more than their line count:

  • They are deliberately well-formed. The corrupt-manifest suite above them proves nothing here — a corrupt manifest suppresses every destructive action wholesale, whereas these manifests give the implementation everything it needs to prune, and it must refuse anyway because the recorded path is not one this SDK could own.
  • "Deleted nothing" runs through an unlink spy, not through checking that /etc/passwd still exists. The test process cannot delete that file anyway, so the obvious assertion would pass against an implementation with no path check at all — permissions would be doing the work. The spy proves the removal is never attempted.

3. One sentence on "*" (row 16 remainder)

It materializes the whole project library, so every skill's description enters the agent's context — including skills no AI Config references and skills belonging to other teams.

4. Windows platform bound (row 2 residual) — deferred, explicitly

Decision: defer, with the bound written down. Reparse-point checks (GetFileAttributesW / FILE_FLAG_OPEN_REPARSE_POINT) are not implemented, and that is now recorded in safe_fs.py, agents.md and the README rather than left implied.

Rationale:

  • Windows is not a supported or tested platform for this release, and neither repository has a Windows CI runner — every matrix job is ubuntu-latest — so the checks would ship unverified. Both codebases already cite that absence as a reason not to add os.name/process.platform branches.
  • The TypeScript SDK could not match them in any case. Node exposes no *at() family on any platform, so its racy lstat floor is universal rather than Windows-only. Hardening Python alone would break the cross-language parity the two SDKs are held to.

Two consequences are recorded rather than left to be rediscovered: on Windows write permission on the managed root is the only boundary, which is what makes privilege separation the mitigation and not merely advice; and this retroactively lowers the priority of the row 25 reserved-device-name work, noted where that code lives. That code stays — it keeps a root written on Linux usable when read from Windows — but it is not evidence that Windows is hardened.

Parity

The Node half is launchdarkly/js-ai-sdk#48, case for case. No customer-visible surface moved: the ld.skills.integrity_failure event name, the eight reason_code tokens, the five SkillOutcomeReason tokens, and the log-record serialization are untouched.

Verification

  • uv run pytest — 1484 passed, 11 skipped
  • uv run ruff check . / ruff format --check . — clean
  • uv run mypy packages/*/src (the CI gate) — no issues in 43 source files

🤖 Generated with Claude Code


Note

Overview
Closes the remaining Agent Skills security review items with documentation and tests only — no runtime behavior changes beyond an expanded safe_fs.py module docstring.

The README and agents.md now spell out deployment and platform limits: run write_skills under a different identity than the agent (with a shell check for agent writability), decline a ReconcileReport root-writability field, warn that "*" materializes the full skill library into context, and document the POSIX-only descriptor-pinned guarantee versus Windows’s racy lstat floor (reparse-point hardening explicitly deferred).

test_skills_fs.py adds TestHostileManifestPrune: well-formed manifests that would prune /etc/passwd, traversal paths, or paths under a symlink-swapped parent must error without os.unlink being attempted (unlink spy), matching existing _prune defenses.

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

… remainder

The four items left open in the response to the Agent Skills security design
review. Three are documentation, one is tests; no behavior changes, and the
code halves of rows 9 and 2 are deliberately untouched.

**Privilege separation** (row 9 docs half, and the agreed counter-proposal for
row 26). The recommended deployment runs the reconcile as a different identity
than the agent, which is the whole reason the ``0644``/``0755`` modes deny
anything: the agent reads its instructions and cannot rewrite them, or the
manifest. That is the mitigation for AZ-1, a prompt-injected agent editing its
own skills. Write access to the manifest is the worse half — it is what tells
the *next* reconcile which paths the SDK may delete — which is why
``_prune`` re-validates every entry from scratch rather than trusting it.

The README documents the pattern and hands the operator the check to run,
because the SDK cannot run it: it knows only its own identity, which trivially
has write access, having just written there. So ``ReconcileReport`` grows no
writability field — the review asked for one and we declined, since any check
the SDK could make would answer a different question than the one asked and
manufacture false confidence exactly where caution is wanted. ``agents.md``
records that reasoning so the field is not added later by someone reading its
absence as an oversight.

**Three hostile-manifest prune tests** (row 12 remainder): a well-formed
manifest listing ``/etc/passwd``, ``../../../etc/passwd``, and a path under a
parent that has since become a symlink. ``_prune`` already refuses all three,
so these turn asserted into verified. Two things make them worth more than
their line count. They are deliberately *well-formed* — the corrupt-manifest
suite above them proves nothing here, because a corrupt manifest suppresses
every destructive action wholesale, whereas these manifests give the
implementation everything it needs to prune. And "deleted nothing" is asserted
through an unlink spy rather than by checking that ``/etc/passwd`` still
exists: the test process cannot delete that file anyway, so the obvious
assertion would pass against an implementation with no path check at all.

**One sentence on** ``"*"`` (row 16 remainder). It materializes the whole
project library, so every skill's ``description`` enters the agent's context —
including skills no AI Config references and skills belonging to other teams.

**The Windows platform bound is now explicit** (row 2 residual), in
``safe_fs.py``, ``agents.md`` and the README. Reparse-point checks
(``GetFileAttributesW`` / ``FILE_FLAG_OPEN_REPARSE_POINT``) are not
implemented, by decision: Windows is not a supported or tested platform for
this release, neither repository has a Windows CI runner so the checks would
ship unverified, and the TypeScript SDK could not match them in any case
because Node exposes no ``*at()`` family on *any* platform. Implementing them
in Python alone would break cross-language parity and trade a documented bound
for an unverified one. Two consequences are recorded rather than left to be
rediscovered: on Windows, write permission on the managed root is the only
boundary, which is what makes privilege separation the mitigation and not
merely advice; and this retroactively lowers the priority of the row 25
reserved-device-name work, noted where that code lives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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