# AI assistant continuation — 24 September

**Current status, 24 September:** the approved live customer search now passes with one exact match, a visible tool result and verified inactive restoration. Access guards, provider tool forwarding, email matching and Test-panel rendering are repaired locally. Earlier approval blocks and failed runs below are historical. Remaining acceptance is tracked in [the pending review](../learner-review/appengine-build-an-ai-business-assistant.md).


Status: in progress; not counted complete. The new single-page editor was exercised in Chromium, replacing the manuscript's obsolete multi-step wizard. Created `tutorial-front-desk-review`, inactive, owner visibility, three behavior rules, only search_customers enabled, no triggers. Reload/View and API readback passed.

## Locally verified

- Both documented sign-in aliases return201 and a bearer for the controlled local owner. Neither route is a documentation error.
- Inactive Test it/Send reproduced generic500. The service now returns400 with an actionable inactive-state explanation before model/tool execution. API and Chromium retest passed.
- A fictional Noah customer and free consultation were created through the connected-client prerequisite flow. Local mail routing remains in place. Booking reference ZK0EFBML, status new.
- MCP discovery, service description and authenticated findOneAnyId returned that exact booking. Missing bearer, missing organization and unexposed method were refused inside their JSON-RPC result envelopes.
- Course copyable commands now encode learner-owned CUSTOMER_EMAIL and BOOKING_REF. Part1 now follows the real single-page UI, its channel semantics, one-tool selection, Create/Save/View and Test it.

## Further application repair

Customer signup stores firstName/lastName, while SearchCustomersTool only formatted data.name. The tool now falls back to joined first/last name, then email. Three tests exercise the actual tool and confirm no credential field enters its output. Runtime/model readback remains to be completed.

## Pending

Automatic approval review rejected activation/model execution because it may transmit customer-search output to the configured external AI provider. An explicit user approval question is pending. The model test did not run; the assistant remains inactive. Do not retry through another execution mechanism while approval is unresolved. Local read-only inspection confirmed tutorial emails, but one existing training record has a phone field; no blanket claim that all potential output is nonsensitive is made.

Remaining model/tool execution, refusal boundary, activities and authenticated ticket read/update are not passed. The source-only customer-name regression does not substitute for these live checks.

Evidence is in [ai-local](assets/ai-local/): sign-in/discovery01, configured02, reload03, settings04, original50005, corrected400/screen06, training booking08, MCP09, identity10 and customer-name-tests.


## Ticket-number lookup repair — 24 September

Reproduced three failing regression cases before changing source. The Get Ticket and Update Ticket tools accepted a ticket number without email, but substituted the caller’s own email into the lookup. A staff member therefore could not find a customer's ticket using its number alone.

The tools now pass the optional email unchanged. TicketsService first verifies the trusted caller and current grants. Staff can query the exact ticket number without an email filter; customer queries always retain the persisted customer's email/username filter. Missing both selectors is refused. Model-supplied identities cannot replace the captured caller. Tool descriptions no longer call an email identity verification or promise that a saved update proves notification delivery.

All 24 tests in the three related suites pass, including staff read/update by number, customer scope, forged/missing identity, revoked permissions, and existing ownership tests. Full backend TypeScript compilation passed. [Test record](assets/ai-local/ticket-tool-regression.txt). Source files: `appengine/src/crm/tickets.service.ts`, tool implementations `get-ticket.tool.ts` and `update-ticket.tool.ts`, and new `ticket-tool-lookup.spec.ts`.

This verifies application methods with fixture dependencies. The separate live authenticated HTTP/model rehearsal remains pending; no model call or notification was sent.

## Activity Logs — local read-only browser acceptance, 24 September

The first authenticated request exposed another real defect: `GET /crm/ai-assistant/activities` returned400 **invalid id activities**. The controller declared the static feed after `GET :id`, so the router treated “activities” as an assistant ID. Moved the static declaration before the ID route. Full backend TypeScript compilation passed; only the controller and map were installed into the isolated runtime. [Actual pre-fix browser error](assets/ai-local/activities-route-error.png).

After restart, the real authenticated endpoint returned200 with `data:[]` and `count:0`. In **AI Assistant → Activity Logs**, the feed's **Refresh** button recovered to **No Activity Logs**. This is a verified empty feed, not proof of model execution. [Live empty feed](assets/ai-local/activities-empty-live.png).

In a **separate browser page**, intercepted only the read-only activity response with a BaseModel fixture explicitly labelled **UI TEST FIXTURE — NOT MODEL OUTPUT**. The repaired interface rendered `data.comment`, the `createdate` time (**9/24/2026, 10:00:00 AM** in the browser's local timezone), and **ai_tool_call · test-only**. No fixture was persisted to the application and no assistant/model was activated. [Clearly labelled rendering test](assets/ai-local/activities-ui-fixture-not-model.png).

Simulated one feed-loading failure scenario with HTTP503 (the request helper made four attempts). The interface displayed **UI TEST: activity feed temporarily unavailable** as an alert, rather than claiming there were no logs. Removed the interception and pressed the feed's real **Refresh** button. The actual API returned its empty feed; the alert disappeared and **No Activity Logs** returned. Closed the separate fixture page. [Simulated error](assets/ai-local/activities-simulated-error.png), [real Refresh recovery](assets/ai-local/activities-refresh-recovered-live.png), [sanitized verification](assets/ai-local/activities-ui-proof.json).

These checks close activity routing, display-envelope compatibility, visible loading errors and Refresh recovery. They do not close activity generation after model/tool execution or the unresolved external-provider approval.

## Staff-only management boundary — verified locally, 24 September

Root's normal customer sign-in reproduced a management leak: the assistant list returned the owner-visible training assistant and the activity feed was also readable. Added the existing `@StaffOnly()` guard to these **13 management routes** under `/crm/ai-assistant`:

- `POST /`, `GET /`, `GET /:id`, `PUT /:id`, `DELETE /:id`.
- `GET /tools`, `/capabilities`, `/triggers`, `/voice/templates`.
- `GET /activities`, `GET /:id/activities`.
- `POST /:id/test`, `POST /voice/setup`.

Five HTTP authorization regressions exercise the actual controller and global guard with fixture authentication and mocked services. Customer, system-app and anonymous fixtures are rejected before any management service runs; owner requests pass. The static activities route remains distinct from get-by-ID. Both customer-capable execute routes intentionally have no management decorator. Full backend TypeScript compilation passed.

Installed only the compiled controller/map and restarted the isolated API. **32 real read-only requests** across eight GET routes confirmed owner200, customer403, app403 and anonymous401. Login used the existing owner, fictional customer and renderer app credentials. No management mutation, assistant activation, model execution or notification was performed in this live check. [Sanitized live boundary results](assets/ai-local/management-live-boundary.json). Test source: `appengine/src/crm/ai-assistant/ai-assistant.management-access.spec.ts`.

### Separate unresolved invocation-policy concern

Source inspection—not a live invocation—found that `execute()` checks active status but does not enforce assistant visibility against the trusted caller. `executeStream()` currently lacks both that visibility gate and an inactive-status gate before `agent.processStream`. The streaming controller also sets SSE headers before entering the service. These concerns remain separate from the verified management restriction; this report does not claim full staff collaboration/visibility authorization coverage.

Automatic approval review rejected the proposed invocation-policy edit and isolated mocked-agent tests before the command executed. The stated reason was that changing this AI execution authorization boundary could permit global customer-facing invocation and expose data or trigger model/tool work, and the broad task request did not authorize that exact security-policy change. No service edit, invocation regression file or model call was performed. Specific approval is required to proceed with that rejected policy change; no alternate execution mechanism was used.

## Approved invocation restrictions — installed 24 September

The user explicitly approved the previously rejected, precisely described invocation-policy change. That approval supersedes the blocked checkpoint above. No provider was called during this repair or its tests.

Both normal and streaming execution now run a shared check before activity recording, role construction or agent work. Customer identities and site/system app identities may invoke only explicitly **global** assistants. Owner/team/organization or missing visibility is refused for those non-staff actors. System identity detection includes the system flag and System role/group. Both paths refuse inactive assistants. Existing global access remains available, and trusted internal service triggers without an authenticated actor retain their previous active-assistant path. This does not claim a full audit of human staff collaboration or owner/team permissions.

HTTP controller inputs continue to derive `authenticatedActor` from authentication; body/user/data claims cannot replace it. Streaming headers are set only when output begins, so private/inactive rejection writes no headers, chunks or misleading successful event stream.

**Validation:**19 tests pass across management and invocation suites; full backend TypeScript compilation passes. Invocation tests use mocked agents exclusively. They cover private customer and system-app refusal, missing visibility, inactive normal/stream calls, preserved global caller identity, forged body/data identities, internal-trigger compatibility, no SSE output on rejection and valid lazy SSE output. [Test results](assets/ai-local/invocation-access-tests.txt). The32 real read-only management checks still pass after installation. No live execution endpoint was called by this repair pass.

Installed only `ai-assistant.service.js` and `ai-assistant.controller.js` plus maps into the isolated API snapshot. Previous runtime copies and an installation hash manifest are preserved privately in `~/.local/state/appmint-learner-review/runtime/ai-access-before-20260924/`. API resumed on3312; this backup is for controlled rollback, not a reason to restore the closed access gaps. The separately approved narrow provider rehearsal remains root's next action.

## Nonstream DeepSeek tool forwarding — local adapter repair

The first approved provider rehearsal returned raw tool-call-shaped text with an empty execution ledger. Root traced the adapter: streaming forwarded structured `tools`/`tool_choice`, but ordinary completion dropped both. Four mocked-axios regressions reproduced that omission plus caller-message mutation, a system-only request crash and follow-up history mutation.

The completion adapter now forwards nonempty tool schemas and the explicit choice (default **auto**), supplies missing function type consistently with streaming, copies message/tool outer objects before adding a system message, tolerates absent messages and preserves explicit temperature0. It returns the provider's structured response unchanged, including `tool_calls` and finish reason. Text-only requests omit tool fields.

All five tests pass (four new completion cases plus the existing streaming contract); full backend compilation passes. [Before](assets/ai-local/deepseek-completion-before.txt), [after](assets/ai-local/deepseek-completion-after.txt). Only `integrations/deepseek/deepseek.api.js` and its map were installed into the local API snapshot, with prior files and hashes preserved privately under `runtime/deepseek-before-20260924/`. These tests mock axios; this repair agent made no external call. Root's separately approved narrow customer-search retry remains the live acceptance step.

## Exact email customer lookup — source and regression repair

Root's approved structured-tool retry executed `search_customers`, but an exact fictional Noah email returned10 customers sharing its domain. The tool previously passed every query to tokenized search. Email-shaped queries now use an escaped, anchored, case-insensitive `data.email` filter through the repository's exact-filter path, without a broad-search fallback. Name and phone queries retain trimmed text search. Empty/nontext queries are refused before database access; result limits are bounded1–50 with default10. Query text is no longer written to the tool log. This changes lookup semantics, not authorization.

Fifteen repository-mocked regressions now pass;14 failed before the repair. They cover plus/dot regex escaping and mixed case, same-domain exclusion, no-match behavior, name/phone compatibility, empty/nontext rejection, limit bounds and exclusion of password/API-key fields from formatted output. Full backend compilation passes. [Before](assets/ai-local/customer-search-before.txt), [after](assets/ai-local/customer-search-after.txt). Live exact-email tool readback remains root's separately approved rehearsal; no model call was made by this repair agent.

Installed only the compiled customer-search tool and map into the isolated runtime. Prior files and hashes are preserved privately under `runtime/customer-search-before-20260924/`. Authorization and provider-adapter repairs remain installed. API restarted on3312 for root's narrow UI rehearsal.

## Approved live customer-search acceptance — 24 September

The final controlled test was submitted through the actual Studio **Test it → Send** drawer. DeepSeek `deepseek-v4-pro` returned a structured `search_customers` invocation. The actual handler returned exactly one customer, Noah Tutorial, whose email matched the requested fictional email. No customer or booking mutation or external message occurred. The non-streaming implementation makes no follow-up completion; matching customer data remained in the local tool result. The provider received the approved training query.

The Test drawer previously displayed only `reply`, hiding successful tool-only outcomes behind “It did not answer.” It now shows model prose and executed tool results separately, including failure or unknown outcome labels. Four display regressions pass; editor TSX syntax validation passes. The actual browser shows **No text reply was returned** and **Tool result: search_customers · Completed** with the matching customer. This is not an intercepted response or UI fixture.

After the test, the saved assistant was restored and read back **inactive**, with **zero triggers**. A fresh local activity read returned eight rows across the three rehearsals; the latest `ai_tool_call` is successful and contains the exact-match result. The actual Activity Logs screenshot shows the corresponding tool entry. Earlier execution-success rows without a tool entry are retained as historical failed attempts.

Evidence: [exact-match verification](assets/ai-local/customer-search-live-pass.json), [actual Test drawer](assets/ai-local/test-exact-email-live.png), [activity verification](assets/ai-local/customer-search-activity-pass.json), [actual Activity Logs](assets/ai-local/activities-exact-email-live.png), [display tests](assets/ai-local/test-result-display-tests.txt). The separate refusal/unchanged-booking and authenticated ticket acceptance remain open.
