Skip to content

[ON HOLD — Gate-routed redesign pending] feat(odoo): live Odoo CRM auto-discovery for /api/v1/scan - #213

Closed
cryptoxdog wants to merge 5 commits into
mainfrom
claude/odoo-crm-auto-discovery-ajgzxh
Closed

cryptoxdog wants to merge 5 commits into
mainfrom
claude/odoo-crm-auto-discovery-ajgzxh

Conversation

@cryptoxdog

@cryptoxdog cryptoxdog commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

EIE can now scan live Odoo Contacts (res.partner) and CRM (crm.lead) fields directly via POST /api/v1/scan with "source": "odoo", removing the need for a human to export field lists — the smallest read-only Odoo 19 JSON-2 adapter feeding the existing scan_crm_fields().

Type of change

  • Feature
  • Bug fix
  • Refactor
  • Tooling / CI
  • Documentation
  • AI-generated (review required)

What changed

Scanner provenance (app/services/crm_field_scanner.py)

  • CRMField and FieldMapping gain optional source_system / source_resource.
  • Provenance propagates to matched and unmapped mappings and is exposed additively in scan_result_to_dict; missing (domain-owned) entries carry none.
  • scan_hash now hashes (source_system, source_resource, name, field_type) so res.partner.phone and crm.lead.phone contribute independently. Legacy CRMField(name=...) stays deterministic and compatible.

Odoo adapter (app/services/crm_source.py, app/services/odoo_crm_source.py)

  • CRMSource protocol + single CRMSourceError; one implementation, no registry/factory.
  • OdooCRMSource inspects res.partner and crm.lead independently and both mandatorily: one fields_get + one bounded search_read per model = 4 requests per scan.
  • Technical field names are identity (labels are not); duplicate names across models are preserved, never renamed or deduplicated.
  • Sampling: default 25 / hard max 100 records per model (enforced in code), domain=[], no pagination, no relation traversal, binary/one2many/reference excluded, ≤5 distinct values per field, sample fill rate (None when no rows). Boolean False and numeric 0 count as populated.
  • Transport: httpx.AsyncClient, 10s timeout, Authorization: bearer <key>, optional X-Odoo-Database, User-Agent versioned from package metadata, fixed model/method allowlist (arbitrary RPC impossible; no mutating methods exist). Errors name provider/model/method/status only — never credentials or response bodies.

API (app/api/v1/discover.py)

  • ScanRequest: exactly one of fields[] (manual, unchanged) or source: "odoo"; sample_limit 1..100 (422 otherwise).
  • Both paths converge on the single existing scanner. Missing ODOO_URL/ODOO_API_KEY → 503 (only when the Odoo source is requested; EIE startup unaffected). Odoo upstream failure, including crm.lead inaccessible after res.partner succeeded → 502, never a partial or empty success.

Config / docs

  • New ODOO_API_KEY setting (app/core/config.py), documented in .env.example and docs/contracts/config/env-contract.yaml. Username/password remain for legacy XML-RPC paths.

Checklist

  • ruff check passes
  • ruff format applied
  • mypy passes on all touched files (repo-wide mypy app has 40 pre-existing errors in 22 untouched files)
  • Tests added / updated for changed behaviour (tests/test_odoo_crm_source.py new; tests/test_crm_field_scanner.py, tests/test_discover_scan_endpoints.py, tests/contracts/test_config_env_contract.py extended)
  • pytest passes locally — 1663 passed, 1 skipped (opt-in real-Odoo test, gated on EIE_TEST_ODOO_URL / EIE_TEST_ODOO_API_KEY), 4 xfailed
  • .env.example updated if new env vars added
  • CHANGELOG.md updated (not touched — outside the development pack's file budget)

AI-generated code notice

  • Reviewed for prompt injection vectors in enrichment inputs — adapter only reads Odoo metadata/samples; no LLM input path added
  • Signal schema validation verified — ScanRequest XOR validator + bounded sample_limit; fields_get/search_read response shapes validated
  • No hardcoded secrets or credentials — tests use test-api-key against respx mocks only
  • Path traversal safety checked if file I/O added — no file I/O added

Scope attestation

  • Odoo repository (IB-Odoo_19) modified: NO
  • Gate modified: NO
  • CEG modified: NO
  • Ranking / matching logic modified: NO
  • Domain compiler modified: NO

Pre-existing findings (not addressed here, outside the pack's scope)

  • tests/test_pr21_packet_router.py:17 — ruff F401 unused TransportPacket import (fails ruff check . on main too).
  • tools/verify_contracts.py — SHA256 mismatch for app/services/gate_client.py (pinned hash predates Migrate EIE to Gate-only egress; retire direct peer transport #203).
  • tests/services/test_gate_registration.py — imports start_reregistration_loop, which no longer exists in app/main.py (collection error).
  • /api/v1/scan resolves domains from converge._domain_specs, which startup configures empty (development pack §79 "PRE-EXISTING DOMAIN REGISTRY GAP"); tests inject the domain spec via the existing seam.

Related issues

🤖 Generated with Claude Code

https://claude.ai/code/session_01BZDbPoPruZTHGhRt7tkFCm

Add optional source_system / source_resource to CRMField and FieldMapping so
fields with identical technical names from different resources (Odoo
res.partner.phone vs crm.lead.phone) stay distinguishable through the scanner.
Provenance propagates to matched and unmapped mappings, is exposed additively
in scan_result_to_dict, and participates in scan_hash. Legacy CRMField(name=...)
callers are unchanged; missing entries carry no provenance.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZDbPoPruZTHGhRt7tkFCm
Add the smallest read-only CRM source seam (CRMSource protocol, CRMSourceError)
and one Odoo implementation. OdooCRMSource inspects res.partner and crm.lead
independently and mandatorily: one fields_get plus one bounded search_read per
model (4 requests total), converting live metadata into CRMField with
source_resource provenance. Sampling is bounded (default 25, hard max 100),
follows no relations, excludes binary/one2many/reference, keeps at most 5
distinct values per field, and reports a sample fill rate (None when no rows).
Boolean False is populated; numeric 0 is populated.

Transport is httpx with a 10s timeout, bearer API-key auth, optional
X-Odoo-Database, and a fixed model/method allowlist so arbitrary Odoo RPC is
impossible. Errors name provider/model/method/status only, never credentials
or response bodies. New setting ODOO_API_KEY (documented in .env.example and
env-contract.yaml); username/password remain for legacy XML-RPC paths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZDbPoPruZTHGhRt7tkFCm
POST /api/v1/scan now accepts exactly one of fields[] (manual, unchanged) or
source="odoo" (live discovery), plus sample_limit (1..100, 422 otherwise).
Both intake paths converge on the single existing scan_crm_fields(). Missing
ODOO_URL/ODOO_API_KEY yields 503 only when the Odoo source is requested;
upstream Odoo failures (including crm.lead inaccessible after res.partner
succeeded) yield 502 rather than a partial or empty success.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZDbPoPruZTHGhRt7tkFCm
Recursive Alignment kernel finding: the adapter hard-coded "2.3.0", duplicating
the version owned by pyproject.toml. Read it via importlib.metadata with an
explicit "unknown" fallback so the version string has a single owner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZDbPoPruZTHGhRt7tkFCm
Copilot AI lite review requested due to automatic review settings September 19, 2026 21:30
@github-actions

github-actions Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

PR Size Report

Metric Value Limit
Lines changed 1225 1000
Files changed 11 50
Additions +1209 -
Deletions -16 -

BLOCKED: PR exceeds 1000 lines changed

Best Practices for Large Changes

  1. Refactoring + Features: Separate into 2 PRs
  2. Multiple Features: One PR per feature
  3. Database + Code: Separate migration from logic
  4. Generated Code: Separate from manual changes

This PR is blocked from merging until size limits are met.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-19T21:34:16.114009Z 2338430 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown

L9 Audit Review

Status

  • Result: blocking
  • Mode: explicit_advisory_blocking_tier_policy
  • Total findings visible: 1220
  • Blocking findings: 1
  • Advisory findings: 1219

Policy

  • Flatcase/style findings are advisory.
  • SQL/auth/security/chassis/transport/ingress/contract/runtime findings are blocking when new or touched.
  • Unknown HIGH/CRITICAL findings fail closed as blocking.

Blocking Findings

  • app/services/odoo_crm_source.py:16 Potential hardcoded API key/secret — new_high_or_critical

Advisory Findings

  • app/agents/deal_risk.py:36 Likely flatcase field 'recommendations' — advisory
  • app/score/score_explainer.py:54 Likely flatcase field 'recommendation' — advisory
  • app/score/score_explainer.py:103 Likely flatcase field 'recommendations' — advisory
  • app/score/score_models.py:96 Likely flatcase field 'recommendation' — advisory
  • app/services/crm/salesforce_client.py:189 f-string SQL query -- injection risk — advisory
  • app/bootstrap/l9_contract_runtime.py:32 FastAPI import in engine module -- chassis isolation violation — advisory
  • app/core/auth.py:34 FastAPI import in engine module -- chassis isolation violation — advisory
  • app/core/auth.py:35 FastAPI import in engine module -- chassis isolation violation — advisory
  • app/score/score_api.py:40 FastAPI import in engine module -- chassis isolation violation — advisory
  • app/middleware/rate_limiter.py:28 FastAPI import in engine module -- chassis isolation violation — advisory
  • app/middleware/rate_limiter.py:29 FastAPI import in engine module -- chassis isolation violation — advisory
  • app/services/enrichment/sources/llm_base.py:25 Deep relative import (level=3) — advisory
  • app/services/enrichment/sources/perplexity_adapter.py:21 Deep relative import (level=3) — advisory
  • app/services/enrichment/sources/perplexity_adapter.py:22 Deep relative import (level=3) — advisory
  • app/services/enrichment/sources/openai_adapter.py:20 Deep relative import (level=3) — advisory
  • app/services/enrichment/sources/anthropic_adapter.py:20 Deep relative import (level=3) — advisory
  • app/api/v1/converge.py:21 Deep relative import (level=3) — advisory
  • app/api/v1/converge.py:22 Deep relative import (level=3) — advisory
  • app/api/v1/converge.py:23 Deep relative import (level=3) — advisory
  • app/api/v1/converge.py:24 Deep relative import (level=3) — advisory
  • app/api/v1/converge.py:25 Deep relative import (level=3) — advisory
  • app/api/v1/converge.py:26 Deep relative import (level=3) — advisory
  • app/api/v1/converge.py:27 Deep relative import (level=3) — advisory
  • app/api/v1/converge.py:28 Deep relative import (level=3) — advisory
  • app/api/v1/converge.py:34 Deep relative import (level=3) — advisory

... truncated 1194 additional findings ...


Generated by L9 Audit Engine (tools/audit_engine.py)

@cryptoxdog cryptoxdog changed the title feat(crm): preserve CRM source provenance in field scans feat(odoo): live Odoo CRM auto-discovery for /api/v1/scan (res.partner + crm.lead via JSON-2) Sep 19, 2026
@pytest.fixture
def odoo_configured(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(converge_mod, "_domain_specs", {"plasticos": PLASTICOS_SPEC})
monkeypatch.setattr(discover_mod, "get_settings", lambda: _odoo_settings())
class CRMSource(Protocol):
"""Smallest useful source contract: produce the live ``CRMField`` list."""

async def fields(self, *, sample_limit: int = DEFAULT_SAMPLE_LIMIT) -> list[CRMField]: ...

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The OpenAPI contract pack (docs/contracts/api/openapi.yaml) is not updated to reflect the new /api/v1/scan request XOR shape and 502/503 responses, leaving contracts out of sync with the implementation.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds a live, read-only Odoo 19 “JSON-2” adapter so POST /api/v1/scan can auto-discover CRM fields (from res.partner and crm.lead) and feed them into the existing scan_crm_fields() pipeline, while extending scan output to carry per-field provenance.

Changes:

  • Introduces CRMSource boundary + OdooCRMSource implementation to fetch fields_get metadata and bounded search_read samples for both Odoo models.
  • Extends scanner models/results with optional source_system / source_resource, and updates scan hashing to distinguish identical field names across different source resources.
  • Extends /api/v1/scan to support XOR input modes: manual fields[] or live source="odoo" with bounded sample_limit, plus config/docs/tests for ODOO_API_KEY.
File Description
app/​api/​v1/​discover.py Adds XOR request validation and Odoo-backed intake path for /api/v1/scan, including 502/503 handling.
app/​services/​crm_field_scanner.py Adds provenance to mappings/results and includes provenance in scan_hash identity.
app/​services/​crm_source.py Defines CRMSource protocol and CRMSourceError, plus sampling bounds constants.
app/​services/​odoo_crm_source.py Implements live Odoo JSON-2 discovery + bounded sampling for res.partner and crm.lead.
app/​core/​config.py Adds odoo_api_key setting for the new discovery path.
.env.example Documents ODOO_API_KEY for live scan configuration.
docs/​contracts/​config/​env-contract.yaml Adds ODOO_API_KEY to the env contract.
tests/​contracts/​test_config_env_contract.py Extends env-contract assertions to include ODOO_API_KEY.
tests/​test_crm_field_scanner.py Adds unit tests for provenance propagation + scan hash behavior.
tests/​test_discover_scan_endpoints.py Adds end-to-end API tests for source="odoo" and error handling paths.
tests/​test_odoo_crm_source.py New hermetic adapter tests (respx-mocked) + opt-in real Odoo integration test.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread app/api/v1/discover.py
Comment on lines 52 to +65
class ScanRequest(BaseModel):
fields: list[CRMFieldInput]
"""CRM scan request. Exactly one of ``fields`` (manual) or ``source`` (live) is required."""

fields: list[CRMFieldInput] | None = None
source: Literal["odoo"] | None = None
sample_limit: int = Field(default=DEFAULT_SAMPLE_LIMIT, ge=1, le=MAX_SAMPLE_LIMIT)
domain: str
tenant_id: str

@model_validator(mode="after")
def require_fields_xor_source(self) -> ScanRequest:
if (self.fields is None) == (self.source is None):
raise ValueError("exactly one of 'fields' or 'source' must be supplied")
return self

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2338430947

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +135 to +136
for model in self.RESOURCES:
collected.extend(await self._fetch_resource(client, model, sample_limit))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Count unique domain matches in live coverage

When both Odoo resources expose common fields such as name and phone, this loop preserves both mappings but the scanner still computes coverage as len(matched) / total_domain_properties. The included live-scan fixture therefore produces six matches for four domain properties and returns coverage_ratio=1.5, which can become a 150% discovery-report coverage value; preserve both provenance-bearing mappings but calculate coverage from unique matched domain keys.

Useful? React with 👍 / 👎.

Comment thread app/api/v1/discover.py
Comment on lines +55 to +57
fields: list[CRMFieldInput] | None = None
source: Literal["odoo"] | None = None
sample_limit: int = Field(default=DEFAULT_SAMPLE_LIMIT, ge=1, le=MAX_SAMPLE_LIMIT)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Publish the live-source request in OpenAPI

For consumers generated or validated against the repository's canonical OpenAPI document, the new request is unusable: docs/contracts/api/openapi.yaml still requires fields, omits source and sample_limit, and sets additionalProperties: false. Such clients reject the exact {..., "source": "odoo"} request now accepted by this model, so update and version the API contract alongside this public interface change.

Useful? React with 👍 / 👎.

Comment thread app/core/config.py
Comment on lines +45 to +47
# Odoo 19 JSON-2 bearer API key — authoritative for live CRM field discovery
# (POST /api/v1/scan with source="odoo"). Username/password stay for legacy XML-RPC.
odoo_api_key: str = ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge CONTRACT C-09 VIOLATION — Prefix the Odoo API key

CONTRACT C-09 VIOLATION — All environment variables must use the L9_ prefix unless explicitly approved as infrastructure-standard. The newly introduced odoo_api_key setting is loaded as ODOO_API_KEY, which has no documented exception; rename it and its .env.example and env-contract references to an L9_-prefixed name.

AGENTS.md reference: AGENTS.md:L173-L173

Useful? React with 👍 / 👎.

@cryptoxdog cryptoxdog changed the title feat(odoo): live Odoo CRM auto-discovery for /api/v1/scan (res.partner + crm.lead via JSON-2) [ON HOLD — Gate-routed redesign pending] feat(odoo): live Odoo CRM auto-discovery for /api/v1/scan Sep 19, 2026
@cryptoxdog
cryptoxdog marked this pull request as draft September 19, 2026 21:56

Copy link
Copy Markdown
Collaborator Author

Status: ON HOLD (draft) — operator decision 2026-09-19.

This PR implements the development pack as written: EIE reads Odoo's JSON-2 API directly (OdooCRMSource, ODOO_URL + ODOO_API_KEY). The operator has ruled that this conflicts with the Gate-only egress law: EIE must never address the Odoo node directly. Odoo CRM schema discovery (res.partner / crm.lead fields_get + bounded search_read) must instead route through Gate as a TransportPacket action, with the read executed on the Odoo node side.

What stays valid from this PR (transport-neutral):

  • Scanner provenance (CRMField / FieldMapping source_system / source_resource, scan_hash, serialization) — commit f701e40.
  • The CRMSource protocol + CRMSourceError seam and the /api/v1/scan fields XOR source request shape.

What is superseded:

  • app/services/odoo_crm_source.py (direct JSON-2 HTTP) and the ODOO_API_KEY configuration path.

A Gate-routed development pack (Gate action + plasticos_gate handler + EIE Gate-backed source) is being compiled; this PR will be reduced or closed per that pack. Do not merge as-is.


Generated by Claude Code

With source provenance, two resources can both match one domain property
(res.partner.phone and crm.lead.phone). Both mappings survive, but coverage
must count the domain property once; the previous len(matched) numerator
could exceed 1.0 (6 mappings / 4 properties = 1.5). Use the unique matched
domain keys already tracked for the missing computation. Regression test added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZDbPoPruZTHGhRt7tkFCm

Copy link
Copy Markdown
Collaborator Author

CI + review triage on head 24f6cfc (PR remains ON HOLD / draft per the Gate-routing decision above).

Pushed in this round

  • 24f6cfc fixes the Codex P1 on coverage: with provenance, two resources matching one domain property inflated coverage_ratio above 1.0. Coverage now counts unique matched domain keys; both provenance-bearing mappings are kept. Regression test test_coverage_counts_unique_domain_properties added. This is in the transport-neutral scanner code that survives the redesign.

Failing checks that are not this PR's (each reproduces on main at the same commit-independent location; none touch this diff):

Check Root cause Status
Lint (Ruff + Mypy), Lint and Type Check tests/test_pr21_packet_router.py:17 F401 unused TransportPacket import pre-existing on main
Test Suite, Baseline Ratchet / Required Tests, Ratchet Verdict tests/services/test_gate_registration.py:215 imports start_reregistration_loop, removed from app/main.py in #203 → collection error pre-existing on main; coverage itself passed at 75.23%
Architecture Compliance / Compliance Gate tools/verify_contracts.py SHA256 mismatch for app/services/gate_client.py (pin predates #203) pre-existing on main
Semgrep Policy Check app/services/gate_client.py:67 semgrep.float-requires-try-except pre-existing on main, file untouched
Security Scanning pip-audit: nltk 3.10.3 PYSEC-2026-3740, no fix version yet dependency, unrelated

No fix PR exists yet for any of these that I have read; several open PRs (#204–#210, #212) may already carry some of them, so I am not porting fixes here to avoid duplicating that work. All five are logged to L9 memory for downstream pickup. I have no permission to re-run jobs from this surface.

Failing checks that are this PR's

  • L9 Audit Review app/services/odoo_crm_source.py:16 "potential hardcoded API key": false positive on a docstring that mentions bearer API-key auth. No fix pushed because that file is superseded by the Gate-routed rework and will be removed.
  • PR Size Report (1209 lines > 1000): the rework will ship as 2–3 PRs under the limit.

Review findings

  • Codex P1 CONTRACT C-09 (ODOO_API_KEY lacks L9_ prefix): correct; superseded — the Gate-routed design removes the EIE-side Odoo credential entirely. Any new EIE env var in the rework will be L9_-prefixed.
  • Copilot / Codex P2 (OpenAPI ScanRequest out of sync with the fields XOR source shape and 502/503): correct; carried into the rework, which updates docs/contracts/api/openapi.yaml alongside the request model.
  • github-code-quality crm_source.py:40 (Protocol method body ...) and the test lambda nit: cosmetic; ride the rework's next push to these files rather than a standalone push on a held PR.

Generated by Claude Code

@sonarqubecloud

Copy link
Copy Markdown

Copy link
Copy Markdown
Collaborator Author

Closing — superseded. Per the operator's Gate-only egress decision, the direct Odoo JSON-2 adapter in this PR is retired. The transport-neutral scanner provenance slice (commits f701e40 and 24f6cfc) has been republished from main as #214 (PR-A of the crm-schema-scan development pack). The Gate-routed replacement (Odoo → Gate → EIE crm-schema-scan action) follows as separate PRs in Constellation.Gate, this repo, and IB-Odoo_19.


Generated by Claude Code

@cryptoxdog cryptoxdog closed this Sep 19, 2026
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.

3 participants