pidriver 0.4.1__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pidriver-0.4.1 → pidriver-0.4.2}/PKG-INFO +6 -2
- {pidriver-0.4.1 → pidriver-0.4.2}/README.md +5 -1
- {pidriver-0.4.1 → pidriver-0.4.2}/pyproject.toml +1 -1
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/__init__.py +1 -1
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/repository.py +58 -15
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_repository.py +73 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/.gitignore +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/.sc/last-chat-settings.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/LICENSE +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/examples/basic_prompt.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/_install.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/_transport.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/assets/pi_login.mjs +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/auth.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/client.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/config.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/errors.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/events.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/instances.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/interaction.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/manager.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/models.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/py.typed +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/session.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/staging.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/store.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/supervisor.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/usage.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/web.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/src/pidriver/workspace.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/conftest.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/README.md +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/_capture_driver.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/agent_end.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/agent_start.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/extension_ui_request.confirm.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/extension_ui_request.editor.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/extension_ui_request.input.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/extension_ui_request.select.approval.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/extension_ui_request.select.ask.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/extension_ui_request.setWidget.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/extension_ui_response.approve.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/extension_ui_response.confirm.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/extension_ui_response.editor.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/extension_ui_response.input.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/extension_ui_response.value.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/message_start_assistant.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/message_start_user.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/message_update.text_delta.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/message_update.thinking_delta.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/message_update.toolcall_end.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/message_update.toolcall_start.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/ready.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/response_prompt_ack.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/tool_execution_end.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/tool_execution_start.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/tool_execution_update.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/turn_end.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/turn_start.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/payloads/get_session_stats.response.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/payloads/get_state.response.json +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_auth.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_config.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_event_summary.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_events.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_fixtures_replay.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_install.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_install_resolve.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_manager.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_models.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_replay_session.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_session.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_staging.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_store.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_supervisor.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_transport.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_ui_methods.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_usage.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_web.py +0 -0
- {pidriver-0.4.1 → pidriver-0.4.2}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pidriver
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Async Python driver for the pi coding agent (pi --mode rpc), built for isolated, headless project work.
|
|
5
5
|
Project-URL: Homepage, https://github.com/bobuk/pidriver
|
|
6
6
|
Project-URL: Repository, https://github.com/bobuk/pidriver
|
|
@@ -543,6 +543,7 @@ session, workspace = await start_repository_task(
|
|
|
543
543
|
client, "Fix the failing tests", "git+ssh://git@github.com/org/project.git",
|
|
544
544
|
root=Path("/srv/pi-jobs"), job_id="task-123",
|
|
545
545
|
ssh_key=Path("/srv/keys/project_ed25519"),
|
|
546
|
+
known_hosts=Path("/srv/keys/known_hosts"), # optional: pin host keys to this file
|
|
546
547
|
)
|
|
547
548
|
# Drain the session's events, then close it before finalizing the workspace.
|
|
548
549
|
async for event in session:
|
|
@@ -563,7 +564,10 @@ untrusted requests: the caller must enforce an allowlist. Empty repositories and
|
|
|
563
564
|
second job with the same ID are rejected. SSH also accepts standard `ssh://` and
|
|
564
565
|
`git@host:path` URLs. The key path (not its contents) is saved by `to_state()` for
|
|
565
566
|
later pushes; the key must still exist then. SSH uses strict host-key checking and
|
|
566
|
-
non-interactive authentication
|
|
567
|
+
non-interactive authentication. Pass `known_hosts=Path(...)` to pin host keys to that
|
|
568
|
+
file alone (`UserKnownHostsFile=<file>`, `GlobalKnownHostsFile=/dev/null`; the path is
|
|
569
|
+
saved by `to_state()` too) — useful in containers where ssh's own `~/.ssh` isn't yours
|
|
570
|
+
to manage; otherwise configure ssh's default `known_hosts` on the host in advance.
|
|
567
571
|
|
|
568
572
|
## Workspace jail
|
|
569
573
|
|
|
@@ -517,6 +517,7 @@ session, workspace = await start_repository_task(
|
|
|
517
517
|
client, "Fix the failing tests", "git+ssh://git@github.com/org/project.git",
|
|
518
518
|
root=Path("/srv/pi-jobs"), job_id="task-123",
|
|
519
519
|
ssh_key=Path("/srv/keys/project_ed25519"),
|
|
520
|
+
known_hosts=Path("/srv/keys/known_hosts"), # optional: pin host keys to this file
|
|
520
521
|
)
|
|
521
522
|
# Drain the session's events, then close it before finalizing the workspace.
|
|
522
523
|
async for event in session:
|
|
@@ -537,7 +538,10 @@ untrusted requests: the caller must enforce an allowlist. Empty repositories and
|
|
|
537
538
|
second job with the same ID are rejected. SSH also accepts standard `ssh://` and
|
|
538
539
|
`git@host:path` URLs. The key path (not its contents) is saved by `to_state()` for
|
|
539
540
|
later pushes; the key must still exist then. SSH uses strict host-key checking and
|
|
540
|
-
non-interactive authentication
|
|
541
|
+
non-interactive authentication. Pass `known_hosts=Path(...)` to pin host keys to that
|
|
542
|
+
file alone (`UserKnownHostsFile=<file>`, `GlobalKnownHostsFile=/dev/null`; the path is
|
|
543
|
+
saved by `to_state()` too) — useful in containers where ssh's own `~/.ssh` isn't yours
|
|
544
|
+
to manage; otherwise configure ssh's default `known_hosts` on the host in advance.
|
|
541
545
|
|
|
542
546
|
## Workspace jail
|
|
543
547
|
|
|
@@ -123,7 +123,7 @@ from .supervisor import PiInstanceSupervisor
|
|
|
123
123
|
from .usage import UsageTotals
|
|
124
124
|
from .workspace import ProjectDir, list_candidate_projects, list_projects, resolve_project
|
|
125
125
|
|
|
126
|
-
__version__ = "0.4.
|
|
126
|
+
__version__ = "0.4.2"
|
|
127
127
|
|
|
128
128
|
__all__ = [
|
|
129
129
|
"__version__",
|
|
@@ -21,11 +21,17 @@ class GitRepositoryWorkspace:
|
|
|
21
21
|
"""Isolated clone and worktree; publishing requires an explicit merge()."""
|
|
22
22
|
|
|
23
23
|
def __init__(
|
|
24
|
-
self,
|
|
24
|
+
self,
|
|
25
|
+
workspace: StagedWorkspace,
|
|
26
|
+
*,
|
|
27
|
+
branch: str,
|
|
28
|
+
ssh_key: Path | None = None,
|
|
29
|
+
known_hosts: Path | None = None,
|
|
25
30
|
) -> None:
|
|
26
31
|
self.workspace = workspace
|
|
27
32
|
self.branch = branch
|
|
28
33
|
self.ssh_key = ssh_key
|
|
34
|
+
self.known_hosts = known_hosts
|
|
29
35
|
|
|
30
36
|
@property
|
|
31
37
|
def path(self) -> Path:
|
|
@@ -49,7 +55,7 @@ class GitRepositoryWorkspace:
|
|
|
49
55
|
"origin",
|
|
50
56
|
f"HEAD:refs/heads/{self.branch}",
|
|
51
57
|
check=False,
|
|
52
|
-
env=_ssh_env(self.ssh_key),
|
|
58
|
+
env=_ssh_env(self.ssh_key, self.known_hosts),
|
|
53
59
|
)
|
|
54
60
|
if push.returncode != 0:
|
|
55
61
|
raise MergeConflict(
|
|
@@ -76,6 +82,7 @@ class GitRepositoryWorkspace:
|
|
|
76
82
|
"workspace": self.workspace.to_state(),
|
|
77
83
|
"branch": self.branch,
|
|
78
84
|
"ssh_key": str(self.ssh_key) if self.ssh_key is not None else None,
|
|
85
|
+
"known_hosts": str(self.known_hosts) if self.known_hosts is not None else None,
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
@classmethod
|
|
@@ -84,41 +91,70 @@ class GitRepositoryWorkspace:
|
|
|
84
91
|
if not isinstance(workspace, dict):
|
|
85
92
|
raise StagingError("invalid repository workspace state")
|
|
86
93
|
key = state.get("ssh_key")
|
|
94
|
+
known_hosts = state.get("known_hosts")
|
|
87
95
|
return cls(
|
|
88
96
|
StagedWorkspace.from_state(workspace),
|
|
89
97
|
branch=str(state["branch"]),
|
|
90
98
|
ssh_key=Path(str(key)) if key is not None else None,
|
|
99
|
+
known_hosts=Path(str(known_hosts)) if known_hosts is not None else None,
|
|
91
100
|
)
|
|
92
101
|
|
|
93
102
|
|
|
94
|
-
def
|
|
95
|
-
if
|
|
103
|
+
def _known_hosts_file(known_hosts: Path | None) -> Path | None:
|
|
104
|
+
if known_hosts is None:
|
|
96
105
|
return None
|
|
97
|
-
|
|
98
|
-
if not
|
|
99
|
-
raise StagingError(f"
|
|
106
|
+
path = Path(known_hosts).expanduser().resolve()
|
|
107
|
+
if not path.is_file():
|
|
108
|
+
raise StagingError(f"known_hosts is not a file: {path}")
|
|
109
|
+
if any(ch.isspace() for ch in str(path)):
|
|
110
|
+
# ssh reads UserKnownHostsFile as a whitespace-separated list of files.
|
|
111
|
+
raise StagingError(f"known_hosts path must not contain whitespace: {path}")
|
|
112
|
+
return path
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _ssh_env(ssh_key: Path | None, known_hosts: Path | None = None) -> dict[str, str] | None:
|
|
116
|
+
"""Git's environment for SSH remotes: the given key only, strict host-key checking against the
|
|
117
|
+
given known_hosts file only (else ssh's defaults), never a prompt. None: Git's own defaults."""
|
|
118
|
+
if ssh_key is None and known_hosts is None:
|
|
119
|
+
return None
|
|
120
|
+
command = ["ssh"]
|
|
121
|
+
if ssh_key is not None:
|
|
122
|
+
key = Path(ssh_key).expanduser()
|
|
123
|
+
if not key.is_file():
|
|
124
|
+
raise StagingError(f"SSH key is not a file: {key}")
|
|
125
|
+
command += ["-i", shlex.quote(str(key.resolve())), "-o", "IdentitiesOnly=yes"]
|
|
126
|
+
command += ["-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=yes"]
|
|
127
|
+
pinned = _known_hosts_file(known_hosts)
|
|
128
|
+
if pinned is not None:
|
|
129
|
+
command += [
|
|
130
|
+
"-o", f"UserKnownHostsFile={shlex.quote(str(pinned))}",
|
|
131
|
+
"-o", "GlobalKnownHostsFile=/dev/null",
|
|
132
|
+
]
|
|
100
133
|
return {
|
|
101
134
|
**os.environ,
|
|
102
|
-
"GIT_SSH_COMMAND": (
|
|
103
|
-
f"ssh -i {shlex.quote(str(key.resolve()))} "
|
|
104
|
-
"-o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes"
|
|
105
|
-
),
|
|
135
|
+
"GIT_SSH_COMMAND": " ".join(command),
|
|
106
136
|
"GIT_SSH_VARIANT": "ssh",
|
|
107
137
|
"GIT_TERMINAL_PROMPT": "0",
|
|
108
138
|
}
|
|
109
139
|
|
|
110
140
|
|
|
111
141
|
def stage_repository(
|
|
112
|
-
url: str,
|
|
142
|
+
url: str,
|
|
143
|
+
*,
|
|
144
|
+
root: Path,
|
|
145
|
+
job_id: str,
|
|
146
|
+
ssh_key: Path | None = None,
|
|
147
|
+
known_hosts: Path | None = None,
|
|
113
148
|
) -> GitRepositoryWorkspace:
|
|
114
149
|
"""Clone a trusted URL into root/job_id and make an isolated pi worktree.
|
|
115
150
|
|
|
116
151
|
Repository URLs must be selected by the host, not supplied by untrusted users.
|
|
117
|
-
Git credentials are inherited from the host
|
|
152
|
+
Git credentials are inherited from the host unless ``ssh_key`` is given; ``known_hosts``
|
|
153
|
+
pins the SSH host keys to that file alone. Origin stays unchanged until merge().
|
|
118
154
|
"""
|
|
119
155
|
if not job_id or job_id != Path(job_id).name or job_id in (".", "..") or "\\" in job_id:
|
|
120
156
|
raise StagingError(f"invalid job_id: {job_id!r}")
|
|
121
|
-
ssh_env = _ssh_env(ssh_key)
|
|
157
|
+
ssh_env = _ssh_env(ssh_key, known_hosts)
|
|
122
158
|
root = Path(root).resolve()
|
|
123
159
|
job = root / job_id
|
|
124
160
|
if job.exists():
|
|
@@ -146,6 +182,7 @@ def stage_repository(
|
|
|
146
182
|
workspace,
|
|
147
183
|
branch=branch,
|
|
148
184
|
ssh_key=Path(ssh_key).expanduser().resolve() if ssh_key is not None else None,
|
|
185
|
+
known_hosts=_known_hosts_file(known_hosts),
|
|
149
186
|
)
|
|
150
187
|
except BaseException:
|
|
151
188
|
shutil.rmtree(job, ignore_errors=True)
|
|
@@ -160,6 +197,7 @@ async def start_repository_task(
|
|
|
160
197
|
root: Path,
|
|
161
198
|
job_id: str,
|
|
162
199
|
ssh_key: Path | None = None,
|
|
200
|
+
known_hosts: Path | None = None,
|
|
163
201
|
) -> tuple[PiSession, GitRepositoryWorkspace]:
|
|
164
202
|
"""Clone, stage and start pi; return session and durable workspace.
|
|
165
203
|
|
|
@@ -167,7 +205,12 @@ async def start_repository_task(
|
|
|
167
205
|
when approval can happen after a host restart.
|
|
168
206
|
"""
|
|
169
207
|
workspace = await asyncio.to_thread(
|
|
170
|
-
stage_repository,
|
|
208
|
+
stage_repository,
|
|
209
|
+
url,
|
|
210
|
+
root=root,
|
|
211
|
+
job_id=job_id,
|
|
212
|
+
ssh_key=ssh_key,
|
|
213
|
+
known_hosts=known_hosts,
|
|
171
214
|
)
|
|
172
215
|
try:
|
|
173
216
|
session = await client.start(task, cwd=workspace.path)
|
|
@@ -120,6 +120,79 @@ def test_ssh_key_used_for_clone_and_push_after_restore(
|
|
|
120
120
|
ws.cleanup()
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
def test_known_hosts_pins_clone_and_push_after_restore(
|
|
124
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
125
|
+
) -> None:
|
|
126
|
+
import pidriver.repository as repository
|
|
127
|
+
import pidriver.staging as staging
|
|
128
|
+
|
|
129
|
+
bare = remote(tmp_path)
|
|
130
|
+
key = tmp_path / "id_ed25519"
|
|
131
|
+
key.write_text("fake private key")
|
|
132
|
+
pinned = tmp_path / "known_hosts"
|
|
133
|
+
pinned.write_text("example.invalid ssh-ed25519 AAAA\n")
|
|
134
|
+
real_run = subprocess.run
|
|
135
|
+
commands: list[str] = []
|
|
136
|
+
|
|
137
|
+
def capture(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
|
|
138
|
+
if "clone" in argv:
|
|
139
|
+
commands.append(kwargs["env"]["GIT_SSH_COMMAND"])
|
|
140
|
+
argv = [*argv[:3], str(bare), argv[4]]
|
|
141
|
+
elif "push" in argv:
|
|
142
|
+
commands.append(kwargs["env"]["GIT_SSH_COMMAND"])
|
|
143
|
+
return real_run(argv, **kwargs)
|
|
144
|
+
|
|
145
|
+
monkeypatch.setattr(repository.subprocess, "run", capture)
|
|
146
|
+
monkeypatch.setattr(staging.subprocess, "run", capture)
|
|
147
|
+
ws = stage_repository(
|
|
148
|
+
"git@example.invalid:project.git",
|
|
149
|
+
root=tmp_path / "jobs",
|
|
150
|
+
job_id="pinned-1",
|
|
151
|
+
ssh_key=key,
|
|
152
|
+
known_hosts=pinned,
|
|
153
|
+
)
|
|
154
|
+
(ws.path / "a.txt").write_text("pinned\n")
|
|
155
|
+
state = ws.to_state()
|
|
156
|
+
assert state["known_hosts"] == str(pinned.resolve())
|
|
157
|
+
GitRepositoryWorkspace.from_state(state).merge()
|
|
158
|
+
assert git(bare, "show", "main:a.txt") == "pinned"
|
|
159
|
+
assert len(commands) == 2
|
|
160
|
+
for command in commands:
|
|
161
|
+
assert f"UserKnownHostsFile={pinned.resolve()}" in command
|
|
162
|
+
assert "GlobalKnownHostsFile=/dev/null" in command
|
|
163
|
+
assert "StrictHostKeyChecking=yes" in command
|
|
164
|
+
ws.cleanup()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_state_without_known_hosts_still_restores(tmp_path: Path) -> None:
|
|
168
|
+
bare = remote(tmp_path)
|
|
169
|
+
ws = stage_repository(str(bare), root=tmp_path / "jobs", job_id="old-state")
|
|
170
|
+
state = ws.to_state()
|
|
171
|
+
del state["known_hosts"] # a 0.4.1 snapshot
|
|
172
|
+
restored = GitRepositoryWorkspace.from_state(state)
|
|
173
|
+
assert restored.known_hosts is None and restored.ssh_key is None
|
|
174
|
+
restored.discard()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@pytest.mark.parametrize("name", ["missing", "with space"])
|
|
178
|
+
def test_bad_known_hosts_rejected_before_cloning(tmp_path: Path, name: str) -> None:
|
|
179
|
+
key = tmp_path / "id_ed25519"
|
|
180
|
+
key.write_text("fake private key")
|
|
181
|
+
pinned = tmp_path / name / "known_hosts"
|
|
182
|
+
if name != "missing":
|
|
183
|
+
pinned.parent.mkdir()
|
|
184
|
+
pinned.write_text("")
|
|
185
|
+
with pytest.raises(StagingError, match="known_hosts"):
|
|
186
|
+
stage_repository(
|
|
187
|
+
"git@host:repo.git",
|
|
188
|
+
root=tmp_path / "jobs",
|
|
189
|
+
job_id="ssh-3",
|
|
190
|
+
ssh_key=key,
|
|
191
|
+
known_hosts=pinned,
|
|
192
|
+
)
|
|
193
|
+
assert not (tmp_path / "jobs" / "ssh-3").exists()
|
|
194
|
+
|
|
195
|
+
|
|
123
196
|
def test_missing_ssh_key_rejected(tmp_path: Path) -> None:
|
|
124
197
|
with pytest.raises(StagingError, match="SSH key"):
|
|
125
198
|
stage_repository(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/extension_ui_request.select.approval.json
RENAMED
|
File without changes
|
{pidriver-0.4.1 → pidriver-0.4.2}/tests/fixtures/frames/extension_ui_request.select.ask.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|