sshscript 4.0.1__tar.gz → 5.0.0__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.
Files changed (116) hide show
  1. sshscript-5.0.0/AI_GUIDE.md +58 -0
  2. {sshscript-4.0.1 → sshscript-5.0.0}/API_GUIDE.md +148 -13
  3. {sshscript-4.0.1 → sshscript-5.0.0}/CHANGELOG.md +22 -0
  4. {sshscript-4.0.1 → sshscript-5.0.0}/MANIFEST.in +3 -0
  5. {sshscript-4.0.1/src/sshscript.egg-info → sshscript-5.0.0}/PKG-INFO +66 -8
  6. {sshscript-4.0.1 → sshscript-5.0.0}/README.md +65 -7
  7. {sshscript-4.0.1 → sshscript-5.0.0}/SECURITY.md +11 -2
  8. {sshscript-4.0.1 → sshscript-5.0.0}/VERSIONING.md +21 -4
  9. sshscript-5.0.0/skills/sshscript/SKILL.md +76 -0
  10. sshscript-5.0.0/skills/sshscript/references/agent-guide.md +360 -0
  11. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/_version.py +1 -1
  12. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/session.py +100 -2
  13. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/session.pyi +15 -3
  14. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/sessionsettings.py +17 -1
  15. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_command_job.py +5 -1
  16. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_logger_integration.py +16 -3
  17. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_openssh_integration.py +30 -0
  18. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_recommended_examples.py +22 -1
  19. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_ssh_config.py +7 -1
  20. sshscript-5.0.0/src/sshscript/unittest/test_ssh_security.py +531 -0
  21. {sshscript-4.0.1 → sshscript-5.0.0/src/sshscript.egg-info}/PKG-INFO +66 -8
  22. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript.egg-info/SOURCES.txt +4 -0
  23. {sshscript-4.0.1 → sshscript-5.0.0}/tools/check_public_types.py +14 -5
  24. {sshscript-4.0.1 → sshscript-5.0.0}/tools/prepare_release.py +3 -0
  25. sshscript-5.0.0/tools/sync_ai_docs.py +97 -0
  26. {sshscript-4.0.1 → sshscript-5.0.0}/tools/sync_api_guide.py +1 -1
  27. sshscript-4.0.1/src/sshscript/unittest/test_ssh_security.py +0 -295
  28. {sshscript-4.0.1 → sshscript-5.0.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  29. {sshscript-4.0.1 → sshscript-5.0.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  30. {sshscript-4.0.1 → sshscript-5.0.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  31. {sshscript-4.0.1 → sshscript-5.0.0}/.github/dependabot.yml +0 -0
  32. {sshscript-4.0.1 → sshscript-5.0.0}/.github/pull_request_template.md +0 -0
  33. {sshscript-4.0.1 → sshscript-5.0.0}/.github/workflows/ci.yml +0 -0
  34. {sshscript-4.0.1 → sshscript-5.0.0}/.github/workflows/codeql.yml +0 -0
  35. {sshscript-4.0.1 → sshscript-5.0.0}/.github/workflows/release.yml +0 -0
  36. {sshscript-4.0.1 → sshscript-5.0.0}/.gitignore +0 -0
  37. {sshscript-4.0.1 → sshscript-5.0.0}/CODE_OF_CONDUCT.md +0 -0
  38. {sshscript-4.0.1 → sshscript-5.0.0}/CONTRIBUTING.md +0 -0
  39. {sshscript-4.0.1 → sshscript-5.0.0}/EXCEPTIONS.md +0 -0
  40. {sshscript-4.0.1 → sshscript-5.0.0}/LICENSE.txt +0 -0
  41. {sshscript-4.0.1 → sshscript-5.0.0}/RELEASING.md +0 -0
  42. {sshscript-4.0.1 → sshscript-5.0.0}/SUPPORT.md +0 -0
  43. {sshscript-4.0.1 → sshscript-5.0.0}/pyproject.toml +0 -0
  44. {sshscript-4.0.1 → sshscript-5.0.0}/setup.cfg +0 -0
  45. {sshscript-4.0.1 → sshscript-5.0.0}/src/_sshscript_cli.py +0 -0
  46. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/__init__.py +0 -0
  47. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/__init__.pyi +0 -0
  48. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/_runtime.py +0 -0
  49. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/channelgeneric.py +0 -0
  50. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/channelssh.py +0 -0
  51. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/channelsubprocess.py +0 -0
  52. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/channelutils.py +0 -0
  53. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/commandjob.py +0 -0
  54. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/commandjob.pyi +0 -0
  55. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/commandresult.py +0 -0
  56. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/commandresult.pyi +0 -0
  57. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/consolejob.py +0 -0
  58. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/dollar.py +0 -0
  59. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/dollarchanger.py +0 -0
  60. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/dollarparser.py +0 -0
  61. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/errorutils.py +0 -0
  62. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/patching.py +0 -0
  63. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/py.typed +0 -0
  64. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/sessionwrapper.py +0 -0
  65. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/sessionwrapper.pyi +0 -0
  66. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/spyimporter.py +0 -0
  67. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/sshconfig.py +0 -0
  68. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/sshscript.py +0 -0
  69. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/stdio.py +0 -0
  70. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/tokenparser.py +0 -0
  71. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/README.console-authentication.md +0 -0
  72. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/README.console-results.md +0 -0
  73. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/README.dollar-syntax-tests.md +0 -0
  74. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/README.module-tests.md +0 -0
  75. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/check_package_asserts.py +0 -0
  76. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/dollar_syntax.spy +0 -0
  77. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/dollar_syntax_fixture.spy +0 -0
  78. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/language_fixture.spy +0 -0
  79. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_channelgeneric_expect.py +0 -0
  80. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_check_file.py +0 -0
  81. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_command_api.py +0 -0
  82. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_console_authentication.py +0 -0
  83. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_console_job.py +0 -0
  84. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_console_results.py +0 -0
  85. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_dollar_command_escapes.py +0 -0
  86. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_file_transfer.py +0 -0
  87. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_logger_api.py +0 -0
  88. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_native_auth_runner.py +0 -0
  89. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_native_console_authentication.py +0 -0
  90. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_production_contract.py +0 -0
  91. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_public_types.py +0 -0
  92. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_python_version.py +0 -0
  93. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_release_inputs.py +0 -0
  94. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_session_close_reporting.py +0 -0
  95. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_session_proxy_cleanup.py +0 -0
  96. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_session_settings.py +0 -0
  97. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_spy_source_mapping.py +0 -0
  98. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_spy_thread_session.py +0 -0
  99. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_sshscript_dollar_syntax.py +0 -0
  100. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_sshscript_module.py +0 -0
  101. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_stdin_contract.py +0 -0
  102. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_stdio_dynamic_string.py +0 -0
  103. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_syntax_error.spy +0 -0
  104. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_update_check.py +0 -0
  105. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript/unittest/test_version_policy.py +0 -0
  106. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript.egg-info/dependency_links.txt +0 -0
  107. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript.egg-info/entry_points.txt +0 -0
  108. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript.egg-info/requires.txt +0 -0
  109. {sshscript-4.0.1 → sshscript-5.0.0}/src/sshscript.egg-info/top_level.txt +0 -0
  110. {sshscript-4.0.1 → sshscript-5.0.0}/tools/check_baseline_examples.py +0 -0
  111. {sshscript-4.0.1 → sshscript-5.0.0}/tools/check_release.py +0 -0
  112. {sshscript-4.0.1 → sshscript-5.0.0}/tools/publish_release.py +0 -0
  113. {sshscript-4.0.1 → sshscript-5.0.0}/tools/run_checks.py +0 -0
  114. {sshscript-4.0.1 → sshscript-5.0.0}/tools/run_native_auth_ci.py +0 -0
  115. {sshscript-4.0.1 → sshscript-5.0.0}/tools/setup_openssh_ci.sh +0 -0
  116. {sshscript-4.0.1 → sshscript-5.0.0}/tools/version_policy.py +0 -0
@@ -0,0 +1,58 @@
1
+ # SSHScript for AI agents
2
+
3
+ **Documentation contract: SSHScript 5.0.0; Python 3.11+.**
4
+
5
+ Start with the [agent guide](skills/sshscript/references/agent-guide.md).
6
+ It explains version checks, API selection, command results, input, deadlines,
7
+ authentication and cleanup. Read only the task-relevant sections of the
8
+ [canonical API guide](API_GUIDE.md) and [version policy](VERSIONING.md).
9
+
10
+ The [SSHScript skill](skills/sshscript/SKILL.md) supplies a compact operating
11
+ workflow. Its folder contains the supporting guide; copy the entire
12
+ `skills/sshscript/` folder when installing it in an agent's skill directory.
13
+ It teaches SSHScript usage, not development of SSHScript itself or a particular
14
+ recipe library. Agents without skill support can read these Markdown files.
15
+
16
+ ## Share one URL
17
+
18
+ The website entry is [SSHScript for AI Agents](https://iapyeh.github.io/sshscript/v3a/ai-agents/).
19
+ After these files are published, users can give that URL and a task to an agent:
20
+
21
+ > Read the SSHScript AI agent entry at
22
+ > https://iapyeh.github.io/sshscript/v3a/ai-agents/ and follow its reading order.
23
+ > Check the SSHScript version in my environment before selecting APIs. Help me
24
+ > complete this task: [describe the task, target hosts and expected output].
25
+ > If you cannot read a required document, report that limitation rather than
26
+ > guessing its contents. Explain the proposed commands before execution and
27
+ > stay within the operations I authorize.
28
+
29
+ A URL supplies reference material for the current task. It does not install
30
+ SSHScript, install a skill, grant execution permission, or guarantee persistent
31
+ memory. An agent needs URL-reading tools, or the user must provide the files.
32
+
33
+ ## Maintain the source and website
34
+
35
+ Maintain this entry, `skills/sshscript/SKILL.md`, and its reference guide in
36
+ the designated `working_branch` checkout on branch `working_branch`.
37
+ The landing page is maintained in `gh-pages/v3a/ai-agents/index.md` on branch
38
+ `gh-pages`. The API guide remains authoritative for public behavior.
39
+
40
+ From `working_branch`, synchronize the generated website guide, plain-text
41
+ files and portable skill ZIP with:
42
+
43
+ ```sh
44
+ python3 tools/sync_ai_docs.py ../gh-pages/v3a
45
+ python3 tools/sync_ai_docs.py ../gh-pages/v3a --check
46
+ ```
47
+
48
+ The ZIP includes the skill and its reference guide, plus snapshots of
49
+ `API_GUIDE.md` and `VERSIONING.md` for offline use. These snapshots are generated
50
+ from the source documents; do not edit the ZIP or generated website copies.
51
+ The sync command checks the destination branch before writing. Website pages
52
+ keep their Last Updated timestamp aligned with their mtime.
53
+
54
+ When the execution contract changes, review the skill and guide, update their
55
+ contract labels and the landing page, then regenerate the website assets.
56
+ Validate links, skill structure, example behavior, source/ZIP equality and
57
+ the rendered website. Source and website publishing are separate operations;
58
+ copying files does not publish either of them.
@@ -1,8 +1,8 @@
1
1
  # Recommended SSHScript API
2
2
 
3
3
  This is the canonical entry point for new code and AI-generated examples.
4
- **Current contract: 4.0.1**, Python 3.11+. Sections marked **4.0** require
5
- SSHScript 4.0.1; installing `sshscript==3.1.5` does not provide them.
4
+ **Current contract: 5.0.0**, Python 3.11+. Sections marked **4.0** require
5
+ SSHScript 4.0.2 or newer; installing `sshscript==3.1.5` does not provide them.
6
6
  Examples labelled 3.1.5 remain verified compatibility examples for that baseline.
7
7
  Pin the exact artifact version and record it with execution diagnostics when
8
8
  handing off automation. See [version policy and migration](VERSIONING.md).
@@ -29,9 +29,11 @@ identifiers and handoff reports are future work, not a current API guarantee.
29
29
  | Long-running program | **4.0:** `start(timeout=None)`, `stop()`, `wait()` |
30
30
  | Session-wide failure/output policy | **4.0:** `set(...)`, `get(...)`, or matching properties |
31
31
 
32
- ## 3.1.5: one command, one retained result
32
+ ## One command, one retained result (4.0; compatible with 3.1.5)
33
33
 
34
- Install the baseline with `python -m pip install "sshscript==3.1.5"`.
34
+ Install the current release with `python -m pip install "sshscript==5.0.0"`.
35
+ For reproducing only the historical compatibility baseline, explicitly install
36
+ `sshscript==3.1.5`; the 4.0 sections require the current release.
35
37
  An argv list is one command, not a batch. Shell-looking argument text stays data.
36
38
 
37
39
  <!-- example: {"id":"argv", "profile":"3.1.5", "stdout":"a; b\n"} -->
@@ -67,7 +69,7 @@ with closing(Session()) as local:
67
69
  print(f"failed: {error.returncode}")
68
70
  ```
69
71
 
70
- ## 3.1.5: SSH and optional dollar syntax
72
+ ## SSH and optional dollar syntax (4.0; compatible with 3.1.5)
71
73
 
72
74
  Replace the example host with your configured host and verified known-host key.
73
75
  CI executes this block with a local SSH protocol fixture in place of the login;
@@ -86,6 +88,139 @@ with closing(Session()) as local:
86
88
  print(result.stdout)
87
89
  ```
88
90
 
91
+ ### Private-key paths and nested connections
92
+
93
+ `parent.connect(..., pkey_path=path)` loads an RSA private key from the
94
+ **calling parent Session's host**. A local Session reads the file on localhost;
95
+ a Session connected to host1 reads it on host1 through SFTP before connecting
96
+ to host2. `connect()` returns a child Session and leaves the parent on its
97
+ original host. Calling the original local Session again therefore still reads
98
+ the key on localhost.
99
+
100
+ For example, `local.connect("host1", pkey_path="/local/key_rsa")` reads
101
+ `/local/key_rsa` locally and returns `host1`. Then
102
+ `host1.connect("host2", pkey_path="/remote/key_rsa")` reads `/remote/key_rsa`
103
+ on host1. In `.spy`, an inner `with $.connect(...)` uses the outer scope's
104
+ current Session as its parent.
105
+
106
+ `key_filename=` is forwarded to Paramiko and reads from the local Python
107
+ process's filesystem even for nested connections; it supports additional key
108
+ formats. For an encrypted RSA key, use
109
+ `pkey=parent.pkey(path, password=key_passphrase)` to retain the same host-based
110
+ reading rule. Supplying both `pkey_path` and `pkey` raises `ValueError`.
111
+ See the [agent guide's complete nested example](https://iapyeh.github.io/sshscript/v3a/ai-agents/guide/#ssh-connections-and-private-key-paths).
112
+
113
+ <a id="host-key-sources-unreleased"></a>
114
+
115
+ ### Host-key sources (5.0)
116
+
117
+ This section requires SSHScript 5.0.0 or newer.
118
+ `known_hosts` selects the host-key trust source independently of `policy`:
119
+
120
+ | Value | Sources, in order |
121
+ | --- | --- |
122
+ | `"parent"` (new default) | Only the calling Session's host |
123
+ | `"local"` | Only the root localhost Session |
124
+ | `"chain"` | Calling Session, then each ancestor through localhost |
125
+
126
+ For localhost → host1 → host2 → host3, `host2.connect("host3",
127
+ known_hosts="chain")` consults host2, host1, then localhost. The nearest source
128
+ with a record for the resolved target hostname and port is authoritative.
129
+ A matching key is accepted; a changed key is rejected even if a more distant
130
+ source matches. Only absence of a target record permits continuing the chain.
131
+ Different key algorithms do not permit falling back to another source.
132
+
133
+ Each source reads its own `known_hosts_path`, or `~/.ssh/known_hosts` if unset.
134
+ Remote files are read over SFTP as that Session's original SSH login account;
135
+ SSH handshakes, user authentication and host-key verification still run in the
136
+ local Python process. Neither sudo/su nor a nested connection changes the SFTP
137
+ account. Remote `~/` paths refer to the login account's SFTP home; `~user` paths
138
+ are unsupported. Relative remote paths use the SFTP working directory.
139
+
140
+ ```py
141
+ local.set(known_hosts="chain")
142
+ # After connecting to host1 and host2:
143
+ host1.set(known_hosts_path="/home/ops/.ssh/known_hosts")
144
+ host2.set(known_hosts_path="/etc/ssh/ssh_known_hosts")
145
+ with host2.connect("deploy@host3", pkey_path="/home/ops/.ssh/host3_rsa") as host3:
146
+ result = host3.exec_command(["hostname"], check=True, command_timeout=10)
147
+ ```
148
+
149
+ The strategy is inherited by future child Sessions; the path is host-specific
150
+ and is **not inherited**. `known_hosts_path=None` restores the standard path.
151
+ A `connect(known_hosts=...)` override affects only that connection; it does not
152
+ change the parent setting or the child's inherited strategy. `set()`, `get()`
153
+ and matching properties expose both settings. `known_hosts_path` accepts text
154
+ paths and `PathLike[str]`; it is a Session setting, not a `connect()` keyword.
155
+
156
+ `pkey_path` always reads only from the calling Session's host, including with
157
+ `known_hosts="chain"`; private keys are never searched through ancestors.
158
+
159
+ An absent trust file, permission failure, read failure, invalid UTF-8 or invalid
160
+ entry raises an error; no distant source or permissive policy bypasses it.
161
+ Empty files, comments, plain host-key entries (including comma-separated names)
162
+ and hashed hostnames are supported. OpenSSH markers such as `@cert-authority`
163
+ and `@revoked`, wildcard/negated host patterns and unsupported key types raise
164
+ an error rather than being silently ignored. A source file is fully parsed
165
+ before its records are used. Nonstandard ports use `[hostname]:port` records.
166
+
167
+ When every selected source lacks the target, `policy` handles the unknown key.
168
+ `AutoAddPolicy` accepts it into the new client's memory only; these trust files
169
+ are read-only and are not automatically updated locally or remotely.
170
+
171
+ **Migration from 4.0.2:** earlier releases always load localhost host keys. The new default
172
+ `"parent"` makes nested host-key sources consistent with `pkey_path`. To retain
173
+ the previous behavior, set `local.set(known_hosts="local")` before connecting.
174
+ All strategies require a readable, valid file, including the default localhost
175
+ path. Unlike 4.0.2, a missing local file is an error; create an empty
176
+ file if the configured missing-key policy should handle unknown hosts.
177
+
178
+ ### Private keys readable only in a privileged console
179
+
180
+ Remote `pkey()` and `pkey_path` use SFTP with the original SSH login account.
181
+ Entering `sudo()` or `su()` does not change that identity, so even
182
+ `$.pkey("/root/.ssh/id_rsa")` inside a root console can fail with a permission
183
+ error. When authorized, read the file with a console command and construct
184
+ the Paramiko key from its captured text:
185
+
186
+ This manual example requires a real privileged console and SSH endpoints;
187
+ it is excluded from credential-free executable examples.
188
+
189
+ ```py
190
+ from contextlib import closing
191
+ from io import StringIO
192
+ import paramiko
193
+ from sshscript import Session
194
+
195
+ with closing(Session()) as local:
196
+ with local.connect("ops@host1.example.net", timeout=10,
197
+ banner_timeout=10, auth_timeout=10) as host1:
198
+ with host1.sudo() as root:
199
+ key_result = root("cat /root/.ssh/id_rsa", check=True, command_timeout=10)
200
+ key = paramiko.RSAKey.from_private_key(StringIO(key_result.stdout))
201
+ with host1.connect("deploy@host2.example.net", pkey=key, timeout=10,
202
+ banner_timeout=10, auth_timeout=10) as host2:
203
+ result = host2.exec_command(["hostname"], check=True, command_timeout=10)
204
+ print(result.stdout)
205
+ ```
206
+
207
+ The example requires authorized sudo/SSH access and verified host keys.
208
+ Configure output logging/display to keep private-key text out of logs before
209
+ running the read; do not print or report `key_result` or its stdout. Key text
210
+ also remains in console buffers. Use the matching Paramiko key class for
211
+ other formats; encrypted RSA keys need `password=` on `from_private_key()`.
212
+ An authorized `su()` console can supply the text in the same way.
213
+
214
+ The console returned by `shell()`/`sudo()`/`su()`/`enter()` has no `connect()`
215
+ method, so `$.connect()` is unavailable while a console is current in `.spy`.
216
+ A retained Python Session can still call `connect()` during an active console;
217
+ the connection uses its original SSH transport and does not inherit console
218
+ privilege. The example leaves the console first and calls the retained
219
+ `host1.connect(pkey=key)`, which needs no SFTP key read. See the
220
+ [agent guide's `.spy` equivalent](https://iapyeh.github.io/sshscript/v3a/ai-agents/guide/#keys-readable-only-after-sudosu).
221
+
222
+ ### Optional dollar syntax
223
+
89
224
  Save dollar syntax as `.spy`, run with `sshscript file.spy`, and syntax-check
90
225
  without execution using `sshscript --check file.spy`.
91
226
 
@@ -99,7 +234,7 @@ print(result.stdout)
99
234
 
100
235
  ## 4.0: settings and managed jobs
101
236
 
102
- Use SSHScript 4.0.1 for the following examples. CLI `-v`, `--stderr`, and
237
+ Use SSHScript 5.0.0 for the following examples. CLI `-v`, `--stderr`, and
103
238
  `--debug` override root defaults only for that execution. Child Sessions copy
104
239
  their parent's settings. `set()` validates before changing anything; `get()`
105
240
  returns a copy. Explicit command options override Session policy.
@@ -118,7 +253,7 @@ the settings dictionary at creation; the policy object itself is shared, so
118
253
  custom policies with mutable state must account for reuse. Changing the setting
119
254
  does not reconfigure existing connections.
120
255
 
121
- <!-- example: {"id":"settings", "profile":"4.0.1", "stdout":"True\n1\n"} -->
256
+ <!-- example: {"id":"settings", "profile":"5.0.0", "stdout":"True\n1\n"} -->
122
257
  ```python
123
258
  from contextlib import closing
124
259
  import paramiko
@@ -140,7 +275,7 @@ semantics; do not combine it with `command_timeout`.
140
275
  For an indefinite command such as tcpdump, use `start(timeout=None)`. This safe
141
276
  local example uses a small Python process instead of capturing network traffic:
142
277
 
143
- <!-- example: {"id":"stop", "profile":"4.0.1", "stdout":"cancelled confirmed\n"} -->
278
+ <!-- example: {"id":"stop", "profile":"5.0.0", "stdout":"cancelled confirmed\n"} -->
144
279
  ```python
145
280
  from contextlib import closing
146
281
  import sys
@@ -179,7 +314,7 @@ legacy SSH behavior which appended a newline to strings: if a consumer
179
314
  needs a line, migrate `input=password` to `input=password + "\n"` explicitly.
180
315
  Published 3.1.5 still has the legacy SSH behavior.
181
316
 
182
- <!-- example: {"id":"exact-stdin", "profile":"4.0.1", "stdout":"'abc'\n'abc\\n'\n"} -->
317
+ <!-- example: {"id":"exact-stdin", "profile":"5.0.0", "stdout":"'abc'\n'abc\\n'\n"} -->
183
318
  ```python
184
319
  from contextlib import closing
185
320
  import sys
@@ -248,7 +383,7 @@ or unpack exactly `stdout, stderr, exitcode`. This changes the released 3.1.5
248
383
  console contract: old `stdout, stderr = shell(command)` must be migrated.
249
384
  Shell calls accept command strings; argv execution belongs to Session.
250
385
 
251
- <!-- example: {"id":"shell-result", "profile":"4.0.1", "stdout":"first 0\n"} -->
386
+ <!-- example: {"id":"shell-result", "profile":"5.0.0", "stdout":"first 0\n"} -->
252
387
  ```python
253
388
  from contextlib import closing
254
389
  from sshscript import Session
@@ -304,7 +439,7 @@ matching: `iter_stdout()` yields chunks, not packets or lines. Put `stop()` in
304
439
  `finally`, so condition matches, exceptions, and KeyboardInterrupt all trigger
305
440
  cleanup. This credential-free example uses a continuous Python process:
306
441
 
307
- <!-- example: {"id":"stream-condition", "profile":"4.0.1", "stdout":"cancelled confirmed\n"} -->
442
+ <!-- example: {"id":"stream-condition", "profile":"5.0.0", "stdout":"cancelled confirmed\n"} -->
308
443
  ```python
309
444
  from contextlib import closing
310
445
  import sys
@@ -369,7 +504,7 @@ the current Bash console's channel, cwd, environment and identity. It does not
369
504
  launch through the underlying Session. Ordinary `console(command)` / `$command`
370
505
  still waits and returns `CommandResult`; no migration is needed for finite commands.
371
506
 
372
- <!-- example: {"id":"console-stream", "profile":"4.0.1", "stdout":"cancelled confirmed\nconsole-alive\n"} -->
507
+ <!-- example: {"id":"console-stream", "profile":"5.0.0", "stdout":"cancelled confirmed\nconsole-alive\n"} -->
373
508
  ```python
374
509
  from contextlib import closing
375
510
  import shlex
@@ -509,7 +644,7 @@ remains available for other authentication prompt formats.
509
644
  These transcripts require a real local account and its authentication policy;
510
645
  they are manual examples, separate from the credential-free CI examples above.
511
646
  Read passwords with `getpass` rather than storing them in the script. The new
512
- behavior requires 4.0.1 and does not apply to the installed 3.1.5 release.
647
+ behavior requires 4.0.2 or newer and does not apply to the installed 3.1.5 release.
513
648
 
514
649
  Open a root console through sudo and allow up to 15 seconds for entry:
515
650
 
@@ -3,6 +3,28 @@
3
3
  This project records user-visible changes here. Release artifacts and their
4
4
  provenance are available from the linked GitHub Release and PyPI pages.
5
5
 
6
+ ## 5.0.0 — 2026-10-10
7
+
8
+ - Add `known_hosts="parent"` (new default), `"local"` and `"chain"` strategies.
9
+ Nested connections now use the calling host's trust file by default, matching
10
+ `pkey_path` ownership. Set `known_hosts="local"` for previous behavior.
11
+ - Add per-Session `known_hosts_path` without path inheritance. Chain lookup uses
12
+ the nearest target record; changed keys and read/parse errors never fall back.
13
+ Authentication and verification still run locally; trust files are read-only.
14
+ - Isolate connection-timeout tests from runner HOME and verify client cleanup.
15
+ - Document host-key source selection, strict file errors and migration in the
16
+ API, website and portable AI agent skill.
17
+
18
+ ## 4.0.2 — 2026-10-09
19
+
20
+ - Align the README, current installation commands, website title, navigation,
21
+ footer, and version metadata with the current 4.0 release.
22
+ - Point general PyPI and GitHub Release links to current entry points; retain
23
+ explicitly labelled historical releases and existing documentation URLs.
24
+ - Check current website version labels and release links during documentation
25
+ validation, including rendered header/footer links and anchors.
26
+ - Preserve the 4.0.1 public execution contract; no runtime behavior changes.
27
+
6
28
  ## 4.0.1 — 2026-10-08
7
29
 
8
30
  - Settle the legacy command worker before propagating timeout/transport errors;
@@ -1,4 +1,7 @@
1
1
  include README.md LICENSE.txt RELEASING.md
2
+ include AI_GUIDE.md
3
+ include skills/sshscript/SKILL.md
4
+ include skills/sshscript/references/agent-guide.md
2
5
  include CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md EXCEPTIONS.md SECURITY.md SUPPORT.md
3
6
  recursive-include tools *.py
4
7
  include tools/setup_openssh_ci.sh
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sshscript
3
- Version: 4.0.1
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
  [![CodeQL](https://github.com/iapyeh/sshscript/actions/workflows/codeql.yml/badge.svg?branch=release)](https://github.com/iapyeh/sshscript/actions/workflows/codeql.yml)
38
38
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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 also provides optional dollar syntax for
42
- compact `.spy` automation files.
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:** [4.0.1](https://github.com/iapyeh/sshscript/releases/tag/v4.0.1)
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 4.0.1** introduces the incompatible 4.0 execution contract.
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==3.1.5"
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 uses the published 3.1.5 command API.
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 v3 documentation](https://iapyeh.github.io/sshscript/v3a/) for
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
@@ -6,9 +6,12 @@
6
6
  [![CodeQL](https://github.com/iapyeh/sshscript/actions/workflows/codeql.yml/badge.svg?branch=release)](https://github.com/iapyeh/sshscript/actions/workflows/codeql.yml)
7
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/iapyeh/sshscript/blob/release/LICENSE.txt)
8
8
 
9
+ **AI agent ready automation for local and remote commands.**
10
+
9
11
  SSHScript is a Python automation library for running commands locally and over
10
- SSH through one `Session` API. It also provides optional dollar syntax for
11
- compact `.spy` automation files.
12
+ SSH through one `Session` API. It provides an agent guide, a portable skill
13
+ bundle, and copyable prompts to help your AI agent use its execution contract.
14
+ Optional dollar syntax supports compact `.spy` automation files.
12
15
 
13
16
  SSHScript aims to help engineers and AI agents execute, understand, and hand
14
17
  off automation reliably: identify the execution host, retain each command's
@@ -16,7 +19,7 @@ output and status, distinguish failure from timeout, and make cleanup and
16
19
  unknown termination explicit. A successful command exit is evidence about
17
20
  that command; verify the deployment or other application outcome separately.
18
21
 
19
- **Current release:** [4.0.1](https://github.com/iapyeh/sshscript/releases/tag/v4.0.1)
22
+ **Current release:** [5.0.0](https://github.com/iapyeh/sshscript/releases/tag/v5.0.0)
20
23
  (Production/Stable) · **Python:** 3.11 or newer · **Tested:** Python
21
24
  3.11–3.14 on Linux and macOS
22
25
 
@@ -26,10 +29,45 @@ that command; verify the deployment or other application outcome separately.
26
29
  [Security](https://github.com/iapyeh/sshscript/security/policy) ·
27
30
  [Support](https://github.com/iapyeh/sshscript/blob/release/SUPPORT.md)
28
31
 
29
- **Version 4.0.1** introduces the incompatible 4.0 execution contract.
32
+ **Version 5.0.0** preserves the 4.0 command execution contract and changes the
33
+ default host-key source to the calling host. Provision its known_hosts file or
34
+ select `known_hosts="local"` when migrating existing nested connections.
30
35
  Upgrade 3.1.5 automation using the migration guide and application regressions.
31
36
  See [version policy and migration](VERSIONING.md) before upgrading automation.
32
37
 
38
+ ## AI agent ready
39
+
40
+ **Give your agent one URL:**
41
+ [SSHScript for AI Agents](https://iapyeh.github.io/sshscript/v3a/ai-agents/).
42
+ Prepare it for future automation or give it a task to complete now.
43
+
44
+ - **A guide for agents:** version-aware API selection, exact input handling,
45
+ retained command results, timeouts, authentication and cleanup.
46
+ - **A portable skill bundle:** `SKILL.md` with the agent guide and API and
47
+ version-policy references, ready to install through your agent's skill
48
+ mechanism.
49
+ - **Prompts for humans:** copy a setup prompt to request skill installation and
50
+ a saved tool preference, or a task prompt to describe hosts, inputs and
51
+ expected results.
52
+
53
+ Start with this prompt, even before you have a task:
54
+
55
+ ```text
56
+ Read https://iapyeh.github.io/sshscript/v3a/ai-agents/ and the Agent Guide.
57
+ If you support skills, download and install the complete skill bundle. If you
58
+ support persistent preferences, save this preference: for future local or
59
+ remote command execution and automation, first assess whether SSHScript is
60
+ suitable and use it when appropriate. Report whether the skill can be loaded
61
+ and the preference was saved, and explain any limits.
62
+ For now, do not connect to remote hosts or run demonstration commands.
63
+ ```
64
+
65
+ The guides are ordinary Markdown and web pages for agents that can read files
66
+ or URLs. Skill installation and persistent preferences depend on the agent's
67
+ capabilities. See the [repository AI entry](AI_GUIDE.md),
68
+ [agent guide](skills/sshscript/references/agent-guide.md) and
69
+ [skill instructions](skills/sshscript/SKILL.md) for the source documents.
70
+
33
71
  ## Recommended API
34
72
 
35
73
  Start with [the canonical API guide](API_GUIDE.md): version-labelled examples,
@@ -51,6 +89,7 @@ and [the cross-system test guide](src/sshscript/unittest/README.console-authenti
51
89
 
52
90
  ## Why SSHScript?
53
91
 
92
+ - Prepare your AI agent with a dedicated guide, portable skill and setup prompts.
54
93
  - Use the same interface for local subprocesses and remote SSH commands.
55
94
  - Traverse nested SSH connections without rebuilding connection logic.
56
95
  - Keep ordinary Python functions, packages, exceptions, data processing, and
@@ -83,7 +122,7 @@ pin SSHScript and all transitive dependencies in your application's lock file.
83
122
  To install this release explicitly:
84
123
 
85
124
  ```sh
86
- python3 -m pip install "sshscript==3.1.5"
125
+ python3 -m pip install "sshscript==5.0.0"
87
126
  ```
88
127
 
89
128
  Use `python3 -m pip install --upgrade sshscript` to upgrade. The optional
@@ -92,7 +131,8 @@ it never installs an update by itself.
92
131
 
93
132
  ## 60-second local quickstart
94
133
 
95
- This example uses the published 3.1.5 command API.
134
+ This example runs on 5.0.0 and retains compatibility with the 3.1.5 command API.
135
+ The metadata below records that older compatibility baseline.
96
136
 
97
137
  <!-- example: {"id":"readme-quickstart", "profile":"3.1.5", "stdout":"sshscript is ready\nexit code: 0\n"} -->
98
138
  ```python
@@ -343,10 +383,28 @@ Unknown and changed host keys are rejected unless the caller explicitly
343
383
  supplies a different Paramiko policy. Do not use automatic key acceptance in a
344
384
  production workflow unless a separate trusted bootstrap process has already
345
385
  verified the key. See the
346
- [SSHScript v3 documentation](https://iapyeh.github.io/sshscript/v3a/) for
386
+ [SSHScript documentation](https://iapyeh.github.io/sshscript/v3a/) for
347
387
  nested connections, timeouts, file transfer, `sudo`, `su`, and interactive
348
388
  programs.
349
389
 
390
+ ## Host-key sources in 5.0
391
+
392
+ SSHScript 5.0.0 uses `known_hosts="parent"` as the default: a local
393
+ Session reads localhost's trust file; `host1.connect("host2")` reads host1's
394
+ trust file over SFTP. This matches the host ownership of `pkey_path`.
395
+ SSHScript 4.0.2 and earlier use localhost for all host-key checks.
396
+
397
+ Set `session.set(known_hosts="chain")` to search from the calling Session
398
+ through its ancestors to localhost. The nearest layer with a target record
399
+ wins; a mismatch or read/parse error stops the connection. Set
400
+ `session.set(known_hosts="local")` to preserve previous behavior.
401
+ Each Session can set its own `known_hosts_path`; this path is not inherited.
402
+ `connect(known_hosts=...)` overrides only one connection. `pkey_path` remains
403
+ on the calling host for every strategy. `policy` handles only unknown keys;
404
+ `AutoAddPolicy` does not write these trust files. See the
405
+ [host-key source contract](API_GUIDE.md#host-key-sources-unreleased) for supported
406
+ file formats, strict error handling and migration.
407
+
350
408
  ## Reusing SSH configuration
351
409
 
352
410
  Connections from a local session read `~/.ssh/config` if it exists. Supported
@@ -65,11 +65,20 @@ termination. Verify application outcomes and reconcile unknown termination.
65
65
 
66
66
  ### SSH identity and host keys
67
67
 
68
- SSH connections load system host keys and reject unknown or changed keys by
69
- default. The application is responsible for provisioning `known_hosts` and
68
+ SSH connections reject unknown or changed keys by default. SSHScript 4.0.2 and earlier
69
+ load localhost system host keys. SSHScript 5.0.0 defaults to
70
+ `known_hosts="parent"`, reading the calling Session host's trust file. Remote
71
+ trust files are read over SFTP using the original SSH login account; verification
72
+ still runs locally. `"chain"` trusts the nearest source with a target record;
73
+ it never falls back after a mismatch or file error. Trusting a parent's records
74
+ also trusts that host/account to maintain them. Use `"local"` to retain the
75
+ previous source selection. See the [host-key source contract](API_GUIDE.md#host-key-sources-unreleased).
76
+
77
+ The application is responsible for provisioning `known_hosts` and
70
78
  verifying a new fingerprint through an independent channel. Supplying an
71
79
  automatic-acceptance Paramiko policy is an explicit opt-out from this default
72
80
  and is not recommended for production enrollment.
81
+ AutoAddPolicy does not persist keys into these local or remote trust files.
73
82
 
74
83
  SSH authentication ultimately follows Paramiko and server policy. Prefer an
75
84
  SSH agent, managed key, or secret manager. Never store passwords or private