@sjawhar/pi-envoy 10.3.0 → 10.5.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/dist/dispatch.js +28 -14
- package/dist/envoy.js +343 -4
- package/dist/skills/dispatch/references/reading.md +3 -2
- package/dist/skills/envoy/SKILL.md +6 -0
- package/package.json +1 -1
package/dist/dispatch.js
CHANGED
|
@@ -16577,14 +16577,14 @@ function toolSchema(tool) {
|
|
|
16577
16577
|
toolSchemas.set(tool, schema);
|
|
16578
16578
|
return schema;
|
|
16579
16579
|
}
|
|
16580
|
-
async function resolveOwnerArguments(tool, input, cwd, env, exec,
|
|
16580
|
+
async function resolveOwnerArguments(tool, input, cwd, env, exec, dashboardUrls, problems) {
|
|
16581
16581
|
if (issueFreeTools[tool] === true)
|
|
16582
16582
|
return { args: input, ref: null, owner: null };
|
|
16583
16583
|
const refArgument = input.ref;
|
|
16584
16584
|
let ref = null;
|
|
16585
16585
|
let args = input;
|
|
16586
16586
|
if (typeof refArgument === "string") {
|
|
16587
|
-
const refText = dispatchRefFromUrl(refArgument,
|
|
16587
|
+
const refText = dashboardUrls.map((dashboard) => dispatchRefFromUrl(refArgument, dashboard)).find((ref2) => ref2 !== undefined) ?? refArgument;
|
|
16588
16588
|
if (refText !== refArgument)
|
|
16589
16589
|
args = { ...input, ref: refText };
|
|
16590
16590
|
const agentArtifact = parseAgentArtifactRef(refText);
|
|
@@ -16846,7 +16846,7 @@ function progressText(progress, sep) {
|
|
|
16846
16846
|
}
|
|
16847
16847
|
return parts.join(sep);
|
|
16848
16848
|
}
|
|
16849
|
-
function issueSummary(issue2, events, references, graph, titles) {
|
|
16849
|
+
function issueSummary(issue2, events, references, graph, linkBase, titles) {
|
|
16850
16850
|
const asks = issue2.open_asks;
|
|
16851
16851
|
const spec = issue2.artifacts?.find((artifact) => artifact.primary);
|
|
16852
16852
|
const specApproval = spec === undefined ? undefined : approvalLine(spec);
|
|
@@ -16864,6 +16864,7 @@ function issueSummary(issue2, events, references, graph, titles) {
|
|
|
16864
16864
|
return [
|
|
16865
16865
|
`Title: ${issue2.title}`,
|
|
16866
16866
|
`Key: ${issue2.key}`,
|
|
16867
|
+
`Link: ${new URL(`/issues/${encodeURIComponent(issue2.key)}`, linkBase).toString()}`,
|
|
16867
16868
|
`Status: ${issue2.status}`,
|
|
16868
16869
|
`Assignee: ${issue2.assignee ?? "unassigned"}`,
|
|
16869
16870
|
`Claimed by: ${issue2.claim === null ? "nobody" : claimText(issue2.claim, titles)}`,
|
|
@@ -17328,6 +17329,7 @@ function withAccount(error48, account) {
|
|
|
17328
17329
|
}
|
|
17329
17330
|
async function executeDispatchTool(input) {
|
|
17330
17331
|
const { url: configUrl, token: configToken } = input.config;
|
|
17332
|
+
const linkBase = input.config.publicUrl ?? configUrl;
|
|
17331
17333
|
const baseFetch = input.fetchImpl ?? fetch;
|
|
17332
17334
|
const fetchImpl = Object.assign(async (...args2) => {
|
|
17333
17335
|
try {
|
|
@@ -17342,7 +17344,7 @@ async function executeDispatchTool(input) {
|
|
|
17342
17344
|
const env = input.env ?? process.env;
|
|
17343
17345
|
const exec = input.exec ?? defaultExec;
|
|
17344
17346
|
const problems = [];
|
|
17345
|
-
const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec, configUrl, problems);
|
|
17347
|
+
const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec, input.config.publicUrl === undefined ? [configUrl] : [configUrl, input.config.publicUrl], problems);
|
|
17346
17348
|
const ownerMissing = issueFreeTools[input.tool] !== true && ownerArguments.owner === null;
|
|
17347
17349
|
const schema = toolSchema(input.tool);
|
|
17348
17350
|
const parsed = schema.safeParse(ownerArguments.args, { reportInput: true });
|
|
@@ -17360,13 +17362,13 @@ async function executeDispatchTool(input) {
|
|
|
17360
17362
|
const project = optionalString(ownerArguments.args, "project");
|
|
17361
17363
|
if (project !== undefined) {
|
|
17362
17364
|
const response2 = await client2.openAsksForProject(project);
|
|
17363
|
-
return { text: formatOpenAsksSummary(response2,
|
|
17365
|
+
return { text: formatOpenAsksSummary(response2, linkBase), details: { ...response2 } };
|
|
17364
17366
|
}
|
|
17365
17367
|
const sessionId = input.sessionId?.trim();
|
|
17366
17368
|
if (!sessionId)
|
|
17367
17369
|
throw new Error("host session id is required for dispatch open-asks");
|
|
17368
17370
|
const response = await client2.openAsks(sessionId);
|
|
17369
|
-
return { text: formatOpenAsksSummary(response,
|
|
17371
|
+
return { text: formatOpenAsksSummary(response, linkBase), details: { ...response } };
|
|
17370
17372
|
}
|
|
17371
17373
|
if (input.tool === "dispatch_whoami") {
|
|
17372
17374
|
const sessionId = input.sessionId?.trim();
|
|
@@ -17448,11 +17450,13 @@ async function executeDispatchTool(input) {
|
|
|
17448
17450
|
isPrimarySpec: spec !== undefined
|
|
17449
17451
|
}),
|
|
17450
17452
|
...componentGuidance === undefined ? [] : [componentGuidance],
|
|
17451
|
-
...renderSuggestions(created.advice?.suggestions,
|
|
17453
|
+
...renderSuggestions(created.advice?.suggestions, linkBase)
|
|
17452
17454
|
];
|
|
17455
|
+
const href = new URL(`/issues/${encodeURIComponent(created.key)}`, linkBase).toString();
|
|
17456
|
+
const assigned = created.assignee === undefined ? "" : created.assignee === null ? "; unassigned" : `; assigned to ${created.assignee}`;
|
|
17453
17457
|
return {
|
|
17454
17458
|
text: [
|
|
17455
|
-
`Created ${created.key}: ${created.title} ${notSubscribed(issueTopic(created.key))}`,
|
|
17459
|
+
`Created ${created.key}: ${created.title} \u2192 ${href}${assigned} ${notSubscribed(issueTopic(created.key))}`,
|
|
17456
17460
|
...adviceLines
|
|
17457
17461
|
].join(`
|
|
17458
17462
|
`),
|
|
@@ -17470,7 +17474,7 @@ async function executeDispatchTool(input) {
|
|
|
17470
17474
|
text: [
|
|
17471
17475
|
`Not created: "${title}" looks like a duplicate.`,
|
|
17472
17476
|
...candidates.map((candidate) => {
|
|
17473
|
-
const href = new URL(candidate.href,
|
|
17477
|
+
const href = new URL(candidate.href, linkBase).toString();
|
|
17474
17478
|
return `${candidate.key} [${candidate.status}] ${candidate.title} \u2192 ${href}`;
|
|
17475
17479
|
}),
|
|
17476
17480
|
"Reference the existing issue, or run dispatch issue again with --force after reading it."
|
|
@@ -17611,7 +17615,7 @@ async function executeDispatchTool(input) {
|
|
|
17611
17615
|
...limit === undefined ? {} : { limit },
|
|
17612
17616
|
...offset === undefined ? {} : { offset }
|
|
17613
17617
|
});
|
|
17614
|
-
return searchAnswer(search, query, offset,
|
|
17618
|
+
return searchAnswer(search, query, offset, linkBase);
|
|
17615
17619
|
}
|
|
17616
17620
|
case "dispatch_issues": {
|
|
17617
17621
|
const project = stringArg(args, "project");
|
|
@@ -17675,7 +17679,7 @@ async function executeDispatchTool(input) {
|
|
|
17675
17679
|
} catch (error48) {
|
|
17676
17680
|
throw refusalWithCode(error48);
|
|
17677
17681
|
}
|
|
17678
|
-
const href = new URL(`/projects/${encodeURIComponent(project.key)}`,
|
|
17682
|
+
const href = new URL(`/projects/${encodeURIComponent(project.key)}`, linkBase).toString();
|
|
17679
17683
|
return {
|
|
17680
17684
|
text: `Created project ${project.key} (${project.name}) at ${project.created_at}: ${href}`,
|
|
17681
17685
|
details: { project: project.key, name: project.name, created_at: project.created_at }
|
|
@@ -17768,7 +17772,7 @@ async function executeDispatchTool(input) {
|
|
|
17768
17772
|
const askOwner = ask.issue_key !== null ? issueTopic(ask.issue_key) : resolved === undefined ? issueTopic(issue2()) : documentTopic(resolved.artifact);
|
|
17769
17773
|
const adviceLines = [
|
|
17770
17774
|
...renderAdvice(input.tool, askOwner.label, ask.advice, {}),
|
|
17771
|
-
...renderSuggestions(ask.advice?.suggestions,
|
|
17775
|
+
...renderSuggestions(ask.advice?.suggestions, linkBase)
|
|
17772
17776
|
];
|
|
17773
17777
|
return {
|
|
17774
17778
|
text: [
|
|
@@ -18249,7 +18253,7 @@ ${trailer.join(`
|
|
|
18249
18253
|
])
|
|
18250
18254
|
]);
|
|
18251
18255
|
return {
|
|
18252
|
-
text: issueSummary(read.issue, read.events, references, [...pictures2.lines, ...graph], titles),
|
|
18256
|
+
text: issueSummary(read.issue, read.events, references, [...pictures2.lines, ...graph], linkBase, titles),
|
|
18253
18257
|
details: { issue: read.issue.key },
|
|
18254
18258
|
...showImages(pictures2)
|
|
18255
18259
|
};
|
|
@@ -18987,7 +18991,17 @@ function dispatchSettings(env, config2) {
|
|
|
18987
18991
|
}
|
|
18988
18992
|
const url2 = (explicitUrl ?? (source.kind === "file" ? source.server : own.serverUrl ?? DEFAULT_SERVER_URL)).replace(/\/+$/, "");
|
|
18989
18993
|
const token = source.kind === "variable" ? source.token : fileBearer(source, url2, "DISPATCH", env);
|
|
18990
|
-
|
|
18994
|
+
const publicUrl = nonEmpty(env.DISPATCH_PUBLIC_URL);
|
|
18995
|
+
const publicParsed = publicUrl === undefined ? undefined : httpUrl2(publicUrl);
|
|
18996
|
+
if (publicUrl !== undefined && (publicParsed === undefined || publicParsed.port === "0")) {
|
|
18997
|
+
return offWithError("DISPATCH_PUBLIC_URL must be an http or https URL");
|
|
18998
|
+
}
|
|
18999
|
+
return {
|
|
19000
|
+
enabled: true,
|
|
19001
|
+
url: url2,
|
|
19002
|
+
token,
|
|
19003
|
+
...publicUrl === undefined ? {} : { publicUrl: publicUrl.replace(/\/+$/, "") }
|
|
19004
|
+
};
|
|
18991
19005
|
} catch (error48) {
|
|
18992
19006
|
return offWithError(messageFor(error48));
|
|
18993
19007
|
}
|
package/dist/envoy.js
CHANGED
|
@@ -30463,6 +30463,12 @@ var DISPATCH_DOCUMENT_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}document.`;
|
|
|
30463
30463
|
function agentSubject(session) {
|
|
30464
30464
|
return `${AGENT_TOPIC_PREFIX}${session}`;
|
|
30465
30465
|
}
|
|
30466
|
+
// ../contracts/src/slack.ts
|
|
30467
|
+
var SLACK_POST_TEXT_MAX = 4000;
|
|
30468
|
+
function slackTopicChannel(topic) {
|
|
30469
|
+
const [scope, source, , channel] = topic.split(".");
|
|
30470
|
+
return scope === "notifications" && source === "slack" ? channel ?? "" : undefined;
|
|
30471
|
+
}
|
|
30466
30472
|
// ../contracts/src/tool-schema.ts
|
|
30467
30473
|
function overCapMessage(length, max) {
|
|
30468
30474
|
return `is ${length - max} characters over the ${max}-character limit (${length}/${max})`;
|
|
@@ -32577,7 +32583,17 @@ function dispatchSettings(env, config2) {
|
|
|
32577
32583
|
}
|
|
32578
32584
|
const url2 = (explicitUrl ?? (source.kind === "file" ? source.server : own.serverUrl ?? DEFAULT_SERVER_URL)).replace(/\/+$/, "");
|
|
32579
32585
|
const token = source.kind === "variable" ? source.token : fileBearer(source, url2, "DISPATCH", env);
|
|
32580
|
-
|
|
32586
|
+
const publicUrl = nonEmpty(env.DISPATCH_PUBLIC_URL);
|
|
32587
|
+
const publicParsed = publicUrl === undefined ? undefined : httpUrl2(publicUrl);
|
|
32588
|
+
if (publicUrl !== undefined && (publicParsed === undefined || publicParsed.port === "0")) {
|
|
32589
|
+
return offWithError("DISPATCH_PUBLIC_URL must be an http or https URL");
|
|
32590
|
+
}
|
|
32591
|
+
return {
|
|
32592
|
+
enabled: true,
|
|
32593
|
+
url: url2,
|
|
32594
|
+
token,
|
|
32595
|
+
...publicUrl === undefined ? {} : { publicUrl: publicUrl.replace(/\/+$/, "") }
|
|
32596
|
+
};
|
|
32581
32597
|
} catch (error48) {
|
|
32582
32598
|
return offWithError(messageFor(error48));
|
|
32583
32599
|
}
|
|
@@ -33698,6 +33714,293 @@ function sessionDispatchSettings(file2, env, options) {
|
|
|
33698
33714
|
};
|
|
33699
33715
|
}
|
|
33700
33716
|
|
|
33717
|
+
// ../envoy-client/src/slack-post.ts
|
|
33718
|
+
import { BlockList, isIP } from "net";
|
|
33719
|
+
var SLACK_BOT_TOKEN_SECRET = "LEGION_SLACK_BOT_TOKEN";
|
|
33720
|
+
var SLACK_POST_API_URL_VARIABLE = "SLACK_POST_API_URL";
|
|
33721
|
+
var POST_TIMEOUT_MS = 30000;
|
|
33722
|
+
var SLACK_ERROR_CODES = [
|
|
33723
|
+
"access_denied",
|
|
33724
|
+
"accesslimited",
|
|
33725
|
+
"account_inactive",
|
|
33726
|
+
"agent_prompt_display_not_allowed",
|
|
33727
|
+
"app_access_restricted",
|
|
33728
|
+
"as_user_not_supported",
|
|
33729
|
+
"attachment_payload_limit_exceeded",
|
|
33730
|
+
"cannot_reply_to_message",
|
|
33731
|
+
"channel_not_found",
|
|
33732
|
+
"deprecated_endpoint",
|
|
33733
|
+
"draft_already_deleted",
|
|
33734
|
+
"draft_already_sent",
|
|
33735
|
+
"draft_has_conflict",
|
|
33736
|
+
"draft_not_found",
|
|
33737
|
+
"duplicate_channel_not_found",
|
|
33738
|
+
"duplicate_message_not_found",
|
|
33739
|
+
"ekm_access_denied",
|
|
33740
|
+
"enterprise_is_restricted",
|
|
33741
|
+
"fatal_error",
|
|
33742
|
+
"internal_error",
|
|
33743
|
+
"invalid_arg_name",
|
|
33744
|
+
"invalid_arguments",
|
|
33745
|
+
"invalid_array_arg",
|
|
33746
|
+
"invalid_auth",
|
|
33747
|
+
"invalid_blocks",
|
|
33748
|
+
"invalid_blocks_format",
|
|
33749
|
+
"invalid_charset",
|
|
33750
|
+
"invalid_form_data",
|
|
33751
|
+
"invalid_metadata_format",
|
|
33752
|
+
"invalid_metadata_schema",
|
|
33753
|
+
"invalid_post_type",
|
|
33754
|
+
"is_archived",
|
|
33755
|
+
"markdown_text_conflict",
|
|
33756
|
+
"message_limit_exceeded",
|
|
33757
|
+
"messages_tab_disabled",
|
|
33758
|
+
"metadata_must_be_sent_from_app",
|
|
33759
|
+
"metadata_too_large",
|
|
33760
|
+
"method_deprecated",
|
|
33761
|
+
"missing_file_data",
|
|
33762
|
+
"missing_post_type",
|
|
33763
|
+
"missing_scope",
|
|
33764
|
+
"msg_blocks_too_long",
|
|
33765
|
+
"no_permission",
|
|
33766
|
+
"no_text",
|
|
33767
|
+
"not_allowed_token_type",
|
|
33768
|
+
"not_authed",
|
|
33769
|
+
"not_in_channel",
|
|
33770
|
+
"org_login_required",
|
|
33771
|
+
"rate_limited",
|
|
33772
|
+
"ratelimited",
|
|
33773
|
+
"request_timeout",
|
|
33774
|
+
"restricted_action",
|
|
33775
|
+
"restricted_action_non_threadable_channel",
|
|
33776
|
+
"restricted_action_read_only_channel",
|
|
33777
|
+
"restricted_action_thread_locked",
|
|
33778
|
+
"restricted_action_thread_only_channel",
|
|
33779
|
+
"send_on_behalf_not_allowed",
|
|
33780
|
+
"service_unavailable",
|
|
33781
|
+
"slack_connect_canvas_sharing_blocked",
|
|
33782
|
+
"slack_connect_file_link_sharing_blocked",
|
|
33783
|
+
"slack_connect_lists_sharing_blocked",
|
|
33784
|
+
"team_access_not_granted",
|
|
33785
|
+
"team_added_to_org",
|
|
33786
|
+
"team_not_found",
|
|
33787
|
+
"token_expired",
|
|
33788
|
+
"token_revoked",
|
|
33789
|
+
"too_many_attachments",
|
|
33790
|
+
"too_many_contact_cards",
|
|
33791
|
+
"two_factor_setup_required"
|
|
33792
|
+
];
|
|
33793
|
+
var POSTER = `
|
|
33794
|
+
const codes = ${JSON.stringify(Object.fromEntries(SLACK_ERROR_CODES.map((code) => [code, true])))};
|
|
33795
|
+
const token = process.env.LEGION_SLACK_BOT_TOKEN || "";
|
|
33796
|
+
const out = (value) => {
|
|
33797
|
+
let line = JSON.stringify(value);
|
|
33798
|
+
if (token !== "") line = line.split(token).join("[redacted]");
|
|
33799
|
+
process.stdout.write(line + "\\n");
|
|
33800
|
+
};
|
|
33801
|
+
let sent = false;
|
|
33802
|
+
try {
|
|
33803
|
+
const input = JSON.parse(await Bun.stdin.text());
|
|
33804
|
+
if (token === "") {
|
|
33805
|
+
out({ invalid: "the poster's environment carries no LEGION_SLACK_BOT_TOKEN" });
|
|
33806
|
+
process.exit(0);
|
|
33807
|
+
}
|
|
33808
|
+
const base = (process.env.SLACK_POST_API_URL || "https://slack.com/api").replace(/\\/+$/, "");
|
|
33809
|
+
let status = 0;
|
|
33810
|
+
let raw = "";
|
|
33811
|
+
try {
|
|
33812
|
+
sent = true;
|
|
33813
|
+
const response = await fetch(base + "/chat.postMessage", {
|
|
33814
|
+
method: "POST",
|
|
33815
|
+
headers: { Authorization: "Bearer " + token, "Content-Type": "application/json; charset=utf-8" },
|
|
33816
|
+
body: JSON.stringify({ channel: input.channel, thread_ts: input.thread_ts, text: input.text }),
|
|
33817
|
+
signal: AbortSignal.timeout(10000),
|
|
33818
|
+
});
|
|
33819
|
+
status = response.status;
|
|
33820
|
+
raw = await response.text();
|
|
33821
|
+
} catch {
|
|
33822
|
+
out({ unanswered: true });
|
|
33823
|
+
process.exit(0);
|
|
33824
|
+
}
|
|
33825
|
+
let answer;
|
|
33826
|
+
try {
|
|
33827
|
+
answer = JSON.parse(raw);
|
|
33828
|
+
} catch {}
|
|
33829
|
+
if (!answer || typeof answer !== "object" || typeof answer.ok !== "boolean") {
|
|
33830
|
+
out({ invalid: "HTTP " + Number(status) + " with a body that is not Slack's JSON" });
|
|
33831
|
+
} else if (!answer.ok) {
|
|
33832
|
+
out({ refused: typeof answer.error === "string" && Object.hasOwn(codes, answer.error) ? answer.error : null });
|
|
33833
|
+
} else if (
|
|
33834
|
+
typeof answer.ts !== "string" || !/^[0-9]{1,12}\\.[0-9]{1,8}$/.test(answer.ts) ||
|
|
33835
|
+
(answer.channel !== undefined && (typeof answer.channel !== "string" || !/^[CDG][A-Z0-9]{1,30}$/.test(answer.channel)))
|
|
33836
|
+
) {
|
|
33837
|
+
out({ invalid: "ok with a ts or channel that is not Slack's" });
|
|
33838
|
+
} else {
|
|
33839
|
+
out({ ts: answer.ts, channel: answer.channel === undefined ? input.channel : answer.channel });
|
|
33840
|
+
}
|
|
33841
|
+
} catch {
|
|
33842
|
+
out(sent ? { unanswered: true } : { invalid: "a reply the poster could not read" });
|
|
33843
|
+
}
|
|
33844
|
+
`;
|
|
33845
|
+
var MAY_HAVE_POSTED = "the reply may have been posted, and posting it again may put it in the thread twice";
|
|
33846
|
+
var URL_CANDIDATE = /(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,31}:\/\/[^\s<>|"'`]+/gi;
|
|
33847
|
+
var WEB_SCHEME = /^https?:\/\//i;
|
|
33848
|
+
var BARE_HOST = /(?<![\w@./:%-])((?:[a-z0-9-]{1,63}\.)*[a-z0-9-]{1,63})\.?(:\d{1,5})?(?![\w-])/gi;
|
|
33849
|
+
var BARE_IPV6 = /(?<![\w@./:%-])\[([0-9a-f:.]{2,45})(?:%[\w.-]{1,32})?\](?::\d{1,5})?/gi;
|
|
33850
|
+
var TRAILING_PUNCTUATION = /[.,;:!?)\]}]+$/;
|
|
33851
|
+
var CLUSTER_ADDRESSES = new BlockList;
|
|
33852
|
+
for (const [network, prefix] of [
|
|
33853
|
+
["0.0.0.0", 8],
|
|
33854
|
+
["10.0.0.0", 8],
|
|
33855
|
+
["100.64.0.0", 10],
|
|
33856
|
+
["127.0.0.0", 8],
|
|
33857
|
+
["169.254.0.0", 16],
|
|
33858
|
+
["172.16.0.0", 12],
|
|
33859
|
+
["192.168.0.0", 16]
|
|
33860
|
+
]) {
|
|
33861
|
+
CLUSTER_ADDRESSES.addSubnet(network, prefix, "ipv4");
|
|
33862
|
+
}
|
|
33863
|
+
for (const [network, prefix] of [
|
|
33864
|
+
["::", 96],
|
|
33865
|
+
["fc00::", 7],
|
|
33866
|
+
["fe80::", 10]
|
|
33867
|
+
]) {
|
|
33868
|
+
CLUSTER_ADDRESSES.addSubnet(network, prefix, "ipv6");
|
|
33869
|
+
}
|
|
33870
|
+
function linkInsideCluster(text, env) {
|
|
33871
|
+
const { DISPATCH_URL: apiUrl, DISPATCH_PUBLIC_URL: publicUrl } = env;
|
|
33872
|
+
const apiHost = hostOf(apiUrl);
|
|
33873
|
+
const publicHost = hostOf(publicUrl);
|
|
33874
|
+
const dispatchHost = apiHost !== undefined && publicHost !== undefined && apiHost !== publicHost ? apiHost : undefined;
|
|
33875
|
+
const decoded = text.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
33876
|
+
for (const match of decoded.matchAll(URL_CANDIDATE)) {
|
|
33877
|
+
if (!WEB_SCHEME.test(match[0]))
|
|
33878
|
+
continue;
|
|
33879
|
+
for (const written of new Set([match[0], match[0].replace(TRAILING_PUNCTUATION, "")])) {
|
|
33880
|
+
const host = hostOf(written);
|
|
33881
|
+
if (host !== undefined && insideCluster(host, dispatchHost, true))
|
|
33882
|
+
return written;
|
|
33883
|
+
}
|
|
33884
|
+
}
|
|
33885
|
+
const prose = decoded.replace(URL_CANDIDATE, (url2) => " ".repeat(url2.length));
|
|
33886
|
+
for (const match of prose.matchAll(BARE_IPV6)) {
|
|
33887
|
+
if (insideCluster(`[${match[1]}]`, dispatchHost, false))
|
|
33888
|
+
return match[0];
|
|
33889
|
+
}
|
|
33890
|
+
for (const match of prose.matchAll(BARE_HOST)) {
|
|
33891
|
+
const host = (match[1] ?? "").toLowerCase();
|
|
33892
|
+
if (!host.includes(".") && match[2] === undefined)
|
|
33893
|
+
continue;
|
|
33894
|
+
if (insideCluster(host, dispatchHost, false))
|
|
33895
|
+
return match[0];
|
|
33896
|
+
}
|
|
33897
|
+
return;
|
|
33898
|
+
}
|
|
33899
|
+
function hostOf(value) {
|
|
33900
|
+
if (value === undefined || value === "")
|
|
33901
|
+
return;
|
|
33902
|
+
try {
|
|
33903
|
+
return new URL(value).hostname.toLowerCase().replace(/\.$/, "") || undefined;
|
|
33904
|
+
} catch {
|
|
33905
|
+
return;
|
|
33906
|
+
}
|
|
33907
|
+
}
|
|
33908
|
+
function insideCluster(host, dispatchHost, fromUrl) {
|
|
33909
|
+
if (host === dispatchHost)
|
|
33910
|
+
return true;
|
|
33911
|
+
if (host.endsWith(".svc") || host.includes(".svc.") || host.endsWith(".cluster.local"))
|
|
33912
|
+
return true;
|
|
33913
|
+
const address = host.startsWith("[") ? host.slice(1, -1) : host;
|
|
33914
|
+
const family = isIP(address);
|
|
33915
|
+
if (family !== 0)
|
|
33916
|
+
return CLUSTER_ADDRESSES.check(address, family === 4 ? "ipv4" : "ipv6");
|
|
33917
|
+
return fromUrl && !host.includes(".");
|
|
33918
|
+
}
|
|
33919
|
+
async function postSlackReply(reply, options = {}) {
|
|
33920
|
+
const env = options.env ?? process.env;
|
|
33921
|
+
const internal = linkInsideCluster(reply.text, env);
|
|
33922
|
+
if (internal !== undefined) {
|
|
33923
|
+
throw new Error(`the reply links ${internal}, an address inside the cluster nobody in Slack can open; link Dispatch's dashboard instead (dispatch prints it when DISPATCH_PUBLIC_URL is set); nothing was posted`);
|
|
33924
|
+
}
|
|
33925
|
+
const override = env[SLACK_POST_API_URL_VARIABLE];
|
|
33926
|
+
if (override !== undefined && override !== "" && !allowedAPIURL(override)) {
|
|
33927
|
+
throw new Error(`${SLACK_POST_API_URL_VARIABLE} must be an https URL, or http on a loopback host (got ${override}); nothing was posted`);
|
|
33928
|
+
}
|
|
33929
|
+
const { PATH: searchPath = "" } = env;
|
|
33930
|
+
const client = Bun.which("agent-secrets", { PATH: searchPath });
|
|
33931
|
+
if (client === null) {
|
|
33932
|
+
throw new Error("agent-secrets is not on PATH: slack_post takes the bot token from the secrets broker, which this session is not enrolled with; nothing was posted");
|
|
33933
|
+
}
|
|
33934
|
+
const reason = `slack_post: reply in Slack channel ${reply.channel}, thread ${reply.threadTs}`;
|
|
33935
|
+
const child = Bun.spawn([client, SLACK_BOT_TOKEN_SECRET, "--reason", reason, "--wait", "0s", "--", "bun", "-e", POSTER], {
|
|
33936
|
+
env,
|
|
33937
|
+
stdin: new Blob([
|
|
33938
|
+
JSON.stringify({ channel: reply.channel, thread_ts: reply.threadTs, text: reply.text })
|
|
33939
|
+
]),
|
|
33940
|
+
stdout: "pipe",
|
|
33941
|
+
stderr: "pipe"
|
|
33942
|
+
});
|
|
33943
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), POST_TIMEOUT_MS);
|
|
33944
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
33945
|
+
new Response(child.stdout).text(),
|
|
33946
|
+
new Response(child.stderr).text(),
|
|
33947
|
+
child.exited
|
|
33948
|
+
]).finally(() => clearTimeout(timer));
|
|
33949
|
+
const said = stderr.trim().split(`
|
|
33950
|
+
`).filter(Boolean).join(" ");
|
|
33951
|
+
const line = posterLine(stdout);
|
|
33952
|
+
if (line === undefined) {
|
|
33953
|
+
if (child.signalCode === "SIGKILL") {
|
|
33954
|
+
throw new Error(`slack_post got no answer within ${POST_TIMEOUT_MS / 1000} s: ${MAY_HAVE_POSTED}`);
|
|
33955
|
+
}
|
|
33956
|
+
if (said.startsWith("agent-secrets:")) {
|
|
33957
|
+
if (code === 75) {
|
|
33958
|
+
throw new Error(`the secrets broker has not granted ${SLACK_BOT_TOKEN_SECRET}: its request is waiting for approval (${said}); nothing was posted`);
|
|
33959
|
+
}
|
|
33960
|
+
if (code === 77) {
|
|
33961
|
+
throw new Error(`the secrets broker denied ${SLACK_BOT_TOKEN_SECRET} (${said}); nothing was posted`);
|
|
33962
|
+
}
|
|
33963
|
+
throw new Error(`the secrets broker did not release ${SLACK_BOT_TOKEN_SECRET} (${said}); nothing was posted`);
|
|
33964
|
+
}
|
|
33965
|
+
throw new Error(`slack_post's poster exited ${code} without an answer: ${MAY_HAVE_POSTED}`);
|
|
33966
|
+
}
|
|
33967
|
+
if (line.refused === null) {
|
|
33968
|
+
throw new Error("Slack refused chat.postMessage with an error code slack_post does not pass on");
|
|
33969
|
+
}
|
|
33970
|
+
if (line.refused !== undefined)
|
|
33971
|
+
throw new Error(`Slack refused chat.postMessage: ${line.refused}`);
|
|
33972
|
+
if (line.unanswered !== undefined) {
|
|
33973
|
+
throw new Error(`Slack did not answer chat.postMessage: ${MAY_HAVE_POSTED}`);
|
|
33974
|
+
}
|
|
33975
|
+
if (line.ts === undefined) {
|
|
33976
|
+
throw new Error(`chat.postMessage answered ${line.invalid ?? "nothing"}: ${MAY_HAVE_POSTED}`);
|
|
33977
|
+
}
|
|
33978
|
+
return { channel: line.channel ?? reply.channel, threadTs: reply.threadTs, ts: line.ts };
|
|
33979
|
+
}
|
|
33980
|
+
function posterLine(stdout) {
|
|
33981
|
+
const last = stdout.trim().split(`
|
|
33982
|
+
`).at(-1);
|
|
33983
|
+
if (last === undefined || last === "")
|
|
33984
|
+
return;
|
|
33985
|
+
try {
|
|
33986
|
+
const parsed = JSON.parse(last);
|
|
33987
|
+
return typeof parsed === "object" && parsed !== null ? parsed : undefined;
|
|
33988
|
+
} catch {
|
|
33989
|
+
return;
|
|
33990
|
+
}
|
|
33991
|
+
}
|
|
33992
|
+
function allowedAPIURL(value) {
|
|
33993
|
+
let url2;
|
|
33994
|
+
try {
|
|
33995
|
+
url2 = new URL(value);
|
|
33996
|
+
} catch {
|
|
33997
|
+
return false;
|
|
33998
|
+
}
|
|
33999
|
+
if (url2.protocol === "https:")
|
|
34000
|
+
return true;
|
|
34001
|
+
return url2.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url2.hostname);
|
|
34002
|
+
}
|
|
34003
|
+
|
|
33701
34004
|
// ../envoy-client/src/tool-contract.ts
|
|
33702
34005
|
var DELIVERY_CONTRACT = "Delivery is at-least-once, possibly out of order across topics; use id for dedupe and at for freshness.";
|
|
33703
34006
|
var TOPIC_GUIDE = "Topic guide (all are under notifications.): agent.<session_id> (subscribe: own inbox); role.<role> " + "(publish-to; holders claim via envoy_role_set). > matches one or more trailing tokens and does not " + "match the base subject. Envoy registers the concrete base when you subscribe to <subject>.>, so " + "the recommended default remains github.<owner>.<repo>.pr.<n>.>. <owner> and <repo> are one token " + "each, with every dot in the name written _ (sjawhar/.github is github.sjawhar._github; " + "acme/site.io is github.acme.site_io), and a topic spelled with the dot receives nothing. " + "Default PR subscription: " + "github.<owner>.<repo>.pr.<n>.> (it receives the quiet PR family): pr.<n> (lifecycle: " + "opened/synchronize/closed; closed carries merged, merge_commit_sha, merged_by, head_sha), " + "pr.<n>.comment, pr.<n>.review, pr.<n>.mention, pr.<n>.checks (one settlement event per commit whose checks settle, the head or not, naming its sha: " + 'passed/failed/cancelled/skipped with failing names and URLs; re-fires with superseded_settlement: "true" ' + "when new runs appear for the same commit). Other GitHub: issue.<n>, issue.<n>.comment, " + "issue.<n>.mention, mention, push.branch.<name>, push.tag.<name>, workflow.<file>.<action> (only " + "runs without an associated PR); slack.<team>.<channel>.message|mention and " + "slack.<team>.<channel>.thread.<ts>.message|mention; ghostwispr.<session>.<kind>; " + "whatsapp.<phone>.<jid>.<kind>; envoy.exceptions.<original-topic>.";
|
|
@@ -33735,7 +34038,8 @@ var EnvoyToolOperation = {
|
|
|
33735
34038
|
setRole: "setRole",
|
|
33736
34039
|
getRole: "getRole",
|
|
33737
34040
|
whoami: "whoami",
|
|
33738
|
-
listSessions: "listSessions"
|
|
34041
|
+
listSessions: "listSessions",
|
|
34042
|
+
slackPost: "slackPost"
|
|
33739
34043
|
};
|
|
33740
34044
|
var envoyToolSpecs = [
|
|
33741
34045
|
{
|
|
@@ -33806,6 +34110,16 @@ var envoyToolSpecs = [
|
|
|
33806
34110
|
title: schema.string().optional()
|
|
33807
34111
|
}),
|
|
33808
34112
|
operation: EnvoyToolOperation.listSessions
|
|
34113
|
+
},
|
|
34114
|
+
{
|
|
34115
|
+
name: "slack_post",
|
|
34116
|
+
description: "Reply in a Slack thread as the Legion Slack app, with the bot token the secrets broker grants this session (LEGION_SLACK_BOT_TOKEN); the session never holds it. channel is the Slack channel id (a mention's channel_id); thread_ts is the thread's root ts: the mention's thread_ts, or its own ts when it has none. Answers the new message's ts. A refusal carries Slack's own error code (not_in_channel: the app is not in that channel) or the broker's state (waiting for approval, denied). Sent once and never retried: after 'Slack did not answer' the reply may already be in the thread, so do not post it again.",
|
|
34117
|
+
arguments: (schema) => ({
|
|
34118
|
+
channel: schema.string({ min: 1 }).describe("Slack channel id, such as C0123ABCD."),
|
|
34119
|
+
thread_ts: schema.string({ min: 1 }).describe("The thread's root ts, which keeps the reply in the mention's thread."),
|
|
34120
|
+
text: schema.string({ min: 1, max: SLACK_POST_TEXT_MAX }).describe(`The reply, in Slack mrkdwn, at most ${SLACK_POST_TEXT_MAX} characters.`)
|
|
34121
|
+
}),
|
|
34122
|
+
operation: EnvoyToolOperation.slackPost
|
|
33809
34123
|
}
|
|
33810
34124
|
];
|
|
33811
34125
|
function sendConfirmationText(result) {
|
|
@@ -34070,7 +34384,7 @@ function sourceSession(sessionID) {
|
|
|
34070
34384
|
|
|
34071
34385
|
// ../pi-shared/src/interface.ts
|
|
34072
34386
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
34073
|
-
var ENVOY_PLUGIN_INTERFACE_VERSION =
|
|
34387
|
+
var ENVOY_PLUGIN_INTERFACE_VERSION = 3;
|
|
34074
34388
|
var ENVOY_PLUGIN_INTERFACE_KEY = Symbol.for("legion.pi-shared.envoy-plugin-interface");
|
|
34075
34389
|
var LEGION_PLUGIN_LOADED_KEY = Symbol.for("legion.pi-legion.loaded");
|
|
34076
34390
|
var LEGACY_LEGION_LOADED_KEY = Symbol.for("legion.pi-envoy.legion-loaded");
|
|
@@ -34084,7 +34398,12 @@ function envoyPluginInterface() {
|
|
|
34084
34398
|
const created = {
|
|
34085
34399
|
version: ENVOY_PLUGIN_INTERFACE_VERSION,
|
|
34086
34400
|
publishers: [],
|
|
34087
|
-
roleClaim: {
|
|
34401
|
+
roleClaim: {
|
|
34402
|
+
instances: [],
|
|
34403
|
+
managedSessions: new Set,
|
|
34404
|
+
regained: undefined,
|
|
34405
|
+
slackRoleOwned: false
|
|
34406
|
+
},
|
|
34088
34407
|
injectedUserTurns: new Map,
|
|
34089
34408
|
bootstrappedSession: { file: undefined }
|
|
34090
34409
|
};
|
|
@@ -35717,9 +36036,21 @@ function envoyExtension(pi) {
|
|
|
35717
36036
|
});
|
|
35718
36037
|
if (registry2 === undefined)
|
|
35719
36038
|
return;
|
|
36039
|
+
const retired = publishedInterface?.roleClaim.slackRoleOwned === true ? registry2.topics.filter((topic) => slackTopicChannel(topic) !== undefined) : [];
|
|
36040
|
+
if (retired.length > 0) {
|
|
36041
|
+
await client.unsubscribe({ sessionID, topics: retired }).catch((error48) => {
|
|
36042
|
+
logger3.warn("envoy: could not unregister a resumed controller's retired Slack topics", {
|
|
36043
|
+
sessionID,
|
|
36044
|
+
topics: retired,
|
|
36045
|
+
error: messageFor(error48)
|
|
36046
|
+
});
|
|
36047
|
+
});
|
|
36048
|
+
}
|
|
35720
36049
|
for (const topic of registry2.topics) {
|
|
35721
36050
|
if (topic === agentSubject(sessionID) || topic.startsWith(ROLE_TOPIC_PREFIX))
|
|
35722
36051
|
continue;
|
|
36052
|
+
if (retired.includes(topic))
|
|
36053
|
+
continue;
|
|
35723
36054
|
await subscribe(topic);
|
|
35724
36055
|
}
|
|
35725
36056
|
};
|
|
@@ -36287,6 +36618,14 @@ Not delivered: that holder is the session this subagent sends as, and the listen
|
|
|
36287
36618
|
title
|
|
36288
36619
|
});
|
|
36289
36620
|
}
|
|
36621
|
+
case EnvoyToolOperation.slackPost: {
|
|
36622
|
+
const posted = await postSlackReply({
|
|
36623
|
+
channel: stringFor(parameters, "channel"),
|
|
36624
|
+
threadTs: stringFor(parameters, "thread_ts"),
|
|
36625
|
+
text: stringFor(parameters, "text")
|
|
36626
|
+
});
|
|
36627
|
+
return toolSuccess(`posted ${posted.ts} in ${posted.channel}, thread ${posted.threadTs}`, { channel: posted.channel, thread_ts: posted.threadTs, ts: posted.ts });
|
|
36628
|
+
}
|
|
36290
36629
|
}
|
|
36291
36630
|
} catch (error48) {
|
|
36292
36631
|
return toolFailure(error48);
|
|
@@ -57,8 +57,9 @@ dispatch://PROJECT/artifact/<document-ref>/comment/<id>
|
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
A bare UUID or `KEY#seq` is not a reference; the `dispatch://` form is what Dispatch links and records. `dispatch read` also
|
|
60
|
-
accepts the dashboard URL of an issue, spec, artifact, ask, comment, or project document on the configured server
|
|
61
|
-
`dispatch://` form above), and an
|
|
60
|
+
accepts the dashboard URL of an issue, spec, artifact, ask, comment, or project document on the configured server, or on its public
|
|
61
|
+
dashboard when `DISPATCH_PUBLIC_URL` names one (it maps to the `dispatch://` form above), and an issue's read names its page
|
|
62
|
+
(`Link:`, on that public dashboard when there is one), the link to hand a person. An ask or comment id may be a unique prefix of at least 8 hex characters; a message id is always the
|
|
62
63
|
full uuid. A non-uuid id on `GET /asks/{id}`, `/comments/{id}`, or `/issues/{key}/messages/{id}` is a 400 `ASK_ID_INPUT` /
|
|
63
64
|
`COMMENT_ID_INPUT` / `MESSAGE_ID_INPUT`, never a 500.
|
|
64
65
|
|
|
@@ -237,6 +237,12 @@ envoy_subscribe([
|
|
|
237
237
|
This follows every message and mention in one thread. Channel-level topics end in `.message` or
|
|
238
238
|
`.mention`; thread timestamps replace dots with underscores.
|
|
239
239
|
|
|
240
|
+
Reply in a thread with `slack_post(channel=<channel_id>, thread_ts=<the message's thread_ts, or
|
|
241
|
+
its ts>, text=…)`, which posts as the Legion Slack app with the bot token the secrets broker grants
|
|
242
|
+
the session (`LEGION_SLACK_BOT_TOKEN`). A refusal carries Slack's own error code, or the broker's
|
|
243
|
+
state when it has not granted the token. The post is sent once; when the answer is that Slack did
|
|
244
|
+
not answer, the reply may already be there, so do not post it again.
|
|
245
|
+
|
|
240
246
|
## Ghost Wispr
|
|
241
247
|
|
|
242
248
|
Ghost Wispr topics are `notifications.ghostwispr.<session>.<kind>`, where `kind` is
|