@mriekki/project-forge 4.24.1 → 4.25.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.25.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": "5d7c1285793f502d58ad4b6f8f2e51c77453073b",
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": "97dfcd7152619a434672da5045c0528f41984ced",
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"},
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.24.1",
4
+ "version": "4.25.0",
5
5
  "components": {
6
6
  "core": {
7
- "revision": "59f5657bbb6f1edc45d1e8619ab1d0875b7907e0",
8
- "version": "4.2.1",
7
+ "revision": "5d7c1285793f502d58ad4b6f8f2e51c77453073b",
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": "fb1f752655c79d7ac68f3424adba2663492a8e9dd0b8d268cabb6dd28880b6fc"
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": "f7d4c611358bec5b348df994515d1253b59c72707b3a8f28e846174468c590c2"
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": "170e90e1d21050fb1134230f539c41a1ca62193c3d695fda5edee4147d56e929"
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": "82c05ae910f65efd170ca6d055cce9e6e5035628eba45bcfc8552a2b1619cce2"
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": "71e25cd0603eecac2be67a696386fddf718c6da26e10c0ade74429ff68a3ac30"
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": "97dfcd7152619a434672da5045c0528f41984ced",
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",
@@ -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": "6b4bb3a23b3df8387f13f544ce02e1d2bee863777de4965044435769c24ab98b"
481
486
  },
482
487
  {
483
488
  "source": "src/project_forge_deployment/doctor.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.
@@ -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
@@ -224,13 +224,15 @@ 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
+
227
229
  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
230
 
229
231
  ## 9. Preview and execute only the approved contract
230
232
 
231
233
  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
234
 
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.
235
+ 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
236
 
235
237
  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
238
 
@@ -198,7 +198,9 @@ 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.
202
204
 
203
205
  #### Real application isolation preflight
204
206
 
@@ -397,6 +397,8 @@ 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.
@@ -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)
@@ -230,6 +230,26 @@ def build_parser() -> argparse.ArgumentParser:
230
230
  agent_add.add_argument("name")
231
231
  agent_add.add_argument("--yes", action="store_true")
232
232
 
233
+ agent_session = target_sub.add_parser(
234
+ "agent-session",
235
+ help="Reuse a workstation SSH agent/key and run one normal Deployment Kit command",
236
+ )
237
+ agent_session.add_argument("name", help="Exact configured SSH target identity to load")
238
+ agent_session.add_argument(
239
+ "session_args", nargs=argparse.REMAINDER,
240
+ help="After --, a normal Deployment Kit command (with its usual authorization gates)",
241
+ )
242
+
243
+ agent_run = target_sub.add_parser(
244
+ "agent-run",
245
+ help="Run one repository-defined local program with the target's SSH agent, no shell",
246
+ )
247
+ agent_run.add_argument("name", help="Exact configured SSH target identity to load")
248
+ agent_run.add_argument(
249
+ "run_args", nargs=argparse.REMAINDER,
250
+ help="After --, exact executable and arguments for a normal local program",
251
+ )
252
+
233
253
  trust_candidates = target_sub.add_parser(
234
254
  "trust-candidates",
235
255
  help="Read candidate host-key fingerprints; candidates are never trusted by discovery",
@@ -373,6 +393,47 @@ def main(argv: list[str] | None = None) -> int:
373
393
  if args.group == "target" and args.command == "agent-status":
374
394
  _print(agent_status(load_profile(args.name)), as_json=args.json)
375
395
  return 0
396
+ if args.group == "target" and args.command == "agent-run":
397
+ import subprocess
398
+
399
+ command = list(args.run_args)
400
+ if command and command[0] == "--":
401
+ command.pop(0)
402
+ if not command:
403
+ raise DeploymentKitError(
404
+ "Supply an exact local executable and argv after '--'. "
405
+ "The agent-run command does not authorize the program's writes."
406
+ )
407
+ from .agent_session import target_agent_session
408
+
409
+ with target_agent_session(load_profile(args.name)) as session:
410
+ print(
411
+ f"SSH agent ready for {session['targetName']} ({session['agent']}); "
412
+ "the launched program retains its own authorization gates.",
413
+ file=sys.stderr,
414
+ )
415
+ # No shell, eval, redirection, or implicit --yes. The caller
416
+ # selects an already-reviewed repository-owned program.
417
+ return subprocess.run(command, check=False).returncode
418
+ if args.group == "target" and args.command == "agent-session":
419
+ command = list(args.session_args)
420
+ if command and command[0] == "--":
421
+ command.pop(0)
422
+ if not command or command[:2] == ["target", "agent-session"]:
423
+ raise DeploymentKitError(
424
+ "Supply a non-nested Deployment Kit subcommand after '--'. "
425
+ "The SSH-agent session does not authorize remote operations."
426
+ )
427
+ from .agent_session import target_agent_session
428
+ from .entrypoint import main as dispatch
429
+
430
+ with target_agent_session(load_profile(args.name)) as session:
431
+ print(
432
+ f"SSH agent ready for {session['targetName']} ({session['agent']}); "
433
+ "normal CLI checks and write gates still apply.",
434
+ file=sys.stderr,
435
+ )
436
+ return dispatch(command)
376
437
  if args.group == "target" and args.command == "agent-add":
377
438
  profile = load_profile(args.name)
378
439
  status = agent_status(profile)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mriekki/project-forge",
3
- "version": "4.24.1",
3
+ "version": "4.25.0",
4
4
  "description": "Public consumer distribution and updater for Project Forge engineering assets.",
5
5
  "type": "module",
6
6
  "bin": {