feat: compile-time safety profiles for command removal#366
Open
drewburchfield wants to merge 4 commits intosteipete:mainfrom
Open
feat: compile-time safety profiles for command removal#366drewburchfield wants to merge 4 commits intosteipete:mainfrom
drewburchfield wants to merge 4 commits intosteipete:mainfrom
Conversation
Contributor
Add a build-time code generation system that produces restricted CLI binaries from a YAML configuration. Disabled commands are removed at compile time via Go build tags, so they cannot be invoked at all. How it works: - Parent struct definitions extracted to *_types.go (build tag !safety_profile) - cmd/gen-safety reads a YAML profile and generates *_cmd_gen.go files with build tag safety_profile, containing only the enabled commands - Building with -tags safety_profile uses the generated structs - Stock "go build" is completely unchanged Key design decisions: - Fail-closed: commands not listed in YAML are excluded by default - Each service section can be toggled with enabled: true/false - Individual subcommands can be selectively included or excluded - Utility commands (version, auth, config, completion) always included Includes: - cmd/gen-safety: code generator with YAML validation and --strict mode - cmd/extract-types: one-time tool for extracting parent structs - build-safe.sh: convenience script (generate + compile) - Preset profiles: full.yaml, readonly.yaml, agent-safe.yaml - Example profile: safety-profile.yaml Also fixes contacts_crud.go parameter type grouping (given/org were bool instead of string), which is an existing upstream bug.
Replace the ~655-line hand-maintained command registry in gen-safety with automatic discovery via Go's AST package. The generator now parses *_types.go source files directly to find all Cmd structs and their hierarchy, eliminating manual sync when upstream adds commands. What changed: - New discover.go: parses *_types.go files, walks from CLI struct down to build serviceSpec list and CLI field categorization automatically - New discover_test.go: 10 tests covering tag parsing, file parsing, multi-struct files, NonCmdPrefix, field categorization, and more - main.go: wired to call AST discovery instead of manual registry; ~655 lines of spec functions deleted - Rename safety-profile.yaml to safety-profile.example.yaml (users should copy and customize, not edit the example directly) - Updated Makefile, build-safe.sh, README.md for the rename The generated output is identical (verified by diffing all *_cmd_gen.go files before and after). Net change: ~400 fewer lines of code and no more manual updates when upstream adds commands.
0e2a88c to
26d21d4
Compare
- buildEmptyStruct: preserve non-command fields (e.g. KeepCmd's ServiceAccount/Impersonate) when service is fully disabled - mapHasEnabledLeaf: fatal on unexpected YAML types instead of silently ignoring (matches isEnabled behavior) - isServiceDisabled: warn on unexpected types before fail-closed - Remove misleading `open` key from utility section (it's an alias, not a utility, and is controllable via aliases.open) - Add main_test.go with tests for fail-closed security contract: isEnabled, filterFields, isServiceDisabled, resolveEnabledFields, mapHasEnabledLeaf (15 total tests now) - Fix doc comments, remove dead code, fix build-safe.sh version suffix
5 tasks
Safety profile builds now use ~/Library/Application Support/gogcli-safe/ instead of gogcli/, preventing credential sharing between stock gog and gog-safe binaries. Uses the existing safety_profile build tag.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds compile-time safety profiles, a system that physically removes CLI commands from the
gogbinary so that AI agents (or other untrusted callers) cannot invoke them regardless of flags, environment variables, or config file changes.//go:build safety_profile*_types.gosource files viago/astto find all commands automatically, so no manual registry updates are needed when upstream adds new commandsgo buildis completely unchanged (zero risk to existing users/CI/Homebrew)full.yaml,readonly.yaml,agent-safe.yamlgogcli-safe/instead ofgogcli/), so credentials are not shared between stockgogand a profiled binarycontacts_crud.gofield updatesWhy this matters
AI agents with shell access need Google Workspace CLI tools but shouldn't have full write/delete access. The Summer Yue incident (Meta AI safety researcher's inbox wiped by an agent) is the cautionary tale. As more people give agents CLI access via
gog, compile-time command removal becomes essential.The Gmail draft problem that OAuth scopes can't solve
This directly addresses #239 (restrict Gmail to creating drafts, never send). Google's OAuth scopes have a gap:
gmail.readonlyblocks ALL writes, including draftsgmail.composeallows drafts AND sendinggmail.draftsscopeThe only way to allow
gog gmail drafts createwhile blockinggog gmail sendandgog gmail drafts sendis to remove those commands from the binary. OAuth scope restriction alone cannot do this.How it works
Two versions of each parent struct exist:
*_types.gowith//go:build !safety_profile-- full struct (normal build)*_cmd_gen.gowith//go:build safety_profile-- trimmed struct (generated from YAML profile)Kong registers commands by walking struct fields with
cmd:""tags. The code generator parses all*_types.gosource files via Go's AST package to auto-discover commands and their hierarchy, then produces*_cmd_gen.gofiles with//go:build safety_profilethat contain trimmed parent structs. The original struct definitions live in*_types.gofiles with//go:build !safety_profile. Implementation code is shared and unchanged.This means no manual updates are needed when upstream adds new commands. The fail-closed default ensures new commands are excluded until explicitly enabled.
Related issues
GOG_ENABLE_COMMANDSlacks sub-command granularity--safeflag for content sanitization (shows receptiveness to safety PRs)--gmail-scope=readonly(complementary but can't solve draft-without-send)Test plan
go build ./cmd/gog/produces identical binary (no safety_profile tag)go test ./...(all existing tests pass, 16 packages)go test ./cmd/gen-safety/(16 tests: 10 AST discovery + 6 fail-closed contract)go run ./cmd/gen-safety safety-profile.example.yamlgenerates correct files with build summarygo build -tags safety_profile ./cmd/gog/compiles successfully--helpomits disabled commandsgmail: falsedisables entire service (empty struct, builds clean)keep: falsecompiles correctly (non-cmd fields preserved in empty struct)send: "false"cause fatal error (not silent enable)gogcli-safe/notgogcli/)--strict(0 warnings):go run ./cmd/gen-safety --strict safety-profiles/full.yaml && go build -tags safety_profile ./cmd/gog/go run ./cmd/gen-safety --strict safety-profiles/readonly.yaml && go build -tags safety_profile ./cmd/gog/go run ./cmd/gen-safety --strict safety-profiles/agent-safe.yaml && go build -tags safety_profile ./cmd/gog/