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
15 changes: 15 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.git
.github
.glassbox
.venv
**/.env*
**/trusted-signers.json
**/.next
**/.open-next
**/.wrangler
**/dist
**/node_modules
**/tsconfig.tsbuildinfo
benchmarks/results
docs/compatibility/*.live.json
videos
23 changes: 23 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,26 @@ GLASSBOX_OTLP_BEARER_TOKEN=
# Public-key trust policy path. The file is not a private key, but modification is a
# security-sensitive operator action.
GLASSBOX_SIGNER_TRUST_POLICY_PATH=

# Read-only loopback API exposed by `glassbox-forensics-mcp --transport streamable-http`.
# The console never receives database credentials or signing keys.
GLASSBOX_FORENSICS_API_URL=http://127.0.0.1:8788
GLASSBOX_FORENSICS_API_TOKEN=

# Authenticated deployment control plane. The master key must be an unpadded
# base64url-encoded 32-byte value and must come from a secret manager.
GLASSBOX_CONTROL_API_URL=http://127.0.0.1:8790
GLASSBOX_CONTROL_API_TOKEN=
GLASSBOX_CONTROL_MASTER_KEY=
GLASSBOX_CONTROL_MASTER_KEY_ID=control-v1
GLASSBOX_CONTROL_DB_PATH=.glassbox/control.sqlite3
GLASSBOX_ORGANIZATION=default

# Human authorization is derived from an upstream OIDC identity. This explicit
# local bypass must remain false in an internet deployment.
GLASSBOX_ADMIN_EMAILS=
GLASSBOX_ALLOW_LOCAL_OPERATOR=false

# Optional public-site hosts. Hosts listed here receive the landing page at `/`
# and never expose operator routes. Leave empty for a console-only self-hosted install.
GLASSBOX_PUBLIC_HOSTS=
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,19 @@ reproducibility-dist/

# Local runtime state
.env
deploy/production/.env.production
deploy/production/trusted-signers.json
.glassbox/
*.db
*.sqlite*
artifacts/private/
traces/
keys/

# Local demo-video production workspace. The public demo is linked from README;
# captures, voice tracks, render caches, and encoded masters stay out of Git.
videos/

# Editors and operating systems
.DS_Store
.idea/
Expand Down
826 changes: 371 additions & 455 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/console/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22.23.1
91 changes: 63 additions & 28 deletions apps/console/README.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,83 @@
# GlassBox Forensics Console

The judge-facing investigation surface for GlassBox. It turns signed decision
receipts and DataHub metadata changes into a compact causal explanation:
The read-only operator application for signed agent decisions, DataHub impact
campaigns, and recovery review. The console has independent routes for overview,
investigations, receipts, campaigns, recovery, trust, and settings.

- whether the receipt itself is trustworthy;
- which observed dependency changed;
- why the change is or is not material to the decision;
- which governed response is allowed next;
- and where DataHub projections stop being cryptographic evidence.
It does not bundle a successful demo response. Every record is read from the
configured GlassBox Forensics API. An absent or unreachable service produces an
explicit connection state rather than invented application data.

The default case is backed by the repository's live DataHub Core 1.6.0 proof.
Its negative-control view demonstrates that GlassBox can prove a decision was
unaffected when complete field lineage excludes the changed field.
## Evidence connection

Run the existing forensics service over its loopback-only HTTP surface:

```bash
glassbox-forensics-mcp \
--transport streamable-http \
--state-postgres-dsn-env GLASSBOX_STATE_POSTGRES_DSN \
--signer-trust-policy "$GLASSBOX_SIGNER_TRUST_POLICY_PATH"
```

Configure the console server:

```bash
GLASSBOX_FORENSICS_API_URL=http://127.0.0.1:8788
GLASSBOX_FORENSICS_API_TOKEN=local-private-service-token
GLASSBOX_CONTROL_API_URL=http://127.0.0.1:8790
GLASSBOX_CONTROL_API_TOKEN=local-private-control-token
GLASSBOX_ALLOW_LOCAL_OPERATOR=true
```

The HTTP surface is loopback-only unless a private service bearer token is supplied.
Remote access belongs behind the authenticated operator boundary in
[`deploy/production`](../../deploy/production/README.md); the browser never receives
the private API credential, PostgreSQL DSN, signing material, control master key, or
DataHub service-account token.

## Connection Center

The Connections route talks to the private `glassbox-control` service through a
server-side allowlisted proxy. An administrator can run a real DataHub SDK test,
prove write permission through a deterministic synthetic Document and direct
readback, save the token encrypted, and create or revoke named agent ingestion keys.
The clear ingestion key is displayed once. A configured DataHub UI origin activates
top-level and entity-specific deep links without putting the service-account token
in the link or browser.

For the live flagship, run `examples.flagship_demo` with `--keep-estate`. The
raw-free report records the retained schema as `estate.state_postgres_schema`;
pass that exact value to the service with `--state-postgres-schema`. The flagship
uses an ephemeral demo signer, so local inspection may use
`--allow-untrusted-signers`; production must supply an operator trust policy.

## Local development

Requires Node.js 22.13 or newer.
Node.js 22.13 or newer is required.

```bash
npm install
npm run dev
GLASSBOX_PUBLIC_HOSTS=glassbox.localhost npm run dev
```

The development server prints the exact local URL. The preferred quality gate
is:
One build intentionally has two faces. `http://glassbox.localhost:3000` serves
the public landing page and documentation, while `http://localhost:3000` keeps
the operator console at `/`. A self-hosted organization that leaves
`GLASSBOX_PUBLIC_HOSTS` empty receives only the console. The production reference
uses `glassboxhq.xyz` for the public surface and `app.glassboxhq.xyz` for the
OAuth-protected console; public hosts cannot reach operator or control API routes.

Quality gates:

```bash
npm test
npm run lint
npm run typecheck
```

`npm test` creates a production build and checks the rendered investigation for
the core forensic claims and the absence of starter or secret material. The console
does not declare database or object-storage bindings because the current experience
is a read-only projection of verified GlassBox evidence.

## Safety posture

This console is deliberately raw-free. It renders receipt digests, governed
metadata, reason codes, evidence identifiers, and proof-gate outcomes. It does
not render model prompts, raw tool payloads, credentials, query results, or
arbitrary DataHub custom properties.
## Trust boundary

The console is a presentation layer. Receipt verification, materiality
classification, quarantine, and replay authorization remain enforced by the
GlassBox backend and the `datahub-agent-forensics` skill.
The console displays only bounded receipt projections, governed URNs,
deterministic findings, workflow state, and fresh verification results. It never
receives prompts, outputs, tool bodies, credentials, raw query results, or signing
keys. Policy, quarantine, and recovery authority remain in the GlassBox services.
23 changes: 23 additions & 0 deletions apps/console/app/(console)/campaigns/[campaignId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Link from "next/link";
import { IdentifierControl } from "../../../components/identifier-control";
import { DataHubEntityLink } from "../../../components/console-shell";
import { ConnectionEmpty, DateValue, PageHeader, StatusPill, formatUrn, humanize, shortDigest, shortId } from "../../../components/ui";
import { getCampaign } from "../../../lib/forensics-api";

export default async function CampaignDetailPage({ params }: { params: Promise<{ campaignId: string }> }) {
const campaignId = decodeURIComponent((await params).campaignId);
const result = await getCampaign(campaignId);
const campaign = result.data?.campaign;
return <>
<PageHeader eyebrow="Campaign" title="Impact scan" description={<IdentifierControl value={campaignId} compact={shortDigest(campaignId)} label="campaign ID" />} action={<div className="header-actions">{campaign && <DataHubEntityLink urn={campaign.incident_urn} label="Open incident" />}<Link className="secondary-action" href="/campaigns">← Back to campaigns</Link></div>} />
{!campaign ? <ConnectionEmpty connection={result.connection} message={result.message} /> : (
<div className="detail-layout campaign-detail">
<div className="detail-main">
<section className="app-panel change-hero"><div className="change-hero-top"><span className="record-mark campaign large">C</span><div><p>DataHub metadata change</p><h2>{humanize(campaign.change.kind)}</h2><code>{formatUrn(campaign.change.entity_urn)}</code></div><StatusPill state={campaign.processing.workflow_status} /></div><div className="receipt-facts"><span className="date-fact"><small>Occurred</small><strong><DateValue value={campaign.change.occurred_at} /></strong></span><span><small>Aspect</small><strong>{campaign.change.aspect_name}</strong></span><span><small>Assessments</small><strong>{campaign.assessments.length}</strong></span><span><small>Writeback</small><strong>{humanize(campaign.processing.datahub_writeback_state)}</strong></span></div></section>
<section className="app-panel detail-panel"><div className="panel-heading"><div><p>Deterministic policy</p><h2>Decision findings</h2></div><span>{campaign.policy_version}</span></div><div className="assessment-list">{campaign.assessments.map((assessment) => <Link href={`/investigations/${encodeURIComponent(assessment.receipt_id)}`} key={assessment.receipt_id}><span className="record-mark">D</span><div><strong className="mono">{shortDigest(assessment.receipt_id)}</strong><small>{humanize(assessment.reason_code)}</small></div><StatusPill state={assessment.state} /><span className="row-arrow">→</span></Link>)}</div></section>
</div>
<aside className="detail-rail"><section className="app-panel evidence-health"><p>Processing</p><h2>{humanize(campaign.processing.workflow_status)}</h2><dl><div><dt>Attempts</dt><dd>{campaign.processing.attempt_count}</dd></div><div><dt>Writeback</dt><dd>{humanize(campaign.processing.datahub_writeback_state)}</dd></div><div><dt>Error recorded</dt><dd>{campaign.processing.last_error_recorded ? "Yes" : "No"}</dd></div><div><dt>Incident</dt><dd>{shortId(campaign.incident_urn, 18, 8)}</dd></div></dl></section></aside>
</div>
)}
</>;
}
24 changes: 24 additions & 0 deletions apps/console/app/(console)/campaigns/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Link from "next/link";
import { ConnectionEmpty, EmptyRecords, PageHeader, StatusPill, formatDate, formatUrn, humanize } from "../../components/ui";
import { getCampaigns } from "../../lib/forensics-api";

export default async function CampaignsPage() {
const result = await getCampaigns();
return <>
<PageHeader eyebrow="DataHub Action" title="Campaigns" description="Persisted impact scans created from metadata changes." />
{result.data === null ? <ConnectionEmpty connection={result.connection} message={result.message} /> : (
<section className="app-panel list-panel">
<div className="list-toolbar"><div><strong>Invalidation history</strong><span>Direct Action state, not projected UI state</span></div><span>{result.data.total} campaigns</span></div>
{result.data.campaigns.length ? <div className="data-table campaigns-table">
<div className="table-head"><span>DataHub change</span><span>Findings</span><span>Writeback</span><span>Processed</span><span /></div>
{result.data.campaigns.map((campaign) => <Link className="table-row" href={`/campaigns/${encodeURIComponent(campaign.campaign_id)}`} key={campaign.campaign_id}>
<span className="table-primary"><b className="record-mark campaign">C</b><span><strong>{humanize(campaign.change.kind)}</strong><small className="mono">{formatUrn(campaign.change.entity_urn)}</small></span></span>
<span className="finding-pills">{campaign.assessments.slice(0, 2).map((item) => <StatusPill state={item.state} key={item.receipt_id} />)}</span>
<StatusPill state={campaign.processing.datahub_writeback_state} />
<span><strong>{formatDate(campaign.change.occurred_at)}</strong></span><span className="row-arrow">→</span>
</Link>)}
</div> : <EmptyRecords title="No campaigns persisted" description="The DataHub Action has not processed a metadata change yet." />}
</section>
)}
</>;
}
Loading