sshscript 2.0.2__tar.gz → 3.1.4__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 (81) hide show
  1. sshscript-3.1.4/.github/dependabot.yml +27 -0
  2. sshscript-3.1.4/.github/workflows/ci.yml +85 -0
  3. sshscript-3.1.4/.github/workflows/codeql.yml +30 -0
  4. sshscript-3.1.4/.github/workflows/release.yml +214 -0
  5. sshscript-3.1.4/CHANGELOG.md +55 -0
  6. sshscript-3.1.4/CONTRIBUTING.md +56 -0
  7. sshscript-3.1.4/EXCEPTIONS.md +45 -0
  8. {sshscript-2.0.2 → sshscript-3.1.4}/LICENSE.txt +1 -1
  9. sshscript-3.1.4/MANIFEST.in +7 -0
  10. sshscript-3.1.4/PKG-INFO +178 -0
  11. sshscript-3.1.4/README.md +153 -0
  12. sshscript-3.1.4/RELEASING.md +29 -0
  13. sshscript-3.1.4/SECURITY.md +37 -0
  14. sshscript-3.1.4/SUPPORT.md +23 -0
  15. sshscript-3.1.4/pyproject.toml +46 -0
  16. sshscript-3.1.4/setup.cfg +4 -0
  17. sshscript-3.1.4/src/sshscript/__init__.py +82 -0
  18. sshscript-3.1.4/src/sshscript/_version.py +3 -0
  19. sshscript-3.1.4/src/sshscript/channelgeneric.py +2028 -0
  20. sshscript-3.1.4/src/sshscript/channelssh.py +289 -0
  21. sshscript-3.1.4/src/sshscript/channelsubprocess.py +294 -0
  22. sshscript-3.1.4/src/sshscript/channelutils.py +444 -0
  23. sshscript-3.1.4/src/sshscript/dollar.py +589 -0
  24. sshscript-3.1.4/src/sshscript/dollarchanger.py +579 -0
  25. sshscript-3.1.4/src/sshscript/dollarparser.py +263 -0
  26. sshscript-3.1.4/src/sshscript/errorutils.py +486 -0
  27. sshscript-3.1.4/src/sshscript/patching.py +190 -0
  28. sshscript-3.1.4/src/sshscript/session.py +1537 -0
  29. sshscript-3.1.4/src/sshscript/sessionwrapper.py +300 -0
  30. sshscript-3.1.4/src/sshscript/spyimporter.py +157 -0
  31. sshscript-3.1.4/src/sshscript/sshscript.py +412 -0
  32. sshscript-3.1.4/src/sshscript/stdio.py +424 -0
  33. sshscript-3.1.4/src/sshscript/tokenparser.py +480 -0
  34. sshscript-3.1.4/src/sshscript/unittest/check_package_asserts.py +18 -0
  35. sshscript-3.1.4/src/sshscript/unittest/dollar_syntax.spy +253 -0
  36. sshscript-3.1.4/src/sshscript/unittest/dollar_syntax_fixture.spy +13 -0
  37. sshscript-3.1.4/src/sshscript/unittest/language.spy +533 -0
  38. sshscript-3.1.4/src/sshscript/unittest/language_fixture.spy +10 -0
  39. sshscript-3.1.4/src/sshscript/unittest/test_channelgeneric_expect.py +89 -0
  40. sshscript-3.1.4/src/sshscript/unittest/test_file_transfer.py +196 -0
  41. sshscript-3.1.4/src/sshscript/unittest/test_logger_api.py +232 -0
  42. sshscript-3.1.4/src/sshscript/unittest/test_logger_integration.py +229 -0
  43. sshscript-3.1.4/src/sshscript/unittest/test_openssh_integration.py +308 -0
  44. sshscript-3.1.4/src/sshscript/unittest/test_production_contract.py +371 -0
  45. sshscript-3.1.4/src/sshscript/unittest/test_session_close_reporting.py +183 -0
  46. sshscript-3.1.4/src/sshscript/unittest/test_session_proxy_cleanup.py +179 -0
  47. sshscript-3.1.4/src/sshscript/unittest/test_spy_source_mapping.py +297 -0
  48. sshscript-3.1.4/src/sshscript/unittest/test_spy_thread_session.py +230 -0
  49. sshscript-3.1.4/src/sshscript/unittest/test_ssh_security.py +281 -0
  50. sshscript-3.1.4/src/sshscript/unittest/test_sshscript_dollar_syntax.py +54 -0
  51. sshscript-3.1.4/src/sshscript/unittest/test_sshscript_module.py +479 -0
  52. sshscript-3.1.4/src/sshscript/unittest/test_stdio_dynamic_string.py +71 -0
  53. sshscript-3.1.4/src/sshscript/unittest/test_syntax_error.spy +21 -0
  54. sshscript-3.1.4/src/sshscript/unittest/test_update_check.py +76 -0
  55. sshscript-3.1.4/src/sshscript.egg-info/PKG-INFO +178 -0
  56. sshscript-3.1.4/src/sshscript.egg-info/SOURCES.txt +63 -0
  57. {sshscript-2.0.2 → sshscript-3.1.4}/src/sshscript.egg-info/dependency_links.txt +0 -0
  58. {sshscript-2.0.2 → sshscript-3.1.4}/src/sshscript.egg-info/entry_points.txt +0 -0
  59. sshscript-3.1.4/src/sshscript.egg-info/requires.txt +2 -0
  60. {sshscript-2.0.2 → sshscript-3.1.4}/src/sshscript.egg-info/top_level.txt +0 -0
  61. sshscript-3.1.4/tools/check_release.py +50 -0
  62. sshscript-3.1.4/tools/prepare_release.py +54 -0
  63. sshscript-3.1.4/tools/publish_release.py +26 -0
  64. sshscript-3.1.4/tools/run_checks.py +19 -0
  65. sshscript-3.1.4/tools/setup_openssh_ci.sh +192 -0
  66. sshscript-2.0.2/PKG-INFO +0 -134
  67. sshscript-2.0.2/README.rst +0 -106
  68. sshscript-2.0.2/setup.cfg +0 -13
  69. sshscript-2.0.2/setup.py +0 -44
  70. sshscript-2.0.2/src/sshscript/__init__.py +0 -33
  71. sshscript-2.0.2/src/sshscript/sshscript.py +0 -293
  72. sshscript-2.0.2/src/sshscript/sshscriptchannel.py +0 -1152
  73. sshscript-2.0.2/src/sshscript/sshscriptchannelutils.py +0 -506
  74. sshscript-2.0.2/src/sshscript/sshscriptdollar.py +0 -421
  75. sshscript-2.0.2/src/sshscript/sshscripterror.py +0 -70
  76. sshscript-2.0.2/src/sshscript/sshscriptparser.py +0 -1049
  77. sshscript-2.0.2/src/sshscript/sshscriptpatching.py +0 -110
  78. sshscript-2.0.2/src/sshscript/sshscriptsession.py +0 -988
  79. sshscript-2.0.2/src/sshscript.egg-info/PKG-INFO +0 -134
  80. sshscript-2.0.2/src/sshscript.egg-info/SOURCES.txt +0 -19
  81. sshscript-2.0.2/src/sshscript.egg-info/requires.txt +0 -1
@@ -0,0 +1,27 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: pip
4
+ directory: /
5
+ target-branch: release
6
+ schedule:
7
+ interval: weekly
8
+ groups:
9
+ python-patch-and-minor:
10
+ patterns:
11
+ - "*"
12
+ update-types:
13
+ - patch
14
+ - minor
15
+
16
+ - package-ecosystem: github-actions
17
+ directory: /
18
+ target-branch: release
19
+ schedule:
20
+ interval: weekly
21
+ groups:
22
+ actions-patch-and-minor:
23
+ patterns:
24
+ - "*"
25
+ update-types:
26
+ - patch
27
+ - minor
@@ -0,0 +1,85 @@
1
+ name: CI
2
+ on: [push, pull_request]
3
+ permissions:
4
+ contents: read
5
+ jobs:
6
+ verify:
7
+ timeout-minutes: 20
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ os: [ubuntu-latest, macos-latest]
12
+ python-version: ['3.11', '3.12', '3.13', '3.14']
13
+ runs-on: ${{ matrix.os }}
14
+ steps:
15
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
16
+ with:
17
+ persist-credentials: false
18
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ - name: Install test and build dependencies
22
+ run: python -m pip install 'paramiko>=2.11,<5' 'packaging>=21' build twine
23
+ - name: Test source
24
+ run: python tools/run_checks.py
25
+ - name: Build and verify installed release
26
+ run: python tools/check_release.py --output '${{ runner.temp }}/verified-artifacts'
27
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
28
+ with:
29
+ name: sshscript-${{ matrix.os }}-${{ matrix.python-version }}
30
+ path: ${{ runner.temp }}/verified-artifacts/
31
+
32
+ openssh-integration:
33
+ name: OpenSSH integration (Python ${{ matrix.python-version }})
34
+ runs-on: ubuntu-latest
35
+ timeout-minutes: 25
36
+ strategy:
37
+ fail-fast: false
38
+ matrix:
39
+ python-version: ['3.11', '3.14']
40
+ steps:
41
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
42
+ with:
43
+ persist-credentials: false
44
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
45
+ with:
46
+ python-version: ${{ matrix.python-version }}
47
+ - name: Install test and build dependencies
48
+ run: python -m pip install 'paramiko>=2.11,<5' 'packaging>=21' build twine
49
+ - name: Build and verify wheel
50
+ run: python tools/check_release.py --output "$RUNNER_TEMP/verified-artifacts"
51
+ - name: Install verified wheel
52
+ run: python -m pip install --force-reinstall "$RUNNER_TEMP"/verified-artifacts/*.whl
53
+ - name: Provision loopback OpenSSH server
54
+ run: bash tools/setup_openssh_ci.sh "$GITHUB_ENV"
55
+ - name: Test installed wheel against OpenSSH
56
+ run: |
57
+ mkdir -p "$RUNNER_TEMP/openssh-tests"
58
+ cp src/sshscript/unittest/test_openssh_integration.py "$RUNNER_TEMP/openssh-tests/"
59
+ cd "$RUNNER_TEMP/openssh-tests"
60
+ python -I test_openssh_integration.py -v
61
+ - name: Upload OpenSSH server log
62
+ if: ${{ failure() && env.SSHSCRIPT_OPENSSH_LOG != '' }}
63
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
64
+ with:
65
+ name: openssh-log-python-${{ matrix.python-version }}
66
+ if-no-files-found: warn
67
+ path: ${{ env.SSHSCRIPT_OPENSSH_LOG }}
68
+ - name: Remove disposable OpenSSH state
69
+ if: always()
70
+ run: |
71
+ if [[ -n "${SSHSCRIPT_OPENSSH_PID_FILE:-}" && -s "$SSHSCRIPT_OPENSSH_PID_FILE" ]]; then
72
+ sudo kill "$(cat "$SSHSCRIPT_OPENSSH_PID_FILE")" || true
73
+ fi
74
+ if [[ -n "${SSHSCRIPT_OPENSSH_SUDOERS:-}" ]]; then
75
+ sudo rm -f "$SSHSCRIPT_OPENSSH_SUDOERS"
76
+ fi
77
+ if [[ -n "${SSHSCRIPT_OPENSSH_USER:-}" ]]; then
78
+ sudo userdel -r "$SSHSCRIPT_OPENSSH_USER" 2>/dev/null || true
79
+ fi
80
+ if [[ -n "${SSHSCRIPT_OPENSSH_TARGET_USER:-}" ]]; then
81
+ sudo userdel -r "$SSHSCRIPT_OPENSSH_TARGET_USER" 2>/dev/null || true
82
+ fi
83
+ if [[ -n "${SSHSCRIPT_OPENSSH_ROOT:-}" && "$SSHSCRIPT_OPENSSH_ROOT" == "$RUNNER_TEMP"/sshscript-openssh.* ]]; then
84
+ sudo rm -rf -- "$SSHSCRIPT_OPENSSH_ROOT"
85
+ fi
@@ -0,0 +1,30 @@
1
+ name: CodeQL
2
+
3
+ on:
4
+ push:
5
+ branches: [release]
6
+ pull_request:
7
+ branches: [release]
8
+ schedule:
9
+ - cron: "17 3 * * 1"
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ analyze:
17
+ name: CodeQL (Python and Actions)
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 20
20
+ permissions:
21
+ contents: read
22
+ security-events: write
23
+ steps:
24
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
25
+ with:
26
+ persist-credentials: false
27
+ - uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4
28
+ with:
29
+ languages: python, actions
30
+ - uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4
@@ -0,0 +1,214 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ build:
13
+ name: Verify and attest release artifacts
14
+ runs-on: ubuntu-latest
15
+ timeout-minutes: 20
16
+ permissions:
17
+ contents: read
18
+ id-token: write
19
+ attestations: write
20
+ steps:
21
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
22
+ with:
23
+ persist-credentials: false
24
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
25
+ with:
26
+ python-version: "3.11"
27
+ - name: Install pinned build dependencies
28
+ run: >-
29
+ python -m pip install
30
+ 'paramiko>=2.11,<5' 'packaging>=21'
31
+ 'build==1.6.1' 'twine==7.0.0'
32
+ - name: Verify tag targets the current release branch
33
+ run: |
34
+ git fetch --no-tags --depth=1 origin release
35
+ test "$(git rev-parse HEAD)" = "$(git rev-parse FETCH_HEAD)"
36
+ - name: Verify tag and package version
37
+ env:
38
+ RELEASE_TAG: ${{ github.ref_name }}
39
+ run: |
40
+ PACKAGE_VERSION=$(python -c "import runpy; print(runpy.run_path('src/sshscript/_version.py')['__version__'])")
41
+ test "$RELEASE_TAG" = "v$PACKAGE_VERSION"
42
+ - name: Stabilize build timestamps
43
+ run: echo "SOURCE_DATE_EPOCH=$(git show -s --format=%ct HEAD)" >> "$GITHUB_ENV"
44
+ - name: Run release gates and build verified artifacts
45
+ run: |
46
+ python tools/run_checks.py
47
+ python tools/check_release.py --output "$RUNNER_TEMP/sshscript-release"
48
+ - name: Attest wheel and source distribution
49
+ uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4
50
+ with:
51
+ subject-path: |
52
+ ${{ runner.temp }}/sshscript-release/*.whl
53
+ ${{ runner.temp }}/sshscript-release/*.tar.gz
54
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
55
+ with:
56
+ name: python-package-distributions
57
+ if-no-files-found: error
58
+ path: ${{ runner.temp }}/sshscript-release/
59
+
60
+ openssh-integration:
61
+ name: Release OpenSSH integration (Python ${{ matrix.python-version }})
62
+ needs: build
63
+ runs-on: ubuntu-latest
64
+ timeout-minutes: 15
65
+ strategy:
66
+ fail-fast: false
67
+ matrix:
68
+ python-version: ['3.11', '3.14']
69
+ steps:
70
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
71
+ with:
72
+ persist-credentials: false
73
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
74
+ with:
75
+ python-version: ${{ matrix.python-version }}
76
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
77
+ with:
78
+ name: python-package-distributions
79
+ path: dist
80
+ - name: Install verified wheel
81
+ run: python -m pip install dist/*.whl
82
+ - name: Provision loopback OpenSSH server
83
+ run: bash tools/setup_openssh_ci.sh "$GITHUB_ENV"
84
+ - name: Test installed wheel against OpenSSH
85
+ run: |
86
+ mkdir -p "$RUNNER_TEMP/openssh-tests"
87
+ cp src/sshscript/unittest/test_openssh_integration.py "$RUNNER_TEMP/openssh-tests/"
88
+ cd "$RUNNER_TEMP/openssh-tests"
89
+ python -I test_openssh_integration.py -v
90
+ - name: Upload OpenSSH server log
91
+ if: ${{ failure() && env.SSHSCRIPT_OPENSSH_LOG != '' }}
92
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
93
+ with:
94
+ name: release-openssh-log-python-${{ matrix.python-version }}
95
+ if-no-files-found: warn
96
+ path: ${{ env.SSHSCRIPT_OPENSSH_LOG }}
97
+ - name: Remove disposable OpenSSH state
98
+ if: always()
99
+ run: |
100
+ if [[ -n "${SSHSCRIPT_OPENSSH_PID_FILE:-}" && -s "$SSHSCRIPT_OPENSSH_PID_FILE" ]]; then
101
+ sudo kill "$(cat "$SSHSCRIPT_OPENSSH_PID_FILE")" || true
102
+ fi
103
+ if [[ -n "${SSHSCRIPT_OPENSSH_SUDOERS:-}" ]]; then
104
+ sudo rm -f "$SSHSCRIPT_OPENSSH_SUDOERS"
105
+ fi
106
+ if [[ -n "${SSHSCRIPT_OPENSSH_USER:-}" ]]; then
107
+ sudo userdel -r "$SSHSCRIPT_OPENSSH_USER" 2>/dev/null || true
108
+ fi
109
+ if [[ -n "${SSHSCRIPT_OPENSSH_TARGET_USER:-}" ]]; then
110
+ sudo userdel -r "$SSHSCRIPT_OPENSSH_TARGET_USER" 2>/dev/null || true
111
+ fi
112
+ if [[ -n "${SSHSCRIPT_OPENSSH_ROOT:-}" && "$SSHSCRIPT_OPENSSH_ROOT" == "$RUNNER_TEMP"/sshscript-openssh.* ]]; then
113
+ sudo rm -rf -- "$SSHSCRIPT_OPENSSH_ROOT"
114
+ fi
115
+
116
+ draft-github-release:
117
+ name: Create draft GitHub Release
118
+ needs: [build, openssh-integration]
119
+ runs-on: ubuntu-latest
120
+ permissions:
121
+ contents: write
122
+ steps:
123
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
124
+ with:
125
+ python-version: "3.11"
126
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
127
+ with:
128
+ name: python-package-distributions
129
+ path: dist
130
+ - name: Verify artifact hashes
131
+ run: |
132
+ python - <<'PY'
133
+ import hashlib
134
+ import json
135
+ from pathlib import Path
136
+
137
+ root = Path("dist")
138
+ manifest = json.loads((root / "verified.json").read_text())
139
+ if len(manifest) != 2:
140
+ raise SystemExit("expected exactly two verified distributions")
141
+ for name, expected in manifest.items():
142
+ if Path(name).name != name:
143
+ raise SystemExit(f"invalid distribution name: {name}")
144
+ actual = hashlib.sha256((root / name).read_bytes()).hexdigest()
145
+ if actual != expected:
146
+ raise SystemExit(f"artifact hash mismatch: {name}")
147
+ PY
148
+ - name: Create immutable draft with verified assets
149
+ env:
150
+ GH_TOKEN: ${{ github.token }}
151
+ RELEASE_TAG: ${{ github.ref_name }}
152
+ run: >-
153
+ gh release create "$RELEASE_TAG"
154
+ dist/*.whl dist/*.tar.gz dist/verified.json
155
+ --repo "$GITHUB_REPOSITORY"
156
+ --verify-tag --draft --generate-notes
157
+ --title "SSHScript ${RELEASE_TAG#v}"
158
+
159
+ publish-to-pypi:
160
+ name: Publish distributions to PyPI
161
+ needs: draft-github-release
162
+ runs-on: ubuntu-latest
163
+ environment:
164
+ name: pypi
165
+ url: https://pypi.org/p/sshscript
166
+ permissions:
167
+ contents: read
168
+ id-token: write
169
+ steps:
170
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
171
+ with:
172
+ python-version: "3.11"
173
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
174
+ with:
175
+ name: python-package-distributions
176
+ path: dist
177
+ - name: Recheck hashes and isolate distributions
178
+ run: |
179
+ python - <<'PY'
180
+ import hashlib
181
+ import json
182
+ from pathlib import Path
183
+
184
+ root = Path("dist")
185
+ manifest = json.loads((root / "verified.json").read_text())
186
+ if len(manifest) != 2:
187
+ raise SystemExit("expected exactly two verified distributions")
188
+ publish = Path("publish")
189
+ publish.mkdir()
190
+ for name, expected in manifest.items():
191
+ if Path(name).name != name:
192
+ raise SystemExit(f"invalid distribution name: {name}")
193
+ source = root / name
194
+ actual = hashlib.sha256(source.read_bytes()).hexdigest()
195
+ if actual != expected:
196
+ raise SystemExit(f"artifact hash mismatch: {name}")
197
+ (publish / name).write_bytes(source.read_bytes())
198
+ PY
199
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
200
+ with:
201
+ packages-dir: publish/
202
+
203
+ publish-github-release:
204
+ name: Publish GitHub Release
205
+ needs: publish-to-pypi
206
+ runs-on: ubuntu-latest
207
+ permissions:
208
+ contents: write
209
+ steps:
210
+ - name: Publish the verified draft
211
+ env:
212
+ GH_TOKEN: ${{ github.token }}
213
+ RELEASE_TAG: ${{ github.ref_name }}
214
+ run: gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false
@@ -0,0 +1,55 @@
1
+ # Changelog
2
+
3
+ ## 3.1.4 - 2026-09-23
4
+
5
+ ### Production hardening
6
+
7
+ - Require Python 3.11 or newer; CI targets 3.11, 3.12, 3.13, and 3.14.
8
+ - Replace package runtime assertions with the stable exception contract in
9
+ EXCEPTIONS.md, including validation under `python -O`.
10
+ - Make listener removal, hijack/release, and last-layer checks atomic and
11
+ non-mutating on failure; fix positive session-stack indexing.
12
+ - Reject invalid command, PTY, pattern, and output arguments before execution
13
+ or buffer mutation. Disconnected SFTP access raises SSHScriptException.
14
+ - Move embedded stdio tests into the credential-free suite and add normal,
15
+ optimized, and package AST assertion release gates.
16
+ - AssertionError was never a supported API contract. User-written `.spy`
17
+ assertions are preserved, but production command-success handling must use
18
+ explicit checks or `check=True`, because optimized Python removes assertions.
19
+ - Validate closed and hijacked channel operations, AST invariants, persistent
20
+ commands, PTY modes, and output buffers with stable exception types.
21
+
22
+
23
+ ### Security
24
+
25
+ - Verify SSH host keys by default; insecure automatic key acceptance now
26
+ requires an explicit Paramiko policy.
27
+ - Stop forwarding the complete local environment to interactive SSH sessions.
28
+ - Read remote private keys through SFTP instead of interpolated shell commands.
29
+ - Remove SSH hostnames and complete local/remote paths from upload/download
30
+ INFO logs.
31
+ - Add CodeQL, Dependabot, private vulnerability reporting guidance, and an
32
+ OIDC-based trusted publishing workflow.
33
+ - Pin release actions and build tooling, attest distributions, retain their
34
+ hash manifest, and keep the GitHub Release in draft until PyPI succeeds.
35
+
36
+ ### Fixed
37
+
38
+ - Drain remote stdout and stderr concurrently and send stdin EOF.
39
+ - Make session cleanup failures observable and optionally strict.
40
+ - Preserve `$.break(code)` as the CLI exit status.
41
+ - Simplify `run_file()` to execute exactly one file; compose scripts through
42
+ include syntax or Python imports instead of directory/glob execution.
43
+ - Make `.spy` importing explicit with `sshscript.spy_imports()` and scope the
44
+ importer automatically while `run_file()` is executing.
45
+ - Stop patching `threading.Thread`, `warnings`, and `__main__` during import.
46
+ - Preserve `.spy` thread session inheritance with a context-aware Thread
47
+ created by the source transformer instead of a process-wide monkey patch.
48
+ - Make `Session()` construction side-effect free and scope stack activation to
49
+ `run()`, context managers, or an explicit unscoped `$.connect()` operation.
50
+
51
+ ### Project
52
+
53
+ - Add packaging metadata, license, contributor guidance, and CI configuration.
54
+ - Add a disposable loopback OpenSSH integration gate covering host keys, SFTP,
55
+ PTY behavior, sudo/su, and timeout handling.
@@ -0,0 +1,56 @@
1
+ # Contributing
2
+
3
+ ## Development setup
4
+
5
+ Use Python 3.11 or newer. The development checkout is a flat source tree, so
6
+ install its development dependencies rather than installing it in editable
7
+ mode:
8
+
9
+ ```sh
10
+ python3 -m venv .venv
11
+ . .venv/bin/activate
12
+ python3 -m pip install 'paramiko>=2.11,<5' 'packaging>=21' build twine
13
+ ```
14
+
15
+ ## Required checks
16
+
17
+ Run the complete credential-free gate before proposing a change:
18
+
19
+ ```sh
20
+ python3 tools/run_checks.py
21
+ python3 tools/check_release.py --output /tmp/sshscript-candidate-UNIQUE
22
+ ```
23
+
24
+ Tests under `unittest/` must not require network access, SSH agents, private
25
+ keys, passwords, or host-specific configuration. A regression fix should add a
26
+ credential-free test whenever the behavior can be reproduced with a fake
27
+ Paramiko client or local subprocess.
28
+
29
+ ## Credentialed integration tests
30
+
31
+ `unittest-v3/` contains historical and site-specific SSH scenarios. Treat them
32
+ as manual integration tests:
33
+
34
+ - run them only against disposable hosts;
35
+ - never commit credentials, keys, host inventories, or captured secrets;
36
+ - review commands for destructive effects before execution;
37
+ - use environment variables or ignored local files for configuration.
38
+
39
+ Credentialed tests are not evidence that the credential-free release gate may
40
+ fail. Both layers must be healthy before a release.
41
+
42
+ The public CI also provisions a disposable loopback OpenSSH server and runs
43
+ `unittest/test_openssh_integration.py` against the built wheel. The setup uses
44
+ ephemeral users, passwords and keys generated inside the CI runner; it must
45
+ never target a persistent host. See `tools/setup_openssh_ci.sh` for the exact
46
+ environment contract.
47
+
48
+ ## Compatibility and public APIs
49
+
50
+ Changes to `Session`, `run_file()`, CLI exit statuses, dollar syntax, logging,
51
+ or SSH security defaults require synchronized implementation, tests, README,
52
+ and changelog updates. Avoid silently accepting insecure behavior.
53
+
54
+
55
+ Before release, run all normal, optimized, compile, and AST gates listed in
56
+ [EXCEPTIONS.md](EXCEPTIONS.md). CI targets Python 3.11 through 3.14.
@@ -0,0 +1,45 @@
1
+ # Stable exception contract
2
+
3
+ The following contract applies with and without `python -O`.
4
+
5
+ | Failure | Exception |
6
+ | --- | --- |
7
+ | Wrong argument type | `TypeError` |
8
+ | Correct type but invalid value, content, or combination | `ValueError` |
9
+ | Invalid session, console, or channel lifecycle state | `RuntimeError` |
10
+ | Operation on a closed channel or transport | `BrokenPipeError` |
11
+ | Channel ends while waiting | `EOFError` |
12
+ | Timeout | `TimeoutError` |
13
+ | Disconnected `Session.sftp`, upload, or download | `SSHScriptException` |
14
+ | Filesystem failure | Appropriate `OSError` subclass |
15
+ | Paramiko failure | Original Paramiko exception and traceback |
16
+ | Internal AST or data-structure invariant failure | Descriptive `RuntimeError` |
17
+ | Stack indexing outside its bounds | `IndexError`, following `deque` |
18
+
19
+ Nonzero command exit status remains data unless `check=True` requests failure
20
+ handling. `AssertionError` was never a supported SSHScript API contract.
21
+
22
+ User-written `assert` in `.spy` files is preserved as Python syntax. Python's
23
+ optimized mode removes these statements, including any calls inside them.
24
+ Production scripts must not depend on `assert` for command-success handling.
25
+ For local execution, use `session.exec_command(command, check=True)`, or explicitly inspect
26
+ `session.exitcode` and raise an application exception when appropriate.
27
+
28
+ `for_with` must be strictly bool. `get_pty` must be None or bool. Commands must
29
+ be nonempty strings, and persistent commands must contain only one line.
30
+ Compiled bytes regular expressions are not accepted by text-output matching.
31
+ Listener removal requires the identical top listener; failed removal, duplicate
32
+ hijack/release, and last-layer removal leave their associated state unchanged.
33
+
34
+ Run release gates from the project root using a supported interpreter:
35
+
36
+ ```sh
37
+ python -m unittest discover -v -s unittest -p 'test_*.py'
38
+ python -O -m unittest discover -v -s unittest -p 'test_*.py'
39
+ python sshscript.py unittest/dollar_syntax.spy
40
+ python -m compileall -q -x 'unittest-v3' .
41
+ python unittest/check_package_asserts.py
42
+ ```
43
+
44
+ CI runs on Python 3.11–3.14 on Linux and macOS. The AST gate scans the flat
45
+ package's root Python modules; tests and historical `unittest-v3` are excluded.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 Hsin Yuan Yeh
3
+ Copyright (c) 2022-2026 Hsin Yuan Yeh
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1,7 @@
1
+ include README.md LICENSE.txt RELEASING.md
2
+ include CHANGELOG.md CONTRIBUTING.md EXCEPTIONS.md SECURITY.md SUPPORT.md
3
+ recursive-include tools *.py
4
+ include tools/setup_openssh_ci.sh
5
+ include .github/dependabot.yml
6
+ recursive-include .github/workflows *.yml
7
+ recursive-include src/sshscript/unittest *.py *.spy