Skip to content

Commit 1e82142

Browse files
Nick Ficanocursoragent
andcommitted
docs: fix audit documentation issues (#28, #34-37, #46, #56-59, #72)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 348e6f5 commit 1e82142

11 files changed

Lines changed: 52 additions & 36 deletions

File tree

docs/getting-started.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,17 @@ app.Run("http://localhost:7878")
141141
On the client side use `WebSocketClientTransport.connectAsync`, which
142142
opens a `ClientWebSocket`, attaches the bearer token (if any) as an
143143
`Authorization` header on the upgrade, and returns an `ITransport`.
144+
145+
> **Warning:** The WebSocket `Authorization` header does **not**
146+
> authenticate the ARCP session. Session auth is sent in
147+
> `session.hello.payload.auth` from `ArcpClientOptions.Auth`.
148+
> `ArcpClientOptions.defaults` sets `AuthScheme.None` (`auth.scheme =
149+
> "none"`), so passing a bearer token only to `connectAsync` leaves
150+
> the ARCP principal anonymous unless you also set
151+
> `Auth = AuthScheme.Bearer token` on the client options. Use
152+
> `defaults` only with runtimes that explicitly allow anonymous
153+
> sessions.
154+
144155
That header is host-layer metadata; ARCP session authentication still
145156
comes from `ArcpClientOptions.Auth`:
146157

docs/transports.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ let _ = server.HandleSessionAsync(serverT, CancellationToken.None)
1717
let client = new ArcpClient(clientT, ArcpClientOptions.defaults)
1818
```
1919

20+
> **Warning:** `ArcpClientOptions.defaults` sends `auth.scheme = "none"`.
21+
> Use it only with runtimes that allow anonymous sessions. For bearer
22+
> auth, set `Auth = AuthScheme.Bearer token` on the client options —
23+
> the WebSocket upgrade `Authorization` header alone does not
24+
> authenticate the ARCP session (see [WebSocket](#websocket) below).
25+
2026
Calling `CloseAsync` on either half completes both channels, ending
2127
the paired `Receive` enumerator on the other side.
2228

@@ -26,8 +32,12 @@ the paired `Receive` enumerator on the other side.
2632
The convenience constructor `connectAsync` opens a `ClientWebSocket`,
2733
adds the bearer token (if any) as the `Authorization` header on the
2834
upgrade, and returns an `ITransport`. Treat that header as host-layer
29-
metadata; ARCP session authentication still comes from
30-
`ArcpClientOptions.Auth`:
35+
metadata only — it does **not** authenticate the ARCP session.
36+
Session auth is sent in `session.hello.payload.auth` from
37+
`ArcpClientOptions.Auth`. When the runtime expects bearer auth, pass
38+
the same token to both `connectAsync` and
39+
`Auth = AuthScheme.Bearer token`; `ArcpClientOptions.defaults` sends
40+
`auth.scheme = "none"` and leaves the ARCP principal anonymous:
3141

3242
```fsharp
3343
open ARCP.Client.Transport

src/Arcp.Client/ArcpClient.fs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,10 @@ type ArcpClient(transport: ITransport, options: ArcpClientOptions) =
8181
let assembler = w.ChunkIndex.GetOrCreate rid
8282

8383
match assembler.Append(chunkSeq, data, enc, more) with
84-
| Ok _ -> w.Channel.Writer.TryWrite payload.Body |> ignore
84+
| Ok _ -> ()
8585
| Error err ->
86-
// Out-of-order or undecodable chunk: tear down the
87-
// handle so callers don't sit on a job that will never
88-
// produce a usable result.
86+
// Out-of-order or undecodable chunk: complete the
87+
// result task with this error and drop the handle.
8988
handles.TryRemove jid |> ignore
9089
w.Channel.Writer.TryComplete() |> ignore
9190
w.ResultSetter.TrySetResult(Error err) |> ignore

src/Arcp.Client/JobHandle.fs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ open ARCP.Client.Internal
1212
///
1313
/// `Events` is an `IAsyncEnumerable<JobEventBody>` over every non-
1414
/// `result_chunk` event; consumers iterate with `await foreach`
15-
/// (C#) or `for x in handle.Events do` (F#). `ResultBytes` returns
16-
/// the assembled bytes from any `result_chunk` stream associated
17-
/// with this job.
15+
/// (C#) or `for x in handle.Events do` (F#). `TryReadResultBytes`
16+
/// returns the assembled bytes for a given `ResultId` once the
17+
/// chunk stream has closed (`byte[] option`).
1818
///
1919
/// `Result` resolves once the terminating `job.result` / `job.error`
2020
/// arrives.

src/Arcp.Client/Transport/WebSocket.fs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,10 @@ type WebSocketClientTransport(socket: WebSocket, ownsSocket: bool, maxMessageByt
164164
module WebSocketClientTransport =
165165
/// Connect a new client transport to `uri`. The bearer token (if
166166
/// provided) is added as the `Authorization` header on the
167-
/// upgrade request. That header is host-layer metadata; ARCP
168-
/// session authentication is configured separately on
169-
/// `ArcpClientOptions`.
167+
/// upgrade request. That header is host-layer metadata only and
168+
/// does not authenticate the ARCP session — set
169+
/// `ArcpClientOptions.Auth = AuthScheme.Bearer token` for session
170+
/// auth (`ArcpClientOptions.defaults` sends `auth.scheme = "none"`).
170171
let connectAsync (uri: Uri) (bearerToken: string option) (ct: CancellationToken) : Task<ITransport> =
171172
task {
172173
let client = new ClientWebSocket()

src/Arcp.Core/Capabilities.fs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@ type AgentInventoryEntry =
5757
Default: string option
5858
}
5959

60-
/// Agent inventory advertised in `session.welcome.payload.capabilities.agents`.
61-
/// The runtime always emits the rich shape when `agent_versions` is
62-
/// in the negotiated feature set; otherwise the flat shape is used.
6360
/// Agent inventory advertised in
6461
/// `session.welcome.payload.capabilities.agents`. The flat shape is
6562
/// emitted when `agent_versions` is not in the negotiated feature

src/Arcp.Core/Errors.fs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@ namespace ARCP.Core
33
open System
44
open System.Text.Json
55

6-
/// Canonical ARCP error taxonomy (spec §12). Fifteen cases.
6+
/// Canonical ARCP error taxonomy (spec §12). Fifteen DU cases —
7+
/// every wire error code has exactly one DU arm.
78
///
8-
/// `retryable` follows §12. Three cases must be `retryable = false`
9-
/// because a naive retry will fail identically:
10-
/// `LeaseExpired`, `BudgetExhausted`, `AgentVersionNotAvailable`.
9+
/// Only `Timeout`, `HeartbeatLost`, and `InternalError` are retryable
10+
/// per §12; the remaining twelve arms are not.
1111
///
1212
/// F# consumers prefer `Result<_, ARCPError>` for expected outcomes.
1313
/// `ArcpException` (below) wraps the same value for C# callers and
1414
/// for fatal paths where exceptions are the idiom.
15-
/// Canonical ARCP error taxonomy (spec §12). Fifteen DU cases —
16-
/// every wire error code has exactly one DU arm.
1715
[<RequireQualifiedAccess>]
1816
type ARCPError =
1917
| PermissionDenied of message: string * details: JsonElement option
@@ -95,9 +93,6 @@ module ARCPError =
9593
| ARCPError.InvalidRequest(_, d) -> d
9694
| _ -> None
9795

98-
/// Exception form for C# interop and for fatal paths in F# where
99-
/// the spec-canonical surface is "throw with code". Carries the
100-
/// underlying `ARCPError`.
10196
/// Convenience alias for `ARCPError`. The protocol uses "ARCP"
10297
/// all-caps, so the spec-named type is `ARCPError`; `SdkError`
10398
/// is the F#-conventional name for callers who prefer it.

src/Arcp.Core/Json.fs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,13 @@ module internal JsonConverters =
329329
/// JSON configuration shared across the SDK.
330330
///
331331
/// The wire format (spec §5.1, §6, §7, §8) puts the discriminator as
332-
/// a top-level `type` field next to peer fields. Custom converters in
333-
/// `JsonConverters` pin the spec-mandated flat shapes for the unions
334-
/// that appear inside payloads.
332+
/// a top-level `type` field next to peer fields. `buildOptions` uses
333+
/// `WithUnionExternalTag()` keyed on `"type"`, combined with
334+
/// `WithUnionUnwrapRecordCases` and related unwrap options, to emit
335+
/// the flat `{ "type": "...", ...fields }` shape rather than a nested
336+
/// case wrapper. Custom converters in `JsonConverters` pin the
337+
/// spec-mandated flat shapes for the unions that appear inside
338+
/// payloads.
335339
[<RequireQualifiedAccess>]
336340
module Json =
337341
let private buildOptions () : JsonSerializerOptions =

src/Arcp.Runtime/ArcpServer.fs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,9 @@ type ArcpServer(options: ArcpServerOptions) =
170170
let registerHandler (name: string) (version: string) (h: ArcpAgentHandler) =
171171
agentHandlers.[name + "@" + version] <- h
172172
// The inventory stores an `AgentHandler` purely as a presence
173-
// marker — `JobSubmitFlow` discards it and dispatches via
174-
// `agentHandlers` keyed by `name@version`. The placeholder
175-
// raises so any regression that routes through it surfaces
176-
// loudly instead of returning a garbage JsonElement.
173+
// marker — `JobSubmitFlow` dispatches via `agentHandlers`
174+
// keyed by `name@version`. The placeholder raises if invoked
175+
// so routing regressions fail fast.
177176
let placeholder: AgentHandler =
178177
fun _ ->
179178
raise (

src/Arcp.Runtime/Internal/JobSubmitFlow.fs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,10 @@ module internal JobSubmitFlow =
278278
| Ok constraints ->
279279
let jobId = JobId.newId ()
280280

281-
// Claim the idempotency key first so a duplicate
282-
// submission short-circuits before any side effects
281+
// Claim the idempotency key before any side effects
283282
// (record registration, watchdog start, provisioner
284-
// call). Without this, two concurrent submits with
285-
// the same key both fell through and created jobs.
283+
// call) so concurrent duplicate submits collapse
284+
// to one job.
286285
let claimResult =
287286
match submit.IdempotencyKey with
288287
| Some key -> jobs.TryClaimIdempotencyKey(key, jobId)

0 commit comments

Comments
 (0)