Releases: ss7172/graph-agents-cli
Release list
graph-agents-cli 0.3.1
0.3.1 is the first release published on PyPI.
Its commands and templates are 0.3.0's (only the version, the documentation, the skills'
install lines and the release workflow change), so a project created by 0.3.0 needs no
migration.
Changed
- Published on PyPI. Install with
uv tool install graph-agents-cli(orpipx install graph-agents-cli); the release taggit+https://github.com/ss7172/graph-agents-cli@v0.3.1
installs the same release. 0.3.0 and earlier stay on their git tags only. - The README and the documentation point to PyPI: the README's install section (with a
PyPI badge), the site's Installation page, announcement bar, home page and tutorials, and
the workflow and scaffold skills' install lines. "Not on PyPI yet" is gone.setup,
update, thescaffold upgradebaseline and generated projects'GRAPH_AGENTS_CLI_SPEC
still install from the release tag, since earlier releases exist only as tags;
GRAPH_AGENTS_CLI_INSTALL_SPEC='graph-agents-cli=={version}'switches them to PyPI.
Fixed
- The release workflow's GitHub Release job is idempotent. A second run for the same
tag (GitHub started two for thev0.3.0push, and the second failed with "a release with
the same tag name already exists") now finds the release, uploads only the assets it lacks
and never replaces one; the PyPI job skips files already on PyPI.
graph-agents-cli 0.3.0
0.3.0 lets agents call other agents for the user they serve, over A2A.
Each agent knows the user and the agent in between, a person's approvals stay with that
person, peer add declares the agents one asks, and graph-agents-cli system checks, wires
and deploys several projects as one. It also keeps A2A tasks in Postgres so that replicas
share them, adds structured final answers (an agent that answers in a JSON shape the
project declares), reasoning effort and the Responses API for OpenAI-API models, a
documentation site, skill rules found with the SkillOpt experiment and the benchmark that
measured them, and fixes. What an upgrade from 0.2.0 changes, and the order to do it in, is
in Upgrading projects; the guide to
the new features is Agents calling agents. Parked
medium- and low-priority issues are listed in KNOWN_ISSUES.md.
Breaking changes and migration
Each change below can need an edit in an existing project; the upgrading guide's
0.2 to 0.3 section lists every other
change in behaviour.
jwtreads the RFC 8693actclaim. A token carrying it is an agent's for the user,
refused (403) untilAUTH_ALLOWED_ACTORSlists the agent; setAUTH_JWT_ACTOR_CLAIM=
(empty) to read every token as the user's own, as 0.2 did. Acustompolicy that returns an
invalid id (empty, over 256 characters, or with control characters) now fails the request
with 500 and logs the bug. A custom policy through which other agents forward users'
credentials must setPrincipal.actor(policies/is not upgraded): see the upgrading
guide.- An API whose
auththe project's auth policy can never serve stops the app outside
dev.lintandapi addcheck each API'sauthagainst the project's auth policy:
auth: exchangeorauth: forwardundershared-bearer, andauth: forwardunderjwt
withoutforward_audience, are errors (exit 3): such an API never had a credential to send,
so every call to it failed with "the caller has no credential". OutsideAPP_ENV=devthe
running app now refuses to start with such an API too, as it does forauth: exchange(the
owner's decision of 2026-09-28; alsoauth: forwardunder thelanggraph-serverruntime);
under dev it logs why and starts. Migration: a 0.2 project with such aforwardAPI
stops starting outside dev after the upgrade until the API getsforward_audience, moves to
auth: exchangeor is removed (scaffold upgradenever rewritesapi-policy.yaml, and
lintnames the API).lintandapi addalso note aforwardAPI with
forward_audienceunderjwt("prefer auth: exchange").
Added
- Agents calling agents: the caller's identity. A request another agent presents for a
user is now told apart from the user's own.Principal.idstays the user (the subject);
the newPrincipal.actornames the agent presenting the request (its id, the chain of
agents before it, and its client).jwtreads the RFC 8693actclaim
(AUTH_JWT_ACTOR_CLAIM, nestedactfor earlier agents), the client fromazp/client_id
(AUTH_JWT_CLIENT_CLAIM), and withAUTH_JWT_DIRECT_CLIENTStreats a token with noact
from any other client as that client's; acustompolicy setsactoritself
(actor_from_claimsandkeep_subject_tokenare exported for it). Every policy then goes
through one rule set (finalize_principal): valid ids, at mostAUTH_MAX_DELEGATION_DEPTH
agents (default 3; else 401), only the agentsAUTH_ALLOWED_ACTORSlists (default none;
else 403), and only the rolesAUTH_DELEGATED_ROLESlends. Threads, A2A tasks and approvals
are owned by the subject and the actor: an agent reaches only what it started for that
user, while the user owns everything done for them: with their own token they read,
continue and delete the threads their agents started, decide their approvals, and read,
list and cancel the A2A tasks those agents started for them (the owner's decision of
2026-09-28; continuing or subscribing to such a task stays with its agent). A delegated
principal's roles never read across, administer or decide as arole:approver, and it
never decides an approval (403approval_direct_only: the person decides with their own
credentials). The actor reaches
tools inattributes["@actor"], is recorded with each approval (requester_actor), and is
logged (actor), kept in run records and named in trace metadata.auth dev-tokenmints
such tokens locally (--act, repeatable, and--azp). The database gains
threads.actor,runs.actorandapprovals.requester_actorat startup (ADD COLUMN IF NOT EXISTS); existing rows are direct, and every 0.2 principal is direct, so 0.2 behaviour is
unchanged for them. KI-042 is narrowed (jwtmapsactandazp; one issuer and no
mapping from scopes to permissions remain). - Tools and the model know when another agent asks for the user.
current_caller()
returns the calling agent (Caller.actor,actor_chain,delegated); the new
require_direct_caller()refuses unless the user asks this agent directly;require_owner
still compares the user.require_user_mentionedfollowsA2A_DELEGATED_MENTIONS:origin
(the default) also needs the id in the user's own words the calling agent forwarded, and
refuses when none were forwarded (so, until the A2A client forwards them, a delegated
write the check guards is refused and the user names the record at this agent directly);
refusealways refuses;requestkeeps the 0.2 reading, andlintandapi showpoint it
out when.envor a values file sets it. In a delegated runUntrustedToolResultsfences
each human message the model reads as that agent's (<agent_request from="...">) and adds
one factual note after the system prompt saying an agent wrote the request, with the user's
own words when forwarded;A2A_CALLER_NOTE=offdrops the note. A bad value of either
setting stops startup. Thefaketest model reads the request inside that fence. - Approvals relayed across agents. An approval rule may let named agents deliver the
requester's decision from another agent:decide_with: relayedwithrelayers(actor
ids) inapi-policy.yaml, written byapi approval NAME --decide-with relayed --relayers concierge(a loosening, reviewed like new approvers;--decide-with directnarrows it
again). The default staysdirect: the person decides with their own credentials. A relayed
decision is accepted only from a listed agent, on a thread it started for that user, with
requesteran approver, naming the approval'sdigest(a SHA-256 of the call as the
approver saw it; missing or different: 409approval_digest_mismatch), and is recorded as
decided_via. How the approvers decide is bound to the approval when it is asked, as the
approvers are: a policy that starts or stops relaying, or changes the relayers, while a call
waits does not keep its approval. The approval object showsdecide_with,decided_via
anddigest; the HTTP decision body and the A2A decision part take an optionaldigest.
Rules that decide differently are different gates for the rule-conflict check,api show
andlintprintapproved by requester; relayed by concierge, andapi show --jsonadds
decide_withandrelayersto a relayed gate. The approvals table gainsdecide_with,
relayers,decided_viaanddisplay_digestat startup, and thelanggraph devapprovals
file moves to version 2 (a version-1 file is read, its approvals direct). - A relayed decision shows what it decides and what will happen. An approval of an A2A
message that approves another agent's approval carriesnested(that approval, as the
message sends it: its call, reason, expiry, digest, and the approval it relays in turn) and
effect(the call that will actually happen, the agent that makes it and the agentsvia
which), in/chat,GET /approvals, the thread's approvals and the A2A approval request. It
expires 5 s before the approval it decides at the latest, and the nested calls' and the
effect's query and body are dropped on decision with the call's own (unless
TRACE_CAPTURE=full).approvals listandrunprint the effect first ("orders (via
billing) will POST /orders/ORD-1002/cancel (cancelOrder), as reported by orders", its body,
and avialine per agent), terminal-safe like the rest of the approval. With
requester_actoranddecided_viaon every approval, this narrows KI-009 (approvers still
see the requester as a hash). - The A2A server speaks to agents calling for a user. An agent's card declares the
graph-agents-cli origin extension
(https://ss7172.github.io/graph-agents-cli/a2a/ext/origin/v1, optional): an agent calling
for a user may put the user's own words in the message metadata under that URI (origin:
text,truncated,hops), and for a delegated caller only they reach the run's private
credentials (@origin, whererequire_user_mentionedand the model's note read them),
capped atA2A_ORIGIN_MAX_CHARS(4000). They are never stored: every task is saved without
them. The run a decision resumes acts on the words of the request that paused it, whatever
words the decision carries (the person's "yes, go ahead" at the agent that relays it, or
none): the approval keeps them while it waits (never shown, and dropped once it is decided or
expired, whateverTRACE_CAPTUREsays; fastapi runtime only, as LangGraph Server passes no
credentials to tools), sorequire_user_mentionedholds again on the resumed run and an
agent relaying the approval one level further rebuilds the very call the person approved.
MorehopsthanAUTH_MAX_DELEGATION_DEPTHfails the task (delegation chain too deep). A
...
graph-agents-cli 0.2.0
graph-agents-cli is now a generic CLI for building, evaluating and deploying LangGraph agents
on self-hosted Kubernetes, for any project and any domain. Nothing in the CLI, the template,
the skills or a generated project is shaped around one consumer: projects choose an auth
policy and declare the outbound APIs their tools may call. This release also closes most of
the production-readiness findings of an independent assessment of 0.1.0 (runtime guardrails,
per-user authentication, deploy safety, supply chain, release engineering); the remaining
ones are listed under "Known limitations" and "Where it is behind" in the README.
Parked medium- and low-priority issues are listed in KNOWN_ISSUES.md.
Install from the release tag (the package is not on PyPI yet):
uv tool install git+https://github.com/ss7172/graph-agents-cli@v0.2.0Breaking changes and migration
--auth-policy product-sessionis nowcustom.create --auth-policy product-session
is refused with a hint. A manifest orAUTH_POLICYthat still saysproduct-sessionis
read ascustom, with a one-line deprecation warning. In the template,
app/policies/product_session.py(ProductSessionPolicy) becameapp/policies/custom.py
(CustomPolicy). SetAUTH_POLICY=customandauth_policy: custom.- The product API policy is now a multi-API outbound policy.
product-policy.yaml
becomesapi-policy.yaml,create --product-policybecomescreate --api-policy, the
manifest blockproduct_api:becomesapi_policy: {policy_file: api-policy.yaml}, the
cookiecutter variablehas_product_policybecomeshas_api_policy, and the tool
declarationPRODUCT_CALLSbecomesAPI_CALLSwith an"api"key per entry. The file
now declares any number of APIs underapis: {<name>: ...};allowed_methodsis
required;auth: forwarded-sessionis nowauth: forward.create,scaffold enhance,
scaffold upgradeandlintstop on a project that still uses the old format and print
the migration steps (exit 3);--product-policyis refused with a rename hint; a tool
module that declaresPRODUCT_CALLSis a lint error. - Outbound API calls fail closed. Without
api-policy.yaml, or for an API the file does
not declare,get_client()raisesApiPolicyErrorand nothing is sent. 0.1.0 sent every
call unrestricted (with a warning) when no policy file existed. - The default guidance file is
AGENTS.md(wasGEMINI.md). Existing projects keep the
file name their manifest records; pass--agent-guidance-filename GEMINI.mdtocreateto
keep the old default. CLI_VERSION_PINis nowGRAPH_AGENTS_CLI_SPECin.github/agent.env(the
cookiecutter variablecli_version_pinis nowcli_install_spec). The value is a full
install spec,git+https://github.com/ss7172/graph-agents-cli@v0.2.0by default, and the
workflows runuvx --from "$GRAPH_AGENTS_CLI_SPEC" graph-agents-cli .... The workflows
refuse anagent.envthat still setsCLI_VERSION_PIN, with a rename hint.- Installation moved to a pinned git reference. The package name
graph-agents-cliwas
never published on PyPI, souv tool install graph-agents-clinever worked;setup,
update, thescaffold upgradebaseline and the generated workflows install
git+https://github.com/ss7172/graph-agents-cli@v<version>instead, and the update check
reads GitHub releases.GRAPH_AGENTS_CLI_INSTALL_SPECoverrides the source (a mirror, a
wheel); write{version}where the version goes. deployandsecrets applyoutsidedevneed an explicit env file and kube context.
They read.env.<env>(or--env-file) and never fall back to.env(exit 3 without one).
The kube context must be recorded asenvironments.<env>.contextor passed with
--context; the kubeconfig's current context is used only after a confirmation prompt, or
--yeswhen there is no terminal (exit 1 otherwise). CI jobs that deploy need--yesor
--context(the generated workflows pass both).- A live
API_KEYis never replaced implicitly. Undershared-bearer,secrets apply
anddeploykeep the key in the cluster unless the env file sets a different one and
--rotate-api-keyis passed. A generated key is written to the env file (mode 0600)
instead of being printed. helm-pushreadsDEPLOY_KUBECONFIGfrom thestagingandproductionGitHub
environments (was the repository secretKUBECONFIG), so the production reviewers gate
it. Create the environment secrets and delete the repository secret.deploy --env staging|prodis refused outside CI inhelm-pushmode even with--image
(--force-directoverrides).- Chart defaults are stricter.
image.tagdefaults to""in every environment and the
chart refuses to render without a tag (neverlatestby default); the tag must be a
quoted string. The HTTPRoute and Ingress publish onlyroute.publicPaths(/chat,
/threads,/a2a/<agent>, plusroute.devPathsunderAPP_ENV=dev) instead of every
path;/health,/readyand/metricsstay inside the cluster. Outsidedevthe app
Secret is required (secretOptional: false): pods do not start without it. - Exit codes are consistent (0 ok, 1 refused or failed gate, 2 tool failure, 3
configuration error): an unexpected crash exits 2 (was 1), running outside a project
exits 3 (was 1),runexits 2 when the agent cannot be reached or goes silent (was 1),
secrets statusexits 1 only when a required key is missing (--strictfor every
allow-listed key), and a local server that cannot start exits 2 fromrunandeval.
scaffold upgradeexits 3 for a manifest without a releasedcli_versionor an
install-spec override without{version}, and 2 whenuvxis missing or cannot fetch and
run the prior release (all were 1); a version-lockedscaffold enhancewithoutuvx
exits 2 (was 1). - Chat API changes. The SSE
errorevent is{code, message, error_id, run_id}with
codeone ofrun_failed,timeout,recursion_limit,thread_busy,unavailable,
forbidden(was the exception class name); details go to the server log (anddetailonly
underAPP_ENV=dev)./chatmetadata outside the caps is refused with 422 (was silently
dropped). Underlanggraph-serverthread ids must be UUIDs. .github/agent.envis data, not shell. The workflows accept onlyIMAGE_REPOSITORY,
RELEASE_NAME,CHART_PATH,RUNTIME,CDandGRAPH_AGENTS_CLI_SPEC, and refuse
anything else.- A run that reaches the step limit ends with a reply, status
step_limit.
RECURSION_LIMITdefaults to 50 (was 25): two steps to answer plus two per sequential tool
call, so 24 calls. A run that reaches it streams a final reply saying so and ends with
message.end"status": "step_limit"(was anerroreventrecursion_limit, now sent only
when the reply cannot be written); its work stays in the thread. Clients that treat any
status other thanokas a failure should acceptstep_limit; the eval client counts it as
an error turn ("message.end status step_limit"). - Run records and statuses. A run is recorded when it starts (
running) and endsok,
step_limit,error,timeout,cancelledorinterrupted(its lease was lost, or its
process died: reconciled about a minute after the lease expires,error_type
ProcessLost). Dashboards keyed on the old statuses need the new ones. GET /threadslists the caller's own threads by default, read-across roles included
(they got every principal's before);?scope=alllists every thread for a role in
AUTH_READ_ACROSS_ROLES(403 otherwise; any other scope is 422). Rows gainowner, the
hashed principal id.- One message cap for every surface.
MAX_MESSAGE_CHARS(default 32000): a longer
message is 422 on/chatand JSON-RPC -32602 over A2A 1.0 and 0.3 (A2A accepted up to
MAX_REQUEST_BYTESbefore). A/chat422 no longer echoes the submitted value (input,
url); a too-long message isvalue_error(wasstring_too_long); text with an unpaired
surrogate is 422 (was 500). - Failed tool calls reach clients as an error id. Outside
APP_ENV=deva failed call's
tool.resultresultand its message inGET /threads/{id}/messagesread "The tool call
did not succeed. Reference: <error_id>." with a newerror_idfield; the error text
(policy rule, limit, upstream status and reason) goes to the model only. API-policy
refusals read "... refused by the API policy: ." (no "(api-policy.yaml)"). - Outbound calls: stricter headers and no method override. Tool-supplied
Host,
method-override (X-HTTP-Method-Overrideand its underscore spelling),X-Forwarded-*,
Forwarded,X-Original-URL,X-Rewrite-URLand hop-by-hop headers are dropped with a
warning; a_methodquery parameter or top-level JSON body key raisesApiPolicyError. - Eval gates can change result.
expect.containsandnot_containsignore case (add
expect.case_insensitive: falsefor exact matching; anot_containsword now also fails
in another case). A quality metric's pass rate is passed / scored over the cases that ran
it (was over every planned case), so a metric declared on only some cases can now miss its
gate.eval_config.yamljudge:accepts onlyprovider,modeland
max_tool_result_chars, and an unknownprompt_templateplaceholder is exit 3 at load. - New projects list
API_KEYinsecrets.keysonly undershared-bearer(the one policy
that reads it);secrets apply,deployandlogin --write-envgenerate it only there.
Existing manifests keep what they list. - Chart: bounded shutdown and a separate metrics Secret. The chart refuses to render when
terminationGracePeriodSeconds(30) is not aboveshutdown.preStopSleepSeconds(5) +
`shutdown.dra...