Skip to content

Add agent_kit helpers: workspaces, forks, checkpoints, coordination#23

Open
katieschilling wants to merge 11 commits intomainfrom
katieschilling/agent-kit-helpers
Open

Add agent_kit helpers: workspaces, forks, checkpoints, coordination#23
katieschilling wants to merge 11 commits intomainfrom
katieschilling/agent-kit-helpers

Conversation

@katieschilling
Copy link
Copy Markdown

Summary

Ports the @tigrisdata/agent-kit (TypeScript) public surface to Python, on top of this library's existing snapshot/fork primitives.

What's new

Area Helpers
Workspaces create_workspace / teardown_workspace — bucket with optional TTL (lifecycle rule) and optional snapshot enablement
Forks create_forks / teardown_forks — snapshot a base bucket then create N independent copy-on-write forks
Checkpoints checkpoint / restore / list_checkpoints + Checkpoint dataclass — labeled snapshots and snapshot-based restore
Coordination setup_coordination / teardown_coordination — webhook notifications via Tigris's PATCH /{bucket} REST endpoint

The Workspace, Fork, ForkSet, and Checkpoint dataclasses are also exported.

How it works

  • Workspaces / forks / checkpoints reuse the existing header-injection helpers (create_snapshot, create_fork, etc.).
  • TTL and webhook coordination need a Tigris-specific PATCH /{bucket} JSON endpoint that's not part of the S3 API. A new internal _rest.py module provides this (urllib3 + SigV4, mirroring bundle.py's pattern). TigrisRestError is exported for error handling.

Out of scope

  • Scoped credentials (per-workspace / per-fork access keys) — agent-kit does this through Tigris IAM, which is a separate SDK and outside the boto3 surface this library extends. Can be added in a follow-up.

Test plan

  • uv run ruff check tigris_boto3_ext — passes
  • uv run ruff format --check tigris_boto3_ext — passes
  • uv run mypy tigris_boto3_ext — passes (8 source files)
  • uv run pytest tests/ --ignore=tests/integration — 59 passed (39 new)
  • Integration tests against real Tigris — collect cleanly; require AWS_ENDPOINT_URL_S3 etc. to run. Worth a manual run before merge to validate the PATCH /{bucket} payload shapes for TTL and notifications.

🤖 Generated with Claude Code

Mirrors the @tigrisdata/agent-kit (TypeScript) public surface as Python
helpers on top of this library's existing snapshot/fork primitives.

- Workspaces: create_workspace / teardown_workspace, with optional TTL
  (via lifecycle rules) and snapshot enablement
- Forks: create_forks / teardown_forks — snapshot a base bucket then
  create N copy-on-write forks
- Checkpoints: checkpoint / restore / list_checkpoints — labeled
  snapshots with a Checkpoint dataclass
- Coordination: setup_coordination / teardown_coordination — webhook
  notifications via Tigris's PATCH /{bucket} REST endpoint

A new internal _rest.py module provides direct urllib3 + SigV4 access
to the Tigris-specific PATCH /{bucket} JSON API (mirrors bundle.py's
pattern). Per-workspace and per-fork scoped credentials are not
included; they require Tigris IAM API support outside the boto3 surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 27, 2026

Greptile Summary

This PR ports the TypeScript @tigrisdata/agent-kit surface to Python, adding workspaces, forks, checkpoints, and webhook coordination on top of existing snapshot/fork primitives. The implementation is clean and well-tested (39 new unit tests + integration suite), and the fork credential-leak fix from the previous review round is correctly applied via _try_provision_credentials.

  • P1 — bucket leak in create_workspace: credentials_role validation (ValueError for unrecognised roles) fires inside _provision_credentials after the bucket is already created. An invalid role (e.g., \"Admin\") creates the workspace bucket, then propagates the exception, leaving the caller with no Workspace handle and no way to clean up. Moving the role check before any S3 call — the same fix pattern already applied to create_forks — resolves this.

Confidence Score: 3/5

Safe to merge after fixing the create_workspace bucket-leak on invalid role; all other paths are well-guarded.

One P1 present: invalid credentials_role leaks the created bucket before the ValueError propagates. No P0 issues. Score pulled below the P1 ceiling of 4 because the leaked-resource pattern was already identified and fixed for forks in this same PR, making the workspace omission stand out.

tigris_boto3_ext/agent_kit.py — specifically the create_workspace function and its early-validation ordering.

Important Files Changed

Filename Overview
tigris_boto3_ext/agent_kit.py New agent-kit module: workspaces, forks, checkpoints, coordination. Fork credential-leak fix applied. One remaining P1: invalid credentials_role in create_workspace creates the bucket before raising ValueError, leaking it.
tigris_boto3_ext/object_notifications.py New module implementing SigV4-signed PATCH /{bucket} for webhook notifications; input validation, auth modes, and error handling are solid.
tigris_boto3_ext/_iam.py New IAM helper for scoped access keys; creates key → policy → attach, with rollback on partial failure and independent suppress on teardown.
tests/test_agent_kit.py 39 new unit tests covering workspaces, forks, checkpoints, coordination, and credential provisioning. Missing a test that verifies no bucket is created when credentials_role is invalid.
tigris_boto3_ext/init.py All new public symbols exported and added to __all__; no issues.
README.md Agent-kit section added with clear usage examples for all four feature areas; no doc issues.
tests/integration/test_agent_kit.py New integration test suite with workspace, fork, checkpoint, and coordination coverage; tests skip cleanly without real credentials.

Reviews (4): Last reviewed commit: "Address review feedback: standard S3/IAM..." | Re-trigger Greptile

katieschilling and others added 2 commits April 27, 2026 13:14
Tigris was rejecting PATCH /{bucket} with SignatureDoesNotMatch because
the canonical request used during signing did not include the body
hash header. SigV4 needs X-Amz-Content-Sha256 set before add_auth()
runs so it is included in the canonical headers and signed.

This mirrors the existing bundle.py implementation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds `credentials_role` ("Editor" | "ReadOnly") to create_workspace and
create_forks. When set, a scoped Tigris access key is provisioned per
bucket and returned on Workspace.credentials / Fork.credentials.
teardown_workspace and teardown_forks revoke the keys before deleting.

Adds a new internal `_iam.py` HTTP transport that mirrors the JS
@tigrisdata/iam SDK: POSTs SigV4-signed (service "iam") form-encoded
requests to https://iam.storageapi.dev/ — `CreateAccessKeyWithBucketsRole`
for provisioning and the AWS-IAM-compatible `DeleteAccessKey` for
revocation. Override the endpoint with the TIGRIS_IAM_ENDPOINT env var.
TigrisIAMError surfaces non-2xx responses.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.59%. Comparing base (16f87a9) to head (b7d6f3c).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #23      +/-   ##
==========================================
+ Coverage   99.24%   99.59%   +0.35%     
==========================================
  Files           6        9       +3     
  Lines         266      499     +233     
==========================================
+ Hits          264      497     +233     
  Misses          2        2              
Flag Coverage Δ
integration 99.59% <100.00%> (+0.35%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Adds targeted tests for previously-uncovered error and edge paths so
codecov/patch hits 100% on the diff:
- _empty_bucket: actual versioned-listing happy path and the unversioned
  fallback when list_object_versions raises
- _iam.py response parsing: empty body, malformed JSON, JSON missing
  AccessKey, malformed XML, XML missing SecretAccessKey
- _rest.py: malformed JSON response

agent_kit.py, _iam.py, _rest.py, and bundle.py are now at 100% line
coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ovaistariq
Copy link
Copy Markdown
Contributor

@greptileai

Comment thread tigris_boto3_ext/agent_kit.py Outdated
Three P1 issues from the Greptile review:

1. _empty_unversioned aborted on first delete_object failure, leaving the
   bucket non-empty so teardown_workspace's later delete_bucket would raise
   BucketNotEmpty. Each per-object delete is now independently suppressed.

2. Checkpoint equality was broken between checkpoint() (client-side
   datetime.now) and list_checkpoints() (server-side CreationDate) because
   the two paths sourced created_at differently. created_at is now excluded
   from dataclass equality so a checkpoint can be matched against a listing.

3. create_forks used break on a per-fork failure, silently abandoning the
   rest. It now skips the failing fork and continues — partial failures
   under-deliver but no longer cut off subsequent attempts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@katieschilling
Copy link
Copy Markdown
Author

@greptileai

Comment thread tigris_boto3_ext/agent_kit.py
@ovaistariq
Copy link
Copy Markdown
Contributor

cursor review

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 28, 2026

Bugbot couldn't run

Bugbot is not enabled for your user on this team.

Ask your team administrator to increase your team's hard limit for Bugbot seats or add you to the allowlist in the Cursor dashboard.

Comment thread tigris_boto3_ext/_iam.py Outdated
@@ -0,0 +1,204 @@
"""Tigris IAM HTTP transport for managing access keys.
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.

Why do we need this? Tigris is IAM compatible, existing IAM sdk should work out of the box. Instead of doing it this way, better to do it as follows:

  • use existing s3 iam sdk
  • create iam policy that restricts the access key to a particular bucket
  • attach the iam policy to the access key

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in a7926b8 — replaced the custom SigV4 transport with boto3.client("iam", endpoint_url="https://iam.storageapi.dev", ...) reusing the S3 client's credentials. The flow is now exactly what you described:

  1. CreateAccessKey
  2. CreatePolicy with a bucket-scoped policy document (s3:* for Editor, GetObject/ListBucket/GetBucketLocation for ReadOnly, resource = arn:aws:s3:::{bucket} + /*)
  3. AttachUserPolicy

Teardown reverses it: DetachUserPolicyDeletePolicyDeleteAccessKey.

One thing worth flagging that I had to discover by probing real Tigris: AttachUserPolicy rejects the UserName field returned by create_access_key ("auto") — Tigris treats the access key id itself as the UserName handle. There's a comment in _iam.py calling that out so the next person doesn't trip on it.

Verified end-to-end against the real Tigris IAM endpoint; all 13 integration tests pass.

Comment thread tigris_boto3_ext/_rest.py Outdated
self.body = body


def patch_bucket_settings(
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.

Tigris supports the PutBucketLifecycleConfiguration API so lifecycle rules can be set using standard S3 API.

Instead of having a generic patch_bucket_settings function, it is best to expose a function that allows users to configure object notifications. The function will be public and live in the object_notifications.py file.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Both addressed in a7926b8.

TTL via standard S3: create_workspace now calls s3_client.put_bucket_lifecycle_configuration(...) directly. No custom transport involved.

Object notifications as a public module: _rest.py is deleted. New public file at tigris_boto3_ext/object_notifications.py exposes set_object_notifications(s3_client, bucket, *, webhook_url, event_filter, auth_token, auth_username, auth_password) and clear_object_notifications(s3_client, bucket), plus ObjectNotificationsError. The Tigris-specific PATCH /{bucket} JSON transport is inlined privately in that file (only this one feature needs it now). setup_coordination / teardown_coordination in agent_kit are now thin wrappers around the public functions.

Three threads of feedback from the maintainer review:

1. _iam.py: replaced the custom SigV4 + custom CreateAccessKeyWithBucketsRole
   transport with the standard boto3 IAM client pointed at the Tigris IAM
   endpoint. The bucket-scoped flow now goes through standard AWS IAM
   actions: CreateAccessKey + CreatePolicy + AttachUserPolicy
   (and DetachUserPolicy + DeletePolicy + DeleteAccessKey on teardown).
   Tigris IAM treats the access key id itself as the "user" handle for
   AttachUserPolicy/DetachUserPolicy — verified end-to-end against real
   Tigris.

2. _rest.py: deleted. TTL on workspaces now uses the standard S3
   PutBucketLifecycleConfiguration API (not a Tigris extension).
   Object-event webhook notifications still need a Tigris-specific
   PATCH /{bucket} JSON endpoint, but that transport is now inlined in a
   new public object_notifications.py module exposing
   set_object_notifications / clear_object_notifications. agent_kit's
   setup_coordination / teardown_coordination are thin wrappers.

3. create_forks: when _provision_credentials raises mid-loop, the fork
   bucket is now still appended to the result (with credentials=None) so
   the caller has a handle to pass to teardown_forks. ValueError on a
   bad role propagates immediately (programmer error).

The Credentials dataclass grew user_name and policy_arn fields (compared
out of equality and repr) to track the bookkeeping needed for the
DetachUserPolicy/DeletePolicy teardown sequence.

13 integration tests against real Tigris all pass; 86 unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@katieschilling
Copy link
Copy Markdown
Author

@greptileai

Comment thread tigris_boto3_ext/agent_kit.py Outdated
Greptile flagged that an invalid credentials_role on create_workspace
or create_forks would create the bucket (and snapshot/forks for the
plural case) before _provision_credentials ran the role check, leaving
the caller with an exception and no handle to clean up.

Both functions now run a single upfront _validate_role check before any
S3 call, so a bad role fails fast with no side effects. The redundant
ValueError handling in _try_provision_credentials is gone.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread README.md Outdated
### 4. Agent Kit

Higher-level workflows for AI agents — workspaces, parallel forks, checkpoints,
and event-driven coordination — composed on top of snapshots and forks. Mirrors
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.

Shouldn't be mirroring the TypeScript project. This is Python :) The file-level comment should reflect what is implemented.
Let's remove the "mirroring Typescript" comment from here and the other place.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 8f1d60e — dropped the "mirrors @tigrisdata/agent-kit" framing from the README intro and the agent_kit.py module docstring.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You were right, there was still one stale reference left — object_notifications.py's module docstring referred to setup_coordination / teardown_coordination as thin wrappers, even though those functions were removed back in 8f1d60e. Cleaned up in aa933a4.

The README intro and the agent_kit.py module docstring no longer mention mirroring or the TS package; this was the last leftover.

Comment thread tigris_boto3_ext/agent_kit.py Outdated
name: str,
*,
ttl_days: Optional[int] = None,
enable_snapshots: bool = False,
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.

Why not have it enabled by default?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 8f1d60eenable_snapshots=True is now the default since checkpointing is the common path. Set enable_snapshots=False to opt out.

Comment thread tigris_boto3_ext/agent_kit.py Outdated
enable_snapshots: bool = False,
credentials_role: Optional[Role] = None,
) -> Workspace:
"""Create a workspace bucket for an agent.
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.

Why call it workspace bucket? Call it just workspace

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 8f1d60e — docstring now just says "workspace". The Workspace dataclass docstring is also tightened to "A single-agent workspace".

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Tightened further in b7d6f3c. The function summary was already "Create a workspace for an agent." — the bucket parameter's Args description was still leaking "Workspace bucket name", which is now just "Name for the workspace."

Comment thread tigris_boto3_ext/agent_kit.py Outdated

Args:
s3_client: boto3 S3 client.
name: Bucket name.
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.

Similarly, name is workspace name. I think calling it both workspace and bucket is a bit odd.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 8f1d60e — renamed the parameter from name to bucket to match checkpoint(s3, bucket, …), restore(s3, bucket, …), etc. Now consistent across the agent_kit surface.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Same fix as above (b7d6f3c) — Args description for bucket no longer says "Bucket name" alongside "workspace". The parameter is renamed to bucket for consistency with checkpoint/restore, and described as "Name for the workspace."

Comment thread tigris_boto3_ext/agent_kit.py Outdated
if workspace.credentials is not None:
with suppress(Exception):
_revoke(s3_client, workspace.credentials)
if force:
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.

Tigris supports force deleting a bucket. The bucket doesn't need to be emptied. Check the implementation in the typescript sdk https://www.tigrisdata.com/docs/sdks/tigris/using-sdk/#deleting-a-bucket.

You can implement a delete_bucket function that takes the force parameter.

You can also remove the _empty_bucket function.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 8f1d60e. New public helper delete_bucket(s3_client, bucket, *, force=False) in helpers.py sends Tigris-Force-Delete: true (matching the JS SDK pattern). teardown_workspace / teardown_forks use it; _empty_bucket / _empty_versioned / _empty_unversioned are gone.

Comment thread tigris_boto3_ext/agent_kit.py Outdated
return None


def _revoke(s3_client: S3Client, credentials: Credentials) -> None:
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.

This function is providing no value. delete_scoped_access_key can be called directly

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed in 8f1d60eteardown_workspace / teardown_forks call delete_scoped_access_key directly.

Comment thread tigris_boto3_ext/agent_kit.py Outdated
)


def _empty_bucket(s3_client: S3Client, bucket: str) -> None:
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.

Not needed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed in 8f1d60e_empty_bucket is gone; teardown uses delete_bucket(force=True) which hits Tigris's force-delete extension.

Comment thread tigris_boto3_ext/agent_kit.py Outdated
_empty_unversioned(s3_client, bucket)


def _empty_versioned(s3_client: S3Client, bucket: str) -> bool:
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.

Not needed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed in 8f1d60e (along with _empty_unversioned).

Comment thread tigris_boto3_ext/agent_kit.py Outdated
return True


def _empty_unversioned(s3_client: S3Client, bucket: str) -> None:
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.

Not needed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed in 8f1d60e (along with _empty_versioned / _empty_bucket).

raise ValueError(msg)


def _patch_bucket(
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.

Better to call it _update_bucket_settings

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 8f1d60e — renamed to _update_bucket_settings.

katieschilling and others added 4 commits May 4, 2026 13:56
Address review feedback from ovaistariq:

- create_workspace: rename `name` parameter to `bucket` for consistency
  with the rest of the surface; default `enable_snapshots=True` since
  checkpointing is the common path; on credential-provisioning failure,
  roll back the bucket so the caller isn't left with an orphan
- teardown_workspace / teardown_forks: drop the manual _empty_bucket
  helpers entirely and use Tigris's standard force-delete extension
  via a new public helpers.delete_bucket(s3, bucket, *, force=False)
  (sends Tigris-Force-Delete: true)
- restore: default fork name now embeds the snapshot id
  (`f"{bucket}-restore-{snapshot_id}"`) so the bucket name reflects the
  point-in-time it represents, not when restore happened
- create_forks: default prefix likewise switches to snapshot_id-based
  for the same reason and to avoid timestamp collisions
- Remove the thin pass-through wrappers and dead code: list_checkpoints
  (callers can use list_snapshots and parse the Name field),
  setup_coordination / teardown_coordination (use
  set_object_notifications / clear_object_notifications directly),
  internal _revoke and _empty_bucket family
- Rename object_notifications._patch_bucket -> _update_bucket_settings
- Drop the "mirrors @tigrisdata/agent-kit" framing from the module
  docstring and README; describe the implementation instead

13 integration tests against real Tigris pass (incl. force-delete on
non-empty buckets and the credential rollback path); 81 unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Conflicting hunks in README.md and tigris_boto3_ext/__init__.py
resolved in favor of main (rename_object additions). Agent Kit /
object notifications exports kept since they're disjoint additions
that were auto-merged at the import level.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
object_notifications.py's module docstring still pointed at
``setup_coordination`` / ``teardown_coordination`` as thin wrappers,
but those were removed in 8f1d60e. The reference was a leftover from
the agent-kit framing the maintainer asked to drop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ovaistariq pushed back twice on the \"workspace bucket\" framing:

- 3165580620: \"Why call it \`workspace bucket\`? Call it just \`workspace\`\"
- 3165581906: \"\`name\` is workspace name. Calling it both workspace and
  bucket is a bit odd.\"

The function summary was already cleaned up. The Args entry for the
\`bucket\` parameter still leaked the dual framing (\"Workspace bucket
name.\"), so simplify it to \"Name for the workspace.\"

Co-Authored-By: Claude Opus 4.7 <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.

2 participants