scv-cli 0.1.32

A small, extensible terminal agent runtime with a TUI and headless server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
# Built-in Tools

Status: final design for v0.1

All tool inputs are validated JSON objects. Tools execute serially and receive a
cancellation token, workspace root, timeout, and output limits from the server.

## `read`

```json
{"path":"src/main.rs","offset":0,"limit":65536}
```

`path` is a required workspace-relative UTF-8 path. `offset` and `limit` are
optional byte values. The result reports the selected content, total byte
length, and whether it was truncated. The selected byte range must be UTF-8 or
the tool returns a clear error in v0.1. Workspace reads are read-only risk;
lexically secret-like paths are elevated to filesystem risk and require
approval. This name heuristic improves visibility but is not a confidentiality
boundary; an OS sandbox is still required for isolation.

## `write`

```json
{"path":"src/main.rs","content":"fn main() {}\n","mode":"replace","expected_sha256":"..."}
```

`mode` is `create` or `replace`. `create` fails if the path exists. `replace`
fails if it does not exist. `expected_sha256` is optional for the first write
but, when supplied, must match the file observed immediately before the write.
It detects stale input but is not a filesystem lock against an external writer.
The tool writes a capability-contained temporary file in the destination
directory, flushes it, and installs it atomically. All writes have filesystem
risk.

## `bash`

```json
{"command":"cargo test --workspace","timeout_seconds":900}
```

`command` is passed to `/bin/bash -lc` in the workspace. Without
`timeout_seconds` a call gets `tools.command_timeout_seconds` (default 600).
A call may choose any timeout up to `tools.max_timeout_seconds` (default 14400),
which the schema advertises as its `maximum`; a larger request is refused
before approval with an error naming the ceiling. The result contains exit status and bounded
combined stdout/stderr, with truncation metadata. Shell execution has process
risk and is not sandboxed.

SCV creates a new process group for shell and native-agent children. On
cancellation it sends `TERM` to the whole group, allows up to two seconds for
cleanup, then sends `KILL` to any remaining members. Reaching the configured
deadline sends `KILL` immediately. SCV also cleans up descendants after the
group leader exits and bounds output-pipe draining, so a background child cannot
keep a tool call alive indefinitely.

## `web_fetch`

```json
{"url":"https://docs.rs/serde/latest/serde/","offset":0}
```

Fetches one public `http` or `https` URL with a GET request and returns it as
text. HTML becomes readable plain text with numbered link references, JSON and
other text types pass through, and a declared binary type such as an image,
PDF, or archive is refused before it downloads. The result begins with the
final URL, status, content type, and the character range returned. SCV
downloads at most `web.fetch_max_bytes` (default 2 MiB) and returns at most
`tools.output_limit_bytes`; a longer page ends with the `offset` that returns
its next part. An HTTP error status returns the body as a failed result.

Requests carry a `scv/<version>` User-Agent and no cookies, credentials, or
referrer, ignore proxy variables, follow at most `web.max_redirects` (default
5) redirects, and stop after `web.fetch_timeout_seconds` (default 30). URLs
with another scheme or embedded credentials are refused.

Only public addresses are reachable unless `web.allow_private_addresses` is
set. Loopback, private, shared (CGNAT), link-local (including cloud metadata
at `169.254.169.254`), multicast, reserved, and documentation ranges, IPv6
unique-local and link-local addresses, and IPv6 forms that embed a refused
IPv4 address are all refused. A host name is resolved once by a checking
resolver: if any of its addresses is refused the name is refused, and the
connection uses only the checked addresses, so a name cannot be rebound to a
private address between check and connect. IP-literal URLs and every redirect
target are checked the same way.

An HTTPS URL whose host is in `web.auto_approve_domains` has `read_only`
risk, so it runs without approval under `on-risk` and is allowed under
`never`. A redirect from such a fetch may lead only to another listed HTTPS
host; otherwise the call fails and names the target, which the model can then
request on its own. Every other URL has `network` risk and needs approval,
because the URL itself can carry data the model has read to a host that a
prompt-injected page chose. The approval summary shows the full URL.

## `web_search`

With `web.search = "provider"`, each model request also offers the endpoint's
hosted Responses tool `{"type":"web_search"}`. The provider runs the searches
and returns the answer with `url_citation` annotations; SCV appends a
`Sources:` list for any cited URL that the answer does not already link. No
SCV tool call or approval is involved, and the queries reach only the model
provider, which already receives the conversation.

With `web.search = "searxng"` or `"brave"`, SCV registers a `web_search` tool:

```json
{"query":"tokio latest version","count":5}
```

It returns up to `count` (at most `web.max_search_results`, default 8) titles,
URLs, and snippets from SearXNG (`GET <web.searxng_url>/search?format=json`)
or the Brave Search API. It has `read_only` risk: the query goes only to the
search service the user configured, not to a host the model picks. A backend
failure returns a failed result with a hint for the common SearXNG and Brave
setup errors.

Tool-free sessions, such as ClawBot senders without remote tools, get neither
web tool nor hosted search. Fetch tests use local servers with a port-aware
address check to cover HTML conversion, content-type and size limits, paging,
redirect limits, and loopback, metadata, name-resolved, and redirected private
targets; search tests use fake SearXNG and Brave responses; a stdio test
covers approval with and without the allowlist and the hosted tool in the
request.

## Native agent adapters

SCV knows five agent CLIs: Claude Code (`agent_claude`), Codex
(`agent_codex`), Grok Build (`agent_grok`), DeepSeek Harness (`agent_dsh`),
and pi (`agent_pi`), plus a nested SCV (`agent_scv`, see
[Nested SCV](#nested-scv-agent_scv)). Each is one descriptor in `scv_tools::adapters` holding
its default command line, where its state lives inside the private home, the
variables it must not inherit, and how it signs in; adding an agent is one
more entry. A session offers only the agents whose executable resolves when
the session starts, so an agent installed later appears in new sessions.
They share this schema:

```json
{"prompt":"Land the fix with the feature-flow skill.","cwd":"scv","timeout_seconds":7200,"model":"sonnet","effort":"medium"}
```

`cwd` is optional: a directory inside the workspace, relative (such as a
project directory) or absolute. It resolves, following symlinks, when the call
runs and must be an existing directory under the workspace; otherwise the call
fails without launching anything. Without it the agent runs in the workspace
root. Running in a project directory is how a delegated agent picks up that
project's `AGENTS.md` or `CLAUDE.md` and its skills (`.agents/skills` for
Codex, `.claude/skills` for Claude Code), exactly as when the user starts the
CLI there. `timeout_seconds` defaults to `tools.agent_timeout_seconds`
(default 3600) and may be raised up to `tools.max_timeout_seconds` (default
14400) for long work such as builds, releases, or landing a change.

`model` and `effort` are optional and offered only when the adapter configures
`model_args` or `effort_args`. Their schema descriptions name the adapter's
model family (Claude aliases such as `sonnet` for `agent_claude`, OpenAI model
IDs for `agent_codex`, Grok model IDs for `agent_grok`, pi model patterns or
`provider/id` for `agent_pi`) and tell the model to set them only when the user asks,
so an omitted value leaves the agent's own configured default in place. A
blank `cwd`, `model`, or `effort` counts as omitted, since models often send
`""` for an optional field they mean to leave unset. A model is 1-128 ASCII letters, digits, or
`._:/@[]-` and cannot start with `-` or `@`; an effort is `low`, `medium`, `high`,
`xhigh`, or `max`. Each selected value becomes one substituted argument, never
shell text, so the CLI itself reports values it does not support.

Each tool resolves only its configured executable and fixed argument vector,
adds any selected model/effort arguments, then any `prompt_args` (for CLIs
whose prompt is a flag value, such as `grok -p`), appends the prompt as one
argument, and starts it directly in the workspace or the selected `cwd`. A
prompt cannot start with `-`, so it is never read as a flag.
The executable is looked up in the user's per-user install directories
(`~/.local/bin`, plus `~/.grok/bin` for Grok) before `PATH`, the order a login
shell uses, because the user service's `PATH` omits them; the daemon therefore
runs the same install the user's shell does.
Native adapters receive an instance-private `HOME`, `SCV_HOME`, and XDG
configuration/data/state directory, plus the agent's own state location inside
it: `CODEX_HOME`, `GROK_HOME` (`.grok`), `DSH_HOME` (`.dsh`), or
`PI_CODING_AGENT_DIR` (`.pi/agent`). Grok also gets
`GROK_DISABLE_AUTOUPDATER=1`. Before those are set, SCV removes every inherited
variable ending in `_API_KEY`, SCV's selector variables, and each adapter's
credential, endpoint, and state variables (`ANTHROPIC_*` keys and tokens,
`CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CONFIG_DIR`, `OPENAI_BASE_URL`,
`CODEX_BASE_URL`, `GROK_*`, `DSH_*`, `DEEPSEEK_BASE_URL`, `PI_*`, and similar),
so no nested agent reuses the parent's configuration or another agent's
credentials from outside its private home. The `bash` tool retains the normal
inherited environment for compatibility.
The model cannot supply other flags or a different executable. Timeout,
cancellation, and process-group behavior match `bash`; the output is the
structured result described below. Adapter execution has delegate risk because
the child agent may independently read, write, run commands, access inherited
credentials, or ask its own model provider.

The default invocation contracts are:

| Tool | Invocation |
| --- | --- |
| `agent_claude` | `claude -p --output-format stream-json --verbose --session-id <uuid> [--model <model>] [--effort <effort>] <prompt>` |
| `agent_codex` | `codex exec --json -o <file> [-m <model>] [-c model_reasoning_effort="<effort>"] <prompt>` |
| `agent_grok` | `grok [-m <model>] [--reasoning-effort <effort>] -p <prompt>` |
| `agent_dsh` | `dsh --profile headless <prompt>` |
| `agent_pi` | `pi -p --mode json [--model <model>] [--thinking <effort>] <prompt>` |

Grok's `-p` and pi's `-p` run one prompt and exit. DeepSeek Harness takes its
model from its profile, so `agent_dsh` offers no `model` or `effort`.
`permissions = "full"` switches follow the fixed arguments, before the output
format arguments.

### Results

Each adapter declares its output format, and SCV reads the CLI's stdout as it
arrives, keeping only the reply, token usage, and error, so a long run's event
log never reaches the parent model:

- Claude Code's `stream-json` events end with a `result` event carrying the
  reply, `is_error`, and usage; SCV picks the `--session-id` itself when a
  conversation starts.
- Codex's `--json` events give the last `agent_message` item as the reply,
  `turn.completed` usage, and `turn.failed` or `error` messages. `-o` names a
  file in a private `tmp` directory of Codex's adapter home holding the final
  message, used if the stream carried none and deleted afterwards.
- pi's `--mode json` gives the last assistant `message_end` text and usage.
- Grok and DeepSeek Harness stay plain text: stdout is the reply. Grok's JSON
  output exists but its success shape could not be verified signed out.

Unknown events and fields are ignored, lines over 4 MiB are skipped, and a
structured stream with no JSON at all falls back to its text. The tool result
is one object:

```json
{"agent":"codex","status":"completed","reply":"…","usage":{"input_tokens":11257,"output_tokens":5},"exit_code":0,"stderr_tail":"…","truncated":false}
```

`status` is `completed`, `failed` (non-zero exit or a reported error),
`timeout`, or `cancelled` (stopped by `scv agents kill`). `reply` is bounded by
`tools.output_limit_bytes` on a character boundary, `stderr_tail` holds the
last 2 KiB of stderr, and `truncated` says whether anything was cut. A failure
that reads like a missing sign-in gains a `hint` (see below). A turn of a
conversation (next section) also carries `"session"` and `"turn"`.

### Progress

While a structured run lasts, its events also become short status lines that
clients see as `tool.progress` (see [protocol](protocol.md#tool-lifecycle-and-approval)):

- Codex: `$ <command>` when a command starts, `exit <code>: <command>` when one
  fails, `<kind> <file>` for each file change, and `search: <query>`.
- Claude Code: `$ <command>` for Bash, `<tool> <file>` for file tools,
  `search:`/`fetch` for web tools, the tool name otherwise, and the first line of
  its interim text.
- pi: `$ <command>` for bash, `<tool> <file>` for file tools, the tool name
  otherwise, and `<tool> failed` for a failed call.
- Grok and DeepSeek Harness: none, since their output is plain text.

Lines never include command output. Paths show their last two components,
URLs lose their query, and values that look like credentials (`Bearer` tokens,
`NAME=value` or `--name value` where the name mentions a key, token, secret, or
password, and well-known token prefixes) become `…`. The redaction is a display
heuristic, not a guarantee. The runtime forwards pending lines at most twice a
second per call and never adds them to the model's history or the tool result.

### Conversations

An agent whose CLI can resume a session takes an optional `session` argument.
Omitting it starts a conversation; the result's `session` is an SCV-issued
handle such as `codex-2`, and passing it back continues that conversation with
the agent's own context:

| Tool | Starts with | Continues with |
| --- | --- | --- |
| `agent_claude` | `--session-id <uuid>` (chosen by SCV) | `--resume <uuid>` |
| `agent_codex` | nothing; the thread ID comes from `thread.started` | `codex exec resume … <thread-id> <prompt>` |
| `agent_pi` | `--session-id <uuid>` (chosen by SCV) | `--session-id <uuid>` |

Grok and DeepSeek Harness start a fresh conversation on every call: their
resume options could not be verified headless here, so their tools offer no
`session` argument and refuse one.

The model only ever sees handles. The CLI's own session IDs stay inside SCV,
and a value that is not one of this session's handles, such as a raw vendor
ID, is refused. A conversation keeps its agent and `cwd`: continuing it
elsewhere is an error, so start a new one there instead. One turn runs at a
time; a second call while a turn runs returns `session busy`. A turn that
times out stays resumable once the CLI has reported its session, so the next
turn can ask the agent to continue where it stopped. A first turn that fails
before the CLI reports a session is forgotten.

Each session remembers at most `agent.max_conversations` conversations
(default 8; starting another forgets the least recently used idle one) and
forgets one left unused for `agent.conversation_idle_seconds` (default 86400).
Handles end with the SCV session. `scv agents ps` shows a running turn's
conversation and turn number.

The CLIs keep transcripts in their private adapter homes (Claude Code under
`.claude/projects`, Codex under `sessions`, pi under `.pi/agent/sessions`).
`scv agents gc` removes old ones:

```sh
scv agents gc --dry-run                 # what would go, per agent
scv agents gc --older-than 7d codex     # default 30d, never less than an hour
```

A live conversation leaves a marker in `$SCV_HOME/run/conversations`, named
after its CLI session ID, and `gc` keeps any transcript a marker whose SCV
process still runs names. Transcripts written in the last hour are always
kept, and symlinks are never followed.

### Tracking and cleanup

Every delegated process gets `SCV_PARENT=<instance>/<session>/<handle>`
(appended to an inherited chain when SCV itself runs delegated) and
`SCV_DELEGATION_DEPTH`, one more than the caller's. Its descendants inherit
both. While it runs, SCV records it in `$SCV_HOME/run/delegations/<handle>.json`
(mode `0600`, directories `0700`, written atomically): handle, agent, parent
session, `cwd`, depth, and the PID plus start time of both the agent and the
SCV process that owns it, so a reused PID never matches.

When a run ends, SCV stops its process group and then any process still tagged
with its handle (TERM, then KILL after 2 seconds), including descendants that
left the group with `setsid`, and removes the record. A run abandoned
mid-flight is killed the same way. The daemon reconciles at startup and every
60 seconds: a record whose owning SCV process is gone (for example an
`scv exec` server that was SIGKILLed) is an orphan, and its group and tagged
processes are stopped and the record removed. A `scv server --stdio` also
reconciles once when it starts. Each pass also removes conversation markers
(below) whose owning SCV process has exited, which a short-lived `scv exec`
leaves behind. On Linux the daemon is a child subreaper, so
descendants a delegated agent leaves behind reparent to it rather than to
init, and it collects those that exit.

```sh
scv agents ps          # running delegations of this instance, from any SCV process
scv agents ps --all    # also orphans awaiting cleanup
scv agents kill codex-3f9a2c
scv agents kill --orphans
```

`scv status` shows the running count and how many orphans the daemon has
stopped. Agent tools are offered only while the session's own depth is below
`agent.max_delegation_depth` (default 2), and a delegated run may not start,
stop, restart, update, or run a daemon, or manage ClawBot. This is cooperative:
see [Delegated runs](security.md#delegated-runs).

By default SCV adds nothing to an agent's own permission settings, and in
print mode Claude Code, for example, refuses Bash, Edit, and Write without a
permission mode. `[agents.<name>] permissions = "full"` is the user's explicit
opt-in to the CLI's own full-autonomy switches, placed after the fixed
arguments: `--permission-mode bypassPermissions` for Claude Code (which also
allows WebSearch and WebFetch), `--dangerously-bypass-approvals-and-sandbox -c
web_search="live"` for Codex (`codex exec` has no `--search` flag), and
`--always-approve` for Grok (web search is on by default); DeepSeek Harness
gets `DSH_PERMISSION_MODE=danger-full-access`, and pi, which has no approval
prompts, sandbox, or built-in web search, gets nothing. The approval summary
then states `FULL PERMISSIONS`. See
[Agent permissions](configuration.md#agent-permissions).

Before approval, SCV resolves the executable through the server environment
and displays its absolute path, full argument vector, bounded prompt, requested
directory, timeout, and delegate-risk warning; the approval request also
carries the session workspace. Project configuration cannot replace the
executable or arguments.

Adapter processes use instance-private state directories under
`$SCV_HOME/adapters/<name>`. In particular, `agent_codex` receives
`CODEX_HOME=$SCV_HOME/adapters/codex` and does not read the user's normal
`~/.codex` state, `agent_claude` does not read `~/.claude`, and Grok, DeepSeek
Harness, and pi never read `~/.grok`, `~/.dsh`, or `~/.pi`.

### Nested SCV (`agent_scv`)

`agent_scv` delegates to another SCV: a separate session in SCV's private
home `$SCV_HOME/adapters/scv`, with its own context, instructions, and tools.
Unlike the CLI adapters, which start one process per turn, it keeps one
`scv server --stdio` running for a whole conversation and speaks the
[client protocol](protocol.md) to it:

```text
initialize (v3) → session.start {cwd, delegation_depth: parent + 1} → turn.start per call
```

- Its schema has `prompt`, `cwd`, `session`, `timeout_seconds`, and `model`
  (a new conversation only, sent as the session's model override); there is no
  `effort`.
- The first call starts the nested SCV and returns a handle such as `scv-1`;
  passing it as `session` sends the next prompt to the same nested session,
  which keeps its history. A conversation keeps its `cwd`, runs one turn at a
  time, and follows `agent.max_conversations` and
  `agent.conversation_idle_seconds` like the CLI conversations.
- The nested SCV's events become `tool.progress` of the calling tool:
  completed lines of its assistant text, `bash …` and `bash done|failed` as
  its tools start and finish, and its own tools' progress lines. Tool output
  never becomes progress.
- Its `approval.requested` goes through the calling session's approval gate
  with the summary prefixed `[scv-1 depth N]`, keeping the nested tool's name
  and risk, and the answer returns as `approval.resolve`. The session's policy
  and its user (or a WeChat owner's auto-approval) therefore decide every
  nested side effect; with no approval gate the request is denied.
- The result is `{"agent":"scv","status","reply","usage","session","turn"}`.
  A call that is cancelled or times out sends `turn.cancel`; a turn that
  settles within 2 seconds leaves the conversation usable (a timed-out turn is
  resumable), otherwise the nested SCV is shut down and the conversation
  forgotten. A nested SCV that exits mid-turn fails the call with its stderr
  tail and ends the conversation.
- The nested SCV is recorded like any delegation, so `scv agents ps` lists it
  with its current turn, `scv agents kill` stops it, and the orphan
  reconcile reaps it if its parent dies. It ends when its conversation is
  forgotten, expires, or its session ends: SCV closes its stdin (the server
  exits on EOF), waits 2 seconds, then kills its process group and anything
  still tagged with it.
- The nested SCV runs one delegation level deeper and declares that depth in
  `session.start`, so `agent.max_delegation_depth` applies on both sides: the
  default of 2 lets it delegate once more, and it cannot start, restart, or
  update a daemon or manage ClawBot. It has no parent daemon socket.

`agent_scv` needs the `scv` executable (searched on `PATH` and in
`~/.cargo/bin`, where `cargo install` puts it) and a provider in its private
home: `scv agents import scv` (or `scv agents login scv`) copies SCV's own
active provider there, as below. Its own delegated agents live under
`$SCV_HOME/adapters/scv/adapters` and are signed out unless signed in there.
Attaching to an already running daemon instead of starting a child is future
work.

### Signing in delegated agents

Each adapter keeps its own sign-in in its private home, separate from the
user's personal login, so token refreshes by one never invalidate the other.
Sign the agents in once on the SCV host:

```sh
scv agents login claude                 # claude auth login
scv agents login codex                  # codex login
scv agents login codex -- --device-auth # extra arguments after --
scv agents import codex                 # or copy your own Codex setup
scv agents login grok -- --device-auth  # grok login, device code for SSH hosts
scv agents import grok                  # or copy your own Grok config and model profiles
scv agents login dsh                    # prompts for a DeepSeek API key
scv agents login pi                     # opens pi: run /login, then /quit
scv agents login pi --openai-compatible # or point pi at any OpenAI-compatible endpoint
scv agents import pi --from-scv-provider # or reuse SCV's own provider
scv agents status                       # every agent's sign-in state
scv agents logout claude
```

They work from any directory. For Claude Code, Codex, and Grok, `login` and
`logout` run the agent's own command with exactly the private home and cleaned
environment that the daemon's `agent_*` tool uses, with the terminal attached
for browser or device-code flows, and the agent CLI itself writes those
credentials under `$SCV_HOME/adapters/<name>` (mode `0700`). For Claude Code
and Codex, `status` runs the CLI's own status command but prints only a
summary, such as `signed in (Claude account, max)` or `signed in (API key)`,
because their output names the account email or part of the key; for the
others SCV reads the credential file and reports only whether one is stored,
never its value.

Grok counts as signed in either through a `grok login` sign-in in
`.grok/auth.json` or through an API key in `.grok/config.toml`: the profile of
its `[models] default` (matched by catalog key or model id) holds an `api_key`,
or an `env_key` naming a variable that is set and that SCV does not remove from
delegated agents. Status then reads `signed in (API key in config, model
"<id>")`. `scv agents import grok` copies your own `~/.grok/config.toml` (or
`$GROK_HOME`, or `--from <dir>`) into `.grok/config.toml`, atomically with mode
`0600`. It merges by top-level table: your tables win, and tables only SCV's
copy has, such as the `[marketplace]` state Grok writes there, are kept. The
merged file is validated before anything is written, and it prints the profiles
and default model, never a key. `auth.json` sign-ins are never copied. Re-run it
after changing your own Grok config.

DeepSeek Harness signs in with an API key only. `scv agents login dsh` reads
it without echo, or from stdin when stdin is not a terminal, and writes it as
`refs.DEEPSEEK_API_KEY` in `.dsh/.credentials.yaml`, DeepSeek Harness's own
credential file, atomically with mode `0600`. `logout` removes that file. A key
is never accepted as an argument. DeepSeek Harness 0.1.7-rc.1 is the tested
version; 0.1.5-rc.2 fails at startup with "cannot create effect on inactive
context" because its sandbox plugin requires a different Cordis framework
version than the one it installs. Signed out, it fails with `MISSING_CREDENTIAL`,
and the `agent_dsh` result names `scv agents login dsh`.

pi's own `/login` covers its built-in providers. For any OpenAI-compatible
endpoint, `scv agents login pi --openai-compatible` asks for the base URL, the
wire API (`responses` or `chat`), and the default model (or takes them from
`--base-url`, `--wire-api`, and `--model`), then reads the key without echo.
`scv agents import pi --from-scv-provider` takes all four from SCV's active
provider instead, reading an `api_key_env` variable at import time because
delegated agents never inherit key variables. Either way SCV writes pi's own
files in `.pi/agent`, each atomically with mode `0600`: provider `scv` in
`models.json` (for the Responses API with
`compat.sessionAffinityFormat = "openai-nosession"`, because pi's default
`session_id` header is rejected by proxies that refuse underscores in header
names), its key in `auth.json`, and `defaultProvider`/`defaultModel` in
`settings.json`, so a bare `agent_pi` call uses it and `model` can name
`scv/<id>`. Other providers and settings in those files are preserved.
`status` shows the default provider, API, endpoint host, and model, and which
providers have stored sign-ins; `logout` removes `auth.json`, the `scv`
provider, and a default that points at it.

`scv agents import scv` gives the nested SCV behind `agent_scv` a copy of
SCV's own active provider: `$SCV_HOME/adapters/scv/config.toml` (mode `0600`,
written atomically) gets `[provider] active = "scv"` and a `[providers.scv]`
profile with the same kind, wire API, model, base URL, timeout, and headers,
plus `[web] search = "provider"` when SCV's own config uses hosted search. The
key is resolved at import time from `api_key` or the `api_key_env` variable
and stored in that file, because delegated agents never inherit key
variables; it is never printed. Other settings already in that file are kept,
and an unreadable file is left untouched. `status` shows the provider, model,
and endpoint host; `logout` removes the file.

`scv agents import codex [--from DIR]` instead copies an existing Codex setup,
by default from `$CODEX_HOME` or `~/.codex`. It is for custom providers such as
an OpenAI-compatible relay (`model_providers` with `base_url`, `wire_api`,
`requires_openai_auth`, or `experimental_bearer_token`), which `codex login`
cannot set up:

- `config.toml` is copied whole, so the model, provider, reasoning effort,
  `sandbox_mode`, `approval_policy`, project trust, and features match the
  user's own Codex. SCV reports the model, provider, and policies it copied.
- `auth.json` is copied only when it holds an API key. A ChatGPT sign-in is
  never copied, because its rotating refresh token must stay in one home; use
  `scv agents login codex` for that.
- A provider that reads its key through `env_key` is flagged: SCV removes
  provider key variables such as `OPENAI_API_KEY` from delegated agents, and
  the user service does not load the shell profile.

Both files are validated before either is written. Copies are atomic, mode
`0600`, and never printed. The import is a snapshot rather than a link, so
delegated Codex runs cannot modify the user's own configuration; re-run it
after changing that configuration. Skills and other Codex state are not
copied. The daemon needs no restart: the next delegated call
uses the new sign-in. When a delegated run fails with output that reads like a
missing sign-in, its tool result gains a `hint` naming
`scv agents login <name>`, since the agent's own advice (`/login`) cannot be
followed from a remote chat.

A fake agent script, run through `bash` so tests never execute a freshly
written file, verifies native-agent argument boundaries, model/effort argument
mapping and validation, workspace and `cwd` selection including symlink
escapes, prompt-flag placement, the timeout ceiling, per-adapter environment
removal, and that uninstalled agents are not offered, without requiring these
CLIs in CI. Canned event streams cover each output format, sign-out, oversized
lines, the Codex `-o` file, and bounded replies; real processes cover records,
kill, a timed-out run's `setsid` descendant, and an orphan left by a
SIGKILLed `scv server --stdio`. Fake SCV homes cover the DeepSeek Harness key file, pi's endpoint
files and import, and that no sign-in output contains a key. Shared process-runner tests cover
output limits, timeout, cancellation, and background-descendant cleanup. A
bash stand-in for `scv server --stdio` covers `agent_scv` conversations on one
child, progress, approval relay (approved, denied, and without a gate), cancel
and timeout relay including a child that ignores `turn.cancel`, a child dying
mid-turn, idle and session-end teardown, and the depth limit; an end-to-end
test runs a real parent and nested `scv` against a fake provider.

## Tool extension contract

A `Tool` supplies a unique name, description, JSON Schema, declared `ToolRisk`,
approval summary, and asynchronous executor. Stable risk values are
`read_only`, `filesystem`, `process`, `delegate`, and `network`. Registration rejects
duplicate names. The loop and TUI do not contain name-specific execution code.

## `read_skill`

```json
{"name":"release-checks"}
```

At session start, the server discovers at most `skills.max_skills` valid skill
directories and builds a name-to-canonical-path map. `read_skill` accepts only a
name from that map, revalidates containment under its original project or user
skill root, and returns at most `skills.max_skill_bytes` of `SKILL.md`. It does
not accept a path and cannot be used as a general out-of-workspace read.
Tool-enabled sessions also map workspace project skills (`.agents/skills` and
`.claude/skills` of the workspace and its child projects) under
`<project>:<name>`, listed separately with the instruction to delegate to that
project with `agent_*` and `cwd`; see
[Project skills](configuration.md#project-skills). Skill
metadata and content remain untrusted instructions.