Skip to content

feat(rpc): read and write bucket policies over /s3/bucket/policy - #89

Merged
pyropy merged 13 commits into
srdjan/feat/iam-tenant-removal-revokesfrom
srdjan/feat/iam-policy-command
Oct 14, 2026
Merged

pyropy merged 13 commits into
srdjan/feat/iam-tenant-removal-revokesfrom
srdjan/feat/iam-policy-command

Conversation

@pyropy

@pyropy pyropy commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Serves S3 GetBucketPolicy, PutBucketPolicy and DeleteBucketPolicy as one Hilt command, /s3/bucket/policy, reached by service keys only. The PUT body must match the signed payload hash, and If-Match / If-None-Match are optional signed preconditions.

  • ?policy classified into three operations with their own permissions
  • pkg/rpc/service/bucket/policy.go: operation, body hash check, preconditions
  • Handler, wiring and failure mapping; client.Policy for Ingot
  • sigv4.PayloadHash and a test presign option
  • The operation acts on the authorized bucket's DID; an empty precondition is invalid
  • Deployments must delegate /s3/bucket/policy from Hilt to Ingot (feat(stack): delegate /s3/bucket/policy from hilt to ingot smelt#65 does it for local stacks)
  • A principal's policy list carries no per-item ETag; a caller reads it from GetBucketPolicy's ETag header
  • A bucket deleted between authorization and a policy write answers UnknownBucket
  • Temporary pin: libforge to feat(s3): add the /s3/bucket/policy command libforge#80; re-pin before merge

🤖 Generated with Claude Code

@pyropy

pyropy commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T09:39:29.037230Z 6a2d7a7 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f08be2a440

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkg/rpc/service/bucket/policy.go Resolved
Comment thread pkg/rpc/service/bucket/policy.go Resolved
Comment thread pkg/rpc/service/bucket/policy.go Resolved
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from 37026f0 to 7d3b768 Compare September 29, 2026 15:22
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from 7d3b768 to 99ed840 Compare September 30, 2026 15:15
pyropy added a commit to fil-forge/ingot that referenced this pull request Sep 30, 2026
…t/policy

GET, PUT and DELETE on /{bucket}?policy are answered ahead of the S3
route table: the signed request and, on a PUT, the body are forwarded to
Hilt as /s3/bucket/policy, which authenticates and authorizes them. The
routes sit ahead of versitygw's controllers because its PutBucketPolicy
validates the body as an AWS-shaped document, which a Forge policy is
not, and its backend seam carries neither preconditions nor an ETag. The
bucket authority maps Hilt's policy rejections; the routes render them
as NoSuchBucketPolicy, MalformedPolicy, PreconditionFailed,
InvalidRequest and OperationAborted, and a CreateBucket's refused
x-bucket-policy header now renders as MalformedPolicy too, the RFC's one
mapping. The in-memory store answers NoSuchBucketPolicy and NotImplemented.

Temporary pins to hilt branch srdjan/feat/iam-policy-command
(fil-forge/hilt#89) and libforge branch
srdjan/feat/s3-bucket-policy-command (fil-forge/libforge#80); re-pin
before merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from 99ed840 to 93b2c13 Compare September 30, 2026 15:35
@pyropy

pyropy commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 93b2c1320d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from 93b2c13 to a30b36c Compare September 30, 2026 16:13
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from a30b36c to 275bf5a Compare September 30, 2026 16:14
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch 2 times, most recently from 103f0f7 to dc4d3b3 Compare September 30, 2026 16:55
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from dc4d3b3 to d7d19a1 Compare September 30, 2026 16:58
@pyropy
pyropy force-pushed the srdjan/feat/iam-policy-command branch from d7d19a1 to 7f48010 Compare September 30, 2026 17:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a2d7a76f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkg/rpc/service/bucket/policy.go Resolved
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 2, 2026
…t/policy

GET, PUT and DELETE on /{bucket}?policy are answered ahead of the S3
route table: the signed request and, on a PUT, the body are forwarded to
Hilt as /s3/bucket/policy, which authenticates and authorizes them. The
routes sit ahead of versitygw's controllers because its PutBucketPolicy
validates the body as an AWS-shaped document, which a Forge policy is
not, and its backend seam carries neither preconditions nor an ETag. The
bucket authority maps Hilt's policy rejections; the routes render them
as NoSuchBucketPolicy, MalformedPolicy, PreconditionFailed,
InvalidRequest and OperationAborted, and a CreateBucket's refused
x-bucket-policy header now renders as MalformedPolicy too, the RFC's one
mapping. The in-memory store answers NoSuchBucketPolicy and NotImplemented.

Temporary pins to hilt branch srdjan/feat/iam-policy-command
(fil-forge/hilt#89) and libforge branch
srdjan/feat/s3-bucket-policy-command (fil-forge/libforge#80); re-pin
before merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 2, 2026
Picks up the principal removal that strips its policies before locking the
principal, and the policy store lock-order fixes. Temporary pin to a draft
branch; re-pin to hilt main before this leaves draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 2, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyropy added a commit to fil-forge/ingot that referenced this pull request Oct 2, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Policy PUT can race bucket deletion and leave grants unrevoked, while the dependency remains temporarily pinned to an open PR.

Review effort: Balanced
Findings: 1 High severity · 2 Medium severity · 1 Low severity

Open (4)
What changed in this PR

Adds /s3/bucket/policy RPC support for forwarding S3 bucket-policy operations through Hilt.

Changes:

  • Implements authenticated GET, PUT, and DELETE policy handling.
  • Adds SigV4 payload hashing and signed preconditions.
  • Wires the RPC route, client API, failure mappings, and tests.
File Description
AGENTS.md Documents the policy RPC.
go.mod Updates libforge dependency.
go.sum Updates dependency checksums.
pkg/​api/​service/​bucketpolicy/​service.go Exposes resolved-bucket policy removal.
pkg/​client/​client.go Adds the policy RPC client method.
pkg/​fx/​rpc.go Registers the new route.
pkg/​fx/​rpc_test.go Verifies route grouping.
pkg/​rpc/​failure.go Maps policy failures.
pkg/​rpc/​failure_test.go Tests failure mapping.
pkg/​rpc/​policy.go Adds the RPC handler.
pkg/​rpc/​rpc_test.go Verifies command registration.
pkg/​rpc/​service/​auth/​auth.go Restricts policy operations to service keys.
pkg/​rpc/​service/​auth/​operation.go Classifies policy requests and permissions.
pkg/​rpc/​service/​auth/​operation_test.go Tests policy classification.
pkg/​rpc/​service/​bucket/​policy.go Implements policy operations.
pkg/​rpc/​service/​bucket/​policy_test.go Tests policy behavior.
pkg/​sigv4/​sign.go Adds payload-hash presigning.
pkg/​sigv4/​sigv4.go Exposes verified payload hashes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/rpc/service/bucket/policy.go Resolved
Comment thread go.mod Outdated
github.com/docker/docker v28.5.2+incompatible
github.com/exaring/otelpgx v0.12.0
github.com/fil-forge/libforge v0.0.0-20260914100934-63f5b20252fe
github.com/fil-forge/libforge v0.0.0-20260928131613-0060aa6cb75a

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Known and tracked: the pin is temporary while libforge#80 is open, and the re-pin to the merged revision follows its merge. The merge order is libforge #80, swarf #22, then this stack.

Comment thread pkg/rpc/service/auth/operation.go Resolved
Comment thread pkg/client/client.go Resolved
@pyropy

pyropy commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

/forge-perf

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

forge-perf

Commit ee884580feca95731c91ae2ad6eac86a66993452
Image ghcr.io/fil-forge/hilt:pr-89-ee88458 (sha256:15100044cd80)
Pairs 1 (main, branch)
State Done: within noise
Pairing exp-hilt-pr89-ee884580feca-37331920218
Logs workflow run
Run Role Class Median p5 Traces
main-20261005t152922z main valid (few_windows, nic_allowance_exceeded, traced) 3.67 GB/s 3.50 GB/s Grafana
main-20261005t154638z branch valid (few_windows, nic_allowance_exceeded, traced) 3.76 GB/s 3.55 GB/s Grafana
Median p5
Branch against main +2.6% +1.2%
Noise band ±7.9% ±4.7%

Verdict: within noise, judged on the median against its noise band. Positive deltas mean the branch ingests faster.

Main runs the current main set; branch swaps in this commit's image. The runs table on the forge-perf page lists each run.

Comment thread pkg/api/service/bucketpolicy/service.go Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete doesn't seem to be called by anything? Can we just have a Delete or a Remove method please?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped Delete alongside Put and Get in 5bd71c9

Comment thread pkg/client/client.go Outdated
// Policy invokes /s3/bucket/policy with a forwarded GetBucketPolicy,
// PutBucketPolicy or DeleteBucketPolicy request; body is the policy document
// on a PUT. It returns no delegations.
func (c *Client) Policy(ctx context.Context, req s3.Request, body []byte, opts ...MethodOption) (*s3bkt.PolicyOK, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call this BucketPolicy?

Suggested change
func (c *Client) Policy(ctx context.Context, req s3.Request, body []byte, opts ...MethodOption) (*s3bkt.PolicyOK, error) {
func (c *Client) BucketPolicy(ctx context.Context, req s3.Request, body []byte, opts ...MethodOption) (*s3bkt.PolicyOK, error) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed in 291bc7f

Comment thread pkg/rpc/service/bucket/policy.go Outdated
}
var opts []bucketpolicysvc.WriteOption
if unconditional {
opts = append(opts, bucketpolicysvc.Unconditional())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, typically I'd expect a functional option, something like bucketpolicysvc.WithPrecondition(ifMatch) that takes the ifMatch parameter (since it's optional) and it wouldn't be passed to Write.

@pyropy pyropy Oct 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 834ce94: Write drops the positional ifMatch; the precondition is now optional via bucketpolicysvc.IfMatch(etag) / bucketpolicysvc.IfNoneMatch() (none = unconditional, as S3 does) — 834ce94

pyropy and others added 12 commits October 8, 2026 19:40
…vice

The bucket RPC service takes the management API's bucket policy service, so
an RPC policy write goes through the same validation, compare-and-set and key
rotation as a policy PUT. Put splits into a public Write that takes an
already-resolved tenant and bucket, and the auth package exports HeaderValue
for reading signed request headers.

Carried over from the closed PR #73 (fil-forge/hilt), without its
x-bucket-policy header on CreateBucket.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ingot forwards S3 GetBucketPolicy, PutBucketPolicy and DeleteBucketPolicy
requests as one command; the request's method selects the operation. A
service key holding s3:GetBucketPolicy, s3:PutBucketPolicy or
s3:DeleteBucketPolicy reaches it, a principal-bound key is refused. A
PUT's body must hash to the request's signed payload hash, so nothing on
the path can replace the document. If-Match and If-None-Match: * are
optional signed headers; without one the write is unconditional, as on
AWS. The policy service's rejections (PolicyNotFound, InvalidPrecondition,
PreconditionFailed, ConcurrentChange) become receipt failures for Ingot to
render.

Temporary pin to libforge branch srdjan/feat/s3-bucket-policy-command
(fil-forge/libforge#80), re-pin before merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uthorized bucket

A write-locked tenant was refused nothing: the authorizer let it through
for reads and left writes to handlers that never checked. It now refuses
uploads, bucket creation and PutBucketPolicy for such a tenant, keeping
reads, listings and deletes, as the management API's TenantStatus
contract says. The refusal is OperationNotPermitted, which Ingot already
renders as AccessDenied.

The bucket policy operation acts on the bucket the authorizer resolved,
by DID, instead of looking it up again by name, and a present but empty
If-Match or If-None-Match is an invalid precondition rather than an
unconditional write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#52 enforces write-locked in the authorizer and revokes the cached write
grants, so this stack no longer carries its own guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n, and test the policy client method

GET and HEAD shared the policy branch, so HEAD /bucket?policy was classified
as GetBucketPolicy and the policy handler answered it with the document; the
operation is GET-only and a HEAD now classifies as the plain bucket operation.
The client's Policy method gains the test its neighbours have: the forwarded
request and body reach the handler and the ETag and policy come back, and a
failure receipt is an error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e header

CreateBucket no longer carries a policy (RFC 30, 113d4ded), so the failure
mapping's comment names the one source left, a PutBucketPolicy body.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bucket-policy ETags travel in the HTTP ETag header; a list cannot carry
one per item, so PrincipalPolicy drops the etag field. Callers that need
a policy's ETag read it with S3 GetBucketPolicy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They resolved a tenant's external id and a bucket name for the partner-key
policy routes, which are gone. The /s3/bucket/policy handler has both
resolved already and calls Write and Remove; the tests now do the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oneMatch

Write took the precondition as a positional *string, nil meaning the
create, plus an Unconditional option to waive it. The precondition is
now optional: without an option the write is unconditional, as an S3
PutBucketPolicy without either header is; IfMatch(etag) and IfNoneMatch()
condition it. precondition in the RPC handler returns the tag and the
If-None-Match flag directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread pkg/api/service/bucketpolicy/service.go Outdated
// IfMatch makes the write replace the bucket's policy only if its current ETag
// is etag.
func IfMatch(etag string) WriteOption {
return func(in *bucketpolicystore.Input) { in.IfMatch, in.Unconditional = &etag, false }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider flipping this from Unconditional to Conditional. The zero value of boolean is false, so in go you often see the default for an option worded in a way that makes sense for it to be false. That way when you create a type instance and want the field to assume the default you simply don't set it.

In this case I think the default is for the request to be unconditional, so the default is Conditional: false.

i.e. you go from:

	in := bucketpolicystore.Input{
		Bucket:        bucketID,
		Tenant:        tenantID,
		Policy:        doc,
		Unconditional: true,
	}

to:

	in := bucketpolicystore.Input{
		Bucket:        bucketID,
		Tenant:        tenantID,
		Policy:        doc,
	}

It also reads better IMO.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9535388: the store input's flag is Conditional, so the zero value is the unconditional write and Write sets nothing unless an option asks for a check.

Comment thread pkg/api/service/bucketpolicy/service.go Outdated
// IfNoneMatch makes the write create the bucket's policy only if the bucket
// has none (If-None-Match: *).
func IfNoneMatch() WriteOption {
return func(in *bucketpolicystore.Input) { in.IfMatch, in.Unconditional = nil, false }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider validating that in.IfMatch is already nil. Currently it will silently overwrite a previous IfMatch option.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9535388: WithCondition returns ErrInvalidPrecondition when the input already carries a condition (WriteOption now returns an error, and Write surfaces it before touching the store). Covered by TestWriteCondition.

Comment thread pkg/rpc/service/bucket/policy.go Outdated
// with the current ETag replaces or deletes, and If-None-Match: * creates.
// Both headers, an If-None-Match other than *, or a header the signature does
// not cover is [bucketpolicysvc.ErrInvalidPrecondition].
func precondition(authz *auth.AuthorizedRequest, headers map[string]string) (ifMatch string, ifNoneMatch bool, err error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, ifNoneMatch bool doesn't really make sense to me. Or at least I would expect the ifMatch return value to hold "*" when this is true.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9535388: precondition returns one *bucketpolicysvc.Condition, nil when neither header was sent, IfMatch(tag) for If-Match and IfNoneMatch("*") for If-None-Match. I kept the two apart rather than putting * in ifMatch, since If-Match: * means the opposite in HTTP (any current policy).

return nil, fmt.Errorf("a DeleteBucketPolicy cannot carry If-None-Match: %w", bucketpolicysvc.ErrInvalidPrecondition)
}
// An empty ifMatch is the unconditional delete.
if err := s.policyWrites.Remove(ctx, tenantID, bucketID, name, ifMatch); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably make it a *string to signify optionality.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9535388: Remove takes ifMatch *string; nil is the unconditional delete. The handler passes &cond.Match or nil.

Comment thread pkg/api/service/bucketpolicy/service.go Outdated
// IfNoneMatch makes the write create the bucket's policy only if the bucket
// has none (If-None-Match: *).
func IfNoneMatch() WriteOption {
return func(in *bucketpolicystore.Input) { in.IfMatch, in.Unconditional = nil, false }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about IfNoneMatch as an option name in general - if none match...what? What are we matching against?

Personally, I'd make a WithCondition(c Condition) WriteOption option:

type Condition struct {
  Negate bool
  Match string
}

func IfNoneMatch(v string) Condition { return Condition{Negate: true, Match: v} }
func IfMatch(v string) Condition { return Condition{Match: v} }

// then you use as:
func main() {
  service.Write(ctx, tenant, bucket, policy, WithCondition(IfNoneMatch("*")))
  // or
  service.Write(ctx, tenant, bucket, policy, WithCondition(IfMatch(etag)))
}

However, we don't really ever want to allow none match on anything other than *. We're really talking about the difference between Add and Replace and I'd perhaps make this more explicit and split the method into these two variations instead of having an option that decides the functionality.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with your sketch in 9535388: Condition{Negate, Match}, IfMatch(etag), IfNoneMatch(v) and one WithCondition(c) WriteOption. WithCondition refuses a negated condition other than "*" and a second condition on the same write. I left Write as one method rather than splitting into Add and Replace: the handler also needs the unconditional write for a PutBucketPolicy without a precondition, which fits neither. #96 is rebased on top (43f9b3a), ingot a671ee5 re-pins.

The service takes one WithCondition option carrying a Condition, built by
IfMatch(etag) or IfNoneMatch("*"); a second condition or a negated one other
than "*" is ErrInvalidPrecondition. The RPC handler parses the headers into
that Condition, and Remove takes an optional ETag. The store input's flag is
Conditional, so the zero value is the unconditional write.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants