A sandboxed Google Workspace CLI that registers a gog command into a
SwiftBash Shell, so a local LLM (or any
shell automation) can read and write Google Drive, Gmail, Calendar, Contacts,
Tasks, Docs, Sheets, Slides, Chat, Forms, YouTube, and Admin Directory — all
confined by SwiftBash's MountedFileSystem and allow-listed network layer.
gog drive ls --max 20 --json | jq '.files[].name'
gog gmail messages -q 'newer_than:7d' --json
gog calendar freebusy --jsongog behaves like any other sandbox command: structured data to stdout,
hints/progress/errors to stderr, composable through pipes.
- The host owns auth.
gogperforms no OAuth — no browser flow, no token endpoint, no Keychain. The host injects a Google access token per run via aGogCredentialProvider; on a401,gogasks the host to refresh once, then fails closed (exit 7) for the host to handle. - Credentials stay out-of-band. The token is never placed in the shell
environment, the command argv, or the mounted filesystem — it is only attached
as an
Authorizationheader by the HTTP layer.printenv/echo $TOKENcannot surface it. - Files stay in the sandbox. All I/O goes through
Shell.fileSystem(MountedFileSystem); paths outside the mounts are rejected, so download and upload targets must be sandbox paths. - Network is allow-listed. Only the Google API hosts you configure are
reachable; with no
networkConfig, networked commands fail closed (exit 7). - Sends and directory writes are gated.
gmail send,chat send, and theadmindirectory writes consult a hostGogPolicy(and support--dry-run). Other write commands (e.g.drive upload,calendar create,sheets update,tasks add) are not policy-gated — restrict those via the token's scopes and the network allow-list.
See PLAN.md for the full architecture and decisions, and
.agents/skills/gog/SKILL.md for the
agent-facing command reference.
SwiftGog depends on SwiftBash. Today the manifest pins SwiftBash as a sibling
checkout (Package.swift declares .package(path: "../SwiftBash")), so clone
both repos side by side and depend on SwiftGog by path:
git clone https://github.com/picomlx/SwiftBash.git
git clone https://github.com/picomlx/SwiftGog.git # sits next to ../SwiftBash// your app's Package.swift
dependencies: [
.package(path: "../SwiftGog"),
],(For remote/URL consumption, switch SwiftGog's own SwiftBash dependency from the
path: pin to a url: pin first.)
The package vends three libraries:
| Library | What it provides |
|---|---|
GogCore |
the host seams: GogCredentialProvider, GogPolicy, GogTransport |
GogCommands |
the gog command tree (ArgumentParser commands) |
GogShell |
Shell.registerGogCommands() — one call installs the whole tree |
Build a sandboxed Shell, allow-list the Google API hosts, register gog, then
run commands with a credential provider (and optional policy) bound around the
run:
import BashInterpreter // Shell, NetworkConfig, AllowedURLEntry, MountedFileSystem
import GogCore // GogCredentials, GogPolicy, GogPolicies, GogCredentialProvider
import GogShell // registerGogCommands()
// 1. The host's token source (no OAuth lives in gog).
struct MyProvider: GogCredentialProvider {
let account: String
func accessToken() async throws -> String { try await myTokenStore.token(for: account) }
func refreshedAccessToken() async throws -> String { try await myTokenStore.refresh(for: account) }
var accountHint: String? { account }
}
// 2. A sandboxed shell: a mounted workspace + an allow-list of Google API hosts.
let shell = Shell(fileSystem: myMountedFileSystem) // e.g. mounts "/gog"
shell.networkConfig = NetworkConfig(
allowedURLPrefixes: [
// The full tree needs every service host it can reach; trim to match
// the commands you actually register/allow (see PLAN.md).
AllowedURLEntry("https://www.googleapis.com/"), // Drive, Calendar
AllowedURLEntry("https://gmail.googleapis.com/"), // Gmail
AllowedURLEntry("https://people.googleapis.com/"), // identity + contacts
AllowedURLEntry("https://tasks.googleapis.com/"), // Tasks
AllowedURLEntry("https://sheets.googleapis.com/"), // Sheets (Docs/Slides export via www)
AllowedURLEntry("https://chat.googleapis.com/"), // Chat
AllowedURLEntry("https://forms.googleapis.com/"), // Forms
AllowedURLEntry("https://youtube.googleapis.com/"), // YouTube
AllowedURLEntry("https://admin.googleapis.com/"), // Admin Directory + Reports
],
allowedMethods: [.GET, .POST, .PATCH, .PUT, .DELETE])
// 3. Install the gog command tree (off-catalog, at /usr/local/bin/gog).
shell.registerGogCommands()
// 4. Run, with the provider (and any policy) bound for this run only.
let run = try await GogCredentials.$current.withValue(MyProvider(account: "alice@corp.com")) {
try await GogPolicies.$current.withValue(GogPolicy(gmailSendDisabled: true)) {
try await shell.runCapturing("gog drive ls --json")
}
}
print(run.stdout) // structured JSON
assert(run.exitStatus == .success)Because the provider and policy are bound as task-local values around the run, LLM-authored bash inside the shell cannot read or change them — they are not command flags or environment variables.
Bind a different GogCredentialProvider per run (or per task) — there is no
global mutable auth state, so concurrent tenants don't interfere:
try await GogCredentials.$current.withValue(tenantA.provider) { … } // tenant A
try await GogCredentials.$current.withValue(tenantB.provider) { … } // tenant BGogPolicy (bound via GogPolicies.$current) lets the host disable specific
mutations. Sending is allowed by default but can be turned off; high-blast-radius
directory mutations are off by default and must be opted in. Every gated
write also supports --dry-run, which builds and prints the request without
calling Google.
// Disable outbound mail and chat for this run.
GogPolicy(gmailSendDisabled: true, chatSendDisabled: true)A blocked mutation fails closed with exit 3 before any network call.
registerGogCommands() installs the full command tree (selective
installation is not part of the public API today), so the remaining levers for
ungated writes are the token's scopes and the network allow-list.
Upstream gogcli guards destructive
directory operations (suspending a user, changing group membership) with an
interactive confirmation prompt plus a --force flag, and in non-interactive
use it refuses them unless --force is passed. SwiftGog runs LLM-authored bash
with no human at a terminal, so it replaces that with a host-bound policy:
directory writes are disabled by default (GogPolicy.adminWriteDisabled —
the one gate that defaults to off, unlike the send gates), and there is
intentionally no --force flag — a command-line escape hatch would let the
model escalate past the gate. The host, not the model, decides whether to
enable directory writes. The fail-closed intent matches gogcli's non-interactive
behaviour; the control simply moves from argv to host policy.
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | a Google API error (HTTP ≥ 400); the message is echoed to stderr |
| 2 | usage / validation error (bad flag, out-of-range --max, bad input) |
| 3 | refused by host policy (sending/admin writes disabled), or --fail-empty with no results |
| 7 | fail-closed: no network configured, or missing / rejected credentials |
| 23 | could not write the requested sandbox destination |
These are enforced and CI-guarded; a consumer can rely on them:
- No host filesystem or networking primitives.
GogCore/GogCommandsnever useFileManager,Data(contentsOf:), orURLSession— a CI lint-guard fails the build if they appear. All file I/O goes throughShell.fileSystem; all HTTPS goes through the allow-listed transport. - Token confinement. The injected token is only ever an
Authorizationheader. It is never written toShell.environment, argv, stdout/stderr, or the mounted FS. - Fail-closed by default. No network config ⇒ exit 7. No credentials ⇒ exit 7. Out-of-mount path ⇒ rejected.
GogTransport is an injectable seam: production uses SecureTransport (over
SwiftBash's allow-listed fetcher), and tests bind a fake via
GogTransportProvider.$current to return canned Google JSON — no real network:
// MockTransport / StubProvider are your own test doubles conforming to
// GogTransport / GogCredentialProvider.
let json = #"{"files":[]}"#
let transport = MockTransport(response: HTTPResponse(status: 200, body: Data(json.utf8)))
try await GogTransportProvider.$current.withValue(transport) {
try await GogCredentials.$current.withValue(StubProvider()) {
try await shell.runCapturing("gog drive ls --json")
}
}See Tests/GogShellTests/GogWiringTests.swift for the full pattern (fakes,
sandbox-deny tests, and per-command behaviour).
The command surface spans identity, Drive, Gmail, Calendar, Contacts, Tasks,
Docs, Sheets, Slides, Chat, Forms, YouTube, and Admin (Directory + Reports),
read-first with gated writes. See
.agents/skills/gog/SKILL.md for the current
command list and PLAN.md for the roadmap.