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

SymbolKindLineRole
loggervar9Global *slog.Logger, initialized in main()
main()func11Entry: parse --debug, loadConfigs(), init logger, switch dispatch
printExportEnv()func78Placeholder (not implemented)

Dispatch table — the complete switch in main() (lines 39-75):

os.Args[1]CallsDefined in
(no args)smartEntry()lifecycle.go:68
initinitProject()init.go:15
upup()lifecycle.go:15
downdown()lifecycle.go:28
ssh, enterenter()smartEntry()lifecycle.go:35
unlockunlock()vault_cmd.go:14
locklock()vault_cmd.go:29
savesave()vault_cmd.go:41
sync, pullpull()sync.go:65
pushpush()sync.go:96
loginlogin()vault_cmd.go:46
updateupdateImage()sync.go:81
vpnvpnCommand()vpn.go:9
setup-storagesetupStorage()lifecycle.go:150
__internal_envprintExportEnv()main.go:78
__internal_sync_daemonsyncDaemon()sync.go:17
--version, -vPrints Version
anything elselogger.Error("Unknown command") + os.Exit(1)main.go:73

lifecycle.go (163 lines) — Container & Smart Entry

SymbolKindLineRoleCalls
up()func15Validate image, ensure container, spawn sync daemonensureContainerUp()
down()func28docker stop + docker rm on container
enter()func35Delegates to smartEntry()smartEntry()
askYN()func39Interactive [y/N] prompt
enterShell()func47docker exec -it -w /workspace <container> /bin/bash
execInContainer()func59Runs a bash command inside the container via docker exec
smartEntry()func68Full guided flow (see call chain below)askYN, initProject, loadConfigs, ensureContainerUp, execInContainer, enterShell
ensureContainerUp()func116Check/create/start container (see call chain below)
setupStorage()func150Create S3 bucketutils.NewS3Client

vault_cmd.go (99 lines) — Vault CLI Glue

SymbolKindLineRoleCalls
unlock()func14Call vault.Unlock(), then bind-mount AWS enclavevault.Unlock, execCommand
lock()func29Unmount AWS bridge, call vault.Lock()utils.IsMounted, execCommand, vault.Lock
save()func41Call vault.Save("")vault.Save
login()func46aws sso login --profile ... via execCommandexecCommand
loadConfigs()func56Read + unmarshal .tazpod/config.yaml into cfgos.ReadFile, yaml.Unmarshal
loadVaultAWSCredentials()func72Load AWS keys from vault into env varsutils.IsMounted, os.ReadFile

sync.go (155 lines) — S3 Sync & Daemon

SymbolKindLineRoleCalls
syncDaemon()func17Background 5-min save+push loop with SIGTERM handlingisVaultUnlocked, vault.Save, pushVaultInternal
isVaultUnlocked()func57Check mount + passcache existenceutils.IsMounted, os.Stat
pull()func65Dispatch subarg: vaultpullVault(), imageupdateImage()
updateImage()func81docker pull <cfg.Image>execCommand
push()func96Dispatch subarg: vaultpushVault()
pullVault()func110Download vault.tar.aes from S3loadVaultAWSCredentials, utils.NewS3Client, s3.DownloadFile
pushVault()func130Upload vault.tar.aes to S3 (with timing)pushVaultInternal
pushVaultInternal()func140Core S3 upload logic (returns error)loadVaultAWSCredentials, utils.NewS3Client, s3.UploadFile

config.go (45 lines) — Structs & Helpers

SymbolKindLineRole
ConfigPathvar10.tazpod/config.yaml (relative)
Versionvar11"dev" — overridden at build time via -ldflags "-X main.Version=..."
Configstruct14Image, ContainerName, User, GhostMode, Features, AwsSso, Providers
Featuresstruct24Debug flag
AwsSsoConfigstruct28Profile, Bucket, Region
ProviderConfigstruct34DBHost
cfgvar38Global config instance
execCommand()func41Helper: creates exec.Cmd with stdio attached

init.go (88 lines) — Project Init

SymbolKindLineRoleCalls
initProject()func15Create .tazpod/ + .tazpod/vault/, prompt config, write YAMLpromptInitConfig, yaml.Marshal, os.WriteFile
promptInitConfig()func52Interactive prompts for DB hosts, S3 bucket, region, AWS profile

vpn.go (49 lines) — VPN (Legacy)

SymbolKindLineRole
vpnCommand()func9Dispatch up/down subcommand
vpnUp()func27sudo wg-quick up wg0
vpnDown()func40sudo wg-quick down wg0

Note: VPN code is untested and not validated. Planned migration to Tailscale.

help.go (27 lines) — Help Output

SymbolKindLineRole
help()func7Prints usage (not wired into main dispatch)

internal/ Package Tree

internal/crypto/crypto.go (94 lines)

SymbolKindRole
SaltSizeconst (32)PBKDF2 salt
KeySizeconst (32)AES-256 key
NonceSizeconst (12)GCM nonce
Iterationsconst (100000)PBKDF2 iterations
Encrypt(data, passphrase)funcGenerate salt → derive key → AES-GCM seal → [salt|nonce|ciphertext]
Decrypt(data, passphrase)funcExtract salt+nonce → re-derive key → AES-GCM open

Wire format: [salt 32B] | [nonce 12B] | [ciphertext + GCM tag]

internal/vault/vault.go (294 lines)

SymbolKindRole
VaultDirconst/workspace/.tazpod/vault
VaultFileconst/workspace/.tazpod/vault/vault.tar.aes
MountPathconst/home/tazpod/secrets (tmpfs)
AwsLocalHomeconst/home/tazpod/.aws
AwsVaultDirconst/home/tazpod/secrets/.aws
PassCacheconst/home/tazpod/secrets/.vault_pass
cachedPassphrasevarIn-memory passphrase cache
Unlock()funcMount tmpfs → decrypt vault.tar.aes → untar → SetupIdentity → bind AWS
Lock()funcUnmount AWS → unmount tmpfs
Save(passphrase)funcTar secrets → encrypt → write vault.tar.aes
SetupIdentity()funcCreate AI tool config dirs under /workspace/.tazpod/
TarDir(src)funcTar+gzip a directory into []byte
Untar(data, dest)funcUngzip+untar []byte into directory
getPassphrase()funcSecure terminal read (existing vault: prompt; new: prompt+confirm)
mountRAM()funcsudo mount -t tmpfs -o size=64M,mode=0700,uid=1000,gid=1000 tmpfs /home/tazpod/secrets
unmountRAM()funcsudo umount -l /home/tazpod/secrets
setupBindAuth()funcBind-mount ~/secrets/.aws~/.aws
bridge()funcGeneric bind-mount with verification
loadCachedPass()funcLoad passphrase from PassCache

internal/utils/utils.go (67 lines)

SymbolKindRole
RunCmd(name, args...)funcExecute command with stdio passthrough
RunOutput(name, args...)funcExecute command, return trimmed stdout
RunWithStdin(input, name, args...)funcExecute command with stdin from string
FileExist(path)funcos.Stat → bool
IsMounted(path)funcParse mount output for path
WaitForDevice(path)funcPoll for device node (up to 4s)
CheckInside()funcCheck for /.dockerenv (container detection)

internal/utils/s3.go (112 lines)

SymbolKindRole
DefaultBucketconst"tazlab-storage"
DefaultRegionconst"eu-central-1"
S3Clientstructclient *s3.Client, bucket string
NewS3Client(bucket, region, profile)funcAWS SDK config with SSO profile support
UploadFile(key, filePath)methods3.PutObject
DownloadFile(key, filePath)methods3.GetObject → write to disk
CreateBucket()methods3.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 savetazpod 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/

FileImports
main.goNo internal imports (stdlib only)
lifecycle.gotazpod/internal/utils, tazpod/internal/vault
vault_cmd.gotazpod/internal/utils, tazpod/internal/vault, gopkg.in/yaml.v3
sync.gotazpod/internal/utils, tazpod/internal/vault
config.goNo internal imports
init.gotazpod/internal/utils, gopkg.in/yaml.v3
vpn.goNo internal imports
help.goNo 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)

  1. No Cobra — dispatch is a switch on os.Args[1] in main.go:39. Keeps the binary light and the call chain obvious.

  2. 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.

  3. Host-network container--network host means the container shares the host’s network namespace. Tailscale runs natively in the container.

  4. 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.

  5. sleep infinity pattern — the container entrypoint is sleep infinity (lifecycle.go:141), not a shell. Interaction happens via docker exec.

  6. Sync daemon spawned by upexec.Command("tazpod", "__internal_sync_daemon").Start() (lifecycle.go:24). The daemon runs as a background process, not inside the container.

  7. Container-dispatch for vault opsexecInContainer() (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.

  8. Cwd-sensitive pushpushVaultInternal() (sync.go:140) resolves vault.tar.aes from os.Getwd(), not from a canonical path. This is TD-022.

  9. config.go loaded before every runloadConfigs() (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).

  10. Build-only versionVersion = "dev" in config.go:11, overridden at link time: go build -ldflags "-X main.Version=$(cat VERSION)".

Known Issues / Technical Debt

TDWhereWhat
TD-017lifecycle.go:86containerUnlocked check via docker exec mountpoint unreliable — can incorrectly prompt unlock when vault is already open
TD-018lifecycle.go:136Default bridge MTU not configurable — --dns 1.1.1.1 but no --mtu flag
TD-022sync.go:142pushVaultInternal() resolves vault.tar.aes from os.Getwd(), not a canonical path
vpn.goWhole VPN feature is untested and not validated
main.go:78printExportEnv() is a stub — not implemented

See Also