Skip to content

Skills Visibility via Feature Flags (Gateway-Controlled)#10183

Merged
noanflaherty merged 9 commits into
mainfrom
feature/skill-feature-flags-gateway
Feb 27, 2026
Merged

Skills Visibility via Feature Flags (Gateway-Controlled)#10183
noanflaherty merged 9 commits into
mainfrom
feature/skill-feature-flags-gateway

Conversation

@noanflaherty
Copy link
Copy Markdown
Contributor

@noanflaherty noanflaherty commented Feb 27, 2026

Summary

Make skill exposure controllable by feature flags so a WIP skill can be turned off and become unavailable everywhere — hidden from UIs, model prompts, skill_load, and active tool projection.

Changes

Milestone PRs (merged into feature branch)

Project issue

Closes #10145

Test plan

  • Set skills.<id>.enabled=false via PATCH and confirm skill disappears from UI skill list
  • Confirm model prompt no longer lists that skill
  • Confirm /skill slash for that skill is unknown
  • Confirm skill_load for that skill fails
  • Confirm previously-loaded tool set for that skill is no longer projected on next turn
  • Confirm PATCH using daemon runtime token fails (403)
  • Confirm PATCH with feature-flag client token succeeds

Generated with Claude Code


Open with Devin

noanflaherty and others added 5 commits February 26, 2026 19:38
* feat: add skill feature flags config and enforcement

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: enforce feature flags on included child skills and dynamic prompt section

Add isSkillFeatureEnabled checks in skill_load for child skills in both
the body-loading loop and the loaded_skill marker loop, so flag-OFF child
skills are fully hidden. Also filter hardcoded browser/twitter references
in buildDynamicSkillWorkflowSection through isSkillFeatureEnabled so the
system prompt does not advertise disabled skills.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
* feat: add gateway feature-flags REST API with config persistence

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: handle malformed URI encoding and config parse errors in feature-flags

- Wrap decodeURIComponent in try/catch in gateway/src/index.ts to return
  400 Bad Request on malformed percent-encoding instead of crashing to
  the global error handler with a misleading 500.

- Refactor readConfigFile to use a discriminated union result type that
  distinguishes "file doesn't exist" (returns empty config) from "file
  exists but can't be parsed" (returns error). The PATCH handler now
  returns 500 with a descriptive message when the config file is
  malformed, preventing silent data loss. The GET handler gracefully
  degrades to empty flags on parse errors (no data loss risk).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: validate parsed config is a plain object in readConfigFile

---------

Co-authored-by: Claude <noreply@anthropic.com>
* feat: add client-only PATCH auth split for feature-flags

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: remove rate-limit on 403 and handle identical token edge case

- Remove authRateLimiter.recordFailure() from 403 path to avoid penalizing
  legitimately authenticated clients who used the wrong token type (Issue 1)
- Only record failures on 401 (truly invalid authentication)
- Skip runtime-token rejection when FEATURE_FLAG_TOKEN is identical to
  runtimeBearerToken to support single-token deployments (Issue 2)

Addresses review feedback on PR #10171

---------

Co-authored-by: Claude <noreply@anthropic.com>
* feat: add feature-flag client token plumbing for macOS/iOS

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: throw on unavailable transport, always use gateway on macOS, clear stale token

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
@noanflaherty
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 7 additional findings.

Open in Devin Review

chatgpt-codex-connector[bot]

This comment was marked as resolved.

…-pair

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

@codex review this PR again — the previous issues have been fixed in commit e2a74b2

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@noanflaherty
Copy link
Copy Markdown
Contributor Author

Thorough review findings

I pulled this PR into a local worktree and did a full diff + build/test pass. Findings below are ordered by severity.

  1. [P1] Gateway type-check is currently broken in this PR
  • GatewayConfig.featureFlagToken is required, but many existing typed config fixtures omit it.
  • New route code also has a union narrowing bug (result.detail accessed when reason: "not_found").
  • Refs:
    • gateway/src/config.ts:94
    • gateway/src/http/routes/feature-flags.ts:136
  1. [P1] iOS feature-flag mutation path is incomplete (token never delivered)
  • iOS expects featureFlagToken in pairing responses and stores it for PATCH auth.
  • Runtime pairing responses do not include featureFlagToken, so iOS will not receive it.
  • Refs:
    • assistant/src/runtime/routes/pairing-routes.ts:88-93
    • assistant/src/runtime/routes/pairing-routes.ts:136-141
    • clients/ios/Views/Settings/QRPairingSheet.swift:364,438
    • clients/shared/IPC/DaemonClient.swift:1545
  1. [P1] PATCH auth boundary can be bypassed if tokens are configured equal
  • PATCH rejects runtime token only when runtimeBearerToken !== featureFlagToken.
  • If tokens are equal, runtime token can mutate feature flags.
  • Ref:
    • gateway/src/index.ts:488-490
  1. [P2] “Hidden from assistant” is incomplete for at least one prompt path
  • System prompt still hardcodes guardian-verification instructions to load guardian-verify-setup regardless of feature-flag state.
  • Refs:
    • assistant/src/config/system-prompt.ts:212-237
  1. [P2] One newly-added integration test fails as written
  • Test expects <available_skills> to disappear when only browser/twitter flags are OFF.
  • Bundled skills remain visible, so this expectation is invalid.
  • Ref:
    • assistant/src/__tests__/skill-feature-flags-integration.test.ts:164

Validation run

  • assistant: bunx tsc --noEmit
  • assistant targeted tests (skill-feature-flags*, skill-load-feature-flag, skill-projection-feature-flag): 19 pass / 1 fail (the integration test above)
  • gateway targeted feature-flag tests: 23/23 pass
  • gateway: bunx tsc --noEmit ❌ (errors above)
  • clients: swift build
  • clients: swift test ❌, but failures are in existing unrelated tests (ThreadSessionRestorerTests.swift) and not in this PR diff

1. [P1] Fix gateway type-check: remove dead `not_found` variant from
   ConfigReadResult union (fixes narrowing bug on `result.detail`), make
   `featureFlagToken` optional in GatewayConfig type.

2. [P1] Add featureFlagToken to pairing responses: runtime now reads
   ~/.vellum/feature-flag-token and includes it in approval responses
   so iOS receives the token during QR pairing.

3. [P1] Fix PATCH auth bypass when tokens equal: loadConfig() now detects
   token collision and regenerates the feature-flag token to ensure the
   auth split is always enforceable.

4. [P2] Gate guardian-verification section on feature flags: system prompt
   skips buildGuardianVerificationRoutingSection() when guardian-verify-setup
   skill flag is OFF.

5. [P2] Fix failing integration test: removed incorrect assertion that
   <available_skills> disappears when only browser/twitter flags are OFF
   (bundled skills remain visible).

6. [P2] Fix macOS local HTTP transport: setFeatureFlag now checks if
   httpTransport.baseURL is localhost before delegating — local HTTP mode
   (localHttpEnabled) falls through to the direct gateway call.

7. [P2] Accept dotted skill IDs in feature-flag keys: updated regex from
   [a-z0-9_-]+ to [a-z0-9][a-z0-9._-]* to match managed skill ID validation.
@noanflaherty
Copy link
Copy Markdown
Contributor Author

Addressed all 7 review findings in commit cdcb35d:

P1 fixes:

  1. Gateway type-check: removed dead not_found union variant (fixes narrowing bug on result.detail), made featureFlagToken optional in GatewayConfig
  2. Pairing responses: runtime now reads ~/.vellum/feature-flag-token and includes it in approval responses for iOS
  3. PATCH auth bypass: loadConfig() detects token collision and regenerates the feature-flag token

P2 fixes:
4. Guardian verification: system prompt now gates buildGuardianVerificationRoutingSection() on isSkillFeatureEnabled('guardian-verify-setup', config)
5. Integration test: removed incorrect <available_skills> disappearance assertion (bundled skills remain visible)
6. macOS local HTTP: setFeatureFlag now checks isLocalBaseURL(httpTransport.baseURL) before delegating, so localHttpEnabled falls through to the direct gateway call
7. Dotted skill IDs: updated regex to [a-z0-9][a-z0-9._-]* matching managed skill ID validation

Validation:

  • gateway: tsc --noEmit ✅, 23/23 feature-flag tests pass ✅
  • assistant: tsc --noEmit ✅, 20/20 skill feature-flag tests pass ✅

…way/resolve-main

# Conflicts:
#	clients/macos/vellum-assistant/Features/MainWindow/PanelCoordinator.swift
@noanflaherty noanflaherty merged commit 793e7ad into main Feb 27, 2026
4 of 6 checks passed
@noanflaherty noanflaherty deleted the feature/skill-feature-flags-gateway branch February 27, 2026 02:33
@noanflaherty
Copy link
Copy Markdown
Contributor Author

Addressed remaining review feedback (P2: serialize feature-flag config writes) in #10244

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.

Skills Visibility via Feature Flags (Gateway-Controlled)

1 participant