@xmarts/genius-setup 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmarts/genius-setup",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "One-command, self-updating onboarding for Xmarts Genius (Brain MCP + per-turn injection + automatic session/time capture + presence heartbeat for honest time-tracking 'amarre' + plan/committee→Brain enforcement + CLAUDE.md protocol + MCP fan-out: provisions the consultant's granted MCP servers into Claude Code & Claude Desktop). Secret-free onboarding via single-use setup-token exchange. Installs once; self-updates daily so new tools/skills/features arrive with zero action.",
5
5
  "bin": {
6
6
  "genius-setup": "bin/genius-setup.js"
@@ -67,6 +67,7 @@ argument shape or hit an edge case.
67
67
  | Deploy / install / upgrade a **module** | `manage_module`, `deploy_module` | ✅ — see [manage-client-modules.md](references/manage-client-modules.md) |
68
68
  | Inspect a model's schema | `get_model_schema`, `list_models` | ✅ |
69
69
  | Read / search a client's source code | `code_read`, `code_search`, `search_webapp_code` | ✅ |
70
+ | Give a **consultant a UI login** on a client | `execute_orm` (create `res.users`) | ✅ — see [provision-consultant-ui-access.md](references/provision-consultant-ui-access.md) |
70
71
  | Snapshot / restore / export the DB | `create_snapshot`, `restore_snapshot`, `export_database` | ✅ |
71
72
  | Restart Odoo | `trigger_restart` | ✅ |
72
73
  | Build a chart/dashboard | `create_echart` | ✅ *(same infra caveat as webapps)* |
@@ -77,6 +78,19 @@ argument shape or hit an edge case.
77
78
  33 tools are routable to clients in total. If a tool you need isn't listed, check
78
79
  [references/tool-catalog.md](references/tool-catalog.md) before concluding it's impossible.
79
80
 
81
+ ## Consultant needs to log into a client's UI → hand them credentials NOW
82
+
83
+ If a consultant says any of *"necesito/quiero acceso a la UI de \<cliente\>"*, *"no puedo entrar
84
+ a la instancia de \<cliente\>"*, *"how do I log into \<client\>"*, or *"olvidé mi contraseña de
85
+ \<cliente\>"* — **provision it and give back URL + user + password on the spot. No waiting, no
86
+ admin hop.** Every consultant has `odoo.write odoo.execute` on their **assigned** clients, so this
87
+ runs in their own session via `target_client`. It is one idempotent `execute_orm` (creates on first
88
+ ask, resets the temp password on re-ask). The **exact one-shot snippet + how to hand back the
89
+ credential without leaking it to the Brain** is in
90
+ [references/provision-consultant-ui-access.md](references/provision-consultant-ui-access.md) — read
91
+ it and act. The only thing that can block you is the client not being in their assignments (then an
92
+ admin assigns it); everything else is self-serve.
93
+
80
94
  ## Always orient before you act
81
95
 
82
96
  The Brain (`query_knowledge`) is the server-side source of truth and it remembers things this
@@ -0,0 +1,169 @@
1
+ # Provisioning a consultant a UI login on a client Odoo
2
+
3
+ **The need (recurring):** a Genius consultant must log into a *client's* Odoo **UI**
4
+ (`https://<client>/web/login`) to work visually — test screens, configure Studio, click
5
+ through flows. Genius manages consultants centrally (`xma.consultant` on the master), but
6
+ each client is a **separate Odoo database**; a master identity is **not** a login on the
7
+ client. There is no SSO federation — the `xma_connect` gateway is HMAC machine-to-machine,
8
+ not user identity. So the consultant needs a real `res.users` **in that client instance**.
9
+
10
+ **Do NOT share the gateway service account** (`xma_genius@<client>`) or any other shared
11
+ login. It has only `base.group_user` (headless), sharing a password kills the audit trail,
12
+ and a plaintext password in a Claude session can be indexed by the Brain. Give each
13
+ consultant their **own** audited user.
14
+
15
+ ## ⚡ Frictionless trigger — do this the moment a consultant asks
16
+
17
+ **Trigger phrases** (any consultant, any session): *"necesito/quiero acceso a la UI de \<cliente\>"*,
18
+ *"how do I log into \<client\>'s Odoo"*, *"no puedo entrar a la instancia de \<cliente\>"*,
19
+ *"olvidé mi contraseña de \<cliente\>"*. → **Provision immediately and hand back URL + user +
20
+ password. Do NOT make them wait or ask an admin.** Every consultant has `odoo.write odoo.execute`
21
+ on their **assigned** clients, so this runs in their own session through `target_client`.
22
+
23
+ **One shot (idempotent — creates on first ask, resets the temp password on re-ask):** run via
24
+ `execute_orm` with `target_client=<client name or UUID>`. Resolve the requesting person's real
25
+ name + xmarts.com email first (from `get_my_profile`, or the consultant record on the master).
26
+
27
+ ```python
28
+ U = env['res.users'].sudo()
29
+ NAME, LOGIN = '<Full Name>', '<person@xmarts.com>' # the consultant who needs access
30
+ env.cr.execute("SELECT replace(gen_random_uuid()::text, '-', '')")
31
+ raw = env.cr.fetchone()[0]
32
+ pw = 'Xma-%s-%s-Ux!' % (raw[:6], raw[6:12]) # temp, no spaces, meets complexity
33
+ user = U.search([('login', '=', LOGIN)], limit=1)
34
+ if user:
35
+ user.write({'password': pw, 'active': True}) # re-ask / forgot password -> fresh temp pw
36
+ status = 'reset'
37
+ else:
38
+ # Clone the RICHEST non-admin consultant peer (best working profile, never over-grant
39
+ # Settings-admin). Filter service accounts / admin IN PYTHON — the gateway's anti-SQL
40
+ # guard mangles `like`/`%` domains (a `not like 'xma_%'` term silently returns nothing).
41
+ # Plain loop — execute_orm forbids lambda closures. Fall back to the Sales baseline if
42
+ # the client has no usable peer yet.
43
+ peers = U.search([('share', '=', False), ('active', '=', True), ('id', '!=', 1)])
44
+ sysg = env.ref('base.group_system', raise_if_not_found=False)
45
+ peer = U.browse(); best = -1
46
+ for p in peers:
47
+ lg = p.login or ''
48
+ if lg == 'admin' or lg.startswith('xma_'):
49
+ continue # skip the admin + service accounts
50
+ if sysg and sysg.id in p.group_ids.ids:
51
+ continue # never clone an admin's groups
52
+ n = len(p.group_ids)
53
+ if n > best:
54
+ best = n; peer = p
55
+ gids = peer.group_ids.ids if peer else []
56
+ if not gids:
57
+ base = env.ref('base.group_user', raise_if_not_found=False)
58
+ sales = env.ref('sales_team.group_sale_salesman', raise_if_not_found=False)
59
+ gids = [g.id for g in (base, sales) if g]
60
+ comp = (peer.company_id.id if peer else False) or env.company.id
61
+ user = U.with_context(no_reset_password=True, mail_create_nosubscribe=True).create({
62
+ 'name': NAME, 'login': LOGIN, 'email': LOGIN, 'password': pw,
63
+ 'company_id': comp, 'company_ids': [Command.set([comp])],
64
+ 'group_ids': [Command.set(gids)]})
65
+ status = 'created'
66
+ result = {'status': status, 'uid': user.id, 'login': user.login, 'password': pw,
67
+ 'url': env['ir.config_parameter'].sudo().get_param('web.base.url') + '/web/login'}
68
+ ```
69
+
70
+ Then **hand it back immediately**, formatting the secret so the capture hook redacts it from the
71
+ Brain but the consultant still sees it live:
72
+
73
+ ```
74
+ url: <result.url>
75
+ login: <result.login>
76
+ password: <result.password> ← temp, change it on first login (Preferences → Account Security)
77
+ ```
78
+
79
+ Only assigned clients are reachable (`target_client` refuses unassigned). If the client is NOT in
80
+ their assignments, tell them to ask an admin to assign it — that is the only gate. Nothing else
81
+ about this should require a human hop.
82
+
83
+ ## The pattern (scalable, sustainable, auditable)
84
+
85
+ **Per-consultant internal user in the client instance, provisioned on demand through the
86
+ Genius gateway, cloning an existing consultant's group profile, with a temp password the
87
+ consultant changes on first login.**
88
+
89
+ - **Auditable** — real per-person `res.users`; `create_uid`/`create_date`/login identify who.
90
+ - **Consistent** — clone the groups of a consultant who already works that client (no
91
+ over/under-grant guesswork). If none exists, use the client's Sales-user baseline
92
+ (`base.group_user` + the Sales "All Documents" group).
93
+ - **Central + repeatable** — one `execute_orm` through `target_client`; no per-client manual admin.
94
+ - **Least-shared-secret** — temp password, changed on first login; deep "modify anything"
95
+ work happens through the consultant's Claude Code + gateway (full ORM/code), NOT the UI login.
96
+
97
+ ### UI login ≠ programmatic access — two distinct rails
98
+ 1. **UI login** (this doc) — for *visual* work. Modest groups (mirror an existing consultant).
99
+ 2. **Programmatic** (Claude Code + `beesmart_genius` MCP + `target_client`) — for reading
100
+ code, editing modules/webapps, running ORM. This already runs with full gateway access, so
101
+ a consultant rarely needs broad UI groups. Grant the UI the minimum for visual work; escalate
102
+ one group at a time only when a specific task in the UI is blocked.
103
+
104
+ ## Runbook (≈1 minute)
105
+
106
+ Resolve the client UUID with `genius_capability_map` (its payload lists every client), then:
107
+
108
+ **1. Probe the client:** confirm reachable, the consultant has no login yet, whether SMTP
109
+ exists (usually not → no email invite → temp password), and an existing consultant to clone.
110
+ ```python
111
+ U = env['res.users'].sudo()
112
+ base_url = env['ir.config_parameter'].sudo().get_param('web.base.url')
113
+ internal = [{'id': u.id, 'login': u.login, 'name': u.name}
114
+ for u in U.search([('share','=',False),('active','=',True)])]
115
+ exists = U.search([('login','=','<consultant.email>')]).ids
116
+ result = {'base_url': base_url, 'internal': internal, 'already_there': exists}
117
+ ```
118
+ Pick the `source` user to clone (an existing consultant on this client). Odoo 19 uses
119
+ `group_ids` (NOT `groups_id`) for direct groups on `res.users`.
120
+
121
+ **2. Create the user** (clone groups + server-side temp password; suppress the reset-email
122
+ attempt that fails with no SMTP):
123
+ ```python
124
+ U = env['res.users'].sudo()
125
+ LOGIN = '<consultant.email>' # match their xmarts.com email for consistency
126
+ if U.search([('login','=',LOGIN)]):
127
+ result = {'status': 'exists'}
128
+ else:
129
+ env.cr.execute("SELECT replace(gen_random_uuid()::text,'-','')")
130
+ raw = env.cr.fetchone()[0]
131
+ pw = 'Xma-%s-%s-Ux!' % (raw[:6], raw[6:12]) # no spaces, meets complexity
132
+ src = U.browse(<source_uid>) # an existing consultant on this client
133
+ comp = src.company_id.id or env.company.id
134
+ user = U.with_context(no_reset_password=True, mail_create_nosubscribe=True).create({
135
+ 'name': '<Consultant Name>', 'login': LOGIN, 'email': LOGIN, 'password': pw,
136
+ 'company_id': comp, 'company_ids': [Command.set([comp])],
137
+ 'group_ids': [Command.set(src.group_ids.ids)],
138
+ })
139
+ result = {'status':'created','uid':user.id,'password':pw,'groups':user.group_ids.ids}
140
+ ```
141
+
142
+ **3. Verify** the hash persisted (login will work) — never read/expose the hash value:
143
+ ```python
144
+ env.cr.execute("SELECT (password IS NOT NULL AND length(password)>20) FROM res_users WHERE id=<uid>")
145
+ result = {'password_ok': bool(env.cr.fetchone()[0])}
146
+ ```
147
+
148
+ **4. Deliver securely.** URL (`<base_url>/web/login`) + login are non-secret → inline is fine.
149
+ The password is a secret: the session-capture hook redacts `password: <value>` (English
150
+ keyword + value) both client- and server-side, so present it in **that exact form** — the
151
+ owner sees it live, the Brain stores `[REDACTED]`. Also write it to `~/.genius/secrets/<client>_<consultant>_ui.txt`
152
+ (chmod 600) for reliable relay. **Mandate a change on first login** (Preferences → Account
153
+ Security) — that burns the transited temp value.
154
+
155
+ **5. Give them Claude Code too** (so they can modify anything programmatically): on the
156
+ master, `env['xma.consultant'].browse(<id>).write({'token_expiry_mode':'7d'})` then
157
+ `.action_generate_setup_package()`; read the newest `xma.consultant.setup.wizard` for that
158
+ consultant → its `npx_command`. They run it once in VSCode (Windows: PowerShell form). It
159
+ installs the MCP + hooks + skills and self-updates daily. Confirm the client is in their
160
+ `client_ids` so `target_client` routing works.
161
+
162
+ ## Notes / gotchas
163
+ - **No `signup_token` in this estate** (auth_signup is partial on xmarts.net/clients) → the
164
+ reset-link flow is unreliable; use the temp-password path. (Brain: "signup_token roto".)
165
+ - **Odoo 19:** `res.users` direct groups = `group_ids`; `all_group_ids` is computed (implied).
166
+ - **Revoke** = archive the user on the client (`delete_record` → `active=False`), reversible.
167
+ - **Don't** grant `base.group_system` (Settings/admin) by default — escalate per task only.
168
+ - This is a per-`(consultant, client)` on-demand action, not an auto-hook: not every
169
+ consultant needs UI on every client.