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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ jobs:
# DO NOT use golangci/golangci-lint-action here. Its pre-built
# binaries are compiled against an older Go toolchain (≤ 1.23)
# and exit 3 on our code because the go.mod toolchain directive
# is 1.25.10 — the embedded linter can't parse 1.25 stdlib/SSA.
# is 1.25.11 — the embedded linter can't parse 1.25 stdlib/SSA.
#
# Instead, `go install` golangci-lint from source. This compiles
# it with the CI runner's Go (matching our toolchain.go1.25.10)
# it with the CI runner's Go (matching our toolchain.go1.25.11)
# so it parses the same way as local developers' brew-installed
# binary. Migration to golangci-lint v2 (which fixes this) is
# tracked for a later cycle.
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Documentation — accuracy audit fixes (2026-06-29)

- Corrected shipped docs against current code and the live Python SDK. The Python SDK ([`agentwrit`](https://pypi.org/project/agentwrit/) v0.3.0) and both demo apps (MedAssist, Support Tickets — published Docker images) are **live**; removed all "coming soon" banners and a fabricated SDK API from `docs/demos.md` and `docs/python-sdk.md`.
- Dropped stale "enterprise module / open-core" framing (Decision 022 — one product, license is the moat) from `docs/faq.md` and `docs/design-decisions.md`.
- Fixed user-facing accuracy bugs: sanitized token-validate error shape, config-driven `iss` claim (`AA_ISSUER`), `/v1/app/auth` rate limit (10/min, burst 3), audit event count (25), `KEY=VALUE` config-file format, `evt-NNNNNN` audit IDs, app deregister status (`inactive`), `go run` → `./bin/` per project rules, and OpenAPI schema fields. Added GitHub Security Advisory as the preferred disclosure channel in `SECURITY.md`.

### Security — Go toolchain bump (2026-06-29)

- Bumped `go.mod` toolchain `go1.25.10` → `go1.25.11`, clearing two Go stdlib advisories flagged by govulncheck: `GO-2026-5039` (net/textproto) and `GO-2026-5037` (crypto/x509). CI reads `go-version-file: go.mod`, so the bump applies across all jobs.

### Security — OSSF Scorecard Tier-1 hardening (2026-05-13)

- Moved top-level write permissions to job level in `.github/workflows/codeql.yml` (`security-events: write`, `actions: read`) and `.github/workflows/release.yml` (`packages: write`, `id-token: write`). Top level keeps `contents: read` only. OSSF Scorecard Token-Permissions check expected to lift from 0 → 10.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ agent.release()
| | `POST` | `/v1/token/validate` | Resource server — verify a token |
| **Auth** | `POST` | `/v1/admin/auth` | Operator — get admin JWT |
| | `POST` | `/v1/app/auth` | App — client credential exchange |
| | `POST` | `/v1/app/launch-tokens` | App — create agent launch tokens within its ceiling |
| | `POST` | `/v1/register` | Agent — register with launch token |
| **Token** | `POST` | `/v1/token/renew` | Agent/App — renew before expiry |
| | `POST` | `/v1/token/release` | Agent — signal task completion |
Expand Down
9 changes: 5 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

## Reporting a Vulnerability

If you discover a security vulnerability in AgentWrit, please report it responsibly to **security@agentwrit.com** rather than using public issue trackers.
If you discover a security vulnerability in AgentWrit, please report it responsibly through a private channel rather than using public issue trackers:

1. **Preferred — [GitHub Security Advisory](https://github.com/devonartis/agentwrit/security/advisories/new):** open a private advisory directly on the repository. This keeps the report private to maintainers and lets us coordinate a fix and disclosure in one place.
2. **Email:** **security@agentwrit.com** if you prefer not to use GitHub.

**We ask that you:**
- Provide detailed information about the vulnerability
Expand All @@ -18,10 +21,8 @@ All vulnerability reports are taken seriously and will be investigated promptly.
| Version | Status | Security Updates |
|---------|--------|------------------|
| 2.0.x | Current | Yes |
| 1.x | EOL | No |
| < 1.0 | Unsupported | No |

Users should upgrade to 2.0.x to receive security patches. Version 1.x is no longer supported and will not receive updates.
`2.0.0` is the only tagged release. Security patches land on the `2.0.x` line; run the latest `2.0.x` to receive them. Earlier version lines were never published.

## What Constitutes a Security Vulnerability

Expand Down
11 changes: 6 additions & 5 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Authentication endpoints are rate-limited to protect against brute-force attacks
| Endpoint | Rate | Burst | Key | Retry-After |
|----------|------|-------|-----|-------------|
| `POST /v1/admin/auth` | 5 req/s | 10 | Per IP address | 1 second |
| `POST /v1/app/auth` | Configurable | Configurable | Per `client_id` (falls back to IP) | 60 seconds |
| `POST /v1/app/auth` | 10 req/min | 3 | Per `client_id` (falls back to IP) | 60 seconds |

When a rate limit is exceeded, the broker returns `429 Too Many Requests` in RFC 7807 Problem Details format with a `Retry-After` header.

Expand Down Expand Up @@ -225,15 +225,16 @@ Verify a token and return its claims. Also checks revocation status.
{
"valid": true,
"claims": {
"iss": "agentwrit",
"iss": "https://broker.example.com",
"sub": "spiffe://agentwrit.local/agent/orch/task/instance",
"exp": 1707600000,
"nbf": 1707599700,
"iat": 1707599700,
"jti": "a1b2c3d4e5f67890...",
"scope": ["read:data:*"],
"task_id": "task-001",
"orch_id": "my-orchestrator"
"orch_id": "my-orchestrator",
"sid": ""
}
}
```
Expand Down Expand Up @@ -678,7 +679,7 @@ Each `AuditEvent`:
|---|---|---|
| `id` | string | Sequential ID (`evt-000001`) |
| `timestamp` | string | RFC3339 timestamp |
| `event_type` | string | One of 23 event types |
| `event_type` | string | One of 25 event types |
| `agent_id` | string | Agent SPIFFE ID (if applicable) |
| `task_id` | string | Task ID (if applicable) |
| `orch_id` | string | Orchestration ID (if applicable) |
Expand All @@ -691,7 +692,7 @@ Each `AuditEvent`:
| `hash` | string | SHA-256 hex hash of this event |
| `prev_hash` | string | SHA-256 hex hash of the previous event |

The 23 event types include the original lifecycle events (`admin_auth`, `agent_registered`, `token_issued`, `token_revoked`, `token_renewed`, `delegation_created`, etc.) plus 6 enforcement audit events:
The 25 event types span the lifecycle events (`admin_auth`, `agent_registered`, `token_issued`, `token_revoked`, `token_renewed`, `delegation_created`, etc.), the app-management events (`app_registered`, `app_authenticated`, `app_updated`, …), and the enforcement audit events below:

| Event Type | Description |
|---|---|
Expand Down
38 changes: 31 additions & 7 deletions docs/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -699,8 +699,10 @@ components:
properties:
agent_id:
type: string
description: SPIFFE ID assigned to the agent
example: spiffe://agentwrit.local/agent/agent-12345
description: >
SPIFFE ID assigned to the agent, in the form
spiffe://{trust_domain}/agent/{orch_id}/{task_id}/{instance_id}.
example: spiffe://agentwrit.local/agent/orch-001/task-analyze-q4/proc-abc123
access_token:
type: string
description: JWT bearer token for the agent
Expand Down Expand Up @@ -899,6 +901,9 @@ components:
items:
type: object
properties:
id:
type: string
description: Sequential event ID (e.g. evt-000001)
timestamp:
type: string
format: date-time
Expand All @@ -908,11 +913,32 @@ components:
type: string
task_id:
type: string
orch_id:
type: string
detail:
type: string
description: Human-readable description (PII-sanitized)
resource:
type: string
description: Target resource path (e.g. API endpoint)
outcome:
type: string
enum: [success, denied]
details:
deleg_depth:
type: integer
description: Delegation chain depth (0 = direct)
deleg_chain_hash:
type: string
description: SHA-256 hash of the delegation chain
bytes_transferred:
type: integer
description: Bytes transferred (for metered operations)
hash:
type: string
description: SHA-256 hex hash of this event
prev_hash:
type: string
description: SHA-256 hex hash of the previous event
total:
type: integer
description: Total number of events matching the query
Expand Down Expand Up @@ -998,16 +1024,14 @@ components:
type: object
description: Policy attached to the launch token
properties:
agent_name:
type: string
allowed_scope:
type: array
items:
type: string
ttl:
type: integer
max_ttl:
type: integer
single_use:
type: boolean

# =========================================================================
# App Registration
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ These are explicit trust boundaries and limitations of the current implementatio
| Dependency | Version | Purpose |
|---|---|---|
| `github.com/prometheus/client_golang` | v1.23.2 | Metrics exposition |
| `github.com/prometheus/client_model` | v0.6.2 | Metrics data model |
| `github.com/prometheus/client_model` | v0.6.2 | Metrics data model (indirect — pulled in by `client_golang`, not a direct dependency) |
| `github.com/spiffe/go-spiffe/v2` | v2.6.0 | SPIFFE ID validation |
| `github.com/spf13/cobra` | v1.10.2 | CLI framework for `awrit` |
| `modernc.org/sqlite` | v1.46.1 | Pure-Go SQLite driver for audit event persistence (zero CGo) |
Expand Down
28 changes: 15 additions & 13 deletions docs/awrit-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ Query the broker's audit trail. Events record all meaningful actions: token issu

| Column | Description |
|--------|-------------|
| ID | Unique event identifier (UUID) |
| ID | Unique event identifier (sequential, e.g. `evt-000001`) |
| TIMESTAMP | RFC3339 timestamp when the event occurred |
| EVENT TYPE | Type of event (token_issued, token_revoked, etc.) |
| AGENT ID | SPIFFE ID of the agent involved (or empty for broker operations) |
Expand All @@ -128,7 +128,7 @@ Query the broker's audit trail. Events record all meaningful actions: token issu
{
"events": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"id": "evt-000001",
"timestamp": "2026-02-27T15:32:10Z",
"event_type": "token_issued",
"agent_id": "spiffe://example.com/agent/crawler",
Expand Down Expand Up @@ -634,15 +634,15 @@ Existing agent tokens issued by this app remain valid until expiration; use `awr
| Column | Description |
|--------|-------------|
| APP_ID | App identifier |
| STATUS | Status after deregistration (deregistered) |
| STATUS | Status after deregistration (`inactive` — soft delete, record retained) |
| DEREGISTERED_AT | Deregistration timestamp |

**Output (JSON):**

```json
{
"app_id": "app-12345",
"status": "deregistered",
"status": "inactive",
"deregistered_at": "2026-02-27T15:32:10Z"
}
```
Expand Down Expand Up @@ -723,18 +723,20 @@ awrit init --force

**Output File Format:**

The config file is YAML containing the admin secret (plaintext in dev, bcrypt hash in prod):
The config file is a plain `KEY=VALUE` file (not YAML) containing the admin secret (plaintext in dev, bcrypt hash in prod). Lines starting with `#` are comments; the parser reads `MODE` and `ADMIN_SECRET`:

```yaml
mode: development
admin_secret: dGVzdC1zZWNyZXQtdmFsdWU=
```ini
# Broker Configuration
# Generated by: awrit init --mode=dev
MODE=development
ADMIN_SECRET=dGVzdC1zZWNyZXQtdmFsdWU=
```

or in prod:

```yaml
mode: production
admin_secret: $2a$12$...bcrypt.hash.here...
```ini
MODE=production
ADMIN_SECRET=$2a$12$...bcrypt.hash.here...
```

---
Expand Down Expand Up @@ -869,7 +871,7 @@ awrit audit events
Output:
```
ID TIMESTAMP EVENT TYPE AGENT ID OUTCOME DETAIL
550e8400-e29b-41d4-a716-446655440000 2026-02-27T15:32:10Z token_issued spiffe://example.com/agent/crawler success Token issued for scope read
evt-000001 2026-02-27T15:32:10Z token_issued spiffe://example.com/agent/crawler success Token issued for scope read
```

**Advantages:**
Expand All @@ -894,7 +896,7 @@ Output:
{
"events": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"id": "evt-000001",
"timestamp": "2026-02-27T15:32:10Z",
"event_type": "token_issued",
"agent_id": "spiffe://example.com/agent/crawler",
Expand Down
31 changes: 17 additions & 14 deletions docs/common-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ except requests.exceptions.RequestException as e:
{
"valid": true,
"claims": {
"iss": "agentwrit",
"iss": "https://broker.example.com",
"sub": "spiffe://agentwrit.local/agent/orch-001/task-analyze-q4/proc-abc123",
"exp": 1745405630,
"iat": 1745405330,
Expand All @@ -243,11 +243,12 @@ except requests.exceptions.RequestException as e:
```json
{
"valid": false,
"error": "token_revoked",
"detail": "Token JTI a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 was revoked at 2026-02-15T10:00:00Z"
"error": "token is invalid or expired"
}
```

The invalid response is deliberately **sanitized**: a single opaque `error` string, no `detail` and no distinct error code. Expired, revoked, and bad-signature tokens are indistinguishable to the caller — the broker won't tell a holder *why* a token failed.

**TypeScript/Node.js example:**

```typescript
Expand All @@ -268,9 +269,8 @@ interface ClaimSet {

interface ValidationResponse {
valid: boolean;
claims?: ClaimSet;
error?: string;
detail?: string;
claims?: ClaimSet; // present only when valid
error?: string; // present only when invalid (opaque, sanitized)
}

async function validateToken(broker: string, token: string): Promise<ClaimSet> {
Expand All @@ -287,7 +287,7 @@ async function validateToken(broker: string, token: string): Promise<ClaimSet> {
const result = (await response.json()) as ValidationResponse;

if (!result.valid) {
throw new Error(`Token invalid: ${result.error} - ${result.detail}`);
throw new Error(`Token invalid: ${result.error}`);
}

return result.claims!;
Expand Down Expand Up @@ -327,13 +327,16 @@ try {

**If this fails:**

| Issue | Meaning | Action |
|-------|---------|--------|
| Broker unreachable | Network connectivity issue | Verify broker is running; check firewall rules |
| Invalid token format | Token is malformed | Ensure you're passing the full JWT string |
| `token_expired` | Token timestamp has passed | Renew the token or re-register to get a fresh one |
| `token_revoked` | Token was revoked by operator | Re-register to get a fresh token; investigate with operator |
| `invalid_signature` | JWT signature verification failed | Token may be corrupted or forged; obtain new one |
A `200 OK` with `"valid": false` covers every token-rejection reason — the response does **not** distinguish them. The reasons below are why a token *can* be invalid, not values you'll see in the response. Re-register to get a fresh token; an operator can confirm the cause from the audit trail.

| Reason (not distinguishable in the response) | Meaning |
|-------|---------|
| Expired | Token timestamp has passed |
| Revoked | Token was revoked by an operator |
| Bad signature | JWT signature verification failed (corrupted or forged) |
| Malformed | Not a parseable JWT — ensure you pass the full token string |

A network error (broker unreachable) surfaces separately as a failed HTTP request, not as `valid: false`.

---

Expand Down
6 changes: 3 additions & 3 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ AgentWrit implements the **Ephemeral Agent Credentialing** pattern, a security a
5. **Immutable Accountability** -- All agent actions are logged in tamper-proof audit trails
6. **Delegation Chain Integrity** -- Multi-agent workflows maintain cryptographic proof of authorization lineage

These principles manifest as **seven concrete components** that work together to eliminate the credential exposure problem:
These principles manifest as **eight concrete components** that work together to eliminate the credential exposure problem:

```mermaid
flowchart TB
Expand Down Expand Up @@ -172,7 +172,7 @@ stateDiagram-v2
```

**What AgentWrit implements:**
- JWTs signed with EdDSA (Ed25519), containing claims: `sub` (SPIFFE ID), `scope`, `task_id`, `orch_id`, `delegation_chain`, `chain_hash`, `jti`, `exp`, `iat`
- JWTs signed with EdDSA (Ed25519), containing claims: `iss`, `sub` (SPIFFE ID), `aud`, `scope`, `task_id`, `orch_id`, `delegation_chain`, `chain_hash`, `jti`, `sid`, `exp`, `nbf`, `iat`
- Scope format: `action:resource:identifier` with wildcard `*` support in the identifier position
- Default TTL of 300 seconds (5 minutes), configurable via `AA_DEFAULT_TTL`
- Scope attenuation enforced at registration, delegation, and app-authenticated launch token creation
Expand Down Expand Up @@ -290,7 +290,7 @@ flowchart LR
E2 -->|"hash feeds into"| E3
```

Each hash is computed over: `prev_hash | event_id | timestamp | event_type | agent_id | task_id | orch_id | detail`. The genesis event uses 64 zeros as its previous hash. If an attacker modifies any event, every subsequent hash becomes invalid.
Each hash is computed over 13 fields: `prev_hash | event_id | timestamp | event_type | agent_id | task_id | orch_id | detail | resource | outcome | deleg_depth | deleg_chain_hash | bytes_transferred`. The genesis event uses 64 zeros as its previous hash. If an attacker modifies any event, every subsequent hash becomes invalid.

**What AgentWrit implements:**
- `AuditLog` provides append-only storage with automatic hash chaining using SHA-256
Expand Down
Loading