feat(server): add access control - #1398
Conversation
…s-control-implementation
…ess-control-implementation
…s-control-implementation
…s-control-implementation
…ess-control-implementation
…s-control-implementation # Conflicts: # tests/test_transport.py
…s-control-implementation
…s-control-implementation # Conflicts: # src/powercontext/builtin/runtime/application.py # src/powercontext/builtin/runtime/composition.py # src/powercontext/client/__init__.py # src/powercontext/client/client.py # src/powercontext/server/app.py
…s-control-implementation # Conflicts: # docs/en/docs/reference/configuration.md # docs/zh/docs/reference/configuration.md # integrations/dsh/plugins/powercontext/lib/index.js # integrations/dsh/plugins/powercontext/openapi/powercontext.yaml # integrations/dsh/plugins/powercontext/src/operations.generated.ts # integrations/opencode/plugins/powercontext/lib/index.js # integrations/opencode/plugins/powercontext/src/operations.generated.ts # integrations/pi/plugins/powercontext/src/operations.generated.ts # openapi/powercontext.yaml # src/powercontext/builtin/persistence/cursors.py # src/powercontext/client/__init__.py # src/powercontext/client/client.py # src/powercontext/http/__init__.py # src/powercontext/http/_generated/models.py # src/powercontext/http/_generated/operations.py # src/powercontext/http/_generated/schema.py # src/powercontext/server/app.py # src/powercontext/server/factory.py # src/powercontext/server/static/skills.js # src/powercontext/server/templates/pages/skills.html # src/powercontext/server/web.py # tests/e2e/real_experience_skill/harness.py # tests/test_api_contract.py # tests/test_dashboard.py
|
…s-control-implementation # Conflicts: # integrations/dsh/plugins/powercontext/lib/index.js # integrations/dsh/plugins/powercontext/openapi/powercontext.yaml # integrations/dsh/plugins/powercontext/src/operations.generated.ts # integrations/opencode/plugins/powercontext/lib/index.js # integrations/opencode/plugins/powercontext/src/operations.generated.ts # integrations/pi/plugins/powercontext/src/operations.generated.ts # openapi/powercontext.yaml # scripts/generate_api.py # src/powercontext/builtin/artifacts/handoff/service.py # src/powercontext/client/client.py # src/powercontext/http/__init__.py # src/powercontext/http/_generated/models.py # src/powercontext/http/_generated/operations.py # src/powercontext/http/_generated/schema.py # src/powercontext/server/app.py # src/powercontext/server/settings.py # src/powercontext/server/web.py # tests/test_api_contract.py # tests/test_client.py # tests/test_dashboard.py
|
Following up from #1395 — congrats on landing this so quickly, the boundary design looks clean (single PEP, fail-closed modes, data-minimized audit are all the right calls). Since the Fit check against the built-in provider's model. The PR's hierarchical RBAC (subject / action / resource / context + role bindings) maps 1:1 onto casbin's
Concrete offer: I'm happy to prototype a One question: for the |
|
@tlyyxjz Thanks for the thoughtful offer. This PR already includes an embedded CasbinAuthorizationProvider and open_casbin_access_control. In the first version, the canonical relational Access repository remains the source of truth for bindings and ownership; the adapter materializes active relationships into a fresh Casbin enforcer for decisions instead of maintaining a second persisted Casbin policy store. RelationshipWriter is intentionally separate from AuthorizationProvider. An external decision provider may supply a paired writer and declare relationship_management=true; without one, binding mutations fail with relationship_management_unavailable and relationships are administered in the external system. Receiver and other bindings are therefore not restricted to the built-in store. Also, handoff.receiver targets the logical Handoff identity across revisions, rather than an exact-revision grant. I clarified this composition and source-of-truth boundary in f6d2600. A native Casbin-backed writer could be a useful follow-up if it preserves the same idempotency, versioning, ownership, audit, and safe-filtering contracts. |
…s-control-implementation # Conflicts: # docs/en/docs/reference/configuration.md # docs/zh/docs/reference/configuration.md # integrations/dsh/plugins/powercontext/lib/index.js # integrations/dsh/plugins/powercontext/src/operations.generated.ts # integrations/opencode/plugins/powercontext/src/operations.generated.ts # integrations/pi/plugins/powercontext/src/operations.generated.ts # openapi/powercontext.yaml # scripts/generate_api.py # src/powercontext/client/client.py # src/powercontext/http/__init__.py # src/powercontext/http/_generated/models.py # src/powercontext/http/_generated/operations.py # src/powercontext/http/_generated/schema.py # src/powercontext/server/app.py # src/powercontext/server/factory.py # tests/test_client.py # uv.lock
…s-control-implementation
|
Thanks for the detailed breakdown — that composition makes sense: enforcer materialized fresh from the relational source of truth on each decision cycle avoids the dual-write consistency problem entirely. Two follow-ups on the native Casbin-backed RelationshipWriter you mentioned:
If the spec looks like a fit, we're glad to prototype a writer + filtered-adapter sync and open a draft PR for review. |
|
@tlyyxjz Thanks for offering to work on this. I'd be happy to review a separate draft PR for a native Casbin writer. Starting with the contract and a small working example makes sense. The most useful references are:
For the Handoff identity, this is the resource we use: ResourceRef.artifact(
"scope-a",
family="handoff",
artifact_id="handoff-a",
)Its canonical key includes the scope, family, and artifact ID. It has no revision field. A On idempotency, Your suggestion to keep writes behind the existing service boundary works for authorization checks and audit emission. The storage side still has to implement the version checks and atomic replacement. For For filtering, For the proposed sync, please describe where the binding IDs, request metadata, versions, and ownership records would live, and how writes become visible to decisions. I want to avoid ending up with two independently writable copies of the same relationship. A short explanation of the storage layout and failure behavior would help us review that before the implementation gets large. I'd start the prototype with one Handoff: establish its owner, grant Bob receiver access, publish another revision, replace Bob with Alice, retry that replacement, then revoke it. Run the permission checks and resource listing throughout, including a caller with no grant and a Handoff in another scope. That would give us something concrete to review early. The |
PsiACE
left a comment
There was a problem hiding this comment.
I noticed two behaviors in resources/list:
- An Experience or Skill created through the base Artifact API can be shared and read by the grantee, but it does not appear in
/v1/access/resources/listbecause discovery only scans approved candidates. - A
server.admincalling/v1/access/resources/listforscope.adminScopes receives a 503 because the server-level parent constraint is rejected, even though this action/resource combination is accepted by validation.
Could you confirm whether these are intended limitations for this version? Other than these, LGTM.
|
@PsiACE Thanks for checking these paths. I've fixed the discovery gaps in c71d370c. For Experience and Skill, I reproduced the missing entries when the caller inherited read access through a Scope role. Direct The I added HTTP regressions for both the built-in and Casbin compositions. They cover both Artifact families, inherited and direct grants, pagination and totals, duplicate removal when grants overlap, isolation from other Scopes, and management without content access. All six new cases failed before the fix and pass now. Validation:
|
|
Thanks — this is exactly the context we needed. Before writing the writer, here is the storage layout and failure behavior, per your question. The short version: the canonical relational Access schema stays the only writable copy of relationships; the Casbin side is derived decision state that is materialized inside the same transaction as the mutation. There is never a second independently writable relationship copy. Where each artifact lives
The idempotency ledger answers the three cases you called out:
Write path and visibility
Failure behavior
One deliberate consequence: because we keep the RFC's "materialize fresh per evaluation" model, Prototype conformance suite (the Handoff scenario you outlined)
Open questions before we cut the draft PR
Happy to adjust the layout based on your review — the draft PR follows once the storage shape is agreed. |
|
@tlyyxjz Thanks for spelling out the storage layout and failure cases. The canonical relational store plus an enforcer built at evaluation time is the direction I would keep. Please target the existing five-table Access schema, reuse the current repository writer, and make the additional behavior of the prototype explicit. A separate Casbin policy table or another idempotency table would add storage without a demonstrated need here. I checked your proposal against the current PR head, 1. The idempotency ledger belongs to the existing canonical Access repository. It already exists as
The physical binding table is therefore Two details matter for the ledger:
Expiry also needs the existing singleton-slot behavior: an expired binding remains available as history while its slot can be claimed by a new binding. A later operation on the old binding must not release or overwrite the successor's slot. This is already handled within the relationship table and does not need a separate claims table. 2. Reuse or extract the existing filter derivation; there is no standalone public helper yet. Today, the built-in provider and Casbin provider each implement The shared behavior needs to cover exact grants, inherited parent constraints, formal ownership, scope/family boundaries, MemoryEntry selectors, expiry, and trusted group membership when the composition supports it. Candidate proposer attestation must not become formal Artifact ownership, and The resulting filter must constrain the resource query before counting, ordering, and pagination. Keep the fail-closed behavior when a complete filter cannot be produced. 3. Keep the canonical revision namespace for this composition. When built-in and Casbin evaluate the same canonical snapshot with the same trusted membership context, they should report the same revision. There is no reason to introduce a separate Casbin counter here. Preserve the existing The current adapter comparison test supports equivalence against a stable store. It does not establish consistency under concurrent writes, or require two independently timed reads to return the same revision when a mutation occurs between them. The transaction and materialization timing need one correction. Your opening says derived decision state is materialized inside the mutation transaction, while the storage table says the enforcer is built fresh from committed state at evaluation time. For the latter model, the guarantees should be described as follows:
Pre-commit validation could be added if there is a concrete need, but a successfully constructed temporary enforcer is not a guarantee that every later evaluation will succeed. The failure table should distinguish those two timings. There is also an existing read-consistency gap in our current implementation that we should acknowledge explicitly. Both providers read The grant is committed and legitimate, but the returned revision labels the wrong policy state. This is an existing limitation in our read path, not evidence that your proposed writer has introduced an authorization bypass. It does mean that reading only committed rows is insufficient to claim a consistent decision snapshot. Please make the read boundary explicit for point checks, batch checks, and resource filters. A consistent database snapshot covering revision, bindings, and ownership is one option, provided the backend isolation actually supplies that guarantee. A bounded revision-check-and-retry approach is another, with fail-closed behavior if a stable read cannot be obtained. Expiry should use one captured evaluation time. A regression that deliberately interleaves a mutation with these reads would provide stronger evidence than sequential adapter parity alone. For the draft PR, there is an existing composition to build on: Your eight-step Handoff scenario is a useful starting point. I would extend the shared conformance coverage with:
The current I would welcome a focused draft along those lines, using the existing five-table layout and stating exactly which consistency guarantees the implementation and tests establish. |
|
Thanks — this review is exactly what the design needed. All three answers adopted, the correction to the consistency wording accepted, and the read-consistency gap you demonstrated is real and needs an explicit boundary. Point-by-point: 1. Ledger: reuse
|
Which issue or RFC does this PR close?
Implements #1395.
Design reference: #1396.
Rationale for this change
The current optional static Bearer token authenticates one deployment-local caller but cannot express per-user, per-scope, or exact-Handoff visibility. Handoff Receipts also record receiver observations; they are not authorization grants.
This change adds an independent Server Access Control boundary so user A can grant user B least-privilege access to one committed Handoff Revision without exposing the surrounding scope. Runtime domain APIs remain free of Principal, role, and permission parameters, and deployments can replace the built-in decision provider and relationship store with an external authorization system.
What changes are included in this PR?
AuthorizationProvider,RelationshipWriter, andAccessAuditStoreboundaries for integrations such as OpenFGA, Casbin, Oso, or an AuthZEN-compatible PDP./v1/access/me, check, batch-check, safe resource listing, role listing, binding management, and audit APIs plus typed Python SDK methods.x-powercontext-accessrequirements and enforce them at one Server PEP before Runtime dependencies or business metrics.disabled,legacy-static-admin, andenforcedrollout modes; the default legacy mode preserves the existing static-token administrator behavior.Are there any user-facing changes?
Yes. Authenticated deployments gain public Access APIs, stable 403 behavior, exact-Handoff receiver grants, Access audit records, and two Access configuration settings. Existing static-token deployments remain compatible through the default
legacy-static-adminmode.The change adds Server-owned Access tables but does not add identity or ACL fields to Handoff, Source, Memory, Work, or other Runtime domain records.
scope_idremains a business partition rather than an authorization credential.How was this change tested?
PRE_COMMIT_HOME=/tmp/powercontext-prek-cache make check.venv/bin/python -m pytest -q -p no:cacheprovider(974 passed, 9 skipped)make contract-test(30 passed)make docs-test.envOceanBase, generation LLM, and Embedding provider:AI usage statement
OpenAI Codex (GPT-5) was used to inspect the repository contracts, implement the Access Control layer and generated API changes, diagnose the real OceanBase SAVEPOINT failure, and run the validation described above. The author directed the design and publication scope.