feat: enforce skill scope (repo / team / label) at agent runtime (CYPACK-1156)#1205
Merged
Conversation
…ACK-1156) - Extend UpdateSkillPayload with optional repositoryIds / linearTeamIds / linearLabelIds so skill scope crosses the cyhost → cypack boundary. - Persist scope as a `scope.json` sidecar next to SKILL.md (kept out of the model's context) and clean it up when an update drops every dimension. - Add scope-aware filtering in SkillsPluginResolver: per-skill scope is matched against a SkillSessionContext (repo id, Linear team id, Linear label ids). Empty/missing sidecar = global skill; populated dimensions AND across, OR within each list. - Pass the filtered skill name allow-list to the Claude Agent SDK via the new `skills` option on ClaudeRunnerConfig / AgentRunnerConfig — the on-disk plugin layout stays untouched. - Thread session context through EdgeWorker.buildSkillSessionContext into both the system-prompt guidance and the runner config so the model sees only the skills it can actually invoke.
…1156) EdgeWorker's ensureUserPluginScaffolded only runs at startup and short-circuits when ~/.cyrus/user-skills-plugin/skills/ doesn't yet exist. So when a user syncs their first ever skill via cyhost AFTER cyrus is already running, the skill ends up on disk with no .claude-plugin/plugin.json sibling — the Claude Agent SDK's plugin loader silently skips the directory and the skill is invisible to every session until cyrus is restarted (or the manifest is hand-rolled). Have handleUpdateSkill write the manifest itself the first time it touches a skill. Idempotent — never overwrites an existing manifest.
Revert the handler-side ensurePluginManifest helper added in 6260f46 and shift the responsibility back to startup-time scaffolding, matching the Application.ensureRequiredDirectories() pattern used for repos / worktrees / mcp-configs. ensureUserPluginScaffolded now eagerly creates the full layout regardless of whether any skills have been synced yet: ~/.cyrus/user-skills-plugin/ ~/.cyrus/user-skills-plugin/skills/ ~/.cyrus/user-skills-plugin/.claude-plugin/plugin.json Idempotent — checked on every startup, never overwrites an existing manifest. Closes the original race where the manifest was only ever written when skills already existed at startup, leaving cyhost's first-ever skill sync invisible to the Claude Agent SDK until the next restart.
Connoropolous
approved these changes
May 13, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Assignee: @pauravhp (paurav)
Summary
Implements runtime enforcement of per-skill scope so user skills synced from cyrus-hosted can be limited to specific repositories, Linear teams, or Linear labels. Scope is captured at the cyhost UI today but, prior to this PR, never made it across the edgeconfig boundary and was not enforced at session time.
Linear: CYPACK-1156
Implementation
UpdateSkillPayloadincyrus-config-updatergains optionalrepositoryIds/linearTeamIds/linearLabelIds. Old payloads without these fields keep working (treated as global).handlers/skills.tswrites ascope.jsonsidecar next toSKILL.mdonly when at least one dimension is populated, and removes stale sidecars when an update drops every dimension. Empty / blank-string arrays are normalized to "no scope".SKILL.mdstays clean so scope metadata is not leaked to the model.SkillsPluginResolverlearns a newSkillSessionContext(repo id, Linear team id, Linear label ids).discoverSkillNames(plugins, context)reads each skill'sscope.jsonand returns only skills whose populated dimensions all match the session context (AND across dimensions, OR within each list). The on-disk plugin layout is untouched.skillsoption. Newskills?: string[] | 'all'field onAgentRunnerConfig,ClaudeRunnerConfig, andRunnerConfigBuilder.buildIssueConfiginput; passed through toquery()so unlisted skills are hidden from the model's listing and rejected by the Skill tool.EdgeWorker.buildSkillSessionContext(repository, fullIssue)derives the context fromrepository.id,fullIssue.teamId, andfullIssue.labelIds. Used both inbuildNewSessionPrompt(so the system-prompt skill guidance reflects only available skills) and inbuildAgentRunnerConfig(so the SDKskillsoption carries the same allow-list).Acceptance criteria
UpdateSkillPayloadcarries optional scope arrays end-to-endTests
packages/edge-worker/test/SkillsPluginResolver.scope.test.ts— 7 new tests covering global, repo / team / label dimensions, multi-dimension AND, malformed sidecars, and the no-context case.packages/config-updater/test/handlers/skills.test.ts— 5 new tests covering sidecar write/skip/cleanup behavior plus end-to-end delete.pnpm test:packages:runpasses locally (618 edge-worker tests, 15 config-updater tests, etc.). The one pre-existing failure inpackages/claude-runner/test/debug-logging.test.tsis environmental (test depends onDEBUG_CLAUDE_AGENT_SDKbeing unset in the shell) and unrelated to this change.pnpm typecheckandpnpm lintclean.Follow-up (cyhost)
Once this ships in
cyrus-core, cyhost needs to:cyrus-corepin incyrus-hosted/apps/app/package.json.repository_ids/linear_team_ids/linear_label_idsfrom the DB row intoUpdateSkillPayloadinlib/skills/sync.ts.Until that lands the cyhost UI still accepts scope but the runtime treats every skill as global — same as today.