@knowttl/ssh-axi 0.1.0 → 0.1.1
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.
- package/README.md +223 -107
- package/dist/arguments.d.ts +14 -0
- package/dist/arguments.js +20 -1
- package/dist/authenticate.d.ts +1 -1
- package/dist/authenticate.js +1 -1
- package/dist/cli.js +5 -12
- package/dist/hosted.js +6 -5
- package/dist/native-execution.d.ts +2 -1
- package/dist/native-execution.js +27 -14
- package/dist/profile.d.ts +1 -0
- package/dist/profile.js +12 -2
- package/package.json +20 -5
package/README.md
CHANGED
|
@@ -1,93 +1,80 @@
|
|
|
1
1
|
# ssh-axi
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Agent-facing SSH CLI: discover approved hosts, check encrypted-key authentication and run confirmed, bounded commands.
|
|
4
|
+
Arbitrary execution always requires explicit confirmation.
|
|
5
|
+
|
|
6
|
+
[Quickstart](#quickstart) | [Commands](#commands) | [Safety](#safety) | [Configuration](#configuration) | [Output and exit codes](#output-and-exit-codes) | [Limitations](#limitations) | [Development](#development)
|
|
7
|
+
|
|
8
|
+
## Why this exists
|
|
9
|
+
|
|
10
|
+
Agents need remote shell access that is bounded, auditable and hard to misuse.
|
|
11
|
+
`ssh-axi` returns compact TOON output (or `--json` with the same fields) with next-step hints, and bounds command time and output size.
|
|
12
|
+
It consults only approved profiles: no SSH config, agent, known-hosts file or ambient key is read.
|
|
13
|
+
Every arbitrary command requires `--confirm`, and server keys must be approved before credentials are sent.
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
Requires Node >=24.11.0.
|
|
6
18
|
|
|
7
19
|
```sh
|
|
20
|
+
npm install --global @knowttl/ssh-axi
|
|
8
21
|
ssh-axi
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
With no configuration, the home view lists no hosts and points to `ssh-axi config path`:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
description: Discover approved SSH hosts and run confirmed bounded commands.
|
|
28
|
+
hosts: []
|
|
29
|
+
count: 0
|
|
30
|
+
message: No configured hosts found.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Create a profile (see [Configuration](#configuration)), then run the first authenticated read:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
9
36
|
ssh-axi host list
|
|
10
37
|
ssh-axi host show lab
|
|
11
38
|
ssh-axi doctor lab --json
|
|
12
|
-
ssh-axi doctor --profile lab
|
|
13
|
-
ssh-axi auth status --profile lab
|
|
14
|
-
ssh-axi profile list
|
|
15
|
-
ssh-axi config path
|
|
16
39
|
ssh-axi exec lab --command 'printf "%s\n" "quoted value"' --confirm --timeout 30s --json
|
|
17
40
|
```
|
|
18
41
|
|
|
19
|
-
|
|
20
|
-
Each command accepts `--help`.
|
|
21
|
-
Unknown arguments and flags fail with exit code 2; operational failures use exit code 1.
|
|
22
|
-
Help, discovery, profile inspection and credential status open no SSH transports.
|
|
23
|
-
Native execution is one-shot.
|
|
24
|
-
Managed execution records, recovery and cancellation use the matching Artium host extension.
|
|
25
|
-
Reusable connections require the matching Artium connection-lifetime extension.
|
|
26
|
-
Managed sudo requires a distinct approved account and SSH credential with a current sudo grant.
|
|
27
|
-
Agents, forwarding and interactive shells remain unavailable.
|
|
42
|
+
## Use with your agent
|
|
28
43
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
No session hooks or agent skill ship with this package yet; this is pending.
|
|
45
|
+
Agents call the CLI directly, and every command accepts `--help`.
|
|
46
|
+
Managed execution, recovery, cancellation and reusable connections are available to hosted callers through Artium (see [Hosted use with Artium](#hosted-use-with-artium)).
|
|
32
47
|
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
"version": 1,
|
|
36
|
-
"defaultProfile": "lab",
|
|
37
|
-
"profiles": {
|
|
38
|
-
"lab": {
|
|
39
|
-
"address": "127.0.0.1",
|
|
40
|
-
"port": 2222,
|
|
41
|
-
"username": "ordinary",
|
|
42
|
-
"allowedAddresses": ["127.0.0.1"],
|
|
43
|
-
"approvedHostKeys": ["ssh-ed25519 <independently-verified-base64-key>"],
|
|
44
|
-
"privateKey": {"file": "/absolute/private/key.pem"},
|
|
45
|
-
"passphrase": {"env": "LAB_KEY_PASSPHRASE"}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
```
|
|
48
|
+
## Commands
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
```sh
|
|
51
|
+
ssh-axi # home view
|
|
52
|
+
ssh-axi host list # list approved hosts, no transport opened
|
|
53
|
+
ssh-axi host show <host> # show one approved host, no transport opened
|
|
54
|
+
ssh-axi doctor <host> # authenticate without running a remote command
|
|
55
|
+
ssh-axi doctor --profile <name>
|
|
56
|
+
ssh-axi auth status --profile <name> # inspect credential sources offline
|
|
57
|
+
ssh-axi profile list
|
|
58
|
+
ssh-axi config path
|
|
59
|
+
ssh-axi exec <host> --command <text> --confirm
|
|
60
|
+
```
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
Shared flags: `--json`, `--help`, `--config <absolute-path>`, `--profile <name>`.
|
|
63
|
+
`exec` also accepts `--command`, `--confirm`, `--timeout`, `--output-limit`, `--detach`, `--request-key`, `--sudo` and `--connection`.
|
|
64
|
+
`--detach`, `--request-key`, `--sudo` and `--connection` depend on the Artium host extension.
|
|
65
|
+
`execution list`, `execution show`, `execution cancel` and the `connection` commands are managed (Artium) only; standalone calls return `HOSTED_REQUIRED`.
|
|
66
|
+
Run `ssh-axi <command> --help` for the exact flags of each command.
|
|
67
|
+
Help, discovery, profile inspection and credential status open no SSH transports.
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
Doctor requests no SSH channel and closes its socket, probe worker and private directories before returning.
|
|
69
|
-
Private-key buffers are released where supported; JavaScript strings and process termination do not provide a secure-erasure guarantee.
|
|
69
|
+
Native execution is one-shot.
|
|
70
|
+
Native `doctor` also checks [write journal custody](#safety) before authentication without reading existing journal contents, creating an empty journal if needed without recording an execution.
|
|
71
|
+
Agents, forwarding and interactive shells are unavailable.
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
Hosted callers may list and show their granted hosts, run doctor and submit confirmed commands when the host write policy permits.
|
|
73
|
-
They cannot supply configuration, account, trust, secret, identity or worker overrides.
|
|
74
|
-
The matching Artium admission change must be installed alongside this artifact; stock Artium does not admit it.
|
|
75
|
-
The matching host extension provides `/admin/ssh` for confirmed, revision-checked host, trust, binding and grant changes.
|
|
76
|
-
Server keys require independent verification and at most two approved keys may overlap during rotation.
|
|
77
|
-
Host and credential administration have separate delegated permissions; only credential administrators can replace or delete encrypted SSH secrets, which are never displayed.
|
|
78
|
-
SSH credential identifiers cannot resolve credentials for other tools.
|
|
79
|
-
Changes fence affected connections before acknowledgement.
|
|
80
|
-
The corresponding authenticated APIs live under `/v1/admin/ssh/`.
|
|
73
|
+
### Hosted use with Artium
|
|
81
74
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Known managed secrets and encoded variants are rejected before command journaling.
|
|
86
|
-
Native command intents are written durably to `~/.local/state/ssh-axi/writes.log`, or the absolute `XDG_STATE_HOME` override.
|
|
87
|
-
`SSH_AXI_WRITE_LOG` selects an absolute protected journal path.
|
|
88
|
-
No local shell is invoked; the exact command text is sent to the remote SSH exec channel.
|
|
89
|
-
Remote stdin is closed without forwarding input; both output streams continue draining.
|
|
90
|
-
Native Ctrl+C requests cancellation through the private worker.
|
|
75
|
+
Managed execution records, recovery and cancellation use the matching Artium host extension.
|
|
76
|
+
Reusable connections require the matching Artium connection-lifetime extension.
|
|
77
|
+
Managed sudo requires a distinct approved account and SSH credential with a current sudo grant.
|
|
91
78
|
|
|
92
79
|
```sh
|
|
93
80
|
artium-axi ssh-axi exec lab --command 'systemctl --failed' --confirm --detach
|
|
@@ -105,72 +92,201 @@ artium-axi ssh-axi exec lab --sudo --command 'printf "%s\n" "value" | cat' --con
|
|
|
105
92
|
artium-axi ssh-axi connection close <connection-id>
|
|
106
93
|
```
|
|
107
94
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
95
|
+
Artium supplies authenticated host grants, separate ordinary and sudo account bindings, encrypted SSH credentials, private IPC and one isolated `artium` profile through its shared writer.
|
|
96
|
+
Hosted callers may list and show their granted hosts, run doctor and submit confirmed commands when the host write policy permits.
|
|
97
|
+
They cannot supply configuration, account, trust, secret, identity or worker overrides.
|
|
98
|
+
The matching Artium admission change must be installed alongside this artifact; stock Artium does not admit it.
|
|
99
|
+
The matching host extension provides `/admin/ssh` for confirmed, revision-checked host, trust, binding and grant changes.
|
|
100
|
+
The corresponding authenticated APIs live under `/v1/admin/ssh/`.
|
|
101
|
+
Host and credential administration have separate delegated permissions.
|
|
102
|
+
Only credential administrators can replace or delete encrypted SSH secrets, which are never displayed.
|
|
103
|
+
SSH credential identifiers cannot resolve credentials for other tools.
|
|
104
|
+
Changes fence affected connections before acknowledgement.
|
|
105
|
+
|
|
106
|
+
Hosted connections:
|
|
107
|
+
|
|
108
|
+
- Belong to one authenticated user and login session family.
|
|
109
|
+
- Start a fresh remote process for each command on the same transport.
|
|
110
|
+
- Must match the selected host and ordinary or sudo binding, with no fallback reconnection.
|
|
111
|
+
- Allow one active command per connection; overlapping use returns `CONNECTION_BUSY`.
|
|
112
|
+
- Require `--confirm` to close while a command is active, which cancels that command; repeated close by the owner is idempotent.
|
|
113
|
+
- Expire when idle after 10 minutes by default, accepting 1-60 minutes within the host ceiling.
|
|
114
|
+
- Are not kept alive by inspection; timeout changes recompute expiry from the existing idle start.
|
|
115
|
+
- Have an 8-hour maximum age, with four per user and sixteen globally, including authentication in progress.
|
|
116
|
+
- Are fenced on revocation: affected workers stop and their private profiles are removed before acknowledgement.
|
|
117
|
+
- Are discarded on server restart, which removes abandoned private directories and retains unproven executions as uncertain without replay.
|
|
118
|
+
|
|
111
119
|
Sudo dispatch sends `sudo -n -- /bin/sh -c <quoted-command>` through its own matching connection.
|
|
112
120
|
Pipelines and redirects remain inside that privileged shell.
|
|
113
121
|
Password-required or terminal-required sudo returns `SUDO_DENIED` with administrator guidance.
|
|
114
|
-
Controlled peers establish account, key and command selection only.
|
|
115
|
-
Actual ordinary-account non-elevation and successful non-root passwordless elevation remain commissioning checks.
|
|
116
|
-
There is one active command per connection; overlapping use returns `CONNECTION_BUSY`.
|
|
117
|
-
Closing an active connection requires `--confirm` and cancels its command.
|
|
118
|
-
Repeated close by the owner is idempotent.
|
|
119
|
-
Idle expiry defaults to 10 minutes and accepts 1-60 minutes within the host ceiling.
|
|
120
|
-
Inspection does not reset idle time; timeout changes recompute expiry from the existing idle start.
|
|
121
|
-
Connections have an 8-hour maximum age, with four per user and sixteen globally, including authentication in progress.
|
|
122
|
-
Revocation fences affected workers and removes their private profiles before acknowledgement.
|
|
123
|
-
Server restart discards connections, removes abandoned private directories and retains unproven executions as uncertain without replay.
|
|
124
122
|
|
|
125
123
|
The hosted client saves a private command-free receipt before submitting a signed preparation key.
|
|
126
124
|
Show without an ID recovers the latest receipt after a client restart, without resubmitting.
|
|
127
|
-
Matching resubmission returns the same execution ID
|
|
125
|
+
Matching resubmission returns the same execution ID, and a durable dispatch claim prevents a second SSH command.
|
|
128
126
|
Unused keys expire after ten minutes; changed inputs, bindings or owners are refused.
|
|
129
127
|
Wait is the default and uses short status requests; `--detach` returns after acceptance.
|
|
130
128
|
An unexpected client disconnect leaves the managed job observable and deadline bounded.
|
|
131
129
|
Ctrl+C requests cancellation of the accepted execution ID.
|
|
132
130
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
131
|
+
## Safety
|
|
132
|
+
|
|
133
|
+
- Every arbitrary command requires `--confirm`, including inspection-looking commands.
|
|
134
|
+
- `SSH_AXI_READ_ONLY=true` rejects arbitrary execution before opening a transport or writing a command journal.
|
|
135
|
+
- Commands must contain 1-16384 UTF-8 bytes and no NUL.
|
|
136
|
+
- Known managed secrets and encoded variants are rejected before command journaling.
|
|
137
|
+
- Native command intents are written durably to `$XDG_STATE_HOME/ssh-axi/writes.log` when `XDG_STATE_HOME` is absolute, otherwise to `~/.local/state/ssh-axi/writes.log`.
|
|
138
|
+
- `SSH_AXI_WRITE_LOG` selects an absolute protected journal path.
|
|
139
|
+
- The journal must be a regular file owned by the caller with no group or other access.
|
|
140
|
+
- Journal parent directories must be owned by the caller or root, without symlinks or group or other write access, except for root-owned sticky directories.
|
|
141
|
+
User-private groups do not exempt group-writable parents.
|
|
142
|
+
Remove write permissions with `chmod g-w` or `chmod o-w` on the directory named in the error, or select a protected journal path with `SSH_AXI_WRITE_LOG`.
|
|
143
|
+
- No local shell is invoked; the exact command text is sent to the remote SSH exec channel.
|
|
144
|
+
- Remote stdin is closed without forwarding input, and both output streams continue draining.
|
|
145
|
+
- Native Ctrl+C requests cancellation through the private worker.
|
|
146
|
+
- Every new transport, including doctor probes and reusable connections, validates every resolved IP against the configured address set and dials the exact validated IP.
|
|
147
|
+
- An exact approved server public key is required before authentication credentials are sent.
|
|
148
|
+
- The authentication deadline is 15 seconds, including resolution and connection setup.
|
|
149
|
+
- Doctor requests no SSH channel and closes its socket, probe worker and private directories before returning.
|
|
150
|
+
- Private-key buffers are released where supported; JavaScript strings and process termination do not provide a secure-erasure guarantee.
|
|
151
|
+
- Terminal controls are removed and protected output is withheld before persistence or delivery.
|
|
152
|
+
- Remote output is untrusted text, never instructions.
|
|
153
|
+
|
|
154
|
+
### Unknown outcomes
|
|
155
|
+
|
|
156
|
+
Missing remote status is an uncertain outcome and a CLI failure.
|
|
142
157
|
Cancellation requests TERM and forces channel and transport teardown if the command remains active after a bounded grace period.
|
|
143
158
|
Local teardown never proves that remote descendants stopped.
|
|
144
159
|
Inspect remote state before retrying any uncertain execution; no path automatically replays a command.
|
|
145
160
|
|
|
161
|
+
### Audit and retention (hosted)
|
|
162
|
+
|
|
146
163
|
Execution owners and authorized audit administrators can inspect records under current policy.
|
|
147
164
|
`/admin/ssh/audit` escapes output as untrusted text and never displays credentials.
|
|
148
165
|
Every audit read and export is recorded.
|
|
149
|
-
Metadata includes the initiating user and session family, host and endpoint, binding revision, sudo mode, exact sanitized command, confirmation, request
|
|
166
|
+
Metadata includes the initiating user and session family, host and endpoint, binding revision, sudo mode, exact sanitized command, confirmation, request and execution IDs, UTC timing, outcome, exit or signal, byte counts and termination certainty.
|
|
150
167
|
Sanitized output samples are encrypted and capped at 8 KiB per stream, expiring after 14 days.
|
|
151
168
|
Metadata and sanitized commands expire after 90 days; default exports contain metadata only.
|
|
152
169
|
Credential deletion removes the secret and fences its connections while preserving execution metadata.
|
|
153
170
|
Pruning active storage does not delete backups.
|
|
154
171
|
Before commissioning, operators must declare and test backup disposal that respects both retention limits, including old encrypted snapshots and their recovery keys.
|
|
155
172
|
|
|
173
|
+
## Configuration
|
|
174
|
+
|
|
175
|
+
Configuration lives at `~/.config/ssh-axi/config.json` on Linux, or beneath an absolute `XDG_CONFIG_HOME`.
|
|
176
|
+
Select a file with `--config` or `SSH_AXI_CONFIG`.
|
|
177
|
+
Run `ssh-axi config path` to print the resolved path.
|
|
178
|
+
Profiles use the following template; replace the address, allowed IP and independently verified host key before use.
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"version": 1,
|
|
183
|
+
"defaultProfile": "lab",
|
|
184
|
+
"profiles": {
|
|
185
|
+
"lab": {
|
|
186
|
+
"address": "127.0.0.1",
|
|
187
|
+
"port": 2222,
|
|
188
|
+
"username": "ordinary",
|
|
189
|
+
"allowedAddresses": ["127.0.0.1"],
|
|
190
|
+
"approvedHostKeys": ["ssh-ed25519 <independently-verified-base64-key>"],
|
|
191
|
+
"privateKey": {"file": "/absolute/private/key.pem"},
|
|
192
|
+
"passphrase": {"env": "LAB_KEY_PASSPHRASE"}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The strict profile contract is shipped as `axi-credential.schema.json`.
|
|
199
|
+
Server keys require independent verification, and at most two approved keys may overlap during rotation.
|
|
200
|
+
|
|
201
|
+
### Credentials
|
|
202
|
+
|
|
203
|
+
Keys and passphrases accept environment or protected-file SecretRefs only.
|
|
204
|
+
Secret values and inline private keys are refused in JSON and arguments.
|
|
205
|
+
An omitted private-key reference defaults to `SSH_AXI_PRIVATE_KEY`, and the passphrase defaults to `SSH_AXI_PASSPHRASE`.
|
|
206
|
+
`auth status` reports credential availability and sources as `env:<name>` or `protected file`.
|
|
207
|
+
It does not display secret values or file paths, and does not check network authentication.
|
|
208
|
+
Linux secret files must be owned by the caller, regular, protected from group and other access and reachable through protected directories without symlinks.
|
|
209
|
+
Configuration files must also be owned and protected against group and other writes.
|
|
210
|
+
|
|
211
|
+
### Profile selection
|
|
212
|
+
|
|
213
|
+
Precedence is explicit host or `--profile`, `SSH_AXI_PROFILE`, a complete environment profile, the default profile, then the only profile.
|
|
214
|
+
An explicitly selected file profile ignores competing endpoint environment fields.
|
|
215
|
+
Environment profiles use `SSH_AXI_ADDRESS`, `SSH_AXI_PORT`, `SSH_AXI_USERNAME`, JSON arrays in `SSH_AXI_APPROVED_HOST_KEYS` and `SSH_AXI_ALLOWED_ADDRESSES`, and the secret environment fields above.
|
|
216
|
+
Partial environment profiles fail.
|
|
217
|
+
|
|
218
|
+
### Environment variables
|
|
219
|
+
|
|
220
|
+
| Variable | Purpose |
|
|
221
|
+
| --- | --- |
|
|
222
|
+
| `SSH_AXI_CONFIG` | Absolute configuration file path |
|
|
223
|
+
| `SSH_AXI_PROFILE` | Profile name to select |
|
|
224
|
+
| `SSH_AXI_ADDRESS`, `SSH_AXI_PORT`, `SSH_AXI_USERNAME` | Environment profile endpoint |
|
|
225
|
+
| `SSH_AXI_APPROVED_HOST_KEYS`, `SSH_AXI_ALLOWED_ADDRESSES` | JSON arrays for an environment profile |
|
|
226
|
+
| `SSH_AXI_PRIVATE_KEY`, `SSH_AXI_PASSPHRASE` | Default private-key and passphrase references |
|
|
227
|
+
| `SSH_AXI_READ_ONLY` | `true` rejects arbitrary execution |
|
|
228
|
+
| `SSH_AXI_WRITE_LOG` | Absolute protected command journal path |
|
|
229
|
+
| `XDG_CONFIG_HOME`, `XDG_STATE_HOME` | Absolute base directories |
|
|
230
|
+
|
|
231
|
+
## Output and exit codes
|
|
232
|
+
|
|
233
|
+
Output defaults to TOON; `--json` returns the same fields and empty-state meaning.
|
|
234
|
+
Unknown arguments and flags fail with exit code 2.
|
|
235
|
+
Operational failures use exit code 1.
|
|
236
|
+
A remote exit such as 7 produces the same CLI exit code.
|
|
237
|
+
The default command deadline is 60 seconds, adjustable with `--timeout 1s` through `600s` within host ceilings.
|
|
238
|
+
Both streams are drained, retaining 65536 bytes each by default.
|
|
239
|
+
`--output-limit <bytes>` sets retention from 0 through 1048576 bytes per stream, subject to managed host ceilings.
|
|
240
|
+
`--full` never bypasses retention or executes another command.
|
|
241
|
+
More than 16 MiB of combined received output triggers cancellation.
|
|
242
|
+
Results carry separate remote exit or signal, received and retained byte counts, truncation and encoding-loss flags.
|
|
243
|
+
`termination` is `deadline` when a timed-out command returns a remote exit or signal, and `remote-exit` for other reported remote exits or signals.
|
|
244
|
+
Without remote status, `termination` remains `unknown`, including after deadline-triggered local teardown; see [Unknown outcomes](#unknown-outcomes).
|
|
245
|
+
|
|
246
|
+
## Limitations
|
|
247
|
+
|
|
248
|
+
- Native execution is one-shot; agents, forwarding and interactive shells are unavailable.
|
|
249
|
+
- Managed executions, connections and sudo require the matching Artium extensions.
|
|
250
|
+
- Actual ordinary-account non-elevation and successful non-root passwordless elevation remain commissioning checks; controlled peers establish account, key and command selection only.
|
|
251
|
+
- On Windows the default configuration path is `%APPDATA%/ssh-axi/config.json`, with an absolute `APPDATA` required.
|
|
252
|
+
DACL verification and native Windows evidence remain pending, so protected-file operations fail closed there.
|
|
253
|
+
- Agent session hooks and a skill are pending.
|
|
254
|
+
|
|
255
|
+
## Development
|
|
256
|
+
|
|
257
|
+
Requires Node >=24.11.0 and pnpm 10.22.0.
|
|
258
|
+
|
|
156
259
|
```sh
|
|
157
260
|
pnpm install --frozen-lockfile
|
|
158
261
|
pnpm build
|
|
159
262
|
pnpm lint
|
|
160
263
|
pnpm test
|
|
161
264
|
pnpm pack --pack-destination /absolute/artifacts
|
|
162
|
-
ARTIUM_FIXTURE_ROOT=/absolute/frozen-export pnpm verify:journey -- --slice B1.1
|
|
163
|
-
ARTIUM_FIXTURE_ROOT=/absolute/frozen-export pnpm verify:journey -- --slice B2.2
|
|
164
|
-
ARTIUM_FIXTURE_ROOT=/absolute/frozen-export pnpm verify:journey -- --slice B3.2
|
|
165
|
-
ARTIUM_FIXTURE_ROOT=/absolute/frozen-export pnpm verify:journey -- --slice B4.1
|
|
166
|
-
ARTIUM_FIXTURE_ROOT=/absolute/frozen-export pnpm verify:journey -- --slice B4.2
|
|
167
|
-
ARTIUM_FIXTURE_ROOT=/absolute/frozen-export pnpm verify:journey -- --slice B4.3
|
|
168
265
|
```
|
|
169
266
|
|
|
170
|
-
Tests generate disposable keys and run all descendants inside a Linux network namespace with loopback only.
|
|
171
|
-
They require `unshare`, `ip` and `
|
|
267
|
+
Tests generate disposable keys and run all test descendants inside a Linux network namespace with loopback only.
|
|
268
|
+
They require `unshare`, `ip`, `mount` and `tar`, and never fall back to unrestricted networking.
|
|
269
|
+
Test preparation packs the local package and installs it offline into a clean prefix inside network isolation.
|
|
270
|
+
Run `pnpm install --frozen-lockfile` first to populate the local dependency store.
|
|
271
|
+
Test preparation reuses the project lockfile so offline installation does not require cached registry metadata.
|
|
272
|
+
The package-contract checks verify the archive allowlist, installed bin help/version, and published admission entry points without an Artium fixture.
|
|
172
273
|
The default suite verifies a reproducible tarball installed into a clean prefix and explicitly skips Artium unless a fixture is provided.
|
|
274
|
+
To iterate on selected tests with the same package preparation and network isolation, run `pnpm test test/package-contract.test.ts`.
|
|
275
|
+
|
|
173
276
|
The opt-in journey requires an exported, admitted Artium fixture with its dependencies installed.
|
|
174
|
-
The fixture harness seeds minimal pre-existing hosts directly in its disposable database and exercises
|
|
175
|
-
|
|
176
|
-
|
|
277
|
+
The fixture harness seeds minimal pre-existing hosts directly in its disposable database and exercises administration changes through public administration; no fixture-seeding route exists in the product.
|
|
278
|
+
|
|
279
|
+
```sh
|
|
280
|
+
ARTIUM_FIXTURE_ROOT=/absolute/frozen-export pnpm verify:journey -- --slice B1.1
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The journey slices are B1.1, B2.1, B2.2, B3.1, B3.2, B4.1, B4.2 and B4.3.
|
|
284
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.
|
|
285
|
+
|
|
286
|
+
## Security
|
|
287
|
+
|
|
288
|
+
No `SECURITY.md` is present yet; this is pending.
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
No `LICENSE` file is present yet; the license is pending.
|
package/dist/arguments.d.ts
CHANGED
|
@@ -4,6 +4,20 @@ export declare const commands: Record<string, {
|
|
|
4
4
|
flags?: Record<string, string>;
|
|
5
5
|
}>;
|
|
6
6
|
export declare const flagHelp = "--json, --help, --config <absolute-path>, --profile <name>";
|
|
7
|
+
export declare function commandHelp(command: string, hosted?: boolean): {
|
|
8
|
+
examples: string[];
|
|
9
|
+
defaults?: {
|
|
10
|
+
timeout: string;
|
|
11
|
+
outputLimit: string;
|
|
12
|
+
confirm: boolean;
|
|
13
|
+
detach: boolean;
|
|
14
|
+
sudo: boolean;
|
|
15
|
+
};
|
|
16
|
+
flags: string;
|
|
17
|
+
commands?: string[];
|
|
18
|
+
availability?: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
7
21
|
export declare function parseArguments(args: readonly string[]): {
|
|
8
22
|
command: string;
|
|
9
23
|
json: boolean;
|
package/dist/arguments.js
CHANGED
|
@@ -2,6 +2,25 @@ import schema from '../axi-credential.schema.json' with { type: 'json' };
|
|
|
2
2
|
import { SshError } from './errors.js';
|
|
3
3
|
export const commands = schema['x-commands'];
|
|
4
4
|
export const flagHelp = '--json, --help, --config <absolute-path>, --profile <name>';
|
|
5
|
+
export function commandHelp(command, hosted = false) {
|
|
6
|
+
const definition = commands[command];
|
|
7
|
+
const managed = command.startsWith('execution ') || command.startsWith('connection ');
|
|
8
|
+
const invocation = managed ? 'artium-axi ssh-axi' : 'ssh-axi';
|
|
9
|
+
const argument = managed && command !== 'connection open' ? ' <id>' : ' <host>';
|
|
10
|
+
const usage = command === 'exec' ? 'exec <host> --command <text> --confirm'
|
|
11
|
+
: command === 'execution cancel' ? 'execution cancel <id> --confirm'
|
|
12
|
+
: command === 'connection set' ? 'connection set <id> --idle-timeout <duration>'
|
|
13
|
+
: `${command}${definition.arguments ? argument : ''}`;
|
|
14
|
+
return {
|
|
15
|
+
description: definition.description,
|
|
16
|
+
...(managed ? { availability: 'Managed (Artium) only; use artium-axi ssh-axi.' } : {}),
|
|
17
|
+
...(command === '' ? { commands: Object.keys(commands).filter(Boolean).map(name => `${name}${name.startsWith('execution ') || name.startsWith('connection ') ? ' (managed, Artium only)' : ''}`) } : {}),
|
|
18
|
+
flags: (hosted ? '--json, --help' : flagHelp) + Object.entries(definition.flags ?? {}).map(([key, type]) => `, --${key}${type === 'boolean' ? '' : ' <value>'}`).join(''),
|
|
19
|
+
...(command === 'exec' ? { defaults: { timeout: '60s', outputLimit: '65536 bytes', confirm: false, detach: false, sudo: false } } : {}),
|
|
20
|
+
examples: command === '' ? ['ssh-axi host list', 'ssh-axi doctor <host>', 'ssh-axi config path']
|
|
21
|
+
: [`${invocation} ${usage}`, `${invocation} ${usage} --json`],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
5
24
|
export function parseArguments(args) {
|
|
6
25
|
let prefix = 0;
|
|
7
26
|
while (['--json', '--help', '--config', '--profile'].includes(args[prefix]))
|
|
@@ -51,7 +70,7 @@ export function parseArguments(args) {
|
|
|
51
70
|
const selectedDoctor = command === 'doctor' && positional.length === count;
|
|
52
71
|
const receiptLookup = command === 'execution show' && positional.length === count;
|
|
53
72
|
if (!definition || positional.length !== count + definition.arguments && !selectedDoctor && !receiptLookup && !(help && positional.length === count)) {
|
|
54
|
-
throw new SshError('USAGE', 'Use ssh-axi host list, host show <host>, doctor <host>, exec <host> --command <text> --confirm, or execution list/show/cancel.', 2);
|
|
73
|
+
throw new SshError('USAGE', 'Use ssh-axi host list, host show <host>, doctor <host>, exec <host> --command <text> --confirm, or execution list/show/cancel (managed, Artium only).', 2);
|
|
55
74
|
}
|
|
56
75
|
if (['doctor', 'host show', 'exec'].includes(command) && positional[count] !== undefined) {
|
|
57
76
|
if (profile !== undefined)
|
package/dist/authenticate.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface ExecutionResult {
|
|
|
11
11
|
outcome: 'completed' | 'failed' | 'cancelled' | 'timed_out' | 'unknown';
|
|
12
12
|
remoteExit: number | null;
|
|
13
13
|
remoteSignal: string | null;
|
|
14
|
-
termination: 'remote-exit' | 'unknown';
|
|
14
|
+
termination: 'remote-exit' | 'deadline' | 'unknown';
|
|
15
15
|
stdout: ReturnType<Output['finish']>;
|
|
16
16
|
stderr: ReturnType<Output['finish']>;
|
|
17
17
|
durationMs: number;
|
package/dist/authenticate.js
CHANGED
|
@@ -151,7 +151,7 @@ export async function openTransport(profile, signal) {
|
|
|
151
151
|
outcome = 'failed';
|
|
152
152
|
}
|
|
153
153
|
resolve({ address, outcome, remoteExit, remoteSignal,
|
|
154
|
-
termination: remoteExit !== null || remoteSignal !== null ? 'remote-exit' : 'unknown',
|
|
154
|
+
termination: remoteExit !== null || remoteSignal !== null ? outcome === 'timed_out' ? 'deadline' : 'remote-exit' : 'unknown',
|
|
155
155
|
...streams, durationMs: Math.round(performance.now() - started), ...(diagnostic ? { diagnostic } : {}) });
|
|
156
156
|
};
|
|
157
157
|
const terminate = () => {
|
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,12 @@ import { encode } from '@toon-format/toon';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
|
+
import { closeSync } from 'node:fs';
|
|
5
6
|
import { configPath, loadConfiguration, selectProfile, resolveSecret } from './profile.js';
|
|
6
7
|
import { doctor } from './doctor.js';
|
|
7
8
|
import { SshError } from './errors.js';
|
|
8
|
-
import { executeNative } from './native-execution.js';
|
|
9
|
-
import {
|
|
9
|
+
import { executeNative, openWriteJournal } from './native-execution.js';
|
|
10
|
+
import { commandHelp, parseArguments } from './arguments.js';
|
|
10
11
|
function projection(name, profile) {
|
|
11
12
|
return { id: name, address: profile.address, port: profile.port, username: profile.username };
|
|
12
13
|
}
|
|
@@ -16,16 +17,7 @@ export async function main(args) {
|
|
|
16
17
|
try {
|
|
17
18
|
const parsed = parseArguments(args);
|
|
18
19
|
if (parsed.help) {
|
|
19
|
-
|
|
20
|
-
: parsed.command === 'execution cancel' ? 'execution cancel <id> --confirm'
|
|
21
|
-
: parsed.command === 'execution show' ? 'execution show <id>'
|
|
22
|
-
: parsed.command === 'connection set' ? 'connection set <id> --idle-timeout 5m'
|
|
23
|
-
: ['connection show', 'connection close'].includes(parsed.command) ? `${parsed.command} <id>`
|
|
24
|
-
: `${parsed.command}${commands[parsed.command].arguments ? ' <host>' : ''}`;
|
|
25
|
-
output({ description: commands[parsed.command].description, flags: flagHelp + Object.keys(commands[parsed.command].flags ?? {}).map(key => `, --${key}`).join(''),
|
|
26
|
-
...(parsed.command === 'exec' ? { defaults: { timeout: '60s', outputLimit: '65536 bytes' } } : {}),
|
|
27
|
-
examples: parsed.command === '' ? ['ssh-axi host list', 'ssh-axi doctor <host>', 'ssh-axi config path']
|
|
28
|
-
: [`ssh-axi ${usage}`, `ssh-axi ${usage} --json`] });
|
|
20
|
+
output(commandHelp(parsed.command));
|
|
29
21
|
return;
|
|
30
22
|
}
|
|
31
23
|
const path = configPath(parsed.config);
|
|
@@ -64,6 +56,7 @@ export async function main(args) {
|
|
|
64
56
|
passphraseSource: source(selected.profile.passphrase, 'SSH_AXI_PASSPHRASE'), network: 'not checked' });
|
|
65
57
|
return;
|
|
66
58
|
}
|
|
59
|
+
closeSync(openWriteJournal());
|
|
67
60
|
output({ host: selected.name, ...await doctor(selected.profile), commandsExecuted: 0 });
|
|
68
61
|
}
|
|
69
62
|
catch (error) {
|
package/dist/hosted.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { encode } from '@toon-format/toon';
|
|
2
|
-
import { parseArguments } from './arguments.js';
|
|
2
|
+
import { commandHelp, parseArguments } from './arguments.js';
|
|
3
3
|
import { configPath, loadConfiguration, selectProfile } from './profile.js';
|
|
4
4
|
import { doctor, executeCommand } from './doctor.js';
|
|
5
5
|
import { SshError } from './errors.js';
|
|
@@ -19,6 +19,10 @@ process.once('message', async (message) => {
|
|
|
19
19
|
const context = message;
|
|
20
20
|
if (context.revision !== 1 || !Array.isArray(context.hosts))
|
|
21
21
|
throw new SshError('IPC_INVALID', 'Incompatible host context.');
|
|
22
|
+
if (parsed.help) {
|
|
23
|
+
output(commandHelp(parsed.command, true));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
22
26
|
if (context.execution) {
|
|
23
27
|
if (context.hosts.length !== 1)
|
|
24
28
|
throw new SshError('IPC_INVALID', 'A single selected execution host is required.');
|
|
@@ -42,10 +46,7 @@ process.once('message', async (message) => {
|
|
|
42
46
|
}
|
|
43
47
|
return;
|
|
44
48
|
}
|
|
45
|
-
if (parsed.
|
|
46
|
-
output({ description: 'Discover granted hosts and run confirmed bounded commands.', commands: ['host list', 'host show <host>', 'doctor <host>', 'exec <host> --command <text> --confirm', 'execution list', 'execution show <id>', 'execution cancel <id> --confirm'], flags: '--help, --json' });
|
|
47
|
-
}
|
|
48
|
-
else if (parsed.command === '' || parsed.command === 'host list') {
|
|
49
|
+
if (parsed.command === '' || parsed.command === 'host list') {
|
|
49
50
|
output({ hosts: context.hosts, count: context.hosts.length,
|
|
50
51
|
...(context.hosts.length ? {} : { message: 'No granted hosts found.' }), help: ['ssh-axi host show <host>', 'ssh-axi doctor <host>'] });
|
|
51
52
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Profile } from './profile.js';
|
|
2
2
|
import type { parseArguments } from './arguments.js';
|
|
3
|
+
export declare function openWriteJournal(): number;
|
|
3
4
|
export declare function executeNative(host: string, profile: Profile, parsed: ReturnType<typeof parseArguments>): Promise<{
|
|
4
5
|
finishedAt: string;
|
|
5
6
|
help: string[];
|
|
@@ -7,7 +8,7 @@ export declare function executeNative(host: string, profile: Profile, parsed: Re
|
|
|
7
8
|
outcome: "completed" | "failed" | "cancelled" | "timed_out" | "unknown";
|
|
8
9
|
remoteExit: number | null;
|
|
9
10
|
remoteSignal: string | null;
|
|
10
|
-
termination: "remote-exit" | "unknown";
|
|
11
|
+
termination: "remote-exit" | "deadline" | "unknown";
|
|
11
12
|
stdout: ReturnType<import("./output.js").Output["finish"]>;
|
|
12
13
|
stderr: ReturnType<import("./output.js").Output["finish"]>;
|
|
13
14
|
durationMs: number;
|
package/dist/native-execution.js
CHANGED
|
@@ -4,8 +4,33 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { executeCommand } from './doctor.js';
|
|
6
6
|
import { checkCommand } from './output.js';
|
|
7
|
-
import {
|
|
7
|
+
import { openPrivateFile, resolveSecret } from './profile.js';
|
|
8
8
|
import { SshError } from './errors.js';
|
|
9
|
+
export function openWriteJournal() {
|
|
10
|
+
const state = process.platform === 'win32' ? process.env.LOCALAPPDATA : process.env.XDG_STATE_HOME && isAbsolute(process.env.XDG_STATE_HOME)
|
|
11
|
+
? process.env.XDG_STATE_HOME : join(homedir(), '.local', 'state');
|
|
12
|
+
if (!state || process.platform === 'win32')
|
|
13
|
+
throw new SshError('PLATFORM_PENDING', 'Native Windows custody validation is pending.');
|
|
14
|
+
const file = process.env.SSH_AXI_WRITE_LOG ?? join(state, 'ssh-axi', 'writes.log');
|
|
15
|
+
if (!isAbsolute(file))
|
|
16
|
+
throw new SshError('CONFIG_INSECURE', 'Write journal path must be absolute.');
|
|
17
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
18
|
+
// Reuse the private-file ownership/parent checks before appending.
|
|
19
|
+
const descriptor = openSync(file, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600);
|
|
20
|
+
try {
|
|
21
|
+
closeSync(openPrivateFile(file, true));
|
|
22
|
+
if (!fstatSync(descriptor).isFile())
|
|
23
|
+
throw new SshError('CONFIG_INSECURE', 'Journal must be a private regular file.');
|
|
24
|
+
return descriptor;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
closeSync(descriptor);
|
|
28
|
+
if (error instanceof SshError && error.code === 'CONFIG_INSECURE') {
|
|
29
|
+
throw new SshError(error.code, `${error.message} Set SSH_AXI_WRITE_LOG to an absolute journal path under protected directories.`);
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
9
34
|
export async function executeNative(host, profile, parsed) {
|
|
10
35
|
if (parsed.values.confirm !== true)
|
|
11
36
|
throw new SshError('CONFIRM_REQUIRED', 'Every arbitrary command requires --confirm.', 2);
|
|
@@ -17,22 +42,10 @@ export async function executeNative(host, profile, parsed) {
|
|
|
17
42
|
throw new SshError('USAGE', 'Use --command <text> --confirm.', 2);
|
|
18
43
|
const command = parsed.values.command;
|
|
19
44
|
checkCommand(command, [resolveSecret(profile.privateKey, 'SSH_AXI_PRIVATE_KEY', true), resolveSecret(profile.passphrase, 'SSH_AXI_PASSPHRASE', false) ?? '']);
|
|
20
|
-
const
|
|
21
|
-
? process.env.XDG_STATE_HOME : join(homedir(), '.local', 'state');
|
|
22
|
-
if (!state || process.platform === 'win32')
|
|
23
|
-
throw new SshError('PLATFORM_PENDING', 'Native Windows custody validation is pending.');
|
|
24
|
-
const file = process.env.SSH_AXI_WRITE_LOG ?? join(state, 'ssh-axi', 'writes.log');
|
|
25
|
-
if (!isAbsolute(file))
|
|
26
|
-
throw new SshError('CONFIG_INSECURE', 'Write journal path must be absolute.');
|
|
27
|
-
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
28
|
-
// Reuse the private-file ownership/parent checks before appending.
|
|
29
|
-
const descriptor = openSync(file, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600);
|
|
45
|
+
const descriptor = openWriteJournal();
|
|
30
46
|
const id = `exec_${randomUUID()}`;
|
|
31
47
|
const startedAt = new Date().toISOString();
|
|
32
48
|
try {
|
|
33
|
-
readPrivateFile(file, true);
|
|
34
|
-
if (!fstatSync(descriptor).isFile())
|
|
35
|
-
throw new SshError('CONFIG_INSECURE', 'Journal must be a private regular file.');
|
|
36
49
|
writeFileSync(descriptor, JSON.stringify({ id, host, command, confirmed: true, startedAt, outcome: 'unknown' }) + '\n');
|
|
37
50
|
fsyncSync(descriptor);
|
|
38
51
|
}
|
package/dist/profile.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface Configuration {
|
|
|
19
19
|
}
|
|
20
20
|
export declare function validateProfile(value: unknown): Profile;
|
|
21
21
|
export declare function configPath(explicit?: string): string;
|
|
22
|
+
export declare function openPrivateFile(file: string, secret: boolean): number;
|
|
22
23
|
export declare function readPrivateFile(file: string, secret: boolean): string;
|
|
23
24
|
export declare function loadConfiguration(file: string): Configuration;
|
|
24
25
|
export declare function selectProfile(config: Configuration, explicit?: string): {
|
package/dist/profile.js
CHANGED
|
@@ -23,7 +23,7 @@ export function configPath(explicit) {
|
|
|
23
23
|
throw new SshError('CONFIG_INVALID', 'An absolute configuration directory is required.');
|
|
24
24
|
return join(base, 'ssh-axi', 'config.json');
|
|
25
25
|
}
|
|
26
|
-
export function
|
|
26
|
+
export function openPrivateFile(file, secret) {
|
|
27
27
|
if (!isAbsolute(file))
|
|
28
28
|
throw new SshError('CONFIG_INSECURE', 'Private file paths must be absolute.');
|
|
29
29
|
// Until native DACL verification is available, Windows custody fails closed.
|
|
@@ -34,7 +34,7 @@ export function readPrivateFile(file, secret) {
|
|
|
34
34
|
const info = lstatSync(parent);
|
|
35
35
|
if (!info.isDirectory() || info.isSymbolicLink() || (info.uid !== process.getuid() && info.uid !== 0)
|
|
36
36
|
|| ((info.mode & 0o022) !== 0 && !(info.uid === 0 && (info.mode & 0o1000) !== 0))) {
|
|
37
|
-
throw new SshError('CONFIG_INSECURE',
|
|
37
|
+
throw new SshError('CONFIG_INSECURE', `Private file parent ${JSON.stringify(parent)} must be an actual directory owned by this user or root and protected against replacement. Remove group write access with chmod g-w (and other write access with chmod o-w), or select a protected location.`);
|
|
38
38
|
}
|
|
39
39
|
parent = dirname(parent);
|
|
40
40
|
}
|
|
@@ -44,6 +44,16 @@ export function readPrivateFile(file, secret) {
|
|
|
44
44
|
if (!info.isFile() || info.uid !== process.getuid() || (info.mode & (secret ? 0o077 : 0o022)) !== 0) {
|
|
45
45
|
throw new SshError('CONFIG_INSECURE', 'Private files must be regular, owned by this user and protected.');
|
|
46
46
|
}
|
|
47
|
+
return descriptor;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
closeSync(descriptor);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function readPrivateFile(file, secret) {
|
|
55
|
+
const descriptor = openPrivateFile(file, secret);
|
|
56
|
+
try {
|
|
47
57
|
return readFileSync(descriptor, 'utf8').replace(/^\uFEFF/, '').trimEnd();
|
|
48
58
|
}
|
|
49
59
|
finally {
|
package/package.json
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowttl/ssh-axi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Discover approved SSH hosts and execute confirmed bounded commands",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/knowttl/ssh-axi.git"
|
|
8
|
+
},
|
|
5
9
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
+
"bin": {
|
|
11
|
+
"ssh-axi": "dist/main.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"axi-credential.schema.json",
|
|
16
|
+
"artium-invocations.json",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=24.11.0"
|
|
24
|
+
},
|
|
10
25
|
"packageManager": "pnpm@10.22.0",
|
|
11
26
|
"scripts": {
|
|
12
27
|
"build": "tsc",
|