@ryubyte/dsh-a2a 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +60 -13
- package/README.md +25 -8
- package/lib/client.js +110 -12
- package/lib/index.js +535 -30
- package/lib/types/a2a-config.d.ts +25 -1
- package/lib/types/a2a-config.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/dashboard.d.ts +8 -2
- package/lib/types/dashboard.d.ts.map +1 -1
- package/lib/types/index.d.ts +59 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/outbound.d.ts.map +1 -1
- package/lib/types/server.d.ts +225 -1
- package/lib/types/server.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/a2a-config.ts +42 -3
- package/src/client/index.ts +111 -12
- package/src/dashboard.ts +16 -4
- package/src/index.ts +285 -20
- package/src/outbound.ts +31 -4
- package/src/server.ts +474 -9
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir, networkInterfaces } from "node:os";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
4
|
//#region src/protocol.ts
|
|
5
5
|
/**
|
|
6
6
|
* A2A (Agent2Agent) Protocol v1.0 — TypeScript type definitions.
|
|
@@ -418,6 +418,17 @@ async function registerAgentTools(tools, options) {
|
|
|
418
418
|
connectionId,
|
|
419
419
|
card
|
|
420
420
|
});
|
|
421
|
+
const contextByAgent = /* @__PURE__ */ new Map();
|
|
422
|
+
const fallbackContextKey = "\0fallback";
|
|
423
|
+
const contextFor = (agentId) => {
|
|
424
|
+
const key = agentId ?? fallbackContextKey;
|
|
425
|
+
let ctxId = contextByAgent.get(key);
|
|
426
|
+
if (ctxId === void 0) {
|
|
427
|
+
ctxId = `a2a-out-${crypto.randomUUID()}`;
|
|
428
|
+
contextByAgent.set(key, ctxId);
|
|
429
|
+
}
|
|
430
|
+
return ctxId;
|
|
431
|
+
};
|
|
421
432
|
let disposers = [];
|
|
422
433
|
if (options.mapSkills === false || skills.length === 0) {
|
|
423
434
|
const name = normalizeToolName(`a2a__${options.name}__agent`);
|
|
@@ -425,7 +436,7 @@ async function registerAgentTools(tools, options) {
|
|
|
425
436
|
id: "agent",
|
|
426
437
|
name: options.name,
|
|
427
438
|
description: `${card.name}: ${card.description} (no skills advertised)`
|
|
428
|
-
}));
|
|
439
|
+
}, contextFor));
|
|
429
440
|
if (dispose) disposers = [{
|
|
430
441
|
name,
|
|
431
442
|
dispose
|
|
@@ -434,7 +445,7 @@ async function registerAgentTools(tools, options) {
|
|
|
434
445
|
const out = [];
|
|
435
446
|
for (const skill of skills) {
|
|
436
447
|
const name = skillToToolName(options.name, skill);
|
|
437
|
-
const dispose = tools.register?.(makeTool(name, client, skill));
|
|
448
|
+
const dispose = tools.register?.(makeTool(name, client, skill, contextFor));
|
|
438
449
|
if (dispose) out.push({
|
|
439
450
|
name,
|
|
440
451
|
dispose
|
|
@@ -451,7 +462,7 @@ async function registerAgentTools(tools, options) {
|
|
|
451
462
|
}
|
|
452
463
|
}));
|
|
453
464
|
}
|
|
454
|
-
function makeTool(name, client, skill) {
|
|
465
|
+
function makeTool(name, client, skill, contextFor) {
|
|
455
466
|
return {
|
|
456
467
|
name,
|
|
457
468
|
description: [skill.description, ...skill.examples?.length ? [`Examples: ${skill.examples.join(" | ")}`] : []].filter(Boolean).join("\n"),
|
|
@@ -474,6 +485,7 @@ function makeTool(name, client, skill) {
|
|
|
474
485
|
const message = {
|
|
475
486
|
messageId: crypto.randomUUID(),
|
|
476
487
|
role: "ROLE_USER",
|
|
488
|
+
contextId: contextFor(exec.agent?.id),
|
|
477
489
|
parts: [{ text: args.prompt }]
|
|
478
490
|
};
|
|
479
491
|
try {
|
|
@@ -567,10 +579,12 @@ var A2AServer = class {
|
|
|
567
579
|
execute;
|
|
568
580
|
listeners = /* @__PURE__ */ new Set();
|
|
569
581
|
baseUrl;
|
|
582
|
+
/** Abort controllers for in-flight tasks, so CancelTask can abort the executor. */
|
|
583
|
+
running = /* @__PURE__ */ new Map();
|
|
570
584
|
constructor(options, store = new TaskStore()) {
|
|
571
585
|
this.options = options;
|
|
572
586
|
this.store = store;
|
|
573
|
-
this.execute = options.execute ??
|
|
587
|
+
this.execute = options.execute ?? notConfiguredExecutor;
|
|
574
588
|
this.endpointPath = options.endpointPath ?? "/a2a";
|
|
575
589
|
this.baseUrl = options.baseUrl;
|
|
576
590
|
this.card = this.buildCard();
|
|
@@ -586,6 +600,25 @@ var A2AServer = class {
|
|
|
586
600
|
url: `${this.baseUrl}${this.endpointPath}`
|
|
587
601
|
}));
|
|
588
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* Set or clear the shared bearer token that gates the inbound endpoint at
|
|
605
|
+
* runtime. Rebuilds the AgentCard so the `bearerAuth` security scheme is
|
|
606
|
+
* advertised (token set) or withdrawn (token cleared); `authorized()` reads
|
|
607
|
+
* `options.authToken` on each request, so the gate flips immediately. Pass
|
|
608
|
+
* `undefined` or an empty string to disable token gating.
|
|
609
|
+
*/
|
|
610
|
+
setAuthToken(token) {
|
|
611
|
+
this.options.authToken = token && token.length > 0 ? token : void 0;
|
|
612
|
+
if (this.options.authToken) applyAuthScheme(this.card, this.options.authToken);
|
|
613
|
+
else {
|
|
614
|
+
delete this.card.securitySchemes;
|
|
615
|
+
delete this.card.securityRequirements;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/** Whether the inbound endpoint is currently token-gated. */
|
|
619
|
+
get authConfigured() {
|
|
620
|
+
return Boolean(this.options.authToken);
|
|
621
|
+
}
|
|
589
622
|
buildCard() {
|
|
590
623
|
const iface = {
|
|
591
624
|
url: `${this.baseUrl.replace(/\/$/, "")}${this.endpointPath}`,
|
|
@@ -593,7 +626,8 @@ var A2AServer = class {
|
|
|
593
626
|
protocolVersion: "1.0"
|
|
594
627
|
};
|
|
595
628
|
const skills = this.options.skills ?? [];
|
|
596
|
-
|
|
629
|
+
const token = this.options.authToken;
|
|
630
|
+
const card = {
|
|
597
631
|
name: this.options.agentName,
|
|
598
632
|
description: this.options.agentDescription,
|
|
599
633
|
version: this.options.agentVersion,
|
|
@@ -612,6 +646,18 @@ var A2AServer = class {
|
|
|
612
646
|
skills,
|
|
613
647
|
...this.options.iconUrl ? { iconUrl: this.options.iconUrl } : {}
|
|
614
648
|
};
|
|
649
|
+
applyAuthScheme(card, token);
|
|
650
|
+
return card;
|
|
651
|
+
}
|
|
652
|
+
/** True when the request carries the configured bearer token. */
|
|
653
|
+
authorized(req) {
|
|
654
|
+
const token = this.options.authToken;
|
|
655
|
+
if (!token) return true;
|
|
656
|
+
const h = req.headers ?? {};
|
|
657
|
+
const auth = h["authorization"] ?? h["Authorization"];
|
|
658
|
+
if (!auth) return false;
|
|
659
|
+
const m = /^Bearer\s+(.+)$/i.exec(auth.trim());
|
|
660
|
+
return !!m && m[1] === token;
|
|
615
661
|
}
|
|
616
662
|
emit(ev) {
|
|
617
663
|
for (const l of this.listeners) l(ev);
|
|
@@ -625,6 +671,19 @@ var A2AServer = class {
|
|
|
625
671
|
body: JSON.stringify(this.card)
|
|
626
672
|
};
|
|
627
673
|
if (req.method === "POST" && path === this.endpointPath) {
|
|
674
|
+
if (!this.authorized(req)) return {
|
|
675
|
+
status: 401,
|
|
676
|
+
contentType: "application/json",
|
|
677
|
+
body: JSON.stringify({
|
|
678
|
+
jsonrpc: "2.0",
|
|
679
|
+
id: null,
|
|
680
|
+
error: {
|
|
681
|
+
code: -32001,
|
|
682
|
+
message: "Unauthorized"
|
|
683
|
+
}
|
|
684
|
+
}),
|
|
685
|
+
headers: { "WWW-Authenticate": "Bearer" }
|
|
686
|
+
};
|
|
628
687
|
let payload;
|
|
629
688
|
try {
|
|
630
689
|
payload = JSON.parse(body);
|
|
@@ -671,6 +730,10 @@ var A2AServer = class {
|
|
|
671
730
|
})}\n\n`);
|
|
672
731
|
return { status: 200 };
|
|
673
732
|
}
|
|
733
|
+
if (!this.authorized(req)) return {
|
|
734
|
+
status: 401,
|
|
735
|
+
headers: { "WWW-Authenticate": "Bearer" }
|
|
736
|
+
};
|
|
674
737
|
let payload;
|
|
675
738
|
try {
|
|
676
739
|
payload = JSON.parse(body);
|
|
@@ -748,14 +811,16 @@ var A2AServer = class {
|
|
|
748
811
|
contextId,
|
|
749
812
|
status: this.store.get(taskId).status
|
|
750
813
|
} });
|
|
751
|
-
const
|
|
814
|
+
const controller = new AbortController();
|
|
815
|
+
this.running.set(taskId, controller);
|
|
752
816
|
try {
|
|
753
817
|
const reply = await this.execute({
|
|
754
818
|
message: msg,
|
|
755
819
|
taskId,
|
|
756
820
|
contextId,
|
|
757
|
-
signal
|
|
821
|
+
signal: controller.signal
|
|
758
822
|
});
|
|
823
|
+
if (controller.signal.aborted || isTerminal(this.store.get(taskId)?.status.state)) return;
|
|
759
824
|
this.store.addArtifact(taskId, {
|
|
760
825
|
artifactId: "result",
|
|
761
826
|
parts: reply.parts,
|
|
@@ -772,6 +837,7 @@ var A2AServer = class {
|
|
|
772
837
|
lastChunk: true
|
|
773
838
|
} });
|
|
774
839
|
} catch (err) {
|
|
840
|
+
if (controller.signal.aborted || isTerminal(this.store.get(taskId)?.status.state)) return;
|
|
775
841
|
const message = {
|
|
776
842
|
messageId: crypto.randomUUID(),
|
|
777
843
|
role: "ROLE_AGENT",
|
|
@@ -784,6 +850,8 @@ var A2AServer = class {
|
|
|
784
850
|
contextId,
|
|
785
851
|
status: this.store.get(taskId).status
|
|
786
852
|
} });
|
|
853
|
+
} finally {
|
|
854
|
+
this.running.delete(taskId);
|
|
787
855
|
}
|
|
788
856
|
}
|
|
789
857
|
async dispatch(rpc) {
|
|
@@ -831,6 +899,7 @@ var A2AServer = class {
|
|
|
831
899
|
code: A2A_ERROR_CODES.TASK_CANCEL_NOT_ALLOWED,
|
|
832
900
|
message: `Task ${id} already in terminal state ${task.status.state}`
|
|
833
901
|
};
|
|
902
|
+
this.running.get(id)?.abort();
|
|
834
903
|
this.store.setStatus(id, "TASK_STATE_CANCELED");
|
|
835
904
|
this.options.onTaskSettled?.(id);
|
|
836
905
|
return this.store.get(id);
|
|
@@ -859,6 +928,20 @@ var A2AServer = class {
|
|
|
859
928
|
};
|
|
860
929
|
}
|
|
861
930
|
};
|
|
931
|
+
/**
|
|
932
|
+
* Advertise the `bearerAuth` HTTP security scheme on an AgentCard when a token
|
|
933
|
+
* is configured. Mutates in place; a no-op when `token` is falsy. Shared by
|
|
934
|
+
* {@link A2AServer.buildCard} and {@link A2AServer.setAuthToken}.
|
|
935
|
+
*/
|
|
936
|
+
function applyAuthScheme(card, token) {
|
|
937
|
+
if (!token) return;
|
|
938
|
+
card.securitySchemes = { bearerAuth: {
|
|
939
|
+
type: "http",
|
|
940
|
+
scheme: "bearer",
|
|
941
|
+
bearerFormat: "opaque"
|
|
942
|
+
} };
|
|
943
|
+
card.securityRequirements = [{ schemes: { bearerAuth: [] } }];
|
|
944
|
+
}
|
|
862
945
|
/** Pull task ids out of a dispatch result for inbound peer tracking. */
|
|
863
946
|
function extractTaskIds(result) {
|
|
864
947
|
if (!result || typeof result !== "object") return [];
|
|
@@ -876,11 +959,34 @@ const defaultSkills = [{
|
|
|
876
959
|
outputModes: ["text/plain"]
|
|
877
960
|
}];
|
|
878
961
|
/**
|
|
879
|
-
*
|
|
962
|
+
* Safe default executor: refuses to act, telling the caller to inject a real
|
|
963
|
+
* executor. Used when an `A2AServer` is constructed without an `execute`
|
|
964
|
+
* option, so an inbound message never triggers arbitrary action (in particular
|
|
965
|
+
* never a shell command) unless the operator explicitly configured one.
|
|
966
|
+
*/
|
|
967
|
+
const notConfiguredExecutor = async ({ message }) => {
|
|
968
|
+
const text = message.parts.map((p) => "text" in p ? p.text : "").filter(Boolean).join("\n").slice(0, 120);
|
|
969
|
+
const detail = text ? ` (prompt: ${text}…)` : "";
|
|
970
|
+
return {
|
|
971
|
+
messageId: crypto.randomUUID(),
|
|
972
|
+
role: "ROLE_AGENT",
|
|
973
|
+
parts: [{ text: `no executor configured — inject one via the \`execute\` option${detail}` }]
|
|
974
|
+
};
|
|
975
|
+
};
|
|
976
|
+
/**
|
|
977
|
+
* Shell executor: run the incoming prompt through the system shell and
|
|
880
978
|
* return the captured stdout/stderr as the task artifact. Honors the
|
|
881
979
|
* AbortSignal (task cancellation / shutdown) by killing the child process.
|
|
980
|
+
*
|
|
981
|
+
* This is local-testing-only: arbitrary text from an inbound message becomes
|
|
982
|
+
* a `/bin/sh -c` command, so only use it against a trusted client. It is
|
|
983
|
+
* deliberately NOT the default — pass it explicitly to opt in:
|
|
984
|
+
*
|
|
985
|
+
* ```ts
|
|
986
|
+
* const server = new A2AServer({ ..., execute: shellExecutor }, store);
|
|
987
|
+
* ```
|
|
882
988
|
*/
|
|
883
|
-
const
|
|
989
|
+
const shellExecutor = async ({ message, signal }) => {
|
|
884
990
|
const text = message.parts.map((p) => "text" in p ? p.text : "").filter(Boolean).join("\n");
|
|
885
991
|
if (!text.trim()) return {
|
|
886
992
|
messageId: crypto.randomUUID(),
|
|
@@ -920,6 +1026,179 @@ const defaultExecutor = async ({ message, signal }) => {
|
|
|
920
1026
|
};
|
|
921
1027
|
}
|
|
922
1028
|
};
|
|
1029
|
+
/**
|
|
1030
|
+
* Backwards-compatible alias for {@link shellExecutor}. Kept for code that
|
|
1031
|
+
* explicitly opted into the shell executor before it was renamed; the safe
|
|
1032
|
+
* default for `A2AServer` is now {@link notConfiguredExecutor}, so using
|
|
1033
|
+
* this alias still requires an explicit opt-in.
|
|
1034
|
+
*/
|
|
1035
|
+
const defaultExecutor = shellExecutor;
|
|
1036
|
+
/** Flatten A2A message parts into a single prompt string (text parts only). */
|
|
1037
|
+
function partsToPrompt(parts) {
|
|
1038
|
+
return parts.map((p) => "text" in p && typeof p.text === "string" ? p.text : "").filter(Boolean).join("\n").trim();
|
|
1039
|
+
}
|
|
1040
|
+
/** Read the last assistant message's text off an agent's derived history. */
|
|
1041
|
+
function lastAssistantText(agent) {
|
|
1042
|
+
const history = agent.session.deriveMessages();
|
|
1043
|
+
for (let i = history.length - 1; i >= 0; i--) if (history[i].role === "assistant") return history[i].content.map((b) => b.type === "text" && typeof b.text === "string" ? b.text : "").filter(Boolean).join("\n").trim();
|
|
1044
|
+
return "";
|
|
1045
|
+
}
|
|
1046
|
+
/** Derive a stable session id for an A2A context id. */
|
|
1047
|
+
function sessionIdForContext(contextId) {
|
|
1048
|
+
return `a2a-${contextId}`;
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* DSH agent executor: forward each inbound A2A task to this harness's own agent
|
|
1052
|
+
* loop and return the assistant reply as the task artifact.
|
|
1053
|
+
*
|
|
1054
|
+
* ONE A2A CONTEXT ⇒ ONE SESSION. A2A already scopes a conversation by
|
|
1055
|
+
* `contextId`, so this executor keeps one workspace-bound agent session per
|
|
1056
|
+
* context: the first task for a context `create()`s it (session id derived from
|
|
1057
|
+
* the context, `meta.cwd` from `opts.cwd`), and later tasks for the same
|
|
1058
|
+
* context reuse the SAME live agent so history accumulates. Distinct contexts
|
|
1059
|
+
* get distinct sessions. A fresh process that sees a known context first tries
|
|
1060
|
+
* `resume()` (load persisted history), falling back to `create()`.
|
|
1061
|
+
*
|
|
1062
|
+
* Sessions are NOT disposed per task — a disposed session couldn't accumulate
|
|
1063
|
+
* context. The returned executor exposes {@link DshAgentExecutor.disposeAll} so
|
|
1064
|
+
* the plugin disposes every live per-context session on unload/teardown.
|
|
1065
|
+
*
|
|
1066
|
+
* Per-context work is serialized on a promise chain so concurrent tasks for the
|
|
1067
|
+
* same context (and the shared `whenIdle()` — a whole-agent quiescence signal,
|
|
1068
|
+
* not a per-message one) can't interleave. Different contexts run in parallel.
|
|
1069
|
+
*
|
|
1070
|
+
* Enable this via the plugin's `execute` precedence in `index.ts`; it is not
|
|
1071
|
+
* `A2AServer`'s default (the default stays {@link notConfiguredExecutor}).
|
|
1072
|
+
*/
|
|
1073
|
+
function createDshAgentExecutor(agents, opts) {
|
|
1074
|
+
const plugin = opts.plugin ?? "a2a";
|
|
1075
|
+
const handles = /* @__PURE__ */ new Map();
|
|
1076
|
+
const tails = /* @__PURE__ */ new Map();
|
|
1077
|
+
/**
|
|
1078
|
+
* Get or open the live agent for a context. Returns the agent and whether it
|
|
1079
|
+
* was JUST opened (so the caller fires the one-time onSessionOpened hook after
|
|
1080
|
+
* the first prompt lands, keeping "open" and "first message" atomic per tail).
|
|
1081
|
+
* `justOpened` is true only on the create/resume path — a cached handle returns
|
|
1082
|
+
* false — so it alone gates the once-per-context hook; serialization on the
|
|
1083
|
+
* per-context tail guarantees no two openings race.
|
|
1084
|
+
*/
|
|
1085
|
+
const agentFor = async (contextId) => {
|
|
1086
|
+
const sessionId = sessionIdForContext(contextId);
|
|
1087
|
+
const existing = handles.get(contextId);
|
|
1088
|
+
if (existing) return {
|
|
1089
|
+
agent: existing.agent,
|
|
1090
|
+
sessionId,
|
|
1091
|
+
justOpened: false
|
|
1092
|
+
};
|
|
1093
|
+
const agentOptions = opts.resolveAgentOptions?.() ?? opts.agentOptions;
|
|
1094
|
+
const withModel = agentOptions ? { agentOptions } : {};
|
|
1095
|
+
const presets = opts.agentPresets;
|
|
1096
|
+
let presetId;
|
|
1097
|
+
let setup;
|
|
1098
|
+
if (presets) try {
|
|
1099
|
+
presetId = (await presets.resolve()).id;
|
|
1100
|
+
if (!presetId) throw new Error("agent preset roster resolved to no id");
|
|
1101
|
+
setup = async (agentCtx) => {
|
|
1102
|
+
await presets.mount(agentCtx, presetId);
|
|
1103
|
+
};
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
throw new Error(`a2a: failed to resolve agent preset: ${err.message}`);
|
|
1106
|
+
}
|
|
1107
|
+
const createOptions = {
|
|
1108
|
+
sessionId,
|
|
1109
|
+
...presetId ? { meta: {
|
|
1110
|
+
cwd: opts.cwd,
|
|
1111
|
+
agentPreset: presetId
|
|
1112
|
+
} } : { meta: { cwd: opts.cwd } },
|
|
1113
|
+
...withModel,
|
|
1114
|
+
...setup ? { setup } : {}
|
|
1115
|
+
};
|
|
1116
|
+
let handle;
|
|
1117
|
+
try {
|
|
1118
|
+
handle = await agents.create(createOptions);
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
if (/already (has|owns)|persisted/i.test(err.message)) handle = await agents.resume({
|
|
1121
|
+
resumeSessionId: sessionId,
|
|
1122
|
+
...withModel,
|
|
1123
|
+
...setup ? { setup } : {}
|
|
1124
|
+
});
|
|
1125
|
+
else throw err;
|
|
1126
|
+
}
|
|
1127
|
+
handles.set(contextId, handle);
|
|
1128
|
+
return {
|
|
1129
|
+
agent: handle.agent,
|
|
1130
|
+
sessionId,
|
|
1131
|
+
justOpened: true
|
|
1132
|
+
};
|
|
1133
|
+
};
|
|
1134
|
+
/** Run one turn on a context's session, serialized behind its tail. */
|
|
1135
|
+
const runTurn = async (contextId, prompt, signal) => {
|
|
1136
|
+
const { agent, sessionId, justOpened } = await agentFor(contextId);
|
|
1137
|
+
const onAbort = () => agent.cancel("a2a-canceled");
|
|
1138
|
+
if (signal?.aborted) onAbort();
|
|
1139
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1140
|
+
const userMessage = Object.freeze({
|
|
1141
|
+
id: crypto.randomUUID(),
|
|
1142
|
+
role: "user",
|
|
1143
|
+
content: [{
|
|
1144
|
+
type: "text",
|
|
1145
|
+
text: prompt
|
|
1146
|
+
}],
|
|
1147
|
+
source: {
|
|
1148
|
+
kind: "plugin",
|
|
1149
|
+
plugin
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
agent.send(userMessage, "next-turn", true);
|
|
1153
|
+
if (justOpened) try {
|
|
1154
|
+
await opts.onSessionOpened?.({
|
|
1155
|
+
sessionId,
|
|
1156
|
+
contextId,
|
|
1157
|
+
session: agent.session,
|
|
1158
|
+
firstPrompt: prompt
|
|
1159
|
+
});
|
|
1160
|
+
} catch {}
|
|
1161
|
+
await agent.whenIdle();
|
|
1162
|
+
return lastAssistantText(agent);
|
|
1163
|
+
};
|
|
1164
|
+
const executor = (async ({ message, contextId, signal }) => {
|
|
1165
|
+
const prompt = partsToPrompt(message.parts);
|
|
1166
|
+
if (!prompt) return {
|
|
1167
|
+
messageId: crypto.randomUUID(),
|
|
1168
|
+
role: "ROLE_AGENT",
|
|
1169
|
+
parts: [{ text: "No prompt provided." }]
|
|
1170
|
+
};
|
|
1171
|
+
const ctxKey = contextId;
|
|
1172
|
+
const run = (tails.get(ctxKey) ?? Promise.resolve()).catch(() => {}).then(() => runTurn(ctxKey, prompt, signal));
|
|
1173
|
+
tails.set(ctxKey, run);
|
|
1174
|
+
try {
|
|
1175
|
+
const replyText = await run;
|
|
1176
|
+
const text = signal?.aborted ? `(canceled) ${replyText}`.trim() : replyText || "(no reply)";
|
|
1177
|
+
return {
|
|
1178
|
+
messageId: crypto.randomUUID(),
|
|
1179
|
+
role: "ROLE_AGENT",
|
|
1180
|
+
parts: [{ text }]
|
|
1181
|
+
};
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
return {
|
|
1184
|
+
messageId: crypto.randomUUID(),
|
|
1185
|
+
role: "ROLE_AGENT",
|
|
1186
|
+
parts: [{ text: `DSH agent executor error: ${err.message}` }]
|
|
1187
|
+
};
|
|
1188
|
+
} finally {
|
|
1189
|
+
if (tails.get(ctxKey) === run) tails.delete(ctxKey);
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
executor.disposeAll = async () => {
|
|
1193
|
+
const live = [...handles.values()];
|
|
1194
|
+
const pending = [...tails.values()];
|
|
1195
|
+
handles.clear();
|
|
1196
|
+
tails.clear();
|
|
1197
|
+
await Promise.allSettled(pending);
|
|
1198
|
+
await Promise.allSettled(live.map((h) => h.dispose()));
|
|
1199
|
+
};
|
|
1200
|
+
return executor;
|
|
1201
|
+
}
|
|
923
1202
|
//#endregion
|
|
924
1203
|
//#region src/dashboard.ts
|
|
925
1204
|
/** Registry of live A2A connections, both directions. */
|
|
@@ -1077,7 +1356,8 @@ var DashboardRegistry = class {
|
|
|
1077
1356
|
ok: false,
|
|
1078
1357
|
message: "discover-agent requires agentCardUrl"
|
|
1079
1358
|
};
|
|
1080
|
-
|
|
1359
|
+
const bearerToken = payload?.bearerToken || void 0;
|
|
1360
|
+
return this.hooks.discoverAgent ? this.hooks.discoverAgent(url, { bearerToken }) : {
|
|
1081
1361
|
ok: false,
|
|
1082
1362
|
message: "discover-agent is not wired"
|
|
1083
1363
|
};
|
|
@@ -1089,7 +1369,8 @@ var DashboardRegistry = class {
|
|
|
1089
1369
|
ok: false,
|
|
1090
1370
|
message: "add-agent requires agentCardUrl"
|
|
1091
1371
|
};
|
|
1092
|
-
|
|
1372
|
+
const bearerToken = payload?.bearerToken || void 0;
|
|
1373
|
+
return this.hooks.addAgent ? this.hooks.addAgent(name, url, { bearerToken }) : {
|
|
1093
1374
|
ok: false,
|
|
1094
1375
|
message: "add-agent is not wired (client mode not mounted)"
|
|
1095
1376
|
};
|
|
@@ -1106,6 +1387,13 @@ var DashboardRegistry = class {
|
|
|
1106
1387
|
ok: false,
|
|
1107
1388
|
message: "server control is not wired (server mode not mounted)"
|
|
1108
1389
|
};
|
|
1390
|
+
case "set-server-auth": {
|
|
1391
|
+
const token = payload?.authToken || void 0;
|
|
1392
|
+
return this.hooks.setServerAuthToken ? this.hooks.setServerAuthToken(token) : {
|
|
1393
|
+
ok: false,
|
|
1394
|
+
message: "set-server-auth is not wired (server mode not mounted)"
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1109
1397
|
case "server-status": return this.hooks.serverStatus ? Promise.resolve(this.hooks.serverStatus()) : {
|
|
1110
1398
|
ok: false,
|
|
1111
1399
|
message: "server-status is not wired (server mode not mounted)"
|
|
@@ -1191,6 +1479,30 @@ function profileNameFromArgv() {
|
|
|
1191
1479
|
}
|
|
1192
1480
|
}
|
|
1193
1481
|
/**
|
|
1482
|
+
* Best-effort profile-name fallback for launchers that resolve a profile
|
|
1483
|
+
* WITHOUT writing `--profile` back into argv.
|
|
1484
|
+
*
|
|
1485
|
+
* The dsh `web` subcommand is a hardcoded alias for `--profile web`: the
|
|
1486
|
+
* launcher resolves the name internally and boots the web profile, but the
|
|
1487
|
+
* process argv keeps the original tokens (`node …/dsh web [flags]`) — there
|
|
1488
|
+
* is no `--profile` flag and no environment variable naming the profile. So
|
|
1489
|
+
* when {@link profileNameFromArgv} finds nothing, check whether the first
|
|
1490
|
+
* non-flag token after argv[0] is a known dsh subcommand alias:
|
|
1491
|
+
*
|
|
1492
|
+
* - `web` → boot profile `web` (alias, never followed by a name)
|
|
1493
|
+
* - `--profile <n>` → already handled above
|
|
1494
|
+
* - anything else → unknown/not a profile alias (e.g. `plugin`, which
|
|
1495
|
+
* is a management mode, not a boot; returns undefined)
|
|
1496
|
+
*
|
|
1497
|
+
* Returns undefined when argv carries no recognizable alias, letting the
|
|
1498
|
+
* caller fall through to the cwd-based resolution steps.
|
|
1499
|
+
*/
|
|
1500
|
+
function profileNameFromDshAlias() {
|
|
1501
|
+
const argv = process.argv ?? [];
|
|
1502
|
+
if (argv.includes("--profile") || argv.some((a) => a.startsWith("--profile="))) return void 0;
|
|
1503
|
+
if (argv[2] === "web") return "web";
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1194
1506
|
* Resolve the a2a.json path for the current process, without depending on
|
|
1195
1507
|
* the launch directory. The plugin must work no matter where `dsh` was
|
|
1196
1508
|
* started from (dsh does NOT chdir into the profile directory), so this
|
|
@@ -1200,7 +1512,9 @@ function profileNameFromArgv() {
|
|
|
1200
1512
|
*
|
|
1201
1513
|
* Order:
|
|
1202
1514
|
* 0. `$DSH_HOME/profiles/<argv --profile>/a2a.json` — the active profile
|
|
1203
|
-
* as named on the command line (most authoritative; works from ANY cwd)
|
|
1515
|
+
* as named on the command line (most authoritative; works from ANY cwd).
|
|
1516
|
+
* Also matches the `dsh web` alias, which boots profile `web` without
|
|
1517
|
+
* writing `--profile` into argv.
|
|
1204
1518
|
* 1. `cwd/a2a.json` — explicit when launched inside a profile dir
|
|
1205
1519
|
* 2. outward from cwd: the nearest ancestor holding `a2a.json` — covers
|
|
1206
1520
|
* launching from e.g. a repo checkout whose parent chain includes the
|
|
@@ -1218,7 +1532,7 @@ function resolveConfigPath() {
|
|
|
1218
1532
|
const cwd = process.cwd();
|
|
1219
1533
|
const home = process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
1220
1534
|
const base = cwd.split(/[\\/]/).pop() ?? "";
|
|
1221
|
-
const profileName = profileNameFromArgv();
|
|
1535
|
+
const profileName = profileNameFromArgv() ?? profileNameFromDshAlias();
|
|
1222
1536
|
if (profileName && profileName !== "." && profileName !== ".." && !profileName.includes("/") && !profileName.includes("\\")) {
|
|
1223
1537
|
const argvFile = join(home, "profiles", profileName, A2A_CONFIG_FILENAME);
|
|
1224
1538
|
if (existsSync(argvFile)) return argvFile;
|
|
@@ -1291,6 +1605,7 @@ function mergePersisted(base, persisted) {
|
|
|
1291
1605
|
if (pServer.agentDescription) merged.agentDescription = pServer.agentDescription;
|
|
1292
1606
|
if (pServer.agentVersion) merged.agentVersion = pServer.agentVersion;
|
|
1293
1607
|
if (pServer.endpointPath) merged.endpointPath = pServer.endpointPath;
|
|
1608
|
+
if (pServer.authToken !== void 0) merged.authToken = pServer.authToken;
|
|
1294
1609
|
if (pServer.skills) merged.skills = pServer.skills;
|
|
1295
1610
|
}
|
|
1296
1611
|
return merged;
|
|
@@ -1599,8 +1914,9 @@ function apply(ctx, config = {}) {
|
|
|
1599
1914
|
message: `connection ${connectionId} reconnected as ${newId}`
|
|
1600
1915
|
};
|
|
1601
1916
|
},
|
|
1602
|
-
addAgent: async (name, agentCardUrl) => {
|
|
1603
|
-
const
|
|
1917
|
+
addAgent: async (name, agentCardUrl, opts) => {
|
|
1918
|
+
const bearerToken = opts?.bearerToken;
|
|
1919
|
+
const cid = await connect(name, agentCardUrl, true, { bearerToken });
|
|
1604
1920
|
if (!cid) return {
|
|
1605
1921
|
ok: false,
|
|
1606
1922
|
message: `failed to connect to ${agentCardUrl}`
|
|
@@ -1609,7 +1925,8 @@ function apply(ctx, config = {}) {
|
|
|
1609
1925
|
name,
|
|
1610
1926
|
agentCardUrl,
|
|
1611
1927
|
configured: true,
|
|
1612
|
-
enabled: true
|
|
1928
|
+
enabled: true,
|
|
1929
|
+
bearerToken
|
|
1613
1930
|
});
|
|
1614
1931
|
persistAgents();
|
|
1615
1932
|
return {
|
|
@@ -1617,11 +1934,11 @@ function apply(ctx, config = {}) {
|
|
|
1617
1934
|
message: `agent "${name}" connected as ${cid} (saved to ${configPath})`
|
|
1618
1935
|
};
|
|
1619
1936
|
},
|
|
1620
|
-
discoverAgent: async (agentCardUrl) => {
|
|
1937
|
+
discoverAgent: async (agentCardUrl, opts) => {
|
|
1621
1938
|
try {
|
|
1622
1939
|
const card = await fetchAgentCard(agentCardUrl, {
|
|
1623
1940
|
timeoutMs: mergedConfig.timeoutMs ?? 8e3,
|
|
1624
|
-
bearerToken: mergedConfig.bearerToken
|
|
1941
|
+
bearerToken: opts?.bearerToken ?? mergedConfig.bearerToken
|
|
1625
1942
|
});
|
|
1626
1943
|
const iface = pickInterface(card);
|
|
1627
1944
|
return {
|
|
@@ -1724,8 +2041,37 @@ function apply(ctx, config = {}) {
|
|
|
1724
2041
|
ctx.inject(["webServer"], (ctx) => {
|
|
1725
2042
|
ctx.effect(() => {
|
|
1726
2043
|
const webServer = ctx.webServer;
|
|
1727
|
-
const baseUrl = mergedConfig.baseUrl ??
|
|
2044
|
+
const baseUrl = mergedConfig.baseUrl ?? inferBaseUrl(webServer.host, webServer.port);
|
|
1728
2045
|
const endpointPath = mergedConfig.endpointPath ?? DEFAULT_ENDPOINT;
|
|
2046
|
+
let execute = mergedConfig.execute;
|
|
2047
|
+
let executorKind = execute ? "custom" : "none";
|
|
2048
|
+
let disposeExecutor;
|
|
2049
|
+
if (!execute) {
|
|
2050
|
+
const agents = probeAgentRegistry(ctx);
|
|
2051
|
+
if (agents) {
|
|
2052
|
+
const inboundCwd = inboundSessionCwd(configPath);
|
|
2053
|
+
const dshExec = createDshAgentExecutor(agents, {
|
|
2054
|
+
cwd: inboundCwd,
|
|
2055
|
+
plugin: "a2a",
|
|
2056
|
+
agentPresets: probeAgentPresets(ctx),
|
|
2057
|
+
resolveAgentOptions: () => {
|
|
2058
|
+
const sel = probeAgentDefaultModel(ctx)?.currentSelection();
|
|
2059
|
+
if (!sel?.model && !sel?.provider) return void 0;
|
|
2060
|
+
return {
|
|
2061
|
+
provider: sel.provider,
|
|
2062
|
+
model: sel.model
|
|
2063
|
+
};
|
|
2064
|
+
},
|
|
2065
|
+
onSessionOpened: ({ session, firstPrompt }) => {
|
|
2066
|
+
probeSessionTitle(ctx)?.rename(session, `A2A: ${summarize(firstPrompt)}`);
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
execute = dshExec;
|
|
2070
|
+
disposeExecutor = () => dshExec.disposeAll();
|
|
2071
|
+
executorKind = "dsh-agent";
|
|
2072
|
+
logger?.info?.(`[a2a] server executor: DSH agent session per A2A context (cwd ${inboundCwd})`);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
1729
2075
|
const server = new A2AServer({
|
|
1730
2076
|
baseUrl,
|
|
1731
2077
|
agentName: mergedConfig.agentName ?? "DSH Agent",
|
|
@@ -1733,7 +2079,8 @@ function apply(ctx, config = {}) {
|
|
|
1733
2079
|
agentVersion: mergedConfig.agentVersion ?? "0.1.0",
|
|
1734
2080
|
skills: mergedConfig.skills,
|
|
1735
2081
|
endpointPath,
|
|
1736
|
-
execute
|
|
2082
|
+
execute,
|
|
2083
|
+
authToken: mergedConfig.authToken,
|
|
1737
2084
|
onInbound: (facts) => {
|
|
1738
2085
|
const peerId = dashboard.touchInbound({
|
|
1739
2086
|
headers: facts.headers,
|
|
@@ -1780,17 +2127,23 @@ function apply(ctx, config = {}) {
|
|
|
1780
2127
|
}
|
|
1781
2128
|
const body = await readBody(req);
|
|
1782
2129
|
if (isStream) {
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
2130
|
+
const stream = await server.handleStream(toServerReq(req), body, (frame) => res.write(frame));
|
|
2131
|
+
if (stream.status !== 200) {
|
|
2132
|
+
res.writeHead(stream.status, {
|
|
2133
|
+
"content-type": "text/plain",
|
|
2134
|
+
...stream.headers ?? {}
|
|
2135
|
+
});
|
|
2136
|
+
res.end("Unauthorized");
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
1789
2139
|
res.end();
|
|
1790
2140
|
return;
|
|
1791
2141
|
}
|
|
1792
2142
|
const out = await server.handle(toServerReq(req), body);
|
|
1793
|
-
res.writeHead(out.status, {
|
|
2143
|
+
res.writeHead(out.status, {
|
|
2144
|
+
"content-type": out.contentType,
|
|
2145
|
+
...out.headers ?? {}
|
|
2146
|
+
});
|
|
1794
2147
|
res.end(out.body);
|
|
1795
2148
|
}
|
|
1796
2149
|
}));
|
|
@@ -1813,7 +2166,9 @@ function apply(ctx, config = {}) {
|
|
|
1813
2166
|
name: s.name,
|
|
1814
2167
|
description: s.description
|
|
1815
2168
|
})),
|
|
1816
|
-
customExecutor: Boolean(mergedConfig.execute)
|
|
2169
|
+
customExecutor: Boolean(mergedConfig.execute),
|
|
2170
|
+
executor: executorKind,
|
|
2171
|
+
authConfigured: server.authConfigured
|
|
1817
2172
|
});
|
|
1818
2173
|
dashboard.setHooks({
|
|
1819
2174
|
setServerEnabled: async (enable) => {
|
|
@@ -1838,6 +2193,27 @@ function apply(ctx, config = {}) {
|
|
|
1838
2193
|
message: "A2A server disabled"
|
|
1839
2194
|
};
|
|
1840
2195
|
},
|
|
2196
|
+
setServerAuthToken: async (token) => {
|
|
2197
|
+
server.setAuthToken(token);
|
|
2198
|
+
const next = loadConfig();
|
|
2199
|
+
next.server = {
|
|
2200
|
+
...next.server ?? {},
|
|
2201
|
+
authToken: token && token.length > 0 ? token : void 0
|
|
2202
|
+
};
|
|
2203
|
+
const res = saveConfig(next);
|
|
2204
|
+
if (!res.ok) {
|
|
2205
|
+
logger?.error?.(`[a2a] failed to persist authToken to ${res.path}: ${res.message}`);
|
|
2206
|
+
return {
|
|
2207
|
+
ok: false,
|
|
2208
|
+
message: `applied in-memory but failed to save: ${res.message}`
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
logger?.info?.(`[a2a] inbound authToken ${token ? "set" : "cleared"} (saved to ${res.path})`);
|
|
2212
|
+
return {
|
|
2213
|
+
ok: true,
|
|
2214
|
+
message: token ? "已设置入站鉴权 token" : "已清除入站鉴权 token"
|
|
2215
|
+
};
|
|
2216
|
+
},
|
|
1841
2217
|
serverStatus: () => ({
|
|
1842
2218
|
ok: true,
|
|
1843
2219
|
message: JSON.stringify({
|
|
@@ -1848,11 +2224,140 @@ function apply(ctx, config = {}) {
|
|
|
1848
2224
|
});
|
|
1849
2225
|
return () => {
|
|
1850
2226
|
unregisterRoutes();
|
|
2227
|
+
disposeExecutor?.().catch((err) => logger?.error?.(`[a2a] executor dispose failed: ${err.message}`));
|
|
1851
2228
|
};
|
|
1852
2229
|
}, "a2a: server routes");
|
|
1853
2230
|
});
|
|
1854
2231
|
}
|
|
1855
2232
|
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Probe for a usable agent registry WITHOUT taking an inject dependency on it.
|
|
2235
|
+
*
|
|
2236
|
+
* `ctx.agents` (the registry) can be present while `create()` still rejects —
|
|
2237
|
+
* that happens on a bare dsh-base profile that has the agent layer but no
|
|
2238
|
+
* agent-loop plugin to register the creation factory. We can't cheaply tell
|
|
2239
|
+
* "factory registered" apart from "registry present" without attempting a
|
|
2240
|
+
* create, so this returns the registry when it's structurally usable and lets
|
|
2241
|
+
* {@link createDshAgentExecutor} degrade to a readable error if a later
|
|
2242
|
+
* `create()` rejects.
|
|
2243
|
+
*/
|
|
2244
|
+
function probeAgentRegistry(ctx) {
|
|
2245
|
+
return probeService(ctx, "agents", "create");
|
|
2246
|
+
}
|
|
2247
|
+
/**
|
|
2248
|
+
* Probe for the agent-preset roster service (`ctx.agentPresets`, owned by
|
|
2249
|
+
* `@deepseek-ai/dsh-agent-presets`) without an inject dependency. When the
|
|
2250
|
+
* deployment composes presets (`dsh-web-app` mounts the roster with a
|
|
2251
|
+
* `default: standard`), inbound sessions must be composed from the same
|
|
2252
|
+
* preset or they publish against the empty global layer — tools, prompt
|
|
2253
|
+
* sections, and skills all absent. Absent rosters keep legacy behavior.
|
|
2254
|
+
*/
|
|
2255
|
+
function probeAgentPresets(ctx) {
|
|
2256
|
+
return probeService(ctx, "agentPresets", "resolve");
|
|
2257
|
+
}
|
|
2258
|
+
/**
|
|
2259
|
+
* Probe for the deployment default-model service without an inject dependency.
|
|
2260
|
+
* Returns undefined when the service is absent, so the executor simply creates
|
|
2261
|
+
* the agent without a seeded selection — fine for compositions that seed the
|
|
2262
|
+
* model another way.
|
|
2263
|
+
*/
|
|
2264
|
+
function probeAgentDefaultModel(ctx) {
|
|
2265
|
+
return probeService(ctx, "agentDefaultModel", "currentSelection");
|
|
2266
|
+
}
|
|
2267
|
+
/** Probe `ctx.sessionTitle` (optional). Used to name inbound sessions. */
|
|
2268
|
+
function probeSessionTitle(ctx) {
|
|
2269
|
+
return probeService(ctx, "sessionTitle", "rename");
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2272
|
+
* Read a strict service off the reflection layer and confirm it exposes the
|
|
2273
|
+
* named method, without taking an inject dependency. Returns undefined when the
|
|
2274
|
+
* service is absent, its fiber inactive, or the method missing.
|
|
2275
|
+
*/
|
|
2276
|
+
function probeService(ctx, name, method) {
|
|
2277
|
+
try {
|
|
2278
|
+
const svc = ctx.reflect?.get(name, true);
|
|
2279
|
+
if (svc && typeof svc[method] === "function") return svc;
|
|
2280
|
+
} catch {}
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* One-line title summary from a prompt: first line, whitespace-collapsed, cut
|
|
2284
|
+
* to a display-friendly length on a word boundary when possible.
|
|
2285
|
+
*/
|
|
2286
|
+
function summarize(prompt, maxChars = 40) {
|
|
2287
|
+
const text = prompt.replace(/\s+/g, " ").trim();
|
|
2288
|
+
if (text.length <= maxChars) return text || "(empty)";
|
|
2289
|
+
const cut = text.slice(0, maxChars);
|
|
2290
|
+
const sp = cut.lastIndexOf(" ");
|
|
2291
|
+
return `${(sp > maxChars * .6 ? cut.slice(0, sp) : cut).trimEnd()}…`;
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* Absolute cwd for inbound per-context sessions: a `.a2a-sessions` subdir of
|
|
2295
|
+
* the profile directory (the dir holding `a2a.json`), kept separate from the
|
|
2296
|
+
* profile root so inbound sessions don't crowd the user's interactive
|
|
2297
|
+
* conversations there. They surface in the session list's "Ungrouped" bucket,
|
|
2298
|
+
* named `A2A: <summary>`. Created if missing (persistence writes under it);
|
|
2299
|
+
* falls back to cwd on any failure.
|
|
2300
|
+
*/
|
|
2301
|
+
function inboundSessionCwd(configPath) {
|
|
2302
|
+
try {
|
|
2303
|
+
const dir = join(dirname(configPath), ".a2a-sessions");
|
|
2304
|
+
mkdirSync(dir, { recursive: true });
|
|
2305
|
+
return dir;
|
|
2306
|
+
} catch {
|
|
2307
|
+
return process.cwd();
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Derive the public base URL advertised in the AgentCard from the real wire
|
|
2312
|
+
* listen address, when the operator did not pin `baseUrl` in config.
|
|
2313
|
+
*
|
|
2314
|
+
* The A2A AgentCard must carry a URL a REMOTE caller can reach. When the web
|
|
2315
|
+
* server binds only loopback (`127.0.0.1`), that address is correct. But when
|
|
2316
|
+
* it binds every interface (`0.0.0.0`), advertising `127.0.0.1` is wrong for
|
|
2317
|
+
* every non-local caller — it points at the caller's own loopback. In that
|
|
2318
|
+
* case pick the machine's best-known LAN address (the primary non-loopback
|
|
2319
|
+
* IPv4 of the interface that routes to the default gateway), and only fall
|
|
2320
|
+
* back to loopback when no such address exists. IPv6 link-local is skipped
|
|
2321
|
+
* (not reachable without a scope id).
|
|
2322
|
+
*
|
|
2323
|
+
* @param host - the web server's configured bind host ('127.0.0.1' | '0.0.0.0').
|
|
2324
|
+
* @param port - the OS-assigned (or configured) listen port.
|
|
2325
|
+
* @returns a scheme+host+port URL for Agents to call back into this DSH.
|
|
2326
|
+
*/
|
|
2327
|
+
function inferBaseUrl(host, port) {
|
|
2328
|
+
return `http://${host === "0.0.0.0" ? primaryLanAddress() ?? "127.0.0.1" : host || "127.0.0.1"}:${port}`;
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Best single LAN IPv4 for this machine: the address of the non-loopback
|
|
2332
|
+
* interface whose route reaches the default gateway (the address a remote
|
|
2333
|
+
* peer on the same subnet would use to reach us). Returns undefined when the
|
|
2334
|
+
* machine has no non-loopback IPv4 (e.g. offline / containers).
|
|
2335
|
+
*
|
|
2336
|
+
* Prefers IPv4 (A2A URLs here are v4-style), skips link-local (169.254.0.0/16)
|
|
2337
|
+
* and CGNAT (100.64.0.0/10) ranges, which are useless to advertise.
|
|
2338
|
+
*/
|
|
2339
|
+
function primaryLanAddress() {
|
|
2340
|
+
try {
|
|
2341
|
+
const ifaces = networkInterfaces();
|
|
2342
|
+
let candidate;
|
|
2343
|
+
for (const name of Object.keys(ifaces)) {
|
|
2344
|
+
const addrs = ifaces[name] ?? [];
|
|
2345
|
+
for (const a of addrs) {
|
|
2346
|
+
if (a.family !== "IPv4" || a.internal) continue;
|
|
2347
|
+
const ip = a.address;
|
|
2348
|
+
if (ip.startsWith("169.254.")) continue;
|
|
2349
|
+
const first = Number(ip.split(".")[0]);
|
|
2350
|
+
const second = Number(ip.split(".")[1]);
|
|
2351
|
+
if (first === 100 && second >= 64 && second <= 127) continue;
|
|
2352
|
+
if (!candidate) candidate = ip;
|
|
2353
|
+
if (name === "en0" || name === "eth0" || name === "ens3" || name === "ens5") return ip;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return candidate;
|
|
2357
|
+
} catch {
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
1856
2361
|
/** Drain the request body from a Node IncomingMessage. */
|
|
1857
2362
|
async function readBody(req) {
|
|
1858
2363
|
return new Promise((resolve, reject) => {
|
|
@@ -1893,4 +2398,4 @@ function toServerReq(req) {
|
|
|
1893
2398
|
};
|
|
1894
2399
|
}
|
|
1895
2400
|
//#endregion
|
|
1896
|
-
export { A2AClient, A2AError, A2AServer, A2A_ERROR_CODES, A2A_METHODS, DashboardRegistry, Role, TaskState, TaskStore, apply, defaultExecutor, defaultSkills, fetchAgentCard, getSharedRegistry, inject, isTerminal, name, registerAgentTools };
|
|
2401
|
+
export { A2AClient, A2AError, A2AServer, A2A_ERROR_CODES, A2A_METHODS, DashboardRegistry, Role, TaskState, TaskStore, apply, createDshAgentExecutor, defaultExecutor, defaultSkills, fetchAgentCard, getSharedRegistry, inferBaseUrl, inject, isTerminal, name, notConfiguredExecutor, registerAgentTools, shellExecutor };
|