ysagc-agent 0.8.5 → 0.9.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 +266 -266
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +3 -3
- package/dist/agent.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +27 -1
- package/dist/cli.js.map +1 -1
- package/dist/executor.d.ts +2 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +41 -28
- package/dist/executor.js.map +1 -1
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/tools.d.ts +6 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +54 -20
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,266 +1,266 @@
|
|
|
1
|
-
# ysagc-agent
|
|
2
|
-
|
|
3
|
-
The **local execution agent** for ysagc. It runs on a user's own computer, opens a
|
|
4
|
-
WebSocket to the platform backend, receives tool instructions, executes them
|
|
5
|
-
**safely inside an authorization root**, and reports results back.
|
|
6
|
-
|
|
7
|
-
`ysagc-agent` is deliberately **state-agnostic**: it never stores balances, never
|
|
8
|
-
makes billing decisions, and never holds platform secrets. It is a dumb, safe,
|
|
9
|
-
well-scoped executor — all policy lives on the platform.
|
|
10
|
-
|
|
11
|
-
## Highlights
|
|
12
|
-
|
|
13
|
-
- **Zero-config startup** — a bare `ysagc-agent` (or `npx ysagc-agent`) starts
|
|
14
|
-
immediately with the default platform, a generated local device identity, `cwd`
|
|
15
|
-
as the authorization root, and connects — **no prompts, no wizard**.
|
|
16
|
-
- **Auto-reconnect until connected** — if the first connection can't be
|
|
17
|
-
established it prints a single notice and then **retries every `--retry-interval`
|
|
18
|
-
(default 1000ms)** until it reaches the platform; it never exits and never
|
|
19
|
-
cascades into a prompt.
|
|
20
|
-
- **Opt-in `--configure` wizard** — run `ysagc-agent --configure` to
|
|
21
|
-
interactively set the address / token / root.
|
|
22
|
-
- **No baked-in secrets** — the default server is the customer platform; the
|
|
23
|
-
device token is self-generated on a zero-config run, never logged, never
|
|
24
|
-
committed anywhere, and never a real platform secret.
|
|
25
|
-
- **Authorization root** — every file operation is forced inside one directory tree.
|
|
26
|
-
- **Danger-command guard** — destructive shell commands are detected and blocked.
|
|
27
|
-
- **Sensitive-path guard** — credential/system paths are blocked regardless of mode.
|
|
28
|
-
- **Secret masking** — secrets are redacted from logs and tool results.
|
|
29
|
-
- **Idempotency** — repeated `tool_call`s with the same key return the cached result
|
|
30
|
-
and never double-execute or double-write. `needsApproval` results are **not**
|
|
31
|
-
cached, so an approved re-send re-runs the job.
|
|
32
|
-
- **Rich tool set** — `search` (grep), `glob`, `tree`, `str_replace_editor`,
|
|
33
|
-
`mv`/`cp`, `delete_file`, `json` pretty-print, and segmented `read_file`
|
|
34
|
-
(`offset`/`length`) alongside the core file + shell tools.
|
|
35
|
-
- **Heartbeat & auto-reconnect** —
|
|
36
|
-
- **Cancel support** — in-flight commands can be aborted by the platform.
|
|
37
|
-
- **Strict TypeScript** — compiled with `tsc` to CommonJS, zero `any` leaks.
|
|
38
|
-
- **Cross-platform** — bash, PowerShell and cmd command detection.
|
|
39
|
-
|
|
40
|
-
## Requirements
|
|
41
|
-
|
|
42
|
-
- Node.js **>= 18**
|
|
43
|
-
|
|
44
|
-
## Quick start
|
|
45
|
-
|
|
46
|
-
### One-command startup (zero config)
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
npm install -g ysagc-agent
|
|
50
|
-
ysagc-agent
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
That's it. With no saved config the agent **silently builds defaults and
|
|
54
|
-
connects immediately** — no wizard, no prompts:
|
|
55
|
-
|
|
56
|
-
- **server** — defaults to `ws://interface.ysagi.cn/api/workbench/agent`, **but with
|
|
57
|
-
local/online auto-detection**: when the default platform URL is in use, the agent
|
|
58
|
-
first tries the **local dev backend** `ws://localhost:5000/api/workbench/agent`,
|
|
59
|
-
and falls through to the default if it's unreachable. So a bare `ysagc-agent`
|
|
60
|
-
works whether you're testing locally or connecting to the platform. If you pass an
|
|
61
|
-
explicit `--server`/`WS_URL`, that one is used exclusively.
|
|
62
|
-
- **deviceId** — generated as `agent-<random>-<n>`
|
|
63
|
-
- **token** — a self-generated local identity (never a real platform secret)
|
|
64
|
-
- **root** — the current working directory (`process.cwd()`)
|
|
65
|
-
|
|
66
|
-
The resolved defaults are saved to `~/.ysagc-agent/config.json` (mode `0600`,
|
|
67
|
-
directory created if missing) so every later run connects the same way. A bare run
|
|
68
|
-
logs its config **without the token** (`describeConfig` omits it).
|
|
69
|
-
|
|
70
|
-
Referenced by `WS_URL`, `YSAGC_TOKEN`, `DEVICE_ID`/`WORKSPACE_ROOT` environment
|
|
71
|
-
variables and the `--server/--token/--device-id/--root` flags still win over the
|
|
72
|
-
defaults, and a silent-save never overrides them.
|
|
73
|
-
|
|
74
|
-
### Configure the address / token / root (opt-in)
|
|
75
|
-
|
|
76
|
-
The interactive wizard is **off by default**. Run it explicitly once to set the
|
|
77
|
-
platform address, device token and workspace root, which are then persisted and
|
|
78
|
-
used on every later run:
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
ysagc-agent --configure
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
It asks, in order:
|
|
85
|
-
|
|
86
|
-
1. **平台地址 / server URL** — the default is `ws://interface.ysagi.cn/api/workbench/agent`; press **Enter** to accept, or type another (`http`/`https` are accepted and converted to `ws`/`wss`).
|
|
87
|
-
2. **设备 Token** — paste the device token copied from the workbench's「如何连接」. Input is **masked** (not echoed). Leave it empty to generate a local device id and a short pairing hint instead.
|
|
88
|
-
3. **工作区目录 / workspace root** — defaults to the current directory; press **Enter** to accept or type an absolute path.
|
|
89
|
-
|
|
90
|
-
Any value already supplied by flags/env is used as-is and its prompt is skipped.
|
|
91
|
-
|
|
92
|
-
> **Non-interactive `--configure`:** `--configure` requires a real terminal. In a
|
|
93
|
-
> piped/non-TTY run it prints `--configure 需要一个交互式终端来运行。` and exits `1`
|
|
94
|
-
> instead of hanging.
|
|
95
|
-
|
|
96
|
-
`npx ysagc-agent` is the same entry and needs no flags either.
|
|
97
|
-
|
|
98
|
-
> **First-run connection failure:** if a zero-config run cannot connect or is
|
|
99
|
-
> rejected, it prints one clear line —
|
|
100
|
-
> `暂未连接平台(<err>),正在每 1s 自动重试,直到连上为止…(Ctrl+C 退出)`
|
|
101
|
-
> — then **retries every `--retry-interval` (default 1000ms) until it connects**.
|
|
102
|
-
> It does **not** prompt and does **not** exit.
|
|
103
|
-
|
|
104
|
-
Once connected the agent sends a `hello` frame, waits for `welcome`, then answers
|
|
105
|
-
`ping`/`tool_call`/`cancel` frames.
|
|
106
|
-
|
|
107
|
-
### Or run it from this checkout
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
npm install
|
|
111
|
-
npm run build # tsc -> dist/
|
|
112
|
-
node dist/cli.js --selftest # run the smoke test
|
|
113
|
-
node dist/cli.js # same zero-config auto-connect
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## Configuration
|
|
117
|
-
|
|
118
|
-
Settings are resolved in this precedence order:
|
|
119
|
-
|
|
120
|
-
**CLI flags > environment variables > saved config (`~/.ysagc-agent/config.json`) > defaults.**
|
|
121
|
-
|
|
122
|
-
The device token is **never logged** — `describeConfig` omits it and the logger
|
|
123
|
-
masks secret-looking values before they reach stdout/stderr.
|
|
124
|
-
|
|
125
|
-
| CLI flag | Environment | Default | Description |
|
|
126
|
-
| --------------------- | ------------------ | ----------------- | ---------------------------------------------------- |
|
|
127
|
-
| `--server <url>` | `WS_URL` | `ws://interface.ysagi.cn/api/workbench/agent` | Platform WebSocket URL (http/https auto-converted to ws/wss) |
|
|
128
|
-
| `--token <token>` | `YSAGC_TOKEN` | _(saved config)_ | Device/agent auth token; **never** defaulted/baked-in |
|
|
129
|
-
| `--device-id <id>` | `DEVICE_ID` | host+pid / generated | Stable device identifier |
|
|
130
|
-
| `--root <dir>` | `WORKSPACE_ROOT` | cwd | Authorization root (all file ops stay inside it) |
|
|
131
|
-
| `--mode <mode>` | `PERMISSION_MODE` | `smart` | Permission mode (see below) |
|
|
132
|
-
| `--web` | `YSAGC_WEB` | off | Enable the local web_search (stub) tool |
|
|
133
|
-
| `--timeout <ms>` | `YSAGC_TIMEOUT` | `120000` | Command timeout in milliseconds |
|
|
134
|
-
| `--max-bytes <n>` | `YSAGC_MAX_BYTES` | `64000` | Per-stream output cap before truncation |
|
|
135
|
-
| `--log-level <level>` | `YSAGC_LOG_LEVEL` | `info` | `silent\|error\|warn\|info\|debug` |
|
|
136
|
-
| `--retry-interval <ms>` | `YSAGC_RETRY_MS` | `1000` | Connection retry interval (retries until connected) |
|
|
137
|
-
| `--configure` | — | — | Run the interactive setup wizard and save the config (opt-in) |
|
|
138
|
-
| `--selftest` | — | — | Run the local smoke test and exit |
|
|
139
|
-
| `-V, --version` | — | — | Print version |
|
|
140
|
-
| `-h, --help` | — | — | Print usage |
|
|
141
|
-
|
|
142
|
-
### Saved config file
|
|
143
|
-
|
|
144
|
-
A zero-config run — or an explicit `ysagc-agent --configure` — persists the
|
|
145
|
-
resolved settings to `~/.ysagc-agent/config.json` (mode `0600`, directory created
|
|
146
|
-
if missing). Override the path with the `YSAGC_CONFIG` environment variable. A
|
|
147
|
-
missing or corrupt file is treated as a first run: the agent silently starts with
|
|
148
|
-
defaults again (it never prompts unless `--configure` is passed).
|
|
149
|
-
|
|
150
|
-
### Permission modes
|
|
151
|
-
|
|
152
|
-
| Mode | Shell behaviour |
|
|
153
|
-
| ---------- | ------------------------------------------------------------------------- |
|
|
154
|
-
| `supervise`| Every `run_command`/`git` is **denied** (requires human approval) |
|
|
155
|
-
| `smart` | Safe commands are allowed; dangerous ones are blocked (default) |
|
|
156
|
-
| `auto` | All non-dangerous commands are allowed |
|
|
157
|
-
|
|
158
|
-
Dangerous commands and sensitive paths are blocked **in every mode**.
|
|
159
|
-
|
|
160
|
-
## Tools
|
|
161
|
-
|
|
162
|
-
| Tool | Args | Returns |
|
|
163
|
-
| --------------------- | ------------------------------------ | --------------------------------------- |
|
|
164
|
-
| `list_dir` | `path`, `depth` | entries (`name`, `type`, `size`, `mtime`) |
|
|
165
|
-
| `fs_drives` | — | machine drive roots (Windows letters / `/`) |
|
|
166
|
-
| `fs_browse` | `path` | **whole-machine read-only** directory browse (any dir, sensitive paths blocked) — used for choosing a workspace |
|
|
167
|
-
| `fs_pick_folder` | — | opens the **OS-native "select folder" dialog** and returns the chosen absolute path |
|
|
168
|
-
| `read_file` | `path`, `offset?`, `length?`/`maxBytes?` | text or hex, truncated, size/offset info |
|
|
169
|
-
| `write_file` | `path`, `content`, `mode?` | `path`, `bytesWritten` |
|
|
170
|
-
| `edit_file` | `path`, `oldText`, `newText` | `path`, `replacements` |
|
|
171
|
-
| `str_replace_editor` | `command`, `path`, `…` | editor ops: `create`, `view`, `str_replace`, `insert`, `delete` |
|
|
172
|
-
| `search` | `pattern`, `path?`, `recursive?`, `caseSensitive?`, `maxResults?` | file/line matches (bounded) |
|
|
173
|
-
| `glob` | `pattern`, `path?`, `maxResults?` | matching paths (bounded) |
|
|
174
|
-
| `tree` | `path?`, `depth?`, `maxEntries?` | nested + rendered tree (bounded) |
|
|
175
|
-
| `run_command` | `cmd`, `timeout?`, `cwd?` | `exitCode`, `stdout`, `stderr`, flags |
|
|
176
|
-
| `web_search` | `query` | labelled **stub** result (real search is platform-side) |
|
|
177
|
-
| `git` | `args` (array) or `command` (string) | git output, `exitCode`, flags |
|
|
178
|
-
| `generate` | `template`, `name` | file list it created |
|
|
179
|
-
| `mv` | `source`, `destination`, `overwrite?` | moved path pair (rename if same dir) |
|
|
180
|
-
| `cp` | `source`, `destination`, `overwrite?` | copied path pair |
|
|
181
|
-
| `delete_file` | `path`, `recursive?` | deleted path (requires approval) |
|
|
182
|
-
| `json` | `value` or `path`, `indent?`, `sortKeys?` | pretty-printed JSON |
|
|
183
|
-
| `skill` | `name?` | loaded skill / available list |
|
|
184
|
-
|
|
185
|
-
Every tool canonicalizes its path, asserts it is inside the authorization root,
|
|
186
|
-
rejects outside/sensitive paths, and never leaks platform internals or keys.
|
|
187
|
-
`mv`/`cp`/`delete_file` and `str_replace_editor` is a single operator;
|
|
188
|
-
`mv` with no `overwrite` refuses to clobber an existing destination (and asks
|
|
189
|
-
for approval when overwriting), and `delete_file` always requires approval in
|
|
190
|
-
`workspace-write` mode.
|
|
191
|
-
|
|
192
|
-
## Security model
|
|
193
|
-
|
|
194
|
-
1. **Authorization root** — `canonicalize()` resolves `..`, symlinks, the
|
|
195
|
-
`\\?\` prefix, trailing dots/spaces and Windows reserved names, then
|
|
196
|
-
`isWithinRoot()` strictly checks the result is under the root. Writes equal
|
|
197
|
-
to the root are rejected; `..` and symlink escapes are rejected too.
|
|
198
|
-
2. **Danger-command detection** — `isDangerCommand()` flags `rm -rf`, `sudo`,
|
|
199
|
-
`format <drive>`, `del /f`, `rd /s /q`, `shutdown`, `reboot`,
|
|
200
|
-
`git reset --hard`, `git clean -fdx`, `find ... -delete`,
|
|
201
|
-
`Remove-Item -Recurse -Force`, `diskpart`, `reg delete`/`reg add`,
|
|
202
|
-
`powershell -enc`, `iex`/`Invoke-Expression`, `certutil -decode`,
|
|
203
|
-
`regsvr32`/`rundll32`/`mshta`/`wmic`/`bitsadmin`, `curl|wget -> sh`,
|
|
204
|
-
`sh/bash -c`, `sc create`, `net user`, `schtasks /create`,
|
|
205
|
-
`icacls`/`cacls`, `takeown`, `vssadmin`, `bcdedit`, `cipher /w`,
|
|
206
|
-
`taskkill /f`, `chmod -R 777 /`, `mkfs`, `dd of=/dev/...`,
|
|
207
|
-
redirects to block devices, fork bombs and more. Cross-platform
|
|
208
|
-
(bash / PowerShell / cmd).
|
|
209
|
-
3. **Sensitive-path detection** — `isSensitivePath()` blocks system and
|
|
210
|
-
credential paths (`/etc/passwd`, `C:\Windows\System32`, `~/.ssh`, `.env`
|
|
211
|
-
with keys, `id_rsa`, `.npmrc` with `_authToken`, and so on) regardless of mode.
|
|
212
|
-
4. **Secret masking** — `maskSecrets()` redacts provider keys, bearer tokens,
|
|
213
|
-
JWTs, private keys and `key=value` secrets from logs and tool output.
|
|
214
|
-
5. **Idempotency** — results are cached by a hash of `(tool, args, idempotencyKey)`;
|
|
215
|
-
replayed jobs return the cached result without re-executing. Request-for-approval
|
|
216
|
-
(`needsApproval`) results are deliberately **not** cached so the platform can
|
|
217
|
-
prompt a human and then re-send the same key to actually run it.
|
|
218
|
-
6. **Environment sanitization** — child processes never inherit the agent's own
|
|
219
|
-
token or credential variables.
|
|
220
|
-
|
|
221
|
-
## Self-test
|
|
222
|
-
|
|
223
|
-
```bash
|
|
224
|
-
node dist/cli.js --selftest
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
Runs 53 checks over path safety, danger-command detection, sensitive-path
|
|
228
|
-
detection, secret masking, the tool registry, command timeout/process-tree kill,
|
|
229
|
-
environment sanitization, config resolution + save/load (never logging the token),
|
|
230
|
-
and idempotency over a real WebSocket connection. It prints `PASS`/`FAIL` lines
|
|
231
|
-
and exits `0`/`1`.
|
|
232
|
-
|
|
233
|
-
## Protocol
|
|
234
|
-
|
|
235
|
-
JSON text frames over a WebSocket.
|
|
236
|
-
|
|
237
|
-
**Client → server**
|
|
238
|
-
|
|
239
|
-
```jsonc
|
|
240
|
-
{ "type": "hello", "token": "...", "deviceId": "...", "version": "0.
|
|
241
|
-
{ "type": "pong", "t": 42 } // reply to a server ping
|
|
242
|
-
{ "type": "tool_result", "jobId": "j1", "ok": true, "result": { }, "durationMs": 12 }
|
|
243
|
-
{ "type": "status", "state": "online" | "busy" | "error", "sessionId": "..." }
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
**Server → client**
|
|
247
|
-
|
|
248
|
-
```jsonc
|
|
249
|
-
{ "type": "welcome", "sessionId": "..." }
|
|
250
|
-
{ "type": "ping", "t": 42 }
|
|
251
|
-
{ "type": "tool_call", "jobId": "j1", "idempotencyKey": "k1", "tool": "write_file", "args": { } }
|
|
252
|
-
{ "type": "cancel", "jobId": "j1" }
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
## Development
|
|
256
|
-
|
|
257
|
-
```bash
|
|
258
|
-
npm install
|
|
259
|
-
npm run build # tsc -> dist/
|
|
260
|
-
npm run test # node dist/cli.js --selftest
|
|
261
|
-
npm start # node dist/cli.js
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
## License
|
|
265
|
-
|
|
266
|
-
MIT
|
|
1
|
+
# ysagc-agent
|
|
2
|
+
|
|
3
|
+
The **local execution agent** for ysagc. It runs on a user's own computer, opens a
|
|
4
|
+
WebSocket to the platform backend, receives tool instructions, executes them
|
|
5
|
+
**safely inside an authorization root**, and reports results back.
|
|
6
|
+
|
|
7
|
+
`ysagc-agent` is deliberately **state-agnostic**: it never stores balances, never
|
|
8
|
+
makes billing decisions, and never holds platform secrets. It is a dumb, safe,
|
|
9
|
+
well-scoped executor — all policy lives on the platform.
|
|
10
|
+
|
|
11
|
+
## Highlights
|
|
12
|
+
|
|
13
|
+
- **Zero-config startup** — a bare `ysagc-agent` (or `npx ysagc-agent`) starts
|
|
14
|
+
immediately with the default platform, a generated local device identity, `cwd`
|
|
15
|
+
as the authorization root, and connects — **no prompts, no wizard**.
|
|
16
|
+
- **Auto-reconnect until connected** — if the first connection can't be
|
|
17
|
+
established it prints a single notice and then **retries every `--retry-interval`
|
|
18
|
+
(default 1000ms)** until it reaches the platform; it never exits and never
|
|
19
|
+
cascades into a prompt.
|
|
20
|
+
- **Opt-in `--configure` wizard** — run `ysagc-agent --configure` to
|
|
21
|
+
interactively set the address / token / root.
|
|
22
|
+
- **No baked-in secrets** — the default server is the customer platform; the
|
|
23
|
+
device token is self-generated on a zero-config run, never logged, never
|
|
24
|
+
committed anywhere, and never a real platform secret.
|
|
25
|
+
- **Authorization root** — every file operation is forced inside one directory tree.
|
|
26
|
+
- **Danger-command guard** — destructive shell commands are detected and blocked.
|
|
27
|
+
- **Sensitive-path guard** — credential/system paths are blocked regardless of mode.
|
|
28
|
+
- **Secret masking** — secrets are redacted from logs and tool results.
|
|
29
|
+
- **Idempotency** — repeated `tool_call`s with the same key return the cached result
|
|
30
|
+
and never double-execute or double-write. `needsApproval` results are **not**
|
|
31
|
+
cached, so an approved re-send re-runs the job.
|
|
32
|
+
- **Rich tool set** — `search` (grep), `glob`, `tree`, `str_replace_editor`,
|
|
33
|
+
`mv`/`cp`, `delete_file`, `json` pretty-print, and segmented `read_file`
|
|
34
|
+
(`offset`/`length`) alongside the core file + shell tools.
|
|
35
|
+
- **Heartbeat & auto-reconnect** — fixed-interval reconnect until connected.
|
|
36
|
+
- **Cancel support** — in-flight commands can be aborted by the platform.
|
|
37
|
+
- **Strict TypeScript** — compiled with `tsc` to CommonJS, zero `any` leaks.
|
|
38
|
+
- **Cross-platform** — bash, PowerShell and cmd command detection.
|
|
39
|
+
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- Node.js **>= 18**
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
### One-command startup (zero config)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install -g ysagc-agent
|
|
50
|
+
ysagc-agent
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
That's it. With no saved config the agent **silently builds defaults and
|
|
54
|
+
connects immediately** — no wizard, no prompts:
|
|
55
|
+
|
|
56
|
+
- **server** — defaults to `ws://interface.ysagi.cn/api/workbench/agent`, **but with
|
|
57
|
+
local/online auto-detection**: when the default platform URL is in use, the agent
|
|
58
|
+
first tries the **local dev backend** `ws://localhost:5000/api/workbench/agent`,
|
|
59
|
+
and falls through to the default if it's unreachable. So a bare `ysagc-agent`
|
|
60
|
+
works whether you're testing locally or connecting to the platform. If you pass an
|
|
61
|
+
explicit `--server`/`WS_URL`, that one is used exclusively.
|
|
62
|
+
- **deviceId** — generated as `agent-<random>-<n>`
|
|
63
|
+
- **token** — a self-generated local identity (never a real platform secret)
|
|
64
|
+
- **root** — the current working directory (`process.cwd()`)
|
|
65
|
+
|
|
66
|
+
The resolved defaults are saved to `~/.ysagc-agent/config.json` (mode `0600`,
|
|
67
|
+
directory created if missing) so every later run connects the same way. A bare run
|
|
68
|
+
logs its config **without the token** (`describeConfig` omits it).
|
|
69
|
+
|
|
70
|
+
Referenced by `WS_URL`, `YSAGC_TOKEN`, `DEVICE_ID`/`WORKSPACE_ROOT` environment
|
|
71
|
+
variables and the `--server/--token/--device-id/--root` flags still win over the
|
|
72
|
+
defaults, and a silent-save never overrides them.
|
|
73
|
+
|
|
74
|
+
### Configure the address / token / root (opt-in)
|
|
75
|
+
|
|
76
|
+
The interactive wizard is **off by default**. Run it explicitly once to set the
|
|
77
|
+
platform address, device token and workspace root, which are then persisted and
|
|
78
|
+
used on every later run:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
ysagc-agent --configure
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
It asks, in order:
|
|
85
|
+
|
|
86
|
+
1. **平台地址 / server URL** — the default is `ws://interface.ysagi.cn/api/workbench/agent`; press **Enter** to accept, or type another (`http`/`https` are accepted and converted to `ws`/`wss`).
|
|
87
|
+
2. **设备 Token** — paste the device token copied from the workbench's「如何连接」. Input is **masked** (not echoed). Leave it empty to generate a local device id and a short pairing hint instead.
|
|
88
|
+
3. **工作区目录 / workspace root** — defaults to the current directory; press **Enter** to accept or type an absolute path.
|
|
89
|
+
|
|
90
|
+
Any value already supplied by flags/env is used as-is and its prompt is skipped.
|
|
91
|
+
|
|
92
|
+
> **Non-interactive `--configure`:** `--configure` requires a real terminal. In a
|
|
93
|
+
> piped/non-TTY run it prints `--configure 需要一个交互式终端来运行。` and exits `1`
|
|
94
|
+
> instead of hanging.
|
|
95
|
+
|
|
96
|
+
`npx ysagc-agent` is the same entry and needs no flags either.
|
|
97
|
+
|
|
98
|
+
> **First-run connection failure:** if a zero-config run cannot connect or is
|
|
99
|
+
> rejected, it prints one clear line —
|
|
100
|
+
> `暂未连接平台(<err>),正在每 1s 自动重试,直到连上为止…(Ctrl+C 退出)`
|
|
101
|
+
> — then **retries every `--retry-interval` (default 1000ms) until it connects**.
|
|
102
|
+
> It does **not** prompt and does **not** exit.
|
|
103
|
+
|
|
104
|
+
Once connected the agent sends a `hello` frame, waits for `welcome`, then answers
|
|
105
|
+
`ping`/`tool_call`/`cancel` frames.
|
|
106
|
+
|
|
107
|
+
### Or run it from this checkout
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm install
|
|
111
|
+
npm run build # tsc -> dist/
|
|
112
|
+
node dist/cli.js --selftest # run the smoke test
|
|
113
|
+
node dist/cli.js # same zero-config auto-connect
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
Settings are resolved in this precedence order:
|
|
119
|
+
|
|
120
|
+
**CLI flags > environment variables > saved config (`~/.ysagc-agent/config.json`) > defaults.**
|
|
121
|
+
|
|
122
|
+
The device token is **never logged** — `describeConfig` omits it and the logger
|
|
123
|
+
masks secret-looking values before they reach stdout/stderr.
|
|
124
|
+
|
|
125
|
+
| CLI flag | Environment | Default | Description |
|
|
126
|
+
| --------------------- | ------------------ | ----------------- | ---------------------------------------------------- |
|
|
127
|
+
| `--server <url>` | `WS_URL` | `ws://interface.ysagi.cn/api/workbench/agent` | Platform WebSocket URL (http/https auto-converted to ws/wss) |
|
|
128
|
+
| `--token <token>` | `YSAGC_TOKEN` | _(saved config)_ | Device/agent auth token; **never** defaulted/baked-in |
|
|
129
|
+
| `--device-id <id>` | `DEVICE_ID` | host+pid / generated | Stable device identifier |
|
|
130
|
+
| `--root <dir>` | `WORKSPACE_ROOT` | cwd | Authorization root (all file ops stay inside it) |
|
|
131
|
+
| `--mode <mode>` | `PERMISSION_MODE` | `smart` | Permission mode (see below) |
|
|
132
|
+
| `--web` | `YSAGC_WEB` | off | Enable the local web_search (stub) tool |
|
|
133
|
+
| `--timeout <ms>` | `YSAGC_TIMEOUT` | `120000` | Command timeout in milliseconds |
|
|
134
|
+
| `--max-bytes <n>` | `YSAGC_MAX_BYTES` | `64000` | Per-stream output cap before truncation |
|
|
135
|
+
| `--log-level <level>` | `YSAGC_LOG_LEVEL` | `info` | `silent\|error\|warn\|info\|debug` |
|
|
136
|
+
| `--retry-interval <ms>` | `YSAGC_RETRY_MS` | `1000` | Connection retry interval (retries until connected) |
|
|
137
|
+
| `--configure` | — | — | Run the interactive setup wizard and save the config (opt-in) |
|
|
138
|
+
| `--selftest` | — | — | Run the local smoke test and exit |
|
|
139
|
+
| `-V, --version` | — | — | Print version |
|
|
140
|
+
| `-h, --help` | — | — | Print usage |
|
|
141
|
+
|
|
142
|
+
### Saved config file
|
|
143
|
+
|
|
144
|
+
A zero-config run — or an explicit `ysagc-agent --configure` — persists the
|
|
145
|
+
resolved settings to `~/.ysagc-agent/config.json` (mode `0600`, directory created
|
|
146
|
+
if missing). Override the path with the `YSAGC_CONFIG` environment variable. A
|
|
147
|
+
missing or corrupt file is treated as a first run: the agent silently starts with
|
|
148
|
+
defaults again (it never prompts unless `--configure` is passed).
|
|
149
|
+
|
|
150
|
+
### Permission modes
|
|
151
|
+
|
|
152
|
+
| Mode | Shell behaviour |
|
|
153
|
+
| ---------- | ------------------------------------------------------------------------- |
|
|
154
|
+
| `supervise`| Every `run_command`/`git` is **denied** (requires human approval) |
|
|
155
|
+
| `smart` | Safe commands are allowed; dangerous ones are blocked (default) |
|
|
156
|
+
| `auto` | All non-dangerous commands are allowed |
|
|
157
|
+
|
|
158
|
+
Dangerous commands and sensitive paths are blocked **in every mode**.
|
|
159
|
+
|
|
160
|
+
## Tools
|
|
161
|
+
|
|
162
|
+
| Tool | Args | Returns |
|
|
163
|
+
| --------------------- | ------------------------------------ | --------------------------------------- |
|
|
164
|
+
| `list_dir` | `path`, `depth` | entries (`name`, `type`, `size`, `mtime`) |
|
|
165
|
+
| `fs_drives` | — | machine drive roots (Windows letters / `/`) |
|
|
166
|
+
| `fs_browse` | `path` | **whole-machine read-only** directory browse (any dir, sensitive paths blocked) — used for choosing a workspace |
|
|
167
|
+
| `fs_pick_folder` | — | opens the **OS-native "select folder" dialog** and returns the chosen absolute path |
|
|
168
|
+
| `read_file` | `path`, `offset?`, `length?`/`maxBytes?` | text or hex, truncated, size/offset info |
|
|
169
|
+
| `write_file` | `path`, `content`, `mode?` | `path`, `bytesWritten` |
|
|
170
|
+
| `edit_file` | `path`, `oldText`, `newText` | `path`, `replacements` |
|
|
171
|
+
| `str_replace_editor` | `command`, `path`, `…` | editor ops: `create`, `view`, `str_replace`, `insert`, `delete` |
|
|
172
|
+
| `search` | `pattern`, `path?`, `recursive?`, `caseSensitive?`, `maxResults?` | file/line matches (bounded) |
|
|
173
|
+
| `glob` | `pattern`, `path?`, `maxResults?` | matching paths (bounded) |
|
|
174
|
+
| `tree` | `path?`, `depth?`, `maxEntries?` | nested + rendered tree (bounded) |
|
|
175
|
+
| `run_command` | `cmd`, `timeout?`, `cwd?` | `exitCode`, `stdout`, `stderr`, flags |
|
|
176
|
+
| `web_search` | `query` | labelled **stub** result (real search is platform-side) |
|
|
177
|
+
| `git` | `args` (array) or `command` (string) | git output, `exitCode`, flags |
|
|
178
|
+
| `generate` | `template`, `name` | file list it created |
|
|
179
|
+
| `mv` | `source`, `destination`, `overwrite?` | moved path pair (rename if same dir) |
|
|
180
|
+
| `cp` | `source`, `destination`, `overwrite?` | copied path pair |
|
|
181
|
+
| `delete_file` | `path`, `recursive?` | deleted path (requires approval) |
|
|
182
|
+
| `json` | `value` or `path`, `indent?`, `sortKeys?` | pretty-printed JSON |
|
|
183
|
+
| `skill` | `name?` | loaded skill / available list |
|
|
184
|
+
|
|
185
|
+
Every tool canonicalizes its path, asserts it is inside the authorization root,
|
|
186
|
+
rejects outside/sensitive paths, and never leaks platform internals or keys.
|
|
187
|
+
`mv`/`cp`/`delete_file` and `str_replace_editor` is a single operator;
|
|
188
|
+
`mv` with no `overwrite` refuses to clobber an existing destination (and asks
|
|
189
|
+
for approval when overwriting), and `delete_file` always requires approval in
|
|
190
|
+
`workspace-write` mode.
|
|
191
|
+
|
|
192
|
+
## Security model
|
|
193
|
+
|
|
194
|
+
1. **Authorization root** — `canonicalize()` resolves `..`, symlinks, the
|
|
195
|
+
`\\?\` prefix, trailing dots/spaces and Windows reserved names, then
|
|
196
|
+
`isWithinRoot()` strictly checks the result is under the root. Writes equal
|
|
197
|
+
to the root are rejected; `..` and symlink escapes are rejected too.
|
|
198
|
+
2. **Danger-command detection** — `isDangerCommand()` flags `rm -rf`, `sudo`,
|
|
199
|
+
`format <drive>`, `del /f`, `rd /s /q`, `shutdown`, `reboot`,
|
|
200
|
+
`git reset --hard`, `git clean -fdx`, `find ... -delete`,
|
|
201
|
+
`Remove-Item -Recurse -Force`, `diskpart`, `reg delete`/`reg add`,
|
|
202
|
+
`powershell -enc`, `iex`/`Invoke-Expression`, `certutil -decode`,
|
|
203
|
+
`regsvr32`/`rundll32`/`mshta`/`wmic`/`bitsadmin`, `curl|wget -> sh`,
|
|
204
|
+
`sh/bash -c`, `sc create`, `net user`, `schtasks /create`,
|
|
205
|
+
`icacls`/`cacls`, `takeown`, `vssadmin`, `bcdedit`, `cipher /w`,
|
|
206
|
+
`taskkill /f`, `chmod -R 777 /`, `mkfs`, `dd of=/dev/...`,
|
|
207
|
+
redirects to block devices, fork bombs and more. Cross-platform
|
|
208
|
+
(bash / PowerShell / cmd).
|
|
209
|
+
3. **Sensitive-path detection** — `isSensitivePath()` blocks system and
|
|
210
|
+
credential paths (`/etc/passwd`, `C:\Windows\System32`, `~/.ssh`, `.env`
|
|
211
|
+
with keys, `id_rsa`, `.npmrc` with `_authToken`, and so on) regardless of mode.
|
|
212
|
+
4. **Secret masking** — `maskSecrets()` redacts provider keys, bearer tokens,
|
|
213
|
+
JWTs, private keys and `key=value` secrets from logs and tool output.
|
|
214
|
+
5. **Idempotency** — results are cached by a hash of `(tool, args, idempotencyKey)`;
|
|
215
|
+
replayed jobs return the cached result without re-executing. Request-for-approval
|
|
216
|
+
(`needsApproval`) results are deliberately **not** cached so the platform can
|
|
217
|
+
prompt a human and then re-send the same key to actually run it.
|
|
218
|
+
6. **Environment sanitization** — child processes never inherit the agent's own
|
|
219
|
+
token or credential variables.
|
|
220
|
+
|
|
221
|
+
## Self-test
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
node dist/cli.js --selftest
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Runs 53 checks over path safety, danger-command detection, sensitive-path
|
|
228
|
+
detection, secret masking, the tool registry, command timeout/process-tree kill,
|
|
229
|
+
environment sanitization, config resolution + save/load (never logging the token),
|
|
230
|
+
and idempotency over a real WebSocket connection. It prints `PASS`/`FAIL` lines
|
|
231
|
+
and exits `0`/`1`.
|
|
232
|
+
|
|
233
|
+
## Protocol
|
|
234
|
+
|
|
235
|
+
JSON text frames over a WebSocket.
|
|
236
|
+
|
|
237
|
+
**Client → server**
|
|
238
|
+
|
|
239
|
+
```jsonc
|
|
240
|
+
{ "type": "hello", "token": "...", "deviceId": "...", "version": "0.9.0", "platform": "node" }
|
|
241
|
+
{ "type": "pong", "t": 42 } // reply to a server ping
|
|
242
|
+
{ "type": "tool_result", "jobId": "j1", "ok": true, "result": { }, "durationMs": 12 }
|
|
243
|
+
{ "type": "status", "state": "online" | "busy" | "error", "sessionId": "..." }
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Server → client**
|
|
247
|
+
|
|
248
|
+
```jsonc
|
|
249
|
+
{ "type": "welcome", "sessionId": "..." }
|
|
250
|
+
{ "type": "ping", "t": 42 }
|
|
251
|
+
{ "type": "tool_call", "jobId": "j1", "idempotencyKey": "k1", "tool": "write_file", "args": { } }
|
|
252
|
+
{ "type": "cancel", "jobId": "j1" }
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Development
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
npm install
|
|
259
|
+
npm run build # tsc -> dist/
|
|
260
|
+
npm run test # node dist/cli.js --selftest
|
|
261
|
+
npm start # node dist/cli.js
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ClientState } from './protocol';
|
|
2
2
|
import { type AgentConfig } from './config';
|
|
3
3
|
/** Kept in sync with package.json. */
|
|
4
|
-
export declare const AGENT_VERSION = "0.
|
|
4
|
+
export declare const AGENT_VERSION = "0.9.1";
|
|
5
5
|
export interface AgentHandle {
|
|
6
6
|
stop(): void;
|
|
7
7
|
}
|
package/dist/agent.js
CHANGED
|
@@ -12,7 +12,7 @@ const tools_1 = require("./tools");
|
|
|
12
12
|
const config_1 = require("./config");
|
|
13
13
|
const tokens_1 = require("./tokens");
|
|
14
14
|
/** Kept in sync with package.json. */
|
|
15
|
-
exports.AGENT_VERSION = '0.
|
|
15
|
+
exports.AGENT_VERSION = '0.9.1';
|
|
16
16
|
const MAX_IDEMPOTENCY_ENTRIES = 512;
|
|
17
17
|
const HEARTBEAT_MS = 30000;
|
|
18
18
|
const READY_OPEN = 1; // ws.WebSocket.OPEN
|
|
@@ -168,7 +168,7 @@ class Agent {
|
|
|
168
168
|
async onToolCall(msg) {
|
|
169
169
|
if (!(0, protocol_1.isToolCall)(msg))
|
|
170
170
|
return;
|
|
171
|
-
const { jobId, tool, args, idempotencyKey, root } = msg;
|
|
171
|
+
const { jobId, tool, args, idempotencyKey, root, preApproved } = msg;
|
|
172
172
|
const key = (0, protocol_1.computeIdempotencyKey)(tool, args ?? {}, idempotencyKey);
|
|
173
173
|
if (this.idempotencyCache.has(key)) {
|
|
174
174
|
const cached = this.idempotencyCache.get(key);
|
|
@@ -181,7 +181,7 @@ class Agent {
|
|
|
181
181
|
this.jobs.set(jobId, controller);
|
|
182
182
|
this.log.info('running tool', { jobId, tool });
|
|
183
183
|
try {
|
|
184
|
-
const result = await this.registry.dispatch(tool, args ?? {}, controller.signal, root);
|
|
184
|
+
const result = await this.registry.dispatch(tool, args ?? {}, controller.signal, root, preApproved === true);
|
|
185
185
|
if (controller.signal.aborted) {
|
|
186
186
|
// The platform sent a cancel for this job; report it explicitly.
|
|
187
187
|
this.sendResult({ ok: false, error: 'Job cancelled by platform', durationMs: result.durationMs }, jobId);
|