TazPod: Code Structure Detail#
Level 3 (Detail) — Package tree, file-by-file function inventory, call chains, and cross-file dependencies.
Package Tree#
tazpod/
├── cmd/tazpod/ # Flat main package (no subcommands)
│ ├── main.go # Entry point, dispatch, logger init
│ ├── lifecycle.go # Container lifecycle + smart entry
│ ├── vault_cmd.go # CLI glue for vault ops (unlock/lock/save/login)
│ ├── sync.go # S3 pull/push + background sync daemon
│ ├── config.go # Config struct, globals, exec helper
│ ├── init.go # Project init + interactive config prompt
│ ├── vpn.go # VPN command (WireGuard, untested/legacy)
│ └── help.go # Help text
├── internal/
│ ├── crypto/crypto.go # AES-256-GCM encrypt/decrypt
│ ├── vault/vault.go # Vault lifecycle: unlock, lock, save, mounts
│ └── utils/
│ ├── utils.go # OS/exec helpers: RunCmd, IsMounted, FileExist
│ └── s3.go # S3 client: UploadFile, DownloadFile, CreateBucket
├── go.mod # Module: tazpod, Go 1.24
└── VERSION # Version string injected via ldflags
All cmd/tazpod/*.go files share the same package main. No Cobra — dispatch is a plain switch in main.go.
cmd/tazpod/ File-by-File Inventory#
main.go (80 lines) — Entry Point & Dispatch#
| Symbol | Kind | Line | Role |
|---|
logger | var | 9 | Global *slog.Logger, initialized in main() |
main() | func | 11 | Entry: parse --debug, loadConfigs(), init logger, switch dispatch |
printExportEnv() | func | 78 | Placeholder (not implemented) |
Dispatch table — the complete switch in main() (lines 39-75):
os.Args[1] | Calls | Defined in |
|---|
| (no args) | smartEntry() | lifecycle.go:68 |
init | initProject() | init.go:15 |
up | up() | lifecycle.go:15 |
down | down() | lifecycle.go:28 |
ssh, enter | enter() → smartEntry() | lifecycle.go:35 |
unlock | unlock() | vault_cmd.go:14 |
lock | lock() | vault_cmd.go:29 |
save | save() | vault_cmd.go:41 |
sync, pull | pull() | sync.go:65 |
push | push() | sync.go:96 |
login | login() | vault_cmd.go:46 |
update | updateImage() | sync.go:81 |
vpn | vpnCommand() | vpn.go:9 |
setup-storage | setupStorage() | lifecycle.go:150 |
__internal_env | printExportEnv() | main.go:78 |
__internal_sync_daemon | syncDaemon() | sync.go:17 |
--version, -v | Prints Version | — |
| anything else | logger.Error("Unknown command") + os.Exit(1) | main.go:73 |
lifecycle.go (163 lines) — Container & Smart Entry#
| Symbol | Kind | Line | Role | Calls |
|---|
up() | func | 15 | Validate image, ensure container, spawn sync daemon | ensureContainerUp() |
down() | func | 28 | docker stop + docker rm on container | — |
enter() | func | 35 | Delegates to smartEntry() | smartEntry() |
askYN() | func | 39 | Interactive [y/N] prompt | — |
enterShell() | func | 47 | docker exec -it -w /workspace <container> /bin/bash | — |
execInContainer() | func | 59 | Runs a bash command inside the container via docker exec | — |
smartEntry() | func | 68 | Full guided flow (see call chain below) | askYN, initProject, loadConfigs, ensureContainerUp, execInContainer, enterShell |
ensureContainerUp() | func | 116 | Check/create/start container (see call chain below) | — |
setupStorage() | func | 150 | Create S3 bucket | utils.NewS3Client |
vault_cmd.go (99 lines) — Vault CLI Glue#
| Symbol | Kind | Line | Role | Calls |
|---|
unlock() | func | 14 | Call vault.Unlock(), then bind-mount AWS enclave | vault.Unlock, execCommand |
lock() | func | 29 | Unmount AWS bridge, call vault.Lock() | utils.IsMounted, execCommand, vault.Lock |
save() | func | 41 | Call vault.Save("") | vault.Save |
login() | func | 46 | aws sso login --profile ... via execCommand | execCommand |
loadConfigs() | func | 56 | Read + unmarshal .tazpod/config.yaml into cfg | os.ReadFile, yaml.Unmarshal |
loadVaultAWSCredentials() | func | 72 | Load AWS keys from vault into env vars | utils.IsMounted, os.ReadFile |
sync.go (155 lines) — S3 Sync & Daemon#
| Symbol | Kind | Line | Role | Calls |
|---|
syncDaemon() | func | 17 | Background 5-min save+push loop with SIGTERM handling | isVaultUnlocked, vault.Save, pushVaultInternal |
isVaultUnlocked() | func | 57 | Check mount + passcache existence | utils.IsMounted, os.Stat |
pull() | func | 65 | Dispatch subarg: vault → pullVault(), image → updateImage() | — |
updateImage() | func | 81 | docker pull <cfg.Image> | execCommand |
push() | func | 96 | Dispatch subarg: vault → pushVault() | — |
pullVault() | func | 110 | Download vault.tar.aes from S3 | loadVaultAWSCredentials, utils.NewS3Client, s3.DownloadFile |
pushVault() | func | 130 | Upload vault.tar.aes to S3 (with timing) | pushVaultInternal |
pushVaultInternal() | func | 140 | Core S3 upload logic (returns error) | loadVaultAWSCredentials, utils.NewS3Client, s3.UploadFile |
config.go (45 lines) — Structs & Helpers#
| Symbol | Kind | Line | Role |
|---|
ConfigPath | var | 10 | .tazpod/config.yaml (relative) |
Version | var | 11 | "dev" — overridden at build time via -ldflags "-X main.Version=..." |
Config | struct | 14 | Image, ContainerName, User, GhostMode, Features, AwsSso, Providers |
Features | struct | 24 | Debug flag |
AwsSsoConfig | struct | 28 | Profile, Bucket, Region |
ProviderConfig | struct | 34 | DBHost |
cfg | var | 38 | Global config instance |
execCommand() | func | 41 | Helper: creates exec.Cmd with stdio attached |
init.go (88 lines) — Project Init#
| Symbol | Kind | Line | Role | Calls |
|---|
initProject() | func | 15 | Create .tazpod/ + .tazpod/vault/, prompt config, write YAML | promptInitConfig, yaml.Marshal, os.WriteFile |
promptInitConfig() | func | 52 | Interactive prompts for DB hosts, S3 bucket, region, AWS profile | — |
vpn.go (49 lines) — VPN (Legacy)#
| Symbol | Kind | Line | Role |
|---|
vpnCommand() | func | 9 | Dispatch up/down subcommand |
vpnUp() | func | 27 | sudo wg-quick up wg0 |
vpnDown() | func | 40 | sudo wg-quick down wg0 |
Note: VPN code is untested and not validated. Planned migration to Tailscale.
help.go (27 lines) — Help Output#
| Symbol | Kind | Line | Role |
|---|
help() | func | 7 | Prints usage (not wired into main dispatch) |
internal/ Package Tree#
internal/crypto/crypto.go (94 lines)#
| Symbol | Kind | Role |
|---|
SaltSize | const (32) | PBKDF2 salt |
KeySize | const (32) | AES-256 key |
NonceSize | const (12) | GCM nonce |
Iterations | const (100000) | PBKDF2 iterations |
Encrypt(data, passphrase) | func | Generate salt → derive key → AES-GCM seal → [salt|nonce|ciphertext] |
Decrypt(data, passphrase) | func | Extract salt+nonce → re-derive key → AES-GCM open |
Wire format: [salt 32B] | [nonce 12B] | [ciphertext + GCM tag]
internal/vault/vault.go (294 lines)#
| Symbol | Kind | Role |
|---|
VaultDir | const | /workspace/.tazpod/vault |
VaultFile | const | /workspace/.tazpod/vault/vault.tar.aes |
MountPath | const | /home/tazpod/secrets (tmpfs) |
AwsLocalHome | const | /home/tazpod/.aws |
AwsVaultDir | const | /home/tazpod/secrets/.aws |
PassCache | const | /home/tazpod/secrets/.vault_pass |
cachedPassphrase | var | In-memory passphrase cache |
Unlock() | func | Mount tmpfs → decrypt vault.tar.aes → untar → SetupIdentity → bind AWS |
Lock() | func | Unmount AWS → unmount tmpfs |
Save(passphrase) | func | Tar secrets → encrypt → write vault.tar.aes |
SetupIdentity() | func | Create AI tool config dirs under /workspace/.tazpod/ |
TarDir(src) | func | Tar+gzip a directory into []byte |
Untar(data, dest) | func | Ungzip+untar []byte into directory |
getPassphrase() | func | Secure terminal read (existing vault: prompt; new: prompt+confirm) |
mountRAM() | func | sudo mount -t tmpfs -o size=64M,mode=0700,uid=1000,gid=1000 tmpfs /home/tazpod/secrets |
unmountRAM() | func | sudo umount -l /home/tazpod/secrets |
setupBindAuth() | func | Bind-mount ~/secrets/.aws → ~/.aws |
bridge() | func | Generic bind-mount with verification |
loadCachedPass() | func | Load passphrase from PassCache |
internal/utils/utils.go (67 lines)#
| Symbol | Kind | Role |
|---|
RunCmd(name, args...) | func | Execute command with stdio passthrough |
RunOutput(name, args...) | func | Execute command, return trimmed stdout |
RunWithStdin(input, name, args...) | func | Execute command with stdin from string |
FileExist(path) | func | os.Stat → bool |
IsMounted(path) | func | Parse mount output for path |
WaitForDevice(path) | func | Poll for device node (up to 4s) |
CheckInside() | func | Check for /.dockerenv (container detection) |
internal/utils/s3.go (112 lines)#
| Symbol | Kind | Role |
|---|
DefaultBucket | const | "tazlab-storage" |
DefaultRegion | const | "eu-central-1" |
S3Client | struct | client *s3.Client, bucket string |
NewS3Client(bucket, region, profile) | func | AWS SDK config with SSO profile support |
UploadFile(key, filePath) | method | s3.PutObject |
DownloadFile(key, filePath) | method | s3.GetObject → write to disk |
CreateBucket() | method | s3.CreateBucket with eu-central-1 constraint |
Key Call Chains#
tazpod up — Complete Startup Flow#
main()
├─ loadConfigs() # vault_cmd.go:56
│ └─ yaml.Unmarshal → cfg # config.go:38
├─ logger init
├─ switch "up" → up() # lifecycle.go:15
│ ├─ validate cfg.Image
│ ├─ ensureContainerUp() # lifecycle.go:116
│ │ ├─ docker ps -a --filter (check exists)
│ │ ├─ docker start (if stopped)
│ │ └─ docker run -d --name <name> \ # create if missing
│ │ --network host --cap-add SYS_ADMIN \
│ │ --cap-add NET_ADMIN --device /dev/net/tun \
│ │ --security-opt apparmor=unconfined \
│ │ --dns 1.1.1.1 --dns 1.0.0.1 \
│ │ -v <cwd>:/workspace \
│ │ -v ~/.ssh:/home/tazpod/.ssh:ro \
│ │ -e HOST_CWD=<cwd> \
│ │ <image> sleep infinity
│ └─ exec.Command("tazpod", "__internal_sync_daemon").Start()
tazpod (no args) — Smart Entry Flow#
main()
├─ loadConfigs()
├─ logger init
├─ os.Args < 2 → smartEntry() # lifecycle.go:68
│ ├─ .tazpod/ missing? → askYN → initProject() → loadConfigs()
│ ├─ validate cfg.ContainerName
│ ├─ ensureContainerUp()
│ ├─ containerUnlocked? (docker exec mountpoint -q ~/secrets)
│ │ ├─ YES → enterShell() # lifecycle.go:47
│ │ │ └─ docker exec -it -w /workspace <container> /bin/bash
│ │ └─ NO → check local vault file
│ │ ├─ vault.tar.aes exists?
│ │ │ ├─ YES → askYN → execInContainer("tazpod unlock")
│ │ │ │ └─ vault.Unlock() # vault.go:36
│ │ │ │ ├─ mountRAM() # 64M tmpfs
│ │ │ │ ├─ crypto.Decrypt()
│ │ │ │ ├─ Untar()
│ │ │ │ ├─ SetupIdentity()
│ │ │ │ └─ setupBindAuth() # AWS bridge
│ │ │ └─ enterShell()
│ │ └─ vault.tar.aes missing?
│ │ └─ askYN("Bootstrap?") → login → pull vault → unlock
│ │ └─ enterShell()
tazpod unlock — Vault Unlock (Direct)#
main() → switch "unlock" → unlock() # vault_cmd.go:14
├─ vault.Unlock() # vault.go:36
│ ├─ IsMounted? → cache hit, return early
│ ├─ mountRAM() # 64M tmpfs at ~/secrets
│ ├─ FileExist(vault.tar.aes)?
│ │ ├─ YES → read → crypto.Decrypt() → Untar()
│ │ └─ NO → prompt new passphrase
│ ├─ write PassCache (plaintext, mode 0600, in tmpfs)
│ ├─ SetupIdentity() # AI config dirs
│ └─ setupBindAuth() # bind ~/secrets/.aws → ~/.aws
├─ os.MkdirAll(~/.aws)
└─ sudo mount --bind ~/secrets/.aws ~/.aws
tazpod save → tazpod push vault#
main() → switch "save" → save() # vault_cmd.go:41
└─ vault.Save("") # vault.go:101
├─ IsMounted? (no → warn, return)
├─ loadCachedPass()
├─ TarDir(MountPath) # tar+gzip ~/secrets/
├─ crypto.Encrypt(rawBytes, passphrase)
└─ os.WriteFile(vault.tar.aes)
main() → switch "push" → push() # sync.go:96
└─ pushVault() # sync.go:130
└─ pushVaultInternal() # sync.go:140
├─ loadVaultAWSCredentials()
├─ utils.NewS3Client()
└─ s3.UploadFile("tazpod/vault/vault.tar.aes", <local>)
Cross-File Dependencies#
cmd/tazpod/ → internal/#
| File | Imports |
|---|
main.go | No internal imports (stdlib only) |
lifecycle.go | tazpod/internal/utils, tazpod/internal/vault |
vault_cmd.go | tazpod/internal/utils, tazpod/internal/vault, gopkg.in/yaml.v3 |
sync.go | tazpod/internal/utils, tazpod/internal/vault |
config.go | No internal imports |
init.go | tazpod/internal/utils, gopkg.in/yaml.v3 |
vpn.go | No internal imports |
help.go | No internal imports |
internal/ dependencies#
internal/vault → internal/crypto
→ internal/utils
→ golang.org/x/term
internal/crypto → golang.org/x/crypto/pbkdf2
internal/utils → github.com/aws/aws-sdk-go-v2/* (s3.go only)
Key Architectural Decisions (Code-Encoded)#
No Cobra — dispatch is a switch on os.Args[1] in main.go:39. Keeps the binary light and the call chain obvious.
Container runs as root — Docker flags --cap-add SYS_ADMIN, --network host, --security-opt apparmor=unconfined in ensureContainerUp() (lifecycle.go:130-134). This is required for tmpfs mounts and Tailscale TUN inside the container.
Host-network container — --network host means the container shares the host’s network namespace. Tailscale runs natively in the container.
Workspace mount — -v <cwd>:/workspace (lifecycle.go:138). The container sees the host project directory at /workspace. Persistence is in the mounted directory, not the container layer.
sleep infinity pattern — the container entrypoint is sleep infinity (lifecycle.go:141), not a shell. Interaction happens via docker exec.
Sync daemon spawned by up — exec.Command("tazpod", "__internal_sync_daemon").Start() (lifecycle.go:24). The daemon runs as a background process, not inside the container.
Container-dispatch for vault ops — execInContainer() (lifecycle.go:59) runs tazpod unlock etc. inside the container, not on the host. The passphrase prompt must be serviced inside the container where the tmpfs is mounted.
Cwd-sensitive push — pushVaultInternal() (sync.go:140) resolves vault.tar.aes from os.Getwd(), not from a canonical path. This is TD-022.
config.go loaded before every run — loadConfigs() (vault_cmd.go:56) is called unconditionally at the top of main(). If .tazpod/config.yaml doesn’t exist, it silently returns (normal on first run).
Build-only version — Version = "dev" in config.go:11, overridden at link time: go build -ldflags "-X main.Version=$(cat VERSION)".
Known Issues / Technical Debt#
| TD | Where | What |
|---|
| TD-017 | lifecycle.go:86 | containerUnlocked check via docker exec mountpoint unreliable — can incorrectly prompt unlock when vault is already open |
| TD-018 | lifecycle.go:136 | Default bridge MTU not configurable — --dns 1.1.1.1 but no --mtu flag |
| TD-022 | sync.go:142 | pushVaultInternal() resolves vault.tar.aes from os.Getwd(), not a canonical path |
| — | vpn.go | Whole VPN feature is untested and not validated |
| — | main.go:78 | printExportEnv() is a stub — not implemented |
See Also#