Skip to content

Latest commit

 

History

History
184 lines (150 loc) · 7.68 KB

File metadata and controls

184 lines (150 loc) · 7.68 KB

CalendarWrite v1 — Per-Instance Environment Configuration (DRAFT)

STATUS: DRAFT. This file documents the env-variable changes required to activate the CalendarWrite v1 Authoritative Guard on each MCP instance, per ADR-0002 amendments A1–A15. It is intentionally NOT applied — applying it is substep 2h (human-gated). Read the diff against pre-calendarwrite-v1 first, then apply per the procedure below.


Why per-instance env vars?

The patched gcalendar/calendar_tools.py is a SHARED Python file consumed by all three MCP instances (google-personal, google-ema, google-llos). ADR-0002 A3 mandates that each instance carries its own CALENDAR_ATTENDEE_POLICY env var; the file does not, and must not, hardcode which instance is permissive.

Default behavior when env vars are unset is fail-closed: policy degrades to "disabled" (no attendee writes allowed). This means a missed restart fails toward "no exfil channel," not "permissive."


v1 Configuration Matrix

MCP instance port CALENDAR_ATTENDEE_POLICY CALENDARWRITE_ALLOWLIST_PATH
google-personal 8001 allowlist ~/.claude/skills/CalendarWrite/config/allowlist.json
google-ema 8002 disabled (unset — env is unused under disabled)
google-llos 8003 disabled (unset — env is unused under disabled)

Rationale:

  • google-personal is the CalendarWrite domain owner. It needs allowlist policy + a path to the seed allowlist (substep 2d-Step-4: Laura + Colin personal, kind: "person" only).
  • google-ema / google-llos had no attendee writes before this patch, and will have none after. disabled preserves prior behavior. If/when EMA or LLOS later need attendee writes, each gets its own allowlist policy + its own allowlist file via a new ADR — never via this one.

launchd does not perform shell ~ expansion. The allowlist loader runs Path(value).expanduser().resolve() (A14), so passing the raw ~/.claude/… string into the plist is safe. The startup log will print the RESOLVED absolute path for verification (Step 3 #16).


Where to make the change

The MCP instances are started by launchd:

Instance launchd label plist
google-personal com.pai.google-personal-mcp ~/Library/LaunchAgents/com.pai.google-personal-mcp.plist
google-ema com.pai.google-ema-mcp ~/Library/LaunchAgents/com.pai.google-ema-mcp.plist
google-llos com.pai.google-llos-mcp ~/Library/LaunchAgents/com.pai.google-llos-mcp.plist

Each plist already has an EnvironmentVariables dict with PATH and HOME. Add the two new keys (or one, for ema/llos) inside that same dict. Do NOT add them to start-mcp.sh's account dispatch — putting them in the plist keeps the per-instance policy at the OS layer rather than entangling it with the shared launch script, which keeps the security policy visible to launchctl print.

Snippet to add — google-personal plist

<key>EnvironmentVariables</key>
<dict>
    <key>PATH</key>
    <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
    <key>HOME</key>
    <string>/Users/colinmorawski</string>
    <!-- CalendarWrite v1 — see ADR-0002 A1-A15 -->
    <key>CALENDAR_ATTENDEE_POLICY</key>
    <string>allowlist</string>
    <key>CALENDARWRITE_ALLOWLIST_PATH</key>
    <string>~/.claude/skills/CalendarWrite/config/allowlist.json</string>
</dict>

Snippet to add — google-ema and google-llos plists

<key>EnvironmentVariables</key>
<dict>
    <key>PATH</key>
    <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
    <key>HOME</key>
    <string>/Users/colinmorawski</string>
    <!-- CalendarWrite v1 — disabled (no attendee writes from this MCP) -->
    <key>CALENDAR_ATTENDEE_POLICY</key>
    <string>disabled</string>
</dict>

Apply procedure (human-gated — substep 2h)

Do this AFTER you've reviewed the diff on feat/calendarwrite-v1-attendees-allowlist, the test suite is green, and (optionally) you've run a final Codex review.

  1. Seed the allowlist (Step 4 of BUILD-PLAN):

    mkdir -p ~/.claude/skills/CalendarWrite/config
    # Write allowlist.json with {version: 1, entries: [...]}.
    # All entries MUST have kind: "person" — group/list/alias entries are
    # rejected by the loader.
    chmod 600 ~/.claude/skills/CalendarWrite/config/allowlist.json
  2. Patch each plist in-place. Apple's plutil is the safe tool:

    # google-personal
    plutil -insert EnvironmentVariables.CALENDAR_ATTENDEE_POLICY -string 'allowlist' \
        ~/Library/LaunchAgents/com.pai.google-personal-mcp.plist
    plutil -insert EnvironmentVariables.CALENDARWRITE_ALLOWLIST_PATH \
        -string '~/.claude/skills/CalendarWrite/config/allowlist.json' \
        ~/Library/LaunchAgents/com.pai.google-personal-mcp.plist
    
    # google-ema
    plutil -insert EnvironmentVariables.CALENDAR_ATTENDEE_POLICY -string 'disabled' \
        ~/Library/LaunchAgents/com.pai.google-ema-mcp.plist
    
    # google-llos
    plutil -insert EnvironmentVariables.CALENDAR_ATTENDEE_POLICY -string 'disabled' \
        ~/Library/LaunchAgents/com.pai.google-llos-mcp.plist

    (Use plutil -replace … if the key already exists.)

  3. Restart all three instances (ADR-0002 A3 — the shared Python file means ema/llos's next restart for ANY reason will load the patched code, so restarting deterministically now is the only way to know the operative state):

    launchctl kickstart -k gui/$(id -u)/com.pai.google-personal-mcp
    launchctl kickstart -k gui/$(id -u)/com.pai.google-ema-mcp
    launchctl kickstart -k gui/$(id -u)/com.pai.google-llos-mcp
  4. Verify the startup log line for each instance (Step 3 #16):

    grep CALENDARWRITE_ALLOWLIST ~/hardened-google-workspace-mcp/mcp_server_debug_8001.log | tail -1
    # expect: status=loaded policy=allowlist path=/Users/colinmorawski/.claude/... entries=<N> sha256=<hash>
    grep CALENDARWRITE_ALLOWLIST ~/hardened-google-workspace-mcp/mcp_server_debug_8002.log | tail -1
    # expect: status=loaded policy=disabled path=- entries=0 sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
    grep CALENDARWRITE_ALLOWLIST ~/hardened-google-workspace-mcp/mcp_server_debug_8003.log | tail -1
    # expect: same as ema

    The google-personal log MUST show an absolute path (no leading ~) — that's the A14 sanity check. The empty-allowlist sha256 above is a known constant (sha256 of the empty string).


Rollback

If anything is wrong after restart, revert to the rollback anchor:

cd ~/hardened-google-workspace-mcp
git diff pre-calendarwrite-v1..HEAD -- gcalendar/ core/  # review
git reset --hard pre-calendarwrite-v1                     # only after confirmation
# Optional: undo the plist env-var inserts
plutil -remove EnvironmentVariables.CALENDAR_ATTENDEE_POLICY \
    ~/Library/LaunchAgents/com.pai.google-personal-mcp.plist
plutil -remove EnvironmentVariables.CALENDARWRITE_ALLOWLIST_PATH \
    ~/Library/LaunchAgents/com.pai.google-personal-mcp.plist
plutil -remove EnvironmentVariables.CALENDAR_ATTENDEE_POLICY \
    ~/Library/LaunchAgents/com.pai.google-ema-mcp.plist
plutil -remove EnvironmentVariables.CALENDAR_ATTENDEE_POLICY \
    ~/Library/LaunchAgents/com.pai.google-llos-mcp.plist
# Restart all three
launchctl kickstart -k gui/$(id -u)/com.pai.google-personal-mcp
launchctl kickstart -k gui/$(id -u)/com.pai.google-ema-mcp
launchctl kickstart -k gui/$(id -u)/com.pai.google-llos-mcp

The pre-CalendarWrite code path rejects all attendees parameter writes (the original CW-MODIFIED removal), so rolling back returns the system to "no attendee writes from any MCP instance" — the safest state.