docs(readme): expand sponsor section with B2B framing and tier ladder#50
Open
arunrajiah wants to merge 26 commits into
Open
docs(readme): expand sponsor section with B2B framing and tier ladder#50arunrajiah wants to merge 26 commits into
arunrajiah wants to merge 26 commits into
Conversation
feat(listing): rewrite index.html with inline styles and table layouts
This release closes the issues reported on r/Odoo (jeconti's audit) and is the recommended upgrade for everyone running 17.0.6.0.0 or earlier. 1. WhatsApp webhook now verifies Meta's X-Hub-Signature-256 (HMAC-SHA256 of the raw body, keyed with the App Secret). Without a configured secret OR a valid signature, the endpoint returns 403. Closes the impersonation hole that let any internet attacker who guessed the webhook URL act as any linked WhatsApp user. 2. Telegram webhook secret is now mandatory. The Register-webhook action auto-generates a 32-byte URL-safe secret if one isn't set and registers it with Telegram. The endpoint rejects any request whose X-Telegram-Bot-Api-Secret-Token header doesn't match. 3. Confirmation callbacks are bound to a per-write nonce. Each staged write generates a fresh secrets.token_urlsafe(12) stored on the session row; the Yes/No payload carries it as confirm:yes:<nonce> and is verified in constant time before execution. Defends against prompt-injection that tries to swap the staged tool between staging and the user clicking Yes. 4. Magic-link tokens moved to a dedicated odoopilot.link.token model that stores SHA-256 digests only and consumes the row atomically on use. Hourly cron garbage-collects expired entries. Re-issuing a token for the same chat invalidates the previous one. Migration: post-migration.py clears in-flight pending writes (pre-7.0 confirmations didn't carry a nonce) and removes legacy link-token system parameters. Operators must re-register the Telegram webhook once and paste their Meta App Secret into Settings. Other changes: - Tests: tests/test_security.py covers HMAC verify, nonce rotation, hashed token storage, and single-use consumption. - ACL: new odoopilot.link.token row in ir.model.access.csv. - Cron: ir_cron_gc_link_tokens runs hourly. - Listing/README: removed all "vs competitor" / pricing-comparison framing per project direction; replaced with a Security model section and feature-only descriptions. Version bumped 17.0.6.0.0 -> 17.0.7.0.0.
CI's ruff format --check step was failing on the 4 files added/modified in 17.0.7.0.0. No behavioural change -- pure formatting.
Establishes a coordinated-disclosure process via GitHub Security Advisories, defines supported versions per Odoo series, lists what is in/out of scope, and documents the addon's threat model in a single paragraph. Credits jeconti for the 2026-04-25 public audit. The motivation is to give future security researchers a private reporting path so production Odoo operators are not exposed to unpatched issues during disclosure.
Five additional findings surfaced after shipping 17.0.7.0.0: 1. Magic-link CSRF (High) — controller now splits into GET (renders a CSRF-protected confirmation form) and POST (consumes the token, does the link write). Cross-site image GETs no longer cause state changes. New `peek()` on the link.token model previews without deleting. 2. Identity hijack (High) — refuse to overwrite existing.user_id when it belongs to a different Odoo user, both at GET preview and POST commit. 3. Wildcard write-target hijack (High) — services/tools.py now has `preflight_write` that resolves the target before staging, stores res_id (not name) in pending_args, and renders the resolved display_name in the confirmation. Wildcard-only / overly-short names are rejected. CRM stage lookups are scoped to the lead's sales team. The five write executors accept resolved IDs. 4. Cost-amplification DoS (Medium) — new services/throttle.py with a sliding-window per-(channel, chat_id) rate limiter and a bounded thread pool; replaces unbounded daemon-thread spawn. Configurable via ir.config_parameter (rate_limit_per_hour, rate_limit_window_seconds, worker_pool_size). 5. Webhook delivery non-idempotency (Medium) — new odoopilot.delivery.seen model with UNIQUE(channel, external_id); the controller dedups on Telegram update_id and WhatsApp messages[].id before submitting work. Hourly cron GCs old rows. Regression tests added in tests/test_security.py for every fix.
…, hygiene Closes the four lower-impact findings from the post-17.0.7 internal review. None is independently exploitable — these are hardening / hygiene fixes that remove easy mistakes a future contributor could make. * Bot token scrub in services/telegram.py: requests exception strings often include the failing URL, which contains the bot token. _scrub redacts the token before any string is passed to the logger; _call now logs only the scrubbed message and the exception type. * WhatsApp verify_token compare: switched from == to hmac.compare_digest in controllers/main.py:whatsapp_verify. Verify tokens are low-value but the change is free. * Trust-boundary rename: the env parameter passed into the dispatch helpers and downstream _handle_* helpers was actually a SUPERUSER_ID Environment. Renamed to sudo_env throughout, and added docstrings to both dispatchers explaining the contract: sudo_env is for the unavoidable privileged lookups; business-data access must use sudo_env(user=identity.user_id.id). * Defensive else for malformed callback payloads in _handle_confirmation and _handle_whatsapp_confirmation. Behaviour was already correct (silent no-op) but now logs at WARNING and returns explicitly. Regression tests for the scrub and compare_digest semantics added in tests/test_security.py.
Inspecting the live page source revealed the App Store sanitiser does two things our previous design relied on: 1. Strips background and background-color from inline styles entirely. This made the dark hero (and white text inside it) invisible and killed the colour-fill cards across every section. 2. Rewrites <a href="...">CustomText</a> into <span href="...">CustomText </span>, which is non-clickable HTML. Every styled link in the listing was broken; the user reported this as "links are not working". Plain URL text is auto-linked by a separate pass that emits a fresh <a rel="nofollow"> which DOES survive the sanitiser. This rewrite: * Removes every background and background-color declaration. Visual hierarchy now comes from borders, text colour, and padding only. * Removes every white text colour. Default text is dark slate (#1e293b) on whatever background the App Store renders. * Replaces every styled CTA <a> with plain URL text. Sponsor button, GitHub button, resource links, footer credit -- all now render as auto-linked URLs with a leading label like "Get OdooPilot ->". * Rebuilds the demo conversation panes with text + coloured left borders instead of background-filled chat bubbles. * Replaces the dark teal Resources block and dark purple Sponsor block with bordered cards using accent colours on text and borders only. A header comment in the file documents the sanitiser's behaviour so future edits don't reintroduce the same problems. Verified zero non-ASCII bytes; all special characters as HTML entities.
The Settings -> OdooPilot page now shows a richer footer with explicit links for the things users actually want to do after install: * Sponsor on GitHub -> https://github.com/sponsors/arunrajiah * Feedback & ideas -> github.com/arunrajiah/odoopilot/discussions/new?category=ideas * Report a bug -> github.com/arunrajiah/odoopilot/issues/new/choose * Report a security issue -> github.com/arunrajiah/odoopilot/security/advisories/new Plus a thin row of quick-reference links to the source code, README, CHANGELOG and SECURITY.md. Each link is a real <a target="_blank" rel="noopener noreferrer"> - this is the in-Odoo settings view, not the App Store description, so the App Store sanitiser does not apply here. Native Odoo HTML rendering preserves both the styling and the click handler.
The pitch needed a sharper frame: this is for your internal team, not
your customers, and the killer use case is "your employees use Odoo
without logging in to Odoo." Every Odoo deployment has the same gap --
non-power-users avoid the desktop UI for routine tasks (apply for leave,
update the pipeline after a sales call, validate a stock transfer at
the dock), so data goes stale and approvals stall. OdooPilot meets
employees where they already are: their phone, in chat, in their language.
Changes across three surfaces, all telling the same story:
* odoopilot/static/description/index.html
- Hero rewritten: "Your team uses Odoo -- without logging in to Odoo"
is the headline. Clarifying note: "For your internal team. Not for
your customers."
- New "A day in the life" section with four concrete employee
scenarios (new hire applies for leave; manager approves; sales rep
updates pipeline from the field; warehouse picker validates transfer
at the dock).
- New "What OdooPilot is not" callout: not a customer chatbot, not
a permission bypass, not a public widget. Each linked user is an
Odoo user with audit-trail accountability.
- Personas reframed from role labels (Executives / Devs) to
employee-centric (Every employee / Managers on the move / Field &
warehouse / IT & developers).
- New "Odoo adoption problem -- solved" before/after comparison
table that names the friction directly.
* __manifest__.py
- Module name and summary lead with the new positioning. Long
description preamble explains the gap and the solution. Keywords
expanded with employee-self-service / mobile / approval / leave-
request terms.
* README.md
- Top section reframed to match. Demo conversation switched from a
generic task-query example to the leave-request / approval flow,
which is the canonical use case the new pitch leads with.
Bundles three already-shipped changes into a tagged release: * feat(settings): four-card community panel (Sponsor / Feedback / Bug / Security) at the bottom of Settings -> OdooPilot, with a quick- reference row for source / docs / changelog / security policy. * docs(listing): redesign for the App Store HTML sanitiser, which strips background declarations and rewrites <a> to <span href>. Listing now uses bordered cards (no fills) and plain URL text (the auto-linker emits clickable <a> that survive the sanitiser). * docs: reposition the pitch around the killer use case -- your team uses Odoo without logging in to Odoo. Updated listing, manifest and README to lead with this frame. No code changes; manifest version bumped from 17.0.9.0.0 to 17.0.10.0.0.
…ter) Three engineering hygiene items shipped together. No code paths changed; upgrade is optional. * Banner regenerated to match the new pitch. The marketplace banner now leads with "Your team uses Odoo - without logging in to Odoo." plus two phone notifications showing the leave request -> approval flow on WhatsApp + Telegram. Source HTML preserved at static/description/banner_source.html and rebuilt via the new scripts/render_banner.py (Playwright + chromium headless). * CI now runs bandit and semgrep on every push and PR. After three security releases in a week this is the right insurance - the next class of issue gets surfaced automatically rather than in a public audit. Both scanners run with continue-on-error: true while we tune the rule set; we'll tighten to a hard gate once the noise floor is known. Bandit: 0 medium/high. Semgrep p/python: 0 blocking. * New CI job runs scripts/check_listing_rendering.py against static/description/index.html and fails the build if any of the three patterns the App Store sanitiser breaks reappears: background declarations, white text, or styled <a> tags. The rules are documented in a header comment inside index.html; this linter enforces them so the listing cannot regress without somebody noticing.
* Add the regenerated banner image at the top so the GitHub landing page leads with the same headline as the marketplace listing. * Architecture diagram redrawn to show both Telegram and WhatsApp channels, the new throttle / dedup / preflight flow, and all five models that actually exist on disk (was Telegram-only and missed link.token + delivery.seen). * Quickstart rewritten for the current setup. Telegram secret is now auto-generated rather than user-supplied (was wrong); WhatsApp setup steps added with the mandatory App Secret called out; LLM provider table updated with the actual default models from services/llm.py (was claude-opus-4-5, llama3-70b-8192 -- neither is correct); optional throttling knobs documented. * /link section rewritten to describe the two-step CSRF-protected handshake added in 17.0.8. * Security section expanded to reflect every fix shipped since the public audit: preflight resolve-before-stage, CSRF protection, hijack defence, sudo_env trust boundary, rate limit + bounded pool, idempotency, token scrub, CI security scanning. Disclosure paragraph now points at GitHub Security Advisories and SECURITY.md instead of a bare email address. * Roadmap replaced with current status -- 5 most-recent shipped versions with one-line themes plus three planned items (Odoo 18, admin views, OCA submission). Was last updated at 17.0.7; we're now on 17.0.11. * LLM providers section deduplicated -- the table now lives in the Quickstart with the actual defaults; this section just notes the no-extra-dependencies design and the Ollama local option. * Sponsor section expanded into Sponsor & feedback with the four links that also appear in the in-Odoo Settings panel.
Replaces the bare list-and-form pair that shipped historically with a
proper admin dashboard. The post-install operator experience now matches
the production-ready security model.
Identity model
--------------
Three new live-computed fields on odoopilot.identity, populated from the
audit table with a 7-day sliding window:
* last_activity -- timestamp of the most recent audit row
* message_count_7d -- count of audit rows in the window
* success_rate_7d -- % that succeeded
Implementation uses two read_group calls per recordset (no N+1) and
compute_sudo=True so the system-only audit table reads through cleanly.
A new action_view_audit method on the identity returns an act_window
that opens the audit log filtered to (user_id, channel) for that
identity, used by both the form-header button and a stat-button.
Linked Users view (renamed from "User Identities")
--------------------------------------------------
* New columns: last_activity, message_count_7d, success_rate_7d
* Decoration: green when active in window; muted when never used or
inactive
* Search filters: Active in last 7 days, Linked but never used,
Telegram, WhatsApp, Inactive (unlinked)
* Group-by: User, Channel, Language
* Form view: smart-button stat tiles (msgs / success %), header
"View activity" button, web_ribbon for inactive identities
* Default search: active only
Audit Log view
--------------
* Decoration-danger for failures; inline error_message column so
trouble is visible without drilling into the form
* Search filters: Failures only, Successes only, Write actions, Read
actions, Telegram, WhatsApp, Today, Last 7 days
* Group-by: User, Tool, Channel, Outcome, Day
* Default open: Last 7 days, grouped by Day -- the most common
operator question is "what happened recently?"
* Form view: tool name as title, user/channel/timestamp as subtitle,
notebook splitting result and JSON args into separate tabs
Tests
-----
* New tests/test_admin_views.py with 7 tests covering: empty-state
behaviour, window cutoff at 7 days, success-rate rounding, channel
isolation, user isolation, action_view_audit payload.
The two existing CI scanners (bandit, semgrep) and the listing linter
remain green.
Companion to the new 18.0 branch (Alpha): * .github/workflows/ci.yml now triggers on push to *.0 branches so the full check set runs on the 18.0 branch (and any future *.0 branches) on direct push. * SECURITY.md supported-versions table updated: 18.0 listed as Alpha preview, 17.0 stays Supported. * CHANGELOG header notes the branch split (17.0.x ships from 17.0, 18.0.x ships from 18.0). No code changes. Operators on 17 don't need to do anything.
A motivated user on a linked Telegram or WhatsApp chat could previously persuade the bot to: * Disclose its system prompt, tool definitions or conversation history * Ignore its rules and roleplay as something else (DAN, dev mode, etc.) * Burn the operator's LLM API budget writing Python code or telling jokes Two layers of defence ship in this release. services/scope_guard.py - pre-LLM regex filter ---------------------------------------------- Runs on every inbound user message before the LLM is called. Catches the obvious extraction / jailbreak / off-topic patterns and short- circuits with a fixed refusal: > "I'm OdooPilot - I can only help with your Odoo data and actions > (tasks, leaves, sales, CRM, inventory, etc.). For anything else, > please use a different tool." Patterns target prompt extraction, tool enumeration, memory dumps, classic 'ignore previous instructions' jailbreaks, role hijacks (act as / you are now / DAN / developer mode), delimiter injection (<system>, <|im_start|>), code generation, creative content and general knowledge questions like 'what's the weather'. The filter is intentionally narrow: false positives on legitimate Odoo questions defeat the product. 22 representative employee queries are pinned in tests as MUST-pass-through; 32 attack strings are pinned as MUST-block. Patterns were tuned against this corpus until both directions reach 100%. Operators can disable via Settings -> Technical -> System Parameters by setting odoopilot.scope_guard_enabled = False. On by default. Hardened SYSTEM_PROMPT in services/agent.py ------------------------------------------- The prompt is the second line of defence and now explicitly spells out what the bot does NOT do, the one-sentence refusal format, and the trust boundary (only this system message contains instructions; user messages / tool results / Odoo record content are untrusted data). Audit visibility ---------------- Blocked attempts write an odoopilot.audit row with tool_name='scope_guard_block', success=False, and the matching pattern's reason in error_message. Operators can filter the Audit Log by Failures or by tool name to see attempted abuse. Tests ----- tests/test_scope_guard.py with 7 test classes: * TestLegitimateOdooQueriesPassThrough -- 22 queries * TestPromptExtractionBlocked * TestMemoryContextExtractionBlocked * TestClassicJailbreaksBlocked * TestDelimiterInjectionBlocked * TestOffTopicComputeBlocked (code-gen / creative / off-topic) * TestEmptyAndEdgeCases CI green: ruff format + check, bandit (0 medium/high), semgrep (0 blocking), listing renderable, all XML well-formed.
Six new tools widen the bot's audience to anyone in the company who needs a one-tap-from-chat workflow. Tool count: 13 -> 19. Read (1) * find_partner -- contact lookup by name / email / phone substring in a single OR-domain search. Write (5, all flow through preflight + nonce + audit pipeline) * clock_in / clock_out -- hr.attendance for field staff and shift workers. Preflight rejects double-clock-in and clock-out-while- not-clocked-in. Execute path re-checks at run time. * submit_expense -- hr.expense draft creation. Stays in draft on purpose: auto-submitting from chat would skip a deliberate human checkpoint in the Expenses module. * submit_timesheet -- account.analytic.line entry against a project and optional task. Hours validated to [0, 24]. Resolved display_name in the confirmation, not the LLM's argument string. * create_calendar_event -- calendar.event with the linked user as organizer. Accepts YYYY-MM-DD HH:MM or full ISO; the LLM converts relative phrases like "tomorrow at 10am" before calling. All wired into TOOL_DEFINITIONS, WRITE_TOOLS, the execute_tool dispatch map, preflight_write, and the _fmt_confirmation fallback. Tests ----- tests/test_employee_tools.py with 5 test classes: * TestToolRegistryHygiene -- catches drift between TOOL_DEFINITIONS, WRITE_TOOLS, and the dispatch map (the four-way registry that would otherwise let the LLM call a tool that crashes at execute time). * TestFindPartner -- finds by name / email / phone; empty / no-match return friendly messages. * TestSubmitExpensePreflight -- short description, zero / negative / non-numeric amount. * TestSubmitTimesheetPreflight -- zero hours, > 24 hours, short project name. * TestCreateCalendarEventPreflight -- short name, missing start, malformed datetime, negative duration; accepts valid input. * TestClockInPreflight -- friendly error when hr.attendance is not installed. README roadmap updated to list shipped 17.0.12 / 17.0.13 / 18.0.1 / 18.0.2 and the upcoming voice-messages workstream. CI green: ruff format + check, bandit (0 medium/high), semgrep (0 blocking), listing renderable, all XML well-formed.
…next sprint After 13 releases in 9 days the threat model has evolved -- scope guard, 6 new tools, admin views, throttle, dedup table all shipped since the last audit. Adding 'internal security audit' as the explicit gate before voice work begins, and elevating voice from a parenthetical to the next named sprint. No code change; README only.
The fourth security audit since the public Reddit one. Two High
findings, two Medium, one hygiene item. None is independently
exploitable today against a default-configured install; closing them
as defence-in-depth before the upcoming voice-messages work expands
the attack surface.
High - Scope guard hardened against Unicode and foreign-language bypasses
-------------------------------------------------------------------------
The pre-LLM regex filter was ASCII-English only, so Cyrillic
homoglyphs ("sуstem prompt"), zero-width-padded keywords, fullwidth
Latin ("Write Python"), and any non-English jailbreak (FR / ES / DE
/ PT / AR) bypassed it. The SYSTEM_PROMPT second-line defence
already refused these, but every bypass cost an LLM call.
Now: input runs through scope_guard._normalise (NFKC + strip
zero-width / bidi-override + Cyrillic+Greek homoglyph map) before
matching. Plus 16 new patterns for the top-5 jailbreaks in five
additional languages.
22 representative legit queries still pass, 32 original attacks still
block, plus 22 new bypass cases now block. Module docstring rewritten
to be honest about what's defended ("best-effort cost-saving filter")
and what isn't (multi-message split, Base64, novel languages).
Trojan Source compliance: invisible-character constants are now
declared by Unicode codepoint (str.maketrans({0x200B: None, ...}))
rather than as raw chars in source -- the file no longer carries the
chars it filters. Bandit B613 clean.
High - submit_expense / submit_timesheet rebind employee_id to env.uid
----------------------------------------------------------------------
The two new write tools shipped in 17.0.14 trusted whatever
employee_id was in the staged args. Today the only writer of those
args is the agent loop after preflight_write (which pins env.uid's
own employee), but a future code path that stages writes with a
different shape would let the executor write to another employee's
record. Defence-in-depth: re-resolve env.uid's hr.employee at execute
time, ignore any mismatched staged value, log a WARNING.
Same pattern as mark_task_done's existing ownership re-check.
Medium - find_partner limit capped at 25
----------------------------------------
The LLM could pass any limit value; record rules filter what the user
can read but a chat-mediated address-book scrape was one
find_partner(name='%', limit=999999) call away. New constant
_FIND_PARTNER_MAX_LIMIT = 25 caps the value.
Medium - RateLimiter._buckets opportunistic GC
----------------------------------------------
Dict was pruning timestamps within a bucket but never deleting empty
buckets, so it grew by one entry per unique (channel, chat_id) ever
seen. Bounded in practice by the number of real linked chats but a
slow leak under churn. New: opportunistic sweep every 256 calls drops
keys whose bucket is empty after pruning.
Hygiene - assert -> RuntimeError
--------------------------------
assert _limiter is not None / assert _pool is not None would be
optimised out under python -O. Replaced with explicit if-raise
branches.
Tests
-----
* test_scope_guard.py: TestUnicodeBypasses (3 cases),
TestForeignLanguageJailbreaks (16 cases across FR/ES/DE/PT/AR)
* test_employee_tools.py: TestFindPartnerLimitCap (huge / negative /
non-numeric limit handled), TestEmployeeIdRebinding (spoofed
employee_id ignored on submit_expense and submit_timesheet)
* test_security.py: TestRateLimiterBucketGC (sweep shrinks the dict
after a churn batch)
CI green: ruff format + check, bandit (0 medium/high), semgrep
(0 blocking), listing renderable, all XML well-formed.
Operators ask "will this handle my team?" -- this section answers it concretely. Three deliverables: * A by-team-size config table (20 / 100 / 300 / 1000 / 5000+ employees) with the specific values to set for worker_pool_size, Odoo --workers, and the LLM provider tier needed. * An honest framing of where the bottleneck actually sits: not OdooPilot, not Odoo, but the LLM provider's rate limit and price. * Provider-by-provider rate-limit table at the tiers most teams actually use (Groq free / OpenAI Tier 1 / Anthropic Tier 2-4 / Ollama). Plus a "watch for" sublist (peak-hour 3-4x average, multi-worker throttle fairness, LLM cost monitoring) and a 5-step self-test recipe to run before the first real user logs in. No code change. No version bump.
Update README + SECURITY.md across all branches to reflect that the 18.0 series is now live at apps.odoo.com/apps/modules/18.0/odoopilot. Specifically: * README install section: link both Odoo 17 and Odoo 18 listings separately (was 17 only). * README status table: 18.0.5.0.0 Beta on App Store (was 18.0.2.0.0 Alpha, GitHub only). * README roadmap: 'Validate Odoo 18 install' moved from open to done. OCA submission promoted to next operator-side item. * SECURITY.md supported-versions table: 18.0 Alpha (preview) -> 18.0 Supported, with App Store URLs in a new column. Mirror of the same docs that ship in 18.0.5.0.0 on the 18 branch. No code change on this branch.
…loop The biggest UX upgrade left for the on-the-go-employee persona. Warehouse pickers, drivers, anyone whose hands aren't free to type can now talk to the bot. Voice notes flow through the same agent loop, scope guard, per-write nonce + audit pipeline as typed text. The only thing different is one extra step at the front of the pipe. How it works ------------ 1. User holds-to-record a voice note in Telegram or WhatsApp. 2. Webhook downloads the audio (Telegram getFile + the file/bot URL, or WhatsApp graph.facebook.com/<media-id> two-step). 3. Audio is transcribed via the configured STT provider's Whisper-compatible /audio/transcriptions endpoint. 4. Transcript fed into OdooPilotAgent.handle_message as if typed. Provider matrix --------------- * groq (default) -- whisper-large-v3, free tier with generous limits. * openai -- whisper-1, ~$0.006/min audio. Anthropic doesn't ship STT; Ollama can run whisper.cpp locally but that's out of scope for v1. Configuration (opt-in) ---------------------- Settings -> OdooPilot -> Voice messages. Five new fields: * odoopilot.voice_enabled -- master flag * odoopilot.stt_provider -- groq / openai * odoopilot.stt_api_key -- separate from llm_api_key * odoopilot.stt_model -- optional override * odoopilot.voice_max_duration_seconds -- default 60s Voice is OFF by default. Auto-deriving from the LLM config would silently route audio to a third party an Anthropic/Ollama operator didn't pick. Failure modes (all surfaced to the user, never silent drops) ----------------------------------------------------------- * Voice not configured -> "Voice messages are not enabled..." * Audio over duration cap -> "Voice message too long..." (cap checked from platform-reported duration BEFORE download) * Download fails or oversized (cap 25 MB) -> "Sorry, I couldn't download that voice message..." * Empty / silent / unintelligible transcript -> "I couldn't make out any words..." * STT 5xx / quota -> "Sorry, I couldn't transcribe that voice message right now..." (key scrubbed from logs) Security properties carried over -------------------------------- * Scope guard runs on the transcript, not the audio bytes -- so "ignore previous instructions" spoken or typed is the same. * API key scrub mirrors the bot-token scrub in services/telegram.py. * No new authorization surface: the linked user is still resolved from chat_id; voice cannot bypass record-rule scoping. Files ----- * services/stt.py (new) -- STTClient w/ groq + openai backends * services/telegram.py -- download_voice(file_id) w/ size cap, MIME inference * services/whatsapp.py -- download_media(media_id), two-step Meta Graph API audio fetch * controllers/main.py -- _stt_client_or_none, _voice_too_long helpers; _transcribe_telegram_voice / _transcribe_whatsapp_voice controller methods; both dispatchers now route audio messages through the new path * models/res_config_settings.py -- five new fields * views/res_config_settings_views.xml -- new "Voice messages" setting box, conditional on master flag * tests/test_voice.py (new) -- 5 test classes, 14 cases: STTClient construction, input validation, key scrubbing, duration cap helper, _stt_client_or_none config gate CI green: ruff format + check, bandit (0 medium/high), semgrep (0 blocking), listing renderable, all XML well-formed.
Voice support shipped in 17.0.16.0.0 / 18.0.6.0.0 but the only place it appeared in user-facing docs was a one-line bullet in README's 'What it does' section. Operators browsing the App Store listing, the manifest summary, or the architecture diagram had no way to discover the feature. Fixing that here. App Store listing (static/description/index.html) ------------------------------------------------- * Hero badges: new "🎙 Voice messages" pill alongside the existing channel / language badges. * Hero subtitle: changed "by chatting with a bot" to "by typing or speaking to a bot" -- one-word edit, big positioning lift. * New dedicated "Voice messages -- speak instead of typing" section between the day-in-the-life and why-OdooPilot-wins blocks. Three bordered cards (who it's for / how it works / what it costs) plus a security-properties callout that pins "scope guard runs on the transcript, not the audio bytes". Manifest description -------------------- * New keyword group: "Odoo voice messages, voice-to-Odoo, speak to Odoo, Whisper Odoo, Odoo STT" (text-search hits on the App Store). * New Key Features bullet between Dual-channel and Multi-language, describing voice support, the provider matrix, and the duration cap. README ------ * Architecture diagram redrawn to show the STT path: text messages go straight to the agent; voice messages route through the new STT Client box first, then converge into the same agent loop. * New "Voice messages (optional)" section in Quickstart's Configuration block, mirroring the in-Odoo settings UI. * Throttling-knobs table now includes odoopilot.voice_max_duration_seconds. * Sizing & capacity section adds a "Voice messages -- capacity notes" subsection: STT cost per provider, the two safeguards (duration cap + 25 MB file cap), and a worked example (300 employees with 10% voice adoption costs ~$1/day extra on OpenAI, $0 on Groq). * Per-chat rate limit description corrected: "voice + text combined." No code change. No version bump. The App Store will pick up the new listing on its next refresh cycle (typically 24h).
No runtime behaviour change. Brings the codebase up to OCA-quality
standards so the actual upstream PR is mostly mechanical when it
lands. Tracked in the new CONTRIBUTING-OCA.md.
Module structure
----------------
* odoopilot/readme/ rewritten as 8 reStructuredText files per OCA's
oca-gen-addon-readme template: DESCRIPTION, INSTALL, CONFIGURE,
USAGE, ROADMAP, CONTRIBUTORS, CREDITS, MAINTAINERS. Previous
Markdown files removed.
* New CONTRIBUTING-OCA.md at repo root documenting what's done and
what's pending for the actual PR.
Manifest
--------
* New maintainers: ["arunrajiah"] field (OCA-required).
* author updated to "arunrajiah, Odoo Community Association (OCA)"
per OCA convention (preempts pylint-odoo C8101).
* Removed redundant installable: True / auto_install: False (both
at default values; flagged by C8116).
* description field kept with a # noqa: C8103 comment -- it is
deprecated per OCA but the Odoo App Store search still indexes it.
Code style (pylint-odoo: 9.90 -> 10.00/10)
------------------------------------------
* W8161 prefer-env-translation: all _() calls in
res_config_settings.py rewritten as self.env._(...).
* W8301 translation-not-lazy: % arg interpolation in translation
calls switched to lazy positional args: self.env._("...", arg).
* W8303 translation-fstring-interpolation: two f-strings inside _()
calls rewritten with lazy substitution.
* W8113 attribute-string-redundant: 6 redundant string=... field
parameters dropped.
* W8138 except-pass: services/agent.py:_audit now logs at WARNING
with exc_info=True instead of bare pass.
CI green: ruff format + check, pylint-odoo 10.00/10, bandit (0 M/H),
semgrep (0 blocking), listing renderable, all XML well-formed.
…users A chatbot icon appears in the Odoo systray when the operator enables it in Settings. Click -> a panel opens inside the Odoo UI with the conversation history and an input field. Same agent loop, same scope guard, same per-write nonce, same audit log as Telegram and WhatsApp. Why a third channel ------------------- Telegram + WhatsApp cover the off-desk audience -- field sales, warehouse, drivers. But desk users (sales reps doing pipeline, accountants doing reconciliation, ops users running POs) live in the Odoo browser tab all day and don't want to switch to a phone app for a quick "who owns this lead?" query. This widget puts the bot a click away in the same window. How it works ------------ * Backend: services/web_chat.py exposes a WebChatClient -- a buffer matching the same surface as TelegramClient. The agent loop runs unchanged; messages accumulate as JSON envelopes which the HTTP route returns as the response body. * Routes: /odoopilot/web/config (master flag for the frontend) and /odoopilot/web/message (POST a message, get back the buffered reply). Both auth="user", json. Identity is request.env.user -- no /link flow, no spoofable chat_id. * Frontend: a small OWL component registered in the systray category. ~150 LOC of JS, a QWeb template, and an SCSS file scoped under .o-odoopilot-systray. Component renders nothing until /config says enabled=true. Messages render as t-esc never t-raw -- a malicious tool result cannot inject HTML into the page. * Confirmation flow: Yes / No buttons inline in the panel; clicks POST 'confirm:yes:<nonce>' to the same endpoint, routed by _handle_web_confirmation through the same nonce-verification logic the Telegram / WhatsApp paths use. Configuration ------------- Single boolean field: odoopilot.web_chat_enabled. Off by default -- operators who deployed Telegram / WhatsApp only might not want a second surface, and turning it off stops the systray icon from rendering on the next page reload. Same per-(channel, chat_id) rate limit applies; web messages share the budget with Telegram and WhatsApp messages from the same Odoo user (channel='web', chat_id=str(user.id)). What's NOT supported on web --------------------------- Voice messages and file uploads. Streaming replies. All three are on the messaging channels (Telegram / WhatsApp); the web channel is text-only sync POST/response by design -- websockets would need Odoo's longpolling worker which is a separate operator deployment. Tests ----- 11 cases in tests/test_web_chat.py: * WebChatClient buffer matches TelegramClient surface, calls accumulate as JSON-serialisable envelopes, reply_markup kwarg silently ignored, answer_callback_query is a safe noop. * _handle_web_confirmation cancels on 'no', rejects bad nonces, executes on 'yes' with correct nonce (mocked execute_confirmed), replies 'nothing to confirm' when no pending, no-ops on malformed payloads. * Web sessions don't collide with Telegram / WhatsApp sessions even when chat_id values overlap -- channel-keyed isolation still works. CI green: ruff format + check, pylint-odoo 10.00/10, bandit (0 M/H), semgrep (0 blocking), listing renderable, all XML well-formed (including the new QWeb template). Docs updated: README "What it does" + new "In-Odoo web chat widget" config block, App Store listing badge, readme/DESCRIPTION.rst, readme/CONFIGURE.rst, readme/USAGE.rst.
…ywhere No code-path behaviour change. Adds the support contact across every operator-facing surface: * __manifest__.py: new 'support' field. The Odoo App Store renders this as a 'Contact' link in the right-hand sidebar of the module detail page. * README.md: new top-level Support section + 'General support' line in the Sponsor & Feedback list. * App Store listing (static/description/index.html): the right-hand Resources card gains a bold 'Email support' line and is renamed 'Community & support'. * Settings community panel: new 5th card with mailto: link pre-filling 'OdooPilot support' as the subject. * readme/MAINTAINERS.rst: lists the email + routes public questions to GitHub Issues / Discussions and security disclosures to GitHub Security Advisories. Verified along with this change: * App Store cache state: 17 listing at 17.0.15.0.0 (3 versions behind), 18 listing at 18.0.6.0.0 (2 versions behind). Both catching up; Voice + Web Chat features will land in the next refresh cycle. * The App Store's per-module detail page does NOT display banner.png (only the icon). The banner ships with the addon and is used for category browse / grid views; nothing wrong on our side.
Adds the audit-pattern sponsor pitch on top of the existing links section: a procurement anchor (a $50/mo team sponsorship is roughly the cost of a single Odoo Enterprise seat), four concrete roadmap items sponsorship currently funds (write-tool coverage in Invoices/Inventory/Purchase, OCA submission, Redis-backed rate limiter, faster issue turnaround), and a three-tier ladder ($10/$50/$200). The existing Sponsor/Feedback/Bug/Security/Email links are preserved. Docs-only change. No code, dependencies, or behavior touched.
5558289 to
3b0119e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the audit-pattern sponsor pitch on top of the existing links section. The new section adds a procurement anchor (a $50/mo team sponsorship is roughly the cost of a single Odoo Enterprise seat), four concrete roadmap items sponsorship currently funds (write-tool coverage in Invoices/Inventory/Purchase, OCA submission, Redis-backed rate limiter, faster issue turnaround), and a three-tier ladder ($10/$50/$200). The existing Sponsor/Feedback/Bug/Security/Email links are preserved.
Docs-only change. No code, dependencies, or behavior touched.