API Sandbox
Overview
Section titled “Overview”Sandbox is a top-level page in the product UI (/sandbox on app.logrite.ai and
dev-app.logrite.ai). It exists because, until now, nothing in the product showed the
ingest → index → retrieve → chat loop working. The Documentation page said “Documentation
Coming Soon”, the Support FAQ said “There is no published API reference yet”, and the only
interactive path we offered a prospect was “import this Postman collection”.
It does four things, and it is only worth anything if it does all four:
- shows what each API does, with required and optional parameters separated;
- runs it, against the caller’s own workspace, with credentials already filled in;
- returns a readable response, including the error contract;
- hands over the code — cURL, Python, Node.js or Go, generated from the same request object the button executed.
Three tabs, URL-addressable as /sandbox/<tab>: Playground (playground), MCP
(mcp) and API reference (reference). API keys are managed from the Keys and cleanup
drawer, opened by the API keys · N active header button, not a tab. A bare
/sandbox or an unknown segment opens the playground. /docs redirects to
/sandbox/reference.
The page stays mounted (2026-10-07)
Section titled “The page stays mounted (2026-10-07)”Leaving a tab, pressing browser Back/Forward, or switching between Playground, MCP and API reference no longer wipes the conversation or the Created in this session list.
- One route key for every tab.
DashboardLayoutkeys itsRouteErrorBoundaryonrouteKey(pathname)(src/utils/routeKey.js). Every/sandbox/*path collapses to/sandbox, so a tab change is no longer a remount. Every other route still keys on its full pathname. - Every tab body stays mounted.
ApiSandbox.jsxrenders all three tabs and hides the inactive ones; a hidden tab keeps its state and any stream it is reading. - The timeline survives a reload. It is written to
sessionStorageunderlogrite:sandbox:transcript:<workspaceId>(per browser tab, per workspace) together with the current conversation id, and restored on mount. Bounds: at most 50 turns and 256 KB of JSON, dropping the oldest turns first. A turn that was still running when the page went away is restored as aborted, not as a spinner. - No credential is ever stored. Request headers are dropped from every stored turn, a
collector flow’s one-time agent token and install one-liner are dropped, and every remaining
string is scrubbed of anything shaped like a Logrite key (
lgp_/lgs_), an agent (lgca_) or coding (lgsi_) token, or a JWT — replaced with[redacted]. The live credentials stay in module scope and die with the page. - Conversations are recorded. Each new Cortex conversation is added to Created in this
session (kind
conversation, labelled with the first 60 characters of the message) so it can be removed.
The credential the Sandbox runs on
Section titled “The credential the Sandbox runs on”Since 2026-09-29 the Sandbox presents a Logrite API key, not the browser session, for
every operation that accepts one. A paste box at the top of the page takes an lgp_live_… key —
including one this browser never minted, which is the normal case, since the plaintext exists
only in a create/rotate response.
The key is checked when you press Use key (or Enter), against
POST /api/v1/api-keys/introspect presenting that same key, and only a key the server accepted
is adopted. A pasted key beats one minted in the same session, because pasting is an explicit
act. Nothing is persisted: it lives in module scope and dies with the page’s JS context.
When no key is active, operations fall back to the login session — one shared rule,
operations/resolveAuth.js (API key when one is active, otherwise the session token). What
changed on 2026-10-07 is that the fallback is no longer silent:
- The box is never auto-filled. The input is
type="password"withautocomplete="new-password",data-1p-ignore,data-lpignore="true"anddata-form-type="other". Previously a password manager refilled it after a reload, showing a key the page was not using while every call went out on the session. - Text in the box that is not in use says so: “Not active yet — press Use key”.
- Every turn carries a credential chip naming what it was actually sent with — never the credential itself:
| Chip | When |
|---|---|
Sent with API key …last4 | an API key was presented (its last four characters) |
Sent with your login session (no API key active) | no key was active, so the session was used |
Sent with your login session (this endpoint takes no API key) | a key was active, but the route is session-only |
Sent with the collector agent token | the ingest shim call |
Above the timeline, the identity bar names the credential the next call will use: Next call: API key ••••last4 (with (pasted) for a pasted key), Next call: your login session (no API key active), or Next call: no credential — sign in or paste a key.
The identity bar’s isolation note reads: “Requests go to the hosts above. Anything you create
lands in your own workspace … — not a separate demo tenant — and is listed under Created in
this session so you can remove it. Logs you ingest are written to …; JSON records carry
sandbox: true.”
Why not the session token: a JWT is minted by signing in, is short-lived, and cannot go in a CI
secret. It is the one credential a reader cannot reproduce outside the tab — so a playground
running on it produced snippets that worked on screen and 401ed in a terminal. Snippets
print the credential as an environment variable ($LOGRITE_API_KEY) by default. The session is still what
mints admin keys and keys that never expire (a key may manage keys only within the limits in
managing keys with a credential),
what mints the upload URL, and the fallback described above when no key is active.
| Credential | Prefix | Verified by | Used for |
|---|---|---|---|
| Logrite API key | lgp_live_ | Spring backend (SandboxApiKeys), and by ai-service / semantic-search via introspection | every Sandbox operation that accepts one, the REST API and the MCP server |
| Session token (JWT) | — | backend, ai-service, semantic API | creating, rotating and revoking keys (any access, any lifetime); minting the upload URL; the fallback when no key is active; anything else in the browser |
| Collector agent token | lgca_live_ | ingest shim | POST {ingest}/ingest only |
Logrite API keys
Section titled “Logrite API keys”POST /api/v1/api-keys · GET /api/v1/api-keys · POST /api/v1/api-keys/{id}/rotate ·
DELETE /api/v1/api-keys/{id} · POST /api/v1/api-keys/introspect
Managed from Sandbox → API keys. Workspace-scoped, resolving to the user who created it,
so every existing OwnershipService guard — membership, role, project visibility — evaluates
exactly as it would for that person. Stored as a deterministic SHA-256 digest; the plaintext is
returned once and the list endpoint returns only the last four characters.
The caller chooses the lifetime: 30 days, 90 days (the default), a year, or no expiry. A workspace may hold 25 active keys.
scope names the estate the key belongs to (2026-10-01): development for a key minted on
dev, production on prod (staging is reserved for a staging- estate). It is derived from the
backend’s own baseDevApplicationPath, so no setting changed. Before this every key — dev’s
included — said production, and a developer holding one of each could not tell them apart
from the create, list or introspection answer. A key only ever works on the estate that minted
it; the scope is a label of that fact, not a permission.
Rotation replaces the plaintext and revokes the old row in the same transaction. A rotation that leaves the old key working is worse than none, because it is believed: the holder is told the credential is dead, stops protecting it, and it keeps working.
Only a live key rotates. Rotating a revoked, already-rotated or
expired key answers 409, with the message
"API key <id> is revoked or expired and cannot be rotated. Create a new key instead."
Before this, rotating a dead row minted a fresh live credential from it, so
revocation was not final and rotating one row twice left two live successors. The successor
also keeps the original creator’s user id: a key acts as the person who created it, and
stamping the rotator’s id silently changed whose projects, role and visibility it carried. Who
rotated it is in the api_key_rotated audit row. The drawer hides Rotate on dead rows and shows
the 409 message if a stale list lets one through.
Creating, rotating and revoking require the Admin or Owner role; any member may read the
list. A production credential that acts as a person in the workspace is an administrative
decision, not an individual one. Every one of the three writes a GovernanceAudit row
(api_key_minted / _rotated / _revoked) carrying the id, last-4, label and expiry — never
the plaintext and never the hash — and since 2026-10-14 via: session, api_key:<last4> or
connection:<clientName>.
What a Member sees. Anyone below Admin (Maintainer, Developer, Viewer) still sees every key
in the workspace — name, last four, creator, expiry, status — but Create, Rotate and
Revoke are disabled, with the note “Creating an API key requires workspace Admin or Owner.
You can still see every key in this workspace below.” Called directly, the API answers 403
("creating an API key requires workspace admin" — likewise rotating / revoking).
Lost a key? Rotate it. The plaintext is shown once and cannot be retrieved — the server keeps only a hash. Rotate issues a new plaintext for the same row and the old one stops working immediately, in the same transaction; update whatever held it. If the key is already dead, create a new one instead (rotation answers 409).
The 25-key cap. Creating a 26th active key answers 409
"This workspace already holds 25 active API keys. Revoke one before creating another."
Revoked, rotated-away and expired keys do not count. Rotation works at the cap: it revokes
the old key and mints its successor in one transaction, with the key being replaced left out of
the count — a rotation never changes the live count, so refusing it at 25 (as it used to) blocked
the one operation a full workspace most needs. A failed mint rolls the revoke back.
The drawer asks before it kills a key. Revoke and Rotate open a confirmation naming the key
by label and last four — “Revoke ‘ci-key’ (…KrFM)?” — because a workspace routinely holds
two keys called ci. Cancel calls nothing. Creating a key whose label matches an active key
shows a non-blocking warning; two keys may share a label, but you are told.
A pasted key from another workspace says so. When the key in the identity bar introspects to a workspace other than the one the browser is signed into, the bar warns that requests will run against the key’s workspace while the page’s index pickers were filled from the session’s. Non-blocking, and silent when either workspace is unknown.
Which endpoints accept an API key
Section titled “Which endpoints accept an API key”Measured on dev on 2026-10-01 with a freshly minted lgp_live_ key in
Authorization: Bearer (the only header that carries it — X-API-Key is never read):
| Accepts the key | Route(s) | Measured |
|---|---|---|
| AI Service | POST /api/v1/log-analyses, POST /api/v1/chat/sync, GET /api/v1/knowledge/packs, POST /api/v1/knowledge/ask | 200 |
| MCP server | POST /sse (every tool), GET /skills-package | 200 |
| Semantic Search | POST /api/v1/search, /search/all, GET /api/v1/indexes (and /indexes/{name}), GET /api/v1/models | 200 (/indexes and /models since 2026-10-01 — before that they answered 401 jwt malformed to a key) |
| Backend — projects & scans | GET /api/v1/project/workspace/{id}, POST /api/v1/scan/initiate, GET /api/v1/scan-runs/latest, GET /api/v1/scan-results/{id}/summary, GET /api/v1/scan-results/workspace/{id}, POST /api/v1/ide/check | 200 (ide/check: reaches the handler) |
| Backend — data | /api/v1/opensearch/** (e.g. GET /api/v1/opensearch/_cat/indices), GET /api/v1/agents, GET /api/v1/workspaces/{id}/addons | 200 |
| Backend — machine-caller routes | GET /api/v1/project/{id}, POST /api/v1/project/enrol, GET /api/v1/warden/library/frameworks | 200 (enrol: 409 with an installUrl when the GitHub App is not on the owner) |
| Backend — the key itself | POST /api/v1/api-keys/introspect | 200 |
Many backend management routes need a signed-in session. Measured examples:
GET /api/v1/access/grants, GET /api/v1/ai-agents and GET /api/v1/agents-flow/picker/{id}
answer 401 "this endpoint does not accept API keys — sign in and use your session" to a
key (measured on dev 2026-10-01) while the same call with a session JWT is 200 — they parse the
bearer as a JWT by hand. Key management (/api/v1/api-keys other than /introspect) answered
403 "an API key cannot manage API keys — sign in and use your session" until 2026-10-14;
it now follows the credential rules.
The Warden, governance, maturity and observability routes answered 401 to every key on both
estates until 2026-10-14 (the framework catalogue GET /api/v1/warden/library/frameworks was
the one exception); they take a key since, scoped to the key’s workspace.
Project routes for machine callers
Section titled “Project routes for machine callers”Both accept a session JWT or an API key. A key stays inside the workspace it was minted in: a project in another workspace of the same person is “not found”.
GET /api/v1/project/{id}→{status, data}wheredatacarriesid,name,fullName,workspaceId,repoUrl,platform,branches[],scannedBranch,createdAtandlatestScan(id,branchName,branchLink,runDate,resultand the four severity totals). 404 when the project does not exist or the caller cannot see it (project-scoped members included) — the two are deliberately indistinguishable.POST /api/v1/project/enrolbody{repoUrl | repoFullName, branch?, workspaceId?}→data=projectId,projectFullName,created,workspaceId(andbranchif sent). The workspace is the key’s own (a differentworkspaceIdis 403), else the explicit id, else the JWT’s workspace claim, else the user’s only workspace; membership is required. An already-enrolled repo is returned withcreated: false. Otherwise the workspace’s GitHub App installation on the repo owner is used (ensureProjectForRepo, sourceapp); none → 409status: "FAIL"withdata=owner,repoFullName,installUrl— the last beinghttps://github.com/apps/<github.app.slug>/installations/newwhen the slug is configured. A GitHub token in the body (oauthToken/token) is refused with 400 — enrolment never takes one. Only github.com references parse (owner/name, https, ssh).
The playground
Section titled “The playground”One operation picker, one request builder, one timeline, one composer.
┌───────────────────────────┬─────────────────────────────────────────┐│ Operation ▾ │ Conversation <id> + New conversation ││ Scenario ▾ │ ┌─────────────────────────────────────┐ ││ [ Form | Code ] │ │ POST /api/v1/chatv2 HTTP 200 4.1 s │ ││ │ │ …answer, Activity ▸, Raw events ▸ │ ││ required fields │ └─────────────────────────────────────┘ ││ Show optional (16) ▸ │ ┌─────────────────────────────────────┐ ││ What this endpoint does ▸│ │ Analyse log files — split │ ││ │ │ 13 uploads · 1 dataset · 13 analyses │ ││ [ Run ] ⌘⏎ │ │ Continue in chat ↩ │ ││ │ └─────────────────────────────────────┘ ││ │ [📎] Ask a follow-up… [➤] │└───────────────────────────┴─────────────────────────────────────────┘The operation registry
Section titled “The operation registry”src/components/ApiSandbox/operations/index.js exports OPERATIONS, an ordered array where
every entry has the same shape. Adding an endpoint is an entry, not a page.
| Field | Meaning |
|---|---|
id, label, group | picker text; group renders as an optgroup |
auth, resolveAuth(credentials) | nominal credential, and the one actually presented |
kind | stream, request, upload or flow — picks the runner |
fields(ctx), defaults(ctx) | RequestPanel field objects and their initial values |
presets | [{id, label, hint, values}] — the Scenario picker |
descriptor(values, bases, auth, ctx) | the one object runRequest executes and buildSnippet prints |
blockers(values, credentials, auth, ctx) | a string or null, shown on the disabled Run button |
notes | prose and tables, in a collapsed What this endpoint does |
continuation(values, result) | seeds Continue in chat |
The operations are chat, analyse-upload, knowledge-ask, collector-ingest,
public-upload-url, os-search, semantic-search / -all / -hybrid, and agent-create.
The timeline
Section titled “The timeline”Every runner writes into one useTurns store, so a streamed chat turn, a query, a 250 MB
upload and an agent flow appear in the order they happened rather than in four places.
Turns are patched by id, never by index: a streamed turn is updated while later turns are
being appended, and an index would name a different turn the moment one lands in between.
Each card is METHOD /path · status · duration, then the body, then collapsed disclosures —
Activity (tool_use_start / tool_use_result / trace_step) and Raw events (every
frame verbatim with elapsed ms and time-to-first-token). Counts sit in the summary line, so
“there were 41 frames” is visible without expanding.
Every non-chat turn carries a Continue in chat button. It switches to Cortex chat and
pre-fills a message from the operation’s own continuation — analyse-upload names the
uploaded file and passes its analysis_id, os-search names the index — because only the
operation knows which handle in its own result is worth naming.
Continuing a conversation
Section titled “Continuing a conversation”The composer sends a follow-up into the same conversation. That is new, and it was impossible before:
Chat has one input: the composer. Its Send is the only run control, so the Run card is
hidden while chat is selected, and the form holds no message field. The remaining fields sit
in a collapsed Advanced group — indexName, projectId, conversation_id and
analysis_id. The conversation_id field is read-only, with a hint, while a conversation is
open. The placeholder becomes Ask a follow-up only once a conversation exists.
A New conversation control clears the timeline and the handle. The conversation id is printed above the timeline, so “is this one conversation or four” is answerable on screen.
Attachments
Section titled “Attachments”The composer mounts useAttachments and renders AttachmentChips — the same two modules the
product’s own Cortex composer uses. That is multi-file and multi-image, with per-file
progress, per-file failure, retry, paste-a-screenshot, the folder refusal and
server-sourced limits, for the cost of two imports. Ready ids are sent as
attachment_ids; chatPayload accepts an array directly as well as the comma-separated text
the optional field still offers.
When GET /api/v1/attachments/config/limits reports the feature off, the paperclip stays
disabled with the server’s reason. No client-side limit is invented to fill the gap.
The composer uploads with your session. Outside the browser, POST /api/v1/attachments and
DELETE /api/v1/conversations/{id} accept a Logrite API key as well as a session JWT.
The composer is disabled for every operation but chat, and says why: “<label> is a one-shot call — press Run.” A greyed control that does not say why reads as a bug.
Uploading log files
Section titled “Uploading log files”Analyse log files is the one operation whose left pane is a drop zone rather than a form.
operations/planUpload.js decides the transport before anything is sent, purely from
{name, size} and the limits the service published. It invents no endpoint: batch and
split are the single-request POST, and chunked is ai-service’s own resumable door —
see Log analysis API for the protocol.
| Selection | Mode | Result |
|---|---|---|
total ≤ PART_BUDGET | batch | 1 request, 1 batch, 1 analysis |
many files, total > PART_BUDGET | split | N sequential requests, all pinned to one dataset |
one file > PART_BUDGET | chunked | POST …/uploads/sessions, one request per part, 1 analysis |
one file > maxTotalBytes, or total > maxBytesPerRequest | refuse | nothing sent, quoting /limits |
Verified end to end on dev, 2026-09-25: 6 files / 42 MB as one batch (456,378 events);
10 files / 250 MB as a split into 4 parts, all four reporting the same dataset
log-analytics-3103-sandbox-20260925-145913 (2.7M events); one 150 MB file through the
chunked door as 10 parts of 16 MB (1,630,056 events). Same panel, three transports.
Everything else is read, never hard-coded. On mount the panel fetches
GET {ai}/api/v1/log-analyses/public-url (JWT) for the signed segment, then
GET {ai}/api/v1/log-analytics/{segment}/limits (no credential) for the ceilings, the
accepted formats, the chunkedUpload block and notes[]. Those notes are rendered
verbatim — they are where the service states the edge cap and the ~2 minute synchronous
cut in its own words, and restating them in the client is how the two drift.
A file above chunkedUpload.analyzableBytes (256 MB) is not refused: the service stores
it whole and analyses its tail, and says so. The panel repeats that as a warning before the
upload starts, because a report about the last 256 MB of a file reads exactly like a report
about the file.
Running, re-running and managing uploads (2026-10-07)
Section titled “Running, re-running and managing uploads (2026-10-07)”- Sizes are human-readable. Every size in the upload panels goes through
src/utils/formatBytes.js:Bbelow 1 KB, otherwiseKB/MB/GBwith one decimal — a 2 KB log shows 2.0 KB (it used to read “0.0 MB”, which says the file was empty). - One press is one upload. A run already in flight ignores a second Run, and ⌘⏎ / Ctrl+⏎
does nothing while anything is running. Each run mints one key and every request in it is
sent with
Idempotency-Key: <run-key>:<part index>. When the run succeeds the selection is cleared, so the same files are not sent again by accident. - The same file is not analysed twice. When the service recognises the upload, the turn
says “Same file as your upload on … — showing that analysis” and offers Analyse again as
a new copy, which re-sends the same files with
force=true. - Uploads are registered like an Analytics upload. A fresh (non-reused) dataset goes
through the same
registerUploadedIndexas the Analytics upload card, so it appears in the workspace’s upload history and index pattern, not only as a Sandbox search source. Each fresh upload run is also listed under Created in this session; a run that reused an existing analysis is not. - Upload snippets run as pasted. The Code view for an upload sends
multipart/form-datain all four languages (curl, Python, Node, Go) and reads the target URL from$LOGRITE_UPLOAD_URL. Required header parameters are emitted; a raw-body endpoint getsX-Filename: app.logand--data-binary @app.log. - Your uploads. A panel lists every file the workspace has had analysed, read from
GET {ai}/api/v1/log-analyses?limit=50(Name, Size, Date, Findings), and refreshes when an upload finishes. Delete opens a confirmation — “Delete this upload?”, naming the file, its size and dataset, “…and the stored file are removed. This cannot be undone.” — then callsDELETE {ai}/api/v1/log-analyses/:id(204; 404 for an unknown or another workspace’s id). A file kept from a chat attachment is listed too, with Findings “Kept from chat (not analysed)”; the list also refreshes after a chat turn that carried an attachment, and the dataset appears under Created in this session as Uploaded file (frontenda606a7ef). A dataset the attachment reused — identical bytes another upload already stored — comes back withdataset.reused: trueand is not listed there, so Remove everything never deletes data this session did not create (ai-service17bca63, frontend95a99864). New conversation also clears attachments that were picked but not yet sent. - Bold renders, stray
**never shows (frontend6894b2bb). The model often wraps inline code in bold —**`PaymentService.charge(…)`** — the crash site— and the renderer paired markers only between code spans, so the**were printed. Code spans are now opaque while bold is paired; an unpaired marker is dropped (an exponent such as2**10is kept), and**inside inline or fenced code stays literal. - Continue in chat after an upload sends
Explain the findings from <fileName>.with the stored analysis asanalysis_idand the dataset pinned asindexName, so Cortex answers from the findings already made.
Cortex chat
Section titled “Cortex chat”Targets POST {ai}/api/v1/chatv2 — the endpoint the product’s own chat uses. /api/v1/chat
and /api/v1/chat/sync are listed in the reference tab and given no Run button: both open
with a DEPRECATED warning in the client, and a playground that runs the deprecated path teaches
the deprecated path.
Transport is fetch + the app’s single SSE reader (Analytics/services/aiService/sseStream).
Never EventSource — it is GET-only and cannot set an Authorization header.
Each assistant turn carries three things, the last two collapsed:
- Answer —
content_deltalive, then replaced bymessage_end.content. They are not always identical; the end frame is what is stored and what a reload shows. - Activity —
tool_use_start/tool_use_result/trace_step. A trace frame is{phase, record}, where the record carriesturn,step,depth,status,offsetMs. - Raw events — every frame verbatim with elapsed ms, plus time-to-first-token.
Measured on dev 2026-09-24 for “List the indexes I can read.”: message_start ×1,
content_delta ×247, content_block ×1, trace_step ×4, message_end ×1, first token at
~1.2 s. There is no [DONE] sentinel and no heartbeat — streaming.service.ts runs no
timer, so a long tool loop goes quiet. The page shows a “still working” state after 6 s and
gives up at 180 s.
Scenarios (2026-10-07)
Section titled “Scenarios (2026-10-07)”- Recent errors (
recent-errors) — hint: “Searches this workspace’s application logs; says so when there are none”. - Explain a stack trace (
explain-trace) now sends a complete example Java stack trace, whose first line is “Example — replace with your own trace” — hint: “Comes with an example trace — replace it with one from your own system. Nothing is stored beyond your conversation.” - A message that ends in a colon blocks Run with “Paste a stack trace after the colon” — a prompt that introduces something and then stops is a paste that never happened.
Blocks, sources and the length nudge (2026-10-07)
Section titled “Blocks, sources and the length nudge (2026-10-07)”content_block frames — sources, tables, follow-ups — render through the product chat’s own
BlockRenderer, so a knowledge answer shows the same source links in the Sandbox as in Cortex.
When the last answer’s metadata.conversation_soft_limit_turns is at or below the number of
user turns, the timeline shows (once the answer has finished): “This conversation has N turns
— Cortex suggests a fresh one after M, so answers keep their context sharp.” with a New
conversation button. The rule is the product chat’s (softLimit.js), so both surfaces agree.
Code snippets (2026-10-07)
Section titled “Code snippets (2026-10-07)”Every snippet must run as pasted, with no packages:
- cURL —
-N -sSfor a stream. - Python 3, standard library only —
urllib.request; the body is encoded to UTF-8 bytes and every stream line decoded as UTF-8, and only thedata:payloads are printed. (The old snippet usedrequests, which is not on a stock Python and decoded the stream as ISO-8859-1.) - Node.js 18+ — global
fetch, wrapped in an async IIFE (top-levelawaitfailed as a CommonJSsnippet.js), exits on!res.ok, prints thedata:payloads. - Go — a complete
package mainprogram (go run main.go); the stream scanner’s buffer grows to 4 MiB per event andsc.Err()is checked.
The credential is an environment variable ($LOGRITE_API_KEY, os.environ[…],
process.env.…, os.Getenv). A snippet never carries the browser’s conversation_id —
a terminal call starts its own conversation — and a streamed snippet says how to continue one:
“To continue a conversation, add “conversation_id” from the message_start event”. The stream
is served as text/event-stream; charset=utf-8.
Trimmed Python for Cortex chat:
# Python 3, standard library only — Cortex streams Server-Sent Events, read line by line# To continue a conversation, add "conversation_id" from the message_start eventimport jsonimport osimport sysimport urllib.errorimport urllib.request
body = json.loads("{\"message\": \"List the indexes I can read.\", \"agent_mode\": \"auto\"}")
req = urllib.request.Request( "https://…/api/v1/chatv2", method="POST", data=json.dumps(body).encode("utf-8"), headers={ "Authorization": "Bearer " + os.environ["LOGRITE_API_KEY"], "Content-Type": "application/json", "Accept": "text/event-stream", },)try: with urllib.request.urlopen(req) as resp: for raw in resp: line = raw.decode("utf-8").rstrip("\r\n") if line.startswith("data:"): print(line[5:].strip(), flush=True)except urllib.error.HTTPError as err: print(err.code, err.read().decode("utf-8"), file=sys.stderr) sys.exit(1)Ingestion
Section titled “Ingestion”The collector-ingest operation’s notes name all five paths — including the webhook receiver on 9999,
which is a deploy webhook and not log ingestion (omitting it reads as an oversight).
The operation has a Run button. The ingest shim answers CORS preflights from the app’s origins, so the browser can post lines straight to it. OTLP/gRPC is gRPC only and has no runnable form; it is named in the notes rather than given a control that can never succeed.
The ingest host is read out of the agent’s own rendered install script
(LOGRITE_INGEST_URL in collector.sh) rather than guessed from a hostname, so by construction
it is the value the customer’s machine will use.
Aggregation examples use severityText.keyword. severityText is a text field with a
.keyword sub-field, and aggregating the analysed field returns 400 illegal_argument_exception
— the defect that broke Cortex’s own aggregations.
Collector agents
Section titled “Collector agents”Mirrors the shipped lifecycle exactly. POST /api/v1/agents requires only name and key
(^[a-z0-9][a-z0-9-]*$, unique per workspace); the Sandbox pre-fills a sandbox-<6 hex> key so
anything created here is identifiable afterwards. A workspaceId in the body is ignored —
the credential’s workspace always wins, which is the tenancy model in one sentence.
mintedToken, collectorScriptUrl and oneLiner are populated on create and rotate only;
every later read returns null for all three, so the panel says so rather than implying the
secret can be re-read.
Labelled as unimplemented rather than implied: retentionTier is stored but creates and
attaches no ISM policy, and the durable data-prepper sink is not wired — the shim writes
straight to _bulk.
Search & indices
Section titled “Search & indices”Four operations — the OpenSearch proxy and the three semantic endpoints — all with index
pickers. The tenancy rule is
shown as the proxy enforces it: log-analytics-<ws> and log-analytics-<ws>-* allowed,
log-analytics-<ws>* refused (no trailing hyphen also matches another workspace), bare * and
_all denied, cross-tenant 403.
The OpenSearch picker opens on the workspace’s whole family (log-analytics-<ws>-*): a
collector writes to log-analytics-<ws>-<agentKey>, so the bare index is usually empty and
opening on it returns hits.total: 0, which reads as “the API found nothing”.
API reference
Section titled “API reference”Read-only: every operation in five services (backend, ai-service, semantic, analyzer and MCP,
plus the MCP tool catalogue from x-mcp-tools), generated at build time by
scripts/build-api-reference.mjs from docs/api-catalog/openapi/*.json in MasterControl into a
pruned index, imported dynamically so only this tab pays for it.
Rows are grouped by flow, not by alphabet. The generated index stays sorted by tag (so
access-controller would lead the backend and sign-in would sit behind billing); the tab
reorders it at render time with src/components/ApiSandbox/reference/sections.js. Each
service is split into sections in the order a developer meets them — the backend runs Sign up
& sign in → API keys → Account → Workspaces → Repositories & projects → Scans → Code fixes & CI
gates → Rules → Log collectors → Analytics → Observability → Compliance → AI Warden → AI agents
& BYO LLM → Integrations → Billing → Demo requests — and inside a section the order a flow
calls them (sign up → verify → log in → refresh; create → list → read → update → delete).
Routes not pinned explicitly fall back to shallower path first, then GET → POST → PUT → PATCH
→ DELETE. A chip row above the table jumps to each section, and the MCP tools are listed in
the order an agent uses them (orient → ingest → search → analyse → comply → ask → audit).
It only reorders: a tag no section names lands in a trailing Other section rather than
disappearing, and the filter, the count and the row test ids are unchanged.
tests/unit/referenceSections.test.js asserts against the real index that every operation and
tool comes out exactly once and that every pinned route still exists, so a renamed endpoint
fails a test instead of sliding silently to the bottom of its section.
Sections collapse. Only the first section is open on arrival. Expand all and Collapse all each take one click and disable themselves when there is nothing left to do. A section’s heading row is its toggle. A jump chip opens its section before scrolling to it. While a filter is typed, every section with a match opens, because a match hidden in a collapsed section reads as “no match”. The count always describes the filter, never what is expanded.
Every expanded row shows a request and a response side by side (stacked below lg):
- the cURL command, with path parameters filled, required query parameters added and an illustrative JSON body
- an illustrative response, with the status code from the spec
Both panes are plain editable, resizable text with Copy, and Reset restores the
generated version. Values come from each field’s name and type
(snippets/referenceExample.js), so email reads [email protected] and an enum takes its
first member. Nothing is recorded from a live call, and the panel says so. A route whose
spec declares no response schema says that rather than inventing {}. The credential is
always $LOGRITE_API_KEY or $LOGRITE_JWT. The generator’s credential field wins when
present; otherwise the Auth label decides.
Controls and styling
Section titled “Controls and styling”The Sandbox uses the app’s own controls, not the browser’s:
CustomSelect: operation picker, select fields, key Lifetime, MCP Outcome- the
logrite-checkbox LogriteDatePicker: MCP activity From and To, with a new optionalisClearable- a one-click × on every text input, as
ListSearchFielddraws it
tests/unit/sandboxCustomControls.test.js fails if a native <select>, a date or radio
input, or an unstyled checkbox returns.
Spacing and colour follow Settings, AI Warden and M-26-14:
p-1 sm:p-3 lg:p-5page padding- a divided header with the tabs on their own row
- section headings with the orange accent bar
#373A41interactive borders with the orange focus ring- bordered tables with an uppercase head band and
px-4 py-2.5cells - semibold primary buttons with pressed states
The MCP tool entries carry family and the full inputSchema, which is what lets the MCP
tab render each tool’s arguments rather than a name and a sentence. They are emitted from the
MCP server’s own tool objects by scripts/dump-tools.py, so the reference cannot list a tool
the server does not serve — which it did, for five tools that never existed.
| Label | Where it comes from |
|---|---|
Public | backend /api/v1/auth/* and GET /api/v1/agents/{agentKey}/collector.sh |
Logrite API key or session JWT | KEY_ROUTES per service — backend /opensearch/**, /agents/**, /workspace/role, /scan/initiate, /scan-runs/**, GET /project/{id}, POST /project/enrol, /ide/check, GET /warden/library/frameworks; ai-service /chat/sync, /chatv2/**, /knowledge/**; semantic /search/**, /embeddings/**, /indexes/**, /models/**; every authenticated MCP route (the MCP server’s default credential is the key) |
Session JWT only (API key refused) | SESSION_ONLY_ROUTES — backend /api/v1/user/**, /api/v1/api-keys/**, /api/v1/mcp/connect/**, /api/v1/mcp/connections/** and GET /api/v1/workspaces (/api-keys/introspect is the exception: it is authenticated by a key). ⚠ Since 2026-10-14 the backend lets keys list /api-keys and /mcp/connections, and an admin key mint, rotate and revoke keys and revoke connections (rules); this label has not caught up |
X-Agent-Token (collector agent) | the collector’s connector fetch, /api/v1/agents/{agentKey}/connector — no session and no key, so the /agents key rule must not claim it |
Bearer JWT | everything else on the backend |
Every operation also carries credential — apiKey, jwt or null (no Authorization
header at all: public, signed-URL, optional-token and agent-token routes) — derived from the
label, so the Auth column and the example-request panel cannot disagree. An operation the
specs do not contain matches nothing: a route reaches the reference only once
docs/api-catalog/openapi/*.json in MasterControl lists it.
No Send buttons here, and that is a settled decision rather than an omission:
docs/api-catalog/author_backend_spec.py records that the catalogue is curated “not the raw
277-endpoint springdoc dump”, and Swagger UI already exists at /swagger-ui.html and was
rejected as the customer surface.
A third tab, /sandbox/mcp, covering the Model Context Protocol server as a product surface
rather than a line in the reference. Five sections:
-
Connect — two routes in, and the first needs no key at all. “Connect without a key” leads: the bare MCP URL plus the three-step path a customer follows in claude.ai (Customize → Connectors → Add custom connector), which runs the OAuth 2.1 flow and ends on the branded consent screen at
/connect/<requestId>. AConnected clients: Nline links to Settings → Connections, where any of them can be revoked. Below a divider, “Or connect with an API key” keeps the install instructions for the Claude Code CLI, the plugin marketplace and a generic JSON client, each a copy block. The key is a<YOUR_API_KEY>placeholder; if the session holds a just-created key there is a second button, “Copy with my new key” — offered only in the one moment that plaintext exists, and never as the default, because the default block is the one people paste into a ticket. -
Skills package — the Claude Code plugin, installed with
curl -fsSL https://mcp.logrite.ai/install.sh | sh(to pass a key, set it onsh:curl -fsSL https://mcp.logrite.ai/install.sh | LOGRITE_API_KEY=… sh; with no key the installer prompts, and an empty answer sets up OAuth — then run/mcp→ Authenticate). The zip download installs with three lines:Terminal window mkdir -p ~/.logrite && unzip logrite-claude-plugin.zip -d ~/.logrite/claude-marketplaceclaude plugin marketplace add ~/.logrite/claude-marketplaceclaude plugin install logrite@logrite-local -s userBelow that, the
/logrite:initwalkthrough lists exactly what it writes, so nobody has to run it to find out. -
Tools — the whole catalogue grouped by family, each row expanding to its JSON Schema with required/optional and defaults, and a Try in Playground link where a tool mirrors a Sandbox operation. Since 2026-10-14 each tool also says what each kind of connection can run (see MCP panels).
-
Activity — the audit trail: time, tool, client, duration, outcome and credential last-4, filterable by tool, date window and outcome. A long error is truncated in its cell, with the full text in the tooltip.
-
Troubleshooting — the three failures this design can actually produce (401, 503, and “add-on not enabled”), each with the exact check.
Full detail: MCP Server.
Cortex add-ons
Section titled “Cortex add-ons”Add-ons are knowledge packs, so their cards sit in the Playground’s Knowledge panel — one
card per add-on in the catalogue, such as the Worksoft add-on card. A disabled card sells the
add-on; an enabled one offers suggested prompts that load into the composer.
The card is generic: it renders whatever GET /api/v1/workspaces/{id}/addons returns and reads
every label from the descriptor, so a second add-on needs no UI work. The toggle itself lives
in Settings → Cortex add-ons, and the identity bar carries an Add-ons: row.
Full detail: Cortex add-ons.
Safety properties
Section titled “Safety properties”- Created in this session, in the Keys and cleanup drawer (opened from the
API keys · N active header button), lists every object a run made — keys, collector
agents, conversations and uploads — with per-row Remove using the real endpoints
(
DELETE /api/v1/agents/{id}?purgeData=true,DELETE /api/v1/conversations/{id}). The list is stored per workspace underlogrite:sandbox:created:<workspaceId>, so switching workspace never shows or removes another workspace’s rows. A row Remove and Remove everything both confirm first, with counts per kind and a note that agents are removed with their data. The result reads “N of M removed”; a row is dropped only on success or 404, and a failed row stays with its error. - Ingested documents are deliberately not deletable from that panel. They land in a real
index, and a delete-by-query issued from a playground is a worse idea than a
sandbox: truefield the customer can filter on. - Live credentials are held in module scope, never in storage — the stored timeline has its
headers dropped and credential-shaped text redacted. A tab switch no longer remounts the page
(
routeKeycollapses/sandbox/*), and the credentials die with the page’s JS context, which is the correct lifetime for a one-time secret.
Close-out changes (2026-10-09)
Section titled “Close-out changes (2026-10-09)”- API keys drawer fits a 1440×900 screen: five dense columns (Key, Created, Last used, Expires, icon-only Rotate/Revoke). “Show revoked and expired” and “inline credential” are plain checkboxes, unticked by default. The button reads “Manage keys · N in workspace”; the key bar says “none active in this tab”.
- Snippets use
$LOGRITE_API_KEYfor operations that accept an API key, even when the run used your session, and say so. - Add-on prompts fill the knowledge-pack question in place instead of switching to chat.
- Your uploads fits the rail (File, Findings, Actions) and has Continue in chat for analysed uploads.
- Search → chat. Continuing a search result in chat sends the top 10 hits as
context_results({id, score, line?, time?, text}≤ 300 characters), so Cortex answers about those results. - Search help reads “Plain language. Matched by meaning where the index has embeddings;
otherwise by words.” and the result pane shows the
KEYWORD_FALLBACKwarning. - The upload turn lists every finding (title, severity, count), expanded when there are 10 or fewer.
- The MCP tab connects Claude Code with the installer (
curl -fsSL <mcp>/install.sh | sh), never a key in argv, and names where things live:.logrite/config.json(safe to commit), the session token in~/.config/logrite/coding/<workspace>.token, the API key in~/.logrite/api-key.
Access levels, admin keys and the MCP panels (2026-10-14)
Section titled “Access levels, admin keys and the MCP panels (2026-10-14)”Shipped with the MCP test report fixes of 13 October 2026 (backend 2f3e3cea, frontend
02ed9614, e6912f15).
Three access levels
Section titled “Three access levels”| Access | REST API | MCP | Minted by |
|---|---|---|---|
read | GET/HEAD/OPTIONS, query POSTs, the Warden dry runs and /governance/verify (see Public API contract) | the read scopes only | session, admin key, or a connection granted ‘Delete and revoke’ |
full (default) | everything the minter’s role allows | every scope except ‘Delete and revoke’ | the same |
admin | the same as full | every scope, including ‘Delete and revoke’ (remove agents, revoke keys, delete policies) | only in the app, by a signed-in Owner or Admin |
Every key acts as the user who minted it, so role floors (for example Maintainer for Warden writes) apply to the minter’s current role.
Keys drawer. The mint form has an Access select, default Full:
Full — read and write, no delete or revoke · Read only · Admin — can also delete and revoke
through MCP, with the help text “Admin keys can remove agents, revoke keys and delete policies
through MCP. Treat one like a password with delete rights.” The mint sends
{name, ttlDays, access}, and the key table has an Access column (full when a row has none).
Managing keys and connections with a credential (2026-10-14)
Section titled “Managing keys and connections with a credential (2026-10-14)”ApiKeyController sorts the caller (CredentialCaller) and applies one rule set; the old
refuseCredential / refuseApiKey refusals are deleted.
| Caller | List | Mint | Rotate | Revoke |
|---|---|---|---|---|
| Session JWT, Admin/Owner | ✓ | any access, any lifetime | ✓ | ✓ |
Admin API key, or an MCP connection granted destructive (minter Admin/Owner) | ✓ | read or full only; ttlDays 30, 90 or 365; never outliving the calling key | ✓ (not an admin key; same lifetime rules) | ✓ |
Full or read key, or a connection without destructive | ✓ (metadata only) | 403 | 403 | 403 |
Refusals:
| Case | Answer |
|---|---|
a credential mints access: "admin" | 403 an admin key can only be minted in the app |
| a credential rotates an admin key | 403 an admin key can only be rotated in the app |
| a credential mints without a lifetime, or not 30/90/365 | 400 a key minted by a credential must expire… |
| the new key would outlive the calling key | 400 ttlDays outlives the calling key |
| a full or read key mints, rotates or revokes | 403 an API key needs admin access to manage API keys |
| a connection without ‘Delete and revoke’ does | 403 this connection was not given 'Delete and revoke' |
A rotation by a credential re-applies the credential lifetime rules to the old key’s policy, so a
legacy key that never expires can only be rotated by a session. Audit rows carry
via: session | api_key:<last4> | connection:<clientName>.
MCP connections. GET /api/v1/mcp/connections and DELETE /api/v1/mcp/connections/{grantId}
resolve a key to its minter and keep the workspace-membership and who-may-revoke checks. Any key
or connection may list. Revoking needs an admin key or a connection granted ‘Delete and revoke’,
else 403 revoking a connection needs an admin API key or 'Delete and revoke'; success stays 200
{grantId, status: "revoked"}. Describe, approve, decline and upgrade stay session-only: they
mint a credential.
MCP panels (2026-10-14)
Section titled “MCP panels (2026-10-14)”Tools. The hint above the catalogue reads “N tools. What a connection may run depends on how
it connects: a full API key runs everything except delete and revoke, a read-only key runs reads
only, and an admin API key or a client connected with ‘Delete and revoke’ runs every action.”
Each tool then gets lines computed by tabs/mcp/toolAvailability.js from the tool’s
actionScopes (or scope) and the spec’s x-mcp-credential-scopes (written by the MCP server’s
scripts/dump-tools.py --merge, carried into the bundle as credentialScopes by
scripts/build-api-reference.mjs):
- “Any full API key or connected client can run every action.”, or “Full API key: 12 of 14 actions — rotate_key, revoke_key need an admin key or a client connected with ‘Delete and revoke’.”;
- then “Not available to a read-only key.” or “Read-only key: list_keys, stats, …”.
There is no admin line: an admin key, like a connection with every permission, runs everything. A bundle without scope data shows no lines. The old claim that the panel showed the tools “exactly as an MCP client sees them” is gone — the catalogue is the full one, and the lines say what each credential can run.
Activity. The outcome cell (McpActivity.jsx) is capped at max-w-[14rem] and truncates
with the full text in title (outcome = ok, the error, or failed), so a long error no longer
pushes the table off the panel.