agentgov-core 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ # --- secrets / credentials (NEVER commit) ---
2
+ .env
3
+ .env.*
4
+ # the template IS committed (no real secrets in it) — keep this negation on its own line;
5
+ # git does NOT support inline comments, so a trailing `# ...` here would break the un-ignore.
6
+ !.env.example
7
+ *.pem
8
+ *.key
9
+ *_secret*
10
+ **/secrets/**
11
+ .mcp.json # contains per-user OAuth; keep a .mcp.json.example instead
12
+ aws-credentials*
13
+ okta-token*
14
+ servicenow-creds*
15
+
16
+ # --- python ---
17
+ __pycache__/
18
+ *.py[cod]
19
+ .venv/
20
+ venv/
21
+ .pytest_cache/
22
+ *.egg-info/
23
+
24
+ # --- node / react ---
25
+ node_modules/
26
+ dist/
27
+ build/
28
+ .vite/
29
+ *.tsbuildinfo
30
+
31
+ # --- terraform (not used, but ignore if anyone adds it) ---
32
+ *.tfstate
33
+ *.tfstate.*
34
+ .terraform/
35
+
36
+ # --- os / editor ---
37
+ .DS_Store
38
+ .idea/
39
+ .vscode/*
40
+ !.vscode/extensions.json
41
+ *.log
42
+
43
+ # --- local dev harness (dev-only; see local/README.md) ---
44
+ local/identity/keys/
45
+ local/*.pid
46
+ local/*.log
47
+ local/.signoz_state.json
48
+
49
+ # --- local data / scratch (synthetic seed IS committed; local dumps are not) ---
50
+ /tmp/
51
+ *.local.json
52
+ seed/**/local-*
53
+ # large binaries — track via LFS or attach separately, not inline
54
+ docs/design/*.docx
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentgov-core
3
+ Version: 0.1.0
4
+ Summary: Shared invoke-boundary core for agent-governance-demo: host adapters, entitlement/policy resolution, the governance envelope, and the registry Postgres schema. Not published to PyPI yet — built as a local wheel and installed into registry-api/services-workflows images (Phase 5 §5.8).
5
+ Author-email: "receptor.bio" <oss@receptor.bio>
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: botocore
9
+ Requires-Dist: cryptography
10
+ Requires-Dist: fastapi
11
+ Requires-Dist: httpx
12
+ Requires-Dist: psycopg[binary]
13
+ Requires-Dist: pyjwt
14
+ Description-Content-Type: text/markdown
15
+
16
+ # agentgov-core
17
+
18
+ Shared invoke-boundary core for [`agent-governance-demo`](https://github.com/receptor-bio/agent-governance-demo):
19
+ per-platform host adapters (`hosts/`), the governance envelope (`envelope.py`), entitlement/policy
20
+ resolution (`openfga.py`, `cedar.py`), the registry Postgres access layer + schema (`db.py`,
21
+ `schema.sql`), and the shared URN/tuple helpers (`urns.py`, `tuples.py`) that `registry-api` and
22
+ `services/workflows` both consume — replacing the `sys.path` trick `services/workflows/activities.py`
23
+ previously used to reach `registry-api`'s app code directly.
24
+
25
+ **Not published to PyPI yet.** Built as a local wheel in each consuming Dockerfile's multi-stage
26
+ build, and installed with `pip install -e packages/agentgov-core` for local dev. See
27
+ `docs/PHASE_STATUS.md`'s Phase 5 §5.8 entry for the plan to publish it for real via PyPI Trusted
28
+ Publishing (the same OIDC pattern `receptor-bio/axiom-sc` already uses) — deferred, not blocking.
29
+
30
+ Not a general-purpose library: this package's boundary is driven entirely by what
31
+ `services/workflows/activities.py` and `services/registry-api/app/` actually import, not by any
32
+ external API-design goal.
@@ -0,0 +1,17 @@
1
+ # agentgov-core
2
+
3
+ Shared invoke-boundary core for [`agent-governance-demo`](https://github.com/receptor-bio/agent-governance-demo):
4
+ per-platform host adapters (`hosts/`), the governance envelope (`envelope.py`), entitlement/policy
5
+ resolution (`openfga.py`, `cedar.py`), the registry Postgres access layer + schema (`db.py`,
6
+ `schema.sql`), and the shared URN/tuple helpers (`urns.py`, `tuples.py`) that `registry-api` and
7
+ `services/workflows` both consume — replacing the `sys.path` trick `services/workflows/activities.py`
8
+ previously used to reach `registry-api`'s app code directly.
9
+
10
+ **Not published to PyPI yet.** Built as a local wheel in each consuming Dockerfile's multi-stage
11
+ build, and installed with `pip install -e packages/agentgov-core` for local dev. See
12
+ `docs/PHASE_STATUS.md`'s Phase 5 §5.8 entry for the plan to publish it for real via PyPI Trusted
13
+ Publishing (the same OIDC pattern `receptor-bio/axiom-sc` already uses) — deferred, not blocking.
14
+
15
+ Not a general-purpose library: this package's boundary is driven entirely by what
16
+ `services/workflows/activities.py` and `services/registry-api/app/` actually import, not by any
17
+ external API-design goal.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agentgov-core"
7
+ version = "0.1.0"
8
+ description = "Shared invoke-boundary core for agent-governance-demo: host adapters, entitlement/policy resolution, the governance envelope, and the registry Postgres schema. Not published to PyPI yet — built as a local wheel and installed into registry-api/services-workflows images (Phase 5 §5.8)."
9
+ readme = "README.md"
10
+ license = {text = "Apache-2.0"}
11
+ # >=3.10, not >=3.11: local dev's .venv (created by run_phase0.sh via `uv venv`) is 3.10.19 — Docker
12
+ # images (python:3.11-slim) satisfy this too, so this is the real floor, not the container version.
13
+ requires-python = ">=3.10"
14
+ authors = [{name = "receptor.bio", email = "oss@receptor.bio"}]
15
+ dependencies = [
16
+ "psycopg[binary]",
17
+ "httpx",
18
+ "fastapi",
19
+ "pyjwt",
20
+ "cryptography",
21
+ "botocore",
22
+ ]
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["src/agentgov_core"]
File without changes
@@ -0,0 +1,56 @@
1
+ """cedar-agent client (permitio/cedar-agent HTTP API).
2
+
3
+ Runs schemaless locally: we load the policy set and evaluate with an inline context
4
+ carrying { employment, classification }. A Cedar *forbid* overrides *permit*, which is
5
+ how the contractor+RESTRICTED denial works.
6
+ """
7
+ import re
8
+
9
+ import httpx
10
+
11
+ from .config import settings
12
+
13
+ TIMEOUT = 10.0
14
+
15
+
16
+ def split_statements(text: str) -> list[str]:
17
+ """cedar-agent stores ONE Cedar statement per policy entry. Strip // comments and
18
+ split the policy set on statement terminators (';' only ends statements here)."""
19
+ no_comments = "\n".join(re.sub(r"//.*$", "", line) for line in text.splitlines())
20
+ return [s.strip() + ";" for s in no_comments.split(";") if s.strip()]
21
+
22
+
23
+ class Cedar:
24
+ def __init__(self, base_url=None):
25
+ self.base = (base_url or settings.CEDAR_URL).rstrip("/")
26
+
27
+ def load_policies(self, policy_text: str | None = None):
28
+ """Replace the cedar-agent policy set with services/policy/policies.cedar."""
29
+ if policy_text is None:
30
+ with open(settings.CEDAR_POLICIES_PATH) as f:
31
+ policy_text = f.read()
32
+ # cedar-agent stores a list of {id, content}, one statement each. PUT replaces the set.
33
+ body = [{"id": f"agentgov_{i}", "content": s} for i, s in enumerate(split_statements(policy_text))]
34
+ r = httpx.put(f"{self.base}/v1/policies", json=body, timeout=TIMEOUT)
35
+ r.raise_for_status()
36
+ return r.json()
37
+
38
+ def is_authorized(self, principal_id: str, action: str, resource_id: str, context: dict,
39
+ resource_type: str = "Agent") -> bool:
40
+ """Return True on Allow. action is 'discover' | 'use'. ``resource_type`` is ``Agent``
41
+ or (Phase 2) ``McpServer`` — the classification/contractor policies are written
42
+ resource-agnostic on purpose, so a governed MCP server is judged by exactly the same
43
+ rules as an agent."""
44
+ body = {
45
+ "principal": f'User::"{principal_id}"',
46
+ "action": f'Action::"{action}"',
47
+ "resource": f'{resource_type}::"{resource_id}"',
48
+ "context": context,
49
+ }
50
+ r = httpx.post(f"{self.base}/v1/is_authorized", json=body, timeout=TIMEOUT)
51
+ r.raise_for_status()
52
+ decision = r.json().get("decision", "Deny")
53
+ return str(decision).lower() == "allow"
54
+
55
+
56
+ cedar = Cedar()
@@ -0,0 +1,128 @@
1
+ """Runtime configuration for registry-api (env-driven; local defaults for docker-compose)."""
2
+ import os
3
+
4
+
5
+ class Settings:
6
+ # Path-prefix awareness (BUILD_PLAN §9). Locally the api answers at /assets/registry/api.
7
+ ROOT_PATH = os.getenv("ROOT_PATH", "/assets/registry/api")
8
+
9
+ # Postgres (spine).
10
+ DB_URL = os.getenv(
11
+ "DB_URL", "postgresql://agentgov:dev@localhost:5432/agentgov"
12
+ )
13
+
14
+ # OpenFGA (spine). Store is resolved by name so ids need not be plumbed around.
15
+ OPENFGA_API_URL = os.getenv("OPENFGA_API_URL", "http://localhost:8080")
16
+ OPENFGA_STORE_NAME = os.getenv("OPENFGA_STORE_NAME", "agentgov")
17
+
18
+ # Cedar (spine).
19
+ CEDAR_URL = os.getenv("CEDAR_URL", "http://localhost:8180")
20
+ # Local-dev-only convenience default (Cedar.load_policies() reads this when called with no
21
+ # explicit policy_text, which is how registry-api's own startup calls it — see main.py).
22
+ # `services/policy/` is an outer-repo fixture this shared package has no business locating via
23
+ # __file__ trickery (that broke when this module moved out of services/registry-api/app/ in
24
+ # Phase 5 §5.8 — __file__ no longer sits inside the repo tree at a fixed depth). Relative-to-cwd
25
+ # is correct here because run_phase*.sh always launches registry-api's uvicorn process with cwd
26
+ # = services/registry-api (`cd services/registry-api && ... exec uvicorn ...`), never repo root.
27
+ # NOT reachable at all inside the deployed Docker image today (services/policy/ isn't copied in)
28
+ # — pre-existing gap, not introduced or fixed by this change; real deployments would need it
29
+ # added to the Dockerfile if/when this startup call needs to succeed there too.
30
+ CEDAR_POLICIES_PATH = os.getenv("CEDAR_POLICIES_PATH", "../policy/policies.cedar")
31
+
32
+ # Local mock-Okta JWT verification. In real Okta this is a JWKS URL; locally we verify
33
+ # against a PEM public key on disk (no network dependency for the demo). Same cwd-relative
34
+ # reasoning as CEDAR_POLICIES_PATH above — real deployments never hit this path at all, they
35
+ # set JWT_JWKS_URL instead.
36
+ JWT_PUBLIC_KEY_PATH = os.getenv("JWT_PUBLIC_KEY_PATH", "../../local/identity/keys/public.pem")
37
+ JWT_ISSUER = os.getenv("JWT_ISSUER", "https://mock-okta.receptor.local/oauth2")
38
+ JWT_AUDIENCE = os.getenv("JWT_AUDIENCE", "agent-registry")
39
+ # Real Okta: set JWT_JWKS_URL to the authz server's JWKS
40
+ # (https://<domain>/oauth2/<authServerId>/v1/keys) and JWT_ISSUER to its issuer.
41
+ # When set, tokens are verified against Okta's rotating keys instead of the local PEM;
42
+ # when empty (default), the local mock PEM path is used. Nothing else changes.
43
+ JWT_JWKS_URL = os.getenv("JWT_JWKS_URL", "")
44
+ # Allow disabling signature verification only for offline unit poking; default on.
45
+ JWT_VERIFY = os.getenv("JWT_VERIFY", "1") == "1"
46
+
47
+ # OTel. If no OTLP endpoint is set we still emit spans to stdout (ConsoleSpanExporter)
48
+ # so the Phase-0 DoD ("discover emits a trace w/ authz span-event") is observable locally.
49
+ OTLP_ENDPOINT = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "")
50
+ OTEL_CONSOLE = os.getenv("OTEL_CONSOLE", "1") == "1"
51
+ SERVICE_NAME = os.getenv("OTEL_SERVICE_NAME", "registry-api")
52
+ # Phase 3 — the SDK default (60s) makes SigNoz-backed usage assertions slow to observe
53
+ # locally; the local test harness shortens this the same way it shortens ACCESS_TTL_MINUTES.
54
+ OTEL_METRIC_EXPORT_INTERVAL_MS = int(os.getenv("OTEL_METRIC_EXPORT_INTERVAL_MS", "60000"))
55
+
56
+ # Phase 1 — Temporal (spine).
57
+ TEMPORAL_ADDRESS = os.getenv("TEMPORAL_ADDRESS", "localhost:7233")
58
+ TEMPORAL_TASK_QUEUE = os.getenv("TEMPORAL_TASK_QUEUE", "agentgov-access")
59
+
60
+ # Phase 1 — ServiceNow. Local dev talks to the mock shim (local/servicenow_mock);
61
+ # swap SERVICENOW_URL to the real PDI once provisioned, hook contract stays identical.
62
+ SERVICENOW_URL = os.getenv("SERVICENOW_URL", "http://localhost:8300")
63
+ # Shared secret authenticating the ServiceNow -> registry-api webhook (dev-only default;
64
+ # real deployments source this from AWS Secrets Manager per CLAUDE.md).
65
+ SERVICENOW_WEBHOOK_SECRET = os.getenv("SERVICENOW_WEBHOOK_SECRET", "local-dev-servicenow-secret")
66
+
67
+ # Governance policy knobs (BUILD_PLAN Phase 1 DoD: "10-min TTL expiry"). Configurable via
68
+ # env so the local test harness can shrink them for fast iteration; the demo/prod default
69
+ # is the real 10-minute TTL. Never accepted as a per-request client override.
70
+ ACCESS_TTL_MINUTES = float(os.getenv("ACCESS_TTL_MINUTES", "10"))
71
+ ACCESS_SLA_MINUTES = float(os.getenv("ACCESS_SLA_MINUTES", "60"))
72
+
73
+ # ---------------------------------------------------------------- Phase 1 seam: real ServiceNow
74
+ # Table-API auth for the real PDI (docs/setup/servicenow.md §4/§6). Empty = the local mock,
75
+ # which needs no auth and returns bare records; set both to switch to the real instance.
76
+ SERVICENOW_USER = os.getenv("SERVICENOW_USER", "")
77
+ SERVICENOW_PASSWORD = os.getenv("SERVICENOW_PASSWORD", "")
78
+
79
+ # ---------------------------------------------------------------- Phase 2: four hosts
80
+ # 'mock' points every adapter at local/hosts_mock (same contract, no cloud spend);
81
+ # 'real' points at the platforms themselves. Only URLs/credentials differ — BUILD_PLAN §Phase 2.
82
+ HOSTS_MODE = os.getenv("AGENTGOV_HOSTS_MODE", "mock")
83
+ HOSTS_MOCK_URL = os.getenv("AGENTGOV_HOSTS_MOCK_URL", "http://localhost:8400")
84
+
85
+ LITELLM_URL = os.getenv("LITELLM_URL", "") # real: http://litellm:4000
86
+ LITELLM_MASTER_KEY = os.getenv("LITELLM_MASTER_KEY", "sk-agentgov-local-master")
87
+
88
+ # AgentCore: the GATEWAY's credentials. Never the user's, and never a static key in real mode —
89
+ # app/hosts/agentcore.py::_gateway_credentials() resolves the ECS task role's temporary STS
90
+ # credentials from the ambient AWS chain instead (§5.7). These two are MOCK-ONLY fallbacks
91
+ # (local/hosts_mock doesn't touch real AWS, so fixed demo values are fine there).
92
+ AGENTCORE_URL = os.getenv("AGENTCORE_URL", "")
93
+ AGENTCORE_REGION = os.getenv("AGENTCORE_REGION", "us-east-1")
94
+ AGENTCORE_ACCESS_KEY_ID = os.getenv("AGENTCORE_ACCESS_KEY_ID", "AKIAAGENTGOVDEMO0000")
95
+ AGENTCORE_SECRET_ACCESS_KEY = os.getenv("AGENTCORE_SECRET_ACCESS_KEY", "agentgov-local-dev-agentcore-secret")
96
+
97
+ DATABRICKS_URL = os.getenv("DATABRICKS_URL", "") # real: https://<workspace>.cloud.databricks.com
98
+ DATABRICKS_UC_SECURABLE = os.getenv("DATABRICKS_UC_SECURABLE", "iops.manufacturing")
99
+ # Privileged admin bearer (PAT or OAuth token scoped to SCIM + Unity Catalog + Apps) the
100
+ # GATEWAY uses for JIT identity: SCIM CREATE USER + UC GRANT/REVOKE. Never a user token.
101
+ # Empty in mock (the mock host doesn't check it).
102
+ DATABRICKS_TOKEN = os.getenv("DATABRICKS_TOKEN", "")
103
+
104
+ SNOWFLAKE_URL = os.getenv("SNOWFLAKE_URL", "") # real: https://<account>.snowflakecomputing.com
105
+ SNOWFLAKE_ROLE_SCOPE = os.getenv("SNOWFLAKE_ROLE_SCOPE", "session:role:GD_CLINICAL_READER")
106
+ # Privileged key-pair for JIT CREATE USER / GRANT / REVOKE (the gateway's admin identity).
107
+ # Store the PRIVATE KEY, not a pre-minted JWT — key-pair JWTs expire in <=1h, so the adapter
108
+ # signs a fresh one per call (app/hosts/snowflake.py::_admin_jwt). Never a user token.
109
+ # Secret: agentgov/prod/snowflake-admin {account,user,private_key,private_key_passphrase}.
110
+ SNOWFLAKE_ADMIN_ACCOUNT = os.getenv("SNOWFLAKE_ADMIN_ACCOUNT", "") # e.g. AULMROR-NLC43968
111
+ SNOWFLAKE_ADMIN_USER = os.getenv("SNOWFLAKE_ADMIN_USER", "") # SECURITYADMIN-capable user
112
+ SNOWFLAKE_ADMIN_ROLE = os.getenv("SNOWFLAKE_ADMIN_ROLE", "SECURITYADMIN")
113
+ SNOWFLAKE_ADMIN_PRIVATE_KEY = os.getenv("SNOWFLAKE_ADMIN_PRIVATE_KEY", "") # PKCS8 PEM
114
+ SNOWFLAKE_ADMIN_PRIVATE_KEY_PASSPHRASE = os.getenv("SNOWFLAKE_ADMIN_PRIVATE_KEY_PASSPHRASE", "")
115
+
116
+ # HostDiscoverySync (Temporal Schedule). Shrunk by the local harness so a scheduled sync
117
+ # is observable during a test run; the demo/prod default is hourly.
118
+ HOST_SYNC_INTERVAL_MINUTES = float(os.getenv("HOST_SYNC_INTERVAL_MINUTES", "60"))
119
+ PULL_HOSTS = [h.strip() for h in os.getenv(
120
+ "AGENTGOV_PULL_HOSTS", "bedrock-agentcore,databricks,snowflake").split(",") if h.strip()]
121
+
122
+ # Browser origins allowed to call the API (the local registry-ui page; the deployed UI is
123
+ # same-origin behind the ALB path prefix, so this stays a dev convenience).
124
+ CORS_ORIGINS = [o.strip() for o in os.getenv(
125
+ "CORS_ORIGINS", "http://localhost:8500,http://127.0.0.1:8500").split(",") if o.strip()]
126
+
127
+
128
+ settings = Settings()