@khorsheed/dsh-local-agent 0.1.0-rc.6
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.en.md +181 -0
- package/README.i18n.yaml +2 -0
- package/README.md +181 -0
- package/cordis.patch.yml +21 -0
- package/lib/client.js +8832 -0
- package/lib/index.js +5281 -0
- package/lib/invariant.js +85 -0
- package/lib/member-bridge.js +298 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +3137 -0
- package/lib/typert.remote-client.d.ts +67 -0
- package/lib/typert.remote-client.js +1971 -0
- package/lib/types/cli-version.d.ts +82 -0
- package/lib/types/cli-version.js +165 -0
- package/lib/types/client/AuthStatusDot.d.ts +17 -0
- package/lib/types/client/AuthStatusDot.js +9 -0
- package/lib/types/client/HarnessModelPicker.d.ts +17 -0
- package/lib/types/client/HarnessModelPicker.js +48 -0
- package/lib/types/client/LocalAgentRecordsAction.d.ts +41 -0
- package/lib/types/client/LocalAgentRecordsAction.js +74 -0
- package/lib/types/client/MemberComposer.d.ts +79 -0
- package/lib/types/client/MemberComposer.js +308 -0
- package/lib/types/client/MemberConfiguration.d.ts +9 -0
- package/lib/types/client/MemberConfiguration.js +46 -0
- package/lib/types/client/MemberInboxView.d.ts +12 -0
- package/lib/types/client/MemberInboxView.js +38 -0
- package/lib/types/client/MemberLiveNode.d.ts +14 -0
- package/lib/types/client/MemberLiveNode.js +74 -0
- package/lib/types/client/MemberPaintDiagnostics.d.ts +7 -0
- package/lib/types/client/MemberPaintDiagnostics.js +20 -0
- package/lib/types/client/ModelConfigurationFields.d.ts +15 -0
- package/lib/types/client/ModelConfigurationFields.js +23 -0
- package/lib/types/client/ProviderAuthBlock.d.ts +59 -0
- package/lib/types/client/ProviderAuthBlock.js +164 -0
- package/lib/types/client/auth-status.d.ts +28 -0
- package/lib/types/client/auth-status.js +56 -0
- package/lib/types/client/icons.d.ts +22 -0
- package/lib/types/client/icons.js +6 -0
- package/lib/types/client/index.d.ts +62 -0
- package/lib/types/client/index.js +135 -0
- package/lib/types/client/live-node.d.ts +14 -0
- package/lib/types/client/live-node.js +34 -0
- package/lib/types/client/live-output.d.ts +34 -0
- package/lib/types/client/live-output.js +94 -0
- package/lib/types/client/live-paint.d.ts +40 -0
- package/lib/types/client/live-paint.js +74 -0
- package/lib/types/client/locales.d.ts +120 -0
- package/lib/types/client/locales.js +227 -0
- package/lib/types/client/member-configuration.d.ts +41 -0
- package/lib/types/client/member-configuration.js +114 -0
- package/lib/types/client/member-dock.d.ts +94 -0
- package/lib/types/client/member-dock.js +102 -0
- package/lib/types/client/member-feed.d.ts +24 -0
- package/lib/types/client/member-feed.js +134 -0
- package/lib/types/client/request-id.d.ts +3 -0
- package/lib/types/client/request-id.js +6 -0
- package/lib/types/client/settings-scope.d.ts +74 -0
- package/lib/types/client/settings-scope.js +98 -0
- package/lib/types/client/use-configuration-menu.d.ts +10 -0
- package/lib/types/client/use-configuration-menu.js +61 -0
- package/lib/types/container.d.ts +69 -0
- package/lib/types/container.js +129 -0
- package/lib/types/env.d.ts +17 -0
- package/lib/types/env.js +89 -0
- package/lib/types/gateway.d.ts +162 -0
- package/lib/types/gateway.js +470 -0
- package/lib/types/index.d.ts +1463 -0
- package/lib/types/index.js +2704 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +105 -0
- package/lib/types/live-flush.d.ts +21 -0
- package/lib/types/live-flush.js +59 -0
- package/lib/types/live-stream.d.ts +41 -0
- package/lib/types/live-stream.js +178 -0
- package/lib/types/member-bridge.d.ts +50 -0
- package/lib/types/member-bridge.js +216 -0
- package/lib/types/member-channel.d.ts +86 -0
- package/lib/types/member-channel.js +232 -0
- package/lib/types/member-control-storage.d.ts +10 -0
- package/lib/types/member-control-storage.js +100 -0
- package/lib/types/member-control.d.ts +62 -0
- package/lib/types/member-control.js +359 -0
- package/lib/types/member-controls.d.ts +23 -0
- package/lib/types/member-controls.js +143 -0
- package/lib/types/member-feed.d.ts +4 -0
- package/lib/types/member-feed.js +45 -0
- package/lib/types/member-inbox.d.ts +30 -0
- package/lib/types/member-inbox.js +225 -0
- package/lib/types/model-directory.d.ts +43 -0
- package/lib/types/model-directory.js +192 -0
- package/lib/types/prepared-members.d.ts +15 -0
- package/lib/types/prepared-members.js +61 -0
- package/lib/types/settings-face.d.ts +61 -0
- package/lib/types/settings-face.js +61 -0
- package/lib/types/types.d.ts +959 -0
- package/lib/types/types.js +24 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +112 -0
package/README.en.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# `@khorsheed/dsh-local-agent`
|
|
2
|
+
|
|
3
|
+
Model/effort controls, directories and visible member output share one member subscription per page, keeping multiple members and member-session navigation from multiplying HTTP/1 connections. Core queues busy selections for the next complete turn; frozen evaluation members reject changes.
|
|
4
|
+
|
|
5
|
+
Live member output uses a streaming Remote and public Conversation nodes. Browser updates are batched for at most 50ms, while incremental recovery checkpoints use an independent one-second cadence. Native final messages replace the transient presentation and retain usage, tools and session navigation. Reconnect obtains a fresh baseline; a host crash may lose the uncheckpointed tail, which remains visibly partial. Browser P95 acceptance remains part of the room coordinator proposal.
|
|
6
|
+
|
|
7
|
+
English | [中文](README.md)
|
|
8
|
+
|
|
9
|
+
Run locally installed coding-agent CLIs — Kimi Code, Codex, Claude Code — from the dsh web GUI. Each CLI gets an isolated home, slash commands for login/sessions/status/logout, and an auth section in Settings.
|
|
10
|
+
|
|
11
|
+
<img src="../../docs/screenshots/08-local-agent.png" width="480" alt="The Local Agent cards under Settings → Plugins, header dots showing each provider's auth state">
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Scoped homes per CLI** — isolated credential/session home under a shared root, created 0700; your native CLI installation is never touched.
|
|
16
|
+
- **Slash commands** — `/<harness> login|sessions|status|logout`, with the device-code login URL in the reply.
|
|
17
|
+
- **A settings card per provider** — on alpha.2 the bundle's own Plugins-page detail view (`plugins.bundle.config`, keyed by package name), on 0.1.5 Settings → Plugins → 可配置插件 (`settings.plugin.item`, keyed by the settings namespace): the auth status dot (visible on the collapsed 0.1.5 header), web login/sign-out, and the hot-swappable resident-mode (live) toggle with mirror granularity; cards compose this package's shared `ProviderAuthBlock`.
|
|
18
|
+
- **Subagent delegation** — hand work to a local CLI and resume it later, even across host restarts.
|
|
19
|
+
- **Member sessions continue in the sidebar (host 0.1.6+)** — the official subagent directory gained an "Open in sidebar" row action: member sessions are one-shot subagents, so opened aside, this family's MemberComposer is elected as the writable composer — a zero-change benefit of this package; 0.1.5 hosts have no such entry and behave as before.
|
|
20
|
+
- **Several logins per harness (named scopes)** — `/<harness> login --scope <name>` opens a second scoped home at `<homesRoot>/<harness>@<name>`: its own login, its own session records, its own `delegations.jsonl`, and nothing copied from the default one. An evaluation can therefore compare two accounts of one harness in a single run.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
Install the core together with at least one harness bundle (`@khorsheed/dsh-local-agent-kimi`, `-codex`): `dsh plugin add` activates only *direct* dependencies, so the harness bundle's transitive dependency alone will not mount this core.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
dsh plugin --profile web add @khorsheed/dsh-local-agent
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then restart the web instance. Uninstall:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
dsh plugin --profile web remove @khorsheed/dsh-local-agent
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Config
|
|
37
|
+
|
|
38
|
+
A custom composition mounts the core once:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
- id: local-agent
|
|
42
|
+
name: '@khorsheed/dsh-local-agent'
|
|
43
|
+
config:
|
|
44
|
+
homesRoot: !!js dshHomePath('local-agent')
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Compatibility
|
|
48
|
+
|
|
49
|
+
- npm release line (`@deepseek-ai/dsh@0.1.5-rc.1`): ✅ full — adapted to 0.1.5-rc.1 (format v2/v3; handle-based sessionPersistence), full build+test green; minHost moves up to 0.1.5-rc.1 — older hosts stay on the previous release line.
|
|
50
|
+
- source line (deepseek-harness master): ✅ (verifiedHost: 0.1.5-rc.1)
|
|
51
|
+
|
|
52
|
+
## Known Limitations
|
|
53
|
+
|
|
54
|
+
- **Login is a captured prompt or a manual handoff** — the web GUI has no interactive terminal: device-code harnesses (kimi/codex) surface the URL in the command reply while the CLI polls in the background; a harness whose auth is TTY-only (claude ≥2.1) declares the manual variant — `/login` replies with the exact command to run in the user's own terminal, and the registry watches the scoped home for the credential.
|
|
55
|
+
- **Homes root placement** — defaults to `$DSH_HOME/local-agent`, pending a standardized `var/state` layout.
|
|
56
|
+
- **Delegation log growth** — each scoped home's `delegations.jsonl` is append-only with no rotation.
|
|
57
|
+
- **One-sample shape** — the harness contract is induced from Kimi alone; not yet frozen.
|
|
58
|
+
- **The member channel authenticates on the per-run token alone** — the token is delivered through the CLI's scoped MCP config (the 0700 scoped home keeps other users out) and invalidated the moment the run settles; host 0.1.5 removed the child pid, so there is no second factor. **Residual exposure**: a same-host, same-user sibling member CLI (its model-driven bash) can read another member's token and replay it — the old pid check was a self-reported field and never stopped a deliberate forgery, so dropping it loses little; but a deliberately crafted cross-member call is now possible. Hardening (kernel-level socket peer credentials, or a spawn-time injected capability token) is tracked in `proposals/active/2026-09-10-member-channel-auth-hardening.md`.
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
<details>
|
|
63
|
+
<summary>Internals (click to expand)</summary>
|
|
64
|
+
|
|
65
|
+
Each harness registers into `ctx.localAgent`: a scoped home, an optional login declaration (a device-code command, or the manual-handoff variant for a TTY-only CLI), a session-records adapter, and optional auth-status and sign-out probes. The glue provisions each home and registers the `/<harness> login|sessions|status|logout` command family; per-harness differences are just `homeEnvVar`, the login invocation, the records adapter, and the auth/sign-out probes.
|
|
66
|
+
|
|
67
|
+
**Delegation stays out of this seam.** Each harness bundle mounts its own subagent-provider row into the existing `subagent` capability (subagent-acp for ACP-over-stdio harnesses, an app-server provider for Codex), reading the scoped home through `localAgent.homeDir(name)`.
|
|
68
|
+
|
|
69
|
+
**Program queries ride a read-only Remote channel.** A `LocalAgentGateway` (service key `localAgentGateway`, generated `./remote`) exposes roster, per-harness status, and scoped sessions to the browser. It emits no session events, so UI polls leave no command nodes in the session log; login and logout stay on the slash-command channel, where a visible command node is the expected feedback.
|
|
70
|
+
|
|
71
|
+
**Evaluation snapshot (`effectiveSettings`).** Every harness may declare a snapshot of the fairness-relevant settings currently in force: drive (exec/live), the sandbox or permission boundary (in each harness's own vocabulary — codex reports its sandbox policy, claude-code its permission mode, kimi its auto-approve state; a harness with no such knob omits the field, and the absence is itself the honest condition-hash input), the reasoning effort, the configured model (each harness reads its own configuration surface — kimi's `default_model`, codex's `model`, claude-code's scoped `settings.json`, dsh's inherited host selection; unreadable means the field drops out, never a guessed default), whether a non-default endpoint is pinned (hostname only, never the full URL), and the CLI version (asked of the CLI itself: `probeCliVersion` runs one `<cli> --version`, cached against the executable's resolved path + mtime + size, so an upgrade re-probes by itself while a missing CLI, a timeout, a non-zero exit, or output with no version-shaped token all just leave the field out). The snapshot is a live read — a person-edited scoped config reports the edited values — pure JSON, never credentials. `registry.effectiveSettings(name)` serves the evaluator's condition hash, and the same snapshot rides `/<harness> status` and the `LocalAgentStatus` Remote (additive fields; existing clients are unaffected).
|
|
72
|
+
|
|
73
|
+
**The credential status says only what is known (`credentialState`).** Beside the `authenticated` boolean, `LocalAgentStatus` carries a grade: `absent` (no credential record in the scoped home), `present-unverified` (a record exists, but nothing in this host process has exercised it — an expired, unrefreshable grant looks exactly like a working one, and saying so is the whole point of this grade), `verified` (a delegation round reached the endpoint and completed since the last login/logout), and `rejected` (a round's endpoint rejected the credential and no fresh login has rewritten the credential marker since). `authenticated` is unchanged and exactly equals `verified || present-unverified`, so the settings card and every existing client keep reading the boolean they always read. The grade is per host process: after a restart a present credential reports `present-unverified`, because that is what is actually known — an eager pre-run liveness probe is not this field's job. Providers call `reportAuthSuccess` when a round settles completed, the symmetric counterpart of the existing `reportAuthFailure`.
|
|
74
|
+
|
|
75
|
+
**Browser half ships in this package.** The `./client` export mounts automatically via the `dsh.client` manifest: the member composer (delegated child sessions stay writable) plus the shared settings-card building blocks (`ProviderAuthBlock`, the auth-status bus, `AuthStatusDot`) — each provider package's settings card (the alpha.2 `plugins.bundle.config` page, the 0.1.5 `settings.plugin.item` card) composes them, keeping the UI provider-neutral (it consumes only the `/<harness>` command family and the read-only gateway).
|
|
76
|
+
|
|
77
|
+
### Adding a harness
|
|
78
|
+
|
|
79
|
+
A harness bundle registers into the core and mounts its own delegation row:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
83
|
+
import type { LocalAgentSessionRecord } from '@khorsheed/dsh-local-agent'
|
|
84
|
+
|
|
85
|
+
const listCodexSessions = async (homeDir: string): Promise<readonly LocalAgentSessionRecord[]> => []
|
|
86
|
+
|
|
87
|
+
/** Register the Codex harness into the local-agent registry. */
|
|
88
|
+
export function registerCodex(ctx: Context): void {
|
|
89
|
+
ctx.localAgent.register({
|
|
90
|
+
name: 'codex', // command prefix + scoped-home dir
|
|
91
|
+
displayName: 'Codex',
|
|
92
|
+
homeEnvVar: 'CODEX_HOME',
|
|
93
|
+
delegationProvider: 'codex', // subagent provider name; cross-checked at load
|
|
94
|
+
login: { command: 'codex', args: ['login'] },
|
|
95
|
+
records: { listSessions: listCodexSessions },
|
|
96
|
+
// Optional: report auth from the scoped home and sign out of it, so
|
|
97
|
+
// /codex status|logout work and the settings tab can switch accounts.
|
|
98
|
+
isAuthenticated: async () => false,
|
|
99
|
+
logout: async () => {},
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`login` is optional: a harness without a device-code flow (e.g. dsh itself, which authenticates through the host instance's `DEEPSEEK_API_KEY`) omits it, and `/dsh login` answers that the harness has no login flow instead of spawning a CLI. Such harnesses still report `status` through `isAuthenticated` and list sessions normally; the status snapshot carries explicit `loginable`/`logoutable` capability flags so surfaces never offer an action the harness would reject.
|
|
105
|
+
|
|
106
|
+
### Delegation registry (resume carrier)
|
|
107
|
+
|
|
108
|
+
The registry also owns the family's **delegation registry**: a per-child-session record of which provider and CLI session a delegation used, plus a per-(parent, provider) FIFO of delegation intents. The family tool (`@khorsheed/dsh-local-agent-tool-subagent`, mounted by each harness bundle's patch) stages exactly one intent per call before `ctx.subagents.start()`, and the owning provider consumes exactly one per start — so fresh and resume rounds stay paired even under parallel delegation. A resume handle (the dsh child session id) resolves through the registry, which rejects a handle naming an unknown child, another parent's session, or the wrong provider; the subagent request descriptor cannot carry the target, so this service is the family-internal carrier. Mappings persist per harness in an append-only `delegations.jsonl` under the harness's scoped home (last line per child session wins), so a resume handle survives a host restart.
|
|
109
|
+
|
|
110
|
+
### Delegation call options (facade)
|
|
111
|
+
|
|
112
|
+
The `options` argument of `registry.start(parent, provider, prompt, options)` and `registry.resume(…)` (`DelegationCallOptions`) — purely additive, every field absent meaning the behavior from before it existed:
|
|
113
|
+
|
|
114
|
+
| Field | What it does |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `label` | Child display label; absent uses the harness's own display name |
|
|
117
|
+
| `signal` | The caller's own cancellation channel, fused with the facade's internal controller |
|
|
118
|
+
| `onProgress` | Per-call progress callback, the same payload the `localAgent/run-progress` event carries |
|
|
119
|
+
| `reattach` | `resume` only: restore a non-live child session from persistence (default true); `false` fails loud instead |
|
|
120
|
+
| `cwd` | The round's CLI working directory; a resume round must repeat the first round's, or it fails loud before any spawn |
|
|
121
|
+
| `exec` | Run this round inside an **already-acquired container**: `{ container, workdir, env? }` |
|
|
122
|
+
| `model` | The model THIS DELEGATION runs: accepted on `start` only — passing it to `resume` fails loud, because the first round's request is recorded and every later round re-requests it (none, when the first named none). Accepted for exec and live alike (the provider binds it when spawning the process/runtime). Order below |
|
|
123
|
+
| `scope` | Run this round against the harness's **named scoped home** (`<homesRoot>/<harness>@<scope>`); a resume round must repeat the first round's scope, absence included, or it fails loud |
|
|
124
|
+
|
|
125
|
+
**One settled observation per round.** A provider calls `recordRoundSettled` once its round's output stream is fully parsed, and the family turns it into one `settled` progress event: the round's `observedModel`, `cliVersion`, `usage`, and its `toolCalls` (`{ count, byName }`). Every field stays absent when the round did not yield it — never guessed, never zero-filled.
|
|
126
|
+
|
|
127
|
+
`toolCalls` is counted only from the transcript events the provider ALREADY parses — no new parse path and no second pass over the stream. `byName` keys are each CLI's own tool vocabulary (codex's `command_execution`, claude's `Bash`/`Read`, the tool names kimi's and dsh's own events carry), kept verbatim and **never normalized across harnesses**: cross-harness comparison is therefore `count` only, and `byName` is for a reader. Normalizing would invent an equivalence the CLIs never agreed to.
|
|
128
|
+
|
|
129
|
+
`observedModel` and `cliVersion` merge into the delegation record (which states the delegation's latest state); `toolCalls` rides the EVENT only — it belongs to one round, and merging it would silently overwrite the previous round's count with the newest one. A settle that lands before the record exists (the record point is each provider's own: a live round records at session/new, an exec round at the post-settle output parse) does not lose its observation either — the family stashes it and `recordDelegation` merges it when the record lands; both the exec and the live drive report.
|
|
130
|
+
|
|
131
|
+
**The model's five layers.** Which model a round starts its CLI with is the first of these that names one:
|
|
132
|
+
|
|
133
|
+
1. **the session-level override** (set through the member composer's model picker; in-memory, gone with a host restart)
|
|
134
|
+
2. **the delegation's own `model`** (the `start` call option; a resume round reads the same value back off the delegation record)
|
|
135
|
+
3. **the harness's `model` plugin-config key** (T30a, harness-wide)
|
|
136
|
+
4. **the harness's scoped configuration file** (codex's `model`, claude's `settings.json`, kimi's `default_model`; dsh inherits the host instance's default model selection)
|
|
137
|
+
5. **the CLI's own default**
|
|
138
|
+
|
|
139
|
+
All five absent means no model flag on the argv at all — byte for byte the behavior before any of these keys existed. Blank counts as absent.
|
|
140
|
+
|
|
141
|
+
**Why `resume` does not take a model.** A model belongs to the DELEGATION, not to one of its rounds: the first round records what it requested and every later round re-requests it. Switching mid-conversation is something the CLI would honour and the transcript would not show, so it is a caller error that fails loud rather than a silently ignored field. To switch models, use the member composer's picker (the session-level override below) — a layer the transcript can account for.
|
|
142
|
+
|
|
143
|
+
**Member model and reasoning configuration.** The member composer and Room coordinator share the core panel, showing current applied values, pending selections and preparation errors. The compact menu shows model and effort, and selections submit immediately; inheritance and CLI defaults remain backend compatibility for existing configurations. Settings and invitations retain a saved choice, otherwise selecting the first visible directory entry; settings still require Save. The selector shows model and effort without custom IDs, catalog details or persistent provenance diagnostics; catalogs still update in the background. Settings and member menus dismiss on outside interaction or Escape and adapt their placement to available space. Missing native effort options show an unavailable explanation rather than guessed choices. Busy selections prepare after the current complete turn and tool continuations settle. Intent, revisions and pending state are durable and reconciled against native state after restart; users can cancel pending selections or retry failed preparation. Frozen evaluation members reject changes. Legacy `setMemberModel` uses the same controller instead of a separate busy-refusal path.
|
|
144
|
+
|
|
145
|
+
`effectiveSettings.model` still reports the answer from **the settings layer down** — "what would a round with no model of its own run", which is the harness-wide setting the condition snapshot is asking about; delegation records and session overrides never enter the snapshot.
|
|
146
|
+
|
|
147
|
+
**Container delegation (the `exec` target).** Given one, the provider spawns `docker exec -w <workdir> [-e NAME…] <container> <the same argv>`; stdio stays piped, and the stream parse, settle, readback and `delegations.jsonl` record are byte-for-byte the host path's. The family owns exactly one docker verb, `exec` — acquiring, mounting and destroying a container belong to the caller (lab).
|
|
148
|
+
|
|
149
|
+
- **Values never ride the argv.** Each forwarded variable appears only as `-e NAME`, and the docker CLI resolves it from its own environment — so a credential the provider resolved stays out of the host process table. What is forwarded: the defined entries of the provider's explicit env layer, overridden per key by `target.env`; the ambient allowlist (`PATH`, `HOME`, the proxy variables) is **not** forwarded — inside the container those belong to the image and the `docker run` that created it.
|
|
150
|
+
- **The caller must name the scoped home.** `target.env` must carry the in-container scoped-home variable (`CODEX_HOME` / `CLAUDE_CONFIG_DIR` / `KIMI_CODE_HOME` / `DSH_HOME`), or the round fails loud before spawning: the host path means nothing inside the unit, and forwarding it would start the CLI from an empty directory — no credentials, no rollout to read back, and nothing in the output naming the cause.
|
|
151
|
+
- **The scoped home is a HOST directory, bind-mounted read-write.** Readback (codex's rollout, kimi's wire log, the sub-dsh session log) reads it straight off the host filesystem, and credential refreshes land back on the host. **The caller stages what it mounts**: mount a live scoped home and its host-only settings ride along (measured: the `https_proxy` in claude's scoped `settings.json`, meant for the host daemon, points at nothing inside the unit and the round dies with `Connection refused`).
|
|
152
|
+
- **A container round is exec-only and carries no member channel.** The live drivers run resident processes on the HOST — the transport the target exists to replace; the member bridge is a host unix socket whose MCP declaration names a host node path. Both are given up on purpose, not missing by accident.
|
|
153
|
+
- **The caller repeats the target on resume.** The recorded anchor is the host `cwd`, which a swapped container leaves equal — that one the record cannot catch.
|
|
154
|
+
|
|
155
|
+
### Named scopes: several scoped homes per harness
|
|
156
|
+
|
|
157
|
+
The default scoped home is `<homesRoot>/<harness>` — byte for byte the directory it always was. Give a NAME (`[a-z0-9-]` only; a scope is a name, never a path) and you get a SIBLING of it: `<homesRoot>/<harness>@<name>`. Not a child, because that directory belongs to the harness's own CLI, and a second state tree inside it is something that CLI will eventually prune or misread.
|
|
158
|
+
|
|
159
|
+
- **Materialized lazily.** The directory is created the first time anything names the scope (`/<harness> login|status|sessions|logout --scope <name>`, a delegation carrying `scope`, the evaluation's mount source): `mkdir` 0700, then the harness's own provisioning (codex's `config.toml`, kimi's provider/model config and permission rules, claude's scoped home, dsh's sub-profile). The default scope's provisioning is unmoved — each harness bundle's `apply` still owns it.
|
|
160
|
+
- **Provisioning one scope deliberately: `provisionScope(name, scope, options)`.** The materialization above is a side effect of naming a directory: once per (harness, scope) per host process, no options, a failure that is only a log line. This verb is the deliberate one — it **waits** (including on a materialization it may have just started, since both writers write the same files), it **throws** (a question asked must have an answer, and a failure must not read as a success), and it hands back the harness's **read-back**. Its only option today is `preset`, because a preset is a property of the SUBJECT rather than of the deployment: when an evaluation condition says "this scope composes this preset", the condition document is what knows, and plugin config has one answer per instance. Only dsh reads it; the other three take the parameter and ignore it, which keeps their provisioning byte-identical.
|
|
161
|
+
- **Credentials are never copied.** A new scope is empty: `status` reports `credentialState: absent` and a delegation fails exactly as it does today, so using one starts with `/<harness> login --scope <name>`. claude's keychain item is keyed by the config directory's path, so a named scope gets its own item for free — the one part of this that is path-safe by nature.
|
|
162
|
+
- **Everything follows the directory.** That scope's session records, its `delegations.jsonl`, its effective-settings snapshot (the harness's `effectiveSettings(homeDir)` takes the directory as a parameter), its CLI-version probe, its credential grade and its delegation read-back all live in its own directory. The delegation record carries the `scope`, and a resume round is anchored to it: another scope — or none, when the first round had one — is refused before any process spawns. Continuing one CLI session under another account's credentials is not a mistake that can be repaired afterwards.
|
|
163
|
+
- **Boundaries (named absences, not gaps).** A scoped delegation is **exec-only**: the resident drivers (codex's app-server, claude's and kimi's ACP, the sub-dsh `serve`) are bound per member to the DEFAULT scoped home, so a scoped round meeting an active live driver is refused rather than quietly downgraded. kimi's member-bridge declaration is written INTO a scoped home's `mcp.json` and `member-bridge.sock` is a single homes-root socket, so a scoped kimi round carries no member channel. dsh's sub-profile follows the directory and needs nothing special.
|
|
164
|
+
|
|
165
|
+
### Active-delegation registry and `/local-agent stop`
|
|
166
|
+
|
|
167
|
+
The registry also owns the **active-delegation registry**: an in-flight run table keyed by dsh child session id. Facade-started runs (`start`/`resume` — the member composer, room, and other programmatic entry points) and runs the family tool starts directly through `ctx.subagents.start()` (registered via `trackDelegationRun`) all land in the same table, and an entry clears itself when the run's result settles. `/local-agent stop <childSessionId>` cancels the in-flight delegation by this table — semantics aligned with the official `subagents.interrupt(targetSessionId)`: fire-and-return (the cancel signal goes out before the reply), and an absent target (unknown child or no in-flight run) is an explicitly-named accepted no-op, not an error. This gives surfaces like taskpilot a landing point for their stop buttons: for a one-shot subagent row with no live agent, the button dispatches `/local-agent stop <childSessionId>` instead, degrading to an explicit "cannot stop" error when the local-agent core is absent.
|
|
168
|
+
|
|
169
|
+
### Model Experience
|
|
170
|
+
|
|
171
|
+
**What the model sees** — nothing from the registry itself: `/<harness>` command replies and the `/local-agent list` roster text are user-visible command text, never model prompts. Model-visible effects start only when a harness bundle mounts a subagent provider; the parent then sees the child's final answer through the subagent tool result.
|
|
172
|
+
|
|
173
|
+
**Token effect** — command discovery, execution, and reply text add no model tokens. Delegation tokens belong to the harness bundle's provider, which pays for an independent child context.
|
|
174
|
+
|
|
175
|
+
**KV Cache effect** — registry metadata and command replies never enter a model request and do not affect its cache; a delegated child owns its cache independently.
|
|
176
|
+
|
|
177
|
+
</details>
|
|
178
|
+
|
|
179
|
+
## Development
|
|
180
|
+
|
|
181
|
+
Part of the [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo (`packages/local-agent`). Issues and contributions welcome there.
|
package/README.i18n.yaml
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# `@khorsheed/dsh-local-agent`
|
|
2
|
+
|
|
3
|
+
模型/effort 配置、模型目录与可见成员的实时输出在每个页面共用一条成员订阅,避免多成员及成员会话入口占满 HTTP/1 连接池。运行中选项由 core 排到下一完整轮次,冻结评测成员拒绝变更。
|
|
4
|
+
|
|
5
|
+
成员实时输出通过流式 Remote 与公开 Conversation 节点呈现。浏览器更新最多合并 50ms,增量恢复检查点独立按一秒节奏保存。原生最终消息替换临时展示,保留用量、工具与会话导航。重连读取新基线;宿主崩溃可能丢失未落检查点的末段,恢复内容明确标为部分记录。浏览器 P95 仍需在 room 协调者提案中验收。
|
|
6
|
+
|
|
7
|
+
[English](README.en.md) | 中文
|
|
8
|
+
|
|
9
|
+
从 dsh web GUI 使用本机安装的编码 agent CLI——Kimi Code、Codex、Claude Code。每个 CLI 获得一个隔离的作用域目录、一组登录/会话/状态/退出的斜杠命令,以及设置里的认证分区。
|
|
10
|
+
|
|
11
|
+
<img src="../../docs/screenshots/08-local-agent.png" width="480" alt="设置 → 插件配置里的 Local Agent 卡片,卡头状态点一眼可见各 provider 授权状态">
|
|
12
|
+
|
|
13
|
+
## 特性
|
|
14
|
+
|
|
15
|
+
- **每个 CLI 一个作用域目录**——共享 homes 根下的隔离凭据/会话目录,以 0700 创建;你的本地 CLI 安装绝不被动到。
|
|
16
|
+
- **斜杠命令族**——`/<harness> login|sessions|status|logout`,device-code 登录 URL 通过命令回复呈现。
|
|
17
|
+
- **每 provider 一张设置卡片**——alpha.2 落在各 bundle 的插件详情页(`plugins.bundle.config`,按包名派发),0.1.5 落在设置 → 插件 → 插件配置(`settings.plugin.item`,按设置命名空间派发):认证状态点(0.1.5 卡头可见)、网页登录/退出、常驻模式(live)热切开关与输出粒度;卡片直接复用本包 client 面的共享 `ProviderAuthBlock`。
|
|
18
|
+
- **子 agent 委派**——把会话工作交给本机 CLI 并在之后 resume,宿主重启也能续上。
|
|
19
|
+
- **成员会话侧栏续写(宿主 0.1.6 起)**——官方 subagent 目录每行新增「在侧边栏打开」入口:成员会话本就是 one-shot subagent,侧栏内本家族 MemberComposer 自动当选为可写 composer,本包零改动受益;0.1.5 宿主没有该入口,行为不变。
|
|
20
|
+
- **一家多份登录(命名 scope)**——`/<harness> login --scope <名>` 在 `<homesRoot>/<家名>@<名>` 里另开一份作用域目录:各自登录、各自会话记录、各自 `delegations.jsonl`,凭证不复制。评测因此能在同一次 run 里比较同一家的两个账号。
|
|
21
|
+
|
|
22
|
+
## 安装
|
|
23
|
+
|
|
24
|
+
core 本身就是 bundle;请把它与至少一个 harness bundle(`@khorsheed/dsh-local-agent-kimi`、`-codex`)一起安装——`dsh plugin add` 只激活**直接**依赖,仅靠 harness bundle 的传递依赖不会挂载本 core。
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
dsh plugin --profile web add @khorsheed/dsh-local-agent
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
然后重启 web 实例。卸载:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
dsh plugin --profile web remove @khorsheed/dsh-local-agent
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 配置
|
|
37
|
+
|
|
38
|
+
自定义组合挂载一次 core:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
- id: local-agent
|
|
42
|
+
name: '@khorsheed/dsh-local-agent'
|
|
43
|
+
config:
|
|
44
|
+
homesRoot: !!js dshHomePath('local-agent')
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Compatibility
|
|
48
|
+
|
|
49
|
+
- npm 发布线(`@deepseek-ai/dsh@0.1.5-rc.1`):✅ 完整——适配 0.1.5-rc.1(format v2/v3;handle 制 sessionPersistence),全量构建测试通过;minHost 前移至 0.1.5-rc.1,旧宿主请停留在旧发布线。
|
|
50
|
+
- 源码线(deepseek-harness master):✅(verifiedHost: 0.1.5-rc.1)
|
|
51
|
+
|
|
52
|
+
## 已知限制
|
|
53
|
+
|
|
54
|
+
- **登录为抓取式 prompt 或人工交接**——web GUI 没有交互式终端面:device-code harness(kimi/codex)的 URL 通过命令回复呈现、CLI 在后台轮询;认证只在 TTY 可用的 harness(claude ≥2.1)声明 manual 变体——`/login` 回复用户在自己终端运行的完整命令,registry 监听作用域目录识别登录完成。
|
|
55
|
+
- **homes 根位置**——默认 `$DSH_HOME/local-agent`,待 `var/state` 布局标准化后再议。
|
|
56
|
+
- **委派日志增长**——每个作用域目录的 `delegations.jsonl` 只增不减、无轮转。
|
|
57
|
+
- **单样本形状**——harness 契约仅由 Kimi 归纳,尚未冻结。
|
|
58
|
+
- **成员通道是 per-run token 单因子认证**——token 经 CLI 作用域 MCP 配置下发(0700 的 scoped home 挡住其他用户),run 落定即焚;宿主 0.1.5 移除子进程 pid 后没有第二因子。**残余风险**:同机同用户的兄弟成员 CLI(其模型驱动的 bash)能读到另一个成员的 token 并回放它——原 pid 校验也是自报字段,本就不防刻意伪造,去掉它没有实质降级;但刻意构造的跨成员调用现在是可能的。加固(socket 内核级 peer 凭证,或 spawn 时注入的 capability token)由 `proposals/active/2026-09-10-member-channel-auth-hardening.md` 跟踪。
|
|
59
|
+
|
|
60
|
+
## 实现原理
|
|
61
|
+
|
|
62
|
+
<details>
|
|
63
|
+
<summary>内部结构(点击展开)</summary>
|
|
64
|
+
|
|
65
|
+
每个 harness 向 `ctx.localAgent` 注册:一个作用域目录、一个可选的登录声明(device-code 命令,或 TTY-only CLI 的 manual 交接变体)、一个会话记录适配器,以及可选的认证状态与退出登录探测。glue 供给每个作用域目录并注册 `/<harness> login|sessions|status|logout` 命令族;harness 间差异只剩 `homeEnvVar`、登录调用、records 适配器与认证/退出探测。
|
|
66
|
+
|
|
67
|
+
**委派不属于这个 seam。** 每个 harness bundle 各自向既有的 `subagent` 能力挂载 subagent-provider 行(讲 stdio ACP 的 harness 用 subagent-acp,Codex 用其 app-server provider),经 `localAgent.homeDir(name)` 读取作用域目录。
|
|
68
|
+
|
|
69
|
+
**程序查询走只读 Remote 通道。** `LocalAgentGateway`(服务键 `localAgentGateway`,生成物 `./remote`)通过 Typert Gateway 向浏览器暴露 roster、各 harness 状态与作用域会话。它不产生任何会话事件,因此 UI 轮询不会在会话日志里留下命令节点;登录与退出仍走斜杠命令通道——用户主动操作产生可见命令节点正是预期反馈。
|
|
70
|
+
|
|
71
|
+
**评测快照(effectiveSettings)。** 每个 harness 可声明一份当前生效的公平性相关设置:drive(exec/live)、沙箱或权限模式(各用自家词汇:codex 报 sandbox 策略、claude 报 permissionMode、kimi 报自动批准与否;没有该旋钮的 harness 字段缺位,缺位本身就是诚实的条件输入)、推理强度、已配置模型(各家读自家配置面:kimi 的 `default_model`、codex 的 `model`、claude 的作用域 `settings.json`、dsh 继承的宿主选择;读不到就不给字段,绝不猜默认值)、端点是否固定(只报主机名,绝不报完整 URL)、CLI 版本(问 CLI 自己:`probeCliVersion` 跑一次 `<cli> --version`,按可执行文件的解析路径+mtime+大小缓存,所以一次升级会自动重探,而没装、超时、非零退出、输出里没有版本形状的 token 都只是字段缺位)。快照是实时读——人改过的作用域配置报人改过的值——纯 JSON、绝不含凭证,`registry.effectiveSettings(name)` 供编排器读条件哈希,`/<harness> status` 与 `LocalAgentStatus` Remote 附带同一份(增量字段,旧客户端不受影响)。
|
|
72
|
+
|
|
73
|
+
**凭证只说知道的(credentialState)。** `LocalAgentStatus` 除 `authenticated` 布尔外还带一档 `credentialState`:`absent`(作用域家目录里没有凭证记录)、`present-unverified`(有记录,但本宿主进程里还没有任何一轮委派碰过它——一份过期且刷不动的凭证和一份能用的凭证形状完全一样,说不知道正是这一档的意义)、`verified`(自上次登录/登出以来有一轮委派真的打通了端点并完成)、`rejected`(某轮的端点拒了这份凭证,且此后没有新登录重写凭证标记)。`authenticated` 保持不变,恒等于 `verified || present-unverified`,所以设置卡片与既有客户端读到的还是原来那个布尔。这一档是每宿主进程的:重启后一份在场的凭证回到 `present-unverified`,因为那才是当下真正知道的;开跑前的主动活性探测不归这个字段。provider 在一轮 settle 成 completed 时调 `reportAuthSuccess`,与既有的 `reportAuthFailure` 对称。
|
|
74
|
+
|
|
75
|
+
**浏览器半身随本包提供。** `./client` 导出通过本包的 `dsh.client` manifest 自动挂载:成员 composer(委派的子会话可继续对话)+ 共享设置卡片构件(`ProviderAuthBlock`、认证状态总线、`AuthStatusDot`)——各 provider 包的设置卡片(alpha.2 `plugins.bundle.config` 页、0.1.5 `settings.plugin.item` 卡)直接组合它们,UI 保持 provider 无关(只消费 `/<harness>` 命令族和只读 gateway)。
|
|
76
|
+
|
|
77
|
+
### 新增一个 harness
|
|
78
|
+
|
|
79
|
+
harness bundle 向 core 注册,并挂载自己的委派行:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
83
|
+
import type { LocalAgentSessionRecord } from '@khorsheed/dsh-local-agent'
|
|
84
|
+
|
|
85
|
+
const listCodexSessions = async (homeDir: string): Promise<readonly LocalAgentSessionRecord[]> => []
|
|
86
|
+
|
|
87
|
+
/** Register the Codex harness into the local-agent registry. */
|
|
88
|
+
export function registerCodex(ctx: Context): void {
|
|
89
|
+
ctx.localAgent.register({
|
|
90
|
+
name: 'codex', // command prefix + scoped-home dir
|
|
91
|
+
displayName: 'Codex',
|
|
92
|
+
homeEnvVar: 'CODEX_HOME',
|
|
93
|
+
delegationProvider: 'codex', // subagent provider name; cross-checked at load
|
|
94
|
+
login: { command: 'codex', args: ['login'] },
|
|
95
|
+
records: { listSessions: listCodexSessions },
|
|
96
|
+
// Optional: report auth from the scoped home and sign out of it, so
|
|
97
|
+
// /codex status|logout work and the settings tab can switch accounts.
|
|
98
|
+
isAuthenticated: async () => false,
|
|
99
|
+
logout: async () => {},
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`login` 是可选的:没有登录流程的 harness(例如 dsh 自身——它通过宿主实例的 `DEEPSEEK_API_KEY` 认证,而非 device-code 流程)省略它,`/dsh login` 会回答"该 harness 无登录流程"而不是 spawn 一个 CLI。这类 harness 仍通过 `isAuthenticated` 报告 `status`,并正常列出会话;status 快照携带显式的 `loginable`/`logoutable` 能力标志,设置界面等表面因此不会提供会得到报错回复的操作。
|
|
105
|
+
|
|
106
|
+
### 委派 registry(resume 载体)
|
|
107
|
+
|
|
108
|
+
registry 还持有家族的**委派 registry**:每个子会话一条记录,记下该委派用的 provider 与 CLI 会话,以及按 (parent, provider) 分组的委派 intent FIFO。家族工具(`@khorsheed/dsh-local-agent-tool-subagent`,由各 harness bundle 的 patch 挂载)在每次调用 `ctx.subagents.start()` 前恰好 stage 一个 intent,归属 provider 每次 start 恰好消费一个——因此即使并行委派,fresh 轮与 resume 轮也能正确配对。resume 轮的句柄(dsh 子会话 id)经 registry 解析,凡是未知子会话、他人 parent 的会话、或错误 provider 的句柄都会被拒绝;subagent 请求 descriptor 无法携带该目标,因此本服务就是家族内部的载体。映射按 harness 持久化在其作用域目录下的 append-only `delegations.jsonl`(同一子会话最后一行生效),resume 句柄因此能跨宿主重启存活。
|
|
109
|
+
|
|
110
|
+
### 委派调用选项(facade)
|
|
111
|
+
|
|
112
|
+
`registry.start(parent, provider, prompt, options)` 与 `registry.resume(…)` 的 `options`(`DelegationCallOptions`)——纯增量,每个字段缺位就是它出现之前的行为:
|
|
113
|
+
|
|
114
|
+
| 字段 | 作用 |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `label` | 子会话显示名;缺位用 harness 自己的显示名 |
|
|
117
|
+
| `signal` | 调用方自己的取消通道,与 facade 内部 controller 熔合 |
|
|
118
|
+
| `onProgress` | 本次调用的进度回调,与 `localAgent/run-progress` 事件同样的载荷 |
|
|
119
|
+
| `reattach` | 仅 `resume`:子会话不在线时从持久化恢复(默认 true),传 `false` 则 fail loud |
|
|
120
|
+
| `cwd` | 本轮 CLI 的工作目录;resume 轮必须与首轮记录一致,否则进程启动前 fail loud |
|
|
121
|
+
| `exec` | 本轮在**已取得的容器里**跑:`{ container, workdir, env? }` |
|
|
122
|
+
| `model` | 本**次委派**跑哪个模型:只在 `start` 上接受,`resume` 传了即 fail loud——首轮请求的值记进委派记录,之后每轮照它重发(首轮没写就一直不写)。exec 与 live 都接受(provider 在起进程/runtime 时绑定它)。取值顺序见下 |
|
|
123
|
+
| `scope` | 本轮跑该家的**命名作用域目录**(`<homesRoot>/<家名>@<scope>`);resume 轮必须与首轮记录的 scope 一致(含"都没有"),否则 fail loud |
|
|
124
|
+
|
|
125
|
+
**每轮的 settle 观测。** provider 在一轮的输出流解析完之后调用 `recordRoundSettled`,家族把它转成一条 `settled` 进度事件:本轮的 `observedModel`、`cliVersion`、`usage`,以及本轮的 `toolCalls`(`{ count, byName }`)。每个字段取不到就缺位,绝不猜、绝不补零。
|
|
126
|
+
|
|
127
|
+
`toolCalls` 只从 provider **已经解析过**的转录事件里数出来——不新开解析路径、不再走一遍流。`byName` 的键是各家 CLI 自己的工具词汇(codex 的 `command_execution`、claude 的 `Bash`/`Read`、kimi 与 dsh 各自事件里的工具名),原样保留、**不跨家归一**:因此横比只比 `count`,`byName` 是给人读的。归一化会凭空造出一份 CLI 之间从未约定过的等价关系。
|
|
128
|
+
|
|
129
|
+
`observedModel` 与 `cliVersion` 会并进委派记录(记录讲的是这次委派的最新状态),`toolCalls` **只走事件**:它属于某一轮,合并进记录等于用最新一轮悄悄盖掉上一轮的计数。settle 先于记录到达(记录点在各 provider 自己手里:live 轮在 session/new 时记录,exec 轮在 settle 后解析输出时记录)也不丢观测——家族先暂存,`recordDelegation` 落地时并入;exec 与 live 两条驱动路径都会上报。
|
|
130
|
+
|
|
131
|
+
**模型的五层取值顺序。** 一轮 CLI 用哪个模型,按固定顺序取第一个有值的:
|
|
132
|
+
|
|
133
|
+
1. **会话级覆盖**(成员 composer 的模型切换器设置;存内存,宿主重启即失效)
|
|
134
|
+
2. **本次委派的 `model`**(`start` 的调用选项;resume 轮从委派记录里读回同一个值)
|
|
135
|
+
3. **该家插件配置的 `model` 键**(T30a,按 provider 全局生效)
|
|
136
|
+
4. **该家作用域配置文件**(codex 的 `model`、claude 的 `settings.json`、kimi 的 `default_model`、dsh 继承宿主实例的默认模型选择)
|
|
137
|
+
5. **CLI 自己的默认**
|
|
138
|
+
|
|
139
|
+
五层都没有 = argv 上一个模型参数都不加,与这些键出现之前逐字节相同。空白算没写。
|
|
140
|
+
|
|
141
|
+
**为什么 resume 不接受 model。** 模型属于这次**委派**,不属于它的某一轮:首轮请求什么就记什么,之后每轮照发。中途换模型 CLI 会照办,而转录里看不出来——所以那是调用方的错误,fail loud,而不是被悄悄忽略的字段。要换模型走成员 composer 的切换器(下面的会话级覆盖),它在转录里是有迹可循的一层。
|
|
142
|
+
|
|
143
|
+
**成员输入栏的模型与推理配置。** 成员输入栏与 Room 协调者入口共用 core 的配置面板,显示本轮有效值、待生效选择及准备错误。常用菜单仅展示模型与强度,选择即提交;继承与 CLI 默认语义只保留为旧配置的后台兼容。设置和邀请保留已有选择,未选择时采用目录第一个可见候选;设置仍需保存。选择菜单只显示模型与强度,不提供自定义 ID、目录详情或常驻的来源诊断;目录仍在后台自动更新。设置与成员菜单均支持点击外部、按 Escape 关闭,浮层按可用空间调整方向。没有原生强度选项时显示不可用说明,不猜测候选。忙时可以选择,新值在当前完整轮次及工具续跑结束后准备生效。配置意图、版本与待生效项持久化,重启后核对原生状态;用户可撤销待生效项或重试失败准备。冻结评测成员禁止变更。legacy `setMemberModel` 接入同一控制器,不再保留忙时拒绝的另一套语义。
|
|
144
|
+
|
|
145
|
+
`effectiveSettings.model` 报的仍是**设置层往下**的答案——「一轮没有自带模型时会跑什么」,也就是条件快照要问的那个 harness 级设置;委派记录与会话覆盖不进快照。
|
|
146
|
+
|
|
147
|
+
**容器内委派(`exec` 目标)。** 给了它,provider 把 argv 换成 `docker exec -w <workdir> [-e NAME…] <container> <原 argv>`;stdio 仍是 pipe,流解析、settle、回读、`delegations.jsonl` 记录全部与宿主路径逐字节相同。家族只用 `exec` 这一个 docker 动词——取得、挂载、销毁容器是调用方(lab)的事。
|
|
148
|
+
|
|
149
|
+
- **值不上 argv。** 每个转发的变量只以 `-e NAME` 出现,值留在 docker 客户端自己的环境里由它解析——provider 解析出的凭据因此不进宿主进程表。转发集 = provider 显式 env 层里有值的项,按键被 `target.env` 覆盖;`PATH`/`HOME`/代理这些走继承白名单的变量**不转发**,容器里它们属于镜像与 `docker run`。
|
|
150
|
+
- **作用域目录必须由调用方点名。** `target.env` 必须给出容器内的作用域目录变量(`CODEX_HOME` / `CLAUDE_CONFIG_DIR` / `KIMI_CODE_HOME` / `DSH_HOME`),否则进程启动前 fail loud:宿主路径在容器里什么都不是,照转会让 CLI 从一个空目录起步——没凭据、没有可回读的 rollout,而且失败原因不出现在任何输出里。
|
|
151
|
+
- **作用域目录是宿主目录、rw bind 挂进容器。** 回读(codex 的 rollout、kimi 的 wire log、子 dsh 的会话日志)直接读宿主文件系统,凭据续期也回写到宿主目录。**挂什么由调用方备好**:把活的作用域目录整个挂进去,宿主专用的设置会跟着进去(实测:claude 作用域 `settings.json` 里给宿主守护进程用的 `https_proxy` 在容器里指向不存在的地址,本轮当场 `Connection refused`)。
|
|
152
|
+
- **容器轮是 exec-only、且没有成员通道。** 长驻驱动跑的是宿主上的常驻进程,正是 `exec` 目标要替换的传输;成员桥是宿主 unix socket,其 MCP 声明还带着宿主 node 路径。两者都是明确放弃,不是没接上。
|
|
153
|
+
- **resume 由调用方重复同一个目标。** 记录里的锚是宿主 `cwd`,换了容器它仍然相等——这一条记录抓不到。
|
|
154
|
+
|
|
155
|
+
### 命名 scope:一家多份作用域目录
|
|
156
|
+
|
|
157
|
+
缺省作用域目录是 `<homesRoot>/<家名>`——逐字节还是那一份。给一个**名字**(只允许 `[a-z0-9-]`,是名字不是路径)就得到与它**同级**的另一份:`<homesRoot>/<家名>@<名>`。不嵌在缺省目录里面,因为那份目录归各家 CLI 自己管,往里塞第二棵状态树迟早被它自己清掉或读串。
|
|
158
|
+
|
|
159
|
+
- **惰性建立。** 目录在第一次被点名时创建(`/<家> login|status|sessions|logout --scope <名>`、带 `scope` 的委派、评测的挂载源都算点名):`mkdir` 0700,然后跑该家自己的 provision(codex 的 `config.toml`、kimi 的 provider/model 配置与权限、claude 的作用域目录、dsh 的子 profile)。缺省目录的 provision 时机不变——仍由各 harness bundle 的 apply 负责。
|
|
160
|
+
- **刻意 provision 一个 scope:`provisionScope(家, 名, 选项)`。** 上面那次是「有人点了这个目录的名」的副作用:每进程每 (家, scope) 只跑一次、不带选项、失败只留一行日志。`provisionScope` 是刻意的那一次——它**等**(并且先等掉上面那次可能刚启动的那一遍,两个写者写的是同一批文件)、**抛**(问了要有答复,失败不能读成成功),并把该家的**回读**交回来。选项目前只有一项 `preset`:preset 是**受试对象**的属性而不是部署的,一条评测条件说「这个 scope 组这个 preset」时,知道答案的是条件文档,而插件 config 一个实例只有一份。只有 dsh 读这一项;另三家收下参数并忽略,provisioning 因此逐字节不变。
|
|
161
|
+
- **凭证不复制。** 新 scope 是空的:`status` 报 `credentialState: absent`,委派按今天的规则失败,要用它先 `/<家> login --scope <名>` 登一次。claude 的 keychain 项按配置目录路径哈希,命名 scope 因此自动拿到自己的项——这是四家里唯一天然按路径安全的部分。
|
|
162
|
+
- **一切按目录走。** 该 scope 的会话记录、`delegations.jsonl`、effective-settings 快照(harness 的 `effectiveSettings(homeDir)` 收目录参数)、CLI 版本探测、登录态与委派回读,全部落在它自己的目录里。委派记录带 `scope`,resume 轮拿它当锚:换了 scope(或首轮有、这轮没有)在进程启动前就拒绝——继续同一个 CLI 会话却换了账号,是事后修不回来的那种错。
|
|
163
|
+
- **边界(明确放弃,不是没接上)。** 带 scope 的委派**只走 exec**:常驻驱动(codex 的 app-server、claude/kimi 的 ACP、子 dsh 的 serve)按成员绑的是缺省作用域目录,撞上 live 直接拒绝而不是悄悄降级。kimi 的成员桥声明写在作用域目录的 `mcp.json` 里、`member-bridge.sock` 又是 homes 根级单例,所以带 scope 的 kimi 轮不带成员通道。dsh 的子 profile 随目录走,不需要特殊处理。
|
|
164
|
+
|
|
165
|
+
### 活跃委派 registry 与 `/local-agent stop`
|
|
166
|
+
|
|
167
|
+
registry 另持有**活跃委派 registry**(以 dsh 子会话 id 为键的在飞 run 表):facade 启动的 run(`start`/`resume`——成员 composer、room 等程序化入口)与家族工具直接 `ctx.subagents.start()` 启动的 run(经 `trackDelegationRun` 登记)都落在同一张表里,条目在 run 结果 settle 时自清。`/local-agent stop <childSessionId>` 命令按这张表取消在飞的委派——语义对齐官方 `subagents.interrupt(targetSessionId)`:fire-and-return(发出取消信号即回复),目标缺席(未知子会话或无在飞 run)是显式说明的 accepted no-op,而非报错。这为 taskpilot 等表面提供了停止按钮的落点:对没有 live agent 的一次性子代理行,按钮改发 `/local-agent stop <childSessionId>`,local-agent 缺席时降级为无法停止的明确报错。
|
|
168
|
+
|
|
169
|
+
### Model Experience
|
|
170
|
+
|
|
171
|
+
**模型看到什么**——registry 本身不提交任何内容:`/<harness>` 命令回复与 `/local-agent list` roster 文本都是用户可见的命令文本,绝不是模型 prompt。模型可见效果只从 harness bundle 挂载 subagent provider 开始;父级随后通过 subagent 工具结果看到子会话的最终回答。
|
|
172
|
+
|
|
173
|
+
**Token 影响**——命令发现、执行与回复文本不产生任何模型 token。委派 token 属于 harness bundle 的 provider,由它为一个独立的子上下文付费。
|
|
174
|
+
|
|
175
|
+
**KV Cache 影响**——registry 元数据与命令回复从不进入模型请求、不影响其缓存;被委派的子会话独立拥有自己的缓存。
|
|
176
|
+
|
|
177
|
+
</details>
|
|
178
|
+
|
|
179
|
+
## 开发
|
|
180
|
+
|
|
181
|
+
隶属 [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo(`packages/local-agent`)。问题与贡献请移步该仓库。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# The dsh-local-agent framework bundle patch: the family core row. Every
|
|
2
|
+
# harness bundle (@khorsheed/dsh-local-agent-kimi, -codex, -claude-code, -dsh)
|
|
3
|
+
# declares this package as an npm dependency, which guarantees the core's
|
|
4
|
+
# module resolves in the profile — it does NOT mount this row. `dsh plugin add`
|
|
5
|
+
# reconciles only the profile's *direct* dependencies into the bundles layer
|
|
6
|
+
# (apps/cli plugin.ts reconcilePlugins), so the core must be installed
|
|
7
|
+
# explicitly alongside a harness bundle: `dsh plugin add .../dsh-local-agent`
|
|
8
|
+
# and then `dsh plugin add .../dsh-local-agent-<harness>`. Installing a harness
|
|
9
|
+
# bundle alone leaves this row unmounted and that provider pending (it never
|
|
10
|
+
# crashes) — see the family README's install section.
|
|
11
|
+
#
|
|
12
|
+
# The core row must NOT be re-inserted by any harness bundle's patch: two
|
|
13
|
+
# top-level `insert` rows with the same id would mount the core twice as soon
|
|
14
|
+
# as two providers are installed.
|
|
15
|
+
|
|
16
|
+
- insert:
|
|
17
|
+
- id: local-agent
|
|
18
|
+
name: '@khorsheed/dsh-local-agent'
|
|
19
|
+
config:
|
|
20
|
+
# Shared scoped-homes root; override in a later patch layer.
|
|
21
|
+
homesRoot: !!js dshHomePath('local-agent')
|