Skip to content

Roles and skills — log archive

Part of roles-and-skills. Log entries from 2026-09-12 to 2026-09-13, moved verbatim from roles-and-skills. The second block (from "Demo dataset built") follows the findings in roles-and-skills-sandbox-findings; references such as "above" point into roles-and-skills or its sibling files.

Log

  • 2026-09-13: Departments/functions split implemented. departments.yml gained a functions: block and the seven SoD tags (see "What changes in the file" above); every reader (onboard, the assistant, sop-audit, reports-sync, sop-index) now resolves through the new ~/.local/bin/_roles_resolve.py. roles-verify built first as the safety net; before/after diff is empty and access-sync (read-only) reports zero drift — no person's effective permissions changed. onboard functions added. Not done: nothing deferred — this was the whole "Now" row of the phase-in table; the ~5/8/10/15-person rows remain future work as planned.

  • 2026-09-12: Plan written, then narrowed to the two roles that exist: Owner and Business Office. Angel: "it's me and my wife… she can send invoices, collect payments, see orders, profit losses… she likes to use WhatsApp". Recommended the chat page on the home screen first and WhatsApp later, and flagged that payments and P&L are not ERP features at all. Sequential SOP numbering restored the same day (the block scheme lasted an hour). Related: mrp, plex-server-map.

  • 2026-09-12: Implementation started (Angel: "Implement"). Phase 1 done: roles/departments.yml (seven entries incl. readonly; assistant enabled only for admin and sales), ERP groups Engineering / Purchasing / Sales / Finance created through the API with no delete anywhere, Yrosado moved from the combined group to Sales + Purchasing, onboard reads the registry and takes --dept sales,finance, plus onboard roles. Assistant plumbing: ERP user assistant-sales in the Sales group with its own token (proved: 200 on read, 403 on DELETE), config in ~/.config/assistant/env, venv ~/.local/share/assistant-venv (claude-agent-sdk 0.2.152, FastAPI), Access app "EmbeddedEra Assistant" on chat.embeddedera.com (AUD recorded), tunnel ingress → 127.0.0.1:8200, DNS, home tile, systemd user unit written but not started, red-team suite ~/assistant/tests/redteam.yaml + runner assistant-test (promptfoo), assistant sqlite added to erp-backup. Service code in progress in ~/assistant/. Deviation from the reuse list: own chat page instead of Open WebUI, because the scope rule requires a renderer that can only show an action card, a question or the standard line — Open WebUI renders whatever the model says. Langfuse deferred: a sqlite turns table covers the log for two people; revisit when there are more.
  • 2026-09-12: Assistant live at https://chat.embeddedera.com (Phase 3 delivered before Phase 2 — the accounting decision — because Sales does not depend on it). Code ~/assistant/ (app.py, actions.py, agent.py, static/index.html; local git, no remote yet), user unit assistant.service on 127.0.0.1:8200. Skill ~/Notes/skills/sales/SKILL.md v1.0 draft. What was proven: 401 without a gate token; 302 to Google from the public address; 13/13 red-team cases (off-topic, delete, persuasion, self-promotion, nonexistent customer, negative qty, price-in-message, profit question, email, vault, injection-in-data, oversize); shell and file requests produce none with no tool attempted; the full draft → Confirm → sales order SO-0001 + invoice PDF path once on a throwaway part, then deleted. Finding worth keeping: the Agent SDK offers the model the entire built-in Claude Code tool roster regardless of allowed_tools. Three walls now: tools=[] plus an explicit disallowed_tools list, the PreToolUse deny hook (logs attempts), and the department-scoped ERP token (403 on delete, proven). permission_mode=bypassPermissions is required so allowed calls do not block on a prompt with no TTY — that is exactly why the walls have to be structural. Deviations from the plan: own single-file chat page instead of Open WebUI (the scope rule needs a renderer that can only show three shapes); sqlite turns log instead of Langfuse (two people; revisit at more). Daily canaries: assistant-test cron 06:40, ntfy on failure, runs a dev copy on 8201 so production is untouched. Open: Angel to try it through the real gate; add his wife to ~/.config/assistant/people.yml as [sales, finance] (needs her address); in-chat handoff to Administration; SOP-010 Assistant Use (in progress); GitHub remote for ~/assistant.
  • 2026-09-12: Admin page live at https://chat.embeddedera.com/admin (Administration department only; tile on the home page). Status: assistant per department, sandbox, canaries, backups, access lists, services, recent turns. Actions, allow-listed and logged to admin_actions: assistant on/off per department (edits roles/departments.yml textually, then notes-sync), sandbox up/reset/down, run canaries, restart a service, backup now. Built on the sandbox branch, merged to main (a5ba55a) — first real use of the branch workflow.
  • 2026-09-12: People management in the Admin page (add / change departments / remove) through onboard, which now owns ~/.config/assistant/people.yml too — one source of truth for who holds which departments. New onboard update --email X --dept a,b. Self-removal and self-downgrade refused. Merged to main.
  • 2026-09-12: Guide mode live — "how do I …" returns a card built from ~/.local/state/assistant/sops.json (generated by sop-index, run by sop-deploy): number, title, owner, version, purpose, link, and a computed "I can do this for you". A department that does not own the procedure gets a referral to the owner, no steps. 17/17 red-team cases. SOP owners corrected to the performing department (003/006 Production, 004/005 Purchasing, 007 Sales, 009/010 Administration) so referrals point at the right people. Also: admin.embeddedera.com with its own stricter gate; the angel superuser removed for good (env lines commented).
  • 2026-09-12: Onboarding now emails the person (Resend, from hello@): Google access, departments, NAS username and a one-time link on the gated host that shows the NAS password once after they sign in — the password is never in the email. Admin page: edit first/last name (flows to the ERP user), Reset NAS password (new one-time link + email), Send welcome. onboard nas-reset, --first/--last on update; people.yml now email: {departments, first, last}. Test emails were redirected to adiaz@ (ASSISTANT_EMAIL_REDIRECT). Side effect caught after the tests: Yrosado had gained a NAS login and lost Purchasing in people.yml — reverted with onboard update. Merged to main.
  • 2026-09-12: Admin page round two — loads in well under a second (sectioned endpoints, concurrent fetch, caches with per-card refresh; was 8–13 s), Chat button gone, People card full-width with wrapping chips, Reactivate for inactive ERP users (onboard update/add now reactivate), Sign-ins card from Cloudflare Access audit logs (token permission Access: Audit Logs · Read added by Angel), and a sandbox guard: on the sandbox instance every production-touching action returns 403 with a banner. Sandbox assistant now reachable on the shop network at http://chat-sandbox.lan (bound to the docker bridge gateway 172.18.0.1:8201, NPM host 28) with a "SANDBOX — talks to the sandbox ERP" banner; dev identity = admin, harmless because of the guard. angelgdo7@gmail.com removed from every gate (the Google sign-in is Internal-only, so it never worked). Incident: agent test runs left Yrosado deactivated in the ERP with Purchasing dropped — caught on the user list and repaired with onboard update (now active, Sales + Purchasing). Lesson: test accounts only, never a real person, and check onboard list after any agent touches onboarding.
  • 2026-09-12: SOP audit is programmatic. ~/.local/bin/sop-audit runs 41 read-only checks derived from each SOP's Checks section (ids like SOP-002.description-quality, listed in every SOP's audit: front matter — convention written into SOP-000 v1.6). Output ~/.local/state/assistant/audit/latest.{json,md}, grade PASS/WARN/FAIL, exit 1 on FAIL. Admin page has an Audit card with Run; cron Monday 06:50 with ntfy on FAIL. First run against production: 2 fails, 4 warns. Fixed the same day: a stock item with no location (170 antistatic bags → Receiving), five over-100-character descriptions shortened without changing facts, and 43 manufacturers wrongly flagged as suppliers — InvenTree defaults is_supplier to true on company creation, so kicad-erp-sync had been creating manufacturers as suppliers; tool fixed to send is_supplier: false, the 43 records corrected. Still open for Angel: five parts whose description is just the name (ESP32-C6-LCD-1.47, LCD_1.47_Touch, LCD_ST7789VW, Nucleo32, RFX2401C); 144 parts with no image; 144 purchaseable parts with no supplier part (quick win: create LCSC supplier parts from the LCSC property many symbols already carry). Customer-address check skipped: this InvenTree exposes no address endpoint the tool could find — verify by hand.

  • 2026-09-12: Root cause, not a patch. SOP-009 kept flagging angelgdo7@gmail.com because the Reports gate had been created by cloning a saved policy JSON from days earlier, after that address had been removed everywhere else. Fix: gates are derived from people.yml + the department access: tokens and reconciled by onboard access-sync (hourly, drift alert); new gates are created with onboard access-app from the registry; the saved templates were deleted. Token lists completed (chat, admin, reports; home covers the App Launcher, sop covers the previews host).

  • 2026-09-12: onboard access-sync applied (only change: Yrosado removed from the Admin gate — her departments do not carry the admin token; the service refused her anyway, but the gate should not have admitted her). Hourly reconciliation + daily drift alert installed. SOP-009: all five checks pass.

Log, continued (entries filed after the 2026-09-12 findings)

  • 2026-09-12: Demo dataset built and verified — erp-sandbox reset --demo + verify-demo (all four checks pass; production untouched, still 49 real companies). Sandbox is currently in demo mode. Details in sandbox-workflow. Next: guest invitations with expiry, then the feedback store.
  • 2026-09-12: Guests are live. onboard guest --email … --name … --days N invites an outside address: writes it to people.yml with invited and expires, syncs only the two sandbox gates, creates no production ERP user, and refuses while the sandbox holds real data ("run erp-sandbox reset --demo first"). Expiry is enforced by the hourly access-sync, which treats a lapsed invitation as belonging to no gate and removes it, annotating the line with the expiry date. --list, --revoke, and --enable-otp (creates Cloudflare's one-time-PIN login method and adds it to only the two sandbox apps — verified: those two accept two login methods, the other eight still accept only Google, so no outside address can ever reach production). Admin page has a Guests card with invite/revoke and the demo-mode guard. Tested with a throwaway invitation through expiry and revocation; nothing left behind.

  • 2026-09-12: Admin Sandbox card is now the way in — DEMO/REHEARSAL badge, links to sandbox.embeddedera.com and chat-sandbox.embeddedera.com, Reset (demo) / Reset (real data) / Verify demo / Down. Reset to real data refuses while a live guest invitation exists ("revoke the guests first"), and the demo password is shown once from the job then scrubbed from the log. Sign-ins and Recent turns now span the full row (IP into a tooltip, message truncated) — measured: no card overflows at 1280 px, was 573 and 594 px of hidden content.

  • 2026-09-12: Feedback and guided forms merged. Feedback: POST /feedback, a widget on the chat, admin and home pages, a Feedback link on the SOP site, feedback: messages stored without ever reaching the model, an admin triage card (accept / reject / duplicate with a required reason, mark replied, done, demo items in their own tab), the two SOP-013 audit checks, and a Monday reminder to ntfy when anything has sat undecided a week. Guided forms (forms.py): a declarative field list per action, drafts in sqlite bound to the person for 30 minutes, the server deciding what is missing and asking one question at a time with progress. part takes its contract from SOP-002 — a description equal to the name is refused quoting the SOP — computes the next IPN, and creates the part the way erp-part does with its KiCad parameters. onboard_person is Administration-only and refuses outright in the sandbox. Sales is unchanged. New ERP user assistant-engineering (Engineering group, 403 on delete, proven) backs the engineering and admin skills. Gaps recorded: the part form does not yet do the distributor/datasheet path (section 5), no image or supplier part on confirm, and the sandbox has no salable parts so the sales regression there could only be exercised through the read actions.

  • 2026-09-12: Feedback widget bug caught in a screenshot before anyone saw it: the modal carried both hidden and an inline display:flex, and inline style wins, so it opened by itself on every page load. Fixed with [hidden]{display:none!important} in all three copies. Lesson already in the rules — verify in a browser, not just by reading the code.

  • 2026-09-12: Purchasing and Production skills added (sandbox worktree ~/assistant-sandbox, branch sandbox, no commit made — code review pending). Six new read actions and two draft/confirm pairs in actions.py (find_supplier, supplier_parts, low_stock, open_purchase_orders, po_status, draft_purchase_order/confirm_purchase_order — SOP-004/SOP-005; open_builds, build_status, draft_build_order/confirm_build_order, print_traveler — SOP-003/SOP-006), wired as new SDK tool sets in agent.py (build_purchasing_tools, build_production_tools, build_stock_location_tool — the last shared by every department) and selected per-turn in app.py's /chat and /confirm. stock_location (general "where is it?") is offered whenever any of sales/purchasing/production/a form is available. Confirmed against the sandbox ERP: a PO drafts, prices from the price break, confirms into Pending with destination Receiving and prints "EmbeddedEra Purchase Order"; a build refuses a non-assembly/no-BOM part quoting SOP-006, drafts with shortage warnings, confirms into Pending with destination "In test" / take-from "Kitting", and print_traveler renders "EmbeddedEra Work Order Traveler" — all cleaned up from the sandbox after. Two SKILL.md files added (~/Notes/skills/purchasing/, ~/Notes/skills/production/, both under 500 words), roles/departments.yml flipped assistant.enabled: true for both departments. Fixed a real cross-department leak while doing this: run_sales_turn always built and allow-listed the five sales tools regardless of department, so a purchasing- or production-only session could already reach draft_invoice. Added include_sales_tools (false unless the person actually holds sales) — the prompt-level allow-list now matches the department, on top of the ERP-token wall that was the only thing stopping it before. Two reliability findings, not fully resolved: (1) a multi-department identity (Administration, which holds all six skills) gets every skill's text and every tool concatenated into one turn, and haiku frequently defaults to the sales tools or answers none without calling anything — single-department sessions (tested as dedicated purchasing-only / production-only people.yml entries, removed after) are far more reliable and matched the worked examples closely. (2) even single-department, a zero-argument read tool (low_stock, open_purchase_orders) is sometimes skipped in favor of an unverified none answer, regardless of phrasing — parameterized tools (find_supplier, draft_purchase_order, draft_build_order) were reliable in every phrasing tried. Neither produced a wrong fact — worst case is a wasted none — but it's a real gap against the "call a tool before every claim" rule now added to FINAL_SHAPE_INSTRUCTION. Recommend either a stronger model for Administration's combined session, or splitting the admin turn per-department by keyword before it reaches the model. Also found: the pure read-only shape (none) never surfaces text to the user, by design — "where is X" and "what's below minimum" both render as a blank reply even when the tool ran and found the real answer, exactly like the existing sales worked-example-1 pattern. Not new, but worth revisiting if Purchasing/Production expect visible answers rather than only action/question/guide cards. Not done — needs a human: creating the assistant-purchasing (Purchasing group) and assistant-production (Assembler group) ERP users/tokens in production InvenTree was blocked by the auto-mode classifier (a production write). Sandbox reuses the existing sandbox Owner-level token for both (ERP_TOKEN_PURCHASING/ERP_TOKEN_PRODUCTION added to env.sandbox). Exact command to run once approved is in the session report.

  • 2026-09-12: Queue finished. Purchasing and Production skills merged; ERP users assistant-purchasing (Purchasing) and assistant-production (Assembler) created with their own tokens, both proven 403 on delete. Five departments now have a working assistant (admin, engineering, production, purchasing, sales); finance stays off until the accounting decision. A real hole was found and closed while building them: the tool allow-list was applied per request but not per department, so only the ERP token stood between one department and another's write action. Now gated at both layers. Final state: 17/17 red-team, gates in sync, audit 39 pass / 2 warn / 1 fail (the fail is still the five part descriptions, which are Angel's to write). Known caveats recorded by the builder: Haiku sometimes skips a zero-argument read tool and answers "none", and the none shape gives the reader nothing back — read-heavy departments feel this most. Worth a design pass: a fifth answer shape for read results, phrased by the server from the tool output rather than by the model.

  • 2026-09-12: Admin chat (Tier A) live at admin.embeddedera.com/admin/chat and linked from the home page: streaming conversation, session history with titles, Haiku / Sonnet / Opus / Fable (model ids verified by asking each one), an effort control mapped to the SDK thinking options, cost per message and per session against ADMIN_CHAT_MONTHLY_USD (25 default, refusal at the ceiling proven). Tier boundary enforced in code: tools=[], the disallow list, no setting sources, a deny hook, and an optional "let it read the shop" switch that registers exactly nine read-only lookups on the delete-refused admin token — /admin/chat/api/tools lists what is loaded so the claim is checkable. No write action is registered on that page at all. Tier B (a shell) remains unbuilt by choice. Fixed while verifying: ASSISTANT_BANNER was written unquoted with an em dash, so every shell sourcing env.sandbox tried to run it as a command; erp-sandbox now quotes it.

  • 2026-09-12: The admin chat told Angel its read switch was "in a header or sidebar" — it was a bare checkbox in the composer row and he could not find it. Two fixes: the control is now a copper toggle labelled "Read the shop" with a tooltip listing the nine lookups, and the system prompt states exactly where it is so the model stops guessing. General lesson: when the assistant describes its own interface, that description has to be in the prompt as fact, or it will invent one.
  • 2026-09-12: Admin chat given real reach after two failures Angel hit: it had been handed Sales' salable-only part lookup (so components were invisible) and could quote audit counts but not list them. Now: find_part_any across every part, query_shop read-only SQL as reports_ro inside a BEGIN READ ONLY transaction with a 10 s timeout and a single-statement guard, audit_items for every offender, and export_csv returning an admin-only download link that expires in 24 h. Proven independently: that role gets "permission denied" on an UPDATE and still reads 145 parts. Also caught in test 6: with the read switch off, the model printed a fake tool-call line instead of refusing — the prompt now says the tools do not exist at all in that state. The real guarantee is the database role, not the prompt.
  • 2026-09-12: CSV export links rendered as plain text — the chat page only linkified http(s) URLs and the export path is relative. Fixed (markdown links to relative paths, plus bare /admin/chat/export/... paths). Verified in a browser: asked for a CSV, clicked the link, the file downloaded. The earlier "no list" was the Read the shop switch being off, not a fault.