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
20 changes: 16 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ dist

# Credentials
*.credentials.json
*.credentials.*
**/*credentials*.json
credentials.*
**/credentials.*
**/*service-account*.json
**/*service_account*.json
*.pem
*.key
*.p12
*.pfx

# Logs / caches
*.log
Expand All @@ -24,10 +34,12 @@ coverage
.vscode

# Local infra volumes
infra/data
infra/data/
infra/.env

# Embedded demo / local DB
.keyring-pglite
.keyring-pglite-demo
.keyring-pglite-*
.keyring-pglite*/
**/.keyring-pglite*/
*.sqlite
*.sqlite3
*.db
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ dist
node_modules
pnpm-lock.yaml
*.credentials.json
infra/data
infra/data/**
.keyring-pglite*
*.sqlite*
*.db
38 changes: 18 additions & 20 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,35 @@
# Contributing

All work goes through pull requests. Do not push directly to `main`.
Keyring changes are reviewed through pull requests. Do not push directly to `main`.

## Getting started
## Start here

1. Install Node 20+ and [pnpm](https://pnpm.io/).
2. Copy `.env.example` to `.env` and fill in local values (never commit real secrets).
3. Start infra (includes Keyring Postgres on `:5432`): `cd infra && cp .env.example .env && docker compose up -d`
4. From the repo root:
Install Node 20 or newer and pnpm 9 or newer.

```bash
pnpm install
pnpm db:migrate
pnpm seed:test-org
pnpm build
pnpm test
pnpm demo
```

## Development
The demo needs no credentials. For server work that uses Postgres, copy `.env.example` to `.env`, start the database from `infra`, and run the migrations.

## Before opening a pull request

Run the checks from the repository root:

```bash
pnpm dev
pnpm lint
pnpm typecheck
pnpm test
pnpm audit:secrets
```

Runs the API server and the web app together.
Keep each change focused. Add a regression test when fixing a bug. Update the relevant documentation when behavior or configuration changes. Do not include generated build output or local database files.

## Before opening a PR
## Secrets

- `pnpm lint`
- `pnpm typecheck`
- `pnpm test`
- Keep commits focused; prefer small PRs.
Never commit `.env`, credential files, provider keys, access tokens, or service account JSON. The only env file intended for the repository is `.env.example`, and it must contain placeholders only.

## Secrets
## Pull requests

Never commit `.env`, `.env.*` (except `.env.example`), or any `*.credentials.json` files.
Describe the user-visible change, the safety impact, and the checks you ran. Call out any live provider behavior that was not tested. Keep commits and review comments factual and specific.
160 changes: 67 additions & 93 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,126 +2,100 @@

Every company can list its employees. None of them can list what those employees can still access.

Keyring is access governance on [TrueForge](https://trueforge.dev): inventory grants across systems, reconcile identities, queue approvals, execute revokes, and keep an append-only audit ledger.
## The problem

## Judge path (verbatim)
When someone leaves a company, their access is spread across many systems. A manager may remove a GitHub collaborator but miss a personal Google Drive share, an old Slack role, or an automation key. Checklists fail because they depend on one person remembering every system and every kind of access. The result is wasted review time, unnecessary risk, broken automation, and no reliable record of what was checked.

Requires **Node 20+** and **pnpm 9+**. No Docker, no API keys, no Postgres install.
## What Keyring does

```bash
pnpm install
pnpm demo
```
Keyring collects access grants from connected systems into one review queue. It links usernames, work emails, personal emails, keys, and directory records without guessing when the evidence is weak. It treats AI agents as first class principals, recording their runtime, purpose, reachable tools, owner, and declaration status. It proposes an action for each grant and keeps protected resources in a separate human approval path. After approval, it can run the action and records the decision, result, and verification data in an append only ledger.

(`pnpm demo` builds once if needed.) Open **http://127.0.0.1:5173**, start a scan for **Ada Lovelace**. The UI runs in **replay** mode from `fixtures/recordings/ada-lovelace.json` against embedded **PGlite**. Execution stays **dry-run** by default.
Read [`docs/AGENT-IDENTITY.md`](docs/AGENT-IDENTITY.md) for the agent identity
model, evidence sources, risk treatment, and OWASP and NIST mapping.

Optional checks (still no credentials):
## Approval queue

```bash
pnpm test
pnpm audit:secrets
```
The queue keeps the important facts visible before anyone decides.

## The problem
```mermaid
sequenceDiagram
participant S as Scan activity
participant U as Unattributed findings
participant A as Attributed findings
participant Q as Approval queue
participant O as Operator
S->>U: Show first
U->>A: Continue review
A->>Q: Show evidence and action
O->>Q: Approve, hold, or reject
Q->>O: Keep protected CI card for named decision
```

Offboarding and access reviews fail because entitlements are scattered: GitHub collaborators, Drive shares to personal Gmail, deploy keys that look orphaned, stale Notion admins. HR can list people; nobody can list what those people can still touch.
## Quickstart

## What the harness does vs what we do
Requirements are Node 20 or newer and pnpm 9 or newer. The demo needs no API keys, Docker, Postgres, or cloud account.

| TrueForge (harness) | Keyring (product) |
| --- | --- |
| Agent loop, model calls, MCP tool invocation | Grant model, identity reconciliation |
| MCP auth / deferred tools / approvals mid-turn | Approval queue UI for access decisions |
| Sandbox, subagents, session persistence | Append-only governance audit ledger |
| | Policy (`keyring.yml`), spend cap, record/replay, execute dry-run |
```bash
pnpm install && pnpm demo
```

Details: [`docs/HARNESS.md`](docs/HARNESS.md).
Open http://127.0.0.1:5173 and select **Run guided demo**, or start a scan for **Ada Lovelace**. The demo uses the checked in recording at `fixtures/recordings/ada-lovelace.json`, embedded PGlite, replay events, and dry run execution. The guided demo pauses at the protected CI card until you select **Continue**. Use **Stop** to discard an aborted demo take and start again.

## Architecture

```
┌─────────────────┐
Operator UI ────►│ apps/web │
(approve/exec) └────────┬────────┘
│ HTTP / SSE
┌────────▼────────┐
│ packages/server │ scans · cards · execute · audit
│ MCP /mcp/scan │ /mcp/mutate (read vs write)
└────────┬────────┘
┌─────────────────┼─────────────────┐
▼ ▼ ▼
packages/core connectors TrueForge
grants · policy GitHub/Google agent + MCP
reconcile · audit FixtureConnector (optional live)
Postgres or PGlite (demo) — append-only audit_records
```mermaid
sequenceDiagram
participant T as TrueForge agent
participant S as Five system subagents
participant B as TrueForge sandbox
participant H as Harness approvals and sessions
participant K as Keyring API
participant C as Keyring core
participant Q as Approval queue
participant L as Audit ledger
T->>S: Start one subagent per system
T->>B: Run identity reconciliation
T->>H: Persist session and gate sensitive tools
S->>K: Return compact grants
B->>K: Return reconciliation
K->>C: Build grants and apply policy
C->>Q: Persist ApprovalCards
Q->>L: Record decisions and results
```

## Setup
TrueForge owns the agent loop, model calls, subagents, sandbox, MCP routing, harness approvals, and session persistence. Keyring owns grant semantics, identity reconciliation rules, policy, connectors, the HTTP API, the approval queue, execution policy, and the audit ledger.

### Offline demo (default for judges)
## How Keyring uses TrueForge

```bash
pnpm install && pnpm demo
```
The agent asks TrueForge for the connected systems and starts one subagent per system. Each subagent inventories access through a read only MCP server and returns a compact grant list. Keyring combines those grants and runs identity reconciliation in the TrueForge sandbox when one is available. The result becomes ApprovalCards in the Keyring queue. Any irreversible or protected action requires a harness approval before the mutate tool can run. TrueForge sessions retain turns and events, so a reconnect can resume the same work instead of starting a second turn.

Uses `KEYRING_DEMO=1` → embedded PGlite at `.keyring-pglite-demo`, `KEYRING_SCAN_DRIVER=replay`, `KEYRING_EXECUTE_DRY_RUN=1`.

### Live / full stack

1. Copy env templates: root `.env.example`, `infra/.env.example` → `infra/.env`
2. Start Postgres (or TrueForge stack): `cd infra && docker compose up keyring-db -d` (or full stack)
3. `export DATABASE_URL=postgresql://keyring:keyring@localhost:5432/keyring`
4. `pnpm db:migrate`
5. Optional model keys in env only: `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` — never commit
6. `pnpm --filter @keyring/server start` and `pnpm --filter @keyring/web dev`
7. Register agent: `pnpm register:agent` (TrueForge up)
## Safety guarantees

## Policy file (`keyring.yml`)
- Dry run is the default. `KEYRING_EXECUTE_DRY_RUN=1` walks the execution path without calling mutating provider APIs.
- The scan path is read only. Inventory uses read credentials and the scan MCP server. Mutations use the separate mutate MCP server after approval.
- The audit ledger is append only. A database trigger rejects updates and deletes, and the hash chain can be verified with `pnpm verify:audit`.
- Protected resources cannot be bulk approved. Policy can identify service accounts, protected resources, staleness rules, and optional auto approval.
- A hard spend cap stops model work cleanly. The default is `$0.50`, controlled by `KEYRING_HARD_CAP_USD`.
- Credentials stay in environment variables or the harness configuration. They are not stored in the agent manifest, recordings, or committed fixtures.

Checked into the **customer’s** repo. Declares protected resources, known service accounts, staleness, and optional auto-approve (off by default). See [`docs/POLICY.md`](docs/POLICY.md).
## Development and testing

```yaml
service_accounts:
- id: ci-payments-cdn
display_name: "GitHub Actions — payments CDN publish"
owner: "platform@keyring-test.example"
key_ids:
- AKIA_KEYRING_CI_ORPHAN_LOOKALIKE
resource_ids:
- keyring-test/payments
Install dependencies, then run:

protected:
- resource: "keyring-test/payments"
system: github
reason: "Prod payments CDN — always individual approval (never bulk)"
```bash
pnpm lint
pnpm typecheck
pnpm test
```

## Safety guarantees

- **Dry-run default** — `KEYRING_EXECUTE_DRY_RUN=1`. Clones cannot revoke by accident. Pass `dryRun:false` only when intentional ([`docs/EXECUTE.md`](docs/EXECUTE.md)).
- **Read-only scan path** — inventory uses `/mcp/scan` and read credentials; mutates only via `/mcp/mutate` after approval.
- **Append-only ledger** — `audit_records` rejects UPDATE/DELETE at the DB; hash chain verifiable with `pnpm verify:audit`.
- **Hard spend cap** — default `$0.50` (`KEYRING_HARD_CAP_USD`); scan stops cleanly as `cost_capped`.
- **Secrets** — API keys only in env; `pnpm audit:secrets` scans the tree (and git history when `.git` exists).

## UI error states
The suite uses local fixtures and embedded test databases. It does not require provider credentials. Use `pnpm audit:secrets` before sharing a branch. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the pull request workflow.

| Condition | Message | Recovery |
| --- | --- | --- |
| Connector auth failure | Clear auth error from MCP | Re-authorize connector in TrueForge, retry |
| Rate limit | 429 / quota | Wait / lower rate, retry; or use replay |
| Partial scan | Some systems failed | Use successful grants; fix connector; re-scan |
| Spend cap | Cap reached, no further model calls | Raise cap or use `pnpm demo` |
| Execution failure | Per-card failure + undo hint when restorable | Check credentials / dry-run; retry cards |
More detail is available in [`docs/HARNESS.md`](docs/HARNESS.md), [`docs/API.md`](docs/API.md), [`docs/CONNECTORS.md`](docs/CONNECTORS.md), [`docs/IDENTITY.md`](docs/IDENTITY.md), [`docs/AGENT-IDENTITY.md`](docs/AGENT-IDENTITY.md), [`docs/POLICY.md`](docs/POLICY.md), [`docs/EXECUTE.md`](docs/EXECUTE.md), [`docs/COSTS.md`](docs/COSTS.md), [`docs/UI.md`](docs/UI.md), [`docs/AGENT.md`](docs/AGENT.md), and [`docs/TEST_ORG.md`](docs/TEST_ORG.md).

## More docs
## Limitations

- [`docs/HARNESS.md`](docs/HARNESS.md) · [`docs/POLICY.md`](docs/POLICY.md) · [`docs/COSTS.md`](docs/COSTS.md)
- [`docs/UI.md`](docs/UI.md) · [`docs/API.md`](docs/API.md) · [`docs/EXECUTE.md`](docs/EXECUTE.md)
- [`docs/AGENT.md`](docs/AGENT.md) · [`docs/IDENTITY.md`](docs/IDENTITY.md) · [`docs/CONNECTORS.md`](docs/CONNECTORS.md) · [`docs/TEST_ORG.md`](docs/TEST_ORG.md)
The default demo is replay based. It verifies the UI, API, decision path, dry run execution, streamed results, and ledger hash verification without making provider calls. The TrueForge driver has been exercised against the local harness and the Keyring MCP endpoints, including the five system fan out, reconciliation, and card persistence. Live GitHub and Google Workspace provider operations remain opt in and require credentials, a configured MCP server, and a throwaway test organization. The repository does not claim that every live provider response or live mutation path has been verified.

## AI-assistance disclosure
## AI assistance disclosure

This project was developed with assistance from **Cursor** (AI coding agent). Humans directed product decisions, safety defaults, and review; Cursor was used for implementation, refactoring, tests, and documentation drafting as required by the competition rules.
This project was developed with assistance from Cursor, an AI coding agent. Humans directed the product decisions, safety defaults, tests, review, and final verification. Cursor assisted with implementation, refactoring, testing, and documentation drafting, as permitted by the hackathon rules.
7 changes: 7 additions & 0 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AgentActivity } from "./components/AgentActivity.js";
import { ApprovalQueue } from "./components/ApprovalQueue.js";
import { ErrorBanner } from "./components/ErrorBanner.js";
import { GuidedDemoPanel } from "./components/GuidedDemoPanel.js";
import { GuidedDemoStatusStrip } from "./components/GuidedDemoStatusStrip.js";
import { useScanSession } from "./hooks/useScanSession.js";
import { useGuidedDemo } from "./hooks/useGuidedDemo.js";
import { classifyClientError, recoveryFor, type ProductErrorKind } from "./lib/errors.js";
Expand Down Expand Up @@ -108,6 +109,12 @@ export function App() {
onStop={guided.stop}
/>
) : null}
{demoMode && guided.state.phase !== "idle" ? (
<GuidedDemoStatusStrip
state={guided.state}
systemCount={Object.keys(session.activity.subagents).length || 6}
/>
) : null}

<footer className="flex shrink-0 flex-wrap items-center justify-between gap-3 border-t border-[var(--color-line)] bg-[var(--color-panel)] px-5 py-2 font-mono text-[11px] text-[var(--color-mute)]">
<div className="flex flex-wrap gap-x-4 gap-y-1">
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export interface ApiCard {
principal: {
kind: string;
identifiers: Array<{ kind: string; value: string; source: string }>;
agentName?: string;
runtime?: string;
declaredPurpose?: string;
reachableTools?: string[];
registeredBy?: string;
declarationStatus?: "declared" | "unregistered";
};
evidence: Array<{ claim: string; source: string; confidence: string }>;
revocable: { possible: boolean; reversible: boolean; method: string };
Expand Down
25 changes: 24 additions & 1 deletion apps/web/src/components/ApprovalCardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function ApprovalCardView({
onHold,
onReject,
actionsDisabled = false,
guidedFocus = false,
}: {
card: ApiCard;
selected: boolean;
Expand All @@ -32,6 +33,7 @@ export function ApprovalCardView({
onHold: () => void;
onReject: () => void;
actionsDisabled?: boolean;
guidedFocus?: boolean;
}) {
const stale = staleness(card.grant.lastUsedAt);
const pending = card.status === "pending";
Expand All @@ -48,7 +50,9 @@ export function ApprovalCardView({
focused
? "border-[var(--color-ink)] ring-1 ring-[var(--color-ink)]"
: "border-[var(--color-line)]"
} ${selected ? "bg-[var(--color-surface-2)]" : ""}`}
} ${selected ? "bg-[var(--color-surface-2)]" : ""} ${
guidedFocus ? "ring-2 ring-[var(--color-ink)] ring-offset-2" : ""
}`}
>
<div className="flex items-start gap-3 px-3.5 py-3">
<label className="mt-0.5 shrink-0 cursor-pointer" onClick={(e) => e.stopPropagation()}>
Expand All @@ -68,6 +72,19 @@ export function ApprovalCardView({
<div className="flex flex-wrap items-center gap-2">
<h3 className="truncate text-[14px] font-semibold tracking-tight">{who}</h3>
<ConfidenceBadge confidence={card.attribution.confidence} />
{card.grant.principal.kind === "ai_agent" ? (
<span
className={`border px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.06em] ${
card.grant.principal.declarationStatus === "unregistered"
? "border-[var(--color-irrev)] bg-[var(--color-irrev-soft)] text-[var(--color-irrev)]"
: "border-[var(--color-hold)] text-[var(--color-hold)]"
}`}
>
{card.grant.principal.declarationStatus === "unregistered"
? "Unregistered agent"
: "AI agent"}
</span>
) : null}
{card.irreversible ? (
<span className="border border-[var(--color-irrev)] bg-[var(--color-irrev-soft)] px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.06em] text-[var(--color-irrev)]">
Permanent
Expand Down Expand Up @@ -103,6 +120,12 @@ export function ApprovalCardView({
{" on "}
<span className="font-medium">{card.grant.resource.displayName}</span>
</p>
{card.grant.principal.kind === "ai_agent" ? (
<p className="mt-1 text-[11px] text-[var(--color-mute)]">
Runtime: {card.grant.principal.runtime ?? "unknown"} · Reachable:{" "}
{card.grant.principal.reachableTools?.join(", ") || "not reported"}
</p>
) : null}
</div>

<div className="shrink-0 text-right">
Expand Down
Loading
Loading