Skip to content

Commit 1e5558b

Browse files
authored
Merge pull request #34 from PredicateSystems/phase5i
support for additional actions
2 parents 68cc0c0 + d94f43f commit 1e5558b

File tree

7 files changed

+1259
-2
lines changed

7 files changed

+1259
-2
lines changed

docs/authorityd-operations.md

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,133 @@ Expected startup output:
665665
predicate-authorityd listening on http://127.0.0.1:8787 (mode=local_only)
666666
```
667667

668-
## 3) Endpoint checks
668+
## 3) API Endpoints
669+
670+
### Core Authorization
671+
672+
| Endpoint | Method | Description |
673+
|----------|--------|-------------|
674+
| `/v1/authorize` | POST | Core authorization check - returns mandate if allowed |
675+
| `/v1/delegate` | POST | Delegate mandate to sub-agent |
676+
| `/v1/execute` | POST | Execute operation via sidecar (zero-trust mode) |
677+
678+
### Operations
679+
680+
| Endpoint | Method | Description |
681+
|----------|--------|-------------|
682+
| `/health` | GET | Health check |
683+
| `/status` | GET | Stats and status |
684+
| `/metrics` | GET | Prometheus metrics |
685+
| `/policy/reload` | POST | Hot-reload policy |
686+
| `/ledger/flush-now` | POST | Trigger immediate audit flush |
687+
| `/ledger/dead-letter` | GET | Inspect quarantined events |
688+
| `/ledger/requeue` | POST | Requeue a dead-letter item |
689+
690+
---
691+
692+
## 3a) Execution Proxying (Zero-Trust Mode)
693+
694+
The `/v1/execute` endpoint enables **zero-trust execution** where the sidecar executes operations on behalf of agents. This prevents "confused deputy" attacks where an agent requests authorization for one resource but accesses another.
695+
696+
### Flow Comparison
697+
698+
```
699+
Traditional (Cooperative): Zero-Trust (Execution Proxy):
700+
┌─────────┐ authorize ┌─────────┐ ┌─────────┐ execute ┌─────────┐
701+
│ Agent │────────────▶│ Sidecar │ │ Agent │───────────▶│ Sidecar │
702+
│ │◀────────────│ │ │ │◀───────────│ │
703+
│ │ ALLOWED │ │ │ │ result │ (reads │
704+
│ │ │ │ │ │ │ file) │
705+
│ reads │ │ │ └─────────┘ └─────────┘
706+
│ file │ │ │
707+
│ (could │ │ │ Agent never touches the resource
708+
│ cheat) │ │ │ directly - sidecar is the executor
709+
└─────────┘ └─────────┘
710+
```
711+
712+
### Using Execute Proxy
713+
714+
**Step 1: Authorize and get a mandate**
715+
716+
```bash
717+
curl -X POST http://127.0.0.1:8787/v1/authorize \
718+
-H "Content-Type: application/json" \
719+
-d '{"principal":"agent:web","action":"fs.read","resource":"/src/index.ts"}'
720+
# Returns: {"allowed":true,"reason":"allowed","mandate_id":"m_abc123"}
721+
```
722+
723+
**Step 2: Execute through the sidecar**
724+
725+
```bash
726+
curl -X POST http://127.0.0.1:8787/v1/execute \
727+
-H "Content-Type: application/json" \
728+
-d '{
729+
"mandate_id": "m_abc123",
730+
"action": "fs.read",
731+
"resource": "/src/index.ts"
732+
}'
733+
# Returns: {"success":true,"result":{"type":"file_read","content":"...","size":1234,"content_hash":"sha256:..."}}
734+
```
735+
736+
### Supported Actions
737+
738+
| Action | Payload | Result |
739+
|--------|---------|--------|
740+
| `fs.read` | None | `FileRead { content, size, content_hash }` |
741+
| `fs.write` | `{ type: "file_write", content, create?, append? }` | `FileWrite { bytes_written, content_hash }` |
742+
| `fs.list` | None | `FileList { entries: [{ name, type, size, modified? }], total_entries }` |
743+
| `fs.delete` | `{ type: "file_delete", recursive? }` | `FileDelete { paths_removed }` |
744+
| `cli.exec` | `{ type: "cli_exec", command, args?, cwd?, timeout_ms? }` | `CliExec { exit_code, stdout, stderr, duration_ms }` |
745+
| `http.fetch` | `{ type: "http_fetch", method, headers?, body? }` | `HttpFetch { status_code, headers, body, body_hash }` |
746+
| `env.read` | `{ type: "env_read", keys: ["VAR_NAME"] }` | `EnvRead { values: { "VAR_NAME": "..." } }` |
747+
748+
### Security Guarantees
749+
750+
- **Mandate validation**: Mandate must exist and not be expired
751+
- **Action matching**: Requested action must match mandate's action
752+
- **Resource scope**: Requested resource must match mandate's resource scope
753+
- **Audit trail**: All executions logged to proof ledger with evidence hashes
754+
- **Recursive delete safety**: `fs.delete` with `recursive: true` requires explicit policy allowlist
755+
- **Env var filtering**: `env.read` only returns values for explicitly authorized keys
756+
757+
### SDK Integration
758+
759+
**Python:**
760+
761+
```python
762+
from predicate_authority import SidecarClient, AuthorizeAndExecuteOptions
763+
764+
async with SidecarClient() as client:
765+
# Combined authorize + execute in one call
766+
response = await client.authorize_and_execute(
767+
AuthorizeAndExecuteOptions(
768+
principal="agent:web",
769+
action="fs.read",
770+
resource="/src/index.ts"
771+
)
772+
)
773+
print(response.result.content)
774+
```
775+
776+
**TypeScript:**
777+
778+
```typescript
779+
import { AuthorityClient } from "@predicatesystems/authority";
780+
781+
const client = new AuthorityClient({ baseUrl: "http://127.0.0.1:8787" });
782+
783+
// Combined authorize + execute
784+
const response = await client.authorizeAndExecute({
785+
principal: "agent:web",
786+
action: "fs.read",
787+
resource: "/src/index.ts",
788+
});
789+
console.log(response.result?.content);
790+
```
791+
792+
---
793+
794+
## 3b) Endpoint checks
669795

670796
### Health
671797

docs/predicate-authority-user-manual.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,80 @@ Expected delegation path output:
470470

471471
---
472472

473+
## Execution Proxying (Zero-Trust Mode)
474+
475+
The `/v1/execute` endpoint enables **zero-trust execution** where the sidecar executes operations on behalf of agents. This prevents "confused deputy" attacks where an agent requests authorization for one resource but accesses another.
476+
477+
### Why Zero-Trust?
478+
479+
In cooperative mode, the agent asks for permission and then executes the operation itself. A compromised agent could authorize `fs.read /safe/file` but actually read `/etc/passwd`. In zero-trust mode, the sidecar executes the operation, ensuring the authorized resource is what gets accessed.
480+
481+
### Using Execute Proxy
482+
483+
```python
484+
from predicate_authority import SidecarClient, AuthorizeAndExecuteOptions
485+
486+
async with SidecarClient() as client:
487+
# Combined authorize + execute in one call
488+
response = await client.authorize_and_execute(
489+
AuthorizeAndExecuteOptions(
490+
principal="agent:web",
491+
action="fs.read",
492+
resource="/src/index.ts"
493+
)
494+
)
495+
print(response.result.content) # File content from sidecar
496+
```
497+
498+
Or step-by-step:
499+
500+
```python
501+
from predicate_authority import SidecarClient
502+
from predicate_contracts import ExecuteRequest
503+
504+
async with SidecarClient() as client:
505+
# Step 1: Authorize
506+
auth = await client.authorize(
507+
principal="agent:web",
508+
action="fs.read",
509+
resource="/src/index.ts"
510+
)
511+
512+
if not auth.allowed:
513+
raise RuntimeError(f"Denied: {auth.reason}")
514+
515+
# Step 2: Execute through sidecar
516+
result = await client.execute(ExecuteRequest(
517+
mandate_id=auth.mandate_id,
518+
action="fs.read",
519+
resource="/src/index.ts"
520+
))
521+
522+
print(result.result.content) # File content
523+
```
524+
525+
### Supported Actions
526+
527+
| Action | Payload | Result |
528+
|--------|---------|--------|
529+
| `fs.read` | None | `FileReadResult { content, size, content_hash }` |
530+
| `fs.write` | `FileWritePayload { content, create?, append? }` | `FileWriteResult { bytes_written, content_hash }` |
531+
| `fs.list` | None | `FileListResult { entries, total_entries }` |
532+
| `fs.delete` | `FileDeletePayload { recursive? }` | `FileDeleteResult { paths_removed }` |
533+
| `cli.exec` | `CliExecPayload { command, args?, cwd?, timeout_ms? }` | `CliExecResult { exit_code, stdout, stderr, duration_ms }` |
534+
| `http.fetch` | `HttpFetchPayload { method, headers?, body? }` | `HttpFetchResult { status_code, headers, body, body_hash }` |
535+
| `env.read` | `EnvReadPayload { keys }` | `EnvReadResult { values }` |
536+
537+
### Security Guarantees
538+
539+
- Mandate must exist and not be expired
540+
- Requested action/resource must match mandate
541+
- All executions logged to proof ledger with evidence hashes
542+
- `fs.delete` with `recursive: true` requires explicit policy allowlist
543+
- `env.read` only returns values for explicitly authorized keys
544+
545+
---
546+
473547
## Local identity registry + flush queue
474548

475549
Enable ephemeral task identity registry and local ledger queue:

predicate_authority/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@
5454
is_sidecar_available,
5555
run_sidecar,
5656
)
57+
from predicate_authority.sidecar_client import AuthorizationResponse as SidecarAuthorizationResponse
58+
from predicate_authority.sidecar_client import (
59+
AuthorizeAndExecuteOptions,
60+
SidecarClient,
61+
SidecarClientConfig,
62+
SidecarClientError,
63+
)
5764
from predicate_authority.telemetry import OpenTelemetryTraceEmitter
5865
from predicate_authority.verify import (
5966
ActualOperation,
@@ -135,4 +142,10 @@
135142
"get_sidecar_version",
136143
"is_sidecar_available",
137144
"run_sidecar",
145+
# Sidecar HTTP client (Phase 5: Execution Proxying)
146+
"AuthorizeAndExecuteOptions",
147+
"SidecarAuthorizationResponse",
148+
"SidecarClient",
149+
"SidecarClientConfig",
150+
"SidecarClientError",
138151
]

0 commit comments

Comments
 (0)