Compare commits

...

71 Commits

Author SHA1 Message Date
b4520bd731 docs(intent/scope): align with ops-warden as first shipped consumer
Some checks failed
CI / Lint (push) Has been cancelled
CI / Build and Test (push) Has been cancelled
ops-warden's SSH signing policy gate (FLEX-WP-0006 finished, FLEX-WP-0007
deploying) makes it flex-auth's first shipped protected-system consumer.
Update the intent baseline to match the implemented reality:

- SCOPE Current State: standalone Go core + /v1/check is implemented;
  FLEX-WP-0001/0005/0006 complete, 0007 blocked only on T4 VAULT_TOKEN.
- SCOPE Related/Overlapping + Disjoint From: ops-warden is now a consumer,
  not merely disjoint; the once-hypothetical "agt as flex-auth subject"
  flow is realized through the signing gate. Disjointness narrowed to the
  identity surface (warden issues certs, flex-auth never does).
- INTENT Consumer Patterns: lead with the shipped action-gate shape
  (ops-warden), keep Markitect as the planned knowledge-pipeline consumer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 20:37:07 +02:00
941501c590 FLEX-WP-0007: production registry fixture, tests, and sync runbook
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Add production_registry_snapshot.json from ops-warden inventory with CI
coverage for real actors, IAM subject binding, ttl_out_of_bounds, and
unknown_actor_resource. Extend serve contract tests with /healthz and
publish the registry sync contract for operator deployment.
2026-06-24 14:52:35 +02:00
fae0f00a69 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-06-24:
  - update .custodian-brief.md for flex-auth
2026-06-24 13:11:03 +02:00
77bcd55ddb chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-06-24:
  - update .custodian-brief.md for flex-auth
2026-06-24 01:45:33 +02:00
f0d1afa237 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-06-23:
  - update .custodian-brief.md for flex-auth
2026-06-23 21:19:00 +02:00
0fde95a87c FLEX-WP-0006: implement ops-warden signing gate policy
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-06-23 21:17:42 +02:00
53e0d055c9 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-06-23:
  - update .custodian-brief.md for flex-auth
2026-06-23 17:35:12 +02:00
e1c141234a chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-06-22:
  - update .custodian-brief.md for flex-auth
2026-06-22 23:21:50 +02:00
8a913d6163 Normalize agent instructions and workplan frontmatter (STATE-WP-0067)
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
- Align agent files with on-disk workplan prefixes (infer from workplan ids)
- Set workplan domain to registered domain_slug; add topic_slug where applicable
- Repair frontmatter delimiter formatting; migrate legacy task status literals
- Regenerate AGENTS.md, CLAUDE.md, and .claude/rules from State Hub templates
2026-06-22 23:16:25 +02:00
1be449dae8 Human-review .repo-classification.yaml (CUST-WP-0050 follow-up)
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-06-22 17:56:17 +02:00
1b899cd41c Add .repo-classification.yaml (CUST-WP-0050 T11 agent first-pass) 2026-06-22 17:47:36 +02:00
2230163de1 Add credential routing instructions for all agent runtimes
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Propagate shared credential-routing section (Codex, Claude, Grok, llm-connect)
from state-hub template via scripts/propagate_credential_routing.py.
2026-06-18 22:48:37 +02:00
3247f5d357 Add capability registry with seed entry from reuse-surface
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Bootstrap registry layout and migrate helix_forge capability owned by
this repository (REUSE-WP-0014-T02).
2026-06-16 01:46:54 +02:00
aa8e3a4e34 Align IAM Profile consumption with v0.2
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-22 14:35:30 +02:00
8354485632 Make INTENT.md self-coherent
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Remove external reference points so the intent stands on its own at the
abstract, stable level: drop named identity/SSO systems, named PDP/policy
products, named directory/enterprise systems, the named first-consumer
project, and the external IAM-profile path. Keep all of flex-auth's own
substance — purpose, responsibility boundary (stated as abstract roles:
identity layer / authorization / enforcement points), design principles,
concepts, API shape, standalone vs delegated mode, non-goals, early work.

Relationships to other systems belong in interface contracts and the
orchestration responsibility map, not in intent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:46:12 +02:00
12c4bed6f4 Refresh agent instruction files
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-18 16:55:41 +02:00
af3e8b2af2 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 07:28:16 +02:00
99a521e176 Document delegated mode operations
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 07:27:45 +02:00
1f5e9626e5 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 07:25:20 +02:00
32933c71f9 Add directory group resolver adapters
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 07:24:50 +02:00
ac9cf09545 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 07:19:17 +02:00
360025e38b Add Keycloak authorization adapter path
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 07:18:45 +02:00
3fdbc7acb7 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 07:13:56 +02:00
ad4895187b Add rule PDP adapter boundary
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 07:13:27 +02:00
4bb329c921 Add relationship PDP adapter boundary
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 07:06:14 +02:00
90021d16b6 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 07:04:59 +02:00
8a61e40bd6 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 06:58:50 +02:00
1ce0181e8f Implement Topaz adapter
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 06:58:04 +02:00
0fbb2a45c2 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 06:41:41 +02:00
184ce5a380 Document Markitect integration flow
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 06:41:07 +02:00
131fd2cd9b chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 06:37:27 +02:00
3d1967cb41 Add Markitect adapter contract tests
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 06:36:52 +02:00
7e09a21c5f Add Markitect check fixtures
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 06:32:05 +02:00
96e53bf1d9 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 06:31:11 +02:00
1c915f12d7 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 06:26:49 +02:00
b6712850c3 Define Markitect action vocabulary
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 06:26:13 +02:00
50e436093a chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 06:22:05 +02:00
9e2591c1f4 Import Markitect resource manifests
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 06:21:28 +02:00
dd4f688ab6 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 06:16:45 +02:00
12dbf52586 Mark core workplan completed
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 06:16:13 +02:00
a285959183 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 06:14:49 +02:00
6586adb4f5 Define Markitect resource namespace
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 06:14:04 +02:00
4c9f964425 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 06:08:42 +02:00
6bff4cd7c9 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 06:05:47 +02:00
18054bd160 Add CARING examples and coverage
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 06:05:18 +02:00
49655e40e0 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 06:00:25 +02:00
61e113f8b6 Add CLI and service skeleton
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 05:59:48 +02:00
ccf68332f8 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 05:52:12 +02:00
2b103ea70b Add local decision log
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 05:51:37 +02:00
4342f98d83 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Lint (push) Has been cancelled
CI / Build and Test (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 05:46:22 +02:00
faea068721 Implement list allowed and explain
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 05:45:36 +02:00
aa70dbebe1 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 05:39:41 +02:00
54984585e3 Implement deterministic check APIs
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 05:38:57 +02:00
fa1b42e678 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 05:31:20 +02:00
550d096cb2 Implement policy package loader
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 05:30:40 +02:00
2cce434d47 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 05:11:06 +02:00
3c4f8fc2b4 Implement local registry store
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 05:10:17 +02:00
4f4c290684 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-17:
  - update .custodian-brief.md for flex-auth
2026-05-17 05:01:55 +02:00
7fdf6d63d5 Implement canonical schema foundation
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 04:59:18 +02:00
dd0b9663c4 Refine workplans for CARING profile
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
2026-05-17 04:15:38 +02:00
f930e96568 IAM Profile consumption doc + claim fixtures; close FLEX-WP-0005
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Completes FLEX-WP-0005 T05 and closes the Foundations and Topaz
Alignment workstream.

docs/iam-profile-consumption.md captures flex-auth's input surface
against NetKingdom IAM Profile v0.1:
- boundary (flex-auth consumes verified claims; upstream layer
  validates signatures and audiences)
- normalized input envelope (matches Markitect's EnterpriseIdentity)
- required, recommended, and tolerated claim variations
- role-claim location union (top-level / realm_access / resource_access)
- scope encoding (string vs array)
- principal-type detection (human / service / emergency)
- group-overage and freshness expectations
- production vs local-development handling

examples/claims/ ships five contract fixtures:
- key-cape-lightweight.yaml (profile minimum)
- keycloak-heavy.yaml (full variation set + MFA)
- service-account.yaml (svc-* hub-to-hub)
- emergency.yaml (break-glass with incident metadata)
- keycloak-group-overage.yaml (Entra-style hasgroups: true)

All fixtures parse as valid YAML. They become contract tests for the
standalone evaluator (FLEX-WP-0002 P2.4) and the Topaz adapter
(FLEX-WP-0004 T01); both code paths must produce identical normalized
envelopes for the same fixture.

FLEX-WP-0005 workstream marked status=done in this file and completed
in the State Hub. FLEX-WP-0002 is now fully unblocked.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 09:09:36 +02:00
7471e07cbb Include topaz bundle .manifest in the spike example
Dotfile was missed by the previous git add — bundle/.manifest is the
OPA bundle root manifest Topaz consults to know which Rego packages
belong in the bundle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 09:05:04 +02:00
82177d88a9 Topaz alignment spike — mapping doc + green e2e example
Closes FLEX-WP-0005 T04. Validates ADR-003's commitment to shape the
standalone core for cheap Topaz adapter work.

Spike output:
- docs/topaz-mapping-spike.md — vocabulary map (subject, group, tenant,
  knowledge_base, document, plus parent / owner_team / reader / steward /
  member relations), Rego module shape, decision envelope, wire-protocol
  ranking (gRPC primary, REST fallback, embedding rejected), schema
  restatement recommendation, implications for FLEX-WP-0002 / 0004.
- examples/topaz/ — runnable docker-compose deploying Topaz with the
  flex-auth-shaped manifest. seed and probe one-shots cover three
  scenarios: alice (steward) allow, bob (group→reader) allow, eve
  (outsider) deny. End-to-end green on 2026-05-16:

    probe: steward-allow OK (check=true)
    probe: reader-allow  OK (check=true)
    probe: outsider-deny OK (check=false)
    probe: all checks passed

Key findings recorded as Implementation Notes in the spike doc:
- Rego input contract bridging (Topaz raw shape ↔ flex-auth canonical
  shape) is adapter scope, not core scope.
- Topaz identity objects are a Topaz convention; the adapter
  materializes them at directory import time.
- Directory-only permission resolution is sufficient for the common
  case; Rego is reserved for context-dependent decisions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 09:04:42 +02:00
52b5575048 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-16:
  - update .custodian-brief.md for flex-auth
2026-05-16 08:09:10 +02:00
f41fa1abb7 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-16:
  - FLEX-WP-0005-T004: todo → in_progress
2026-05-16 08:09:08 +02:00
f885e6d762 chore(consistency): sync task status from DB [auto]
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Updated by fix-consistency on 2026-05-16:
  - update .custodian-brief.md for flex-auth
2026-05-16 02:06:19 +02:00
e2d410de6e Pin FlexAuthResourceManifest schema (resource-registration-v0)
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Closes FLEX-WP-0005 T03. Shape pinned against the Markitect-side emitter
in markitect-tool/src/markitect_tool/policy/enterprise.py (FlexAuthResource
+ FlexAuthResourceManifest dataclasses, MKTT-WP-0014).

Artifacts:
- schemas/resource_manifest.schema.json (JSON Schema draft 2020-12)
- examples/markitect/resource_manifest.yaml (mirrors markitect-tool's
  example; metadata.flex_auth_contract = resource-registration-v0)
- pkg/api/resource_manifest.go (Go type with json + yaml tags, plus
  FlexAuthContractV0 const)
- pkg/api/resource_manifest_test.go (golden parse of the example +
  minimal-fields round-trip)

First external dep: gopkg.in/yaml.v3 v3.0.1. SBOM ingested into State Hub
(2 entries) — repo last_sbom_at now non-null. Makefile sbom target gains
a GOPATH/bin fallback so it works without ~/go/bin on PATH.

Interface change published to State Hub (a4a5293e-…) and inbox-notified
markitect-tool. The change is additive — Markitect's existing emitter
matches the pinned schema exactly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 02:04:00 +02:00
55120ec20a Land foundations: assessment, ADR-001/002/003, FLEX-WP-0005, Go skeleton
Pre-implementation assessment and boundary review
(docs/pre-implementation-assessment.md) lead to three ADRs:
- ADR-001 Go + repo skeleton
- ADR-002 Rego-in-Markdown policy package format
- ADR-003 Topaz-aligned MVP (Topaz spike moves into foundations)

New workplan FLEX-WP-0005 (Foundations and Topaz Alignment) is inserted
between WP-0001 (done) and WP-0002 (core). WP-0002 pins Rego-in-Markdown
for P2.3; WP-0004 P4.1 refocused from Topaz evaluation to Topaz adapter.

Go skeleton at repo root: cmd/flex-auth + internal/{registry,policy,
decision,audit,adapters} + pkg/api + Makefile + .golangci.yml + GitHub
Actions CI. make ci green locally; bin/flex-auth --version works.

INTENT/SCOPE cite the NetKingdom IAM Profile and add the ops-warden /
ops-bridge disjoint-surface clarifications.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:54:44 +02:00
485b3992de chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-15:
  - update .custodian-brief.md for flex-auth
2026-05-15 23:17:18 +02:00
36a3d3c898 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-15:
  - FLEX-WP-0005-T006: todo → done
2026-05-15 23:17:18 +02:00
15155c4c40 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-15:
  - FLEX-WP-0005-T002: todo → in_progress
2026-05-15 23:17:18 +02:00
178 changed files with 18930 additions and 168 deletions

20
.claude/rules/agents.md Normal file
View File

@@ -0,0 +1,20 @@
## Kaizen Agents
Specialized agent personas available on demand via the state-hub MCP.
**Discover:** `list_kaizen_agents()` — returns all agents with name, description, category
**Load:** `get_kaizen_agent("tdd-workflow")` — returns full instructions; read and follow them
Common agents:
| Agent | Category | When to use |
|-------|----------|-------------|
| `tdd-workflow` | testing | Step-by-step TDD8 workflow for any feature |
| `code-refactoring` | quality | Code quality analysis and safe refactoring |
| `test-maintenance` | testing | Diagnose and fix failing tests |
| `requirements-engineering` | process | Prevent interface/mock mismatches upfront |
| `keepaTodofile` | process | Maintain TODO.md during work |
| `project-management` | process | Track status, determine next steps |
| `datamodel-optimization` | quality | Optimize dataclasses and data structures |
All 17 agents: call `list_kaizen_agents()` for the full list.

View File

@@ -0,0 +1,8 @@
## Architecture
<!-- TODO: Describe the key design decisions and component structure.
Key modules, data flows, external integrations, state machines, etc. -->
## Quick Reference
`~/state-hub/mcp_server/TOOLS.md` — MCP tool reference

View File

@@ -0,0 +1,50 @@
# Credential and access routing
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
for inference. Run this check **before** requesting secrets, API keys, SSH access,
login tokens, or database passwords — in any repo, not only `ops-warden`.
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
other credential need belongs to another subsystem. **Do not** message
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
### Lookup (do this first)
```bash
warden route find "<describe your need>" --json
warden route show <catalog-id> --json
```
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
| Agent runtime | How to orient |
| --- | --- |
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=flex-auth` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
### Quick routing table
| I need… | Owner | ops-warden executes? |
| --- | --- | --- |
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes**`warden sign` |
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
| Authorization decision | flex-auth | No — route only |
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
### Anti-patterns (do not do these)
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
- Pasting secrets into Git, State Hub, workplans, logs, or chat
### Other capabilities (reuse-surface)
Non-credential capabilities are usually discovered through **reuse-surface** federation
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
every repo's agent instructions because it is high-frequency, high-risk, and easy to
get wrong.
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`

View File

@@ -0,0 +1,38 @@
## First Session Protocol
Triggered when `get_domain_summary("infotech")` shows **no workstreams**.
The project is registered but work has not yet been structured.
**Step 1 — Read, don't write**
- `~/the-custodian/canon/projects/infotech/project_charter_v0.1.md` — purpose, scope
- `~/the-custodian/canon/projects/infotech/roadmap_v0.1.md` — planned phases
- Scan repo root: README, directory structure, existing code or docs
**Step 2 — Survey in-progress work**
Look for TODOs, open branches, half-finished files. Note done vs. started but incomplete.
**Step 3 — Propose workstreams to Bernd**
Propose 13 workstreams — each a coherent strand, weeks to months, anchored to a
roadmap phase. **Wait for approval before creating.**
**Step 4 — Create workplan file first, then DB record (ADR-001)**
```
workplans/FLEX-WP-NNNN-<slug>.md ← write this first
```
Then register in the hub:
```
create_workstream(topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", title="...", owner="...", description="...")
create_task(workstream_id="<id>", title="...", priority="high|medium|low")
```
**Step 5 — Record the setup**
```
add_progress_event(
summary="First session: structured infotech into N workstreams, M tasks",
event_type="milestone",
topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a",
detail={"workstreams": [...], "tasks_created": M}
)
```
<!-- Delete or archive this file once past first session -->

View File

@@ -0,0 +1,8 @@
## Repo boundary
This repo owns **flex-auth** only. It does not own:
<!-- TODO: List what belongs in adjacent repos, e.g.:
- SSH key management → railiance-infra/
- State hub code → state-hub/
-->

View File

@@ -0,0 +1,5 @@
**Purpose:** flex-auth - (fill in purpose)
**Domain:** infotech
**Repo slug:** flex-auth
**Topic ID:** cee7bedf-2b48-46ef-8601-006474f2ad7a

View File

@@ -0,0 +1,85 @@
## Session Protocol
Dev Hub (State Hub API): http://127.0.0.1:8000
MCP server name in `~/.claude.json`: `dev-hub`
**Step 1 — Orient**
Read the offline-safe brief first — it works without a live hub connection:
```bash
cat .custodian-brief.md
```
Then call the MCP tool for richer cross-domain context when MCP tools are exposed:
```
get_domain_summary("infotech")
```
If MCP tools are unavailable in the current agent session, use the REST API:
```bash
curl -s "http://127.0.0.1:8000/state/summary" | python3 -m json.tool
```
If the hub is offline: `cd ~/state-hub && make api`
**Step 2 — Check inbox**
With MCP tools:
```
get_messages(to_agent="flex-auth", unread_only=True)
```
Mark read with `mark_message_read(message_id)`. Reply or act on coordination
requests before proceeding.
Without MCP tools:
```bash
curl -s "http://127.0.0.1:8000/messages/?to_agent=flex-auth&unread_only=true" \
| python3 -m json.tool
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
-H "Content-Type: application/json" -d '{}'
```
**Step 3 — Scan workplans**
```bash
ls workplans/
```
For each file with `status: ready`, `active`, or `blocked`, note pending
`wait`/`todo`/`progress` tasks.
**Step 4 — Present brief**
1. **Active workstreams** for `infotech` — title, task counts, blocking decisions
2. **Pending tasks** from `workplans/` + any `[repo:flex-auth]` hub tasks
3. **Goal guidance** — if `goal_guidance` in summary:
- `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"*
- `alignment_warnings`: flag if active work is not aligned with current goal
4. **Suggested next action** — highest-priority open item
5. **SBOM status** — flag if `last_sbom_at` is unset for this repo
If no workstreams: follow First Session Protocol (`first-session.md`).
**During work:** `record_decision()` · `add_progress_event()` · `resolve_decision()`
> State Hub is a *read model*. Bootstrap tools (`create_workstream`, `create_task`)
> are First Session Protocol only. Work structure belongs in repo files (ADR-001).
**Session close:**
With MCP tools:
```
add_progress_event(summary="...", topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", workstream_id="<uuid>")
```
Without MCP tools:
```bash
curl -s -X POST http://127.0.0.1:8000/progress/ \
-H "Content-Type: application/json" \
-d '{"topic_id":"cee7bedf-2b48-46ef-8601-006474f2ad7a","workstream_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
```
If workplan files were modified, ensure the local copy is up to date first:
```bash
git -C <repo_path> pull --ff-only
cd ~/state-hub && make fix-consistency REPO=flex-auth
```
For repos where implementation runs on a remote machine (e.g. CoulombCore),
use the combined target which pulls before fixing:
```bash
cd ~/state-hub && make fix-consistency-remote REPO=flex-auth
```
**C-15** (DB task ahead of file) is normal in multi-machine workflows — writeback
will sync the file to match DB. **C-16** (repo behind remote) blocks all writes
until you pull — intentional to prevent clobbering remote progress.

View File

@@ -0,0 +1,19 @@
## Stack
<!-- TODO: Fill in language, frameworks, and key dependencies -->
- **Language:**
- **Key deps:**
## Dev Commands
```bash
# TODO: Fill in the standard commands for this repo
# Install dependencies
# Run tests
# Lint / type check
# Build / package (if applicable)
```

View File

@@ -0,0 +1,40 @@
## Workplan Convention (ADR-001)
File location: `workplans/FLEX-WP-NNNN-<slug>.md`
ID prefix: `FLEX-WP-`
Work items originate as files in this repo **before** being registered in the hub.
Canonical workplan/workstream frontmatter statuses are:
`proposed`, `ready`, `active`, `blocked`, `backlog`, `finished`, `archived`.
Use `proposed` for a newly drafted plan, `ready` after review against current
repo state, and `finished` when implementation is complete. `stalled` and
`needs_review` are derived health labels, not stored statuses.
Closed workplans may be moved to `workplans/archived/` with a completion-date
prefix: `YYMMDD-FLEX-WP-NNNN-<slug>.md`. The frontmatter id remains
unchanged; the prefix is only for quick visual reference.
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
`workplans/ADHOC-YYYY-MM-DD.md`, workstream slug `adhoc-YYYY-MM-DD`, and task ids
`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed
directly. Promote anything requiring analysis, design, approval, dependencies, or
multiple planned phases into a normal workplan.
Ecosystem todos from other agents arrive as `[repo:flex-auth]` hub tasks —
visible at session start. Pick one up by creating the workplan file, then registering
the workstream.
Task blocks use this shape:
```task
id: FLEX-WP-NNNN-T01
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
```
Status progression is `todo``progress``done`; use `wait` for waiting or
blocked work and `cancel` for stopped work.
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->

View File

@@ -1,61 +1,23 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — flex-auth
**Domain:** netkingdom
**Last synced:** 2026-05-15 20:29 UTC
**Domain:** infotech
**Last synced:** 2026-06-24 11:11 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### Foundations and Topaz Alignment
Progress: 1/6 done | workstream_id: `e37d42a9-0018-4a67-a672-ff4e9716b338`
### Ops-Warden Policy Gate Production Deployment
Progress: 4/5 done | workstream_id: `358ce697-2611-4fe9-89ab-63e86ceb00fa`
**Open tasks:**
- · P5.2 - Land Go project skeleton `8ac73c33`
- · P5.3 - Pin FlexAuthResourceManifest schema `80285e1e`
- · P5.4 - Topaz alignment spike `b8a314c3`
- · P5.5 - Cite NetKingdom IAM Profile and pin claim consumption `b31dab7b`
- · P5.6 - Confirm ops-warden boundary `dcd45a14`
### Standalone Policy-as-Code Core
Progress: 0/8 done | workstream_id: `aa60e183-9a87-4e03-99b0-15786bfa11ae`
**Open tasks:**
- · P2.1 - Define canonical schemas `534e5251`
- · P2.2 - Implement local registry store `d8045124`
- · P2.3 - Implement policy package loader and validator `09be0f25`
- · P2.4 - Implement deterministic check and batch_check APIs `f6427575`
- · P2.5 - Implement list_allowed and explain `e8fcbabd`
- · P2.6 - Add local decision log `2def10c1`
- · P2.7 - Add CLI and service skeleton `ee9ae6dd`
- … and 1 more open tasks
### Markitect Consumer Integration
Progress: 0/6 done | workstream_id: `c0a6c9f6-bb6b-416d-b537-f30504c63d75`
**Open tasks:**
- · P3.1 - Define Markitect resource namespace `53f2fa67`
- · P3.2 - Import Markitect resource manifests `90082eaf`
- · P3.3 - Define Markitect action vocabulary `cfc78bbb`
- · P3.4 - Implement Markitect check fixtures `1d5de3b2`
- · P3.5 - Add Markitect adapter contract tests `f9297b0d`
- · P3.6 - Document integration flow `e34b0303`
### Delegated PDP and Directory Adapters
Progress: 0/6 done | workstream_id: `99a82976-d376-42b0-89cc-c44e01c0bec6`
**Open tasks:**
- · P4.1 - Evaluate Topaz as MVP delegated backend `9046418c`
- · P4.2 - Add relationship PDP adapter boundary `b77a0b70`
- · P4.3 - Add rule PDP adapter boundary `4e4e5e45`
- · P4.4 - Add Keycloak Authorization Services adapter path `8d3bbc28`
- · P4.5 - Add Entra/Graph and SCIM group resolver adapters `4fc3fb91`
- · P4.6 - Add delegated-mode operations docs `491260f9`
- ! T4 — Joint OpenBao + policy gate production smoke `32a96f1c`
*(wait: Awaiting scoped VAULT_TOKEN refresh)*
---
## MCP Orientation (when available)
If the state-hub MCP server is reachable, call:
`get_domain_summary("netkingdom")`
`get_domain_summary("infotech")`
This provides richer cross-domain context.
If the MCP call fails, use this file as your orientation source.

51
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: CI
on:
push:
branches: ["**"]
pull_request:
branches: ["**"]
jobs:
build-and-test:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.22"
- name: Verify dependencies
run: go mod verify
- name: Vet
run: go vet ./...
- name: Test
run: go test -race ./...
- name: Build
run: |
mkdir -p bin
go build -o bin/flex-auth ./cmd/flex-auth
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.22"
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest

6
.gitignore vendored
View File

@@ -1,3 +1,9 @@
# ---> Go
bin/
*.test
*.out
coverage.txt
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/

26
.golangci.yml Normal file
View File

@@ -0,0 +1,26 @@
run:
timeout: 5m
tests: true
linters:
disable-all: true
enable:
- errcheck
- gofmt
- goimports
- govet
- ineffassign
- misspell
- staticcheck
- unconvert
- unused
linters-settings:
goimports:
local-prefixes: github.com/netkingdom/flex-auth
issues:
exclude-rules:
- path: _test\.go
linters:
- errcheck

25
.repo-classification.yaml Normal file
View File

@@ -0,0 +1,25 @@
repo_classification:
standard: Repo Classification Standard
version: '1.0'
classified_at: '2026-06-22'
classified_by: human
category: product
domain: infotech
secondary_domains:
- government
capability_tags:
- identity
- access-control
- policy
- governance
- audit
business_stake:
- technology
- legal
- operations
- product
business_mechanics:
- control
- coordination
- adaptation
notes: Policy-as-code authorization registry; human corrected domain from communication→infotech.

219
AGENTS.md Normal file
View File

@@ -0,0 +1,219 @@
# flex-auth — Agent Instructions
## Repo Identity
**Purpose:** flex-auth - (fill in purpose)
**Domain:** infotech
**Repo slug:** flex-auth
**Topic ID:** `cee7bedf-2b48-46ef-8601-006474f2ad7a`
**Workplan prefix:** `FLEX-WP-`
---
## State Hub Integration
The Custodian State Hub tracks work across all domains. Interact via HTTP REST —
there is no MCP server for Codex agents.
| Context | URL |
|---------|-----|
| Local workstation | `http://127.0.0.1:8000` |
| Remote via tunnel | `http://127.0.0.1:18000` |
### Orient at session start
```bash
# Offline brief — works without hub connection
cat .custodian-brief.md
# Active workstreams for this domain
curl -s "http://127.0.0.1:8000/workstreams/?topic_id=cee7bedf-2b48-46ef-8601-006474f2ad7a&status=active" \
| python3 -m json.tool
# Check inbox
curl -s "http://127.0.0.1:8000/messages/?to_agent=flex-auth&unread_only=true" \
| python3 -m json.tool
```
Mark a message read:
```bash
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
-H "Content-Type: application/json" -d '{}'
```
### Log progress (required at session close)
```bash
curl -s -X POST http://127.0.0.1:8000/progress/ \
-H "Content-Type: application/json" \
-d '{
"summary": "what was done",
"event_type": "note",
"author": "codex",
"workstream_id": "<uuid>",
"task_id": "<uuid>"
}'
```
Omit `workstream_id` / `task_id` when not applicable.
### Update task status
```bash
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
-H "Content-Type: application/json" \
-d '{"status": "progress"}'
# values: wait | todo | progress | done | cancel
```
### Flag a task for human review
```bash
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
-H "Content-Type: application/json" \
-d '{"needs_human": true, "intervention_note": "reason"}'
```
---
## Session Protocol
**Start:**
1. `cat .custodian-brief.md` — domain goal and open workstreams (offline-safe)
2. Check inbox: `GET /messages/?to_agent=flex-auth&unread_only=true`; mark read
3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks
4. Check human-needed tasks: `GET /tasks/?needs_human=true`
**During work:**
- Update task statuses in workplan files as tasks progress
- Record significant decisions via `POST /decisions/`
**Close:**
1. Update workplan file task statuses to reflect progress
2. Log: `POST /progress/` with a summary of what changed
3. Note for the custodian operator: after workplan file changes, run from
`~/state-hub`:
```bash
make fix-consistency REPO=flex-auth
```
This syncs task status from files into the hub DB.
---
## Credential and access routing
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
for inference. Run this check **before** requesting secrets, API keys, SSH access,
login tokens, or database passwords — in any repo, not only `ops-warden`.
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
other credential need belongs to another subsystem. **Do not** message
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
### Lookup (do this first)
```bash
warden route find "<describe your need>" --json
warden route show <catalog-id> --json
```
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
| Agent runtime | How to orient |
| --- | --- |
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=flex-auth` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
### Quick routing table
| I need… | Owner | ops-warden executes? |
| --- | --- | --- |
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` |
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
| Authorization decision | flex-auth | No — route only |
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
### Anti-patterns (do not do these)
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
- Pasting secrets into Git, State Hub, workplans, logs, or chat
### Other capabilities (reuse-surface)
Non-credential capabilities are usually discovered through **reuse-surface** federation
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
every repo's agent instructions because it is high-frequency, high-risk, and easy to
get wrong.
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`
<!-- REPO-AGENTS-EXTENSIONS -->
<!-- Append repo-specific agent instructions below this marker.
The state-hub template sync preserves content after this line. -->
---
## Workplan Convention (ADR-001)
Work items originate as files in this repo — not in the hub. The hub is a
read/cache/index layer that rebuilds from files.
**File location:** `workplans/FLEX-WP-NNNN-<slug>.md`
**Archived location:** finished workplans may move to
`workplans/archived/YYMMDD-FLEX-WP-NNNN-<slug>.md`. The `YYMMDD` prefix is
the completion/archive date; the frontmatter `id` does not change.
**Ad Hoc Tasks:** small opportunistic fixes discovered during a session use
`workplans/ADHOC-YYYY-MM-DD.md` with task ids `ADHOC-YYYY-MM-DD-T01`, etc. Use
this only for low-risk work completed directly; create a normal workplan for
anything needing analysis, design, approval, dependencies, or multiple phases.
**Frontmatter:**
```yaml
---
id: FLEX-WP-NNNN
type: workplan
title: "..."
domain: infotech
repo: flex-auth
status: proposed | ready | active | blocked | backlog | finished | archived
owner: codex
topic_slug: ...
created: "YYYY-MM-DD"
updated: "YYYY-MM-DD"
state_hub_workstream_id: "<uuid>" # written by fix-consistency — do not edit
---
```
Use `proposed` for a new draft, `ready` after review against current repo
state, and `finished` after implementation. `stalled` and `needs_review` are
derived health labels, not frontmatter statuses.
**Task block format** (one per `##` section):
```
## Task Title
` ` `task
id: FLEX-WP-NNNN-T01
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
` ` `
Task description text.
```
Status progression: `todo` → `progress` → `done`; use `wait` for waiting/blocked work and `cancel` for stopped work.
To create a new workplan:
1. Write the file following the format above
2. Notify the custodian operator to run `make fix-consistency REPO=flex-auth`
(or send a message to the hub agent via `POST /messages/`)

12
CLAUDE.md Normal file
View File

@@ -0,0 +1,12 @@
# flex-auth — Claude Code Instructions
@SCOPE.md
@.claude/rules/repo-identity.md
@.claude/rules/session-protocol.md
@.claude/rules/first-session.md
@.claude/rules/workplan-convention.md
@.claude/rules/stack-and-commands.md
@.claude/rules/architecture.md
@.claude/rules/repo-boundary.md
@.claude/rules/credential-routing.md
@.claude/rules/agents.md

119
INTENT.md
View File

@@ -1,6 +1,9 @@
# Flex-Auth Intent
Date: 2026-05-04
> This file captures **why this repository exists**, the **direction it is
> moving toward**, and the **kind of system it is meant to become**.
> It is intentionally **aspirational and stable**, not a description of
> current implementation.
## Intent
@@ -9,20 +12,19 @@ organizations that want to grow from simple access rules into enterprise-grade
authorization without giving up clear ownership, local development ergonomics,
or inspectable policy decisions.
It belongs in the NetKingdom tooling landscape as the authorization counterpart
to key-cape/NetKingdom identity:
It is the **authorization layer** in the path from verified identity to
protected resources:
```text
key-cape / NetKingdom SSO
-> verified OIDC/SAML identity claims
verified identity claims
-> flex-auth policy-as-code and authorization registry
-> protected systems and knowledge tools
-> protected systems and their resources
```
Flex-auth should run usefully on its own, but it should also delegate to or
coordinate with established authorization engines such as Topaz, OpenFGA,
SpiceDB, OPA, Cedar, Keycloak Authorization Services, and enterprise directory
systems.
Flex-auth should run usefully on its own, and should also be able to delegate
to or coordinate established authorization engines — relationship/graph
engines, rule and attribute policy engines, and directory systems — without
binding its own model to any one of them.
## Why This Exists
@@ -36,7 +38,7 @@ conditionals. Over time they need richer policy:
- emergency/break-glass controls
- policy tests and reviewable changes
- durable decision logs and explainability
- integration with SSO, MFA, service accounts, and directory groups
- integration with identity, MFA, service accounts, and directory groups
Flex-auth should give those organizations a path that starts small and grows
cleanly instead of forcing an early leap into a large IAM platform or letting
@@ -44,14 +46,17 @@ authorization logic sprawl across applications.
## Responsibility Boundary
### key-cape / NetKingdom Owns Identity
Flex-auth consumes **verified identity claims** as normative input and never
re-defines them. Identity proves who an actor is and how they were
authenticated; flex-auth decides what that actor is allowed to do.
- OIDC discovery and token issuance.
- Human login, MFA, PKCE, service accounts, token lifecycle.
- Canonical IAM profile and required claims.
- Coarse app roles/scopes and assurance claims.
### The Identity Layer Owns Identity
### flex-auth Owns Authorization
- Authentication, login, MFA, and token issuance and lifecycle.
- The canonical identity claim contract and required claims.
- Coarse roles, scopes, and assurance claims.
### Flex-Auth Owns Authorization
- Protected-system registration.
- Resource namespaces and resource hierarchy.
@@ -65,17 +70,17 @@ authorization logic sprawl across applications.
### Protected Systems Own Enforcement
Applications such as Markitect remain policy enforcement points. They extract
resource metadata, call flex-auth for decisions, enforce allow/deny/redact
results, and emit local diagnostics. They do not own central enterprise policy
administration.
Applications remain policy enforcement points. They extract resource
metadata, call flex-auth for decisions, enforce allow/deny/redact results,
and emit local diagnostics. They do not own central policy administration.
## Design Principles
- Policy is code: versioned, reviewed, tested, and explainable.
- Identity is not authorization: SSO claims are inputs, not final decisions.
- Identity is not authorization: identity claims are inputs, not final
decisions.
- Start standalone, scale outward: a local flex-auth deployment should be
useful before Topaz/OpenFGA/OPA integrations are available.
useful before any external policy engine integration is available.
- Backend-neutral core: flex-auth has its own resource, action, request,
decision, and audit vocabulary.
- Pluggable PDPs: relationship, rule, and directory engines are adapters, not
@@ -83,7 +88,7 @@ administration.
- Fail visibly: denied, redacted, stale, partial, and uncertain decisions must
produce useful diagnostics.
- Grow into enterprise: the same model should support local dev, small teams,
NetKingdom-managed deployments, and larger Keycloak/Entra environments.
and larger enterprise environments.
## First-Class Concepts
@@ -126,40 +131,62 @@ Standalone flex-auth should provide:
- deterministic check and batch-check APIs
- local decision log
- CLI and service mode
- test fixtures for Keycloak/key-cape-like claims
- test fixtures for representative identity claims
Standalone mode should be enough for development, smaller deployments, and
integration tests.
## Delegated Mode
Delegated mode should let flex-auth coordinate established systems:
Delegated mode should let flex-auth coordinate established systems without
adopting their models as its own:
- Topaz for a local directory plus OPA/Rego policy evaluation.
- OpenFGA or SpiceDB for graph-heavy relationship authorization.
- OPA or Cedar for attribute/rule policies.
- Keycloak Authorization Services for Keycloak-centric deployments.
- Microsoft Graph or SCIM/LDAP/Keycloak APIs for directory group resolution.
- relationship/graph engines for relationship-heavy authorization
- rule and attribute policy engines for attribute/rule policies
- directory systems for group resolution
- a local directory plus policy-evaluation engine for self-contained
delegated setups
Flex-auth remains the stable control plane even when the PDP backend changes.
Flex-auth remains the stable control plane even when the backend changes.
## First Consumer: Markitect
## Consumer Patterns
Markitect is the first concrete consumer:
Two consumer shapes drive flex-auth, and the first one to ship deliberately
is not a document pipeline — proving the control plane stays generic.
- Markitect registers knowledge bases, repositories, documents, sections,
context packages, workflow artifacts, and exports.
- Markitect sends policy checks before returning query/search/context results.
- Markitect can redact or drop results based on decisions.
- flex-auth owns central policy administration and durable audit.
**First shipped consumer — an action gate (ops-warden SSH signing).** A
protected system asks flex-auth a single "may this actor perform this action
now?" question before doing irreversible work:
This first consumer should shape flex-auth around real Markdown knowledge
pipelines without making the policy service Markitect-specific.
- it registers a protected system, a resource type (`ssh-certificate`), and an
action (`sign`)
- it sends one policy check per request, passing subject, resource, and
context (actor type, principals, TTL, key fingerprint)
- it enforces the allow/deny decision and records the decision id for audit
- flex-auth owns the policy and durable decision log; the protected system
keeps custody of its own keys and secrets
This first consumer validated that flex-auth's resource/action/context model,
`POST /v1/check` contract, and decision envelope work for a non-document,
high-stakes gate without any consumer-specific routes.
**First knowledge-pipeline consumer (planned) — a document and knowledge
pipeline (Markitect):**
- it registers knowledge bases, repositories, documents, sections, context
packages, workflow artifacts, and exports
- it sends policy checks before returning query/search/context results
- it can redact or drop results based on decisions
- flex-auth owns central policy administration and durable audit
Together these shape flex-auth around real authorization needs — both
point-in-time action gates and result-filtering pipelines — without making the
policy service consumer-specific.
## Non-Goals
- Flex-auth is not an identity provider.
- Flex-auth is not a replacement for key-cape or NetKingdom SSO.
- Flex-auth is not a replacement for an identity or SSO system.
- Flex-auth is not a mandatory dependency for every local development use case.
- Flex-auth should not force one PDP backend.
- Flex-auth should not hide policy complexity behind opaque admin toggles.
@@ -169,7 +196,7 @@ pipelines without making the policy service Markitect-specific.
1. Define the resource/action/decision model.
2. Define policy package structure and test fixtures.
3. Implement standalone registry and check API.
4. Add Markitect resource manifest and policy adapter.
5. Evaluate Topaz as the first delegated backend.
6. Add OpenFGA/SpiceDB and OPA adapter spikes.
7. Add Keycloak/key-cape and Entra integration examples.
4. Add a first protected-system resource manifest and policy adapter.
5. Evaluate a delegated directory-plus-policy backend.
6. Add relationship-engine and rule-engine adapter spikes.
7. Add identity and enterprise-directory integration examples.

59
Makefile Normal file
View File

@@ -0,0 +1,59 @@
BIN_DIR ?= bin
BIN := $(BIN_DIR)/flex-auth
PKG := ./...
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo 0.0.0-dev)
LDFLAGS := -X main.version=$(VERSION)
.PHONY: all build test vet lint fmt tidy sbom clean ci
all: vet lint test build
build:
@mkdir -p $(BIN_DIR)
go build -ldflags "$(LDFLAGS)" -o $(BIN) ./cmd/flex-auth
test:
go test -race $(PKG)
vet:
go vet $(PKG)
fmt:
gofmt -l -w .
tidy:
go mod tidy
lint:
@if command -v golangci-lint >/dev/null 2>&1; then \
golangci-lint run $(PKG); \
else \
echo "golangci-lint not installed; run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
echo "falling back to: go vet"; \
go vet $(PKG); \
fi
GOBIN_PATH := $(shell go env GOPATH)/bin
sbom:
@mkdir -p $(BIN_DIR)
@if command -v cyclonedx-gomod >/dev/null 2>&1; then \
cyclonedx-gomod mod -json -output $(BIN_DIR)/sbom.cdx.json .; \
echo "SBOM written to $(BIN_DIR)/sbom.cdx.json (cyclonedx-gomod)"; \
elif [ -x "$(GOBIN_PATH)/cyclonedx-gomod" ]; then \
"$(GOBIN_PATH)/cyclonedx-gomod" mod -json -output $(BIN_DIR)/sbom.cdx.json .; \
echo "SBOM written to $(BIN_DIR)/sbom.cdx.json (cyclonedx-gomod via GOPATH)"; \
elif command -v syft >/dev/null 2>&1; then \
syft . -o cyclonedx-json=$(BIN_DIR)/sbom.cdx.json; \
echo "SBOM written to $(BIN_DIR)/sbom.cdx.json (syft)"; \
else \
echo "no SBOM tool found. install one:"; \
echo " go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest"; \
echo " or syft: https://github.com/anchore/syft"; \
exit 1; \
fi
clean:
rm -rf $(BIN_DIR)
ci: vet lint test build

View File

@@ -4,11 +4,18 @@ Policy-as-code authorization registry and control plane for NetKingdom-aligned
systems.
Start with [INTENT.md](INTENT.md) for the project boundary and direction.
Research notes live in [docs/](docs/).
Research notes and ADRs live in [docs/](docs/) and [docs/adr/](docs/adr/).
The product boundary is captured in [SCOPE.md](SCOPE.md), and the current
Product Requirements Document is
[docs/ProductRequirementsDocument.md](docs/ProductRequirementsDocument.md).
Initial workplans live in [workplans/](workplans/), with sequencing captured
in [docs/workplan-planning-map.md](docs/workplan-planning-map.md).
The 2026-05-15 pre-implementation assessment that shapes the current
sequencing is in
[docs/pre-implementation-assessment.md](docs/pre-implementation-assessment.md).
The CARING reference-implementation approach is captured in
[docs/caring-architecture-blueprint.md](docs/caring-architecture-blueprint.md).
Workplans live in [workplans/](workplans/), with sequencing captured in
[docs/workplan-planning-map.md](docs/workplan-planning-map.md).

View File

@@ -72,11 +72,22 @@ can be coordinated behind a stable flex-auth API.
## Current State
The repository contains the intent baseline, authorization landscape research,
and initial workplans. `FLEX-WP-0001` is complete. Current implementation work
starts with `FLEX-WP-0002`, the standalone policy-as-code core. Markitect
consumer integration and delegated PDP/directory adapters are planned after
the core contracts stabilize.
The standalone core is implemented. The repository carries the intent
baseline, authorization landscape research, ADR set, and a working Go
service (`cmd/flex-auth`) with `validate`, `load-registry`, `serve`, and
`POST /v1/check` plus registry, policy, decision, and audit internals.
`FLEX-WP-0001`, `FLEX-WP-0005` (foundations and Topaz alignment), and
`FLEX-WP-0006` (the ops-warden SSH signing policy gate) are complete.
The **first shipped protected-system consumer is ops-warden**: its opt-in
pre-sign gate calls `POST /v1/check` for `resource.type: ssh-certificate`,
`action: sign` decisions (`examples/ops-warden/`, policy package, allow/deny
fixtures, and tests). `FLEX-WP-0007` deploys flex-auth as a reachable
production runtime for that gate; it is `blocked` only on T4 — the joint
OpenBao-backed smoke awaiting a refreshed scoped `VAULT_TOKEN` — with all
repo-side artifacts already published. Markitect consumer integration
(`FLEX-WP-0003`) and delegated PDP/directory adapters (`FLEX-WP-0004`)
remain planned on top of the stable core contracts.
State Hub integration is present through:
@@ -120,17 +131,43 @@ local diagnostics.
## Related / Overlapping
- key-cape / NetKingdom SSO: identity source and coarse claims provider.
- Markitect: first protected-system consumer and policy enforcement point.
- Topaz: candidate MVP delegated backend combining local directory and
OPA/Rego evaluation.
- key-cape / NetKingdom SSO: identity source and coarse claims provider;
flex-auth consumes the **NetKingdom IAM Profile**
(`~/net-kingdom/canon/standards/iam-profile_v0.2.md`).
- ops-warden: first **shipped** protected-system consumer. Its opt-in
pre-sign gate calls flex-auth for `ssh-certificate` / `sign` decisions
before issuing a short-lived SSH certificate (`FLEX-WP-0006`,
`FLEX-WP-0007`). ops-warden owns the SSH CA, OpenBao signing, and actor
inventory; flex-auth owns the policy decision. ops-warden's routing
charter names flex-auth as the owner of every "may I perform action X?"
question.
- Markitect: first planned **knowledge-pipeline** consumer and policy
enforcement point (`FLEX-WP-0003`).
- Topaz: aligned evaluator. Per ADR-003 the standalone core is shaped
to match Topaz's Rego + directory model from day one; the Topaz
adapter in `FLEX-WP-0004` is therefore a small step rather than a
conversion.
- OpenFGA and SpiceDB: candidate relationship authorization backends.
- OPA and Cedar: candidate rule and typed-policy engines.
- Keycloak Authorization Services: adapter path for Keycloak-centric
deployments.
deployments. Default architecture is "Keycloak as SSO only,
flex-auth owns authorization"; Keycloak AuthZ is one optional
delegated PDP.
- Entra, Graph, SCIM, LDAP, and Keycloak APIs: directory and group resolver
sources.
## Disjoint From
- **ops-warden** is a flex-auth *consumer*, not an overlap (see Related /
Overlapping). The two remain disjoint on **identity surface**: ops-warden
issues SSH certificates for ops actors (`adm`/`agt`/`atm`) and is not a
resource-policy engine; flex-auth decides whether a given sign request is
allowed and never issues certificates. The once-hypothetical flow of
surfacing an `agt` actor as a flex-auth subject is now realized through
the signing policy gate.
- **ops-bridge** owns SSH reverse-tunnel connectivity and explicitly
disclaims being a credential authority or policy engine. No overlap.
## Provided Capabilities
```capability

505
cmd/flex-auth/main.go Normal file
View File

@@ -0,0 +1,505 @@
// Command flex-auth is the CLI entry point for the flex-auth authorization
// registry and control plane.
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"gopkg.in/yaml.v3"
"github.com/netkingdom/flex-auth/internal/audit"
decisioncore "github.com/netkingdom/flex-auth/internal/decision"
"github.com/netkingdom/flex-auth/internal/policy"
"github.com/netkingdom/flex-auth/internal/registry"
"github.com/netkingdom/flex-auth/pkg/api"
)
// version is set at build time via -ldflags "-X main.version=...".
var version = "0.0.0-dev"
func main() {
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
}
func run(args []string, stdout, stderr io.Writer) int {
if len(args) == 0 {
printUsage(stderr)
return 64
}
switch args[0] {
case "version", "--version", "-version":
fmt.Fprintln(stdout, version)
return 0
case "validate":
return runValidate(args[1:], stdout, stderr)
case "load-registry":
return runLoadRegistry(args[1:], stdout, stderr)
case "test-policy":
return runTestPolicy(args[1:], stdout, stderr)
case "check":
return runCheck(args[1:], stdout, stderr)
case "batch-check":
return runBatchCheck(args[1:], stdout, stderr)
case "list-allowed":
return runListAllowed(args[1:], stdout, stderr)
case "explain":
return runExplain(args[1:], stdout, stderr)
case "serve":
return runServe(args[1:], stdout, stderr)
case "help", "-h", "--help":
printUsage(stdout)
return 0
default:
fmt.Fprintf(stderr, "flex-auth: unknown subcommand %q\n", args[0])
printUsage(stderr)
return 64
}
}
func runValidate(args []string, stdout, stderr io.Writer) int {
fs := newFlagSet("validate", stderr)
kind := fs.String("kind", "", "resource-manifest, subject-manifest, protected-system, relationship, access-descriptor, or policy")
file := fs.String("file", "", "YAML/JSON file to validate")
if err := fs.Parse(args); err != nil {
return 64
}
if *kind == "" || *file == "" {
fmt.Fprintln(stderr, "validate requires --kind and --file")
return 64
}
switch *kind {
case "resource-manifest":
var manifest api.ResourceManifest
if err := readYAML(*file, &manifest); err != nil {
return fail(stderr, err)
}
if err := registry.NewStore().ImportResourceManifest(manifest); err != nil {
return fail(stderr, err)
}
case "subject-manifest":
var manifest api.SubjectManifest
if err := readYAML(*file, &manifest); err != nil {
return fail(stderr, err)
}
if err := registry.NewStore().ImportSubjectManifest(manifest); err != nil {
return fail(stderr, err)
}
case "protected-system":
var manifest api.ProtectedSystemManifest
if err := readYAML(*file, &manifest); err != nil {
return fail(stderr, err)
}
if err := registry.NewStore().PutProtectedSystem(manifest); err != nil {
return fail(stderr, err)
}
case "relationship":
var fact api.RelationshipFact
if err := readYAML(*file, &fact); err != nil {
return fail(stderr, err)
}
if err := registry.NewStore().PutRelationship(fact); err != nil {
return fail(stderr, err)
}
case "access-descriptor":
var descriptor api.CaringAccessDescriptor
if err := readYAML(*file, &descriptor); err != nil {
return fail(stderr, err)
}
if err := validateDescriptor(descriptor); err != nil {
return fail(stderr, err)
}
case "policy", "policy-package":
pkg, err := policy.LoadAndValidateFile(context.Background(), *file)
if err != nil {
return fail(stderr, err)
}
if err := writeJSON(stdout, pkg.Validation); err != nil {
return fail(stderr, err)
}
if !pkg.Valid {
return 1
}
return 0
default:
fmt.Fprintf(stderr, "unsupported validate kind %q\n", *kind)
return 64
}
return writeStatus(stdout, "valid", map[string]any{"kind": *kind, "file": *file})
}
func runLoadRegistry(args []string, stdout, stderr io.Writer) int {
fs := newFlagSet("load-registry", stderr)
file := fs.String("file", "", "registry snapshot JSON file")
if err := fs.Parse(args); err != nil {
return 64
}
if *file == "" {
fmt.Fprintln(stderr, "load-registry requires --file")
return 64
}
store, err := registry.LoadFile(*file)
if err != nil {
return fail(stderr, err)
}
snapshot := store.Snapshot()
return writeStatus(stdout, "loaded", map[string]any{
"file": *file,
"systems": len(snapshot.Systems),
"resource_manifests": len(snapshot.ResourceManifests),
"subjects": len(snapshot.Subjects),
"groups": len(snapshot.Groups),
"teams": len(snapshot.Teams),
"tenants": len(snapshot.Tenants),
"relationships": len(snapshot.Relationships),
})
}
func runTestPolicy(args []string, stdout, stderr io.Writer) int {
fs := newFlagSet("test-policy", stderr)
file := fs.String("file", "", "policy package Markdown file")
if err := fs.Parse(args); err != nil {
return 64
}
if *file == "" {
fmt.Fprintln(stderr, "test-policy requires --file")
return 64
}
pkg, err := policy.LoadAndValidateFile(context.Background(), *file)
if err != nil {
return fail(stderr, err)
}
if err := writeJSON(stdout, pkg.Validation); err != nil {
return fail(stderr, err)
}
if !pkg.Valid {
return 1
}
return 0
}
func runCheck(args []string, stdout, stderr io.Writer) int {
fs := newFlagSet("check", stderr)
registryPath := fs.String("registry", "", "registry snapshot JSON file")
policyPath := fs.String("policy", "", "policy package Markdown file")
requestPath := fs.String("request", "", "check request YAML/JSON file")
logPath := fs.String("log", "", "optional JSONL decision log path")
if err := fs.Parse(args); err != nil {
return 64
}
if *registryPath == "" || *policyPath == "" || *requestPath == "" {
fmt.Fprintln(stderr, "check requires --registry, --policy, and --request")
return 64
}
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
if err != nil {
return fail(stderr, err)
}
var request api.CheckRequest
if err := readYAML(*requestPath, &request); err != nil {
return fail(stderr, err)
}
decision, err := engine.Check(context.Background(), request)
if err != nil {
return fail(stderr, err)
}
if err := writeJSON(stdout, decision); err != nil {
return fail(stderr, err)
}
return 0
}
func runBatchCheck(args []string, stdout, stderr io.Writer) int {
fs := newFlagSet("batch-check", stderr)
registryPath := fs.String("registry", "", "registry snapshot JSON file")
policyPath := fs.String("policy", "", "policy package Markdown file")
requestPath := fs.String("request", "", "batch check request YAML/JSON file")
logPath := fs.String("log", "", "optional JSONL decision log path")
if err := fs.Parse(args); err != nil {
return 64
}
if *registryPath == "" || *policyPath == "" || *requestPath == "" {
fmt.Fprintln(stderr, "batch-check requires --registry, --policy, and --request")
return 64
}
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
if err != nil {
return fail(stderr, err)
}
var request api.BatchCheckRequest
if err := readYAML(*requestPath, &request); err != nil {
return fail(stderr, err)
}
decisions, err := engine.BatchCheck(context.Background(), request)
if err != nil {
return fail(stderr, err)
}
if err := writeJSON(stdout, decisions); err != nil {
return fail(stderr, err)
}
return 0
}
func runListAllowed(args []string, stdout, stderr io.Writer) int {
fs := newFlagSet("list-allowed", stderr)
registryPath := fs.String("registry", "", "registry snapshot JSON file")
policyPath := fs.String("policy", "", "policy package Markdown file")
subject := fs.String("subject", "", "subject id")
action := fs.String("action", "", "action name")
system := fs.String("system", "", "protected system id")
resourceType := fs.String("resource-type", "", "resource type")
logPath := fs.String("log", "", "optional JSONL decision log path")
var filters keyValueFlags
fs.Var(&filters, "filter", "resource filter as key=value; may be repeated")
if err := fs.Parse(args); err != nil {
return 64
}
if *registryPath == "" || *policyPath == "" || *subject == "" || *action == "" {
fmt.Fprintln(stderr, "list-allowed requires --registry, --policy, --subject, and --action")
return 64
}
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
if err != nil {
return fail(stderr, err)
}
decisions, err := engine.ListAllowed(context.Background(), decisioncore.ListAllowedRequest{
Subject: api.SubjectRef{ID: *subject},
Action: *action,
System: *system,
ResourceType: *resourceType,
Filters: filters.Map(),
})
if err != nil {
return fail(stderr, err)
}
if err := writeJSON(stdout, decisions); err != nil {
return fail(stderr, err)
}
return 0
}
func runExplain(args []string, stdout, stderr io.Writer) int {
fs := newFlagSet("explain", stderr)
logPath := fs.String("decision-log", "", "JSONL decision log path")
decisionID := fs.String("decision-id", "", "decision id to explain")
if err := fs.Parse(args); err != nil {
return 64
}
if *logPath == "" || *decisionID == "" {
fmt.Fprintln(stderr, "explain requires --decision-log and --decision-id")
return 64
}
log := audit.NewJSONLDecisionLog(*logPath)
envelope, ok, err := log.Find(*decisionID)
if err != nil {
return fail(stderr, err)
}
if !ok {
return fail(stderr, fmt.Errorf("decision %q not found", *decisionID))
}
if err := writeJSON(stdout, decisioncore.ExplainEnvelope(envelope)); err != nil {
return fail(stderr, err)
}
return 0
}
func runServe(args []string, stdout, stderr io.Writer) int {
fs := newFlagSet("serve", stderr)
addr := fs.String("addr", "127.0.0.1:8080", "HTTP listen address")
registryPath := fs.String("registry", "", "registry snapshot JSON file")
policyPath := fs.String("policy", "", "policy package Markdown file")
logPath := fs.String("log", "", "optional JSONL decision log path")
if err := fs.Parse(args); err != nil {
return 64
}
if *registryPath == "" || *policyPath == "" {
fmt.Fprintln(stderr, "serve requires --registry and --policy")
return 64
}
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
if err != nil {
return fail(stderr, err)
}
mux := newServeMux(engine)
fmt.Fprintf(stderr, "flex-auth serving on http://%s\n", *addr)
if err := http.ListenAndServe(*addr, mux); err != nil {
return fail(stderr, err)
}
return 0
}
func newServeMux(engine *decisioncore.Engine) *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("content-type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
mux.HandleFunc("/v1/check", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var request api.CheckRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
decision, err := engine.Check(r.Context(), request)
writeHTTP(w, decision, err)
})
mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var request api.BatchCheckRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
decisions, err := engine.BatchCheck(r.Context(), request)
writeHTTP(w, decisions, err)
})
return mux
}
func buildEngine(ctx context.Context, registryPath, policyPath, logPath string) (*decisioncore.Engine, error) {
store, err := registry.LoadFile(registryPath)
if err != nil {
return nil, err
}
pkg, err := policy.LoadAndValidateFile(ctx, policyPath)
if err != nil {
return nil, err
}
engine, err := decisioncore.NewEngine(store, pkg)
if err != nil {
return nil, err
}
if logPath != "" {
engine.SetDecisionLog(audit.NewJSONLDecisionLog(logPath))
}
return engine, nil
}
func validateDescriptor(descriptor api.CaringAccessDescriptor) error {
if descriptor.Profile != api.CaringProfileCaring040RC2 {
return fmt.Errorf("unsupported CARING profile %q", descriptor.Profile)
}
if descriptor.SubjectType == "" {
return fmt.Errorf("subject_type is required")
}
if descriptor.OrganizationRelation == "" {
return fmt.Errorf("organization_relation is required")
}
if descriptor.CanonicalRole == "" {
return fmt.Errorf("canonical_role is required")
}
if descriptor.Scope.Level == "" || descriptor.Scope.ID == "" {
return fmt.Errorf("scope.level and scope.id are required")
}
if len(descriptor.Planes) == 0 {
return fmt.Errorf("at least one plane is required")
}
if len(descriptor.Capabilities) == 0 {
return fmt.Errorf("at least one capability is required")
}
return nil
}
func readYAML(path string, out any) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
if err := yaml.Unmarshal(data, out); err != nil {
return fmt.Errorf("unmarshal %s: %w", path, err)
}
return nil
}
func writeJSON(w io.Writer, value any) error {
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
if err := encoder.Encode(value); err != nil {
return err
}
return nil
}
func writeStatus(w io.Writer, status string, extra map[string]any) int {
out := map[string]any{"status": status}
for key, value := range extra {
out[key] = value
}
if err := writeJSON(w, out); err != nil {
return fail(w, err)
}
return 0
}
func writeHTTP(w http.ResponseWriter, value any, err error) {
w.Header().Set("content-type", "application/json")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(value)
}
func newFlagSet(name string, stderr io.Writer) *flag.FlagSet {
fs := flag.NewFlagSet(name, flag.ContinueOnError)
fs.SetOutput(stderr)
return fs
}
func fail(stderr io.Writer, err error) int {
fmt.Fprintln(stderr, "flex-auth:", err)
return 1
}
func printUsage(w io.Writer) {
fmt.Fprintln(w, "usage: flex-auth <command> [options]")
fmt.Fprintln(w, "commands: version, validate, load-registry, test-policy, check, batch-check, list-allowed, explain, serve")
}
type keyValueFlags []string
func (f *keyValueFlags) String() string {
return strings.Join(*f, ",")
}
func (f *keyValueFlags) Set(value string) error {
if !strings.Contains(value, "=") {
return fmt.Errorf("filter must be key=value")
}
*f = append(*f, value)
return nil
}
func (f keyValueFlags) Map() map[string]any {
out := make(map[string]any, len(f))
for _, item := range f {
key, value, _ := strings.Cut(item, "=")
out[key] = value
}
return out
}

340
cmd/flex-auth/main_test.go Normal file
View File

@@ -0,0 +1,340 @@
package main
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/netkingdom/flex-auth/pkg/api"
)
func TestRunVersion(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run([]string{"version"}, &stdout, &stderr)
if code != 0 {
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
}
if strings.TrimSpace(stdout.String()) == "" {
t.Fatal("version output is empty")
}
}
func TestRunTestPolicy(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run([]string{"test-policy", "--file", examplePath("policy_package.md")}, &stdout, &stderr)
if code != 0 {
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
}
if !strings.Contains(stdout.String(), `"valid": true`) {
t.Fatalf("stdout = %s; want valid policy result", stdout.String())
}
}
func TestRunCheck(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run([]string{
"check",
"--registry", examplePath("registry_snapshot.json"),
"--policy", examplePath("policy_package.md"),
"--request", examplePath("check_request.yaml"),
}, &stdout, &stderr)
if code != 0 {
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
}
var decision api.DecisionEnvelope
if err := json.Unmarshal(stdout.Bytes(), &decision); err != nil {
t.Fatalf("unmarshal decision: %v\n%s", err, stdout.String())
}
if decision.Effect != api.DecisionEffectAllow {
t.Fatalf("decision.Effect = %q; want allow", decision.Effect)
}
}
func TestRunBatchCheck(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run([]string{
"batch-check",
"--registry", examplePath("registry_snapshot.json"),
"--policy", examplePath("policy_package.md"),
"--request", examplePath("batch_check_request.yaml"),
}, &stdout, &stderr)
if code != 0 {
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
}
var decisions []api.DecisionEnvelope
if err := json.Unmarshal(stdout.Bytes(), &decisions); err != nil {
t.Fatalf("unmarshal decisions: %v\n%s", err, stdout.String())
}
if len(decisions) != 2 || decisions[0].Effect != api.DecisionEffectAllow || decisions[1].Effect != api.DecisionEffectDeny {
t.Fatalf("decisions = %+v; want allow then deny", decisions)
}
}
func TestRunCheckOpsWarden(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run([]string{
"check",
"--registry", opsPath("registry_snapshot.json"),
"--policy", opsPath("policy_package.md"),
"--request", opsPath("check_request_allow_adm.json"),
}, &stdout, &stderr)
if code != 0 {
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
}
var decision api.DecisionEnvelope
if err := json.Unmarshal(stdout.Bytes(), &decision); err != nil {
t.Fatalf("unmarshal decision: %v\n%s", err, stdout.String())
}
if decision.Effect != api.DecisionEffectAllow {
t.Fatalf("decision.Effect = %q; want allow", decision.Effect)
}
if decision.ID == "" {
t.Fatal("decision.ID is empty; ops-warden needs a policy_decision_id")
}
}
func TestServeOpsWardenCheckContract(t *testing.T) {
logPath := filepath.Join(t.TempDir(), "decisions.jsonl")
engine, err := buildEngine(context.Background(), opsPath("registry_snapshot.json"), opsPath("policy_package.md"), logPath)
if err != nil {
t.Fatalf("buildEngine: %v", err)
}
server := httptest.NewServer(newServeMux(engine))
defer server.Close()
resp, err := http.Get(server.URL + "/healthz")
if err != nil {
t.Fatalf("GET /healthz: %v", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("GET /healthz status = %d; want 200", resp.StatusCode)
}
allow := postCheck(t, server.URL+"/v1/check", opsPath("check_request_allow_adm.json"))
if allow.Effect != api.DecisionEffectAllow || allow.ID == "" {
t.Fatalf("allow decision = %+v; want allow with id", allow)
}
deny := postCheck(t, server.URL+"/v1/check", opsPath("check_request_deny_ttl_above_max.json"))
if deny.Effect != api.DecisionEffectDeny || deny.Reason != "ttl_out_of_bounds" {
t.Fatalf("deny decision = %+v; want ttl_out_of_bounds deny", deny)
}
resp, err = http.Get(server.URL + "/v1/check")
if err != nil {
t.Fatalf("GET /v1/check: %v", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusMethodNotAllowed {
t.Fatalf("GET /v1/check status = %d; want 405", resp.StatusCode)
}
resp, err = http.Post(server.URL+"/v1/check", "application/json", strings.NewReader(`{"subject":`))
if err != nil {
t.Fatalf("POST malformed /v1/check: %v", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("malformed POST status = %d; want 400", resp.StatusCode)
}
logData, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("read decision log: %v", err)
}
if !strings.Contains(string(logData), allow.ID) || !strings.Contains(string(logData), deny.ID) {
t.Fatalf("decision log does not contain both decision ids\nlog: %s\nallow: %s deny: %s", string(logData), allow.ID, deny.ID)
}
}
func TestRunLoadRegistryOpsWardenProduction(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run([]string{"load-registry", "--file", opsPath("production_registry_snapshot.json")}, &stdout, &stderr)
if code != 0 {
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
}
var result map[string]any
if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
t.Fatalf("unmarshal load-registry output: %v; stdout = %s", err, stdout.String())
}
if result["subjects"] != float64(4) || result["relationships"] != float64(4) || result["resource_manifests"] != float64(1) {
t.Fatalf("load-registry result = %+v; want production actor registry counts", result)
}
}
func TestOpsWardenProductionRegistryActors(t *testing.T) {
engine, err := buildEngine(context.Background(), opsPath("production_registry_snapshot.json"), opsPath("policy_package.md"), "")
if err != nil {
t.Fatalf("buildEngine: %v", err)
}
cases := []struct {
name string
subjectID string
actor string
actorType string
principal string
ttlHours float64
wantEffect api.DecisionEffect
wantReason string
}{
{
name: "state hub bridge agent allow",
subjectID: "agt-state-hub-bridge",
actor: "agt-state-hub-bridge",
actorType: "agt",
principal: "agt-task-bridge",
ttlHours: 1,
wantEffect: api.DecisionEffectAllow,
},
{
name: "state hub bridge IAM subject allow",
subjectID: "iam:agt-state-hub-bridge",
actor: "agt-state-hub-bridge",
actorType: "agt",
principal: "agt-task-bridge",
ttlHours: 1,
wantEffect: api.DecisionEffectAllow,
},
{
name: "codex interhub bootstrap agent allow",
subjectID: "agt-codex-interhub-bootstrap",
actor: "agt-codex-interhub-bootstrap",
actorType: "agt",
principal: "agt-interhub-bootstrap",
ttlHours: 1,
wantEffect: api.DecisionEffectAllow,
},
{
name: "admin actor allow",
subjectID: "adm-example",
actor: "adm-example",
actorType: "adm",
principal: "adm-full",
ttlHours: 4,
wantEffect: api.DecisionEffectAllow,
},
{
name: "automation actor allow",
subjectID: "atm-backup-daily",
actor: "atm-backup-daily",
actorType: "atm",
principal: "atm-backup-daily",
ttlHours: 1,
wantEffect: api.DecisionEffectAllow,
},
{
name: "ttl above production max denies",
subjectID: "agt-state-hub-bridge",
actor: "agt-state-hub-bridge",
actorType: "agt",
principal: "agt-task-bridge",
ttlHours: 999,
wantEffect: api.DecisionEffectDeny,
wantReason: "ttl_out_of_bounds",
},
{
name: "unregistered production actor denies",
subjectID: "agt-missing",
actor: "agt-missing",
actorType: "agt",
principal: "agt-missing",
ttlHours: 1,
wantEffect: api.DecisionEffectDeny,
wantReason: "unknown_actor_resource",
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
decision, err := engine.Check(context.Background(), opsWardenProductionSignRequest(tt.subjectID, tt.actor, tt.actorType, tt.principal, tt.ttlHours))
if err != nil {
t.Fatalf("Check: %v", err)
}
if decision.Effect != tt.wantEffect {
t.Fatalf("decision.Effect = %q; want %q; decision: %+v", decision.Effect, tt.wantEffect, decision)
}
if tt.wantReason != "" && decision.Reason != tt.wantReason {
t.Fatalf("decision.Reason = %q; want %q; decision: %+v", decision.Reason, tt.wantReason, decision)
}
if tt.wantEffect == api.DecisionEffectAllow && decision.ID == "" {
t.Fatal("allow decision ID is empty")
}
})
}
}
func TestRunValidateAccessDescriptor(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run([]string{"validate", "--kind", "access-descriptor", "--file", examplePath("access_descriptor.yaml")}, &stdout, &stderr)
if code != 0 {
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
}
if !strings.Contains(stdout.String(), `"status": "valid"`) {
t.Fatalf("stdout = %s; want valid status", stdout.String())
}
}
func examplePath(name string) string {
return filepath.Join("..", "..", "examples", "caring", name)
}
func opsPath(name string) string {
return filepath.Join("..", "..", "examples", "ops-warden", name)
}
func opsWardenProductionSignRequest(subjectID, actor, actorType, principal string, ttlHours float64) api.CheckRequest {
return api.CheckRequest{
ID: "check:ops-warden-production-" + actor,
Tenant: "tenant:platform",
Subject: api.SubjectRef{
ID: subjectID,
Type: api.SubjectType(actorType),
},
Action: "sign",
Resource: api.ResourceRef{
ID: "ssh-cert:actor/" + actor,
Type: "ssh-certificate",
System: "ops-warden",
},
Context: map[string]any{
"principals": []string{principal},
"actor_type": actorType,
"ttl_hours": ttlHours,
"pubkey_fingerprint": "SHA256:example-production-fingerprint",
},
}
}
func postCheck(t *testing.T, url, path string) api.DecisionEnvelope {
t.Helper()
body, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
t.Fatalf("POST %s: %v", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("POST %s status = %d; want 200", path, resp.StatusCode)
}
var decision api.DecisionEnvelope
if err := json.NewDecoder(resp.Body).Decode(&decision); err != nil {
t.Fatalf("decode %s response: %v", path, err)
}
return decision
}

View File

@@ -0,0 +1,83 @@
# ADR-0001: Implementation Language and Repo Skeleton
Date: 2026-05-15
Status: Accepted
Deciders: Bernd, with assessment from Claude (Opus 4.7)
Supersedes: —
## Context
flex-auth is a policy-as-code authorization registry and control plane. It
must run as a CLI and, later, a service. Its peers in the NetKingdom
ecosystem are written in a mix of languages: `key-cape` is Go, `ops-bridge`
and `ops-warden` are Python, the State Hub itself is Python. There is a
recorded State Hub decision noting that Go was the right call for key-cape
because of orchestration-heavy HTTP adapter code, fast iteration, and
clean domain boundaries.
flex-auth shares the relevant traits with key-cape: HTTP/gRPC adapters to
multiple PDPs and directory backends, latency-sensitive check paths, and
a need to ship a single static binary for local-development ergonomics.
## Decision
- **Language: Go.**
- **Module path: `github.com/netkingdom/flex-auth`** (placeholder; adjust
if the repo moves under a different GitHub org during publication).
- **Minimum Go version: matching key-cape at time of skeleton landing.**
- **Repo layout:**
```text
cmd/flex-auth/ CLI entrypoint
cmd/flex-authd/ service entrypoint (added when the service layer lands)
internal/registry/ resource / subject / relationship store
internal/policy/ policy package model, Rego evaluation, fixtures
internal/decision/ check, batch_check, list_allowed, explain, decision log
internal/audit/ compact decision-envelope persistence
internal/adapters/ pluggable PDP and directory adapters (later WPs)
pkg/api/ public types and OpenAPI schemas
schemas/ JSON Schema for manifests and envelopes
examples/ runnable example manifests, policies, fixtures
docs/adr/ this ADR series
```
- **Build, lint, test:** `Makefile` targets `build`, `test`, `lint`,
`tidy`, `sbom`. Linting via `golangci-lint`. Tests via the standard
`go test ./...` plus contract fixtures.
- **SBOM:** generate on each release tag and on `make sbom`; register via
the State Hub `ingest_sbom_tool` so `last_sbom_at` stops being `null`.
## Rationale
- Aligns with the only language decision in the NetKingdom ecosystem that
has already been validated in production (KeyCape v0.1).
- Single static binary makes the standalone-first mode trivial to ship
for local development across NetKingdom repos.
- Strong concurrency primitives suit batch-check and list-allowed paths.
- Excellent OPA tooling for Go (`open-policy-agent/opa/rego`) means the
Rego evaluator chosen in ADR-0002 has first-class library support.
- Topaz (the target alignment from ADR-0003) is Go-native — adapter work
in FLEX-WP-0004 stays in the same language.
## Consequences
- New flex-auth contributors need Go in their toolchain. Python is still
used elsewhere in the ecosystem; cross-repo work that hits the State
Hub or ops-bridge must accept the language switch.
- The Go decision is reversible while the repo is empty. Once `cmd/` and
`internal/` have been populated by FLEX-WP-0005 T01, reversal becomes
expensive — flag any reservations during the skeleton task, not later.
## Out of Scope
- Database choice (SQLite vs Postgres vs file-backed) is settled in
FLEX-WP-0002 T02 and recorded in a later ADR.
- Service framework (net/http vs Connect vs gRPC) is deferred to the
service-skeleton task in FLEX-WP-0002 T07.
## Related
- ADR-0002: Rego-in-Markdown policy format.
- ADR-0003: Topaz-aligned MVP.
- State Hub recorded decision: "Implementation language for KeyCape: Go"
(resolved 2026-03-25).

View File

@@ -0,0 +1,184 @@
# ADR-0002: Rego-in-Markdown Policy Package Format
Date: 2026-05-15
Status: Accepted
Deciders: Bernd, with assessment from Claude (Opus 4.7)
Supersedes: the "simple declarative rule format" placeholder in
FLEX-WP-0002 P2.3.
## Context
flex-auth's product surface is policy-as-code with reviewable, testable,
versioned packages and an `explain(decision_id)` API. Three candidate
package formats were on the table:
1. **Bespoke YAML rules now, port to Rego later.** Lowest immediate
complexity, but every adapter spike (Topaz, OPA, OPAL) would need a
translator, and the "thin-wrapper-around-Topaz" trap (see assessment)
gets worse the longer the bespoke layer exists.
2. **Rego from day one.** Standard, battle-tested policy language with
first-class Go library support (`open-policy-agent/opa/rego`). Reuse
for Topaz, OPA, OPAL, Permit, and most ReBAC-adjacent ecosystems.
3. **Cedar.** Strong typing and analyzability; smaller ecosystem; less
alignment with Topaz, which is the MVP backend target in ADR-0003.
The product also values **intent + validation co-location**. Markdown is
already the lingua franca of the NetKingdom knowledge plane (Markitect is
the first consumer), and reviewable policy benefits more from prose
context than YAML or pure `.rego` files can provide on their own.
## Decision
A **policy package is a Markdown document** with:
1. YAML frontmatter describing package metadata (id, version, namespace,
action vocabulary scope, owner, status, activation, fixtures source).
2. Prose sections (free-form) capturing **intent**: what the policy
protects, why, what the failure modes look like, what break-glass
semantics apply.
3. Fenced `rego` blocks containing the rules.
4. Fenced `rego` blocks tagged `test` containing OPA-compatible tests.
5. Fenced `yaml` blocks tagged `fixture` containing decision fixtures
(input/expected-decision pairs) that the loader can also evaluate
against the Rego module.
The loader extracts and concatenates the `rego` blocks into one OPA
module per package (using the `package` declaration in frontmatter), the
`test` blocks into a sibling test module, and the `fixture` blocks into
a fixtures table. Validation runs `opa parse`, `opa test`, and the
fixtures evaluator before a package can be marked `valid`.
### Minimal example
```markdown
---
id: markitect.documents.internal-read
version: 0.1.0
namespace: markitect:document
package: flexauth.markitect.documents
actions: [read, query, search]
owner: team:platform-architecture
status: draft
fixtures:
- examples/markitect/internal_doc_allow.yaml
- examples/markitect/internal_doc_deny.yaml
---
# Internal Document Read
This package gates `read`, `query`, and `search` on documents labelled
`internal`. A subject must be in the `reader` group of the document's
owning team, or hold the `steward` role on the document's repository.
## Failure modes
- Stale group membership: resolver freshness must be within 15 minutes
or the decision becomes `audit_only` with a `stale_directory` reason.
- Break-glass: an `emergency_principal` claim with a logged reason
produces `allow` with an obligation to record an export receipt.
## Rules
```rego
package flexauth.markitect.documents
import future.keywords.if
import future.keywords.in
default decision := {"effect": "deny", "reason": "no_matching_rule"}
decision := {"effect": "allow", "reason": "reader_group"} if {
input.action in {"read", "query", "search"}
input.resource.labels[_] == "internal"
some g in input.subject.groups
g == sprintf("reader:%s", [input.resource.owner_team])
}
decision := {"effect": "allow", "reason": "steward_role"} if {
input.action in {"read", "query", "search"}
"steward" in input.subject.roles_on[input.resource.repository]
}
```
## Tests
```rego test
package flexauth.markitect.documents_test
import data.flexauth.markitect.documents
test_reader_group_allows_read if {
documents.decision.effect == "allow" with input as {
"action": "read",
"subject": {"groups": ["reader:platform-architecture"]},
"resource": {"labels": ["internal"], "owner_team": "platform-architecture"}
}
}
```
## Fixtures
```yaml fixture
- name: reader group allow
input:
action: read
subject: {groups: ["reader:platform-architecture"]}
resource: {labels: ["internal"], owner_team: "platform-architecture"}
expect: {effect: allow, reason: reader_group}
- name: no group deny
input:
action: read
subject: {groups: []}
resource: {labels: ["internal"], owner_team: "platform-architecture"}
expect: {effect: deny, reason: no_matching_rule}
```
```
(Fence backticks above are zero-width-spaced for documentation. The real
loader expects normal triple backticks.)
## Rationale
- **Intent and validation co-located.** A reviewer reading the package
sees *why* alongside *what*. ADR text and policy code don't drift.
- **Standard evaluator.** OPA's `rego` library is mature, well-tested,
and the dominant Rego implementation. flex-auth gets correctness and
performance work for free.
- **Topaz alignment from day one.** ADR-0003 commits to shaping the
standalone core so the Topaz adapter (FLEX-WP-0004 T01) is a small
step. Rego is Topaz's native policy language.
- **Markitect synergy.** The first consumer is a Markdown knowledge
system. Policy packages and the documents they protect share one
authoring substrate; Markitect renders policy packages natively;
policy package versions sit comfortably in the same review process as
any other document.
- **Tooling reuse.** `opa fmt`, `opa test`, `opa eval`, and the broader
OPA tool ecosystem just work on extracted modules.
## Consequences
- The loader must implement Markdown extraction. Off-the-shelf Goldmark
+ a small block walker covers it; tests must lock the fence syntax.
- Authors must learn Rego. This is the universally accepted cost of
Rego adoption. The literate format lowers the cliff by surrounding
the language with intent prose.
- Some pure-data fixtures still live in `examples/` and are referenced
from the frontmatter — keeps large fixture files out of the policy
document body.
- An ADR amendment will be needed if Cedar or a typed policy language
enters the picture as a *peer* (not adapter). The literate Markdown
envelope can accommodate other fenced languages, but `rego` is the
baseline.
## Out of Scope
- Whether activation, rollout, and rollback semantics live in
frontmatter or in a sibling activation manifest — settled in
FLEX-WP-0002 P2.3.
- The exact extraction library — implementation detail of P2.3.
## Related
- ADR-0001: Go implementation (provides the OPA library path).
- ADR-0003: Topaz-aligned MVP (provides the backend rationale).
- FLEX-WP-0002 P2.3 (policy package loader and validator).

View File

@@ -0,0 +1,87 @@
# ADR-0003: Topaz-Aligned MVP
Date: 2026-05-15
Status: Accepted
Deciders: Bernd, with assessment from Claude (Opus 4.7)
Supersedes: implicit "standalone first, evaluate Topaz later" sequencing
in the original FLEX-WP-0004 P4.1.
## Context
The pre-implementation assessment names a primary threat: the
"thin-wrapper-around-Topaz" trap. Topaz already combines OPA/Rego
evaluation with a local directory inspired by Zanzibar (users, groups,
resources, relations) and a local deployment story. If flex-auth's
standalone core reimplements 60% of Topaz badly and then adapts to Topaz
anyway, the abstraction earns no product value.
ADR-0002 commits to Rego from day one. That removes the largest source
of friction between the standalone evaluator and Topaz, because the
policy language is identical.
The remaining question is sequencing. The original workplans placed the
Topaz evaluation at FLEX-WP-0004 P4.1, *after* the standalone core
shapes its registry, check API, and decision log. That sequencing is
inverted: by the time the spike runs, the core has already made shape
decisions that the Topaz adapter will then have to translate.
## Decision
- **flex-auth's actual product surface is registry + audit + explain +
multi-consumer + literate policy packages.** The PDP is an
implementation detail behind a stable contract.
- **Shape the standalone core to be Rego/Topaz-aligned from day one.**
Concretely:
- The standalone evaluator embeds the OPA Rego library and evaluates
the same Rego modules a Topaz deployment would.
- The registry's resource/subject/relation model uses vocabulary that
maps trivially onto Topaz directory objects and relations.
- The decision envelope carries the same provenance fields whether
evaluation is local or delegated.
- **The Topaz evaluation spike moves earlier.** It runs in
FLEX-WP-0005 (Foundations), not FLEX-WP-0004, so its findings inform
the registry, policy loader, and check API before they are written.
- **FLEX-WP-0004 keeps Topaz only as one delegated-mode adapter**
alongside OpenFGA, SpiceDB, OPA-as-remote, Cedar, and Keycloak
Authorization Services. Topaz is no longer the sole evaluation
question of FLEX-WP-0004.
## Rationale
- Aligning vocabulary up front is cheap; aligning it retroactively is
not.
- The standalone mode remains genuinely useful (zero external services,
single binary, fixtures-first development) while staying Topaz-shaped
internally.
- Topaz remains an opinionated, single-vendor product. The product
surface (registry, audit, explain, multi-consumer) is where flex-auth
competes; the evaluation engine is where flex-auth integrates.
## Consequences
- The Topaz spike (FLEX-WP-0005 T04) becomes a hard prerequisite for
FLEX-WP-0002 P2.1 schemas. Its output is a mapping document showing
how flex-auth resources/relations correspond to Topaz directory
objects/relations, plus a recommendation on whether to embed Topaz's
directory contracts directly or restate them.
- FLEX-WP-0004 is slimmed (Topaz evaluation moves out, leaving the
Topaz adapter implementation in T01 and the relationship/rule/
Keycloak/directory adapter work in T02T06).
- Keycloak Authorization Services remains an adapter path for
Keycloak-centric deployments — flex-auth is the canonical PAP/PDP,
Keycloak AuthZ is one delegated PDP option.
## Out of Scope
- The exact wire protocol of the Topaz adapter (gRPC vs HTTP vs library
embed) is decided in FLEX-WP-0004 T01.
- The directory-store implementation in standalone mode (SQLite vs
file-backed vs in-memory) is decided in FLEX-WP-0002 T02.
## Related
- ADR-0001: Go implementation (matches Topaz's language).
- ADR-0002: Rego-in-Markdown policy format (matches Topaz's policy
language).
- FLEX-WP-0005 T04: Topaz mapping spike.
- FLEX-WP-0004: Delegated PDP and directory adapters.

View File

@@ -0,0 +1,203 @@
# CARING Architecture Blueprint For Flex-Auth
Date: 2026-05-17
Status: Draft architecture blueprint
Source standard: CARING 0.4.0-RC2 (`/home/worsch/helix-forge/wiki/CaringStandardRc2.md`)
## Purpose
This blueprint describes how flex-auth can serve as the practical,
efficient reference implementation of the CARING standard while keeping
the core authorization path small enough to build and operate.
CARING remains the semantic standard. Flex-auth implements the subset
needed to make CARING descriptors, policy conformance, decisions,
explanations, and audit events executable.
## Architecture Position
CARING answers:
```text
What access semantics should a system expose, validate, explain, and audit?
```
Flex-auth answers:
```text
Given a subject, action, resource, context, policy package, and registry
state, what decision should protected systems enforce?
```
The reference implementation relationship is:
```text
CARING standard
-> flex-auth CARING profile schemas
-> policy packages and conformance checks
-> check/batch/list/explain APIs
-> decision and exposure-event audit records
```
## Minimal CARING Profile
The first implementation should pin a `caring_profile` version and define
the following canonical fields in flex-auth API and schema artifacts:
```text
subject_type
organization_relation
canonical_role
scope
plane
capabilities
exposure_mode
conditions
lifecycle_state
restrictions
exposure_event
derived_capabilities
access_path
```
The core does not need to implement every CARING benchmark or native-system
mapping immediately. It only needs the descriptor shape, enums, validation,
and a place in decisions and audit records.
## Core Data Types
`pkg/api` should expose:
```text
CaringProfile
CaringAccessDescriptor
CaringConformanceFinding
CaringExposureEvent
CaringRestriction
CaringDerivedCapability
```
These types should be referenced by:
```text
SubjectManifest
ResourceManifest
RelationshipFact
PolicyPackageMetadata
CheckRequest
DecisionEnvelope
AuditEvent
```
The `ResourceManifest` remains consumer-owned and lightweight. CARING
classification should be optional on resources at first, with policy
packages allowed to require specific fields for a protected system.
## Decision Pipeline
The efficient runtime path is:
```text
1. Normalize identity claims into a Subject.
2. Load registry facts for resource, relationships, tenant, and groups.
3. Build a CheckRequest with CARING context.
4. Evaluate restrictions first.
5. Evaluate scoped policy package rules.
6. Produce a DecisionEnvelope.
7. Attach CARING conformance findings and provenance.
8. Persist audit/exposure-event records when required.
```
CARING conformance should not block the fast allow/deny path unless a
policy marks a finding as enforcement-grade. Most early findings should
be diagnostics, warnings, or audit requirements.
## Conformance Model
Flex-auth should support two CARING modes:
```text
descriptive
Map existing local roles, policy objects, and grants into CARING
descriptors. Report ambiguity, bundling, missing scope, missing plane,
induced access, and exposure gaps.
prescriptive
Validate new policy packages and manifests against a CARING profile.
Fail on required fields, restriction precedence, tenant-boundary
violations, and explicit policy-plane or secret-plane violations.
```
The conformance output should use stable severities:
```text
info
warning
violation
blocked
```
## Policy Package Shape
Rego-in-Markdown policy packages should carry CARING frontmatter:
```yaml
caring:
profile: caring-0.4.0-rc2
canonical_roles: [Doer]
organization_relations: [Customer]
planes: [Data]
capabilities: [View]
exposure_modes: [Masked, Plaintext]
conditions: [PurposeBound, Logged]
restrictions: [ExportBlocked, CrossTenantBlocked]
```
The policy evaluator should provide this metadata to Rego and copy the
matched CARING descriptor into the decision envelope.
## Reference Implementation Boundaries
Flex-auth should implement:
```text
CARING descriptor schemas
CARING-aware policy package validation
CARING-aware decision envelopes
CARING explain output
CARING exposure-event audit records
CARING conformance fixtures
```
Flex-auth should not own:
```text
The CARING standard text
The NetKingdom IAM Profile
Identity-provider role issuance
Consumer-specific product semantics
Full benchmark mappings for every native access system
```
## Implementation Slices
1. Pin the CARING profile and descriptors in `FLEX-WP-0002 P2.1`.
2. Add registry fields and validation in `FLEX-WP-0002 P2.2`.
3. Add policy frontmatter and diagnostics in `FLEX-WP-0002 P2.3`.
4. Attach CARING metadata to `check` and `batch_check` in `FLEX-WP-0002 P2.4`.
5. Use CARING language in `list_allowed` and `explain` in `FLEX-WP-0002 P2.5`.
6. Persist CARING exposure events in `FLEX-WP-0002 P2.6`.
7. Map Markitect fixtures as the first consumer benchmark in `FLEX-WP-0003`.
8. Require delegated adapters in `FLEX-WP-0004` to preserve CARING envelope
fields even when backend-native semantics differ.
## Open Design Choices
The first implementation should decide:
```text
Whether CARING enum values are strict or extension-friendly.
Whether conformance findings are always non-blocking by default.
How much derived-capability analysis belongs in core versus adapters.
How to version profile changes after CARING leaves RC status.
```

View File

@@ -0,0 +1,133 @@
# Delegated Mode Operations
Status: implemented for FLEX-WP-0004 P4.6.
## Purpose
Delegated mode lets flex-auth coordinate external authorization systems
without changing the protected-system-facing API. Protected systems keep
using flex-auth check, batch, list, explanation, and audit contracts.
Topaz, relationship PDPs, rule PDPs, Keycloak Authorization Services,
and directory resolvers stay behind adapter boundaries.
## Deployment Order
1. Load protected-system manifests and resource manifests into the
flex-auth registry.
2. Load subject manifests or configure directory resolvers.
3. Import resources and relationships into the delegated backend.
4. Import policy artifacts or backend-native policy mirrors.
5. Run adapter health checks.
6. Start serving delegated checks only after the backend reports healthy
and the latest import has a recorded consistency token.
For Topaz, the local reference topology is `examples/topaz`. For
Keycloak, register resources from flex-auth manifests before enabling
UMA permission checks. For OpenFGA/SpiceDB-style systems, import tuples
from the canonical registry snapshot. For OPA/Cedar-style systems,
import policy artifacts from validated flex-auth policy packages.
## Health Checks
Each delegated adapter exposes a health boundary. Runtime health should
be tracked separately for:
- backend reachability
- policy import freshness
- directory or tuple import freshness
- resolver freshness
- last successful decision time
Health failures should not silently fall back to stale allow decisions.
They should produce observable diagnostics and deny fail-closed unless a
deployment has explicitly accepted a narrower fail-open mode for a
non-sensitive action.
## Fail-Closed Default
The default behavior is fail-closed:
| Condition | Decision effect | Typical reason |
| --- | --- | --- |
| backend unavailable | deny | `topaz_unavailable`, `relationship_backend_unavailable`, `rule_backend_unavailable`, `keycloak_unavailable` |
| stale directory or tuple data | deny | `topaz_directory_stale`, `relationship_data_stale` |
| stale policy | deny | `rule_policy_stale`, `keycloak_policy_stale` |
| partial backend result | deny | `*_partial_result` |
| request cannot be translated | deny | `*_request_incomplete` |
Fail-open is only acceptable for explicitly classified low-risk
capabilities and must be recorded as a policy decision, not an adapter
default. Data, identity, policy, secret, audit, and commercial planes
should remain fail-closed.
## Caching
Recommended cache layers:
- resource manifest snapshot cache
- subject/group resolver cache
- delegated backend import token cache
- decision-result cache for idempotent low-risk checks
Cache entries must record source, retrieval time, max age, and expiry.
Directory resolver results already expose `Freshness`; tuple and
Topaz/Keycloak imports expose consistency metadata through the decision
provenance fields. Cache hits should still include provenance so audit
readers can tell whether evidence came from a fresh backend call or a
bounded cache.
## Consistency
flex-auth uses `DecisionEnvelope.provenance.directory_etag` as the
adapter-neutral consistency token field:
- Topaz: relation etag when available
- OpenFGA/SpiceDB-style backends: model id, zedtoken, or tuple-store
freshness token
- directory resolvers: freshness metadata attached to subject metadata
- Keycloak/rule PDPs: policy version in `provenance.policy_version`
Adapters should deny or mark stale when a request demands a newer token
than the backend can satisfy. Read-your-writes paths should import
registry data, record the returned token, and require that token for the
first protected-system checks after import.
## Audit Behavior
Delegated decisions must log the same canonical envelope as standalone
decisions:
- subject and resource references
- effect, reason, matched rule, matched policy version
- obligations
- diagnostics
- evaluator and mode
- consistency token
- CARING descriptor, restrictions, exposure modes, derived
capabilities, exposure events, and conformance findings
Backend-native traces can appear in diagnostics, but they must not
replace the canonical fields. Explanations should be generated from the
flex-auth envelope so switching backends does not change protected-system
or auditor vocabulary.
## Adapter Notes
- Topaz operations: `docs/topaz-adapter-operations.md`
- Relationship PDPs: `docs/relationship-pdp-adapter-boundary.md`
- Rule PDPs: `docs/rule-pdp-adapter-boundary.md`
- Keycloak Authorization Services:
`docs/keycloak-authz-adapter-path.md`
- Directory resolvers: `docs/directory-group-resolver-adapters.md`
## Operational Checklist
- Backend configured and reachable.
- Policy/resource/tuple import completed.
- Latest import token recorded.
- Directory resolver freshness thresholds configured.
- Fail-closed behavior tested for unavailable, stale, partial, and
invalid-request cases.
- Decision log captures delegated provenance and CARING metadata.
- Rollback path can switch a protected system back to standalone mode or
to another delegated adapter without changing the protected-system API.

View File

@@ -0,0 +1,65 @@
# Directory Group Resolver Adapters
Status: implemented for FLEX-WP-0004 P4.5.
## Role
Directory resolvers enrich flex-auth subjects with group and role
evidence from external identity systems. They do not decide access by
themselves. They feed the standalone, relationship, rule, Topaz, or
Keycloak decision paths with fresh subject metadata and provenance.
## Resolver Sources
Implemented resolver boundaries:
- Microsoft Graph group overage (`GraphResolver`)
- SCIM provisioning (`SCIMResolver`)
- LDAP/Active Directory (`LDAPResolver`)
- Keycloak admin API (`KeycloakResolver`)
Each resolver returns `ResolveResult` with normalized groups, roles,
freshness, overage, and metadata.
## Freshness
Every result carries:
- source
- retrieval time
- max age
- expiry time
- stale flag
Decision adapters can use this metadata to deny stale directory evidence
or to include freshness diagnostics in the decision envelope.
## Overage
Graph tokens can omit groups and instead emit overage indicators such
as `_claim_names.groups` or `hasgroups=true`. The Graph resolver records
that condition in `OverageMetadata` so downstream policy can distinguish
"no groups" from "groups omitted, lookup required".
## CARING Provenance
Each `GroupGrant` and `RoleGrant` identifies:
- source provider
- originating claim name
- organization relation
- subject type
- optional CARING descriptor
This makes it possible to explain that a canonical role or group came
from Graph, SCIM, LDAP/AD, or Keycloak rather than from an opaque token
claim. The source remains inspectable in CARING conformance reviews.
## Subject Enrichment
`MergeResults` deduplicates groups and roles across providers while
preserving freshness, overage, and descriptors. `ApplyToSubject` returns
a subject with resolved groups/roles and directory metadata attached.
The enriched subject can then flow through any flex-auth decision path
without changing protected-system request or decision contracts.

View File

@@ -0,0 +1,181 @@
# NetKingdom IAM Profile — flex-auth Consumption Surface
Date: 2026-05-22
Status: Aligned with NetKingdom IAM Profile v0.2; binds the input contract
for the standalone evaluator (FLEX-WP-0002) and every PDP adapter
(FLEX-WP-0004).
Upstream: `~/net-kingdom/canon/standards/iam-profile_v0.2.md`.
## Boundary
The NetKingdom IAM Profile defines the OIDC contract shared across
platform, tenant, service, and agent principals. flex-auth **consumes
verified claims**; it does not
verify token signatures, fetch JWKS, or terminate OIDC sessions. Those
responsibilities belong upstream:
- **key-cape (lightweight mode)** validates tokens against its local
OIDC provider and emits claims that conform to the profile.
- **Keycloak (heavy mode)** signs tokens; integration code (e.g.
Markitect's `NetKingdomIdentityClaimsAdapter`) validates issuer,
audience, signature, expiry, and clock skew before handing claims to
flex-auth.
A flex-auth deployment that exposes a network endpoint MUST be fronted
by an identity layer that does the verification. The flex-auth core
accepts a normalized claim envelope and is responsible for everything
*after* "this caller is authenticated".
## Input Envelope
flex-auth's standalone evaluator and adapters consume a normalized
envelope identical to Markitect's `EnterpriseIdentity` shape:
```yaml
issuer: <oidc issuer URL> # required
subject: <stable subject id> # required
tenant: tenant:platform | tenant:<id> # required
principal_type: human | service | agent
audience: [<aud>, ...] # required, non-empty
authorized_party: <azp or client_id, optional>
preferred_username: <string> # required for humans
roles: [<role>, ...] # required, non-empty
scopes: [<scope>, ...] # required, non-empty
groups: [<group>, ...] # required, may be empty
assurance:
level: aal0 | aal1 | aal2 | aal3 | break_glass
methods: [<method>, ...]
mfa: <bool>
source: <identity or MFA evidence source>
at: <unix timestamp, optional>
acr: <oidc acr value, optional>
amr: [<oidc amr value>, ...] # tolerated provider-native input
agent:
id: <agent id, optional>
mode: autonomous | delegated
directory:
groups_claim_present: <bool>
group_overage: <bool> # Microsoft Entra-style group overage
claims: { ... } # full original claim map (minus 'groups')
provenance:
source: claims | jwt | jwt-fixture
verified_signature: <bool>
```
This is the envelope every check API call receives, regardless of
which upstream identity provider produced the token.
## Required Claims (per IAM Profile v0.2 "Core Claims")
flex-auth treats the following as hard requirements. Missing any
produces a `validation_error` before the request reaches a policy
package.
| Claim | flex-auth field | Notes |
| --- | --- | --- |
| `iss` | `issuer` | Must match the deployment's expected issuer; production rejects local-dev issuers (`localhost`, `127.0.0.1`, `.local`, `dev.local`). |
| `sub` | `subject` | Stable identifier; not a username. |
| `aud` | `audience` | Must include the flex-auth instance or the protected system. |
| `exp` | (validated upstream) | flex-auth tolerates ≤60s clock skew per profile §"Token Lifecycle". |
| `iat` | (validated upstream) | Same. |
| `tenant` | `tenant` | Required for platform/tenant boundary decisions. |
| `principal_type` | `principal_type` | `human`, `service`, or `agent`; emergency is a role plus `assurance.level=break_glass`. |
| `groups` | `groups` | Required, possibly empty; overage is handled by directory resolvers. |
| `scope` or `scp` | `scopes` | At least one scope required. Empty scope is a hard fail. |
| `roles` | `roles` | Canonical role source. At least one role required by current flex-auth policy fixtures. |
| `assurance` | `assurance` | Required normalized evidence object with level, methods, mfa, and source. |
| `preferred_username` | `preferred_username` | Required for `principal_type=human`. Optional for service and agent principals. |
## Recommended Claims
| Claim | flex-auth field | Use |
| --- | --- | --- |
| `email` | `claims.email` | Contact identity; **never** used for authorization decisions. |
| `name` | `claims.name` | Display only. |
| `azp` | `authorized_party` | Distinguishes service-account client from impersonating client. |
| `acr` | `assurance.acr` | Authentication context class; gates high-trust scopes. |
| `amr` | `assurance.amr` | Authentication methods; `otp`/`mfa`/`hwk` lift `assurance.mfa` to true. |
## Tolerated Variations
flex-auth normalizes — protected systems never see the variation.
- **Role claim location.** IAM Profile v0.2 makes top-level `roles`
canonical. During migration, flex-auth may also accept Keycloak's
`realm_access.roles` and `resource_access.<client>.roles`, but those
are provider-native compatibility inputs.
- **Scope encoding.** `scope` (space-separated string) and `scp`
(array) both accepted; both produce the same `scopes` array.
- **Audience encoding.** `aud` as a single string or as an array;
flex-auth always normalizes to an array.
- **MFA signal.** IAM Profile v0.2 uses `assurance.mfa` and
`assurance.level`. Legacy/provider-native `amr` and `acr` are
tolerated as inputs to the normalized assurance object.
## Principal-Type Detection
IAM Profile v0.2 supplies `principal_type` directly. flex-auth uses that
claim as normative input. Legacy fixtures may be classified by:
1. If `client_id` is set and `service` is in `roles``service`.
2. If `azp` starts with `svc-` or `service` is in `roles``service`.
3. If agent metadata is present → `agent`.
4. Otherwise → `human`.
This matches Markitect's `NetKingdomIdentityClaimsAdapter._principal_type`
as a compatibility path. New claim envelopes should not force flex-auth
to infer principal type.
## Group Overage and Freshness
Microsoft Entra and Keycloak both clip the `groups` claim once a
threshold is reached; the token then carries `hasgroups: true` (Entra)
or `_claim_names.groups` (also Entra). flex-auth's directory layer is
responsible for resolving the full set via Graph/SCIM/Keycloak admin
API; the claim envelope carries `directory.group_overage = true` so
policy packages can decide whether to fail-closed or accept the
partial set with an `audit_only` outcome.
Group freshness is tracked at the directory-resolver layer (out of
scope for this document; see FLEX-WP-0004 T05).
## Production vs Local Development
Per IAM Profile §"Local Development Profile":
- Local-development issuers (`localhost`, `127.0.0.1`, hostnames
ending in `.local`, `dev.local`) are rejected when
`environment=production` is set in the request context.
- A development token marked clearly through issuer/audience is
accepted in non-production environments.
- The local-development path exists to keep flex-auth useful before
Keycloak is wired in; it never weakens production rules.
## Emergency Principals
Per IAM Profile §"Human Override and Emergency Access":
- Emergency access is represented as a human, service, or agent
principal with an `emergency`/`break-glass` role and
`assurance.level: break_glass`.
- Every decision involving an emergency principal MUST record a
`record_emergency` obligation in the decision envelope.
- Policy packages MAY allow emergency principals; flex-auth's audit
layer ensures the action is durable regardless.
## Reference Implementation
Markitect's `NetKingdomIdentityClaimsAdapter` (at
`markitect-tool/src/markitect_tool/policy/enterprise.py`) implements
the validation steps above in Python. flex-auth's Go implementation
(FLEX-WP-0002 P2.4) mirrors its behavior and stays in sync via
contract tests against the fixtures in `examples/claims/`.
## Compatibility Notes
- `roles` is canonical in IAM Profile v0.2. `realm_access.roles` and
`resource_access.<client>.roles` remain tolerated provider-native inputs
while Keycloak mappings are updated.
- Workload identity may enter through a documented token-exchange path,
but the normalized envelope still carries `principal_type: service` or
`principal_type: agent`, `tenant`, and `assurance`.

View File

@@ -0,0 +1,78 @@
# Keycloak Authorization Services Adapter Path
Status: implemented for FLEX-WP-0004 P4.4.
## Role
The Keycloak path is for deployments that already use Keycloak as the
identity provider and want to evaluate some authorization decisions
through Keycloak Authorization Services. flex-auth still remains the
source of truth for protected-system resources, CARING descriptors, and
decision envelopes.
## Mapping
flex-auth maps a check to Keycloak's UMA permission shape:
| flex-auth | Keycloak |
| --- | --- |
| protected system | resource server / audience |
| resource id | resource id |
| action | scope |
| subject | requesting party |
| context and CARING descriptor | claim token |
The adapter builds a permission as `resource_id#scope`, for example
`document:internal-note#read`.
## Resource Registration
`ResourceRegistrationsFromManifest` converts a
`ResourceManifest` into Keycloak resource registrations:
- resource id and type are preserved;
- manifest actions become scopes;
- path becomes the URI;
- labels, trust zone, owner, parent, system, and type are stored as
resource attributes.
Keycloak can mirror these resources, but flex-auth keeps the original
manifest as the canonical record.
## Decision Wrapping
Keycloak allow/deny results are wrapped into the standard
`DecisionEnvelope`:
- `provenance.evaluator=keycloak-authz`
- `provenance.mode=delegated`
- Keycloak RPT token id and permission appear in diagnostics
- CARING descriptor and conformance findings are preserved
Backend-native Keycloak policy names do not replace CARING canonical
roles in protected-system responses.
## Failure Behavior
The adapter fails closed for:
- Keycloak unavailable: `keycloak_unavailable`
- stale policy state: `keycloak_policy_stale`
- partial result: `keycloak_partial_result`
- untranslatable request: `keycloak_request_incomplete`
Each failure returns a deny envelope with `diagnostics.keycloak_failure`
and a CARING conformance finding.
## Boundaries
This path intentionally does not make Keycloak the only policy source of
truth. flex-auth continues to own:
- resource manifests from protected systems;
- CARING descriptors and conformance findings;
- audit and explanation envelope shape;
- adapter-neutral request/decision APIs.
Keycloak is a delegated evaluator and resource mirror for Keycloak-heavy
installations, not the canonical model for the whole product.

View File

@@ -0,0 +1,24 @@
# Markitect Action Vocabulary
This document defines the action vocabulary for Markitect as a flex-auth
protected system. Actions are normalized before policy evaluation so Markitect
local behavior maps cleanly to CARING capabilities and exposure modes.
| Action | Markitect policy-gateway meaning | CARING capabilities | CARING planes | Exposure modes | Decision effects |
| --- | --- | --- | --- | --- | --- |
| `read` | Render or fetch one document/resource. | `View` | `Data` | `Metadata`, `Masked`, `Plaintext` | `allow`, `deny`, `redact` |
| `query` | Answer over a bounded resource set. | `ViewCollection`, `Observe` | `Data` | `Metadata`, `Aggregated`, `Masked` | `allow`, `deny`, `redact` |
| `search` | Search index or metadata across resources. | `ViewCollection`, `Observe` | `Data` | `Metadata`, `Aggregated`, `Masked` | `allow`, `deny`, `redact` |
| `package` | Build a context package from selected resources. | `Create`, `Bind`, `ViewCollection` | `Intent`, `Data` | `Metadata`, `Masked` | `allow`, `deny`, `audit_only` |
| `activate_context` | Activate a prepared context package for model/tool use. | `Use`, `Execute` | `Intent`, `Policy` | `Metadata`, `Masked` | `allow`, `deny`, `audit_only` |
| `export` | Materialize or transfer content outside Markitect. | `Export` | `Data`, `Audit` | `Exportable`, `Plaintext` | `allow`, `deny`, `audit_only` |
| `workflow_run` | Execute a workflow using Markitect resources. | `Execute`, `Operate` | `Execution`, `Data`, `Audit` | `Metadata`, `Masked`, `Plaintext` | `allow`, `deny`, `audit_only` |
| `admin` | Configure Markitect policy, identity, or resource controls. | `Configure`, `Grant`, `Revoke`, `Audit` | `Configuration`, `Identity`, `Policy`, `Audit` | `Metadata`, `Plaintext` | `allow`, `deny`, `audit_only` |
`read`, `query`, and `search` never imply `Export`. Export is separate because
it changes the exposure mode to `Exportable` and usually requires explicit
conditions such as MFA and logging.
The code-level source of truth is `internal/markitect/actions.go`. The pinned
manifest example in `examples/markitect/protected_system_manifest.yaml` mirrors
that vocabulary as protected-system action definitions.

View File

@@ -0,0 +1,119 @@
# Markitect Integration Flow
This document describes how Markitect should use flex-auth as its first
protected-system consumer integration.
## 1. Publish Resources
Markitect emits a `FlexAuthResourceManifest` for each knowledge base or
resource slice it wants flex-auth to authorize. The emitted manifest should use
the namespace in `docs/markitect-resource-namespace.md` and include:
- stable resource ids such as `document:internal-note`
- resource type and parent links
- path, labels, trust zone, owner, and durable backend metadata
- `metadata.flex_auth_contract: resource-registration-v0`
- `caring_profile: caring-0.4.0-rc2` when the emitter can provide it
flex-auth imports the manifest through `internal/markitect.ImportResourceManifest`.
The importer enriches resources with CARING scope and plane classification and
returns diagnostics when a resource type, trust zone, label set, or CARING
profile is missing or ambiguous.
## 2. Submit Check Requests
For one resource, Markitect submits `CheckRequest`:
```text
subject + action + resource + context + optional caring_context
```
For repeated checks, Markitect submits `BatchCheckRequest` with the same
subject/action/context and a resource list. Resource order is preserved in the
response.
Markitect should pass local frontmatter and backend metadata as resource
attributes when they affect policy:
- `labels`
- `trust_zone`
- `markitect_path`
- `frontmatter_visibility`
- `source_revision`
- `workflow_state`
- `freshness_seconds`
- `data_classes`
Subject roles and groups should be normalized into subject attributes or loaded
into the registry as subject/group/team manifests. CARING dimensions should be
attached as `caring_context` when Markitect already knows the intended
descriptor; otherwise flex-auth can derive descriptors from relationship facts.
## 3. Enforce Decisions
Markitect maps flex-auth decision envelopes into its gateway contract with
`internal/markitect.ToGatewayDecision`.
Expected effects:
- `allow`: render, answer, package, activate, export, or run workflow.
- `deny`: block the operation.
- `redact`: continue with returned obligations such as `mask_fields`.
- `audit_denied`: block or quarantine while preserving an audit-grade record.
Markitect should enforce obligations before exposing data. For example, a
`mask_fields` obligation must be applied before rendering or model use, and a
`record_context_activation` obligation must be logged when a context package is
activated.
## 4. Record Decision IDs
Every gateway operation should persist the flex-auth decision id alongside the
Markitect request id, workflow id, rendered artifact id, or export id.
At minimum, always record:
- denies
- redactions
- exports
- context package activations
- workflow runs
- support, break-glass, or other CARING exposure events
The local flex-auth JSONL log is suitable for local development. Markitect may
also write the same decision id into its own event log.
## 5. Explain Decisions
When users need an explanation, Markitect can call flex-auth `explain` or use
the decision id to retrieve the persisted envelope and project it with
`ToGatewayDecision`.
Explanations should preserve CARING language:
- subject relation and canonical role, for example `Customer Doer`
- scope, for example `Resource document:internal-note`
- plane, for example `Data`
- capability, for example `View` or `Export`
- exposure mode, for example `Masked`, `Plaintext`, or `Exportable`
- restrictions, conditions, and conformance findings
Example explanation:
```text
Customer Doer may View Data Plane resource document:internal-note because reader_group.
```
## Local Mapping Summary
| Markitect local concept | flex-auth field | CARING dimension |
| --- | --- | --- |
| frontmatter visibility | `resource.attributes.frontmatter_visibility` | Exposure mode hint |
| document labels | `resource.attributes.labels` | Scope and exposure hint |
| owner/steward | `resource.owner`, subject groups/roles | Canonical role and relation |
| workflow state | `resource.attributes.workflow_state`, request context | Lifecycle/condition hint |
| context freshness | `request.context.freshness_seconds` | Condition and conformance finding |
| export request | `action: export` | `Export` capability and `Exportable` exposure |
The examples in `examples/markitect/` are the executable contract fixtures for
this flow.

View File

@@ -0,0 +1,76 @@
# Markitect Resource Namespace
This document defines the Markitect protected-system namespace consumed by
flex-auth. It is the P3.1 contract between Markitect resource metadata and the
generic flex-auth registry.
The namespace is intentionally Markitect-specific at the edge and generic once
registered. Markitect may keep its local frontmatter and backend metadata
names, but emitted resource manifests should normalize them into the resource
types and CARING dimensions below.
## Hierarchy
```text
knowledge_base
-> repository
-> document
-> section
-> span
-> context_package
-> workflow_artifact
-> export
```
Markitect may emit a partial tree. For example, a document can be parented
directly to a knowledge base when the repository boundary is not material to a
policy decision. flex-auth treats `parent` as a stable relationship hint; P3.2
and P3.4 add importer and check fixtures that make inherited behavior explicit.
## CARING Mapping
| Markitect resource type | Parent types | CARING scope | CARING planes | Notes |
| --- | --- | --- | --- | --- |
| `knowledge_base` | none | `Workspace` | `Intent`, `Data` | Top-level user-visible knowledge container. |
| `repository` | `knowledge_base` | `Project` | `Build`, `Data` | Versioned source or storage boundary behind a knowledge base. |
| `document` | `repository`, `knowledge_base` | `Resource` | `Data` | Renderable document or page. Markitect `path` maps to resource `path`. |
| `section` | `document` | `Subresource` | `Data` | Stable heading or block region inside a document. |
| `span` | `section`, `document` | `Field` | `Data` | Fine-grained text range, cell, token span, or field-level surface. |
| `context_package` | `knowledge_base`, `repository`, `document` | `Dataset` | `Intent`, `Data`, `Policy` | Bundled context prepared for model/tool use. |
| `workflow_artifact` | `context_package`, `document` | `Process` | `Execution`, `Data`, `Audit` | Generated workflow output, review artifact, or intermediate. |
| `export` | `workflow_artifact`, `context_package`, `document` | `Record` | `Data`, `Audit` | Materialized package, file, archive, or external transfer. |
## Frontmatter Compatibility
Markitect document frontmatter can remain local, but manifests should preserve
the following mappings:
- `id` or stable slug -> `resources[].id`
- document kind -> `resources[].type`
- source path -> `resources[].path`
- parent knowledge base, repository, or document -> `resources[].parent`
- labels, classification, or visibility -> `resources[].labels`
- tenant/customer boundary -> `resources[].attributes.tenant` when it is not
already represented by the request subject/resource tenant
- owner team or steward -> `resources[].owner`
- freshness, workflow state, and source revision -> `resources[].attributes`
## Backend Metadata Compatibility
Backend metadata can be richer than the flex-auth contract. The manifest should
keep durable values in `attributes` and avoid embedding backend-only transient
state in resource ids.
Recommended backend metadata keys:
- `markitect_path`
- `frontmatter_visibility`
- `source_revision`
- `workflow_state`
- `freshness_seconds`
- `data_classes`
- `tenant`
The examples in `examples/markitect/protected_system_manifest.yaml` and
`examples/markitect/namespace_resource_manifest.yaml` are the pinned schema
examples for this namespace.

View File

@@ -0,0 +1,104 @@
# Ops-Warden Policy Gate Handoff
Date: 2026-06-23
Workplan: FLEX-WP-0006
Ops-warden unblocker: WARDEN-WP-0009 T01
## Published flex-auth assets
- Policy package: examples/ops-warden/policy_package.md
- Policy fixtures: examples/ops-warden/policy_fixtures.yaml
- Combined registry fixture: examples/ops-warden/registry_snapshot.json
- Protected-system manifest: examples/ops-warden/protected_system_manifest.yaml
- Resource manifest: examples/ops-warden/resource_manifest.yaml
- Subject manifest: examples/ops-warden/subject_manifest.yaml
- Service request fixtures: examples/ops-warden/check_request_*.json
## Local service command
flex-auth serve --addr 127.0.0.1:8080 --registry examples/ops-warden/registry_snapshot.json --policy examples/ops-warden/policy_package.md --log /tmp/flex-auth-ops-warden-decisions.jsonl
Ops-warden can point policy.flex_auth_url at that base URL for local smoke.
Production should keep policy.fail_closed true unless an explicit break-glass
procedure exists.
## Fixture coverage
Allow fixtures:
- fixture:ops-warden-adm-sign-allow
- fixture:ops-warden-agt-sign-allow
- fixture:ops-warden-atm-sign-allow
Deny fixtures:
- fixture:ops-warden-unknown-subject-deny
- fixture:ops-warden-actor-type-mismatch-deny
- fixture:ops-warden-ttl-above-max-deny
- fixture:ops-warden-disallowed-principal-deny
- fixture:ops-warden-missing-fingerprint-deny
## Non-secret smoke evidence
CLI validation on 2026-06-23:
- protected-system manifest: valid
- resource manifest: valid
- subject manifest: valid
- registry snapshot: loaded 1 system, 1 resource manifest, 3 subjects,
3 groups, 3 relationships, and 1 tenant
- policy package: valid with 8 passing fixtures
Local /v1/check service smoke on 2026-06-23:
- allow request: effect allow, reason signing_policy_matched,
decision id decision:706efe49f68d9ef1
- deny request: effect deny, reason ttl_out_of_bounds,
decision id decision:b69bdc25a988f367
- GET /v1/check: HTTP 405
- malformed POST /v1/check: HTTP 400
- decision log contained both decision ids
## Production sequence for ops-warden
1. Deploy the flex-auth registry and policy package above to the selected
flex-auth runtime.
2. Configure ops-warden policy.flex_auth_url to the flex-auth base URL.
3. Set policy.enabled: true.
4. Keep policy.tenant as tenant:platform unless a tenant-specific policy package
is introduced.
5. Run one allow-path sign smoke and confirm signatures.log includes
policy_decision_id.
6. Run one deny-path smoke with fail_closed true and preserve only non-secret
evidence.
## Ownership boundary
flex-auth owns the authorization decision for the signing request. ops-warden
continues to own actor inventory, SSH CA operation, OpenBao SSH engine
integration, host documentation, and signatures.log production evidence.
No SSH private keys, OpenBao tokens, database credentials, or real public-key
material are stored in these fixtures.
## FLEX-WP-0007 Production Update
Additional published assets:
- Production registry fixture: examples/ops-warden/production_registry_snapshot.json
- Registry sync runbook: docs/ops-warden-registry-sync.md
Production runtime command:
flex-auth serve --addr 0.0.0.0:8080 --registry examples/ops-warden/production_registry_snapshot.json --policy examples/ops-warden/policy_package.md --log /var/log/flex-auth/ops-warden-decisions.jsonl
Use http://flex-auth.flex-auth.svc.cluster.local:8080 when cluster DNS is
reachable from warden workstations. Otherwise use the approved operator tunnel
or ingress URL. Always pre-flight GET /healthz from the same workstation before
enabling policy.enabled with fail_closed true.
Production actor coverage now verifies agt-state-hub-bridge,
agt-codex-interhub-bootstrap, adm-example, atm-backup-daily, ttl_out_of_bounds,
unknown_actor_resource, and the iam:agt-state-hub-bridge subject path used by
WARDEN_POLICY_SUBJECT.

View File

@@ -0,0 +1,128 @@
# Ops-Warden Registry Sync
Date: 2026-06-23
Workplan: FLEX-WP-0007
This is the flex-auth side of the production policy gate runbook for ops-warden
SSH signing. ops-warden owns actor inventory and generated registry content;
flex-auth hosts that registry, evaluates the policy package, and returns the
decision envelope used by warden sign.
## Production Runtime Target
Use the NetKingdom operator-reachable service URL as the canonical
policy.flex_auth_url. The preferred target is an in-cluster flex-auth Service
fronted by the existing operator access path:
http://flex-auth.flex-auth.svc.cluster.local:8080
If cluster DNS is not reachable from the workstation that runs warden sign, use
an approved operator tunnel or ingress URL with the same base path semantics. Do
not turn on policy.enabled with fail_closed true until this pre-flight succeeds
from the same workstation:
curl -fsS <policy.flex_auth_url>/healthz
Start the runtime with the production registry snapshot and the ops-warden
policy package:
flex-auth serve --addr 0.0.0.0:8080 --registry examples/ops-warden/production_registry_snapshot.json --policy examples/ops-warden/policy_package.md --log /var/log/flex-auth/ops-warden-decisions.jsonl
The checked-in production snapshot is a non-secret fixture and initial load
target. Regenerate it from ops-warden inventory whenever actors, principals, or
TTL defaults change.
## Current Operator Tunnel
As of 2026-06-24, the reachable operator-tunnel URL for CoulombCore is:
http://127.0.0.1:18090
The tunnel name is flex-auth-coulombcore. It forwards CoulombCore
127.0.0.1:18090 to the local flex-auth runtime on 127.0.0.1:18090. Verified
checks from CoulombCore:
- GET /healthz returned HTTP 200.
- POST /v1/check for agt-state-hub-bridge returned allow with decision:873c6c682a52bebc.
This is an operator tunnel pattern, not a substitute for a future in-cluster
Service if flex-auth should run inside the cluster.
## Ownership Contract
| Concern | Owner | Notes |
| --- | --- | --- |
| Actor names and actor types | ops-warden | inventory.yaml defines adm, agt, and atm actors. |
| Default principals and TTLs | ops-warden | Used by warden sign and by generated registry attributes. |
| Registry hosting and reload | flex-auth | Runtime serves the generated snapshot and evaluates it with the policy package. |
| Policy package semantics | flex-auth | examples/ops-warden/policy_package.md owns allow and deny reasons. |
| OpenBao SSH signing | ops-warden | flex-auth never receives SSH private keys or Vault tokens. |
| Production policy.enabled flip | ops-warden operator | Only after healthz and allow/deny smoke pass. |
## Sync Procedure
1. In ops-warden, update the managed inventory source or ~/.config/warden/inventory.yaml.
2. Regenerate the flex-auth snapshot from ops-warden:
python scripts/build_flex_auth_registry.py ~/.config/warden/inventory.yaml -o registry/flex-auth/production_registry_snapshot.json
3. Validate the generated file before handoff:
flex-auth load-registry --file registry/flex-auth/production_registry_snapshot.json
4. Copy or promote the snapshot to the flex-auth runtime. For repo-level drift
coverage, update examples/ops-warden/production_registry_snapshot.json when
the intended production fixture changes.
5. Restart or reload the flex-auth runtime with the new snapshot.
6. From the workstation that runs warden sign, verify:
curl -fsS <policy.flex_auth_url>/healthz
7. Run one allow smoke and one deny smoke. Record only non-secret evidence:
actor name, decision id, effect, reason, backend, and whether a certificate
was issued.
## Current Production Fixture
The initial fixture mirrors ops-warden production inventory as of 2026-06-23.
It registers:
| Actor | Type | Principal | Max TTL hours | Allowed subjects |
| --- | --- | --- | --- | --- |
| adm-example | adm | adm-full | 48 | adm-example, iam:adm-example |
| agt-codex-interhub-bootstrap | agt | agt-interhub-bootstrap | 2 | agt-codex-interhub-bootstrap, iam:agt-codex-interhub-bootstrap |
| agt-state-hub-bridge | agt | agt-task-bridge | 24 | agt-state-hub-bridge, iam:agt-state-hub-bridge |
| atm-backup-daily | atm | atm-backup-daily | 8 | atm-backup-daily, iam:atm-backup-daily |
The IAM subject form is intended for WARDEN_POLICY_SUBJECT. If that environment
variable is unset, ops-warden sends the actor name and the same policy path
continues to work.
## Smoke Expectations
Allow path:
warden sign agt-state-hub-bridge
Expected non-secret evidence: decision effect allow, reason
signing_policy_matched, signatures.log includes policy_decision_id.
Deny path:
warden sign agt-state-hub-bridge --ttl 999
Expected non-secret evidence: effect deny, reason ttl_out_of_bounds, no
certificate issued. With fail_closed true, unreachable flex-auth must also block
signing.
OpenBao-backed signing remains an operator smoke because it requires a scoped
VAULT_TOKEN. The previous session returned HTTP 403 on 2026-06-23; retry with:
SMOKE_VAULT=1 ~/ops-warden/scripts/policy_gate_production_smoke.sh
## References
- docs/ops-warden-policy-gate-handoff.md
- examples/ops-warden/production_registry_snapshot.json
- ~/ops-warden/wiki/PolicyGatedSigning.md
- ~/ops-warden/history/2026-06-23-flex-auth-policy-gate-production-smoke.md

View File

@@ -0,0 +1,142 @@
# flex-auth Pre-Implementation Assessment
Date: 2026-05-15
Author: Claude (Opus 4.7) with Bernd
Status: Accepted — feeds FLEX-WP-0005 Foundations and the refresh of FLEX-WP-0002/0004
## Purpose
Captures the SWOT and boundary review performed before flex-auth code work
begins. The conclusions of this assessment are turned into three ADRs
(`docs/adr/0001`-`0003`) and a new foundations workplan
(`workplans/FLEX-WP-0005`), which together precede the standalone core
(`FLEX-WP-0002`).
## Overall Verdict
The repository is planning-mature and code-blank. INTENT, SCOPE, the PRD,
the authorization-landscape research note, and the four initial workplans
are internally consistent and mirrored in the State Hub. `FLEX-WP-0001` is
done.
Starting implementation directly at `FLEX-WP-0002 P2.1` is reasonable in
shape but premature in detail: several decisions the workplans leave open
would be made implicitly by the first commit. Those decisions are pulled
forward into `FLEX-WP-0005` so the standalone core lands on settled
foundations.
## SWOT
### Strengths
- Ownership boundary stated and repeated consistently:
- **key-cape / NetKingdom SSO** owns identity.
- **flex-auth** owns authorization.
- **protected systems** own enforcement.
- Backend-neutral vocabulary commitment: Topaz, OpenFGA, SpiceDB, OPA,
Cedar, and Keycloak Authorization Services are framed as adapters, not
the product.
- Concrete first consumer (Markitect) with its side already in flight
(`MKTT-WP-0014`).
- Standalone-first mode keeps flex-auth useful before any enterprise PDP
is wired in.
- Hard problems named, not papered over: group overage, directory
freshness, fail-open vs fail-closed, stale/partial/uncertain decisions,
explain APIs, audit-only versus deny.
- State Hub integration in place from day one (workstream and task IDs
in workplan frontmatter, custodian brief committed, dispatch active).
### Weaknesses
- No implementation language or repo skeleton committed.
- Policy package format is described as "a simple declarative rule format
with room for OPA/Rego, Cedar, and Topaz later" — central artefact, but
unpinned.
- No ADRs. `net-kingdom/DECISIONS.md` and the key-cape spec habit are not
mirrored here yet.
- The `FlexAuthResourceManifest` is referenced as already implemented on
the Markitect side without being pinned in this repo. Cross-repo
contract drift risk.
- NetKingdom IAM Profile
(`~/the-custodian/canon/standards/iam-profile_v0.1.md`) is only cited
at the bottom of the research note — it is the upstream identity
contract flex-auth consumes and deserves first-class citation.
- No project skeleton, Makefile, lint, CI, or SBOM yet.
- Emergency principal / break-glass listed as a first-class subject type
with no mechanics described.
### Opportunities
- Markitect is aligned and waiting. Tight feedback loop available.
- `explain(decision_id)` is a real differentiator versus Topaz, Cerbos,
and OPA in isolation. Literate, reviewable policy packages amplify the
same lever.
- CLI-first standalone mode can ship usefully across NetKingdom repos
early, before service mode lands.
- Register flex-auth as a State Hub capability with extension points so
Markitect and later consumers discover it natively.
### Threats / Risks
- **Thin-wrapper-around-Topaz trap.** Topaz already combines OPA/Rego,
local directory, and relations. If the standalone core reimplements
60% of Topaz badly and then adapts to Topaz anyway, the abstraction
earns nothing. The escape is to make the *registry + audit + explain +
multi-consumer* surface the actual product, and to align the
standalone evaluator with Rego from day one so the later Topaz
adapter is a small step.
- Markitect-side manifest exists; flex-auth has not pinned it. Easy to
lock in the wrong shape.
- Schedule coupling: `FLEX-WP-0003` is blocked on `0002`. Every week of
core slippage is a week Markitect waits.
- "Yet another authz layer" perception if a bespoke rules format ships
before the Topaz/Rego direction is recorded.
## Boundary Review
| Repo | Owns | Overlap with flex-auth | Verdict |
| --- | --- | --- | --- |
| `key-cape` | OIDC/PKCE, MFA, token lifecycle, NetKingdom IAM Profile impl, coarse roles and scopes | flex-auth consumes verified claims as inputs; coarse roles live in key-cape, resource-specific decisions in flex-auth | Clean. IAM Profile citation made explicit. |
| `net-kingdom` | Security core, Keycloak (heavy mode), IAM Profile spec, canon | Keycloak Authorization Services is itself a PDP. flex-auth's research recommends "Keycloak as SSO only, flex-auth owns authorization" as the canonical pattern, with Keycloak AuthZ available as one adapter. | Pinned in ADR-003 / FLEX-WP-0004. Not a boundary problem, a recorded decision. |
| `ops-bridge` | SSH reverse tunnels, connectivity | Disclaims being a credential authority or policy engine. | No overlap. |
| `ops-warden` | SSH cert CA for `adm`/`agt`/`atm` actors; short-lived SSH certificates | Different identity universe (SSH actors, not OIDC subjects). An `agt` authenticated via warden SSH cert may later appear as a flex-auth subject in some flow, but the two surfaces do not collide. | No overlap. Boundary line added to SCOPE.md. |
## Refinements Adopted
1. **ADR-001** — Implementation language & repo skeleton: Go, aligned with
key-cape's vindicated language decision.
2. **ADR-002** — Policy-package format: Rego-in-Markdown, from day one.
Literate policy packages co-locate intent, rules, and tests.
3. **ADR-003** — MVP backend alignment: shape the standalone core to be
Rego/Topaz-aligned so the later Topaz adapter is a small step.
4. **FLEX-WP-0005 Foundations** is inserted between `0001` (done) and
`0002` (core). It performs the Topaz spike *before* the core's policy
loader and check API are written, pins the resource manifest schema,
and lands the repo skeleton.
5. **INTENT/SCOPE** cite the NetKingdom IAM Profile explicitly and record
the ops-warden boundary.
## Sequencing After Refinement
```text
FLEX-WP-0001 done Repo intent and authorization-landscape baseline
FLEX-WP-0005 todo P0 Foundations and Topaz alignment (ADRs, skeleton,
spike, manifest pinning)
FLEX-WP-0002 blocked Standalone policy-as-code core, Rego-in-Markdown
FLEX-WP-0003 blocked Markitect consumer integration
FLEX-WP-0004 blocked Delegated PDP and directory adapters (Topaz
evaluation now in 0005)
```
## Traceability
- `INTENT.md`, `SCOPE.md`, `README.md`, `.custodian-brief.md`
- `docs/ProductRequirementsDocument.md`
- `docs/flex-auth-authorization-registry-research.md`
- `docs/workplan-planning-map.md`
- `docs/adr/0001-implementation-language-and-skeleton.md` *(new)*
- `docs/adr/0002-rego-in-markdown-policy-format.md` *(new)*
- `docs/adr/0003-topaz-aligned-mvp.md` *(new)*
- `workplans/FLEX-WP-0001-…`, `0002-…`, `0003-…`, `0004-…`
- `workplans/FLEX-WP-0005-foundations-and-topaz-alignment.md` *(new)*
- NetKingdom IAM Profile: `~/the-custodian/canon/standards/iam-profile_v0.1.md`

View File

@@ -0,0 +1,105 @@
# Relationship PDP Adapter Boundary
Status: implemented for FLEX-WP-0004 P4.2.
## Role
The relationship PDP adapter is the common boundary for OpenFGA,
SpiceDB, and other tuple-oriented authorization systems. These backends
answer questions like:
```text
is subject S related to object O through relation R?
```
flex-auth keeps the protected-system API stable. The adapter translates
`CheckRequest`, `BatchCheckRequest`, registry relationships, and
`list_allowed` queries into tuple checks, then wraps backend responses
back into the canonical `DecisionEnvelope`.
## Tuple Mapping
Canonical flex-auth relationship facts map to:
| flex-auth | tuple field |
| --- | --- |
| resource type | `object_type` |
| resource id | `object_id` |
| action or relation | `relation` |
| subject type | `subject_type` |
| subject id | `subject_id` |
| group membership indirection | `subject_relation=member` |
Registry group and team membership become `group#member` tuples.
Resource parent edges become `parent` tuples. Resource owners become
`owner_team` tuples. Relationship facts keep conditions, provenance,
metadata, and CARING descriptors on the imported tuple so backend
results can preserve explanatory context.
## Inheritance
Relationship backends represent inheritance differently:
- OpenFGA usually models it in the authorization model through rewrites.
- SpiceDB usually models it through relation definitions and caveats.
- flex-auth records inherited evidence in
`TupleCheckResult.InheritedFrom`.
The envelope does not expose backend-native rewrite syntax. It records
the fact that inheritance participated through diagnostics and preserves
the matched CARING descriptor from direct or inherited tuples.
## Batch And List
`BatchCheck` preserves request order. If the backend returns a partial
batch, flex-auth emits fail-closed deny envelopes for the affected
resources.
`ListAllowed` returns a `ListAllowedResult` containing allow envelopes,
the backend consistency token, and diagnostics. It intentionally returns
envelopes instead of raw resource ids so downstream consumers keep the
same audit and CARING metadata they receive from single checks.
## Consistency Metadata
Tuple backends expose different consistency tokens:
- OpenFGA: model id plus optional tuple-store continuation/freshness
metadata.
- SpiceDB: zedtoken.
The adapter stores the backend token in
`DecisionEnvelope.provenance.directory_etag`. The field name is kept for
compatibility with the existing flex-auth envelope; for relationship PDPs
it means "relationship backend consistency token".
## Failure Behavior
The adapter fails closed for:
- backend unavailable: `relationship_backend_unavailable`
- stale consistency token: `relationship_data_stale`
- partial backend result: `relationship_partial_result`
- untranslatable request: `relationship_request_incomplete`
Each failure is a deny envelope with `diagnostics.relationship_failure`
and a CARING conformance finding. This keeps delegated tuple behavior
aligned with standalone fail-closed behavior.
## CARING Preservation
Tuple systems do not understand CARING directly. flex-auth therefore
keeps CARING metadata at the adapter boundary:
- request descriptor wins when supplied;
- backend result descriptor is next;
- matched tuple descriptor is next;
- inherited tuple descriptor is next;
- otherwise the envelope includes
`RELATIONSHIP-CARING-DESCRIPTOR-MISSING`.
The adapter copies scope, planes, capabilities, exposure modes,
restrictions, derived capabilities, conformance findings, and
exposure-event hooks into the decision envelope. Backend-native role or
relation names should not leak into protected systems as a replacement
for CARING canonical roles.

View File

@@ -0,0 +1,103 @@
# Rule PDP Adapter Boundary
Status: implemented for FLEX-WP-0004 P4.3.
## Role
The rule PDP adapter is the common boundary for OPA/Rego, Cedar-style
policy services, and other engines that evaluate a policy language over
a structured request. It is separate from the relationship-PDP boundary:
relationship backends answer tuple reachability questions, while rule
backends evaluate policy logic over subject, action, resource, context,
and CARING metadata.
## Canonical Input
All rule backends receive the same canonical input shape:
```text
input.subject
input.action
input.resource
input.context
input.caring_context
input.policy.package
input.policy.version
```
OPA/Rego can consume this shape directly. Cedar adapters translate the
same fields into principal/action/resource/context entities at the
backend boundary. Protected systems do not see backend-native input
syntax.
## Policy Artifacts
`PolicyArtifactFromPackage` converts a validated Rego-in-Markdown
package into a delegated artifact:
- `language=rego`
- package id and version from frontmatter
- extracted Rego module unchanged
- test blocks and fixtures preserved
- CARING policy metadata preserved
Cedar and other rule engines use the same `PolicyArtifact` envelope,
but may reject unsupported artifacts with `rule_policy_unsupported`.
## Fixtures
`EvaluateFixtures` runs `api.PolicyFixture` values through the delegated
adapter and compares the returned effect, reason, and obligations. This
keeps delegated backends honest against the same fixtures used by the
standalone evaluator.
## Obligations And Diagnostics
Rule backends can return obligations such as masking, audit, or approval
requirements. The adapter copies them into the canonical
`DecisionEnvelope`. Backend diagnostics are preserved and supplemented
with:
- `adapter=rule`
- backend name
- delegated mode
- language
- policy package and version
- fail-closed reason when present
## Versioning
The envelope records backend policy version in
`matched_policy_version` and `provenance.policy_version`. A backend may
return a newer concrete revision than the request asked for; the adapter
records what actually matched.
## Failure Behavior
The adapter fails closed for:
- backend unavailable: `rule_backend_unavailable`
- stale policy: `rule_policy_stale`
- partial result: `rule_partial_result`
- invalid input: `rule_request_incomplete`
- unsupported policy artifact: `rule_policy_unsupported`
Each failure returns a deny envelope with `diagnostics.rule_failure` and
a CARING conformance finding.
## CARING Preservation
Rule engines vary in how much of CARING they can represent natively.
flex-auth keeps CARING outside the backend-specific language contract:
- request descriptor wins;
- backend result descriptor is next;
- policy frontmatter supplies profile and expected dimensions;
- gaps become `RULE-CARING-METADATA-GAP` or
`RULE-CARING-DESCRIPTOR-MISSING` findings.
The decision envelope preserves descriptor, scope, planes,
capabilities, exposure modes, restrictions, derived capabilities,
conformance findings, exposure-event hooks, obligations, and diagnostics.
Backend-native policy names should never replace canonical CARING roles
in protected-system responses.

View File

@@ -0,0 +1,107 @@
# Topaz Adapter Operations
Status: implemented for FLEX-WP-0004 P4.1.
## Role
The Topaz adapter is a delegated PDP and directory adapter behind the
stable flex-auth API. Protected systems still send `CheckRequest`,
`BatchCheckRequest`, registry snapshots, and Rego-in-Markdown policy
packages to flex-auth. The adapter translates those into Topaz directory
objects, relations, permission checks, and an OPA bundle, then wraps the
result back into the same `DecisionEnvelope` used by standalone mode.
## Wire Protocol
The production recommendation remains gRPC because it is Topaz's native
API and gives the strongest typed client surface for authorizer,
reader, writer, and model operations. The implementation added in P4.1
keeps that choice behind `internal/adapters/topaz.Client` and ships an
HTTP REST client for the runnable `examples/topaz` topology.
This split is deliberate:
- `Client` is the stable flex-auth boundary.
- `HTTPClient` speaks the same REST endpoints used by the spike
(`/api/v3/directory/check`, `/object`, `/relation`, `/manifest`).
- A future gRPC client can replace `HTTPClient` without changing
protected-system contracts or CARING envelope behavior.
- Embedded Topaz remains out of scope because it would couple
flex-auth releases to Topaz internals.
## Startup
1. Start Topaz with the manifest and bundle paths mounted. The
`examples/topaz/docker-compose.yml` file is the local reference.
2. Create a `topaz.HTTPClient` with the directory REST gateway URL.
3. Configure a `topaz.FileBundleSink` that points at the mounted bundle
directory.
4. Build a `topaz.Adapter` with the client and policy metadata.
5. Call `ImportManifest`, `ImportDirectory`, and `ImportPolicy` before
accepting delegated checks.
For local verification:
```sh
cd examples/topaz
docker compose up --abort-on-container-exit --exit-code-from probe
```
The Go integration test is present but skipped by default. Run it with:
```sh
FLEX_AUTH_RUN_TOPAZ_INTEGRATION=1 go test ./internal/adapters/topaz
```
## Directory Consistency
`ImportDirectory` converts the canonical registry snapshot into Topaz
objects and relations. Subjects and service accounts become Topaz
`user` objects. Each subject also receives an `identity:<subject>` object
with an `identifier` relation to the user. Groups and teams become Topaz
`group` objects; teams keep the `team:` prefix. Resources keep their
canonical type names, labels, trust zone, path, owner, and system in
properties.
Relation writes return an optional etag. The adapter records the latest
etag in `DecisionEnvelope.provenance.directory_etag` when Topaz returns
one. Reads may be served from flex-auth's local registry for explanation
or from Topaz for authorization. The decision envelope must say which
backend produced the answer through `provenance.evaluator` and
`provenance.mode`.
## Policy Import
`ImportPolicy` extracts the validated Rego module from a
Rego-in-Markdown package without translation. `FileBundleSink` writes:
- `.manifest` with the package root.
- `policy/<package/path>.rego` with the exact extracted module.
This matches the local-bundle mode used by the Topaz example. Clustered
Topaz deployments can replace the bundle sink with a remote bundle
publisher without changing the adapter contract.
## Fail-Closed Defaults
Delegated checks do not leak backend errors to protected systems as
ambiguous success. The adapter returns a deny envelope for:
- Topaz unavailable: `reason=topaz_unavailable`.
- Stale directory: `reason=topaz_directory_stale`.
- Partial result: `reason=topaz_partial_result`.
- Untranslatable request: `reason=topaz_request_incomplete`.
Each failure includes a CARING conformance finding and
`diagnostics.topaz_failure`. This keeps delegated mode behavior
compatible with standalone fail-closed decisions and makes backend
health visible to audits.
## CARING Preservation
The adapter preserves CARING descriptors from the request or backend
result. It copies descriptor, restrictions, exposure modes, derived
capabilities, conformance findings, and exposure-event hooks into the
decision envelope. If no descriptor is available, the decision still
contains a `TOPAZ-CARING-DESCRIPTOR-MISSING` warning so conformance
checks can distinguish an authorization deny from a metadata gap.

306
docs/topaz-mapping-spike.md Normal file
View File

@@ -0,0 +1,306 @@
# Topaz Alignment Spike
Date: 2026-05-16
Status: Spike output for FLEX-WP-0005 P5.4. Feeds FLEX-WP-0002 P2.1
(schemas) and FLEX-WP-0004 T01 (Topaz adapter).
Author: claude with Bernd
## Goal
Validate ADR-003's commitment to shape the flex-auth standalone core so
that the Topaz adapter is a small step. Concretely: confirm that
flex-auth's resource, subject, group, team, and relationship vocabulary
can map cleanly onto Topaz directory objects and relations, and that
flex-auth's Rego-in-Markdown policy packages decompose to Topaz policy
modules without translation.
## Topaz In Two Paragraphs
Topaz bundles three things behind one process: an OPA Rego evaluator,
an embedded Zanzibar-style directory ("EDS" — Edge Authorization
Database), and a gRPC/HTTP API that ties them together. The directory
stores objects (typed entities) and relations (typed edges between
objects). A `manifest` declares the object types, relation types, and
permission expressions. Policy modules in Rego consult both the request
input *and* directory data (via builtins like `ds.check`,
`ds.object`, `ds.relation`) to produce a decision.
Topaz exposes four service surfaces: **reader** (read directory),
**writer** (mutate directory), **model** (manage manifest), and
**authorizer** (evaluate policy). The `authorizer.Is` and
`authorizer.DecisionTree` RPCs are the primary integration points for a
policy decision point.
## Vocabulary Map
flex-auth's canonical vocabulary maps onto Topaz as follows.
| flex-auth concept | Topaz representation | Notes |
| --- | --- | --- |
| Subject (human) | `object{type:"user", id:<oidc subject>}` | `display_name` from `preferred_username` or claims |
| Subject (service account) | `object{type:"user", id:<sub>}` with `properties.principal_type:"service"` | Same type; principal_type carried in properties |
| Group | `object{type:"group", id:<group slug>}` | Members via `member` relation |
| Team | `object{type:"group", id:"team:<slug>"}` | Modeled as a kind of group; slug-prefixed for clarity |
| Tenant | `object{type:"tenant", id:<slug>}` | Resources reference tenant via `tenant` relation |
| Group membership | `relation{subject:user, name:"member", object:group}` | Recursive group→group also supported |
| Resource (Markitect knowledge_base / document / …) | `object{type:<flex_auth_type>, id:<resource id>}` | Type names match `FlexAuthResource.type` exactly |
| Resource parent | `relation{subject:child, name:"parent", object:parent}` | Walks inheritance |
| Resource owner team | `relation{subject:group, name:"owner_team", object:resource}` | Group-on-resource |
| Resource labels | `object.properties.labels: [...]` | Carried as JSON properties; consulted by Rego |
| Resource trust_zone | `object.properties.trust_zone: <str>` | Same |
| Action | Topaz permission name | `read`, `query`, `search`, `package`, `export`, `workflow_run`, `admin` |
| Relationship fact (subject↔resource) | `relation{subject, name:<relation>, object:resource}` | e.g. `reader`, `editor`, `steward` |
| Policy package (Rego-in-Markdown) | One Rego module per package | Extracted from the Markdown by flex-auth's loader (ADR-002) |
| Decision envelope | Topaz `authorizer.Is.Response` + Rego decision object | Topaz returns booleans per decision name; the Rego program returns the richer envelope and flex-auth wraps |
| Policy version | `metadata.version` in the package frontmatter | Threaded as `input.policy.version` for Topaz; recorded in audit |
| Provenance | `metadata` on Topaz objects/relations and on the decision | Topaz exposes `etag` for relations; flex-auth records both sides |
### Why these specific choices
- **Subject and service-account share `user` type.** Topaz's
authorizer.Is treats `user` as the canonical identity subject. The
`principal_type` distinction lives in `properties` so Rego rules
can branch on it, but the directory shape stays uniform.
- **Team modeled as group.** Markitect (and most consumers) treat
teams as groups for authorization purposes. A separate `team` type
is possible but adds an indirection. The `team:` id-prefix keeps
semantics legible without forking the type.
- **Labels and trust_zone in `properties` rather than as separate
objects.** They are attributes of the resource, not first-class
edges. Putting them in properties keeps the directory small and
policies legible.
- **Owner is a relation, not a property.** A team owns multiple
resources; the inverse query "what does team T own?" is a relation
walk, not a property scan.
## Rego Module Shape
A flex-auth Rego-in-Markdown package (ADR-002) extracts to a single
Rego module. The same module runs in both flex-auth's embedded
evaluator and in Topaz, because Topaz exposes the same `data.<package>`
namespace and supports the same `ds.*` builtins flex-auth's adapter
will mock when running standalone.
### Standalone-mode contract
When evaluated by the flex-auth standalone evaluator
(`internal/policy`), the module sees:
```text
input.subject # normalized subject (id, groups, roles, scopes, principal_type, assurance)
input.action # action name
input.resource # normalized resource (id, type, labels, trust_zone, owner, parent, …)
input.context # request/environment/assurance/workflow attributes
data.policy # registered package metadata table
data.directory # standalone registry contents, exposed under the same shape ds.* would return
```
The `ds.*` builtins are **shimmed locally** in standalone mode:
flex-auth provides `ds.check`, `ds.object`, and `ds.relation`
implementations that consult `internal/registry` instead of Topaz.
The shim is a Go-side `rego.Function` registration.
### Delegated-mode contract
When evaluated by Topaz, the same module runs unchanged. `ds.*` calls
hit Topaz's directory. The decision envelope returned to the protected
system is identical because flex-auth's adapter wraps Topaz's response
into the same `decision_envelope.schema.json` shape (FLEX-WP-0002
P2.1).
### Decision object shape
The Rego package's `decision` rule produces:
```rego
decision := {
"effect": "allow" | "deny" | "redact" | "audit_only" | "not_applicable",
"reason": "<rule id>",
"obligations": [...], # optional, per ADR-002
"diagnostics": {...}, # optional, free-form
}
```
flex-auth's adapter (standalone *or* Topaz) wraps this into the full
decision envelope by adding subject metadata, resource metadata,
matched policy version, matched rule, and provenance.
## Wire-Protocol Candidates
Ranked from primary to fallback:
1. **gRPC (Topaz's native API)** — primary. Type-safe, generated Go
clients (`github.com/aserto-dev/topaz/pkg/cli` exposes the protos),
low overhead. Authorizer.Is, DecisionTree, Reader/Writer/Model.
2. **HTTP/REST gateway** — fallback for environments where gRPC is
awkward (e.g. browser tooling, debugging). Topaz exposes a REST
gateway on a sibling port; same RPCs, JSON-encoded.
3. **Embedded library***not* recommended. Topaz's directory is
tightly coupled to its process model and bolt/postgres storage;
embedding it ties flex-auth's release cycle to Topaz's and erodes
the adapter boundary. Reserved as a future optimization if a
single-process deployment is ever required.
Adapter implementation under `internal/adapters/topaz/` consumes the
gRPC client. The HTTP gateway is documented but not implemented in
FLEX-WP-0004 T01.
## Adapter Surface (Preview For FLEX-WP-0004 T01)
```go
package topaz
type Adapter interface {
// Check evaluates a single decision against Topaz.
Check(ctx context.Context, req CheckRequest) (DecisionEnvelope, error)
// BatchCheck evaluates multiple resources for one subject+action.
BatchCheck(ctx context.Context, req BatchCheckRequest) ([]DecisionEnvelope, error)
// ImportDirectory writes flex-auth registry data into Topaz.
ImportDirectory(ctx context.Context, snapshot DirectorySnapshot) error
// ImportPolicy installs a bundle (Rego modules extracted from
// policy packages) into Topaz's OPA.
ImportPolicy(ctx context.Context, bundle PolicyBundle) error
}
```
`DirectorySnapshot` and `PolicyBundle` are the canonical flex-auth
types from `pkg/api/`; the adapter translates them into Topaz's
proto types at the boundary.
## Recommendation: Schema Restatement, Not Embedding
flex-auth should **restate** its directory model in its own canonical
schemas (FLEX-WP-0002 P2.1), not embed Topaz's directory proto.
Reasoning:
- **The map is small.** flex-auth's vocabulary is six object kinds
(user, group, tenant, resource, …) and a handful of relation
conventions. The translation cost is tiny.
- **Vocabulary divergence is likely.** flex-auth wants to record
things Topaz's directory does not natively model — e.g. policy
package version on a decision, IAM Profile assurance bundles,
decision-time provenance. Embedding would force those into
Topaz-shaped properties bags.
- **Multi-backend goal.** OpenFGA and SpiceDB are next on the adapter
list; both have similar-but-not-identical models. flex-auth's
canonical schemas need to be the lingua franca, not Topaz's.
- **Stable contract for Markitect.** Markitect already emits
`FlexAuthResourceManifest`; that is the canonical input. Topaz is a
consumer of the translation, not the other way around.
Adopt Topaz's manifest *style* (object types + relation types +
permission expressions) for the standalone evaluator's directory
shim, but keep flex-auth's own schema files as the source of truth.
## Runnable Example
`examples/topaz/` ships:
- `manifest.yaml` — Topaz v3 directory manifest declaring `user`,
`group`, `tenant`, `knowledge_base`, `document` object types and
relations `member`, `parent`, `owner_team`, `reader`, `steward`,
plus the permission expressions for `read`, `query`, `search`,
`export`, `admin`.
- `policy/markitect.documents.rego` — Rego module matching the
Rego-in-Markdown example in ADR-002 (internal document read gated
on `reader` relation or `steward` role). This module shows the
*flex-auth-shaped* input contract — bridging to Topaz's raw input
is adapter scope (see Implementation Notes below).
- `data/objects.json`, `data/relations.json` — seed directory data
derived from `examples/markitect/resource_manifest.yaml` plus a
small subject/group set.
- `cfg/config.yaml` — Topaz v2 config (BoltDB at `/db`, TLS auto-
managed in `/certs`, plaintext HTTP gateways for spike convenience,
local bundle from `/bundle`).
- `docker-compose.yml` — Topaz service + a one-shot `seed` container
(pushes the manifest, objects, relations via the directory REST
gateway) + a one-shot `probe` container that calls the directory
`check` API for three scenarios.
- `README.md` — usage.
**Verification status: green (2026-05-16).** `docker compose up
--abort-on-container-exit` produced all three expected outcomes:
| Scenario | Subject | Resource | Permission | Expected | Actual |
| --- | --- | --- | --- | --- | --- |
| steward allow | alice (steward) | document:internal-note | read | true | true |
| reader allow | bob (member of reader:platform-architecture) | document:internal-note | read | true | true |
| outsider deny | eve (no relation) | document:internal-note | read | false | false |
## Implementation Notes Surfaced By The Spike
1. **Rego input contract bridging is adapter scope.** Topaz's
authorizer delivers a fixed input shape
(`input.user`, `input.identity`, `input.policy`, `input.resource`)
that comes from its identity-resolution flow. flex-auth's
policy packages use the canonical input shape from ADR-002
(`input.subject`, `input.action`, `input.resource`, `input.context`).
The Topaz adapter (FLEX-WP-0004 T01) is responsible for the wrapper
that translates between the two. The standalone evaluator (P2.4)
produces the canonical shape directly. **Implication for P2.1:**
the canonical input shape must be locked early so both code paths
target it.
2. **Identity objects are Topaz-canonical, not flex-auth-canonical.**
Topaz expects an `identity` object type with an `identifier`
relation to a `user` object when `identity_context.type ==
IDENTITY_TYPE_SUB`. flex-auth's subject model is flat (a Subject
*is* the principal). The adapter materializes Topaz `identity`
objects from the flex-auth subject registry at directory-import
time, mapping subject id → identity → user. The standalone path
doesn't need this indirection.
3. **Permission resolution from the directory alone is sufficient
for the common case.** The probe demonstrates that the Topaz
manifest's permission expressions (`reader | steward | parent->read`
etc.) resolve correctly with only directory data. Rego is only
needed when policy decisions consume request context (assurance
claims, time-of-day, trust_zone interaction) that the directory
can't express. **Implication for P2.3/P2.4:** the policy package
loader and check API should accept "permission-only" packages
(no Rego rules, just a permission expression referencing the
directory manifest) as a first-class case alongside Rego packages.
4. **The Topaz config schema is not stable across versions.** Keys
moved between `directory:` and `directory_service:`; relation
rewrites use `->` only in `permissions:` not `relations:`;
`disable_tls` is not honored as a server flag (use cert paths in
a writable volume). The FLEX-WP-0004 T01 adapter implementation
should pin a tested Topaz version and bump it deliberately.
## Implications for FLEX-WP-0002
- **P2.1 schemas** — define `directory_snapshot.schema.json` with the
vocabulary above. Decision envelope unchanged from ADR-002 sketch.
- **P2.2 registry** — store objects+relations in the same canonical
shape; the in-memory representation is what gets translated to
Topaz tuples at adapter time.
- **P2.3 policy loader** — Markdown extractor produces *one* Rego
module per package, identical for standalone and Topaz delivery.
Standalone evaluator registers `ds.*` shim builtins.
- **P2.4 check API** — `Check` returns the canonical envelope; the
same code path serves standalone evaluation and Topaz-delegated
evaluation; only the evaluator and `ds.*` resolution differ.
## Implications for FLEX-WP-0004
- **T01 Topaz adapter** — implement against the surface preview
above; consume the proto client from `github.com/aserto-dev/topaz`.
No new schema work; adapter is a translator and request/response
shaper.
- **T02 relationship PDP boundary** (OpenFGA, SpiceDB) — reuse the
same `DirectorySnapshot` and decision envelope; only the directory
protocol differs. The Topaz mapping above is reusable with small
per-backend deltas.
## Open Items (Not Blocking P2.1)
- **Bundle distribution.** Topaz can load Rego from a remote bundle
service (`opa.bundles`). Decision: load via local bundle path in
the standalone deployment; switch to bundle service when running
Topaz in clustered mode. Recorded for FLEX-WP-0004 T01.
- **Consistency tokens.** Topaz's relation writes return an `etag`.
Decision envelopes should carry it for read-your-writes guarantees;
envelope field `provenance.directory_etag` is reserved for this.
- **Group overage / freshness.** Out of scope for this spike; handled
by directory resolver adapters in FLEX-WP-0004 T05.

View File

@@ -1,10 +1,10 @@
# Flex-Auth Workplan Planning Map
Date: 2026-05-04
Date: 2026-06-23
## Purpose
This document captures the initial sequencing view for flex-auth workplans.
This document captures the current sequencing view for flex-auth workplans.
## Priority Scale
@@ -20,27 +20,66 @@ This document captures the initial sequencing view for flex-auth workplans.
| Workplan | Priority | Status | Depends On | Current View |
| --- | --- | --- | --- | --- |
| `FLEX-WP-0001` | complete | done | none | Repo intent, boundaries, and authorization landscape research are complete. |
| `FLEX-WP-0002` | P0 | todo | `FLEX-WP-0001` | Standalone policy-as-code core: schemas, local registry, policy packages, check APIs, explanations, decision log, CLI/service skeleton, tests. |
| `FLEX-WP-0003` | P1 | todo | `FLEX-WP-0002` | Markitect consumer integration: resource namespace, manifest import, action vocabulary, decision fixtures, integration docs. |
| `FLEX-WP-0004` | P2 | todo | `FLEX-WP-0002` | Delegated PDP and directory adapters: Topaz, OpenFGA/SpiceDB, OPA/Cedar, Keycloak Authorization Services, Entra/Graph/SCIM. |
| `FLEX-WP-0005` | complete | done | `FLEX-WP-0001` | Foundations and Topaz alignment are complete: ADR-001/002/003, Go skeleton, `FlexAuthResourceManifest` schema pin, Topaz mapping spike, IAM Profile citation, ops-warden boundary clarification. |
| `FLEX-WP-0002` | complete | completed | `FLEX-WP-0001`, `FLEX-WP-0005` | Standalone policy-as-code core is complete: schemas, local registry, CARING profile/descriptors, Rego-in-Markdown policy packages, check APIs, explanations, decision log, CLI/service skeleton, tests. |
| `FLEX-WP-0003` | complete | completed | `FLEX-WP-0002` | Markitect consumer integration and first CARING benchmark are complete: resource namespace, manifest import, action vocabulary, descriptor fixtures, decision fixtures, integration docs. |
| `FLEX-WP-0004` | complete | completed | `FLEX-WP-0002`, `FLEX-WP-0005` | Delegated PDP and directory adapter boundary work is complete: Topaz adapter shape, OpenFGA/SpiceDB, OPA/Cedar, Keycloak Authorization Services, Entra/Graph/SCIM, CARING envelope preservation. |
| `FLEX-WP-0006` | complete | finished | `FLEX-WP-0002`, `FLEX-WP-0005` | Ops-warden unblocker is complete: flex-auth publishes `ssh-certificate` / `sign` policies, fixtures, and `/v1/check` smoke evidence for the opt-in pre-sign gate shipped in ops-warden `WARDEN-WP-0007` and tracked for production in `WARDEN-WP-0009`. |
| `FLEX-WP-0007` | `P0` | blocked | `FLEX-WP-0006` | Repo-side production registry fixture, sync contract, runtime command, healthz coverage, and real actor/IAM tests are implemented. Operator deployment and OpenBao smoke remain blocked on reachable runtime selection and scoped VAULT_TOKEN refresh. |
## Dependency Notes
`FLEX-WP-0002` should come first because the protected-system-facing API must
be stable before flex-auth delegates decisions to external engines.
`FLEX-WP-0005` is inserted between `0001` and `0002` per the
pre-implementation assessment in `docs/pre-implementation-assessment.md`.
It pulls forward the decisions the original `0002` left implicit (language,
policy format, evaluator alignment) and runs the Topaz mapping spike
before the core's schemas and check API are written.
`FLEX-WP-0003` follows the core and uses Markitect as the first concrete
consumer. Markitect has already completed its side of the initial contract in
`MKTT-WP-0014`, but flex-auth must still implement the service-side registry
and decision behavior.
`docs/caring-architecture-blueprint.md` adds the 2026-05-17 CARING
refinement: CARING remains the semantic standard, while flex-auth becomes
the practical reference implementation for descriptors, conformance
findings, decision metadata, explain output, and exposure-event audit
records. This refinement changes the shape of `FLEX-WP-0002` but does not
add a new predecessor workplan.
`FLEX-WP-0004` should wait for the standalone core so delegated engines do not
define the whole architecture accidentally.
`FLEX-WP-0002` comes after `0005` so the standalone evaluator embeds the
OPA Rego library and produces decision envelopes shaped to match the
delegated-mode envelopes added later. It now also pins the executable
CARING profile in the same schema slice.
`FLEX-WP-0003` follows the core. Markitect has already completed its
side of the contract in `MKTT-WP-0014`; flex-auth pins the manifest in
`FLEX-WP-0005 T03` and implements the service-side registry and decision
behavior in `0003`.
It also becomes the first consumer benchmark for proving local roles and
resource semantics can map cleanly into CARING dimensions.
`FLEX-WP-0004` waits for the standalone core for the same reason as
before, but its Topaz evaluation task moved to `0005 T04`; this workplan
now implements the Topaz adapter against the spike's output.
Delegated adapters must preserve flex-auth's CARING descriptor and
conformance fields even when backend-native role semantics differ.
`FLEX-WP-0006` was the cross-repo integration unblocker for
ops-warden. ops-warden already implements the opt-in policy call
(`policy.enabled: true`) and production OpenBao signing works without the
gate. flex-auth now publishes the protected-system manifest,
`ssh-certificate` / `sign` policy package, allow/deny fixtures, and
`POST /v1/check` evidence that ops-warden can use before enabling
`policy.enabled` in production.
## State Hub Mirror
Native State Hub dependency edges should mirror:
Native State Hub dependency edges:
- `FLEX-WP-0002 -> FLEX-WP-0001`
- `FLEX-WP-0005 -> FLEX-WP-0001`
- `FLEX-WP-0002 -> FLEX-WP-0005`
- `FLEX-WP-0002 -> FLEX-WP-0001` (preserved)
- `FLEX-WP-0003 -> FLEX-WP-0002`
- `FLEX-WP-0004 -> FLEX-WP-0002`
- `FLEX-WP-0004 -> FLEX-WP-0005` (Topaz adapter consumes the spike)
- `FLEX-WP-0006 -> FLEX-WP-0002`
- `FLEX-WP-0006 -> FLEX-WP-0005`
- ops-warden: `WARDEN-WP-0009` finished (caller + registry smoke). Production
`policy.enabled: true` waits for `FLEX-WP-0007` (reachable flex-auth runtime).
- `FLEX-WP-0007 -> FLEX-WP-0006`

22
examples/README.md Normal file
View File

@@ -0,0 +1,22 @@
# examples/
Runnable examples used both as documentation and as test fixtures.
Expected layout (filled in across FLEX-WP-0002 / FLEX-WP-0003 /
FLEX-WP-0005):
```text
examples/
claims/ # key-cape lightweight-mode and Keycloak heavy-mode
# claim envelopes (P5.5)
caring/ # executable CARING descriptor, request,
# decision, registry, and audit fixtures (P2.1)
markitect/ # FlexAuthResourceManifest fixtures, decision
# fixtures, and Rego-in-Markdown policy packages
ops-warden/ # SSH certificate signing policy-gate fixtures
# for ops-warden policy.enabled smoke checks
topaz/ # docker-compose + sample directory and policy
# for the Topaz alignment spike (P5.4)
policies/ # generic Rego-in-Markdown packages used by
# the standalone core tests
```

12
examples/caring/README.md Normal file
View File

@@ -0,0 +1,12 @@
# CARING examples
Small fixtures for the executable CARING 0.4.0-RC2 profile used by
`FLEX-WP-0002`.
These are intentionally compact. They prove that the canonical descriptor,
request, decision, registry, audit, and Rego-in-Markdown policy package
shapes can round-trip through `pkg/api` and `internal/policy`.
The set includes local subjects, groups, teams, project resources, inherited
relationship facts, exposure events, allow/deny fixtures, and a
redact-with-obligation policy package.

View File

@@ -0,0 +1,26 @@
id: descriptor:tenant-alpha-document-reader
profile: caring-0.4.0-rc2
subject_type: Human
organization_relation: Customer
canonical_role: Doer
scope:
level: Resource
id: document:internal-note
tenant: tenant:alpha
resource: document:internal-note
planes:
- Data
capabilities:
- View
exposure_modes:
- Masked
- Plaintext
conditions:
- PurposeBound
- Logged
lifecycle_state: Operate
restrictions:
- ExportBlocked
access_path: direct
metadata:
source: examples/caring

View File

@@ -0,0 +1,22 @@
{
"id": "audit:decision:tenant-alpha-internal-note",
"type": "decision",
"decision_id": "decision:tenant-alpha-internal-note",
"subject": {
"id": "user:alice",
"type": "Human",
"tenant": "tenant:alpha"
},
"resource": {
"id": "document:internal-note",
"type": "document",
"system": "markitect-tool",
"tenant": "tenant:alpha"
},
"action": "read",
"effect": "allow",
"timestamp": "2026-05-17T00:00:00Z",
"metadata": {
"profile": "caring-0.4.0-rc2"
}
}

View File

@@ -0,0 +1,12 @@
id: batch:tenant-alpha-documents
subject:
id: user:alice
type: Human
tenant: tenant:alpha
action: read
resources:
- id: document:internal-note
system: markitect-tool
- id: document:missing
type: document
system: markitect-tool

View File

@@ -0,0 +1,41 @@
id: check:tenant-alpha-internal-note
subject:
id: user:alice
type: Human
tenant: tenant:alpha
action: read
resource:
id: document:internal-note
type: document
system: markitect-tool
tenant: tenant:alpha
context:
purpose: knowledge-base-read
assurance:
mfa: true
caring_context:
id: descriptor:tenant-alpha-document-reader
profile: caring-0.4.0-rc2
subject_type: Human
organization_relation: Customer
canonical_role: Doer
scope:
level: Resource
id: document:internal-note
tenant: tenant:alpha
resource: document:internal-note
planes:
- Data
capabilities:
- View
exposure_modes:
- Masked
- Plaintext
conditions:
- PurposeBound
- Logged
lifecycle_state: Operate
restrictions:
- ExportBlocked
access_path: direct
policy_version: markitect.documents.v1

View File

@@ -0,0 +1,69 @@
{
"id": "decision:tenant-alpha-internal-note",
"request_id": "check:tenant-alpha-internal-note",
"effect": "allow",
"reason": "reader_relation",
"matched_policy_version": "markitect.documents.v1",
"matched_rule": "allow_document_read",
"resource": {
"id": "document:internal-note",
"type": "document",
"system": "markitect-tool",
"tenant": "tenant:alpha"
},
"subject": {
"id": "user:alice",
"type": "Human",
"tenant": "tenant:alpha"
},
"obligations": [
{
"type": "log_access",
"parameters": {
"level": "standard"
}
}
],
"diagnostics": {
"policy_package": "examples/caring"
},
"provenance": {
"evaluator": "flex-auth",
"mode": "standalone",
"policy_package": "markitect.documents",
"policy_version": "v1",
"decision_time": "2026-05-17T00:00:00Z"
},
"caring": {
"profile": "caring-0.4.0-rc2",
"descriptor": {
"id": "descriptor:tenant-alpha-document-reader",
"profile": "caring-0.4.0-rc2",
"subject_type": "Human",
"organization_relation": "Customer",
"canonical_role": "Doer",
"scope": {
"level": "Resource",
"id": "document:internal-note",
"tenant": "tenant:alpha",
"resource": "document:internal-note"
},
"planes": ["Data"],
"capabilities": ["View"],
"exposure_modes": ["Masked", "Plaintext"],
"conditions": ["PurposeBound", "Logged"],
"lifecycle_state": "Operate",
"restrictions": ["ExportBlocked"],
"access_path": "direct"
},
"restrictions_evaluated": ["ExportBlocked"],
"exposure_modes": ["Masked", "Plaintext"],
"conformance_findings": [
{
"code": "CARING-EXPORT-SEPARATION",
"severity": "info",
"message": "View is allowed, but Exportable exposure remains separately blocked."
}
]
}
}

View File

@@ -0,0 +1,20 @@
{
"id": "exposure:tenant-alpha-support-001",
"type": "X-Support",
"actor": "user:alice",
"subject": "user:bob",
"scope": {
"level": "Resource",
"id": "document:alpha-plan",
"tenant": "tenant:alpha",
"resource": "document:alpha-plan"
},
"planes": ["Data"],
"exposure_modes": ["Masked"],
"reason": "Support review of masked project plan",
"decision_id": "decision:tenant-alpha-support-001",
"timestamp": "2026-05-17T00:00:00Z",
"metadata": {
"source": "examples/caring/exposure_event.json"
}
}

View File

@@ -0,0 +1,38 @@
- id: rel:reviewers-project-reviewer
system: markitect-tool
subject: team:project-reviewers
relation: reviewer
object: project:alpha-redesign
tenant: tenant:alpha
conditions:
- Logged
caring:
id: descriptor:tenant-alpha-project-reviewer
profile: caring-0.4.0-rc2
subject_type: Group
organization_relation: Customer
canonical_role: Verifier
scope:
level: Project
id: project:alpha-redesign
tenant: tenant:alpha
resource: project:alpha-redesign
planes:
- Data
capabilities:
- Review
exposure_modes:
- Masked
conditions:
- Logged
restrictions:
- ExportBlocked
- id: rel:alpha-plan-inherits-project-reviewer
system: markitect-tool
subject: document:alpha-plan
relation: inherits
object: project:alpha-redesign
tenant: tenant:alpha
metadata:
inheritance: parent
source: examples/caring/inherited_relationships.yaml

View File

@@ -0,0 +1,45 @@
id: fixture:markitect-internal-read-allow
request:
id: check:tenant-alpha-internal-note
subject:
id: user:alice
type: Human
tenant: tenant:alpha
action: read
resource:
id: document:internal-note
type: document
system: markitect-tool
tenant: tenant:alpha
caring_context:
id: descriptor:tenant-alpha-document-reader
profile: caring-0.4.0-rc2
subject_type: Human
organization_relation: Customer
canonical_role: Doer
scope:
level: Resource
id: document:internal-note
tenant: tenant:alpha
resource: document:internal-note
planes:
- Data
capabilities:
- View
exposure_modes:
- Masked
- Plaintext
conditions:
- PurposeBound
- Logged
restrictions:
- ExportBlocked
expect:
effect: allow
reason: reader_relation
conformance_findings:
- code: CARING-EXPORT-SEPARATION
severity: info
message: View is allowed, but Exportable exposure remains separately blocked.
metadata:
source: examples/caring

View File

@@ -0,0 +1,137 @@
---
id: markitect.documents.internal-read
name: Markitect internal document read
namespace: markitect:document
version: v1
status: draft
package: flexauth.markitect.documents
actions:
- read
owner: team:platform-architecture
fixtures:
- policy_fixture.yaml
caring:
profile: caring-0.4.0-rc2
enforce: false
canonical_roles:
- Doer
organization_relations:
- Customer
scopes:
- level: Resource
id: document:internal-note
tenant: tenant:alpha
planes:
- Data
capabilities:
- View
exposure_modes:
- Masked
- Plaintext
conditions:
- PurposeBound
- Logged
restrictions:
- ExportBlocked
activation:
mode: local
metadata:
source: examples/caring/policy_package.md
---
# Markitect Internal Document Read
This package authorizes read access to an internal Markitect document when
the request carries a CARING descriptor for a customer Doer with View
capability on the document resource and an explicit ExportBlocked restriction.
## Rules
```rego
import future.keywords.if
import future.keywords.in
default decision := {"effect": "deny", "reason": "no_matching_rule"}
decision := {
"effect": "allow",
"reason": "reader_relation",
"conformance_findings": [{
"code": "CARING-EXPORT-SEPARATION",
"severity": "info",
"message": "View is allowed, but Exportable exposure remains separately blocked."
}]
} if {
input.action == "read"
input.resource.system == "markitect-tool"
input.resource.type == "document"
input.caring_context.profile == "caring-0.4.0-rc2"
input.caring_context.organization_relation == "Customer"
input.caring_context.canonical_role == "Doer"
"View" in input.caring_context.capabilities
"ExportBlocked" in input.caring_context.restrictions
}
```
## Tests
```rego test
package flexauth.markitect.documents_test
import future.keywords.if
import data.flexauth.markitect.documents
test_reader_relation_allows if {
documents.decision.effect == "allow" with input as {
"action": "read",
"resource": {
"id": "document:internal-note",
"type": "document",
"system": "markitect-tool",
"tenant": "tenant:alpha"
},
"caring_context": {
"profile": "caring-0.4.0-rc2",
"organization_relation": "Customer",
"canonical_role": "Doer",
"capabilities": ["View"],
"restrictions": ["ExportBlocked"]
}
}
}
test_missing_caring_context_denies if {
documents.decision.effect == "deny" with input as {
"action": "read",
"resource": {
"id": "document:internal-note",
"type": "document",
"system": "markitect-tool",
"tenant": "tenant:alpha"
}
}
}
```
## Fixtures
```yaml fixture
id: fixture:markitect-internal-read-deny
request:
id: check:tenant-alpha-internal-note-deny
subject:
id: user:bob
type: Human
tenant: tenant:alpha
action: read
resource:
id: document:internal-note
type: document
system: markitect-tool
tenant: tenant:alpha
expect:
effect: deny
reason: no_matching_rule
metadata:
source: examples/caring/policy_package.md
```

View File

@@ -0,0 +1,29 @@
id: markitect.documents.internal-read
name: Markitect internal document read
version: v1
status: draft
package: flexauth.markitect.documents
caring:
profile: caring-0.4.0-rc2
canonical_roles:
- Doer
organization_relations:
- Customer
scopes:
- level: Resource
id: document:internal-note
tenant: tenant:alpha
planes:
- Data
capabilities:
- View
exposure_modes:
- Masked
- Plaintext
conditions:
- PurposeBound
- Logged
restrictions:
- ExportBlocked
metadata:
source: examples/caring

View File

@@ -0,0 +1,27 @@
id: markitect-project-resources
system: markitect-tool
resources:
- id: project:alpha-redesign
type: project
path: /projects/alpha-redesign
labels:
- project
trust_zone: internal
owner: team:project-reviewers
- id: document:alpha-plan
type: document
path: /projects/alpha-redesign/plan
parent: project:alpha-redesign
labels:
- internal
- pii
trust_zone: internal
owner: team:project-reviewers
actions:
- read
- review
- export
caring_profile: caring-0.4.0-rc2
metadata:
flex_auth_contract: resource-registration-v0
source: examples/caring/project_resource_manifest.yaml

View File

@@ -0,0 +1,132 @@
---
id: markitect.documents.mask-pii
name: Markitect masked PII read
namespace: markitect:document
version: v1
status: draft
package: flexauth.markitect.redact
actions:
- read
owner: team:project-reviewers
caring:
profile: caring-0.4.0-rc2
enforce: false
canonical_roles:
- Verifier
organization_relations:
- Customer
scopes:
- level: Resource
id: document:alpha-plan
tenant: tenant:alpha
planes:
- Data
capabilities:
- View
- Mask
exposure_modes:
- Masked
conditions:
- Logged
restrictions:
- ExportBlocked
metadata:
source: examples/caring/redact_policy_package.md
---
# Markitect Masked PII Read
This package returns a redaction decision when a verifier may inspect a
document only through masked fields.
## Rules
```rego
import future.keywords.if
import future.keywords.in
default decision := {"effect": "deny", "reason": "no_matching_rule"}
decision := {
"effect": "redact",
"reason": "masked_pii",
"obligations": [{
"type": "mask_fields",
"parameters": {"fields": ["email", "phone"]}
}]
} if {
input.action == "read"
input.resource.id == "document:alpha-plan"
"Mask" in input.caring_context.capabilities
"Masked" in input.caring_context.exposure_modes
}
```
## Tests
```rego test
package flexauth.markitect.redact_test
import future.keywords.if
import data.flexauth.markitect.redact
test_masked_reader_gets_redaction if {
redact.decision.effect == "redact" with input as {
"action": "read",
"resource": {"id": "document:alpha-plan"},
"caring_context": {
"capabilities": ["View", "Mask"],
"exposure_modes": ["Masked"]
}
}
}
```
## Fixtures
```yaml fixture
id: fixture:masked-pii-redact
request:
id: check:masked-pii
subject:
id: user:bob
type: Human
tenant: tenant:alpha
action: read
resource:
id: document:alpha-plan
type: document
system: markitect-tool
tenant: tenant:alpha
caring_context:
id: descriptor:tenant-alpha-masked-pii-reviewer
profile: caring-0.4.0-rc2
subject_type: Human
organization_relation: Customer
canonical_role: Verifier
scope:
level: Resource
id: document:alpha-plan
tenant: tenant:alpha
resource: document:alpha-plan
planes:
- Data
capabilities:
- View
- Mask
exposure_modes:
- Masked
conditions:
- Logged
restrictions:
- ExportBlocked
expect:
effect: redact
reason: masked_pii
obligations:
- type: mask_fields
parameters:
fields:
- email
- phone
```

View File

@@ -0,0 +1,99 @@
{
"systems": [
{
"id": "markitect-tool",
"name": "Markitect Tool",
"resource_types": [
{
"name": "document",
"scope_level": "Resource",
"planes": ["Data"]
}
],
"actions": [
{
"name": "read",
"capabilities": ["View"],
"planes": ["Data"],
"exposure_modes": ["Masked", "Plaintext"]
}
],
"caring_profiles": ["caring-0.4.0-rc2"]
}
],
"resource_manifests": [
{
"id": "markitect-example-knowledge-base",
"system": "markitect-tool",
"resources": [
{
"id": "document:internal-note",
"type": "document",
"parent": "knowledge-base:markitect-example",
"labels": ["internal"],
"trust_zone": "internal",
"owner": "team:platform"
}
],
"actions": ["read", "query", "search", "package", "export"],
"caring_profile": "caring-0.4.0-rc2",
"metadata": {
"flex_auth_contract": "resource-registration-v0"
}
}
],
"tenants": [
{
"id": "tenant:alpha",
"name": "Tenant Alpha"
}
],
"subjects": [
{
"id": "user:alice",
"type": "Human",
"display_name": "Alice Example",
"organization_relation": "Customer",
"roles": ["Doer"],
"groups": ["group:platform-architecture"],
"tenant": "tenant:alpha"
}
],
"groups": [
{
"id": "group:platform-architecture",
"display_name": "Platform Architecture",
"members": ["user:alice"],
"tenant": "tenant:alpha"
}
],
"relationships": [
{
"id": "rel:alice-reader-internal-note",
"system": "markitect-tool",
"subject": "group:platform-architecture",
"relation": "reader",
"object": "document:internal-note",
"tenant": "tenant:alpha",
"conditions": ["Logged"],
"caring": {
"id": "descriptor:tenant-alpha-document-reader",
"profile": "caring-0.4.0-rc2",
"subject_type": "Group",
"organization_relation": "Customer",
"canonical_role": "Doer",
"scope": {
"level": "Resource",
"id": "document:internal-note",
"tenant": "tenant:alpha",
"resource": "document:internal-note"
},
"planes": ["Data"],
"capabilities": ["View"],
"exposure_modes": ["Masked", "Plaintext"],
"conditions": ["Logged"],
"restrictions": ["ExportBlocked"]
}
}
]
}

View File

@@ -0,0 +1,32 @@
id: rel:alice-reader-internal-note
system: markitect-tool
subject: group:platform-architecture
relation: reader
object: document:internal-note
tenant: tenant:alpha
conditions:
- Logged
caring:
id: descriptor:tenant-alpha-document-reader
profile: caring-0.4.0-rc2
subject_type: Group
organization_relation: Customer
canonical_role: Doer
scope:
level: Resource
id: document:internal-note
tenant: tenant:alpha
resource: document:internal-note
planes:
- Data
capabilities:
- View
exposure_modes:
- Masked
- Plaintext
conditions:
- Logged
restrictions:
- ExportBlocked
provenance:
source: examples/caring

View File

@@ -0,0 +1,22 @@
id: subjects:tenant-alpha
subjects:
- id: user:alice
type: Human
display_name: Alice Example
organization_relation: Customer
roles:
- Doer
groups:
- group:platform-architecture
tenant: tenant:alpha
groups:
- id: group:platform-architecture
display_name: Platform Architecture
members:
- user:alice
tenant: tenant:alpha
tenants:
- id: tenant:alpha
name: Tenant Alpha
metadata:
source: examples/caring

View File

@@ -0,0 +1,37 @@
id: tenant-alpha-project-team
tenants:
- id: tenant:alpha
name: Tenant Alpha
subjects:
- id: user:alice
type: Human
display_name: Alice Example
organization_relation: Customer
roles:
- Doer
groups:
- group:platform-architecture
tenant: tenant:alpha
- id: user:bob
type: Human
display_name: Bob Example
organization_relation: Customer
roles:
- Verifier
groups:
- team:project-reviewers
tenant: tenant:alpha
groups:
- id: group:platform-architecture
display_name: Platform Architecture
members:
- user:alice
tenant: tenant:alpha
teams:
- id: team:project-reviewers
display_name: Project Reviewers
members:
- user:bob
tenant: tenant:alpha
metadata:
source: examples/caring/team_subject_manifest.yaml

23
examples/claims/README.md Normal file
View File

@@ -0,0 +1,23 @@
# examples/claims/
Contract fixtures for the NetKingdom IAM Profile v0.2 claim shapes
flex-auth must accept. Each file is the *raw verified claim map* as
flex-auth receives it from the upstream identity layer (key-cape or
Keycloak); flex-auth's normalization produces the same
`EnterpriseIdentity`-shaped envelope for all of them.
See `docs/iam-profile-consumption.md` for the full consumption
surface.
| Fixture | Provider | Demonstrates |
| --- | --- | --- |
| `key-cape-lightweight.yaml` | key-cape lightweight mode | Profile-conformant minimum: single audience, top-level `roles` array, explicit tenant/principal/assurance. |
| `keycloak-heavy.yaml` | Keycloak production | Full variation set: canonical `roles`, provider-native role sources, scope as space-separated string, MFA assurance, multiple audiences. |
| `service-account.yaml` | Either provider | Service account; `principal_type: service`, `service` + `operator` roles, no `preferred_username`, narrow scope. |
| `emergency.yaml` | Either provider | Break-glass human identity; `emergency` role, `assurance.level: break_glass`, short expiry, audit-trail metadata in an `emergency` claim. |
| `keycloak-group-overage.yaml` | Entra/Keycloak | Group-claim overage signal (`hasgroups: true`); flex-auth's directory resolver fetches the full set. |
These fixtures are loaded by the standalone evaluator's contract tests
(`FLEX-WP-0002 P2.4`) and by the Topaz adapter's contract tests
(`FLEX-WP-0004 T01`). Both code paths MUST produce identical
normalized envelopes for the same fixture.

View File

@@ -0,0 +1,44 @@
# Claim envelope for an emergency (break-glass) human principal. Short
# expiry, emergency role, requires MFA per the profile, and triggers
# durable audit recording on every flex-auth decision that involves it.
#
# Reference: NetKingdom IAM Profile v0.2 "Emergency And Break-Glass
# Access". flex-auth maps the emergency role plus break_glass assurance to
# a `record_emergency` obligation on every decision.
iss: https://sso.netkingdom.example/realms/netkingdom
sub: f1c4f64e-2c0c-4cda-8c9f-9f3f8f3a2b0e
aud:
- flex-auth
exp: 1767226200 # iat + 10 minutes; emergency tokens are short-lived
iat: 1767225600
auth_time: 1767225595
tenant: tenant:platform
principal_type: human
azp: ops-console
preferred_username: ada
email: ada@netkingdom.example
scope: openid profile hub:admin
roles:
- emergency
- admin
groups:
- /platform/stewards
amr:
- pwd
- otp
- hwk
acr: "3"
assurance:
level: break_glass
methods:
- pwd
- otp
- hwk
mfa: true
source: keycloak
at: 1767225595
emergency:
incident_id: INC-2026-0042
authorized_by: "team:platform-stewards"
reason: "credential rotation playbook step 4"

View File

@@ -0,0 +1,33 @@
# Claim envelope a key-cape (lightweight mode) deployment emits for an
# authenticated human user. Profile-conformant minimum: required claims
# only, single audience, simple roles list, OIDC standard amr values.
#
# Reference: docs/iam-profile-consumption.md, NetKingdom IAM Profile v0.2
# "Core Claims" and "Local Development Profile".
iss: https://idp.netkingdom.local/keycape
sub: user-7f9e2b
aud:
- flex-auth
exp: 4102444800 # 2100-01-01, kept far-future for stable fixtures
iat: 1767225600 # 2026-01-01
tenant: tenant:platform
principal_type: human
preferred_username: ada
email: ada@netkingdom.local
name: Ada Lovelace
scope: openid profile hub:read
roles:
- viewer
amr:
- pwd
acr: "1"
groups:
- /markitect/readers
assurance:
level: aal1
methods:
- pwd
mfa: false
source: key-cape
at: 1767225600

View File

@@ -0,0 +1,26 @@
# Claim envelope when the token-side `groups` list has been clipped by
# the IdP. Both Microsoft Entra and Keycloak signal this differently;
# this fixture shows the Entra-style `hasgroups: true` flag. flex-auth
# sets directory.group_overage = true and depends on the directory
# resolver (FLEX-WP-0004 T05) to fetch the full set.
#
# Reference: docs/iam-profile-consumption.md §"Group Overage and
# Freshness".
iss: https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/v2.0
sub: f1c4f64e-2c0c-4cda-8c9f-9f3f8f3a2b0e
aud:
- flex-auth
exp: 4102444800
iat: 1767225600
preferred_username: ada
name: Ada Lovelace
scope: openid profile hub:read
roles:
- viewer
hasgroups: true
_claim_names:
groups: src1
_claim_sources:
src1:
endpoint: https://graph.microsoft.com/v1.0/users/f1c4f64e/getMemberObjects

View File

@@ -0,0 +1,55 @@
# Claim envelope a Keycloak (heavy mode) deployment emits for an
# authenticated human user with MFA. Demonstrates the full set of
# variations flex-auth must normalize: roles in realm_access AND
# resource_access, scope as space-separated string, multiple audiences,
# enriched assurance via amr=otp.
#
# Reference: docs/iam-profile-consumption.md §"Tolerated Variations".
iss: https://sso.netkingdom.example/realms/netkingdom
sub: f1c4f64e-2c0c-4cda-8c9f-9f3f8f3a2b0e
aud:
- flex-auth
- markitect-tool
exp: 4102444800
iat: 1767225600
auth_time: 1767225590
tenant: tenant:platform
principal_type: human
azp: markitect-cli
preferred_username: ada
email: ada@netkingdom.example
email_verified: true
name: Ada Lovelace
given_name: Ada
family_name: Lovelace
scope: openid profile email hub:read hub:write hub:capability
roles:
- operator
realm_access:
roles:
- default-roles-netkingdom
- operator
resource_access:
flex-auth:
roles:
- reader
markitect-tool:
roles:
- editor
groups:
- /platform/architecture
- /markitect/readers
amr:
- pwd
- otp
acr: "2"
assurance:
level: aal2
methods:
- pwd
- otp
mfa: true
source: keycloak
at: 1767225590
sid: 4c0a3a8a-3a47-4f2f-8e89-9e5f9b0a0a0a

View File

@@ -0,0 +1,29 @@
# Claim envelope for a hub-to-hub service account (client_credentials
# grant). Profile-required `service` role, scoped tightly to the
# operation it performs. No preferred_username (service identities are
# named after the service and environment per the profile).
#
# Reference: NetKingdom IAM Profile v0.2 "Service Account Flow".
iss: https://sso.netkingdom.example/realms/netkingdom
sub: svc-markitect-tool-prod
aud:
- flex-auth
exp: 4102444800
iat: 1767225600
tenant: tenant:platform
principal_type: service
azp: svc-markitect-tool-prod
client_id: svc-markitect-tool-prod
scope: hub:read hub:capability
roles:
- service
- operator
groups: []
assurance:
level: aal1
methods:
- client_secret
mfa: false
source: keycloak
at: 1767225600

View File

@@ -0,0 +1,13 @@
id: markitect-ambiguous-example
system: markitect-tool
caring_profile: caring-0.4.0-rc2
resources:
- id: document:ambiguous-note
type: document
parent: knowledge-base:markitect-example
path: examples/policy/ambiguous-note.md
actions:
- read
metadata:
source: examples/markitect/ambiguous_resource_manifest.yaml
flex_auth_contract: resource-registration-v0

View File

@@ -0,0 +1,239 @@
- id: fixture:markitect-public-document-allow
request:
id: check:markitect-public-document
subject:
id: user:visitor
type: Human
tenant: tenant:alpha
action: read
resource:
id: document:public-note
type: document
system: markitect-tool
tenant: tenant:alpha
attributes:
labels:
- public
trust_zone: public
caring_context:
id: descriptor:public-document-reader
profile: caring-0.4.0-rc2
subject_type: Human
organization_relation: Customer
canonical_role: Doer
scope:
level: Resource
id: document:public-note
tenant: tenant:alpha
planes:
- Data
capabilities:
- View
exposure_modes:
- Plaintext
conditions:
- Logged
expect:
effect: allow
reason: public_document
metadata:
expected_caring_descriptor: descriptor:public-document-reader
expected_conformance_findings: []
expected_exposure_modes:
- Plaintext
expected_audit_behavior: sampled_allow
- id: fixture:markitect-internal-document-deny
request:
id: check:markitect-internal-document-deny
subject:
id: user:visitor
type: Human
tenant: tenant:alpha
attributes:
groups: []
action: read
resource:
id: document:internal-note
type: document
system: markitect-tool
tenant: tenant:alpha
attributes:
labels:
- internal
trust_zone: internal
expect:
effect: deny
reason: no_matching_rule
metadata:
expected_caring_descriptor: null
expected_conformance_findings: []
expected_exposure_modes:
- None
expected_audit_behavior: always_record
- id: fixture:markitect-internal-document-reader-allow
request:
id: check:markitect-internal-document-reader
subject:
id: user:alice
type: Human
tenant: tenant:alpha
attributes:
groups:
- group:platform-architecture
action: read
resource:
id: document:internal-note
type: document
system: markitect-tool
tenant: tenant:alpha
attributes:
labels:
- internal
trust_zone: internal
caring_context:
id: descriptor:internal-document-reader
profile: caring-0.4.0-rc2
subject_type: Human
organization_relation: Customer
canonical_role: Doer
scope:
level: Resource
id: document:internal-note
tenant: tenant:alpha
planes:
- Data
capabilities:
- View
exposure_modes:
- Masked
- Plaintext
conditions:
- Logged
restrictions:
- ExportBlocked
expect:
effect: allow
reason: reader_group
metadata:
expected_caring_descriptor: descriptor:internal-document-reader
expected_conformance_findings: []
expected_exposure_modes:
- Masked
- Plaintext
expected_audit_behavior: sampled_allow
- id: fixture:markitect-restricted-export-steward-mfa
request:
id: check:markitect-restricted-export
subject:
id: user:steward
type: Human
tenant: tenant:alpha
attributes:
roles:
- steward
action: export
resource:
id: export:internal-note-review-bundle
type: export
system: markitect-tool
tenant: tenant:alpha
attributes:
labels:
- export
trust_zone: external
context:
mfa: true
reason: customer-approved export
caring_context:
id: descriptor:restricted-export-steward
profile: caring-0.4.0-rc2
subject_type: Human
organization_relation: Customer
canonical_role: Maintainer
scope:
level: Record
id: export:internal-note-review-bundle
tenant: tenant:alpha
planes:
- Data
- Audit
capabilities:
- Export
exposure_modes:
- Exportable
- Plaintext
conditions:
- MFARequired
- Logged
expect:
effect: allow
reason: steward_export_mfa
conformance_findings:
- code: MARKITECT-EXPORT-MFA-LOGGED
severity: info
message: Export is allowed only with steward role, MFA, and logging.
metadata:
expected_caring_descriptor: descriptor:restricted-export-steward
expected_exposure_modes:
- Exportable
- Plaintext
expected_audit_behavior: always_record
- id: fixture:markitect-context-package-activation
request:
id: check:markitect-context-package-activation
subject:
id: user:alice
type: Human
tenant: tenant:alpha
action: activate_context
resource:
id: context-package:internal-note-review
type: context_package
system: markitect-tool
tenant: tenant:alpha
attributes:
labels:
- internal
- generated
context:
freshness_seconds: 600
policy_version: markitect-gateway-v1
caring_context:
id: descriptor:context-package-activation
profile: caring-0.4.0-rc2
subject_type: Human
organization_relation: Customer
canonical_role: Verifier
scope:
level: Dataset
id: context-package:internal-note-review
tenant: tenant:alpha
planes:
- Intent
- Policy
capabilities:
- Use
- Execute
exposure_modes:
- Metadata
- Masked
conditions:
- PurposeBound
- Logged
expect:
effect: allow
reason: fresh_context_package
obligations:
- type: record_context_activation
parameters:
freshness_seconds: 600
conformance_findings:
- code: MARKITECT-CONTEXT-FRESHNESS
severity: info
message: Context package activation includes policy version and freshness metadata.
metadata:
expected_caring_descriptor: descriptor:context-package-activation
expected_exposure_modes:
- Metadata
- Masked
expected_audit_behavior: always_record

View File

@@ -0,0 +1,152 @@
---
id: markitect.gateway.check-fixtures
name: Markitect gateway check fixtures
namespace: markitect:gateway
version: v1
status: draft
package: flexauth.markitect.gateway
actions:
- read
- export
- activate_context
owner: team:platform-architecture
fixtures:
- check_fixtures.yaml
caring:
profile: caring-0.4.0-rc2
enforce: false
canonical_roles:
- Doer
- Maintainer
- Verifier
organization_relations:
- Customer
scopes:
- level: Resource
id: document:public-note
tenant: tenant:alpha
- level: Resource
id: document:internal-note
tenant: tenant:alpha
- level: Dataset
id: context-package:internal-note-review
tenant: tenant:alpha
planes:
- Intent
- Data
- Audit
capabilities:
- View
- Export
- Use
- Execute
exposure_modes:
- Metadata
- Masked
- Plaintext
- Exportable
conditions:
- MFARequired
- PurposeBound
- Logged
restrictions:
- ExportBlocked
metadata:
source: examples/markitect/check_policy_package.md
---
# Markitect Gateway Check Fixtures
This package captures the first Markitect gateway scenarios as executable Rego
and external fixtures.
## Rules
```rego
import future.keywords.if
import future.keywords.in
default decision := {"effect": "deny", "reason": "no_matching_rule"}
decision := {"effect": "allow", "reason": "public_document"} if {
input.action == "read"
input.resource.type == "document"
"public" in object.get(input.resource.attributes, "labels", [])
}
decision := {"effect": "allow", "reason": "reader_group"} if {
input.action == "read"
input.resource.type == "document"
"internal" in object.get(input.resource.attributes, "labels", [])
"group:platform-architecture" in object.get(input.subject.attributes, "groups", [])
"View" in input.caring_context.capabilities
}
decision := {
"effect": "allow",
"reason": "steward_export_mfa",
"conformance_findings": [{
"code": "MARKITECT-EXPORT-MFA-LOGGED",
"severity": "info",
"message": "Export is allowed only with steward role, MFA, and logging."
}]
} if {
input.action == "export"
"steward" in object.get(input.subject.attributes, "roles", [])
input.context.mfa == true
"Export" in input.caring_context.capabilities
"Exportable" in input.caring_context.exposure_modes
}
decision := {
"effect": "allow",
"reason": "fresh_context_package",
"obligations": [{
"type": "record_context_activation",
"parameters": {"freshness_seconds": input.context.freshness_seconds}
}],
"conformance_findings": [{
"code": "MARKITECT-CONTEXT-FRESHNESS",
"severity": "info",
"message": "Context package activation includes policy version and freshness metadata."
}]
} if {
input.action == "activate_context"
input.resource.type == "context_package"
input.policy_version != ""
input.context.freshness_seconds <= 900
"Use" in input.caring_context.capabilities
"Execute" in input.caring_context.capabilities
}
```
## Tests
```rego test
package flexauth.markitect.gateway_test
import future.keywords.if
import data.flexauth.markitect.gateway
test_public_document_allows if {
gateway.decision.effect == "allow" with input as {
"action": "read",
"resource": {
"type": "document",
"attributes": {"labels": ["public"]}
}
}
}
test_export_requires_mfa if {
gateway.decision.effect == "deny" with input as {
"action": "export",
"subject": {"attributes": {"roles": ["steward"]}},
"context": {"mfa": false},
"caring_context": {
"capabilities": ["Export"],
"exposure_modes": ["Exportable"]
}
}
}
```

View File

@@ -0,0 +1,83 @@
id: markitect-namespace-example
system: markitect-tool
caring_profile: caring-0.4.0-rc2
resources:
- id: knowledge-base:markitect-example
type: knowledge_base
labels:
- internal
trust_zone: internal
owner: team:platform-architecture
- id: repository:markitect-policy
type: repository
parent: knowledge-base:markitect-example
path: repos/markitect-policy
labels:
- internal
trust_zone: internal
owner: team:platform-architecture
- id: document:internal-note
type: document
parent: repository:markitect-policy
path: examples/policy/private/internal-note.md
labels:
- internal
- pii
trust_zone: internal
owner: team:platform-architecture
attributes:
markitect_path: examples/policy/private/internal-note.md
frontmatter_visibility: internal
source_revision: rev:example
- id: section:internal-note#risk
type: section
parent: document:internal-note
path: examples/policy/private/internal-note.md#risk
labels:
- internal
trust_zone: internal
- id: span:internal-note#risk:customer-email
type: span
parent: section:internal-note#risk
labels:
- pii
trust_zone: restricted
attributes:
data_classes:
- email
- id: context-package:internal-note-review
type: context_package
parent: document:internal-note
labels:
- internal
- generated
trust_zone: internal
attributes:
freshness_seconds: 900
workflow_state: prepared
- id: workflow-artifact:internal-note-review-run
type: workflow_artifact
parent: context-package:internal-note-review
labels:
- generated
trust_zone: internal
attributes:
workflow_state: completed
- id: export:internal-note-review-bundle
type: export
parent: workflow-artifact:internal-note-review-run
labels:
- export
trust_zone: external
actions:
- read
- query
- search
- package
- activate_context
- export
- workflow_run
- admin
metadata:
source: examples/markitect/namespace_resource_manifest.yaml
flex_auth_contract: resource-registration-v0

View File

@@ -0,0 +1,155 @@
id: markitect-tool
name: Markitect Tool
description: Markitect protected-system namespace for flex-auth.
caring_profiles:
- caring-0.4.0-rc2
actions:
- name: read
capabilities:
- View
planes:
- Data
exposure_modes:
- Metadata
- Masked
- Plaintext
- name: query
capabilities:
- ViewCollection
- Observe
planes:
- Data
exposure_modes:
- Metadata
- Aggregated
- Masked
- name: search
capabilities:
- ViewCollection
- Observe
planes:
- Data
exposure_modes:
- Metadata
- Aggregated
- Masked
- name: package
capabilities:
- Create
- Bind
- ViewCollection
planes:
- Intent
- Data
exposure_modes:
- Metadata
- Masked
- name: activate_context
capabilities:
- Use
- Execute
planes:
- Intent
- Policy
exposure_modes:
- Metadata
- Masked
- name: export
capabilities:
- Export
planes:
- Data
- Audit
exposure_modes:
- Exportable
- Plaintext
- name: workflow_run
capabilities:
- Execute
- Operate
planes:
- Execution
- Data
- Audit
exposure_modes:
- Metadata
- Masked
- Plaintext
- name: admin
capabilities:
- Configure
- Grant
- Revoke
- Audit
planes:
- Configuration
- Identity
- Policy
- Audit
exposure_modes:
- Metadata
- Plaintext
resource_types:
- name: knowledge_base
scope_level: Workspace
planes:
- Intent
- Data
- name: repository
parent_types:
- knowledge_base
scope_level: Project
planes:
- Build
- Data
- name: document
parent_types:
- repository
- knowledge_base
scope_level: Resource
planes:
- Data
- name: section
parent_types:
- document
scope_level: Subresource
planes:
- Data
- name: span
parent_types:
- section
- document
scope_level: Field
planes:
- Data
- name: context_package
parent_types:
- knowledge_base
- repository
- document
scope_level: Dataset
planes:
- Intent
- Data
- Policy
- name: workflow_artifact
parent_types:
- context_package
- document
scope_level: Process
planes:
- Execution
- Data
- Audit
- name: export
parent_types:
- workflow_artifact
- context_package
- document
scope_level: Record
planes:
- Data
- Audit
metadata:
source: examples/markitect/protected_system_manifest.yaml
namespace_doc: docs/markitect-resource-namespace.md

View File

@@ -0,0 +1,40 @@
# Pinned example of the FlexAuthResourceManifest shape.
#
# Source: markitect-tool/examples/policy/flex-auth-resource-manifest.yaml
# (emitted by markitect_tool.policy.enterprise.FlexAuthResourceManifest in
# MKTT-WP-0014). Schema: ../../schemas/resource_manifest.schema.json.
id: markitect-example-knowledge-base
system: markitect-tool
actions:
- read
- query
- search
- package
- export
resources:
- id: knowledge-base:markitect-example
type: knowledge_base
labels:
- public
trust_zone: public
owner: team:platform-architecture
- id: document:public-note
type: document
parent: knowledge-base:markitect-example
path: examples/policy/public-note.md
labels:
- public
trust_zone: public
owner: team:platform-architecture
- id: document:internal-note
type: document
parent: knowledge-base:markitect-example
path: examples/policy/private/internal-note.md
labels:
- internal
trust_zone: internal
owner: team:platform-architecture
metadata:
source: markitect example policy fixtures
flex_auth_contract: resource-registration-v0

View File

@@ -0,0 +1,49 @@
# Ops-Warden SSH Signing Policy Gate
This example is the flex-auth side of ops-warden's opt-in pre-sign gate.
When `policy.enabled: true`, ops-warden calls `POST /v1/check` before signing
or issuing an SSH certificate.
Files:
- `protected_system_manifest.yaml` declares the `ops-warden` protected system,
`ssh-certificate` resource type, and `sign` action.
- `resource_manifest.yaml` declares fixture SSH certificate actor resources and
non-secret policy attributes such as allowed principals and TTL maxima.
- `subject_manifest.yaml` declares non-secret fixture actors for `adm`, `agt`,
and `atm` signing paths.
- `registry_snapshot.json` is the combined local registry used by the CLI and
service examples.
- `policy_package.md` is the Rego-in-Markdown policy package.
- `policy_fixtures.yaml` contains allow and deny expectations for package
validation.
- `check_request_*.json` files are ops-warden-shaped `/v1/check` requests.
Run locally:
```bash
flex-auth validate --kind protected-system --file examples/ops-warden/protected_system_manifest.yaml
flex-auth validate --kind resource-manifest --file examples/ops-warden/resource_manifest.yaml
flex-auth validate --kind subject-manifest --file examples/ops-warden/subject_manifest.yaml
flex-auth load-registry --file examples/ops-warden/registry_snapshot.json
flex-auth test-policy --file examples/ops-warden/policy_package.md
flex-auth check --registry examples/ops-warden/registry_snapshot.json --policy examples/ops-warden/policy_package.md --request examples/ops-warden/check_request_allow_adm.json
```
The fixture public-key fingerprints are examples only. Do not put real keys,
OpenBao tokens, or private signing material in these files.
## Production Registry Fixture
production_registry_snapshot.json is a non-secret fixture generated by
ops-warden for FLEX-WP-0007 coverage. It mirrors the current production actor
names used by ops-warden inventory and should be refreshed when that inventory
changes.
Validate both registries locally:
flex-auth load-registry --file examples/ops-warden/registry_snapshot.json
flex-auth load-registry --file examples/ops-warden/production_registry_snapshot.json
The production sync contract is documented in docs/ops-warden-registry-sync.md.

View File

@@ -0,0 +1,23 @@
{
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "platform-steward",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"platform",
"root"
],
"actor_type": "adm",
"ttl_hours": 4,
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
}
}

View File

@@ -0,0 +1,22 @@
{
"id": "check:ops-warden-ci-deploy-agent-agt",
"tenant": "tenant:platform",
"subject": {
"id": "ci-deploy-agent",
"type": "agt"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"deploy"
],
"actor_type": "agt",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
}
}

View File

@@ -0,0 +1,22 @@
{
"id": "check:ops-warden-backup-automation-atm",
"tenant": "tenant:platform",
"subject": {
"id": "backup-automation",
"type": "atm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/backup-automation",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"backup"
],
"actor_type": "atm",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-atm-fingerprint"
}
}

View File

@@ -0,0 +1,22 @@
{
"id": "check:ops-warden-ci-deploy-agent-agt",
"tenant": "tenant:platform",
"subject": {
"id": "ci-deploy-agent",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"deploy"
],
"actor_type": "agt",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
}
}

View File

@@ -0,0 +1,22 @@
{
"id": "check:ops-warden-ci-deploy-agent-agt",
"tenant": "tenant:platform",
"subject": {
"id": "ci-deploy-agent",
"type": "agt"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"root"
],
"actor_type": "agt",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
}
}

View File

@@ -0,0 +1,21 @@
{
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "platform-steward",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"platform"
],
"actor_type": "adm",
"ttl_hours": 4
}
}

View File

@@ -0,0 +1,22 @@
{
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "platform-steward",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"platform"
],
"actor_type": "adm",
"ttl_hours": 12,
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
}
}

View File

@@ -0,0 +1,22 @@
{
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "unknown-actor",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden"
},
"context": {
"principals": [
"platform"
],
"actor_type": "adm",
"ttl_hours": 4,
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
}
}

View File

@@ -0,0 +1,337 @@
[
{
"id": "fixture:ops-warden-adm-sign-allow",
"request": {
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "platform-steward",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "platform-steward",
"actor_type": "adm",
"allowed_subjects": [
"platform-steward",
"iam:platform-steward"
],
"allowed_principals": [
"platform",
"root"
],
"max_ttl_hours": 8
}
},
"context": {
"principals": [
"platform",
"root"
],
"actor_type": "adm",
"ttl_hours": 4,
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
}
},
"expect": {
"effect": "allow",
"reason": "signing_policy_matched"
}
},
{
"id": "fixture:ops-warden-agt-sign-allow",
"request": {
"id": "check:ops-warden-ci-deploy-agent-agt",
"tenant": "tenant:platform",
"subject": {
"id": "ci-deploy-agent",
"type": "agt"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "ci-deploy-agent",
"actor_type": "agt",
"allowed_subjects": [
"ci-deploy-agent",
"iam:ci-deploy-agent"
],
"allowed_principals": [
"deploy",
"git"
],
"max_ttl_hours": 2
}
},
"context": {
"principals": [
"deploy"
],
"actor_type": "agt",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
}
},
"expect": {
"effect": "allow",
"reason": "signing_policy_matched"
}
},
{
"id": "fixture:ops-warden-atm-sign-allow",
"request": {
"id": "check:ops-warden-backup-automation-atm",
"tenant": "tenant:platform",
"subject": {
"id": "backup-automation",
"type": "atm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/backup-automation",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "backup-automation",
"actor_type": "atm",
"allowed_subjects": [
"backup-automation",
"iam:backup-automation"
],
"allowed_principals": [
"backup"
],
"max_ttl_hours": 1
}
},
"context": {
"principals": [
"backup"
],
"actor_type": "atm",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-atm-fingerprint"
}
},
"expect": {
"effect": "allow",
"reason": "signing_policy_matched"
}
},
{
"id": "fixture:ops-warden-unknown-subject-deny",
"request": {
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "unknown-actor",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "platform-steward",
"actor_type": "adm",
"allowed_subjects": [
"platform-steward",
"iam:platform-steward"
],
"allowed_principals": [
"platform",
"root"
],
"max_ttl_hours": 8
}
},
"context": {
"principals": [
"platform"
],
"actor_type": "adm",
"ttl_hours": 4,
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
}
},
"expect": {
"effect": "deny",
"reason": "unknown_subject"
}
},
{
"id": "fixture:ops-warden-actor-type-mismatch-deny",
"request": {
"id": "check:ops-warden-ci-deploy-agent-agt",
"tenant": "tenant:platform",
"subject": {
"id": "ci-deploy-agent",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "ci-deploy-agent",
"actor_type": "agt",
"allowed_subjects": [
"ci-deploy-agent",
"iam:ci-deploy-agent"
],
"allowed_principals": [
"deploy",
"git"
],
"max_ttl_hours": 2
}
},
"context": {
"principals": [
"deploy"
],
"actor_type": "agt",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
}
},
"expect": {
"effect": "deny",
"reason": "actor_type_mismatch"
}
},
{
"id": "fixture:ops-warden-ttl-above-max-deny",
"request": {
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "platform-steward",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "platform-steward",
"actor_type": "adm",
"allowed_subjects": [
"platform-steward",
"iam:platform-steward"
],
"allowed_principals": [
"platform",
"root"
],
"max_ttl_hours": 8
}
},
"context": {
"principals": [
"platform"
],
"actor_type": "adm",
"ttl_hours": 12,
"pubkey_fingerprint": "SHA256:example-adm-fingerprint"
}
},
"expect": {
"effect": "deny",
"reason": "ttl_out_of_bounds"
}
},
{
"id": "fixture:ops-warden-disallowed-principal-deny",
"request": {
"id": "check:ops-warden-ci-deploy-agent-agt",
"tenant": "tenant:platform",
"subject": {
"id": "ci-deploy-agent",
"type": "agt"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "ci-deploy-agent",
"actor_type": "agt",
"allowed_subjects": [
"ci-deploy-agent",
"iam:ci-deploy-agent"
],
"allowed_principals": [
"deploy",
"git"
],
"max_ttl_hours": 2
}
},
"context": {
"principals": [
"root"
],
"actor_type": "agt",
"ttl_hours": 1,
"pubkey_fingerprint": "SHA256:example-agt-fingerprint"
}
},
"expect": {
"effect": "deny",
"reason": "disallowed_principal"
}
},
{
"id": "fixture:ops-warden-missing-fingerprint-deny",
"request": {
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {
"id": "platform-steward",
"type": "adm"
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "platform-steward",
"actor_type": "adm",
"allowed_subjects": [
"platform-steward",
"iam:platform-steward"
],
"allowed_principals": [
"platform",
"root"
],
"max_ttl_hours": 8
}
},
"context": {
"principals": [
"platform"
],
"actor_type": "adm",
"ttl_hours": 4
}
},
"expect": {
"effect": "deny",
"reason": "missing_pubkey_fingerprint"
}
}
]

View File

@@ -0,0 +1,257 @@
---
id: ops-warden.ssh-certificate.sign
name: Ops-Warden SSH certificate signing
namespace: ops-warden:ssh-certificate
version: v1
status: ready
package: flexauth.ops_warden.ssh_signing
actions:
- sign
owner: team:platform-security
fixtures:
- policy_fixtures.yaml
caring:
profile: caring-0.4.0-rc2
enforce: false
canonical_roles:
- Operator
organization_relations:
- ServiceProvider
scopes:
- level: Platform
id: platform:ssh-signing
tenant: tenant:platform
planes:
- Identity
- Secret
- Audit
capabilities:
- Use
- Operate
- Audit
exposure_modes:
- Metadata
conditions:
- TimeLimited
- Logged
restrictions:
- PrivilegeEscalationBlocked
- SecretAccessBlocked
activation:
mode: local
metadata:
source: examples/ops-warden/policy_package.md
ops_warden_policy_gate: v2
---
# Ops-Warden SSH Certificate Signing
This package authorizes ops-warden's opt-in pre-sign policy gate. The caller
keeps SSH CA custody, actor inventory, and OpenBao signing; flex-auth decides
whether a specific `sign` request is allowed now.
## Rules
```rego
import future.keywords.contains
import future.keywords.if
import future.keywords.in
actor_types := {"adm", "agt", "atm"}
decision := {"effect": "allow", "reason": "signing_policy_matched"} if {
allowed
} else := {"effect": "deny", "reason": first_denial} if {
true
}
allowed if {
input.action == "sign"
input.resource.system == "ops-warden"
input.resource.type == "ssh-certificate"
effective_tenant == "tenant:platform"
valid_actor_type
subject_type_matches_context
actor_type_matches_resource
resource_id_matches_actor
subject_id_allowed
valid_ttl
has_pubkey_fingerprint
principals_allowed
}
default effective_tenant := ""
effective_tenant := input.tenant if {
is_string(input.tenant)
input.tenant != ""
} else := input.resource.tenant if {
is_string(input.resource.tenant)
input.resource.tenant != ""
} else := input.subject.tenant if {
is_string(input.subject.tenant)
input.subject.tenant != ""
}
default first_denial := "no_matching_rule"
first_denial := "wrong_action" if {
input.action != "sign"
} else := "wrong_system" if {
input.resource.system != "ops-warden"
} else := "wrong_resource_type" if {
input.resource.type != "ssh-certificate"
} else := "wrong_tenant" if {
effective_tenant != "tenant:platform"
} else := "unknown_actor_resource" if {
not has_actor_resource
} else := "unknown_subject" if {
not subject_id_allowed
} else := "actor_type_mismatch" if {
not valid_actor_type
} else := "actor_type_mismatch" if {
not subject_type_matches_context
} else := "actor_type_mismatch" if {
not actor_type_matches_resource
} else := "actor_resource_mismatch" if {
not resource_id_matches_actor
} else := "ttl_out_of_bounds" if {
not valid_ttl
} else := "missing_pubkey_fingerprint" if {
not has_pubkey_fingerprint
} else := "missing_principal" if {
not has_principals
} else := "disallowed_principal" if {
count(disallowed_principals) > 0
}
has_actor_resource if {
is_string(input.resource.attributes.actor_id)
input.resource.attributes.actor_id != ""
}
valid_actor_type if {
is_string(input.context.actor_type)
input.context.actor_type in actor_types
}
subject_type_matches_context if {
input.subject.type == input.context.actor_type
}
subject_type_matches_context if {
input.subject.attributes.actor_type == input.context.actor_type
}
actor_type_matches_resource if {
input.context.actor_type == input.resource.attributes.actor_type
}
resource_id_matches_actor if {
input.resource.id == sprintf("ssh-cert:actor/%s", [input.resource.attributes.actor_id])
}
subject_id_allowed if {
input.subject.id in input.resource.attributes.allowed_subjects
}
has_ttl if {
is_number(input.context.ttl_hours)
}
valid_ttl if {
has_ttl
input.context.ttl_hours > 0
input.context.ttl_hours <= input.resource.attributes.max_ttl_hours
}
has_pubkey_fingerprint if {
is_string(input.context.pubkey_fingerprint)
input.context.pubkey_fingerprint != ""
}
has_principals if {
count(input.context.principals) > 0
}
principals_allowed if {
has_principals
count(disallowed_principals) == 0
}
allowed_principal(principal) if {
principal in input.resource.attributes.allowed_principals
}
disallowed_principals contains principal if {
principal := input.context.principals[_]
not allowed_principal(principal)
}
```
## Tests
```rego test
package flexauth.ops_warden.ssh_signing_test
import future.keywords.if
import data.flexauth.ops_warden.ssh_signing
adm_request := {
"id": "check:ops-warden-platform-steward-adm",
"tenant": "tenant:platform",
"subject": {"id": "platform-steward", "type": "adm"},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"system": "ops-warden",
"attributes": {
"actor_id": "platform-steward",
"actor_type": "adm",
"allowed_subjects": ["platform-steward", "iam:platform-steward"],
"allowed_principals": ["platform", "root"],
"max_ttl_hours": 8
}
},
"context": {
"actor_type": "adm",
"principals": ["platform"],
"pubkey_fingerprint": "SHA256:example-adm-fingerprint",
"ttl_hours": 4
}
}
test_adm_sign_allowed if {
ssh_signing.decision.effect == "allow" with input as adm_request
}
test_high_ttl_denied if {
ssh_signing.decision.reason == "ttl_out_of_bounds" with input as {
"tenant": "tenant:platform",
"subject": {"id": "platform-steward", "type": "adm"},
"action": "sign",
"resource": adm_request.resource,
"context": {
"actor_type": "adm",
"principals": ["platform"],
"pubkey_fingerprint": "SHA256:example-adm-fingerprint",
"ttl_hours": 12
}
}
}
test_missing_fingerprint_denied if {
ssh_signing.decision.reason == "missing_pubkey_fingerprint" with input as {
"tenant": "tenant:platform",
"subject": {"id": "platform-steward", "type": "adm"},
"action": "sign",
"resource": adm_request.resource,
"context": {
"actor_type": "adm",
"principals": ["platform"],
"ttl_hours": 4
}
}
}
```

View File

@@ -0,0 +1,450 @@
{
"systems": [
{
"id": "ops-warden",
"name": "Ops Warden",
"resource_types": [
{
"name": "ssh-certificate",
"scope_level": "Resource",
"planes": [
"Identity",
"Secret",
"Audit"
],
"metadata": {
"description": "Short-lived SSH certificate signing request."
}
}
],
"actions": [
{
"name": "sign",
"capabilities": [
"Use",
"Operate",
"Audit"
],
"planes": [
"Identity",
"Secret",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"metadata": {
"required_context": [
"principals",
"actor_type",
"pubkey_fingerprint",
"ttl_hours"
]
}
}
],
"caring_profiles": [
"caring-0.4.0-rc2"
],
"metadata": {
"flex_auth_contract": "protected-system-v0",
"ops_warden_policy_gate": "v2",
"policy_enabled_config": "policy.enabled",
"tenant": "tenant:platform"
}
}
],
"resource_manifests": [
{
"id": "ops-warden-ssh-certificates",
"system": "ops-warden",
"resources": [
{
"id": "ssh-cert:actor/adm-example",
"type": "ssh-certificate",
"labels": [
"ssh-signing",
"adm"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "adm-example",
"actor_type": "adm",
"allowed_subjects": [
"adm-example",
"iam:adm-example"
],
"allowed_principals": [
"adm-full"
],
"max_ttl_hours": 48
}
},
{
"id": "ssh-cert:actor/agt-codex-interhub-bootstrap",
"type": "ssh-certificate",
"labels": [
"ssh-signing",
"agt"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "agt-codex-interhub-bootstrap",
"actor_type": "agt",
"allowed_subjects": [
"agt-codex-interhub-bootstrap",
"iam:agt-codex-interhub-bootstrap"
],
"allowed_principals": [
"agt-interhub-bootstrap"
],
"max_ttl_hours": 2
}
},
{
"id": "ssh-cert:actor/agt-state-hub-bridge",
"type": "ssh-certificate",
"labels": [
"ssh-signing",
"agt"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "agt-state-hub-bridge",
"actor_type": "agt",
"allowed_subjects": [
"agt-state-hub-bridge",
"iam:agt-state-hub-bridge"
],
"allowed_principals": [
"agt-task-bridge"
],
"max_ttl_hours": 24
}
},
{
"id": "ssh-cert:actor/atm-backup-daily",
"type": "ssh-certificate",
"labels": [
"ssh-signing",
"atm"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "atm-backup-daily",
"actor_type": "atm",
"allowed_subjects": [
"atm-backup-daily",
"iam:atm-backup-daily"
],
"allowed_principals": [
"atm-backup-daily"
],
"max_ttl_hours": 8
}
}
],
"actions": [
"sign"
],
"caring_profile": "caring-0.4.0-rc2",
"metadata": {
"flex_auth_contract": "resource-registration-v0",
"tenant": "tenant:platform"
}
}
],
"tenants": [
{
"id": "tenant:platform",
"name": "Platform Tenant"
}
],
"subjects": [
{
"id": "adm-example",
"type": "Agent",
"display_name": "Example human operator \u2014 replace with per-person adm-* actors",
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
],
"groups": [
"group:ops-warden-admins"
],
"tenant": "tenant:platform",
"metadata": {
"actor_type": "adm"
}
},
{
"id": "agt-codex-interhub-bootstrap",
"type": "Agent",
"display_name": "Short-lived agent access for attended Inter-Hub bootstrap",
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
],
"groups": [
"group:ops-warden-agents"
],
"tenant": "tenant:platform",
"metadata": {
"actor_type": "agt"
}
},
{
"id": "agt-state-hub-bridge",
"type": "Agent",
"display_name": "ops-bridge tunnel agent for state-hub",
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
],
"groups": [
"group:ops-warden-agents"
],
"tenant": "tenant:platform",
"metadata": {
"actor_type": "agt"
}
},
{
"id": "atm-backup-daily",
"type": "Automation",
"display_name": "Example nightly automation actor",
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
],
"groups": [
"group:ops-warden-automations"
],
"tenant": "tenant:platform",
"metadata": {
"actor_type": "atm"
}
}
],
"groups": [
{
"id": "group:ops-warden-admins",
"display_name": "Ops Warden Admins",
"members": [
"adm-example"
],
"tenant": "tenant:platform"
},
{
"id": "group:ops-warden-agents",
"display_name": "Ops Warden Agents",
"members": [
"agt-codex-interhub-bootstrap",
"agt-state-hub-bridge"
],
"tenant": "tenant:platform"
},
{
"id": "group:ops-warden-automations",
"display_name": "Ops Warden Automations",
"members": [
"atm-backup-daily"
],
"tenant": "tenant:platform"
}
],
"relationships": [
{
"id": "rel:adm-example-sign-adm-example",
"system": "ops-warden",
"subject": "group:ops-warden-admins",
"relation": "signer",
"object": "ssh-cert:actor/adm-example",
"tenant": "tenant:platform",
"conditions": [
"TimeLimited",
"Logged"
],
"caring": {
"id": "descriptor:ops-warden-adm-signer",
"profile": "caring-0.4.0-rc2",
"subject_type": "Group",
"organization_relation": "ServiceProvider",
"canonical_role": "Operator",
"scope": {
"level": "Resource",
"id": "ssh-cert:actor/adm-example",
"tenant": "tenant:platform",
"resource": "ssh-cert:actor/adm-example"
},
"planes": [
"Identity",
"Secret",
"Audit"
],
"capabilities": [
"Use",
"Operate",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"conditions": [
"TimeLimited",
"Logged"
],
"restrictions": [
"PrivilegeEscalationBlocked",
"SecretAccessBlocked"
],
"access_path": "mediated"
}
},
{
"id": "rel:agt-codex-interhub-bootstrap-sign-agt-codex-interhub-bootstrap",
"system": "ops-warden",
"subject": "group:ops-warden-agents",
"relation": "signer",
"object": "ssh-cert:actor/agt-codex-interhub-bootstrap",
"tenant": "tenant:platform",
"conditions": [
"TimeLimited",
"Logged"
],
"caring": {
"id": "descriptor:ops-warden-agt-signer",
"profile": "caring-0.4.0-rc2",
"subject_type": "Group",
"organization_relation": "ServiceProvider",
"canonical_role": "Operator",
"scope": {
"level": "Resource",
"id": "ssh-cert:actor/agt-codex-interhub-bootstrap",
"tenant": "tenant:platform",
"resource": "ssh-cert:actor/agt-codex-interhub-bootstrap"
},
"planes": [
"Identity",
"Secret",
"Audit"
],
"capabilities": [
"Use",
"Operate",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"conditions": [
"TimeLimited",
"Logged"
],
"restrictions": [
"PrivilegeEscalationBlocked",
"SecretAccessBlocked"
],
"access_path": "mediated"
}
},
{
"id": "rel:agt-state-hub-bridge-sign-agt-state-hub-bridge",
"system": "ops-warden",
"subject": "group:ops-warden-agents",
"relation": "signer",
"object": "ssh-cert:actor/agt-state-hub-bridge",
"tenant": "tenant:platform",
"conditions": [
"TimeLimited",
"Logged"
],
"caring": {
"id": "descriptor:ops-warden-agt-signer",
"profile": "caring-0.4.0-rc2",
"subject_type": "Group",
"organization_relation": "ServiceProvider",
"canonical_role": "Operator",
"scope": {
"level": "Resource",
"id": "ssh-cert:actor/agt-state-hub-bridge",
"tenant": "tenant:platform",
"resource": "ssh-cert:actor/agt-state-hub-bridge"
},
"planes": [
"Identity",
"Secret",
"Audit"
],
"capabilities": [
"Use",
"Operate",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"conditions": [
"TimeLimited",
"Logged"
],
"restrictions": [
"PrivilegeEscalationBlocked",
"SecretAccessBlocked"
],
"access_path": "mediated"
}
},
{
"id": "rel:atm-backup-daily-sign-atm-backup-daily",
"system": "ops-warden",
"subject": "group:ops-warden-automations",
"relation": "signer",
"object": "ssh-cert:actor/atm-backup-daily",
"tenant": "tenant:platform",
"conditions": [
"TimeLimited",
"Logged"
],
"caring": {
"id": "descriptor:ops-warden-atm-signer",
"profile": "caring-0.4.0-rc2",
"subject_type": "Group",
"organization_relation": "ServiceProvider",
"canonical_role": "Operator",
"scope": {
"level": "Resource",
"id": "ssh-cert:actor/atm-backup-daily",
"tenant": "tenant:platform",
"resource": "ssh-cert:actor/atm-backup-daily"
},
"planes": [
"Identity",
"Secret",
"Audit"
],
"capabilities": [
"Use",
"Operate",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"conditions": [
"TimeLimited",
"Logged"
],
"restrictions": [
"PrivilegeEscalationBlocked",
"SecretAccessBlocked"
],
"access_path": "mediated"
}
}
]
}

View File

@@ -0,0 +1,36 @@
id: ops-warden
name: Ops Warden
resource_types:
- name: ssh-certificate
scope_level: Resource
planes:
- Identity
- Secret
- Audit
metadata:
description: Short-lived SSH certificate signing request.
actions:
- name: sign
capabilities:
- Use
- Operate
- Audit
planes:
- Identity
- Secret
- Audit
exposure_modes:
- Metadata
metadata:
required_context:
- principals
- actor_type
- pubkey_fingerprint
- ttl_hours
caring_profiles:
- caring-0.4.0-rc2
metadata:
flex_auth_contract: protected-system-v0
ops_warden_policy_gate: v2
policy_enabled_config: policy.enabled
tenant: tenant:platform

View File

@@ -0,0 +1,366 @@
{
"systems": [
{
"id": "ops-warden",
"name": "Ops Warden",
"resource_types": [
{
"name": "ssh-certificate",
"scope_level": "Resource",
"planes": [
"Identity",
"Secret",
"Audit"
],
"metadata": {
"description": "Short-lived SSH certificate signing request."
}
}
],
"actions": [
{
"name": "sign",
"capabilities": [
"Use",
"Operate",
"Audit"
],
"planes": [
"Identity",
"Secret",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"metadata": {
"required_context": [
"principals",
"actor_type",
"pubkey_fingerprint",
"ttl_hours"
]
}
}
],
"caring_profiles": [
"caring-0.4.0-rc2"
],
"metadata": {
"flex_auth_contract": "protected-system-v0",
"ops_warden_policy_gate": "v2",
"policy_enabled_config": "policy.enabled",
"tenant": "tenant:platform"
}
}
],
"resource_manifests": [
{
"id": "ops-warden-ssh-certificates",
"system": "ops-warden",
"resources": [
{
"id": "ssh-cert:actor/platform-steward",
"type": "ssh-certificate",
"labels": [
"ssh-signing",
"adm"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "platform-steward",
"actor_type": "adm",
"allowed_subjects": [
"platform-steward",
"iam:platform-steward"
],
"allowed_principals": [
"platform",
"root"
],
"max_ttl_hours": 8
}
},
{
"id": "ssh-cert:actor/ci-deploy-agent",
"type": "ssh-certificate",
"labels": [
"ssh-signing",
"agt"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "ci-deploy-agent",
"actor_type": "agt",
"allowed_subjects": [
"ci-deploy-agent",
"iam:ci-deploy-agent"
],
"allowed_principals": [
"deploy",
"git"
],
"max_ttl_hours": 2
}
},
{
"id": "ssh-cert:actor/backup-automation",
"type": "ssh-certificate",
"labels": [
"ssh-signing",
"atm"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "backup-automation",
"actor_type": "atm",
"allowed_subjects": [
"backup-automation",
"iam:backup-automation"
],
"allowed_principals": [
"backup"
],
"max_ttl_hours": 1
}
}
],
"actions": [
"sign"
],
"caring_profile": "caring-0.4.0-rc2",
"metadata": {
"flex_auth_contract": "resource-registration-v0",
"tenant": "tenant:platform"
}
}
],
"tenants": [
{
"id": "tenant:platform",
"name": "Platform Tenant"
}
],
"subjects": [
{
"id": "platform-steward",
"type": "Agent",
"display_name": "Platform Steward",
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
],
"groups": [
"group:ops-warden-admins"
],
"tenant": "tenant:platform",
"metadata": {
"actor_type": "adm"
}
},
{
"id": "ci-deploy-agent",
"type": "Agent",
"display_name": "CI Deploy Agent",
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
],
"groups": [
"group:ops-warden-agents"
],
"tenant": "tenant:platform",
"metadata": {
"actor_type": "agt"
}
},
{
"id": "backup-automation",
"type": "Automation",
"display_name": "Backup Automation",
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
],
"groups": [
"group:ops-warden-automations"
],
"tenant": "tenant:platform",
"metadata": {
"actor_type": "atm"
}
}
],
"groups": [
{
"id": "group:ops-warden-admins",
"display_name": "Ops Warden Admin Actors",
"members": [
"platform-steward"
],
"tenant": "tenant:platform"
},
{
"id": "group:ops-warden-agents",
"display_name": "Ops Warden Agent Actors",
"members": [
"ci-deploy-agent"
],
"tenant": "tenant:platform"
},
{
"id": "group:ops-warden-automations",
"display_name": "Ops Warden Automation Actors",
"members": [
"backup-automation"
],
"tenant": "tenant:platform"
}
],
"relationships": [
{
"id": "rel:platform-steward-sign-platform-steward",
"system": "ops-warden",
"subject": "group:ops-warden-admins",
"relation": "signer",
"object": "ssh-cert:actor/platform-steward",
"tenant": "tenant:platform",
"conditions": [
"TimeLimited",
"Logged"
],
"caring": {
"id": "descriptor:ops-warden-adm-signer",
"profile": "caring-0.4.0-rc2",
"subject_type": "Group",
"organization_relation": "ServiceProvider",
"canonical_role": "Operator",
"scope": {
"level": "Resource",
"id": "ssh-cert:actor/platform-steward",
"tenant": "tenant:platform",
"resource": "ssh-cert:actor/platform-steward"
},
"planes": [
"Identity",
"Secret",
"Audit"
],
"capabilities": [
"Use",
"Operate",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"conditions": [
"TimeLimited",
"Logged"
],
"restrictions": [
"PrivilegeEscalationBlocked",
"SecretAccessBlocked"
],
"access_path": "mediated"
}
},
{
"id": "rel:ci-deploy-agent-sign-ci-deploy-agent",
"system": "ops-warden",
"subject": "group:ops-warden-agents",
"relation": "signer",
"object": "ssh-cert:actor/ci-deploy-agent",
"tenant": "tenant:platform",
"conditions": [
"TimeLimited",
"Logged"
],
"caring": {
"id": "descriptor:ops-warden-agt-signer",
"profile": "caring-0.4.0-rc2",
"subject_type": "Group",
"organization_relation": "ServiceProvider",
"canonical_role": "Operator",
"scope": {
"level": "Resource",
"id": "ssh-cert:actor/ci-deploy-agent",
"tenant": "tenant:platform",
"resource": "ssh-cert:actor/ci-deploy-agent"
},
"planes": [
"Identity",
"Secret",
"Audit"
],
"capabilities": [
"Use",
"Operate",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"conditions": [
"TimeLimited",
"Logged"
],
"restrictions": [
"PrivilegeEscalationBlocked",
"SecretAccessBlocked"
],
"access_path": "mediated"
}
},
{
"id": "rel:backup-automation-sign-backup-automation",
"system": "ops-warden",
"subject": "group:ops-warden-automations",
"relation": "signer",
"object": "ssh-cert:actor/backup-automation",
"tenant": "tenant:platform",
"conditions": [
"TimeLimited",
"Logged"
],
"caring": {
"id": "descriptor:ops-warden-atm-signer",
"profile": "caring-0.4.0-rc2",
"subject_type": "Group",
"organization_relation": "ServiceProvider",
"canonical_role": "Operator",
"scope": {
"level": "Resource",
"id": "ssh-cert:actor/backup-automation",
"tenant": "tenant:platform",
"resource": "ssh-cert:actor/backup-automation"
},
"planes": [
"Identity",
"Secret",
"Audit"
],
"capabilities": [
"Use",
"Operate",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"conditions": [
"TimeLimited",
"Logged"
],
"restrictions": [
"PrivilegeEscalationBlocked",
"SecretAccessBlocked"
],
"access_path": "mediated"
}
}
]
}

View File

@@ -0,0 +1,59 @@
id: ops-warden-ssh-certificates
system: ops-warden
resources:
- id: ssh-cert:actor/platform-steward
type: ssh-certificate
labels:
- ssh-signing
- adm
trust_zone: platform
owner: team:platform-security
attributes:
actor_id: platform-steward
actor_type: adm
allowed_subjects:
- platform-steward
- iam:platform-steward
allowed_principals:
- platform
- root
max_ttl_hours: 8
- id: ssh-cert:actor/ci-deploy-agent
type: ssh-certificate
labels:
- ssh-signing
- agt
trust_zone: platform
owner: team:platform-security
attributes:
actor_id: ci-deploy-agent
actor_type: agt
allowed_subjects:
- ci-deploy-agent
- iam:ci-deploy-agent
allowed_principals:
- deploy
- git
max_ttl_hours: 2
- id: ssh-cert:actor/backup-automation
type: ssh-certificate
labels:
- ssh-signing
- atm
trust_zone: platform
owner: team:platform-security
attributes:
actor_id: backup-automation
actor_type: atm
allowed_subjects:
- backup-automation
- iam:backup-automation
allowed_principals:
- backup
max_ttl_hours: 1
actions:
- sign
caring_profile: caring-0.4.0-rc2
metadata:
flex_auth_contract: resource-registration-v0
tenant: tenant:platform

View File

@@ -0,0 +1,54 @@
id: subjects:ops-warden-platform
tenants:
- id: tenant:platform
name: Platform Tenant
subjects:
- id: platform-steward
type: Agent
display_name: Platform Steward
organization_relation: ServiceProvider
roles:
- Operator
groups:
- group:ops-warden-admins
tenant: tenant:platform
metadata:
actor_type: adm
- id: ci-deploy-agent
type: Agent
display_name: CI Deploy Agent
organization_relation: ServiceProvider
roles:
- Operator
groups:
- group:ops-warden-agents
tenant: tenant:platform
metadata:
actor_type: agt
- id: backup-automation
type: Automation
display_name: Backup Automation
organization_relation: ServiceProvider
roles:
- Operator
groups:
- group:ops-warden-automations
tenant: tenant:platform
metadata:
actor_type: atm
groups:
- id: group:ops-warden-admins
display_name: Ops Warden Admin Actors
members:
- platform-steward
tenant: tenant:platform
- id: group:ops-warden-agents
display_name: Ops Warden Agent Actors
members:
- ci-deploy-agent
tenant: tenant:platform
- id: group:ops-warden-automations
display_name: Ops Warden Automation Actors
members:
- backup-automation
tenant: tenant:platform

100
examples/topaz/README.md Normal file
View File

@@ -0,0 +1,100 @@
# Topaz alignment example
Runnable validation for the alignment commitment in ADR-003 and the
mapping recorded in `docs/topaz-mapping-spike.md`. Boots Topaz, seeds
a directory shaped like flex-auth's canonical vocabulary, and probes
three permission scenarios.
## Quick start
```sh
cd examples/topaz
docker compose up --abort-on-container-exit --exit-code-from probe
```
Expected outcome (exit code 0):
```
probe-1 | probe: steward-allow OK (check=true)
probe-1 | probe: reader-allow OK (check=true)
probe-1 | probe: outsider-deny OK (check=false)
probe-1 | probe: all checks passed
```
Tear down:
```sh
docker compose down -v
```
## What the example proves
- Topaz's v3 manifest can express flex-auth's canonical object types
(`user`, `identity`, `group`, `tenant`, `knowledge_base`, `document`)
and relations (`identifier`, `member`, `parent`, `owner_team`,
`reader`, `steward`).
- The Markitect fixture data
(`examples/markitect/resource_manifest.yaml`, mirrored here) seeds
the directory without translation.
- Group→reader edges (`reader:platform-architecture` group with a
`member` relation, plus a `reader` relation from the document to
that group with `subject_relation=member`) resolve correctly via
the manifest's `reader | group#member` union.
- The `check` decision is fully derivable from directory data for the
read-path case; no Rego is involved.
## File map
```
manifest.yaml # Topaz v3 directory manifest
policy/markitect.documents.rego # Rego module showing flex-auth's
# canonical input shape (used by the
# standalone evaluator; FLEX-WP-0004
# T01 will bridge to Topaz's input)
bundle/ # OPA bundle loaded into Topaz
bundle/.manifest # OPA bundle root manifest
bundle/policy/markitect.documents.rego # same Rego, mounted into Topaz
data/objects.json # seed objects
data/relations.json # seed relations
cfg/config.yaml # Topaz config
scripts/seed.sh # writes manifest + objects + relations
scripts/probe.sh # three directory checks via REST
docker-compose.yml # topaz, seed (one-shot), probe (one-shot)
```
## Ports
When running, Topaz exposes (on `127.0.0.1` only):
| Port | Service |
| --- | --- |
| 8282 | authorizer gRPC |
| 8383 | authorizer REST |
| 9292 | directory gRPC (reader, writer, model, exporter, importer) |
| 9393 | directory REST gateway |
| 9494 | health |
| 9696 | metrics |
Plaintext HTTP on the gateways. Internal gRPC uses TLS with
auto-generated self-signed certs in a `topaz-certs` named volume; the
`remote_directory.insecure: true` flag tells the in-process clients to
accept them.
## Caveats
- Plaintext gateways are for the spike only. Real deployments use
certs everywhere; see `docs/topaz-mapping-spike.md` §"Wire-Protocol
Candidates" for the production posture.
- The probe deliberately uses the directory `check` API instead of the
authorizer `is` API. Bridging flex-auth's Rego input shape into
Topaz's raw authorizer input is the Topaz adapter's job
(FLEX-WP-0004 T01) and is intentionally out of scope for this
validation. See `docs/topaz-mapping-spike.md` §"Implementation Notes
Surfaced By The Spike".
## Pinned Topaz version
`ghcr.io/aserto-dev/topaz:latest` as resolved on 2026-05-16
(digest `sha256:11fa7e2075870f3fe523afaadd942a6559b612f44b6bdb1296fe65299f5831fa`).
FLEX-WP-0004 T01 will pin a specific tagged version once the adapter
lands.

View File

@@ -0,0 +1 @@
{"roots":["flexauth/markitect/documents"]}

View File

@@ -0,0 +1,64 @@
package flexauth.markitect.documents
import future.keywords.if
import future.keywords.in
# This module is the Rego extracted from a flex-auth Rego-in-Markdown
# policy package (ADR-002). Identical bytes ship to the standalone
# evaluator and to Topaz; only the resolution of ds.* differs.
#
# Decision shape per ADR-002:
# decision := {"effect": "...", "reason": "...", "obligations": [...]}
# flex-auth wraps this into the canonical decision envelope.
default decision := {"effect": "deny", "reason": "no_matching_rule"}
# Reader on the document (direct or via group, or inherited from the
# parent knowledge_base) is allowed to read/query/search.
decision := {"effect": "allow", "reason": "reader_relation"} if {
input.action in {"read", "query", "search"}
input.resource.type == "document"
is_reader
}
# A steward on the document or parent may always read and may also
# export (which carries an audit-export obligation).
decision := {"effect": "allow", "reason": "steward_role"} if {
input.action in {"read", "query", "search"}
input.resource.type == "document"
is_steward
}
decision := {
"effect": "allow",
"reason": "steward_export",
"obligations": [{"type": "record_export_receipt"}],
} if {
input.action == "export"
input.resource.type == "document"
is_steward
}
# Helpers — these consult the directory shim (standalone) or Topaz's
# ds.* builtins (delegated). The standalone evaluator registers
# ds.check_relation / ds.check_permission with identical signatures.
is_reader if {
ds.check_relation({
"object_type": "document",
"object_id": input.resource.id,
"relation": "reader",
"subject_type": "user",
"subject_id": input.subject.id,
})
}
is_steward if {
ds.check_relation({
"object_type": "document",
"object_id": input.resource.id,
"relation": "steward",
"subject_type": "user",
"subject_id": input.subject.id,
})
}

View File

@@ -0,0 +1,107 @@
# Topaz config for the flex-auth alignment spike.
# Plaintext HTTP gateways for local convenience — never use this shape
# in production. See docs/topaz-mapping-spike.md.
version: 2
logging:
prod: false
log_level: info
directory:
db_path: /db/directory.db
request_timeout: 5s
seed_metadata: false
remote_directory:
address: "0.0.0.0:9292"
insecure: true
jwt:
acceptable_time_skew_seconds: 5
api:
health:
listen_address: "0.0.0.0:9494"
metrics:
listen_address: "0.0.0.0:9696"
services:
reader:
grpc:
listen_address: "0.0.0.0:9292"
certs:
tls_key_path: "/certs/grpc.key"
tls_cert_path: "/certs/grpc.crt"
tls_ca_cert_path: "/certs/grpc-ca.crt"
gateway:
listen_address: "0.0.0.0:9393"
allowed_origins:
- "*"
http: true
read_timeout: 2s
write_timeout: 2s
idle_timeout: 30s
writer:
needs: [reader]
grpc:
listen_address: "0.0.0.0:9292"
certs:
tls_key_path: "/certs/grpc.key"
tls_cert_path: "/certs/grpc.crt"
tls_ca_cert_path: "/certs/grpc-ca.crt"
gateway:
listen_address: "0.0.0.0:9393"
allowed_origins: ["*"]
http: true
model:
needs: [reader]
grpc:
listen_address: "0.0.0.0:9292"
certs:
tls_key_path: "/certs/grpc.key"
tls_cert_path: "/certs/grpc.crt"
tls_ca_cert_path: "/certs/grpc-ca.crt"
gateway:
listen_address: "0.0.0.0:9393"
allowed_origins: ["*"]
http: true
exporter:
needs: [reader]
grpc:
listen_address: "0.0.0.0:9292"
certs:
tls_key_path: "/certs/grpc.key"
tls_cert_path: "/certs/grpc.crt"
tls_ca_cert_path: "/certs/grpc-ca.crt"
importer:
needs: [reader]
grpc:
listen_address: "0.0.0.0:9292"
certs:
tls_key_path: "/certs/grpc.key"
tls_cert_path: "/certs/grpc.crt"
tls_ca_cert_path: "/certs/grpc-ca.crt"
authorizer:
needs: [reader]
grpc:
connection_timeout_seconds: 2
listen_address: "0.0.0.0:8282"
certs:
tls_key_path: "/certs/grpc.key"
tls_cert_path: "/certs/grpc.crt"
tls_ca_cert_path: "/certs/grpc-ca.crt"
gateway:
listen_address: "0.0.0.0:8383"
allowed_origins: ["*"]
http: true
read_timeout: 2s
write_timeout: 2s
idle_timeout: 30s
opa:
instance_id: "flex-auth-spike"
graceful_shutdown_period_seconds: 2
local_bundles:
paths:
- "/bundle"
skip_verification: true

View File

@@ -0,0 +1,23 @@
{
"objects": [
{"type": "tenant", "id": "platform"},
{"type": "group", "id": "team:platform-architecture", "display_name": "Platform Architecture"},
{"type": "group", "id": "reader:platform-architecture", "display_name": "Platform Architecture Readers"},
{"type": "user", "id": "alice@example.test", "display_name": "Alice (steward)"},
{"type": "identity", "id": "identity:alice@example.test", "properties": {"identifier": "alice@example.test", "subject": "alice@example.test"}},
{"type": "user", "id": "bob@example.test", "display_name": "Bob (reader)"},
{"type": "identity", "id": "identity:bob@example.test", "properties": {"identifier": "bob@example.test", "subject": "bob@example.test"}},
{"type": "user", "id": "eve@example.test", "display_name": "Eve (outsider)"},
{"type": "identity", "id": "identity:eve@example.test", "properties": {"identifier": "eve@example.test", "subject": "eve@example.test"}},
{
"type": "knowledge_base",
"id": "knowledge-base:markitect-example",
"properties": {"trust_zone": "public", "labels": ["public"]}
},
{
"type": "document",
"id": "document:internal-note",
"properties": {"trust_zone": "internal", "labels": ["internal"], "path": "examples/policy/private/internal-note.md"}
}
]
}

View File

@@ -0,0 +1,13 @@
{
"relations": [
{"object_type": "group", "object_id": "team:platform-architecture", "relation": "member", "subject_type": "user", "subject_id": "alice@example.test"},
{"object_type": "group", "object_id": "reader:platform-architecture", "relation": "member", "subject_type": "user", "subject_id": "bob@example.test"},
{"object_type": "identity", "object_id": "identity:alice@example.test", "relation": "identifier", "subject_type": "user", "subject_id": "alice@example.test"},
{"object_type": "identity", "object_id": "identity:bob@example.test", "relation": "identifier", "subject_type": "user", "subject_id": "bob@example.test"},
{"object_type": "identity", "object_id": "identity:eve@example.test", "relation": "identifier", "subject_type": "user", "subject_id": "eve@example.test"},
{"object_type": "knowledge_base", "object_id": "knowledge-base:markitect-example", "relation": "owner_team", "subject_type": "group", "subject_id": "team:platform-architecture"},
{"object_type": "document", "object_id": "document:internal-note", "relation": "parent", "subject_type": "knowledge_base", "subject_id": "knowledge-base:markitect-example"},
{"object_type": "document", "object_id": "document:internal-note", "relation": "steward", "subject_type": "user", "subject_id": "alice@example.test"},
{"object_type": "document", "object_id": "document:internal-note", "relation": "reader", "subject_type": "group", "subject_id": "reader:platform-architecture", "subject_relation": "member"}
]
}

View File

@@ -0,0 +1,68 @@
# Runnable Topaz example for the flex-auth alignment spike.
#
# Boot order:
# 1. topaz — runs topazd with the spike config; serves authorizer
# on :8282 (gRPC) and :8383 (REST), directory on :9292
# (gRPC) and :9393 (REST), health on :9494.
# 2. seed — one-shot container that pushes the manifest and seeds
# directory objects/relations via REST. Exits on success.
# 3. probe — one-shot container that runs three authorizer checks
# (steward allow, reader allow, outsider deny) and exits
# non-zero if any decision is unexpected.
#
# Usage:
# docker compose up --abort-on-container-exit --exit-code-from probe
#
# See docs/topaz-mapping-spike.md and README.md.
services:
topaz:
image: ghcr.io/aserto-dev/topaz:latest
command: ["run", "--config-file", "/cfg/config.yaml", "--bundle", "/bundle"]
ports:
- "127.0.0.1:8282:8282" # authorizer gRPC
- "127.0.0.1:8383:8383" # authorizer REST
- "127.0.0.1:9292:9292" # directory gRPC
- "127.0.0.1:9393:9393" # directory REST
- "127.0.0.1:9494:9494" # health
volumes:
- ./cfg:/cfg:ro
- ./bundle:/bundle:ro
- topaz-db:/db
- topaz-certs:/certs
healthcheck:
# Topaz's image has no curl/wget; nc is in busybox. Probe TCP on
# the authorizer REST port — the gateway only listens once the
# backing gRPC service is ready.
test: ["CMD-SHELL", "nc -z 127.0.0.1 8383 || exit 1"]
interval: 2s
timeout: 2s
retries: 30
seed:
image: alpine:3.20
depends_on:
topaz:
condition: service_healthy
volumes:
- ./data:/data:ro
- ./scripts:/scripts:ro
- ./manifest.yaml:/manifest.yaml:ro
entrypoint: ["/bin/sh", "/scripts/seed.sh"]
environment:
DIRECTORY_REST: "http://topaz:9393"
probe:
image: alpine:3.20
depends_on:
seed:
condition: service_completed_successfully
volumes:
- ./scripts:/scripts:ro
entrypoint: ["/bin/sh", "/scripts/probe.sh"]
environment:
AUTHORIZER_REST: "http://topaz:8383"
volumes:
topaz-db:
topaz-certs:

View File

@@ -0,0 +1,53 @@
# Topaz v3 manifest for the flex-auth alignment spike.
#
# Mirrors flex-auth's canonical resource/subject/group/relation
# vocabulary, scoped to the subset the Markitect internal-document
# fixture exercises. Reference: docs/topaz-mapping-spike.md.
#
# Notes on Topaz syntax:
# - relations: union types only ( | ) and group-member shorthand ( # ).
# - permissions: also support the parent-walk operator ( -> ).
# yaml-language-server: $schema=https://www.topaz.sh/schema/manifest.json
---
model:
version: 3
types:
user:
relations:
manager: user
identity:
relations:
identifier: user
group:
relations:
member: user | group#member
tenant:
relations:
member: user | group#member
knowledge_base:
relations:
tenant: tenant
owner_team: group
reader: user | group#member
steward: user | group#member
permissions:
read: reader | steward
admin: steward
document:
relations:
parent: knowledge_base
owner_team: group
reader: user | group#member
steward: user | group#member
permissions:
read: reader | steward | parent->read
query: reader | steward | parent->read
search: reader | steward | parent->read
export: steward
admin: steward | parent->admin

View File

@@ -0,0 +1,64 @@
package flexauth.markitect.documents
import future.keywords.if
import future.keywords.in
# This module is the Rego extracted from a flex-auth Rego-in-Markdown
# policy package (ADR-002). Identical bytes ship to the standalone
# evaluator and to Topaz; only the resolution of ds.* differs.
#
# Decision shape per ADR-002:
# decision := {"effect": "...", "reason": "...", "obligations": [...]}
# flex-auth wraps this into the canonical decision envelope.
default decision := {"effect": "deny", "reason": "no_matching_rule"}
# Reader on the document (direct or via group, or inherited from the
# parent knowledge_base) is allowed to read/query/search.
decision := {"effect": "allow", "reason": "reader_relation"} if {
input.action in {"read", "query", "search"}
input.resource.type == "document"
is_reader
}
# A steward on the document or parent may always read and may also
# export (which carries an audit-export obligation).
decision := {"effect": "allow", "reason": "steward_role"} if {
input.action in {"read", "query", "search"}
input.resource.type == "document"
is_steward
}
decision := {
"effect": "allow",
"reason": "steward_export",
"obligations": [{"type": "record_export_receipt"}],
} if {
input.action == "export"
input.resource.type == "document"
is_steward
}
# Helpers — these consult the directory shim (standalone) or Topaz's
# ds.* builtins (delegated). The standalone evaluator registers
# ds.check_relation / ds.check_permission with identical signatures.
is_reader if {
ds.check_relation({
"object_type": "document",
"object_id": input.resource.id,
"relation": "reader",
"subject_type": "user",
"subject_id": input.subject.id,
})
}
is_steward if {
ds.check_relation({
"object_type": "document",
"object_id": input.resource.id,
"relation": "steward",
"subject_type": "user",
"subject_id": input.subject.id,
})
}

66
examples/topaz/scripts/probe.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/bin/sh
# Probe the Topaz directory's Check API to verify the seeded manifest
# correctly resolves reader/steward/outsider permissions for the
# Markitect internal-document fixture. Exits 0 if all checks match
# expectations.
#
# This probe deliberately uses the directory Check API rather than the
# authorizer Is API. The manifest permissions are the substrate the
# Topaz adapter (FLEX-WP-0004 T01) and the standalone evaluator both
# consult; demonstrating it works end-to-end here is the spike's actual
# validation question. Bridging flex-auth's Rego input shape into
# Topaz's raw authorizer input is adapter work, intentionally out of
# this spike's scope (see docs/topaz-mapping-spike.md §"Implementation
# Notes").
set -eu
apk add --no-cache curl jq >/dev/null
DIR="${DIRECTORY_REST:-http://topaz:9393}"
echo "probe: directory REST = $DIR"
check() {
name="$1"
subject="$2"
resource="$3"
permission="$4"
expect="$5" # "true" or "false"
body=$(cat <<EOF
{
"object_type": "document",
"object_id": "$resource",
"relation": "$permission",
"subject_type": "user",
"subject_id": "$subject"
}
EOF
)
response=$(curl -sf -X POST "$DIR/api/v3/directory/check" \
-H 'Content-Type: application/json' \
-d "$body")
echo "probe: $name => $response"
got=$(echo "$response" | jq -r '.check')
if [ "$got" = "$expect" ]; then
echo "probe: $name OK (check=$got)"
else
echo "probe: $name FAIL (check=$got; expected=$expect)"
exit 1
fi
}
# Three scenarios on the seeded directory:
# 1. Alice is a steward on the document, so read should be permitted.
# 2. Bob is a member of reader:platform-architecture, which is the
# reader on the document via subject_relation=member, so read should
# be permitted via the reader|group#member union in the manifest.
# 3. Eve has no relation to the document, so read should be denied.
check "steward-allow" "alice@example.test" "document:internal-note" "read" "true"
check "reader-allow" "bob@example.test" "document:internal-note" "read" "true"
check "outsider-deny" "eve@example.test" "document:internal-note" "read" "false"
echo "probe: all checks passed"

43
examples/topaz/scripts/seed.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/bin/sh
# Seed the Topaz directory: push the manifest, then objects and relations.
# Uses Topaz's directory REST gateway. Exits 0 on success.
set -eu
apk add --no-cache curl jq >/dev/null
DIR="${DIRECTORY_REST:-http://topaz:9393}"
echo "seed: directory REST = $DIR"
# 1. Push the directory model (manifest).
echo "seed: setting model"
curl -sf -X POST "$DIR/api/v3/directory/manifest" \
-H 'Content-Type: application/yaml' \
--data-binary @/manifest.yaml \
|| curl -sf -X POST "$DIR/api/v3/model" \
-H 'Content-Type: application/yaml' \
--data-binary @/manifest.yaml
echo
# 2. Push objects.
echo "seed: writing objects"
jq -c '.objects[]' /data/objects.json | while IFS= read -r obj; do
curl -sf -X POST "$DIR/api/v3/directory/object" \
-H 'Content-Type: application/json' \
-d "{\"object\":$obj}" >/dev/null
printf '.'
done
echo
# 3. Push relations.
echo "seed: writing relations"
jq -c '.relations[]' /data/relations.json | while IFS= read -r rel; do
curl -sf -X POST "$DIR/api/v3/directory/relation" \
-H 'Content-Type: application/json' \
-d "{\"relation\":$rel}" >/dev/null
printf '.'
done
echo
echo "seed: done"

41
go.mod Normal file
View File

@@ -0,0 +1,41 @@
module github.com/netkingdom/flex-auth
go 1.22
require (
github.com/open-policy-agent/opa v0.70.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/agnivade/levenshtein v1.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/sys v0.26.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

154
go.sum Normal file
View File

@@ -0,0 +1,154 @@
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=
github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/open-policy-agent/opa v0.70.0 h1:B3cqCN2iQAyKxK6+GI+N40uqkin+wzIrM7YA60t9x1U=
github.com/open-policy-agent/opa v0.70.0/go.mod h1:Y/nm5NY0BX0BqjBriKUiV81sCl8XOjjvqQG7dXrggtI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=
github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

Some files were not shown because too many files have changed in this diff Show More