Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions cmd/helios/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,18 +130,16 @@ func main() {
"username_set", cfg.XUsername != "",
)
xFeedRunner := &xfeed.Runner{
Enabled: cfg.XFeedEnabled,
SkillDir: cfg.XFeedSkillDir,
IncludeAttackerCA: cfg.XFeedIncludeAttackerCA,
CardEnabled: cfg.XFeedCardEnabled,
CardPythonBin: cfg.XFeedCardPythonBin,
CardTimeout: time.Duration(cfg.XFeedCardTimeoutSeconds) * time.Second,
Mentions: mentionIndex,
Enabled: cfg.XFeedEnabled,
SkillDir: cfg.XFeedSkillDir,
CardEnabled: cfg.XFeedCardEnabled,
CardPythonBin: cfg.XFeedCardPythonBin,
CardTimeout: time.Duration(cfg.XFeedCardTimeoutSeconds) * time.Second,
Mentions: mentionIndex,
}
logger.Info("x feed runner configured",
"enabled", xFeedRunner.Configured(),
"skill_dir", cfg.XFeedSkillDir,
"include_attacker_ca", cfg.XFeedIncludeAttackerCA,
"card_enabled", cfg.XFeedCardEnabled,
"card_python_bin", cfg.XFeedCardPythonBin,
"telegram_publish_enabled", cfg.TelegramPublishEnabled,
Expand Down
4 changes: 3 additions & 1 deletion deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,10 +391,12 @@ HELIOS_X_FEED_CARD_TIMEOUT_SECONDS=20
TELEGRAM_PUBLISH_ENABLED=true
```

The flow is GitHub publish -> x-feed draft -> X main post/reply -> Telegram
The flow is GitHub publish -> x-feed draft -> X rich main post/Tx-only reply -> Telegram
publish. Telegram gets the same main X body with `GitHub:` and `X:` links
appended. When the card switch is enabled, Backlight also generates a public-safe
exploit-flow card from the x-feed brief and attaches the PNG to the main X post.
Before any live media upload or post, Backlight verifies that the refreshed OAuth
token belongs to `X_ACCOUNT_USERNAME` and aborts on a mismatch.
Install `requirements-x-feed.txt` in the configured Python environment to produce
the PNG, for example a dedicated venv under `/srv/backlight/data/x-feed-venv`
with `HELIOS_X_FEED_CARD_PYTHON_BIN=/srv/backlight/data/x-feed-venv/bin/python`.
Expand Down
1 change: 0 additions & 1 deletion deploy/env/backlight.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ GH_TOKEN=
# X_DRY_RUN=true
# HELIOS_X_FEED_ENABLED=false
# HELIOS_X_FEED_SKILL_DIR=skills/x-feed
# HELIOS_X_FEED_INCLUDE_ATTACKER_CA=false
# HELIOS_X_FEED_CARD_ENABLED=true
# HELIOS_X_FEED_CARD_PYTHON_BIN=python3
# HELIOS_X_FEED_CARD_TIMEOUT_SECONDS=20
Expand Down
12 changes: 7 additions & 5 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,10 @@ precedence; `.env.local` can override `.env`.
| `X_REFRESH_TOKEN_FILE` | no | empty | optional 0600 JSON/raw-token file used before `X_REFRESH_TOKEN` and updated when X rotates the refresh token |
| `X_POST_TEMPLATE_PATH` | no | embedded default | optional Markdown `text/template` file for the X verified-incident post body |
| `X_API_BASE` | no | `https://api.x.com` | override for tests or proxies |
| `X_ACCOUNT_USERNAME` | no | empty | optional username used to build the public `post_url`; omit to use `https://x.com/i/web/status/{id}` |
| `X_ACCOUNT_USERNAME` | recommended when X publish enabled and dry-run false | empty | expected publishing username; Backlight verifies `/2/users/me` matches it before media upload or posting and uses it to build the public `post_url` |
| `X_DRY_RUN` | no | `true` | when true, records the generated X post text as `x_publish` without calling X |
| `HELIOS_X_FEED_ENABLED` | no | `false` | enables the structured x-feed draft path after successful GitHub publish |
| `HELIOS_X_FEED_SKILL_DIR` | no | `skills/x-feed` | vendored x-feed skill directory containing `incident-post-format.md` |
| `HELIOS_X_FEED_INCLUDE_ATTACKER_CA` | no | `false` | opt-in switch for attacker CA details in the X reply |
| `HELIOS_X_FEED_CARD_ENABLED` | no | `true` | runs the vendored `exploit-flow-card` renderer during x-feed draft generation |
| `HELIOS_X_FEED_CARD_PYTHON_BIN` | no | `python3` | Python executable used for the exploit-flow-card renderer |
| `HELIOS_X_FEED_CARD_TIMEOUT_SECONDS` | no | `20` | timeout for the exploit-flow-card renderer; failures are recorded but do not block text publishing |
Expand Down Expand Up @@ -161,19 +160,22 @@ lumoskit child process per ADR-0018 in the `lumoskit` repo.
Backlight can persist rotated refresh tokens with `0600` permissions. SQLite
events record only `refresh_returned` / `refresh_token_updated`; token
material is never stored in case events.
- **X account preflight.** Set `X_ACCOUNT_USERNAME=BackwardLabs` in live mode.
After token refresh and before any media upload or post, Backlight calls
`/2/users/me` and aborts if the authenticated username does not match.
- **x-feed publish sequence.** When `HELIOS_X_FEED_ENABLED=true`, Backlight waits
for GitHub artifact publish to succeed, drafts `x-feed-main-post.txt`,
`x-feed-reply-post.txt`, `x-feed-telegram-post.txt`, and `x-feed-status.json`,
then publishes the X main post and reply. `ready_to_publish=false` blocks X
then publishes the rich X main post and a Tx-only reply. `ready_to_publish=false` blocks X
posting when required public URLs or the vendored format file are missing.
- **Exploit-flow card generation.** With `HELIOS_X_FEED_CARD_ENABLED=true`,
Backlight writes a public-safe `x-feed-card-brief.md`, runs the vendored
`skills/exploit-flow-card/scripts/render_card.py`, and records SVG/PNG paths in
`x-feed-status.json`. The renderer always attempts SVG output; PNG output
requires `requirements-x-feed.txt` installed in
`HELIOS_X_FEED_CARD_PYTHON_BIN`'s environment.
- **X media upload.** If the x-feed draft supplies `image_path`, Backlight uploads
it through X API v2 media upload before creating the main post, then attaches
- **X media upload.** If the x-feed draft supplies `image_path`, Backlight sends
it as a base64 JSON payload to X API v2 media upload before creating the main post, then attaches
the returned media id to `POST /2/tweets`. The exploit-flow card PNG becomes
that `image_path` when rendering succeeds. If card rendering fails or only SVG
is produced, Backlight records `card_error` and still publishes the text thread
Expand Down
2 changes: 0 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ type Config struct {
XDryRun bool
XFeedEnabled bool
XFeedSkillDir string
XFeedIncludeAttackerCA bool
XFeedCardEnabled bool
XFeedCardPythonBin string
XFeedCardTimeoutSeconds int
Expand Down Expand Up @@ -107,7 +106,6 @@ func Load() (*Config, error) {
XDryRun: envBool("X_DRY_RUN", true),
XFeedEnabled: envBool("HELIOS_X_FEED_ENABLED", false),
XFeedSkillDir: envDefault("HELIOS_X_FEED_SKILL_DIR", "skills/x-feed"),
XFeedIncludeAttackerCA: envBool("HELIOS_X_FEED_INCLUDE_ATTACKER_CA", false),
XFeedCardEnabled: envBool("HELIOS_X_FEED_CARD_ENABLED", true),
XFeedCardPythonBin: envDefault("HELIOS_X_FEED_CARD_PYTHON_BIN", "python3"),
XFeedCardTimeoutSeconds: envInt("HELIOS_X_FEED_CARD_TIMEOUT_SECONDS", 20),
Expand Down
20 changes: 18 additions & 2 deletions internal/worker/worker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@ func TestWorkerXFeedPublishesXThreadThenTelegram(t *testing.T) {
"expires_in": 7200,
"token_type": "bearer",
})
case "/2/users/me":
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]string{"id": "account-id", "username": "BackwardLabs"},
})
case "/2/tweets":
xPostCount++
var body map[string]any
Expand All @@ -287,14 +291,26 @@ func TestWorkerXFeedPublishesXThreadThenTelegram(t *testing.T) {
switch xPostCount {
case 1:
text := body["text"].(string)
if !strings.Contains(text, "[Backlight Initial Analysis]") || !strings.Contains(text, "Need more detail? Check our repo and analysis thread below ↓ 🧵") {
for _, want := range []string{
"🚨 yETH — Under review",
"Key info",
"TL;DR",
"Why it matters",
"Builder takeaway",
} {
if !strings.Contains(text, want) {
t.Fatalf("main X body missing %q: %q", want, text)
}
}
if strings.Contains(text, "[Backlight") || strings.Contains(text, "Tx: ") {
t.Fatalf("main X body = %q", text)
}
_ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]string{"id": "main-id", "text": text}})
case 2:
text := body["text"].(string)
reply, ok := body["reply"].(map[string]any)
if !ok || reply["in_reply_to_tweet_id"] != "main-id" || !strings.Contains(text, "1/ Artifacts + analysis 🧾") {
want := "Tx: https://etherscan.io/tx/0x" + strings.Repeat("8", 64)
if !ok || reply["in_reply_to_tweet_id"] != "main-id" || text != want {
t.Fatalf("reply X body = %#v", body)
}
_ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]string{"id": "reply-id", "text": text}})
Expand Down
Loading
Loading