Skip to content

Releases: Snaga-AI/uarp-sdks

v0.12.0

Choose a tag to compare

@chabanov chabanov released this 18 Oct 22:10
4460278

Build c65c901c, digest 092f377d6340cec2. The five existing SDKs change only
in their version; what is new is a sixth.

Changed — runs.create is typed by the document alone

Build c65c901c lists 202 as the only success of POST /api/v1/runs and
moves the sponsored 200/201 answers, which only a body carrying
execution_requirements receives, to x-uarp-sponsored-responses. The
generator's pin that typed createRun by its 202 Run in 0.11.0 is removed;
the generated code is byte-identical, and the test that pinned it now holds
from the document (it fails against build 0837bb4d without the pin).

Added — a Python SDK

packages/python, published to PyPI as uarp-sdk (import uarp_sdk). Python
3.10+, one dependency, httpx. It is generated from the same document by a
sixth emitter, generator/src/emit/python.ts, and covers the same surface as
the other five: 843 operations in 53 groups and 2064 named types, every one
reachable from a blocking Client and from an asyncio AsyncClient.

  • Models are keyword-only dataclasses and enums str enums. Responses
    decode strictly, as Rust, Swift and Kotlin do: an absent required member or a
    value of the wrong type raises DecodeError with its path, and keeps the
    parsed body on the error. An enum value the server adds later decodes to an
    instance that reports is_known as false. An optional property left at
    None is not sent; a body passed as a plain dict goes on the wire as it is,
    which is how to send an explicit null.
  • Transport behaves as the other five: the same retry statuses and
    backoff, Retry-After and X-Should-Retry, an Idempotency-Key on every
    mutating /api/v1/* call and retries only for calls that carry one, strict
    percent-encoding, form bodies in schema order, code and reason on
    APIError, event streams that reconnect with Last-Event-ID, and
    stream_post, which never retries.
  • Checked: contract/run.sh runs the 22 scenarios through a Python runner,
    and its 24 requests are identical to TypeScript's and Rust's, byte for byte,
    with the same probe readings. The 209 bodies read from production at build
    e0b37b03 all decode through the generated models, and the four doctored
    control bodies fail exactly as they fail in Rust (three refused, one
    accepted). Every parameterless GET was then called on production through
    the SDK itself, read-only: 128 answers decoded, four event streams opened and
    delivered, and the rest were refusals the key explains (77 403 on admin
    routes, six 404, two 400, one 501). mypy --strict passes over the
    package, and every fixture's output is imported and type-checked by the
    generator's compile test.
  • Falsified: each of fourteen calls in the runtime was removed in turn
    (the idempotency key, the 429 retry, the problem document, reason, the
    unknown-enum fallback, the omission of unset fields, lower-case booleans,
    JSON comment frames, Last-Event-ID replay, the content-type check in
    stream_post, decoding, the repeated-cursor guard, schema-ordered form
    fields, strict query encoding). Each turns between one and ten of the 90
    tests red and leaves the others green.
  • Not yet: exportMyAccount — the document offers the export as JSON or
    as a zip, every SDK asks for JSON (Accept: application/json), and
    production answered the zip, so the Python call raises DecodeError. The
    generator types every SDK from the same JSON half; this is the open item on
    how exports are typed, not something Python adds. The live scenario in smoke/live
    ran against production on 2026-10-18 through TypeScript, Python, Rust, Swift
    and Kotlin: all five read the same values, and the agent it created was
    deleted.

v0.11.0

Choose a tag to compare

@chabanov chabanov released this 18 Oct 22:10
2d58022

Build 0837bb4d, canonical digest 47152b3cf58faed3. Seven platform builds
are collected here: ef5b646d (2026-10-12), 435ca06c and 199b0863
(2026-10-13), b108e9a9 (2026-10-15), d926b90e (2026-10-16), e0b37b03 and
0837bb4d (2026-10-17). Against 0.10.0: 585 → 642 paths, 789 → 857 operations
(+68, −0), 382 → 453 schemas (+71, −0), 50 → 54 groups, 1845 → 2129 named
types (+284, −0 net).

One family of exported names leaves. Measured against the released 0.10.0
rather than against the intermediate drafts: 343 names are added and four are
removed: GetReadyResponse, GetReadyResponseChecks, GetReadyResponseStatus
and GET_READY_RESPONSE_STATUS_VALUES. GET /ready now answers the shared
ReadinessReport, as GET /health/ready already did (below). Every other
import that compiled against 0.10.0 still resolves. The eleven /companies
operations warn at compile time rather than break.

The minor moves because types that 0.10.0 already exported change shape
under their own names — ResourceUsageEntry.limit becomes nullable,
RunEventType renames seven values, the workspace copy and move requests stop
requiring their paths, build b108e9a9 corrects eight of them to the wire,
getSession answers a new type, build e0b37b03 corrects five more to
the wire, and AgentStatus gains inactive (all below). Under 1.0 a caret does not
cross the minor, so ^0.10 keeps building against 0.10.x.

Changed — ResourceUsageEntry.limit is nullable

The field is still required and its type is now number | null. Null is not
zero
: it means the plan defines no limit for that resource, and over_by is
0 beside it, while limit: 0 means the plan allows none. A client that printed
the two the same way told a founder "3 of 0" about a resource with no ceiling
(macOS lane, measured 2026-10-12). In TypeScript a comparison against limit
now needs the null case handled; in Rust, Swift, Kotlin and Ada the field goes
from a plain number to an optional one. That is the break, it is one field, and
one of the reasons this release is not a patch.

Changed — RunEventType renames seven values

Seven company.* event types become initiative.*: tick_start, tick_end,
paused, resumed, budget_updated, escalation, objective_updated. A
consumer comparing an emitted RunEvent.type against the old literals stops
matching — in TypeScript at compile time, which is the good case.

Worth knowing for anyone filtering a stream: the types query parameter of
streamActivityFeed is a free-form string, not this enum, so a filter is an
exact match with nothing checking it. types=company.tick_start does not fail;
it returns an empty stream, which reads exactly like "nothing happened".
FeedEntryEventType and WebhookDeliveryAttemptEventType keep the old
spellings and gain the new ones, so historical deliveries still decode.

Changed — workspace copy and move no longer require their paths

CopyWorkspaceFileRequest.source_path/dest_path and
MoveWorkspaceFileRequest.from_path/to_path go from required to optional
(build ef5b646d). Sending them is unchanged in TypeScript, Swift and Kotlin,
whose constructors take the old arguments as before. In Rust the fields become
Option<String>, so a struct literal that set source_path: "a".into() stops
compiling and needs Some(…); in Kotlin and Swift, code that reads the field
back gets an optional. Found on 2026-10-14 by diffing the IR of the released
0.10.0 document against this one, after the two breaks above had already been
written down as the whole list.

Added — an API error carries the code to switch on

Every problem document the platform sends has a code — SESSION_NOT_ACTIVE,
QUOTA_EXCEEDED, resource_limit_reached, AUTH_ERROR — and the document
says it is what a client should switch on, with detail being prose for a
person. No SDK exposed it. Swift and Kotlin dropped it during decoding, and
TypeScript and Rust kept it only as an untyped leftover. So the macOS client
recognised an ended chat by matching the exact English sentence (reported by
the macOS lane, 2026-10-15). All five now have it:
error.code / api.code() / Problem.Code.

It is a string, not the ErrorCode enum, on purpose: the canon tenant was
receiving SIGNING_NOT_CONFIGURED from GET /registry/keys while the enum did
not list it, and with an enum that whole error would have failed to decode.
Build d926b90e lists it, with nine other codes; a string keeps the next code
the server sends before the document names it from failing the same way.

Two more things came out of reading real error bodies:

  • The request id. It was read only from the deprecated correlationId.
    It is now read from correlation_id first, then correlationId. The header
    fallback read X-Correlation-Id, which the server does not send; it now
    reads X-Request-Id, which it does (production, 2026-10-15).
    X-Correlation-Id is still read after it.
  • Every other member. Swift's Problem.extensions and Kotlin's
    Problem.extensions ([String: JSONValue] / Map<String, JsonElement>)
    hold the members the type does not model, such as session_status, reason
    and retry_after. Rust already had them in problem.extra and TypeScript in
    the index signature. Swift's APIError also gains body, the raw bytes,
    which Kotlin's ApiException and Ada's Problem.Raw already had.
  • reason, typed. A refused X-Active-Tenant answers 403 FORBIDDEN
    with reason set to not_a_member or membership_suspended, which is how a
    client tells a lost company from any other refusal (the macOS lane's W33).
    error.reason / api.reason() / Problem.Reason read it in all five. It is
    read from the member, not decoded as a field, and is absent when the member
    is absent or not a string, so a different shape cannot fail the whole error.

What can break:

  • Rust: Problem gains code and correlation_id_legacy, so an
    exhaustive struct literal needs them, or ..Default::default().
    problem.correlation_id now holds correlation_id.
  • Ada: Problem gains Code and Reason, so a record aggregate
    that names every component needs them, or others => <>.
  • Kotlin: Problem.correlationId now reads correlation_id. A server that
    sends only the old spelling, as the 501 above does, leaves it null and fills
    legacyCorrelationId. ApiException.correlationId reads both.

Tested on two real bodies in all five SDKs: the 401 production answered with no
key, and the canon 501. Ada's test is written but, as before, not run here.
Removing the read of code and of X-Request-Id turns 3 tests red in
TypeScript, Rust and Kotlin and 2 in Swift (its third reads problem.code
directly), and leaves the rest green. Extension members are tested on
the 422 the session route answers for a closed session, with
code: SESSION_NOT_ACTIVE and a top-level session_status: "closed". The API
lane captured those bytes from the deployed handler, since reproducing the
refusal on canon would take a write. Dropping the collected members turns 1
test red in Swift and 1 in Kotlin, out of 63 each.

reason is tested on three bodies. not_a_member is what production answers
at e0b37b03 (2026-10-17). membership_suspended is the API lane's capture
from handleRequest at d6b1c66e5, the code that build deploys: producing it
on the wire would mean suspending a membership on canon. The third is the same
refusal as production answered it at d926b90e, the day before, with no code
and no reason: there reason must be absent, and is. Removing the read of reason
turns that one test red in each of TypeScript (1 of 68), Rust (1 of 46),
Swift (2 assertions in 1 of 65) and Kotlin (1 of 65), and leaves every other
test green. Ada's check is written, not run.

Changed — build b108e9a9: eight types follow the wire

Every one of these is the document catching up with what the server already
sent or required, measured before the deploy: on build 199b0863 the Rust SDK
failed to decode eight live canon bodies, and after regenerating it decodes all
eight, re-fetched from b108e9a9 and as captured before it. A consumer of
0.10.0 that hit these operations was already getting an error, which is why
fixing them breaks code that compiles rather than code that worked.

  • CreateVotingProposalRequest: the field is type, typed by the
    CreateVotingProposalRequestType enum. The server has required type since
    2026-08-31 and dropped proposal_type, so 0.10.0's required proposal_type
    got a 422 on every call; from this build the server also reads
    proposal_type as a deprecated alias when type is absent. Both are
    optional in the type, since either one satisfies the server. In Rust a struct
    literal setting proposal_type: "…".into() needs Some(…).
  • GetTeamChatHistoryResponse.chat_state and the squad equivalent: an
    object in 0.10.0, a string on the wire (IDLE, RUNNING, DONE and three
    more), now the GetTeamChatHistoryResponseChatState enum.
  • TeamRunDetail.runs: Run[] → TeamRunDetailRun[], a projection
    without tenant_id, which the route never sent.
  • Goal.agent_id, ArbiterCase.filed_by / against_agent_id / rule_ids,
    VotingProposal.type / proposed_by, AuthProvider.email
    : required →
    optional. An unlinked auth provider has no email; the governance rows did not
    carry the others. In Rust, Swift and Kotlin these become optionals for code
    that reads them.
  • Added: next_step on the tenant company overview (with
    GetTenantCompanyOverviewResponseNextStepKind: setup, resume, decide,
    problem, wait) and company.objective (string | null), both optional,
    because a server before this build does not send them. SESSION_NOT_ACTIVE
    joins ErrorCode. Nine operations gain x-capability.

Changed — build d926b90e: getSession answers its own type, and seven hi...

Read more

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 12 Oct 13:02
eaab09d

Build 0db7945f, canonical digest 68d0c59b8cb92f65. Against 0.9.0 the
counts do not move: 585 paths, 789 operations, 382 schemas.

The minor moves because one exported field goes away (below). Under 1.0 a
caret does not cross the minor, so ^0.9 keeps building against 0.9.x.

Removed — RunEvaluationRequest.agent_version

The document no longer declares it on POST /agents/{agentId}/evaluations.
The route never read it, so a value sent there did nothing. Code that set it
stops compiling; delete the line.

Added

  • price_interval and price_interval_count on every plan from
    GET /billing/plans and GET /public/plans. They give how often
    price_amount_cents bills: "99,00 $ / month", or every 3 months for a
    quarterly price. Both are null for a free plan and whenever Stripe cannot be
    read; show the amount alone then. price_interval is typed as a nullable
    string, not an enum. The document lists day, week, month, year and
    one_time together with null, and the generator still types any enum
    that lists null as a plain string. Seventeen fields elsewhere already
    work that way.
  • scorers (names of the scorers to apply) and auto_rollback_on_regression
    on RunEvaluationRequest.
  • METHOD_NOT_ALLOWED in ErrorCode. Every 400, 401, 404, 405, 409, 413, 422
    and 429 now carries a code; a 403 carries one only where its call site
    names it.

Changed — descriptions only

Session.updated_at: creating or switching a branch (build 928c3ff3), like
pinning and archiving, does not move it. POST /sessions and the metadata
of PUT /sessions/{sessionId}: metadata.project_id must name an existing
project the caller may edit, or the call answers 404 or 403.
EvalCase.expected_output is described.

Read on the wire

GET /public/plans needs no key, so the plan fields were read on production
on 2026-10-12: four plans. free has price_interval and
price_interval_count both null. starter, pro and enterprise have
"month" and 1. GET /billing/plans needs a key, which is not available
here, so it was not read.

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 06:33
59fde9d

Build 917d6ba8, canonical digest 945fb7ee308afd8b. Against 0.8.0: 568 →
585 paths, 766 → 789 operations (+23, −0), 361 → 382 schemas (+21, −0).
The refresh was cut at 15987a5f; 917d6ba8 (uarp #556) deployed while it
was in CI and changed only the descriptions of Session.updated_at, pinned
and archived: a pin or archive no longer bumps updated_at.

The minor moves because eight exported type names change (below). Under
1.0 a caret does not cross the minor, so ^0.8 keeps building. The Swift
mirror's from: requirement does cross it: an app that pins from: "0.8.0"
resolves 0.9.0. An app that pins upToNextMinorVersion stays where it is.

Changed — map value types are named after their map

Until now, every additionalProperties object was hoisted as Value, and a
counter told them apart. One new map earlier in the document therefore
renumbered every later one. This build's pricing would have taken Value6.
The published Value6 ({rpm}) would then have become Value7, and Value7
({en, uk}) would have become Value8. In TypeScript that change is silent:
Value7 would compile against a different shape. Each value type now takes
its map's name, as every other hoisted type takes its parent's:

0.8.0 0.9.0 Map
Value LandingConfigSectionTextsValue LandingConfigSection.texts
Value2 SpecToolCatalogToolsValue SpecToolCatalog.tools
Value3 FleetLayoutUpdatePositionsValue FleetLayoutUpdate.positions
Value4 FleetLayoutPositionsValue FleetLayout.positions
Value5 GetAgentVersionDiffResponseDiffValue GetAgentVersionDiffResponse.diff
Value6 UpdateAdminLLMAdaptersConfigRequestProviderRateLimitsValue UpdateAdminLLMAdaptersConfigRequest.provider_rate_limits
Value7 AdminPutLandingConfigRequestTextsValue AdminPutLandingConfigRequest.texts
— GetClientConfigResponsePricingValue GetClientConfigResponse.pricing (new)

Code that reads through the map (positions["a"].x) is unaffected. Only
code that spells the type's name changes.

Fixed — array query parameters, in four of the five SDKs

listPublicJobs is the first operation in the document with array query
parameters (workplace_type, employment_type, seniority, category and
country, each style: form, explode: true). Only TypeScript sent them
right, as seniority=junior&seniority=senior. The other four failed in four
different ways:

SDK What seniority = [junior, senior] did
Rust Error::Encode before sending: serde_urlencoded cannot write a sequence. Replaced by serde_html_form, which writes the key once per item, in field order
Swift sent seniority=[UARP.JobSeniority(rawValue: "junior"), …] via String(describing:)
Kotlin sent seniority=[junior, senior] via toString()
Ada did not compile; CI caught it, and the other three compiled

All five now repeat the key, one item at a time. Ada also learned to send an
array in a multipart body: applyToJob's links goes as one text part holding
a compact JSON array, as the other four already sent it. Contract scenarios 21
and 22 compare the bytes. Reverting the Kotlin fix sends
seniority=[junior, senior], and scenario 21 catches it.

Added — fields clients were sending or reading by hand

  • Sessions archive and pin. updateSession accepts archived and
    pinned. Session gains archived, archived_at, pinned and
    pinned_at; each is absent while its flag is off. listSessions takes
    archived (false by default, true, all) and pinned.
  • Sign in with Apple. appleNativeAuth takes authorization_code.
    DELETE /me answers apple_tokens_revoked.
  • McpServerAuth.type gains oauth.
  • ErrorCode gains ACTIVE_TENANT_NOT_SUPPORTED, STARTED_BY_ANOTHER_USER,
    run_approval_expired, run_stalled, and the careers codes
    (consent_required, cv_invalid, cv_too_large, duplicate_application,
    job_apply_elsewhere, job_closed).
  • patchTenant declares its body, which had no properties before.
    createSessionTodo declares six more fields. createTenant (admin) takes
    email.

Added — twenty-three operations

  • MCP one-click connect (6): listMcpCatalog, connectMcpServer,
    reconnectMcpServer, completeMcpOAuth, and the two public OAuth routes
    mcpOAuthCallback and getMcpOAuthClientMetadata. They come with the
    McpCatalogEntry, McpConnectRequest, McpConnectResult,
    McpOAuthReturnTo and McpServerOAuth schemas.
  • Careers (15): twelve under /admin/jobs and three public operations,
    listPublicJobs, getPublicJob and applyToJob. applyToJob takes a
    multipart body carrying the CV.
  • Registry (2): installEssentialsOnHeadAgents and
    listRegistrySigningKeys.

Not verified on the wire

No API key was available where this refresh was made. The request and
response shapes of the twenty-three new operations come from the document,
and no live call has checked them.

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 06 Oct 13:58
52f8d57

Build 4663ef46, canonical digest 072de50db92ed09c. Against 0.7.0: 544 →
568 paths, 736 → 766 operations (+30, −0), 352 → 361 schemas (+9, −0).

The minor moves because Rust's floor does (1.88 → 1.89, below): a caret
under 1.0 does not cross the minor, so a 1.88 toolchain pinned to ^0.7 keeps
building. Nothing else breaks on purpose — no exported name is removed (see
"enum names are pinned") and the completions keep their JSON signatures.

Added — streamPost, a POST read as an event stream, in all five SDKs

The platform cuts a silent non-streamed POST /api/v1/llm/chat/completions at
120 s (measured 2026-10-05, 504 Request timed out); the same request with
"stream": true answers text/event-stream in about three seconds and ran
438 s to the end. Since 4663ef46 the platform gives the non-streamed call
600 s too, but a streamed answer is still the one no gateway or proxy cuts.

SDK Call
TypeScript client.streamPost(path, body, options?) → EventStream
Rust Client::stream_post(path, &body) → EventStream
Swift UARPClient.streamPost(path:body:options:) → EventStream
Kotlin UarpClient.streamPost(path, body, options) → Flow<ServerEvent>
Ada UARP.Client.Execute_Stream (non-raising) and Stream_Post

Same rules everywhere (contract/SCENARIOS.md, "Streaming a POST"):
Accept: text/event-stream, the JSON body and the automatic Idempotency-Key;
one attempt — no reconnect and no retry, not even on 429, because a second
POST would run and bill the model twice; the stream ends at data: [DONE]
(not delivered); a refusal, or a 2xx that is not text/event-stream, is the
SDK's API error with the status and the body, never events; a connection that
breaks mid-stream is an error, not an end. Contract scenarios 17–19.

Added — form-encoded request bodies in every SDK

POST /api/v1/auth/oauth/{provider}/callback (completeOAuthLoginFormPost,
Sign in with Apple on the web) takes application/x-www-form-urlencoded — the
first such body in the document. TypeScript rendered form bodies already;
Rust, Swift, Kotlin and Ada now do too, all byte for byte the way the WHATWG
serializer (URLSearchParams) does: fields in schema order, absent ones
skipped, a space as +, everything but A-Z a-z 0-9 * - . _ percent-encoded
as upper-case UTF-8 — ~ included. Contract scenario 20 compares the bytes.
Ada renders string fields; a field of another type stops generation rather
than guess an encoding (no such field exists today). New runtime pieces:
Rust form module, Kotlin Body.Form, Swift RequestBody.form, Ada
UARP.Form.

Added — thirty operations

  • Video orders and templates (20): the public order flow
    (createPublicVideoOrder — multipart — then getPublicVideoOrder, its
    checkout, feedback, regenerate, retry, video, inputs, and the
    template list and preview) and the admin side (template CRUD, preview,
    publish, unpublish, test, and the order list).
  • Anthropic-compatible POST /v1/messages (anthropicMessages) and
    POST /v1/messages/count_tokens, in a new AnthropicCompat group.
  • completeOAuthLoginFormPost (above); GET/PUT /admin/config/search;
    GET /bridge/deleted-machines and DELETE …/{machineId} (reconnect);
    POST /sessions/import; POST /tools/web_search;
    POST /acp/session/{sessionId}.
  • Paging for eighteen lists that now declare their cursor: run steps and
    audit logs, session messages and annotations, agent memory, bookmarks and
    evaluations, users and invites, webhook deliveries, squad and team chat and
    runs, knowledge-base documents, governance violations, company activity,
    the admin audit trail. Each gets its paging helper.

Changed

  • Rust: MSRV 1.88 → 1.89. uuid 1.27 declares 1.89; the crate keeps no
    lockfile, so the MSRV job resolves it and went red. The promise follows the
    tree rather than capping uuid away from its fixes.
  • The completions stay typed by their JSON. The document now declares
    text/event-stream beside application/json for llmChatCompletion,
    chatCompletion and anthropicMessages. Typing them by the stream would
    have turned methods that returned a completion into ones that return
    events — breaking every caller who never asked to stream — and the
    reconnecting GET stream would have replayed a billed POST. They are typed by
    their JSON answer, as in 0.7.0; the streamed mode is streamPost.
  • Enum names are pinned (generator/enum-names.json). A shared enum is
    named after the first schema that uses its value set, so this refresh would
    have renamed seventeen published types (AgentExecutionMode →
    AgentUpdateExecutionMode, TeamPoliciesEffort →
    AgentUpdateThinkingEffortVariant1, …). Published names are now kept, new
    ones are added on regeneration, and --check fails if the pins are stale.

Fixed

  • Ada: a stream the caller stops is stopped over HTTP/2. libcurl reports
    the abort as CURLE_RECV_ERROR (56) over HTTP/2, CURLE_WRITE_ERROR (23)
    over HTTP/1.1, and only 23 counted — every streamed completion that ended at
    [DONE] against api.snaga.ai raised Transport_Error, and so did a run
    stream stopped at run.completed. Verified live; the mock speaks HTTP/1.1.
  • Swift: enum path parameters went out as
    TypeName(rawValue: "apple"), percent-encoded, instead of apple —
    seventeen call sites, the OAuth start and callback among them.
  • Multipart text parts of enum type (createPublicVideoOrder's locale
    and consents) are sent as their wire values in Kotlin, Swift and Ada.
  • Ada: an alias of free-form JSON (BridgeAgentStats) generated calls to
    conversions that do not exist.
  • TypeScript: a form body's fields follow the schema order, not the
    caller's.

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 23 Sep 08:44
b9d555c

Build f9f433ee, canonical digest 9de209f5e0d40b51 — sorted keys, no
whitespace, which is what check-spec-freshness.sh compares and what the
release refuses to proceed without. Against 0.6.0, the last release published
to any registry: 535 → 544 paths, 723 → 736 operations (+16, −3), 352 → 352
schemas (+3, −3).

The first v0.7.0 tag, on 2026-09-22 at build 34cc8169, published nothing:
the platform deployed 3a9d4c08 between the regeneration and the tag, and the
release job refused the stale document in its first step, before any registry
was touched. 5732283f, c5694dc0 and f9f433ee followed while the refresh
was in CI. This entry describes the second cut.

The minor moves, and that is the point. A caret under 1.0 does not cross
the minor, so ^0.6.0 admits 0.6.1 automatically and refuses 0.7.0. This
release removes exported names and adds required request fields; on a patch
those would have arrived in builds nobody touched, and the first anyone heard
of it would have been a red compile. The number is the announcement.

Removed — three operations and eighteen exported names

The SPEC-package surface is withdrawn on the platform (uarp f058b582), so
it leaves the clients:

  • GET and PUT /admin/config/spec-packages, and
    POST /admin/config/spec-packages/{packageId}/stripe-price.
  • Eighteen names disappear from the TypeScript models (1679 exports → 1720,
    with these gone): SpecPackage, SpecPackagePricing,
    SpecPackagePricingBillingInterval, SpecPackageIncludedInPlan,
    SpecPackageProgram, SpecPackageProgramNav, SpecPackageProgramPage,
    AdminSpecPackagesList, UpdateAdminSpecPackagesRequest,
    CreateAdminSpecPackageStripePriceResponse,
    CreateSpecPackageCheckoutSessionRequest,
    ListBillingSpecPackagesResponsePackage and its …Entitlement,
    DeleteCustomPlanForce, and the four *_VALUES consts of those enums.

GET /billing/spec-packages and its checkout do NOT leave: the withdrawal
left the list answering [] and the checkout answering 410, both marked
deprecated, and both stay generated for clients that still call them.

Changed — six operations now require body fields they did not

Breaking for any call site that compiled without them:

Operation Newly required
POST /governance/goals title, description, rationale, expected_impact, alignment_justification, resource_estimate_usd
POST /governance/improvement/{agentId} title, description, rationale, changes, baseline_success_rate, failed_run_ids
POST /governance/arbiter/cases description, rule_ids
POST /bridge/delegate message
POST /marketplace/listings/{listingId}/subscribe stripe_subscription_id
POST /programs/{programId}/apply start_date

POST /a2a/tasks tightens without a new top-level field: each entry of
messages now requires role ('user' | 'agent', no longer any string) and
a non-empty parts typed as A2APart[] rather than bare objects, and the
list itself needs at least one entry. The handler refused all of these with
422 already; the types now say so before the request is sent.

The first three were unusable in 0.6.0 rather than merely under-declared:
CreateGoalRequest shipped with one field beside a doc comment naming seven,
so no client generated from that document could form a valid request. They
compile differently here because they now describe what the handler actually
demands.

Added — a failed run says why, in a field

  • Run.error_code and Run.error_details, and the same pair on
    TenantOverviewRunsRecentItem — the thinner shape the fleet board reads.
    error_code is absent when the failure carries nothing to branch on, which
    is deliberate; error_details carries what the code cannot
    (retry_after_ms, quota_exhausted, stale_seconds).
  • ErrorCode and ERROR_CODE_VALUES, 78 values — among the newest,
    approval_rejected for a run that stopped because a person refused its tool
    call, and kb_embedding_failed. New, not widened: 0.6.0
    declared no code on Error at all, in the tag or in the published
    tarball, so there was nothing for a client to branch on.

Until this release no generated client could learn why a run failed except by
matching English in error — a sentence with no Accept-Language behind it.

Added — the rest of the surface

  • SpecToolCatalogSpec with tools_waiting and
    status: 'ready' | 'needs_connection': an installed SPEC that is waiting on
    a connection says so instead of vanishing.
  • UsageQuotaCounter and its eight kinds (runs, tokens, tokens_daily,
    tool_calls, agents, teams, knowledge_bases, workspaces).
  • GET|PUT /agents/{agentId}/mcp-servers with McpServerAuth — the connect
    surface between an agent and the MCP servers its tenant has installed.
  • Billing: GET|PUT|DELETE /billing/budget, GET|PUT /billing/overage,
    GET /billing/promo, POST /billing/promo/redeem.
  • GET /admin/data-explorer/export, POST /admin/data-explorer/import,
    POST /auth/oauth/nonce (served undocumented until uarp #487),
    GET /bridge/agent-specs, GET /companies/{companyId}/events.
  • SubjectSweep, TenantSocialLinks.
  • GET /agents/{agentId}/memory/core and ListCoreMemoryBlocksResponse —
    every core memory block of an agent in one call, beside the per-label routes.
  • GET /agents/{agentId}/experiments and ListExperimentsResponse — an
    agent's evaluation experiments, newest first.
  • InboxItem.tools (InboxItemTool): an approval row names each tool the
    run waits on, with how many calls named it, as data rather than prose.
  • Run.approvals (RunApproval, decision 'approved' | 'rejected'): each
    human decision on a tool approval the run waited for, oldest first. Absent
    on runs from before 2026-09-22, when nothing was recorded.
  • POST /runs/{runId}/resume takes an optional ResumeRunRequest; a string
    input.note is handed to the model as a user turn when the run continues.
  • Objective.strikes_used and abort_reason, AARRootCause.code,
    AARObjectiveOutcome.abort_reason, MissionStartResponse.run_started.
  • PUT /agents/{agentId}/memory/core/{label} documents 409, and the public
    session upload, respond and share routes document 410 for an agent made
    private after the session opened.

Fixed — the tooling that made the last cut cost two attempts

  • scripts/update-spec.sh gains --allow-shrink "<reason>". Its guard
    correctly refuses a document smaller than the vendored copy, but its advice
    on refusal was to pass a different url, which cannot work: the guard weighs
    whatever is fetched, so the only url that satisfies it is a document stale
    enough to still carry the withdrawn surface. The override requires a reason
    and NAMES what leaves — paths, operations and schemas, each listed.
  • scripts/set-version.sh now moves three things it silently left behind:
    generator/package-lock.json, the crate's own version in
    packages/rust/Cargo.lock, and the generator goldens, which bake
    SDK_VERSION in four languages. That last one is why the first v0.6.0 tag
    published nothing — 40 of 100 generator tests red at the tag, every language
    job dead before its publish step. Measured again here: 48 of 114 red
    immediately after the bump, before the refresh.
  • scripts/check-prose-schema.ts reports 0 divergences over all 736
    operations.

Known

  • The SwiftPM mirror is stuck at 0.5.13 (2026-08-21) because
    SWIFT_MIRROR_TOKEN expired; 0.5.15, 0.5.21, 0.5.24 and 0.6.0 never reached
    it. Swift consumers do not get this release until that token is reissued —
    this is not something the tag can fix.
  • examples/react-landing still asks for uarp-sdk: ^0.6.0. It cannot ask
    for ^0.7.0 before 0.7.0 exists on npm, so that bump follows the publish
    rather than preceding it.

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 14 Sep 14:20
e9ce403

spec/openapi.json is https://api.snaga.ai/api/v1/openapi.json normalised
by python3 -m json.tool, which is what scripts/update-spec.sh writes.
Earlier entries quoted a sha256 of the served bytes; this one does not,
because the vendored file is no longer those bytes. The identity that means
something is the canonical digest — sorted keys, no whitespace —
d281e0b33c55a670, which is exactly what check-spec-freshness.sh compares
and what the release refuses to proceed without. Build d19c367e
(uarp #456–#477). Against 0.5.24 (the last release published to any
registry): 709 → 723 operations (+14, none removed), 142 with a changed
shape; 271 → 352 schemas (+81, one member removed — see below), 31 with a
changed shape.

This tag is cut a second time. v0.6.0 was first tagged on 2026-09-12 from
a tree whose generated TypeScript did not compile (agents.ts had a string
cursor reaching a number parameter) and whose generator goldens still said
0.5.24; every job of the release run failed before its publish step, so
npm, crates.io, Maven Central and the SwiftPM mirror never saw it — 0.5.24
remained the latest everywhere. Nothing consumed the number, so the number
is reused rather than burned. The tag now points at a tree that compiles.

Removed — one field, and it is a breaking change

  • Invite.secret is gone from the schema (uarp #471). The accept token is
    not something an administrator reading the invite list is given; only the
    invitee's own link carries it. Code that read invite.secret compiled
    against 0.5.24 and does not compile here — that is the whole of the
    breakage in this release, and the reason to read the note before bumping.

Added — the Drawings surface (uarp #472, stage 1)

  • Nine paths and twelve operations: GET|POST /sessions/{sessionId}/drawings,
    GET|DELETE /drawings/{drawingId}, GET|POST /drawings/{drawingId}/ops,
    GET /drawings/{drawingId}/render,
    GET /drawings/{drawingId}/tiles/{layerId}/{tx}/{ty},
    POST /drawings/{drawingId}/masks, GET /drawings/{drawingId}/masks/{maskId}
    and its /content. listSessionDrawings pages on the list convention.
  • Eight schemas: Drawing, DrawingLayer, DrawingOp, DrawingBrush,
    DrawingMask, DrawingStrokePoint, DrawingSelectionShape,
    DrawingJournalEntry.

Added — deletions the API grew

  • DELETE /sessions/{sessionId}/branches/{branchId} (uarp #472).
  • DELETE /runs/{runId}/feedback and
    DELETE /sessions/{sessionId}/runs/{runId}/feedback (uarp #474, #477):
    a reaction can be taken back, and the feedback reason now comes back on
    the read rather than only on the write.

Changed — the versions cursor is an opaque string

  • GET /agents/{agentId}/versions: cursor is a string, in the request and
    in the answer, where the first draft of this paging made it the version
    number (uarp #471). Treat it as opaque and pass it back unchanged. No
    published release ever carried the integer form — it existed only in the
    0.6.0 tag that failed to publish — so for anyone upgrading from 0.5.24
    this parameter is simply new.

Changed — typed responses (121 operations, 73 new schemas)

  • Operations that decoded to a bare object now have a model: the whole
    admin/config family (Auth, Guardrails, FeatureFlags, RateLimits,
    Retention, SSE, WorkerPool, ToolSecurity, LlmAdapters, Logging,
    CodeInterpreter, RunCommand, Stripe, Smtp…), admin audit, marketplace
    reviews and subscriptions, company objectives and activity, workspace
    trash, activity-stats, public states and agents, team-run messages.

Changed — types

  • Error: title is an enum of the twenty HTTP reason phrases (422 is
    "Validation Error"), detail is required, code is the machine code.
  • ConversationEntry.message_id is required — the derived id of every
    transcript entry ({run_id}, {run_id}-reply[-N], {run_id}-user-N,
    {run_id}-tool-N, {run_id}-system-N), the canonical key for reactions,
    bookmarks and annotations; content is a string on v1. The session detail
    (Session.conversation_history) carries message_id too.
  • ConstitutionRule.advisory (true when no code path can raise the rule;
    on GET /governance/constitution and /governance/obligations/{agentId})
    and obligated_action.
  • TenantOverview: nine members are optional — usage, cost, system,
    runs.cost_24h_usd, fleet.top_by_cost, fleet.by_execution_mode,
    fleet.bridge, fleet.head_agent_id, schedules.at_risk. v1 still
    serves all nine; readers must tolerate absence.
  • Additive fields on Agent, AgentUpdate, RunMetrics, Session,
    Company, A2ATask, PublicSessionView, PublicAgentCard
    (tenant_slug, tenant_name), PublicState, RunCheckpoint,
    McpServer*; RunOutput.search_sources.
  • AgentVersion.changelog names the eleven values the server writes.

Added — parameters and fields

  • GET /agents/{agentId}/versions: limit, cursor, fields=summary;
    with limit the answer carries cursor and has_more (the document's
    list convention). Without limit the answer is unchanged.
  • GET /governance/ledger: category, action (they filter now).
  • POST /admin/config/stripe/test success: active_key_matches (required)
    and warning — whether the billing manager serving checkout holds the
    key the panel stores; livemode is derived from the key prefix.
  • PUT /workspaces/{workspaceId}/files: If-Match / If-None-Match;
    the 412 is Error plus an optional current_etag (absent when the file
    does not exist).

Documented — what the wire always did

  • PUT /admin/config/stripe body: omit = unchanged, empty string = clear,
    a redacted read-back is ignored.
  • Billing return_url / success_url / cancel_url accept the app scheme
    (snaga://) beside same-origin URLs.
  • The public chat's 401 and 410 carry the bare fact
    "This conversation has expired."
  • message_id on feedback, bookmarks and annotations is stored as sent; a
    non-canonical one is counted, never refused.
  • Workspace move/copy/overwrite and what they do to file_id.

v0.5.24

Choose a tag to compare

@github-actions github-actions released this 10 Sep 22:32

The copy follows the served document byte for byte: spec/openapi.json is
curl https://api.snaga.ai/api/v1/openapi.json verbatim, sha256
ba6417e34fcafb5a…, build 5011669e (uarp #453, #454), info.version
0.4.0. 709 operations, 271 schemas. This is the first release since 0.5.21;
0.5.22 and 0.5.23 were cut as commits and never tagged — everything listed
under them ships here, including the generator fix below. From now on one
SDK release follows one deployed batch.

Fixed — all five clients

  • The generator regression of 0.5.21 (see 0.5.23): fifteen 204 operations
    were typed by the in-flight 202 body. Fixed here for every registry.

Changed — source-breaking, though nothing that works today breaks

  • Nineteen path parameters are named after their resource: {id} became
    {companyId}, {knowledgeBaseId}, {integrationId}, {proposalId},
    {ambassadorId}, {requestId}, {caseId}, {goalId}. Method parameter
    names follow; the URLs are unchanged.
  • 56 responses that decoded to a bare object now decode into models:
    RunOutput (with search_sources — the URLs a real web_search returned,
    at most 8, present only when non-empty — and output_truncated),
    UsageQuota, EvalDataset, EvalRun, AgentScorer, PublicAgentCard,
    PublicSessionView, JsonRpcResponse (MCP and A2A envelopes),
    SearchResult, ReplayResult, CreatedTask, MarketplaceSubscription,
    DeadlockReport, RootAttestation, DataSubjectAccessReport,
    DataSubjectErasureResult, SessionAnnotation, A2AAgentCard,
    OpenAiChatCompletion, TenantUser on getUser, Tenant on
    PUT/PATCH /tenants/me, and the team/squad graph node and edge records.
  • GET /agents/{agentId}/memory/{entryId} returns a MemoryEntry. Until
    build 5011669e the server answered the agent's list on that path; no
    client had called it.
  • Response codes corrected at the source, so a call that used to land in
    "unexpected status" now decodes: DELETE /agents/{agentId}/integrations/{integrationId}
    204; DELETE /mcp/servers/{serverId} 200 with { ok, cascade };
    POST /runs/{runId}/replay 200; ambassador bootstrap and requests 201;
    billing without Stripe 501 (was documented as 502).
  • Scope analytics:read replaces read:analytics in the catalogue (the
    server accepts both); billing:write enters the catalogue; api_keys:read
    / api_keys:write are declared on the key operations.
  • Agent.metadata.ui is typed (AgentMetadataUi: avatar, drop_genome,
    drop_genome_source); a partial PATCH of metadata.ui.avatar keeps the
    avatar fields it does not name.

v0.5.21

Choose a tag to compare

@github-actions github-actions released this 10 Sep 18:48

The copy follows the served document byte for byte: spec/openapi.json is
curl https://api.snaga.ai/api/v1/openapi.json verbatim, sha256
ba5a7338214ddbe6cb76b9cac203ddf726f7f52692ee637bf8c1cd64125112ca, build eaa42cb2, info.version 0.4.0. 709 operations, 249 schemas.

Added

  • execution_mode (async | bridge) on GET /analytics/overview →
    runs.recent[], passed through from the run record — a bridge report with
    no transcript is now distinguishable from "nothing ran".

Changed

  • uarp #450 (CTR-07 tranche 2a): 48 responses on governance, teams/squads,
    a2a, public, playground, files, programs, users, webhooks, mfa, guardrails,
    marketplace, workspaces, notifications and tenant keys now have the shape
    they serve — new ReadinessReport, FileRecord, TeamRunDetail,
    PlaygroundAgentState, PlaygroundTemplate, MfaEnrolment.
  • Status codes the wire actually answers: POST /governance/ambassador/ ambassadors, POST /governance/goals, POST /governance/improvement/ {agentId} are 201 (not 200); POST /playground/agents/{agentId}/run
    is 202; DELETE /marketplace/listings/{listingId} is 204.

v0.5.15

Choose a tag to compare

@github-actions github-actions released this 29 Aug 00:06

The first release cut from a document that matches what the platform serves.
559 operations to 641, 163 schemas to 219 — the 0.5.14 tag was cut before the
document refresh landed, so everything below has been on main and in no
installable package.

Fixed — all five clients

  • quota_overrides stopped being a copy of the plan's quotas. It was
    declared $ref: TenantQuotas, which requires sixteen fields, and overrides
    are partial by definition — a tenant that overrides six sends six. So every
    strict client failed to decode GET /tenants/me outright. Measured against
    the released 0.5.14 model and a live production body:

    before   DECODE FAILED: missing field `max_workers_per_team`
    after    DECODED OK
    

    Rust, Swift and Kotlin all generated the sixteen as non-optional, so this
    was a live break on a published package rather than a latent one. The fix is
    a dedicated all-optional TenantQuotaOverrides; TenantQuotas is correct as
    it stands for the effective set and is unchanged.

  • cursor is nullable on /files and /public/tenants. Both were
    declared a plain string and both serve null on the last page — the same
    strict-decoder hazard, on pagination, which every consumer touches.

  • Workspace file upload sent no body at all. uploadWorkspaceFile had no
    body parameter, so the route's arrayBuffer() received zero bytes and wrote
    an EMPTY file under a 200. downloadWorkspaceFile ran binary responses
    through a JSON parser, corrupting any png or pdf into something that still
    opens.

  • Team runs. startTeamRun takes input and metadata instead of four
    top-level fields the handler was discarding in silence; listTeamRuns gained
    the limit and cursor the handler has always read, so a client is no longer
    stuck on the first fifty; streamTeamRunEvents takes teamRunId like every
    sibling under that prefix.

Added

  • 82 operations and 56 schemas the vendored document had been missing.
  • getAgentActivityStats declares the fourteen fields it serves, not three.
    cancelledRuns, guardrailBlockedRuns, errorRatePercent, avgStepsPerRun,
    avgDurationMs, avgInputTokens, avgOutputTokens, avgThinkingTokens,
    toolBreakdown, topErrorMessages and runsByDay were being served and were
    invisible in all five clients.

Changed — source-breaking, though nothing that works today breaks

  • uploadWorkspaceFile gains a required body parameter and
    downloadWorkspaceFile returns binary (Blob / bytes::Bytes / Data /
    ByteArray / UARP.Types.Text). Calling either today writes an empty file
    or corrupts a download, so there is no working call site to regress.
  • Tenant.quota_overrides changes type from TenantQuotas to
    TenantQuotaOverrides. Anyone naming that type in a signature must rename it.
  • getGovernanceLedger's from and to change from string to integer. They
    are ledger SEQUENCE NUMBERS, not timestamps, and were declared as strings —
    so callers reasonably sent an ISO timestamp, the server ran it through
    parseInt, and 2026-08-28T18:00:00Z silently became sequence 2026. The
    request then answered 200 with an empty page over a ledger holding thousands
    of entries. Non-integer values are now rejected with 400 instead. The
    parameter also gained count, which is what most callers actually want.

Internal

  • The freshness gate hashes the whole document canonically rather than its
    schema and path NAMES. The name digest could not see a request body being
    declared, a media type added or a false required dropped, and it reported
    current over a document three fixes behind.
  • The generator refuses two operations in one group that would emit the same
    method name — five compiler errors' worth of information, available before
    any of them run. It checks the emitted name, not the operationId: the live
    document has a duplicate id across two groups and it is harmless.
  • A route-coverage gate compares the BUILT client against the operations the
    platform serves, because the Ada manifest is generated from the TypeScript
    declarations rather than from openapi — so a gate on the spec alone goes
    green while the last link stays stale.
  • CI enforces the commit-trailer rule the repository documents. There was no
    commit-msg hook and never had been.
  • scripts/set-version.sh writes the version into package-lock.json, which
    it never did, so every release left the lockfile a version behind (#42).
  • The contract gate no longer dies on macOS's /usr/bin/java stub, which
    exists and fails; it now runs four SDKs where it ran none.