Skip to content

feat: enforce skill scope (repo / team / label) at agent runtime (CYPACK-1156)#1205

Merged
Connoropolous merged 5 commits into
mainfrom
cypack-1156
May 13, 2026
Merged

feat: enforce skill scope (repo / team / label) at agent runtime (CYPACK-1156)#1205
Connoropolous merged 5 commits into
mainfrom
cypack-1156

Conversation

@cyrusagent
Copy link
Copy Markdown
Contributor

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

  • Payload contractUpdateSkillPayload in cyrus-config-updater gains optional repositoryIds / linearTeamIds / linearLabelIds. Old payloads without these fields keep working (treated as global).
  • On-disk persistencehandlers/skills.ts writes a scope.json sidecar next to SKILL.md only 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.md stays clean so scope metadata is not leaked to the model.
  • Runtime filteringSkillsPluginResolver learns a new SkillSessionContext (repo id, Linear team id, Linear label ids). discoverSkillNames(plugins, context) reads each skill's scope.json and 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.
  • SDK seam — wires through the Claude Agent SDK ≥ 0.2.120 skills option. New skills?: string[] | 'all' field on AgentRunnerConfig, ClaudeRunnerConfig, and RunnerConfigBuilder.buildIssueConfig input; passed through to query() so unlisted skills are hidden from the model's listing and rejected by the Skill tool.
  • ThreadingEdgeWorker.buildSkillSessionContext(repository, fullIssue) derives the context from repository.id, fullIssue.teamId, and fullIssue.labelIds. Used both in buildNewSessionPrompt (so the system-prompt skill guidance reflects only available skills) and in buildAgentRunnerConfig (so the SDK skills option carries the same allow-list).

Acceptance criteria

  • UpdateSkillPayload carries optional scope arrays end-to-end
  • A skill scoped to repo X loads for runs on repo X and not on repo Y
  • A skill scoped to Linear team T loads only for issues in team T
  • A skill scoped to label L loads only when the issue has label L
  • Global (unscoped) skills load for every run
  • Old payloads without scope fields continue to work (treated as global)

Tests

  • 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.
  • Full pnpm test:packages:run passes locally (618 edge-worker tests, 15 config-updater tests, etc.). The one pre-existing failure in packages/claude-runner/test/debug-logging.test.ts is environmental (test depends on DEBUG_CLAUDE_AGENT_SDK being unset in the shell) and unrelated to this change.
  • pnpm typecheck and pnpm lint clean.

Follow-up (cyhost)

Once this ships in cyrus-core, cyhost needs to:

  1. Bump the cyrus-core pin in cyrus-hosted/apps/app/package.json.
  2. Forward repository_ids / linear_team_ids / linear_label_ids from the DB row into UpdateSkillPayload in lib/skills/sync.ts.

Until that lands the cyhost UI still accepts scope but the runtime treats every skill as global — same as today.


Tip: I will respond to comments that @ mention @cyrusagent on this PR. You can also submit a review with all your feedback at once, and I will automatically wake up to address each comment.

…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 Connoropolous merged commit dad2d89 into main May 13, 2026
4 checks passed
@cyrusagent cyrusagent mentioned this pull request May 13, 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.

2 participants