@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.
@@ -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 two buttons — the interaction parser keys on
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 approval. Each button
39
- * carries the `approvalId` in its `value`, and the actions block's `block_id`
40
- * carries it too (`crewhaus_approval:<id>`), so the interaction parser can
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 two buttons — the interaction parser keys on
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 approval. Each button
25
- * carries the `approvalId` in its `value`, and the actions block's `block_id`
26
- * carries it too (`crewhaus_approval:<id>`), so the interaction parser can
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 verb = args.decision === "grant" ? "Approved" : "Denied";
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 text = `${icon} ${verb}${tool} by ${args.by}`;
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 decision = action["action_id"] === APPROVE_ACTION_ID
134
+ const actionId = action["action_id"];
135
+ const decision = actionId === APPROVE_ACTION_ID || actionId === APPROVE_ALWAYS_ACTION_ID
121
136
  ? "grant"
122
- : action["action_id"] === DENY_ACTION_ID
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.1",
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.1"
18
+ "@crewhaus/errors": "0.5.2"
19
19
  },
20
20
  "license": "Apache-2.0",
21
21
  "author": {