Skip to content

feat(artifact): neutral twins for the S3-named artifact keys and keywords - #99

Merged
earakely-scale merged 3 commits into
mainfrom
edgararakelyan/artifact-object-url-keys
Oct 7, 2026
Merged

earakely-scale merged 3 commits into
mainfrom
edgararakelyan/artifact-object-url-keys

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Neutral twins for the S3-named artifact keys.

  • Artifact attributes are already store-neutral: object_url, tar_gz_object_url, build_context_object_url, bundle_object_url, skill_object_url and cli_object_url. Their documents only carry the S3-named aliases: s3_url, tar_gz_s3_url, build_context_s3_url, bundle_s3_url, skill_s3_url and cli_s3_url.
  • Each of the five classes now accepts either key on read. When a stored doc's two keys differ, the legacy one wins, as with the precedent: only a writer outside core could make them differ, and those only know the legacy key.
  • Every dump writes both keys, taken from the attribute. This follows the environment_* / service_* precedent: a wrap model_serializer per class, since the Artifact base is plugin surface.
  • A dump that excludes the field still excludes both keys.
  • The serializers have no return annotation. Pydantic builds the serialization JSON schema from one, so -> dict[str, Any] turned each model's into a bare object without its fields. EnvironmentArtifact and EnvironmentUniverseArtifact already had that problem and are fixed here too.
  • Readers on older versions ignore the extra key, since the base model keeps pydantic's default extra='ignore'. Stored documents are not rewritten, and read compatibility for the legacy keys is permanent.

Neutral keywords for the put helpers.

  • FileArtifactUniverse.put(bundle_object_url=), put_bundled(prefix_url=) and put_existing(prefix_url=);
  • DockerImageArtifact.put_tar(tar_gz_object_url=, build_context_object_url=);
  • SkillArtifact.validate(object_url=).

The S3-named keywords keep working for now through utils.deprecation.warn_deprecated, which warns and logs a countable agent_env_deprecated_symbol line. They default to deprecation.OMITTED, so an old keyword passed as None still counts. Passing both spellings is a TypeError. Core's own callers and the test fakes move to the neutral spellings.

Also in this PR:

  • Sandbox aliases: Sandbox.write_file_from_s3 / VmSandbox.load_s3_file, from refactor(sandbox): object-store names for sandbox object copies; read handed-in urls from the store that holds them #97, now go through warn_deprecated too, so their remaining callers can be counted before they're removed.
  • FileArtifact.put_existing: drops its <scheme>://<bucket>/<key> split. It refuses a trailing /, and the store decides whether anything exists at the URL. S3ObjectStore now treats a URL with no key as no object, instead of sending S3 an empty-key HEAD that botocore refuses as malformed, so a bucket-root URL is still refused as "does not exist".
  • Explorer: reads s3_url, else object_url, for a universe's files, the same order as the models.
  • Docstrings: the touched artifact modules say "object store" instead of S3.

Testing

  • Unit:
    • For each class, a document keyed only by the legacy names, or only by the neutral ones, loads, and its dump (by_alias either way) carries both keys.
    • When the two keys differ, the legacy one wins.
    • An excluded field stays excluded.
    • Each class's serialization schema, including the two precedent classes, keeps its fields.
    • Each deprecated keyword warns and still works, and its use is logged as agent_env_deprecated_symbol, even when passed as None. Passing both spellings is a TypeError, including with the old one None, and a required keyword is still required.
    • The sandbox aliases log the same event.
    • An S3 bucket URL resolves to no object without a request, and put_existing on one is refused as "does not exist".
    • The explorer returns a file's URL whether its doc is keyed legacy, neutral, or both with the legacy one winning.
    • The goldens are regenerated: six added keys, each equal to its legacy twin. _AE3_TWINS gains the six pairs, so the both-or-neither invariant covers them.
    • Suite: 6161 passed, 13 skipped.
  • Mutants (out-of-tree): 16 of 16 caught. Dropping a twin write, populate_by_name, the twin's exclude check, the keyword warning, its both-spellings check or its OMITTED sentinel each fails a test, as does reannotating a serializer (new or precedent). So do reverting the put_existing change or S3's empty-key guard, flipping the explorer to neutral-first or dropping its fallback, and removing the sandbox alias's counted warning or making it a plain warnings.warn.
  • Downstream: the sdk's unit suite passes against this core, apart from its two signed_post tests, which the earlier signed_post removal already breaks.
  • Dev stage (Mongo + S3):
    • One artifact of each type, written through this branch, stored both keys with equal values.
    • The same documents load with 0.9.1282 (pre-twin) and with this branch.
    • A FileArtifact written earlier by older code, with only s3_url, loads with this branch.

Labelled bump-version.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the earlier old-keyword issue is fixed.

What we checked:

  • Old None keyword goes unnoticed: No. renamed_keyword checks whether the old keyword was omitted, then warns when it was passed, even as None.

Summary

Artifact URLs can now use store-neutral names while saved documents keep both the old and new keys. The PR also adds counted deprecation paths for old helper keywords and sandbox methods, updates Explorer reads, and changes how existing-file URLs are checked.

  • Artifact documents write both names for each object URL.
  • Artifact helpers accept neutral URL keywords and count use of old spellings.
  • Existing-file URLs are checked by the object store.
  • Explorer file details read either URL key spelling.
  • Old S3-named sandbox methods log their use as deprecated.
  • Artifact docstrings use object-store wording.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Old or neutral URL key] --> B[Artifact model]
    B --> C[Artifact URL attribute]
    C --> D[Stored document with both keys]
Loading

Reviews (3) · Last reviewed commit: "fix(artifact): an S3-named keyword passe..." · Reviewed by Greptile

…d keywords

Artifact docs keep their S3-named keys (s3_url, tar_gz_s3_url,
build_context_s3_url, bundle_s3_url, skill_s3_url, cli_s3_url) and now also
carry the attribute's own name beside each (object_url, tar_gz_object_url, ...).
A doc keyed either way loads, and every dump writes both, from the attribute, so
readers of either spelling keep working; a field a dump excludes stays excluded.
Readers on older versions ignore the extra key.

The put helpers take neutral keywords: FileArtifactUniverse.put(bundle_object_url=),
put_bundled(prefix_url=), put_existing(prefix_url=),
DockerImageArtifact.put_tar(tar_gz_object_url=, build_context_object_url=) and
SkillArtifact.validate(object_url=). The S3-named ones still work, through
utils.deprecation's warn_deprecated, whose log line counts their callers; so do
the sandbox's write_file_from_s3 / load_s3_file aliases now. Core's own callers
move to the neutral spellings.

FileArtifact.put_existing no longer splits <scheme>://<bucket>/<key>: it refuses
a trailing slash and leaves the rest to the store. The explorer reads either key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale
earakely-scale requested a review from a team as a code owner October 7, 2026 17:11
Comment thread src/agent_env/utils/deprecation.py Outdated
earakely-scale and others added 2 commits October 7, 2026 10:26
… key first, and size no S3 bucket

- The twin-writing wrap serializers lose their `-> dict[str, Any]` return
  annotation: pydantic builds the serialization JSON schema from it, so every
  twinned model (and EnvironmentArtifact / EnvironmentUniverseArtifact, which
  already had it) served a bare object without its fields to anything rendering
  that schema, such as a FastAPI response model.
- The explorer reads a universe file's s3_url before its object_url, as the
  model does: while both are written, a raw-doc writer that knows only s3_url
  would leave the twin stale.
- S3ObjectStore treats a URL without a key as no object instead of sending S3 an
  empty-key HEAD, which botocore refuses as a malformed request; put_existing on a
  bucket URL is refused as "does not exist" again rather than with a
  ParamValidationError.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
renamed_keyword took an old keyword passed as None for one left out, so a
caller passing an optional old keyword through (build_context_s3_url=None)
was neither warned nor counted, and would break unseen when the keyword is
removed; with the new spelling also set it skipped the both-spellings
TypeError. The old keywords now default to deprecation.OMITTED instead of None.

Two tests still passing put_bundled / put_existing the old s3_url= keyword move
to prefix_url=.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale
earakely-scale merged commit 0f06886 into main Oct 7, 2026
14 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/artifact-object-url-keys branch October 7, 2026 17:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant