Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
2.1.0 - 2026-10-15
Added
GatewayClient.execute()gains an optionalattachmentsparameter: a list of the newAttachmentobjects (type, plus exactly one ofurlor rawdata(bytes,bytearrayormemoryview), and an optional plain-filenamename) sent asattachmentsin thePOST /api/v1/executebody.datais base64-encoded by the SDK. Invalid attachments raiseNovaOSErrorbefore any request is made. Omitted or empty, the request body is unchanged.AgentCatalogClientandCatalogAgent(novaos_client_sdk.catalog) — find published agents through the marketplace's public catalog (GET /api/agents) by the skills their Agent Card declares, withskill_match="all"or"any". This is the external counterpart to Agent-DNS, which is cluster-internal and carries no capabilities. Results follow the catalog's cursor, stop if the cursor does not advance, and map HTTP failures onto the SDK's exception hierarchy.CatalogAgent.gateway_url()and.card_url()give the gateway addresses to call an agent and read its card.AgentExecutionContext.custom_headers— the caller's custom request headers, filtered and lower-cased. Everyctx.tools.*call now forwards them to the MCP Gateway; headers the SDK sets itself (Authorization,X-Tenant-ID, ...) always take precedence. Newnovaos_client_sdk.header_safetymodule (is_safe_custom_header_name,filter_custom_headers) applies the platform's header-name rules.MemoryClient.update()andctx.tools.memory.update()— edit one of the agent's ownagent-scope memories in place, using theversionread fromquery. Returns the newUpdateResult.MemoryClient.hint_status()andctx.tools.memory.hint_status()read the outcome of a submitted hint (newHintStatus);ctx.tools.memory.proposal_status()reads the outcome of a proposed write.- Optional
memory_type(semantic,episodic,procedural) on memorywrite,queryandassemble_context, and optionalconversation_idonqueryandassemble_context, to filter recall by type or conversation. Omitted, requests are unchanged.MemoryTypeis exported andMemoryMetadatagainsmemory_type. MemoryClient.write()gainsttl_secondsandclassification;MemoryClient.query()gainstoken_budgetandlatency_budget_ms.ctx.tools.memorynow also exposesforget,hintandassemble, andwrite/queryaccept the same extra fields.
Security
AgentDiscoveryServicegainsguard_connect_ips(andallow_private_ips) so the Agent Card fetch itself can refuse abase_urlthat resolves to a private, loopback, link-local or reserved address, and connect to the address it checked. It is off by default so a localhttp://localhost:8000agent keeps working; turn it on whenbase_urlcomes from a user or other untrusted source, since the request carries theauthheaders.validate_host_allowed(theNOVAOS_ALLOWED_HOSTScheck used for the gateway and JWKS URLs) now compares hostnames in their IDNA wire form, so a Unicode host matches its punycode entry, and a host that cannot be encoded is rejected.- The JSON-RPC, HTTP+JSON and card-fetch HTTP clients now set
follow_redirects=Falseexplicitly instead of relying on the httpx default. - The gRPC transport now connects to the IDNA 2008 (punycode) form of a Unicode agent host, the same form
validate_agent_urlchecks. gRPC's resolver does not encode Unicode hosts itself, so an internationalised agent host previously failed to resolve instead of connecting to the validated name.grpc_channel_factoryreceives the same normalised target. A host that cannot be IDNA-encoded is rejected when the transport is created. - Clients built by
AgentDNSRecord.to_client(),AgentDNSClient.get_agent_client(),discover_and_connect()andAgentDiscoveryService.to_client()now re-check the address at connect time and connect to the address they checked, closing the gap in which a short-TTL DNS answer could be public forvalidate_agent_urland private for the real connection (DNS rebinding). The host name still drives TLS verification and theHostheader. This applies to the JSON-RPC and HTTP+JSON transports, and to gRPC unlessgrpc_channel_factorybuilds the channel, which the SDK cannot pin. AClientbuilt directly from a URL is unchanged, solocalhoststill works; it can opt in with the newguard_connect_ips=True. Requests routed through anHTTP(S)_PROXYenvironment variable connect to the proxy instead. validate_agent_url(and soAgentDNSRecord.to_clientandAgentDiscoveryService.to_client) now checks the hostname in the same IDNA 2008 form the HTTP transport connects to. Previously a Unicode host such asfaß.examplewas resolved asfass.examplewhile httpx dialledxn--fa-hia.example, so the private-IP guard could validate one destination and connect to another. Allowlist suffixes are normalised the same way, and a hostname that cannot be IDNA-encoded is rejected.idna>=3.0is now a declared dependency.
Changed
- Breaking:
AgentDiscoveryService.to_client()now requiresallowed_host_suffixesnaming the domains the Agent Card's URL may point at. A card is remote input, and the client'sheaders/authare sent to whatever host it names, so a call without an allowlist now raisesNovaOSErrorbefore any request is made. There is no opt-out. The private, loopback and link-local IP block still applies on top of the allowlist. To migrate, pass your deployment's or partner's domains:await discovery.to_client(allowed_host_suffixes=[".partner.example"]).
Fixed
ObservabilityClient.get_run_eventsno longer fails with HTTP 403 when a user reads their own run. A newrouteargument ("auto"by default,"user","service") selects the endpoint;"auto"falls back to the user route on a 403.RunEventgains optionalsequence,trace_idandspan_id(Nonewhen the platform does not return them).- A skill that raises an uncaught exception now reports
<type>: <message>as its failure text instead ofstr(exception)alone, so the exception type reaches the platform and an exception with no message (TimeoutError()) no longer yields an empty failure. The exception is also logged with its traceback, except whenmask_output_piiis on, where the traceback is omitted because it carries the message. NovaOSServerErrormessages raised for non-2xx responses from the Memory Service, MCP Gateway, Gateway observability and agent-ops clients now append a short excerpt of the response body (RFC 9457errors[].detail, JSON-RPCerror.message, adetailfield, or the first ~300 characters), so the reason a call was rejected is visible. The... returned HTTP <code>prefix is unchanged, the httpx documentation link is dropped, and the parsed or raw body is available asdetails["response_body"].- A duplicate knowledge upload (identical content already indexed) is reported as a
completedjob whose status carriesduplicate_of, the original document's id.KnowledgeTool.poll_until_completereturns it like any completed job instead of waiting for aduplicatestatus, andIngestJobStatus(fromget_ingest_statusandpoll_batch_until_complete) gainsduplicate_ofandis_duplicate. KnowledgeTool.poll_until_completeandpoll_batch_until_completenow stop on acancelledingestion job (its collection was deleted before it finished) instead of polling until the timeout.
Documentation
AgentDiscoveryServicedocuments passingallowed_host_suffixeswhen connecting to third-party partner agents, and why.- The memory guide now covers typed memories, conversation recall, which writes need human approval, and the
nullscore for run- and task-scope results. The observability note ontrace_id/span_idavailability is corrected.
2.0.0 - 2026-09-30
The SDK adopts protocol-per-major versioning: 1.x lines track ACP, 2.x lines track A2A. The A2A wire protocol itself already replaced ACP outright in an earlier 0.x release — AgentBuilder/Client rewritten onto A2A v1.0.0 JSON-RPC, handle_acp_request and every ACP-shaped type removed, with no compatibility shim — and this line has shipped nothing but A2A since. 2.0.0 draws the major-version boundary under that work and folds in everything accumulated since: the migrate CLI's three pipeline stages (analyze/transform/verify) for porting an ACP agent onto the A2A shell, a malformed-request-body fix, SSE reconnect support, an Agent Card emitter CLI, SendMessage content negotiation/history-length/reference-task handling, and the SkillResult status-vocabulary and metadata-content-type fixes below. It also carries forward, as its ACP-line baseline, fixes shipped on the 1.x line after this branch diverged: a KnowledgeTool routing-prefix rename, a fail-closed behavior change for a tool with no backend, and configurable per-tool MCP call timeouts.
Version line: 2.0.0 is a breaking change from the 1.x ACP line (ACP surface removed, A2A JSON-RPC surface added). See the versioning and support page in the documentation.
Removed
MemoryTool(ctx.tools.memory.*) is deleted. It routed through the MCP Gateway, which never had a matching backend ("tool not found"), and there were zero production consumers.MemoryClient(direct HTTP to the memory service) is, and remains, the canonical path.
Added
HITLTool.requires_write_hitl(tool_fqn)— a synchronous, no-network classification of whether a specific MCP tool call is likely to need write approval, mirroring the TypeScript agent SDK'srequiresWriteHitland the platform's own Gate A classifier. Lets a skill decide up front whether to build a pause before calling a write-class tool, instead of guessing or hand-rolling a fail-then-recover loop.AgentExecutionContext.mcp_write_hitl_active— a coarse, run-level mint-time hint (does this run's token carry any write-capablemcp:*scope?), read from the platform'sexecution_hints. AlwaysFalsefor a service-account run, since an SA has no interactive HITL resume path.ConnectorTool.call()gains atierargument ("standard"/"licensed"/"ext", default"standard") so licensed and third-party connectors are reachable through the SDK's governed surface. Licensed/ext-tier connectors are registered one segment longer than standard ones; previously, calling one throughConnectorTool.call()always built the standard-tier name and failed with tool-not-found. Backward-compatible: every existing caller, which never passestier, is unaffected.- 2.x documentation set: an A2A primer, a migration guide from 1.x, and a
migrateCLI reference; guides updated to the A2A client and agent surfaces. python -m novaos_client_sdk.migrate verifyruns a migrated agent's own pre-migration fixtures against it and checks its Agent Card. Whereanalyzeonly reads an agent's source andtransformonly rewrites it,verifyis the pipeline stage that actually calls the migrated agent: it converts each fixture into the A2ASendMessagerequest an equivalent caller would send today, fires it at a live agent process over this SDK's ownClient, and asserts the resultingTaskreached the expected state.--streamalso probes a skill withSendStreamingMessage, and an Agent Card check compares the agent's declared skills against its manifest in both directions.python -m novaos_client_sdk.migrate transformmechanically rewrites an ACP agent onto the A2A shell, using a lossless concrete-syntax-tree rewriter (notast) so it can edit a file in place while preserving every comment and formatting choice elsewhere in it. It refuses rather than guesses when a migration plan is not eligible or contains an action with no handler, and a second run against an already-migrated file is a no-op (idempotent).python -m novaos_client_sdk.migrate analyzeclassifies an ACP agent ahead of its A2A migration. It determines which of three request-handling shapes an agent's server implements, surfaces every ACP-shaped construct in its source, and flags anything that would crash or silently misbehave after a mechanical port — all from pure source analysis; the target agent is never imported or executed.--fleetruns the same analysis over a directory of agents at once.AgentBuilder.handle_a2a_bytes()/handle_a2a_bytes_with_headers()andasgi_endpoint()— a malformed request body now answers JSON-RPC-32700, not an HTTP 500. Previously every entry point took an already-decoded body, so a pod route had to decode it itself before reaching the SDK, and invalid JSON there surfaced as an unhandled 500.asgi_endpoint()returns a ready-made ASGI callable that dispatchesSendStreamingMessage/SubscribeToTaskto an SSE response and everything else to JSON, so a template's entire A2A route collapses to one line.GatewayClient.events()can now resume a dropped SSE connection, andevents_with_reconnect()does it automatically.StreamEventgains anidfield carrying the SSE event id;events()gains alast_event_idargument.events_with_reconnect()tracks the last id seen and reconnects on a retryable disconnect with exponential backoff; a non-retryable error (e.g. an authentication failure) still propagates immediately.python -m novaos_client_sdk.agent_cardemits a live agent's Agent Card as JSON, so tooling outside the Python process (a CLI publish step, an activation pipeline) can read it without hand-rolling the shape. Exits non-zero with a one-line reason on stderr for a bad module, a missing attribute, or an attribute that isn't anAgentBuilder.AgentBuildernow negotiates content types server-side, honoursSendMessage's nestedhistoryLength, and resolvesreferenceTaskIds. ASendMessage/SendStreamingMessagewhose accepted output modes share nothing with the resolved skill's supported modes now answers a request-levelContentTypeNotSupportedError(-32005) before any task is created or persisted.configuration.historyLengthnow boundsTask.historyonSendMessage/SendStreamingMessagethemselves, not justGetTask/ListTasks. Behaviour change: aSendMessage/SendStreamingMessagenaming a still-open task's id now accumulates onto that task's stored history instead of resetting it — fixing a gap that silently discarded the turn a pausedINPUT_REQUIRED/AUTH_REQUIREDtask exists to let a caller answer.- A dedicated "Agent memory" guide (
docs/public/guides/memory.md) covering both doors to Memory Service —ctx.memory.client()(direct) andctx.tools.memory(through the MCP Gateway) — with a comparison of when to use which, the id table, write idempotency, pre-fetched context, degradation-state fields, and consistency guarantees. ctx.tools.memoryis back:ToolBindings.memory(MemoryTool) withwrite()andquery(), calling the realnovaos.memory.write/novaos.memory.queryMCP Gateway tools. This is a new binding, not a revival of the one2.0.0deleted (which targeted Gateway tools with no backend). It is governed like every otherctx.toolsbinding — Gateway policy, audit, metering andtool.call/tool.resultstream events — and needsmcp:tools:memory(ormcp:tools:*) on the execution token in addition tomemory:*scopes.- Memory pre-fetch before a skill runs. When the platform attaches a
memory_budgetto a request (it does for agents that declarememoryinpermissions.tools),AgentBuildercallsassemble_context()once per turn before the skill handler starts and exposes the result asctx.memory.context;ctx.memory_budgetexposes the budget itself. A pre-fetch failure is logged and never fails the turn. Each successful pre-fetch emits amemory.assembledplatform event — as aWORKINGstatus frame on streaming requests, and folded into the terminal message's platform metadata part on non-streaming ones.ctx.memoryis now cached per execution so the pre-fetched value is visible to the skill. MemoryClient.hint()defaultsconversation_idfromctx.context_id,assemble_context()defaultsrequest_id/trace_idfromctx.run_id/ctx.trace_id(it takes noconversation_id— Memory Service scopes live messages from the token), andwrite()defaults its idempotency key fromctx.idempotency_keyplus a per-execution ordinal, when actxis passed. The ordinal is tracked onctxitself (not on theMemoryClientinstance, which is typically constructed fresh per call), so repeated writes across one execution get distinct, stable keys instead of a fresh random UUID every time. Backward-compatible:ctxis optional and explicit arguments still override the derived defaults.
Changed
- Removed internal ticket, decision-record, repository and customer references from shipped docstrings, comments and CLI help; no behaviour change.
Fixed
- BREAKING:
SkillResult.statusoutside the known vocabulary no longer silently maps toTASK_STATE_COMPLETED. Four legacy spellings (QUEUED,RUNNING,IN_PROGRESS,ERROR) are now mapped to the A2A state they always meant; anything still outside the vocabulary now fails the task asTASK_STATE_FAILED, with the bad value named in both an error-level log and the task's own status message. Behaviour change: an agent relying on the old silent-COMPLETEDfallback for an out-of-vocabulary status now gets a failed task instead. A skill returningQUEUED/RUNNING/IN_PROGRESSfor a non-idempotent effect should deduplicate onctx.idempotency_key, since these states are not inINTERRUPT_TASK_STATESand a caller cannot distinguish a deliberate open task from a dropped connection. - A skill's own
SkillResult.metadatano longer collides with platform metadata on the wire, so it reaches the run-artifacts API. It was previously folded into aDataParttagged with the same content-type the platform uses for its own internal metadata, so the platform's event pipeline silently excluded it from caller-visible run data. A newNOVAOS_SKILL_METADATA_CONTENT_TYPEmarks a skill's own folded-in result distinctly. Wire-shape change: an agent'sSkillResult.metadatais now carried in aDataParttagged a different content-type than before — a minor-version-bump-worthy change. AgentExecutionContext.trace_idnow carries the bare W3C trace id, not the wholetraceparentheader. It previously stored the full header string, so anything correlating on the trace id alone — logs, spans, downstream lookups — never matched. The raw header is available as a newtraceparentfield for propagation, and tool-call spans now also carrynovaos.run_id. Behaviour change: code readingtrace_idand expecting a00-<trace>-<span>-01string should readtraceparentinstead.MemoryClient(max_retries=0)now issues one attempt instead of none. Previously the retry loop ranged directly overmax_retries, so0skipped the loop body entirely — the only place that calls the memory service — and the method raised aTypeErrorfrom an unset error variable instead of ever reaching the service or surfacing its real response.max_retries=0now means "no retries", matching its documented intent, not "no attempts": the one attempt still runs, and its real error (or result) is what a caller sees on failure.