Skip to content

Run the CLI from Slack over Socket Mode, behind an admission gate - #23

Merged
Shashankss1205 merged 1 commit into
mainfrom
feat/slack-socket-gateway
Jul 30, 2026
Merged

Run the CLI from Slack over Socket Mode, behind an admission gate#23
Shashankss1205 merged 1 commit into
mainfrom
feat/slack-socket-gateway

Conversation

@Shashankss1205

Copy link
Copy Markdown
Collaborator

A Slack front door for the grapharc CLI, hostable on a laptop: python -m grapharc.slack holds one outbound Socket Mode WebSocket, so there is no public URL, no open port, no reverse proxy. /grapharc metrics t.jsonl r1 (or an @mention) from any Slack client runs the command on the host and posts the piped-mode bytes back in a code fence — the CLI's stable form, not a third output dialect.

The gate

Anyone in the workspace can talk to the bot, so what text may become an argv is an admission decision, in the repo's own idiom:

  • Subcommands are allowlisted: demo run plan models replay diff trace metrics viz. agent (arbitrary tool execution on the host) and serve are not in the list.
  • Flags are allowlisted per subcommand; --registry (imports an arbitrary module), --config, --json, --no-color are unreachable from Slack.
  • Every path — positional or flag value, either --flag value or --flag=value form — must resolve inside the bot's working directory, refused before any process spawns.
  • --model/--reviewer-model are refused unless the operator sets GRAPHARC_SLACK_ALLOW_MODEL=1 in the shell that starts the bot. Default spend from Slack: zero — every reachable command runs the scripted path.
  • Argv only, never a shell string; timeout (default 120s) kills a runaway command and says so.

Modularity

command.py   the gate: Slack text -> argv, or a refusal that says why   (stdlib)
runner.py    sys.executable -m grapharc.cli.main, cwd = the confined dir (stdlib)
format.py    exit-code semantics (0/1/2) -> one Slack message, announced truncation, fence-escape defused (stdlib)
bot.py       the only file that touches slack-bolt, imported lazily
config.py    env only — deliberately NOT the .env upward search (issue #20)
__main__.py  python -m grapharc.slack; exit 2 + stderr when the env is not runnable

A module entry rather than a grapharc slack subcommand, because the parser help is printed verbatim in README.md and byte-compared by the suite; a daemon also doesn't belong in a parser whose every other command terminates.

Packaging and docs

  • New slack extra (slack-bolt>=1.20), joined to [all] so the CI wheel-check imports the module with the dependency present; every module except bot.py imports without it.
  • docs/cookbook/07-slack.md: Slack app manifest (scopes commands, app_mentions:read, chat:write), token setup, run instructions, the allow/refuse table, and the honest caveats (bot dies with the laptop lid; the workspace is the trust boundary).
  • README extras list gains one line.

Testing

tests/test_slack_gateway.py — 21 tests, no token, no network: the gate's allow/deny/confinement decisions, an end-to-end run of the real CLI through the runner+formatter (success, exit-2, and the timeout kill), announced truncation, fence-escape defusal, config validation, and the missing-extra install hint (import failure faked, not skipped).

Full suite green locally, ruff check . clean.

🤖 Generated with Claude Code

`python -m grapharc.slack` holds one outbound WebSocket to Slack, so a
laptop behind NAT is a viable host: no public URL, no open port. A
workspace member types `/grapharc metrics t.jsonl r1` or mentions the
bot, and the piped-mode bytes come back in a code fence — the CLI's
stable form, not a third dialect.

Anyone in the workspace can talk to the bot, so what Slack text may
become an argv is an admission decision: subcommands are allowlisted
(`agent` and `serve` are not in the list), flags are allowlisted per
subcommand (`--registry`, `--config`, `--json` are not reachable),
every path must resolve inside the bot's working directory, and
`--model` is refused unless the operator opts in — the default answer
to "can Slack cost me money?" is no.

Only `bot.py` touches slack-bolt, lazily, so the wheel imports without
the new `slack` extra; the gate, runner and formatter are stdlib-only
and tested without a token or a network in tests/test_slack_gateway.py.
A module entry rather than a `grapharc slack` subcommand, because the
parser's help is printed verbatim in README.md and byte-compared.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Shashankss1205
Shashankss1205 merged commit 723a177 into main Jul 30, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant