dsh-a2a-server 0.1.0
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/LICENSE +21 -0
- package/README.md +335 -0
- package/README.zh-CN.md +316 -0
- package/cordis.patch.yml +30 -0
- package/lib/config.d.ts +116 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +110 -0
- package/lib/config.js.map +1 -0
- package/lib/contexts.d.ts +84 -0
- package/lib/contexts.d.ts.map +1 -0
- package/lib/contexts.js +103 -0
- package/lib/contexts.js.map +1 -0
- package/lib/index.d.ts +33 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +505 -0
- package/lib/index.js.map +1 -0
- package/lib/projection.d.ts +131 -0
- package/lib/projection.d.ts.map +1 -0
- package/lib/projection.js +98 -0
- package/lib/projection.js.map +1 -0
- package/lib/protocol/brand.d.ts +32 -0
- package/lib/protocol/brand.d.ts.map +1 -0
- package/lib/protocol/brand.js +25 -0
- package/lib/protocol/brand.js.map +1 -0
- package/lib/protocol/card.d.ts +43 -0
- package/lib/protocol/card.d.ts.map +1 -0
- package/lib/protocol/card.js +58 -0
- package/lib/protocol/card.js.map +1 -0
- package/lib/protocol/index.d.ts +23 -0
- package/lib/protocol/index.d.ts.map +1 -0
- package/lib/protocol/index.js +20 -0
- package/lib/protocol/index.js.map +1 -0
- package/lib/protocol/jsonrpc.d.ts +93 -0
- package/lib/protocol/jsonrpc.d.ts.map +1 -0
- package/lib/protocol/jsonrpc.js +81 -0
- package/lib/protocol/jsonrpc.js.map +1 -0
- package/lib/protocol/normalize.d.ts +72 -0
- package/lib/protocol/normalize.d.ts.map +1 -0
- package/lib/protocol/normalize.js +278 -0
- package/lib/protocol/normalize.js.map +1 -0
- package/lib/protocol/sse.d.ts +20 -0
- package/lib/protocol/sse.d.ts.map +1 -0
- package/lib/protocol/sse.js +23 -0
- package/lib/protocol/sse.js.map +1 -0
- package/lib/protocol/wire.d.ts +164 -0
- package/lib/protocol/wire.d.ts.map +1 -0
- package/lib/protocol/wire.js +26 -0
- package/lib/protocol/wire.js.map +1 -0
- package/lib/router.d.ts +56 -0
- package/lib/router.d.ts.map +1 -0
- package/lib/router.js +432 -0
- package/lib/router.js.map +1 -0
- package/lib/security.d.ts +89 -0
- package/lib/security.d.ts.map +1 -0
- package/lib/security.js +171 -0
- package/lib/security.js.map +1 -0
- package/lib/tasks.d.ts +75 -0
- package/lib/tasks.d.ts.map +1 -0
- package/lib/tasks.js +84 -0
- package/lib/tasks.js.map +1 -0
- package/lib/types.d.ts +71 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +17 -0
- package/lib/types.js.map +1 -0
- package/package.json +94 -0
package/lib/security.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication, rate limiting, inbound defanging, and outbound redaction.
|
|
3
|
+
*
|
|
4
|
+
* The real security boundary is not in this file: it is the sandbox scope and
|
|
5
|
+
* the deterministically-rejected approval an A2A agent runs under. Injection
|
|
6
|
+
* defanging here is noise reduction, and must never be cited as a reason to
|
|
7
|
+
* widen what a peer-driven agent is allowed to do.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-a2a/security
|
|
10
|
+
*/
|
|
11
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
12
|
+
/**
|
|
13
|
+
* Compare two secrets without leaking their relationship through timing.
|
|
14
|
+
* @param a - the presented value.
|
|
15
|
+
* @param b - the stored value.
|
|
16
|
+
* @returns whether they are byte-identical.
|
|
17
|
+
*/
|
|
18
|
+
export function constantTimeEqual(a, b) {
|
|
19
|
+
const left = Buffer.from(a, 'utf8');
|
|
20
|
+
const right = Buffer.from(b, 'utf8');
|
|
21
|
+
// timingSafeEqual requires equal lengths; comparing a digest of each keeps the
|
|
22
|
+
// length difference from short-circuiting before the comparison happens.
|
|
23
|
+
if (left.length !== right.length) {
|
|
24
|
+
// Still burn a comparison so a length mismatch is not measurably faster.
|
|
25
|
+
timingSafeEqual(left, left);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return timingSafeEqual(left, right);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract the bearer token from an Authorization header.
|
|
32
|
+
* @param header - the raw header value, if any.
|
|
33
|
+
* @returns the token, or undefined when absent or not a bearer scheme.
|
|
34
|
+
*/
|
|
35
|
+
export function parseBearer(header) {
|
|
36
|
+
if (header === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
39
|
+
return match?.[1];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the presented token to a declared peer identity.
|
|
43
|
+
*
|
|
44
|
+
* Every candidate is compared even after a match, so response time does not
|
|
45
|
+
* reveal which peer matched or how many peers are declared.
|
|
46
|
+
* @param token - the presented bearer token, if any.
|
|
47
|
+
* @param credentials - declared peer name to its resolved secret.
|
|
48
|
+
* @returns the matched identity, or undefined.
|
|
49
|
+
*/
|
|
50
|
+
export function identifyPeer(token, credentials) {
|
|
51
|
+
if (token === undefined || token.length === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
let matched;
|
|
54
|
+
for (const [peer, secret] of credentials) {
|
|
55
|
+
if (constantTimeEqual(token, secret) && matched === undefined)
|
|
56
|
+
matched = peer;
|
|
57
|
+
}
|
|
58
|
+
return matched;
|
|
59
|
+
}
|
|
60
|
+
/** Sliding-window request counter, keyed by authenticated identity. */
|
|
61
|
+
export class RateLimiter {
|
|
62
|
+
perMinute;
|
|
63
|
+
now;
|
|
64
|
+
hits = new Map();
|
|
65
|
+
/**
|
|
66
|
+
* @param perMinute - allowed requests per identity per rolling minute.
|
|
67
|
+
* @param now - clock injection point for tests.
|
|
68
|
+
*/
|
|
69
|
+
constructor(perMinute, now = Date.now) {
|
|
70
|
+
this.perMinute = perMinute;
|
|
71
|
+
this.now = now;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Record a request and report whether it is within budget.
|
|
75
|
+
* @param identity - the authenticated peer.
|
|
76
|
+
* @returns true when the request may proceed.
|
|
77
|
+
*/
|
|
78
|
+
allow(identity) {
|
|
79
|
+
const cutoff = this.now() - 60_000;
|
|
80
|
+
const window = (this.hits.get(identity) ?? []).filter(at => at > cutoff);
|
|
81
|
+
if (window.length >= this.perMinute) {
|
|
82
|
+
this.hits.set(identity, window);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
window.push(this.now());
|
|
86
|
+
this.hits.set(identity, window);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/** Drop all counters; used on teardown. */
|
|
90
|
+
clear() {
|
|
91
|
+
this.hits.clear();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Per-context turn counter that stops one peer from looping a context forever. */
|
|
95
|
+
export class TurnTracker {
|
|
96
|
+
turns = new Map();
|
|
97
|
+
/**
|
|
98
|
+
* Count one inbound message on a context.
|
|
99
|
+
* @param contextId - the context being extended.
|
|
100
|
+
* @returns the turn number this message occupies, starting at 1.
|
|
101
|
+
*/
|
|
102
|
+
track(contextId) {
|
|
103
|
+
const next = (this.turns.get(contextId) ?? 0) + 1;
|
|
104
|
+
this.turns.set(contextId, next);
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Reset a context's counter, as an explicit cancellation should.
|
|
109
|
+
* @param contextId - the context to reset.
|
|
110
|
+
*/
|
|
111
|
+
reset(contextId) {
|
|
112
|
+
this.turns.delete(contextId);
|
|
113
|
+
}
|
|
114
|
+
/** Drop all counters; used on teardown. */
|
|
115
|
+
clear() {
|
|
116
|
+
this.turns.clear();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Markers an adversarial peer might embed to hijack the agent's turn.
|
|
121
|
+
*
|
|
122
|
+
* These are neutralized rather than rejected: a legitimate task that merely
|
|
123
|
+
* MENTIONS one still gets through, with the marker defanged.
|
|
124
|
+
*/
|
|
125
|
+
const INJECTION_PATTERNS = [
|
|
126
|
+
/<\|im_(start|end)\|>/gi,
|
|
127
|
+
/<\|(system|user|assistant|end|endoftext)\|>/gi,
|
|
128
|
+
/\[\/?(?:INST|SYS|SYSTEM)\]/gi,
|
|
129
|
+
/^\s*(system|assistant|developer)\s*:\s*/gim,
|
|
130
|
+
/ignore (?:all|any|the) (?:previous|prior|above) instructions/gi,
|
|
131
|
+
/disregard (?:all|any|the) (?:previous|prior|above)/gi,
|
|
132
|
+
/<\/?(?:system|assistant|tool)[^>]*>/gi,
|
|
133
|
+
];
|
|
134
|
+
/** What a defanged marker is replaced with. */
|
|
135
|
+
export const FILTERED = '[filtered]';
|
|
136
|
+
/**
|
|
137
|
+
* Defang prompt-injection markers in inbound task text.
|
|
138
|
+
* @param text - the peer's raw text.
|
|
139
|
+
* @returns the text with markers replaced.
|
|
140
|
+
*/
|
|
141
|
+
export function filterInbound(text) {
|
|
142
|
+
let out = text;
|
|
143
|
+
for (const pattern of INJECTION_PATTERNS)
|
|
144
|
+
out = out.replace(pattern, FILTERED);
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
/** Credential-shaped strings that must never reach a peer. */
|
|
148
|
+
const REDACTIONS = [
|
|
149
|
+
[/sk-ant-[A-Za-z0-9_-]{16,}/g, 'sk-ant-[redacted]'],
|
|
150
|
+
[/sk-[A-Za-z0-9_-]{16,}/g, 'sk-[redacted]'],
|
|
151
|
+
[/ghp_[A-Za-z0-9]{20,}/g, 'ghp_[redacted]'],
|
|
152
|
+
[/xox[bapr]-[A-Za-z0-9-]{10,}/g, 'xox-[redacted]'],
|
|
153
|
+
[/AKIA[0-9A-Z]{16}/g, 'AKIA[redacted]'],
|
|
154
|
+
[/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, '[redacted-jwt]'],
|
|
155
|
+
[/(?<=\b)[Bb]earer\s+[A-Za-z0-9._-]{20,}/g, 'Bearer [redacted]'],
|
|
156
|
+
];
|
|
157
|
+
/**
|
|
158
|
+
* Scrub credential-shaped substrings before text leaves this process.
|
|
159
|
+
*
|
|
160
|
+
* This applies to REPLIES, not just to a hypothetical outbound client: an
|
|
161
|
+
* inbound-only server still ships model-authored text to a peer.
|
|
162
|
+
* @param text - the text about to be sent.
|
|
163
|
+
* @returns the scrubbed text.
|
|
164
|
+
*/
|
|
165
|
+
export function redactOutbound(text) {
|
|
166
|
+
let out = text;
|
|
167
|
+
for (const [pattern, replacement] of REDACTIONS)
|
|
168
|
+
out = out.replace(pattern, replacement);
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../src/security.ts"],"names":[],"mappings":"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"}
|
package/lib/tasks.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task slots and the three-stage turn correlation.
|
|
3
|
+
*
|
|
4
|
+
* The hard part of bridging A2A onto the harness is that ONE SUBMITTED MESSAGE
|
|
5
|
+
* IS NOT ONE TURN. A task spans from the message being accepted until the whole
|
|
6
|
+
* agent falls idle, which may be several turns if tools queue more work. Three
|
|
7
|
+
* hooks cooperate:
|
|
8
|
+
*
|
|
9
|
+
* agent/inbox/claimed → bind this task's messageId to the turn that took it
|
|
10
|
+
* turn/end → record that turn's ending (a model error fails now)
|
|
11
|
+
* agent.whenIdle() → settle, using the recorded ending
|
|
12
|
+
*
|
|
13
|
+
* This mirrors what `dsh-acp` does for its prompts, and for the same reason:
|
|
14
|
+
* steering and injected work may contribute before quiescence, so the bridge
|
|
15
|
+
* does not claim a task-specific turn outcome.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-a2a/tasks
|
|
18
|
+
*/
|
|
19
|
+
import type { A2AArtifact, A2ATaskId, A2ATaskState } from './protocol/index.ts';
|
|
20
|
+
/** How a task's final state was reached, for `Task.metadata.dsh.stopReason`. */
|
|
21
|
+
export interface TaskSettlement {
|
|
22
|
+
state: A2ATaskState;
|
|
23
|
+
/** The harness's real turn ending, which A2A's state enum cannot express. */
|
|
24
|
+
stopReason?: string;
|
|
25
|
+
/** Text the agent committed during this task's span. */
|
|
26
|
+
artifacts: A2AArtifact[];
|
|
27
|
+
}
|
|
28
|
+
/** One in-flight task awaiting settlement. */
|
|
29
|
+
export interface TaskSlot {
|
|
30
|
+
readonly taskId: A2ATaskId;
|
|
31
|
+
/** The user message id, used to correlate with the turn that claims it. */
|
|
32
|
+
readonly messageId: string;
|
|
33
|
+
/** Set by `agent/inbox/claimed` once a turn takes the message. */
|
|
34
|
+
turn: number | undefined;
|
|
35
|
+
/** Set at `turn/end` for the correlated turn; undefined means turnless. */
|
|
36
|
+
endReason: string | undefined;
|
|
37
|
+
/** Committed assistant text collected during this task's span. */
|
|
38
|
+
readonly texts: string[];
|
|
39
|
+
/** Resolves when the task reaches a terminal state. */
|
|
40
|
+
readonly settled: Promise<TaskSettlement>;
|
|
41
|
+
/** Fulfils `settled` exactly once. */
|
|
42
|
+
settle: (settlement: TaskSettlement) => void;
|
|
43
|
+
/** Whether `settle` has already run. */
|
|
44
|
+
done: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Create an unsettled task slot.
|
|
48
|
+
* @param taskId - the minted task id.
|
|
49
|
+
* @param messageId - the id of the user message this task submitted.
|
|
50
|
+
* @returns the slot, with its settlement promise already armed.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createSlot(taskId: A2ATaskId, messageId: string): TaskSlot;
|
|
53
|
+
/**
|
|
54
|
+
* Build the artifact list from the text a task's agent committed.
|
|
55
|
+
*
|
|
56
|
+
* A2A has artifacts; the harness has assistant messages. One text artifact per
|
|
57
|
+
* task keeps the mapping honest without inventing structure the agent never
|
|
58
|
+
* produced. Tool calls and results stay out — A2A's opaque-execution principle
|
|
59
|
+
* says a peer sees results, not how they were reached.
|
|
60
|
+
* @param taskId - used to derive a stable artifact id.
|
|
61
|
+
* @param texts - committed assistant texts, in order.
|
|
62
|
+
* @returns zero or one artifact.
|
|
63
|
+
*/
|
|
64
|
+
export declare function artifactsFromTexts(taskId: A2ATaskId, texts: readonly string[]): A2AArtifact[];
|
|
65
|
+
/**
|
|
66
|
+
* Map a harness turn ending onto an A2A terminal state.
|
|
67
|
+
*
|
|
68
|
+
* A token ceiling is NOT a task-level failure: the agent stopped for its own
|
|
69
|
+
* reasons having produced real work, and A2A's `failed` would tell the peer to
|
|
70
|
+
* discard it. The true ending still travels in `Task.metadata.dsh.stopReason`.
|
|
71
|
+
* @param endReason - the harness turn ending kind, or undefined for a turnless slot.
|
|
72
|
+
* @returns the A2A state to settle with.
|
|
73
|
+
*/
|
|
74
|
+
export declare function stateFromEnding(endReason: string | undefined): A2ATaskState;
|
|
75
|
+
//# sourceMappingURL=tasks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAE/E,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,YAAY,CAAA;IACnB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,wDAAwD;IACxD,SAAS,EAAE,WAAW,EAAE,CAAA;CACzB;AAED,8CAA8C;AAC9C,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,kEAAkE;IAClE,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IACxB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAA;IACxB,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IACzC,sCAAsC;IACtC,MAAM,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,IAAI,CAAA;IAC5C,wCAAwC;IACxC,IAAI,EAAE,OAAO,CAAA;CACd;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,QAAQ,CAkBzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,WAAW,EAAE,CAQ7F;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,CAK3E"}
|
package/lib/tasks.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task slots and the three-stage turn correlation.
|
|
3
|
+
*
|
|
4
|
+
* The hard part of bridging A2A onto the harness is that ONE SUBMITTED MESSAGE
|
|
5
|
+
* IS NOT ONE TURN. A task spans from the message being accepted until the whole
|
|
6
|
+
* agent falls idle, which may be several turns if tools queue more work. Three
|
|
7
|
+
* hooks cooperate:
|
|
8
|
+
*
|
|
9
|
+
* agent/inbox/claimed → bind this task's messageId to the turn that took it
|
|
10
|
+
* turn/end → record that turn's ending (a model error fails now)
|
|
11
|
+
* agent.whenIdle() → settle, using the recorded ending
|
|
12
|
+
*
|
|
13
|
+
* This mirrors what `dsh-acp` does for its prompts, and for the same reason:
|
|
14
|
+
* steering and injected work may contribute before quiescence, so the bridge
|
|
15
|
+
* does not claim a task-specific turn outcome.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-a2a/tasks
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Create an unsettled task slot.
|
|
21
|
+
* @param taskId - the minted task id.
|
|
22
|
+
* @param messageId - the id of the user message this task submitted.
|
|
23
|
+
* @returns the slot, with its settlement promise already armed.
|
|
24
|
+
*/
|
|
25
|
+
export function createSlot(taskId, messageId) {
|
|
26
|
+
let fulfil;
|
|
27
|
+
const settled = new Promise((resolve) => { fulfil = resolve; });
|
|
28
|
+
const slot = {
|
|
29
|
+
taskId,
|
|
30
|
+
messageId,
|
|
31
|
+
turn: undefined,
|
|
32
|
+
endReason: undefined,
|
|
33
|
+
texts: [],
|
|
34
|
+
settled,
|
|
35
|
+
done: false,
|
|
36
|
+
settle: (settlement) => {
|
|
37
|
+
if (slot.done)
|
|
38
|
+
return;
|
|
39
|
+
slot.done = true;
|
|
40
|
+
fulfil?.(settlement);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return slot;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build the artifact list from the text a task's agent committed.
|
|
47
|
+
*
|
|
48
|
+
* A2A has artifacts; the harness has assistant messages. One text artifact per
|
|
49
|
+
* task keeps the mapping honest without inventing structure the agent never
|
|
50
|
+
* produced. Tool calls and results stay out — A2A's opaque-execution principle
|
|
51
|
+
* says a peer sees results, not how they were reached.
|
|
52
|
+
* @param taskId - used to derive a stable artifact id.
|
|
53
|
+
* @param texts - committed assistant texts, in order.
|
|
54
|
+
* @returns zero or one artifact.
|
|
55
|
+
*/
|
|
56
|
+
export function artifactsFromTexts(taskId, texts) {
|
|
57
|
+
const joined = texts.filter(text => text.length > 0).join('\n');
|
|
58
|
+
if (joined.length === 0)
|
|
59
|
+
return [];
|
|
60
|
+
return [{
|
|
61
|
+
artifactId: `${taskId}-result`,
|
|
62
|
+
name: 'result',
|
|
63
|
+
parts: [{ kind: 'text', text: joined }],
|
|
64
|
+
}];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Map a harness turn ending onto an A2A terminal state.
|
|
68
|
+
*
|
|
69
|
+
* A token ceiling is NOT a task-level failure: the agent stopped for its own
|
|
70
|
+
* reasons having produced real work, and A2A's `failed` would tell the peer to
|
|
71
|
+
* discard it. The true ending still travels in `Task.metadata.dsh.stopReason`.
|
|
72
|
+
* @param endReason - the harness turn ending kind, or undefined for a turnless slot.
|
|
73
|
+
* @returns the A2A state to settle with.
|
|
74
|
+
*/
|
|
75
|
+
export function stateFromEnding(endReason) {
|
|
76
|
+
if (endReason === undefined)
|
|
77
|
+
return 'canceled';
|
|
78
|
+
if (endReason === 'error')
|
|
79
|
+
return 'failed';
|
|
80
|
+
if (endReason === 'cancelled' || endReason === 'canceled')
|
|
81
|
+
return 'canceled';
|
|
82
|
+
return 'completed';
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=tasks.js.map
|
package/lib/tasks.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks.js","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAgCH;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,MAAiB,EAAE,SAAiB;IAC7D,IAAI,MAA0D,CAAA;IAC9D,MAAM,OAAO,GAAG,IAAI,OAAO,CAAiB,CAAC,OAAO,EAAE,EAAE,GAAG,MAAM,GAAG,OAAO,CAAA,CAAC,CAAC,CAAC,CAAA;IAC9E,MAAM,IAAI,GAAa;QACrB,MAAM;QACN,SAAS;QACT,IAAI,EAAE,SAAS;QACf,SAAS,EAAE,SAAS;QACpB,KAAK,EAAE,EAAE;QACT,OAAO;QACP,IAAI,EAAE,KAAK;QACX,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE;YACrB,IAAI,IAAI,CAAC,IAAI;gBAAE,OAAM;YACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;YAChB,MAAM,EAAE,CAAC,UAAU,CAAC,CAAA;QACtB,CAAC;KACF,CAAA;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAiB,EAAE,KAAwB;IAC5E,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC/D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IAClC,OAAO,CAAC;YACN,UAAU,EAAE,GAAG,MAAM,SAAS;YAC9B,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;SACxC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,SAA6B;IAC3D,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,UAAU,CAAA;IAC9C,IAAI,SAAS,KAAK,OAAO;QAAE,OAAO,QAAQ,CAAA;IAC1C,IAAI,SAAS,KAAK,WAAW,IAAI,SAAS,KAAK,UAAU;QAAE,OAAO,UAAU,CAAA;IAC5E,OAAO,WAAW,CAAA;AACpB,CAAC"}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary this plugin merges into the harness's own type surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Two declaration merges carry the whole durable contract:
|
|
5
|
+
*
|
|
6
|
+
* - `a2a/task` extends `SessionEventMap` so a task's state lives in the
|
|
7
|
+
* append-only log. `tasks/get` after a restart therefore has an answer,
|
|
8
|
+
* which a process-local task table could never give.
|
|
9
|
+
* - `a2aPeer` extends `MessageSourceMap` so an inbound message carries
|
|
10
|
+
* structured provenance instead of a warning string glued onto its text.
|
|
11
|
+
* The prose banner approach costs tokens on every message AND varies the
|
|
12
|
+
* reusable request prefix by peer name; a source field does neither.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-a2a/types
|
|
15
|
+
*/
|
|
16
|
+
import type { A2ATaskId, A2ATaskState } from './protocol/index.ts';
|
|
17
|
+
declare module '@deepseek-ai/dsh-session/types' {
|
|
18
|
+
interface SessionEventMap {
|
|
19
|
+
/**
|
|
20
|
+
* One A2A task lifecycle edge. Log-only: no surface operation, absent from
|
|
21
|
+
* model history, retained by the append-only log across compaction.
|
|
22
|
+
*
|
|
23
|
+
* `contextId` is deliberately absent — it IS the SessionId this event is
|
|
24
|
+
* logged under, and recording it twice would create two truths that can
|
|
25
|
+
* disagree.
|
|
26
|
+
*/
|
|
27
|
+
'a2a/task': {
|
|
28
|
+
/** The task this edge belongs to. */
|
|
29
|
+
taskId: A2ATaskId;
|
|
30
|
+
/** The authenticated peer that submitted it. Never body-asserted. */
|
|
31
|
+
peer: string;
|
|
32
|
+
/** The state this edge transitions to. */
|
|
33
|
+
state: A2ATaskState;
|
|
34
|
+
/** The turn this task correlates with, once one has claimed its message. */
|
|
35
|
+
turn?: number;
|
|
36
|
+
/** Terminal edges only: the real harness turn ending, for Task.metadata. */
|
|
37
|
+
stopReason?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Terminal edges only: the committed assistant text this task produced.
|
|
40
|
+
*
|
|
41
|
+
* Carried on the edge rather than derived later because of the
|
|
42
|
+
* projection contract's whole-value rule — a state-carrying event must
|
|
43
|
+
* hold the complete post-change state. Without it a peer polling
|
|
44
|
+
* `tasks/get` gets `completed` and an empty artifact list, which reads as
|
|
45
|
+
* "it worked and produced nothing" rather than "ask again".
|
|
46
|
+
*/
|
|
47
|
+
output?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
declare module '@deepseek-ai/dsh-llm' {
|
|
52
|
+
interface MessageSourceMap {
|
|
53
|
+
/**
|
|
54
|
+
* A message a remote A2A peer addressed to this agent.
|
|
55
|
+
*
|
|
56
|
+
* `form: 'relay'` is the harness's own vocabulary for exactly this — its
|
|
57
|
+
* documented meaning is "a message another agent addressed to this one" —
|
|
58
|
+
* so no new context form is invented here.
|
|
59
|
+
*/
|
|
60
|
+
a2aPeer: {
|
|
61
|
+
kind: 'a2a-peer';
|
|
62
|
+
/** Authenticated identity, resolved from the presented credential only. */
|
|
63
|
+
peer: string;
|
|
64
|
+
/** The task whose submission carried this message. */
|
|
65
|
+
taskId: A2ATaskId;
|
|
66
|
+
form: 'relay';
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export {};
|
|
71
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAElE,OAAO,QAAQ,gCAAgC,CAAC;IAC9C,UAAU,eAAe;QACvB;;;;;;;WAOG;QACH,UAAU,EAAE;YACV,qCAAqC;YACrC,MAAM,EAAE,SAAS,CAAA;YACjB,qEAAqE;YACrE,IAAI,EAAE,MAAM,CAAA;YACZ,0CAA0C;YAC1C,KAAK,EAAE,YAAY,CAAA;YACnB,4EAA4E;YAC5E,IAAI,CAAC,EAAE,MAAM,CAAA;YACb,4EAA4E;YAC5E,UAAU,CAAC,EAAE,MAAM,CAAA;YACnB;;;;;;;;eAQG;YACH,MAAM,CAAC,EAAE,MAAM,CAAA;SAChB,CAAA;KACF;CACF;AAED,OAAO,QAAQ,sBAAsB,CAAC;IACpC,UAAU,gBAAgB;QACxB;;;;;;WAMG;QACH,OAAO,EAAE;YACP,IAAI,EAAE,UAAU,CAAA;YAChB,2EAA2E;YAC3E,IAAI,EAAE,MAAM,CAAA;YACZ,sDAAsD;YACtD,MAAM,EAAE,SAAS,CAAA;YACjB,IAAI,EAAE,OAAO,CAAA;SACd,CAAA;KACF;CACF;AAED,OAAO,EAAE,CAAA"}
|
package/lib/types.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary this plugin merges into the harness's own type surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Two declaration merges carry the whole durable contract:
|
|
5
|
+
*
|
|
6
|
+
* - `a2a/task` extends `SessionEventMap` so a task's state lives in the
|
|
7
|
+
* append-only log. `tasks/get` after a restart therefore has an answer,
|
|
8
|
+
* which a process-local task table could never give.
|
|
9
|
+
* - `a2aPeer` extends `MessageSourceMap` so an inbound message carries
|
|
10
|
+
* structured provenance instead of a warning string glued onto its text.
|
|
11
|
+
* The prose banner approach costs tokens on every message AND varies the
|
|
12
|
+
* reusable request prefix by peer name; a source field does neither.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-a2a/types
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=types.js.map
|
package/lib/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA2DH,OAAO,EAAE,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-a2a-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Inbound A2A (Agent2Agent) protocol server for DeepSeek Harness: publish an Agent Card and let any A2A-compliant peer submit tasks to a harness agent",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"dsh-plugin",
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"a2a",
|
|
10
|
+
"agent2agent",
|
|
11
|
+
"cordis"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"types": "lib/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./lib/index.d.ts",
|
|
20
|
+
"default": "./lib/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./protocol": {
|
|
23
|
+
"types": "./lib/protocol/index.d.ts",
|
|
24
|
+
"default": "./lib/protocol/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib",
|
|
30
|
+
"cordis.patch.yml"
|
|
31
|
+
],
|
|
32
|
+
"dsh": {
|
|
33
|
+
"bundle": {
|
|
34
|
+
"patch": "./cordis.patch.yml"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.build.json",
|
|
39
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"serve": "tsx example/serve.ts",
|
|
43
|
+
"probe": "node example/probe.mjs",
|
|
44
|
+
"prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
48
|
+
"@deepseek-ai/dsh-agent": "0.1.0-rc.6",
|
|
49
|
+
"@deepseek-ai/dsh-credentials": "0.1.0-rc.6",
|
|
50
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.0-rc.6",
|
|
51
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
52
|
+
"@deepseek-ai/dsh-session": "0.1.0-rc.6"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@deepseek-ai/schemastery": "3.18.1"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
59
|
+
"@deepseek-ai/cordis-plugin-loader": "1.0.2",
|
|
60
|
+
"@deepseek-ai/dsh-agent": "0.1.0-rc.6",
|
|
61
|
+
"@deepseek-ai/dsh-agent-loop": "0.1.0-rc.6",
|
|
62
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "0.1.0-rc.6",
|
|
63
|
+
"@deepseek-ai/dsh-brand": "0.1.0-rc.6",
|
|
64
|
+
"@deepseek-ai/dsh-credentials": "0.1.0-rc.6",
|
|
65
|
+
"@deepseek-ai/dsh-credentials-local": "0.1.0-rc.6",
|
|
66
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.0-rc.6",
|
|
67
|
+
"@deepseek-ai/dsh-invariants": "0.1.0-rc.6",
|
|
68
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
69
|
+
"@deepseek-ai/dsh-llm-deepseek": "0.1.0-rc.6",
|
|
70
|
+
"@deepseek-ai/dsh-llm-replay": "0.1.0-rc.6",
|
|
71
|
+
"@deepseek-ai/dsh-scope": "0.1.0-rc.6",
|
|
72
|
+
"@deepseek-ai/dsh-session": "0.1.0-rc.6",
|
|
73
|
+
"@deepseek-ai/dsh-session-projection": "0.1.0-rc.6",
|
|
74
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.0-rc.6",
|
|
75
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.6",
|
|
76
|
+
"@types/node": "^22.10.0",
|
|
77
|
+
"tsx": "^4.19.2",
|
|
78
|
+
"typescript": "^5.7.2",
|
|
79
|
+
"vitest": "^2.1.8",
|
|
80
|
+
"zod": "^4.4.3"
|
|
81
|
+
},
|
|
82
|
+
"repository": {
|
|
83
|
+
"type": "git",
|
|
84
|
+
"url": "git+https://github.com/huangjuhua-aigc/dsh-a2a.git"
|
|
85
|
+
},
|
|
86
|
+
"homepage": "https://github.com/huangjuhua-aigc/dsh-a2a#readme",
|
|
87
|
+
"bugs": {
|
|
88
|
+
"url": "https://github.com/huangjuhua-aigc/dsh-a2a/issues"
|
|
89
|
+
},
|
|
90
|
+
"publishConfig": {
|
|
91
|
+
"registry": "https://registry.npmjs.org",
|
|
92
|
+
"access": "public"
|
|
93
|
+
}
|
|
94
|
+
}
|