foldyard 0.0.1__tar.gz

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.
Files changed (189) hide show
  1. foldyard-0.0.1/.github/workflows/foldyard-e2e.yml +209 -0
  2. foldyard-0.0.1/.github/workflows/foldyard.yml +26 -0
  3. foldyard-0.0.1/.github/workflows/release.yml +53 -0
  4. foldyard-0.0.1/.gitignore +21 -0
  5. foldyard-0.0.1/CHANGELOG.md +27 -0
  6. foldyard-0.0.1/CLAUDE.md +1 -0
  7. foldyard-0.0.1/CONTRIBUTING.md +30 -0
  8. foldyard-0.0.1/DEVELOPMENT.md +324 -0
  9. foldyard-0.0.1/LICENSE +202 -0
  10. foldyard-0.0.1/NOTICE +6 -0
  11. foldyard-0.0.1/PKG-INFO +168 -0
  12. foldyard-0.0.1/README.md +133 -0
  13. foldyard-0.0.1/SECURITY.md +33 -0
  14. foldyard-0.0.1/docs/README.md +43 -0
  15. foldyard-0.0.1/docs/adrs/0001-rootless-podman-vm-isolation-boundary.md +96 -0
  16. foldyard-0.0.1/docs/adrs/0002-stack-colocation-project-as-isolation-unit.md +86 -0
  17. foldyard-0.0.1/docs/adrs/0003-name-foldyard.md +69 -0
  18. foldyard-0.0.1/docs/adrs/0004-one-machine-worktrees-as-compose-projects.md +84 -0
  19. foldyard-0.0.1/docs/adrs/0005-secretless-by-default-posture-axes.md +78 -0
  20. foldyard-0.0.1/docs/adrs/0006-host-side-enforcement-single-supervisor.md +82 -0
  21. foldyard-0.0.1/docs/adrs/0007-credential-injection-at-egress-proxy.md +86 -0
  22. foldyard-0.0.1/docs/adrs/0008-keyless-agent-auth.md +85 -0
  23. foldyard-0.0.1/docs/adrs/0009-monitoring-cooperative-enforcement-locked.md +95 -0
  24. foldyard-0.0.1/docs/adrs/0010-podman-everywhere-container-host.md +87 -0
  25. foldyard-0.0.1/docs/adrs/0011-machine-backends-one-socket-contract.md +140 -0
  26. foldyard-0.0.1/docs/adrs/0012-uv-tool-distribution-no-mutable-daemon-source.md +94 -0
  27. foldyard-0.0.1/docs/adrs/0013-in-repo-carve-out-until-extraction.md +79 -0
  28. foldyard-0.0.1/docs/adrs/0014-consumer-supplied-box-image.md +117 -0
  29. foldyard-0.0.1/docs/adrs/0015-plugin-framework-per-consumer-registry.md +89 -0
  30. foldyard-0.0.1/docs/adrs/0016-per-worktree-posture.md +91 -0
  31. foldyard-0.0.1/docs/adrs/0017-nested-virt-validation-strategy.md +87 -0
  32. foldyard-0.0.1/docs/adrs/0018-zed-editor-rejected.md +47 -0
  33. foldyard-0.0.1/docs/adrs/0019-consumer-repo-plugins-trust-model.md +92 -0
  34. foldyard-0.0.1/docs/adrs/0020-post-extraction-consumption-model.md +131 -0
  35. foldyard-0.0.1/docs/adrs/0021-per-kernel-git-index-split.md +105 -0
  36. foldyard-0.0.1/docs/adrs/0022-host-runs-the-adopted-config.md +140 -0
  37. foldyard-0.0.1/docs/adrs/0023-no-host-executed-code-from-the-repo-mount.md +164 -0
  38. foldyard-0.0.1/docs/adrs/0024-declarative-consumer-axes-no-repo-path-plugins.md +122 -0
  39. foldyard-0.0.1/docs/adrs/README.md +46 -0
  40. foldyard-0.0.1/docs/compose-overlays.md +88 -0
  41. foldyard-0.0.1/docs/configuration.md +713 -0
  42. foldyard-0.0.1/docs/lima-backend-scope.md +63 -0
  43. foldyard-0.0.1/docs/lima-network-forcing-kit/README.md +222 -0
  44. foldyard-0.0.1/docs/lima-network-forcing-kit/agent-claude.sh +69 -0
  45. foldyard-0.0.1/docs/lima-network-forcing-kit/install.sh +147 -0
  46. foldyard-0.0.1/docs/lima-network-forcing-kit/proxy.py +168 -0
  47. foldyard-0.0.1/docs/lima-network-forcing-kit/run.sh +44 -0
  48. foldyard-0.0.1/docs/lima-network-forcing-kit/test.sh +72 -0
  49. foldyard-0.0.1/docs/lima-network-forcing-kit/transparent/README.md +47 -0
  50. foldyard-0.0.1/docs/lima-network-forcing-kit/transparent/install-cwall.sh +117 -0
  51. foldyard-0.0.1/docs/lima-network-forcing-kit/transparent/run-cwall.sh +35 -0
  52. foldyard-0.0.1/docs/lima-network-forcing-kit/transparent/test-cwall.sh +52 -0
  53. foldyard-0.0.1/docs/lima-network-forcing-kit/transparent/tproxy.py +169 -0
  54. foldyard-0.0.1/docs/lima-network-forcing-kit/wall-nested.yaml +68 -0
  55. foldyard-0.0.1/docs/lima-network-forcing-kit/wall.yaml +51 -0
  56. foldyard-0.0.1/docs/lima-wall-machine-integration.md +200 -0
  57. foldyard-0.0.1/docs/mode-state-consolidation.md +165 -0
  58. foldyard-0.0.1/docs/modes.md +194 -0
  59. foldyard-0.0.1/docs/nested-virt.md +179 -0
  60. foldyard-0.0.1/docs/networking.md +112 -0
  61. foldyard-0.0.1/docs/podman-multi-vm-issue-26281.md +34 -0
  62. foldyard-0.0.1/docs/prior-art.md +112 -0
  63. foldyard-0.0.1/docs/quickstart.md +132 -0
  64. foldyard-0.0.1/docs/security.md +184 -0
  65. foldyard-0.0.1/docs/testing-modes.md +86 -0
  66. foldyard-0.0.1/example/.gitignore +2 -0
  67. foldyard-0.0.1/example/README.md +114 -0
  68. foldyard-0.0.1/example/api/Dockerfile +14 -0
  69. foldyard-0.0.1/example/api/main.py +39 -0
  70. foldyard-0.0.1/example/box.Dockerfile +25 -0
  71. foldyard-0.0.1/example/compose.feature.yml +8 -0
  72. foldyard-0.0.1/example/compose.yml +30 -0
  73. foldyard-0.0.1/example/foldyard.toml +76 -0
  74. foldyard-0.0.1/example-lima-wall/.gitignore +2 -0
  75. foldyard-0.0.1/example-lima-wall/README.md +79 -0
  76. foldyard-0.0.1/example-lima-wall/box.Dockerfile +17 -0
  77. foldyard-0.0.1/example-lima-wall/compose.yml +47 -0
  78. foldyard-0.0.1/example-lima-wall/foldyard.toml +85 -0
  79. foldyard-0.0.1/example-lima-wall/test_network.sh +175 -0
  80. foldyard-0.0.1/foldyard.toml +37 -0
  81. foldyard-0.0.1/justfile +134 -0
  82. foldyard-0.0.1/pyproject.toml +203 -0
  83. foldyard-0.0.1/src/foldyard/__init__.py +11 -0
  84. foldyard-0.0.1/src/foldyard/__main__.py +9 -0
  85. foldyard-0.0.1/src/foldyard/allowlist.py +470 -0
  86. foldyard-0.0.1/src/foldyard/assets/box/Dockerfile +39 -0
  87. foldyard-0.0.1/src/foldyard/assets/box/git-index-shim.sh +285 -0
  88. foldyard-0.0.1/src/foldyard/assets/machine-wall/machine-wall.sh +235 -0
  89. foldyard-0.0.1/src/foldyard/assets/proxy/egress_proxy.py +743 -0
  90. foldyard-0.0.1/src/foldyard/assets/skills/bootstrap-devbox/SKILL.md +134 -0
  91. foldyard-0.0.1/src/foldyard/assets/skills/foldyard/SKILL.md +84 -0
  92. foldyard-0.0.1/src/foldyard/assets/skills/foldyard/references/egress-and-capture.md +50 -0
  93. foldyard-0.0.1/src/foldyard/assets/skills/foldyard/references/posture-and-credentials.md +54 -0
  94. foldyard-0.0.1/src/foldyard/assets/skills/foldyard/references/when-a-change-doesnt-take.md +32 -0
  95. foldyard-0.0.1/src/foldyard/box.py +1112 -0
  96. foldyard-0.0.1/src/foldyard/browser.py +40 -0
  97. foldyard-0.0.1/src/foldyard/cli.py +816 -0
  98. foldyard-0.0.1/src/foldyard/config.py +1557 -0
  99. foldyard-0.0.1/src/foldyard/configpin.py +868 -0
  100. foldyard-0.0.1/src/foldyard/devmode.py +1484 -0
  101. foldyard-0.0.1/src/foldyard/docs.py +129 -0
  102. foldyard-0.0.1/src/foldyard/exposure.py +483 -0
  103. foldyard-0.0.1/src/foldyard/githeal.py +240 -0
  104. foldyard-0.0.1/src/foldyard/init.py +486 -0
  105. foldyard-0.0.1/src/foldyard/keyless.py +371 -0
  106. foldyard-0.0.1/src/foldyard/machine.py +480 -0
  107. foldyard-0.0.1/src/foldyard/machine_backend.py +649 -0
  108. foldyard-0.0.1/src/foldyard/plugins/__init__.py +898 -0
  109. foldyard-0.0.1/src/foldyard/plugins/_passthrough_bundles.py +246 -0
  110. foldyard-0.0.1/src/foldyard/plugins/auth0_sim.py +157 -0
  111. foldyard-0.0.1/src/foldyard/plugins/claude.py +187 -0
  112. foldyard-0.0.1/src/foldyard/plugins/codex.py +242 -0
  113. foldyard-0.0.1/src/foldyard/plugins/codex_chatgpt_token.py +187 -0
  114. foldyard-0.0.1/src/foldyard/plugins/fakecred.py +121 -0
  115. foldyard-0.0.1/src/foldyard/plugins/fakecred_minter.py +64 -0
  116. foldyard-0.0.1/src/foldyard/plugins/gcp.py +639 -0
  117. foldyard-0.0.1/src/foldyard/plugins/gcp_metadata/README.md +104 -0
  118. foldyard-0.0.1/src/foldyard/plugins/gcp_metadata/__init__.py +11 -0
  119. foldyard-0.0.1/src/foldyard/plugins/gcp_metadata/minter.py +254 -0
  120. foldyard-0.0.1/src/foldyard/plugins/gcp_metadata/server.py +260 -0
  121. foldyard-0.0.1/src/foldyard/plugins/gh_cli_token.py +53 -0
  122. foldyard-0.0.1/src/foldyard/plugins/github.py +482 -0
  123. foldyard-0.0.1/src/foldyard/plugins/github_app_token.py +288 -0
  124. foldyard-0.0.1/src/foldyard/plugins/inject.py +162 -0
  125. foldyard-0.0.1/src/foldyard/plugins/llm.py +65 -0
  126. foldyard-0.0.1/src/foldyard/plugins/proxy.py +625 -0
  127. foldyard-0.0.1/src/foldyard/plugins/static_token.py +56 -0
  128. foldyard-0.0.1/src/foldyard/plugins/vscode.py +25 -0
  129. foldyard-0.0.1/src/foldyard/ports.py +85 -0
  130. foldyard-0.0.1/src/foldyard/preflight.py +166 -0
  131. foldyard-0.0.1/src/foldyard/reconcile.py +276 -0
  132. foldyard-0.0.1/src/foldyard/skills.py +76 -0
  133. foldyard-0.0.1/src/foldyard/stack.py +1465 -0
  134. foldyard-0.0.1/src/foldyard/state_view.py +38 -0
  135. foldyard-0.0.1/src/foldyard/supervisor.py +1208 -0
  136. foldyard-0.0.1/src/foldyard/term.py +127 -0
  137. foldyard-0.0.1/src/foldyard/transcripts.py +400 -0
  138. foldyard-0.0.1/src/foldyard/tui.py +1983 -0
  139. foldyard-0.0.1/src/foldyard/verify.py +279 -0
  140. foldyard-0.0.1/src/foldyard/vscode.py +601 -0
  141. foldyard-0.0.1/src/foldyard/worktree.py +512 -0
  142. foldyard-0.0.1/tests/conftest.py +232 -0
  143. foldyard-0.0.1/tests/pbt.py +169 -0
  144. foldyard-0.0.1/tests/test_agent_guide.py +187 -0
  145. foldyard-0.0.1/tests/test_allowlist.py +343 -0
  146. foldyard-0.0.1/tests/test_box.py +988 -0
  147. foldyard-0.0.1/tests/test_browser.py +58 -0
  148. foldyard-0.0.1/tests/test_capture_box_e2e.py +571 -0
  149. foldyard-0.0.1/tests/test_cli.py +210 -0
  150. foldyard-0.0.1/tests/test_codex_chatgpt_token.py +136 -0
  151. foldyard-0.0.1/tests/test_config.py +798 -0
  152. foldyard-0.0.1/tests/test_config_pin.py +936 -0
  153. foldyard-0.0.1/tests/test_devmode.py +1009 -0
  154. foldyard-0.0.1/tests/test_docs.py +82 -0
  155. foldyard-0.0.1/tests/test_dual_home.py +43 -0
  156. foldyard-0.0.1/tests/test_e2e.py +223 -0
  157. foldyard-0.0.1/tests/test_example.py +127 -0
  158. foldyard-0.0.1/tests/test_example_lima_wall.py +87 -0
  159. foldyard-0.0.1/tests/test_exposure.py +307 -0
  160. foldyard-0.0.1/tests/test_fakecred.py +91 -0
  161. foldyard-0.0.1/tests/test_gcp_metadata_box_e2e.py +326 -0
  162. foldyard-0.0.1/tests/test_git_shim.py +470 -0
  163. foldyard-0.0.1/tests/test_githeal.py +131 -0
  164. foldyard-0.0.1/tests/test_github_minters.py +388 -0
  165. foldyard-0.0.1/tests/test_init.py +272 -0
  166. foldyard-0.0.1/tests/test_keyless.py +359 -0
  167. foldyard-0.0.1/tests/test_machine.py +698 -0
  168. foldyard-0.0.1/tests/test_machine_backend.py +416 -0
  169. foldyard-0.0.1/tests/test_minter.py +412 -0
  170. foldyard-0.0.1/tests/test_plugins.py +2413 -0
  171. foldyard-0.0.1/tests/test_ports.py +116 -0
  172. foldyard-0.0.1/tests/test_preflight.py +266 -0
  173. foldyard-0.0.1/tests/test_properties.py +261 -0
  174. foldyard-0.0.1/tests/test_proxy_box_e2e.py +409 -0
  175. foldyard-0.0.1/tests/test_proxy_e2e.py +292 -0
  176. foldyard-0.0.1/tests/test_proxy_inject.py +884 -0
  177. foldyard-0.0.1/tests/test_reconcile_model.py +473 -0
  178. foldyard-0.0.1/tests/test_reconcile_scenarios.py +205 -0
  179. foldyard-0.0.1/tests/test_skills.py +47 -0
  180. foldyard-0.0.1/tests/test_stack.py +1446 -0
  181. foldyard-0.0.1/tests/test_state_view.py +82 -0
  182. foldyard-0.0.1/tests/test_supervisor.py +1271 -0
  183. foldyard-0.0.1/tests/test_term.py +116 -0
  184. foldyard-0.0.1/tests/test_transcripts.py +456 -0
  185. foldyard-0.0.1/tests/test_tui.py +1856 -0
  186. foldyard-0.0.1/tests/test_verify.py +312 -0
  187. foldyard-0.0.1/tests/test_vscode.py +664 -0
  188. foldyard-0.0.1/tests/test_worktree.py +527 -0
  189. foldyard-0.0.1/uv.lock +2243 -0
@@ -0,0 +1,209 @@
1
+ name: foldyard · live e2e
2
+
3
+ # foldyard's live container e2es. Two advisory tiers run here:
4
+ # - Docker-in-container topology for proxy/box e2es that need a sibling box to reach the test
5
+ # process by container IP.
6
+ # - Native Linux host topology with `[machine].backend = "native"` so CI exercises the real
7
+ # host-side `foldyard up` / worktree stack lifecycle without nested virtualization.
8
+ #
9
+ # The VM-backed `podman machine` lifecycle still needs a real VM provider (/dev/kvm or macOS
10
+ # virtualization) and stays in DEVELOPMENT.md, deliberately out of shared-runner CI.
11
+ #
12
+ # Split out from foldyard.yml so the fast lint/types/unit suite runs on every push (uniform with the
13
+ # other areas), while this heavy 25-min job is OPT-IN — mirroring platform-e2e. The workflow triggers
14
+ # on every push, but the job only runs on `main` (post-merge net), on a manual `workflow_dispatch`,
15
+ # or when a commit message carries `[run-e2e]` (the pre-merge opt-in you add when a branch is ready
16
+ # for the full check). On an ordinary feature-branch push the job is simply skipped — near-zero cost.
17
+ # Deliberately excluded from the CI gate (ci-gate.yml) as too heavy, so it's advisory and never
18
+ # blocks the required check.
19
+ on:
20
+ push: # all branches; the job-level `if` decides whether to run
21
+ workflow_dispatch: # allow manual runs
22
+
23
+ concurrency:
24
+ group: foldyard-e2e-${{ github.ref }}
25
+ cancel-in-progress: true
26
+
27
+ jobs:
28
+ live-e2e:
29
+ name: live e2e (example stack + proxy + box, on Docker)
30
+ # Opt-in (mirrors platform-e2e): main, a manual dispatch, or a `[run-e2e]` commit message.
31
+ if: >-
32
+ github.ref == 'refs/heads/main'
33
+ || contains(github.event.head_commit.message, '[run-e2e]')
34
+ || github.event_name == 'workflow_dispatch'
35
+ runs-on: ubuntu-latest
36
+ timeout-minutes: 25
37
+ steps:
38
+ # Repo-only read: don't leave the GitHub token in .git/config. This job bind-mounts the
39
+ # workspace into a sibling container, so a persisted token would ride into it — and nothing
40
+ # here pushes. persist-credentials: false keeps the checkout but drops the credential.
41
+ - uses: actions/checkout@v6
42
+ with:
43
+ persist-credentials: false
44
+
45
+ # Warm the host Docker cache, in PARALLEL, so neither the `docker run` below (the uv image)
46
+ # nor the in-test pulls (the api base, the box image, postgres, alpine) wait on a serial pull.
47
+ # python:3.12-slim is the example api's FROM: without it warm, `foldyard up`'s buildkit does a
48
+ # live `load metadata for python:3.12-slim` against Docker Hub, which intermittently 502s and
49
+ # fails the build hard (no buildkit retry). Pre-pulling it (with retry) lets the in-test
50
+ # `docker compose build` resolve the base from the local cache instead of the registry.
51
+ - name: Pre-pull e2e images (parallel)
52
+ run: |
53
+ pull() { for i in 1 2 3 4 5; do docker pull "$1" && return 0; sleep $((i * 5)); done; return 1; }
54
+ for img in \
55
+ ghcr.io/astral-sh/uv:python3.12-bookworm \
56
+ quay.io/podman/stable:latest \
57
+ docker.io/library/python:3.12-slim \
58
+ docker.io/library/postgres:16-alpine \
59
+ docker.io/library/alpine:latest; do
60
+ pull "$img" &
61
+ done
62
+ wait
63
+
64
+ # The proxy/box live e2es need the test process to run INSIDE a container so a sibling box can
65
+ # reach the in-process proxy/emulator by container IP. Use docker CLI only (no podman), mount
66
+ # the host socket, and keep the repo at the same path so daemon-side bind mounts resolve.
67
+ # IN_DEVBOX=1 is the box signature `config.in_box()` keys on — a preset DOCKER_HOST alone is
68
+ # NOT one (Linux/WSL2 hosts export a socket too). It is what stands the launch gates down
69
+ # here exactly as in the dev box: preflight's fail-closed default-backend block, the
70
+ # config-adopt gate and `machine.ensure` all skip in-box, because there is no VM to manage
71
+ # and nothing host-side to adopt.
72
+ - name: live e2e inside a docker-CLI container
73
+ run: |
74
+ docker run --rm \
75
+ -v /var/run/docker.sock:/var/run/docker.sock \
76
+ -v "$GITHUB_WORKSPACE:$GITHUB_WORKSPACE" \
77
+ -w "$GITHUB_WORKSPACE" \
78
+ -e FOLDYARD_E2E=1 \
79
+ -e IN_DEVBOX=1 \
80
+ -e DOCKER_HOST=unix:///var/run/docker.sock \
81
+ ghcr.io/astral-sh/uv:python3.12-bookworm \
82
+ bash -lc '
83
+ set -euo pipefail
84
+ apt-get update -qq
85
+ apt-get install -y -qq --no-install-recommends docker.io curl ca-certificates >/dev/null
86
+ # docker compose v2 plugin (the example-stack e2e shells `docker compose`); the static
87
+ # release binary, dropped into both plugin dirs the docker.io CLI searches.
88
+ for d in /usr/local/lib/docker/cli-plugins /usr/libexec/docker/cli-plugins; do
89
+ mkdir -p "$d"
90
+ curl -fsSL https://github.com/docker/compose/releases/download/v2.30.3/docker-compose-linux-x86_64 \
91
+ -o "$d/docker-compose"
92
+ chmod +x "$d/docker-compose"
93
+ done
94
+ docker compose version
95
+ uv run --group e2e pytest tests -k e2e -v
96
+ '
97
+
98
+ native-host-e2e:
99
+ name: native host e2e (example + worktree, on Podman)
100
+ # Same opt-in policy as the heavier Docker topology.
101
+ if: >-
102
+ github.ref == 'refs/heads/main'
103
+ || contains(github.event.head_commit.message, '[run-e2e]')
104
+ || github.event_name == 'workflow_dispatch'
105
+ runs-on: ubuntu-latest
106
+ timeout-minutes: 25
107
+ env:
108
+ FOLDYARD_E2E: "1"
109
+ FOLDYARD_ENGINE: podman
110
+ MACHINE_BACKEND: native
111
+ FOLDYARD_STATE_DIR: /tmp/foldyard-state
112
+ # NB: XDG_RUNTIME_DIR is set by the "Enable systemd --user session" step below to
113
+ # /run/user/$UID (NOT a /tmp path), so rootless podman can find the user systemd manager
114
+ # that schedules healthcheck timers. The native socket path derives from it
115
+ # (machine_backend.NativeBackend._socket_path → $XDG_RUNTIME_DIR/podman/podman.sock).
116
+ steps:
117
+ # Repo-only read (no push) — keep the token out of .git/config.
118
+ - uses: actions/checkout@v6
119
+ with:
120
+ persist-credentials: false
121
+ - uses: astral-sh/setup-uv@v8.2.0
122
+
123
+ # Rootless podman schedules a container's healthcheck INTERVALS via `systemd --user`
124
+ # transient timers. On a GH runner the `runner` user has no login session — so no user
125
+ # systemd manager and no /run/user/$UID — and the old custom XDG_RUNTIME_DIR=/tmp/... made
126
+ # it strictly worse (podman then can't find ANY user systemd). With no timer the example db's
127
+ # healthcheck never re-runs, its health stays `starting`, and compose's `depends_on:
128
+ # service_healthy` (the api → db) blocks `foldyard up` FOREVER — the job hung to its 25-min
129
+ # timeout. enable-linger starts user@$UID.service + creates /run/user/$UID; we point
130
+ # XDG_RUNTIME_DIR + the D-Bus session there so podman's `systemd-run --user` reaches it.
131
+ - name: Enable systemd --user session (rootless podman healthcheck timers)
132
+ run: |
133
+ set -euo pipefail
134
+ sudo loginctl enable-linger "$(id -un)"
135
+ uid="$(id -u)"
136
+ for _ in $(seq 1 30); do
137
+ test -S "/run/user/$uid/bus" && break
138
+ sleep 1
139
+ done
140
+ {
141
+ echo "XDG_RUNTIME_DIR=/run/user/$uid"
142
+ echo "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus"
143
+ } >> "$GITHUB_ENV"
144
+ # Sanity: the user manager is reachable (degraded is fine — we only need the timer path).
145
+ XDG_RUNTIME_DIR="/run/user/$uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" \
146
+ systemctl --user is-system-running || true
147
+
148
+ - name: Install native Podman backend
149
+ run: |
150
+ set -euo pipefail
151
+ sudo apt-get update -qq
152
+ sudo apt-get install -y -qq --no-install-recommends \
153
+ podman podman-compose uidmap slirp4netns fuse-overlayfs
154
+ mkdir -p "$XDG_RUNTIME_DIR/podman"
155
+ chmod 700 "$XDG_RUNTIME_DIR"
156
+ podman system service --time=0 "unix://$XDG_RUNTIME_DIR/podman/podman.sock" \
157
+ >"$RUNNER_TEMP/podman-service.log" 2>&1 &
158
+ echo "$!" > "$RUNNER_TEMP/podman-service.pid"
159
+ for _ in $(seq 1 20); do
160
+ if test -S "$XDG_RUNTIME_DIR/podman/podman.sock" && podman info >/dev/null 2>&1; then
161
+ break
162
+ fi
163
+ sleep 1
164
+ done
165
+ podman info
166
+ podman compose version || podman-compose --version
167
+
168
+ - name: Pre-pull native e2e images
169
+ run: |
170
+ set -euo pipefail
171
+ for img in \
172
+ docker.io/library/python:3.12-slim \
173
+ docker.io/library/postgres:16-alpine \
174
+ docker.io/library/alpine:latest; do
175
+ podman pull "$img" &
176
+ done
177
+ wait
178
+
179
+ # Prove the systemd --user timer path works BEFORE the e2e, so a broken healthcheck fails
180
+ # here in ~20s with a clear message instead of hanging `foldyard up`'s depends_on to the
181
+ # 25-min job timeout. A container with a trivial healthcheck must flip to `healthy`.
182
+ - name: Verify rootless healthchecks fire (fail fast, not a 25-min hang)
183
+ run: |
184
+ set -euo pipefail
185
+ cid=$(podman run -d --health-cmd 'true' --health-interval 2s --health-retries 3 \
186
+ docker.io/library/alpine:latest sleep 300)
187
+ state=""
188
+ for _ in $(seq 1 15); do
189
+ state=$(podman inspect -f '{{.State.Health.Status}}' "$cid" 2>/dev/null || echo "")
190
+ [ "$state" = "healthy" ] && break
191
+ sleep 2
192
+ done
193
+ podman rm -f "$cid" >/dev/null 2>&1 || true
194
+ if [ "$state" != "healthy" ]; then
195
+ echo "✗ rootless podman healthchecks are NOT firing (last state: '${state:-none}')."
196
+ echo " The example db's health would never flip → foldyard up hangs on depends_on."
197
+ echo " Fix the systemd --user wiring (enable-linger / XDG_RUNTIME_DIR / DBUS above)."
198
+ exit 1
199
+ fi
200
+ echo "✓ rootless healthchecks fire — the example db will reach healthy."
201
+
202
+ - name: Native host e2e
203
+ run: uv run pytest tests/test_e2e.py -v
204
+
205
+ - name: Native backend diagnostics
206
+ if: always()
207
+ run: |
208
+ podman ps -a || true
209
+ cat "$RUNNER_TEMP/podman-service.log" || true
@@ -0,0 +1,26 @@
1
+ name: foldyard
2
+
3
+ # foldyard's fast lint/types/unit suite (no container engine, no KVM). Runs on every push that
4
+ # touches foldyard, on ALL branches — uniform with the other areas' lint/test workflows
5
+ # (platform-test, infra-lint, contracts) so the CI gate (ci-gate.yml, PR-only) reliably finds this
6
+ # check across the PR's commits. The heavy live Docker e2es are split out into foldyard-e2e.yml,
7
+ # which stays PR + post-merge scoped.
8
+ on:
9
+ push:
10
+
11
+ concurrency:
12
+ group: foldyard-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ check:
17
+ name: lint + types + unit
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 10
20
+ steps:
21
+ - uses: actions/checkout@v6
22
+ - uses: extractions/setup-just@v4
23
+ - uses: astral-sh/setup-uv@v8.2.0
24
+ # ruff (lint + format-check) + pyright + ty + the unit/golden/TUI suite. No engine. typecheck
25
+ # installs the `e2e` group so the opt-in proxy/box e2e tests resolve cryptography/requests.
26
+ - run: just check
@@ -0,0 +1,53 @@
1
+ name: release
2
+
3
+ # Tag → build → publish to PyPI. Lives under foldyard/.github/ in the origin monorepo so that repo
4
+ # never runs it; in the standalone repo it is the .github/workflows/release.yml at the root.
5
+ #
6
+ # Trusted publishing: PyPI is told to trust THIS workflow in THIS repo (project settings →
7
+ # Publishing → add a GitHub publisher: owner twistco, repo foldyard, workflow release.yml,
8
+ # environment pypi). No API token is stored anywhere; `uv publish` exchanges the job's OIDC
9
+ # token for a short-lived upload token. Build and publish are separate jobs so the publish job
10
+ # holds the id-token permission and nothing else, and never runs project code.
11
+ on:
12
+ push:
13
+ tags: ["v*"]
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 10
19
+ steps:
20
+ - uses: actions/checkout@v6
21
+ with:
22
+ persist-credentials: false
23
+ - uses: astral-sh/setup-uv@v8.2.0
24
+ - name: Tag must match pyproject version
25
+ run: |
26
+ set -euo pipefail
27
+ tag="${GITHUB_REF_NAME#v}"
28
+ version="$(uv version --short)"
29
+ if [ "$tag" != "$version" ]; then
30
+ echo "✗ tag v$tag does not match [project].version = $version — bump pyproject first"
31
+ exit 1
32
+ fi
33
+ - run: uv build
34
+ - uses: actions/upload-artifact@v4
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+ if-no-files-found: error
39
+
40
+ publish:
41
+ needs: build
42
+ runs-on: ubuntu-latest
43
+ timeout-minutes: 10
44
+ environment: pypi
45
+ permissions:
46
+ id-token: write
47
+ steps:
48
+ - uses: astral-sh/setup-uv@v8.2.0
49
+ - uses: actions/download-artifact@v4
50
+ with:
51
+ name: dist
52
+ path: dist/
53
+ - run: uv publish
@@ -0,0 +1,21 @@
1
+ # VM-visible scratch the box+proxy e2e mounts into a sibling box (cleaned up per run)
2
+ .e2e-tmp/
3
+
4
+ # hypothesis example database (property-based tests)
5
+ .hypothesis/
6
+
7
+ # uv-managed virtualenv + lockfile churn / build artefacts
8
+ .venv/
9
+ __pycache__/
10
+ *.pyc
11
+ .pytest_cache/
12
+ dist/
13
+ build/
14
+ *.egg-info/
15
+
16
+ # Covered by the monorepo root .gitignore today; listed here so the standalone repo has them too.
17
+ .DS_Store
18
+ .ruff_cache/
19
+ # Per-developer overrides (deep-merged over foldyard.toml — documented by foldyard.local.toml.example)
20
+ foldyard.local.toml
21
+ .claude/settings.local.*
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ Notable changes, per release. Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
4
+ versions follow [SemVer](https://semver.org/) once past `1.0`. Before that, minor versions may
5
+ break config or CLI shape, and say so here.
6
+
7
+ ## Unreleased
8
+
9
+ This repository is a force-pushed snapshot of the foldyard tree in its origin monorepo until
10
+ `0.1.0` (see `CONTRIBUTING.md`).
11
+
12
+ ## 0.1.0 — unreleased
13
+
14
+ - The first release intended for general use.
15
+
16
+ ## 0.0.1 — 2026-09-06
17
+
18
+ First published release, and an **alpha**. The engine has run daily inside Twist's monorepo for
19
+ months, but this is its first life as a standalone package — cut early to establish the name on
20
+ PyPI and to exercise the release path before it matters. Expect the config and CLI shape to move
21
+ before `0.1.0`; see the Status section of `README.md` for what is and isn't validated (the host
22
+ side is exercised on macOS only).
23
+
24
+ - Extracted from Twist's monorepo as a squash-start ([ADR-0013](./docs/adrs/0013-in-repo-carve-out-until-extraction.md)).
25
+ - Consumption is PyPI-first ([ADR-0020](./docs/adrs/0020-post-extraction-consumption-model.md),
26
+ amended): `uv tool install foldyard`. Published by tag from the release workflow via PyPI
27
+ trusted publishing — no API token exists anywhere.
@@ -0,0 +1 @@
1
+ DEVELOPMENT.md
@@ -0,0 +1,30 @@
1
+ # Contributing to foldyard
2
+
3
+ Thanks for looking. Two things to know before you start.
4
+
5
+ **Where development happens.** Until `0.1.0` this repository is a *snapshot*: foldyard is still
6
+ developed inside the monorepo it is being extracted from, and `main` here is re-created from it
7
+ (one squash commit, force-pushed) as that work lands. Issues are welcome and read; a pull
8
+ request will be ported by hand rather than merged, and its history will not survive. After
9
+ `0.1.0` this becomes the home and that caveat goes away — `CHANGELOG.md` says which side of the
10
+ line we are on.
11
+
12
+ **How to work on it.** [`DEVELOPMENT.md`](./DEVELOPMENT.md) is the guide: the module map, the
13
+ test tiers (what runs anywhere, what needs an engine, what only runs on a real host), the
14
+ conventions, and the don't-break list. The short version:
15
+
16
+ ```bash
17
+ uv tool install --force --editable ".[host]" # the `fy` on your PATH runs your checkout
18
+ just check # ruff + pyright + ty + the suite — what CI gates on
19
+ just test -k <pattern> # one area
20
+ ```
21
+
22
+ - Design decisions live in [`docs/adrs/`](./docs/adrs/). A change that reverses one needs a new
23
+ ADR that supersedes it, not an edit to the old one.
24
+ - New property-based tests are validated **red first** — mutate the code, watch hypothesis find
25
+ the counterexample, revert. Commit before you mutate.
26
+ - Write "host", not "Mac"; the host being macOS is a fact about today's users, not the design.
27
+ - Security-relevant findings go to the address in [`SECURITY.md`](./SECURITY.md), not an issue.
28
+
29
+ By contributing you agree that your contributions are licensed under the Apache License 2.0
30
+ that covers the project (`LICENSE`).