Skip to content
Open
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
90 changes: 43 additions & 47 deletions .agents/skills/orglabeler/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ OrgLabeler watches certified actor records:
- `app.certified.actor.profile`
- `app.certified.actor.organization`

It merges profile and organization context by actor DID, scores that merged actor context, and publishes a quality-tier label against the **actor DID**.
It assesses profile and organization context together by actor DID with Jev, then publishes a quality-tier label against the **actor DID** when the result is sufficiently confident.

These instructions are only for OrgLabeler labels from the OrgLabeler source. Do not generalize `likely-test`, `standard`, or `high-quality` to labels from other labelers.

Expand Down Expand Up @@ -57,17 +57,17 @@ high-quality

Interpretation:

- `likely-test`: hard test or placeholder evidence was found.
- `standard`: no hard test evidence, but the score is below the high-quality threshold.
- `high-quality`: no hard test evidence, and the score is at least 70.
- `likely-test`: configured test-PDS policy matched or Jev found sufficiently strong contextual test-data evidence.
- `standard`: Jev found a substantive, coherent organization context with standard-level quality confidence.
- `high-quality`: Jev found a specific, coherent organization context with high-quality confidence.

Important:

- `likely-test` is not a general low-quality bucket.
- Incomplete real records become `standard`, not `likely-test`.
- Incomplete real records are not automatically `likely-test`; if evidence is ambiguous or below confidence thresholds, the result can remain uncertain with no new label.
- Missing labels mean `unknown` or `not labeled yet`, not `standard`.
- Labels target actor DIDs, not individual `app.certified.actor.profile` or `app.certified.actor.organization` record URIs.
- The signed AT Protocol label carries the tier. Score breakdowns, test signals, and validation notes are available from dashboard/debug APIs when exposed.
- The signed AT Protocol label carries only the tier. The dashboard API exposes assessment status, label freshness, model/question/policy versions, probabilities, and validation notes; those details are not part of the signed label.

## Choose the right integration path

Expand All @@ -89,7 +89,7 @@ Use dashboard REST APIs for browsing and diagnostics:
- `GET /api/recent?limit=20&offset=0&tier=high-quality`
- `GET /api/recent?q=<did-or-name>`

Dashboard APIs may include score, breakdown, `testSignals`, and `validationNotes`. Use them to explain a label, not as the primary signed label stream if the product can consume XRPC labels directly.
Dashboard APIs expose current assessment status and probabilities for diagnostics. Treat uncertain, awaiting-configuration, awaiting-PDS-resolution, and stale-label states as distinct from the signed active tier. Use dashboard details to explain a label, not as the primary signed label stream if the product can consume XRPC labels directly.

## Query current labels

Expand Down Expand Up @@ -266,7 +266,7 @@ Use this flow when an app displays certified actors and wants to hide test data:
4. Show actors with `standard` or `high-quality`.
5. Hide actors with `likely-test`.
6. Decide product behavior for `unknown` actors.
7. Optionally use `/api/recent?q=<did>` in admin/debug views to explain why an actor received a tier.
7. Optionally use `/api/recent?q=<did>` in debug views to inspect the assessment status and current probabilities.

## Dashboard/debug API shapes

Expand All @@ -283,13 +283,19 @@ Example response:
"standard": 50,
"likely-test": 10
},
"byAssessmentStatus": {
"queued": 2,
"assessing": 1,
"assessed": 80,
"uncertain": 10,
"awaiting-configuration": 5,
"awaiting-pds-resolution": 1,
"failed": 1,
"legacy-stale": 0
},
"staleLabels": 4,
"last24h": 5,
"last7d": 30,
"hfCoverage": {
"classified": 20,
"pending": 80,
"total": 100
}
"last7d": 30
}
}
```
Expand All @@ -311,34 +317,29 @@ Example response:
{
"activities": [
{
"id": 1,
"did": "did:plc:<actor>",
"rkey": "record-key",
"uri": "at://did:plc:<actor>/app.certified.actor.organization/<rkey>",
"displayName": "Example Org",
"score": 82,
"tier": "high-quality",
"breakdown": {
"displayName": 5,
"description": 10,
"organizationType": 5,
"websitePresent": 10,
"websiteResolves": 15,
"websiteMatchesName": 5,
"organizationUrlsPresent": 5,
"organizationUrlsResolve": 5,
"locationValid": 10,
"foundedDateValid": 5,
"foundedDateAge": 5,
"avatar": 10,
"banner": 10,
"trustedPds": 0
"status": "assessed",
"publishedTier": "high-quality",
"labelStatus": "current",
"model": "jev-latest",
"questionVersion": "organization-assessment-questions-v1",
"policyVersion": "provisional-confidence-policy-v1",
"probabilities": {
"test_data": { "type": "noul", "noul": 0.001 },
"substantive_purpose": { "type": "noul", "noul": 0.99 },
"consistency_quality": {
"type": "choice",
"choice": "high_quality",
"confidence": 0.998,
"probabilities": { "high_quality": 0.998, "standard": 0.001, "uncertain": 0.001 }
}
},
"testSignals": [],
"validationNotes": [],
"labeledAt": "2026-01-01T00:00:00.000Z",
"hfLabel": null,
"hfScore": null
"updatedAt": "2026-01-01T00:00:00.000Z",
"assessedAt": "2026-01-01T00:00:00.000Z"
}
],
"total": 1
Expand All @@ -349,8 +350,7 @@ Supported `/api/recent` parameters:

- `limit`: clamped to 1-100.
- `offset`: pagination offset, minimum 0.
- `tier`: `all`, `likely-test`, `standard`, or `high-quality`.
- `hf=true`: only rows with HuggingFace classification fields.
- `tier`: `all`, `likely-test`, `standard`, or `high-quality`; filters by published tier.
- `q`: search by display name, actor DID, or organization record URI.

## Troubleshooting
Expand All @@ -367,7 +367,7 @@ Check:

### Record looks incomplete but is not `likely-test`

This is expected. `likely-test` only comes from hard test/placeholder evidence. Incomplete non-test records are labeled `standard` unless they reach the `high-quality` threshold.
This is expected. `likely-test` comes from the configured test-PDS override or sufficiently strong contextual Jev evidence, not incompleteness alone. An incomplete or ambiguous non-test record can remain uncertain and unlabeled; `standard` is published only when Jev's confidence policy is satisfied.

### Need to know why something is `likely-test`

Expand All @@ -378,19 +378,15 @@ curl -G "https://orglabeler.hypercerts.dev/api/recent" \
--data-urlencode "q=did:plc:exampleactor"
```

Inspect:

- `testSignals`
- `validationNotes`
- `breakdown`
Inspect `probabilities` when Jev ran; a configured test-PDS override bypasses Jev and may have no Jev probability payload. `validationNotes` describe record parsing/fallback and are not Jev explanations.

### Need exact score
### Need assessment details

The signed AT Protocol label only carries the tier. Use `/api/recent` for score and breakdown.
The signed AT Protocol label only carries the tier. Use `/api/recent` to inspect assessment status, label freshness, model/question/policy versions, and current probabilities. This is a content-quality assessment, not identity or legitimacy verification.

### Label changed unexpectedly

Labels can change after profile updates, organization record updates, URL enrichment, PDS resolution, recomputation, or backfill. Re-query `queryLabels` for the actor DID and rebuild active state.
Labels can change after profile updates, organization record updates, URL evidence changes, PDS resolution, a forced reassessment, or backfill. Re-query `queryLabels` for the actor DID and rebuild active state.

## Do not do

Expand Down
5 changes: 5 additions & 0 deletions .changeset/jev-assessments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"certified-organization-labeler": minor
---

Replace legacy scoring and Hugging Face classification with durable, versioned Jev assessments, confidence-gated actor-DID labels, and assessment freshness in the dashboard. Configured test-PDS hosts remain authoritative; non-test records wait without `TYPESAFE_API_KEY`. Admins can sign in with allowlisted AT Protocol DIDs and start protected, progress-tracked reassessment runs.
32 changes: 25 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ LABELER_PORT=4100
# Port for the Prometheus metrics endpoint
METRICS_PORT=4101

# Path to the SQLite database for activity log
# Path to SQLite database for snapshots, Jev assessments, jobs, admin sessions, and encrypted OAuth login state
ACTIVITY_DB_PATH=activity-log.db

# Path to the labels database (used by @skyware/labeler LabelerServer)
Expand All @@ -40,14 +40,32 @@ PDS_URL=https://certified.one
TAP_URL=https://your-tap-service.example.com
TAP_ADMIN_PASSWORD=

# Comma-separated PDS hosts whose actors should always be labeled likely-test.
# When set, URL enrichment is deferred until each actor's PDS is known and skipped for matching test PDS hosts.
# Comma-separated PDS hosts whose actors are authoritatively labeled likely-test.
# URL enrichment waits until each actor's PDS is known and skips matching test PDS hosts.
TEST_PDS_HOSTS=

# Comma-separated PDS hosts whose actors receive a score bonus after DID resolution.
# This checks the actor PDS, not the profile website or organization URLs.
TRUSTED_PDS_HOSTS=certified.one,gainforest.id
TRUSTED_PDS_BONUS=10
# TypeSafe credentials and Jev assessment policy.
# Without a key, saved non-test records wait for configuration and existing labels remain stale.
TYPESAFE_API_KEY=
JEV_MODEL=jev-latest
JEV_REQUEST_TIMEOUT_MS=15000
JEV_TEST_THRESHOLD=0.98
JEV_MAX_TEST_PROBABILITY_FOR_QUALITY=0.02
JEV_PURPOSE_THRESHOLD=0.95
JEV_QUALITY_THRESHOLD=0.95

# Verified AT Protocol OAuth admin login. Only DIDs in ADMIN_DIDS can receive a local session.
# Use the same stable public origin across deploys; metadata, JWKS, and callback URLs derive from it.
# OAuth client metadata: /api/admin/oauth/client-metadata
# Public JWKS: /api/admin/oauth/jwks
# Callback: /api/admin/oauth/callback
ADMIN_DIDS=
ADMIN_ORIGIN=https://orglabeler.hypercerts.dev
# Stable base64-encoded unencrypted P-256 PKCS#8 PRIVATE KEY PEM; never commit or rotate per deploy.
ADMIN_OAUTH_SIGNING_KEY=
# 32 random bytes as exactly 64 hex characters; encrypts PKCE/DPoP pre-auth state in ACTIVITY_DB_PATH.
ADMIN_OAUTH_ENCRYPTION_KEY=
ADMIN_SESSION_TTL_MS=28800000

# --- Railway deployment ---
# Mount a persistent volume at /app/data and set:
Expand Down
17 changes: 10 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This is the **Certified Organization Labeler** fork — an AT Protocol labeler f
```
src/
├── app/ # Next.js App Router (dashboard)
│ ├── api/ # API routes (read from configured ACTIVITY_DB_PATH)
│ ├── api/ # Public read routes and fail-closed admin routes
│ ├── feed/ # Feed page with tier filtering
│ ├── globals.css # Tailwind v4 design system (OKLCH)
│ ├── layout.tsx # Root layout with fonts + theme
Expand All @@ -22,10 +22,12 @@ src/
│ └── reset-db.ts # Database reset utility
└── lib/ # Shared code (used by BOTH labeler and dashboard)
├── config.ts # Environment config
├── constants.ts # Label definitions + scoring thresholds
├── db.ts # SQLite activity log (better-sqlite3)
├── actor-evidence.ts # Merged record and URL evidence for Jev
├── admin-auth.ts # Admin session checks, DID allowlist, CSRF/origin policy
├── constants.ts # Label definitions
├── db.ts # SQLite snapshots, assessments, jobs, and sessions
├── jev.ts # TypeSafe Jev adapter and conservative decision policy
├── resolve-pds.ts # PDS resolution from DID document
├── scorer.ts # Activity record scoring engine
└── types.ts # TypeScript interfaces (zero runtime imports)
```

Expand All @@ -34,9 +36,10 @@ src/
1. **src/labeler/ must NEVER import from React or Next.js** — it runs as a standalone Node.js process
2. **src/lib/ is shared** — importable by both labeler and Next.js, but must not import from src/labeler/
3. **src/components/ and src/app/ are Next.js only** — they can import from src/lib/ but never from src/labeler/
4. **Two app databases**: `LABELS_DB_PATH` (AT Proto, managed by @skyware/labeler) and `ACTIVITY_DB_PATH` (dashboard, managed by src/lib/db.ts)
5. **Design system**: Tailwind CSS v4, OKLCH colors (hue 260), Syne display font, no component libraries
6. **Code style**: no semicolons, single quotes, 2-space indent
4. **Two app databases**: `LABELS_DB_PATH` (AT Proto, managed by @skyware/labeler) and `ACTIVITY_DB_PATH` (snapshots, assessments, jobs, admin sessions, and encrypted short-lived OAuth login state, managed by src/lib/db.ts)
5. **Admin security**: allowlisted DIDs, opaque server-side sessions, exact-origin and CSRF checks for mutations; OAuth session issuance must follow official-client identity verification
6. **Design system**: Tailwind CSS v4, OKLCH colors (hue 260), Syne display font, no component libraries
7. **Code style**: no semicolons, single quotes, 2-space indent

## Development

Expand Down
24 changes: 17 additions & 7 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ At runtime, the labeler process starts:

1. the AT Protocol labeler server on `LABELER_PORT` (default `4100`)
2. the Prometheus metrics server on `METRICS_PORT` (default `4101`)
3. the Tap consumer that scores incoming records and writes to SQLite
3. the Tap consumer that saves records and queues durable Jev assessments

Tap is a separate service. The labeler process connects to it via `TAP_URL` instead of launching it locally, and startup should fail if `TAP_URL` is missing.

Expand Down Expand Up @@ -70,11 +70,21 @@ If your Tap deployment uses admin auth, configure `TAP_ADMIN_PASSWORD` on the Ta

### Optional app settings

- `HF_TOKEN` — enables Hugging Face classification; if unset, HF scoring stays disabled
- `TYPESAFE_API_KEY` — enables Jev assessment; if unset, saved records wait for configuration and existing labels remain marked stale
- `JEV_MODEL` — TypeSafe model identifier; default `jev-latest`
- `JEV_REQUEST_TIMEOUT_MS` — TypeSafe request timeout; default `15000`
- `JEV_TEST_THRESHOLD`, `JEV_MAX_TEST_PROBABILITY_FOR_QUALITY`, `JEV_PURPOSE_THRESHOLD`, `JEV_QUALITY_THRESHOLD` — confidence thresholds; defaults `0.98`, `0.02`, `0.95`, and `0.95`
- `HYPERSCAN_RECORD_URL_BASE` — base URL used when the dashboard links to source AT Protocol records; default `https://hyperscan.dev/data`
- `TEST_PDS_HOSTS` — comma-separated PDS hosts whose actors should always be labeled `likely-test`; URL enrichment waits for actor PDS resolution and skips matching test PDS hosts
- `TRUSTED_PDS_HOSTS` — comma-separated PDS hosts whose actors receive the trusted-PDS score bonus; default `certified.one,gainforest.id`
- `TRUSTED_PDS_BONUS` — score points added for trusted actor PDS hosts; default `10`
- `TEST_PDS_HOSTS` — comma-separated authoritative test-PDS overrides; URL enrichment waits for PDS resolution and skips matching hosts
- `ADMIN_DIDS` — comma-separated allowlist of admin DIDs; empty or invalid values deny access
- `ADMIN_ORIGIN` — exact stable dashboard origin, HTTPS in production; `http://localhost` is accepted for local development
- `ADMIN_OAUTH_SIGNING_KEY` — stable base64-encoded unencrypted P-256 PKCS#8 private-key PEM for confidential OAuth client assertions
- `ADMIN_OAUTH_ENCRYPTION_KEY` — 32 random bytes encoded as 64 hexadecimal characters; encrypts temporary PKCE/DPoP login state stored in `ACTIVITY_DB_PATH`
- `ADMIN_SESSION_TTL_MS` — maximum local admin session lifetime; default `28800000`

The admin client metadata, public JWKS, and callback URLs derive from `ADMIN_ORIGIN`: `/api/admin/oauth/client-metadata`, `/api/admin/oauth/jwks`, and `/api/admin/oauth/callback`. Configure the origin before deployment and keep both OAuth keys stable in the platform secret manager. Generate and store the keys outside the deployment environment; never commit them. Rotating the encryption key invalidates in-flight logins. The client requests exactly `atproto`; OAuth credentials are not stored after callback verification, and revocation is attempted before the temporary SDK session is discarded.

`http://localhost` is accepted for development, but an external OAuth server may not be able to fetch a JWKS endpoint on the developer's machine. Use an HTTPS development tunnel if the provider cannot handle loopback metadata. Admin mutations continue to require the exact origin and CSRF token; the app has no shared-password fallback.
- `PORT` — public Caddy HTTP port (hosted platforms usually set this; Caddy fallback `8080`)
- `NEXT_PORT` — internal Next.js HTTP port (`3000` by default)
- `HOST` — bind host for the labeler server (`0.0.0.0` by default; `127.0.0.1` is recommended when only same-container Caddy should reach it)
Expand All @@ -89,14 +99,14 @@ If your Tap deployment uses admin auth, configure `TAP_ADMIN_PASSWORD` on the Ta
- `URL_CHECK_FAILED_TTL_MS` — downgrade window for hard failed URL checks before another attempt; default `86400000`
- `URL_CHECK_RETRY_BASE_MS` — initial retry delay for temporary URL check failures; default `300000`
- `URL_CHECK_MAX_RETRY_MS` — maximum retry delay for temporary URL check failures; default `3600000`
- `URL_CHECK_HARD_FAILURE_ATTEMPTS` — number of hard failures required before URL scoring removes resolve points; default `2`
- `URL_CHECK_HARD_FAILURE_ATTEMPTS` — hard failures required before URL evidence is recorded as failed; default `2`
- `URL_CHECK_MAX_URLS_PER_DID` — maximum profile/organization URLs cached and checked per DID; default `5`

## Persistence requirements

This fork uses two SQLite databases in the app service and one in the Tap service:

- `ACTIVITY_DB_PATH` — dashboard data and scoring history
- `ACTIVITY_DB_PATH` — actor snapshots, Jev assessments, durable jobs, admin sessions, and encrypted short-lived OAuth login state
- `LABELS_DB_PATH` — AT Protocol label records
- Tap service database — Tap cursor / replay state

Expand Down
Loading
Loading