Skip to content

API Sandbox

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:

  1. shows what each API does, with required and optional parameters separated;
  2. runs it, against the caller’s own workspace, with credentials already filled in;
  3. returns a readable response, including the error contract;
  4. 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.

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. DashboardLayout keys its RouteErrorBoundary on routeKey(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.jsx renders 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 sessionStorage under logrite: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.

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" with autocomplete="new-password", data-1p-ignore, data-lpignore="true" and data-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:
ChipWhen
Sent with API key …last4an 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 tokenthe 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.

CredentialPrefixVerified byUsed for
Logrite API keylgp_live_Spring backend (SandboxApiKeys), and by ai-service / semantic-search via introspectionevery Sandbox operation that accepts one, the REST API and the MCP server
Session token (JWT)—backend, ai-service, semantic APIcreating, 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 tokenlgca_live_ingest shimPOST {ingest}/ingest only

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.

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 keyRoute(s)Measured
AI ServicePOST /api/v1/log-analyses, POST /api/v1/chat/sync, GET /api/v1/knowledge/packs, POST /api/v1/knowledge/ask200
MCP serverPOST /sse (every tool), GET /skills-package200
Semantic SearchPOST /api/v1/search, /search/all, GET /api/v1/indexes (and /indexes/{name}), GET /api/v1/models200 (/indexes and /models since 2026-10-01 — before that they answered 401 jwt malformed to a key)
Backend — projects & scansGET /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/check200 (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}/addons200
Backend — machine-caller routesGET /api/v1/project/{id}, POST /api/v1/project/enrol, GET /api/v1/warden/library/frameworks200 (enrol: 409 with an installUrl when the GitHub App is not on the owner)
Backend — the key itselfPOST /api/v1/api-keys/introspect200

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.

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} where data carries id, name, fullName, workspaceId, repoUrl, platform, branches[], scannedBranch, createdAt and latestScan (id, branchName, branchLink, runDate, result and 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/enrol body {repoUrl | repoFullName, branch?, workspaceId?} → data = projectId, projectFullName, created, workspaceId (and branch if sent). The workspace is the key’s own (a different workspaceId is 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 with created: false. Otherwise the workspace’s GitHub App installation on the repo owner is used (ensureProjectForRepo, source app); none → 409 status: "FAIL" with data = owner, repoFullName, installUrl — the last being https://github.com/apps/<github.app.slug>/installations/new when 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).

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… [➤] │
└───────────────────────────┴─────────────────────────────────────────┘

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.

FieldMeaning
id, label, grouppicker text; group renders as an optgroup
auth, resolveAuth(credentials)nominal credential, and the one actually presented
kindstream, 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
notesprose 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.

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.

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.

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.

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.

SelectionModeResult
total ≤ PART_BUDGETbatch1 request, 1 batch, 1 analysis
many files, total > PART_BUDGETsplitN sequential requests, all pinned to one dataset
one file > PART_BUDGETchunkedPOST …/uploads/sessions, one request per part, 1 analysis
one file > maxTotalBytes, or total > maxBytesPerRequestrefusenothing 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: B below 1 KB, otherwise KB / MB / GB with 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 registerUploadedIndex as 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-data in 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 gets X-Filename: app.log and --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 calls DELETE {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 (frontend a606a7ef). A dataset the attachment reused — identical bytes another upload already stored — comes back with dataset.reused: true and is not listed there, so Remove everything never deletes data this session did not create (ai-service 17bca63, frontend 95a99864). New conversation also clears attachments that were picked but not yet sent.
  • Bold renders, stray ** never shows (frontend 6894b2bb). 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 as 2**10 is 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 as analysis_id and the dataset pinned as indexName, so Cortex answers from the findings already made.

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_delta live, then replaced by message_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 carries turn, 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.

  • 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.

Every snippet must run as pasted, with no packages:

  • cURL — -N -sS for 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 the data: payloads are printed. (The old snippet used requests, 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-level await failed as a CommonJS snippet.js), exits on !res.ok, prints the data: payloads.
  • Go — a complete package main program (go run main.go); the stream scanner’s buffer grows to 4 MiB per event and sc.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 event
import json
import os
import sys
import urllib.error
import 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)

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.

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.

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”.

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.

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 optional isClearable
  • a one-click × on every text input, as ListSearchField draws 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-5 page padding
  • a divided header with the tabs on their own row
  • section headings with the orange accent bar
  • #373A41 interactive borders with the orange focus ring
  • bordered tables with an uppercase head band and px-4 py-2.5 cells
  • 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.

LabelWhere it comes from
Publicbackend /api/v1/auth/* and GET /api/v1/agents/{agentKey}/collector.sh
Logrite API key or session JWTKEY_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 JWTeverything 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:

  1. 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>. A Connected clients: N line 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.

  2. Skills package — the Claude Code plugin, installed with curl -fsSL https://mcp.logrite.ai/install.sh | sh (to pass a key, set it on sh: 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-marketplace
    claude plugin marketplace add ~/.logrite/claude-marketplace
    claude plugin install logrite@logrite-local -s user

    Below that, the /logrite:init walkthrough lists exactly what it writes, so nobody has to run it to find out.

  3. 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).

  4. 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.

  5. 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.

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.

  • 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 under logrite: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: true field 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 (routeKey collapses /sandbox/*), and the credentials die with the page’s JS context, which is the correct lifetime for a one-time secret.
  • 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_KEY for 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_FALLBACK warning.
  • 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).

AccessREST APIMCPMinted by
readGET/HEAD/OPTIONS, query POSTs, the Warden dry runs and /governance/verify (see Public API contract)the read scopes onlysession, admin key, or a connection granted ‘Delete and revoke’
full (default)everything the minter’s role allowsevery scope except ‘Delete and revoke’the same
adminthe same as fullevery 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.

CallerListMintRotateRevoke
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)403403403

Refusals:

CaseAnswer
a credential mints access: "admin"403 an admin key can only be minted in the app
a credential rotates an admin key403 an admin key can only be rotated in the app
a credential mints without a lifetime, or not 30/90/365400 a key minted by a credential must expire…
the new key would outlive the calling key400 ttlDays outlives the calling key
a full or read key mints, rotates or revokes403 an API key needs admin access to manage API keys
a connection without ‘Delete and revoke’ does403 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.

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.