A policy-as-code engine for infrastructure access control. Define roles, resources, and safety tiers in YAML. PolicyForge evaluates requests and returns allow, deny, or require_approval — with tamper-evident audit logs, compliance evidence bundles, and drift detection built in.
No external dependencies. No database. Just Go and a YAML file.
⭐ If this project is useful or interesting, consider giving it a star — it helps others discover it.
Infrastructure teams need a way to enforce access rules that is auditable, deterministic, and doesn't require a complex policy runtime. PolicyForge answers the question: "Can this identity perform this action on this resource at this automation tier?" — and produces a compliance-ready evidence trail for every answer.
| Feature | Description |
|---|---|
| RBAC + Safety Tiers | Roles define allowed actions, resources, and automation tiers. A max_tier cap triggers approval when exceeded. |
| Agent Policy Envelopes | Non-human identities (bots, CI, AI) operate inside envelopes that restrict scope independent of RBAC. |
| Tamper-Evident Audit Log | Every decision is appended to a SHA-256 hash-chained JSONL file. |
| Evidence Bundles | Each evaluation produces a compliance-ready JSON artifact with mapped controls (PCI-DSS). |
| Approval Workflow | require_approval decisions create persistent records you can approve or reject. |
| Drift Detection | Re-evaluates the audit log against the current policy to find decisions that would now be denied. |
| Session-Backed Auth | Bearer token and OIDC stub authentication with session lifecycle management. |
| Agent TTL Enforcement | Agent sessions expire after a configurable window — denied before engine evaluation. |
| CLI + API Parity | Both interfaces use the same evaluation pipeline and produce identical outputs. |
This project demonstrates:
- Policy engine design — deterministic evaluation with structured deny/approval/allow reasons
- Hash-chained audit logs — tamper detection without a database
- Layered authorization — RBAC + agent envelopes + safety tier caps
- Compliance automation — evidence bundles with automatic control mapping
- Session security — TTL enforcement, revocation, identity override to prevent escalation
- Drift detection — post-hoc analysis of policy changes against historical decisions
- CLI/API parity — shared service layer guarantees identical behavior
Approval workflow + drift detection:
Identity & sessions — bearer token auth, session revocation, debug OIDC:
git clone https://github.com/texasbe2trill/policyforge.git
cd policyforge
go test ./...Run three scenarios:
# Allow — viewer reads staging
go run ./cmd/policyforge --policy ./configs/policy.yaml \
--subject alice --role viewer --resource staging/payment-service \
--action read --tier read_only
# Deny — viewer attempts restart
go run ./cmd/policyforge --policy ./configs/policy.yaml \
--subject alice --role viewer --resource staging/payment-service \
--action restart --tier read_only
# Require approval — operator restarts prod
go run ./cmd/policyforge --policy ./configs/policy.yaml \
--subject chris --role operator --resource prod/payment-service \
--action restart --tier supervised_writeOr run the bundled demo scripts via Make:
make demo-cli # runs scripts/demo.sh
make demo-api # runs scripts/demo-api.shThese are Make targets, not standalone files. They do not require VHS. If jq is installed the JSON output is pretty-printed; otherwise the scripts fall back to raw JSON.
Ready-to-use policy configurations for common scenarios in examples/policy-packs/:
| Pack | Use Case |
|---|---|
pci-demo.yaml |
PCI-DSS-aligned environment with strict prod controls |
prod-sre.yaml |
SRE team with tiered escalation |
ci-agent.yaml |
CI/CD pipeline agents with narrow, time-boxed permissions |
breakglass-admin.yaml |
Emergency access with mandatory approval on every action |
cp examples/policy-packs/pci-demo.yaml configs/policy.yaml
go run ./cmd/policyforge --policy configs/policy.yaml --drift-checkStatic examples of every response type are in docs/samples/:
allow-response.json— Clean passdeny-response.json— Hard deny with reasonapproval-response.json— Soft gate requiring approvaldrift-findings.json— Drift detection findingevidence-bundle.json— Full compliance evidence bundle
go run ./cmd/policyforge [flags]
| Flag | Default | Description |
|---|---|---|
--policy |
configs/policy.yaml |
Path to the policy YAML file |
--input |
(none) | Path to a JSON request file. Overrides all individual request flags. |
--subject |
(required if no --input) | Identity making the request |
--role |
(required if no --input) | Role assigned to the subject |
--resource |
(required if no --input) | Resource being accessed |
--action |
(required if no --input) | Action being performed |
--tier |
(required if no --input) | Requested safety tier |
--auto-approve |
false |
Converts a require_approval decision to allow |
--agent |
(none) | Agent envelope name to apply on top of RBAC |
--drift-check |
(flag) | Scan audit log for policy drift and exit |
--list-approvals |
(flag) | Print all approval records and exit |
--approve-id |
(none) | Approve a pending request by approval ID |
--reject-id |
(none) | Reject a pending request by approval ID |
--decided-by |
(none) | Name of person making the approval decision |
--decision-note |
(none) | Optional note for the approval decision |
--list-sessions |
(flag) | Print all sessions from artifacts/sessions.json and exit |
--revoke-session-id |
(none) | Revoke the session with the given ID and exit |
--version |
(flag) | Print version and exit |
go run ./cmd/policyforge \
--policy ./configs/policy.yaml \
--subject alice \
--role auditor \
--resource prod/api-gateway \
--action read \
--tier read_onlygo run ./cmd/policyforge --policy ./configs/policy.yaml --input ./examples/request.json2026/04/04 18:31:31 invalid CLI request: missing required request fields: subject, resource, action, requested_tier
An agent is a non-human identity — a remediation bot, CI pipeline, AI system, or any automated workflow. Because agents act autonomously, they operate inside a policy envelope that further restricts what they may do, on top of normal RBAC.
If a request includes an agent field, both the role-based checks and the envelope checks must pass. Failing either returns a deny or require_approval.
Envelopes live in configs/policy.yaml under agent_envelopes:
agent_envelopes:
- name: "remediation-bot"
allowed_resources:
- "staging/*" # wildcard: matches any staging/ resource
allowed_actions:
- "read"
- "restart"
max_tier: "autonomous_write"
session_ttl_minutes: 30
- name: "prod-operator-bot"
allowed_resources:
- "prod/payment-service"
allowed_actions:
- "read"
max_tier: "read_only"
session_ttl_minutes: 15allowed_resources supports prefix/* wildcard patterns. max_tier caps the envelope independently of the role's own cap.
# allow: remediation-bot restarts staging resource within its envelope
go run ./cmd/policyforge \
--policy ./configs/policy.yaml \
--subject bot \
--role operator \
--resource staging/payment-service \
--action restart \
--tier read_only \
--agent remediation-bot{
"decision": "allow",
"reasons": ["allow: all policy checks passed"],
"timestamp": "2026-04-05T00:25:51Z",
"request_id": "req-1775348751231064000",
"matched_resource": "staging/payment-service",
"evaluated_role": "operator"
}# deny: remediation-bot tries to access a prod resource (outside its staging/* envelope)
go run ./cmd/policyforge \
--policy ./configs/policy.yaml \
--subject bot \
--role operator \
--resource prod/payment-service \
--action read \
--tier read_only \
--agent remediation-bot{
"decision": "deny",
"reasons": ["deny: agent 'remediation-bot' is not allowed to access resource 'prod/payment-service'"],
"timestamp": "2026-04-05T00:25:51Z",
"request_id": "req-1775348751288228000",
"matched_resource": "prod/payment-service",
"evaluated_role": "operator"
}# deny: prod-operator-bot tries to restart (only 'read' is in its envelope)
go run ./cmd/policyforge \
--policy ./configs/policy.yaml \
--subject bot \
--role operator \
--resource prod/payment-service \
--action restart \
--tier read_only \
--agent prod-operator-bot{
"decision": "deny",
"reasons": ["deny: agent 'prod-operator-bot' cannot perform action 'restart'"],
"timestamp": "2026-04-05T00:26:04Z",
"request_id": "req-1775348764835152000",
"matched_resource": "prod/payment-service",
"evaluated_role": "operator"
}Include agent in the JSON body:
curl -X POST http://localhost:8080/evaluate \
-H "Content-Type: application/json" \
-d '{
"subject": "bot",
"role": "operator",
"resource": "staging/payment-service",
"action": "restart",
"requested_tier": "read_only",
"agent": "remediation-bot"
}'go run ./cmd/policyforge --policy ./configs/policy.yaml --input ./examples/request-agent.jsonPolicyForge ships a second entrypoint, cmd/policyforge-api, that exposes the same evaluation logic over HTTP.
go run ./cmd/policyforge-api --policy ./configs/policy.yaml
# 2026/04/05 00:06:36 policyforge-api listening on :8080Or with a custom address:
go run ./cmd/policyforge-api --addr :9090| Method | Path | Description |
|---|---|---|
GET |
/health |
Returns {"status":"ok"} when ready |
POST |
/evaluate |
Evaluates a decision request |
GET |
/sessions |
List all sessions (admin role required) |
POST |
/sessions/revoke |
Revoke a session by ID (admin role required) |
Request body is the same schema as a JSON input file:
curl -X POST http://localhost:8080/evaluate \
-H "Content-Type: application/json" \
-d '{
"subject": "chris",
"role": "operator",
"resource": "prod/payment-service",
"action": "restart",
"requested_tier": "supervised_write"
}'{
"decision": "require_approval",
"reasons": [
"approval: resource 'prod/payment-service' requires approval",
"approval: tier 'supervised_write' requires approval"
],
"timestamp": "2026-04-05T00:06:38Z",
"request_id": "req-1775347598162694000",
"matched_resource": "prod/payment-service",
"evaluated_role": "operator"
}Pass ?auto_approve=true to convert a require_approval decision to allow:
curl -X POST "http://localhost:8080/evaluate?auto_approve=true" \
-H "Content-Type: application/json" \
-d '{"subject":"chris","role":"operator","resource":"prod/payment-service","action":"restart","requested_tier":"supervised_write"}'When the query parameter is used, the response includes "auto-approved via query parameter" in the reasons list.
Every API request produces an audit log entry and evidence bundle, identical to the CLI.
By default the API runs unauthenticated (backward compatible). Pass --tokens to enable bearer-token auth:
go run ./cmd/policyforge-api \
--policy ./configs/policy.yaml \
--tokens ./configs/tokens.yamltokens:
- token: "dev-admin-token"
subject: "chris"
role: "admin"
auth_type: "local_token"
- token: "operator-token"
subject: "alex"
role: "operator"
auth_type: "local_token"
- token: "agent-remediation-token"
subject: "policyforge-agent"
agent: "remediation-bot"
role: "operator"
auth_type: "agent_token"Pass the token in the Authorization header:
curl -X POST http://localhost:8080/evaluate \
-H "Authorization: Bearer dev-admin-token" \
-H "Content-Type: application/json" \
-d '{
"resource": "prod/payment-service",
"action": "restart",
"requested_tier": "supervised_write"
}'When auth is enabled, subject, role, and agent are always sourced from the token — request body identity fields are overridden. This prevents callers from escalating their own privileges.
For local development without a real OIDC provider, set the environment variable POLICYFORGE_ENABLE_DEBUG_OIDC=true. This enables the X-Debug-OIDC-Subject and X-Debug-OIDC-Role request headers as an auth path:
POLICYFORGE_ENABLE_DEBUG_OIDC=true go run ./cmd/policyforge-api \
--policy ./configs/policy.yaml --tokens ./configs/tokens.yaml
# Then from another terminal:
curl -X POST http://localhost:8080/evaluate \
-H "X-Debug-OIDC-Subject: chris" \
-H "X-Debug-OIDC-Role: admin" \
-H "Content-Type: application/json" \
-d '{"resource":"staging/payment-service","action":"read","requested_tier":"read_only"}'Never enable debug OIDC in production. The env var is intentionally verbose to make accidental enablement obvious.
When auth is enabled, two session-management endpoints are available:
| Method | Path | Description |
|---|---|---|
GET |
/sessions |
List all sessions (admin role required) |
POST |
/sessions/revoke |
Revoke a session by ID (admin role required) |
# List all sessions
curl -H "Authorization: Bearer dev-admin-token" http://localhost:8080/sessions
# Revoke a session
curl -X POST http://localhost:8080/sessions/revoke \
-H "Authorization: Bearer dev-admin-token" \
-H "Content-Type: application/json" \
-d '{"session_id": "<id>"}'Non-admin requests to these endpoints return 403 Forbidden.
Every authenticated request is backed by a session — a persistent record stored in artifacts/sessions.json.
- First request with a valid token → a new session is created with an
issued_attimestamp and TTL. - Subsequent requests with the same token → the existing active session is reused (no new row per request).
- A revoked session → all future requests with that token return
401 Unauthorized.
{
"session_id": "sess-1775400000000000000",
"subject": "chris",
"role": "admin",
"auth_type": "local_token",
"issued_at": "2026-04-05T00:00:00Z",
"expires_at": "2026-04-05T01:00:00Z",
"status": "active"
}| Field | Description |
|---|---|
session_id |
Unique ID for this session |
subject |
Authenticated user or service identity |
role |
Role at session creation time |
auth_type |
local_token, oidc_stub, or agent_token |
issued_at |
RFC3339 timestamp the session was created |
expires_at |
RFC3339 timestamp when the session expires |
status |
active, expired, or revoked |
| Auth type | Default TTL |
|---|---|
| Human token | 60 minutes |
| Agent token | 30 minutes (or session_ttl_minutes from envelope) |
# List all sessions
go run ./cmd/policyforge --list-sessions
# Revoke a specific session
go run ./cmd/policyforge --revoke-session-id sess-1775400000000000000Agent sessions have a time-bound window controlled by session_ttl_minutes in the agent envelope:
agent_envelopes:
- name: "remediation-bot"
session_ttl_minutes: 30 # deny after 30 minutes from session issued_atWhen an agent request arrives via the API with a valid token, the evaluator checks whether time.Since(session.IssuedAt) > TTL. If the TTL is exceeded, the request is denied immediately — before engine evaluation:
{
"decision": "deny",
"reasons": ["deny: agent session for 'remediation-bot' exceeded TTL"],
"timestamp": "2026-04-05T00:35:00Z",
"request_id": "req-1775349300000000000"
}To continue, the operator must revoke the old session and re-authenticate (re-request with the token, which creates a fresh session).
Policies are defined in configs/policy.yaml. The file has three sections.
Tiers define the level of automation. Each tier can optionally require approval.
safety_tiers:
- name: "read_only"
requires_approval: false # read operations, no approval needed
- name: "supervised_write"
requires_approval: true # writes require human sign-off
- name: "autonomous_write"
requires_approval: false # fully automated, granted to admins onlyEach role defines what actions, tiers, and resources it can access. max_tier caps the automation level a role can use autonomously — requesting above it triggers require_approval regardless of other tier settings.
roles:
- name: "admin"
allowed_actions: ["read", "write", "restart", "scale"]
allowed_tiers: ["read_only", "supervised_write", "autonomous_write"]
max_tier: "autonomous_write"
allowed_resources:
- "prod/payment-service"
- "prod/api-gateway"
- "staging/payment-service"
- name: "operator"
allowed_actions: ["read", "restart", "scale"]
allowed_tiers: ["read_only", "supervised_write"]
max_tier: "supervised_write"
allowed_resources:
- "prod/payment-service"
- "staging/payment-service"
- name: "auditor"
allowed_actions: ["read"]
allowed_tiers: ["read_only"]
max_tier: "read_only"
allowed_resources:
- "prod/payment-service"
- "prod/api-gateway"
- "staging/payment-service"
- name: "viewer"
allowed_actions: ["read"]
allowed_tiers: ["read_only"]
max_tier: "read_only"
allowed_resources:
- "staging/payment-service"Resources declare which infrastructure targets exist, and whether accessing them inherently requires approval.
resources:
- name: "prod/payment-service"
requires_approval: true # production — always require approval
- name: "prod/api-gateway"
requires_approval: true # production — always require approval
- name: "staging/payment-service"
requires_approval: false # staging — safe to allow directlyThe engine evaluates every request against the same deterministic sequence of checks. The first failing check stops evaluation and returns immediately.
| # | Check | Failing outcome |
|---|---|---|
| 1 | Role exists in policy | deny |
| 2 | Action is in role's allowed_actions |
deny |
| 3 | Resource exists in policy | deny |
| 4 | Resource is in role's allowed_resources |
deny |
| 5 | Requested tier exists in policy | deny |
| 6 | Requested tier is in role's allowed_tiers |
deny |
| 7 | Requested tier does not exceed role's max_tier |
require_approval |
| 8 | Agent envelope: exists, resource allowed, action allowed, tier ≤ max | deny / require_approval |
| 9 | Resource requires_approval: false and tier requires_approval: false |
require_approval |
| — | All checks pass | allow |
Reason messages are structured so they are easy to parse:
deny: ...for hard failuresapproval: ...for soft gatesallow: ...for clean passes
Every response is a JSON object printed to stdout:
{
"decision": "allow | deny | require_approval",
"reasons": ["structured reason message"],
"timestamp": "2026-04-04T23:41:52Z",
"request_id": "req-1775346112600030000",
"matched_resource": "staging/payment-service",
"evaluated_role": "viewer"
}| Field | Description |
|---|---|
decision |
Outcome: allow, deny, or require_approval |
reasons |
Ordered list explaining the decision |
timestamp |
RFC3339 UTC time the decision was made |
request_id |
Unique ID for this evaluation (also in audit log) |
matched_resource |
Resource name resolved from policy |
evaluated_role |
Role name resolved from policy |
Scenario A — allow (examples/request-allow.json):
Viewer reads staging payment service with read_only tier.
{
"decision": "allow",
"reasons": [
"allow: all policy checks passed"
],
"timestamp": "2026-04-04T23:41:52Z",
"request_id": "req-1775346112600030000",
"matched_resource": "staging/payment-service",
"evaluated_role": "viewer"
}Scenario B — deny (examples/request-deny-action.json):
Viewer attempts restart, which is not in their allowed_actions.
{
"decision": "deny",
"reasons": [
"deny: action 'restart' is not allowed for role 'viewer'"
],
"timestamp": "2026-04-04T23:41:52Z",
"request_id": "req-1775346112640457000",
"matched_resource": "staging/payment-service",
"evaluated_role": "viewer"
}Scenario C — require_approval + auto-approve (examples/request-require-approval.json with --auto-approve):
Operator restarts prod payment service via supervised_write. Both resource and tier require approval. --auto-approve converts the decision to allow.
{
"decision": "allow",
"reasons": [
"approval: resource 'prod/payment-service' requires approval",
"approval: tier 'supervised_write' requires approval",
"auto-approved via CLI flag"
],
"timestamp": "2026-04-04T23:41:52Z",
"request_id": "req-1775346112679835000",
"matched_resource": "prod/payment-service",
"evaluated_role": "operator"
}Every evaluation — regardless of outcome — is appended to artifacts/audit.jsonl in JSON Lines format. The directory is created automatically.
Each line is a single JSON object:
{"request_id":"req-1775346112600030000","timestamp":"2026-04-04T23:41:52Z","subject":"alex","role":"viewer","resource":"staging/payment-service","action":"read","decision":"allow","reasons":["allow: all policy checks passed"]}
{"request_id":"req-1775346112640457000","timestamp":"2026-04-04T23:41:52Z","subject":"pat","role":"viewer","resource":"staging/payment-service","action":"restart","decision":"deny","reasons":["deny: action 'restart' is not allowed for role 'viewer'"]}Each record includes a hash (SHA-256 of the key fields) and a previous_hash that chains each entry to the one before it. This makes undetected modification of the log difficult — any change to a record invalidates its hash and breaks the chain.
The audit log is append-only. It is never overwritten by the CLI or API. To query it:
# view the last 10 decisions
tail -n 10 artifacts/audit.jsonl
# filter for denials only
grep '"decision":"deny"' artifacts/audit.jsonl
# filter for a specific subject
grep '"subject":"chris"' artifacts/audit.jsonl
# pretty-print a single entry
tail -n 1 artifacts/audit.jsonl | jq .Every evaluation — CLI or API — writes a compliance-ready JSON file to artifacts/evidence/<bundle_id>.json. The directory is created automatically.
Each bundle captures the full context of the decision:
{
"bundle_id": "ev_1775347598163202000_97651",
"request_id": "req-1775347598162694000",
"timestamp": "2026-04-05T00:06:38Z",
"decision": "require_approval",
"subject": "chris",
"role": "operator",
"resource": "prod/payment-service",
"action": "restart",
"requested_tier": "supervised_write",
"reasons": [
"approval: resource 'prod/payment-service' requires approval",
"approval: tier 'supervised_write' requires approval"
],
"controls": [
"PCI-DSS-7.2",
"PCI-DSS-10.2"
]
}Controls are mapped automatically from the request context:
| Condition | Control |
|---|---|
Resource path contains prod |
PCI-DSS-7.2 |
Action is restart, write, or scale |
PCI-DSS-10.2 |
Decision is deny |
SECURITY-ENFORCEMENT |
When a request evaluates to require_approval, PolicyForge now creates a persisted approval record in artifacts/approvals.json. This turns the soft gate into a real workflow — you can list pending approvals, approve them, or reject them.
- A
require_approvaldecision creates a record withstatus: pending --auto-approvecreates the record withstatus: approvedimmediately- A human reviewer uses
--approve-idor--reject-idto resolve it
go run ./cmd/policyforge \
--policy ./configs/policy.yaml \
--input ./examples/request-require-approval.jsonApproval required -- approval ID: apr-1775349830126355000
{
"decision": "require_approval",
...
}
go run ./cmd/policyforge --list-approvals
# or:
make approvals[
{
"approval_id": "apr-1775349830126355000",
"request_id": "req-1775349830121985000",
"status": "pending",
"subject": "chris",
"role": "operator",
"resource": "prod/payment-service",
"action": "restart",
"requested_tier": "supervised_write",
"reasons": ["approval: resource 'prod/payment-service' requires approval"],
"requested_at": "2026-04-05T00:06:38Z"
}
]go run ./cmd/policyforge \
--approve-id apr-1775349830126355000 \
--decided-by chris \
--decision-note "approved for emergency maintenance"{"approved": "apr-1775349830126355000", "decided_by": "chris"}go run ./cmd/policyforge \
--reject-id apr-1775349830126355000 \
--decided-by security-team \
--decision-note "insufficient justification"Approval records are stored in artifacts/approvals.json and are linked to their evidence bundles via approval_id.
Drift detection re-evaluates every record in the audit log against the current policy. If a request was previously allowed but today's policy would deny or require_approval it, that's a finding.
This answers the question: "Has our policy tightened since these decisions were made? Are there any past actions that would no longer be permitted?"
go run ./cmd/policyforge \
--policy ./configs/policy.yaml \
--drift-check
# or:
make driftFindings are written to artifacts/drift/findings.json and printed to stdout.
{"findings": [], "summary": "no drift detected"}[
{
"finding_id": "drift-1775349830126355000",
"timestamp": "2026-04-05T01:00:00Z",
"request_id": "req-1775346112600030000",
"subject": "alice",
"role": "viewer",
"resource": "staging/payment-service",
"action": "restart",
"requested_tier": "read_only",
"observed_decision": "allow",
"expected_decision": "deny",
"severity": "high",
"drift_type": "unauthorized_action",
"message": "request was allowed but current policy would deny: deny: action 'restart' is not allowed for role 'viewer'"
}
]drift_type |
Meaning |
|---|---|
decision_mismatch |
Policy outcome changed but no specific violation |
unauthorized_resource_access |
Role no longer allowed to access this resource |
unauthorized_action |
Action no longer in role's allowed_actions |
agent_envelope_violation |
Agent envelope no longer permits the action/resource |
tier_exceeded |
Tier request exceeds current role max_tier |
| Severity | Condition |
|---|---|
high |
Was allow, current policy would deny |
medium |
Was allow, current policy would require_approval |
low |
Any other mismatch |
When a decision is require_approval the CLI prints a notification line to stderr and writes the JSON response to stdout:
Approval required -- approval ID: apr-1775349830126355000
{ ... }
Pass --auto-approve to immediately convert the decision to allow and append the reason auto-approved via CLI flag to the response. The audit log records the final outcome (allow), preserving all original approval reasons alongside it.
go run ./cmd/policyforge \
--policy ./configs/policy.yaml \
--input ./examples/request-require-approval.json \
--auto-approvepolicyforge/
├── cmd/
│ ├── policyforge/
│ │ └── main.go # CLI entry point
│ └── policyforge-api/
│ ├── main.go # HTTP API entry point
│ └── handler_test.go # API handler tests
├── internal/
│ ├── approval/ # Approval CRUD with JSON persistence
│ ├── audit/ # Append-only JSONL logger with hash chain
│ ├── auth/ # Bearer token auth, OIDC stub, middleware
│ ├── compliance/ # PCI-DSS control mapping
│ ├── config/ # YAML policy + JSON request loaders
│ ├── drift/ # Post-hoc drift detection
│ ├── evidence/ # Evidence bundle generation + CSV index
│ ├── policy/ # Deterministic evaluation engine
│ ├── service/ # Shared evaluation pipeline (CLI + API)
│ ├── session/ # Session lifecycle management
│ ├── types/ # Shared domain types
│ └── version/ # Version constant
├── configs/
│ ├── policy.yaml # Default policy
│ └── tokens.yaml # Token definitions (for auth mode)
├── examples/
│ ├── policy-packs/ # Ready-to-use policy configurations
│ └── *.json # Request examples
├── docs/
│ ├── architecture.md # System diagram and package breakdown
│ ├── overview.md # Project overview
│ └── samples/ # Static sample outputs
├── scripts/
│ ├── demo.sh # CLI demo script
│ └── demo-api.sh # API demo script
├── artifacts/ # Runtime outputs (gitignored)
├── Makefile
├── CONTRIBUTING.md
├── ROADMAP.md
├── SECURITY.md
└── README.md
See docs/architecture.md for the full system diagram and package breakdown.
Request → Auth → Identity Override → Agent TTL Check → Policy Engine → Decision
├→ Audit Log (hash-chained)
├→ Evidence Bundle
└→ Approval Record
make test # run all tests
make lint # go vet + gofmt check
make build # build binaries to bin/
make version # print current version
make fmt # format all Go files
make vet # run go vet
make demo-cli # runs scripts/demo.sh
make demo-api # runs scripts/demo-api.sh
make drift # run drift detection
make approvals # list pending approvals
make sessions # list sessionsmake demo-cli and make demo-api are scripted walkthroughs. Use make api or make api-auth when you want to start a long-running API server yourself.
make api # unauthenticated
make api-auth # with bearer token authOr directly:
go build -o bin/policyforge ./cmd/policyforge
go build -o bin/policyforge-api ./cmd/policyforge-apigo fmt ./...
go vet ./...Requires VHS (brew install vhs):
make demo # vhs demo.tape → artifacts/demo.gif
make demo-approvals # vhs demo-approvals.tape → artifacts/demo-approvals.gif| Tape | What it shows |
|---|---|
demo.tape |
Core evaluation: allow, deny, require_approval (auto), agent deny, audit log, evidence bundle |
demo-approvals.tape |
Approval workflow + drift detection: submit → list → approve → drift check |
demo-auth.tape |
Identity & sessions: bearer token auth, 401 enforcement, session list, revoke, debug OIDC |
All logic is covered by table-driven tests in internal/policy/engine_test.go and internal/config/request_test.go. When adding new engine checks or policy fields, add a corresponding test case before committing.
gopkg.in/yaml.v3— YAML parsing
MIT License. See LICENSE.
If PolicyForge is useful to you, consider supporting development:


