Repository navigation
feat(subjects): distribution_digest, the digest a well-known skill index carries - #343
Conversation
|
✅ OpenCodeReview: Review complete: 0 finding(s) across 4 selected item(s). Review details
|
| assert scorecard["subject"]["distribution_digest"] == "sha256:" + hashlib.sha256(b"body").hexdigest() | ||
| validate_artifact("subject_scorecard", scorecard) |
There was a problem hiding this comment.
Nit: This descriptor has no members, so deleting the new per-member distribution_digest field from build_subject_scorecard would leave all distribution-digest tests passing. Schema validation would not catch it either: members has no item constraints. Add a bundle scorecard test with a standalone SKILL.md member and a resource-bearing or non-local member, asserting their digest and null values respectively.
There was a problem hiding this comment.
🟡 Changes recommended
File references omit the distribution digest for accompanying resources, allowing evaluation evidence to be associated with a standalone skill installation that lacks those resources.
1 open issue
What changed in this PR
1 new finding.
This PR adds a distribution digest to subject scorecards so published evaluation evidence can be matched to an installed skill.
Changes:
- Adds nullable
distribution_digestvalues for subjects and bundle members without changingref_digest. - Hashes eligible local
SKILL.mdbytes and extends the scorecard schema. - Adds digest tests and a changelog entry.
| File | Description |
|---|---|
dokimi/tests/test_subjects_digest.py |
Tests digest eligibility, hashing, and scorecard integration. |
dokimi/dokimi/subjects/scorecard.py |
Computes distribution digests for subjects and members. |
dokimi/dokimi/schemas/subject_scorecard.v2.json |
Allows the nullable subject digest field. |
dokimi/changelog.d/20261031_120000_napetrov_distribution-digest.md |
Documents the new field and intended matching behavior. |
Review effort: Balanced
| if path.is_dir(): | ||
| # Any other entry makes it an archive; one stray file is enough, so stop at the second. | ||
| if [p.name for _, p in zip(range(2), path.iterdir())] != ["SKILL.md"]: | ||
| return None | ||
| path = path / "SKILL.md" | ||
| if path.name != "SKILL.md" or not path.is_file(): | ||
| return None |
There was a problem hiding this comment.
Fixed in a5c5500. A SKILL.md file ref now digests only when its directory holds nothing else; a sibling resource gives null. Directory refs unchanged.
Red then green test added; mutation check caught the old behaviour.
…dex carries subject_scorecard.v2 gains subject.distribution_digest: sha256 of the SKILL.md bytes for a file ref or a directory holding only SKILL.md, the value a .well-known/agent-skills index.json entry of type skill-md carries (agentskills PR intel#254). An archive's bytes depend on how it was packed, so a skill with resources gets null, as does a non-local ref. ref_digest is unchanged. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
a2bd867 to
099c2b8
Compare
| """ | ||
| if not ref: | ||
| return None | ||
| path = Path(ref.removeprefix("local:")) |
There was a problem hiding this comment.
Must-fix: Non-local references are interpreted as local paths rather than explicitly rejected. On POSIX, create base/context7:/org/lib/SKILL.md as the directory's only entry, then call distribution_digest("context7:/org/lib", base=base): this returns a SHA-256 digest instead of None. Descriptor validation does not reject this reference, so both subject and member scorecards can incorrectly identify remote content using unrelated local bytes. Reject non-local reference schemes before filesystem lookup (while preserving Windows drive paths), and add a collision regression test; the current remote-reference assertion only covers a nonexistent path.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #343 +/- ##
==========================================
+ Coverage 94.05% 94.17% +0.12%
==========================================
Files 297 304 +7
Lines 38018 38678 +660
Branches 6424 6553 +129
==========================================
+ Hits 35756 36426 +670
+ Misses 1452 1442 -10
Partials 810 810
🚀 New features to boost your workflow:
|
antonwolfy
left a comment
There was a problem hiding this comment.
A file ref gets a digest the skill's own directory form refuses, and that is worth fixing before merge. Two nits inline.
What I ran, on a Linux review host at 099c2b8:
make ci-local: 30 passed, 0 failed, 7 skipped; changed-line coverage passes- reproductions for the code findings, with the LLM transport mocked
- mutation runs on
tests/test_subjects_digest.py: deleting the per-member line, and cutting the entry check to one entry, both still pass the file.
Checked, no issues
- Schema: the new property is optional and nullable on an open object, so
schema_compatpasses and artifacts written earlier still validate; no version bump is needed. - Changelog: the fragment follows the layout of the existing ones and its claims match the code.
- Commit: it carries a
Signed-off-by.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
antonwolfy
left a comment
There was a problem hiding this comment.
I pushed a commit per nit, up to 1dbde6a, which fix the two nits from my review:
de58607:dokimi/dokimi/subjects/scorecard.py:156:distribution_digestreturns None for a ref with a scheme other thanlocal:, and a test creates the colliding directory.1dbde6a:dokimi/tests/test_subjects_digest.py:62: a bundle scorecard test asserts a digest for a standaloneSKILL.mdmember and None for a non-local member.
make ci-local at 1dbde6a: 30 passed, 0 failed, 7 skipped.
Not approving yet. Still open: the risk from my review.
A SKILL.md named by its file ref was hashed even when its directory held resources, while the directory ref got None. The index serves such a skill as an archive under either spelling, so both now get None. The member test covers a standalone member, a bundled one with resources, and a non-local ref. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
| colliding = tmp_path / "context7:" / "org" / "lib" | ||
| colliding.mkdir(parents=True) |
There was a problem hiding this comment.
Must-fix: this fixture creates a directory named context7:, which is invalid on Windows. colliding.mkdir(parents=True) therefore raises before the scheme guard is tested, causing the test suite to fail on Windows even though the implementation preserves drive-letter paths. Mark this POSIX-only collision test with a Windows skip, or mock the filesystem lookup to test rejection without creating an invalid filename.
There was a problem hiding this comment.
Fixed in 08cd011: the colon-named collision test is skipped on win32, since a colon is not valid in a Windows directory name. The rest of the file still runs on every platform. Local: test_subjects_digest.py 4 passed; full suite 12025 passed, 8 skipped.
A colon is not allowed in a Windows directory name, so the fixture raised before the scheme guard ran and the Windows smoke path failed. The collision case stays on POSIX; the rest of the file runs everywhere. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. A SKILL.md file ref with a sibling resource now gets null, like its directory ref, for the subject and for bundle members. The scheme guard stays, and the member test checks each member on its own. One nit inline.
What I ran, on a Linux review host at 08cd011:
make ci-local: 29 passed, 1 failed, 7 skipped. The failure istest_the_regex_limit_is_shared_by_every_pattern_of_one_check, a wall-clock comparison intests/test_reference_checks.py, a file this PR does not touch. It passes when the file runs alone (70 passed). Changed-line coverage passes.- mutation runs on
distribution_digestand its two call sites: 20 mutants. Dropping either call site, passing the subject's ref for a member, dropping the scheme guard,removeprefix("local:"), thebase /join and thepath.parentstep each fail a test. The surviving mutant that changes a digest value is in the inline comment. - probes of odd refs (trailing slash,
..,local:/abs, drive-letter paths, symlinkedSKILL.md, a directory namedSKILL.md, empty file): they agree with the docstring.
Checked, no issues
- Schema: the new property is optional and nullable on an open object, so no version bump is needed.
- Commits: every non-merge commit carries a
Signed-off-by. - Changelog: it matches the code, including the file-ref wording.
Bot points I checked that no longer apply at this head (3)
- Copilot's point on the file ref getting a digest despite sibling resources does not apply:
a5c5500returnsNonefor it, for the subject and for members, and the test pins both. - exai-bot's point on non-local refs reading a colliding local directory does not apply:
distribution_digestreturnsNonefor ascheme:ref other thanlocal:, and a test creates the colliding directory. - exai-bot's point on the colon-named fixture failing on Windows does not apply:
08cd011skips that test on win32.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. I pushed a commit per nit, up to 3f98c22, which fix the nit from my review:
3f98c22:dokimi/tests/test_subjects_digest.py:21: the fixture SKILL.md and the expected digest now use CRLF bytes, so a text-decoding read changes the digest and fails the test.
make ci-local at 3f98c22: 30 passed, 0 failed, 7 skipped.

Summary
subject_scorecard.v2gainssubject.distribution_digest(and per member):sha256:of the SKILL.md bytes, the value a.well-known/agent-skills/index.jsonentry of typeskill-mdcarries (agentskills#254). A file ref or a directory holding only SKILL.md gets the digest; a skill with resources (archive bytes depend on packing) or a non-local ref getsnull.ref_digestis unchanged.Needed so a published evidence document can be matched to the skill a client installed (see the stacked exporter PR).
Evidence
tests/test_subjects_digest.py; full suite 12002 passed locally.Merge danger
Low: additive nullable schema field.
🤖 Generated with Claude Code