mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* feat(telegram): V2 inbound — command router, actionable approve/reject keyboards, chat-gated poll loop * fix(release,x,video,telegram): terminal-state guards on approve/reject; sender-identity check * docs(map,rag): Telegram V2 inbound surfaces and terminal-state approve/reject guards --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
54 lines
5.0 KiB
Markdown
54 lines
5.0 KiB
Markdown
# X (Twitter) Engine
|
|
|
|
## What It Is
|
|
|
|
RoboCo can draft posts for the company's X (Twitter) account — release announcements and mention replies — implemented in `roboco/services/x_engine.py` (`XEngine`) and `roboco/services/x_post_service.py` (`XPostService`). Every draft is HELD for an explicit, per-post CEO approval; nothing is ever posted automatically. It mirrors the `ReleaseManagerEngine` "detect → originate a CEO-gated artifact → hold" shape.
|
|
|
|
## Enable/Disable
|
|
|
|
| Variable | Default | Effect |
|
|
|----------|---------|--------|
|
|
| `ROBOCO_X_ENGINE_ENABLED` | `false` | Master switch. Off = no draft is originated and no X API call is ever made — the release-post hook and the mentions poll are both no-ops. Panel-toggleable (Settings → Feature Flags). |
|
|
|
|
Even when enabled, the engine only drafts once stored credentials are present AND acts only through the CEO's explicit per-post approval — two independent gates beyond the flag.
|
|
|
|
## Two draft sources
|
|
|
|
**Release posts** (event-driven). `XEngine.draft_release_post(version, highlights)` is called from `ReleaseProposalService.approve()`'s publish-success branch — a release post is only ever drafted for a release that actually shipped. Dedup by version: a retry never drafts twice for the same `version`.
|
|
|
|
**Mention replies** (periodic poll). `XEngine.run_cycle()` fetches mentions via the X API, filters for "meaningful" ones (not a bare retweet, real text, and an engagement floor — `like + reply + retweet counts >= ROBOCO_X_MENTIONS_MIN_ENGAGEMENT`), and dedups against `x_seen_mentions` (migration `059`) so a mention is never turned into a second held reply.
|
|
|
|
Both are bounded by `ROBOCO_X_MAX_OPEN_POSTS` (rolling cap on concurrently-open held drafts, both sources combined) and the mentions poll additionally by `ROBOCO_X_MENTIONS_MAX_PER_CYCLE` (per-cycle origination cap).
|
|
|
|
## Drafting
|
|
|
|
Draft bodies are written by a **local-model** chat call (`_chat`, hitting `ROBOCO_LOCAL_LLM_BASE_URL` — never a cloud LLM in the hot path), in a fixed Head-of-Marketing voice prompt, then hard-clamped to 280 characters (`_clamp_tweet`). A local-model failure falls back to a plain template body rather than failing the draft outright. Drafting is **not** an agent spawn — no agent (including Head of Marketing) is spawned to write these; see `docs/rag/roles/head-marketing.md` for why the HoM's tool surface doesn't change.
|
|
|
|
## Ownership and the CEO gate
|
|
|
|
Every held draft is a real task: `team=main_pm`, `assigned_to=secretary-1`, `source` one of `X_POST_SOURCE` / `X_REPLY_SOURCE`, `confirmed_by_human=False` (HELD — skipped by every dispatcher, never delivered to an agent). The body lives on an `orchestration_markers` marker, editable up to the point of posting.
|
|
|
|
The CEO acts through panel-only REST, CEO-role-gated (`require_ceo_role`), never a gateway verb:
|
|
|
|
| Endpoint | Effect |
|
|
|----------|--------|
|
|
| `GET /api/x/posts` | List every held draft (both sources) awaiting decision. |
|
|
| `POST /api/x/posts/{task_id}/approve` | Post to X (optionally with an edited body, still 280-char clamped). Idempotent — approving an already-posted draft returns `already_posted` without a second API call. |
|
|
| `POST /api/x/posts/{task_id}/reject` | Cancel the draft with a reason. Terminal — a rejected draft is never posted: `approve` refuses a CANCELLED draft outright (returns `already_rejected` without calling the X API), so a stale approve can't resurrect it; a fresh one originates on the next cycle/release if still relevant. |
|
|
| `GET /api/x/credentials` | Whether all four OAuth secrets are stored (`has_credentials` boolean — never the secrets). |
|
|
| `POST /api/x/credentials` | Set (or, passing all four empty, clear) the four secrets. All-or-nothing — a partial set raises a validation error. |
|
|
|
|
Approval runs under a Redis single-flight lock (`roboco:x_post:{task_id}`, plain `SET NX`, 60s TTL) so a double-click can't double-post; the task is marked `COMPLETED` under the same lock before it releases.
|
|
|
|
## Credentials and signing
|
|
|
|
Credentials are entered in the **panel only** — never in `.env` or an agent-visible setting. The four OAuth 1.0a user-context secrets (`api_key`, `api_secret`, `access_token`, `access_token_secret`) are Fernet-encrypted (`ROBOCO_ENCRYPTION_KEY`) in the singleton `x_credentials` table (migration `059`); `get_decrypted()` is called only server-side, by `x_post_service` / `x_engine` — the API surface is write-only (`has_credentials` boolean, matching the `has_git_token` pattern for per-project git tokens).
|
|
|
|
Requests are signed with a **hand-rolled OAuth 1.0a HMAC-SHA1** signer (`roboco/services/x_client.py`, no new dependency) — no library does this signing for X's v2 API in the project's existing dependency set. Without credentials, `build_x_client` returns a `NullXClient` that never raises and never egresses, exactly like the research `NullProvider`.
|
|
|
|
## Related
|
|
|
|
- `docs/rag/architecture/config-reference.md` — full env var table
|
|
- `docs/rag/roles/head-marketing.md` — why the HoM's tool surface is unchanged
|
|
- `docs/rag/roles/ceo.md` — the CEO approval queues
|