feat: compile-time safety profiles for command removal#366
feat: compile-time safety profiles for command removal#366drewburchfield wants to merge 4 commits intosteipete:mainfrom
Conversation
0e2a88c to
26d21d4
Compare
3843d83 to
03ed5f0
Compare
|
Hey! We've been using this branch in production — built a safety profile skill around it that makes it easy to pick a level (L1 draft-only, L2 collaborate, L3 full write) and build/deploy a profiled binary. Here's the skill: https://clawhub.com/skills/gog-safety It's working great as-is, but it would be awesome to get this merged into main so we're not building off a branch. Happy to help with any cleanup needed to get it across the line. |
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. refactor(safety): replace manual registry with AST auto-discovery 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. fix(safety): address review findings from quality gate - 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 feat(safety): isolate config directory for safety profile builds 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.
fix(safety): enforce --strict in build-safe.sh Ensures YAML typos in safety profiles cause build failures instead of silent warnings. Without --strict, a mistyped key like 'gmal' instead of 'gmail' would silently exclude the service (fail-closed is safe but violates user intent). All three preset profiles already pass --strict with zero warnings.
…profiles New upstream commands: - gmail: archive, mark-read, unread, trash (convenience organize commands) - sheets: links/hyperlinks (read cell hyperlinks) Profile decisions: - full: all enabled - readonly: mark-read/unread enabled, archive/trash disabled - agent-safe: archive/mark-read/unread enabled, trash disabled - example: archive/mark-read/unread enabled, trash disabled fix(safety): readonly profile - mark-read and unread are write ops Both commands modify message state via Gmail API (add/remove UNREAD label). Setting them true violated the readonly profile's stated contract of no writes. All four gmail organize commands now correctly disabled: archive, mark-read, unread, trash all false in readonly. fix(safety): harden gen-safety and build-safe.sh robustness - build-safe.sh: anchor to repo root via cd $(dirname $0) so relative paths work correctly when script is invoked from outside repo root - discover.go: buildSpecsForStruct now fatal() instead of warn() when a struct is not found; warn() was semantically wrong since missing struct guarantees a compile failure anyway - better to fail loud early - main.go: fatal on empty/null YAML profile instead of silently disabling all services with no warnings (empty profile now gives clear actionable error message) - main.go: generateCLIFile now skips services where all leaf commands are false, matching the stated comment and preventing ghost empty top-level commands in the CLI struct
- Remove cmd/extract-types/ (one-time migration artifact, not ongoing tooling) - Add docs/safety-profiles.md explaining the *_types.go convention, YAML format, build process, and contributor workflow - Unify utility-command exception lists: buildKnownKeys now derives tolerated YAML keys from utilityTypes via buildCLIFields, removing the manually maintained config/time hardcoded entries - Add TestEndToEndSafeBuild: generates from full.yaml with --strict then compiles with -tags safety_profile - Add TestValidateYAMLKeys, TestBuildEmptyStruct, TestBuildEmptyStructWithNonCmdPrefix
a719a7d to
6c25871
Compare
|
Thanks @BrennerSpear! Great to hear you're running this in production. Custom profiles for different/custom use cases is exactly what this was designed for. I just force-pushed a cleaned up version (4 squashed commits, rebased on current main). @salmonumbrella @steipete @visionik happy to address any feedback whenever you get a chance to look. |
Rebrand header as gogcli-safe, add "Why this fork?" section explaining compile-time safety profiles for AI agent use. Links to upstream PR steipete#366.
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 binarydocs/safety-profiles.mdWhy 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)Commits
feat: compile-time safety profiles with AST-based generator-- Core feature: types file split, generator, build script, profiles, docsfeat(safety): add sedmat and contacts.other to safety profiles-- Sync with upstream (sedmat, contacts.other commands)feat(safety): add gmail organize commands and sheets links to safety profiles-- Sync with upstream (gmail archive/mark-read/unread/trash, sheets links)chore(safety): remove migration tool, add docs and tests-- Remove one-time migration artifact, add contributor docs, add end-to-end build tests, harden test isolationTest plan
go build ./cmd/gog/produces identical binary (no safety_profile tag)go vet ./...cleango test ./...(all existing tests pass)go test ./cmd/gen-safety/...(20 tests pass)full.yamlandreadonly.yaml--helpomits disabled commands--strictmode makes warnings fatalgogcli-safe/notgogcli/)--strict:./build-safe.sh safety-profiles/full.yaml./build-safe.sh safety-profiles/readonly.yaml./build-safe.sh safety-profiles/agent-safe.yaml