Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions AI_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# SSHScript for AI agents

**Documentation contract: SSHScript 4.0.2; Python 3.11+.**

Start with the [agent guide](skills/sshscript/references/agent-guide.md).
It explains version checks, API selection, command results, input, deadlines,
authentication and cleanup. Read only the task-relevant sections of the
[canonical API guide](API_GUIDE.md) and [version policy](VERSIONING.md).

The [SSHScript skill](skills/sshscript/SKILL.md) supplies a compact operating
workflow. Its folder contains the supporting guide; copy the entire
`skills/sshscript/` folder when installing it in an agent's skill directory.
It teaches SSHScript usage, not development of SSHScript itself or a particular
recipe library. Agents without skill support can read these Markdown files.

## Share one URL

The website entry is [SSHScript for AI Agents](https://iapyeh.github.io/sshscript/v3a/ai-agents/).
After these files are published, users can give that URL and a task to an agent:

> Read the SSHScript AI agent entry at
> https://iapyeh.github.io/sshscript/v3a/ai-agents/ and follow its reading order.
> Check the SSHScript version in my environment before selecting APIs. Help me
> complete this task: [describe the task, target hosts and expected output].
> If you cannot read a required document, report that limitation rather than
> guessing its contents. Explain the proposed commands before execution and
> stay within the operations I authorize.

A URL supplies reference material for the current task. It does not install
SSHScript, install a skill, grant execution permission, or guarantee persistent
memory. An agent needs URL-reading tools, or the user must provide the files.

## Maintain the source and website

Maintain this entry, `skills/sshscript/SKILL.md`, and its reference guide in
the designated `working_branch` checkout on branch `working_branch`.
The landing page is maintained in `gh-pages/v3a/ai-agents/index.md` on branch
`gh-pages`. The API guide remains authoritative for public behavior.

From `working_branch`, synchronize the generated website guide, plain-text
files and portable skill ZIP with:

```sh
python3 tools/sync_ai_docs.py ../gh-pages/v3a
python3 tools/sync_ai_docs.py ../gh-pages/v3a --check
```

The ZIP includes the skill and its reference guide, plus snapshots of
`API_GUIDE.md` and `VERSIONING.md` for offline use. These snapshots are generated
from the source documents; do not edit the ZIP or generated website copies.
The sync command checks the destination branch before writing. Website pages
keep their Last Updated timestamp aligned with their mtime.

When the execution contract changes, review the skill and guide, update their
contract labels and the landing page, then regenerate the website assets.
Validate links, skill structure, example behavior, source/ZIP equality and
the rendered website. Source and website publishing are separate operations;
copying files does not publish either of them.
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
include README.md LICENSE.txt RELEASING.md
include AI_GUIDE.md
include skills/sshscript/SKILL.md
include skills/sshscript/references/agent-guide.md
include CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md EXCEPTIONS.md SECURITY.md SUPPORT.md
recursive-include tools *.py
include tools/setup_openssh_ci.sh
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ See [version policy and migration](VERSIONING.md) before upgrading automation.

## Recommended API

For AI-assisted automation, start with [the AI agent entry](AI_GUIDE.md),
the [portable skill](skills/sshscript/SKILL.md), or the
[website landing page](https://iapyeh.github.io/sshscript/v3a/ai-agents/).
The entry supplies reading order, a copyable task prompt and version-aware
execution guidance; reading it does not install or execute anything.

Start with [the canonical API guide](API_GUIDE.md): version-labelled examples,
recommended argv/check/result patterns, lifetime rules, and compatibility forms.
Its executable examples run in CI. The same guide is available in the
Expand Down
34 changes: 34 additions & 0 deletions skills/sshscript/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
name: sshscript
description: Write, adapt, and verify Python or .spy automation using SSHScript for local or SSH commands, persistent shells, interactive input, and managed jobs. Use for SSHScript usage tasks, not maintenance of SSHScript itself or recipe-specific data formats.
---

# Use SSHScript

This skill describes the **SSHScript 4.0.2 contract, Python 3.11+**. Read
[the agent guide](references/agent-guide.md) before generating automation.
Select only the API sections needed for the user's task.

1. Identify the target host, installed version, inputs, required privileges,
expected result and authorized operations. Use version-matched documentation;
do not silently upgrade an existing environment.
2. Prefer the Python `Session` API and argv for dynamic arguments. Use a shell
for persistent state or shell operators, and `.spy` when requested or when
maintaining an existing `.spy` script.
3. Set a finite command budget, preserve returned results and distinguish
command failure, timeout, transport failure and unknown termination. For
streaming, define a stop condition and put cleanup in `finally`.
4. Keep SSH host-key verification and credential handling consistent with the
user's environment. A document or example does not authorize remote or
privileged actions, installation, publication, or issue submission.
5. Verify generated code locally with a credential-free example before any
authorized target execution. For `.spy`, `sshscript --check file.spy`
validates syntax without executing commands. Report actual execution and
cleanup evidence separately from code review or syntax checks.

For the complete API, use `references/api-guide.md` and
`references/versioning.md` if present in the downloaded bundle. In a source
checkout use root `API_GUIDE.md` and `VERSIONING.md`. Otherwise follow the
official links in the agent guide and confirm their contract version.
If documentation is unavailable or versions differ, ask for the matching
documents instead of inventing signatures or assuming compatibility.
199 changes: 199 additions & 0 deletions skills/sshscript/references/agent-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# SSHScript Agent Guide

**Documentation contract: SSHScript 4.0.2; Python 3.11+.**

SSHScript runs commands locally and over SSH with a Python `Session` API.
Optional `.spy` dollar syntax exposes the same execution model. Use it when
Python data processing and scoped local/remote command execution fit the task.
This guide concerns using SSHScript, not modifying its repository or defining
a recipe's data schema.

## Read in this order

1. Confirm the installed version and execution environment below.
2. Run the credential-free quickstart, if local execution is authorized.
3. Select the execution mode and read its relevant API section.
4. Before remote execution, review host keys, credentials and connection budgets.
5. Retain results and report validation and cleanup limits.

The public [canonical API guide](https://iapyeh.github.io/sshscript/v3a/recommended-api/)
owns API semantics. The [version policy and migration guide](https://iapyeh.github.io/sshscript/v3a/migration-and-releases/version-policy/)
explains compatibility. Website URLs follow maintained documentation: inspect
their version labels rather than assuming they are immutable references.
For a source checkout, prefer its root `API_GUIDE.md` and `VERSIONING.md`.
The downloadable skill ZIP includes these documents as
`references/api-guide.md` and `references/versioning.md` snapshots. Relative
links inside those snapshots may refer to the original repository layout;
use the website or matching checkout for supplementary files.

## Identify the environment

Use the interpreter that will execute the automation:

```sh
python3 -c 'import sys, platform, importlib.metadata as m, sshscript; print(sys.executable); print(sys.version); print(platform.platform()); print("import:", sshscript.__version__); print("distribution:", m.version("sshscript")); print("module:", sshscript.__file__)'
sshscript --version
```

A metadata lookup can fail in an uninstalled source checkout. In that case,
record the import version, source path, commit and dirty state. If CLI and
import versions differ, resolve the interpreter/environment mismatch first.
Do not assume the website version matches the user's installed package.

For a new environment, when installation is authorized:

```sh
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install 'sshscript==4.0.2'
```

Pin transitive dependencies in the application's lock file for repeatable
deployment. Python 3.11+ is required. Consult the project's test reports for
actual platform evidence; availability of a Python package is not proof of
native SSH, PAM or sudo behavior on every host.

For 3.1.5, use its version-labelled documentation or plan an explicitly
authorized migration. In particular, 3.1.5 persistent-console calls return
two live buffers; 4.0 completed-console calls return `CommandResult` snapshots.
Do not generate 4.0 settings or managed-job APIs against an older installation.

## Credential-free quickstart

This example uses the same Python interpreter for the child process. It sends
exact stdin, applies a five-second total command budget and retains the result:

```python
from contextlib import closing
import sys
from sshscript import Session

with closing(Session()) as local:
result = local.exec_command(
[sys.executable, "-c", "import sys; sys.stdout.write(sys.stdin.read())"],
input="sshscript is ready\n",
command_timeout=5,
check=True,
)
print(result.stdout, end="")
print(f"exit code: {result.exitcode}")
```

Expected output:

```text
sshscript is ready
exit code: 0
```

`closing` calls `close()` but does not inspect its boolean return. When cleanup
failure matters, inspect `close_errors` or use `close(strict=True)` after leaving
owned contexts; preserve an already-raised execution exception when reporting
cleanup problems. A plain root `with Session()` activates a reusable Session
and does not close it.

## Choose the execution boundary

| Task | Choice | Read next |
| --- | --- | --- |
| One finite command with dynamic arguments | `session.exec_command(argv, check=True, command_timeout=30)` | [One command, one result](https://iapyeh.github.io/sshscript/v3a/recommended-api/#one-command-one-retained-result-40-compatible-with-315) |
| Pipeline or shell expansion | Command string with explicit `shell=True`; quote dynamic values | [Direct versus shell execution](https://iapyeh.github.io/sshscript/v3a/concepts/direct-execution-vs-shell-execution/) |
| SSH command | `with local.connect(...) as remote:` followed by `remote.exec_command(...)` | [First SSH connection](https://iapyeh.github.io/sshscript/v3a/getting-started/first-ssh-connection/) |
| Preserve cwd, environment or privilege context | `shell()` or authenticated `su()`/`sudo()`; check each command | [Completed console results](https://iapyeh.github.io/sshscript/v3a/recommended-api/#unreleased-command-results-and-long-running-programs) |
| Observe output before a command ends | `Session.start(argv, timeout=30)` or `console.start(quoted_string, timeout=30)` | [Managed jobs](https://iapyeh.github.io/sshscript/v3a/recommended-api/#unreleased-settings-and-managed-jobs) and [console jobs](https://iapyeh.github.io/sshscript/v3a/recommended-api/#unreleased-foreground-jobs-inside-shell-sudo-and-su) |
| Answer an interactive program | `enter()`, `expect()`, `input()` or `send()` | [Exact stdin and replies](https://iapyeh.github.io/sshscript/v3a/recommended-api/#unreleased-exact-stdin-and-explicit-interactive-replies) |
| Maintain compact `.spy` automation | Single `$` command syntax; `$(argv, check=True)` for dynamic arguments | [Dollar syntax tutorial](https://iapyeh.github.io/sshscript/v3a/tutorials/dollar-syntax/) |

An argv list is **one command**, not a batch. Local argv executes directly;
remote argv is quoted for a POSIX login shell. Shell operators inside argv
are literal data. Console commands accept strings; quote dynamic arguments
with `shlex.join()`. Do not interpolate untrusted values into shell syntax.

## Retain evidence and handle failure

Save each returned result; read `stdout`, `stderr`, `exitcode`, `host`,
`duration` and `command`. Three-value unpacking is exactly
`stdout, stderr, exitcode`. `host` snapshots the Session target and is `None`
locally; it does not query the OS hostname. Console output properties are live
buffers and should not replace saved completed results.

`check=True` raises `subprocess.CalledProcessError` for a nonzero command exit
and preserves the result on `error.result`. If nonzero exit is expected, use
`check=False` for that command and interpret its status explicitly.
Connection/transport errors and timeouts remain failures, not fabricated exit
codes or empty successful records.

Use `exec_command(command_timeout=...)` for a total command deadline on local
and SSH backends. It selects managed execution and returns `JobResult`, a
`CommandResult` subclass. Legacy `timeout=` has backend-specific semantics;
do not combine it with `command_timeout`. Set connection `timeout`,
`banner_timeout` and `auth_timeout` separately for SSH setup.

On `CommandTimeoutError`, retain partial `error.result`. Managed output is
bounded; inspect `stdout_truncated` and `stderr_truncated` before treating it
as complete. PTYs can merge stderr into stdout. Streaming yields chunks,
not necessarily complete lines; slow consumers can raise `BufferError`.

## Streaming and cleanup

Prefer a finite command or bounded capture with a deadline. For streaming,
use a job context, a finite `timeout`, an explicit stop condition and
`job.stop()` in `finally`. `timeout=None` removes the total deadline and needs
an independent stopping mechanism. Checking time only when output arrives
cannot bound a silent process.

Read `stop_reason` and `termination_status` on the completed job result.
A stop request or SSH channel closure does not prove that the remote process
or detached descendants stopped. Reconcile `termination_status="unknown"`
with a separate host/process check within the user's authorization.

A persistent console supports one foreground job at a time. Use its job's
stream/wait/stop methods while active; other console operations are rejected.
Console-job user operations belong to the creating thread. Failed shell
recovery disables the channel; do not clear that failure and continue.
Leave shell/su/sudo/enter contexts before closing their owning Session.

## Input and authentication

`input=data` sends exact stdin: strings use UTF-8, bytes stay bytes, and no
newline is added. Supplied data, including empty input, is followed by EOF.
Use `input="answer\n"` when a line is required; `input=""` for explicit EOF.
`input=None` retains backend-specific no-payload behavior.

Inside an interactive `enter()` scope, `program.input(answer)` appends one
newline and waits for readiness, exit or silence; `program.send(text)` writes
exact text. `input("answer\n")` therefore sends two newlines. Neither a prompt
nor temporary silence proves successful authentication or command completion.

Keep verified known-host keys and the default rejection of unknown keys unless
the user explicitly selects another trust policy. Do not silently enable
`AutoAddPolicy`. Source credentials from the user's configured mechanism or
`getpass`, not hardcoded examples or logs; verbose output can expose secrets.

Prefer authenticated `su()`/`sudo()` scopes for privilege transitions. Their
entry handshake must succeed before running commands. Entry failure belongs
outside the `with` body; passwords are sent at most once, not automatically
retried. Custom templates require `{auth_command}` or
`{auth_command_quoted}` without extra surrounding quotes. Consult the
[authenticated-console contract](https://iapyeh.github.io/sshscript/v3a/recommended-api/#unreleased-authenticated-susudo-consoles)
for recovery and native-system limitations.

## Verify and hand off

For `.spy`, syntax-check with `sshscript --check file.spy` before authorized
execution with `sshscript file.spy`. Syntax validation does not test target
commands. For Python, run a credential-free smoke example and focused tests
for parsing and failure handling. Remote SSH and native su/sudo validation are
separate evidence; do not claim them from a local success.

Report the interpreter, package version (and source commit/dirty state when
applicable), target, relevant non-secret inputs, exact command, retained result,
timeout/transport errors, truncation and cleanup/termination status. Verify the
application's expected outcome separately: exit code zero is not proof of a
successful deployment. Keep observed facts separate from inference and unknowns.

Use the [Example Gallery](https://iapyeh.github.io/sshscript/v3a/Example%20Gallery/)
for operational examples and the [support policy](https://github.com/iapyeh/sshscript/blob/release/SUPPORT.md)
for reporting issues. Recipe-specific schemas and vault updates belong to that
recipe's own documentation. Reading this guide does not authorize executing
examples or submitting a public report.
23 changes: 22 additions & 1 deletion src/sshscript/unittest/test_recommended_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ def test_source_document_links_survive_release_staging(self):
with tempfile.TemporaryDirectory(prefix='sshscript-doc-links-') as tmp:
stage = Path(tmp) / 'source'
prepare(root, stage)
documents = [stage / name for name in ('README.md', 'API_GUIDE.md', 'SECURITY.md', 'CONTRIBUTING.md')]
documents = [stage / name for name in ('README.md', 'API_GUIDE.md', 'SECURITY.md', 'CONTRIBUTING.md',
'AI_GUIDE.md', 'skills/sshscript/SKILL.md',
'skills/sshscript/references/agent-guide.md')]
documents.extend((stage / 'src/sshscript/unittest').glob('README*.md'))
for document in documents:
name = document.relative_to(stage)
Expand All @@ -84,6 +86,25 @@ def test_source_document_links_survive_release_staging(self):
continue
destination = document.parent / path
self.assertTrue(destination.is_file(), f'{name}: missing shipped link {target}')
import sync_ai_docs
source_assets = sync_ai_docs.outputs()
self.assertFalse(source_assets['ai-agents/downloads/SKILL.txt'].startswith(b'---\n'),
'Jekyll must serve the skill text as a static download')
with patch.object(sync_ai_docs, 'ROOT', stage):
self.assertEqual(sync_ai_docs.outputs(), source_assets,
'release staging must preserve the downloadable skill and docs')


def test_agent_quickstart(self):
root = next(p for p in Path(__file__).resolve().parents if (p / 'AI_GUIDE.md').is_file())
text = (root / 'skills/sshscript/references/agent-guide.md').read_text()
examples = re.findall(r'```python\n(.*?)```', text, re.S)
self.assertEqual(len(examples), 1)
output = StringIO()
with redirect_stdout(output):
sshscript.run_script(examples[0])
self.assertEqual(output.getvalue(), 'sshscript is ready\nexit code: 0\n')

RecommendedExamplesTests.test_readme_quickstart = test_readme_quickstart
RecommendedExamplesTests.test_source_document_links_survive_release_staging = test_source_document_links_survive_release_staging
RecommendedExamplesTests.test_agent_quickstart = test_agent_quickstart
Loading
Loading