@crvouga/dev-fleet 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
- package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
- package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
- package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
- package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
- package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
- package/LICENSE +21 -0
- package/README.md +357 -2
- package/agents/antigravity-proxy.ts +3 -0
- package/agents/audit.md +89 -0
- package/agents/claude-proxy +5 -0
- package/agents/claude-proxy.ts +3 -0
- package/agents/codex-proxy.ts +3 -0
- package/agents/grok-proxy.ts +3 -0
- package/agents/managed-proxy.ts +55 -0
- package/agents/opencode-proxy.ts +3 -0
- package/agents/registry.json +209 -0
- package/agents/registry.schema.json +149 -0
- package/agents/setup.md +158 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +9 -0
- package/ansible/monitoring/README.md +215 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +172 -0
- package/ansible/monitoring/beszel-agent.service +42 -0
- package/ansible/monitoring/compose.yml +74 -0
- package/ansible/monitoring/hub-darwin.yml +102 -0
- package/ansible/monitoring/hub-route-darwin.yml +51 -0
- package/ansible/monitoring/lock.json +16 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/pyproject.toml +12 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-gc.py +336 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +52 -0
- package/ansible/ubuntu/recovery.yml +15 -0
- package/ansible/ubuntu/t3/policy.json +7 -0
- package/ansible/ubuntu/t3-desktop.yml +220 -0
- package/ansible/ubuntu/t3-health.sh +33 -0
- package/ansible/ubuntu/t3-policy.yml +76 -0
- package/ansible/ubuntu/t3-route.yml +28 -0
- package/ansible/ubuntu/tailscale.yml +78 -0
- package/ansible/ubuntu/tasks/credentials.yml +47 -0
- package/ansible/ubuntu/tasks/docker.yml +31 -0
- package/ansible/ubuntu/tasks/packages.yml +56 -0
- package/ansible/ubuntu/tasks/power.yml +51 -0
- package/ansible/ubuntu/tasks/recovery.yml +39 -0
- package/ansible/ubuntu/tasks/resources.yml +312 -0
- package/ansible/ubuntu/tasks/services.yml +31 -0
- package/ansible/ubuntu/tasks/ssh.yml +71 -0
- package/ansible/ubuntu/tasks/user.yml +120 -0
- package/ansible/uv.lock +190 -0
- package/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +80 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +234 -0
- package/bootstrap/fleet-manager-run.sh +103 -0
- package/bootstrap/fleet-remote-bootstrap.sh +258 -0
- package/bootstrap/global-config.ts +171 -0
- package/bootstrap/install-launcher.sh +10 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +358 -0
- package/bootstrap/release-version.ts +157 -0
- package/bootstrap/setup-status.ts +51 -0
- package/bootstrap/setup.ts +88 -0
- package/bootstrap/shell-path.ts +105 -0
- package/bootstrap/verify-t3-archive.py +49 -0
- package/bootstrap/worker-auth.ts +65 -0
- package/bootstrap/worker-github.ts +47 -0
- package/bootstrap/worker-tools.ts +24 -0
- package/cliproxy/AGENTS.md +11 -0
- package/cliproxy/README.md +269 -0
- package/cliproxy/clients/bootstrap.py +143 -0
- package/cliproxy/clients/launcher.py +57 -0
- package/cliproxy/compose.yaml +90 -0
- package/cliproxy/config.template.yaml +70 -0
- package/cliproxy/docs/cloud-adoption.md +16 -0
- package/cliproxy/docs/deployment.png +0 -0
- package/cliproxy/docs/deployment.svg +109 -0
- package/cliproxy/docs/health-workflow.example.yml +185 -0
- package/cliproxy/docs/migration.md +131 -0
- package/cliproxy/docs/model-health-report.schema.json +364 -0
- package/cliproxy/docs/model-health.md +393 -0
- package/cliproxy/image.lock.json +5 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
- package/cliproxy/models.json +1618 -0
- package/cliproxy/requirements.lock +78 -0
- package/cliproxy/scripts/accounts.py +38 -0
- package/cliproxy/scripts/audit.py +123 -0
- package/cliproxy/scripts/backup +234 -0
- package/cliproxy/scripts/check.py +34 -0
- package/cliproxy/scripts/checkpoint.py +226 -0
- package/cliproxy/scripts/cli.py +64 -0
- package/cliproxy/scripts/cliproxy_host.py +257 -0
- package/cliproxy/scripts/cloud_state.py +150 -0
- package/cliproxy/scripts/container-health +6 -0
- package/cliproxy/scripts/docker +24 -0
- package/cliproxy/scripts/external-health.py +79 -0
- package/cliproxy/scripts/fleet_config.py +43 -0
- package/cliproxy/scripts/fleet_sync.py +274 -0
- package/cliproxy/scripts/initialize +84 -0
- package/cliproxy/scripts/install-dependencies +48 -0
- package/cliproxy/scripts/install.py +406 -0
- package/cliproxy/scripts/model_health.py +16 -0
- package/cliproxy/scripts/modelhealth/__init__.py +5 -0
- package/cliproxy/scripts/modelhealth/checks.py +429 -0
- package/cliproxy/scripts/modelhealth/cli.py +117 -0
- package/cliproxy/scripts/modelhealth/config.py +186 -0
- package/cliproxy/scripts/modelhealth/emulator.py +536 -0
- package/cliproxy/scripts/modelhealth/expect.py +88 -0
- package/cliproxy/scripts/modelhealth/findings.py +183 -0
- package/cliproxy/scripts/modelhealth/protocol.py +330 -0
- package/cliproxy/scripts/modelhealth/redact.py +28 -0
- package/cliproxy/scripts/modelhealth/report.py +150 -0
- package/cliproxy/scripts/modelhealth/runner.py +390 -0
- package/cliproxy/scripts/modelhealth/scenario.py +131 -0
- package/cliproxy/scripts/modelhealth/schema.py +63 -0
- package/cliproxy/scripts/modelhealth/simulate.py +111 -0
- package/cliproxy/scripts/modelhealth/transport.py +213 -0
- package/cliproxy/scripts/monitor +68 -0
- package/cliproxy/scripts/ops +527 -0
- package/cliproxy/scripts/public-endpoint +127 -0
- package/cliproxy/scripts/recover_node.py +177 -0
- package/cliproxy/scripts/restore-verify +60 -0
- package/cliproxy/scripts/retention.py +144 -0
- package/cliproxy/scripts/service-start +7 -0
- package/cliproxy/scripts/validate-config +56 -0
- package/cliproxy/scripts/vault-access.py +57 -0
- package/cliproxy/scripts/vault-backup-login +55 -0
- package/cliproxy/scripts/vault-recovery +56 -0
- package/cliproxy/scripts/vault-renew +30 -0
- package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
- package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
- package/cliproxy/systemd/cliproxy-backup.service +13 -0
- package/cliproxy/systemd/cliproxy-backup.timer +8 -0
- package/cliproxy/systemd/cliproxy-fleet.service +17 -0
- package/cliproxy/systemd/cliproxy-gc.service +13 -0
- package/cliproxy/systemd/cliproxy-gc.timer +8 -0
- package/cliproxy/systemd/cliproxy-monitor.service +8 -0
- package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
- package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
- package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
- package/cliproxy/vendor/LICENSE +22 -0
- package/cliproxy/vendor/config.upstream.yaml +1349 -0
- package/docs/consumer-setup.md +408 -0
- package/docs/declared.md +89 -0
- package/docs/machine-setup.md +399 -0
- package/docs/operations.md +276 -0
- package/docs/releasing.md +103 -0
- package/docs/state.md +254 -0
- package/docs/topology.md +67 -0
- package/docs/ubuntu-sync.md +351 -0
- package/docs/worker-contract.md +67 -0
- package/examples/fleet.json +100 -0
- package/examples/roster.json +13 -0
- package/llms.txt +35 -0
- package/mise.lock +1521 -0
- package/mise.toml +110 -0
- package/package.json +74 -4
- package/schema/fleet.schema.json +420 -0
- package/schema/roster.schema.json +77 -0
- package/src/agents/account-relay.ts +111 -0
- package/src/agents/accounts.ts +85 -0
- package/src/agents/audit.ts +77 -0
- package/src/agents/cli.ts +13 -0
- package/src/agents/command.ts +204 -0
- package/src/agents/converge.ts +123 -0
- package/src/agents/doctor.ts +107 -0
- package/src/agents/errors.ts +2 -0
- package/src/agents/files.ts +73 -0
- package/src/agents/fleet-verify.ts +165 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +371 -0
- package/src/agents/provider-errors.ts +57 -0
- package/src/agents/registry.ts +61 -0
- package/src/agents/render.ts +110 -0
- package/src/agents/round-trip.ts +192 -0
- package/src/agents/secrets.ts +13 -0
- package/src/agents/toolchain.ts +34 -0
- package/src/agents/verify-cache.ts +103 -0
- package/src/agents/verify.ts +127 -0
- package/src/auth-checks.ts +163 -0
- package/src/cli/agent-guide.ts +129 -0
- package/src/cli/app.ts +122 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/command-index.ts +57 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-state.ts +499 -0
- package/src/cli/fleet-sync-checkout.ts +130 -0
- package/src/cli/fleet-sync-enrollment.ts +140 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +249 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +317 -0
- package/src/cli/fleet-sync-privilege.ts +27 -0
- package/src/cli/fleet-sync-progress.ts +109 -0
- package/src/cli/fleet-sync-remote.ts +242 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-state.ts +86 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +375 -0
- package/src/cli/fleet.ts +165 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +121 -0
- package/src/cli/node-t3.ts +31 -0
- package/src/cli/options.ts +141 -0
- package/src/cli/probe-source.ts +7 -0
- package/src/cli/program.ts +185 -0
- package/src/cli/project.ts +51 -0
- package/src/cli/register-fleet.ts +296 -0
- package/src/cli/registry.ts +185 -0
- package/src/cli/repair-apply.ts +67 -0
- package/src/cli/repair-guard.ts +11 -0
- package/src/cli/repair-inspect.ts +150 -0
- package/src/cli/repair-script.ts +76 -0
- package/src/cli/repair.ts +209 -0
- package/src/cli/roster.ts +105 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +67 -0
- package/src/cli/source-operations.ts +102 -0
- package/src/cli/t3/command.ts +90 -0
- package/src/cli/t3/register.ts +55 -0
- package/src/cli/tailnet.ts +127 -0
- package/src/cli/ui-actions.ts +133 -0
- package/src/cli/ui-controller.ts +113 -0
- package/src/cli/ui-data.ts +106 -0
- package/src/cli/ui-execute.ts +163 -0
- package/src/cli/ui-repairs.ts +34 -0
- package/src/cli/ui-reports.ts +211 -0
- package/src/cli/ui-targets.ts +100 -0
- package/src/cli/ui-terminal.ts +109 -0
- package/src/cli/ui-types.ts +49 -0
- package/src/cli/ui-wizard.ts +101 -0
- package/src/cli/ui.ts +9 -0
- package/src/cli/worker-health-output.ts +37 -0
- package/src/config.ts +186 -0
- package/src/configure.ts +255 -0
- package/src/definition.ts +290 -0
- package/src/diagnostics.ts +92 -0
- package/src/doctor-check.ts +10 -0
- package/src/errors.ts +1 -0
- package/src/fingerprint.ts +120 -0
- package/src/fleet-config.ts +174 -0
- package/src/input-hashes.ts +73 -0
- package/src/machine/local-source.ts +52 -0
- package/src/machine/lock-error.ts +2 -0
- package/src/machine/platform.ts +44 -0
- package/src/machine/source.ts +68 -0
- package/src/machine/state.ts +173 -0
- package/src/machine-health.ts +95 -0
- package/src/managed-source.ts +47 -0
- package/src/manager-failure.ts +48 -0
- package/src/manager-sync.ts +405 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/agent-evidence.ts +146 -0
- package/src/monitoring/apply.ts +183 -0
- package/src/monitoring/beszel.ts +198 -0
- package/src/monitoring/enroll.ts +111 -0
- package/src/monitoring/hub-access.ts +114 -0
- package/src/monitoring/hub-backup.ts +121 -0
- package/src/monitoring/hub-viewer.ts +142 -0
- package/src/monitoring/hub.ts +93 -0
- package/src/monitoring/manager-agent.ts +73 -0
- package/src/monitoring/manager-hub.ts +248 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +374 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/package-info.ts +45 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +66 -0
- package/src/platform/darwin.ts +203 -0
- package/src/platform/detect.ts +11 -0
- package/src/platform/fallback.ts +110 -0
- package/src/platform/index.ts +38 -0
- package/src/platform/launchd.ts +98 -0
- package/src/platform/linux.ts +139 -0
- package/src/platform/ps.ts +45 -0
- package/src/platform/resource-verify.ts +132 -0
- package/src/platform/systemd-user.ts +95 -0
- package/src/platform/t3-policy.ts +137 -0
- package/src/platform/t3-route.ts +68 -0
- package/src/platform/t3-service.ts +75 -0
- package/src/platform/types.ts +123 -0
- package/src/platform/ubuntu-audit.ts +66 -0
- package/src/platform/ubuntu-verify.ts +105 -0
- package/src/platform/windows.ts +148 -0
- package/src/power-recovery.ts +55 -0
- package/src/probe.ts +51 -0
- package/src/protocol.ts +119 -0
- package/src/proxy-converge.ts +85 -0
- package/src/remote-bootstrap.ts +210 -0
- package/src/remote-entry.ts +89 -0
- package/src/remote-lock.ts +12 -0
- package/src/repair-catalog.ts +206 -0
- package/src/repair-plan.ts +78 -0
- package/src/repair-runner.ts +84 -0
- package/src/report.ts +71 -0
- package/src/runner.ts +104 -0
- package/src/secrets.ts +169 -0
- package/src/service-auth.ts +115 -0
- package/src/ssh.ts +135 -0
- package/src/state/bucket.ts +208 -0
- package/src/state/check.ts +74 -0
- package/src/state/connection.ts +92 -0
- package/src/state/enrollment.ts +45 -0
- package/src/state/legacy.ts +89 -0
- package/src/state/litestream.ts +143 -0
- package/src/state/operator.ts +20 -0
- package/src/state/paths.ts +40 -0
- package/src/state/roster.ts +62 -0
- package/src/state/store.ts +169 -0
- package/src/state/sync.ts +352 -0
- package/src/sync.ts +104 -0
- package/src/t3/accounts.ts +81 -0
- package/src/t3/desired.ts +165 -0
- package/src/t3/files.ts +81 -0
- package/src/t3/names-check.ts +53 -0
- package/src/t3/names.ts +155 -0
- package/src/t3/plan.ts +82 -0
- package/src/t3/providers.ts +74 -0
- package/src/t3/reconcile.ts +123 -0
- package/src/t3-endpoint.ts +104 -0
- package/src/t3-linked-check.ts +123 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/toolchain-checks.ts +108 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +99 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +168 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +181 -0
- package/src/worker-policy.ts +64 -0
- package/src/worker-readiness.ts +64 -0
- package/src/workstation-checks.ts +29 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""Recover disk space by deleting only what a build or install can recreate.
|
|
3
|
+
|
|
4
|
+
Managed by personal fleet-sync (tasks/resources.yml) as
|
|
5
|
+
personal-fleet-disk-gc.service, started hourly by personal-fleet-disk-gc.timer.
|
|
6
|
+
|
|
7
|
+
What it deletes:
|
|
8
|
+
* dependency and build directories (node_modules, .turbo, dist, Rust target,
|
|
9
|
+
...) inside git checkouts under the home directory, only when git says the
|
|
10
|
+
directory is ignored, the checkout has been idle for --idle-days and no
|
|
11
|
+
process is running inside it;
|
|
12
|
+
* Docker images and build cache that no container uses and that are older
|
|
13
|
+
than --docker-age-hours (never containers or volumes);
|
|
14
|
+
* the apt package cache.
|
|
15
|
+
When a watched filesystem has less than --pressure-mib free, idle checkouts
|
|
16
|
+
qualify after --pressure-idle-hours, Docker leftovers after
|
|
17
|
+
--pressure-docker-age-hours, and aged entries of each --pressure-cache
|
|
18
|
+
directory are removed too.
|
|
19
|
+
|
|
20
|
+
It never deletes a checkout, a tracked file, uncommitted work, a container or
|
|
21
|
+
a volume. Every decision is one JSON line on stdout.
|
|
22
|
+
|
|
23
|
+
Usage: disk-gc.py --home H [--path P ...] [--idle-days N] [--pressure-mib N]
|
|
24
|
+
[--pressure-idle-hours N] [--docker-age-hours N]
|
|
25
|
+
[--pressure-docker-age-hours N] [--pressure-cache DIR ...]
|
|
26
|
+
[--max-depth N] [--proc DIR] [--home-only] [--dry-run]
|
|
27
|
+
|
|
28
|
+
--home-only skips the machine-wide Docker and apt steps.
|
|
29
|
+
"""
|
|
30
|
+
import argparse
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import shutil
|
|
34
|
+
import stat
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
import time
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
MIB = 1024 * 1024
|
|
41
|
+
HOUR = 3600
|
|
42
|
+
# Directory names that hold only installed dependencies or build output.
|
|
43
|
+
REGENERABLE = frozenset({
|
|
44
|
+
'node_modules', '.cache', '.turbo', '.next', '.nuxt', '.svelte-kit', '.astro',
|
|
45
|
+
'.vite', '.parcel-cache', '.expo', '.gradle', 'dist', 'build', 'coverage',
|
|
46
|
+
'.pytest_cache', '.mypy_cache', '.ruff_cache', '__pycache__', '.venv',
|
|
47
|
+
})
|
|
48
|
+
# Directory names that are regenerable only beside the manifest that builds them.
|
|
49
|
+
REGENERABLE_BESIDE = {'target': 'Cargo.toml'}
|
|
50
|
+
# Hidden directories under the home directory that still hold checkouts.
|
|
51
|
+
HIDDEN_ROOTS = frozenset({'.t3'})
|
|
52
|
+
GIT = ['git', '-c', 'safe.directory=*']
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def log(**event):
|
|
56
|
+
print(json.dumps(event, sort_keys=True), flush=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def free_bytes(path):
|
|
60
|
+
st = os.statvfs(path)
|
|
61
|
+
return st.f_bavail * st.f_frsize
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def mtime(path):
|
|
65
|
+
try:
|
|
66
|
+
return os.lstat(path).st_mtime
|
|
67
|
+
except OSError:
|
|
68
|
+
return 0.0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_real_dir(path):
|
|
72
|
+
try:
|
|
73
|
+
return stat.S_ISDIR(os.lstat(path).st_mode)
|
|
74
|
+
except OSError:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def find_checkouts(home, max_depth):
|
|
79
|
+
"""Git checkouts (repositories and worktrees) under home, outermost only."""
|
|
80
|
+
found = []
|
|
81
|
+
|
|
82
|
+
def walk(directory, depth):
|
|
83
|
+
if os.path.lexists(os.path.join(directory, '.git')):
|
|
84
|
+
found.append(directory)
|
|
85
|
+
return
|
|
86
|
+
if depth >= max_depth:
|
|
87
|
+
return
|
|
88
|
+
try:
|
|
89
|
+
entries = list(os.scandir(directory))
|
|
90
|
+
except OSError:
|
|
91
|
+
return
|
|
92
|
+
for entry in entries:
|
|
93
|
+
if not entry.is_dir(follow_symlinks=False):
|
|
94
|
+
continue
|
|
95
|
+
name = entry.name
|
|
96
|
+
if name in REGENERABLE or name in REGENERABLE_BESIDE:
|
|
97
|
+
continue
|
|
98
|
+
if name.startswith('.') and not (depth == 0 and name in HIDDEN_ROOTS):
|
|
99
|
+
continue
|
|
100
|
+
walk(entry.path, depth + 1)
|
|
101
|
+
|
|
102
|
+
walk(home, 0)
|
|
103
|
+
return sorted(found)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def git_dir(checkout):
|
|
107
|
+
"""The checkout's git directory, following a worktree's .git file."""
|
|
108
|
+
dot_git = os.path.join(checkout, '.git')
|
|
109
|
+
if is_real_dir(dot_git):
|
|
110
|
+
return dot_git
|
|
111
|
+
try:
|
|
112
|
+
text = Path(dot_git).read_text()
|
|
113
|
+
except OSError:
|
|
114
|
+
return None
|
|
115
|
+
if not text.startswith('gitdir:'):
|
|
116
|
+
return None
|
|
117
|
+
target = text[len('gitdir:'):].strip()
|
|
118
|
+
target = os.path.normpath(os.path.join(checkout, target))
|
|
119
|
+
return target if is_real_dir(target) else None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def run_git(checkout, args, data=None):
|
|
123
|
+
try:
|
|
124
|
+
out = subprocess.run(GIT + ['-C', checkout] + args, input=data,
|
|
125
|
+
capture_output=True, timeout=600)
|
|
126
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
127
|
+
return None
|
|
128
|
+
return out
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def candidates(checkout):
|
|
132
|
+
"""Regenerable directories in a checkout, outermost only, never through a link."""
|
|
133
|
+
found = []
|
|
134
|
+
for current, dirs, files in os.walk(checkout):
|
|
135
|
+
keep = []
|
|
136
|
+
for name in dirs:
|
|
137
|
+
path = os.path.join(current, name)
|
|
138
|
+
if name == '.git' or os.path.islink(path):
|
|
139
|
+
continue
|
|
140
|
+
if os.path.lexists(os.path.join(path, '.git')):
|
|
141
|
+
continue # a nested repository answers for itself
|
|
142
|
+
beside = REGENERABLE_BESIDE.get(name)
|
|
143
|
+
if name in REGENERABLE or (beside and beside in files):
|
|
144
|
+
found.append(path)
|
|
145
|
+
continue
|
|
146
|
+
keep.append(name)
|
|
147
|
+
dirs[:] = keep
|
|
148
|
+
return found
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def ignored(checkout, paths):
|
|
152
|
+
"""The subset of paths git ignores; tracked or unignored paths are kept."""
|
|
153
|
+
if not paths:
|
|
154
|
+
return []
|
|
155
|
+
rel = [os.path.relpath(p, checkout) for p in paths]
|
|
156
|
+
out = run_git(checkout, ['check-ignore', '--stdin', '-z'],
|
|
157
|
+
data=('\0'.join(rel) + '\0').encode())
|
|
158
|
+
if out is None or out.returncode not in (0, 1):
|
|
159
|
+
return []
|
|
160
|
+
hits = {item for item in out.stdout.decode(errors='replace').split('\0') if item}
|
|
161
|
+
return [p for p, r in zip(paths, rel) if r in hits or r.rstrip('/') in hits]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def git_activity(checkout, gdir):
|
|
165
|
+
"""The latest commit, checkout or index refresh: cheap, read first."""
|
|
166
|
+
marks = [checkout, os.path.join(gdir, 'index'), os.path.join(gdir, 'HEAD'),
|
|
167
|
+
os.path.join(gdir, 'logs', 'HEAD')]
|
|
168
|
+
return max(mtime(p) for p in marks)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def last_activity(checkout, gdir, dirs):
|
|
172
|
+
"""The latest sign of work: git activity, changed files, build output writes."""
|
|
173
|
+
latest = max([git_activity(checkout, gdir)] + [mtime(p) for p in dirs])
|
|
174
|
+
out = run_git(checkout, ['status', '--porcelain=v1', '-z', '--untracked-files=all'])
|
|
175
|
+
if out is None or out.returncode != 0:
|
|
176
|
+
return None
|
|
177
|
+
entries = out.stdout.decode(errors='replace').split('\0')
|
|
178
|
+
for entry in entries:
|
|
179
|
+
if len(entry) > 3:
|
|
180
|
+
latest = max(latest, mtime(os.path.join(checkout, entry[3:])))
|
|
181
|
+
return latest
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def busy_roots(proc):
|
|
185
|
+
"""Working directories of every running process."""
|
|
186
|
+
roots = set()
|
|
187
|
+
try:
|
|
188
|
+
pids = [p for p in os.listdir(proc) if p.isdigit()]
|
|
189
|
+
except OSError:
|
|
190
|
+
return roots
|
|
191
|
+
for pid in pids:
|
|
192
|
+
try:
|
|
193
|
+
roots.add(os.readlink(os.path.join(proc, pid, 'cwd')))
|
|
194
|
+
except OSError:
|
|
195
|
+
continue
|
|
196
|
+
return roots
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def in_use(checkout, roots):
|
|
200
|
+
prefix = checkout.rstrip('/') + '/'
|
|
201
|
+
return any(r == checkout or r.startswith(prefix) for r in roots)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def size_of(path):
|
|
205
|
+
total = 0
|
|
206
|
+
for current, dirs, files in os.walk(path):
|
|
207
|
+
for name in dirs + files:
|
|
208
|
+
try:
|
|
209
|
+
total += os.lstat(os.path.join(current, name)).st_blocks * 512
|
|
210
|
+
except OSError:
|
|
211
|
+
continue
|
|
212
|
+
return total
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def remove(path, dry_run):
|
|
216
|
+
"""Deletes one directory without following links; returns bytes freed."""
|
|
217
|
+
if not is_real_dir(path):
|
|
218
|
+
return 0
|
|
219
|
+
size = size_of(path)
|
|
220
|
+
if not dry_run:
|
|
221
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
222
|
+
if os.path.lexists(path):
|
|
223
|
+
return 0
|
|
224
|
+
return size
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def collect_checkouts(home, idle_seconds, max_depth, proc, now, dry_run):
|
|
228
|
+
roots = busy_roots(proc)
|
|
229
|
+
freed = 0
|
|
230
|
+
for checkout in find_checkouts(home, max_depth):
|
|
231
|
+
gdir = git_dir(checkout)
|
|
232
|
+
if gdir is None:
|
|
233
|
+
log(event='checkout_skipped', checkout=checkout, reason='no_git_dir')
|
|
234
|
+
continue
|
|
235
|
+
if in_use(checkout, roots) or now - git_activity(checkout, gdir) < idle_seconds:
|
|
236
|
+
continue
|
|
237
|
+
dirs = ignored(checkout, candidates(checkout))
|
|
238
|
+
if not dirs:
|
|
239
|
+
continue
|
|
240
|
+
latest = last_activity(checkout, gdir, dirs)
|
|
241
|
+
if latest is None:
|
|
242
|
+
log(event='checkout_skipped', checkout=checkout, reason='git_status_failed')
|
|
243
|
+
continue
|
|
244
|
+
if now - latest < idle_seconds:
|
|
245
|
+
continue
|
|
246
|
+
for path in dirs:
|
|
247
|
+
got = remove(path, dry_run)
|
|
248
|
+
freed += got
|
|
249
|
+
log(event='removed', path=path, mib=got // MIB, dry_run=dry_run)
|
|
250
|
+
return freed
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def collect_cache(directory, age_seconds, now, dry_run):
|
|
254
|
+
"""Removes top-level cache entries not written for age_seconds."""
|
|
255
|
+
freed = 0
|
|
256
|
+
try:
|
|
257
|
+
entries = list(os.scandir(directory))
|
|
258
|
+
except OSError:
|
|
259
|
+
return 0
|
|
260
|
+
for entry in entries:
|
|
261
|
+
if now - mtime(entry.path) < age_seconds:
|
|
262
|
+
continue
|
|
263
|
+
if entry.is_dir(follow_symlinks=False):
|
|
264
|
+
got = remove(entry.path, dry_run)
|
|
265
|
+
elif entry.is_file(follow_symlinks=False):
|
|
266
|
+
got = entry.stat(follow_symlinks=False).st_blocks * 512
|
|
267
|
+
if not dry_run:
|
|
268
|
+
try:
|
|
269
|
+
os.unlink(entry.path)
|
|
270
|
+
except OSError:
|
|
271
|
+
got = 0
|
|
272
|
+
else:
|
|
273
|
+
continue
|
|
274
|
+
freed += got
|
|
275
|
+
if freed:
|
|
276
|
+
log(event='cache_pruned', cache=directory, mib=freed // MIB, dry_run=dry_run)
|
|
277
|
+
return freed
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def command(argv, dry_run, name):
|
|
281
|
+
if shutil.which(argv[0]) is None:
|
|
282
|
+
return
|
|
283
|
+
if dry_run:
|
|
284
|
+
log(event='would_run', step=name)
|
|
285
|
+
return
|
|
286
|
+
try:
|
|
287
|
+
out = subprocess.run(argv, capture_output=True, timeout=1800)
|
|
288
|
+
log(event='ran', step=name, ok=out.returncode == 0)
|
|
289
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
290
|
+
log(event='ran', step=name, ok=False, error=type(exc).__name__)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def main(argv):
|
|
294
|
+
parser = argparse.ArgumentParser()
|
|
295
|
+
parser.add_argument('--home', required=True)
|
|
296
|
+
parser.add_argument('--path', action='append', default=[])
|
|
297
|
+
parser.add_argument('--idle-days', type=float, default=7)
|
|
298
|
+
parser.add_argument('--pressure-mib', type=int, default=15360)
|
|
299
|
+
parser.add_argument('--pressure-idle-hours', type=float, default=12)
|
|
300
|
+
parser.add_argument('--docker-age-hours', type=int, default=168)
|
|
301
|
+
parser.add_argument('--pressure-docker-age-hours', type=int, default=24)
|
|
302
|
+
parser.add_argument('--pressure-cache', action='append', default=[])
|
|
303
|
+
parser.add_argument('--max-depth', type=int, default=4)
|
|
304
|
+
parser.add_argument('--proc', default='/proc')
|
|
305
|
+
parser.add_argument('--home-only', action='store_true')
|
|
306
|
+
parser.add_argument('--dry-run', action='store_true')
|
|
307
|
+
args = parser.parse_args(argv)
|
|
308
|
+
home = os.path.realpath(args.home)
|
|
309
|
+
paths = args.path or ['/', home]
|
|
310
|
+
now = time.time()
|
|
311
|
+
before = {p: free_bytes(p) for p in paths}
|
|
312
|
+
pressure = min(before.values()) < args.pressure_mib * MIB
|
|
313
|
+
idle = args.pressure_idle_hours * HOUR if pressure else args.idle_days * 24 * HOUR
|
|
314
|
+
docker_age = args.pressure_docker_age_hours if pressure else args.docker_age_hours
|
|
315
|
+
log(event='started', pressure=pressure, idle_hours=idle / HOUR,
|
|
316
|
+
docker_age_hours=docker_age, dry_run=args.dry_run,
|
|
317
|
+
free_mib={p: v // MIB for p, v in before.items()})
|
|
318
|
+
|
|
319
|
+
freed = collect_checkouts(home, idle, args.max_depth, args.proc, now, args.dry_run)
|
|
320
|
+
if pressure:
|
|
321
|
+
for cache in args.pressure_cache:
|
|
322
|
+
freed += collect_cache(os.path.join(home, cache), idle, now, args.dry_run)
|
|
323
|
+
if not args.home_only:
|
|
324
|
+
command(['docker', 'image', 'prune', '--all', '--force', '--filter',
|
|
325
|
+
f'until={docker_age}h'], args.dry_run, 'docker_images')
|
|
326
|
+
command(['docker', 'builder', 'prune', '--all', '--force', '--filter',
|
|
327
|
+
f'until={docker_age}h'], args.dry_run, 'docker_build_cache')
|
|
328
|
+
command(['apt-get', 'clean'], args.dry_run, 'apt_cache')
|
|
329
|
+
|
|
330
|
+
after = {p: free_bytes(p) for p in paths}
|
|
331
|
+
log(event='finished', checkout_mib=freed // MIB, dry_run=args.dry_run,
|
|
332
|
+
free_mib={p: v // MIB for p, v in after.items()})
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
if __name__ == '__main__':
|
|
336
|
+
main(sys.argv[1:])
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""Stop the user process group that is filling the disk before it runs out.
|
|
3
|
+
|
|
4
|
+
Managed by personal fleet-sync (tasks/resources.yml) as
|
|
5
|
+
personal-fleet-disk-guard.service. Every interval it measures free space on the
|
|
6
|
+
watched filesystems and the bytes each leaf cgroup under user.slice wrote. When
|
|
7
|
+
any watched filesystem is below the floor, it kills only the single cgroup that
|
|
8
|
+
wrote the most during the last interval, and only if that is at least the write
|
|
9
|
+
threshold. It never deletes files, never touches system services and never
|
|
10
|
+
kills idle work. Free space must be recovered by a person.
|
|
11
|
+
|
|
12
|
+
Usage: disk-guard.py --path / --path /home/me [--floor-mib N] [--write-mib N]
|
|
13
|
+
[--interval S] [--cgroup-root DIR] [--dry-run]
|
|
14
|
+
"""
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
MIB = 1024 * 1024
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def leaf_writes(root):
|
|
26
|
+
"""Bytes written so far by every leaf cgroup below root, from io.stat."""
|
|
27
|
+
writes = {}
|
|
28
|
+
for current, dirs, files in os.walk(root):
|
|
29
|
+
if dirs or 'io.stat' not in files:
|
|
30
|
+
continue
|
|
31
|
+
try:
|
|
32
|
+
text = Path(current, 'io.stat').read_text()
|
|
33
|
+
except OSError:
|
|
34
|
+
continue
|
|
35
|
+
total = 0
|
|
36
|
+
for line in text.splitlines():
|
|
37
|
+
for field in line.split()[1:]:
|
|
38
|
+
key, _, value = field.partition('=')
|
|
39
|
+
if key == 'wbytes' and value.isdigit():
|
|
40
|
+
total += int(value)
|
|
41
|
+
writes[current] = total
|
|
42
|
+
return writes
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def choose_victim(previous, current, low, threshold):
|
|
46
|
+
"""The leaf that wrote the most since the previous sample, if it is a runaway."""
|
|
47
|
+
if not low:
|
|
48
|
+
return None
|
|
49
|
+
best, best_delta = None, 0
|
|
50
|
+
for path, total in current.items():
|
|
51
|
+
delta = total - previous.get(path, total)
|
|
52
|
+
if delta > best_delta:
|
|
53
|
+
best, best_delta = path, delta
|
|
54
|
+
if best is None or best_delta < threshold:
|
|
55
|
+
return None
|
|
56
|
+
return best, best_delta
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def free_bytes(path):
|
|
60
|
+
st = os.statvfs(path)
|
|
61
|
+
return st.f_bavail * st.f_frsize
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def log(**event):
|
|
65
|
+
print(json.dumps(event, sort_keys=True), flush=True)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main(argv):
|
|
69
|
+
parser = argparse.ArgumentParser()
|
|
70
|
+
parser.add_argument('--path', action='append', required=True)
|
|
71
|
+
parser.add_argument('--floor-mib', type=int, default=3072)
|
|
72
|
+
parser.add_argument('--write-mib', type=int, default=32)
|
|
73
|
+
parser.add_argument('--interval', type=float, default=5.0)
|
|
74
|
+
parser.add_argument('--cgroup-root', default='/sys/fs/cgroup/user.slice')
|
|
75
|
+
parser.add_argument('--dry-run', action='store_true')
|
|
76
|
+
args = parser.parse_args(argv)
|
|
77
|
+
floor, threshold = args.floor_mib * MIB, args.write_mib * MIB
|
|
78
|
+
log(event='started', paths=args.path, floor_mib=args.floor_mib,
|
|
79
|
+
write_mib=args.write_mib, interval=args.interval, dry_run=args.dry_run)
|
|
80
|
+
previous = leaf_writes(args.cgroup_root)
|
|
81
|
+
while True:
|
|
82
|
+
time.sleep(args.interval)
|
|
83
|
+
try:
|
|
84
|
+
current = leaf_writes(args.cgroup_root)
|
|
85
|
+
free = {path: free_bytes(path) for path in args.path}
|
|
86
|
+
low = [path for path, value in free.items() if value < floor]
|
|
87
|
+
victim = choose_victim(previous, current, bool(low), threshold)
|
|
88
|
+
previous = current
|
|
89
|
+
if victim is None:
|
|
90
|
+
continue
|
|
91
|
+
path, delta = victim
|
|
92
|
+
event = dict(event='runaway_writer', cgroup=path,
|
|
93
|
+
wrote_mib=delta // MIB, low=low,
|
|
94
|
+
free_mib={p: v // MIB for p, v in free.items()})
|
|
95
|
+
if not args.dry_run:
|
|
96
|
+
Path(path, 'cgroup.kill').write_text('1\n')
|
|
97
|
+
event['killed'] = True
|
|
98
|
+
log(**event)
|
|
99
|
+
except OSError as exc:
|
|
100
|
+
log(event='check_failed', error=type(exc).__name__)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == '__main__':
|
|
104
|
+
main(sys.argv[1:])
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"release": "26.04",
|
|
4
|
+
"architectures": ["amd64", "arm64"],
|
|
5
|
+
"snapshot": "20261101T000000Z",
|
|
6
|
+
"packages": {
|
|
7
|
+
"sudo": "1.9.17p2-1ubuntu3.2",
|
|
8
|
+
"openssh-server": "1:10.2p1-2ubuntu3.8",
|
|
9
|
+
"dconf-cli": "0.49.0-4",
|
|
10
|
+
"docker.io": "29.1.3-0ubuntu4.1",
|
|
11
|
+
"docker-compose-v2": "2.40.3+ds1-0ubuntu1",
|
|
12
|
+
"docker-buildx": "0.30.1-0ubuntu1",
|
|
13
|
+
"build-essential": "12.12ubuntu2.26.04.2",
|
|
14
|
+
"ca-certificates": "20260816ubuntu0.26.04.2",
|
|
15
|
+
"curl": "8.18.0-1ubuntu2.8",
|
|
16
|
+
"git": "1:2.53.0-1ubuntu1.1",
|
|
17
|
+
"python3": "3.14.3-0ubuntu2",
|
|
18
|
+
"unzip": "6.0-29ubuntu1",
|
|
19
|
+
"xz-utils": "5.8.3-1ubuntu0.1",
|
|
20
|
+
"ansible-core": "2.20.1-1"
|
|
21
|
+
},
|
|
22
|
+
"tailscale": {
|
|
23
|
+
"version": "1.104.5",
|
|
24
|
+
"keySha256": "3e03dacf222698c60b8e2f990b809ca1b3e104de127767864284e6c228f1fb39"
|
|
25
|
+
},
|
|
26
|
+
"aptLockTimeoutSeconds": 120,
|
|
27
|
+
"minimumFreeDiskMiB": 4096,
|
|
28
|
+
"minimumFreeInodes": 10000,
|
|
29
|
+
"alwaysOnServices": ["ssh", "tailscaled", "docker", "beszel-agent"],
|
|
30
|
+
"resourceSafety": {
|
|
31
|
+
"swapFileMiB": 8192,
|
|
32
|
+
"userMemoryMax": "90%",
|
|
33
|
+
"agentMemoryMax": "30%",
|
|
34
|
+
"agentSwapMax": "2G",
|
|
35
|
+
"journalMaxUse": "1G",
|
|
36
|
+
"journalKeepFree": "4G",
|
|
37
|
+
"tmpMaxSize": "25%",
|
|
38
|
+
"tmpAge": "2d",
|
|
39
|
+
"varTmpAge": "14d",
|
|
40
|
+
"diskGuardFloorMiB": 3072,
|
|
41
|
+
"diskGuardWriteMiB": 32,
|
|
42
|
+
"diskGuardIntervalSeconds": 5,
|
|
43
|
+
"diskGcIdleDays": 7,
|
|
44
|
+
"diskGcPressureMiB": 15360,
|
|
45
|
+
"diskGcPressureIdleHours": 12,
|
|
46
|
+
"diskGcDockerAgeHours": 168,
|
|
47
|
+
"diskGcPressureDockerAgeHours": 24,
|
|
48
|
+
"diskGcPressureCaches": [".bun/install/cache"],
|
|
49
|
+
"dockerLogMaxSize": "50m",
|
|
50
|
+
"dockerLogMaxFiles": "3"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
- name: Enable recovery for an already verified T3 desktop app
|
|
3
|
+
hosts: localhost
|
|
4
|
+
connection: local
|
|
5
|
+
become: true
|
|
6
|
+
gather_facts: false
|
|
7
|
+
tasks:
|
|
8
|
+
- name: Verify the desktop unit runs before adding any recovery trigger
|
|
9
|
+
ansible.builtin.command:
|
|
10
|
+
argv: [systemctl, --user, is-active, t3code-desktop.service]
|
|
11
|
+
become_user: '{{ development_user }}'
|
|
12
|
+
environment:
|
|
13
|
+
XDG_RUNTIME_DIR: '/run/user/{{ development_uid }}'
|
|
14
|
+
changed_when: false
|
|
15
|
+
- ansible.builtin.import_tasks: tasks/recovery.yml
|