Skip to content

Commit 48ebe1e

Browse files
feat: feature configuration service for scope-based toggles (#284)
* feat: add feature configuration service for scope-based feature toggles Implements a three-layer configuration system (defaults → settings → env overrides) that lets users and contributors control which services and OAuth scopes the extension requests. Each service is split into read/write feature groups. Disabled features have their tools unregistered and their scopes removed from OAuth requests. Closes #255 * style: fix prettier formatting in feature configuration files * fix: prefix unused variables to satisfy lint rules * style: fix prettier formatting in feature-resolver test * fix: resolve type error in registerTool wrapper * fix: address review feedback on feature configuration - Remove overly broad `drive` scope from docs.write (documents scope suffices) - Remove unused `_featureGroupKey` alias and `_enabledTools` destructuring in tests - Return `server` instead of `undefined` when skipping disabled tools to preserve method chaining * docs: remove drive scope from docs.write in feature config table * fix: address Abhi's review feedback on feature configuration PR - Fix warning admonition rendering in docs (multi-line syntax) - Improve duplicate tool test to show which tools are duplicated - Add readonly modifiers to FeatureGroup interface fields - Add ServiceName literal union for stronger type safety - Use as const satisfies for FEATURE_GROUPS declaration - Precompute GROUP_INDEX and TOOL_INDEX as module-level constants * fix: revert warning admonition to single-line format for prettier
1 parent 6feba6d commit 48ebe1e

10 files changed

Lines changed: 1020 additions & 70 deletions

File tree

docs/.vitepress/config.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default defineConfig({
99
// https://vitepress.dev/reference/default-theme-config
1010
nav: [
1111
{ text: 'Home', link: '/' },
12+
{ text: 'Configuration', link: '/feature-configuration' },
1213
{ text: 'Development', link: '/development' },
1314
{ text: 'Release', link: '/release' },
1415
{ text: 'Release Notes', link: '/release_notes' },
@@ -19,6 +20,7 @@ export default defineConfig({
1920
text: 'Documentation',
2021
items: [
2122
{ text: 'Overview', link: '/' },
23+
{ text: 'Feature Configuration', link: '/feature-configuration' },
2224
{ text: 'Development Guide', link: '/development' },
2325
{ text: 'GCP Setup Guide', link: '/GCP-RECREATION' },
2426
{ text: 'Release Guide', link: '/release' },

docs/development.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ used to maintain dot notation and avoid breaking existing configurations.
156156
- `__tests__/`: Contains all the tests.
157157
- `auth/`: Handles authentication.
158158
- `cli/`: CLI tools (e.g., headless OAuth login).
159+
- `features/`: Feature configuration registry and resolver. See the
160+
[Feature Configuration](../feature-configuration) docs.
159161
- `services/`: Contains the business logic for each service.
160162
- `utils/`: Contains utility functions.
161163
- `config/`: Contains configuration files.

docs/feature-configuration.md

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Feature Configuration
2+
3+
The extension provides a feature configuration system that lets you control
4+
which services and scopes are enabled. Each Google Workspace service is split
5+
into **read** and **write** feature groups, giving you granular control over
6+
what the extension can access.
7+
8+
## Feature Groups
9+
10+
| Service | Group | Scopes | Default |
11+
| ---------- | ----- | ----------------------------------------------------------------------------- | ------- |
12+
| `docs` | read | `documents` | ON |
13+
| `docs` | write | `documents` | ON |
14+
| `drive` | read | `drive.readonly` | ON |
15+
| `drive` | write | `drive` | ON |
16+
| `calendar` | read | `calendar.readonly` | ON |
17+
| `calendar` | write | `calendar` | ON |
18+
| `chat` | read | `chat.spaces.readonly`, `chat.messages.readonly`, `chat.memberships.readonly` | ON |
19+
| `chat` | write | `chat.spaces`, `chat.messages`, `chat.memberships` | ON |
20+
| `gmail` | read | `gmail.readonly` | ON |
21+
| `gmail` | write | `gmail.modify` | ON |
22+
| `people` | read | `userinfo.profile`, `directory.readonly` | ON |
23+
| `slides` | read | `presentations.readonly` | ON |
24+
| `slides` | write | `presentations` | **OFF** |
25+
| `sheets` | read | `spreadsheets.readonly` | ON |
26+
| `sheets` | write | `spreadsheets` | **OFF** |
27+
| `time` | read | _(none)_ | ON |
28+
| `tasks` | read | `tasks.readonly` | **OFF** |
29+
| `tasks` | write | `tasks` | **OFF** |
30+
31+
**Read** groups contain tools with no side effects (search, get, list).
32+
**Write** groups contain tools that perform mutations (create, update, delete,
33+
send).
34+
35+
Services whose write scopes aren't in the published GCP project (Slides write,
36+
Sheets write, Tasks) default to **OFF**. These can be enabled by contributors
37+
using their own GCP projects.
38+
39+
## Configuration via `WORKSPACE_FEATURE_OVERRIDES`
40+
41+
Use the `WORKSPACE_FEATURE_OVERRIDES` environment variable to enable or disable
42+
feature groups and individual tools.
43+
44+
### Syntax
45+
46+
```
47+
WORKSPACE_FEATURE_OVERRIDES="key:on|off,key:on|off,..."
48+
```
49+
50+
Each entry is a comma-separated `key:value` pair where:
51+
52+
- `key` is a feature group (e.g., `gmail.write`) or a tool name (e.g.,
53+
`calendar.deleteEvent`)
54+
- `value` is `on` or `off`
55+
56+
### Group-Level Overrides
57+
58+
Disable or enable entire feature groups:
59+
60+
```bash
61+
# Disable Gmail write tools (send, createDraft, modify, etc.)
62+
export WORKSPACE_FEATURE_OVERRIDES="gmail.write:off"
63+
64+
# Disable all of Chat
65+
export WORKSPACE_FEATURE_OVERRIDES="chat.read:off,chat.write:off"
66+
67+
# Enable experimental features (Slides write, Tasks)
68+
export WORKSPACE_FEATURE_OVERRIDES="slides.write:on,tasks.read:on,tasks.write:on"
69+
```
70+
71+
### Tool-Level Overrides
72+
73+
Disable specific tools within an enabled group (subtractive only):
74+
75+
```bash
76+
# Keep calendar.write enabled but disable delete
77+
export WORKSPACE_FEATURE_OVERRIDES="calendar.deleteEvent:off"
78+
79+
# Disable destructive Gmail tools while keeping modify/label tools
80+
export WORKSPACE_FEATURE_OVERRIDES="gmail.send:off,gmail.sendDraft:off"
81+
82+
# Combine group and tool overrides
83+
export WORKSPACE_FEATURE_OVERRIDES="gmail.write:off,calendar.deleteEvent:off,slides.write:on"
84+
```
85+
86+
::: warning Tool-level overrides are **subtractive only**. You cannot use
87+
`tool:on` to enable a tool whose feature group is disabled. To enable tools,
88+
enable their parent feature group. :::
89+
90+
### Precedence
91+
92+
The configuration follows a three-layer precedence model:
93+
94+
1. **Baked-in defaults** — Current services default ON; experimental services
95+
default OFF
96+
2. **Settings** — Future: overrides from the install-time settings UI
97+
3. **`WORKSPACE_FEATURE_OVERRIDES`** — Highest precedence; overrides everything
98+
99+
### Effects
100+
101+
When a feature group is disabled:
102+
103+
- Its **tools are not registered** with the MCP server (clients won't see them)
104+
- Its **OAuth scopes are not requested** during authentication
105+
- If you re-enable a previously disabled feature, you may need to
106+
re-authenticate to grant the new scopes
107+
108+
## Tools by Feature Group
109+
110+
### `docs.read`
111+
112+
- `docs.getSuggestions`
113+
- `docs.getText`
114+
115+
### `docs.write`
116+
117+
- `docs.create`
118+
- `docs.writeText`
119+
- `docs.replaceText`
120+
- `docs.formatText`
121+
122+
### `drive.read`
123+
124+
- `drive.getComments`
125+
- `drive.findFolder`
126+
- `drive.search`
127+
- `drive.downloadFile`
128+
129+
### `drive.write`
130+
131+
- `drive.createFolder`
132+
- `drive.moveFile`
133+
- `drive.trashFile`
134+
- `drive.renameFile`
135+
136+
### `calendar.read`
137+
138+
- `calendar.list`
139+
- `calendar.listEvents`
140+
- `calendar.getEvent`
141+
- `calendar.findFreeTime`
142+
143+
### `calendar.write`
144+
145+
- `calendar.createEvent`
146+
- `calendar.updateEvent`
147+
- `calendar.respondToEvent`
148+
- `calendar.deleteEvent`
149+
150+
### `chat.read`
151+
152+
- `chat.listSpaces`
153+
- `chat.findSpaceByName`
154+
- `chat.getMessages`
155+
- `chat.findDmByEmail`
156+
- `chat.listThreads`
157+
158+
### `chat.write`
159+
160+
- `chat.sendMessage`
161+
- `chat.sendDm`
162+
- `chat.setUpSpace`
163+
164+
### `gmail.read`
165+
166+
- `gmail.search`
167+
- `gmail.get`
168+
- `gmail.downloadAttachment`
169+
- `gmail.listLabels`
170+
171+
### `gmail.write`
172+
173+
- `gmail.modify`
174+
- `gmail.batchModify`
175+
- `gmail.modifyThread`
176+
- `gmail.send`
177+
- `gmail.createDraft`
178+
- `gmail.sendDraft`
179+
- `gmail.createLabel`
180+
181+
### `people.read`
182+
183+
- `people.getUserProfile`
184+
- `people.getMe`
185+
- `people.getUserRelations`
186+
187+
### `slides.read`
188+
189+
- `slides.getText`
190+
- `slides.getMetadata`
191+
- `slides.getImages`
192+
- `slides.getSlideThumbnail`
193+
194+
### `sheets.read`
195+
196+
- `sheets.getText`
197+
- `sheets.getRange`
198+
- `sheets.getMetadata`
199+
200+
### `time.read`
201+
202+
- `time.getCurrentDate`
203+
- `time.getCurrentTime`
204+
- `time.getTimeZone`
205+
206+
## For Contributors
207+
208+
When adding a new service or tools:
209+
210+
1. Define read and write feature group entries in
211+
`workspace-server/src/features/feature-config.ts`
212+
2. Set the default state — **ON** for scopes in the published GCP project,
213+
**OFF** otherwise
214+
3. Register your tools in `index.ts` as usual — the feature config wrapper
215+
automatically skips disabled tools
216+
217+
This lets contributors develop and merge new features without being blocked by
218+
the published GCP project's scope configuration. Contributors can test with
219+
their own GCP projects by enabling the feature via
220+
`WORKSPACE_FEATURE_OVERRIDES`.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect } from '@jest/globals';
8+
import { FEATURE_GROUPS, featureGroupKey } from '../../features/feature-config';
9+
10+
describe('feature-config', () => {
11+
it('should have unique feature group keys', () => {
12+
const keys = FEATURE_GROUPS.map(featureGroupKey);
13+
expect(keys.length).toBe(new Set(keys).size);
14+
});
15+
16+
it('should not have duplicate tool names across groups', () => {
17+
const allTools: string[] = [];
18+
for (const fg of FEATURE_GROUPS) {
19+
allTools.push(...fg.tools);
20+
}
21+
const duplicates = allTools.filter(
22+
(tool, i) => allTools.indexOf(tool) !== i,
23+
);
24+
expect(duplicates).toEqual([]);
25+
});
26+
27+
it('should have slides.write, sheets.write, tasks.read, and tasks.write defaulted to OFF', () => {
28+
const offByDefault = FEATURE_GROUPS.filter((fg) => !fg.defaultEnabled).map(
29+
featureGroupKey,
30+
);
31+
expect(offByDefault).toContain('slides.write');
32+
expect(offByDefault).toContain('sheets.write');
33+
expect(offByDefault).toContain('tasks.read');
34+
expect(offByDefault).toContain('tasks.write');
35+
});
36+
37+
it('should have all default-ON services with at least one tool', () => {
38+
const defaultOnWithNoTools = FEATURE_GROUPS.filter(
39+
(fg) => fg.defaultEnabled && fg.tools.length === 0,
40+
);
41+
expect(defaultOnWithNoTools).toEqual([]);
42+
});
43+
44+
it('should have valid scope URLs', () => {
45+
for (const fg of FEATURE_GROUPS) {
46+
for (const scope of fg.scopes) {
47+
expect(scope).toMatch(/^https:\/\/www\.googleapis\.com\/auth\//);
48+
}
49+
}
50+
});
51+
52+
it('should have time.read with no scopes', () => {
53+
const timeRead = FEATURE_GROUPS.find(
54+
(fg) => fg.service === 'time' && fg.group === 'read',
55+
);
56+
expect(timeRead).toBeDefined();
57+
expect(timeRead!.scopes).toEqual([]);
58+
});
59+
});

0 commit comments

Comments
 (0)