Skip to content

feat(skills): render full skill directories#2604

Merged
k11kirky merged 3 commits into
mainfrom
posthog-code/skills-01-full-rendering
Jun 12, 2026
Merged

feat(skills): render full skill directories#2604
k11kirky merged 3 commits into
mainfrom
posthog-code/skills-01-full-rendering

Conversation

@k11kirky

@k11kirky k11kirky commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Problem

The Skills tab only rendered SKILL.md from each skill directory and had no awareness of companion files (references/, scripts/, assets). There was also no way to distinguish editable skills (user/repo) from read-only ones (bundled/marketplace) in the UI.

Changes

Shared types

  • Added editable: boolean to SkillInfo — computed server-side based on source, not in the UI.
  • Added SkillFileEntry type (path, size) to @posthog/shared.

Backend (workspace-server)

  • Added listSkillFiles() to skill-discovery.ts: recursively walks a skill directory, skips symlinks (preventing path escape via crafted skills), sorts with SKILL.md first, and respects a file cap.
  • Added isEditableSource() helper; readSkillMetadataFromDir now sets editable on each SkillInfo.
  • Added getSkillContents(skillPath) and readSkillFile(skillPath, filePath) to SkillsService. Both validate the requested path against the known discovery roots before touching the filesystem — getSkillContents checks the skill directory is directly under a known root and contains SKILL.md; readSkillFile additionally checks the resolved file path stays within the skill directory, rejecting ../ traversal and absolute paths.
  • Refactored listSkills to derive skill roots via a shared getSkillRoots() helper used by all three methods.
  • Added Zod schemas (skillContentsInput/Output, readSkillFileInput/Output, skillFileEntry) to schemas.ts.

Router (host-router)

  • Exposed skills.contents and skills.readFile tRPC query procedures as one-line forwards to SkillsService.

UI

  • SkillDetailPanel: replaced the single useAbsoluteFileContent call with useSkillContents + useSkillFile. When a skill has more than one file, a collapsible file tree appears above the content area. SKILL.md continues to render as markdown; all other files open in a read-only CodeMirror editor. A "Read-only" lock badge is shown for non-editable skills.
  • Added SkillFileTree component: builds a nested directory tree from flat SkillFileEntry[] and renders it using the existing TreeDirectoryRow/TreeFileRow primitives with expand/collapse state.
  • Added useSkillContents / useSkillFile hooks backed by the new tRPC procedures.
  • Added a key={selectedSkill.path} to SkillDetailPanel in SkillsView so state resets when switching between skills.
  • Added an implementation plan document (docs/plans/skills-tab.md) covering the full seven-PR roadmap for the Skills tab.

How did you test this?

  • Added unit tests for listSkillFiles: nested directory walking with size reporting, symlink skipping, and file-cap enforcement.
  • Added unit tests for readSkillMetadataFromDir: verifies editable is true for user/repo sources and false for bundled/marketplace.
  • Added a new skills.test.ts covering SkillsService end-to-end against a real temp directory: listSkills editable flags, getSkillContents full file listing, path-traversal rejection on both getSkillContents and readSkillFile, absolute path rejection, empty path rejection, and null return for missing files.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

k11kirky added 2 commits June 11, 2026 12:48
Skills become directories in the UI, not a single markdown file:

- SkillsService.getSkillContents(skillPath) returns the skill's file
  tree (relative paths + sizes); readSkillFile(skillPath, relPath)
  returns file contents. Both validate the target resolves to a skill
  directory directly under a discovery root, so the endpoints cannot be
  used for arbitrary filesystem reads. Symlinked files are skipped.
- skills.contents / skills.readFile host-router queries (one-line
  forwards with Zod output schemas).
- SkillInfo gains a derived editable flag (user/repo true,
  bundled/marketplace false), computed server-side.
- SkillDetailPanel shows a file tree for multi-file skills; SKILL.md
  renders as markdown (frontmatter stripped) as before, other files
  open in read-only CodeMirror. Non-editable skills get a lock badge.
- Unit tests cover discovery, the file walker (symlink skipping, file
  cap) and ../ traversal rejection on both new endpoints.

Generated-By: PostHog Code
Task-Id: f4e84f1a-19c9-490c-9b98-47787a7dddcf
The plan behind the skills-01..07 stack: goals, non-goals, source/
editability model, per-PR scope, and security notes.

Generated-By: PostHog Code
Task-Id: f4e84f1a-19c9-490c-9b98-47787a7dddcf
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown

React Doctor could not complete this scan.

No React dependency found in /tmp/react-doctor-baseline-p7tPRP/package.json. Add "react" to dependencies (or peerDependencies) and re-run.

Reviewed by React Doctor for commit abb385b.

@k11kirky k11kirky marked this pull request as ready for review June 11, 2026 13:52
@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Security Review

  • Symlink traversal in readSkillFile (skills.ts line 60): fs.promises.stat follows symbolic links. A symlink placed inside a skill directory and pointing to an arbitrary file passes the path-containment check but allows the endpoint to read the symlink target. listSkillFiles already skips symlinks for this reason; readSkillFile should use lstat and reject symlink entries.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/workspace-server/src/services/skills/skills.ts:59-61
`readSkillFile` uses `fs.promises.stat`, which follows symbolic links. If a skill directory contains a symlink pointing outside itself, the resolved path passes the `startsWith(skillDir + path.sep)` check because the symlink's own path is within the skill dir — but `stat` follows the link and `readFile` reads the target. `listSkillFiles` intentionally skips symlinks to prevent this exact escape; `readSkillFile` needs the same protection. Using `lstat` is the minimal fix: it does not follow the final symlink, so `isFile()` returns false for any symlink entry.

```suggestion
    try {
      const stat = await fs.promises.lstat(resolved);
      if (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_SKILL_FILE_BYTES) return null;
```

### Issue 2 of 2
packages/workspace-server/src/services/skills/skills.ts:103
`fs.existsSync` is a synchronous call inside an otherwise fully-async method, blocking the event loop while it hits the filesystem. The rest of the service consistently uses `fs.promises.*`. `fs.promises.access` (or a `stat` call that is already needed) should be used instead.

```suggestion
    const skillMdExists = await fs.promises
      .access(path.join(resolved, "SKILL.md"))
      .then(() => true)
      .catch(() => false);
    if (!isUnderKnownRoot || !skillMdExists) {
```

Reviews (1): Last reviewed commit: "docs: add the Skills tab v2 implementati..." | Re-trigger Greptile

Comment thread packages/workspace-server/src/services/skills/skills.ts
Comment thread packages/workspace-server/src/services/skills/skills.ts Outdated
…stsSync

Review feedback on #2604: realpath containment check covers both direct
and intermediate-directory symlink escapes; resolveKnownSkillDir now uses
async fs.promises.access.

Generated-By: PostHog Code
Task-Id: f4e84f1a-19c9-490c-9b98-47787a7dddcf
@k11kirky k11kirky force-pushed the posthog-code/skills-01-full-rendering branch from d3d7209 to abb385b Compare June 12, 2026 08:19

k11kirky commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Merge activity

  • Jun 12, 11:31 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jun 12, 11:31 AM UTC: @k11kirky merged this pull request with Graphite.

@k11kirky k11kirky merged commit bb16aa2 into main Jun 12, 2026
24 checks passed
@k11kirky k11kirky deleted the posthog-code/skills-01-full-rendering branch June 12, 2026 11:31
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