@crewhaus/channel-adapter-slack 0.5.1 → 0.5.2
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/approvals.d.ts +16 -6
- package/dist/approvals.js +33 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/package.json +2 -2
package/dist/approvals.d.ts
CHANGED
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
* `postApproval` / `ackApproval` do the I/O, and the channel-generic store +
|
|
14
14
|
* orchestration live in `@crewhaus/channel-adapter-base`.
|
|
15
15
|
*/
|
|
16
|
-
/** Block Kit `action_id`s for the
|
|
17
|
-
* these, so they are part of the wire contract. */
|
|
16
|
+
/** Block Kit `action_id`s for the three buttons — the interaction parser keys
|
|
17
|
+
* on these, so they are part of the wire contract. */
|
|
18
18
|
export declare const APPROVE_ACTION_ID = "crewhaus_approve";
|
|
19
|
+
/** #383 — grant AND persist a standing `alwaysAllow` rule for the tool. */
|
|
20
|
+
export declare const APPROVE_ALWAYS_ACTION_ID = "crewhaus_approve_always";
|
|
19
21
|
export declare const DENY_ACTION_ID = "crewhaus_deny";
|
|
20
22
|
/** `block_id` prefix carrying the approval id (a redundant channel to the
|
|
21
23
|
* buttons' `value`, so the id survives even if Slack ever elides `value`). */
|
|
@@ -35,10 +37,14 @@ export type SlackApprovalMessage = {
|
|
|
35
37
|
readonly blocks: readonly unknown[];
|
|
36
38
|
};
|
|
37
39
|
/**
|
|
38
|
-
* Build the Approve/Deny Block Kit message for a parked
|
|
39
|
-
* carries the `approvalId` in its `value`, and the
|
|
40
|
-
* carries it too (`crewhaus_approval:<id>`), so the
|
|
41
|
-
* recover the id from either.
|
|
40
|
+
* Build the Approve / Always allow / Deny Block Kit message for a parked
|
|
41
|
+
* approval. Each button carries the `approvalId` in its `value`, and the
|
|
42
|
+
* actions block's `block_id` carries it too (`crewhaus_approval:<id>`), so the
|
|
43
|
+
* interaction parser can recover the id from either. "Always allow" (#383)
|
|
44
|
+
* grants this call AND persists a standing `alwaysAllow` rule for the tool in
|
|
45
|
+
* the harness's `.crewhaus/settings.json`, so a tool the loop calls repeatedly
|
|
46
|
+
* with varying input stops re-prompting (a plain Approve is one-shot and keyed
|
|
47
|
+
* on the exact input).
|
|
42
48
|
*/
|
|
43
49
|
export declare function buildApprovalMessage(content: ApprovalPromptContent): SlackApprovalMessage;
|
|
44
50
|
/**
|
|
@@ -50,6 +56,8 @@ export declare function buildApprovalAckMessage(args: {
|
|
|
50
56
|
readonly decision: "grant" | "deny";
|
|
51
57
|
readonly by: string;
|
|
52
58
|
readonly toolName?: string;
|
|
59
|
+
/** #383 — the grant was recorded as a standing allow. */
|
|
60
|
+
readonly always?: boolean;
|
|
53
61
|
}): SlackApprovalMessage;
|
|
54
62
|
/** A parsed Slack interactivity webhook. Only Approve/Deny `block_actions` are
|
|
55
63
|
* surfaced; every other interaction type is `skip`. */
|
|
@@ -57,6 +65,8 @@ export type ParsedInteraction = {
|
|
|
57
65
|
readonly kind: "approval_action";
|
|
58
66
|
readonly approvalId: string;
|
|
59
67
|
readonly decision: "grant" | "deny";
|
|
68
|
+
/** #383 — the click was "Always allow": grant + persist a standing rule. */
|
|
69
|
+
readonly always?: boolean;
|
|
60
70
|
/** The Slack user who clicked (deciding identity). */
|
|
61
71
|
readonly userId: string;
|
|
62
72
|
readonly channelId?: string;
|
package/dist/approvals.js
CHANGED
|
@@ -13,18 +13,24 @@
|
|
|
13
13
|
* `postApproval` / `ackApproval` do the I/O, and the channel-generic store +
|
|
14
14
|
* orchestration live in `@crewhaus/channel-adapter-base`.
|
|
15
15
|
*/
|
|
16
|
-
/** Block Kit `action_id`s for the
|
|
17
|
-
* these, so they are part of the wire contract. */
|
|
16
|
+
/** Block Kit `action_id`s for the three buttons — the interaction parser keys
|
|
17
|
+
* on these, so they are part of the wire contract. */
|
|
18
18
|
export const APPROVE_ACTION_ID = "crewhaus_approve";
|
|
19
|
+
/** #383 — grant AND persist a standing `alwaysAllow` rule for the tool. */
|
|
20
|
+
export const APPROVE_ALWAYS_ACTION_ID = "crewhaus_approve_always";
|
|
19
21
|
export const DENY_ACTION_ID = "crewhaus_deny";
|
|
20
22
|
/** `block_id` prefix carrying the approval id (a redundant channel to the
|
|
21
23
|
* buttons' `value`, so the id survives even if Slack ever elides `value`). */
|
|
22
24
|
export const APPROVAL_BLOCK_ID_PREFIX = "crewhaus_approval:";
|
|
23
25
|
/**
|
|
24
|
-
* Build the Approve/Deny Block Kit message for a parked
|
|
25
|
-
* carries the `approvalId` in its `value`, and the
|
|
26
|
-
* carries it too (`crewhaus_approval:<id>`), so the
|
|
27
|
-
* recover the id from either.
|
|
26
|
+
* Build the Approve / Always allow / Deny Block Kit message for a parked
|
|
27
|
+
* approval. Each button carries the `approvalId` in its `value`, and the
|
|
28
|
+
* actions block's `block_id` carries it too (`crewhaus_approval:<id>`), so the
|
|
29
|
+
* interaction parser can recover the id from either. "Always allow" (#383)
|
|
30
|
+
* grants this call AND persists a standing `alwaysAllow` rule for the tool in
|
|
31
|
+
* the harness's `.crewhaus/settings.json`, so a tool the loop calls repeatedly
|
|
32
|
+
* with varying input stops re-prompting (a plain Approve is one-shot and keyed
|
|
33
|
+
* on the exact input).
|
|
28
34
|
*/
|
|
29
35
|
export function buildApprovalMessage(content) {
|
|
30
36
|
const text = `Approval needed: \`${content.toolName}\` wants to run (surface: ${content.surface})`;
|
|
@@ -47,6 +53,12 @@ export function buildApprovalMessage(content) {
|
|
|
47
53
|
style: "primary",
|
|
48
54
|
value: content.approvalId,
|
|
49
55
|
},
|
|
56
|
+
{
|
|
57
|
+
type: "button",
|
|
58
|
+
action_id: APPROVE_ALWAYS_ACTION_ID,
|
|
59
|
+
text: { type: "plain_text", text: `Always allow ${truncateForButton(content.toolName)}` },
|
|
60
|
+
value: content.approvalId,
|
|
61
|
+
},
|
|
50
62
|
{
|
|
51
63
|
type: "button",
|
|
52
64
|
action_id: DENY_ACTION_ID,
|
|
@@ -61,7 +73,7 @@ export function buildApprovalMessage(content) {
|
|
|
61
73
|
elements: [
|
|
62
74
|
{
|
|
63
75
|
type: "mrkdwn",
|
|
64
|
-
text: `id \`${content.approvalId}\` · or run \`crewhaus approvals grant ${content.approvalId}
|
|
76
|
+
text: `id \`${content.approvalId}\` · or run \`crewhaus approvals grant ${content.approvalId}\` (add \`--always\` for a standing allow)`,
|
|
65
77
|
},
|
|
66
78
|
],
|
|
67
79
|
},
|
|
@@ -74,10 +86,12 @@ export function buildApprovalMessage(content) {
|
|
|
74
86
|
* decided. Returned as text + a single context block (buttons removed).
|
|
75
87
|
*/
|
|
76
88
|
export function buildApprovalAckMessage(args) {
|
|
77
|
-
const
|
|
89
|
+
const always = args.decision === "grant" && args.always === true;
|
|
90
|
+
const verb = args.decision === "grant" ? (always ? "Approved (always)" : "Approved") : "Denied";
|
|
78
91
|
const icon = args.decision === "grant" ? "✅" : "🚫";
|
|
79
92
|
const tool = args.toolName !== undefined ? ` \`${args.toolName}\`` : "";
|
|
80
|
-
const
|
|
93
|
+
const suffix = always ? " — future calls run pre-approved" : "";
|
|
94
|
+
const text = `${icon} ${verb}${tool} by ${args.by}${suffix}`;
|
|
81
95
|
return {
|
|
82
96
|
text,
|
|
83
97
|
blocks: [{ type: "context", elements: [{ type: "mrkdwn", text }] }],
|
|
@@ -117,13 +131,15 @@ export function parseSlackInteraction(body) {
|
|
|
117
131
|
const action = actions[0];
|
|
118
132
|
if (action === undefined)
|
|
119
133
|
return { kind: "skip" };
|
|
120
|
-
const
|
|
134
|
+
const actionId = action["action_id"];
|
|
135
|
+
const decision = actionId === APPROVE_ACTION_ID || actionId === APPROVE_ALWAYS_ACTION_ID
|
|
121
136
|
? "grant"
|
|
122
|
-
:
|
|
137
|
+
: actionId === DENY_ACTION_ID
|
|
123
138
|
? "deny"
|
|
124
139
|
: undefined;
|
|
125
140
|
if (decision === undefined)
|
|
126
141
|
return { kind: "skip" };
|
|
142
|
+
const always = actionId === APPROVE_ALWAYS_ACTION_ID;
|
|
127
143
|
// Prefer the button's `value`; fall back to the actions block_id suffix.
|
|
128
144
|
const approvalId = (typeof action["value"] === "string" && action["value"].length > 0
|
|
129
145
|
? action["value"]
|
|
@@ -164,6 +180,7 @@ export function parseSlackInteraction(body) {
|
|
|
164
180
|
kind: "approval_action",
|
|
165
181
|
approvalId,
|
|
166
182
|
decision,
|
|
183
|
+
...(always ? { always: true } : {}),
|
|
167
184
|
userId,
|
|
168
185
|
...(channelId !== undefined ? { channelId } : {}),
|
|
169
186
|
...(messageTs !== undefined ? { messageTs } : {}),
|
|
@@ -185,3 +202,8 @@ function truncateForBlock(preview, max = 500) {
|
|
|
185
202
|
const safe = preview.replace(/```/g, "'''");
|
|
186
203
|
return safe.length > max ? `${safe.slice(0, max)}…` : safe;
|
|
187
204
|
}
|
|
205
|
+
/** Slack caps a button's plain_text at 75 chars; keep room for the
|
|
206
|
+
* "Always allow " prefix so a long MCP tool name can't break the block. */
|
|
207
|
+
function truncateForButton(toolName, max = 40) {
|
|
208
|
+
return toolName.length > max ? `${toolName.slice(0, max)}…` : toolName;
|
|
209
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { CrewhausError } from "@crewhaus/errors";
|
|
26
26
|
import { type ApprovalPromptContent, type ParsedInteraction } from "./approvals.js";
|
|
27
27
|
export { signSlackBody, verifySlackSignature } from "./verify.js";
|
|
28
|
-
export { APPROVE_ACTION_ID, DENY_ACTION_ID, APPROVAL_BLOCK_ID_PREFIX, type ApprovalPromptContent, type SlackApprovalMessage, type ParsedInteraction, buildApprovalMessage, buildApprovalAckMessage, parseSlackInteraction, } from "./approvals.js";
|
|
28
|
+
export { APPROVE_ACTION_ID, APPROVE_ALWAYS_ACTION_ID, DENY_ACTION_ID, APPROVAL_BLOCK_ID_PREFIX, type ApprovalPromptContent, type SlackApprovalMessage, type ParsedInteraction, buildApprovalMessage, buildApprovalAckMessage, parseSlackInteraction, } from "./approvals.js";
|
|
29
29
|
export declare class SlackAdapterError extends CrewhausError {
|
|
30
30
|
readonly name = "SlackAdapterError";
|
|
31
31
|
constructor(message: string, cause?: unknown);
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ import { buildApprovalAckMessage, buildApprovalMessage, parseSlackInteraction, }
|
|
|
27
27
|
import { signSlackBody, verifySlackSignature } from "./verify.js";
|
|
28
28
|
export { signSlackBody, verifySlackSignature } from "./verify.js";
|
|
29
29
|
// Loop contract 0.4 (Batch C, G11) — Slack interactive approval surface.
|
|
30
|
-
export { APPROVE_ACTION_ID, DENY_ACTION_ID, APPROVAL_BLOCK_ID_PREFIX, buildApprovalMessage, buildApprovalAckMessage, parseSlackInteraction, } from "./approvals.js";
|
|
30
|
+
export { APPROVE_ACTION_ID, APPROVE_ALWAYS_ACTION_ID, DENY_ACTION_ID, APPROVAL_BLOCK_ID_PREFIX, buildApprovalMessage, buildApprovalAckMessage, parseSlackInteraction, } from "./approvals.js";
|
|
31
31
|
export class SlackAdapterError extends CrewhausError {
|
|
32
32
|
name = "SlackAdapterError";
|
|
33
33
|
constructor(message, cause) {
|
|
@@ -258,6 +258,8 @@ export function createSlackAdapter(config, opts = {}) {
|
|
|
258
258
|
const ack = buildApprovalAckMessage({
|
|
259
259
|
decision: args.decision,
|
|
260
260
|
by: args.by,
|
|
261
|
+
// #383 — an "Always allow" click acks as a standing approval.
|
|
262
|
+
...(args.interaction.always === true ? { always: true } : {}),
|
|
261
263
|
...(args.toolName !== undefined ? { toolName: args.toolName } : {}),
|
|
262
264
|
});
|
|
263
265
|
if (args.interaction.responseUrl !== undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/channel-adapter-slack",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Slack channel adapter: webhook signature verification, event parsing, reply-to-thread (Section 12)",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"test": "bun test src"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@crewhaus/errors": "0.5.
|
|
18
|
+
"@crewhaus/errors": "0.5.2"
|
|
19
19
|
},
|
|
20
20
|
"license": "Apache-2.0",
|
|
21
21
|
"author": {
|