Skip to content

Commit 4ff705c

Browse files
author
Nick Ficano
committed
Implement provisioned credentials v1.1
Closes #15
1 parent ff17347 commit 4ff705c

37 files changed

Lines changed: 1062 additions & 46 deletions

ARCP.slnx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@
1313
<Project Path="tests/Arcp.UnitTests/Arcp.UnitTests.fsproj" />
1414
<Project Path="tests/Arcp.IntegrationTests/Arcp.IntegrationTests.fsproj" />
1515
</Folder>
16+
<Folder Name="/samples/">
17+
<Project Path="samples/ProvisionedCredentials/ProvisionedCredentials.fsproj" />
18+
<Project Path="samples/LiteLLM/LiteLLM.fsproj" />
19+
</Folder>
1620
</Solution>

CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,16 @@ Initial public release of the F# ARCP SDK against
2626

2727
**Feature coverage**
2828

29-
All nine flag-gated features ship: `heartbeat`, `ack`, `list_jobs`,
29+
All eleven flag-gated features ship: `heartbeat`, `ack`, `list_jobs`,
3030
`subscribe`, `lease_expires_at`, `cost.budget`, `progress`,
31-
`result_chunk`, `agent_versions`.
31+
`result_chunk`, `agent_versions`, `model.use`, `provisioned_credentials`.
32+
33+
- `model.use` lease capability validates model-tier operations through
34+
the existing glob lease path.
35+
- `provisioned_credentials` adds `Credential` wire types,
36+
`ICredentialProvisioner`, per-job credential issue/revoke tracking,
37+
accepted-job client exposure, rotation status events, and list-job
38+
redaction.
3239

3340
**Conventions**
3441

CONFORMANCE.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# ARCP F# SDK Conformance
2+
3+
This SDK targets `spec/docs/draft-arcp-1.1.md`.
4+
5+
## v1.1 Feature Coverage
6+
7+
- `model.use` lease capability: supported through `Capabilities.ModelUse`,
8+
`Lease.validateLeaseOp`, and `Lease.isSubset`.
9+
- `provisioned_credentials` feature flag: negotiated only when a runtime has
10+
an `ICredentialProvisioner` configured.
11+
- Provisioned credential wire shape: `Credential` and
12+
`CredentialConstraints` serialize through `Json.Options`.
13+
- Credential lifecycle: runtime issues credentials before `job.accepted`,
14+
exposes them on submitter `JobHandle.Credentials`, emits
15+
`credential_rotated` status events, and revokes tracked ids on terminal job
16+
states.
17+
- Credential confidentiality: `JobSummary`, `session.list_jobs`, and
18+
`job.subscribed` omit credential values.

README.md

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,9 @@ var result = await handle.Result;
114114
115115
## Feature support
116116

117-
All nine flag-gated features ship by default:
117+
All eleven flag-gated features ship by default. The runtime advertises
118+
`model.use` and `provisioned_credentials` only when an
119+
`ICredentialProvisioner` and `ICredentialStore` are configured.
118120

119121
- `heartbeat``session.ping` / `session.pong`
120122
- `ack``session.ack`; auto-ack scheduler (32 events / 250 ms)
@@ -125,10 +127,27 @@ All nine flag-gated features ship by default:
125127
- `progress``progress` event body
126128
- `result_chunk` — streamed `result_chunk` events + reassembly
127129
- `agent_versions``name@version`; rich agent inventory in `session.welcome`
130+
- `model.use` — model-tier lease namespace validated through the same glob path
131+
- `provisioned_credentials` — lease-bound credentials on `job.accepted`
132+
133+
### Provisioned credentials
134+
135+
```fsharp
136+
let options =
137+
{ ArcpServerOptions.defaults with
138+
Provisioner = Some myProvisioner
139+
CredentialStore = Some (InMemoryCredentialStore() :> ICredentialStore) }
140+
let server = ArcpServer(options)
141+
```
142+
143+
Provisioners implement `ICredentialProvisioner` and return wire-shaped
144+
`Credential` records. `JobHandle.Credentials` exposes the accepted-job
145+
snapshot to the submitter only; `session.list_jobs` and `job.subscribed`
146+
do not include credential values.
128147

129148
## Samples
130149

131-
Twenty-two runnable F# samples under [`samples/`](./samples) — one per
150+
Twenty-four runnable F# samples under [`samples/`](./samples) — one per
132151
feature, plus host-integration samples for ASP.NET Core, Giraffe, and
133152
OpenTelemetry. Each sample is a single `Program.fs` paired with a small
134153
shared harness.
@@ -138,6 +157,7 @@ dotnet run --project samples/QuickStart
138157
dotnet run --project samples/SubmitAndStream
139158
dotnet run --project samples/CostBudget
140159
dotnet run --project samples/AgentVersions
160+
dotnet run --project samples/ProvisionedCredentials
141161
dotnet run --project samples/AspNetCore # listens on http://127.0.0.1:7878/arcp
142162
```
143163

@@ -157,10 +177,11 @@ dotnet test ARCP.slnx
157177
```
158178

159179
Unit tests (xUnit + FsCheck) cover envelope round-trip, codec dispatch,
160-
lease/glob/budget arithmetic, chunk assembly, and feature-set property
161-
laws. Integration tests boot a paired client + runtime over the in-memory
162-
transport and exercise handshake, job lifecycle, idempotency, subscribe,
163-
list-jobs, lease expiry, and budget exhaustion.
180+
lease/glob/budget arithmetic, credential JSON shape, chunk assembly, and
181+
feature-set property laws. Integration tests boot a paired client + runtime
182+
over the in-memory transport and exercise handshake, job lifecycle,
183+
idempotency, subscribe, list-jobs, lease expiry, budget exhaustion,
184+
provisioned credentials, credential rotation, and revocation.
164185

165186
## Architecture
166187

docs/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# ARCP F# SDK Docs
2+
3+
## Start here
4+
5+
- [Leases and provisioned credentials](guides/leases.md)
6+
7+
## Reference
8+
9+
- [Root conformance summary](../CONFORMANCE.md)
10+
- [Project README](../README.md)

docs/guides/leases.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Leases And Provisioned Credentials
2+
3+
ARCP leases are immutable grants from capability namespace to glob patterns.
4+
The F# SDK exposes them as `LeaseGrant` and validates operations with
5+
`JobContext.ValidateOpAsync`.
6+
7+
```fsharp
8+
let lease =
9+
Lease.empty
10+
|> Lease.withCapability Capabilities.ModelUse [ "tier-fast/*" ]
11+
|> Lease.withCapability Capabilities.CostBudget [ "USD:1.00" ]
12+
13+
do! ctx.ValidateOpAsync(
14+
Capabilities.ModelUse,
15+
"tier-fast/gpt-4o-mini",
16+
ctx.CancellationToken)
17+
```
18+
19+
`model.use` is a normal lease namespace, so it follows the same matching and
20+
subsetting behavior as other glob capabilities. A child lease may keep or
21+
narrow the parent model set, but may not add a model pattern the parent did
22+
not grant.
23+
24+
Provisioned credentials are enabled by configuring both
25+
`ArcpServerOptions.Provisioner` and `ArcpServerOptions.CredentialStore`.
26+
When a job is accepted, the runtime calls the provisioner after the lease is
27+
finalized and before `job.accepted` is sent. Returned credentials are attached
28+
to `job.accepted.payload.credentials` and exposed on `JobHandle.Credentials`
29+
for the submitting client.
30+
31+
Credential values are secrets. The SDK does not include credentials in
32+
`JobSummary`, `session.list_jobs`, or `job.subscribed`. On terminal job states
33+
the runtime asks the provisioner to revoke every tracked credential, with
34+
bounded retry through the configured `ICredentialStore`.

samples/LiteLLM/LiteLLM.fsproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<ItemGroup>
3+
<Compile Include="..\_common\SampleHarness.fs" Link="SampleHarness.fs" />
4+
<Compile Include="Program.fs" />
5+
</ItemGroup>
6+
<ItemGroup>
7+
<ProjectReference Include="..\..\src\Arcp\Arcp.fsproj" />
8+
</ItemGroup>
9+
</Project>

samples/LiteLLM/Program.fs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
module ArcpSamples.LiteLLM
2+
3+
open System
4+
open System.Linq
5+
open System.Net.Http
6+
open System.Net.Http.Headers
7+
open System.Text
8+
open System.Text.Json
9+
open System.Threading
10+
open System.Threading.Tasks
11+
open ARCP.Core
12+
open ARCP.Runtime
13+
open ArcpSamples.SampleHarness
14+
15+
type LiteLLMProvisioner(baseUrl: Uri, adminKey: string, http: HttpClient) =
16+
let modelPatterns (lease: LeaseGrant) =
17+
Map.tryFind Capabilities.ModelUse lease.Capabilities
18+
|> Option.defaultValue []
19+
20+
let maxBudget (lease: LeaseGrant) =
21+
Map.tryFind Capabilities.CostBudget lease.Capabilities
22+
|> Option.bind (
23+
List.tryPick (fun amount ->
24+
match Lease.parseBudgetAmount amount with
25+
| Ok (_, value) -> Some value
26+
| Error _ -> None))
27+
28+
let postJsonAsync (path: string) (body: obj) (ct: CancellationToken) =
29+
task {
30+
use request = new HttpRequestMessage(HttpMethod.Post, Uri(baseUrl, path))
31+
request.Headers.Authorization <- AuthenticationHeaderValue("Bearer", adminKey)
32+
request.Content <-
33+
new StringContent(
34+
JsonSerializer.Serialize body,
35+
Encoding.UTF8,
36+
"application/json")
37+
let! response = http.SendAsync(request, ct)
38+
response.EnsureSuccessStatusCode() |> ignore
39+
return! response.Content.ReadAsStringAsync ct
40+
}
41+
42+
interface ICredentialProvisioner with
43+
member _.IssueAsync(ctx, ct) =
44+
task {
45+
let body =
46+
{| key_alias = ctx.JobId.Value
47+
duration =
48+
ctx.LeaseConstraints
49+
|> Option.map (fun c -> c.ExpiresAt - DateTimeOffset.UtcNow)
50+
|> Option.map (fun span -> max 1 (int span.TotalSeconds))
51+
|> Option.defaultValue 300
52+
models = modelPatterns ctx.Lease |> List.toArray
53+
max_budget = maxBudget ctx.Lease |> Option.toNullable |}
54+
:> obj
55+
let! raw = postJsonAsync "/key/generate" body ct
56+
use doc = JsonDocument.Parse raw
57+
let root = doc.RootElement
58+
let value =
59+
match root.TryGetProperty("key") with
60+
| true, p -> p.GetString()
61+
| _ ->
62+
match root.TryGetProperty("token") with
63+
| true, p -> p.GetString()
64+
| _ -> null
65+
if String.IsNullOrWhiteSpace value then
66+
return raise (InvalidOperationException "LiteLLM response did not include key or token")
67+
else
68+
let credential: Credential = {
69+
Id = CredentialId.newId ()
70+
Scheme = "bearer"
71+
Value = value
72+
Endpoint = baseUrl.ToString().TrimEnd('/')
73+
Profile = Some "litellm"
74+
Constraints =
75+
Some {
76+
CostBudget = Map.tryFind Capabilities.CostBudget ctx.Lease.Capabilities
77+
ModelUse = Map.tryFind Capabilities.ModelUse ctx.Lease.Capabilities
78+
ExpiresAt = ctx.LeaseConstraints |> Option.map (fun c -> c.ExpiresAt)
79+
}
80+
}
81+
return [ credential ]
82+
}
83+
84+
member _.RevokeAsync(credentialId, ct) =
85+
task {
86+
let body = {| key = credentialId |} :> obj
87+
let! _ = postJsonAsync "/key/delete" body ct
88+
return true
89+
}
90+
91+
[<EntryPoint>]
92+
let main _argv =
93+
runAsync (fun () ->
94+
task {
95+
let baseUrl = Environment.GetEnvironmentVariable "LITELLM_BASE_URL"
96+
let adminKey = Environment.GetEnvironmentVariable "LITELLM_ADMIN_KEY"
97+
if String.IsNullOrWhiteSpace baseUrl || String.IsNullOrWhiteSpace adminKey then
98+
writeLine "Set LITELLM_BASE_URL and LITELLM_ADMIN_KEY to run this sample."
99+
return 0
100+
else
101+
use http = new HttpClient()
102+
let provisioner =
103+
LiteLLMProvisioner(Uri(baseUrl), adminKey, http) :> ICredentialProvisioner
104+
let withLiteLLM (options: ArcpServerOptions) =
105+
{ options with
106+
Provisioner = Some provisioner
107+
CredentialStore = Some (InMemoryCredentialStore() :> ICredentialStore) }
108+
let features =
109+
Set.ofList [
110+
Features.ProvisionedCredentials
111+
Features.ModelUse
112+
Features.LeaseExpiresAt
113+
]
114+
let! p =
115+
connectWithOptions
116+
withLiteLLM
117+
(fun s ->
118+
s.RegisterAgent("llm", fun ctx ->
119+
task {
120+
do! ctx.ValidateOpAsync(
121+
Capabilities.ModelUse,
122+
"gpt-4o-mini",
123+
ctx.CancellationToken)
124+
return jsonString "LiteLLM credential issued"
125+
}))
126+
features
127+
let lease =
128+
Lease.empty
129+
|> Lease.withCapability Capabilities.ModelUse [ "gpt-4o-mini" ]
130+
|> Lease.withCapability Capabilities.CostBudget [ "USD:1.00" ]
131+
let! handle =
132+
p.Client.SubmitAsync(
133+
{ Agent = "llm"
134+
Input = jsonInt 0
135+
LeaseRequest = Some lease
136+
LeaseConstraints = Some { ExpiresAt = DateTimeOffset.UtcNow.AddMinutes 10.0 }
137+
IdempotencyKey = None
138+
MaxRuntimeSec = None },
139+
CancellationToken.None)
140+
writeLine (sprintf "issued %d LiteLLM credential(s)" handle.Credentials.Length)
141+
let! _ = handle.Result
142+
do! teardown p
143+
return 0
144+
})

0 commit comments

Comments
 (0)