Skip to content

feat: Google Workspace MCP server integration with strict sandbox#113

Merged
provos merged 8 commits intomasterfrom
feat/google-workspace-integration
Mar 18, 2026
Merged

feat: Google Workspace MCP server integration with strict sandbox#113
provos merged 8 commits intomasterfrom
feat/google-workspace-integration

Conversation

@provos
Copy link
Copy Markdown
Owner

@provos provos commented Mar 17, 2026

Summary

  • Add Google Workspace MCP server (@alanse/mcp-server-google-workspace) with credential-file rendezvous pattern: IronCurtain writes access-token-only .gworkspace-credentials.json files (no refresh_token), with a TokenFileRefresher proactively refreshing before expiry
  • Implement strict filesystem sandbox (denyRead: ["~"] with explicit allowRead) and dynamic node path discovery (discoverNodePaths()) that resolves nvm/volta/fnm/asdf installations for re-allowing
  • Add rewriteServerSettings() to patch srt settings after OAuth credential directories are created
  • Add constitution principles, argument roles, policy scenarios, and 20 new test files covering all integration points

Test plan

  • All 2615 unit tests pass
  • Build succeeds
  • Smoke test (scripts/test-gworkspace.mjs) successfully lists 5 recent emails through sandboxed MCP server
  • Manual test on Linux with nvm-based node installation
  • Verify sandbox blocks reads outside allowRead (e.g., ~/.ssh)

provos added 3 commits March 17, 2026 14:19
Integrate @alanse/mcp-server-google-workspace (128 tools across Gmail,
Calendar, Drive, Docs, Sheets, Slides) into IronCurtain using a
credential-file rendezvous pattern.

Core components:
- gworkspace-credentials.ts: atomic write of access-token-only credential
  files (refresh_token intentionally omitted to prevent rotation races)
- token-file-refresher.ts: proactive 5-minute interval refresher that
  writes fresh credential files before access tokens expire
- Google OAuth scope picker expanded with gmail.modify, gmail.labels,
  documents, spreadsheets, presentations
- Constitution principles for read-first/write-with-approval policy
- Handwritten scenarios and policy engine tests for all tool categories
- Argument roles for google-resource-id, email-address, email-body,
  calendar-datetime
Flip the google-workspace sandbox from a blocklist (denyRead specific
dirs) to an allowlist (denyRead ~ with explicit allowRead). This
prevents the MCP server from reading any user data under home.

Changes:
- Add allowRead support to SandboxFilesystemConfig, ResolvedSandboxParams,
  resolveSandboxConfig, and writeServerSettings
- Add discoverNodePaths() to find node/npm install dirs that need
  re-allowing when home is denied (handles homebrew, nvm, volta, fnm, asdf)
- Add rewriteServerSettings() to patch srt settings after OAuth setup
  adds the per-session credential directory
- Strip NODE_OPTIONS from MCP server child env to prevent IDE debugger
  preloads from referencing blocked paths under home
- Add google-workspace entry to mcp-servers.json with denyRead: ["~"]
  and allowRead for system dirs + OAuth credential injection
- Add test-gworkspace.mjs smoke test script
…ipping

- Use homedir() instead of manual HOME/USERPROFILE env var lookup in
  discoverNodePaths for consistency with the rest of the codebase
- Use expandTilde() for tilde matching in denyRead checks instead of
  ad-hoc string comparisons
- Scope NODE_OPTIONS='' to sandboxed servers only (unsandboxed servers
  don't need IDE preload stripping)
- Consolidate two rewriteServerSettings calls into one for OAuth
  servers, combining node path and credential dir injection
- Remove Homebrew paths from discoverNodePaths (not under ~, already
  readable) and /opt/homebrew from static allowRead in mcp-servers.json
Copilot AI review requested due to automatic review settings March 17, 2026 18:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Integrates the @alanse/mcp-server-google-workspace MCP server into IronCurtain with a strict sandbox profile and a proxy-side OAuth credential-file rendezvous + proactive refresh loop, along with policy/role/scenario updates and tests.

Changes:

  • Add proxy-side OAuth credential file writing (.gworkspace-credentials.json without refresh_token) plus TokenFileRefresher to refresh access tokens before expiry.
  • Extend sandbox integration to support allowRead, dynamic node path discovery (discoverNodePaths()), and post-write settings patching (rewriteServerSettings()).
  • Add Google Workspace policy fixtures, argument roles, handwritten scenarios, scope registry updates, and new/updated tests + smoke-test script + design doc.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test/token-file-refresher.test.ts Unit tests for proactive token refresh loop.
test/gworkspace-credentials.test.ts Unit tests for atomic credential-file writing + permissions + refresh_token omission.
test/sandbox-integration.test.ts Adds coverage for allowRead, rewriteServerSettings, and discoverNodePaths.
test/policy-engine.test.ts Adds policy evaluation tests for Google Workspace tools (allow/escalate/deny).
test/handwritten-scenarios.test.ts Updates expected handwritten scenario count after adding Workspace scenarios.
test/fixtures/test-policy.ts Adds compiled-policy rules + tool annotations + domain allowlist for google-workspace.
test/auth/google-scopes.test.ts Updates scope registry expectations (more services, default semantics).
test/argument-roles.test.ts Extends role registry tests for new Workspace roles and canonicalization behavior.
src/types/argument-roles.ts Adds new Workspace argument roles and registry entries.
src/trusted-process/token-file-refresher.ts Implements interval-based access token refresh + credential file rewrite.
src/trusted-process/gworkspace-credentials.ts Implements atomic, 0o600 access-token-only credential file writer.
src/trusted-process/sandbox-integration.ts Adds allowRead support, node path discovery, and settings rewrite helper.
src/trusted-process/mcp-proxy-server.ts Injects OAuth env + creds dir, starts refreshers, patches sandbox settings, strips NODE_OPTIONS for sandboxed servers.
src/pipeline/handwritten-scenarios.ts Adds handwritten Google Workspace scenarios (ground truth).
src/config/types.ts Extends sandbox filesystem config with allowRead.
src/config/mcp-servers.json Adds google-workspace server entry with strict sandbox + allowed domains.
src/config/constitution-user-base.md Adds constitutional principles for Google Workspace access patterns.
src/auth/providers/google-scopes.ts Adds additional Google scopes for Docs/Sheets/Slides and more Gmail scopes.
scripts/test-gworkspace.mjs Adds smoke test spawning the server in srt and calling Gmail tools.
docs/designs/google-workspace-integration.md Adds design doc for the credential-file rendezvous + sandbox strategy.

Comment on lines +1186 to +1194
env: {
...(process.env as Record<string, string>),
// Strip NODE_OPTIONS for sandboxed servers to prevent IDE debugger preloads
// (e.g., Cursor/VS Code) from referencing paths under ~ that denyRead blocks.
...(resolved.sandboxed ? { NODE_OPTIONS: '' } : {}),
...(config.env ?? {}),
...serverCredentials,
...oauthEnv,
},
Comment on lines +1191 to +1194
...(config.env ?? {}),
...serverCredentials,
...oauthEnv,
},
Comment thread test/fixtures/test-policy.ts Outdated
Comment on lines +495 to +497
toolName: 'gmail_list_messages',
serverName: 'google-workspace',
comment: 'Lists Gmail messages matching a query.',
Comment on lines +310 to +320
'email-address',
{
description: 'Email address (recipient, attendee, share target)',
isResourceIdentifier: false,
category: 'opaque',
canonicalize: lowercase,
annotationGuidance:
'Assign to arguments that are email addresses (recipients, attendees, share targets). ' +
'Includes the "to" field on send/draft, "attendees" on calendar events, "email" on file sharing.',
serverNames: ['google-workspace'],
},
Comment thread scripts/test-gworkspace.mjs Outdated
Comment on lines +155 to +181
// 5. Connect MCP client and call gmail_list_messages
// ---------------------------------------------------------------------------

const client = new Client({ name: 'gws-test', version: '1.0.0' });

try {
await client.connect(transport);
process.stderr.write('Connected to MCP server. Listing tools...\n');

const { tools } = await client.listTools();
process.stderr.write(`Server exposes ${tools.length} tools.\n`);

// Find the gmail_search_messages tool
const gmailSearch = tools.find((t) => t.name === 'gmail_search_messages');
if (!gmailSearch) {
process.stderr.write('Tool gmail_search_messages not found. Available tools:\n');
for (const t of tools.slice(0, 20)) {
process.stderr.write(` ${t.name}\n`);
}
process.exit(1);
}

process.stderr.write('Searching for 5 most recent emails...\n');

const searchResult = await client.callTool({
name: 'gmail_search_messages',
arguments: { query: '', maxResults: 5 },
provos added 2 commits March 17, 2026 14:45
…dress role

- Redact oauthEnv (CLIENT_SECRET etc.) in stderr logging alongside
  serverCredentials to prevent unredacted secret leakage to session logs
- Rename gmail_list_messages → gmail_search_messages in fixtures,
  scenarios, and tests to match the actual MCP server tool name
- Fix smoke test comment to say gmail_search_messages (matching code)
- Change email-address canonicalize from lowercase to identity since
  isResourceIdentifier: false means prepareToolArgs won't invoke it
…share-permission

Remove google-resource-id and calendar-datetime argument roles — both
are opaque with isResourceIdentifier: false, making them functionally
identical to 'none' with no policy evaluation impact.

Add share-permission role for drive_share_file's role argument, where
the small set of known values (viewer/commenter/editor) can meaningfully
drive policy decisions like restricting editor access.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a sandboxed Google Workspace MCP server integration, including OAuth credential-file injection (access-token-only) managed by the trusted proxy process, plus policy/test updates to classify Google Workspace tools as read-only vs mutation operations.

Changes:

  • Add Google Workspace server entry and strict sandboxing support with allowRead (for re-allowing Node/version-manager paths under denyRead: ["~"]).
  • Implement proxy-side OAuth token injection via per-session credential files and a periodic TokenFileRefresher.
  • Extend policy fixtures/scenarios, argument roles, and tests for Google Workspace tool decisions and scope registry updates.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/trusted-process/mcp-proxy-server.ts Injects OAuth credentials for the google-workspace server, rewrites sandbox settings for creds/node paths, starts/stops token refreshers, and redacts OAuth env in logs.
src/trusted-process/token-file-refresher.ts New interval-based refresher that proactively refreshes and rewrites token credential files before expiry.
src/trusted-process/gworkspace-credentials.ts Writes access-token-only .gworkspace-credentials.json atomically with 0o600 permissions.
src/trusted-process/sandbox-integration.ts Adds allowRead, plus helpers to discover Node install paths and rewrite existing srt settings files.
src/config/mcp-servers.json Adds the google-workspace MCP server (npx-based) with strict sandbox settings and domain allowlist.
src/config/types.ts Extends sandbox filesystem config with allowRead.
src/types/argument-roles.ts Adds Google Workspace argument roles (email-address, email-body, share-permission).
src/auth/providers/google-scopes.ts Adds additional Google scopes (Gmail modify/labels + Docs/Sheets/Slides).
src/config/constitution-user-base.md Adds Google Workspace principles for read-first vs write-with-approval behavior.
src/pipeline/handwritten-scenarios.ts Adds handwritten Google Workspace policy scenarios (allow read-only, escalate mutations, deny unknown).
docs/designs/google-workspace-integration.md Design doc describing the credential-file rendezvous, sandbox strategy, and operational/security tradeoffs.
scripts/test-gworkspace.mjs Smoke test script to spawn the server in srt, then list recent Gmail messages.
test/token-file-refresher.test.ts Unit tests for refresh thresholding, scope propagation, error logging, and start/stop behavior.
test/gworkspace-credentials.test.ts Unit tests for credential file format, permissions, atomic overwrite, and no refresh_token.
test/sandbox-integration.test.ts Extends sandbox integration tests for allowRead, settings rewrite, and node path discovery.
test/policy-engine.test.ts Adds policy engine tests validating allow/escalate/deny decisions for representative GWS tools.
test/fixtures/test-policy.ts Adds test compiled-policy rules + tool annotations for google-workspace, plus domain allowlist entries.
test/handwritten-scenarios.test.ts Updates expected handwritten scenario count for the newly added Workspace scenarios.
test/auth/google-scopes.test.ts Updates scope registry tests for additional service groups and default-scope expectations.
test/argument-roles.test.ts Updates registry-size expectations and adds tests for new Workspace roles.

Comment on lines +66 to +72
this.intervalHandle = setInterval(() => {
void this.refreshIfNeeded();
}, intervalMs);

// Ensure the interval doesn't keep the process alive
this.intervalHandle.unref();
}
Comment thread src/config/mcp-servers.json Outdated
"google-workspace": {
"description": "Google Workspace tools (Gmail, Calendar, Drive, Docs, Sheets, Slides)",
"command": "npx",
"args": ["-y", "@alanse/mcp-server-google-workspace"],
Comment on lines +337 to +339
*/
function addIfUnderHome(paths: Set<string>, dir: string, home: string): void {
if (home && dir.startsWith(home + '/')) {
provos added 2 commits March 18, 2026 10:43
…ymlinks

- Add in-flight guard to TokenFileRefresher to prevent concurrent
  refresh attempts when a slow network call overlaps the next interval
- Pin @alanse/mcp-server-google-workspace to v1.0.2 for reproducibility
- Resolve homedir() through realpath in discoverNodePaths so prefix
  comparisons work when home involves symlinks (e.g., macOS /var)
Remove git and google-workspace scenarios from handwritten-scenarios.ts
— these are constitution-dependent and would conflict with user
constitutions that make different policy choices for those servers.
Handwritten scenarios constrain the LLM compiler, so only universal
invariants (filesystem sandbox containment) belong here. Server-specific
policy behavior is already tested in policy-engine.test.ts.
Copilot AI review requested due to automatic review settings March 18, 2026 14:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Integrates the Google Workspace MCP server into IronCurtain with an access-token-only credential-file rendezvous pattern, proactive token refreshing in the proxy process, and stricter sandboxing (notably denyRead: ["~"] plus explicit re-allows).

Changes:

  • Add proxy-side OAuth token injection for google-workspace, including per-session credential file writing and a TokenFileRefresher.
  • Extend sandbox settings to support allowRead, plus runtime patching of settings and node path discovery for denyRead: ["~"] setups.
  • Add/adjust policy fixtures, argument roles, constitution text, and tests for the new integration points.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/token-file-refresher.test.ts Unit tests for proactive credential refresh behavior and lifecycle.
test/sandbox-integration.test.ts Adds coverage for allowRead, rewriteServerSettings(), and discoverNodePaths().
test/policy-engine.test.ts Adds policy decision tests for key Google Workspace tools.
test/handwritten-scenarios.test.ts Updates expected scenario count after pruning handwritten scenarios.
test/gworkspace-credentials.test.ts Verifies credential file format, atomic writes, and permissions.
test/fixtures/test-policy.ts Adds Google Workspace tool annotations, rules, and domain allowlist entries for tests.
test/auth/google-scopes.test.ts Updates expectations for expanded Google scope registry and defaults behavior.
test/argument-roles.test.ts Updates role registry tests for new Google Workspace-related roles.
src/types/argument-roles.ts Adds new opaque roles (email-address, email-body, share-permission) with registry entries.
src/trusted-process/token-file-refresher.ts Implements periodic, non-throwing refresh loop for credential files.
src/trusted-process/sandbox-integration.ts Adds allowRead, node-path discovery, and settings rewrite utility.
src/trusted-process/mcp-proxy-server.ts Adds OAuth-backed server setup (credential dir, env injection, refresher lifecycle) and NODE_OPTIONS stripping for sandboxed servers.
src/trusted-process/gworkspace-credentials.ts Writes access-token-only .gworkspace-credentials.json atomically with 0o600 perms.
src/pipeline/handwritten-scenarios.ts Narrows handwritten scenarios to universal invariants only.
src/config/types.ts Extends sandbox filesystem config with allowRead.
src/config/mcp-servers.json Adds google-workspace server entry with strict sandbox + network allowlist.
src/config/constitution-user-base.md Adds Google Workspace principles (read-first, write-with-approval, etc.).
src/auth/providers/google-scopes.ts Adds additional Gmail/Docs/Sheets/Slides scope entries.
scripts/test-gworkspace.mjs Adds a smoke test script to run the server in an srt sandbox and exercise Gmail tools.
docs/designs/google-workspace-integration.md Design doc for the integration approach, security model, and tradeoffs.

Comment thread test/sandbox-integration.test.ts Outdated
Comment on lines +423 to +437
const home = process.env.HOME ?? '';
if (!home) return; // skip if no HOME
const paths = discoverNodePaths();
for (const p of paths) {
// All returned paths should be under home (since only those need allowRead)
// except macOS Homebrew paths which are system-wide
if (!p.startsWith('/opt/homebrew') && !p.startsWith('/usr/local')) {
expect(p.startsWith(home + '/')).toBe(true);
}
}
});

it('includes node installation prefix when node is under home', () => {
const home = process.env.HOME ?? '';
if (!home || !process.execPath.startsWith(home + '/')) return;
Comment on lines +123 to +129
const npxPath = process.env.NPX_PATH ?? 'npx';

// srt -s <settings> -c "<command>"
// The -c flag runs the command string through a shell, so no escaping needed
// for the simple npx invocation.
const serverCommand = `${npxPath} -y @alanse/mcp-server-google-workspace@1.0.2`;

Comment on lines +51 to +60
"network": {
"allowedDomains": [
"googleapis.com",
"*.googleapis.com",
"accounts.google.com",
"oauth2.googleapis.com",
"registry.npmjs.org",
"*.npmjs.org"
]
}
Comment on lines 118 to 133
const sandboxConfig = serverConfig.sandbox ?? {};
const fsConfig = sandboxConfig.filesystem ?? {};
const networkConfig = sandboxConfig.network;

const allowWrite = buildAllowWrite(sessionSandboxDir, fsConfig.allowWrite ?? []);
const allowRead = fsConfig.allowRead ?? [];

const denyRead = fsConfig.denyRead ?? DEFAULT_DENY_READ;
const denyWrite = fsConfig.denyWrite ?? [];

const network = resolveNetworkConfig(networkConfig);

return {
sandboxed: true,
config: { allowWrite, denyRead, denyWrite, network },
config: { allowWrite, allowRead, denyRead, denyWrite, network },
};
- Expand tilde paths in allowRead during resolveSandboxConfig for
  consistency with allowWrite normalization (prevents silent failures
  if someone puts ~/.nvm in allowRead in mcp-servers.json)
- Use realpath-resolved homedir in discoverNodePaths test to match
  the implementation and avoid flakiness on symlinked home dirs
- Add _comment explaining npm registry domains in mcp-servers.json
  are needed for npx -y initial install
@provos provos merged commit 13b45d0 into master Mar 18, 2026
9 checks passed
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