@khorsheed/dsh-local-agent-claude-code 0.2.0-rc.1 → 0.2.0-rc.3
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/CHANGELOG.md +11 -0
- package/README.en.md +35 -29
- package/README.i18n.yaml +2 -2
- package/README.md +31 -29
- package/lib/client.js +843 -316
- package/lib/index.js +1491 -1520
- package/lib/types/claude-cli-provider.d.ts +13 -4
- package/lib/types/claude-cli-provider.js +61 -49
- package/lib/types/client/SettingsCard.d.ts +75 -24
- package/lib/types/client/SettingsCard.js +106 -128
- package/lib/types/client/index.d.ts +12 -25
- package/lib/types/client/index.js +135 -50
- package/lib/types/client/locales.d.ts +36 -24
- package/lib/types/client/locales.js +40 -28
- package/lib/types/endpoint-auth.d.ts +181 -0
- package/lib/types/endpoint-auth.js +281 -0
- package/lib/types/index.d.ts +32 -16
- package/lib/types/index.js +128 -62
- package/lib/types/live-driver.d.ts +9 -1
- package/lib/types/live-driver.js +18 -18
- package/lib/types/live-switch.d.ts +4 -1
- package/lib/types/model-broker.d.ts +4 -2
- package/lib/types/model-broker.js +19 -8
- package/lib/types/model-catalog.d.ts +35 -6
- package/lib/types/model-catalog.js +58 -68
- package/lib/types/provision.d.ts +34 -27
- package/lib/types/provision.js +49 -31
- package/lib/types/records.d.ts +4 -62
- package/lib/types/records.js +5 -382
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +20 -21
- package/lib/invariant.js +0 -31
- package/lib/types/invariant.d.ts +0 -16
- package/lib/types/invariant.js +0 -33
package/lib/types/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Claude Code harness instantiation of the local-agent family. Registers the
|
|
3
3
|
* `claude-code` harness into the core registry: scoped home under the shared
|
|
4
|
-
* homes root (`CLAUDE_CONFIG_DIR`),
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* session records. Delegation mounts through
|
|
8
|
-
* (one-shot `claude -p
|
|
9
|
-
* bundle patch's `tool-subagent-claude-code-local` row puts the tool on
|
|
10
|
-
* profile root, so every agent preset can delegate.
|
|
4
|
+
* homes root (`CLAUDE_CONFIG_DIR`), endpoint-key authentication (the scope's
|
|
5
|
+
* `endpoint-auth.json` over the plugin config's `baseUrl`/`apiKey`/
|
|
6
|
+
* `authToken` triple — no OAuth, no browser flow, nothing to authorize inside
|
|
7
|
+
* a container), and project-file session records. Delegation mounts through
|
|
8
|
+
* the `claude-local` provider (one-shot `claude -p` under the scoped home);
|
|
9
|
+
* the bundle patch's `tool-subagent-claude-code-local` row puts the tool on
|
|
10
|
+
* the profile root, so every agent preset can delegate.
|
|
11
11
|
*
|
|
12
12
|
* @module @khorsheed/dsh-local-agent-claude-code
|
|
13
13
|
*/
|
|
@@ -17,7 +17,7 @@ import z from '@deepseek-ai/schemastery';
|
|
|
17
17
|
export declare const name = "local-agent-claude-code";
|
|
18
18
|
/** Services required before the harness can register. */
|
|
19
19
|
export declare const inject: string[];
|
|
20
|
-
/** Plugin config: the permission mode fresh delegations default to, plus the live driver. */
|
|
20
|
+
/** Plugin config: the permission mode fresh delegations default to, the endpoint credential, plus the live driver. */
|
|
21
21
|
export interface Config {
|
|
22
22
|
/**
|
|
23
23
|
* `claude -p` permission handling; `skip` passes
|
|
@@ -36,17 +36,33 @@ export interface Config {
|
|
|
36
36
|
*/
|
|
37
37
|
model?: string;
|
|
38
38
|
/**
|
|
39
|
-
* `ANTHROPIC_BASE_URL` for
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* The deployment-level endpoint: `ANTHROPIC_BASE_URL` for every delegation
|
|
40
|
+
* (e.g. a relay like `https://openproxy.to/claude`, or absent for the
|
|
41
|
+
* official API). A scope's own `endpoint-auth.json` overrides it field by
|
|
42
|
+
* field — the settings card's credential form edits THAT file (never the
|
|
43
|
+
* profile). See endpoint-auth.ts for the whole auth model — there is no
|
|
44
|
+
* OAuth path in this package.
|
|
43
45
|
*/
|
|
44
46
|
baseUrl?: string;
|
|
45
47
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* The deployment-level credential, sent as `ANTHROPIC_API_KEY` (the
|
|
49
|
+
* `x-api-key` header). Keep it in the LOCAL profile row only — never commit
|
|
50
|
+
* it (the repo hygiene gate blocks credential-shaped strings, but the
|
|
51
|
+
* profile is not the repo's business).
|
|
52
|
+
*/
|
|
53
|
+
apiKey?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The deployment-level credential, sent as `ANTHROPIC_AUTH_TOKEN` (the
|
|
56
|
+
* `Authorization: Bearer` header) — some relays expect Bearer instead of
|
|
57
|
+
* `x-api-key`. When both keys are set the CLI's own precedence applies.
|
|
58
|
+
*/
|
|
59
|
+
authToken?: string;
|
|
60
|
+
/**
|
|
61
|
+
* HTTP proxy for the child CLI's own traffic, provisioned into the scoped
|
|
62
|
+
* `settings.json` env block. Needed when the host process environment
|
|
63
|
+
* carries no proxy (a supervisor-spawned instance does not inherit the
|
|
64
|
+
* user's shell exports). Distinct from {@link baseUrl}: this is a local
|
|
65
|
+
* traffic forwarder, not the model endpoint.
|
|
50
66
|
*/
|
|
51
67
|
proxyUrl?: string;
|
|
52
68
|
/**
|
package/lib/types/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Claude Code harness instantiation of the local-agent family. Registers the
|
|
3
3
|
* `claude-code` harness into the core registry: scoped home under the shared
|
|
4
|
-
* homes root (`CLAUDE_CONFIG_DIR`),
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* session records. Delegation mounts through
|
|
8
|
-
* (one-shot `claude -p
|
|
9
|
-
* bundle patch's `tool-subagent-claude-code-local` row puts the tool on
|
|
10
|
-
* profile root, so every agent preset can delegate.
|
|
4
|
+
* homes root (`CLAUDE_CONFIG_DIR`), endpoint-key authentication (the scope's
|
|
5
|
+
* `endpoint-auth.json` over the plugin config's `baseUrl`/`apiKey`/
|
|
6
|
+
* `authToken` triple — no OAuth, no browser flow, nothing to authorize inside
|
|
7
|
+
* a container), and project-file session records. Delegation mounts through
|
|
8
|
+
* the `claude-local` provider (one-shot `claude -p` under the scoped home);
|
|
9
|
+
* the bundle patch's `tool-subagent-claude-code-local` row puts the tool on
|
|
10
|
+
* the profile root, so every agent preset can delegate.
|
|
11
11
|
*
|
|
12
12
|
* @module @khorsheed/dsh-local-agent-claude-code
|
|
13
13
|
*/
|
|
@@ -15,19 +15,22 @@ import z from '@deepseek-ai/schemastery';
|
|
|
15
15
|
import { settingsFace, vol } from '@khorsheed/dsh-local-agent';
|
|
16
16
|
import { endpointHost } from '@khorsheed/dsh-local-agent/types';
|
|
17
17
|
import { ClaudeCliProvider, claudeCliVersion } from './claude-cli-provider.js';
|
|
18
|
+
import { checkClaudeEndpoint, clearEndpointAuth, endpointAuthFileExists, endpointAuthStamp, hasEndpointCredential, readEndpointAuthFile, resolveEndpointAuth, writeEndpointAuth, } from './endpoint-auth.js';
|
|
18
19
|
import { DEFAULT_LIVE_IDLE_MS } from './live-driver.js';
|
|
19
20
|
import { LiveDriverSwitch } from './live-switch.js';
|
|
20
21
|
import { ClaudeModelCatalog } from './model-catalog.js';
|
|
21
22
|
import { ClaudeModelBroker, ClaudeScopedModelMemory } from './model-broker.js';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
23
|
+
import { firstModelOf, readModelList, readModelListSync, writeModelList } from '@khorsheed/dsh-local-agent';
|
|
24
|
+
import { listClaudeSessions, readClaudeTranscriptModel } from './records.js';
|
|
25
|
+
import { provisionClaudeHome, readClaudeConfiguredModel } from './provision.js';
|
|
24
26
|
/** Stable Cordis plugin name; the bundle patch row id. */
|
|
25
27
|
export const name = 'local-agent-claude-code';
|
|
26
28
|
/** Services required before the harness can register. */
|
|
27
29
|
export const inject = ['localAgent', 'subagents', 'subprocess', 'settings'];
|
|
28
30
|
/**
|
|
29
31
|
* The settings fields (`live`, `liveMirrorGranularity`, `model`,
|
|
30
|
-
* `recentModels
|
|
32
|
+
* `recentModels`, and the endpoint credential `baseUrl`/`apiKey`/`authToken`)
|
|
33
|
+
* ride the plugin Config, marked `.volatile()` where the
|
|
31
34
|
* host's schemastery has it (3.18.4+, the rc.1 line — probed, never sniffed):
|
|
32
35
|
* there the SettingsForms writes hot-track the running fiber's Volatile
|
|
33
36
|
* references without a remount, and the official one-shot import moves an old
|
|
@@ -58,6 +61,8 @@ export const Config = z.object({
|
|
|
58
61
|
]),
|
|
59
62
|
model: volatilize(SETTINGS_FIELDS.model),
|
|
60
63
|
baseUrl: z.string(),
|
|
64
|
+
apiKey: z.string(),
|
|
65
|
+
authToken: z.string(),
|
|
61
66
|
proxyUrl: z.string(),
|
|
62
67
|
live: volatilize(SETTINGS_FIELDS.live),
|
|
63
68
|
liveIdleMs: z.number().default(DEFAULT_LIVE_IDLE_MS),
|
|
@@ -85,7 +90,6 @@ const CLAUDE_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
|
|
|
85
90
|
export function apply(ctx, config) {
|
|
86
91
|
ctx.effect(() => {
|
|
87
92
|
const permissionMode = config.permissionMode ?? DEFAULT_PERMISSION_MODE;
|
|
88
|
-
const baseUrl = config.baseUrl;
|
|
89
93
|
const homeDir = ctx.localAgent.homeDir('claude-code');
|
|
90
94
|
// Claude creates the scoped home lazily; create it eagerly so the
|
|
91
95
|
// harness's homeDir contract is uniform with the other harnesses.
|
|
@@ -123,15 +127,28 @@ export function apply(ctx, config) {
|
|
|
123
127
|
legacyBase,
|
|
124
128
|
read: readSettings,
|
|
125
129
|
});
|
|
126
|
-
// The
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
const
|
|
130
|
+
// The endpoint credential's deployment level: the plugin config triple,
|
|
131
|
+
// merged per field UNDER the scope's own endpoint-auth.json at resolve
|
|
132
|
+
// time. Resolved PER READ against the scope's home — a card write (which
|
|
133
|
+
// lands in the file through the endpointAuth face) takes effect on the
|
|
134
|
+
// next round with no reload.
|
|
135
|
+
const endpointConfig = {
|
|
136
|
+
...config.baseUrl === undefined ? {} : { baseUrl: config.baseUrl },
|
|
137
|
+
...config.apiKey === undefined ? {} : { apiKey: config.apiKey },
|
|
138
|
+
...config.authToken === undefined ? {} : { authToken: config.authToken },
|
|
139
|
+
};
|
|
140
|
+
const resolveEndpoint = (scopeHome) => resolveEndpointAuth(scopeHome, endpointConfig);
|
|
141
|
+
// The memberless model key: the plugin-config/settings `model`, blank = unset.
|
|
142
|
+
const settingsModelKey = () => {
|
|
132
143
|
const model = face.get().model?.trim();
|
|
133
144
|
return model === undefined || model === '' ? undefined : model;
|
|
134
145
|
};
|
|
146
|
+
// The harness's default model for a scope: the curated model list's
|
|
147
|
+
// FIRST entry (models.json, edited on the card), ahead of the settings
|
|
148
|
+
// key; an empty list leaves the pre-existing chain untouched. Read sync
|
|
149
|
+
// per call — one small file, and a card write always reaches the next
|
|
150
|
+
// round.
|
|
151
|
+
const resolveModel = (scope) => firstModelOf(readModelListSync(ctx.localAgent.homeDir('claude-code', scope))) ?? settingsModelKey();
|
|
135
152
|
// The member model layers above the settings key: the session-level
|
|
136
153
|
// override map (the composer's picker writes it through the broker; both
|
|
137
154
|
// round paths and the live spawn read it) and the scoped-settings.json
|
|
@@ -152,18 +169,24 @@ export function apply(ctx, config) {
|
|
|
152
169
|
const named = delegationModel?.trim();
|
|
153
170
|
if (named !== undefined && named !== '')
|
|
154
171
|
return named;
|
|
155
|
-
|
|
172
|
+
// The member's scope decides whose list answers: a named scope's rounds
|
|
173
|
+
// read that scope's models.json, exactly like its endpoint credential.
|
|
174
|
+
const record = ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId);
|
|
175
|
+
return resolveModel(record?.scope);
|
|
156
176
|
};
|
|
157
177
|
const liveSwitch = new LiveDriverSwitch(ctx, face, {
|
|
158
178
|
...config.permissionMode === undefined ? {} : { permissionMode: config.permissionMode },
|
|
159
|
-
|
|
179
|
+
endpoint: resolveEndpoint,
|
|
160
180
|
...config.liveIdleMs === undefined ? {} : { liveIdleMs: config.liveIdleMs },
|
|
161
181
|
model: effectiveModel,
|
|
162
182
|
provisionModel: (home, model) => scopedModelMemory.provision(home, model),
|
|
163
183
|
});
|
|
164
184
|
const modelCatalog = new ClaudeModelCatalog({
|
|
165
|
-
|
|
166
|
-
|
|
185
|
+
endpoint: resolveEndpoint,
|
|
186
|
+
// Re-key on the config baseUrl; the scope file's own edits (the card's
|
|
187
|
+
// credential writes land there) are covered by its fingerprint in the
|
|
188
|
+
// key's file list.
|
|
189
|
+
endpointLabel: () => config.baseUrl,
|
|
167
190
|
warn: message => ctx.logger.warn(message),
|
|
168
191
|
});
|
|
169
192
|
const modelContext = (childSessionId) => {
|
|
@@ -177,6 +200,7 @@ export function apply(ctx, config) {
|
|
|
177
200
|
return { home, directory: refresh => refresh ? modelCatalog.refresh(home, binding.cwd) : modelCatalog.read(home, binding.cwd) };
|
|
178
201
|
},
|
|
179
202
|
settingsModel: resolveModel,
|
|
203
|
+
modelList: scope => readModelListSync(ctx.localAgent.homeDir('claude-code', scope)),
|
|
180
204
|
cliDefault: childSessionId => {
|
|
181
205
|
const context = modelContext(childSessionId);
|
|
182
206
|
return context.home === homeDir ? scopedModelMemory.cliDefault(homeDir) : readClaudeConfiguredModel(context.home);
|
|
@@ -198,7 +222,7 @@ export function apply(ctx, config) {
|
|
|
198
222
|
// live-settle report.
|
|
199
223
|
transcriptModel: (cliSessionId, childSessionId) => readClaudeTranscriptModel(modelContext(childSessionId).home, cliSessionId),
|
|
200
224
|
});
|
|
201
|
-
const disposeProvider = ctx.subagents.registerProvider(new ClaudeCliProvider(ctx, permissionMode,
|
|
225
|
+
const disposeProvider = ctx.subagents.registerProvider(new ClaudeCliProvider(ctx, permissionMode, resolveEndpoint, liveSwitch.resolve, effectiveModel));
|
|
202
226
|
const disposeHarness = ctx.localAgent.register({
|
|
203
227
|
prepareMember: async ({ binding, childSession, configuration, signal }) => {
|
|
204
228
|
const driver = liveSwitch.resolve(binding.childSessionId);
|
|
@@ -211,57 +235,90 @@ export function apply(ctx, config) {
|
|
|
211
235
|
displayName: 'Claude Code',
|
|
212
236
|
homeEnvVar: 'CLAUDE_CONFIG_DIR',
|
|
213
237
|
delegationProvider: 'claude-local',
|
|
214
|
-
login:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// via /claude-code code <value>. The relay env is scrubbed and the
|
|
219
|
-
// scoped home pinned so the credential lands where delegations read
|
|
220
|
-
// it (the wrapper passes the harness env through).
|
|
221
|
-
// The argv pins the config directory itself (an `env NAME=VALUE`
|
|
222
|
-
// assignment outranks the spawn env), so it is built from the
|
|
223
|
-
// directory being logged in: `/claude-code login --scope <name>`
|
|
224
|
-
// authorizes THAT scope, not the default one.
|
|
225
|
-
pty: {
|
|
226
|
-
command: 'env',
|
|
227
|
-
args: (scopedHome) => ['-u', 'ANTHROPIC_API_KEY', '-u', 'ANTHROPIC_BASE_URL', `CLAUDE_CONFIG_DIR=${scopedHome}`, 'claude', 'auth', 'login'],
|
|
228
|
-
},
|
|
229
|
-
// The watch syncs the keychain credential into the runtime-readable
|
|
230
|
-
// file first (claude 2.1.236 writes keychain but reads the file),
|
|
231
|
-
// then probes.
|
|
232
|
-
watch: async (home) => {
|
|
233
|
-
await syncClaudeCredentialFile(home, message => { ctx.logger.warn(message); });
|
|
234
|
-
return claudeAuthenticated(home, message => { ctx.logger.warn(message); });
|
|
235
|
-
},
|
|
236
|
-
},
|
|
238
|
+
// No `login` declaration: the harness authenticates by endpoint key
|
|
239
|
+
// (endpoint-auth.ts), so the core reports the harness as not loginable
|
|
240
|
+
// and `/claude-code login` answers that there is no login flow — no
|
|
241
|
+
// browser, no OAuth, nothing to authorize inside a container.
|
|
237
242
|
records: { listSessions: homeDir => listClaudeSessions(homeDir) },
|
|
238
243
|
// A NAMED scope's directory is provisioned through this hook when the
|
|
239
244
|
// registry materializes it — the same eager creation and settings the
|
|
240
245
|
// default scope gets from the apply above.
|
|
241
246
|
provision: scopedHome => provisionClaudeHome(scopedHome, config.proxyUrl).then(() => { }),
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
247
|
+
// Endpoint-key presence: the scope's endpoint-auth.json merged over the
|
|
248
|
+
// plugin config must carry a non-empty key. The verdict rides the
|
|
249
|
+
// family's credential grades untouched: presence grades
|
|
250
|
+
// `present-unverified`, a completed round grades `verified`, a round's
|
|
251
|
+
// 401 grades `rejected` until the endpoint file's mtime moves past the
|
|
252
|
+
// mark (editing the file IS the re-login gesture).
|
|
253
|
+
isAuthenticated: home => resolveEndpoint(home).then(endpoint => hasEndpointCredential(endpoint)),
|
|
254
|
+
// The settings card's verify button: a cheap token-free probe of the
|
|
255
|
+
// SAVED endpoint (the reply tells the user to save first when a
|
|
256
|
+
// credential is missing). Session-less via the gateway Remote.
|
|
257
|
+
checkEndpoint: home => resolveEndpoint(home).then(endpoint => checkClaudeEndpoint(endpoint)),
|
|
258
|
+
// The settings card's credential form: read the resolved per-field
|
|
259
|
+
// state (secrets as booleans), write edits into the scope's own
|
|
260
|
+
// endpoint-auth.json — the harness's 0600 store, never the profile
|
|
261
|
+
// patch document. A cleared field falls back to the config triple.
|
|
262
|
+
endpointAuth: {
|
|
263
|
+
read: async (home) => {
|
|
264
|
+
const endpoint = await resolveEndpoint(home);
|
|
265
|
+
return {
|
|
266
|
+
...endpoint.baseUrl === undefined ? {} : { baseUrl: endpoint.baseUrl },
|
|
267
|
+
apiKeySet: endpoint.apiKey !== undefined,
|
|
268
|
+
authTokenSet: endpoint.authToken !== undefined,
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
write: async (home, fields) => {
|
|
272
|
+
const current = await readEndpointAuthFile(home) ?? {};
|
|
273
|
+
const next = { ...current };
|
|
274
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
275
|
+
if (value === undefined)
|
|
276
|
+
continue;
|
|
277
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
278
|
+
if (value === null || trimmed === '')
|
|
279
|
+
delete next[field];
|
|
280
|
+
else
|
|
281
|
+
next[field] = trimmed;
|
|
282
|
+
}
|
|
283
|
+
if (Object.keys(next).length === 0)
|
|
284
|
+
await clearEndpointAuth(home);
|
|
285
|
+
else
|
|
286
|
+
await writeEndpointAuth(home, next);
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
credentialStamp: endpointAuthStamp,
|
|
290
|
+
// Logout clears the scope's OWN endpoint file. A config-sourced
|
|
291
|
+
// credential outlives it — said out loud, or the status would flip
|
|
292
|
+
// right back and read as a failed logout.
|
|
293
|
+
logout: async (home) => {
|
|
294
|
+
if (await endpointAuthFileExists(home)) {
|
|
295
|
+
await clearEndpointAuth(home);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
throw new Error('claude-code has no scoped endpoint-auth.json to clear — the credential comes from the settings card or the profile row (baseUrl/apiKey/authToken); clear it there instead');
|
|
299
|
+
},
|
|
248
300
|
// The model surface: the settings card reads the memberless info ("what
|
|
249
301
|
// would a round run with"), the member composer reads and switches a
|
|
250
302
|
// member's session-level override through the same broker.
|
|
251
303
|
modelBroker,
|
|
304
|
+
// The settings card's curated model list (the default scope's
|
|
305
|
+
// models.json; named scopes carry their own, read per round).
|
|
306
|
+
modelList: {
|
|
307
|
+
read: home => readModelList(home),
|
|
308
|
+
write: (home, models) => writeModelList(home, models),
|
|
309
|
+
},
|
|
252
310
|
// The eval snapshot: the permission mode is the plugin config resolved
|
|
253
311
|
// at apply (it selects the spawn flags); the endpoint mirrors the
|
|
254
|
-
// provider's own resolution
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
// settings that scope's rounds would run with.
|
|
312
|
+
// provider's own resolution — the scope's file over the config triple.
|
|
313
|
+
// The model follows the family's fixed order: the plugin config key (it
|
|
314
|
+
// rides every argv) before the scoped settings.json's own `model`. The
|
|
315
|
+
// CLI's own default model is never guessed, so neither one configured
|
|
316
|
+
// means no field. The directory is a PARAMETER, not the apply-time
|
|
317
|
+
// capture: a status read (or an evaluation snapshot) of a named scope
|
|
318
|
+
// must report the settings that scope's rounds would run with.
|
|
262
319
|
effectiveSettings: async (scopedHome) => {
|
|
263
|
-
const
|
|
264
|
-
const baseUrlHost =
|
|
320
|
+
const endpoint = await resolveEndpoint(scopedHome);
|
|
321
|
+
const baseUrlHost = endpoint.baseUrl !== undefined ? endpointHost(endpoint.baseUrl) : undefined;
|
|
265
322
|
const [scopedModel, cliVersion] = await Promise.all([
|
|
266
323
|
readClaudeConfiguredModel(scopedHome).catch(() => undefined),
|
|
267
324
|
claudeCliVersion(ctx, scopedHome).catch(() => undefined),
|
|
@@ -270,7 +327,7 @@ export function apply(ctx, config) {
|
|
|
270
327
|
return {
|
|
271
328
|
drive: face.get().live ? 'live' : 'exec',
|
|
272
329
|
permissionMode,
|
|
273
|
-
baseUrlSet:
|
|
330
|
+
baseUrlSet: endpoint.baseUrl !== undefined,
|
|
274
331
|
...baseUrlHost !== undefined ? { baseUrlHost } : {},
|
|
275
332
|
...model !== undefined ? { model } : {},
|
|
276
333
|
...cliVersion !== undefined ? { cliVersion } : {},
|
|
@@ -287,6 +344,15 @@ export function apply(ctx, config) {
|
|
|
287
344
|
: records.map(r => `${r.id} ${r.workDir}`).join('\n'),
|
|
288
345
|
}));
|
|
289
346
|
}
|
|
347
|
+
// `/claude-code check`: the settings card's verify button — a cheap
|
|
348
|
+
// token-free probe of the SAVED endpoint (the reply tells the user to
|
|
349
|
+
// save first when a credential is missing).
|
|
350
|
+
if (verb === 'check') {
|
|
351
|
+
const scopeHome = invocation.agent.ctx.localAgent.homeDir('claude-code');
|
|
352
|
+
return resolveEndpoint(scopeHome)
|
|
353
|
+
.then(endpoint => checkClaudeEndpoint(endpoint))
|
|
354
|
+
.then(text => ({ kind: 'success', text }));
|
|
355
|
+
}
|
|
290
356
|
return undefined;
|
|
291
357
|
},
|
|
292
358
|
});
|
|
@@ -3,6 +3,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
3
3
|
import type { Session } from '@deepseek-ai/dsh-session';
|
|
4
4
|
import { type SubagentRun, type SubagentStartRequest } from '@deepseek-ai/dsh-subagent';
|
|
5
5
|
import type { Config } from './index.ts';
|
|
6
|
+
import type { ClaudeEndpointAuth } from './endpoint-auth.ts';
|
|
6
7
|
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
7
8
|
export declare const DEFAULT_LIVE_IDLE_MS: number;
|
|
8
9
|
/** Default timeout for the turn's system/init ack (a cold boot can be slow). */
|
|
@@ -85,7 +86,14 @@ export declare class ClaudeLiveDriver {
|
|
|
85
86
|
*/
|
|
86
87
|
private draining;
|
|
87
88
|
private readonly disposeController;
|
|
88
|
-
constructor(ctx: Context, config: Pick<Config, 'permissionMode'
|
|
89
|
+
constructor(ctx: Context, config: Pick<Config, 'permissionMode'> & {
|
|
90
|
+
/**
|
|
91
|
+
* Resolver for the spawn's endpoint credential (endpoint-auth.ts: the
|
|
92
|
+
* member's scoped endpoint-auth.json over the plugin config triple),
|
|
93
|
+
* read per RUNTIME SPAWN — a key rotation reaches the resident process
|
|
94
|
+
* at its next respawn. Absent, no `ANTHROPIC_*` variables are set.
|
|
95
|
+
*/
|
|
96
|
+
endpoint?: (homeDir: string) => Promise<ClaudeEndpointAuth>;
|
|
89
97
|
liveIdleMs?: number;
|
|
90
98
|
liveMirrorGranularity?: ClaudeLiveMirrorGranularity;
|
|
91
99
|
/** Maximum batching wait for incremental streaming messages. */
|
package/lib/types/live-driver.js
CHANGED
|
@@ -12,12 +12,10 @@ import { claudeDirectory } from './model-catalog.js';
|
|
|
12
12
|
* continuable) and the same-shape push stream folded live per turn.
|
|
13
13
|
*
|
|
14
14
|
* Auth discipline (a hard requirement): the resident process runs under the
|
|
15
|
-
* family's scoped `CLAUDE_CONFIG_DIR`
|
|
16
|
-
* the
|
|
17
|
-
*
|
|
18
|
-
* never touches the user's global `~/.claude
|
|
19
|
-
* and only adds `ANTHROPIC_BASE_URL` when the plugin config supplies it (the
|
|
20
|
-
* exec path's exact env rule).
|
|
15
|
+
* family's scoped `CLAUDE_CONFIG_DIR` plus the resolved endpoint credential's
|
|
16
|
+
* `ANTHROPIC_*` variables (the scope's endpoint-auth.json over the plugin
|
|
17
|
+
* config — the exec path's exact env rule, resolved per spawn). The driver
|
|
18
|
+
* never touches the user's global `~/.claude` and never runs any `auth` verb.
|
|
21
19
|
*
|
|
22
20
|
* Protocol facts (all probed): the first stdin `user` message triggers
|
|
23
21
|
* `system/init` carrying the server-assigned session id (EVERY turn emits a
|
|
@@ -62,11 +60,13 @@ import { claudeDirectory } from './model-catalog.js';
|
|
|
62
60
|
*/
|
|
63
61
|
import { ClaudeControlRequests } from './control-requests.js';
|
|
64
62
|
import { StringDecoder } from 'node:string_decoder';
|
|
63
|
+
import { randomUUID } from 'node:crypto';
|
|
65
64
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
65
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
66
66
|
import { settleRunResult, subprocessRunHandle, } from '@deepseek-ai/dsh-subagent';
|
|
67
67
|
import { delegationEnv, persistChildSession, LiveStreamPublisher, LiveFlush, LIVE_FLUSH_INTERVAL_MS } from '@khorsheed/dsh-local-agent';
|
|
68
68
|
import { appendClaudeTranscriptLine, assistantEvent, claudeLineText, ClaudeStreamParser, claudeVersionFromInit, DEFAULT_DISPOSE_GRACE_MS, registerClaudeMemberRun, textTask, toolCallsOf, } from './claude-cli-provider.js';
|
|
69
|
-
import {
|
|
69
|
+
import { endpointAuthEnv } from './endpoint-auth.js';
|
|
70
70
|
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
71
71
|
export const DEFAULT_LIVE_IDLE_MS = 30 * 60_000;
|
|
72
72
|
/** Default timeout for the turn's system/init ack (a cold boot can be slow). */
|
|
@@ -419,14 +419,10 @@ export class ClaudeLiveDriver {
|
|
|
419
419
|
if (this.disposed) {
|
|
420
420
|
throw new LiveChannelUnavailableError('the live driver is disposed');
|
|
421
421
|
}
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
// next spawn). The login watch's sync alone cannot keep a resident
|
|
427
|
-
// runtime authenticatable across rotations. Best-effort: a missing grant
|
|
428
|
-
// fails the turn with the CLI's own "Not logged in", not here.
|
|
429
|
-
await syncClaudeCredentialFile(spec.homeDir, message => { this.ctx.logger.warn(message); }).catch(() => false);
|
|
422
|
+
// The endpoint credential is resolved per spawn (the scope's
|
|
423
|
+
// endpoint-auth.json over the plugin config): a key rotation reaches the
|
|
424
|
+
// resident runtime at its next respawn, with no reload and no reconcile.
|
|
425
|
+
const endpoint = await this.config.endpoint?.(spec.homeDir);
|
|
430
426
|
// The member bridge rides the spawn argv (`--mcp-config` + `--allowedTools`),
|
|
431
427
|
// exactly as in the exec path; the resident process serves one member, so
|
|
432
428
|
// its token lives with the process.
|
|
@@ -460,11 +456,11 @@ export class ClaudeLiveDriver {
|
|
|
460
456
|
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' },
|
|
461
457
|
graceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
462
458
|
// The auth discipline: exactly the exec path's env — the scoped config
|
|
463
|
-
// dir
|
|
459
|
+
// dir plus the resolved endpoint credential's `ANTHROPIC_*` variables.
|
|
464
460
|
env: delegationEnv({
|
|
465
461
|
CLAUDE_CONFIG_DIR: spec.homeDir,
|
|
466
462
|
...spec.configuration === undefined ? {} : { CLAUDE_CODE_EFFORT_LEVEL: spec.configuration.effort ?? 'auto' },
|
|
467
|
-
...
|
|
463
|
+
...endpointAuthEnv(endpoint),
|
|
468
464
|
}),
|
|
469
465
|
};
|
|
470
466
|
let child;
|
|
@@ -1039,7 +1035,11 @@ export class ClaudeLiveDriver {
|
|
|
1039
1035
|
// never hold it hostage — a cancel returns immediately.
|
|
1040
1036
|
await Promise.race([accepted, abortBranch.catch(() => undefined)]);
|
|
1041
1037
|
return subprocessRunHandle({
|
|
1042
|
-
|
|
1038
|
+
// Host 0.2.1: a fresh PER-EXECUTION run id — the activation manager
|
|
1039
|
+
// rejects a run id naming a live Session (this child session is live),
|
|
1040
|
+
// and resume rounds reuse the child session id. The family's durable
|
|
1041
|
+
// tracking key (the child session id) rides the staged intent instead.
|
|
1042
|
+
id: SessionId(randomUUID()),
|
|
1043
1043
|
result,
|
|
1044
1044
|
signal: request.signal,
|
|
1045
1045
|
onAbort,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { Context } from '@deepseek-ai/cordis';
|
|
14
14
|
import type { SettingsFace } from '@khorsheed/dsh-local-agent';
|
|
15
15
|
import type { Config } from './index.ts';
|
|
16
|
+
import type { ClaudeEndpointAuth } from './endpoint-auth.ts';
|
|
16
17
|
import { ClaudeLiveDriver } from './live-driver.ts';
|
|
17
18
|
import type { ClaudeLiveMirrorGranularity } from './live-driver.ts';
|
|
18
19
|
/** The resolved live settings (rc.1: volatile row config; 0.1.5: schema defaults ← YAML base ← user layer). */
|
|
@@ -25,8 +26,10 @@ export interface ClaudeLiveSettings {
|
|
|
25
26
|
recentModels?: readonly string[];
|
|
26
27
|
}
|
|
27
28
|
/** The Cordis-config inputs every driver generation shares. */
|
|
28
|
-
type DriverBase = Pick<Config, 'permissionMode'
|
|
29
|
+
type DriverBase = Pick<Config, 'permissionMode'> & {
|
|
29
30
|
liveIdleMs?: number;
|
|
31
|
+
/** Per-spawn endpoint credential resolver, handed to every driver generation. */
|
|
32
|
+
endpoint?: (homeDir: string) => Promise<ClaudeEndpointAuth>;
|
|
30
33
|
/**
|
|
31
34
|
* Per-spawn member model resolver, handed to every driver generation:
|
|
32
35
|
* answers the member's effective model from its session-level override, the
|
|
@@ -70,8 +70,10 @@ export interface ClaudeModelBrokerDeps {
|
|
|
70
70
|
home: string;
|
|
71
71
|
directory: (refresh?: boolean) => LocalAgentModelDirectory | Promise<LocalAgentModelDirectory>;
|
|
72
72
|
};
|
|
73
|
-
/** The settings layer: the plugin-config model, read per call. */
|
|
74
|
-
readonly settingsModel: () => string | undefined;
|
|
73
|
+
/** The settings layer: the curated list's first entry, else the plugin-config model, read per call. */
|
|
74
|
+
readonly settingsModel: (scope?: string) => string | undefined;
|
|
75
|
+
/** The scope's curated model list, read per call (empty = the fallback chain decides the vocabulary too). */
|
|
76
|
+
readonly modelList?: (scope?: string) => readonly string[];
|
|
75
77
|
/** The scoped settings.json's own model, minus live-spawn scratch writes. */
|
|
76
78
|
readonly cliDefault: (childSessionId?: string) => Promise<string | undefined>;
|
|
77
79
|
readonly directory?: (childSessionId?: string, refresh?: boolean) => LocalAgentModelDirectory | Promise<LocalAgentModelDirectory>;
|
|
@@ -111,7 +111,7 @@ export class ClaudeModelBroker {
|
|
|
111
111
|
if (directory.status === 'loading')
|
|
112
112
|
directory = await context.directory(true);
|
|
113
113
|
const model = selection.model.mode === 'value' ? selection.model.value
|
|
114
|
-
: (selection.model.mode === 'inherit' ? binding.model : undefined) ?? this.deps.settingsModel()
|
|
114
|
+
: (selection.model.mode === 'inherit' ? binding.model : undefined) ?? this.deps.settingsModel(binding.scope)
|
|
115
115
|
?? await readClaudeConfiguredModel(context.home) ?? directory.defaultModel;
|
|
116
116
|
const entry = directory.entries.find(entry => entry.value === model || entry.resolvedModel === model);
|
|
117
117
|
const effort = (selection.effort.mode === 'value' ? selection.effort.value : selection.effort.mode === 'inherit' ? binding.effort : undefined)
|
|
@@ -179,9 +179,10 @@ export class ClaudeModelBroker {
|
|
|
179
179
|
* would a round run with" read.
|
|
180
180
|
*/
|
|
181
181
|
async modelInfo(childSessionId, delegationModel) {
|
|
182
|
+
const record = childSessionId === undefined ? undefined : this.deps.localAgent.getDelegation(childSessionId);
|
|
182
183
|
const override = childSessionId === undefined ? undefined : this.deps.overrides.get(childSessionId);
|
|
183
184
|
const delegation = delegationModel?.trim() === '' ? undefined : delegationModel?.trim();
|
|
184
|
-
const settings = this.deps.settingsModel();
|
|
185
|
+
const settings = this.deps.settingsModel(record?.scope);
|
|
185
186
|
const cliDefault = await this.deps.cliDefault(childSessionId);
|
|
186
187
|
const native = await this.deps.directory?.(childSessionId);
|
|
187
188
|
const [effective, source] = override !== undefined
|
|
@@ -193,9 +194,14 @@ export class ClaudeModelBroker {
|
|
|
193
194
|
: cliDefault !== undefined
|
|
194
195
|
? [cliDefault, 'cli-config']
|
|
195
196
|
: [native?.defaultModel, 'cli-builtin'];
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
// The curated list is the whole vocabulary when non-empty — nothing from
|
|
198
|
+
// outside the endpoint's served catalog sneaks in. Empty falls back to
|
|
199
|
+
// the pre-list chain (native candidates plus configuration and history).
|
|
200
|
+
const curated = this.deps.modelList?.(record?.scope) ?? [];
|
|
201
|
+
const choices = curated.length > 0
|
|
202
|
+
? [...curated]
|
|
203
|
+
: [...new Set([settings, cliDefault, ...native?.entries.filter(entry => !entry.hidden).map(entry => entry.value) ?? [], ...this.deps.recentModels()]
|
|
204
|
+
.filter((value) => value !== undefined))];
|
|
199
205
|
const lastObserved = await this.lastObserved(childSessionId);
|
|
200
206
|
const active = childSessionId !== undefined && this.deps.localAgent.isDelegationActive(childSessionId);
|
|
201
207
|
return {
|
|
@@ -207,7 +213,11 @@ export class ClaudeModelBroker {
|
|
|
207
213
|
...cliDefault === undefined ? {} : { cliDefault },
|
|
208
214
|
...lastObserved === undefined ? {} : { lastObserved },
|
|
209
215
|
choices,
|
|
210
|
-
|
|
216
|
+
// The picker's extended vocabulary follows the same rule as choices:
|
|
217
|
+
// a curated list is complete on its own (no history entries sneak in);
|
|
218
|
+
// settings/cliDefault still extend so the member's CURRENT bound model
|
|
219
|
+
// stays selectable even when it is not in the list.
|
|
220
|
+
...native === undefined ? {} : { directory: extendModelDirectory(native, [settings, cliDefault], curated.length > 0 ? [] : [...this.deps.recentModels(), lastObserved]) },
|
|
211
221
|
live: this.deps.live(),
|
|
212
222
|
switchable: !active,
|
|
213
223
|
...active
|
|
@@ -238,8 +248,9 @@ export class ClaudeModelBroker {
|
|
|
238
248
|
else
|
|
239
249
|
this.deps.overrides.set(childSessionId, next);
|
|
240
250
|
// What the next spawn would bind, in the round resolvers' own order.
|
|
241
|
-
const
|
|
242
|
-
const
|
|
251
|
+
const record = this.deps.localAgent.getDelegation(childSessionId);
|
|
252
|
+
const delegation = record?.model?.trim();
|
|
253
|
+
const spawnModel = next ?? (delegation === undefined || delegation === '' ? undefined : delegation) ?? this.deps.settingsModel(record?.scope);
|
|
243
254
|
const host = this.deps.liveSwitch.hostingDriver(childSessionId);
|
|
244
255
|
if (host !== undefined && host.runtimeModel(childSessionId) !== spawnModel) {
|
|
245
256
|
await host.retireRuntime(childSessionId);
|