Skip to content

rfc: forge S3 tenant IAM - #30

Open
pyropy wants to merge 30 commits into
mainfrom
rfc/forge-s3-tenant-iam
Open

pyropy wants to merge 30 commits into
mainfrom
rfc/forge-s3-tenant-iam

Conversation

@pyropy

@pyropy pyropy commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

Summary

Extends the tenant management RFC with principals, bucket policies, and principal-bound access keys, implementing the fil-one bucket policies ADR (proposed in fil-one/fil-one#696) on Hilt and Ingot.

📖 Preview

Extends the tenant management RFC with principals, bucket policies, and
principal-bound access keys, implementing the fil-one bucket policies ADR
(IAM M2) on Hilt and Ingot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pyropy pyropy self-assigned this Sep 9, 2026
@pyropy
pyropy requested a balanced review from Copilot September 9, 2026 12:52
The only caller was the one-off migration sweep, which loops over the
idempotent single-principal PUT instead.

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.

🟡 Changes recommended

Cache consistency, revocation recovery, concurrency, and API contract gaps must be resolved before implementation.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Extends Forge tenant management with principal-based IAM enforced by Hilt and Ingot.

Changes:

  • Adds principals, service credentials, and principal-bound access keys.
  • Defines bucket policies, authorization, and cache invalidation.
  • Specifies schema, migration, and example workflows.
File summaries
File Description
rfcs/2026-09-forge-s3-tenant-iam.md Defines the proposed Forge S3 tenant IAM architecture and APIs.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 11
  • Review effort level: Balanced

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

Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
A principal is a (tenant, userId) row with no key material; per-request
delegations are signed with the tenant key. Narrowings reach the gateway
through a new Swarf /principal/invalidate command and firehose event,
published by Hilt's service identity from a configured publisher list.

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.

🟡 Changes recommended

Cache invalidation races and incomplete cross-gateway bucket deletion can preserve revoked access.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (4)

rfcs/2026-09-forge-s3-tenant-iam.md:152

  • Including s3:ListAllMyBuckets in effective(p, b) means the set is never empty, contradicting both the omitted-empty-buckets rule and authorization step 6. An existing but inaccessible bucket would consequently take the 403 path instead of UnknownBucket/404. Keep this tenant-level action outside the per-bucket effective set.
plus `s3:ListAllMyBuckets` (see [action vocabulary](#action-vocabulary)). A bucket with no policy has an empty effective set for every principal. The service credential is not a principal and is not evaluated against policies.

rfcs/2026-09-forge-s3-tenant-iam.md:66

  • Both minting paths return the only copy of the secret once, but retries return either a credential-less 200 or 409, and there is no delete/rotation route. If Hilt commits and the response is lost, the console cannot recover the credential and migration or tenant setup is permanently stuck. Define an idempotency/replay mechanism or a reset/rotation operation before relying on this flow.
- MUST be returned once, in the body of the call that minted it, as `serviceCredential: { accessKeyId, secretAccessKey }`.
- MAY be minted for a tenant that has none through `POST /tenants/{tenantId}/service-credential`, which returns it the same way and answers 409 when one exists. This is the migration path for existing tenants.

rfcs/2026-09-forge-s3-tenant-iam.md:141

  • This item is under “reject with 422” but says the same condition is reported as 404, leaving the API contract contradictory. Keep document-validation failures in the list and state the cross-tenant/not-found bucket response separately.
- a bucket that belongs to another tenant, reported as 404.

rfcs/2026-09-forge-s3-tenant-iam.md:502

  • In PostgreSQL, UNIQUE (bucket_id, principal) permits multiple rows where principal IS NULL, so it does not enforce one wildcard index entry per bucket as intended. Use NULLS NOT DISTINCT on supported PostgreSQL versions, or separate partial unique indexes for explicit and wildcard principals.
    UNIQUE (bucket_id, principal)
  • Files reviewed: 1/1 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
@pyropy

pyropy commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 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-09-23T11:02:30.225316Z d2cd4a6 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: bfce0390e5

ℹ️ 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 rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
…locked publish

Service credentials become a list per tenant with mint, list, and delete
routes, so a lost mint response or a rotation is a second mint and a
delete. A policy write publishes an invalidation for every principal
whose effective set changed in either direction, so a widening reaches a
warm gateway cache. The write publishes inside its transaction with the
policy, key, or principal row locked, and the authorize path reads those
rows with a shared lock, so a gateway cannot refill its cache from the
old policy between publish and commit.

The Tenant API bucket create is dropped: a bucket without a policy is
reachable by service credentials only, and the console writes the policy
right after creating the bucket over S3. With it goes the Ingot
requirement to register buckets learned from bucket-info. Bucket deletion
states the one-gateway assumption it relies on. ListAllMyBuckets leaves
the per-bucket effective set, the cross-tenant bucket case is a 404 on
its own, the two bucket-configuration reads are authorized at Hilt on
every request, and the wildcard index row gets a partial unique index.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The invalidation invocation names Hilt as its own subject, the shape the
revoke command already uses, because the standard validator refuses a
foreign subject with no proofs. Existing keys are removed by `hilt migrate
iam` before the schema migration, which now refuses to run while a key
remains. A service credential stores its accessKeyId as its name so the
schema is unchanged. Principal removal holds the principal row lock across
idempotent cleanup steps instead of claiming one transaction over tables
and the vault.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyropy and others added 2 commits September 22, 2026 16:19
The implementation mirrors the parent RFC's service-key path on
/s3/request/authorize: each per-request delegation is keyed to itself and
Ingot obtains the chain through the key's stored delegations over
/s3/bucket/info, so the two key kinds answer in one shape. Bucket
creation issues the named principals' delegations after the policy write
rather than in one transaction with it; a failure still deletes the
bucket with everything the create wrote.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A write whose revocations reach Swarf but whose commit fails left Ingot
refilling its cache from the old delegations, and the console's retry
republished identical revoke invocations that Swarf deduplicated, so the
change waited for midnight. Ingot now records revoked CIDs until the next
UTC midnight and serves, without caching, any authorize response that
carries one. The failure and retry text moves into one Error recovery
subsection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pyropy

pyropy commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

@alanshaw @bajtos I've done another round of changes to the RFC. Most notable changes are:

  • Removing the markers and issuing delegations to access keys instead. On a bucket policy change we're now going to issue and or revoke delegations for every permission given and or removed to a principle for that bucket. These delegations and revocations will be given to all access keys belonging to that principle. This is more in line with what we're doing with existing access keys (service keys as we name them in the RFC) at the expense of having more messages.
  • Adding a clear recovery path for bucket policy write failures. In the previous version I have tried to mitigate the issue of not being able to send same revocations on retry by adding a random nonce. Reason for resending these revocations is evicting the cached delegations from Ingot. Now Ingot will cache revocations too. If a request holding an access key signature that we have revoked delegations for recently comes to Ingot it will consult Hilt before authorizing the request. If the delegations returned from Hilt hold a revoked delegation Ingot will not cache those delegations and will continue to consult Hilt on every subsequent request until Hilt does not recover it's state.

@pyropy

pyropy commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

@codex review

@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: d2cd4a6c94

ℹ️ 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 rfcs/2026-09-forge-s3-tenant-iam.md

**A response that outran a write.** An authorize response that left Hilt before a write took its locks can arrive at Ingot after the write's revocations. Its delegations are in the set, so Ingot serves that one request under the old state and caches nothing. Swarf polls its store once per second before emitting a record, so the response must stay in flight longer than that interval for this to occur.

**Restart.** Ingot keeps the set in memory and a restart clears it together with the cache. A restart between a failed commit and its successful retry refills the cache from the stored delegations, and the retry's duplicate revocations emit nothing, so that key sees the change at midnight.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Re-emit invalidations after a failed commit

If a write publishes successfully but its database commit fails, and Ingot restarts before the retry, this sequence explicitly allows the restarted gateway to cache the old delegations and then receive no event when the retry commits because Swarf suppresses the duplicate revocations. A narrowing or deletion can therefore be acknowledged while the old access remains usable until midnight, contradicting the stated firehose-latency staleness bound; the retry needs a newly emitted invalidation or the revoked-CID state must survive restarts.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's not possible to re-emit the invalidations.


1. Publishes a revocation for every delegation of each of the principal's keys, in one Swarf request.
2. Removes the principal from every statement naming it. A statement left with no principal is deleted, and a policy left with no statement is deleted.
3. Deletes the principal's keys: rows, then vault entries.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not delete vault keys before the database commit

If the database commit fails after these vault deletions, PostgreSQL can restore the key rows and delegations but cannot restore the externally deleted private keys. The principal then appears intact after rollback but /s3/request/authorize cannot read its signing keys, contradicting the following claim that a failed removal changes nothing. Vault deletion needs a post-commit/outbox workflow or another compensating mechanism rather than occurring before commit.

Useful? React with 👍 / 👎.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
pyropy added a commit to fil-one/fil-one that referenced this pull request Sep 25, 2026
A policy edit rewrites the delegations of every key bound to a changed
principal over the edited bucket, so the principal needs no identity of
its own for a revocation to target, and "touches no key" no longer
describes the mechanism. Matches RFC fil-one/RFC#30.

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

@alanshaw alanshaw left a comment

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.

This is nearly there, there's a couple of things we have to figure out before I can approve this though. Please see the comments.

Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated

1. The storage system computes a principal's effective actions on a bucket from that bucket's policy alone: `allow` minus `deny`, with explicit `deny` taking precedence.
2. A policy edit changes the authority of every key bound to an affected principal without reissuing any key.
3. Before Hilt acknowledges a policy change, it publishes to Swarf every revocation required by a narrowing of a principal's effective actions or by a widening on a bucket the key already reaches. Ingot's existing firehose consumer then clears the affected caches within firehose latency.

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.

within firehose latency

Not sure I understand what this means?

@pyropy pyropy Sep 25, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think that Claude inferred we're using AWS Firehose; will remove this sentance.

Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated
2. A policy edit changes the authority of every key bound to an affected principal without reissuing any key.
3. Before Hilt acknowledges a policy change, it publishes to Swarf every revocation required by a narrowing of a principal's effective actions or by a widening on a bucket the key already reaches. Ingot's existing firehose consumer then clears the affected caches within firehose latency.
4. The console signs member traffic, including presigned URLs, with a key bound to that member's principal, while retaining service keys for traffic with no member actor.
5. One policy model governs every principal. Existing keys continue to work, and a tenant can migrate to principals without reissuing keys or introducing a window in which the network rejects requests.

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.

You might want to define terms before this since you're using many of them here.

| Body | Kind | Authority |
| ---------------------------------------------- | ------------------- | ----------------------------------------------------------------------------- |
| `{ name, permissions, buckets?, expiresAt? }` | Service key | The `permissions` and `buckets` in the request, as the parent RFC specifies. |
| `{ name, principalId, expiresAt? }` | Principal-bound key | Whatever the bucket policies give the principal, held as delegations that follow the policies. |

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.

Might be an idea to have a discriminator key or value e.g.

type AccessKeyBody =
  | { type: 'service', name: string, permissions: string[], buckets?: string[], expiresAt?: string }
  | { type: 'principal', name: string, principalId: string, expiresAt?: string }
type AccessKeyBody =
  | { $service: { name: string, permissions: string[], buckets?: string[], expiresAt?: string } }
  | { $principal: { name: string, principalId: string, expiresAt?: string } }

The latter is easier to model in a typed language.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

While I do not disagree, I would prefer to keep the current access key creation contract unchanged.

Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated

### Tenant creation

`PUT /tenants/{tenantId}` is unchanged. Its body currently names the region, and Hilt binds the tenant to the provider that serves it. This RFC does not change Hilt's tenant-to-region model; changes that allow a tenant to span multiple Forge regions are tracked separately in [FIL-1133](https://linear.app/filecoin-foundation/issue/FIL-1133). As today, the console creates the tenant and then creates a service key with no bucket list for bucket provisioning. Nothing in the IAM model below depends on a tenant serving exactly one region.

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 don't think you really need any of this explanation for this RFC...maybe just the first sentence. IMO it is implicit that if you're not describing a change in this document then it does not change.

IDK if you want to say something about not being able to assign permissions to principals that belong to other tenants...I don't think this is something you can do with AWS anyway so perhaps it doesn't matter.

}
]
}
```

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.

Have you given any thought to how a policy is encoded deterministically so that you can easily determine equivalency of two policies? ...also useful for the etag.

Might be overkill but you might want to encode with IPLD and transform the arrays into sets (I don't think the order actually matters for statement lists, principal lists or action lists - i.e. they are sets not arrays).

Example schema in typescript:

interface Policy {
  statement: {
    [link: Link<Statement>]: {}
  }
}

interface Statement {
  sid?: string
  effect: 'allow' | 'deny'
  principal: '*' | { [name: string]: {} }
  action: { [name: string]: {} }
}

When the statements, principals and actions are defined as objects we gain determinism. IPLD will sort object keys so that the order you added them does not matter, you also gain deduplication for free.

i.e. you encode (deterministically) each statement and use set of CIDs as the policy statements, then you can deterministically encode a policy and use that CID as the Etag, for example.

So, for example if someone gives you either one of:

{
  "statement": [{
    "effect": "allow",
    "principal": ["a", "b"],
    "action": ["s3:PutObject", "s3:GetObject"]
  }]
}

or

{
  "statement": [{
    "effect": "allow",
    "action": ["s3:PutObject", "s3:GetObject", "s3:PutObject"],
    "principal": ["b", "a"]
  }]
}

...it'll encode to the same value and produce the same hash (CID).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I haven't considered this, but I love the idea ❤️

Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated

- an action outside the [policy vocabulary](#action-vocabulary); in particular `s3:CreateBucket`, `s3:DeleteBucket`, and `s3:ListAllMyBuckets`. `s3:*` is accepted and stands for the whole vocabulary,
- a `principal` that is neither the string `"*"` nor a non-empty list of ids of live principals of the tenant. `"*"` inside a list is rejected; the wildcard has one spelling,
- an empty `statement` list or a statement with an empty `action` list. The caller deletes the policy instead,

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, not reject with 422? Oh, you mean the caller needs to send a DELETE request instead. Worth clarifying.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

So, not reject with 422? Oh, you mean the caller needs to send a DELETE request instead. Worth clarifying.

Yes, since the empty statement and or empty action are invalid, caller would need to issue an explicit DELETE request.

Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated

Buckets are created and deleted over S3 only with a service key. `CreateBucket` cannot be granted by a bucket policy because the target bucket does not exist yet. This RFC also deliberately excludes `DeleteBucket` from the policy vocabulary so that bucket lifecycle operations remain service-key-only, matching the ADR's decision that member keys cannot create or delete buckets. A principal-bound key therefore receives `AccessDenied` from both operations. `aws s3 mb` with a member's key fails, and members create buckets through the console. The user-facing S3 documentation needs to state this limitation.

A new bucket's policy is carried on the create request in the Forge-specific `x-bucket-policy` header as base64-encoded JSON; AWS does not define this header. Ingot forwards the S3 request to Hilt in `/s3/bucket/create` as it does today, headers included, so the header requires no new Ingot behavior. Hilt decodes it, validates it exactly as `PUT .../policy` would, creates the bucket, writes the policy, and issues every key of each named principal its delegations over the new bucket. If the policy write or the issuance fails, Hilt deletes the bucket with whatever it had written. No bucket outlives a failed write of the policy its create request carried. The header MUST be among the request's `SignedHeaders`; Hilt refuses a create whose policy header is present and unsigned, since otherwise anything on the path could replace the document. A document that fails validation refuses the create with `InvalidBucketPolicy`, which Ingot renders as `InvalidArgument` (400). Ingot caps the request head at 8 KB, leaving roughly 5 KB for the decoded JSON policy under the expected request headers. The console MUST check the encoded request size before sending the create and refuse to submit a request that would exceed Ingot's header limit; an oversized request may otherwise be rejected by Ingot before it reaches Hilt.

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.

Why would we not use PutBucketPolicy for this?

What if I'm creating a bucket with aws s3 mb, can I not then add a policy? I have to go via the console?

Do we need /tenants/{tenantId}/buckets/{bucketName}/policy? Why don't we just add a new command for policy management /s3/bucket/policy and have Ingot invoke it when it receives a PutBucketPolicy or GetBucketPolicy request?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Why don't we just add a new command for policy management /s3/bucket/policy and have Ingot invoke it when it receives a PutBucketPolicy or GetBucketPolicy request?

I like that idea, that could work 👍🏻

```jsonc
{
"name": "laptop", // unique per principal
"principalId": "8f2c...", // makes the key principal-bound

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.

Why is principalId in the request body and not in the URL path?

I just realized that the reason we include principalId inside the access key creation body is to reuse the same access key creation route for both service and principle bound keys.

By this reasoning should we not also reuse the same endpoint for listing a principal's keys and not create a new one?

Comment thread rfcs/2026-09-forge-s3-tenant-iam.md Outdated

**Swarf rejects the publish, or the publish fails.** Hilt returns 500 and commits nothing. The old state remains in force, so no principal receives authority beyond the committed state.

**Swarf accepts the revocations and the commit fails.** The delegations are revoked at Swarf and still stored in Hilt. Ingot consumes the revocations, drops each affected key's per-key cache, and records each revoked CID in an in-memory set kept until the next UTC midnight plus clock skew, the same horizon as the cache. On the key's next request Ingot misses the cache, calls Hilt, and receives the same stored delegations. Their CIDs are in the set, so Ingot authorizes the request from the response and caches nothing. Every request from that key reaches Hilt until a committed write returns delegations with new CIDs. The member holds exactly the access the committed state grants, at one Hilt round trip per request.

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.

Wait so you're saying Ingot keeps a set of CIDs it knows are revoked, and will authorize the request but not cache the proofs if they are revoked? I don't think that makes sense 😬. How does Ingot distinguish between these and proofs that have genuinely been revoked - like when a key is deleted?

I think the revoke needs to happen inside the transaction and rollback if it fails.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Wait so you're saying Ingot keeps a set of CIDs it knows are revoked, and will authorize the request but not cache the proofs if they are revoked? I don't think that makes sense 😬.

Yes, that's correct. Ingot rejects to cache outdated proofs, but continues to defer the authorization to Hilt. Ingot will only cache proofs once the Hilt state is consisted with the published proof chain. Motivation for this type of behaviour is that ultimately Hilt keeps the ultimate authority over what get's authorized, and in my opinion requests should be authorized or not depending on current Hilt state.

How does Ingot distinguish between these and proofs that have genuinely been revoked - like when a key is deleted?

In both cases (proofs don't match cached revocations; proofs match cached revocations) keys have been genuinely revoked, but in former case Hilt state is not yet consistent. Note that this state only happens when the transaction commit fails.

I think the revoke needs to happen inside the transaction and rollback if it fails.

We do revocations inside the transaction, but we can't rollback publishing the revocations over Swarf. In case of the transaction commit failure revocations will be published, but the transaction commit needs to retried.

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.

Ah, ok. I guess that makes sense.

pyropy and others added 12 commits September 25, 2026 14:51
Co-authored-by: ash <alan138@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

4 participants