@mriekki/project-forge 4.8.1 → 4.10.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.8.1",
4
+ "version": "4.10.0",
5
5
  "defaultProfile": "greenfield-project",
6
6
  "profiles": {
7
7
  "greenfield-project": {
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "components": {
15
15
  "core": {
16
- "revision": "a78c2482f4f559ead8efbab6c25b4b05496db735",
16
+ "revision": "8796fd8dc22df8b64078e5b460e1008a8c7a5f7d",
17
17
  "version": "4.0.0",
18
18
  "distribution": "greenfield-workspace",
19
19
  "descriptor": "assets/core/distribution.json",
@@ -21,7 +21,7 @@
21
21
  "canonicalDescriptor": "distributions/greenfield-workspace.json"
22
22
  },
23
23
  "deployment": {
24
- "revision": "aaf5d24c897b3d89b3d1791c79bed4979414ca40",
24
+ "revision": "23799aec3e7c9a99873aaf912b040d43658fa4fb",
25
25
  "distribution": "greenfield-project",
26
26
  "descriptor": "assets/deployment/distribution.json",
27
27
  "sourceRoot": "assets/generated/deployment",
@@ -16,6 +16,9 @@
16
16
  {"source": "src/project_forge_deployment/box_platform.py", "destination": "tools/project-forge-deployment/project_forge_deployment/box_platform.py"},
17
17
  {"source": "src/project_forge_deployment/box_platform_assets.py", "destination": "tools/project-forge-deployment/project_forge_deployment/box_platform_assets.py"},
18
18
  {"source": "src/project_forge_deployment/box_platform_remote.py", "destination": "tools/project-forge-deployment/project_forge_deployment/box_platform_remote.py"},
19
+ {"source": "src/project_forge_deployment/cloudflare_api.py", "destination": "tools/project-forge-deployment/project_forge_deployment/cloudflare_api.py"},
20
+ {"source": "src/project_forge_deployment/cloudflare_ingress.py", "destination": "tools/project-forge-deployment/project_forge_deployment/cloudflare_ingress.py"},
21
+ {"source": "src/project_forge_deployment/cloudflare_ingress_remote.py", "destination": "tools/project-forge-deployment/project_forge_deployment/cloudflare_ingress_remote.py"},
19
22
  {"source": "src/project_forge_deployment/cli.py", "destination": "tools/project-forge-deployment/project_forge_deployment/cli.py"},
20
23
  {"source": "src/project_forge_deployment/doctor.py", "destination": "tools/project-forge-deployment/project_forge_deployment/doctor.py"},
21
24
  {"source": "src/project_forge_deployment/entrypoint.py", "destination": "tools/project-forge-deployment/project_forge_deployment/entrypoint.py"},
@@ -26,6 +29,7 @@
26
29
  {"source": "src/project_forge_deployment/errors.py", "destination": "tools/project-forge-deployment/project_forge_deployment/errors.py"},
27
30
  {"source": "src/project_forge_deployment/home_lab.py", "destination": "tools/project-forge-deployment/project_forge_deployment/home_lab.py"},
28
31
  {"source": "src/project_forge_deployment/image_transfer.py", "destination": "tools/project-forge-deployment/project_forge_deployment/image_transfer.py"},
32
+ {"source": "src/project_forge_deployment/ingress_cli.py", "destination": "tools/project-forge-deployment/project_forge_deployment/ingress_cli.py"},
29
33
  {"source": "src/project_forge_deployment/keys.py", "destination": "tools/project-forge-deployment/project_forge_deployment/keys.py"},
30
34
  {"source": "src/project_forge_deployment/local_cli.py", "destination": "tools/project-forge-deployment/project_forge_deployment/local_cli.py"},
31
35
  {"source": "src/project_forge_deployment/local_runtime.py", "destination": "tools/project-forge-deployment/project_forge_deployment/local_runtime.py"},
@@ -52,6 +56,7 @@
52
56
  {"source": "src/project_forge_deployment/teardown_remote.py", "destination": "tools/project-forge-deployment/project_forge_deployment/teardown_remote.py"},
53
57
  {"source": "src/project_forge_deployment/topology_cli.py", "destination": "tools/project-forge-deployment/project_forge_deployment/topology_cli.py"},
54
58
  {"source": "src/project_forge_deployment/trust.py", "destination": "tools/project-forge-deployment/project_forge_deployment/trust.py"},
59
+ {"source": "docs/CLOUDFLARE_INGRESS.md", "destination": "docs/project-forge/CLOUDFLARE_INGRESS.md"},
55
60
  {"source": "docs/DEVELOPER_ENVIRONMENTS.md", "destination": "docs/project-forge/DEVELOPER_ENVIRONMENTS.md"},
56
61
  {"source": "docs/WORKSPACE_INSTANCES.md", "destination": "docs/project-forge/WORKSPACE_INSTANCES.md"},
57
62
  {"source": "docs/ENVIRONMENT_PROOF.md", "destination": "docs/project-forge/ENVIRONMENT_PROOF.md"},
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.8.1",
4
+ "version": "4.10.0",
5
5
  "components": {
6
6
  "core": {
7
- "revision": "a78c2482f4f559ead8efbab6c25b4b05496db735",
7
+ "revision": "8796fd8dc22df8b64078e5b460e1008a8c7a5f7d",
8
8
  "version": "4.0.0",
9
9
  "distribution": "greenfield-workspace",
10
10
  "files": [
@@ -126,7 +126,7 @@
126
126
  {
127
127
  "source": "core/skills/tear-down/SKILL.md",
128
128
  "destination": ".github/skills/tear-down/SKILL.md",
129
- "sha256": "7565b5600af2883abfc47bac29abfd056cfa81536c52408b07da82f696b335b8"
129
+ "sha256": "468314030c6033885c46aba50f54410928cbe44c74d73b998b88338fe495f3d9"
130
130
  },
131
131
  {
132
132
  "source": "core/skills/coach/SKILL.md",
@@ -326,7 +326,7 @@
326
326
  ]
327
327
  },
328
328
  "deployment": {
329
- "revision": "aaf5d24c897b3d89b3d1791c79bed4979414ca40",
329
+ "revision": "23799aec3e7c9a99873aaf912b040d43658fa4fb",
330
330
  "distribution": "greenfield-project",
331
331
  "files": [
332
332
  {
@@ -394,6 +394,21 @@
394
394
  "destination": "tools/project-forge-deployment/project_forge_deployment/box_platform_remote.py",
395
395
  "sha256": "65de94ac4b6e980d83ce99ea55b9555aa96959c11808cce8c6ed537b27c15533"
396
396
  },
397
+ {
398
+ "source": "src/project_forge_deployment/cloudflare_api.py",
399
+ "destination": "tools/project-forge-deployment/project_forge_deployment/cloudflare_api.py",
400
+ "sha256": "de383926e04f6ebb3b8fd95948b475a9dc40d8da01ff2499afbe0f3a7305d4df"
401
+ },
402
+ {
403
+ "source": "src/project_forge_deployment/cloudflare_ingress.py",
404
+ "destination": "tools/project-forge-deployment/project_forge_deployment/cloudflare_ingress.py",
405
+ "sha256": "98c77950df1d0bba36da1244001bdd157251238778e7068be0c6c20d2683d2d4"
406
+ },
407
+ {
408
+ "source": "src/project_forge_deployment/cloudflare_ingress_remote.py",
409
+ "destination": "tools/project-forge-deployment/project_forge_deployment/cloudflare_ingress_remote.py",
410
+ "sha256": "35ac39b4f4538d4ff23f68982cc4b176782378cf03539fa51a228becbdfcf4d0"
411
+ },
397
412
  {
398
413
  "source": "src/project_forge_deployment/cli.py",
399
414
  "destination": "tools/project-forge-deployment/project_forge_deployment/cli.py",
@@ -407,7 +422,7 @@
407
422
  {
408
423
  "source": "src/project_forge_deployment/entrypoint.py",
409
424
  "destination": "tools/project-forge-deployment/project_forge_deployment/entrypoint.py",
410
- "sha256": "6b767380f502acf0763b71f46aff6efd62674e63a526ed270097efa68c320a1e"
425
+ "sha256": "021434f8f601ebbff0aae354e6c6b25b36d598a521a7948d02d5f1cc797c22fd"
411
426
  },
412
427
  {
413
428
  "source": "src/project_forge_deployment/environment_cli.py",
@@ -444,6 +459,11 @@
444
459
  "destination": "tools/project-forge-deployment/project_forge_deployment/image_transfer.py",
445
460
  "sha256": "dadd1fe7724372a4ca0e04ab2280769b6c2381f0f9ae75a096f6c5a5f5add755"
446
461
  },
462
+ {
463
+ "source": "src/project_forge_deployment/ingress_cli.py",
464
+ "destination": "tools/project-forge-deployment/project_forge_deployment/ingress_cli.py",
465
+ "sha256": "0e4088a17734f560c750528942fbd34d2464bcdd06d60467a9dc684a73fc18e1"
466
+ },
447
467
  {
448
468
  "source": "src/project_forge_deployment/keys.py",
449
469
  "destination": "tools/project-forge-deployment/project_forge_deployment/keys.py",
@@ -552,17 +572,17 @@
552
572
  {
553
573
  "source": "src/project_forge_deployment/teardown.py",
554
574
  "destination": "tools/project-forge-deployment/project_forge_deployment/teardown.py",
555
- "sha256": "08215b8b5fa87ca8abf96f10f38aca0207840bd1b1faf4c2c53cb691caf20a72"
575
+ "sha256": "0c75bb1ec77218d1b6ee3f54a626556ee46f2de8e91d1e9afa87200bce39254e"
556
576
  },
557
577
  {
558
578
  "source": "src/project_forge_deployment/teardown_cli.py",
559
579
  "destination": "tools/project-forge-deployment/project_forge_deployment/teardown_cli.py",
560
- "sha256": "214987895cde77648b5274042f1c21557046c3bf76224bfdd415ea34fec3d873"
580
+ "sha256": "5dbe92b1d5683824bbf3b4892f9099db2d68ad95c0a514bf73f7b596a62775be"
561
581
  },
562
582
  {
563
583
  "source": "src/project_forge_deployment/teardown_remote.py",
564
584
  "destination": "tools/project-forge-deployment/project_forge_deployment/teardown_remote.py",
565
- "sha256": "ad764f5a67c43f81b4973dd5639b2c28b23ae883a85e5ae29efc14dad506e88e"
585
+ "sha256": "5a898ee8ce981c60839d12c63a776bf742e2c32bc409fbb29cdfd798cf9baa19"
566
586
  },
567
587
  {
568
588
  "source": "src/project_forge_deployment/topology_cli.py",
@@ -574,6 +594,11 @@
574
594
  "destination": "tools/project-forge-deployment/project_forge_deployment/trust.py",
575
595
  "sha256": "b143fa828c3a23df3909dacfd78ae56b9ec1159628eb45261d69c0aa6ddd1a7b"
576
596
  },
597
+ {
598
+ "source": "docs/CLOUDFLARE_INGRESS.md",
599
+ "destination": "docs/project-forge/CLOUDFLARE_INGRESS.md",
600
+ "sha256": "e0d95aa8e1c204390a51a7900ef60542537c1a9d2eeb017f34920e8dcb5e8329"
601
+ },
577
602
  {
578
603
  "source": "docs/DEVELOPER_ENVIRONMENTS.md",
579
604
  "destination": "docs/project-forge/DEVELOPER_ENVIRONMENTS.md",
@@ -1,27 +1,52 @@
1
1
  ---
2
2
  name: tear-down
3
- description: "Guided destructive cleanup for local state, one exact repository-defined named environment, or all explicitly configured Project environments. Preserves ownership boundaries and resolved Production-data authorization."
4
- argument-hint: "[local|<environment>|all]"
3
+ description: "List target-owned Project environments and guide destructive cleanup for local state, one exact named environment, or all explicitly configured Project environments. Preserves ownership boundaries, shared Project Forge infrastructure, and separate Production/public-ingress authorization."
4
+ argument-hint: "[list [<target>]|local|<environment>|all]"
5
5
  user-invocable: true
6
6
  disable-model-invocation: true
7
7
  ---
8
8
 
9
9
  # Tear Down
10
10
 
11
- `/tear-down` is the human-facing destructive cleanup workflow for repository-owned Project state.
11
+ `/tear-down` is the human-facing inventory and destructive-cleanup workflow for Project-owned deployment state.
12
12
 
13
13
  Logical environment names are data-driven and are not limited to Staging/Production:
14
14
 
15
15
  ```text
16
+ /tear-down list production
16
17
  /tear-down demo
17
18
  /tear-down qa
18
19
  /tear-down staging
19
20
  /tear-down prod-east
20
21
  ```
21
22
 
22
- The skill owns exact scope selection, read-only preflight, destructive preview, and Human-in-the-Loop authorization. Repository-provided deterministic tooling owns deletion mechanics.
23
+ The skill owns target selection, exact scope selection, read-only inventory/preflight, destructive preview, and Human-in-the-Loop authorization. Deployment Kit deterministic tooling owns inventory and deletion mechanics.
23
24
 
24
- ## 1. Resolve exact project topology
25
+ ## 0. List what is installed before destructive selection
26
+
27
+ When the Builder asks what is deployed, what can be removed, or invokes `list`, use the Deployment Kit read-only target inventory:
28
+
29
+ ```text
30
+ project-forge-deploy target inventory <target> --json
31
+ ```
32
+
33
+ This is the canonical application-cleanup inventory. It correlates the target's managed runtime, trusted-LAN route, and managed public-ingress ownership into application/environment tear-down candidates while reporting the shared host platform separately.
34
+
35
+ The command is intentionally independent of the consumer repository. If a consumer checkout or even its GitHub repository has already been deleted, the same installed/standalone Deployment Kit command may still inventory the target from host-owned metadata and user-scoped target access configuration.
36
+
37
+ Inside a consumer repository, use the repository-provided Deployment Kit launcher when that is the installed contract. Detect the current platform/runtime and invoke the portable Python entrypoint or installed `project-forge-deploy`; do not assume PowerShell syntax or Windows-only paths. macOS/Linux shells and Windows/PowerShell must reach the same deterministic CLI semantics.
38
+
39
+ Do not reconstruct inventory by ad-hoc `ssh`, `docker ps`, filesystem listing, or Cloudflare commands when Deployment Kit target inventory is available.
40
+
41
+ If a public-ingress scope cannot be inferred from the application/environment name, pass only the exact additional scope established by repository topology or trusted operator evidence:
42
+
43
+ ```text
44
+ --ingress-scope <scope>
45
+ ```
46
+
47
+ Listing is read-only. It does not authorize later deletion.
48
+
49
+ ## 1. Resolve exact project topology when the repository exists
25
50
 
26
51
  Prefer:
27
52
 
@@ -37,8 +62,12 @@ If `demo` is configured, `/tear-down demo` means exactly `demo`. If `prod-east`
37
62
 
38
63
  Never guess a similar environment and never widen scope after failure.
39
64
 
65
+ When the repository no longer exists, use target inventory only for target-owned application identity and resource ownership. Do not invent repository-only topology facts. In particular, if a non-literal custom environment's Production classification cannot be established from trusted evidence, stop before destructive authorization rather than weakening the Production-data gate.
66
+
40
67
  ## 2. Select exact destructive scope
41
68
 
69
+ The destructive scope contract remains exactly `[local|<environment>|all]`; `list` is a separate read-only discovery mode and never becomes a destructive scope.
70
+
42
71
  ```text
43
72
  local
44
73
  <one exact repository-defined named environment>
@@ -47,13 +76,15 @@ all
47
76
 
48
77
  If no scope is named, present only scopes the repository actually supports; never silently default to a Production environment or `all`.
49
78
 
79
+ For orphaned deployments where the consumer repository is unavailable, present only exact application/environment candidates returned by target inventory.
80
+
50
81
  `all` means only all explicitly configured scopes for this Project; it never means machine-wide cleanup.
51
82
 
52
83
  Selecting scope does not authorize deletion. Physical colocation never merges ownership: deleting `demo` must not remove `staging` or `prod-east` merely because they share hardware.
53
84
 
54
- If the requested name is absent from project topology, stop. Do not substitute another environment.
85
+ If the requested name is absent from project topology or target inventory, stop. Do not substitute another environment.
55
86
 
56
- ## 3. Resolve Production classification from the deletion set
87
+ ## 3. Resolve Production and public-ingress classification from the deletion set
57
88
 
58
89
  For each selected environment identify:
59
90
 
@@ -62,6 +93,8 @@ environment name
62
93
  purpose / Production classification
63
94
  placement / target
64
95
  repository/application identity
96
+ trusted-LAN route
97
+ managed public-ingress scope/hostname, when present
65
98
  ownership markers required by installed tooling
66
99
  ```
67
100
 
@@ -71,7 +104,13 @@ For current Deployment Kit consumers, custom Production names may be passed expl
71
104
  --production-environment prod-east
72
105
  ```
73
106
 
74
- Use actual installed help/version evidence. If the installed tool cannot preserve a custom Production classification, stop rather than downgrading the destructive gate.
107
+ When target inventory reports an owned public route, preserve its exact environment-to-ingress-scope binding for cleanup, conceptually:
108
+
109
+ ```text
110
+ --public-ingress-scope production=production
111
+ ```
112
+
113
+ Use actual installed help/version evidence. If the installed tool cannot preserve a custom Production classification or exact public-ingress ownership, stop rather than downgrading the destructive gate.
75
114
 
76
115
  ## 4. Present an exact destructive preview
77
116
 
@@ -86,10 +125,17 @@ Selected environments:
86
125
  <name> → <placement/target> — <purpose/classification>
87
126
 
88
127
  Will remove:
89
- <only tool-owned resources for exact selected scopes>
128
+ owned public hostname/DNS + tunnel rule, when selected
129
+ owned trusted-LAN route
130
+ owned application container/runtime
131
+ owned releases, evidence, persistent data, and backups
132
+ remote immutable image only when no other managed release/container still references it
90
133
 
91
134
  Will preserve:
92
- <shared/unrelated environment and platform resources>
135
+ shared Project Forge Caddy/network/platform
136
+ shared Cloudflare tunnel/runtime
137
+ unrelated Projects/environments
138
+ remote images still referenced by another managed release/container
93
139
  ```
94
140
 
95
141
  Do not promise deletion of named/shared Docker volumes unless the installed deterministic contract proves exact ownership and explicitly includes them. Do not promise deletion of any shared/unrelated resource whose ownership cannot be proven.
@@ -108,33 +154,45 @@ whenever **any selected environment is Production-class**, require the stronger
108
154
  [PRODUCTION DATA DELETE]
109
155
  ```
110
156
 
157
+ Whenever the reviewed deletion set includes an owned public-ingress route, also require the separate public-ingress removal authorization, conceptually:
158
+
159
+ ```text
160
+ [PUBLIC INGRESS ROUTE REMOVE]
161
+ ```
162
+
111
163
  Resolve exact phrases from the installed tool/version. Do not tie the Production-data gate only to literal scope strings such as `production` or `all`; it depends on the actual resolved deletion set.
112
164
 
113
- A prior `/setup`, environment proof, `/deploy`, merge, CI, or other approval never authorizes destructive cleanup.
165
+ A prior `/setup`, environment proof, `/deploy`, merge, CI, public-route registration, or other approval never authorizes destructive cleanup.
114
166
 
115
- If identity, selected environment set, classification, binding, ownership evidence, or deletion set changes after preview, require renewed review/authorization.
167
+ If identity, selected environment set, classification, binding, ownership evidence, public-ingress scope, or deletion set changes after preview, require renewed review/authorization.
116
168
 
117
- ## 6. Execute only repository-provided deterministic cleanup
169
+ ## 6. Execute only Deployment Kit deterministic cleanup
118
170
 
119
171
  After authorization, invoke only the approved tear-down command with exact named scopes/bindings. Do not improvise shell commands.
120
172
 
121
173
  For a generalized current Deployment Kit invocation, exact bindings may look conceptually like:
122
174
 
123
175
  ```text
124
- --environment-target demo=<target>
125
176
  --environment-target staging=<target>
126
- --environment-target prod-east=<target>
177
+ --environment-target production=<target>
178
+ --public-ingress-scope production=production
127
179
  --production-environment prod-east
128
180
  ```
129
181
 
130
- Use the installed contract rather than inventing flags for older versions.
182
+ Use target inventory output and the installed contract rather than inventing flags for older versions.
131
183
 
132
- Never add machine-wide Docker prune, shared platform/network removal, unrelated environment cleanup, host reprovisioning, target rebinding, credential/trust deletion, or registry-package deletion unless a separate lifecycle operation explicitly owns and authorizes it.
184
+ Public ingress must be removed before the trusted-LAN route/runtime so a public hostname is never deliberately left pointing at a removed origin. The shared Cloudflare tunnel/runtime is host infrastructure and must remain.
185
+
186
+ Remote image cleanup is bounded: remove only the selected environment's immutable image when no other managed release metadata or container still references it. Never use Docker system/image/volume prune.
187
+
188
+ Never add machine-wide Docker prune, shared platform/network removal, unrelated environment cleanup, host reprovisioning, target rebinding, credential/trust deletion, registry-package deletion, or shared Cloudflare tunnel deletion unless a separate lifecycle operation explicitly owns and authorizes it.
133
189
 
134
190
  If aggregate `all` cleanup stops partway, report completed and remaining scopes. Do not manually compensate by deleting the rest.
135
191
 
136
192
  ## 7. Verify and report outcome
137
193
 
138
- Use repository-provided status/evidence commands when available. Report only observed evidence: requested scope, exact environment names/targets processed, Production-class environments included, local state processed, removed versus already-absent resources, and any remaining scopes after a partial stop.
194
+ Re-run target inventory after cleanup when available. Report only observed evidence: requested scope, exact environment names/targets processed, Production-class environments included, public routes removed/already absent, trusted-LAN routes removed/already absent, runtime/data removal, remote image removed/retained, local state processed, and any remaining scopes after a partial stop.
195
+
196
+ Shared Project Forge platform state and unrelated application candidates should remain present and unchanged.
139
197
 
140
198
  When the installed deterministic contract is idempotent, already-absent owned resources should remain safe to re-check/re-run without broadening scope.
@@ -0,0 +1,226 @@
1
+ # Cloudflare Tunnel public ingress
2
+
3
+ Status: **PARTIAL — IMPLEMENTED MECHANICS; LIVE DOMAIN/GENERATED-CONSUMER PROOF PENDING**
4
+
5
+ Project Forge models Cloudflare Tunnel as optional **public ingress**, not as a deployment target or remote-management transport.
6
+
7
+ The supported shape is:
8
+
9
+ ```text
10
+ placement: remote-target
11
+ adapter: remote-ssh
12
+ exposure: public
13
+ ingress provider: cloudflare-tunnel
14
+ ```
15
+
16
+ `remote-ssh` continues to own verified workstation-to-target management. Cloudflare Tunnel owns only internet-facing application ingress.
17
+
18
+ ## Architecture
19
+
20
+ ```text
21
+ Internet
22
+ -> Cloudflare edge / public DNS / HTTPS
23
+ -> remotely managed Cloudflare Tunnel
24
+ -> cloudflared container on the selected Ubuntu target
25
+ -> shared Project Forge caddy-net
26
+ -> project-forge-caddy:80
27
+ -> existing Project Forge application container
28
+ ```
29
+
30
+ The cloudflared runtime publishes no host ports. It initiates outbound connections to Cloudflare and joins the existing shared Docker network.
31
+
32
+ Public routes do **not** replace the trusted-LAN Route Registry. Each public Cloudflare hostname forwards to the shared Caddy container while setting `originRequest.httpHostHeader` to an already-owned `.local` Project Forge hostname. Caddy therefore reuses the existing Project/environment route and upstream ownership contract.
33
+
34
+ Example:
35
+
36
+ ```text
37
+ inventory.example.com
38
+ -> Cloudflare Tunnel
39
+ -> http://project-forge-caddy:80
40
+ Host: inventory-production.local
41
+ -> existing Caddy route
42
+ -> inventory Production container
43
+ ```
44
+
45
+ ## Shared versus Project-owned state
46
+
47
+ The shared ingress scope owns:
48
+
49
+ - one remotely managed Cloudflare Tunnel identity;
50
+ - one cloudflared container;
51
+ - the host-side tunnel token;
52
+ - the Cloudflare account/zone/tunnel non-secret identifiers;
53
+ - the connector Compose definition;
54
+ - a host-side public-route ownership registry.
55
+
56
+ A Project/environment public route owns:
57
+
58
+ - exact `ownerRepo`, environment, app slug and route ID;
59
+ - one exact public hostname;
60
+ - one exact existing `.local` origin hostname;
61
+ - one exact Cloudflare tunnel ingress rule;
62
+ - one exact proxied Cloudflare CNAME record.
63
+
64
+ Removing a Project route must preserve the shared tunnel, cloudflared runtime, Caddy platform, unrelated DNS records and unrelated Projects.
65
+
66
+ ## Secrets boundary
67
+
68
+ The Cloudflare API token is workstation-only. It is read from:
69
+
70
+ ```text
71
+ PROJECT_FORGE_CLOUDFLARE_API_TOKEN
72
+ ```
73
+
74
+ The token is never accepted as a CLI argument and is not written to:
75
+
76
+ - Git;
77
+ - `PROJECT_BOOTSTRAP.json`;
78
+ - `DEPLOYMENT_TOPOLOGY.json`;
79
+ - the portable Project Forge developer profile;
80
+ - the Production host;
81
+ - command-line process arguments.
82
+
83
+ The remotely managed Tunnel token is retrieved only when applying shared ingress. It is streamed through the existing verified SSH operation and written beneath the selected root-owned ingress scope. It is not printed or returned in plans/status.
84
+
85
+ Re-applying shared ingress force-recreates the cloudflared container after securely replacing the token file, so refreshed Tunnel credentials are actually consumed by the running connector.
86
+
87
+ ## Shared runtime commands
88
+
89
+ Read-only:
90
+
91
+ ```text
92
+ project-forge-deploy ingress cloudflare status <target> --scope production
93
+ project-forge-deploy ingress cloudflare doctor <target> --scope production
94
+ ```
95
+
96
+ Configure/create/adopt the shared scope after the domain exists:
97
+
98
+ ```text
99
+ project-forge-deploy ingress cloudflare configure <target> \
100
+ --scope production \
101
+ --zone example.com
102
+ ```
103
+
104
+ The write requires:
105
+
106
+ ```text
107
+ [HOST PUBLIC INGRESS WRITE]
108
+ ```
109
+
110
+ The default tunnel name is:
111
+
112
+ ```text
113
+ project-forge-<scope>
114
+ ```
115
+
116
+ An existing tunnel is adopted only when it is remotely managed and has no application ingress rules beyond the final fail-closed `http_status:404` rule. Project Forge never silently takes over a tunnel with existing application configuration.
117
+
118
+ ## Project public routes
119
+
120
+ Inventory:
121
+
122
+ ```text
123
+ project-forge-deploy ingress cloudflare route inventory <target> --scope production
124
+ ```
125
+
126
+ Register:
127
+
128
+ ```text
129
+ project-forge-deploy ingress cloudflare route register <target> \
130
+ --scope production \
131
+ --owner-repo owner/repository \
132
+ --environment production \
133
+ --app-slug inventory \
134
+ --public-hostname inventory.example.com \
135
+ --origin-hostname inventory-production.local
136
+ ```
137
+
138
+ Registration requires:
139
+
140
+ ```text
141
+ [PUBLIC INGRESS ROUTE WRITE]
142
+ ```
143
+
144
+ Remove:
145
+
146
+ ```text
147
+ project-forge-deploy ingress cloudflare route remove <target> \
148
+ --scope production \
149
+ --owner-repo owner/repository \
150
+ --environment production \
151
+ --app-slug inventory
152
+ ```
153
+
154
+ Removal requires:
155
+
156
+ ```text
157
+ [PUBLIC INGRESS ROUTE REMOVE]
158
+ ```
159
+
160
+ ## Route safety
161
+
162
+ Before publishing a hostname, Deployment Kit verifies:
163
+
164
+ 1. the selected shared Cloudflare ingress scope is ready;
165
+ 2. the exact Project/environment `.local` route already exists and owns the requested origin hostname;
166
+ 3. the public hostname is not owned by another managed Project route;
167
+ 4. the Cloudflare tunnel configuration has the expected final fail-closed 404 rule;
168
+ 5. no conflicting tunnel ingress rule exists;
169
+ 6. no conflicting DNS record exists;
170
+ 7. any existing DNS record exactly matches Project Forge's tunnel target and ownership marker.
171
+
172
+ The current partial adapter intentionally supports one first-level subdomain beneath the selected zone, such as `inventory.example.com`. Deeper names such as `api.inventory.example.com` require an explicit certificate/support review rather than being silently accepted.
173
+
174
+ ## Domain is deliberately late-bound
175
+
176
+ A Builder does not need a domain to generate or develop a Project.
177
+
178
+ Bootstrap may record:
179
+
180
+ ```text
181
+ exposure: public
182
+ ingress.provider: cloudflare-tunnel
183
+ ingress.bindingState: deferred
184
+ ```
185
+
186
+ without a zone or public hostname. Once a domain is registered and active in Cloudflare, environment setup/readiness can bind the shared ingress scope and each Project can register its exact public hostname. Regeneration is not required merely to choose the domain later.
187
+
188
+ The shared Cloudflare zone is environment infrastructure and should not be copied into every Project as a credential-like default. The exact application hostname remains Project/environment state.
189
+
190
+ ## Current cloudflared runtime
191
+
192
+ The first partial implementation selects Cloudflare release `2026.10.4` and pins the actual Docker pull to the verified multi-platform image index digest:
193
+
194
+ ```text
195
+ cloudflare/cloudflared:2026.10.4@sha256:c6c7ae1d5dfdcb094ad344ed68ccffc26635c9c6be80965664c33baf064b0515
196
+ ```
197
+
198
+ `latest` is never used, and a mutable tag is not the final runtime identity. Promotion from `partial` to `supported` remains blocked until the package/release process and unrelated generated consumers prove this pinned artifact together with the live Cloudflare integration path.
199
+
200
+ ## Required live proof before support promotion
201
+
202
+ At minimum:
203
+
204
+ 1. create/adopt a real remotely managed Tunnel without manually logging into the Ubuntu host;
205
+ 2. install cloudflared through verified Project Forge SSH and prove no inbound host/router port is required;
206
+ 3. publish a real first-level hostname and verify external HTTPS reaches the expected Project container;
207
+ 4. publish a second unrelated Project through the same shared scope and prove isolation/collision checks;
208
+ 5. remove one Project route while preserving the other Project and the shared tunnel;
209
+ 6. exercise DNS/provider failure rollback;
210
+ 7. exercise changed host/provider state and prove stale plans fail closed;
211
+ 8. prove separate logical Production/Staging scopes when both are configured;
212
+ 9. prove generated/default-branch CI and human workspace acceptance.
213
+
214
+ Tunnel connector health alone is not sufficient application evidence. Final Production readiness must include an external HTTPS application probe.
215
+
216
+ ## Deliberate V1 exclusions
217
+
218
+ - Cloudflare Access / Zero Trust identity policy;
219
+ - wildcard DNS publication for arbitrary applications;
220
+ - direct public router/firewall fallback;
221
+ - automatic domain purchase or registrar mutation;
222
+ - deeper-than-first-level public hostnames;
223
+ - implicit adoption of existing unmanaged Tunnel/DNS rules;
224
+ - deleting a shared Tunnel as part of normal Project tear-down.
225
+
226
+ These remain separate capabilities or future decisions.