Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions BUILDLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -475,3 +475,39 @@ sessions from before the cache split stay empty rather than wrong.

On the real fleet that is 27 of 194 rows priced today, one of them an estimate.
The rest fill in as collectors write split-aware events.

## 2026-09-07 - Masking secrets before they reach the disk

A session records whatever crossed it. A key pasted into a prompt, a
`printenv`, a `.env` read back by a tool: all of it landed in `agentlens.db` in
clear text, a file with no authentication in front of it. A token in a
transcript is a token at rest.

The pattern table is the tier-1 set from Grafana's agento11y, Apache-2.0, whose
patterns are hand-curated from Gitleaks. Twenty-two high-confidence formats:
cloud and provider API keys, GitHub and Slack tokens, private-key blocks,
connection strings carrying credentials, bearer tokens. Their mask format is
kept verbatim so their own fixtures serve as the tests here, and all 28
light-mode cases pass unchanged, which is the only real proof a transcribed
regex is faithful.

Their tier 2 is deliberately left out. It guesses at key=value shapes, which is
right for a product that must not leak and wrong here, because
`DB_PASSWORD=hunter2` in a transcript is often the thing you opened the
transcript to find.

Masking happens in the two prepared statements every adapter writes through,
not per field and not per adapter, so prompts, model output, tool arguments and
tool results are covered in one pass. The event is already serialized JSON at
that point and every mask is plain ASCII, so masking the serialized form cannot
break it. Cost is 2 microseconds for text with no secret in it and 4 with one.

Scanning the existing 44,934 events, 12 across 8 sessions contain something the
masker would now remove. Some of those are certainly the example tokens from
today's own work on this feature, and I did not look to find out which. Old rows
are not rewritten; `AGENTLENS_REDACT=0` turns the whole thing off.

A pleasing detail: the first push was rejected by GitHub's own secret scanning,
because the borrowed fixtures carry realistic example keys. That is exactly the
right behaviour from it, so the corpus is stored base64 and decoded by the test
rather than allowlisted.
13 changes: 13 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
AgentLens
Copyright (c) 2026 Weigang Geng

This product includes software developed at Grafana Labs.

packages/server/src/redact-secrets.ts contains the tier-1 secret-pattern table
from Grafana agento11y (https://github.com/grafana/agento11y), licensed under
the Apache License, Version 2.0. Those patterns are in turn hand-curated from
Gitleaks (https://github.com/gitleaks/gitleaks), licensed under the MIT License.

packages/server/test/fixtures/agento11y-light.json contains the light-mode test
cases from the same project, under the same licence, used to verify that the
ported patterns behave identically.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,18 @@ are not read.
Other variables: `AGENTLENS_DB` (`agentlens.db`), `PORT` (`8788`), `MCP_PORT`
(`8791`), `AGENTLENS_HOST` (`127.0.0.1`, see Shared server below).

Secrets are masked before an event is stored. A session records whatever
crossed it - a key pasted into a prompt, a `printenv`, a `.env` read back by a
tool - and `agentlens.db` is a file with no authentication in front of it, so a
token in a transcript is a token at rest. The pattern table is the tier-1 set
from Grafana's [agento11y](https://github.com/grafana/agento11y) (Apache-2.0,
patterns from Gitleaks; see [NOTICE](NOTICE)): cloud and provider API keys,
GitHub and Slack tokens, private-key blocks, connection strings with
credentials, bearer tokens. Their tier-2 key/value guesses are deliberately
left out, since `DB_PASSWORD=hunter2` in a transcript is often the thing you
opened the transcript to find. `AGENTLENS_REDACT=0` turns masking off, and rows
written before this existed are not rewritten.

Claude Code and dsh transcripts are tailed with per-file cursors (subagent
transcripts become threads); OpenCode is polled read-only from its SQLite
database (child sessions become threads); Gemini and Roo files are re-read when
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"tsx": "^4.20.0",
"typescript": "^5.9.0"
},
"version": "0.9.1",
"version": "0.10.0",
"description": "Local observability for coding-agent harnesses: fleet view and trace viewer for Claude Code, OpenCode, dsh, Cursor Agent, Antigravity, TrueForge and more.",
"license": "MIT",
"repository": {
Expand Down
21 changes: 19 additions & 2 deletions packages/server/src/db.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Database from "better-sqlite3";
import { redactEventRaw } from "./redact-secrets.js";

export const db = new Database(process.env.AGENTLENS_DB ?? "agentlens.db");
db.pragma("journal_mode = WAL");
Expand Down Expand Up @@ -127,18 +128,34 @@ export const upsertTurn = db.prepare(`
ON CONFLICT(id) DO UPDATE SET completed_at=@completed_at, status=@status, error=@error, ingested=@ingested, pending_actions=@pending_actions
`);

export const insertEvent = db.prepare(`
const insertEventStmt = db.prepare(`
INSERT OR IGNORE INTO events (id, session_id, turn_id, thread_id, type, created_at, raw, seq)
VALUES (@id, @session_id, @turn_id, @thread_id, @type, @created_at, @raw, ${NEXT_SEQ})
`);

// For sources that mutate records in place after first write (OpenCode).
export const upsertEvent = db.prepare(`
const upsertEventStmt = db.prepare(`
INSERT INTO events (id, session_id, turn_id, thread_id, type, created_at, raw, seq)
VALUES (@id, @session_id, @turn_id, @thread_id, @type, @created_at, @raw, ${NEXT_SEQ})
ON CONFLICT(id) DO UPDATE SET raw=excluded.raw, created_at=excluded.created_at, seq=${NEXT_SEQ}
`);

// Every adapter writes through one of these two, so masking secrets here covers
// all of them: prompts, model output, tool arguments and tool results alike.
// The statements keep their `.run(row)` shape so no call site changes.
type EventRow = {
id: string;
session_id: string;
turn_id: string;
thread_id: string | null;
type: string;
created_at: string | null;
raw: string;
};
const masked = (e: EventRow): EventRow => ({ ...e, raw: redactEventRaw(e.raw) });
export const insertEvent = { run: (e: EventRow) => insertEventStmt.run(masked(e)) };
export const upsertEvent = { run: (e: EventRow) => upsertEventStmt.run(masked(e)) };

// TrueForge wraps MCP tool failures as a content string starting with {"error"
// (prefix match, not %error%, so tool output that merely quotes an error is
// not flagged). Other adapters set a normalized raw.error flag instead.
Expand Down
65 changes: 65 additions & 0 deletions packages/server/src/redact-secrets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Masks secrets before an event reaches SQLite, because a session records
// whatever crossed it: a key pasted into a prompt, a `printenv`, a .env read
// back by a tool. The store is a file on your disk with no authentication in
// front of it, so a token in a transcript is a token at rest.
//
// The pattern table is the tier-1 set from Grafana's agento11y (Apache-2.0,
// https://github.com/grafana/agento11y/blob/main/redaction/patterns.json),
// whose patterns are in turn hand-curated from Gitleaks (MIT,
// https://github.com/gitleaks/gitleaks). Their mask format is kept verbatim so
// their own fixtures serve as this file's tests. See NOTICE.
//
// Only the high-confidence formats are ported. Their tier 2 guesses at
// key=value shapes, which is the right call for a product that must not leak
// and the wrong one here: `DB_PASSWORD=hunter2` in a transcript is often the
// thing you are trying to read when debugging.
//
// AGENTLENS_REDACT=0 turns this off.

type Pattern = { id: string; re: RegExp };

const PATTERNS: Pattern[] = [
{ id: "grafana-cloud-token", re: /\bglc_[A-Za-z0-9_-]{20,}/g },
{ id: "grafana-service-account-token", re: /\bglsa_[A-Za-z0-9_-]{20,}/g },
{ id: "aws-access-token", re: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}\b/g },
{ id: "github-pat", re: /\bghp_[A-Za-z0-9_]{36,}/g },
{ id: "github-oauth", re: /\bgho_[A-Za-z0-9_]{36,}/g },
{ id: "github-app-token", re: /\bghs_[A-Za-z0-9_]{36,}/g },
{ id: "github-fine-grained-pat", re: /\bgithub_pat_[A-Za-z0-9_]{82}/g },
{ id: "anthropic-api-key", re: /\bsk-ant-api03-[a-zA-Z0-9_-]{93}AA/g },
{ id: "anthropic-admin-key", re: /\bsk-ant-admin01-[a-zA-Z0-9_-]{93}AA/g },
{ id: "openai-api-key", re: /\bsk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20}/g },
{ id: "openai-project-key", re: /\bsk-proj-[a-zA-Z0-9_-]{40,}/g },
{ id: "openai-svcacct-key", re: /\bsk-svcacct-[a-zA-Z0-9_-]{40,}/g },
{ id: "gcp-api-key", re: /\bAIza[A-Za-z0-9_-]{35}/g },
{ id: "private-key", re: /-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----/g },
{ id: "connection-string", re: /(?:postgres|mysql|mongodb|redis|amqp):\/\/[^ \t\n\f\r\xa0'"]+@[^ \t\n\f\r\xa0'"]+/g },
{ id: "bearer-token", re: /[Bb]earer[ \t\n\f\r\xa0]+[A-Za-z0-9_.\-~+/]{20,}={0,3}/g },
{ id: "slack-token", re: /\bxox[bporas]-[A-Za-z0-9-]{10,}/g },
{ id: "stripe-key", re: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{20,}/g },
{ id: "sendgrid-api-key", re: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g },
{ id: "twilio-api-key", re: /\bSK[a-f0-9]{32}/g },
{ id: "npm-token", re: /\bnpm_[A-Za-z0-9]{36}/g },
{ id: "pypi-token", re: /\bpypi-[A-Za-z0-9_-]{50,}/g },
];

// Cheap gate: nothing in the table can match without one of these fragments,
// so most events skip 22 regexes entirely. Every tool result runs through here.
const HINTS = /glc_|glsa_|A3T|AKIA|ASIA|ABIA|ACCA|ghp_|gho_|ghs_|github_pat_|sk-|AIza|PRIVATE KEY|:\/\/|earer|xox|k_live_|k_test_|SG\.|\bSK|npm_|pypi-/;

export const redactionEnabled = () => process.env.AGENTLENS_REDACT !== "0";

export function redactSecrets(text: string): string {
if (!text || !HINTS.test(text)) return text;
let out = text;
for (const { id, re } of PATTERNS) out = out.replace(re, `[REDACTED:${id}]`);
return out;
}

// Events are stored as a JSON string, and every pattern's mask is plain ASCII
// with no quotes or backslashes, so masking the serialized form cannot break
// the JSON. Doing it here rather than per field covers prompts, model output,
// tool arguments and tool results in one pass.
export function redactEventRaw(raw: string): string {
return redactionEnabled() ? redactSecrets(raw) : raw;
}
5 changes: 5 additions & 0 deletions packages/server/test/fixtures/agento11y-light.json.b64
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Base64 of the light-mode cases from grafana/agento11y
# redaction/fixtures/strings.json (Apache-2.0). Encoded because the inputs
# are deliberately realistic example keys, and GitHub's own secret scanning
# blocks a push that contains them in the clear. Decoded by the test.
WwogewogICJpZCI6ICJ0aWVyMS1ncmFmYW5hLWNsb3VkLXRva2VuLWxpZ2h0IiwKICAiaW5wdXQiOiAiZ2xjX2FiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNCIsCiAgImV4cGVjdGVkIjogIltSRURBQ1RFRDpncmFmYW5hLWNsb3VkLXRva2VuXSIKIH0sCiB7CiAgImlkIjogInRpZXIxLWdyYWZhbmEtc2VydmljZS1hY2NvdW50LXRva2VuLWxpZ2h0IiwKICAiaW5wdXQiOiAiZ2xzYV9hYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejEyMzQiLAogICJleHBlY3RlZCI6ICJbUkVEQUNURUQ6Z3JhZmFuYS1zZXJ2aWNlLWFjY291bnQtdG9rZW5dIgogfSwKIHsKICAiaWQiOiAidGllcjEtYXdzLWFjY2Vzcy10b2tlbi1saWdodCIsCiAgImlucHV0IjogIkFLSUFJT1NGT0ROTjdFWEFNUExFIiwKICAiZXhwZWN0ZWQiOiAiW1JFREFDVEVEOmF3cy1hY2Nlc3MtdG9rZW5dIgogfSwKIHsKICAiaWQiOiAidGllcjEtZ2l0aHViLXBhdC1saWdodCIsCiAgImlucHV0IjogImdocF9BQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWoiLAogICJleHBlY3RlZCI6ICJbUkVEQUNURUQ6Z2l0aHViLXBhdF0iCiB9LAogewogICJpZCI6ICJ0aWVyMS1naXRodWItb2F1dGgtbGlnaHQiLAogICJpbnB1dCI6ICJnaG9fQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqIiwKICAiZXhwZWN0ZWQiOiAiW1JFREFDVEVEOmdpdGh1Yi1vYXV0aF0iCiB9LAogewogICJpZCI6ICJ0aWVyMS1naXRodWItYXBwLXRva2VuLWxpZ2h0IiwKICAiaW5wdXQiOiAiZ2hzX0FCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaYWJjZGVmZ2hpaiIsCiAgImV4cGVjdGVkIjogIltSRURBQ1RFRDpnaXRodWItYXBwLXRva2VuXSIKIH0sCiB7CiAgImlkIjogInRpZXIxLWdpdGh1Yi1maW5lLWdyYWluZWQtcGF0LWxpZ2h0IiwKICAiaW5wdXQiOiAiZ2l0aHViX3BhdF9hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwKICAiZXhwZWN0ZWQiOiAiW1JFREFDVEVEOmdpdGh1Yi1maW5lLWdyYWluZWQtcGF0XSIKIH0sCiB7CiAgImlkIjogInRpZXIxLWFudGhyb3BpYy1hcGkta2V5LWxpZ2h0IiwKICAiaW5wdXQiOiAic2stYW50LWFwaTAzLWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYUFBIiwKICAiZXhwZWN0ZWQiOiAiW1JFREFDVEVEOmFudGhyb3BpYy1hcGkta2V5XSIKIH0sCiB7CiAgImlkIjogInRpZXIxLWFudGhyb3BpYy1hZG1pbi1rZXktbGlnaHQiLAogICJpbnB1dCI6ICJzay1hbnQtYWRtaW4wMS1hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFBQSIsCiAgImV4cGVjdGVkIjogIltSRURBQ1RFRDphbnRocm9waWMtYWRtaW4ta2V5XSIKIH0sCiB7CiAgImlkIjogInRpZXIxLW9wZW5haS1hcGkta2V5LWxpZ2h0IiwKICAiaW5wdXQiOiAic2stYWFhYWFhYWFhYWFhYWFhYWFhYWFUM0JsYmtGSmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwKICAiZXhwZWN0ZWQiOiAiW1JFREFDVEVEOm9wZW5haS1hcGkta2V5XSIKIH0sCiB7CiAgImlkIjogInRpZXIxLW9wZW5haS1wcm9qZWN0LWtleS1saWdodCIsCiAgImlucHV0IjogInNrLXByb2otYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsCiAgImV4cGVjdGVkIjogIltSRURBQ1RFRDpvcGVuYWktcHJvamVjdC1rZXldIgogfSwKIHsKICAiaWQiOiAidGllcjEtb3BlbmFpLXN2Y2FjY3Qta2V5LWxpZ2h0IiwKICAiaW5wdXQiOiAic2stc3ZjYWNjdC1iYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwKICAiZXhwZWN0ZWQiOiAiW1JFREFDVEVEOm9wZW5haS1zdmNhY2N0LWtleV0iCiB9LAogewogICJpZCI6ICJ0aWVyMS1nY3AtYXBpLWtleS1saWdodCIsCiAgImlucHV0IjogIkFJemFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsCiAgImV4cGVjdGVkIjogIltSRURBQ1RFRDpnY3AtYXBpLWtleV0iCiB9LAogewogICJpZCI6ICJ0aWVyMS1wcml2YXRlLWtleS1saWdodCIsCiAgImlucHV0IjogIi0tLS0tQkVHSU4gUlNBIFBSSVZBVEUgS0VZLS0tLS1cbk1JSUVwQUlCQUFLQ0FRRUEwWjNWUzVKSmNkczN4Zm4veWdXeUY4UGJuR3k1QWhFaVMwQzVcbi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tIiwKICAiZXhwZWN0ZWQiOiAiW1JFREFDVEVEOnByaXZhdGUta2V5XSIKIH0sCiB7CiAgImlkIjogInRpZXIxLWNvbm5lY3Rpb24tc3RyaW5nLWxpZ2h0IiwKICAiaW5wdXQiOiAicG9zdGdyZXM6Ly9hZG1pbjpzM2NyZXRQNHNzQGRiLmV4YW1wbGUuY29tOjU0MzIvbXlkYiIsCiAgImV4cGVjdGVkIjogIltSRURBQ1RFRDpjb25uZWN0aW9uLXN0cmluZ10iCiB9LAogewogICJpZCI6ICJ0aWVyMS1iZWFyZXItdG9rZW4tbGlnaHQiLAogICJpbnB1dCI6ICJCZWFyZXIgYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoiLAogICJleHBlY3RlZCI6ICJbUkVEQUNURUQ6YmVhcmVyLXRva2VuXSIKIH0sCiB7CiAgImlkIjogInRpZXIxLXNsYWNrLXRva2VuLWxpZ2h0IiwKICAiaW5wdXQiOiAieG94Yi0xMjM0NTY3ODkwLWFiY2RlZmdoaWoiLAogICJleHBlY3RlZCI6ICJbUkVEQUNURUQ6c2xhY2stdG9rZW5dIgogfSwKIHsKICAiaWQiOiAidGllcjEtc3RyaXBlLWtleS1saWdodCIsCiAgImlucHV0IjogInNrX2xpdmVfYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoiLAogICJleHBlY3RlZCI6ICJbUkVEQUNURUQ6c3RyaXBlLWtleV0iCiB9LAogewogICJpZCI6ICJ0aWVyMS1zZW5kZ3JpZC1hcGkta2V5LWxpZ2h0IiwKICAiaW5wdXQiOiAiU0cuYWFhYWFhYWFhYWFhYWFhYWFhYWFhYS5iYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwKICAiZXhwZWN0ZWQiOiAiW1JFREFDVEVEOnNlbmRncmlkLWFwaS1rZXldIgogfSwKIHsKICAiaWQiOiAidGllcjEtdHdpbGlvLWFwaS1rZXktbGlnaHQiLAogICJpbnB1dCI6ICJTS2ExYjJjM2Q0ZTVmNjA3MTgyOTNhNGI1YzZkN2U4ZjkwIiwKICAiZXhwZWN0ZWQiOiAiW1JFREFDVEVEOnR3aWxpby1hcGkta2V5XSIKIH0sCiB7CiAgImlkIjogInRpZXIxLW5wbS10b2tlbi1saWdodCIsCiAgImlucHV0IjogIm5wbV9hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLAogICJleHBlY3RlZCI6ICJbUkVEQUNURUQ6bnBtLXRva2VuXSIKIH0sCiB7CiAgImlkIjogInRpZXIxLXB5cGktdG9rZW4tbGlnaHQiLAogICJpbnB1dCI6ICJweXBpLWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwKICAiZXhwZWN0ZWQiOiAiW1JFREFDVEVEOnB5cGktdG9rZW5dIgogfSwKIHsKICAiaWQiOiAidGllcjEtdGhyZWUtYWRqYWNlbnQtdG9rZW5zLWxpZ2h0IiwKICAiaW5wdXQiOiAiZ2xjX2FiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNCBnbHNhX2FiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNCBBS0lBSU9TRk9ETk43RVhBTVBMRSIsCiAgImV4cGVjdGVkIjogIltSRURBQ1RFRDpncmFmYW5hLWNsb3VkLXRva2VuXSBbUkVEQUNURUQ6Z3JhZmFuYS1zZXJ2aWNlLWFjY291bnQtdG9rZW5dIFtSRURBQ1RFRDphd3MtYWNjZXNzLXRva2VuXSIKIH0sCiB7CiAgImlkIjogIm5vbmFzY2lpLXRva2VuLWFmdGVyLWNqay1saWdodCIsCiAgImlucHV0IjogIlx1NWJjNlx1OTRhNWdsY19hYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejEyMzQiLAogICJleHBlY3RlZCI6ICJcdTViYzZcdTk0YTVbUkVEQUNURUQ6Z3JhZmFuYS1jbG91ZC10b2tlbl0iCiB9LAogewogICJpZCI6ICJlbWFpbC1kaXNhYmxlZC1saWdodCIsCiAgImlucHV0IjogImNvbnRhY3QgcGVyc29uQGV4YW1wbGUuY29tIGZvciBoZWxwIiwKICAiZXhwZWN0ZWQiOiAiY29udGFjdCBwZXJzb25AZXhhbXBsZS5jb20gZm9yIGhlbHAiCiB9LAogewogICJpZCI6ICJ0aWVyMi1lbnYtc2VjcmV0LXZhbHVlLWxpZ2h0IiwKICAiaW5wdXQiOiAiREJfUEFTU1dPUkQ9aHVudGVyMnNlY3JldHZhbHVlIiwKICAiZXhwZWN0ZWQiOiAiREJfUEFTU1dPUkQ9aHVudGVyMnNlY3JldHZhbHVlIgogfSwKIHsKICAiaWQiOiAianNvbi1zZWNyZXQtZmllbGQtbGlnaHQtbGVhdmVzLXZhbHVlIiwKICAiaW5wdXQiOiAie1wiYXBpX2tleVwiOiBcInMzY3JldFwifSIsCiAgImV4cGVjdGVkIjogIntcImFwaV9rZXlcIjogXCJzM2NyZXRcIn0iCiB9LAogewogICJpZCI6ICJub29wLWVtcHR5LWxpZ2h0IiwKICAiaW5wdXQiOiAiIiwKICAiZXhwZWN0ZWQiOiAiIgogfQpdCg==
Loading