Logrite in Claude Code

The MCP server and its 32 tools, and the plugin's skills, commands and hooks — what each one is, what it calls, and which screen in the Logrite app reflects it. Written from the source and from live calls against both estates, not from memory.

Updated 2026-10-08 for the 32-tool catalogue and plugin 1.2.0. The live numbers in §01 and §09 were read 2026-10-05 against mcp.logrite.ai (workspace 252, logrite-ops) and dev-mcp.logrite.ai (workspace 3103).

01

Two planes, and only one of them was working

data plane vs operating plane

Logrite monitoring Logrite is two separate things, and they fail independently.

The data plane — already true

Two collector agents (logrite-prod-vm, logrite-dev-vm) run from cron every five minutes under ~/logrite-agent/ and ship into log-analytics-252-<agentKey>. Nobody has to do anything for this to work.

The operating plane — the gap

A developer in an editor, reaching the same workspace through MCP, writing one row per call to mcp-activity-252. The tools were connected and authenticated; the skills, commands and hooks were never loaded on any machine — see §02.

Workspace 252, index document counts read live 2026-10-05

3,773,595log-analytics-252-logrite-prod-vm
2,045,508log-analytics-252-logrite-dev-vm
40log-analytics-252-prod-probe-c0a219
28mcp-activity-252
17indexes visible to this workspace in total

5.8 million documents on one side, 28 on the other. That ratio is the whole point of this page. The ingest path has been carrying the estate for weeks; the operating path had seven calls from a real editor in its entire history before this work (2026-09-29 → 2026-10-01), because the half of the product that makes an editor use those tools had never loaded.

02

Install and load path

what one command produces, and what used to go missing
curl -fsSL https://mcp.logrite.ai/install.sh | sh

The key comes from LOGRITE_API_KEY set on sh (curl -fsSL https://mcp.logrite.ai/install.sh | LOGRITE_API_KEY=lgp_live_… sh), otherwise from a prompt on /dev/tty with echo off. An empty answer, or no terminal and no variable, installs in OAuth mode. The key is never a command-line argument — the installer refuses one, because argv lands in shell history and in ps.

The script is served only by the MCP host, and it rewrites its own three default URLs for the estate that served it — so dev-mcp.logrite.ai/install.sh validates against the dev backend with no variables set.

StepWhat it producesWhy it is in that order
1. validatePOST /api/v1/api-keys/introspect (key mode only) Before anything is written. A half-installed plugin pointing at a dead key fails later and somewhere else. 401 and “no answer at all” are told apart, because they need opposite actions. curl reads the header from stdin (-K -), never from argv.
2. unpack~/.logrite/claude-marketplace/logrite/ + a generated .claude-plugin/marketplace.json beside it with "source": "./logrite" A directory under ~/.claude/plugins/ with no marketplace registration is never loaded by Claude Code. In OAuth mode the download is the generic plugin (/skills-package?for=installer, no credential).
3. legacy sweepremoves ~/.claude/plugins/logrite/ and an old user-level copy of the init command, each only when it is recognisably ours Older installers put the payload where nothing reads it, and copied a command file that went stale.
4. store the key~/.logrite/api-key (0600) and the headers helper ~/.logrite/mcp-headers.sh (key mode only) Claude Code runs the helper for the server's Authorization header, so the key is in neither a process list nor Claude Code's MCP config.
5. registerkey mode: claude mcp add-json -s user logrite with headersHelper; OAuth mode: claude mcp add -s user --transport http logrite https://mcp.logrite.ai/sseUser scope, because an unscoped add binds the server to whichever directory the installer happened to run in. In OAuth mode the user then runs /mcp → logrite → Authenticate.
6. loadclaude plugin marketplace add → claude plugin install logrite@logrite-local -s userSkills, commands and hooks. The one init command is /logrite:init.

The zip from Sandbox → MCP installs the same way by hand; its root is the logrite-local marketplace:

mkdir -p ~/.logrite && unzip logrite-claude-plugin.zip -d ~/.logrite/claude-marketplace
claude plugin marketplace add ~/.logrite/claude-marketplace
claude plugin install logrite@logrite-local -s user

Only one MCP registration, deliberately. The zip the installer downloads has mcpServers and userConfig stripped from plugin.json. If the plugin also declared the server you would get two registrations of the same tools — logrite and plugin:logrite:logrite — the way claude mcp list already shows claude.ai Slack beside plugin:slack-by-salesforce:slack.

The fallback has to name what it lost. By the time the plugin install can fail, the MCP server is already registered — so claude mcp list says Connected and the install reads as complete while not one skill, command or hook is active. The installer prints “the skills, the /logrite: commands and the hooks are NOT active” and the commands to finish it.

What this looked like before — two defects that cancelled the whole feature

#DefectEvidence
D1The documented one-liner left skills, commands and hooks inert on every machine it had ever run on. It unzipped to ~/.claude/plugins/logrite and then only ran claude mcp add. claude plugin list named only slack-by-salesforce@synced; enabledPlugins was {}; known_marketplaces.json listed only claude-plugins-official.
D2The advertised alternative was a repository that does not exist. Both the Sandbox’s Connect pill and the plugin’s own manifest pointed at it. gh api repos/Deployd-io/logrite-claude-plugin → 404.

Both are fixed. The manifest shipped inside the plugin now carries "source": "./" — the marketplace manifest lives at <plugin root>/.claude-plugin/, so the marketplace root is the plugin directory, which is the shape Anthropic’s own catalogue uses for in-repo plugins. The Sandbox pill now shows the installer one-liner.

Verified on this machine, 2026-10-05: claude plugin list → logrite@logrite-local, scope user, ✔ enabled; claude mcp list → logrite … ✔ Connected with no second row; a real claude session lists the /logrite:* commands and the skills.

03

What one tool call actually does

transport → credential → contextvars → handler → trail
1
POST /sse
MCP Streamable HTTP, JSON-RPC 2.0, stateless=True. One ASGI callable splits on path: /sse is handled inline, everything else falls through to a separate CORS’d Starlette app carrying /health, /skills-package and /install.sh. CORS is on that app only — the MCP transport never grows a preflight surface, because MCP clients are not browsers.
↓
—
GET /sse → 405
Deliberate, with Allow: POST, DELETE. A stateless server has no server-to-client streams; answering 405 lets a client fall back to POST-only mode instead of hanging on a stream that will never produce an event. verified live: 405
↓
2
verify_credential(auth_header)
The single entry point, used by /sse and /skills-package. Chooses by prefix: lgp_live_ / lgs_live_ → API key, anything else → JWT. The old prefix is still accepted because a key minted before 2026-09 is a valid credential until it expires; dropping it would not revoke those keys, it would make them unrecognisable.
↓
2a
API key → POST {backend}/api/v1/api-keys/introspect
Cached 60 s (MCP_API_KEY_CACHE_TTL) keyed on sha256(key). Accepted only when the reply carries both active and workspaceId. A negative is never cached — it is evicted, so a revoked key stops working promptly rather than at the end of a TTL.
↓
2b
JWT → decoded locally, HS512 / HS256
Secret from SECURITY_JWT_SECRET, base64-decoded only if the round-trip is lossless — mirroring the backend. Workspace comes from the workspaceId claim; a token without one is refused.
↓
3
contextvars: current_workspace, current_identity
Set only after verification, reset in a finally. A contextvar rather than a parameter because the MCP SDK owns the call path between the ASGI app and a tool handler. The raw token rides on the identity in process only, because the platform tools call Logrite as the caller — so each tool reaches exactly what that person reaches, and nothing more.
↓
4
scope gate → handler → downstream
An unknown tool is 404 (“Unknown tool '…'. Call tools/list for the available tools.”). Seven tools refuse an API key locally with connected_client_required and a connectUrl, before any upstream call. At most four cortex_chat / analyse_log_file / run_compliance_scan calls run at once per identity; a fifth is too_many_concurrent_calls (429), not queued. Every tool then goes out over HTTP to the backend / AI service / semantic search / ingest shim; none talks to OpenSearch directly. structuredContent is trimmed to 32 KB with truncated: true and total. §04.
↓
5
activity.record() → bounded queue → daemon thread → mcp-activity-<ws>
Fire-and-forget, MCP_ACTIVITY_QUEUE_MAX default 1000. Never on the request path. An audit write that can fail a tool call turns logging into an outage; one that can block a tool call turns a slow cluster into a hung editor. Overflow is counted and logged, because a silent drop makes the trail look complete when it is not.

Two rules the code pins, both learned the hard way

An unreachable backend is 503, never 401. A failed introspection hop raises BackendUnavailable, caught before the bare except. Telling a developer their key is invalid while the backend is down sends them to create another key that will not help either — and they conclude the product is broken rather than that a hop is. The same split is honoured on /skills-package.

isError is not ok. Returning a bare list of content makes the SDK answer isError: false, so “Cannot connect to OpenSearch” reached the client looking like a result — and a model relays that to a developer as a finding about their logs. Every failure now leaves as a CallToolResult with the flag set, and the trail records ok:false, status:502 to agree with it. The live acceptance test used to pass those tools for exactly the same reason: it checked the flag, and the flag said fine.

Message hygiene on the way out

Every internal base URL is replaced by the service’s public name, an HTML error page is reduced to its <title>, and everything is clipped to 300 characters — the reader is a model with a context window. A result used to carry "source": "http://backend:8080/…", a docker-network address that means nothing to a customer and maps the estate for anyone who reads it; the path is kept, because that is the public route a developer can call with the same key.

Configuration that changes behaviour

VariableDefaultWhy it exists
MCP_API_KEY_CACHE_TTL60Seconds a verified key stays resolved in-process.
MCP_ACTIVITY_ENABLEDtrueMaster switch for the trail.
MCP_ACTIVITY_QUEUE_MAX1000Queue bound; overflow counted, never sampled.
OPENSEARCH_VERIFY_CERTSfalseThe cluster serves a self-signed certificate — the reason every runbook spells the curl -sk. Left verifying, every OpenSearch-backed tool failed CERTIFICATE_VERIFY_FAILED and silently emptied the activity trail.
MCP_HTTP_USER_AGENTlogrite-mcp/1.0urllib’s default UA is answered 403 … 1010 by Cloudflare, which made every introspection fail and every tool call 503.
MCP_PUBLIC_ESTATESbuilt-in 2 rowsHost → {mcp, backend, app}. Invalid JSON raises at import: loud at start-up beats a silently prod-pointing installer at request time.

There is no auth bypass and there must not be one. No environment variable short-circuits the credential check, a JWT without exp is refused, and a test fails if a bypass reappears in code.

04

The 32 tools

12 single-operation · 20 grouped by object · all over HTTP as the caller

Nothing is filtered by credential, so tools/list is the same for everyone; what differs is what each call reaches, because every tool carries the caller’s own token. A grouped tool takes an action argument; arguments are camelCase and every limit is capped at 100. A destructive action needs confirm equal to the object's name, never its id. Tools tagged connected client refuse an API key.

All 32 rows are listed below; the filter needs JavaScript and the table is complete without it.

ToolFamilyR/WDownstream
get_workspace_contextWorkspaceread backend: GET /api/v1/project/workspace/{ws}, GET /api/v1/workspaces/{ws}/addons, GET /api/v1/warden/library/frameworks
list_projectsWorkspaceread backend GET /api/v1/project/workspace/{ws}
get_projectWorkspaceread backend GET /api/v1/project/{id}
enrol_repositoryWorkspacewrite backend POST /api/v1/project/enrol; needs the Logrite GitHub App on the owner
api_keysWorkspaceread write connected client backend /api/v1/api-keys, …/{keyId}/rotate, DELETE …/{keyId}; introspect (allowed with a key) → /api/v1/api-keys/introspect
project_adminWorkspaceread write backend /api/v1/project/{projectId}, …/access, …/push-status, …/index-binding
project_membersWorkspaceread write backend /api/v1/project/{projectId}/members, …/invitations/{id}
workspace_addonsWorkspaceread write backend /api/v1/workspaces/{ws}/addons
workspace_membersWorkspaceread write backend /api/v1/workspace/{ws}/members, …/seats, /api/v1/invite, /api/v1/workspace/role; the invite's workspace name comes from the credential
workspace_settingsWorkspaceread write backend /api/v1/workspaces/{ws}/settings
cortex_chatCortexread ai-service POST /api/v1/chat/sync
knowledge_searchCortexread ai-service POST /api/v1/knowledge/ask
analyse_log_fileLogswrite ai-service POST /api/v1/log-analyses (text/plain, x-filename)
ingest_log_linesLogswrite ingest-shim POST /ingest, NDJSON, authed with a collector-agent token
semantic_searchLogsread semantic-search POST /api/v1/search/hybrid
analytics_queryLogsread backend OpenSearch proxy /api/v1/analytics; a foreign index is refused before the request
collector_agentCollectorsread write backend /api/v1/agents; rotate is destructive, confirmed by the agent's name
collector_telemetryCollectorsread backend /api/v1/agents/{agentId}/…
list_compliance_frameworksComplianceread backend GET /api/v1/warden/library/frameworks
run_compliance_scanCompliancewrite backend POST /api/v1/scan/initiate; poll /api/v1/scan-runs/latest
scan_resultsComplianceread backend /api/v1/scan-runs/…, /api/v1/scan-results/…
compliance_libraryComplianceread write connected client backend /api/v1/warden/library
compliance_logsComplianceread backend OpenSearch proxy /api/v1/analytics on compliance-logs-<ws>; actions search, services, summary
maturityComplianceread write connected client backend /api/v1/maturity
warden_rulesAI Wardenread write connected client backend /api/v1/warden/rulesets, /api/v1/custom-rules, /api/v1/warden/agents/{agentId}/…
warden_relayAI Wardenread write connected client backend /api/v1/warden/relay-keys, …/requests, …/stats, …/violations/suppress
integrationsIntegrationsread write backend /api/v1/integrations/discord, /api/v1/integrations/salesforce
observabilityIntegrationsread write connected client backend /api/v1/observability/connections, /api/v1/observability/feed
get_mcp_activityAuditread backend GET /api/v1/mcp/activity
governance_auditAuditread connected client backend /api/v1/governance
logrite_connectionConnectionread local for whoami, scopes, connect_url; backend /api/v1/mcp/connections
coding_sessionsAI Codingread write backend /api/v1/coding/…; the session token only with includeToken

No tool talks to OpenSearch directly. compliance_logs and analytics_query go through the backend's OpenSearch proxy as the caller, so the backend authorises every index against the caller's workspace a second time. The server's own OpenSearch client is used only to write the activity trail and coding events.

05

Tenant scoping

derived from the credential, never from the arguments

The workspace comes from the verified credential and is never an argument. Two checks carry the boundary.

CheckWhat it does
belongs_or_shared(idx, ws) Applied to every index analytics_query is asked for: a foreign index is refused before any request leaves the server. compliance_logs has no index argument at all.
the backend's OpenSearch proxy Authorises each request again against the caller's own token. Two layers, because a check in one service is a claim and the second one is a proof.

Own workspace plus an explicit shared list — nothing else. There is no default tenant: *-logrite indexes are refused like any other foreign index, and top_queries-* is not shared.

The family list lives in two languages. mcp-activity and compliance-logs are registered in the server’s WS_FAMILIES and in the backend’s IndexNames. A one-sided edit is silent in both directions: absent from the Java side, the OpenSearch proxy treats the family as “not a tenant index” and lets any workspace read it; absent from the Python side, the activity read answers empty about a workspace that is generating rows.

06

The activity document

15 fields, and the one that is deliberately absent

One document per JSON-RPC request, into mcp-activity-<workspaceId>.

{
  "ts":              "2026-10-05T11:02:00.621658+00:00",
  "workspaceId":     "252",
  "userId":          8,
  "credential":      "api_key",          // or "jwt"
  "keyLast4":        "cvEs",             // null for a JWT
  "method":          "tools/call",
  "tool":            "get_workspace_context",
  "argumentsDigest": "<sha256 of canonical JSON>",
  "ok":              true,
  "status":          200,                // 502 when the handler flagged the result
  "durationMs":      5,
  "resultBytes":     412,
  "error":           null,
  "client":          { "name": "claude-code/2.1.289 (sdk-cli)", "version": "" },
  "sourceIp":        "…"
}

The shape of the row a live claude session produced on this machine on 2026-10-05, read back through get_mcp_activity.

Arguments are hashed, never stored. The digest is sha256 of json.dumps(args, sort_keys=True, separators=(",", ":"), default=str) — reproduce it with a bare sort_keys=True and it will not match. A tool call carries customer log content and search terms; an audit index holding them is a second copy of the data with none of the retention rules of the first, in an index nobody thinks of as data. The backend strips any arguments field on the way out as a tripwire — its comment notes that a field appearing there later would mean somebody had started to.

Key lifecycle does not go here. api_key_minted / rotated / revoked are hash-chained GovernanceAudit rows in the backend. A coding session issues hundreds of tool calls a minute; hash-chaining that would be the wrong instrument.

client.name is the field that distinguishes an editor from a script: it reads x-mcp-client-name, falling back to the user agent. Absent for a raw curl — a fact worth recording rather than a reason to reject the call.

07

Skills, commands and hooks on a session timeline

what fires when, and what each one needs
▶
SessionStart → lr.sh session_start, then lr.sh flush (async)
Prints the workspace, frameworks and add-ons plus a line telling the model to use the MCP tools rather than guess; flush ships any spooled events.
↓
▶
UserPromptSubmit → lr.sh nudge, lr.sh prompt
If the prompt looks like an error and the repository has add-ons, names knowledge_search instead of letting the model answer from memory.
↓
◆
the model chooses a skill
Keyed on the SKILL.md description — quoted in full below, because that string is the trigger.
↓
▶
PostToolUse(Write|Edit|MultiEdit) → lr.sh edit
Scans only the lines the edit added, against the workspace's coding policy. Advisory, never blocking.
↓
▶
Stop → lr.sh stop, then lr.sh flush
Records the turn, scans every file it changed (including through Bash) and may ask the agent for one fix round.
↓
■
SessionEnd → lr.sh session_end
Spool only.

Every hook fails open, and every hook needs .logrite/config.json. lr.sh exits 0 silently with no python3, and lr_hook.py swallows every exception. A hook that blocks a developer’s edit because a SaaS was unreachable gets the plugin removed, and the developer is right to remove it. The corollary is that an unenrolled repository produces silence, not an error.

The four skills — descriptions quoted verbatim

Skilldescription — the string the model matches onTools, in the order the file states
logrite-compliance “Use when a change touches audit logging, access control, data handling or retention in a Logrite-enrolled repository, or when the user asks which compliance frameworks apply, what a finding means, or whether a change will pass the pipeline.” list_compliance_frameworks first → run_compliance_scan. “If the call fails, say the frameworks could not be read. Do not fall back on the standard.”
logrite-logging “Use when writing, reviewing or changing log statements in a repository enrolled with Logrite — choosing a level, deciding what to include, avoiding PII, and adding the correlation fields the workspace's compliance frameworks require.” workspace_settings + list_compliance_frameworks before writing → analyse_log_file to verify
logrite-observability “Use when investigating a production problem, reading a log file, deciding what to instrument, or answering questions about what a service is doing — search, log analysis, and getting from a symptom to evidence with Logrite.” A selection table, no fixed order: analyse_log_file, semantic_search, analytics_query, knowledge_search, cortex_chat.
worksoft-certify “Use when the question concerns Worksoft — Certify processes, recordsets, layouts, CTM suites, the Certify Results API, Worksoft Portal, Business Capture, Process Intelligence, Impact, or a log line from a Worksoft .NET service.” workspace_addons → knowledge_search with packs:["worksoft"] → relay the citation and the attribution line. Shipped only when the workspace has the worksoft-cortex add-on.

The commands

CommandWhat it doesTools it names
/logrite:initEnrol this repository. When enrolment is refused (400/409) or there is no remote, writes a local-only config with "projectId": null and points at Settings → Workspace → Source control → Link existing installationget_workspace_context, enrol_repository, list_compliance_frameworks, workspace_addons, coding_sessions
/logrite:log-reviewReview the diff's logging for coverage, level and PIIlist_compliance_frameworks, workspace_settings
/logrite:compliance-checkScan local changes, then optionally the pushed branchlist_compliance_frameworks then run_compliance_scan
/logrite:explain-errorExplain an error or log line, grounded in the enabled packsknowledge_search
/logrite:add-loggingAdd log statements in the workspace’s conventionslist_compliance_frameworks, workspace_settings
/logrite:fixApply the proposed fix for a Logrite AI coding issuecoding_sessions
/logrite:privacyShow what Logrite records about AI coding sessions here, and how to opt outnone
/logrite:uninstallUndo everything /logrite:init wrote in this repositorynone

One init command

Type /logrite:init. The MCP prompt init (/mcp__logrite__init) serves the same text: the prompt reads plugin/commands/init.md on every call, so the two cannot drift.

What /logrite:init writes: .logrite/config.json and, only on an explicit yes, a write-only session token in ~/.config/logrite/coding/ and a warn-only git pre-commit block. Hooks: nothing to write. They are active as soon as the plugin is installed. The key is never written into the repository — with the installer it lives in ~/.logrite/api-key — and .logrite/config.json is meant to be committed.

08

Where each workflow shows up in the app

route · component · endpoint · what is not shown
WorkflowScreenReadsVisible / not visible
API key lifecycle /sandbox → API keys drawer
the drawer has no route of its own
/api/v1/api-keys, …/{id}/rotate, DELETE …/{id} Label, prefix…last4, Created, Created by, Last used, Expires. Not the plaintext after mint, nor per-key call counts.
Every tool call /sandbox/mcp → Activity GET /api/v1/mcp/activity ← mcp-activity-<ws> Six columns: When / Tool / Client / Duration / Outcome / Credential. Not userId, sourceIp, argumentsDigest, status or resultBytes — all present in the document, none rendered. No paging.
Cortex add-ons three surfaces: /settings/addons, the Sandbox playground card, and the chip on /analytics/logrite-cortex GET|PUT /api/v1/workspaces/{id}/addons One shared hook behind all three. Nothing MCP-specific: the UI never shows that a tool call consumed an add-on.
Key create / rotate / revoke /m26-14 → Operate → Governance Audit GET /api/v1/governance/audit Newest 12 only, as action · actor and a timestamp. details, rowHash and prevRowHash are persisted and never rendered; the only chain affordance is a “Verify chain” button.
run_compliance_scan /compliance/project/:id/scan/:scanId POST /api/v1/scan/initiate → /api/v1/scan-runs/latest Appears as an ordinary scan row. Unattributed — nothing says Claude Code started it.
ingest_log_lines /analytics/discover the log-analytics-<ws>-<agentKey> documents Unattributed by construction: the tool sends only {"message": line} and the shim derives workspace, service and index from the agent token and from nothing else. There is no field a screen could key on.
enrol_repository the project list on /complianceGET /api/v1/project/workspace/{ws} The enrolled repository appears as an ordinary project. Unattributed.
analyse_log_file nowhere— No collection GET exists for log analyses, and the UI never reads one by id either.

“Last used” advances on every successful credential resolution — not on every call. A call rejected for a revoked or unknown key leaves it untouched, deliberately: a failed guess must not make a dead key look alive.

One empty state on the Activity tab is wrong, and it is the one workspace 252 hits. The “this workspace has no API key yet” branch is driven by whether this browser session minted a key, not by the workspace’s key list. On a workspace whose key was minted elsewhere — the normal case for a long-lived integration key — an empty filtered result renders “no API key yet” rather than “no tool call matches this filter”.

09

Defect register

nine found
#WhatEvidenceState
D1The documented install left skills, commands and hooks inert everywhere claude plugin list named only the synced plugin; enabledPlugins {} fixed
D2The advertised marketplace repo does not exist gh api repos/Deployd-io/logrite-claude-plugin → 404 fixed
D3list_compliance_frameworks is JWT-only, so an API-key caller can never read the framework library — and three skills plus three commands open by calling it Live on prod today: “could not be read — the compliance library is served by the Warden control plane, which accepts only a signed-in session (a JWT)” open
D4semantic_search defaulted to an index that does not exist was no such index [log-analytics-252]; collectors write log-analytics-252-<agentKey> fixed
D5…and reported 0 hit(s) over a populated result set, because the Response<T> envelope was never unwrapped before counting prose said 0 hit(s) beside 10 rows in structuredContent fixed
D6A downstream error came back with isError: false the no such index reply carried "isError":false fixed
D7enrol_repository appeared to post to a route prod did not serve Prod now answers POST /api/v1/project/enrol — a repository without the GitHub App gets the backend's 409 “GitHub App is not installed” sentence, which only the served route produces. fixed
D8run_compliance_scan could not pass validation — it posted to the Gatekeeper CI route, whose DTO shares no field name with what the handler sent Now posts POST /api/v1/scan/initiate, which prod’s backend serves. The Gatekeeper route was abandoned deliberately: it wants a CI diff payload and can open pull requests. fixed
D9semantic_search discarded the caller’s result count handler sent limit; the service declares k and an unknown body field is dropped, so limit:1 returned 10 rows fixed

Verified on prod after the 2026-10-05 promote

semantic_search {"query":"error"}                      → 10 hit(s) … in log-analytics-252-*   (D4, D5)
semantic_search {"query":"error","limit":3}            → 3 rows in structuredContent          (D9)
semantic_search {"query":"error","index":"does-not-…"} → isError: true                        (D6)
knowledge_search {"packs":["worksoft"]}                → isError: true, ADDON_NOT_ENABLED,
                                                         the backend's own wording preserved

D3 costs more than its size suggests. logrite-compliance opens with “Call list_compliance_frameworks before answering anything”, logrite-logging requires it too, and /logrite:compliance-check, /logrite:add-logging and /logrite:log-review each name it first. The refusal is at least honest and explains itself — but until that route accepts an API key, the compliance half of this integration reads its frameworks in the browser or not at all.

An add-on refusal is flagged, and keeps the backend’s words. A 403 from knowledge_search arrives as isError: true carrying ADDON_NOT_ENABLED and the backend’s exact sentence — “This workspace has not enabled Worksoft. Turn it on in Settings → Cortex add-ons…”. The reasoning is that the question was not answered, and an unflagged refusal reads as an answer; rewording it into “no results” is how an entitlement problem becomes an unanswerable support ticket.

10

The ops account

workspace 252 · prod only
Workspace252 — logrite-ops, owned by the ops service account, userId 8. The address is deliberately not written out: Cloudflare's email obfuscation rewrites any user@host literal into a /cdn-cgi/l/email-protection link and injects an external decoder script — which would both break this page's one hard rule and leave the address unreadable with JavaScript off.
Credentialclaude-code-local, last four cvEs, scope production, expires 2027-09-29. Prod only — the same key is 401 on the dev backend, which is correct and not a fault.
Collectorslogrite-prod-vm and logrite-dev-vm under ~/logrite-agent/, in the crontab every five minutes
Enrolled repoLogriteMasterControl — .logrite/config.json with projectId: null: a local-only enrolment, the shape /logrite:init writes when the backend refuses enrolment.

The key is never printed anywhere. Not in this page, not in the installer’s output, not in a prompt body — a prompt body ends up in a transcript, and a transcript is copied into tickets. The last four characters are an identifier; the key is not.