@openma/deepseek-harness-acp 0.4.4 → 0.4.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/dist/bridge.js CHANGED
@@ -22,7 +22,6 @@ import { createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
22
22
  import { SessionId } from "@deepseek-ai/dsh-session";
23
23
  import { foldSessionTitle } from "@deepseek-ai/dsh-session-title";
24
24
  import { SANDBOX_MODES, setSandboxMode } from "@deepseek-ai/dsh-sandbox-policy";
25
- import { credentialRef } from "@deepseek-ai/dsh-credentials";
26
25
  import * as agentModule from "@deepseek-ai/dsh-agent";
27
26
  import * as mcpClient from "@deepseek-ai/dsh-mcp-client";
28
27
  function selfHarness() {
@@ -35,7 +34,6 @@ function selfHarness() {
35
34
  foldSessionTitle,
36
35
  setSandboxMode,
37
36
  sandboxModes: SANDBOX_MODES,
38
- credentialRef,
39
37
  ...installModelSelection !== void 0 ? { installModelSelection } : {},
40
38
  mcpClient
41
39
  };
@@ -56,11 +54,245 @@ import {
56
54
  PROTOCOL_VERSION,
57
55
  RequestError
58
56
  } from "@agentclientprotocol/sdk";
57
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
59
58
 
60
59
  // src/version.ts
61
60
  import { createRequire } from "node:module";
62
61
  var VERSION = createRequire(import.meta.url)("../package.json").version;
63
62
 
63
+ // src/auth.ts
64
+ var LOGIN_DESCRIPTION = "Save an API key to the harness credential store shared with the dsh Web UI";
65
+ function isDeepseekRoute(provider) {
66
+ return provider === void 0 || provider === "deepseek-official" || provider === "deepseek";
67
+ }
68
+ function credentialEnvNames(provider) {
69
+ if (provider === void 0 || isDeepseekRoute(provider)) return ["DEEPSEEK_API_KEY", "DEEPSEEK_BASE_URL"];
70
+ if (provider === "anthropic") return ["ANTHROPIC_API_KEY"];
71
+ if (provider === "openai" || provider.startsWith("openai")) return ["OPENAI_API_KEY"];
72
+ return [`${provider.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase()}_API_KEY`];
73
+ }
74
+ function primaryCredentialName(provider) {
75
+ return credentialEnvNames(provider)[0] ?? "DEEPSEEK_API_KEY";
76
+ }
77
+ function providerFromAuthMethodId(methodId) {
78
+ if (!methodId) return void 0;
79
+ for (const prefix of ["api-key:", "terminal-login:"]) {
80
+ if (methodId.startsWith(prefix)) return methodId.slice(prefix.length);
81
+ }
82
+ return void 0;
83
+ }
84
+ function apiKeyFromAuthenticate(params) {
85
+ const meta = params._meta;
86
+ const block = meta && typeof meta === "object" && !Array.isArray(meta) ? meta["api-key"] : void 0;
87
+ const record = block && typeof block === "object" && !Array.isArray(block) ? block : void 0;
88
+ const key = typeof record?.apiKey === "string" ? record.apiKey.trim() : void 0;
89
+ const providerFromMeta = typeof record?.provider === "string" ? record.provider : void 0;
90
+ const provider = providerFromMeta || providerFromAuthMethodId(params.methodId);
91
+ return {
92
+ ...provider && provider.length > 0 ? { provider } : {},
93
+ ...key && key.length > 0 ? { key } : {}
94
+ };
95
+ }
96
+ function gatewayFromAuthenticate(params) {
97
+ const meta = params._meta;
98
+ const block = meta && typeof meta === "object" && !Array.isArray(meta) ? meta.gateway : void 0;
99
+ const record = block && typeof block === "object" && !Array.isArray(block) ? block : void 0;
100
+ const baseUrl = typeof record?.baseUrl === "string" ? record.baseUrl.trim() : void 0;
101
+ const headers = stringRecord(record?.headers);
102
+ const providerName = typeof record?.providerName === "string" ? record.providerName.trim() : void 0;
103
+ const key = apiKeyFromHeaders(headers);
104
+ return {
105
+ ...baseUrl && baseUrl.length > 0 ? { baseUrl } : {},
106
+ ...headers ? { headers } : {},
107
+ ...providerName && providerName.length > 0 ? { providerName } : {},
108
+ ...key && key.length > 0 ? { key } : {}
109
+ };
110
+ }
111
+ function isBrowserAuthMethod(methodId) {
112
+ return methodId === "browser";
113
+ }
114
+ function isGatewayAuthMethod(methodId) {
115
+ return methodId === "gateway";
116
+ }
117
+ function credentialBaseUrlName(provider) {
118
+ if (isDeepseekRoute(provider) || provider === void 0) return "DEEPSEEK_BASE_URL";
119
+ if (provider === "anthropic") return "ANTHROPIC_BASE_URL";
120
+ if (provider === "openai" || provider.startsWith("openai")) return "OPENAI_BASE_URL";
121
+ return `${provider.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase()}_BASE_URL`;
122
+ }
123
+ function advertisedAuthMethods(providers, clientCapabilities, env = process.env) {
124
+ const methods = [...apiKeyAuthMethods(providers)];
125
+ if (shouldOfferLocalAuthPage(env)) methods.push(browserAuthMethod());
126
+ if (clientCapabilities?.auth?._meta?.["gateway"] === true) methods.push(gatewayAuthMethod());
127
+ return methods;
128
+ }
129
+ function apiKeyAuthMethods(providers) {
130
+ if (providers.length <= 1) {
131
+ const provider = providers[0];
132
+ return [apiKeyAuthMethod(provider?.id, provider?.name, providers.length === 0)];
133
+ }
134
+ return providers.map((provider) => apiKeyAuthMethod(provider.id, provider.name, false));
135
+ }
136
+ function apiKeyAuthMethod(provider, name3, unscoped) {
137
+ const scoped = !unscoped && provider !== void 0;
138
+ return {
139
+ id: scoped ? `api-key:${provider}` : "api-key",
140
+ name: scoped ? `${name3 ?? provider} API key` : "API Key",
141
+ description: LOGIN_DESCRIPTION,
142
+ _meta: { "api-key": scoped ? { provider } : {} }
143
+ };
144
+ }
145
+ function shouldOfferLocalAuthPage(env = process.env) {
146
+ if (env["NO_BROWSER"]) return false;
147
+ if (env["NODE_ENV"] === "test" || env["CI"] === "true" || env["CI"] === "1") return false;
148
+ if (env["DSH_ACP_AUTH_PAGE"] === "0") return false;
149
+ return true;
150
+ }
151
+ function browserAuthMethod() {
152
+ return {
153
+ id: "browser",
154
+ name: "Browser",
155
+ description: "Open a local sign-in page to save an API key. The secret never travels over ACP."
156
+ };
157
+ }
158
+ function gatewayAuthMethod() {
159
+ return {
160
+ id: "gateway",
161
+ name: "Custom model gateway",
162
+ description: "Use a custom OpenAI-compatible gateway",
163
+ _meta: { gateway: { protocol: "openai", restartRequired: "false" } }
164
+ };
165
+ }
166
+ function stringRecord(value) {
167
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
168
+ const entries = Object.entries(value).filter(
169
+ (entry) => typeof entry[1] === "string" && entry[1].trim().length > 0
170
+ );
171
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
172
+ }
173
+ function apiKeyFromHeaders(headers) {
174
+ if (!headers) return void 0;
175
+ const authorization = headers["Authorization"] ?? headers["authorization"];
176
+ if (authorization) {
177
+ const bearer = authorization.match(/^Bearer\s+(.+)$/i);
178
+ const token = (bearer?.[1] ?? authorization).trim();
179
+ if (token.length > 0) return token;
180
+ }
181
+ const first = Object.values(headers).find((value) => value.trim().length > 0);
182
+ return first?.trim();
183
+ }
184
+
185
+ // src/auth-page.ts
186
+ import { createServer } from "node:http";
187
+ import { execFile } from "node:child_process";
188
+ function startLocalAuthPage(options) {
189
+ return new Promise((resolve, reject) => {
190
+ const server = createServer((request, response) => {
191
+ void handle(request, response);
192
+ });
193
+ let settle;
194
+ let fail;
195
+ const completed = new Promise((res, rej) => {
196
+ settle = res;
197
+ fail = rej;
198
+ });
199
+ const timeoutMs = options.timeoutMs ?? 10 * 6e4;
200
+ const timer = setTimeout(() => {
201
+ server.close();
202
+ fail?.(new Error("local auth page timed out"));
203
+ }, timeoutMs);
204
+ timer.unref?.();
205
+ const handle = async (request, response) => {
206
+ if (request.method === "GET" || request.method === void 0) {
207
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
208
+ response.end(authPageHtml(options.credentialName));
209
+ return;
210
+ }
211
+ if (request.method === "POST") {
212
+ const body = await readBody(request);
213
+ const key = new URLSearchParams(body).get("api-key")?.trim() ?? "";
214
+ if (key.length === 0) {
215
+ response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
216
+ response.end(authPageHtml(options.credentialName, "Enter an API key."));
217
+ return;
218
+ }
219
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
220
+ response.end(
221
+ "<!doctype html><html><body><p>Saved. You can return to the client.</p></body></html>"
222
+ );
223
+ clearTimeout(timer);
224
+ server.close();
225
+ settle?.(key);
226
+ return;
227
+ }
228
+ response.writeHead(405);
229
+ response.end();
230
+ };
231
+ server.once("error", (error) => {
232
+ clearTimeout(timer);
233
+ reject(error);
234
+ });
235
+ server.listen(0, "127.0.0.1", () => {
236
+ const address = server.address();
237
+ if (!address || typeof address === "string") {
238
+ server.close();
239
+ reject(new Error("local auth page failed to bind"));
240
+ return;
241
+ }
242
+ const url = `http://127.0.0.1:${address.port}/`;
243
+ resolve({
244
+ url,
245
+ close: () => {
246
+ clearTimeout(timer);
247
+ server.close();
248
+ },
249
+ completed
250
+ });
251
+ });
252
+ });
253
+ }
254
+ function openLocalAuthPage(url) {
255
+ const platform = process.platform;
256
+ if (platform === "darwin") {
257
+ execFile("open", [url], () => void 0);
258
+ return;
259
+ }
260
+ if (platform === "win32") {
261
+ execFile("cmd", ["/c", "start", "", url], () => void 0);
262
+ return;
263
+ }
264
+ execFile("xdg-open", [url], () => void 0);
265
+ }
266
+ function authPageHtml(credentialName, error) {
267
+ const message = error ? `<p>${escapeHtml(error)}</p>` : "";
268
+ return `<!doctype html>
269
+ <html lang="en">
270
+ <head><meta charset="utf-8"><title>DeepSeek Harness sign in</title></head>
271
+ <body>
272
+ <h1>Save API key</h1>
273
+ <p>Stored as ${escapeHtml(credentialName)} in the harness credential store.</p>
274
+ ${message}
275
+ <form method="post">
276
+ <label>API key <input type="password" name="api-key" autocomplete="off" required></label>
277
+ <button type="submit">Save</button>
278
+ </form>
279
+ </body>
280
+ </html>`;
281
+ }
282
+ function escapeHtml(value) {
283
+ return value.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char] ?? char);
284
+ }
285
+ function readBody(request) {
286
+ return new Promise((resolve, reject) => {
287
+ const chunks = [];
288
+ request.on("data", (chunk) => {
289
+ chunks.push(Buffer.from(chunk));
290
+ });
291
+ request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
292
+ request.on("error", reject);
293
+ });
294
+ }
295
+
64
296
  // src/log.ts
65
297
  var PREFIX = "[dsh-acp]";
66
298
  function logWarn(message) {
@@ -604,7 +836,7 @@ ${contextBlock(resource.uri, resource.text)}
604
836
 
605
837
  // src/bridge/index.ts
606
838
  var name = "acp-bridge";
607
- var inject = ["agents"];
839
+ var inject = ["agents", "credentials", "llm", "agentDefaultModel", "sessionPersistence", "approval", "permissionPresets", "commands", "agentPresets", "skills", "subagents"];
608
840
  function invalidParams(detail) {
609
841
  return RequestError.invalidParams(void 0, detail);
610
842
  }
@@ -614,9 +846,6 @@ function internalError(detail) {
614
846
  function authRequired(detail) {
615
847
  return RequestError.authRequired(void 0, detail);
616
848
  }
617
- function envCredentialPresent() {
618
- return Boolean(process.env["DEEPSEEK_API_KEY"] || process.env["DEEPSEEK_BASE_URL"]);
619
- }
620
849
  var MODE_LABELS = {
621
850
  "read-only": { name: "Read-only", description: "Bash and file mutations are denied by the sandbox" },
622
851
  "workspace-write": {
@@ -630,6 +859,16 @@ var MODE_LABELS = {
630
859
  };
631
860
  async function apply(ctx, config = {}) {
632
861
  const agents = ctx.agents;
862
+ const credentials = ctx.credentials;
863
+ const llm = ctx.llm;
864
+ const agentDefaultModel = ctx.agentDefaultModel;
865
+ const sessionPersistence = ctx.sessionPersistence;
866
+ const approval = ctx.approval;
867
+ const permissionPresets = ctx.permissionPresets;
868
+ const commandRuntime = ctx.commands;
869
+ const agentPresets = ctx.agentPresets;
870
+ const skillRegistry = ctx.skills;
871
+ const subagents = ctx.subagents;
633
872
  const harness = config.harness ?? (await Promise.resolve().then(() => (init_self_harness(), self_harness_exports))).selfHarness();
634
873
  const { createUserMessage: createUserMessage2, errorChain: errorChain2, sessionId: SessionId2, foldSessionTitle: foldSessionTitle2, setSandboxMode: setSandboxMode2 } = harness;
635
874
  const SANDBOX_MODES2 = harness.sandboxModes;
@@ -644,10 +883,8 @@ async function apply(ctx, config = {}) {
644
883
  return [...seen];
645
884
  };
646
885
  const defaultSelection = () => {
647
- const service = ctx.get("agentDefaultModel");
648
- if (service === void 0) return {};
649
886
  try {
650
- return service.currentSelection();
887
+ return agentDefaultModel.currentSelection();
651
888
  } catch (error) {
652
889
  logDebug(`agentDefaultModel.currentSelection failed: ${String(error)}`);
653
890
  return {};
@@ -664,28 +901,25 @@ async function apply(ctx, config = {}) {
664
901
  const effortCache = /* @__PURE__ */ new Map();
665
902
  const discoverModels = async () => {
666
903
  if (liveCatalog !== void 0) return liveCatalog;
667
- const llm = ctx.get("llm");
668
904
  const found = [];
669
- if (llm !== void 0) {
670
- try {
671
- const providers = llm.listProviders();
672
- const multi = providers.length > 1;
673
- for (const provider of providers) {
674
- const models = await llm.listModels(provider.id).catch((error) => {
675
- logDebug(`listModels(${provider.id}) failed: ${String(error)}`);
676
- return [];
905
+ try {
906
+ const providers = llm.listProviders();
907
+ const multi = providers.length > 1;
908
+ for (const provider of providers) {
909
+ const models = await llm.listModels(provider.id).catch((error) => {
910
+ logDebug(`listModels(${provider.id}) failed: ${String(error)}`);
911
+ return [];
912
+ });
913
+ for (const model of models) {
914
+ found.push({
915
+ provider: provider.id,
916
+ model: model.id,
917
+ label: multi ? `${model.name} (${provider.name})` : model.name
677
918
  });
678
- for (const model of models) {
679
- found.push({
680
- provider: provider.id,
681
- model: model.id,
682
- label: multi ? `${model.name} (${provider.name})` : model.name
683
- });
684
- }
685
919
  }
686
- } catch (error) {
687
- logDebug(`model discovery failed: ${String(error)}`);
688
920
  }
921
+ } catch (error) {
922
+ logDebug(`model discovery failed: ${String(error)}`);
689
923
  }
690
924
  liveCatalog = found;
691
925
  return found;
@@ -721,10 +955,9 @@ async function apply(ctx, config = {}) {
721
955
  if (provider === void 0 || model === void 0) return void 0;
722
956
  const key = `${provider}::${model}`;
723
957
  if (effortCache.has(key)) return effortCache.get(key);
724
- const llm = ctx.get("llm");
725
958
  let catalog;
726
959
  try {
727
- const reasoning = (await llm?.resolveModelInfo?.(provider, model))?.reasoning;
960
+ const reasoning = (await llm.resolveModelInfo(provider, model)).reasoning;
728
961
  if (reasoning !== void 0 && reasoning.efforts.length > 0) {
729
962
  catalog = {
730
963
  efforts: reasoning.efforts.map((effort) => ({
@@ -827,27 +1060,38 @@ async function apply(ctx, config = {}) {
827
1060
  }
828
1061
  setApprovalPolicy(record, record.approvals);
829
1062
  };
830
- const credentialPresent = async () => {
831
- if (envCredentialPresent()) return true;
832
- const brand = harness.credentialRef;
833
- if (brand === void 0) return false;
834
- const deadline = Date.now() + 5e3;
835
- for (; ; ) {
836
- const seam = ctx.get("credentials");
837
- if (seam !== void 0) {
838
- try {
839
- return await seam.resolve(brand("DEEPSEEK_API_KEY")) !== void 0;
840
- } catch (error) {
841
- logWarn(`credential lookup failed: ${String(error)}`);
842
- return false;
843
- }
844
- }
845
- if (Date.now() >= deadline) {
846
- logDebug("credential gate: no credentials service registered within 5s");
847
- return false;
1063
+ const listProviderRoutes = async () => {
1064
+ try {
1065
+ const raw = llm.listProviders();
1066
+ if (!Array.isArray(raw)) return [];
1067
+ return raw.flatMap((entry) => {
1068
+ if (!entry || typeof entry !== "object") return [];
1069
+ const id = entry.id;
1070
+ if (typeof id !== "string" || id.length === 0) return [];
1071
+ const name3 = entry.name;
1072
+ return [{ id, ...typeof name3 === "string" && name3.length > 0 ? { name: name3 } : {} }];
1073
+ });
1074
+ } catch (error) {
1075
+ logDebug(`listProviders failed: ${String(error)}`);
1076
+ return [];
1077
+ }
1078
+ };
1079
+ const credentialPresent = async (provider) => {
1080
+ const names = credentialEnvNames(provider).filter((name3) => !name3.endsWith("_BASE_URL"));
1081
+ for (const name3 of names) {
1082
+ try {
1083
+ if (await credentials.resolve(credentialRef(name3)) !== void 0) return true;
1084
+ } catch (error) {
1085
+ logWarn(`credential lookup failed: ${String(error)}`);
848
1086
  }
849
- await new Promise((resolve) => setTimeout(resolve, 100));
850
1087
  }
1088
+ return false;
1089
+ };
1090
+ const requireCredential = async (provider) => {
1091
+ if (await credentialPresent(provider)) return;
1092
+ throw authRequired(
1093
+ 'no credential found for this provider: call authenticate with `_meta["api-key"].apiKey` or `_meta.gateway`, use the browser method, run `dsh-acp login`, or save a key in the dsh Web UI (Settings \u2192 Models)'
1094
+ );
851
1095
  };
852
1096
  const agentOptionsFor = (model, provider) => ({
853
1097
  ...provider !== void 0 ? { provider } : config.provider !== void 0 ? { provider: config.provider } : {},
@@ -866,7 +1110,6 @@ async function apply(ctx, config = {}) {
866
1110
  const requireOrRestoreSession = async (sessionId) => {
867
1111
  const record = sessions.get(sessionId);
868
1112
  if (record !== void 0) return record;
869
- if (ctx.get("sessionPersistence") === void 0) throw invalidParams(`unknown session: ${sessionId}`);
870
1113
  logWarn(`restoring session ${sessionId} from the persisted log`);
871
1114
  try {
872
1115
  return await restoreSession(sessionId, { replay: false });
@@ -906,7 +1149,7 @@ async function apply(ctx, config = {}) {
906
1149
  }
907
1150
  const presets = presetsService();
908
1151
  const storedPreset = presetFromLog(storedHeader, events);
909
- const presetId = presets === void 0 ? void 0 : (await presets.resolve(storedPreset)).id;
1152
+ const presetId = (await presets.resolve(storedPreset)).id;
910
1153
  const handle = await agents.resume({
911
1154
  resumeSessionId: SessionId2(sessionId),
912
1155
  agentOptions: agentOptionsFor(config.model),
@@ -929,7 +1172,7 @@ async function apply(ctx, config = {}) {
929
1172
  if (presetId !== void 0) record.preset = presetId;
930
1173
  {
931
1174
  const permission = permissionService();
932
- const storedMode = permission?.current(events);
1175
+ const storedMode = permission.current(events);
933
1176
  if (storedMode !== void 0) record.modeId = storedMode;
934
1177
  for (let index = events.length - 1; index >= 0; index -= 1) {
935
1178
  const event = events[index];
@@ -958,9 +1201,9 @@ async function apply(ctx, config = {}) {
958
1201
  record.inflight = void 0;
959
1202
  inflight.resolve(reason);
960
1203
  };
961
- const presetsService = () => ctx.get("agentPresets");
1204
+ const presetsService = () => agentPresets;
962
1205
  const presetSetup = (presets, presetId) => {
963
- if (presets === void 0 || presetId === void 0) return void 0;
1206
+ if (presetId === void 0) return void 0;
964
1207
  return async (agentCtx) => {
965
1208
  logDebug(`preset setup: mounting "${presetId}"`);
966
1209
  await presets.mount(agentCtx, presetId);
@@ -979,7 +1222,7 @@ async function apply(ctx, config = {}) {
979
1222
  const presetLabel = (id) => id.charAt(0).toUpperCase() + id.slice(1);
980
1223
  const modeState = (record) => {
981
1224
  const permission = permissionService();
982
- if (permission !== void 0 && permission.names.length > 0) {
1225
+ if (permission.names.length > 0) {
983
1226
  return {
984
1227
  currentModeId: record.modeId,
985
1228
  availableModes: permission.names.map((name3) => {
@@ -1001,7 +1244,6 @@ async function apply(ctx, config = {}) {
1001
1244
  throw invalidParams("cannot switch presets while a prompt is running");
1002
1245
  }
1003
1246
  const presets = presetsService();
1004
- if (presets === void 0) throw invalidParams(`unknown mode: ${presetId}`);
1005
1247
  let resolved;
1006
1248
  try {
1007
1249
  resolved = (await presets.resolve(presetId)).id;
@@ -1009,29 +1251,44 @@ async function apply(ctx, config = {}) {
1009
1251
  throw invalidParams(`unknown preset: ${presetId} (${errorChain2(error)})`);
1010
1252
  }
1011
1253
  if (resolved === record.preset) return;
1012
- try {
1013
- record.agent.session.append("agent-preset/selected", { agentPreset: resolved });
1014
- } catch (error) {
1015
- logWarn(`recording preset selection failed: ${String(error)}`);
1016
- }
1254
+ const previous = record.preset;
1017
1255
  const sessionId = record.agent.session.id;
1256
+ const resumeWith = (id) => agents.resume({
1257
+ resumeSessionId: sessionId,
1258
+ agentOptions: agentOptionsFor(record.model ?? config.model, record.provider),
1259
+ ...presetSetup(presets, id) !== void 0 ? { setup: presetSetup(presets, id) } : {}
1260
+ });
1018
1261
  await record.dispose().catch((error) => {
1019
1262
  logWarn(`dispose during preset switch failed: ${String(error)}`);
1020
1263
  });
1021
1264
  let handle;
1022
1265
  try {
1023
- handle = await agents.resume({
1024
- resumeSessionId: sessionId,
1025
- agentOptions: agentOptionsFor(record.model ?? config.model, record.provider),
1026
- setup: presetSetup(presets, resolved)
1027
- });
1266
+ handle = await resumeWith(resolved);
1028
1267
  } catch (error) {
1029
- sessions.delete(acpSessionId);
1030
- throw internalError(`preset switch failed: ${errorChain2(error)}`);
1268
+ try {
1269
+ handle = await resumeWith(previous);
1270
+ } catch (restoreError) {
1271
+ sessions.delete(acpSessionId);
1272
+ throw internalError(
1273
+ `preset switch failed: ${errorChain2(error)}; restore also failed: ${errorChain2(restoreError)}`
1274
+ );
1275
+ }
1276
+ record.agent = handle.agent;
1277
+ record.dispose = () => handle.dispose();
1278
+ delete record.selection;
1279
+ ensureSelection(record);
1280
+ setApprovalPolicy(record, record.approvals);
1281
+ publishCommands(acpSessionId, record);
1282
+ throw invalidParams(`preset "${resolved}" failed to mount: ${errorChain2(error)}`);
1031
1283
  }
1032
1284
  record.agent = handle.agent;
1033
1285
  record.dispose = () => handle.dispose();
1034
1286
  record.preset = resolved;
1287
+ try {
1288
+ handle.agent.session.append("agent-preset/selected", { agentPreset: resolved });
1289
+ } catch (error) {
1290
+ logWarn(`recording preset selection failed: ${String(error)}`);
1291
+ }
1035
1292
  delete record.selection;
1036
1293
  ensureSelection(record);
1037
1294
  setApprovalPolicy(record, record.approvals);
@@ -1039,16 +1296,16 @@ async function apply(ctx, config = {}) {
1039
1296
  };
1040
1297
  const setApprovalPolicy = (record, policy) => {
1041
1298
  try {
1042
- ctx.get("approval")?.setPolicy?.(record.agent, policy);
1299
+ approval.setPolicy(record.agent, policy);
1043
1300
  } catch (error) {
1044
1301
  logWarn(`approval policy switch failed: ${String(error)}`);
1045
1302
  }
1046
1303
  record.approvals = policy;
1047
1304
  };
1048
- const permissionService = () => ctx.get("permissionPresets") ?? ctx.get("permission");
1305
+ const permissionService = () => permissionPresets;
1049
1306
  const applyMode = (record, sessionId, modeId) => {
1050
1307
  const permission = permissionService();
1051
- if (permission !== void 0 && permission.names.includes(modeId)) {
1308
+ if (permission.names.includes(modeId)) {
1052
1309
  let bundledApproval = record.approvals;
1053
1310
  permission.apply(record.agent.session, modeId, (policy) => {
1054
1311
  bundledApproval = policy;
@@ -1090,7 +1347,7 @@ async function apply(ctx, config = {}) {
1090
1347
  const result = [];
1091
1348
  {
1092
1349
  const permission = permissionService();
1093
- const levels = permission !== void 0 && permission.names.length > 0 ? permission.names.map((name3) => {
1350
+ const levels = permission.names.length > 0 ? permission.names.map((name3) => {
1094
1351
  const spec = permission.resolve(name3);
1095
1352
  return { value: name3, name: spec.name, description: spec.description };
1096
1353
  }) : SANDBOX_MODES2.map((mode) => {
@@ -1145,14 +1402,14 @@ async function apply(ctx, config = {}) {
1145
1402
  });
1146
1403
  }
1147
1404
  const presets = presetsService();
1148
- if (presets !== void 0 && record.preset !== void 0) {
1405
+ if (record.preset !== void 0) {
1149
1406
  try {
1150
1407
  const roster = await presets.list();
1151
1408
  if (roster.length >= 2) {
1152
1409
  result.push({
1153
1410
  type: "select",
1154
- id: "preset",
1155
- name: "Preset",
1411
+ id: "agent",
1412
+ name: "Agent",
1156
1413
  currentValue: record.preset,
1157
1414
  options: roster.map((preset) => ({
1158
1415
  value: preset.id,
@@ -1169,12 +1426,6 @@ async function apply(ctx, config = {}) {
1169
1426
  };
1170
1427
  const BUILTIN_COMMANDS = [
1171
1428
  { name: "status", description: "Show adapter, model, mode, and token status" },
1172
- {
1173
- name: "login",
1174
- description: "Save a DeepSeek API key into the harness credential store (~/.dsh/.credentials.yaml)",
1175
- input: { hint: "<api-key>" }
1176
- },
1177
- { name: "logout", description: "Remove the API key stored in the harness credential store" },
1178
1429
  {
1179
1430
  name: "model",
1180
1431
  description: "Select the model for this conversation",
@@ -1184,42 +1435,36 @@ async function apply(ctx, config = {}) {
1184
1435
  const availableCommandsFor = async (record) => {
1185
1436
  const list = [...BUILTIN_COMMANDS];
1186
1437
  const seen = new Set(list.map((command) => command.name));
1187
- const commandsSvc = ctx.get("commands");
1188
- if (commandsSvc !== void 0) {
1189
- try {
1190
- for (const descriptor of commandsSvc.list(record.agent)) {
1191
- if (seen.has(descriptor.name)) continue;
1192
- seen.add(descriptor.name);
1193
- list.push({
1194
- name: descriptor.name,
1195
- description: descriptor.description,
1196
- ...descriptor.input !== void 0 ? { input: { hint: descriptor.input.hint } } : {}
1197
- });
1198
- }
1199
- } catch (error) {
1200
- logWarn(`command listing failed: ${String(error)}`);
1438
+ try {
1439
+ for (const descriptor of commandRuntime.list(record.agent)) {
1440
+ if (seen.has(descriptor.name)) continue;
1441
+ seen.add(descriptor.name);
1442
+ list.push({
1443
+ name: descriptor.name,
1444
+ description: descriptor.description,
1445
+ ...descriptor.input !== void 0 ? { input: { hint: descriptor.input.hint } } : {}
1446
+ });
1201
1447
  }
1448
+ } catch (error) {
1449
+ logWarn(`command listing failed: ${String(error)}`);
1202
1450
  }
1203
- const skillsSvc = ctx.get("skills");
1204
- if (skillsSvc !== void 0) {
1205
- try {
1206
- const skills = await skillsSvc.list({
1207
- ...record.agent.session.header.cwd !== void 0 ? { cwd: record.agent.session.header.cwd } : {},
1208
- scope: record.agent
1451
+ try {
1452
+ const skills = await skillRegistry.list({
1453
+ ...record.agent.session.header.cwd !== void 0 ? { cwd: record.agent.session.header.cwd } : {},
1454
+ scope: record.agent
1455
+ });
1456
+ for (const skill of skills) {
1457
+ if (skill.invocation.userInvocable !== true) continue;
1458
+ if (seen.has(skill.name)) continue;
1459
+ seen.add(skill.name);
1460
+ list.push({
1461
+ name: skill.name,
1462
+ description: skill.description,
1463
+ input: { hint: "instructions for the skill" }
1209
1464
  });
1210
- for (const skill of skills) {
1211
- if (skill.invocation.userInvocable !== true) continue;
1212
- if (seen.has(skill.name)) continue;
1213
- seen.add(skill.name);
1214
- list.push({
1215
- name: skill.name,
1216
- description: skill.description,
1217
- input: { hint: "instructions for the skill" }
1218
- });
1219
- }
1220
- } catch (error) {
1221
- logDebug(`skill listing failed: ${String(error)}`);
1222
1465
  }
1466
+ } catch (error) {
1467
+ logDebug(`skill listing failed: ${String(error)}`);
1223
1468
  }
1224
1469
  return list;
1225
1470
  };
@@ -1246,7 +1491,7 @@ async function apply(ctx, config = {}) {
1246
1491
  const events = record.agent.session.events;
1247
1492
  if (events === void 0) return;
1248
1493
  const permission = permissionService();
1249
- const storedMode = permission?.current(events);
1494
+ const storedMode = permission.current(events);
1250
1495
  for (let index = events.length - 1; index >= 0; index -= 1) {
1251
1496
  const event = events[index];
1252
1497
  if (event?.type === "approval/policy") {
@@ -1263,69 +1508,35 @@ async function apply(ctx, config = {}) {
1263
1508
  });
1264
1509
  }
1265
1510
  };
1266
- const credentialStore = () => {
1267
- const brand = harness.credentialRef;
1268
- const store = ctx.get("credentials");
1269
- if (brand === void 0 || store === void 0) return void 0;
1270
- return { store, ref: brand("DEEPSEEK_API_KEY") };
1271
- };
1272
- const maskKey = (value) => value.length <= 8 ? "\u2026" : `${value.slice(0, 4)}\u2026${value.slice(-4)}`;
1273
- const describeCredential = async () => {
1274
- if (envCredentialPresent()) {
1275
- const viaEnv = process.env["DEEPSEEK_API_KEY"] !== void 0 ? "DEEPSEEK_API_KEY" : "DEEPSEEK_BASE_URL";
1276
- return `process environment (${viaEnv})`;
1277
- }
1278
- const cs = credentialStore();
1279
- if (cs === void 0) return "not configured";
1280
- try {
1281
- const info = await cs.store.describe(cs.ref);
1282
- if (!info.configured) return "not configured";
1283
- return info.source ?? "credential store";
1284
- } catch (error) {
1285
- logWarn(`credential describe failed: ${String(error)}`);
1286
- return "unknown";
1511
+ const describeCredential = async (provider) => {
1512
+ const names = credentialEnvNames(provider).filter((candidate) => !candidate.endsWith("_BASE_URL"));
1513
+ for (const name3 of names) {
1514
+ try {
1515
+ const info = await credentials.describe(credentialRef(name3));
1516
+ if (info.configured) return info.source ?? "credential store";
1517
+ } catch (error) {
1518
+ logWarn(`credential describe failed: ${String(error)}`);
1519
+ }
1287
1520
  }
1521
+ return "not configured";
1288
1522
  };
1289
- const loginText = async (rawArgument) => {
1290
- const key = rawArgument.trim();
1291
- const cs = credentialStore();
1292
- if (cs === void 0) {
1293
- return "This composition has no writable credential store; export DEEPSEEK_API_KEY in the environment that launches the agent instead.";
1294
- }
1295
- if (key.length === 0) {
1296
- return [
1297
- "Usage: `/login <api-key>` \u2014 stores the key in the harness credential store",
1298
- "(`~/.dsh/.credentials.yaml`, the same file the dsh Web UI writes; mode 600).",
1299
- "",
1300
- `Current credential source: ${await describeCredential()}.`,
1301
- "",
1302
- "Note: text entered here also lands in your client's chat history. If you",
1303
- "prefer to keep the key out of it, save the key in the dsh Web UI instead."
1304
- ].join("\n");
1305
- }
1306
- try {
1307
- await cs.store.set(cs.ref, key);
1308
- } catch (error) {
1309
- return [
1310
- `Could not store the key: ${error instanceof Error ? error.message : String(error)}`,
1311
- "",
1312
- "A read-only source (usually the process environment) is currently supplying",
1313
- "this credential, so a stored value would be shadowed. Unset DEEPSEEK_API_KEY",
1314
- "in the launching environment, or keep using it as the credential."
1315
- ].join("\n");
1316
- }
1317
- return `Saved DEEPSEEK_API_KEY (${maskKey(key)}) to the harness credential store \u2014 source now: ${await describeCredential()}. The dsh Web UI sees the same key.`;
1523
+ const saveCredential = async (provider, key) => {
1524
+ await credentials.set(credentialRef(primaryCredentialName(provider)), key);
1318
1525
  };
1319
- const logoutText = async () => {
1320
- const cs = credentialStore();
1321
- if (cs === void 0) return "This composition has no writable credential store; nothing to remove.";
1322
- try {
1323
- await cs.store.unset(cs.ref);
1324
- } catch (error) {
1325
- return `Could not remove the stored key: ${error instanceof Error ? error.message : String(error)}`;
1526
+ const saveGateway = async (provider, key, baseUrl) => {
1527
+ await saveCredential(provider, key);
1528
+ await credentials.set(credentialRef(credentialBaseUrlName(provider)), baseUrl);
1529
+ };
1530
+ const logoutStored = async () => {
1531
+ const routes = await listProviderRoutes();
1532
+ const providers = routes.length > 0 ? routes.map((route) => route.id) : [config.provider];
1533
+ for (const provider of providers) {
1534
+ try {
1535
+ await credentials.unset(credentialRef(primaryCredentialName(provider)));
1536
+ } catch (error) {
1537
+ logWarn(`credential unset failed: ${String(error)}`);
1538
+ }
1326
1539
  }
1327
- const remaining = await describeCredential();
1328
- return remaining === "not configured" ? "Removed the stored DEEPSEEK_API_KEY. No credential is configured now." : `Removed the stored DEEPSEEK_API_KEY; requests now authenticate via: ${remaining}.`;
1329
1540
  };
1330
1541
  const modelCommandText = async (record, acpSessionId, query) => {
1331
1542
  const discovered = await discoverModels();
@@ -1382,7 +1593,7 @@ async function apply(ctx, config = {}) {
1382
1593
  `| Provider | ${route.provider ?? "(composition default)"} |`,
1383
1594
  `| Model | ${route.model ?? "(composition default)"} |`,
1384
1595
  `| Reasoning | ${effort ?? "(adapter default)"} |`,
1385
- `| Credential | ${await describeCredential()} |`,
1596
+ `| Credential | ${await describeCredential(route.provider)} |`,
1386
1597
  ...record.preset !== void 0 ? [`| Preset | ${record.preset} |`] : [],
1387
1598
  `| Permission mode | ${record.modeId} |`,
1388
1599
  `| Approvals | ${record.approvals} |`,
@@ -1453,11 +1664,7 @@ async function apply(ctx, config = {}) {
1453
1664
  }).then(({ outcome }) => {
1454
1665
  if (outcome.outcome === "cancelled") return "cancelled";
1455
1666
  if (outcome.optionId === "allow-always") {
1456
- try {
1457
- ctx.get("approval")?.setPolicy?.(record.agent, "never");
1458
- } catch (error) {
1459
- logWarn(`approval policy switch failed: ${String(error)}`);
1460
- }
1667
+ setApprovalPolicy(record, "never");
1461
1668
  return "allowed-once";
1462
1669
  }
1463
1670
  return outcome.optionId === "allow-once" ? "allowed-once" : "rejected";
@@ -1466,39 +1673,77 @@ async function apply(ctx, config = {}) {
1466
1673
  const makeAgent = (connection) => {
1467
1674
  conn = connection;
1468
1675
  return {
1469
- initialize(params) {
1470
- const hasPersistence = ctx.get("sessionPersistence") !== void 0;
1676
+ async initialize(params) {
1471
1677
  const requested = params.protocolVersion;
1472
1678
  const capsMeta = params.clientCapabilities?.["_meta"];
1473
1679
  clientTerminalOutput = capsMeta !== null && typeof capsMeta === "object" ? capsMeta["terminal_output"] === true : false;
1474
- return Promise.resolve({
1680
+ const providers = await listProviderRoutes();
1681
+ return {
1475
1682
  protocolVersion: typeof requested === "number" && requested >= 1 && requested < PROTOCOL_VERSION ? requested : PROTOCOL_VERSION,
1476
1683
  agentInfo: { name: "dsh-acp", title: "DeepSeek Harness", version: VERSION },
1477
1684
  agentCapabilities: {
1478
- loadSession: hasPersistence,
1685
+ loadSession: true,
1479
1686
  promptCapabilities: { image: false, audio: false, embeddedContext: true },
1480
1687
  // Stdio servers always work; streamable HTTP maps onto
1481
1688
  // mcp-client's second transport. Legacy SSE does not.
1482
1689
  mcpCapabilities: { http: true, sse: false },
1483
- ...hasPersistence ? { sessionCapabilities: { list: {} } } : {}
1690
+ auth: { logout: {} },
1691
+ sessionCapabilities: { list: {} }
1484
1692
  },
1485
- authMethods: []
1486
- });
1693
+ authMethods: advertisedAuthMethods(
1694
+ providers,
1695
+ params.clientCapabilities
1696
+ )
1697
+ };
1487
1698
  },
1488
- async authenticate(_params) {
1489
- if (!await credentialPresent()) {
1490
- throw authRequired(
1491
- "no DeepSeek credential found: save one in the dsh Web UI (Settings \u2192 Models) or set DEEPSEEK_API_KEY"
1699
+ async authenticate(params) {
1700
+ const gateway = gatewayFromAuthenticate(params);
1701
+ if (isGatewayAuthMethod(params.methodId) || gateway.baseUrl) {
1702
+ if (!gateway.baseUrl || !gateway.key) {
1703
+ throw authRequired(
1704
+ "authenticate gateway requires `_meta.gateway.baseUrl` and an Authorization header"
1705
+ );
1706
+ }
1707
+ await saveGateway(
1708
+ gateway.providerName ?? config.provider,
1709
+ gateway.key,
1710
+ gateway.baseUrl
1492
1711
  );
1712
+ return;
1713
+ }
1714
+ const submitted = apiKeyFromAuthenticate(params);
1715
+ const provider = submitted.provider ?? providerFromAuthMethodId(params.methodId) ?? config.provider;
1716
+ if (submitted.key) {
1717
+ await saveCredential(provider, submitted.key);
1718
+ return;
1719
+ }
1720
+ if (await credentialPresent(provider)) return;
1721
+ if (isBrowserAuthMethod(params.methodId) && shouldOfferLocalAuthPage()) {
1722
+ const page = await startLocalAuthPage({
1723
+ credentialName: primaryCredentialName(provider)
1724
+ });
1725
+ openLocalAuthPage(page.url);
1726
+ try {
1727
+ const key = await page.completed;
1728
+ await saveCredential(provider, key);
1729
+ return;
1730
+ } finally {
1731
+ page.close();
1732
+ }
1493
1733
  }
1734
+ await requireCredential(provider);
1735
+ },
1736
+ async logout(_params) {
1737
+ await logoutStored();
1494
1738
  },
1495
1739
  async newSession(params) {
1496
1740
  assertOpen();
1741
+ await requireCredential(config.provider);
1497
1742
  validateCwd(params.cwd);
1498
1743
  syncMcpServers(params.mcpServers, params.cwd);
1499
1744
  const sessionId = SessionId2(randomUUID());
1500
1745
  const presets = presetsService();
1501
- const presetId = presets === void 0 ? void 0 : (await presets.resolve(void 0)).id;
1746
+ const presetId = (await presets.resolve(void 0)).id;
1502
1747
  const handle = await agents.create({
1503
1748
  sessionId,
1504
1749
  meta: { cwd: params.cwd, ...presetId !== void 0 ? { agentPreset: presetId } : {} },
@@ -1528,6 +1773,7 @@ async function apply(ctx, config = {}) {
1528
1773
  },
1529
1774
  async loadSession(params) {
1530
1775
  assertOpen();
1776
+ await requireCredential(config.provider);
1531
1777
  validateCwd(params.cwd);
1532
1778
  syncMcpServers(params.mcpServers, params.cwd);
1533
1779
  requirePersistence();
@@ -1587,10 +1833,6 @@ async function apply(ctx, config = {}) {
1587
1833
  return { stopReason: "end_turn" };
1588
1834
  };
1589
1835
  if (commandMatch?.[1] === "status") return respond(await statusText(record));
1590
- if (commandMatch?.[1] === "login") {
1591
- return respond(await loginText(trimmed.slice(commandMatch[0].length)));
1592
- }
1593
- if (commandMatch?.[1] === "logout") return respond(await logoutText());
1594
1836
  if (commandMatch?.[1] === "model") {
1595
1837
  return respond(
1596
1838
  await modelCommandText(
@@ -1601,40 +1843,26 @@ async function apply(ctx, config = {}) {
1601
1843
  );
1602
1844
  }
1603
1845
  if (commandMatch !== null && commandMatch[1] !== void 0) {
1604
- const commandsSvc = ctx.get("commands");
1605
- if (commandsSvc !== void 0) {
1606
- let execution;
1607
- try {
1608
- execution = await commandsSvc.execute(
1609
- record.agent,
1610
- trimmed,
1611
- new AbortController().signal
1612
- );
1613
- } catch (error) {
1614
- return respond(`\u26A0 /${commandMatch[1]} failed: ${errorChain2(error)}`);
1615
- }
1616
- if (execution !== void 0) {
1617
- const { result } = execution;
1618
- const text = result.text ?? (result.kind === "success" ? `/${commandMatch[1]} \u2713` : `/${commandMatch[1]} failed`);
1619
- syncPermissionState(record, params.sessionId);
1620
- publishCommands(params.sessionId, record);
1621
- publishConfigOptions(params.sessionId, record);
1622
- return respond(result.kind === "error" ? `\u26A0 ${text}` : text);
1623
- }
1846
+ let execution;
1847
+ try {
1848
+ execution = await commandRuntime.execute(
1849
+ record.agent,
1850
+ trimmed,
1851
+ new AbortController().signal
1852
+ );
1853
+ } catch (error) {
1854
+ return respond(`\u26A0 /${commandMatch[1]} failed: ${errorChain2(error)}`);
1855
+ }
1856
+ if (execution !== void 0) {
1857
+ const { result } = execution;
1858
+ const text = result.text ?? (result.kind === "success" ? `/${commandMatch[1]} \u2713` : `/${commandMatch[1]} failed`);
1859
+ syncPermissionState(record, params.sessionId);
1860
+ publishCommands(params.sessionId, record);
1861
+ publishConfigOptions(params.sessionId, record);
1862
+ return respond(result.kind === "error" ? `\u26A0 ${text}` : text);
1624
1863
  }
1625
1864
  }
1626
- if (!await credentialPresent()) {
1627
- return respond(
1628
- [
1629
- "No DeepSeek credential is configured, so this prompt was not sent to the model.",
1630
- "",
1631
- "Configure one of:",
1632
- "- `/login <api-key>` \u2014 store a key in the harness credential store right here,",
1633
- "- the dsh Web UI (`dsh web`, Settings \u2192 Models),",
1634
- "- `DEEPSEEK_API_KEY` in the environment that launches this agent."
1635
- ].join("\n")
1636
- );
1637
- }
1865
+ await requireCredential(record.provider ?? config.provider);
1638
1866
  if (agents.get(record.agent.id) !== record.agent) {
1639
1867
  throw internalError("prompt was not queued: the agent was disposed outside the bridge");
1640
1868
  }
@@ -1686,6 +1914,7 @@ async function apply(ctx, config = {}) {
1686
1914
  applyMode(record, params.sessionId, value);
1687
1915
  break;
1688
1916
  }
1917
+ case "agent":
1689
1918
  case "preset": {
1690
1919
  await switchPreset(record, params.sessionId, value);
1691
1920
  break;
@@ -1738,14 +1967,7 @@ async function apply(ctx, config = {}) {
1738
1967
  };
1739
1968
  };
1740
1969
  function requirePersistence() {
1741
- const persistence = getPersistence();
1742
- if (persistence === void 0) {
1743
- throw internalError("session persistence is not composed; session history is unavailable");
1744
- }
1745
- return persistence;
1746
- }
1747
- function getPersistence() {
1748
- return ctx.get("sessionPersistence");
1970
+ return sessionPersistence;
1749
1971
  }
1750
1972
  function validateCwd(cwd) {
1751
1973
  if (!isAbsolute(cwd)) throw invalidParams(`cwd must be an absolute path: ${cwd}`);
@@ -1850,13 +2072,10 @@ async function apply(ctx, config = {}) {
1850
2072
  settlePrompt(record, "cancelled");
1851
2073
  }
1852
2074
  quiescing = (async () => {
1853
- const subagents = ctx.get("subagents");
1854
- if (subagents !== void 0) {
1855
- try {
1856
- await subagents.drainContinuableDescendants(records.map((record) => record.agent));
1857
- } catch (error) {
1858
- logWarn(`continuable subagent teardown failed: ${String(error)}`);
1859
- }
2075
+ try {
2076
+ await subagents.drainContinuableDescendants(records.map((record) => record.agent));
2077
+ } catch (error) {
2078
+ logWarn(`continuable subagent teardown failed: ${String(error)}`);
1860
2079
  }
1861
2080
  const disposals = await Promise.allSettled(records.map((record) => record.dispose()));
1862
2081
  const failures = disposals.filter(