Skip to content

feat: skill install config sync with workspace detection#643

Merged
lefarcen merged 22 commits intomainfrom
feat/skill-install-config-sync
Mar 30, 2026
Merged

feat: skill install config sync with workspace detection#643
lefarcen merged 22 commits intomainfrom
feat/skill-install-config-sync

Conversation

@alchemistklk
Copy link
Copy Markdown
Contributor

What

When skills are installed/uninstalled/imported, immediately sync the OpenClaw config with an explicit per-agent skills allowlist — so agents detect skill changes within seconds instead of relying on the unreliable extraDirs file watcher.

Why

The bug: Skill files were installed to disk, but OpenClaw never loaded them. The agent couldn't detect new skills for 31+ minutes because:

  1. No syncAll() triggered after skill install/uninstall/import
  2. The config compiler never wrote a skills allowlist on agents
  3. OpenClaw relied solely on an unreliable file watcher (extraDirs)

Additionally, when agents install skills via clawhub install in conversation, those workspace skills were invisible to Nexu's skill ledger, config allowlist, and UI.

How

Phase 1: Shared Skill Config Sync

  • Add skills?: string[] to agent config schema (OpenClaw already supports it)
  • Compiler reads installed skill slugs from SkillDb and writes them to each agent's skills field
  • Sync service passes installed slugs to the compiler
  • syncAll() triggered after every install/uninstall/import via onSyncNeeded callback
  • Legacy compatibility: When ledger is empty (upgrade from old version), skills field is omitted so OpenClaw auto-discovers all skills from extraDirs

Phase 2: Workspace Skills & Upgrade Compatibility

  • Extend skill ledger with "workspace" source and agentId field
  • New WorkspaceSkillScanner scans agents/*/skills/ directories per agent
  • SkillDirWatcher reconciles workspace skills on startup (populates ledger from disk)
  • Compiler merges shared + workspace slugs per agent with deduplication
  • Detects symlinked skills (clawhub install creates symlinks in workspace)
  • API returns agentId and agentName on installed skills

Phase 3: UI

  • "Installed" tab shows custom shared skills at top, workspace skills grouped by agent below
  • Agent name and ID displayed in workspace skill group headers
  • Skill count reflects both custom and workspace skills

Key design decisions

  • Late-binding pattern in container.ts for circular dependency between SkillhubService and SyncService
  • Per-agent merge in compiler: [...new Set([...sharedSlugs, ...workspaceSlugs])] — each agent gets shared skills + its own workspace skills
  • Startup reconciliation for upgrade compat: disk → ledger sync before any config compilation
  • agentName resolved at request time (not stored in DB) so renames propagate instantly

Affected areas

  • Controller (backend / API)
  • Web dashboard (React UI)
  • Shared schemas / packages
  • Desktop app (Electron shell)
  • OpenClaw runtime
  • Skills
  • Build / CI / Tooling

Checklist

  • pnpm typecheck passes
  • pnpm lint passes (changed files clean; pre-existing failures unrelated)
  • pnpm test passes (105 tests pass; 13 pre-existing failures on main)
  • pnpm generate-types run (API schema changes)
  • No credentials or tokens in code or logs
  • No any types introduced

Test plan

Automated tests (30+ new)

  • Schema: agent skills field parsing (3 tests)
  • Compiler: skill assignment, per-agent merge, dedup, legacy fallback (8 tests)
  • SkillDb: workspace source, agentId, persistence, legacy compat (5 tests)
  • WorkspaceSkillScanner: detection, multi-agent, symlinks (6 tests)
  • SkillDirWatcher: workspace reconciliation (7 tests)
  • Sync service: installed slugs in compiled config (1 test)
  • Integration: full install → ledger → compile → agent chain (4 tests)
  • CatalogManager: workspace skill enrichment (5 tests)

Manual tests (all passed on packaged build)

Test Result
Fresh install (empty ledger) → skills omitted
Install via SkillHub UI → config updated ~2s
Agent detects installed skill
Uninstall via UI → skill removed from config
Agent no longer has uninstalled skill
Import custom zip → skill in config
Workspace skill detected (symlink)
Workspace skill visible in UI
Workspace removed from disk → uninstalled
Upgrade compat (delete ledger → restart)
Post-upgrade install preserves all skills

Notes for reviewers

  • scripts/test-skill-sync.mjs is a diagnostic inspector for manual testing — run node scripts/test-skill-sync.mjs to see ledger, config, and workspace state at a glance
  • The 13 pre-existing test failures in openclaw-config-writer.test.ts, openclaw-sync.test.ts, route-compat.test.ts, and model-provider-service.test.ts are on main — not introduced by this PR
  • OpenClaw sessions cache the skill snapshot — after install, the agent needs a new conversation (not same thread) to see the updated skill list. This is OpenClaw behavior, not a Nexu issue.

compileOpenClawConfig accepts an optional installedSkillSlugs param.
When provided and non-empty, each agent gets a skills field with the
list. When empty or undefined the field is omitted so OpenClaw falls
back to auto-discovering all skills from extraDirs (legacy compat).
Wire onSyncNeeded callback through SkillhubService so the OpenClaw
config is recompiled whenever a skill finishes installing or a queued
install is cancelled. Also add explicit syncAll calls in the uninstall
and import routes for immediate sync on those HTTP paths.
Add "workspace" to SkillSource union and agentId field to SkillRecord
for tracking agent-installed skills from clawhub. Update upsertRecord
matching to use agentId for workspace source disambiguation. Add
getInstalledByAgent query method and update API route schemas.
Extend SkillDirWatcher to scan per-agent workspace skill directories
during syncNow(), recording workspace skills in the ledger with
source: "workspace" and the appropriate agentId. Shared-dir
reconciliation now excludes workspace records to prevent false
uninstall marking.
…Name

- Add agentId to InstalledSkill type
- Add agentId and agentName to skillhub catalog response schema
- CatalogManager resolves SKILL.md from workspace agent dir for workspace skills
- Route handler resolves agentName from current bot config (not stored in DB)
- Non-workspace skills return agentId: null, agentName: null
Add "Agent Skills" sub-tab under "Yours" tab that displays workspace
skills grouped by agent name. Extends SkillSource type with "workspace"
and InstalledSkill with agentId/agentName fields. Includes en/zh-CN i18n.
clawhub install creates symlinks in workspace/skills/ pointing to
.agents/skills/. Node's isDirectory() returns false for symlinks,
so the scanner missed them. Now checks isSymbolicLink() too.
The Installed tab now shows custom shared skills at top and workspace
skills grouped by agent name below. Also fixes inspector script to
detect symlinked skill directories.
- Remove separate Agent Skills tab
- Show workspace skills grouped by agent below custom skills in Installed tab
- Display agent ID in workspace skill group headers
- Fix installed count to separate custom vs workspace
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 07401a325d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/controller/src/services/skillhub/skill-dir-watcher.ts Outdated
Comment thread apps/controller/src/services/skillhub-service.ts
Comment thread apps/web/src/pages/skills.tsx Outdated
…pace skills

The uninstall endpoint now accepts optional source and agentId fields.
For workspace skills, agentId identifies which agent's installation to
remove. Shared skill uninstall (slug-only) remains backward compatible.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Mar 28, 2026

Deploying nexu-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7543d37
Status: ✅  Deploy successful!
Preview URL: https://6b9dca16.nexu-docs.pages.dev
Branch Preview URL: https://feat-skill-install-config-sy.nexu-docs.pages.dev

View logs

@alchemistklk
Copy link
Copy Markdown
Contributor Author

/cr

@slack-code-review-channel
Copy link
Copy Markdown

✅ CR topic created in Feishu topic group Refly CR.

Copy link
Copy Markdown

@JiwaniZakir JiwaniZakir left a comment

Choose a reason for hiding this comment

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

The mutable syncService variable in container.ts (initialized as null, then assigned after openclawSyncService is constructed) is a workaround for a circular dependency between SkillhubService and OpenClawSyncService. This pattern is fragile — if the callback fires before syncService is assigned (unlikely here given synchronous construction, but non-obvious), it silently no-ops. More importantly, the .catch(() => {}) in the onSyncNeeded callback completely swallows sync errors with no logging, which will make debugging sync failures difficult in production. At minimum, a console.error or structured log call should be added there.

In openclaw-config-compiler.ts, the merged array uses new Set for deduplication, which is correct, but the ordering guarantee (shared slugs before workspace slugs) is implicit — if skill loading order matters for conflict resolution downstream, that assumption should be documented or enforced explicitly.

In openapi.json, agentId and agentName are listed in the required array while also marked nullable: true. Technically valid in JSON Schema, but it means consumers must always include these fields even when their value is null. It's worth confirming this is intentional rather than agentId/agentName being truly optional fields that should be omitted from required.

Add "user" source type to the skill system so skills installed via
clawhub CLI in ~/.agents/skills/ are tracked in the ledger and
included in the OpenClaw allowlist. Also simplify symlink detection
by relying on existsSync (which follows symlinks) instead of
explicit isSymbolicLink() checks.
@alchemistklk
Copy link
Copy Markdown
Contributor Author

  1. Mutable syncService + swallowed .catch(() =>{})
    Acknowledged. The late-binding pattern is safe here, - syncService is assigned synchronously 10 lines later(line 124) and onSyncNeeded is never invoked during SkillhubService.create()(it's only called when a skill install completes asynchronously). However, the client error swallowing is a valid concern fro producing debugging. Will add logger.error to catch the handler
  2. new Set ordering in compiler
    The ordering of merged array is irrelevent here.
  3. agentId/agentName required
    This is intentional. Using required + nullable (rather than optional) means every InstalledSkill object always has a consistent shape:
  • Shared skills: { agentId: null, agentName: null }
  • Workspace skills: { agentId: "bot-123", agentName: "Nexu Assistant" }
    This is better for frontend consumers — no need to check "agentId" in skill, just check skill.agentId !== null. The generated
    TypeScript SDK types reflect this as agentId: string | null rather than agentId?: string | null | undefined, which is cleaner.

@lefarcen lefarcen merged commit f33c509 into main Mar 30, 2026
12 checks passed
@lefarcen lefarcen mentioned this pull request Mar 30, 2026
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.

3 participants