@mriekki/project-forge 4.24.1 → 4.26.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.24.1",
4
+ "version": "4.26.0",
5
5
  "defaultProfile": "greenfield-project",
6
6
  "profiles": {
7
7
  "greenfield-project": {
@@ -13,15 +13,15 @@
13
13
  },
14
14
  "components": {
15
15
  "core": {
16
- "revision": "59f5657bbb6f1edc45d1e8619ab1d0875b7907e0",
17
- "version": "4.2.1",
16
+ "revision": "39bc7661db49266743317e9dfedab800674f9c30",
17
+ "version": "4.3.0",
18
18
  "distribution": "greenfield-workspace",
19
19
  "descriptor": "assets/core/distribution.json",
20
20
  "sourceRoot": "assets/generated/core",
21
21
  "canonicalDescriptor": "distributions/greenfield-workspace.json"
22
22
  },
23
23
  "deployment": {
24
- "revision": "3b31a8690720aa1eb8f5af27ddac5e0e6d3779ba",
24
+ "revision": "e9ca6e12e5b19f691b25ce0aba758a43c3df2913",
25
25
  "distribution": "greenfield-project",
26
26
  "descriptor": "assets/deployment/distribution.json",
27
27
  "sourceRoot": "assets/generated/deployment",
@@ -7,6 +7,7 @@
7
7
  {"source": "src/project_forge_deployment/__init__.py", "destination": "tools/project-forge-deployment/project_forge_deployment/__init__.py"},
8
8
  {"source": "src/project_forge_deployment/__main__.py", "destination": "tools/project-forge-deployment/project_forge_deployment/__main__.py"},
9
9
  {"source": "src/project_forge_deployment/access_cli.py", "destination": "tools/project-forge-deployment/project_forge_deployment/access_cli.py"},
10
+ {"source": "src/project_forge_deployment/agent_session.py", "destination": "tools/project-forge-deployment/project_forge_deployment/agent_session.py"},
10
11
  {"source": "src/project_forge_deployment/access_diagnosis.py", "destination": "tools/project-forge-deployment/project_forge_deployment/access_diagnosis.py"},
11
12
  {"source": "src/project_forge_deployment/app_cli.py", "destination": "tools/project-forge-deployment/project_forge_deployment/app_cli.py"},
12
13
  {"source": "src/project_forge_deployment/app_deployment.py", "destination": "tools/project-forge-deployment/project_forge_deployment/app_deployment.py"},
@@ -43,11 +44,13 @@
43
44
  {"source": "src/project_forge_deployment/profiles.py", "destination": "tools/project-forge-deployment/project_forge_deployment/profiles.py"},
44
45
  {"source": "src/project_forge_deployment/project_topology.py", "destination": "tools/project-forge-deployment/project_forge_deployment/project_topology.py"},
45
46
  {"source": "src/project_forge_deployment/proof_receipt.py", "destination": "tools/project-forge-deployment/project_forge_deployment/proof_receipt.py"},
47
+ {"source": "src/project_forge_deployment/proxy_peer.py", "destination": "tools/project-forge-deployment/project_forge_deployment/proxy_peer.py"},
46
48
  {"source": "src/project_forge_deployment/recovery_scope.py", "destination": "tools/project-forge-deployment/project_forge_deployment/recovery_scope.py"},
47
49
  {"source": "src/project_forge_deployment/registry_access.py", "destination": "tools/project-forge-deployment/project_forge_deployment/registry_access.py"},
48
50
  {"source": "src/project_forge_deployment/registry_cli.py", "destination": "tools/project-forge-deployment/project_forge_deployment/registry_cli.py"},
49
51
  {"source": "src/project_forge_deployment/route_registry.py", "destination": "tools/project-forge-deployment/project_forge_deployment/route_registry.py"},
50
52
  {"source": "src/project_forge_deployment/route_registry_remote.py", "destination": "tools/project-forge-deployment/project_forge_deployment/route_registry_remote.py"},
53
+ {"source": "src/project_forge_deployment/safe_diagnostics.py", "destination": "tools/project-forge-deployment/project_forge_deployment/safe_diagnostics.py"},
51
54
  {"source": "src/project_forge_deployment/security.py", "destination": "tools/project-forge-deployment/project_forge_deployment/security.py"},
52
55
  {"source": "src/project_forge_deployment/sqlite_adapter.py", "destination": "tools/project-forge-deployment/project_forge_deployment/sqlite_adapter.py"},
53
56
  {"source": "src/project_forge_deployment/sqlite_adapter_remote.py", "destination": "tools/project-forge-deployment/project_forge_deployment/sqlite_adapter_remote.py"},
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.24.1",
4
+ "version": "4.26.0",
5
5
  "components": {
6
6
  "core": {
7
- "revision": "59f5657bbb6f1edc45d1e8619ab1d0875b7907e0",
8
- "version": "4.2.1",
7
+ "revision": "39bc7661db49266743317e9dfedab800674f9c30",
8
+ "version": "4.3.0",
9
9
  "distribution": "greenfield-workspace",
10
10
  "files": [
11
11
  {
@@ -56,7 +56,7 @@
56
56
  {
57
57
  "source": "core/skills/setup/SKILL.md",
58
58
  "destination": ".github/skills/setup/SKILL.md",
59
- "sha256": "8f92aed1684a3dfd2ecffeb584b5da4daa9c7d7d7ab7ca7f01c8c6a4c82eaa1d"
59
+ "sha256": "30a9dbfbd132f7c333ca515736a7957494bbe2fd4b5860dfff90ba3437bc135d"
60
60
  },
61
61
  {
62
62
  "source": "core/skills/build/SKILL.md",
@@ -91,7 +91,7 @@
91
91
  {
92
92
  "source": "core/skills/deliver/SKILL.md",
93
93
  "destination": ".github/skills/deliver/SKILL.md",
94
- "sha256": "6a3886493e9b59725ff53652972c1830478176a8c8da648448401d9051820db4"
94
+ "sha256": "5e4d64d91e429b636e0fda4094a84dd2f0b09aa7c1f281cbba7d630b67820076"
95
95
  },
96
96
  {
97
97
  "source": "core/skills/work-item-status/SKILL.md",
@@ -126,12 +126,12 @@
126
126
  {
127
127
  "source": "core/skills/deployment-readiness/SKILL.md",
128
128
  "destination": ".github/skills/deployment-readiness/SKILL.md",
129
- "sha256": "30202702cc368c872e62ce731e8ffe84a796aae243b287e3e670c8c8e1cd6531"
129
+ "sha256": "8b83d06056cbc10c0a9df69d1bb76db62753b52f5d4be9858488e400cd4b836d"
130
130
  },
131
131
  {
132
132
  "source": "core/skills/deploy/SKILL.md",
133
133
  "destination": ".github/skills/deploy/SKILL.md",
134
- "sha256": "f5d40e549ca958cb51606ced7ca482651d9a436327b6070a9a94171f22401700"
134
+ "sha256": "3ac67207c9cb6870c56313b2c4ce02d15c0756cb092937013f66512d2ec9576a"
135
135
  },
136
136
  {
137
137
  "source": "core/skills/tear-down/SKILL.md",
@@ -196,7 +196,7 @@
196
196
  {
197
197
  "source": "core/process/INTERACTIVE_SESSION_GUIDE.md",
198
198
  "destination": "docs/process/INTERACTIVE_SESSION_GUIDE.md",
199
- "sha256": "f7573b9926878da3db4353db7297b66c9d907844142ae6486c7fd7580f187e3b"
199
+ "sha256": "43bb5959fcc71755a76f9c4adcbc24576907db817d9dc0ced4e1607630493843"
200
200
  },
201
201
  {
202
202
  "source": "core/process/COACHING_GUIDE.md",
@@ -386,7 +386,7 @@
386
386
  ]
387
387
  },
388
388
  "deployment": {
389
- "revision": "3b31a8690720aa1eb8f5af27ddac5e0e6d3779ba",
389
+ "revision": "e9ca6e12e5b19f691b25ce0aba758a43c3df2913",
390
390
  "distribution": "greenfield-project",
391
391
  "files": [
392
392
  {
@@ -409,6 +409,11 @@
409
409
  "destination": "tools/project-forge-deployment/project_forge_deployment/access_cli.py",
410
410
  "sha256": "007d01ae0c4e28e64f75fb96f9e8e7b99db5c4c8129698a78923baeac2f51953"
411
411
  },
412
+ {
413
+ "source": "src/project_forge_deployment/agent_session.py",
414
+ "destination": "tools/project-forge-deployment/project_forge_deployment/agent_session.py",
415
+ "sha256": "c1bc83cc9a4daa50e86fdfe54cc5902ae5052fe1ad8c193082d0b1966cf8a1d0"
416
+ },
412
417
  {
413
418
  "source": "src/project_forge_deployment/access_diagnosis.py",
414
419
  "destination": "tools/project-forge-deployment/project_forge_deployment/access_diagnosis.py",
@@ -442,7 +447,7 @@
442
447
  {
443
448
  "source": "src/project_forge_deployment/box_platform.py",
444
449
  "destination": "tools/project-forge-deployment/project_forge_deployment/box_platform.py",
445
- "sha256": "163c022bbd8ecccadebbebdc8cef8e272fc95dd0dc3cdb35d4e6e10013c91040"
450
+ "sha256": "a11603daa47e33570732e349f59720811b5fdc2ec26ea04d08896764e089b682"
446
451
  },
447
452
  {
448
453
  "source": "src/project_forge_deployment/box_platform_assets.py",
@@ -452,7 +457,7 @@
452
457
  {
453
458
  "source": "src/project_forge_deployment/box_platform_remote.py",
454
459
  "destination": "tools/project-forge-deployment/project_forge_deployment/box_platform_remote.py",
455
- "sha256": "65de94ac4b6e980d83ce99ea55b9555aa96959c11808cce8c6ed537b27c15533"
460
+ "sha256": "18a47000d0755c645d5fe8a950f917331abedc332f3e11b9d6638946843a3f0e"
456
461
  },
457
462
  {
458
463
  "source": "src/project_forge_deployment/cloudflare_api.py",
@@ -477,7 +482,7 @@
477
482
  {
478
483
  "source": "src/project_forge_deployment/cli.py",
479
484
  "destination": "tools/project-forge-deployment/project_forge_deployment/cli.py",
480
- "sha256": "3c3e7d4fadeccc1b8a7691c1a213250790fb728400914894c0d4e70a1ec91cc7"
485
+ "sha256": "5f22a0fed79fe7dcb12b94f670bda04ea503f5254ccacfd841f03dfedd2ceeb5"
481
486
  },
482
487
  {
483
488
  "source": "src/project_forge_deployment/doctor.py",
@@ -507,7 +512,7 @@
507
512
  {
508
513
  "source": "src/project_forge_deployment/environment_setup.py",
509
514
  "destination": "tools/project-forge-deployment/project_forge_deployment/environment_setup.py",
510
- "sha256": "47ce6af4ab3b03c441725ec8cc1b356212ef7a1706635fe191affd959b09a5f5"
515
+ "sha256": "934b6d8f2309128da07ced858ea4b9c56b04c5ff67b6efc5c52d81a10a1a696b"
511
516
  },
512
517
  {
513
518
  "source": "src/project_forge_deployment/errors.py",
@@ -589,6 +594,11 @@
589
594
  "destination": "tools/project-forge-deployment/project_forge_deployment/proof_receipt.py",
590
595
  "sha256": "9b4bbb559747fc4ee3ba984a050fc6425e532527ae82f9897c73e04d8d0291b6"
591
596
  },
597
+ {
598
+ "source": "src/project_forge_deployment/proxy_peer.py",
599
+ "destination": "tools/project-forge-deployment/project_forge_deployment/proxy_peer.py",
600
+ "sha256": "ae6c8cb26b83ed07765fa4740ce5c68f52cafd2209b4f055887a4457cfb41c03"
601
+ },
592
602
  {
593
603
  "source": "src/project_forge_deployment/recovery_scope.py",
594
604
  "destination": "tools/project-forge-deployment/project_forge_deployment/recovery_scope.py",
@@ -614,6 +624,11 @@
614
624
  "destination": "tools/project-forge-deployment/project_forge_deployment/route_registry_remote.py",
615
625
  "sha256": "c8f397a09ff9efc904230eeec68aa85bab9c21529e44dd925e5db0f02f406b6f"
616
626
  },
627
+ {
628
+ "source": "src/project_forge_deployment/safe_diagnostics.py",
629
+ "destination": "tools/project-forge-deployment/project_forge_deployment/safe_diagnostics.py",
630
+ "sha256": "58372e220e25fc468be490c8a788e347a1026ccbdff899217cdbe8aecb7c00ce"
631
+ },
617
632
  {
618
633
  "source": "src/project_forge_deployment/security.py",
619
634
  "destination": "tools/project-forge-deployment/project_forge_deployment/security.py",
@@ -642,7 +657,7 @@
642
657
  {
643
658
  "source": "src/project_forge_deployment/ssh.py",
644
659
  "destination": "tools/project-forge-deployment/project_forge_deployment/ssh.py",
645
- "sha256": "88d59b62f63dc6b7206b6074dd109af4ea7c8f022f506e67f7204f1f06240d26"
660
+ "sha256": "b45ace8c35e622cb336ade9aeafa7405f96660e1e4faed0303faa4edb24b95c5"
646
661
  },
647
662
  {
648
663
  "source": "src/project_forge_deployment/teardown.py",
@@ -104,7 +104,7 @@ When the host has no structured question UI, use a concise textual fallback. A n
104
104
 
105
105
  When an authorized action must run in the developer's own terminal because it requires an interactive prompt, secure local input, or another terminal-owned interaction, treat the command handoff as continuation of the workflow rather than as a generic blocker.
106
106
 
107
- - If the action has its own bounded authorization, collect that authorization first through the host-native structured question control when available. Once the exact action is authorized, do not ask for the same permission again merely because execution moves to the terminal.
107
+ - If the action has its own bounded authorization, collect that authorization first through the host-native structured question control when available. Once the exact action is authorized, do not ask for the same permission again merely because execution moves to the terminal. When the installed deterministic CLI exposes a scoped `--yes` option, prefer it **only after** the matching exact plan/stage has been approved through the native Chat gate, avoiding duplicate typed confirmation phrases. Do not use `--yes` as a standing default, bypass a changed plan, or suppress independent destructive/trust/credential authorizations. SSH passphrase entry remains interactive and never belongs in Chat.
108
108
  - Immediately after authorization, provide the exact command in a **standalone fenced code block**. State the working directory when it is not already unambiguous. Do not bury the command inside explanatory prose.
109
109
  - Briefly describe the local prompt the developer should expect and which sensitive values must be entered **only in the terminal**. Never ask for a passphrase, password, token, private key, or other secret through chat, and never place such a secret in the command arguments merely to avoid an interactive prompt.
110
110
  - State what observable result or concise evidence the developer should report back so the workflow can verify the action and continue. Prefer deterministic verification after the local step instead of asking for secret-bearing terminal transcripts.
@@ -140,3 +140,72 @@ Compound action skills may also define their own bounded authorization. In parti
140
140
  If a tool/channel interruption leaves a compound workflow partially complete, a later invocation should inspect current state and resume from the furthest safe point rather than assuming nothing happened or requiring the same routine authorization again.
141
141
 
142
142
  `/wait-what` is different. It is a one-turn, read-only communication repair for the immediately preceding explanation. It re-establishes context and clarity, then returns control to the current workflow without silently continuing a pending action or approval gate.
143
+
144
+ ## Operational continuity and bounded retries (PF-03/PF-04)
145
+
146
+ Use these rules within existing Project Forge skills and repository-defined
147
+ operations. This is **not** a new approval store, model selection policy, or
148
+ background execution system.
149
+
150
+ ### Scope a working authorization before a consequential action
151
+
152
+ Before a multi-stage or unattended continuation, state a short **reviewed-stage
153
+ envelope** in the already-existing work item/review context:
154
+
155
+ - Exact Project, named logical environment/target, host identity where relevant,
156
+ and intended operation(s).
157
+ - Immutable artifact, plan identity and reviewed evidence, including freshness
158
+ and expiry when the installed capability exposes them.
159
+ - Allowed mechanical continuation and **separate** Production runtime, public
160
+ route, trust/credential, destructive data, merge, and release boundaries.
161
+ - Remaining secret-input or human decision points and the precise stop conditions.
162
+
163
+ One currently approved, unchanged reviewed stage authorizes its supported
164
+ routine mechanics; never request a second identical shell phrase or chat
165
+ permission when the installed deterministic command supports scoped execution.
166
+ However, authorization is **not** a standing token: changed target, plan, peer,
167
+ data/backup risk, credential, identity, exposure, trust or other consequence
168
+ reopens the relevant gate. Never infer indefinite authority from elapsed time
169
+ or a prior session. Explain any interactive passphrase/provider action **before**
170
+ the developer leaves an unattended workflow; do not promise unattended
171
+ completion if required local input or a human approval remains.
172
+
173
+ ### Read safe results without losing the failed phase
174
+
175
+ When the installed Deployment Kit supplies an additive safe
176
+ `diagnosticSummary`, use it only if the **observed installed** schema and
177
+ capability version match the documented contract (currently schema 1,
178
+ `remote-environment-setup` capability 1). Preserve phase, fixed reasonCode,
179
+ proved/failed/blocked/notChecked dimensions, observedAt, mutation, consequence
180
+ and any safe evidence reference. An unknown capability, reason, malformed
181
+ summary or mixed stderr becomes **unclassified**, never green/success.
182
+ Report unsupported or older installed tooling honestly; do not reconstruct the
183
+ same field by parsing private exceptions or create a new runtime prerequisite
184
+ during Bootstrap. A concise summary is not a complete reviewed mutation plan.
185
+
186
+ ### Avoid no-information attempts and context inflation
187
+
188
+ If the **same** fixed reason code and non-secret exact target/phase/context have
189
+ failed twice with unchanged inputs **and no new evidence**, change the method
190
+ (e.g. inspect a different bounded proof) or stop with the precise missing fact.
191
+ Do not perform a third identical attempt, rerun an unchanged reviewer objective,
192
+ replay a failed login, or repeatedly ask for the same fact. A newly loaded SSH
193
+ agent, changed plan, confirmed host identity, updated installed helper or other
194
+ material new evidence resets the same-phase retry comparison.
195
+
196
+ Start a continuation with the smallest authoritative repository/state
197
+ checkpoint: current work-item state, exact commit/PR/CI, package/component
198
+ provenance, observed capability and proof freshness, unresolved decision, and
199
+ next safe action. Follow links to deeper current evidence only as needed.
200
+ Do not reload a whole incident transcript, broad skill catalog or all historical
201
+ retries just because context is available. Favor one bounded wait/read of a
202
+ known CI run ID over repeated short polling and large tool output. Use only
203
+ relevant skill and artifact slices; keep full evidence available separately.
204
+
205
+ Measure improvements with one matched, bounded replay holding the same fault,
206
+ safety obligations and environmental assumptions. Record model/tool rounds,
207
+ operator prompts, elapsed/waiting time and cache-adjusted token volume:
208
+ input tokens minus cache reads plus output tokens, respecting each counter's
209
+ semantics. Candidate >=50% reductions are **targets**, not guaranteed savings.
210
+ Never hide failed checks, truncate material mutation evidence, skip security
211
+ gates, or change model tier automatically to make metrics look better.
@@ -21,7 +21,7 @@ Existing repository conventions outrank Project Forge defaults. Preserve branch
21
21
 
22
22
  ## 1. Authorization model
23
23
 
24
- Because `/deliver` is manually invoked, invoking it is one bounded authorization for the routine delivery mechanics of the already-reviewed/accepted work item:
24
+ Because `/deliver` is manually invoked, invoking it is one bounded authorization for the routine delivery mechanics of the already-reviewed/accepted work item. Keep a delivery started in VS Code Copilot Chat in that interface when host tools can run the deterministic repository commands; a separate Copilot CLI session is not required for automation:
25
25
 
26
26
  ```text
27
27
  establish/recover the short-lived work branch when needed
@@ -185,6 +185,12 @@ Delivery state preserved. Re-run /deliver to resume.
185
185
  ```
186
186
 
187
187
  Do not insert a new write-authorization prompt merely because CI is pending.
188
+ Use the exact known workflow/run ID and one bounded wait/recheck when possible
189
+ rather than repeated short polling, reloading the same CI output, or relaunching
190
+ a Reviewer on an unchanged objective. Treat two unchanged, no-information
191
+ attempts at one phase as a stop/change-method condition. Preserve the PR/commit
192
+ and CI status as a concise authoritative resumption checkpoint. See
193
+ `core/process/INTERACTIVE_SESSION_GUIDE.md` for freshness and context rules.
188
194
 
189
195
  ### Failed
190
196
 
@@ -224,13 +224,36 @@ Resolve the exact phrase from the installed tool/version. For a staged rollout,
224
224
 
225
225
  Authorization applies only to the exact environment, resolved classification, placement/target/provider, artifact, reviewed plan/stage, current isolation evidence and route evidence, and evidence shown. Material changes require renewed review/authorization.
226
226
 
227
+ **Avoid duplicate terminal approval:** when the Builder has explicitly approved this exact current stage in VS Code Copilot Chat, the assistant may pass the installed Deployment Kit's supported `--yes` flag for **that stage only** instead of making the Builder type the displayed phrase again in Bash/PowerShell. `--yes` is an execution flag, not approval evidence; never use it because the human previously approved a different plan, artifact, target, route, session, or stage. If the installed command lacks the flag, preserve its normal gate. Destructive resets/restores and SSH trust/credential changes are independent consequences and are not authorized by a deployment approval.
228
+
229
+ Before unattended rollout, identify a concise reviewed-stage envelope:
230
+ exact target, immutable artifact, permitted operations, current plan/proof
231
+ identity and freshness, distinct runtime/route/data/trust boundaries, and any
232
+ remaining local secret or human approval stop. Do not promise to finish
233
+ unattended if those stops remain. Repeat safe mechanics within unchanged
234
+ authority; reopen only for changed evidence or a genuinely new consequence.
235
+ After two unchanged failures with no new evidence, change the method or
236
+ stop instead of repeatedly recreating the same plan. Apply the bounded
237
+ continuation policy in `core/process/INTERACTIVE_SESSION_GUIDE.md`.
238
+
227
239
  A runtime authorization never authorizes the route stage. A route authorization never retroactively authorizes a changed runtime artifact. If the rollout contains both stages, preserve the separate gates even though the Builder reviewed one coherent outcome.
228
240
 
241
+ For authenticated public ingress relying on a pinned exact Caddy peer,
242
+ preserve current read-only managed peer evidence (when supported) as a
243
+ separate plan dimension from DNS, route and image readiness. If observed
244
+ Caddy container/network identity or peer IP changed from the reviewed
245
+ application binding, **do not silently update the trusted proxy**, change
246
+ the release image, or continue the public-authentication rollout. Prepare a
247
+ separate app-owned reviewed convergence/health/rollback stage.
248
+ Proxy observation alone neither authorizes runtime writes nor proves live end-to-end
249
+ OIDC/public-origin behavior. Follow the PF-06 producer's immutable identity
250
+ review boundary, or report the installed capability as unavailable.
251
+
229
252
  ## 9. Preview and execute only the approved contract
230
253
 
231
254
  Before mutation, show concise non-secret evidence such as exact environment, purpose, placement, target/provider when applicable, proof freshness, immutable application artifact, persistence/migration, exposure/route, application isolation state/fingerprint when exposed, shared failure-domain note when relevant, planned stages, endpoint state at each stage, and the exact required authorization phrase for the next consequence only.
232
255
 
233
- After authorization, invoke only the repository-provided deterministic launcher. Never redirect to another environment/host/provider, weaken trust/exposure, force through lock contention, or invent recovery writes.
256
+ After authorization, invoke only the repository-provided deterministic launcher. For remote SSH placement, prefer the installed Deployment Kit's `target agent-session <target> -- <normal Deployment Kit command>` wrapper when supported; it reuses local SSH authentication across WSL/Linux, macOS, and native Windows VS Code sessions without copying long socket paths. For a Project-owned, repository-reviewed deployment script, use `target agent-run <target> -- <explicit local executable and argv>` when installed; it does not execute a shell, add `--yes`, or replace the script's approvals. Do not switch the workflow from VS Code Copilot Chat to Copilot CLI merely for terminal automation. A key passphrase, when required, stays in the developer-owned terminal and never in Chat. The session wrapper does not provide remote authorization or replace current plan/host checks. Never redirect to another environment/host/provider, weaken trust/exposure, force through lock contention, or invent recovery writes.
234
257
 
235
258
  The deterministic launcher must revalidate current isolation immediately before real application mutation when the installed contract provides that capability. If current isolation changed or became blocked after review, stop and create/review a fresh plan rather than continuing under the old authorization.
236
259
 
@@ -198,7 +198,46 @@ The current address is connectivity information, not trusted identity. Preserve
198
198
 
199
199
  Before asking questions, inspect existing non-secret profile/target evidence. Classify the workstation's target setup as `CONFIGURED`, `PARTIAL`, or `NOT CONFIGURED` and ask only for the smallest missing fact.
200
200
 
201
- Only after explicit `[REMOTE READ ONLY]` authorization under the installed/repository contract may new remote checks occur. Never scan/select another host or weaken host-key verification merely to get a green result.
201
+ Invoking `/deployment-readiness <environment>` or directly asking to check readiness authorizes bounded **read-only** diagnostics for the exact resolved environment/target. Do not ask for a second `[REMOTE READ ONLY]` typed phrase or chat approval for each check once that scope is established. The consequence label remains useful in the plan, but the workflow request is the read-only authorization. Never scan/select another host, expand to a different target, change target trust, or weaken host-key verification merely to get a green result.
202
+
203
+ When the installed Deployment Kit supports `target agent-session`, run supported remote checks via `target agent-session <target> -- <normal Deployment Kit command>` rather than copying `SSH_AUTH_SOCK` paths into each command. For existing Project-owned Python readiness scripts that use the same verified target, use the installed `target agent-run <target> -- <exact reviewed local executable and argv>` to inherit this SSH context without shell evaluation; do not invent script arguments or bypass their gates. This prepares only local SSH authentication (WSL/Linux stable private agent; macOS existing SSH agent or private fallback; native Windows OpenSSH agent service). It must not start or configure a Windows system service, enroll trust, skip an SSH passphrase, or authorize any remote write. If the key needs interactive local input, hand the exact non-secret command to the developer-owned terminal once and resume validation afterward. For older installed kits, use their supported SSH workflow and report the update opportunity rather than fabricating a command.
204
+
205
+ ### Safe diagnostic continuation and no-progress bound
206
+
207
+ When the installed Deployment Kit exposes a recognized v1 `diagnosticSummary`,
208
+ read the fixed reasonCode, failure phase, independently proved/blocked/notChecked
209
+ dimensions, observedAt and mutation status **without** losing unrelated good
210
+ readiness evidence. Continue using the existing exact-target read-only
211
+ authorization. If schema/capability versions are unknown or no usable reason
212
+ is present, state that diagnosis is unclassified, not successful.
213
+ For `local-signing-unproven`, use the supported configured
214
+ `target agent-session` before investigating remote policy; a server's
215
+ preliminary key acceptance does not prove that the client signed. Older
216
+ installed kits may lack these fields: do not invent them.
217
+
218
+ Use the bounded retry and evidence-freshness policy in
219
+ `core/process/INTERACTIVE_SESSION_GUIDE.md`: after two unchanged,
220
+ no-information attempts at the same phase, change the diagnostic method or
221
+ report the exact evidence gap. Do not replay CI/reviewers or request the
222
+ same unchanged read authorization.
223
+
224
+ ### Exact authenticated-origin peer convergence (PF-06)
225
+
226
+ For an application whose approved public-origin policy pins the managed Caddy
227
+ peer, use the **installed** Deployment Kit's read-only
228
+ `platform peer-inspect <target> --configured-peer <ip> --previous-container-id <full-id> --previous-network-id <full-id> --json`
229
+ when supported and the application's previously reviewed identity evidence
230
+ exists. Preserve the exact named target and classify a mismatch as a
231
+ **review-required stop**. A missing previous full identity is
232
+ `needs-reviewed-identity-baseline`, not established trust. Do not resolve a
233
+ missing baseline by assuming current Docker identity is implicitly approved.
234
+
235
+ This observation is not a token/certificate proving Caddy authenticity, not
236
+ permission to rewrite the application-configured peer, and not proof of
237
+ Cloudflare forwarding or of application middleware enforcement. Never trust
238
+ a Docker subnet or allow all LAN peers as a workaround. If the installed Kit
239
+ does not support this inspection, report the capability gap rather than
240
+ inventing a new script or changing Production.
202
241
 
203
242
  #### Real application isolation preflight
204
243
 
@@ -397,11 +397,30 @@ remote account/source-policy rejection
397
397
 
398
398
  Keep credentials, private keys, raw secure-prompt content, and unbounded SSH transcripts out of conversational diagnostics and journals.
399
399
 
400
+ For workstation SSH-agent readiness, prefer the installed Deployment Kit's `target agent-session <target> -- <normal Deployment Kit command>` when supported; for repository-owned Python operations requiring SSH use `target agent-run <target> -- <exact local executable and arguments>` when available. This is a **local** convenience that works with the WSL/Linux private reused socket, the macOS agent or private fallback, and the native Windows OpenSSH agent service; the remote target OS is independent. Do not require the Builder to copy `SSH_AUTH_SOCK` paths across VS Code Chat terminal calls, switch to Copilot CLI, or type `[SSH AGENT WRITE]` as a second authorization merely to load the already-configured key. Keep the OpenSSH passphrase in the interactive terminal; never change the Windows agent service, host trust, or saved credentials without separate authority. After the key is ready, continue normal exact-target verification and the original write gates.
401
+
400
402
  For fingerprint input, show the expected public form such as `SHA256:<fingerprint>` and validate that shape before progressing. Distinguish the **server host fingerprint**, the **client public-key fingerprint**, and passwords/passphrases. If input looks credential-like rather than fingerprint-shaped, do not repeat it into commands, reports, journals, or upstream issues.
401
403
 
402
404
  If a local key/agent operation requires a passphrase or another terminal-owned secret prompt, follow the interactive-terminal handoff contract: collect any distinct bounded authorization once, then immediately provide the exact standalone copyable command, correct machine/working directory, expected local prompt, and the non-secret completion evidence to report. Do not ask for the passphrase through chat or ask a second ceremonial question merely to repeat the same authorized command.
403
405
 
404
- If client-side evidence shows that the configured key reached the server but access is still rejected, stop reinstalling the same key. Route to a trusted target console or already-authorized administrative session to inspect effective account/source policy. Do not guess that `AllowUsers`, `AllowGroups`, PAM, or another server rule is the exact cause until server-side evidence establishes it.
406
+ **SSH signing proof matters:** a preliminary `Server accepts key` message
407
+ means the public key was eligible, **not** that local signing finished or the
408
+ remote account rejected an authenticated user. If the selected agent is absent
409
+ or signing is unproven (the fixed diagnostic code is `local-signing-unproven`),
410
+ try the already-supported exact-target
411
+ `target agent-session` in a trusted local terminal (only with the required
412
+ bounded local authority), then retry the unchanged read-only check once.
413
+ If client signature evidence exists but SSH is still rejected, report remote
414
+ authentication failure without claiming a particular account policy. Inspect
415
+ server account/source policy from a trusted console only when the evidence
416
+ actually justifies that branch. Do not repeatedly reinstall a key, weaken
417
+ trust, turn on password authentication or invent a server configuration fix.
418
+
419
+ Use recognized installed v1 `diagnosticSummary` fields and the bounded
420
+ continuation/no-progress policy in `core/process/INTERACTIVE_SESSION_GUIDE.md`
421
+ when available; older Kit output remains an explicitly limited capability.
422
+ Keep independent readiness evidence and do not reflect arbitrary exceptions
423
+ or secret-bearing stderr to Chat.
405
424
 
406
425
  ### One bounded trusted-console diagnostic bundle
407
426
 
@@ -0,0 +1,154 @@
1
+ """Explicit, reusable workstation SSH-agent session for Deployment Kit commands.
2
+
3
+ This owns *local* authentication only. It does not authorize any remote write,
4
+ alter target host trust, start an SSH tunnel, or change an operating-system service.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from contextlib import contextmanager
9
+ import hashlib
10
+ import os
11
+ from pathlib import Path
12
+ import platform
13
+ import shutil
14
+ import stat
15
+ import subprocess
16
+ from typing import Iterator
17
+
18
+ from .errors import DeploymentKitError
19
+ from .keys import add_key_to_agent, agent_status
20
+ from .security import check_identity_security
21
+
22
+
23
+ def _private_agent_directory(profile: dict[str, str]) -> Path:
24
+ identity = Path(profile["identityFile"])
25
+ check_identity_security(identity)
26
+ ssh_dir = next((p for p in identity.parents if p.name.lower() == ".ssh"), None)
27
+ if ssh_dir is None:
28
+ raise DeploymentKitError("The configured identity is outside the user's .ssh directory.")
29
+ directory = ssh_dir / "project-forge-agents"
30
+ if not directory.exists() and not directory.is_symlink():
31
+ directory.mkdir(mode=0o700)
32
+ try:
33
+ metadata = directory.lstat()
34
+ except OSError as error:
35
+ raise DeploymentKitError("Cannot inspect the dedicated SSH-agent directory.") from error
36
+ if (
37
+ not stat.S_ISDIR(metadata.st_mode)
38
+ or metadata.st_uid != os.getuid()
39
+ or metadata.st_mode & 0o077
40
+ ):
41
+ raise DeploymentKitError(
42
+ "Dedicated SSH-agent directory must be a real, current-user-owned directory with mode 0700."
43
+ )
44
+ return directory
45
+
46
+
47
+ def _agent_socket(profile: dict[str, str]) -> Path:
48
+ directory = _private_agent_directory(profile)
49
+ digest = hashlib.sha256(
50
+ str(Path(profile["identityFile"]).resolve()).encode("utf-8")
51
+ ).hexdigest()[:16]
52
+ socket = directory / f"pf-{digest}.sock"
53
+ # Some OSes limit Unix-domain socket paths to ~104 bytes.
54
+ if len(os.fsencode(socket)) > 100:
55
+ raise DeploymentKitError(
56
+ "The dedicated SSH-agent socket path is too long for portable Unix socket support."
57
+ )
58
+ return socket
59
+
60
+
61
+ def _ensure_posix_agent_socket(profile: dict[str, str]) -> str:
62
+ socket = _agent_socket(profile)
63
+ if socket.is_symlink():
64
+ raise DeploymentKitError("Refusing to follow a symlink at the dedicated SSH-agent socket.")
65
+ if socket.exists():
66
+ metadata = socket.lstat()
67
+ if not stat.S_ISSOCK(metadata.st_mode) or metadata.st_uid != os.getuid():
68
+ raise DeploymentKitError("Dedicated SSH-agent socket is not a current-user-owned Unix socket.")
69
+ else:
70
+ agent = shutil.which("ssh-agent")
71
+ if not agent:
72
+ raise DeploymentKitError("OpenSSH ssh-agent is not installed on this workstation.")
73
+ result = subprocess.run(
74
+ [agent, "-a", str(socket), "-s"],
75
+ check=False, text=True, capture_output=True,
76
+ )
77
+ if result.returncode or not socket.is_socket():
78
+ raise DeploymentKitError(
79
+ "Cannot start the dedicated SSH agent. Inspect the private socket directory; "
80
+ "no existing agent process or socket will be removed automatically."
81
+ )
82
+
83
+ # Do not trust an old socket solely because it looks like a socket: it must
84
+ # respond to the usual identity check under the exact selected environment.
85
+ return str(socket)
86
+
87
+
88
+ def _set_socket(value: str | None) -> None:
89
+ if value is None:
90
+ os.environ.pop("SSH_AUTH_SOCK", None)
91
+ else:
92
+ os.environ["SSH_AUTH_SOCK"] = value
93
+
94
+
95
+ @contextmanager
96
+ def target_agent_session(profile: dict[str, str]) -> Iterator[dict[str, str | bool]]:
97
+ """Temporarily bind the selected target's agent to commands in this process.
98
+
99
+ WSL/Linux: a stable per-identity local OpenSSH agent survives separate
100
+ VS Code terminal processes. macOS: prefer its existing launchd agent when
101
+ available, otherwise use the same private stable POSIX fallback.
102
+ Native Windows: use the existing Windows OpenSSH agent *service* only.
103
+ Its service setup/start requires a separately authorized workstation action.
104
+ """
105
+ system = platform.system()
106
+ previous = os.environ.get("SSH_AUTH_SOCK")
107
+ managed_socket = False
108
+ try:
109
+ if system == "Windows":
110
+ # A native Windows OpenSSH service does not use SSH_AUTH_SOCK.
111
+ # Never start or reconfigure the privileged service automatically.
112
+ _set_socket(None)
113
+ elif system in {"Linux", "Darwin"}:
114
+ if system == "Darwin" and previous:
115
+ status = agent_status(profile)
116
+ if status["state"] == "agent-unavailable":
117
+ _set_socket(_ensure_posix_agent_socket(profile))
118
+ managed_socket = True
119
+ else:
120
+ _set_socket(_ensure_posix_agent_socket(profile))
121
+ managed_socket = True
122
+ else:
123
+ raise DeploymentKitError(
124
+ f"Unsupported workstation OS for SSH-agent session: {system}."
125
+ )
126
+
127
+ status = agent_status(profile)
128
+ if status["state"] == "agent-unavailable":
129
+ if system == "Windows":
130
+ raise DeploymentKitError(
131
+ "Windows OpenSSH ssh-agent service is unavailable. "
132
+ "Have the workstation administrator enable/start the ssh-agent service; "
133
+ "Project Forge will not change its configuration automatically."
134
+ )
135
+ raise DeploymentKitError(
136
+ "The selected SSH-agent socket is unresponsive. "
137
+ "Inspect the private socket and agent process; Project Forge will not delete it automatically."
138
+ )
139
+
140
+ # The explicit 'agent-session' command authorizes this bounded local
141
+ # key load. SSH passphrase input remains with OpenSSH in the terminal.
142
+ if not status["loaded"]:
143
+ result = add_key_to_agent(profile)
144
+ if not result["loaded"]:
145
+ raise DeploymentKitError("Dedicated SSH identity is not loaded.")
146
+ yield {
147
+ "targetName": profile["targetName"],
148
+ "loaded": True,
149
+ "agent": "windows-service" if system == "Windows"
150
+ else "dedicated-socket" if managed_socket
151
+ else "existing-macos-agent",
152
+ }
153
+ finally:
154
+ _set_socket(previous)
@@ -101,7 +101,7 @@ def _parse_status(output: str) -> dict[str, Any]:
101
101
 
102
102
 
103
103
  def _run_remote(profile: dict[str, Any], address: str, action: str, *, sudo: bool) -> str:
104
- if action not in {"status", "install", "upgrade"}:
104
+ if action not in {"status", "install", "upgrade", "proxy-peer"}:
105
105
  raise DeploymentKitError(f"Unsupported host-platform action: {action}")
106
106
  remote_command = "bash -s -- " + shlex.quote(action)
107
107
  if sudo:
@@ -130,6 +130,36 @@ def platform_status(name: str) -> dict[str, Any]:
130
130
  return status
131
131
 
132
132
 
133
+ def platform_proxy_peer(
134
+ name: str,
135
+ *,
136
+ configured_peer: str | None = None,
137
+ previous_container_id: str | None = None,
138
+ previous_network_id: str | None = None,
139
+ ) -> dict[str, Any]:
140
+ """Read current Caddy peer and produce a no-write app convergence decision.
141
+
142
+ This is NOT authority to update an application's trusted peer. That
143
+ requires its own verified plan, meaningful review and runtime write gate.
144
+ """
145
+ from .proxy_peer import assess_configured_peer, parse_peer_observation
146
+
147
+ profile = load_profile(name)
148
+ evidence = verify_access(profile)
149
+ observed = parse_peer_observation(
150
+ _run_remote(profile, evidence["address"], "proxy-peer", sudo=True)
151
+ )
152
+ plan = assess_configured_peer(
153
+ observed,
154
+ configured_peer,
155
+ previous_container_id=previous_container_id,
156
+ previous_network_id=previous_network_id,
157
+ )
158
+ plan["targetName"] = name
159
+ plan["mutated"] = False
160
+ return plan
161
+
162
+
133
163
  def platform_doctor(name: str) -> dict[str, Any]:
134
164
  status = platform_status(name)
135
165
  next_actions = {