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. |
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.