sshscript 4.0.1__py3-none-any.whl → 5.0.0__py3-none-any.whl
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.
- sshscript/_version.py +1 -1
- sshscript/session.py +100 -2
- sshscript/session.pyi +15 -3
- sshscript/sessionsettings.py +17 -1
- {sshscript-4.0.1.dist-info → sshscript-5.0.0.dist-info}/METADATA +66 -8
- {sshscript-4.0.1.dist-info → sshscript-5.0.0.dist-info}/RECORD +10 -10
- {sshscript-4.0.1.dist-info → sshscript-5.0.0.dist-info}/WHEEL +0 -0
- {sshscript-4.0.1.dist-info → sshscript-5.0.0.dist-info}/entry_points.txt +0 -0
- {sshscript-4.0.1.dist-info → sshscript-5.0.0.dist-info}/licenses/LICENSE.txt +0 -0
- {sshscript-4.0.1.dist-info → sshscript-5.0.0.dist-info}/top_level.txt +0 -0
sshscript/_version.py
CHANGED
sshscript/session.py
CHANGED
|
@@ -23,6 +23,7 @@ else:
|
|
|
23
23
|
import patching
|
|
24
24
|
|
|
25
25
|
import ast
|
|
26
|
+
import base64
|
|
26
27
|
import contextvars
|
|
27
28
|
import threading
|
|
28
29
|
import paramiko
|
|
@@ -402,6 +403,9 @@ class Session(object):
|
|
|
402
403
|
# A child shares its tree lock; independent roots do not block each other.
|
|
403
404
|
self._lifecycle_lock = parent._lifecycle_lock if parent is not None else threading.RLock()
|
|
404
405
|
self._settings = parent.get() if parent is not None else root_defaults()
|
|
406
|
+
if parent is not None:
|
|
407
|
+
# A path belongs to this host, not to the host of the parent.
|
|
408
|
+
self._settings['known_hosts_path'] = None
|
|
405
409
|
self.logger = SessionLogger(self._settings)
|
|
406
410
|
## when exec_command() or withdollar() was called,
|
|
407
411
|
## this value was stored, so user can access its stdout, stderr and exitcode
|
|
@@ -458,6 +462,8 @@ class Session(object):
|
|
|
458
462
|
verbose_stderr = property(lambda self: self.get('verbose_stderr'), lambda self, v: self.set(verbose_stderr=v))
|
|
459
463
|
log_level = property(lambda self: self.get('log_level'), lambda self, v: self.set(log_level=v))
|
|
460
464
|
policy = property(lambda self: self.get('policy'), lambda self, v: self.set(policy=v))
|
|
465
|
+
known_hosts = property(lambda self: self.get('known_hosts'), lambda self, v: self.set(known_hosts=v))
|
|
466
|
+
known_hosts_path = property(lambda self: self.get('known_hosts_path'), lambda self, v: self.set(known_hosts_path=v))
|
|
461
467
|
|
|
462
468
|
## added in v2.0.3
|
|
463
469
|
## always return the session which is not connected to execute commands by subprocess at localhost
|
|
@@ -677,8 +683,88 @@ class Session(object):
|
|
|
677
683
|
self.logger.debug('Script requested exit (exit_code=%s)', code)
|
|
678
684
|
raise SSHScriptExit(message,code)
|
|
679
685
|
|
|
686
|
+
def _read_known_hosts(self):
|
|
687
|
+
"""Read a host-owned trust file strictly; never execute a remote shell."""
|
|
688
|
+
self._ensure_open()
|
|
689
|
+
path = self.known_hosts_path or '~/.ssh/known_hosts'
|
|
690
|
+
source = self.host if self._client is not None else 'localhost'
|
|
691
|
+
try:
|
|
692
|
+
if self._client is not None:
|
|
693
|
+
sftp = self.sftp
|
|
694
|
+
if path == '~' or path.startswith('~/'):
|
|
695
|
+
path = posixpath.join(sftp.normalize('.'), path[2:] if path != '~' else '')
|
|
696
|
+
elif path.startswith('~'):
|
|
697
|
+
raise ValueError('remote ~user paths are not supported')
|
|
698
|
+
with sftp.open(path, 'rb') as stream:
|
|
699
|
+
data = stream.read()
|
|
700
|
+
else:
|
|
701
|
+
path = os.path.expanduser(path)
|
|
702
|
+
with open(path, 'rb') as stream:
|
|
703
|
+
data = stream.read()
|
|
704
|
+
text = data.decode('utf-8') if isinstance(data, bytes) else data
|
|
705
|
+
except Exception as exc:
|
|
706
|
+
raise SSHScriptException(
|
|
707
|
+
f'Unable to read known_hosts on {source}: {path} ({type(exc).__name__})'
|
|
708
|
+
) from exc
|
|
709
|
+
|
|
710
|
+
keys = paramiko.HostKeys()
|
|
711
|
+
for lineno, line in enumerate(text.splitlines(), 1):
|
|
712
|
+
line = line.strip()
|
|
713
|
+
if not line or line.startswith('#'):
|
|
714
|
+
continue
|
|
715
|
+
try:
|
|
716
|
+
fields = line.split()
|
|
717
|
+
if len(fields) < 3 or fields[0].startswith('@'):
|
|
718
|
+
raise ValueError('expected a plain host-key entry')
|
|
719
|
+
# Paramiko matches literal names and hashed names, not patterns.
|
|
720
|
+
if any(c in fields[0] for c in '*?!'):
|
|
721
|
+
raise ValueError('host patterns are not supported')
|
|
722
|
+
base64.b64decode(fields[2], validate=True)
|
|
723
|
+
entry = paramiko.hostkeys.HostKeyEntry.from_line(' '.join(fields[:3]), lineno)
|
|
724
|
+
if entry is None or entry.key is None or entry.key.get_name() != fields[1]:
|
|
725
|
+
raise ValueError('unsupported host-key entry')
|
|
726
|
+
for name in entry.hostnames:
|
|
727
|
+
if not name:
|
|
728
|
+
raise ValueError('empty hostname')
|
|
729
|
+
if name.startswith('|'):
|
|
730
|
+
# Validate without relying on Paramiko's assertions (-O).
|
|
731
|
+
parts = name.split('|')
|
|
732
|
+
if (len(parts) != 4 or parts[:2] != ['', '1'] or
|
|
733
|
+
any(len(base64.b64decode(part, validate=True)) != 20
|
|
734
|
+
for part in parts[2:])):
|
|
735
|
+
raise ValueError('invalid hashed hostname')
|
|
736
|
+
keys.add(name, entry.key.get_name(), entry.key)
|
|
737
|
+
except Exception:
|
|
738
|
+
# Do not include raw key lines in error reports.
|
|
739
|
+
raise SSHScriptException(
|
|
740
|
+
f'Invalid known_hosts on {source}: {path}, line {lineno}'
|
|
741
|
+
) from None
|
|
742
|
+
return keys
|
|
743
|
+
|
|
744
|
+
def _load_connection_host_keys(self, client, host, port, strategy):
|
|
745
|
+
if strategy == 'local':
|
|
746
|
+
sources = [self.local_session]
|
|
747
|
+
elif strategy == 'parent':
|
|
748
|
+
sources = [self]
|
|
749
|
+
else:
|
|
750
|
+
sources = []
|
|
751
|
+
source = self
|
|
752
|
+
while source is not None:
|
|
753
|
+
sources.append(source)
|
|
754
|
+
source = source.parent
|
|
755
|
+
identity = host if port == 22 else f'[{host}]:{port}'
|
|
756
|
+
for source in sources:
|
|
757
|
+
keys = source._read_known_hosts()
|
|
758
|
+
found = keys.lookup(identity)
|
|
759
|
+
if found is not None:
|
|
760
|
+
# Only this layer is installed. Paramiko negotiates its key types
|
|
761
|
+
# and rejects a mismatch without consulting more distant layers.
|
|
762
|
+
for key_type, key in found.items():
|
|
763
|
+
client.get_host_keys().add(identity, key_type, key)
|
|
764
|
+
return
|
|
765
|
+
|
|
680
766
|
@export2Dollar
|
|
681
|
-
def connect(self,host,username=None,password=None,port=None,policy=UNSET,*,ssh_config=None,**kw):
|
|
767
|
+
def connect(self,host,username=None,password=None,port=None,policy=UNSET,*,ssh_config=None,known_hosts=UNSET,**kw):
|
|
682
768
|
"""Return a connected child session, optionally through this session's SSH link.
|
|
683
769
|
|
|
684
770
|
Use with session.connect("user@host") as remote to scope the connection.
|
|
@@ -701,6 +787,15 @@ class Session(object):
|
|
|
701
787
|
parent; its connect/banner/auth timeouts default to 30 seconds and may be
|
|
702
788
|
overridden. KEEPALIVE_INTERVAL sets keepalive seconds (default 60; 0 disables).
|
|
703
789
|
Connection failures propagate.
|
|
790
|
+
|
|
791
|
+
known_hosts selects local, parent (default: this Session's host), or
|
|
792
|
+
chain (this host, then ancestors through localhost). Children inherit
|
|
793
|
+
the setting; a connect override affects only that connection. Each
|
|
794
|
+
source uses its own known_hosts_path or ~/.ssh/known_hosts. Paths are
|
|
795
|
+
not inherited. The nearest source containing the target is authoritative:
|
|
796
|
+
a mismatch never falls back. Read/parse failures propagate in all modes.
|
|
797
|
+
Unknown keys use policy only after all selected sources lack the target.
|
|
798
|
+
These sources are read-only, including with AutoAddPolicy.
|
|
704
799
|
"""
|
|
705
800
|
|
|
706
801
|
if self.closed:
|
|
@@ -708,6 +803,9 @@ class Session(object):
|
|
|
708
803
|
|
|
709
804
|
if policy is UNSET:
|
|
710
805
|
policy = self.policy
|
|
806
|
+
if known_hosts is UNSET:
|
|
807
|
+
known_hosts = self.known_hosts
|
|
808
|
+
validate({'known_hosts': known_hosts})
|
|
711
809
|
|
|
712
810
|
## host might be in format of "username@hostname"
|
|
713
811
|
if isinstance(host, str) and '@' in host:
|
|
@@ -742,7 +840,7 @@ class Session(object):
|
|
|
742
840
|
def connect_client(host,username,password,port,policy,**kw):
|
|
743
841
|
client = paramiko.SSHClient()
|
|
744
842
|
try:
|
|
745
|
-
|
|
843
|
+
self._load_connection_host_keys(client, host, port, known_hosts)
|
|
746
844
|
if policy is not None:
|
|
747
845
|
client.set_missing_host_key_policy(policy)
|
|
748
846
|
client.connect(host,username=username,password=password,port=port,**kw)
|
sshscript/session.pyi
CHANGED
|
@@ -8,12 +8,15 @@ from .sessionwrapper import SessionWrapper
|
|
|
8
8
|
|
|
9
9
|
Command = str | list[str] | tuple[str, ...]
|
|
10
10
|
HostKeyPolicy = MissingHostKeyPolicy | type[MissingHostKeyPolicy] | None
|
|
11
|
+
KnownHosts = Literal['local', 'parent', 'chain']
|
|
11
12
|
class SessionSettings(TypedDict):
|
|
12
13
|
check: bool
|
|
13
14
|
verbose: bool
|
|
14
15
|
verbose_stderr: bool
|
|
15
16
|
log_level: int
|
|
16
17
|
policy: HostKeyPolicy
|
|
18
|
+
known_hosts: KnownHosts
|
|
19
|
+
known_hosts_path: str | None
|
|
17
20
|
|
|
18
21
|
class Session:
|
|
19
22
|
closed: bool
|
|
@@ -24,12 +27,17 @@ class Session:
|
|
|
24
27
|
verbose: bool
|
|
25
28
|
verbose_stderr: bool
|
|
26
29
|
policy: HostKeyPolicy
|
|
30
|
+
known_hosts: KnownHosts
|
|
31
|
+
@property
|
|
32
|
+
def known_hosts_path(self) -> str | None: ...
|
|
33
|
+
@known_hosts_path.setter
|
|
34
|
+
def known_hosts_path(self, value: str | PathLike[str] | None) -> None: ...
|
|
27
35
|
@property
|
|
28
36
|
def log_level(self) -> int: ...
|
|
29
37
|
@log_level.setter
|
|
30
38
|
def log_level(self, value: int | str) -> None: ...
|
|
31
39
|
def __init__(self, parent: Session | None = ...) -> None: ...
|
|
32
|
-
def set(self, *, check: bool = ..., verbose: bool = ..., verbose_stderr: bool = ..., log_level: int | str = ..., policy: HostKeyPolicy = ...) -> None: ...
|
|
40
|
+
def set(self, *, check: bool = ..., verbose: bool = ..., verbose_stderr: bool = ..., log_level: int | str = ..., policy: HostKeyPolicy = ..., known_hosts: KnownHosts = ..., known_hosts_path: str | PathLike[str] | None = ...) -> None: ...
|
|
33
41
|
@overload
|
|
34
42
|
def get(self, name: None = ...) -> SessionSettings: ...
|
|
35
43
|
@overload
|
|
@@ -39,7 +47,11 @@ class Session:
|
|
|
39
47
|
@overload
|
|
40
48
|
def get(self, name: Literal['policy']) -> HostKeyPolicy: ...
|
|
41
49
|
@overload
|
|
42
|
-
def get(self, name:
|
|
50
|
+
def get(self, name: Literal['known_hosts']) -> KnownHosts: ...
|
|
51
|
+
@overload
|
|
52
|
+
def get(self, name: Literal['known_hosts_path']) -> str | None: ...
|
|
53
|
+
@overload
|
|
54
|
+
def get(self, name: str) -> bool | int | str | HostKeyPolicy: ...
|
|
43
55
|
@property
|
|
44
56
|
def host(self) -> str | None: ...
|
|
45
57
|
@property
|
|
@@ -57,7 +69,7 @@ class Session:
|
|
|
57
69
|
__call__ = exec_command
|
|
58
70
|
send_line = exec_command
|
|
59
71
|
def start(self, cmd: Command, *, shell: bool | str | None = ..., shell_executable: str | None = ..., timeout: float | None = ..., stop_timeout: float = ..., capture_limit: int = ..., check: bool = ..., input: str | bytes | None = ..., env: dict[str, str] | None = ..., get_pty: bool = ...) -> CommandJob: ...
|
|
60
|
-
def connect(self, host: str, username: str | None = ..., password: str | None = ..., port: int | None = ..., policy: Any = ..., *, ssh_config: str | PathLike[str] | bool | None = ..., **kw: Any) -> Session: ...
|
|
72
|
+
def connect(self, host: str, username: str | None = ..., password: str | None = ..., port: int | None = ..., policy: Any = ..., *, ssh_config: str | PathLike[str] | bool | None = ..., known_hosts: KnownHosts = ..., **kw: Any) -> Session: ...
|
|
61
73
|
@staticmethod
|
|
62
74
|
def resolve_connection(host: str, **kwargs: Any) -> dict[str, Any]: ...
|
|
63
75
|
def shell(self, command: str | None = ..., funcname: str = ..., get_pty: bool = ..., *args: Any, **kw: Any) -> ContextManager[SessionWrapper]: ...
|
sshscript/sessionsettings.py
CHANGED
|
@@ -7,7 +7,8 @@ from paramiko import MissingHostKeyPolicy
|
|
|
7
7
|
from types import MappingProxyType
|
|
8
8
|
|
|
9
9
|
DEFAULTS = MappingProxyType(dict(check=False, verbose=False, verbose_stderr=False,
|
|
10
|
-
log_level=logging.INFO, policy=None
|
|
10
|
+
log_level=logging.INFO, policy=None,
|
|
11
|
+
known_hosts='parent', known_hosts_path=None))
|
|
11
12
|
UNSET = object()
|
|
12
13
|
_defaults = ContextVar('sshscript_execution_defaults', default={})
|
|
13
14
|
|
|
@@ -23,6 +24,21 @@ def validate(values):
|
|
|
23
24
|
(isinstance(value, type) and issubclass(value, MissingHostKeyPolicy))
|
|
24
25
|
):
|
|
25
26
|
raise TypeError('policy must be a MissingHostKeyPolicy instance, subclass, or None')
|
|
27
|
+
elif key == 'known_hosts':
|
|
28
|
+
if not isinstance(value, str):
|
|
29
|
+
raise TypeError('known_hosts must be local, parent, or chain')
|
|
30
|
+
if value not in ('local', 'parent', 'chain'):
|
|
31
|
+
raise ValueError('known_hosts must be local, parent, or chain')
|
|
32
|
+
elif key == 'known_hosts_path':
|
|
33
|
+
if value is not None:
|
|
34
|
+
if not isinstance(value, (str, os.PathLike)):
|
|
35
|
+
raise TypeError('known_hosts_path must be a text path or None')
|
|
36
|
+
value = os.fspath(value)
|
|
37
|
+
if not isinstance(value, str):
|
|
38
|
+
raise TypeError('known_hosts_path must be a text path or None')
|
|
39
|
+
if not value or '\x00' in value:
|
|
40
|
+
raise ValueError('known_hosts_path must be nonempty and contain no NUL')
|
|
41
|
+
result[key] = value
|
|
26
42
|
elif key == 'log_level':
|
|
27
43
|
if isinstance(value, str):
|
|
28
44
|
value = logging.getLevelName(value.upper())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sshscript
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0
|
|
4
4
|
Summary: Python automation for local processes, SSH, and SSHScript .spy files
|
|
5
5
|
Author-email: "Yeh, Hsin-Yuan" <iapyeh@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,9 +37,12 @@ Dynamic: license-file
|
|
|
37
37
|
[](https://github.com/iapyeh/sshscript/actions/workflows/codeql.yml)
|
|
38
38
|
[](https://github.com/iapyeh/sshscript/blob/release/LICENSE.txt)
|
|
39
39
|
|
|
40
|
+
**AI agent ready automation for local and remote commands.**
|
|
41
|
+
|
|
40
42
|
SSHScript is a Python automation library for running commands locally and over
|
|
41
|
-
SSH through one `Session` API. It
|
|
42
|
-
|
|
43
|
+
SSH through one `Session` API. It provides an agent guide, a portable skill
|
|
44
|
+
bundle, and copyable prompts to help your AI agent use its execution contract.
|
|
45
|
+
Optional dollar syntax supports compact `.spy` automation files.
|
|
43
46
|
|
|
44
47
|
SSHScript aims to help engineers and AI agents execute, understand, and hand
|
|
45
48
|
off automation reliably: identify the execution host, retain each command's
|
|
@@ -47,7 +50,7 @@ output and status, distinguish failure from timeout, and make cleanup and
|
|
|
47
50
|
unknown termination explicit. A successful command exit is evidence about
|
|
48
51
|
that command; verify the deployment or other application outcome separately.
|
|
49
52
|
|
|
50
|
-
**Current release:** [
|
|
53
|
+
**Current release:** [5.0.0](https://github.com/iapyeh/sshscript/releases/tag/v5.0.0)
|
|
51
54
|
(Production/Stable) · **Python:** 3.11 or newer · **Tested:** Python
|
|
52
55
|
3.11–3.14 on Linux and macOS
|
|
53
56
|
|
|
@@ -57,10 +60,45 @@ that command; verify the deployment or other application outcome separately.
|
|
|
57
60
|
[Security](https://github.com/iapyeh/sshscript/security/policy) ·
|
|
58
61
|
[Support](https://github.com/iapyeh/sshscript/blob/release/SUPPORT.md)
|
|
59
62
|
|
|
60
|
-
**Version
|
|
63
|
+
**Version 5.0.0** preserves the 4.0 command execution contract and changes the
|
|
64
|
+
default host-key source to the calling host. Provision its known_hosts file or
|
|
65
|
+
select `known_hosts="local"` when migrating existing nested connections.
|
|
61
66
|
Upgrade 3.1.5 automation using the migration guide and application regressions.
|
|
62
67
|
See [version policy and migration](VERSIONING.md) before upgrading automation.
|
|
63
68
|
|
|
69
|
+
## AI agent ready
|
|
70
|
+
|
|
71
|
+
**Give your agent one URL:**
|
|
72
|
+
[SSHScript for AI Agents](https://iapyeh.github.io/sshscript/v3a/ai-agents/).
|
|
73
|
+
Prepare it for future automation or give it a task to complete now.
|
|
74
|
+
|
|
75
|
+
- **A guide for agents:** version-aware API selection, exact input handling,
|
|
76
|
+
retained command results, timeouts, authentication and cleanup.
|
|
77
|
+
- **A portable skill bundle:** `SKILL.md` with the agent guide and API and
|
|
78
|
+
version-policy references, ready to install through your agent's skill
|
|
79
|
+
mechanism.
|
|
80
|
+
- **Prompts for humans:** copy a setup prompt to request skill installation and
|
|
81
|
+
a saved tool preference, or a task prompt to describe hosts, inputs and
|
|
82
|
+
expected results.
|
|
83
|
+
|
|
84
|
+
Start with this prompt, even before you have a task:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
Read https://iapyeh.github.io/sshscript/v3a/ai-agents/ and the Agent Guide.
|
|
88
|
+
If you support skills, download and install the complete skill bundle. If you
|
|
89
|
+
support persistent preferences, save this preference: for future local or
|
|
90
|
+
remote command execution and automation, first assess whether SSHScript is
|
|
91
|
+
suitable and use it when appropriate. Report whether the skill can be loaded
|
|
92
|
+
and the preference was saved, and explain any limits.
|
|
93
|
+
For now, do not connect to remote hosts or run demonstration commands.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The guides are ordinary Markdown and web pages for agents that can read files
|
|
97
|
+
or URLs. Skill installation and persistent preferences depend on the agent's
|
|
98
|
+
capabilities. See the [repository AI entry](AI_GUIDE.md),
|
|
99
|
+
[agent guide](skills/sshscript/references/agent-guide.md) and
|
|
100
|
+
[skill instructions](skills/sshscript/SKILL.md) for the source documents.
|
|
101
|
+
|
|
64
102
|
## Recommended API
|
|
65
103
|
|
|
66
104
|
Start with [the canonical API guide](API_GUIDE.md): version-labelled examples,
|
|
@@ -82,6 +120,7 @@ and [the cross-system test guide](src/sshscript/unittest/README.console-authenti
|
|
|
82
120
|
|
|
83
121
|
## Why SSHScript?
|
|
84
122
|
|
|
123
|
+
- Prepare your AI agent with a dedicated guide, portable skill and setup prompts.
|
|
85
124
|
- Use the same interface for local subprocesses and remote SSH commands.
|
|
86
125
|
- Traverse nested SSH connections without rebuilding connection logic.
|
|
87
126
|
- Keep ordinary Python functions, packages, exceptions, data processing, and
|
|
@@ -114,7 +153,7 @@ pin SSHScript and all transitive dependencies in your application's lock file.
|
|
|
114
153
|
To install this release explicitly:
|
|
115
154
|
|
|
116
155
|
```sh
|
|
117
|
-
python3 -m pip install "sshscript==
|
|
156
|
+
python3 -m pip install "sshscript==5.0.0"
|
|
118
157
|
```
|
|
119
158
|
|
|
120
159
|
Use `python3 -m pip install --upgrade sshscript` to upgrade. The optional
|
|
@@ -123,7 +162,8 @@ it never installs an update by itself.
|
|
|
123
162
|
|
|
124
163
|
## 60-second local quickstart
|
|
125
164
|
|
|
126
|
-
This example
|
|
165
|
+
This example runs on 5.0.0 and retains compatibility with the 3.1.5 command API.
|
|
166
|
+
The metadata below records that older compatibility baseline.
|
|
127
167
|
|
|
128
168
|
<!-- example: {"id":"readme-quickstart", "profile":"3.1.5", "stdout":"sshscript is ready\nexit code: 0\n"} -->
|
|
129
169
|
```python
|
|
@@ -374,10 +414,28 @@ Unknown and changed host keys are rejected unless the caller explicitly
|
|
|
374
414
|
supplies a different Paramiko policy. Do not use automatic key acceptance in a
|
|
375
415
|
production workflow unless a separate trusted bootstrap process has already
|
|
376
416
|
verified the key. See the
|
|
377
|
-
[SSHScript
|
|
417
|
+
[SSHScript documentation](https://iapyeh.github.io/sshscript/v3a/) for
|
|
378
418
|
nested connections, timeouts, file transfer, `sudo`, `su`, and interactive
|
|
379
419
|
programs.
|
|
380
420
|
|
|
421
|
+
## Host-key sources in 5.0
|
|
422
|
+
|
|
423
|
+
SSHScript 5.0.0 uses `known_hosts="parent"` as the default: a local
|
|
424
|
+
Session reads localhost's trust file; `host1.connect("host2")` reads host1's
|
|
425
|
+
trust file over SFTP. This matches the host ownership of `pkey_path`.
|
|
426
|
+
SSHScript 4.0.2 and earlier use localhost for all host-key checks.
|
|
427
|
+
|
|
428
|
+
Set `session.set(known_hosts="chain")` to search from the calling Session
|
|
429
|
+
through its ancestors to localhost. The nearest layer with a target record
|
|
430
|
+
wins; a mismatch or read/parse error stops the connection. Set
|
|
431
|
+
`session.set(known_hosts="local")` to preserve previous behavior.
|
|
432
|
+
Each Session can set its own `known_hosts_path`; this path is not inherited.
|
|
433
|
+
`connect(known_hosts=...)` overrides only one connection. `pkey_path` remains
|
|
434
|
+
on the calling host for every strategy. `policy` handles only unknown keys;
|
|
435
|
+
`AutoAddPolicy` does not write these trust files. See the
|
|
436
|
+
[host-key source contract](API_GUIDE.md#host-key-sources-unreleased) for supported
|
|
437
|
+
file formats, strict error handling and migration.
|
|
438
|
+
|
|
381
439
|
## Reusing SSH configuration
|
|
382
440
|
|
|
383
441
|
Connections from a local session read `~/.ssh/config` if it exists. Supported
|
|
@@ -2,7 +2,7 @@ _sshscript_cli.py,sha256=Z3BwvYFLKJAmy8Wi6xDUqHiIbXT498_uX4WEiguD9n0,576
|
|
|
2
2
|
sshscript/__init__.py,sha256=bVqkgcifvePikqOTTMBC8bTSFz-UeFjSV-12seFONgw,3114
|
|
3
3
|
sshscript/__init__.pyi,sha256=HOvKjFhwqm6OqTfBDkSGHyo2pGcmAB0cHOPz2jEow2c,810
|
|
4
4
|
sshscript/_runtime.py,sha256=wJt8XSxK21Ntt-oSUhEF4lN8Nx10H1X8JyIG2ebaPSo,443
|
|
5
|
-
sshscript/_version.py,sha256=
|
|
5
|
+
sshscript/_version.py,sha256=ssRZ5iGxPpxYOxW6pHi-PtvxOGEaaBmJt3P5ZoMxybk,77
|
|
6
6
|
sshscript/channelgeneric.py,sha256=J_tBZa4xrXLjDN3xjfkHukzynhkT0-bKkz-Ds3RKIaA,75822
|
|
7
7
|
sshscript/channelssh.py,sha256=aVdI3KsUoSOujCDLpuI0I3m5YDWZCvIeB8a4J93P7r0,12038
|
|
8
8
|
sshscript/channelsubprocess.py,sha256=mWPMxFMV6ZMx-VPWPNgtRvOAW9pjRZNtXddl0qY7UQk,12162
|
|
@@ -18,9 +18,9 @@ sshscript/dollarparser.py,sha256=7cl4QgffzfueeGOPN-NeoLP_3bKm04I9cVtCyag7JcE,963
|
|
|
18
18
|
sshscript/errorutils.py,sha256=MgwYoPipEVCa9L33FmVoYXl77CD-Mrq958q_ft3cuvw,16386
|
|
19
19
|
sshscript/patching.py,sha256=OM6vjGeP0iyBnzpai5BHn9qqkzJp9375npVb97D06LY,6581
|
|
20
20
|
sshscript/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
sshscript/session.py,sha256=
|
|
22
|
-
sshscript/session.pyi,sha256=
|
|
23
|
-
sshscript/sessionsettings.py,sha256=
|
|
21
|
+
sshscript/session.py,sha256=AvaqFhWbNRERE5qJfYp0uWdR8mQlzl2aT32FQSwqcIs,78406
|
|
22
|
+
sshscript/session.pyi,sha256=gK0INrScwjTk0WqKGX458Iw-toOkRikRtSvdTvX0_-s,4857
|
|
23
|
+
sshscript/sessionsettings.py,sha256=QJnjQtPpJBNUbIboICKcuNxZh3yN4vAuL_yw_5LL3d4,4612
|
|
24
24
|
sshscript/sessionwrapper.py,sha256=r6BI6xBwrA-nqqa6YsXzaUUtxz5pv_483Xu6Ywlzs4I,19019
|
|
25
25
|
sshscript/sessionwrapper.pyi,sha256=vzdJjl-vPB1bP57uXe78jGpjC4ag2t_Aqowg_4ggbVY,1585
|
|
26
26
|
sshscript/spyimporter.py,sha256=ASAs2nPBU1mtpI7FquXYJctLFQR_PeGMnhjcw1G6LPg,5574
|
|
@@ -28,9 +28,9 @@ sshscript/sshconfig.py,sha256=aPfVOd-bx4k7gBuDeSb55Y812G4QNGeL3sbZPLPdiic,6295
|
|
|
28
28
|
sshscript/sshscript.py,sha256=BXQG1yA-9M3l0JDbCwR3Lg5CCpn3qnJhQ1Xx6Q9-2eA,16940
|
|
29
29
|
sshscript/stdio.py,sha256=Ql_7wX7oJ5vY74uOlCaotwoyqXrNteYdSl-aM4ersVM,16040
|
|
30
30
|
sshscript/tokenparser.py,sha256=dGHY2s8Twjz0Q-r6nanmAL8VCQxZWqmM6g4KvTgIuzs,22396
|
|
31
|
-
sshscript-
|
|
32
|
-
sshscript-
|
|
33
|
-
sshscript-
|
|
34
|
-
sshscript-
|
|
35
|
-
sshscript-
|
|
36
|
-
sshscript-
|
|
31
|
+
sshscript-5.0.0.dist-info/licenses/LICENSE.txt,sha256=FPqC-mLQDBkQ26LK_CAUGEUQQ7eyFTbftywlk2GMlls,1075
|
|
32
|
+
sshscript-5.0.0.dist-info/METADATA,sha256=WrYqMxN3lzhnL8whxN7gYOdVw7WKrrAoteeEue7tHBo,29353
|
|
33
|
+
sshscript-5.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
34
|
+
sshscript-5.0.0.dist-info/entry_points.txt,sha256=JLkqym2UQso2R41RvFz9RAnCKFY8-nnrYgB4ER-zTGo,50
|
|
35
|
+
sshscript-5.0.0.dist-info/top_level.txt,sha256=5d41PAJBhS-f-8aJKJegdhs8BIg_jpigSG83tCQhhfg,25
|
|
36
|
+
sshscript-5.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|