a2a-exposed 0.7.19 → 0.7.20
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/lib/feedback.mjs +76 -2
- package/lib/main.mjs +14 -4
- package/package.json +1 -1
- package/worker/src/feedback.ts +44 -2
- package/worker/src/index.ts +9 -5
- package/worker/src/mcp.ts +2 -1
- package/worker/src/version.ts +1 -1
- package/worker/test/feedback.test.ts +17 -2
- package/worker/test/mcp.test.ts +4 -0
package/lib/feedback.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Feedback: post to a hosted control plane when this inbox points at one, otherwise print a GitHub issue link.
|
|
2
2
|
import os from "node:os";
|
|
3
|
+
import readline from "node:readline/promises";
|
|
3
4
|
import { CliError } from "./a2a.mjs";
|
|
4
5
|
import * as C from "./config.mjs";
|
|
5
6
|
|
|
@@ -12,14 +13,42 @@ const TOKEN_RE = /(?:a2aow_|s2a_|a2amcp_|a2amcpr_|a2aot_)[A-Za-z0-9_-]{20,}/g;
|
|
|
12
13
|
const SETUP_RE = /([?&])t=[A-Za-z0-9_-]{16,}/g;
|
|
13
14
|
const USERINFO_RE = /([a-z][a-z0-9+.-]*:\/\/)[^/?\s]*@/gi;
|
|
14
15
|
const BEARER_RE = /bearer\s+\S{8,200}/gi;
|
|
16
|
+
const PEM_RE = /-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----[\s\S]*?(?:-----END (?:[A-Z0-9]+ )?PRIVATE KEY-----|$)/g;
|
|
17
|
+
const API_KEY_RE = /(?:(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|\bAKIA[0-9A-Z]{16}\b|\bsk_(?:live|test)_[A-Za-z0-9]{10,}|\brk_(?:live|test)_[A-Za-z0-9]{10,}|\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}|\bxox[baprs]-[A-Za-z0-9-]{10,}|\bAIza[0-9A-Za-z_-]{35}\b)/g;
|
|
15
18
|
|
|
16
|
-
/** Same shapes as the Worker's scrubSecretText, plus a Bearer value. */
|
|
19
|
+
/** Same shapes as the Worker's scrubSecretText, plus a Bearer value, a PEM private key, and common API-key prefixes. */
|
|
17
20
|
export function scrubFeedbackText(value) {
|
|
18
21
|
return String(value ?? "")
|
|
19
22
|
.replace(TOKEN_RE, "[redacted]")
|
|
20
23
|
.replace(SETUP_RE, "$1t=[redacted]")
|
|
21
24
|
.replace(USERINFO_RE, "$1[redacted]@")
|
|
22
|
-
.replace(BEARER_RE, "Bearer [redacted]")
|
|
25
|
+
.replace(BEARER_RE, "Bearer [redacted]")
|
|
26
|
+
.replace(PEM_RE, "[redacted]")
|
|
27
|
+
.replace(API_KEY_RE, "[redacted]");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
31
|
+
|
|
32
|
+
/** The repository that will receive the issue. The prefilled form is ISSUE_REPO. A hosted post uses the control plane's name. */
|
|
33
|
+
export function publicIssueWarning(repo = ISSUE_REPO) {
|
|
34
|
+
const name = typeof repo === "string" && REPO_RE.test(repo) && repo.length <= 100 && !repo.includes("..") ? repo : ISSUE_REPO;
|
|
35
|
+
return `This report is filed as a public GitHub issue on ${name}. Do not include confidential information: secrets, tokens, owner or pairing tokens, private URLs or hostnames, message contents, or personal data.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const PUBLIC_ISSUE_WARNING = publicIssueWarning(ISSUE_REPO);
|
|
39
|
+
|
|
40
|
+
/** Print the public-issue warning. On a terminal, wait for y or yes unless `yes` is set. Off a terminal, `--yes` is required. */
|
|
41
|
+
export async function confirmPublicIssue(yes, input = process.stdin, output = process.stderr, repo = ISSUE_REPO) {
|
|
42
|
+
output.write(`${publicIssueWarning(repo)}\n`);
|
|
43
|
+
if (yes) return;
|
|
44
|
+
if (!input.isTTY) throw new CliError("feedback files a public GitHub issue; pass --yes to send it");
|
|
45
|
+
const rl = readline.createInterface({ input, output });
|
|
46
|
+
try {
|
|
47
|
+
const answer = (await rl.question("File this public issue? [y/N] ")).trim().toLowerCase();
|
|
48
|
+
if (answer !== "y" && answer !== "yes") throw new CliError("feedback not sent");
|
|
49
|
+
} finally {
|
|
50
|
+
rl.close();
|
|
51
|
+
}
|
|
23
52
|
}
|
|
24
53
|
|
|
25
54
|
const ENVIRONMENTS = ["local", "cloud VM", "CI", "cloud agent sandbox", "other"];
|
|
@@ -163,6 +192,51 @@ async function readJson(response) {
|
|
|
163
192
|
try { return await response.json(); } catch { return null; }
|
|
164
193
|
}
|
|
165
194
|
|
|
195
|
+
function namedRepo(value) {
|
|
196
|
+
const repo = typeof value === "string" ? value.trim() : "";
|
|
197
|
+
if (!REPO_RE.test(repo) || repo.length > 100 || repo.includes("..")) return "";
|
|
198
|
+
return repo;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Repository named in the warning before a report is sent.
|
|
203
|
+
* No hosted endpoint: the prefilled form on ISSUE_REPO.
|
|
204
|
+
* Hosted: GET /api/feedback must return `{ repo }`. A missing name does not guess ISSUE_REPO.
|
|
205
|
+
*/
|
|
206
|
+
export async function feedbackPublicRepo(input) {
|
|
207
|
+
const base = String(input.baseUrl || "").replace(/\/$/, "");
|
|
208
|
+
const token = String(input.ownerToken || "");
|
|
209
|
+
const call = input.fetchImpl ?? fetch;
|
|
210
|
+
if (!base || !token) return ISSUE_REPO;
|
|
211
|
+
let probe;
|
|
212
|
+
try {
|
|
213
|
+
probe = await call(`${base}/owner/feedback`, {
|
|
214
|
+
headers: { accept: "application/json", authorization: `Bearer ${token}` },
|
|
215
|
+
redirect: "error",
|
|
216
|
+
signal: AbortSignal.timeout(5000),
|
|
217
|
+
});
|
|
218
|
+
} catch {
|
|
219
|
+
probe = null;
|
|
220
|
+
}
|
|
221
|
+
const info = probe && probe.ok ? await readJson(probe) : null;
|
|
222
|
+
if (!info || info.hosted !== true || !feedbackUrlOk(info.url)) return ISSUE_REPO;
|
|
223
|
+
let named;
|
|
224
|
+
try {
|
|
225
|
+
named = await call(info.url, {
|
|
226
|
+
method: "GET",
|
|
227
|
+
headers: { accept: "application/json" },
|
|
228
|
+
redirect: "error",
|
|
229
|
+
signal: AbortSignal.timeout(5000),
|
|
230
|
+
});
|
|
231
|
+
} catch {
|
|
232
|
+
named = null;
|
|
233
|
+
}
|
|
234
|
+
const body = named && named.ok ? await readJson(named) : null;
|
|
235
|
+
const repo = namedRepo(body?.repo);
|
|
236
|
+
if (!repo) throw new CliError("the control plane did not name the public repository; feedback was not sent");
|
|
237
|
+
return repo;
|
|
238
|
+
}
|
|
239
|
+
|
|
166
240
|
/**
|
|
167
241
|
* Post when GET /owner/feedback says this inbox is hosted and names an https /api/feedback URL.
|
|
168
242
|
* A missing inbox, a probe failure, or a 404 prints the prefilled issue URL. 400, 429, and 5xx throw.
|
package/lib/main.mjs
CHANGED
|
@@ -209,16 +209,23 @@ Feedback
|
|
|
209
209
|
feedback "message" [--kind bug|idea|other] [--contact x]
|
|
210
210
|
[--steps S] [--expected S] [--actual S] [--logs S] [--agent A]
|
|
211
211
|
[--environment local|cloud VM|CI|cloud agent sandbox|other]
|
|
212
|
-
[--connection webhook|proxy|polling]
|
|
212
|
+
[--connection webhook|proxy|polling] [--yes]
|
|
213
213
|
report a bug or an idea. A bug needs --steps.
|
|
214
|
+
The report is a public GitHub issue. The warning names
|
|
215
|
+
the repository before it sends. A hosted inbox uses the
|
|
216
|
+
control plane's FEEDBACK_REPO. Otherwise the link opens
|
|
217
|
+
the form on telegraphic-dev/a2a-exposed. Do not include
|
|
218
|
+
confidential information: secrets, tokens, owner or pairing
|
|
219
|
+
tokens, private URLs or hostnames, message contents, or
|
|
220
|
+
personal data. Prints that warning and asks before sending,
|
|
221
|
+
unless --yes is set. Off a terminal, --yes is required.
|
|
214
222
|
Posts to the control plane when this inbox is hosted and that
|
|
215
223
|
plane accepts feedback. Otherwise prints a link to the bug
|
|
216
224
|
report form and does not open a browser. The CLI adds its
|
|
217
225
|
version, the inbox Worker version when the inbox reports it,
|
|
218
226
|
the OS, the Node version, the connection (webhook, proxy, or
|
|
219
227
|
polling), and hosted or self-hosted. --connection overrides
|
|
220
|
-
that detection.
|
|
221
|
-
message. Remove them from --logs too.
|
|
228
|
+
that detection. Remove tokens from the message and from --logs.
|
|
222
229
|
|
|
223
230
|
Presets: grok-bot | claude-code | openclaw-wake | openclaw-agent | hermes | generic
|
|
224
231
|
Config: ${C.CONFIG_FILE} (override dir with --config-dir DIR or A2A_CONFIG_DIR; env vars override file values)
|
|
@@ -246,7 +253,7 @@ const SPEC = {
|
|
|
246
253
|
signup: { "control-url": S, json: B },
|
|
247
254
|
login: { "control-url": S, json: B },
|
|
248
255
|
pair: { json: B, web: B, ttl: S, issuer: S, "client-id": S, subjects: S, methods: S, "secret-stdin": B },
|
|
249
|
-
feedback: { kind: S, contact: S, steps: S, expected: S, actual: S, logs: S, agent: S, environment: S, connection: S },
|
|
256
|
+
feedback: { kind: S, contact: S, steps: S, expected: S, actual: S, logs: S, agent: S, environment: S, connection: S, yes: B },
|
|
250
257
|
};
|
|
251
258
|
const STATES = ["completed", "input-required", "failed", "rejected", "working"];
|
|
252
259
|
|
|
@@ -336,6 +343,9 @@ export async function main(argv) {
|
|
|
336
343
|
const kind = o.kind || "other";
|
|
337
344
|
if (!fb.KINDS.includes(kind)) throw new CliError("--kind must be bug, idea, or other");
|
|
338
345
|
if (o.connection && !fb.CONNECTIONS.includes(o.connection)) throw new CliError("--connection must be webhook, proxy, or polling");
|
|
346
|
+
if (kind === "bug" && !String(o.steps || "").trim()) throw new CliError("a bug report needs reproduction steps (--steps)");
|
|
347
|
+
const repo = await fb.feedbackPublicRepo({ baseUrl: C.get("A2A_BASE_URL"), ownerToken: C.get("A2A_OWNER_TOKEN") });
|
|
348
|
+
await fb.confirmPublicIssue(Boolean(o.yes), undefined, undefined, repo);
|
|
339
349
|
const result = await fb.runFeedback({
|
|
340
350
|
message, kind, contact: o.contact || "", version: VERSION,
|
|
341
351
|
steps: o.steps || "", expected: o.expected || "", actual: o.actual || "", logs: o.logs || "",
|
package/package.json
CHANGED
package/worker/src/feedback.ts
CHANGED
|
@@ -6,10 +6,49 @@ import type { TenantContext } from "./tenancy.ts";
|
|
|
6
6
|
import { VERSION } from "./version.ts";
|
|
7
7
|
|
|
8
8
|
export const ISSUE_REPO = "telegraphic-dev/a2a-exposed";
|
|
9
|
+
const REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
10
|
+
|
|
11
|
+
/** Name the repository that receives the issue. The prefilled form uses ISSUE_REPO. A filed issue uses the repository in its URL. */
|
|
12
|
+
export function publicIssueNote(repo: string): string {
|
|
13
|
+
const name = REPO_RE.test(repo) && !repo.includes("..") ? repo : ISSUE_REPO;
|
|
14
|
+
return `This report is filed as a public GitHub issue on ${name}. Do not include confidential information: secrets, tokens, owner or pairing tokens, private URLs or hostnames, message contents, or personal data.`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const PUBLIC_ISSUE_NOTE = publicIssueNote(ISSUE_REPO);
|
|
18
|
+
|
|
19
|
+
/** Owner/name from a GitHub issue URL or the prefilled new-issue URL. Empty when the URL is not that shape. */
|
|
20
|
+
export function repoFromIssueUrl(value: string): string {
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(value);
|
|
23
|
+
const match = url.pathname.match(/^\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/issues(?:\/|$)/);
|
|
24
|
+
if (url.protocol !== "https:" || url.hostname !== "github.com" || url.username || url.password || !match) return "";
|
|
25
|
+
const repo = `${match[1]}/${match[2]}`;
|
|
26
|
+
return REPO_RE.test(repo) && !repo.includes("..") ? repo : "";
|
|
27
|
+
} catch {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function feedbackResultText(result: { filed: boolean; issue_url: string }): string {
|
|
33
|
+
const note = publicIssueNote(repoFromIssueUrl(result.issue_url));
|
|
34
|
+
return result.filed
|
|
35
|
+
? `${note}\nFeedback filed: ${result.issue_url}`
|
|
36
|
+
: `${note}\nFeedback was not filed from here. Open this link to create the issue:\n${result.issue_url}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Tool description. A repository is named only when this call already knows it. A hosted inbox learns the name from the issue URL in the result. */
|
|
40
|
+
export function feedbackToolDescription(repo: string): string {
|
|
41
|
+
const note = REPO_RE.test(repo) && !repo.includes("..")
|
|
42
|
+
? publicIssueNote(repo)
|
|
43
|
+
: "This report is filed as a public GitHub issue on this deployment's configured feedback repository. The result names that repository. Do not include confidential information: secrets, tokens, owner or pairing tokens, private URLs or hostnames, message contents, or personal data.";
|
|
44
|
+
return `${note} Redact before sending. Report a bug or an idea about this inbox to its developers. A bug requires reproduction steps. Include exact commands, error output with tokens and secrets removed, which agent you are, and where you run (local, cloud VM, CI, cloud agent sandbox, or other). Version, connection (webhook, proxy, or polling), and deployment (hosted or self-hosted) are filled by the inbox when you omit them. When this deployment does not file the report, the result is a link to the bug report form. You do not open a browser.`;
|
|
45
|
+
}
|
|
9
46
|
/** Inbox build. The same value as `version` in cli/package.json. */
|
|
10
47
|
export const WORKER_VERSION = VERSION;
|
|
11
48
|
const TIMEOUT_MS = 5000;
|
|
12
49
|
const BEARER_RE = /bearer\s+\S{8,200}/gi;
|
|
50
|
+
const PEM_RE = /-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----[\s\S]*?(?:-----END (?:[A-Z0-9]+ )?PRIVATE KEY-----|$)/g;
|
|
51
|
+
const API_KEY_RE = /(?:(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|\bAKIA[0-9A-Z]{16}\b|\bsk_(?:live|test)_[A-Za-z0-9]{10,}|\brk_(?:live|test)_[A-Za-z0-9]{10,}|\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}|\bxox[baprs]-[A-Za-z0-9-]{10,}|\bAIza[0-9A-Za-z_-]{35}\b)/g;
|
|
13
52
|
const KINDS = ["bug", "idea", "other"] as const;
|
|
14
53
|
const CONNECTIONS = ["webhook", "proxy", "polling"] as const;
|
|
15
54
|
const ENVIRONMENTS = ["local", "cloud VM", "CI", "cloud agent sandbox", "other"] as const;
|
|
@@ -36,9 +75,12 @@ export interface FeedbackNote {
|
|
|
36
75
|
logs?: string;
|
|
37
76
|
}
|
|
38
77
|
|
|
39
|
-
/** scrubSecretText, then a Bearer value. The rest of the text stays. */
|
|
78
|
+
/** scrubSecretText, then a Bearer value, a PEM private key, and common API-key prefixes. The rest of the text stays. */
|
|
40
79
|
export function scrubFeedbackText(value: string): string {
|
|
41
|
-
return scrubSecretText(value)
|
|
80
|
+
return scrubSecretText(value)
|
|
81
|
+
.replace(BEARER_RE, "Bearer [redacted]")
|
|
82
|
+
.replace(PEM_RE, "[redacted]")
|
|
83
|
+
.replace(API_KEY_RE, "[redacted]");
|
|
42
84
|
}
|
|
43
85
|
|
|
44
86
|
/**
|
package/worker/src/index.ts
CHANGED
|
@@ -2414,9 +2414,7 @@ async function runTool(ctx: TenantContext, ectx: ExecutionContext, name: string,
|
|
|
2414
2414
|
connection: argStr(a, "connection", false),
|
|
2415
2415
|
deployment: argStr(a, "deployment", false),
|
|
2416
2416
|
});
|
|
2417
|
-
return result
|
|
2418
|
-
? `Feedback filed: ${result.issue_url}`
|
|
2419
|
-
: `Feedback was not filed from here. Open this link to create the issue:\n${result.issue_url}`;
|
|
2417
|
+
return Fb.feedbackResultText(result);
|
|
2420
2418
|
}
|
|
2421
2419
|
}
|
|
2422
2420
|
throw new ToolError(`unknown tool ${name}`);
|
|
@@ -2430,6 +2428,12 @@ const mcpInstructions = `This is your A2A inbox: other agents send you tasks her
|
|
|
2430
2428
|
const unsupportedVersion = (id: Json, requested: string) =>
|
|
2431
2429
|
rpcError(id, M.ERR.UNSUPPORTED_VERSION, "Unsupported protocol version", 400, { supported: M.PROTOCOL_VERSIONS, requested });
|
|
2432
2430
|
|
|
2431
|
+
/** Hosted feedback is filed on FEEDBACK_REPO, which this inbox does not know until the issue URL comes back. Do not name the prefilled-form repository in that description. */
|
|
2432
|
+
function mcpTools(ctx: TenantContext) {
|
|
2433
|
+
if (!Fb.feedbackEndpoint(ctx)) return M.TOOLS;
|
|
2434
|
+
return M.TOOLS.map((tool) => tool.name === "send_feedback" ? { ...tool, description: Fb.feedbackToolDescription("") } : tool);
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2433
2437
|
/** One tools/call, shared by both eras. */
|
|
2434
2438
|
async function callTool(ctx: TenantContext, ectx: ExecutionContext, grant: Json, params: Json): Promise<{ error?: [number, string]; result?: Json }> {
|
|
2435
2439
|
const name = String(params.name || "");
|
|
@@ -2501,7 +2505,7 @@ async function handleMcp(req: Request, ctx: TenantContext, ectx: ExecutionContex
|
|
|
2501
2505
|
case "server/discover":
|
|
2502
2506
|
return done({ supportedVersions: M.PROTOCOL_VERSIONS, capabilities: { tools: {} }, instructions: mcpInstructions, ttlMs: M.LIST_TTL_MS, cacheScope: "private" });
|
|
2503
2507
|
case "tools/list":
|
|
2504
|
-
return done({ tools:
|
|
2508
|
+
return done({ tools: mcpTools(ctx), ttlMs: M.LIST_TTL_MS, cacheScope: "private" });
|
|
2505
2509
|
case "tools/call": {
|
|
2506
2510
|
const r = await callTool(ctx, ectx, grant, params);
|
|
2507
2511
|
return r.error ? rpcError(id, r.error[0], r.error[1]) : done(r.result);
|
|
@@ -2530,7 +2534,7 @@ async function handleMcp(req: Request, ctx: TenantContext, ectx: ExecutionContex
|
|
|
2530
2534
|
capabilities: { tools: { listChanged: false } }, serverInfo: serverInfo(ctx), instructions: mcpInstructions });
|
|
2531
2535
|
}
|
|
2532
2536
|
case "ping": return rpcResult(id, {});
|
|
2533
|
-
case "tools/list": return rpcResult(id, { tools:
|
|
2537
|
+
case "tools/list": return rpcResult(id, { tools: mcpTools(ctx) });
|
|
2534
2538
|
case "tools/call": {
|
|
2535
2539
|
const r = await callTool(ctx, ectx, grant, params);
|
|
2536
2540
|
return r.error ? rpcError(id, r.error[0], r.error[1]) : rpcResult(id, r.result);
|
package/worker/src/mcp.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// metadata. Pure helpers live here; the routes (D1, approval password, owner API) are in index.ts.
|
|
4
4
|
import { esc } from "./pairing.ts";
|
|
5
5
|
import { isPrivateHost } from "./a2a.ts";
|
|
6
|
+
import { feedbackToolDescription, ISSUE_REPO } from "./feedback.ts";
|
|
6
7
|
import { derivePeerKey } from "./tenancy.ts";
|
|
7
8
|
|
|
8
9
|
export const SCOPE = "inbox";
|
|
@@ -286,7 +287,7 @@ export const TOOLS: { name: string; title: string; description: string; inputSch
|
|
|
286
287
|
description: "Agents asking to connect to this inbox (read-only). Show your human who is asking, the code and the link; NEVER approve or deny on your own: your human decides on the /device link with their approval password. The names and card URLs are claimed by the requester (untrusted).",
|
|
287
288
|
inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
288
289
|
{ name: "send_feedback", title: "Send feedback", annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
289
|
-
description:
|
|
290
|
+
description: feedbackToolDescription(ISSUE_REPO),
|
|
290
291
|
inputSchema: { type: "object", properties: {
|
|
291
292
|
message: str("What happened, at most 4000 characters. Include the exact commands you ran."),
|
|
292
293
|
kind: str("bug, idea, or other. A bug requires steps.", { enum: ["bug", "idea", "other"] }),
|
package/worker/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Inbox build. Keep this equal to `version` in cli/package.json. */
|
|
2
|
-
export const VERSION = "0.7.
|
|
2
|
+
export const VERSION = "0.7.20";
|
|
@@ -3,7 +3,7 @@ import { test } from "node:test";
|
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
4
|
import worker, { dispatch } from "../src/index.ts";
|
|
5
5
|
import { sha256 } from "../src/a2a.ts";
|
|
6
|
-
import { connectionOf, feedbackEndpoint, feedbackIssueUrl, scrubFeedbackText, sendFeedback, WORKER_VERSION } from "../src/feedback.ts";
|
|
6
|
+
import { connectionOf, feedbackEndpoint, feedbackIssueUrl, feedbackResultText, feedbackToolDescription, scrubFeedbackText, sendFeedback, WORKER_VERSION } from "../src/feedback.ts";
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
8
|
import { hostedTenantContext, resolveTenant, type WorkerBindings } from "../src/tenancy.ts";
|
|
9
9
|
import { d1 } from "./d1.ts";
|
|
@@ -38,9 +38,12 @@ async function call(env: WorkerBindings, path: string, headers: Record<string, s
|
|
|
38
38
|
|
|
39
39
|
test("scrubFeedbackText removes token shapes and Bearer values", () => {
|
|
40
40
|
const token = "a2aow_" + "b".repeat(24);
|
|
41
|
-
const
|
|
41
|
+
const pem = "-----BEGIN RSA PRIVATE KEY-----\nSECRETKEYMATERIAL\n-----END RSA PRIVATE KEY-----";
|
|
42
|
+
const text = scrubFeedbackText(`token ${token} and Bearer supersecretvalue stay out ${pem} AKIAIOSFODNN7EXAMPLE`);
|
|
42
43
|
assert.equal(text.includes(token), false);
|
|
43
44
|
assert.equal(text.includes("supersecretvalue"), false);
|
|
45
|
+
assert.equal(text.includes("SECRETKEYMATERIAL"), false);
|
|
46
|
+
assert.equal(text.includes("AKIAIOSFODNN7EXAMPLE"), false);
|
|
44
47
|
assert.match(text, /\[redacted\]/);
|
|
45
48
|
assert.match(text, /Bearer \[redacted\]/);
|
|
46
49
|
});
|
|
@@ -88,6 +91,18 @@ test("GET /owner/feedback is hosted only for a hosted tenant", async () => {
|
|
|
88
91
|
assert.deepEqual(await res.json(), { hosted: true, url: "https://control.example.com/api/feedback", version: WORKER_VERSION, connection: "polling" });
|
|
89
92
|
});
|
|
90
93
|
|
|
94
|
+
test("the public-issue note names the repository in the issue URL", () => {
|
|
95
|
+
const filed = feedbackResultText({ filed: true, issue_url: "https://github.com/example-org/example-repo/issues/4" });
|
|
96
|
+
assert.match(filed, /public GitHub issue on example-org\/example-repo/);
|
|
97
|
+
assert.equal(filed.includes("telegraphic-dev/a2a-exposed"), false);
|
|
98
|
+
assert.match(filed, /Feedback filed: https:\/\/github.com\/example-org\/example-repo\/issues\/4/);
|
|
99
|
+
const form = feedbackResultText({ filed: false, issue_url: feedbackIssueUrl({ title: "hi" }) });
|
|
100
|
+
assert.match(form, /public GitHub issue on telegraphic-dev\/a2a-exposed/);
|
|
101
|
+
assert.match(feedbackToolDescription(""), /configured feedback repository/);
|
|
102
|
+
assert.equal(feedbackToolDescription("").includes("telegraphic-dev/a2a-exposed"), false);
|
|
103
|
+
assert.match(feedbackToolDescription("example-org/example-repo"), /public GitHub issue on example-org\/example-repo/);
|
|
104
|
+
});
|
|
105
|
+
|
|
91
106
|
test("sendFeedback does not call out from a self-hosted inbox", async () => {
|
|
92
107
|
const ctx = resolveTenant(selfEnv({ FEEDBACK_URL: "https://control.example.com" }));
|
|
93
108
|
const result = await sendFeedback(ctx, { message: "hello", kind: "other", contact: "", version: "" }, () => {
|
package/worker/test/mcp.test.ts
CHANGED
|
@@ -342,6 +342,8 @@ test("send_feedback returns a GitHub issue link and strips tokens", async (t) =>
|
|
|
342
342
|
message: `dropped a task ${token}`, kind: "bug", steps: `ran ${token}`, agent: "Claude Code", environment: "cloud agent sandbox",
|
|
343
343
|
});
|
|
344
344
|
assert.equal(r.isError, false);
|
|
345
|
+
assert.match(r.content[0].text, /public GitHub issue on telegraphic-dev\/a2a-exposed/);
|
|
346
|
+
assert.match(r.content[0].text, /confidential information/);
|
|
345
347
|
const link = r.content[0].text.split("\n").pop() ?? "";
|
|
346
348
|
const url = new URL(link);
|
|
347
349
|
assert.equal(`${url.origin}${url.pathname}`, "https://github.com/telegraphic-dev/a2a-exposed/issues/new");
|
|
@@ -357,6 +359,8 @@ test("send_feedback returns a GitHub issue link and strips tokens", async (t) =>
|
|
|
357
359
|
assert.equal(url.searchParams.get("version"), `worker ${WORKER_VERSION}`);
|
|
358
360
|
assert.equal((url.searchParams.get("message") ?? "").includes("hook.example.net"), false);
|
|
359
361
|
const listed = (await s.mcp(a.access_token, "tools/list")).data.result.tools.find((tool: { name: string }) => tool.name === "send_feedback");
|
|
362
|
+
assert.match(listed.description, /public GitHub issue on telegraphic-dev\/a2a-exposed/);
|
|
363
|
+
assert.match(listed.description, /Redact before sending/);
|
|
360
364
|
assert.match(listed.description, /exact commands/);
|
|
361
365
|
assert.match(listed.description, /filled by the inbox/);
|
|
362
366
|
assert.match(listed.inputSchema.properties.steps.description, /Remove tokens and secrets/);
|