@yachiyo-5i/xlyra-agent 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,6 +58,176 @@ var LlmError = class extends Error {
58
58
  }
59
59
  };
60
60
 
61
+ // src/llm/credential.ts
62
+ var StaticCredentialProvider = class {
63
+ constructor(credential) {
64
+ this.credential = credential;
65
+ }
66
+ credential;
67
+ getCredential() {
68
+ return Promise.resolve(this.credential);
69
+ }
70
+ };
71
+ var XlyraCallbackCredentialProvider = class {
72
+ constructor(url, opts) {
73
+ this.url = url;
74
+ this.fetchImpl = opts?.fetchImpl ?? fetch;
75
+ }
76
+ url;
77
+ context = null;
78
+ /** runId → 当前有效 token */
79
+ tokensByRun = /* @__PURE__ */ new Map();
80
+ /** runId → 进行中的 callback(并发去重) */
81
+ inflight = /* @__PURE__ */ new Map();
82
+ fetchImpl;
83
+ /** 更新当前 run 上下文。token 按 runId 隔离,切换 run 不影响其他 run 的 token */
84
+ setRunContext(context) {
85
+ this.context = context;
86
+ }
87
+ async getCredential() {
88
+ const context = this.requireContext();
89
+ const cached = this.tokensByRun.get(context.runId);
90
+ if (cached) return cached;
91
+ const refreshed = await this.refreshCredential("initial");
92
+ if (!refreshed) {
93
+ throw new LlmError("xLyra LLM \u8C03\u7528\u51ED\u8BC1\u4E0D\u53EF\u7528\uFF1Arun \u5DF2\u7ED3\u675F\u6216 callback \u672A\u7B7E\u53D1 token");
94
+ }
95
+ return refreshed;
96
+ }
97
+ /**
98
+ * 按 run 取凭证(并发安全):chatStream 在请求开始时调用一次并全程使用
99
+ * 返回值——并发 run 各自的 token 互不干扰(setRunContext 只影响下一次
100
+ * 请求开始时的解析)。
101
+ */
102
+ async getCredentialFor(runId) {
103
+ const cached = this.tokensByRun.get(runId);
104
+ if (cached) return cached;
105
+ const refreshed = await this.refreshCredentialFor(runId, "initial");
106
+ if (!refreshed) {
107
+ throw new LlmError("xLyra LLM \u8C03\u7528\u51ED\u8BC1\u4E0D\u53EF\u7528\uFF1Arun \u5DF2\u7ED3\u675F\u6216 callback \u672A\u7B7E\u53D1 token");
108
+ }
109
+ return refreshed;
110
+ }
111
+ /** 按 run 刷新(401 重试用):与 getCredentialFor 相同的并发去重 */
112
+ refreshCredentialFor(runId, reason) {
113
+ const context = this.requireContext();
114
+ if (context.runId !== runId) return Promise.resolve(null);
115
+ return this.refreshCredential(reason);
116
+ }
117
+ /**
118
+ * 调用 xLyra credential callback 取新 token(并发去重:同一 run 的并发
119
+ * 调用合并为一次 in-flight 请求)。
120
+ * 返回 null:run inactive(active: false)或 callback 失败——调用方不得重试。
121
+ * token 不进入任何日志/错误信息。
122
+ */
123
+ refreshCredential(reason) {
124
+ const context = this.requireContext();
125
+ const pending = this.inflight.get(context.runId);
126
+ if (pending) return pending;
127
+ const task = this.doRefresh(context, reason).finally(() => {
128
+ this.inflight.delete(context.runId);
129
+ });
130
+ this.inflight.set(context.runId, task);
131
+ return task;
132
+ }
133
+ requireContext() {
134
+ if (!this.context) {
135
+ throw new LlmError("xLyra LLM \u51ED\u8BC1\u7F3A\u5C11 run \u4E0A\u4E0B\u6587\uFF08agent_instance_id/session_id/run_id\uFF09");
136
+ }
137
+ return this.context;
138
+ }
139
+ async doRefresh(context, reason) {
140
+ let res;
141
+ try {
142
+ res = await this.fetchImpl(this.url, {
143
+ method: "POST",
144
+ headers: { "content-type": "application/json" },
145
+ body: JSON.stringify({
146
+ agent_instance_id: context.agentInstanceId,
147
+ session_id: context.sessionId,
148
+ run_id: context.runId,
149
+ model: context.model,
150
+ reason
151
+ })
152
+ });
153
+ } catch {
154
+ return null;
155
+ }
156
+ if (!res.ok) return null;
157
+ let payload;
158
+ try {
159
+ payload = await res.json();
160
+ } catch {
161
+ return null;
162
+ }
163
+ if (payload.active !== true || typeof payload.token !== "string" || !payload.token) {
164
+ return null;
165
+ }
166
+ const credential = {
167
+ kind: "temporary-bearer",
168
+ headerName: "authorization",
169
+ headerValue: `Bearer ${payload.token}`
170
+ };
171
+ this.tokensByRun.set(context.runId, credential);
172
+ if (this.tokensByRun.size > 64) {
173
+ const oldest = this.tokensByRun.keys().next().value;
174
+ if (oldest !== void 0) this.tokensByRun.delete(oldest);
175
+ }
176
+ return credential;
177
+ }
178
+ };
179
+ function resolveCredentialProvider(endpoint) {
180
+ if (endpoint.mode === "remote-xlyra") {
181
+ if (endpoint.credential?.type === "xlyra-callback") {
182
+ return new XlyraCallbackCredentialProvider(endpoint.credential.url);
183
+ }
184
+ const token = endpoint.credential?.token;
185
+ if (!token) throw new LlmError(`\u7AEF\u70B9 ${endpoint.name} \u672A\u914D\u7F6E\u4E34\u65F6 LLM \u51ED\u8BC1`);
186
+ return new StaticCredentialProvider({
187
+ kind: "temporary-bearer",
188
+ headerName: "authorization",
189
+ headerValue: `Bearer ${token}`
190
+ });
191
+ }
192
+ if (!endpoint.api_key) {
193
+ throw new LlmError(`\u7AEF\u70B9 ${endpoint.name} \u672A\u914D\u7F6E api_key`);
194
+ }
195
+ return new StaticCredentialProvider(
196
+ endpoint.protocol === "anthropic-messages" ? { kind: "api-key", headerName: "x-api-key", headerValue: endpoint.api_key } : {
197
+ kind: "api-key",
198
+ headerName: "authorization",
199
+ headerValue: `Bearer ${endpoint.api_key}`
200
+ }
201
+ );
202
+ }
203
+ function isRefreshable(provider) {
204
+ return provider instanceof XlyraCallbackCredentialProvider;
205
+ }
206
+
207
+ // src/llm/http-error.ts
208
+ async function httpErrorSummary(res, secrets = []) {
209
+ const raw = await res.text().catch(() => "");
210
+ let msg = "";
211
+ try {
212
+ const parsed = JSON.parse(raw);
213
+ if (parsed && typeof parsed === "object") {
214
+ const j = parsed;
215
+ const candidate = j.error?.message ?? j.message;
216
+ if (typeof candidate === "string") msg = candidate;
217
+ }
218
+ } catch {
219
+ }
220
+ msg = msg.slice(0, 200);
221
+ for (const secret of secrets) {
222
+ if (secret) msg = msg.split(secret).join("***");
223
+ }
224
+ return `\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${res.status}\uFF09\uFF1A${msg || res.statusText}`;
225
+ }
226
+ function credentialSecrets(cred) {
227
+ const bare = cred.headerValue.startsWith("Bearer ") ? cred.headerValue.slice("Bearer ".length) : cred.headerValue;
228
+ return bare === cred.headerValue ? [cred.headerValue] : [cred.headerValue, bare];
229
+ }
230
+
61
231
  // src/llm/sse.ts
62
232
  async function* parseSse(body) {
63
233
  const reader = body.getReader();
@@ -200,17 +370,50 @@ function toAnthropicTools(tools) {
200
370
  var AnthropicMessagesProtocol = class {
201
371
  name = "anthropic-messages";
202
372
  baseUrl;
203
- apiKey;
373
+ credential;
204
374
  provider;
205
375
  apiVersion;
206
376
  fetchImpl;
377
+ runContext = null;
207
378
  constructor(opts) {
208
379
  this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
209
- this.apiKey = opts.apiKey;
380
+ if (opts.credential) {
381
+ this.credential = opts.credential;
382
+ } else if (opts.apiKey) {
383
+ this.credential = new StaticCredentialProvider({
384
+ kind: "api-key",
385
+ headerName: "x-api-key",
386
+ headerValue: opts.apiKey
387
+ });
388
+ } else {
389
+ throw new LlmError("AnthropicMessagesProtocol \u7F3A\u5C11\u51ED\u8BC1\uFF08credential \u6216 apiKey\uFF09");
390
+ }
210
391
  this.provider = opts.providerName ?? "anthropic";
211
392
  this.apiVersion = opts.apiVersion ?? "2023-06-01";
212
393
  this.fetchImpl = opts.fetchImpl ?? fetch;
213
394
  }
395
+ /** 更新 run 上下文:仅 xlyra-callback 凭证消费;静态凭证为 no-op */
396
+ setRunContext(context) {
397
+ this.runContext = context;
398
+ if (isRefreshable(this.credential)) this.credential.setRunContext(context);
399
+ }
400
+ /**
401
+ * 请求开始时解析本次 run 的凭证:xlyra-callback 按 runId 定向取
402
+ * (并发 run 各自独立),静态凭证直接返回。
403
+ */
404
+ resolveRequestCredential() {
405
+ if (isRefreshable(this.credential) && this.runContext) {
406
+ return this.credential.getCredentialFor(this.runContext.runId);
407
+ }
408
+ return this.credential.getCredential();
409
+ }
410
+ /** 401 后按本次 run 定向刷新(run 上下文缺失时无法刷新) */
411
+ refreshRequestCredential() {
412
+ if (isRefreshable(this.credential) && this.runContext) {
413
+ return this.credential.refreshCredentialFor(this.runContext.runId, "unauthorized");
414
+ }
415
+ return Promise.resolve(null);
416
+ }
214
417
  async *chatStream(req) {
215
418
  const { system, messages } = toAnthropicMessages(req.messages);
216
419
  const maxTokens = req.settings.max_tokens ?? DEFAULT_MAX_TOKENS;
@@ -222,29 +425,59 @@ var AnthropicMessagesProtocol = class {
222
425
  const budget = Math.min(THINKING_BUDGET[req.settings.reasoning_effort], maxTokens - 1);
223
426
  if (budget >= 1024) body.thinking = { type: "enabled", budget_tokens: budget };
224
427
  }
428
+ let cred;
429
+ try {
430
+ cred = await this.resolveRequestCredential();
431
+ } catch (err) {
432
+ yield { type: "error", error: errMsg(err) };
433
+ return;
434
+ }
435
+ const doFetch = (c) => this.fetchImpl(`${this.baseUrl}/v1/messages`, {
436
+ method: "POST",
437
+ headers: {
438
+ "content-type": "application/json",
439
+ [c.headerName]: c.headerValue,
440
+ "anthropic-version": this.apiVersion
441
+ },
442
+ body: JSON.stringify(body),
443
+ signal: req.signal ?? null
444
+ });
225
445
  let res;
226
446
  try {
227
- res = await this.fetchImpl(`${this.baseUrl}/v1/messages`, {
228
- method: "POST",
229
- headers: {
230
- "content-type": "application/json",
231
- "x-api-key": this.apiKey,
232
- "anthropic-version": this.apiVersion
233
- },
234
- body: JSON.stringify(body),
235
- signal: req.signal ?? null
236
- });
447
+ res = await doFetch(cred);
237
448
  } catch (err) {
238
449
  if (req.signal?.aborted) return;
239
450
  yield { type: "error", error: `\u8BF7\u6C42\u5931\u8D25\uFF08\u7F51\u7EDC\u9519\u8BEF\uFF09\uFF1A${errMsg(err)}` };
240
451
  return;
241
452
  }
453
+ if (res.status === 401 && isRefreshable(this.credential)) {
454
+ await res.body?.cancel().catch(() => {
455
+ });
456
+ let refreshed = null;
457
+ try {
458
+ refreshed = await this.refreshRequestCredential();
459
+ } catch (err) {
460
+ yield { type: "error", error: errMsg(err) };
461
+ return;
462
+ }
463
+ if (!refreshed) {
464
+ yield {
465
+ type: "error",
466
+ error: "xLyra LLM \u8C03\u7528\u51ED\u8BC1\u5DF2\u8FC7\u671F\uFF0C\u4E14 xLyra \u672A\u7B7E\u53D1\u65B0 token\uFF08run \u5DF2\u7ED3\u675F\u6216\u4E0D\u53EF\u7528\uFF09\uFF08HTTP 401\uFF09"
467
+ };
468
+ return;
469
+ }
470
+ cred = refreshed;
471
+ try {
472
+ res = await doFetch(cred);
473
+ } catch (err) {
474
+ if (req.signal?.aborted) return;
475
+ yield { type: "error", error: `\u8BF7\u6C42\u5931\u8D25\uFF08\u7F51\u7EDC\u9519\u8BEF\uFF09\uFF1A${errMsg(err)}` };
476
+ return;
477
+ }
478
+ }
242
479
  if (!res.ok || !res.body) {
243
- const detail = await res.text().catch(() => "");
244
- yield {
245
- type: "error",
246
- error: `\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${res.status}\uFF09\uFF1A${detail.slice(0, 500) || res.statusText}`
247
- };
480
+ yield { type: "error", error: await httpErrorSummary(res, credentialSecrets(cred)) };
248
481
  return;
249
482
  }
250
483
  const blockKinds = /* @__PURE__ */ new Map();
@@ -438,15 +671,48 @@ function responseFromCompleted(response, provider) {
438
671
  var OpenAIResponsesProtocol = class {
439
672
  name = "openai-responses";
440
673
  baseUrl;
441
- apiKey;
674
+ credential;
442
675
  provider;
443
676
  fetchImpl;
677
+ runContext = null;
444
678
  constructor(opts) {
445
679
  this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
446
- this.apiKey = opts.apiKey;
680
+ if (opts.credential) {
681
+ this.credential = opts.credential;
682
+ } else if (opts.apiKey) {
683
+ this.credential = new StaticCredentialProvider({
684
+ kind: "api-key",
685
+ headerName: "authorization",
686
+ headerValue: `Bearer ${opts.apiKey}`
687
+ });
688
+ } else {
689
+ throw new LlmError("OpenAIResponsesProtocol \u7F3A\u5C11\u51ED\u8BC1\uFF08credential \u6216 apiKey\uFF09");
690
+ }
447
691
  this.provider = opts.providerName ?? "openai";
448
692
  this.fetchImpl = opts.fetchImpl ?? fetch;
449
693
  }
694
+ /** 更新 run 上下文:仅 xlyra-callback 凭证消费;静态凭证为 no-op */
695
+ setRunContext(context) {
696
+ this.runContext = context;
697
+ if (isRefreshable(this.credential)) this.credential.setRunContext(context);
698
+ }
699
+ /**
700
+ * 请求开始时解析本次 run 的凭证:xlyra-callback 按 runId 定向取
701
+ * (并发 run 各自独立),静态凭证直接返回。
702
+ */
703
+ resolveRequestCredential() {
704
+ if (isRefreshable(this.credential) && this.runContext) {
705
+ return this.credential.getCredentialFor(this.runContext.runId);
706
+ }
707
+ return this.credential.getCredential();
708
+ }
709
+ /** 401 后按本次 run 定向刷新(run 上下文缺失时无法刷新) */
710
+ refreshRequestCredential() {
711
+ if (isRefreshable(this.credential) && this.runContext) {
712
+ return this.credential.refreshCredentialFor(this.runContext.runId, "unauthorized");
713
+ }
714
+ return Promise.resolve(null);
715
+ }
450
716
  async *chatStream(req) {
451
717
  const { instructions, input } = toResponsesInput(req.messages);
452
718
  const body = { model: req.model, input, stream: true };
@@ -458,28 +724,58 @@ var OpenAIResponsesProtocol = class {
458
724
  body.reasoning = { effort: req.settings.reasoning_effort, summary: "auto" };
459
725
  body.include = ["reasoning.encrypted_content"];
460
726
  }
727
+ let cred;
728
+ try {
729
+ cred = await this.resolveRequestCredential();
730
+ } catch (err) {
731
+ yield { type: "error", error: errMsg2(err) };
732
+ return;
733
+ }
734
+ const doFetch = (c) => this.fetchImpl(`${this.baseUrl}/responses`, {
735
+ method: "POST",
736
+ headers: {
737
+ "content-type": "application/json",
738
+ [c.headerName]: c.headerValue
739
+ },
740
+ body: JSON.stringify(body),
741
+ signal: req.signal ?? null
742
+ });
461
743
  let res;
462
744
  try {
463
- res = await this.fetchImpl(`${this.baseUrl}/responses`, {
464
- method: "POST",
465
- headers: {
466
- "content-type": "application/json",
467
- authorization: `Bearer ${this.apiKey}`
468
- },
469
- body: JSON.stringify(body),
470
- signal: req.signal ?? null
471
- });
745
+ res = await doFetch(cred);
472
746
  } catch (err) {
473
747
  if (req.signal?.aborted) return;
474
748
  yield { type: "error", error: `\u8BF7\u6C42\u5931\u8D25\uFF08\u7F51\u7EDC\u9519\u8BEF\uFF09\uFF1A${errMsg2(err)}` };
475
749
  return;
476
750
  }
751
+ if (res.status === 401 && isRefreshable(this.credential)) {
752
+ await res.body?.cancel().catch(() => {
753
+ });
754
+ let refreshed = null;
755
+ try {
756
+ refreshed = await this.refreshRequestCredential();
757
+ } catch (err) {
758
+ yield { type: "error", error: errMsg2(err) };
759
+ return;
760
+ }
761
+ if (!refreshed) {
762
+ yield {
763
+ type: "error",
764
+ error: "xLyra LLM \u8C03\u7528\u51ED\u8BC1\u5DF2\u8FC7\u671F\uFF0C\u4E14 xLyra \u672A\u7B7E\u53D1\u65B0 token\uFF08run \u5DF2\u7ED3\u675F\u6216\u4E0D\u53EF\u7528\uFF09\uFF08HTTP 401\uFF09"
765
+ };
766
+ return;
767
+ }
768
+ cred = refreshed;
769
+ try {
770
+ res = await doFetch(cred);
771
+ } catch (err) {
772
+ if (req.signal?.aborted) return;
773
+ yield { type: "error", error: `\u8BF7\u6C42\u5931\u8D25\uFF08\u7F51\u7EDC\u9519\u8BEF\uFF09\uFF1A${errMsg2(err)}` };
774
+ return;
775
+ }
776
+ }
477
777
  if (!res.ok || !res.body) {
478
- const detail = await res.text().catch(() => "");
479
- yield {
480
- type: "error",
481
- error: `\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${res.status}\uFF09\uFF1A${detail.slice(0, 500) || res.statusText}`
482
- };
778
+ yield { type: "error", error: await httpErrorSummary(res, credentialSecrets(cred)) };
483
779
  return;
484
780
  }
485
781
  const calls = new ToolCallBuffer();
@@ -635,7 +931,7 @@ var EndpointResolver = class {
635
931
  if (!protocol) {
636
932
  const opts = {
637
933
  baseUrl: endpoint.base_url,
638
- apiKey: endpoint.api_key,
934
+ credential: resolveCredentialProvider(endpoint),
639
935
  providerName: endpoint.name
640
936
  };
641
937
  protocol = endpoint.protocol === "anthropic-messages" ? new AnthropicMessagesProtocol(opts) : new OpenAIResponsesProtocol(opts);
@@ -1325,6 +1621,7 @@ ${tail}`;
1325
1621
  }
1326
1622
 
1327
1623
  // src/agent/escalation.ts
1624
+ import path2 from "path";
1328
1625
  var COMMAND_EXECUTION_GRANT = "capability://exec_command";
1329
1626
  var EscalationRequiredError = class extends Error {
1330
1627
  constructor(requestedPath, resolvedPath, message, resourceType = "path", requestedCommand) {
@@ -1358,7 +1655,8 @@ var EscalationGrants = class {
1358
1655
  const set = this.grants.get(sessionId);
1359
1656
  if (!set) return false;
1360
1657
  for (const prefix of set) {
1361
- if (resolvedPath === prefix || resolvedPath.startsWith(prefix + "/")) return true;
1658
+ const relative = path2.relative(prefix, resolvedPath);
1659
+ if (relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative)) return true;
1362
1660
  }
1363
1661
  return false;
1364
1662
  }
@@ -1430,6 +1728,14 @@ var AgentRunner = class {
1430
1728
  if (!contextWindow) {
1431
1729
  this.logger.info(`\u6A21\u578B\u672A\u58F0\u660E\u4E0A\u4E0B\u6587\u7A97\u53E3\uFF0C\u81EA\u52A8\u538B\u7F29\u505C\u7528 model=${modelId}`);
1432
1730
  }
1731
+ if (runOpts.sessionId) {
1732
+ protocol.setRunContext?.({
1733
+ agentInstanceId: runOpts.agentInstanceId ?? "default",
1734
+ sessionId: runOpts.sessionId,
1735
+ runId,
1736
+ model: modelId
1737
+ });
1738
+ }
1433
1739
  const history = (params.history ?? []).map((m) => chatMessageSchema.parse(m));
1434
1740
  const settings = modelSettingsSchema.parse(params.settings ?? {});
1435
1741
  const definitions = this.tools.map((t) => t.definition);
@@ -1686,22 +1992,22 @@ ${availableToolsPrompt}` : buildSystemPrompt({ availableTools: definitions.map((
1686
1992
 
1687
1993
  // src/tools/workdir.ts
1688
1994
  import fs2 from "fs";
1689
- import path2 from "path";
1995
+ import path3 from "path";
1690
1996
  function resolveSandboxed(workdir, raw, opts = {}) {
1691
1997
  const baseReal = fs2.realpathSync(workdir);
1692
- const candidate = path2.resolve(path2.isAbsolute(raw) ? raw : path2.join(baseReal, raw));
1998
+ const candidate = path3.resolve(path3.isAbsolute(raw) ? raw : path3.join(baseReal, raw));
1693
1999
  if (opts.allowOutsideWorkdir) return candidate;
1694
2000
  let existing = candidate;
1695
2001
  const missing = [];
1696
2002
  while (!fs2.existsSync(existing)) {
1697
- missing.unshift(path2.basename(existing));
1698
- const parent = path2.dirname(existing);
2003
+ missing.unshift(path3.basename(existing));
2004
+ const parent = path3.dirname(existing);
1699
2005
  if (parent === existing) break;
1700
2006
  existing = parent;
1701
2007
  }
1702
2008
  const existingReal = fs2.realpathSync(existing);
1703
- const realTarget = path2.join(existingReal, ...missing);
1704
- if (realTarget !== baseReal && !realTarget.startsWith(baseReal + path2.sep)) {
2009
+ const realTarget = path3.join(existingReal, ...missing);
2010
+ if (realTarget !== baseReal && !realTarget.startsWith(baseReal + path3.sep)) {
1705
2011
  if (opts.grants && opts.sessionId && opts.grants.isGranted(opts.sessionId, realTarget)) {
1706
2012
  return realTarget;
1707
2013
  }
@@ -1713,7 +2019,7 @@ function resolveSandboxed(workdir, raw, opts = {}) {
1713
2019
  // src/tools/apply-patch.ts
1714
2020
  import crypto3 from "crypto";
1715
2021
  import fs3 from "fs";
1716
- import path3 from "path";
2022
+ import path4 from "path";
1717
2023
  import { z as z4 } from "zod";
1718
2024
  var argsSchema = z4.object({ patch: z4.string().min(1) });
1719
2025
  function makeApplyPatchTool(workdir, sandbox = {}) {
@@ -1761,7 +2067,7 @@ function makeApplyPatchTool(workdir, sandbox = {}) {
1761
2067
  } else {
1762
2068
  installedAdds.push(item.file);
1763
2069
  }
1764
- fs3.mkdirSync(path3.dirname(item.file), { recursive: true });
2070
+ fs3.mkdirSync(path4.dirname(item.file), { recursive: true });
1765
2071
  fs3.renameSync(item.tmp, item.file);
1766
2072
  }
1767
2073
  for (const deletion of deletes) {
@@ -1872,18 +2178,18 @@ function assertRegularFile(rawPath, file) {
1872
2178
  if (!fs3.statSync(file).isFile()) throw new Error(`${rawPath} \u4E0D\u662F\u666E\u901A\u6587\u4EF6`);
1873
2179
  }
1874
2180
  function stageWrite(write) {
1875
- fs3.mkdirSync(path3.dirname(write.file), { recursive: true });
1876
- const tmp = path3.join(path3.dirname(write.file), `.xlyra-tmp-${crypto3.randomUUID()}${path3.extname(write.file)}`);
2181
+ fs3.mkdirSync(path4.dirname(write.file), { recursive: true });
2182
+ const tmp = path4.join(path4.dirname(write.file), `.xlyra-tmp-${crypto3.randomUUID()}${path4.extname(write.file)}`);
1877
2183
  fs3.writeFileSync(tmp, write.content);
1878
2184
  return { ...write, tmp };
1879
2185
  }
1880
2186
  function backupPath(file) {
1881
- return path3.join(path3.dirname(file), `.xlyra-backup-${crypto3.randomUUID()}${path3.extname(file)}`);
2187
+ return path4.join(path4.dirname(file), `.xlyra-backup-${crypto3.randomUUID()}${path4.extname(file)}`);
1882
2188
  }
1883
2189
 
1884
2190
  // src/tools/create.ts
1885
2191
  import fs4 from "fs";
1886
- import path4 from "path";
2192
+ import path5 from "path";
1887
2193
  import { z as z5 } from "zod";
1888
2194
  var argsSchema2 = z5.object({
1889
2195
  path: z5.string(),
@@ -1900,7 +2206,7 @@ function makeCreateTool(workdir, sandbox = {}) {
1900
2206
  handler: async (args) => {
1901
2207
  const { path: rawPath, content } = argsSchema2.parse(args);
1902
2208
  const file = resolveSandboxed(workdir, rawPath, sandbox);
1903
- fs4.mkdirSync(path4.dirname(file), { recursive: true });
2209
+ fs4.mkdirSync(path5.dirname(file), { recursive: true });
1904
2210
  try {
1905
2211
  fs4.writeFileSync(file, content, { flag: "wx" });
1906
2212
  } catch (err) {
@@ -1917,7 +2223,7 @@ function makeCreateTool(workdir, sandbox = {}) {
1917
2223
  // src/tools/edit.ts
1918
2224
  import crypto4 from "crypto";
1919
2225
  import fs5 from "fs";
1920
- import path5 from "path";
2226
+ import path6 from "path";
1921
2227
  import { z as z6 } from "zod";
1922
2228
  var argsSchema3 = z6.object({
1923
2229
  path: z6.string(),
@@ -1964,7 +2270,7 @@ function countMatches(text, needle) {
1964
2270
  }
1965
2271
  }
1966
2272
  function atomicReplace(file, content) {
1967
- const tmp = path5.join(path5.dirname(file), `.xlyra-tmp-${crypto4.randomUUID()}${path5.extname(file)}`);
2273
+ const tmp = path6.join(path6.dirname(file), `.xlyra-tmp-${crypto4.randomUUID()}${path6.extname(file)}`);
1968
2274
  try {
1969
2275
  fs5.writeFileSync(tmp, content);
1970
2276
  fs5.renameSync(tmp, file);
@@ -2170,7 +2476,7 @@ ${result.output}`);
2170
2476
 
2171
2477
  // src/tools/list.ts
2172
2478
  import fs7 from "fs";
2173
- import path6 from "path";
2479
+ import path7 from "path";
2174
2480
  import { z as z8 } from "zod";
2175
2481
  var MAX_ENTRIES = 500;
2176
2482
  var IGNORE_NAMES = /* @__PURE__ */ new Set([
@@ -2224,8 +2530,8 @@ function makeListTool(workdir, sandbox = {}) {
2224
2530
  entries.sort((a, b) => a.name.localeCompare(b.name));
2225
2531
  for (const entry of entries) {
2226
2532
  if (IGNORE_NAMES.has(entry.name)) continue;
2227
- const full = path6.join(current, entry.name);
2228
- const rel = path6.relative(dir, full);
2533
+ const full = path7.join(current, entry.name);
2534
+ const rel = path7.relative(dir, full);
2229
2535
  const display = entry.isDirectory() ? `${rel}/` : rel;
2230
2536
  if (!matcher || matcher.test(display)) {
2231
2537
  if (results.length < MAX_ENTRIES) results.push(display);
@@ -2257,14 +2563,14 @@ function countRemaining(dir, recursive) {
2257
2563
  for (const entry of entries) {
2258
2564
  if (IGNORE_NAMES.has(entry.name)) continue;
2259
2565
  count += 1;
2260
- if (recursive && entry.isDirectory()) count += countRemaining(path6.join(dir, entry.name), true);
2566
+ if (recursive && entry.isDirectory()) count += countRemaining(path7.join(dir, entry.name), true);
2261
2567
  }
2262
2568
  return count;
2263
2569
  }
2264
2570
 
2265
2571
  // src/tools/read.ts
2266
2572
  import fs8 from "fs";
2267
- import path7 from "path";
2573
+ import path8 from "path";
2268
2574
  import { z as z9 } from "zod";
2269
2575
  var MAX_LINES = 2e3;
2270
2576
  var MAX_BYTES = 5e4;
@@ -2289,7 +2595,7 @@ function makeReadTool(workdir, sandbox = {}) {
2289
2595
  if (fs8.statSync(file).isDirectory()) {
2290
2596
  throw new Error(`${rawPath} \u662F\u76EE\u5F55\u4E0D\u662F\u6587\u4EF6\uFF1B\u67E5\u770B\u76EE\u5F55\u5185\u5BB9\u8BF7\u7528 list \u5DE5\u5177`);
2291
2597
  }
2292
- if (IMAGE_SUFFIXES.has(path7.extname(file).toLowerCase())) {
2598
+ if (IMAGE_SUFFIXES.has(path8.extname(file).toLowerCase())) {
2293
2599
  throw new Error("\u6682\u4E0D\u652F\u6301\u8BFB\u53D6\u56FE\u7247\u6587\u4EF6\uFF0C\u53EA\u652F\u6301\u6587\u672C\u6587\u4EF6");
2294
2600
  }
2295
2601
  const lines = fs8.readFileSync(file, "utf-8").split("\n");
@@ -2319,7 +2625,7 @@ function makeReadTool(workdir, sandbox = {}) {
2319
2625
 
2320
2626
  // src/tools/search.ts
2321
2627
  import fs9 from "fs";
2322
- import path8 from "path";
2628
+ import path9 from "path";
2323
2629
  import { z as z10 } from "zod";
2324
2630
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
2325
2631
  var HARD_MAX_RESULTS = 500;
@@ -2369,13 +2675,13 @@ function makeSearchTool(workdir, sandbox = {}) {
2369
2675
  if (stat.isDirectory()) {
2370
2676
  for (const entry of fs9.readdirSync(entryPath, { withFileTypes: true })) {
2371
2677
  if (IGNORE_NAMES2.has(entry.name)) continue;
2372
- visit(path8.join(entryPath, entry.name));
2678
+ visit(path9.join(entryPath, entry.name));
2373
2679
  if (results.length >= parsed.max_results || context?.signal?.aborted) break;
2374
2680
  }
2375
2681
  return;
2376
2682
  }
2377
- const relative = path8.relative(workdir, entryPath) || path8.basename(entryPath);
2378
- const portable = relative.split(path8.sep).join("/");
2683
+ const relative = path9.relative(workdir, entryPath) || path9.basename(entryPath);
2684
+ const portable = relative.split(path9.sep).join("/");
2379
2685
  if (matcher && !matcher.test(portable)) return;
2380
2686
  if (stat.size > MAX_FILE_BYTES || isBinary(entryPath)) return;
2381
2687
  let text;
@@ -2439,7 +2745,7 @@ function globToRegExp2(glob) {
2439
2745
 
2440
2746
  // src/tools/write.ts
2441
2747
  import fs10 from "fs";
2442
- import path9 from "path";
2748
+ import path10 from "path";
2443
2749
  import crypto6 from "crypto";
2444
2750
  import { z as z11 } from "zod";
2445
2751
  var argsSchema8 = z11.object({
@@ -2463,9 +2769,9 @@ function makeWriteTool(workdir, sandbox = {}) {
2463
2769
  if (fs10.statSync(file).isDirectory()) {
2464
2770
  throw new Error(`${rawPath} \u662F\u76EE\u5F55\u4E0D\u662F\u6587\u4EF6`);
2465
2771
  }
2466
- const tmp = path9.join(
2467
- path9.dirname(file),
2468
- `.xlyra-tmp-${crypto6.randomUUID()}${path9.extname(file)}`
2772
+ const tmp = path10.join(
2773
+ path10.dirname(file),
2774
+ `.xlyra-tmp-${crypto6.randomUUID()}${path10.extname(file)}`
2469
2775
  );
2470
2776
  try {
2471
2777
  fs10.writeFileSync(tmp, content);
@@ -2515,9 +2821,9 @@ function makeWriteStdinTool(manager, sandbox = {}) {
2515
2821
 
2516
2822
  // src/tools/index.ts
2517
2823
  import fs11 from "fs";
2518
- import path10 from "path";
2824
+ import path11 from "path";
2519
2825
  function builtinTools(opts) {
2520
- const workdir = path10.resolve(opts.workdir);
2826
+ const workdir = path11.resolve(opts.workdir);
2521
2827
  fs11.mkdirSync(workdir, { recursive: true });
2522
2828
  const sandbox = {
2523
2829
  allowOutsideWorkdir: opts.allowOutsideWorkdir ?? false,
@@ -2544,15 +2850,59 @@ function builtinTools(opts) {
2544
2850
 
2545
2851
  // src/server/config.ts
2546
2852
  import fs12 from "fs";
2547
- import path11 from "path";
2853
+ import path12 from "path";
2548
2854
  import { z as z13 } from "zod";
2549
2855
  var endpointSchema = z13.object({
2550
2856
  name: z13.string().min(1, "\u7AEF\u70B9\u540D\u4E0D\u80FD\u4E3A\u7A7A"),
2551
2857
  protocol: z13.enum(["openai-responses", "anthropic-messages"]),
2552
- base_url: z13.string().min(1, "base_url \u4E0D\u80FD\u4E3A\u7A7A"),
2553
- api_key: z13.string().min(1, "api_key \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u53EF\u7528 ${ENV_VAR} \u5F15\u7528\u73AF\u5883\u53D8\u91CF\uFF09"),
2858
+ /** 省略时按 direct 处理(兼容旧配置) */
2859
+ mode: z13.enum(["direct", "remote-xlyra"]).default("direct"),
2860
+ base_url: z13.url("base_url \u5FC5\u987B\u662F\u5408\u6CD5 URL"),
2861
+ api_key: z13.string().min(1, "api_key \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u53EF\u7528 ${ENV_VAR} \u5F15\u7528\u73AF\u5883\u53D8\u91CF\uFF09").optional(),
2862
+ credential: z13.discriminatedUnion("type", [
2863
+ z13.object({
2864
+ type: z13.literal("temporary-bearer"),
2865
+ token: z13.string().min(1, "credential.token \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u53EF\u7528 ${ENV_VAR} \u5F15\u7528\u73AF\u5883\u53D8\u91CF\uFF09")
2866
+ }),
2867
+ z13.object({
2868
+ type: z13.literal("xlyra-callback"),
2869
+ url: z13.url("credential.url \u5FC5\u987B\u662F\u5408\u6CD5 URL")
2870
+ })
2871
+ ]).optional(),
2554
2872
  models: z13.record(z13.string(), z13.object({ context_window: z13.number().positive().optional() })).optional(),
2555
2873
  default_model: z13.string().optional()
2874
+ }).superRefine((endpoint, ctx) => {
2875
+ if (endpoint.mode === "remote-xlyra") {
2876
+ if (!endpoint.credential) {
2877
+ ctx.addIssue({
2878
+ code: "custom",
2879
+ path: ["credential"],
2880
+ message: "remote-xlyra \u6A21\u5F0F\u5FC5\u987B\u914D\u7F6E\u4E34\u65F6\u51ED\u8BC1\uFF08temporary-bearer \u6216 xlyra-callback\uFF09"
2881
+ });
2882
+ }
2883
+ if (endpoint.api_key !== void 0) {
2884
+ ctx.addIssue({
2885
+ code: "custom",
2886
+ path: ["api_key"],
2887
+ message: "remote-xlyra \u6A21\u5F0F\u4E0D\u5141\u8BB8\u914D\u7F6E api_key"
2888
+ });
2889
+ }
2890
+ return;
2891
+ }
2892
+ if (!endpoint.api_key) {
2893
+ ctx.addIssue({
2894
+ code: "custom",
2895
+ path: ["api_key"],
2896
+ message: "direct \u6A21\u5F0F\u5FC5\u987B\u914D\u7F6E api_key"
2897
+ });
2898
+ }
2899
+ if (endpoint.credential !== void 0) {
2900
+ ctx.addIssue({
2901
+ code: "custom",
2902
+ path: ["credential"],
2903
+ message: "direct \u6A21\u5F0F\u4E0D\u5141\u8BB8\u914D\u7F6E credential"
2904
+ });
2905
+ }
2556
2906
  });
2557
2907
  var appConfigSchema = z13.object({
2558
2908
  endpoints: z13.array(endpointSchema).min(1, "endpoints \u4E3A\u7A7A\uFF1A\u81F3\u5C11\u914D\u7F6E\u4E00\u4E2A\u6A21\u578B\u7AEF\u70B9"),
@@ -2564,12 +2914,14 @@ var appConfigSchema = z13.object({
2564
2914
  workdir: z13.string().optional(),
2565
2915
  agent_name: z13.string().optional(),
2566
2916
  persona: z13.string().optional(),
2917
+ /** 部署方注入的 agent 实例标识(xlyra-callback 凭证用),run 启动参数可覆盖 */
2918
+ agent_instance_id: z13.string().optional(),
2567
2919
  /** 本机命令可访问当前用户权限范围内的系统资源,必须显式开启。 */
2568
2920
  enable_command_execution: z13.boolean().optional()
2569
2921
  }).optional()
2570
2922
  });
2571
2923
  function defaultConfigPath() {
2572
- return path11.join(defaultDataDir(), "config.json");
2924
+ return path12.join(defaultDataDir(), "config.json");
2573
2925
  }
2574
2926
  function loadConfig(configPath) {
2575
2927
  const file = configPath ?? defaultConfigPath();
@@ -2592,16 +2944,23 @@ function loadConfig(configPath) {
2592
2944
  ${issues}`);
2593
2945
  }
2594
2946
  for (const endpoint of parsed.data.endpoints) {
2595
- endpoint.api_key = interpolateEnv(endpoint.api_key, file);
2947
+ if (endpoint.api_key !== void 0) {
2948
+ endpoint.api_key = interpolateEnv(endpoint.api_key, file);
2949
+ }
2950
+ if (endpoint.credential?.type === "temporary-bearer") {
2951
+ endpoint.credential.token = interpolateEnv(endpoint.credential.token, file);
2952
+ }
2596
2953
  }
2597
2954
  return parsed.data;
2598
2955
  }
2599
2956
  function saveConfig(config, configPath) {
2600
2957
  const file = configPath ?? defaultConfigPath();
2601
- fs12.mkdirSync(path11.dirname(file), { recursive: true });
2958
+ const validated = appConfigSchema.parse(config);
2959
+ fs12.mkdirSync(path12.dirname(file), { recursive: true, mode: 448 });
2602
2960
  const tmp = `${file}.tmp`;
2603
- fs12.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
2961
+ fs12.writeFileSync(tmp, JSON.stringify(validated, null, 2) + "\n", { mode: 384 });
2604
2962
  fs12.renameSync(tmp, file);
2963
+ fs12.chmodSync(file, 384);
2605
2964
  }
2606
2965
  function configExists(configPath) {
2607
2966
  return fs12.existsSync(configPath ?? defaultConfigPath());
@@ -2616,10 +2975,11 @@ function upsertEndpoint(endpoint, configPath) {
2616
2975
  config = { endpoints: [] };
2617
2976
  }
2618
2977
  const idx = config.endpoints.findIndex((e) => e.name === endpoint.name);
2978
+ const parsedEndpoint = endpointSchema.parse(endpoint);
2619
2979
  if (idx >= 0) {
2620
- config.endpoints[idx] = endpoint;
2980
+ config.endpoints[idx] = parsedEndpoint;
2621
2981
  } else {
2622
- config.endpoints.push(endpoint);
2982
+ config.endpoints.push(parsedEndpoint);
2623
2983
  }
2624
2984
  const validated = appConfigSchema.parse(config);
2625
2985
  saveConfig(validated, file);
@@ -2675,7 +3035,7 @@ var RunRegistry = class {
2675
3035
  };
2676
3036
  this.runs.set(runId, run);
2677
3037
  this.latestBySession.set(opts.sessionId, runId);
2678
- void this.execute(run, runner, params);
3038
+ void this.execute(run, runner, params, opts.agentInstanceId);
2679
3039
  return runId;
2680
3040
  }
2681
3041
  /** 按公开会话编号读取当前(或最近一轮)事件 */
@@ -2733,11 +3093,13 @@ var RunRegistry = class {
2733
3093
  this.latestBySession.clear();
2734
3094
  }
2735
3095
  /** 消费 runner 事件并写入日志,兜住所有退出路径补齐终态 */
2736
- async execute(run, runner, params) {
3096
+ async execute(run, runner, params, agentInstanceId) {
2737
3097
  try {
2738
3098
  for await (const event of runner.start(params, {
2739
3099
  runId: run.runId,
2740
- signal: run.controller.signal
3100
+ signal: run.controller.signal,
3101
+ sessionId: run.sessionId,
3102
+ ...agentInstanceId ? { agentInstanceId } : {}
2741
3103
  })) {
2742
3104
  await this.publish(run, event);
2743
3105
  }
@@ -2938,12 +3300,15 @@ function fail(status, message) {
2938
3300
  var startPayloadSchema = z14.object({
2939
3301
  content: z14.string().min(1, "\u6D88\u606F\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A"),
2940
3302
  session_id: z14.string().optional(),
2941
- model: z14.string().default("")
3303
+ model: z14.string().default(""),
3304
+ /** 部署方注入的 agent 实例标识(xlyra-callback 凭证用),缺省用服务级配置 */
3305
+ agent_instance_id: z14.string().optional()
2942
3306
  });
2943
3307
  var retryPayloadSchema = z14.object({
2944
3308
  message_id: z14.string().min(1),
2945
3309
  content: z14.string().optional(),
2946
- model: z14.string().default("")
3310
+ model: z14.string().default(""),
3311
+ agent_instance_id: z14.string().optional()
2947
3312
  });
2948
3313
  function createAgentRoutes(ctx) {
2949
3314
  const app = new Hono();
@@ -2984,10 +3349,14 @@ function createAgentRoutes(ctx) {
2984
3349
  model: args.model,
2985
3350
  system_prompt: args.systemPrompt ?? buildSystem()
2986
3351
  },
2987
- { sessionId: args.sessionId, onTerminal: recorder.onTerminal }
3352
+ {
3353
+ sessionId: args.sessionId,
3354
+ onTerminal: recorder.onTerminal,
3355
+ ...args.agentInstanceId ?? ctx.agentInstanceId ? { agentInstanceId: args.agentInstanceId ?? ctx.agentInstanceId } : {}
3356
+ }
2988
3357
  );
2989
3358
  await recorder.begin(runId);
2990
- return { sessionId: args.sessionId, messageId };
3359
+ return { sessionId: args.sessionId, messageId, runId };
2991
3360
  }
2992
3361
  app.post("/sessions", async (c) => {
2993
3362
  const payload = startPayloadSchema.parse(await c.req.json());
@@ -3010,14 +3379,15 @@ function createAgentRoutes(ctx) {
3010
3379
  history = [];
3011
3380
  entryCount = 0;
3012
3381
  }
3013
- const { messageId } = await launchUserMessage({
3382
+ const { messageId, runId } = await launchUserMessage({
3014
3383
  sessionId,
3015
3384
  content: payload.content,
3016
3385
  model: payload.model,
3017
3386
  history,
3018
- entryCount
3387
+ entryCount,
3388
+ ...payload.agent_instance_id ? { agentInstanceId: payload.agent_instance_id } : {}
3019
3389
  });
3020
- return c.json(ok({ session_id: sessionId, message_id: messageId }, "\u7528\u6237\u6D88\u606F\u5DF2\u63D0\u4EA4"), 202);
3390
+ return c.json(ok({ session_id: sessionId, message_id: messageId, run_id: runId }, "\u7528\u6237\u6D88\u606F\u5DF2\u63D0\u4EA4"), 202);
3021
3391
  });
3022
3392
  app.get("/sessions", (c) => {
3023
3393
  const limit = Math.min(Number(c.req.query("limit") ?? 50), 200);
@@ -3128,14 +3498,15 @@ function createAgentRoutes(ctx) {
3128
3498
  ctx.store.discardFromUserMessage(sessionId, payload.message_id);
3129
3499
  const history = ctx.store.buildHistory(sessionId);
3130
3500
  const remaining = ctx.store.read(sessionId).entries.length;
3131
- const { messageId } = await launchUserMessage({
3501
+ const { messageId, runId } = await launchUserMessage({
3132
3502
  sessionId,
3133
3503
  content,
3134
3504
  model: payload.model,
3135
3505
  history,
3136
- entryCount: remaining
3506
+ entryCount: remaining,
3507
+ ...payload.agent_instance_id ? { agentInstanceId: payload.agent_instance_id } : {}
3137
3508
  });
3138
- return c.json(ok({ session_id: sessionId, message_id: messageId }, "\u5DF2\u91CD\u65B0\u63D0\u4EA4"), 202);
3509
+ return c.json(ok({ session_id: sessionId, message_id: messageId, run_id: runId }, "\u5DF2\u91CD\u65B0\u63D0\u4EA4"), 202);
3139
3510
  });
3140
3511
  app.post("/sessions/:id/grant-access", async (c) => {
3141
3512
  const sessionId = c.req.param("id");
@@ -3168,7 +3539,7 @@ function createAgentRoutes(ctx) {
3168
3539
  if (!lastUser || !isMessageEntry(lastUser)) fail(400, "\u4F1A\u8BDD\u4E2D\u6CA1\u6709\u53EF\u7EED\u8DD1\u7684\u7528\u6237\u6D88\u606F");
3169
3540
  ctx.resolver.resolve("");
3170
3541
  const history = ctx.store.buildHistoryBeforeEscalation(sessionId, escalation_id);
3171
- const { messageId } = await launchUserMessage({
3542
+ const { messageId, runId } = await launchUserMessage({
3172
3543
  sessionId,
3173
3544
  content: "",
3174
3545
  model: "",
@@ -3177,7 +3548,7 @@ function createAgentRoutes(ctx) {
3177
3548
  recordUser: false
3178
3549
  });
3179
3550
  return c.json(
3180
- ok({ session_id: sessionId, message_id: messageId, granted_path: resolved_path }, "\u5DF2\u6388\u6743\u5E76\u7EE7\u7EED\u6267\u884C"),
3551
+ ok({ session_id: sessionId, message_id: messageId, run_id: runId, granted_path: resolved_path }, "\u5DF2\u6388\u6743\u5E76\u7EE7\u7EED\u6267\u884C"),
3181
3552
  202
3182
3553
  );
3183
3554
  });
@@ -3244,7 +3615,7 @@ data: ${JSON.stringify(payload)}
3244
3615
 
3245
3616
  // src/server/session-index.ts
3246
3617
  import fs13 from "fs";
3247
- import path12 from "path";
3618
+ import path13 from "path";
3248
3619
  var HEARTBEAT_TIMEOUT_MS = 3e4;
3249
3620
  var SessionIndex = class {
3250
3621
  file;
@@ -3353,7 +3724,7 @@ var SessionIndex = class {
3353
3724
  this.persist();
3354
3725
  }
3355
3726
  persist() {
3356
- const dir = path12.dirname(this.file);
3727
+ const dir = path13.dirname(this.file);
3357
3728
  fs13.mkdirSync(dir, { recursive: true });
3358
3729
  const tmp = `${this.file}.tmp`;
3359
3730
  fs13.writeFileSync(tmp, JSON.stringify({ sessions: [...this.metas.values()] }, null, 2));
@@ -3362,42 +3733,71 @@ var SessionIndex = class {
3362
3733
  };
3363
3734
 
3364
3735
  // src/server/index.ts
3365
- import path13 from "path";
3736
+ import path14 from "path";
3366
3737
  import { serve } from "@hono/node-server";
3367
3738
 
3368
3739
  // src/server/config-routes.ts
3369
3740
  import { Hono as Hono2 } from "hono";
3741
+ import fs14 from "fs";
3370
3742
  import { z as z15 } from "zod";
3371
3743
  function maskApiKey(key) {
3372
3744
  if (key.includes("${")) return key;
3373
3745
  if (key.length <= 8) return "****";
3374
3746
  return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
3375
3747
  }
3748
+ function maskCredential(credential) {
3749
+ if (credential.type === "temporary-bearer") {
3750
+ return { type: credential.type, token: maskApiKey(credential.token) };
3751
+ }
3752
+ return { type: credential.type, url: credential.url, configured: true };
3753
+ }
3376
3754
  function maskedConfig(config) {
3377
3755
  return {
3378
3756
  ...config,
3379
- endpoints: config.endpoints.map((e) => ({ ...e, api_key: maskApiKey(e.api_key) }))
3757
+ endpoints: config.endpoints.map((e) => ({
3758
+ ...e,
3759
+ ...e.api_key !== void 0 ? { api_key: maskApiKey(e.api_key) } : {},
3760
+ ...e.credential ? { credential: maskCredential(e.credential) } : {}
3761
+ }))
3380
3762
  };
3381
3763
  }
3764
+ function retainSecret(incoming, existingRaw, existingResolved) {
3765
+ if (incoming === void 0 || existingRaw === void 0) return incoming;
3766
+ if (incoming === existingRaw) return existingRaw;
3767
+ if (existingResolved !== void 0 && incoming === maskApiKey(existingResolved)) return existingRaw;
3768
+ return incoming;
3769
+ }
3770
+ function retainCredential(incoming, existingRaw, existingResolved) {
3771
+ const prevRaw = existingRaw?.credential;
3772
+ const prevResolved = existingResolved?.credential;
3773
+ if (!incoming || !prevRaw) return incoming;
3774
+ if (prevRaw.type === "temporary-bearer" && incoming.type === "temporary-bearer") {
3775
+ const resolvedToken = prevResolved?.type === "temporary-bearer" ? prevResolved.token : void 0;
3776
+ const token = retainSecret(incoming.token, prevRaw.token, resolvedToken);
3777
+ return token === void 0 ? incoming : { type: "temporary-bearer", token };
3778
+ }
3779
+ if (prevRaw.type === "xlyra-callback" && incoming.type === "xlyra-callback" && incoming.url === prevRaw.url) {
3780
+ return prevRaw;
3781
+ }
3782
+ return incoming;
3783
+ }
3382
3784
  var putPayloadSchema = z15.object({
3383
- endpoints: z15.array(
3384
- z15.object({
3385
- name: z15.string().min(1),
3386
- protocol: z15.enum(["openai-responses", "anthropic-messages"]),
3387
- base_url: z15.string().min(1),
3388
- api_key: z15.string().min(1),
3389
- models: z15.record(z15.string(), z15.object({ context_window: z15.number().positive().optional() })).optional(),
3390
- default_model: z15.string().optional()
3391
- })
3392
- ).min(1, "endpoints \u4E3A\u7A7A\uFF1A\u81F3\u5C11\u914D\u7F6E\u4E00\u4E2A\u6A21\u578B\u7AEF\u70B9"),
3785
+ endpoints: z15.array(endpointSchema).min(1, "endpoints \u4E3A\u7A7A\uFF1A\u81F3\u5C11\u914D\u7F6E\u4E00\u4E2A\u6A21\u578B\u7AEF\u70B9"),
3786
+ /** 显式确认修改 remote-xlyra 端点的 base_url/callback 地址(SSRF 防护闸) */
3787
+ allow_remote_endpoint_override: z15.boolean().optional(),
3393
3788
  server: z15.object({ port: z15.number().int().positive().optional(), token: z15.string().optional() }).optional(),
3394
3789
  agent: z15.object({
3395
3790
  workdir: z15.string().optional(),
3396
3791
  agent_name: z15.string().optional(),
3397
3792
  persona: z15.string().optional(),
3793
+ agent_instance_id: z15.string().optional(),
3398
3794
  enable_command_execution: z15.boolean().optional()
3399
3795
  }).optional()
3400
3796
  });
3797
+ function loadRawConfig(configPath) {
3798
+ const raw = JSON.parse(fs14.readFileSync(configPath, "utf-8"));
3799
+ return appConfigSchema.parse(raw);
3800
+ }
3401
3801
  function createConfigRoutes(ctx) {
3402
3802
  const app = new Hono2();
3403
3803
  app.onError((err, c) => {
@@ -3413,19 +3813,34 @@ function createConfigRoutes(ctx) {
3413
3813
  });
3414
3814
  app.put("/config", async (c) => {
3415
3815
  const payload = putPayloadSchema.parse(await c.req.json());
3416
- const current = loadConfig(ctx.configPath);
3816
+ const current = loadRawConfig(ctx.configPath);
3817
+ const resolved = loadConfig(ctx.configPath);
3417
3818
  const endpoints = payload.endpoints.map((incoming) => {
3418
- const existing = current.endpoints.find((e) => e.name === incoming.name);
3419
- if (existing && incoming.api_key === maskApiKey(existing.api_key)) {
3420
- return { ...incoming, api_key: existing.api_key };
3819
+ const existingRaw = current.endpoints.find((e) => e.name === incoming.name);
3820
+ const existingResolved = resolved.endpoints.find((e) => e.name === incoming.name);
3821
+ const apiKey = retainSecret(incoming.api_key, existingRaw?.api_key, existingResolved?.api_key);
3822
+ const credential = retainCredential(incoming.credential, existingRaw, existingResolved);
3823
+ const merged = { ...incoming, credential };
3824
+ if (apiKey !== void 0) merged.api_key = apiKey;
3825
+ else delete merged.api_key;
3826
+ if (incoming.mode === "remote-xlyra" && existingRaw?.mode === "remote-xlyra") {
3827
+ const callbackUrlChanged = incoming.credential?.type === "xlyra-callback" && existingRaw.credential?.type === "xlyra-callback" && incoming.credential.url !== existingRaw.credential.url;
3828
+ if (incoming.base_url !== existingRaw.base_url || callbackUrlChanged) {
3829
+ if (payload.allow_remote_endpoint_override !== true) {
3830
+ throw new HttpError(
3831
+ 400,
3832
+ `\u7AEF\u70B9 ${incoming.name} \u662F remote-xlyra \u6A21\u5F0F\uFF0Cbase_url/callback \u5730\u5740\u53EA\u80FD\u7531 runner \u6216\u53D7\u4FE1\u4EFB\u7BA1\u7406\u63A5\u53E3\u4FEE\u6539\uFF08PUT \u9700\u5E26 allow_remote_endpoint_override: true \u663E\u5F0F\u786E\u8BA4\uFF09`
3833
+ );
3834
+ }
3835
+ }
3421
3836
  }
3422
- return incoming;
3837
+ return merged;
3423
3838
  });
3424
- const next = {
3839
+ const next = appConfigSchema.parse({
3425
3840
  endpoints,
3426
3841
  ...payload.server ? { server: payload.server } : {},
3427
3842
  ...payload.agent ? { agent: payload.agent } : {}
3428
- };
3843
+ });
3429
3844
  saveConfig(next, ctx.configPath);
3430
3845
  return c.json({ success: true, code: 0, message: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF08\u90E8\u5206\u542F\u52A8\u671F\u88C5\u914D\u9879\u9700\u91CD\u542F\u670D\u52A1\u540E\u751F\u6548\uFF09", data: maskedConfig(next) });
3431
3846
  });
@@ -3434,9 +3849,13 @@ function createConfigRoutes(ctx) {
3434
3849
  const config = loadConfig(ctx.configPath);
3435
3850
  const endpoint = config.endpoints.find((e) => e.name === name);
3436
3851
  if (!endpoint) throw new HttpError(404, `\u7AEF\u70B9\u4E0D\u5B58\u5728\uFF1A${name}`);
3852
+ if (endpoint.mode === "remote-xlyra" && endpoint.credential?.type === "xlyra-callback") {
3853
+ throw new HttpError(400, `\u7AEF\u70B9 ${name} \u4F7F\u7528 xlyra-callback \u51ED\u8BC1\uFF0C\u9700\u5728 run \u4E0A\u4E0B\u6587\u4E2D\u6D4B\u8BD5\uFF08\u6B64\u5904\u65E0\u6D3B\u52A8 run\uFF09`);
3854
+ }
3437
3855
  const model = endpoint.default_model ?? Object.keys(endpoint.models ?? {})[0];
3438
3856
  if (!model) throw new HttpError(400, `\u7AEF\u70B9 ${name} \u672A\u914D\u7F6E default_model \u6216 models\uFF0C\u65E0\u6CD5\u6D4B\u8BD5`);
3439
- const protocol = endpoint.protocol === "anthropic-messages" ? new AnthropicMessagesProtocol({ baseUrl: endpoint.base_url, apiKey: endpoint.api_key, providerName: endpoint.name }) : new OpenAIResponsesProtocol({ baseUrl: endpoint.base_url, apiKey: endpoint.api_key, providerName: endpoint.name });
3857
+ const credential = resolveCredentialProvider(endpoint);
3858
+ const protocol = endpoint.protocol === "anthropic-messages" ? new AnthropicMessagesProtocol({ baseUrl: endpoint.base_url, credential, providerName: endpoint.name }) : new OpenAIResponsesProtocol({ baseUrl: endpoint.base_url, credential, providerName: endpoint.name });
3440
3859
  const started = Date.now();
3441
3860
  for await (const event of protocol.chatStream({
3442
3861
  model,
@@ -3462,14 +3881,14 @@ function createConfigRoutes(ctx) {
3462
3881
 
3463
3882
  // src/server/index.ts
3464
3883
  function createAgentServer(config, opts = {}) {
3465
- const dataDir = path13.resolve(opts.dataDir ?? defaultDataDir());
3466
- const configPath = opts.configPath ?? path13.join(dataDir, "config.json");
3884
+ const dataDir = path14.resolve(opts.dataDir ?? defaultDataDir());
3885
+ const configPath = opts.configPath ?? path14.join(dataDir, "config.json");
3467
3886
  const resolver = new EndpointResolver(config.endpoints);
3468
- const store = new AgentSessionStore(path13.join(dataDir, "sessions"));
3469
- const index = new SessionIndex(path13.join(dataDir, "index.json"));
3887
+ const store = new AgentSessionStore(path14.join(dataDir, "sessions"));
3888
+ const index = new SessionIndex(path14.join(dataDir, "index.json"));
3470
3889
  index.rebuild(store.scanAll());
3471
3890
  const registry = new RunRegistry();
3472
- const workdir = path13.resolve(config.agent?.workdir ?? path13.join(dataDir, "workspace"));
3891
+ const workdir = path14.resolve(config.agent?.workdir ?? path14.join(dataDir, "workspace"));
3473
3892
  const grants = new EscalationGrants();
3474
3893
  const app = createAgentRoutes({
3475
3894
  resolver,
@@ -3486,6 +3905,7 @@ function createAgentServer(config, opts = {}) {
3486
3905
  workdir,
3487
3906
  ...config.agent?.agent_name ? { agentName: config.agent.agent_name } : {},
3488
3907
  ...config.agent?.persona ? { persona: config.agent.persona } : {},
3908
+ ...config.agent?.agent_instance_id ? { agentInstanceId: config.agent.agent_instance_id } : {},
3489
3909
  ...config.server?.token ? { token: config.server.token } : {}
3490
3910
  });
3491
3911
  const configRoutes = createConfigRoutes({ configPath });
@@ -3502,7 +3922,7 @@ function createAgentServer(config, opts = {}) {
3502
3922
  return { app, registry, store, index };
3503
3923
  }
3504
3924
  function serveFromConfig(opts) {
3505
- const configPath = opts.configPath ?? path13.join(defaultDataDir(), "config.json");
3925
+ const configPath = opts.configPath ?? path14.join(defaultDataDir(), "config.json");
3506
3926
  const config = loadConfig(configPath);
3507
3927
  const { app } = createAgentServer(config, { configPath });
3508
3928
  const port = opts.port ?? config.server?.port ?? 3210;
@@ -3525,6 +3945,12 @@ export {
3525
3945
  modelSettingsSchema,
3526
3946
  responseToMessage,
3527
3947
  LlmError,
3948
+ StaticCredentialProvider,
3949
+ XlyraCallbackCredentialProvider,
3950
+ resolveCredentialProvider,
3951
+ isRefreshable,
3952
+ httpErrorSummary,
3953
+ credentialSecrets,
3528
3954
  parseSse,
3529
3955
  ToolCallBuffer,
3530
3956
  AnthropicMessagesProtocol,
@@ -3587,4 +4013,4 @@ export {
3587
4013
  createAgentServer,
3588
4014
  serveFromConfig
3589
4015
  };
3590
- //# sourceMappingURL=chunk-QH6SEOO6.js.map
4016
+ //# sourceMappingURL=chunk-3DOSX63Z.js.map