Skip to content

feat(skills): SKILL.md skills UI — browse, create, install from URL (#681)#740

Merged
senamakel merged 20 commits intotinyhumansai:mainfrom
oxoxDev:feat/681-skill-detail-drawer
Apr 22, 2026
Merged

feat(skills): SKILL.md skills UI — browse, create, install from URL (#681)#740
senamakel merged 20 commits intotinyhumansai:mainfrom
oxoxDev:feat/681-skill-detail-drawer

Conversation

@oxoxDev
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev commented Apr 21, 2026

Summary

  • Adds the skills catalog panel with detail drawer, resource tree, and size-gated resource preview so users can browse installed SKILL.md skills from the app.
  • Adds two authoring flows: New skill scaffolds a local SKILL.md, Install from URL fetches a remote SKILL.md directly into the skills directory.
  • Introduces three new core RPCs — skills.read_resource, skills.create, and skills.install_from_url — under the skills namespace, wired through the controller registry.
  • Install is direct HTTPS fetch, not a shell-out. Validates YAML frontmatter, enforces a 1 MiB size cap and 1–600s timeout, normalizes GitHub blob→raw URLs, and rejects path traversal.
  • Frontend error copy is user-friendly — backend error prefixes are categorized into titles like "URL rejected", "SKILL.md too large", "Fetch timed out", "SKILL.md did not parse". Raw backend text is preserved under a details disclosure.

Problem

Issue #681 asks for a skills UI so users can view, create, and install SKILL.md-format skills without leaving the app. Before this change the app had no skills panel and no authoring surface — skills could only be managed by editing files under ~/.openhuman/skills/ manually. The install story in particular needed to work for "any normie" with a GitHub URL, not require a Node toolchain for first-time install.

Solution

Core (Rust)

  • src/openhuman/skills/ops.rs gained install_skill_from_url, which fetches a single SKILL.md over HTTPS using the existing reqwest client, parses frontmatter with serde_yaml, derives a slug, and writes into the resolved skills directory. Helpers: normalize_install_url (GitHub blob→raw rewriting), derive_install_slug (filesystem-safe slug from frontmatter name), parse_skill_md_str (frontmatter + body validation). Size cap MAX_SKILL_MD_BYTES = 1 MiB; timeout clamp [1, 600]s; 12 unit tests.
  • src/openhuman/skills/schemas.rs registers the new RPCs in the controller registry — no domain branches in core/jsonrpc.rs or core/cli.rs.
  • skills.read_resource and skills.create follow the same pattern for browsing bundled resources and scaffolding.

App (React/Tauri)

  • SkillsGrid → click → SkillDetailDrawer (right-side panel) with SkillResourceTree (grouped by top-level directory) and SkillResourcePreview (size-gated text/image preview).
  • CreateSkillModal scaffolds SKILL.md via skills.create.
  • InstallSkillDialog calls skills.install_from_url. Errors from the core are funneled through categorizeInstallError(raw) which maps Rust error prefixes (invalid url:, unsupported url form:, fetch too large:, fetch timed out, invalid SKILL.md:, skill already installed, write failed:, …) to friendly titles + hints; unknown errors show a generic title with the raw message tucked under <details>.
  • Typed client wrappers in app/src/api/skillsApi.ts.

Design tradeoffs

  • Chose direct HTTPS fetch over shelling out to npx skills add. The CLI writes to ./claude-code/skills/ / ./cursor/skills/ which do not match openhuman's discovery paths, and requiring Node on first install blocks non-technical users. Direct fetch covers the common "paste a GitHub URL" path with zero prerequisites.
  • Install is unconditional — no feature gate — per the product direction that any user should be able to install a skill.
  • Full package/tarball install (npm install + fallback to npx skills add) is intentionally deferred to a follow-up; this PR ships the SKILL.md path that issue Integrate skills.md #681 actually exercises.

Submission Checklist

  • Unit tests — 12 new cargo test cases in skills/ops.rs (URL normalization, slug derivation, frontmatter parsing, path traversal). 8 Vitest cases in InstallSkillDialog.test.tsx cover the direct-fetch fixtures and error categorization branches. Tests for SkillDetailDrawer, SkillResourceTree, SkillResourcePreview, CreateSkillModal also included.
  • E2E / integration — Not added in this PR; relying on unit + component coverage while the drawer and install dialog stabilize. Can be added in a follow-up once the UX settles.
  • N/A
  • Doc comments/// on new public functions in ops.rs (install_skill_from_url, helpers) and new types in schemas.rs. Component files carry brief JSDoc headers.
  • Inline comments — Added where the logic is non-obvious (GitHub URL rewriting, size-cap enforcement, error-prefix categorization mapping).

Impact

  • Runtime: Desktop only (skills browsing + install are surfaced through the Tauri app). No mobile impact.
  • Security: Install path restricted to https:// URLs, single-file SKILL.md only, 1 MiB hard cap, timeout cap at 600s, path-traversal guard on the derived slug. No arbitrary code execution — SKILL.md is data, not executed on install.
  • Performance: One HTTP GET per install; fire-and-forget filesystem write. cargo check / clippy pass (one pre-existing derivable_impls warning on SkillScope unrelated to this PR).
  • Migration / compatibility: Additive. New RPCs under skills.* namespace; no existing RPC or UI route changed.

Related

  • Issue(s): Closes Integrate skills.md #681
  • Follow-up PR(s)/TODOs:
    • Full package install (npm install + npx skills add fallback) for skills that need a Node runtime at install time
    • E2E spec for the install + detail drawer flow

Summary by CodeRabbit

  • New Features

    • Full Skills system: discover, create, install, list, and view discovered SKILL.md skills via Skills page with “New skill” and “Install from URL” controls.
    • Skill detail drawer with metadata, warnings, bundled-resource tree and in-app file preview (with sizes).
    • Create-skill modal with slug preview, validation, focus/close handling.
    • Install-from-URL dialog with HTTPS validation, timeout, progress, diagnostics and categorized error messaging.
  • Tests

    • Added comprehensive UI and API tests covering create, install, listing, drawer, resource preview, and API behaviors.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e74b5eb-9e07-48ba-9833-15083abe776e

📥 Commits

Reviewing files that changed from the base of the PR and between e95640e and 35aa3ec.

📒 Files selected for processing (1)
  • src/openhuman/skills/ops.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/openhuman/skills/ops.rs

📝 Walkthrough

Walkthrough

Adds end-to-end SKILL.md support: new frontend UI (create/install modals, detail drawer, resource tree/preview), a typed frontend skillsApi and tests, and Rust core controllers/ops for listing, reading, creating, and installing skills from HTTPS URLs.

Changes

Cohort / File(s) Summary
Frontend: Skill UI Components
app/src/components/skills/CreateSkillModal.tsx, app/src/components/skills/InstallSkillDialog.tsx, app/src/components/skills/SkillDetailDrawer.tsx, app/src/components/skills/SkillResourcePreview.tsx, app/src/components/skills/SkillResourceTree.tsx
New React components: create/install modals with validation/submission flows, a right-side detail drawer, resource tree grouping, and resource preview with fetch/cancel/error handling, focus management, and close behaviors.
Frontend: Tests
app/src/components/skills/__tests__/*
Adds Vitest + RTL suites covering validation, success/error flows, focus/close behavior, and mocked skillsApi interactions for the new components.
Frontend: Page Integration
app/src/pages/Skills.tsx
Integrates discovered skills state, selection, create/install controls and modals, refresh/optimistic-update flows, and opens SkillDetailDrawer on selection.
Client API & Tests
app/src/services/api/skillsApi.ts, app/src/services/api/__tests__/skillsApi.test.ts
New typed skillsApi wrapper for JSON‑RPC (list, read_resource, create, install_from_url) with envelope unwrapping, snake_case↔camelCase normalization, param mapping, and RPC-level tests.
Rust: Skills Ops
src/openhuman/skills/ops.rs
Core skill logic: SKILL.md parsing, secure read_skill_resource (path/symlink/size/UTF‑8 checks), create_skill scaffolding, install_skill_from_url (HTTPS normalization, host validation, atomic write), constants, and unit tests.
Rust: Schemas & Controllers
src/openhuman/skills/schemas.rs, src/openhuman/skills/mod.rs, src/core/all.rs
New JSON‑RPC controller schemas and handlers for the skills namespace (list, read_resource, create, install_from_url), controller registration wiring, schema exports, and workspace/config resolution helpers.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Frontend as Skills Page
    participant CreateModal as CreateSkillModal
    participant API as skillsApi
    participant RPC as Core JSON‑RPC
    participant FS as File System

    User->>Frontend: Open "New skill"
    Frontend->>CreateModal: Mount modal
    User->>CreateModal: Submit form
    CreateModal->>API: createSkill(params)
    API->>RPC: callCoreRpc('openhuman.skills_create', params)
    RPC->>FS: Scaffold skill dir + write SKILL.md
    RPC-->>API: SkillSummary
    API-->>CreateModal: Created skill
    CreateModal->>Frontend: onCreated(skill)
    Frontend->>Frontend: Add/refresh discoveredSkills
Loading
sequenceDiagram
    actor User
    participant Frontend as Skills Page
    participant InstallDialog as InstallSkillDialog
    participant API as skillsApi
    participant RPC as Core JSON‑RPC
    participant Network as HTTPS Host
    participant FS as File System

    User->>Frontend: Open "Install from URL"
    User->>InstallDialog: Submit URL (+timeout)
    InstallDialog->>API: installSkillFromUrl({url, timeout})
    API->>RPC: callCoreRpc('openhuman.skills_install_from_url', params)
    RPC->>Network: Fetch SKILL.md
    Network-->>RPC: Content
    RPC->>RPC: Parse & validate frontmatter
    RPC->>FS: Atomic write SKILL.md, discover skill
    RPC-->>API: { new_skills, stdout, stderr }
    API-->>InstallDialog: Result
    InstallDialog->>Frontend: onInstalled(result)
    Frontend->>Frontend: Refresh & select installed skill
Loading
sequenceDiagram
    actor User
    participant Frontend as Skills Page
    participant Drawer as SkillDetailDrawer
    participant Tree as SkillResourceTree
    participant Preview as SkillResourcePreview
    participant API as skillsApi
    participant RPC as Core JSON‑RPC

    User->>Frontend: Open skill details
    Frontend->>Drawer: Mount drawer
    User->>Tree: Select resource path
    Tree->>Drawer: onSelect(path)
    Drawer->>Preview: Mount preview(skillId,path)
    Preview->>API: readSkillResource({skillId,relativePath})
    API->>RPC: callCoreRpc('openhuman.skills_read_resource', params)
    RPC->>RPC: Validate path & read file
    RPC-->>API: {content, bytes}
    API-->>Preview: Content & size
    Preview->>User: Render file + size
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

  • #781 — Related: implements SKILL.md discovery, install/create flows and controller wiring aligned with that issue's objectives.

Possibly related PRs

Suggested reviewers

  • senamakel

Poem

🐰 I hopped into code with a curious grin,
SKILL.md tucked under my paw within.
Modals, drawers, and previews take flight,
Install and create make the day bright.
🌱 A rabbit's cheer for skills done right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements SKILL.md UI components and API handlers but does not address the core Node.js runtime resolver, tool exposure, or integration testing required by issue #681. Implement the Node.js runtime resolver, expose Node.js/npm tools to agents, add integrity checks, and validate with real external skills as specified in #681 acceptance criteria.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main feature added: SKILL.md skills UI with browse, create, and install from URL functionality.
Out of Scope Changes check ✅ Passed All changes align with the stated PR objectives: frontend UI components, API client wrappers, Rust API handlers, and URL-based skill installation features are within scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands and usage tips.

@oxoxDev oxoxDev force-pushed the feat/681-skill-detail-drawer branch from 6b8968f to dc0f531 Compare April 21, 2026 22:59
@oxoxDev oxoxDev marked this pull request as ready for review April 22, 2026 13:50
@oxoxDev oxoxDev requested a review from a team April 22, 2026 13:50
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
app/src/components/skills/SkillResourceTree.tsx (1)

96-109: Expose selected state to assistive tech on resource buttons.

Selection is currently visual only; add ARIA state so screen-reader users can detect which resource is active.

♿ Suggested tweak
                   <button
                     type="button"
+                    aria-pressed={isSelected}
                     onClick={() => {
                       log('click path=%s', path);
                       onSelect(path);
                     }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/skills/SkillResourceTree.tsx` around lines 96 - 109, The
resource buttons only convey selection visually; update the button in
SkillResourceTree (the button that calls onSelect(path) and uses
isSelected/path) to expose the state to assistive tech by adding ARIA attributes
— e.g., set aria-pressed={isSelected} (or aria-selected if you switch the role
to option/treeitem) and/or aria-current={isSelected ? "true" : undefined} so
screen readers can detect the active resource; keep the existing onClick, title,
and styling logic unchanged.
app/src/components/skills/__tests__/SkillDetailDrawer.test.tsx (1)

1-11: Docblock overstates coverage (backdrop click closes the drawer).

I don’t see a backdrop-click assertion in this file. Either add that test or trim this bullet to keep test intent accurate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/skills/__tests__/SkillDetailDrawer.test.tsx` around lines
1 - 11, The docblock at the top of SkillDetailDrawer.test.tsx incorrectly claims
there is a "backdrop click closes the drawer" test; either remove that bullet or
add the missing test: if removing, simply delete or edit that line in the header
to reflect actual coverage; if adding, implement a test that renders the
SkillDetailDrawer (passing a mock onClose), simulates a backdrop click (e.g.,
locate the backdrop element for SkillDetailDrawer and trigger a click via
fireEvent or userEvent), and assert the mock onClose was called — reference the
test file SkillDetailDrawer.test.tsx, the SkillDetailDrawer component, and the
onClose/mock handler when adding the assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/components/skills/CreateSkillModal.tsx`:
- Around line 58-76: The previewSlug function currently calls
name.normalize('NFKD') which decomposes accented letters (e.g., É -> E + ◌́) so
the ASCII filter keeps the base letter and yields different slugs than the Rust
backend; change previewSlug to avoid NFKD decomposition (remove the .normalize
call or use a form that does not decompose) so non-ASCII characters are dropped
byte-for-byte like the Rust slugifier, keep the ASCII-only test ((ch >= 'a' &&
ch <= 'z') || (ch >= '0' && ch <= '9')), preserve the hyphen/coalescing logic
and final trim (out.replace(/^-+|-+$/g, '')) so the frontend slug matches
src/openhuman/skills/ops.rs::slugify_skill_name().

In `@src/openhuman/skills/ops.rs`:
- Around line 1320-1345: The code currently creates target_dir (variable
target_dir) before writing SKILL_MD, which leaves an empty/partial directory
behind if std::fs::write or std::fs::rename fails and causes future installs to
fail due to target_dir.exists(); fix by staging the file write outside the final
install directory (e.g., write temp file in a system temp dir or in skills_root
with a unique name) and then atomically move/rename the staged file into
target_dir (or create a temp directory and rename the whole directory into
place) instead of creating target_dir first; additionally ensure cleanup on
error by removing any created temp files/dirs or removing target_dir if you must
create it early (handle errors from create_dir_all, write, and rename and
perform fs::remove_dir_all on target_dir or fs::remove_file on temp_file in the
same function where create_dir_all, std::fs::write, and std::fs::rename are
used).
- Around line 1405-1416: The blob/tree/raw branch handling assumes the ref is
exactly segments[3], which breaks for refs containing slashes; update the logic
in the normalized computation (the block that builds normalized from
parsed.path_segments() and the segments Vec) to locate the index of the marker
token ("blob", "tree" or "raw") with segments.iter().position(...) and then
treat everything after that index as a single tail (tail =
segments[index+1..].join("/")); keep owner = segments[0] and repo = segments[1]
and build the raw URL using that tail (e.g.
format!("https://raw.githubusercontent.com/{owner}/{repo}/{tail}")), and apply
the same approach for the "tree" / "raw" branches so refs with slashes are
preserved.
- Around line 925-947: Before creating a user-scope skill, ensure you reject
slug collisions that exist in the other scope: when params.scope ==
SkillScope::User (the block constructing scope_root), perform a cross-scope
lookup for the desired slug (the same check used later that does find(|s| s.name
== slug) or reuse the discover_skills()/list_skills() helper) to see if a
trusted project-scope skill in workspace_dir already owns that slug and return
an Err if so; similarly, when creating a project-scope skill, check the user
scope (home_dir.join(".openhuman").join("skills")) for the same slug and error
if found. Use the existing symbols params.scope, slug, scope_root,
workspace_dir, home_dir and the discovery function used elsewhere to implement
the cross-scope existence check before writing the new directory (also apply the
same fix around the other creation site referenced at lines ~1002-1006).
- Around line 977-985: create_skill_inner() currently passes &slug into
render_skill_md(), causing the slug to be written into the frontmatter name and
Markdown heading; change the call(s) to pass the original user-entered name (the
variable that holds the friendly title from the form, e.g. params.name or name)
into render_skill_md() instead of &slug so the human-readable title is
persisted; update every place render_skill_md(...) is called (including the
other occurrence around the second create path) to use the user-provided name
variable and ensure render_skill_md() continues to accept &str for the name
parameter.

---

Nitpick comments:
In `@app/src/components/skills/__tests__/SkillDetailDrawer.test.tsx`:
- Around line 1-11: The docblock at the top of SkillDetailDrawer.test.tsx
incorrectly claims there is a "backdrop click closes the drawer" test; either
remove that bullet or add the missing test: if removing, simply delete or edit
that line in the header to reflect actual coverage; if adding, implement a test
that renders the SkillDetailDrawer (passing a mock onClose), simulates a
backdrop click (e.g., locate the backdrop element for SkillDetailDrawer and
trigger a click via fireEvent or userEvent), and assert the mock onClose was
called — reference the test file SkillDetailDrawer.test.tsx, the
SkillDetailDrawer component, and the onClose/mock handler when adding the
assertion.

In `@app/src/components/skills/SkillResourceTree.tsx`:
- Around line 96-109: The resource buttons only convey selection visually;
update the button in SkillResourceTree (the button that calls onSelect(path) and
uses isSelected/path) to expose the state to assistive tech by adding ARIA
attributes — e.g., set aria-pressed={isSelected} (or aria-selected if you switch
the role to option/treeitem) and/or aria-current={isSelected ? "true" :
undefined} so screen readers can detect the active resource; keep the existing
onClick, title, and styling logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e87b65aa-3489-47ac-ad52-3a56856b912c

📥 Commits

Reviewing files that changed from the base of the PR and between 0ce1253 and 2e0b1a3.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • app/src/components/skills/CreateSkillModal.tsx
  • app/src/components/skills/InstallSkillDialog.tsx
  • app/src/components/skills/SkillDetailDrawer.tsx
  • app/src/components/skills/SkillResourcePreview.tsx
  • app/src/components/skills/SkillResourceTree.tsx
  • app/src/components/skills/__tests__/CreateSkillModal.test.tsx
  • app/src/components/skills/__tests__/InstallSkillDialog.test.tsx
  • app/src/components/skills/__tests__/SkillDetailDrawer.test.tsx
  • app/src/components/skills/__tests__/SkillResourcePreview.test.tsx
  • app/src/pages/Skills.tsx
  • app/src/services/api/__tests__/skillsApi.test.ts
  • app/src/services/api/skillsApi.ts
  • src/core/all.rs
  • src/openhuman/skills/mod.rs
  • src/openhuman/skills/ops.rs
  • src/openhuman/skills/schemas.rs

Comment on lines +58 to +76
function previewSlug(name: string): string {
const lower = name.normalize('NFKD').toLowerCase();
let out = '';
let prevHyphen = false;
for (const ch of lower) {
// ASCII alnum pass-through
if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) {
out += ch;
prevHyphen = false;
continue;
}
if ((ch === '-' || ch === '_' || /\s/.test(ch)) && !prevHyphen) {
out += '-';
prevHyphen = true;
}
}
// Trim leading/trailing hyphens
return out.replace(/^-+|-+$/g, '');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep the slug preview byte-for-byte aligned with the Rust slugifier.

This preview normalizes with NFKD, so names like Éclair render as eclair here, while src/openhuman/skills/ops.rs::slugify_skill_name() drops non-ASCII characters and would create clair. That makes the modal promise a different install path than the backend will actually write.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/skills/CreateSkillModal.tsx` around lines 58 - 76, The
previewSlug function currently calls name.normalize('NFKD') which decomposes
accented letters (e.g., É -> E + ◌́) so the ASCII filter keeps the base letter
and yields different slugs than the Rust backend; change previewSlug to avoid
NFKD decomposition (remove the .normalize call or use a form that does not
decompose) so non-ASCII characters are dropped byte-for-byte like the Rust
slugifier, keep the ASCII-only test ((ch >= 'a' && ch <= 'z') || (ch >= '0' &&
ch <= '9')), preserve the hyphen/coalescing logic and final trim
(out.replace(/^-+|-+$/g, '')) so the frontend slug matches
src/openhuman/skills/ops.rs::slugify_skill_name().

Comment on lines +925 to +947
let scope_root = match params.scope {
SkillScope::User => {
let home =
home_dir.ok_or_else(|| "could not resolve user home directory".to_string())?;
home.join(".openhuman").join("skills")
}
SkillScope::Project => {
if !is_workspace_trusted(workspace_dir) {
return Err(format!(
"workspace {} is not trusted; create {}/.openhuman/trust to enable project-scope skills",
workspace_dir.display(),
workspace_dir.display(),
));
}
workspace_dir.join(".openhuman").join("skills")
}
SkillScope::Legacy => {
return Err(
"cannot create skill in legacy scope; choose 'user' or 'project'".to_string(),
);
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject cross-scope slug collisions before creating a user-scope skill.

The existence check only looks inside the destination scope. If a trusted project-scope skill already owns the same slug, a user-scope create will still write a new directory, but re-discovery immediately shadows it and find(|s| s.name == slug) returns the existing project skill instead of the one that was just created. The caller gets the wrong Skill, and the new file is effectively invisible in that workspace.

Also applies to: 1002-1006

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/skills/ops.rs` around lines 925 - 947, Before creating a
user-scope skill, ensure you reject slug collisions that exist in the other
scope: when params.scope == SkillScope::User (the block constructing
scope_root), perform a cross-scope lookup for the desired slug (the same check
used later that does find(|s| s.name == slug) or reuse the
discover_skills()/list_skills() helper) to see if a trusted project-scope skill
in workspace_dir already owns that slug and return an Err if so; similarly, when
creating a project-scope skill, check the user scope
(home_dir.join(".openhuman").join("skills")) for the same slug and error if
found. Use the existing symbols params.scope, slug, scope_root, workspace_dir,
home_dir and the discovery function used elsewhere to implement the cross-scope
existence check before writing the new directory (also apply the same fix around
the other creation site referenced at lines ~1002-1006).

Comment on lines +977 to +985
let skill_md_path = skill_dir.join(SKILL_MD);
let skill_md = render_skill_md(
&slug,
description,
params.license.as_deref(),
params.author.as_deref(),
&params.tags,
&params.allowed_tools,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Persist the user-entered name, not the slug.

create_skill_inner() passes &slug into render_skill_md(), and render_skill_md() writes that value into frontmatter name: and the Markdown heading. A skill created from My Demo Skill therefore comes back as my-demo-skill in the catalog, which loses the human-readable title the form just collected.

Suggested fix
-fn render_skill_md(
-    slug: &str,
+fn render_skill_md(
+    display_name: &str,
+    slug: &str,
     description: &str,
     license: Option<&str>,
     author: Option<&str>,
     tags: &[String],
     allowed_tools: &[String],
 ) -> String {
     let mut out = String::new();
     out.push_str("---\n");
-    out.push_str(&format!("name: {slug}\n"));
+    out.push_str(&format!("name: {}\n", yaml_scalar(display_name)));
     out.push_str(&format!("description: {}\n", yaml_scalar(description)));
@@
     }
     out.push_str("---\n\n");
-    out.push_str(&format!("# {slug}\n\n"));
+    out.push_str(&format!("# {}\n\n", display_name));
     out.push_str(description);
     let skill_md = render_skill_md(
+        display_name,
         &slug,
         description,
         params.license.as_deref(),
         params.author.as_deref(),
         &params.tags,

Also applies to: 1045-1081

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/skills/ops.rs` around lines 977 - 985, create_skill_inner()
currently passes &slug into render_skill_md(), causing the slug to be written
into the frontmatter name and Markdown heading; change the call(s) to pass the
original user-entered name (the variable that holds the friendly title from the
form, e.g. params.name or name) into render_skill_md() instead of &slug so the
human-readable title is persisted; update every place render_skill_md(...) is
called (including the other occurrence around the second create path) to use the
user-provided name variable and ensure render_skill_md() continues to accept
&str for the name parameter.

Comment thread src/openhuman/skills/ops.rs Outdated
Comment thread src/openhuman/skills/ops.rs
Comment thread src/openhuman/skills/ops.rs
oxoxDev and others added 18 commits April 22, 2026 19:31
…ds (tinyhumansai#681)

Introduces `read_skill_resource(skill_id, relative_path)` in the skills
ops module. Used by the new `skills.read_resource` RPC (landed in a
follow-up commit) to let the UI preview files bundled alongside a
SKILL.md without having to shell out to the Node runtime.

Guards rejecting each known attack surface have their own unit test:
- empty skill_id / empty relative_path
- unknown skill
- absolute paths
- `..` traversal escapes (checked after canonicalization against the
  skill root, reusing the pattern from the Node exec allowlist)
- directory targets
- symlinked leaves (reject via `symlink_metadata` before open)
- files over the 128 KB cap
- non-UTF-8 content (binary allowlist is text-only)

Happy-path test covers a small text resource under the skill root.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ansai#681)

Adds `skills` RPC namespace with `skills.list` and `skills.read_resource`
handlers. `read_resource` delegates to `read_skill_resource` (previous
commit) and surfaces path-traversal / size / encoding errors back to the
caller verbatim so the UI can render the error string as-is.

- `src/openhuman/skills/schemas.rs` (new): controller + schema
  definitions, plus unit tests for schema name stability, round-trip of
  the minimum `SkillSummary` fields, and controller list/schema length
  parity.
- `src/openhuman/skills/mod.rs`: declare `pub mod schemas` and re-export
  `all_skills_controller_schemas`, `all_skills_registered_controllers`,
  and `skills_schemas`.
- `src/core/all.rs`: register the controllers + schemas and add a
  namespace description so the RPC discovery endpoint surfaces it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…umansai#681)

Thin typed wrapper around the `skills.list` and `skills.read_resource`
RPCs added in the previous commit. The client normalises the backend
response shape (bytes + UTF-8 content) and rethrows backend error
strings verbatim so the preview pane can render them unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…inyhumansai#681)

Presentational component that takes the `resources: PathBuf[]` from a
loaded Skill and renders it as a grouped list (scripts, assets,
references, etc. based on the first path segment). Selecting a leaf
calls `onSelect(relativePath)` so the parent drawer can drive preview
state.

Stateless — no fetching, no effects. Styling follows the stone/coral
design tokens used across the Skills page.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ansai#681)

Presentational component that fetches a single bundled resource via
`skillsApi.readSkillResource`. Backend caps payloads at 128 KB and
either returns UTF-8 text or a plain error string, so the preview pane
has three visual states: loading, error, success.

- On error (e.g. "path escape", ">128KB", "non-UTF-8"), renders the
  backend message verbatim in a coral panel.
- On success, renders a monospace pre block with the byte count in the
  footer.
- `key={id:path}` on the mount site (in SkillDetailDrawer, next commit)
  drives a remount when the selected resource changes — so no
  setState-in-effect hack is needed to reset loading state.

Vitest specs cover: loading state, success rendering with byte footer,
error rendering for traversal / oversize / encoding strings, cancelled
fetch guard on unmount.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…humansai#681)

Slide-in right-hand drawer that displays frontmatter metadata
(description, version, author, license, tags, allowed_tools) and hosts
SkillResourceTree + SkillResourcePreview. Opened by clicking a skill
card on the Skills page (wired in the next commit).

- Focus management: on mount, focuses the close button via
  `window.requestAnimationFrame` and restores the previously focused
  element on unmount.
- Esc + backdrop click dismiss.
- Preview pane is conditionally rendered and keyed on
  `${skill.id}:${selectedResource}` so changing the selected resource
  remounts the previewer (avoids setState-in-effect pattern).

Vitest specs cover: render with frontmatter, resource tree click opens
preview, close button / Esc / backdrop dismiss paths, focus
restoration, empty-resources case.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…i#681)

Wires the Skills page to the new drawer: clicking a skill card sets
`selectedSkill` state, which mounts `SkillDetailDrawer`. Dismissing the
drawer clears the state. Cards gain an explicit "View details"
affordance for discoverability.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…oring (tinyhumansai#681)

Adds `create_skill` in ops.rs plus the `skills.create` controller + handler
in schemas.rs. Writes a minimal SKILL.md (with optional license/author/tags/
allowed-tools frontmatter) under the selected scope, scaffolds scripts/
references/assets subdirs, and re-discovers the skill to return the parsed
SkillSummary. Legacy scope rejects; Project scope requires the trust marker;
User scope is always allowed when a home directory is available.

Hardened against path traversal the same way read_skill_resource is:
canonicalize the scope root, canonicalize the target dir, reject unless the
target starts with the root. Slug derivation is ASCII-only (collapse whitespace/
-/_ to a single hyphen, drop other chars, trim hyphens, enforce MAX_NAME_LEN).

Tests (hermetic via create_skill_inner):
- user-scope happy path (slug, metadata, SKILL.md on disk, subdirs)
- slug collision rejection
- invalid name (no alphanumerics) rejection
- project-scope without trust marker rejection
- project-scope with trust marker happy path
- legacy-scope rejection
- empty-description rejection
- slugify edge cases

Closes part of tinyhumansai#681 (backend scope for create flow).
…tinyhumansai#681)

Introduces `install_skill_from_url(url, timeout_secs?)` — a JSON-RPC method
that shells out to `npx --yes skills add <url>` under the managed Node
runtime so the UI can install published SKILL.md packages directly.

Security posture:
- https scheme only (no http, file, ssh, git+https…)
- Rejects `localhost`, `*.localhost`, `*.local`, RFC1918 private IPv4,
  loopback, link-local, multicast, broadcast, unspecified, 100.64/10 CGN,
  0.0.0.0/8, and IPv6 loopback/unspecified/multicast, fc00::/7 ULA,
  fe80::/10 link-local. Explicitly covers 169.254.169.254 cloud metadata.
- Trims + caps URL at 2048 chars; parses with the `url` crate.
- IPv6 brackets stripped from `host_str()` before address parse.

Process posture:
- Reuses `NodeBootstrap` so the managed toolchain resolves first.
- `env_clear()` + explicit PATH injection (bootstrap bin_dir first) + a
  narrow safe-env allow-list (HOME, TERM, LANG, LC_ALL, LC_CTYPE, USER,
  SHELL, TMPDIR). Matches the npm_exec pattern from tinyhumansai#723.
- Default 60s wall-clock timeout, capped at 600s.
- Captures stdout/stderr; returns both on success or failure.
- Diff-based `new_skills`: snapshots discovered skills pre-install and
  reports slugs that appear post-install.

Surface:
- JSON-RPC: `openhuman.skills_install_from_url`
  params:  { url: string, timeout_secs?: number }
  result:  { url, stdout, stderr, new_skills[] }
- Wired into `all_skills_controller_schemas()` and
  `all_skills_registered_controllers()`.

Tests (5 new unit tests, all pass — 51/51 in skills::):
- validate_install_url_accepts_public_https
- validate_install_url_rejects_non_https_scheme (http, file, ftp, ssh,
  git+https, javascript)
- validate_install_url_rejects_empty_and_oversized
- validate_install_url_rejects_private_and_loopback (20 URLs inc. CGN,
  cloud metadata, IPv6 ULA/link-local/loopback/multicast)
- validate_install_url_rejects_malformed (missing scheme, empty host,
  non-https scheme, unparseable bracketed host)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…inyhumansai#681)

Adds typed frontend wrappers for the two new skill-authoring RPC methods:

- `skillsApi.createSkill(input)` — scaffolds a new SKILL.md skill via
  `openhuman.skills_create`. Accepts camelCase `allowedTools` and rekeys
  it to the `allowed-tools` spelling the SKILL.md frontmatter convention
  expects, matching `SkillsCreateParams` in `src/openhuman/skills/schemas.rs`.
  Optional fields are only sent when explicitly provided so the Rust
  `#[serde(default)]` defaults apply cleanly.

- `skillsApi.installSkillFromUrl(input)` — installs a published skill
  package via `openhuman.skills_install_from_url`. Accepts camelCase
  `timeoutSecs` and rekeys it to `timeout_secs`. Normalizes the response
  (snake_case `new_skills` -> camelCase `newSkills`, missing list -> []).

Both wrappers reuse the existing `unwrapEnvelope` helper so they
tolerate either a bare RPC payload or the `{ data: … }` envelope some
transports emit.

Adds `CreateSkillInput`, `InstallSkillFromUrlInput`, and
`InstallSkillFromUrlResult` type exports for downstream modal components.

Tests (vitest, 6 new specs, all pass):
- createSkill forwards inputs and rekeys allowedTools
- createSkill omits optional fields when absent
- createSkill unwraps envelope responses
- installSkillFromUrl forwards url and rekeys timeoutSecs
- installSkillFromUrl omits timeout_secs + defaults newSkills to []
- installSkillFromUrl unwraps envelope responses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…umansai#681)

Adds a centered white modal that scaffolds a new SKILL.md skill via
`skillsApi.createSkill`, matching the settings-modal design rules
(520px desktop, 16px radius, backdrop+blur, Escape/click-out to close,
focus capture).

Form fields mirror the Rust `SkillsCreateParams` schema:
  - name (required) — display name, also slugified into the on-disk
    directory; a live slug preview surfaces what will hit disk
  - description (required) — short prose; written as the
    `description:` field in the generated YAML frontmatter
  - scope (user | project radio) — `legacy` is hidden because that
    layout is read-only and being phased out
  - license (optional) — free-form SPDX-style string
  - author (optional)
  - tags (optional, CSV) — normalised client-side; empty entries dropped
  - allowedTools (optional, CSV) — rekeyed to `allowed-tools` on the
    JSON-RPC wire by `skillsApi.createSkill`

The slug preview mirrors `slugify_skill_name` on the Rust side
(lowercase ASCII alnum + `-`, collapse repeats, trim edge hyphens) so
the user sees what the Rust slugifier will produce; the Rust side stays
authoritative when the skill is persisted.

On success `onCreated(skill)` fires with the freshly-discovered
`SkillSummary`, letting the parent grid insert the new row without a
full refetch. On failure the Rust error string is surfaced verbatim in
a coral-styled alert and the submit button re-enables.

Vitest specs cover: required-field rendering, live slug preview,
submit-disabled gating, Escape close, wire-format rekey of
`allowedTools` → `'allowed-tools'`, `onCreated` dispatch, and
error-banner recovery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ai#681)

Adds a centered white modal that installs a published skill package
via `skillsApi.installSkillFromUrl`. The Rust side shells out to
`npx --yes skills add <url>` under the managed Node toolchain, with
an allow-list on the URL (https only, no private/loopback/link-local/
multicast/cloud-metadata hosts) and a wall-clock timeout (default 60s,
max 600s).

UI contract:
  - Single URL input plus optional timeout in seconds.
  - Client-side `isLikelyValidUrl` fails fast on non-https URLs so the
    user doesn't pay a round-trip for shape errors the Rust side would
    reject anyway; the Rust side remains authoritative.
  - Timeout field validates `1 <= n <= 600` client-side to mirror the
    server-side clamp range.
  - While the RPC is in flight we render a spinner with "Running
    `npx skills add`…" copy and disable close / backdrop dismiss so we
    don't orphan the subprocess.
  - On success we surface the list of `newSkills` (ids that appeared
    post-install) plus captured stdout/stderr panes inside collapsible
    <details> elements, then hand the full result back to the caller
    via `onInstalled` so the parent can refetch the skills list and
    auto-select the new row.
  - On failure the Rust error string is rendered verbatim in a coral
    alert and the submit button re-enables.

Vitest specs cover: required-field rendering, URL shape gating (empty,
malformed, http://, https://), timeout range validation, `timeoutSecs`
forwarding on submit, success panel with newSkills rendering, blank
timeout omitted from payload, and error-banner recovery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…yhumansai#681)

Adds a header row on the Skills page with two buttons:
  - **New skill** → opens `CreateSkillModal`
  - **Install from URL** → opens `InstallSkillDialog`

Extracts the existing `listSkills` effect into a reusable
`refreshDiscoveredSkills` helper so both new flows can reconcile their
results against the freshly-discovered `SkillSummary` rows rather than
relying on the optimistic payload from the RPC alone.

Create flow:
  - Optimistically appends the returned `SkillSummary` to
    `discoveredSkills` (dedupe by id).
  - Auto-opens the detail drawer for the new skill so the user lands in
    context — matches the install flow's UX.
  - Follows up with `refreshDiscoveredSkills()` so version/author/
    warnings picked up by the Rust discoverer end up in state too.

Install flow:
  - Always refreshes the list (the install can add multiple skills if
    the package declares several).
  - Auto-opens the detail drawer for the first newly-installed skill
    when at least one id is reported back; otherwise leaves the grid in
    its refreshed state.

Both buttons sit in a flush `max-w-lg` header above the existing
search bar, styled consistent with `UnifiedSkillCard` CTAs — ocean
primary for "New skill" (positive action), neutral stone for "Install
from URL" (secondary).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…inyhumansai#681)

Installer no longer shells out to the vercel-labs/skills CLI. It now fetches
SKILL.md over HTTPS, validates YAML frontmatter, and writes into the user's
skills dir. Size cap (1 MiB), timeout clamp (1-600s), GitHub blob->raw URL
normalization, and path-traversal guards are covered by unit tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… fetch (tinyhumansai#681)

Dialog subtitle, helper text, and in-flight indicator reflect the new direct
SKILL.md fetch flow. Errors from the core are categorized into friendly titles
(URL rejected, too large, timeout, parse failure, already installed, write
failed) with the raw backend message tucked under a details disclosure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rization (tinyhumansai#681)

Fixtures updated to raw GitHub SKILL.md URLs. New cases assert the
categorization helper surfaces the right title for invalid SKILL.md, unsupported
URL form, and unknown backend errors (raw text hidden under details).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…#681)

Project scope (`<ws>/.openhuman/skills/`) is gated on a `<ws>/.openhuman/trust`
marker that the workspace rarely has, so freshly-installed skills were
invisible to `skills.list` until the user opted the workspace into trust.
Route installs to `~/.openhuman/skills/<slug>` — the user-scope root that
`discover_skills` always scans — so "Install from URL" surfaces the new
skill immediately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…yhumansai#681)

`install_from_url` stopped shelling out to `npx --yes skills add <url>` when
it was rewritten to fetch SKILL.md over HTTPS directly, but schema
descriptions and SDK wrappers still described the old subprocess flow. Fix
the module-level rustdoc, the JSON-RPC schema `description`/`comment`
fields, and the TS client wrapper doc comments so the surface documents
what actually runs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@oxoxDev oxoxDev force-pushed the feat/681-skill-detail-drawer branch from 2e0b1a3 to 0fab789 Compare April 22, 2026 14:10
…nyhumansai#681)

Two fixes surfaced by CodeRabbit review on PR tinyhumansai#740:

* `validate_install_url` only inspected literal-IP hosts, so a
  public-looking hostname like `evil.example.com` with an A record
  pointing at `127.0.0.1` / `169.254.x` / etc. would still be handed to
  `reqwest`. Resolve the host via `tokio::net::lookup_host` before the
  GET and reject if any returned address falls in loopback / private /
  link-local / multicast / unspecified ranges. Document the remaining
  DNS-rebinding gap (pinning to a `SocketAddr` + custom reqwest
  resolver is tracked separately).
* If `std::fs::write` or `std::fs::rename` fails after `create_dir_all`
  succeeded, the empty/partial target directory used to survive and
  permanently block retries under the same slug. Wrap the write+rename
  in a rollback that removes the temp file + the just-created directory
  on failure (best-effort; cleanup errors are logged and the original
  write error is surfaced).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
app/src/components/skills/InstallSkillDialog.tsx (1)

43-59: Consider removing unused onSelectSkill prop or marking it as reserved.

The onSelectSkill prop is declared but never invoked within the component. The docstring notes it's "for symmetry with CreateSkillModal" and "not invoked by the dialog itself." If this is intentional API surface for future use, consider adding a // @ts-expect-error`` or removing it until needed to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/skills/InstallSkillDialog.tsx` around lines 43 - 59, The
onSelectSkill prop declared on the Props interface is never used inside the
InstallSkillDialog component; either remove onSelectSkill from Props (and any
call sites) to shrink the public API, or keep it intentionally but add a clear
comment on the Props interface and above the prop (e.g., “// reserved for
symmetry with CreateSkillModal — intentionally unused”) so reviewers won’t treat
it as dead code; update the Props interface and the InstallSkillDialog component
signature accordingly and ensure any references to onSelectSkill elsewhere are
removed if you choose the former.
app/src/services/api/skillsApi.ts (1)

84-92: Narrow CreateSkillInput.scope to creatable scopes.

SkillScope includes 'legacy', but skills_create only documents 'user' and 'project'. Exposing the broader union here makes an unsupported value look valid and pushes the failure to runtime.

Proposed fix
 export type SkillScope = 'user' | 'project' | 'legacy';
+export type CreateSkillScope = 'user' | 'project';
@@
 export interface CreateSkillInput {
   name: string;
   description: string;
-  scope?: SkillScope;
+  scope?: CreateSkillScope;
   license?: string;
   author?: string;
   tags?: string[];
   allowedTools?: string[];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/services/api/skillsApi.ts` around lines 84 - 92, The
CreateSkillInput.scope field currently uses the broad SkillScope (which includes
'legacy') but the skills_create endpoint only accepts 'user' or 'project';
change CreateSkillInput so scope is narrowed to only the creatable values (e.g.,
replace scope?: SkillScope with scope?: 'user' | 'project' or introduce a new
type alias like CreatableSkillScope and use that) and update any references to
CreateSkillInput in the codebase (e.g., functions that call skills_create) to
use the narrowed type so invalid values are caught at compile time rather than
runtime.
src/openhuman/skills/schemas.rs (1)

163-193: Match the standard schemas.rs export surface.

This module exports skills_schemas / all_skills_*, but the repo contract for domain schema modules is schemas, all_controller_schemas, and all_registered_controllers. Keeping the common surface avoids per-domain exceptions and makes these modules interchangeable.

As per coding guidelines, src/openhuman/**/schemas.rs requires schemas(function: &str) -> ControllerSchema, all_controller_schemas() -> Vec<ControllerSchema>, and all_registered_controllers() -> Vec<RegisteredController>.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/skills/schemas.rs` around lines 163 - 193, Rename the exported
functions in this module to match the repo contract: change
skills_schemas(function: &str) -> ControllerSchema to schemas(function: &str) ->
ControllerSchema, change all_skills_controller_schemas() ->
Vec<ControllerSchema> to all_controller_schemas(), and change
all_skills_registered_controllers() -> Vec<RegisteredController> to
all_registered_controllers(); update the vectors that currently call
skills_schemas(...) and construct RegisteredController entries (which reference
handlers handle_skills_list, handle_skills_read_resource, handle_skills_create,
handle_skills_install_from_url) to call the renamed schemas(...) and ensure any
external call sites or uses import the new symbol names so the module exports
are schemas, all_controller_schemas, and all_registered_controllers as required
by the coding guidelines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/services/api/skillsApi.ts`:
- Around line 225-229: The installSkillFromUrl function currently logs the raw
input.url with log('installSkillFromUrl: request url=%s', input.url); — replace
that raw log with a namespaced debug call and a redacted URL: parse input.url in
installSkillFromUrl, strip credentials and query string (or mask query values)
and log only the safe parts (hostname + pathname or a masked query) using a
debug namespace like debug('services:skillsApi') instead of the global log
function; ensure no JWTs, API keys, or full PII from input.url are emitted.
- Around line 101-107: The InstallSkillFromUrlInput.timeoutSecs must be
validated as a non-negative integer before sending: update the runtime check
where InstallSkillFromUrlInput is consumed (the request-sender / install-skill
call at the usage site) to verify Number.isInteger(timeoutSecs) and timeoutSecs
>= 0 (or undefined), and reject/throw a clear client-side error if not; do not
rely on server clamping. Ensure the check runs before serializing the payload so
fractional or negative values are blocked (or coerce intentionally only if you
add explicit truncation), and include this validation alongside the
InstallSkillFromUrlInput handling.

In `@src/openhuman/skills/schemas.rs`:
- Around line 457-470: The debug logs in the install_from_url RPC handler
currently log full URLs (wire.url and outcome.url), which can leak
tokens/credentials; update the tracing::debug! invocations in the
install_from_url flow (the handler that converts wire into
InstallSkillFromUrlParams and calls install_skill_from_url) to log a redacted
URL instead of the raw string—implement or call a small sanitizer (e.g.,
redact_query_or_credentials(url: &str) -> String) and pass its output to the
trace statements for both the initial wire.url and outcome.url so no query
params or userinfo are written to logs.
- Around line 431-432: The mutating handlers (skills.create and
skills.install_from_url) currently call resolve_workspace_dir() /
resolve_config() which silently fall back to a default workspace on errors;
change these code paths so that resolve_workspace_dir() and resolve_config()
errors are propagated (fail-closed) for write operations instead of returning a
fallback. Specifically, in the callers around create_skill(...) and the
install_from_url handler, detect and return the error from
resolve_workspace_dir()/resolve_config() (or convert it to a suitable API error)
before attempting create_skill or any disk writes, ensuring that
create_skill(...) and any disk-mutating logic only run when an authoritative
workspace/config was successfully resolved. Ensure read-only handlers may keep
the existing fallback behavior but all mutating functions require a successful,
non-fallback resolution.

---

Nitpick comments:
In `@app/src/components/skills/InstallSkillDialog.tsx`:
- Around line 43-59: The onSelectSkill prop declared on the Props interface is
never used inside the InstallSkillDialog component; either remove onSelectSkill
from Props (and any call sites) to shrink the public API, or keep it
intentionally but add a clear comment on the Props interface and above the prop
(e.g., “// reserved for symmetry with CreateSkillModal — intentionally unused”)
so reviewers won’t treat it as dead code; update the Props interface and the
InstallSkillDialog component signature accordingly and ensure any references to
onSelectSkill elsewhere are removed if you choose the former.

In `@app/src/services/api/skillsApi.ts`:
- Around line 84-92: The CreateSkillInput.scope field currently uses the broad
SkillScope (which includes 'legacy') but the skills_create endpoint only accepts
'user' or 'project'; change CreateSkillInput so scope is narrowed to only the
creatable values (e.g., replace scope?: SkillScope with scope?: 'user' |
'project' or introduce a new type alias like CreatableSkillScope and use that)
and update any references to CreateSkillInput in the codebase (e.g., functions
that call skills_create) to use the narrowed type so invalid values are caught
at compile time rather than runtime.

In `@src/openhuman/skills/schemas.rs`:
- Around line 163-193: Rename the exported functions in this module to match the
repo contract: change skills_schemas(function: &str) -> ControllerSchema to
schemas(function: &str) -> ControllerSchema, change
all_skills_controller_schemas() -> Vec<ControllerSchema> to
all_controller_schemas(), and change all_skills_registered_controllers() ->
Vec<RegisteredController> to all_registered_controllers(); update the vectors
that currently call skills_schemas(...) and construct RegisteredController
entries (which reference handlers handle_skills_list,
handle_skills_read_resource, handle_skills_create,
handle_skills_install_from_url) to call the renamed schemas(...) and ensure any
external call sites or uses import the new symbol names so the module exports
are schemas, all_controller_schemas, and all_registered_controllers as required
by the coding guidelines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ccbf3593-9632-465f-a8b3-d32758493e38

📥 Commits

Reviewing files that changed from the base of the PR and between 2e0b1a3 and 0fab789.

📒 Files selected for processing (16)
  • app/src/components/skills/CreateSkillModal.tsx
  • app/src/components/skills/InstallSkillDialog.tsx
  • app/src/components/skills/SkillDetailDrawer.tsx
  • app/src/components/skills/SkillResourcePreview.tsx
  • app/src/components/skills/SkillResourceTree.tsx
  • app/src/components/skills/__tests__/CreateSkillModal.test.tsx
  • app/src/components/skills/__tests__/InstallSkillDialog.test.tsx
  • app/src/components/skills/__tests__/SkillDetailDrawer.test.tsx
  • app/src/components/skills/__tests__/SkillResourcePreview.test.tsx
  • app/src/pages/Skills.tsx
  • app/src/services/api/__tests__/skillsApi.test.ts
  • app/src/services/api/skillsApi.ts
  • src/core/all.rs
  • src/openhuman/skills/mod.rs
  • src/openhuman/skills/ops.rs
  • src/openhuman/skills/schemas.rs
✅ Files skipped from review due to trivial changes (2)
  • src/openhuman/skills/mod.rs
  • app/src/components/skills/tests/SkillDetailDrawer.test.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/core/all.rs
  • app/src/components/skills/tests/CreateSkillModal.test.tsx
  • app/src/components/skills/tests/SkillResourcePreview.test.tsx
  • app/src/pages/Skills.tsx

Comment on lines +101 to +107
* `timeoutSecs` is optional — the Rust side defaults to 60s and caps at
* 600s. Values outside that range are clamped server-side.
*/
export interface InstallSkillFromUrlInput {
url: string;
timeoutSecs?: number;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does Serde with serde_jsonaccept JSON numbers like -1 or 0.5 when deserializing into a Rustu64, or are those values rejected as invalid?

💡 Result:

No, Serde with serde_json rejects JSON numbers like -1 or 0.5 when deserializing into a Rust u64. These values are rejected as invalid.

Citations:


🏁 Script executed:

sed -n '230,240p' app/src/services/api/skillsApi.ts

Repository: tinyhumansai/openhuman

Length of output: 460


🏁 Script executed:

grep -n "timeoutSecs\|timeout_secs" app/src/services/api/skillsApi.ts

Repository: tinyhumansai/openhuman

Length of output: 263


Validate timeoutSecs as an integer before sending it.

The interface accepts any number, but the backend expects a Rust u64. Negative and fractional values will fail deserialization before the server-side clamp logic runs, contradicting the documentation claim that "Values outside that range are clamped server-side." Either validate on the client (reject non-integer, non-positive values) or update the docs to reflect what actually gets clamped.

Occurs at: lines 106 (interface definition) and 235 (usage).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/services/api/skillsApi.ts` around lines 101 - 107, The
InstallSkillFromUrlInput.timeoutSecs must be validated as a non-negative integer
before sending: update the runtime check where InstallSkillFromUrlInput is
consumed (the request-sender / install-skill call at the usage site) to verify
Number.isInteger(timeoutSecs) and timeoutSecs >= 0 (or undefined), and
reject/throw a clear client-side error if not; do not rely on server clamping.
Ensure the check runs before serializing the payload so fractional or negative
values are blocked (or coerce intentionally only if you add explicit
truncation), and include this validation alongside the InstallSkillFromUrlInput
handling.

Comment on lines +225 to +229
installSkillFromUrl: async (
input: InstallSkillFromUrlInput
): Promise<InstallSkillFromUrlResult> => {
log('installSkillFromUrl: request url=%s', input.url);
const response = await callCoreRpc<
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't log raw install URLs from the UI.

A full URL can include signed query params or embedded credentials. Emit a redacted form instead of the raw user input.

As per coding guidelines, React/TypeScript code in app/src/ must use namespaced debug logs for development diagnostics and avoid logging secrets, raw JWTs, API keys, or full PII.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/services/api/skillsApi.ts` around lines 225 - 229, The
installSkillFromUrl function currently logs the raw input.url with
log('installSkillFromUrl: request url=%s', input.url); — replace that raw log
with a namespaced debug call and a redacted URL: parse input.url in
installSkillFromUrl, strip credentials and query string (or mask query values)
and log only the safe parts (hostname + pathname or a masked query) using a
debug namespace like debug('services:skillsApi') instead of the global log
function; ensure no JWTs, API keys, or full PII from input.url are emitted.

Comment on lines +431 to +432
let workspace = resolve_workspace_dir().await;
match create_skill(workspace.as_path(), payload.into()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail closed for write operations when config/workspace resolution fails.

skills.create and skills.install_from_url both write to disk, but resolve_workspace_dir() / resolve_config() silently downgrade config load failures to a fallback workspace. That can create/install a skill into the wrong workspace after a timeout/error, and in the worst case drops to ./.openhuman/workspace if the default root lookup also fails. The read-only handlers can keep the fallback, but the mutating ones should require an authoritative workspace/config.

Also applies to: 462-465, 490-543

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/skills/schemas.rs` around lines 431 - 432, The mutating
handlers (skills.create and skills.install_from_url) currently call
resolve_workspace_dir() / resolve_config() which silently fall back to a default
workspace on errors; change these code paths so that resolve_workspace_dir() and
resolve_config() errors are propagated (fail-closed) for write operations
instead of returning a fallback. Specifically, in the callers around
create_skill(...) and the install_from_url handler, detect and return the error
from resolve_workspace_dir()/resolve_config() (or convert it to a suitable API
error) before attempting create_skill or any disk writes, ensuring that
create_skill(...) and any disk-mutating logic only run when an authoritative
workspace/config was successfully resolved. Ensure read-only handlers may keep
the existing fallback behavior but all mutating functions require a successful,
non-fallback resolution.

Comment on lines +457 to +470
tracing::debug!(
url = %wire.url,
timeout_secs = ?wire.timeout_secs,
"[skills][rpc] install_from_url"
);
let config = resolve_config().await;
let workspace = config.workspace_dir.clone();
let payload: InstallSkillFromUrlParams = wire.into();
match install_skill_from_url(workspace.as_path(), payload).await {
Ok(outcome) => {
tracing::debug!(
url = %outcome.url,
new_count = outcome.new_skills.len(),
"[skills][rpc] install_from_url: ok"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redact install URLs before logging them.

Both debug logs emit the full install URL. Remote install links can contain query tokens or embedded credentials, so this leaks secrets into local logs on a security-sensitive path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/skills/schemas.rs` around lines 457 - 470, The debug logs in
the install_from_url RPC handler currently log full URLs (wire.url and
outcome.url), which can leak tokens/credentials; update the tracing::debug!
invocations in the install_from_url flow (the handler that converts wire into
InstallSkillFromUrlParams and calls install_skill_from_url) to log a redacted
URL instead of the raw string—implement or call a small sanitizer (e.g.,
redact_query_or_credentials(url: &str) -> String) and pass its output to the
trace statements for both the initial wire.url and outcome.url so no query
params or userinfo are written to logs.

…sai#681)

CI's cargo fmt (stable) rewraps the long `.map(..).unwrap_or(false)` chain
that passed local fmt. Apply the break so the pre-push hook and the
upstream lint job agree.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@senamakel senamakel merged commit 581f379 into tinyhumansai:main Apr 22, 2026
6 of 8 checks passed
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.

Integrate skills.md

2 participants