Skip to content

v0.2.29

Choose a tag to compare

@github-actions github-actions released this 08 Oct 21:48
· 8 commits to main since this release
af0a642

Added

  • devops vault logout: revokes the token devops vault login stored, at the address and namespace that issued it (auth/token/revoke-self), then deletes it from the OS keyring. The local copy is deleted even when Vault is unreachable or the token has expired, and the output says the revoke did not happen (#709).

Changed

  • A token an earlier vault login stored as a bare string is no longer used; run devops vault login again (#709).
  • devops gh secrets sync --source vault --dry-run no longer reads Vault: it lists each secret it would read, seal and upload, so the dry run sends no request (#709).
  • devops gh project sync and devops gh project reconcile add no card: an issue reaches the board through devops roadmap intake, and a pull request gets no card, its progress showing on its issue's card. New pull requests no longer get board cards, and sync no longer adds the repository's open issues. Reconcile reports how many open issues wait for intake (#892).
  • devops gh project reconcile --plan reads the board and the repository and lists the changes; --dry-run, and the global dry run, make no request and list the requests a run makes. MCP gh_project_reconcile takes mode (dry-run, plan by default, or write) in place of dry_run, so a call with no arguments no longer writes (#892).
  • devops gh project sync reports the fields it created and reconcile's outcome, prints reconcile's stop line and exits 1 when reconcile stops early or does not start, and its dry run makes no request and prints no board number (#878, #892).
  • Every devops gh project and devops gh views --template option defaults to .github/project-template.json at the repository root, so the commands work from a subdirectory; reconcile --project-number reads the board's own Status options and loads no template (#1010, #892).
  • DEVOPS_CLI_AI_ALLOW_PRIVATE_NETWORK is read only through ai.allow_private_network, when a client is built: validate_url, the LLM client and the HTTP broker no longer parse the variable themselves, and the broker no longer freezes it at import. A check made with allow_private=False stays public-only, and an AIConfig built in code (an agent given a model name, model_request_sync, the chaos fallback, GatewayRouter() with no config) takes its own allow_private_network rather than the variable (#898).
  • new_http_client and new_async_http_client take a required level and own verify, http2 and limits; tenacity retries are opt-in with retries=, because the retry transport turns every 4xx and 5xx into an error. HttpClientBroker keeps one client per level, and request, arequest, new_client, get_client and get_async_client take the level; every Vault request (vault_request) uses the private level. get_shared_client and get_shared_async_client take the level, which is part of the pool key, so a connection never crosses levels (#898).
  • The LLM client builds its clients at the configured level, loopback or private when ai.allow_private_network is set, and raises when one cannot be built instead of falling back to a client without the retry transport (#898).
  • The broker's request hooks check only a request's own egress policy and the scheme; the async hook no longer runs a blocking DNS lookup on the event loop (#898).
  • httpcore2 and anyio are declared dependencies, at the versions already locked, and h11, which the test fakes use to write and read HTTP/1.1, is a declared dev dependency (#898).
  • A release whose pull request has merged, and whose GitHub Release is not published yet, is now merged (ReleaseState.MERGED) instead of cut, as the reprioritize plan's "Current release" line shows. A critical fix filed then goes first into the next release, with a "the release pull request of vX.Y.Z has merged" comment, and the start of a release a person cut early, its pull request open or merged, still pulls nothing in (#1294).
  • plan_intake and intake_candidate take the repository they run on (repo), which devops roadmap intake and the Service's intake job pass (#1358).
  • The intake --dry-run request plan lists the .github/labels.yml read first among the candidate reads, before the timeline reads and the embedding call, with the condition that the run has a candidate; for a --title candidate it has no condition (#1358).
  • devops roadmap intake --limit N decides the N oldest candidates and lists those it left; without it intake decides every candidate, as before, and its --dry-run still makes no request (#1360).
  • The reprioritize row's webhook hints are milestone closed and release published (CONST_ROADMAP_REPRIORITIZE_BATCH_KEYS); any other milestone event is no longer one (#1360).
  • ChangeKind.RELEASE_STARTED, which nothing recorded, is removed, and IssueRecord carries the issue's updated_at (#1360).
  • RoadmapStore.add_item returns the Item it added, Item and BoardEntry carry the card's node id (card_id), and changes_since takes except_actor, an empty one leaving out nothing. The run record card is written by the id gh project item-create --format json returns, with no board read after the create (#1361).
  • The --dry-run request plans list each board write's card read and its item-edit by node ids, and the board's fields and each filter's listing once a run; intake's placement lists no board read after its add (#1361).
  • require_budget, require_write_floor and require_floor put the budget's reset in their error's details (reset_at, ISO 8601), and refusal_reset reads it back (#1400).
  • A roadmap store that has sent no GraphQL request reports no spend and sends no budget query (#1400).
    • A devops roadmap command whose store sent none ends with no GraphQL line.
    • The --dry-run plans list the closing budget read only for a run that sent a GraphQL request.
    • RoadmapStore.graphql_spend(read=False) reports from the budgets already held and sends nothing.
  • LocalDurabilityCapability is built on pydantic-ai's public durable engine API: an engine_spec and a backend that runs each operation in-process. Toolsets are not wrapped, and model requests go through pydantic-ai's durable model in the same process. An event_stream_handler must be async, as pydantic-ai's EventStreamHandler type requires. pydantic-ai now rejects a second durability capability on the same agent (#1446).
  • A PydanticAgent tool that takes a context must annotate it with pydantic-ai's RunContext. A tool annotated with devops_cli.ai.agents.context.RunContext now fails schema generation, because the private pydantic-ai hook that accepted the subclass no longer exists. Use isinstance to narrow to the subclass when the tool reads loaded_capability_ids or tool_call_approved (#1446).
  • calculate_usage_cost prices through genai_prices.calc_price and returns its PriceCalculation, or None when genai-prices cannot price the model or usage. get_active_traceparent reads the active span through OpenTelemetry's W3C trace-context propagator (#1446).

Removed

  • The devops_cli_llm_cache_hits, devops_cli_llm_cache_misses and devops_cli_llm_cache_writes gauges (#816). Each sent 1.0 from a short-lived process and counted nothing; devops_cli_ai_requests_total's cached label carries hits.
  • devops gh project sync --task-file, the task-file parser (parse_tasks_to_project_items, ProjectItem) and sync's invented item counts (#878, #892).
  • DEFAULT_GH_GRAPHQL_SAFETY_THRESHOLD; DEFAULT_GH_GRAPHQL_BUDGET_FLOOR is the one GraphQL threshold (#892).
  • The broker's per-request allow_private_network override, its allow_private_networks argument and HttpClientBroker.validate_url; ALLOW_PRIVATE_NETWORK_ENV in devops_cli.ai.client and CONST_AI_ALLOW_PRIVATE_NETWORK_ENV; the Jaeger and probe resolve-and-check copies (_is_unsafe_ip, _resolve_safe_jaeger_target, _resolve_safe_socket_addr, _is_blocked_metadata_host). connection_limits moved from devops_cli.http.pool to devops_cli.http.client (#898).
  • devops_cli.ai, devops_cli.ai.agents, devops_cli.ai.agents.pydantic_agent, devops_cli.ai.result and devops_cli.ai.run no longer re-export these pydantic-ai internals: best_effort_price (removed from pydantic-ai), OutputSchema, OutputValidator, TextOutputSchema, run_image_process_hooks, run_output_with_hooks, SyncStreamBridge and current_otel_traceparent (#1446).

Fixed

  • Vault commands use the token devops vault login stores: the broker resolves its token from the argument, then the stored login (only for the Vault address and namespace that issued it), then VAULT_TOKEN, then DEVOPS_CLI_VAULT_TOKEN, through the shared resolver, so devops vault audit records each lookup. vault status shows a Token row naming the source, or the address a stored token that was passed over was issued for, and the dry runs of get, set, sync and leases name the token source (#709).
  • vault set, vault sync and every vault leases mode exit 1 before any request when no token is usable, naming devops vault login, VAULT_TOKEN and DEVOPS_CLI_VAULT_TOKEN. When Vault answers 401 or 403, commands exit 1 naming the token's source and the status, with no keyring fallback. A refused vault leases --revoke prints the status Vault answered (#709).
  • vault get answers from the OS keyring only with no usable token, when Vault has no value at the path, or when Vault is unreachable, and says which source answered; the not-found message names only the sources checked. vault sync, devops gh secrets sync --source vault and the shared secret chain read Vault alone, and vault sync of a path with no secret exits 1 instead of reporting zero secrets (#709).
  • vault login --store exits 1 before logging in when no encrypted, unlocked OS keyring can keep the token, instead of reporting success with the token kept only in memory, and a Vault refusal of the login is reported instead of a traceback (#709).
  • Response-cache hits are counted as cached requests (#816). A reply the response cache answered was never recorded, so the spend ledger's cached count, devops_cli_ai_requests_total{cached="true"} and every review profile.json's cached_calls stayed 0. The client now records each hit, after the cached entry passes its check, as one cached request with no tokens and no cost, in the stage of the caller: chat, the persona agent or the verifier. A cached entry the client rejects and an append-cache starting point are not hits. devops ai spend leaves hits out of its active and per-provider server counts and its local requests, the ledger's Prometheus exporter counts only requests sent to a model, and the AI spend dashboard gains a "Response Cache Hit Share" panel.
  • KubernetesService.read_pod_logs returns a PodLogStream in both follow and non-follow modes using unpreloaded streaming (_preload_content=False), eliminating raw JSON representation errors and framing lines across chunk boundaries using an incremental UTF-8 decoder split strictly on newline delimiters (#864).
  • PodLogStream enforces a bounded line length ceiling (DEFAULT_LOG_MAX_LINE_CHARS = 65536) only when pending text strictly exceeds the cap, ensuring lines of exact cap length with newlines in subsequent chunks frame without spurious empty lines (#864).
  • devops k8s logs and devops argo workflows logs stream framed log lines directly through write_stdout with secrets masked and without Rich markup or terminal hard-wrapping, while mid-stream errors in non-follow mode raise immediately without duplicate fallback output (#864).
  • devops docs check resolves every devops and uv run devops argv list under src/devops_cli, not only the MCP server's. It reports a computed token left over where the command takes no more arguments, so a stack handed to deploy-stack without --stack fails the check instead of failing at run time (#868).
  • When Qdrant is unreachable, devops ai rag index and devops ai rag index-kb print one tip, devops k8s deploy-stack --stack llm, a command deploy-stack accepts (#868).
  • devops gh project reconcile reads an unknown or expired GraphQL quota instead of treating it as spent and printing Would change 0 of 0: it starts with GraphQL's own budget query and refuses, with exit 1, only when fewer than 250 points would be left (#892).
  • Reconcile plans every change on the cards already on the board before making any, so the plan and the live run match. A run stopped by the mutation budget, the GraphQL quota or a failed item-edit says why and how many planned changes remain, and exits 1; a failed write is no longer dropped (#892).
  • A planned Status or Priority value that the board's field has no option for, such as an open pull request's In Review on a board without it, refuses devops gh project reconcile, --plan included, before any write, naming the value and the board's options; it used to fail the same write on every run (#892).
  • Reconcile's and sync's help and MCP descriptions name Status and Priority only; neither ever wrote Milestone (#892).
  • run_gh fails closed on a paginated API read when a page exits 0 but is empty, malformed JSON, contains a preamble, or has an unexpected response shape, returning exit code 1 naming the endpoint and page rather than returning a truncated listing (#1034).
  • A paginated read (run_gh with --paginate) carrying -q/--jq, -t/--template, -i/--include, or --silent is rejected before making any request, exiting non-zero and naming the incompatible flag in stderr (#1034).
  • devops gh api graphql --paginate is classified as a query and takes the paged path, exiting non-zero with a message stating that GraphQL queries do not support --paginate (#1034).
  • Paginated reads inject per_page=100 by default into page URLs when omitted, and fail closed naming the page cap when reaching DEFAULT_GH_MAX_PAGINATED_PAGES while still full (#1034).
  • _query_timeline_copilot_state queries issues/{pr_number}/timeline?per_page=100 without --jq and filters events in Python using actor.login or user.login (#1034).
  • Release pushes reach the person's credential helper (devops release pr, devops release prepare --create-pr, devops release tag --push):
    • The cut branch push, the release tag push and the cut's fetch of the release branch now run git with the person's whole environment, as their own git push does. The devcontainer's helper in GIT_CONFIG_*, VS Code's helper socket, the keyring's session bus and an SSH agent reach git. Before, the sanitized subprocess environment dropped them, and git failed with "could not read Username" (#1124).
    • Git still gets only the session's GitHub token (#767). It no longer asks for a username on the terminal, though an askpass program the person configured, such as VS Code's, still runs (#1124).
    • A failed push or fetch stops the command with git's reason, credentials masked and its text shown as written, so [remote rejected] keeps its brackets. A failed fetch of the release branch no longer lets the cut go on from a stale origin/release/v<version> (#1124).
    • release tag --push pushes the release tag alone (refs/tags/v<version>), not every local tag (#1124).
    • From an agent's shell, release pr and release tag --push now authenticate. Direct pushes to release/v<version> by release prepare --create-pr and the automatic cut require Write role bypass on GitHub release ruleset 23059172; ruleset refusals (GH013) raise a typed ReleasePushRefusedError naming the bypass requirement (#1124, #1280).
  • Automated release cutting diagnoses ruleset refusals and raises typed errors (roadmap-service, devops release pr, devops release prepare):
    • Direct pushes to release/v<version> refused by GitHub branch rulesets (GH013) raise a typed ReleasePushRefusedError naming the ruleset refusal and the Write role bypass requirement on ruleset 23059172 (#1280).
    • Every failure mode in cut_release now raises a typed DevOpsCLIError subclass naming its cause instead of a bare typer.Exit(1): ReleaseWorkingTreeDirtyError for dirty working trees, ReleaseRemoteFetchError for remote fetch failures, ReleaseBranchMissingError for missing tracking refs, ReleasePushError / ReleasePushRefusedError for push failures, and ReleasePRCreationError for PR creation failures (#1280).
    • When cut_release fails during roadmap-service background closure jobs, the job failure record and Loki logs capture the structured exception and cause rather than an uninformative Exit(1) (#1280).
  • A critical fix filed into the current release while its release pull request is open now joins it: devops roadmap intake places it there and devops roadmap reprioritize admits it with an "is cut, and a critical fix still joins it" comment, so the fix's pull request merges into release/vX.Y.Z before the release pull request does. Before, the job moved it to the next release, and a known regression shipped unless a person put the fix back by hand, as #1290 needed (#1294).
  • pytest_collection_modifyitems adds the bubblewrap skip to bwrap-marked tests only where HostSandbox().is_available() is false, restoring execution of all 22 live bubblewrap test cases in the devcontainer (#1337).
  • An item a person places in a release now stays there, and the roadmap reprioritize job comments on a rule it breaks instead of moving it out. When a person moves an item into a started release or a cut release, the item is admitted (Action.ADMIT) with a comment explaining the bypassed rule, and tracked with a distinct Joined mark so later runs do not add duplicate comments. Additionally, devops roadmap intake preserves candidate milestones, and release field writes verify milestone state before updating (#1349).
  • The roadmap Service's metrics job now clones its repository, as close and refine do, and walks the release tags of that clone. It used to run git tag in the pod's working directory, which is not a git repository, so every run logged Failed to inspect git tags and devops_cli_project_releases_total read 0 for every repository. A clone that can't be prepared now fails the job, as it fails close and refine (#1358).
  • The metrics and refine jobs fail, naming the job and needs_clone, when they are run without their repository's clone, instead of reading the working directory (#1358).
  • collect_project_metrics_report(root=...) walks the tags of the repository at root as given, so a clone kept under .data/ inside a workspace checkout reports its own releases, not the checkout's. Without root, as in devops gh metrics, it still uses the working tree around the current directory (#1358).
  • Intake reads .github/labels.yml at a run's first candidate, before any timeline read, embedding call or proposal, and a run with no candidate does not read it. A missing file, or one that declares no type/* label other than type/epic, now stops intake with a configuration error that names the repository and the file and says the file must declare the type/* labels intake may assign; a file that is no label specs names the repository too. Any other failed read of the file is still reported as GitHub's error (#1358).
  • A shipped release starts the next one without a person: the roadmap poll on GitHub now reports a GitHub Release published since the jobs last ran as a ship, and a release pull request opened, or closed unmerged, as a cut or an un-cut, read from the REST releases and pull request listings 20 entries a page, with no GraphQL request. A ship is reported whoever published it, so the Service never drops one as its own change. Before, only the in-memory store reported release changes, and a ship was seen only through the milestone webhook hint, which a restart right after the ship lost (#1360).
  • The Service decides reprioritize with reprioritize.is_due, the rule the lifecycle machine tests, given the current release: a ship, cut or un-cut, a change to an item in the current release since reprioritize last ran, and once a day for the stall check, which the Service never ran before. A change in a planned release, or one older than reprioritize's own last success, no longer makes it due, and judging a change reads nothing from the store (#1360).
  • A round runs close, reprioritize and metrics before intake and refine, and intake decides at most 5 candidates a round, never-decided or since-edited ones first, oldest first. Its record of the candidates it left (intake.json, beside schedule.json) survives a restart, makes intake due at the next poll while fresh ones remain, and is written before any placement, so intake keeps the round's skips and folds when a placement raises, and a candidate whose placement raised is decided again after the fresh ones (#1443 gives each placement its own failure); a record that can't be read fails the intake job, naming the file and the remedy. A burst of new issues no longer holds back a release's start or the metrics for an hour or more (#1360).
  • The roadmap store writes a board field by the card's node id and reads only that card first, so a job-record mark costs about 3 GraphQL points instead of about 215, and filing and placing a critical fix a small constant (about 17 in the tests' fake GitHub, which leaves out the few queries gh makes to resolve an add) instead of about 1,500, whatever the board's size. It no longer sends gh project field-list or an item-edit that names a card by URL or a field by name, each of which reads the board's first 100 items with every field value, and no longer re-reads the whole board before each write (#1361).
  • Intake writes to the card gh project item-add returns, so a placement no longer fails with "#N is not on the board after intake added it" while GitHub's board listing has not shown the new card yet (#1361).
  • A store reads the board's fields once, and each filter's board listing once, for its life, one command or one Service round, and its own writes keep that listing current: a reprioritize run reads the board once instead of twice, and a Service round reads it once however many changes its triggers look up. Closing an issue drops the listing, because the board's own "Item closed" workflow then sets the card's Status, so a release cut rendered in the same round shows Done (#1361).
  • A roadmap job's board write stops with RoadmapCardChangedError before any edit when someone changed that field after the job's store read the board, unless the card already holds the value the job writes, so a job never reverts a person's change it did not see; the job fails and the next round plans with the change (#1361).
  • A Service round whose new events are all its own reads no board: the store drops them before it attaches job records (#1361).
  • A board write still refuses before it starts while fewer than the 250 reserved GraphQL points are left, now read from its card read's rateLimit (#1361).
  • devops roadmap close also closes the items delivered into a release whose milestone closed before closure ran. For each closed Release that still holds an open issue, it reads the pull requests merged into that release's branch first and closes the items of that release they deliver, with the usual comment. It lists the rest under the release's own heading, and none of them holds the current release's cut. An item a person reopened there stays open and is named, since a job never reverts a person's change. An item a pull request into the current release's branch closes is listed once, as a closing. Closure reads release/vX.Y.Z, the branch the cut makes, even for a milestone titled without its v. A run with no such Release reads what it read before, and the dry run lists the extra reads under their own condition (#1362).
  • chat_structured tells the model the schema its reply must match, for intake and for both refine steps: the system message ends with the schema's JSON Schema, introduced by the structured_output_schema.md prompt, the gateway provider also sends it as response_format (type json_schema, without strict) and the ollama provider as format. Its first attempt validates the reply before the response cache serves or stores it, so a reply that fails the schema is never cached or replayed and still gets the reflection retry, which now names every violation with its fix hint. StructuredOutputValidationError counts the last reply's violations (violations), and its message keeps only that count and the error types, never a key the model invented (#1363).
  • devops roadmap refine and the service's refine job skip only the item whose model call fails: the other items are refined and written, the plan lists the skipped item with its error class and schema violation count but no model text, and the run then exits 1 with RoadmapRefineError (ROADMAP_REFINE_FAILED). Rejected credentials still stop the run at the first item, and intake's refine hook still reports a failed refine (#1363).
  • Refine's research and proposal prompts move to roadmap_refine_research.md and roadmap_refine_proposal.md; the proposal prompt names every RefinementProposal field, and the issue reaches the model as one JSON document (#1363).
  • A gh call in the second or two after a quota window's reset no longer fails with "Rate limit state for subcommand 'core' is in an unknown or broken state". When GitHub's rate_limit answer still reports the window that has just ended, run_gh waits until one second past that reset and then runs the command. It logs one INFO record naming the resource, the reset in UTC, how far the clock is past it and the wait. The roadmap Service lost a round, a job or a metrics read to this error about once an hour (#1364).
  • run_gh still fails closed with GitHubRateLimitError, and the command never runs, when GitHub's answer has no remaining and reset for the resource (the error names the resources it does report), or when the local clock is more than 5 s past the reset GitHub just reported (the error names both times in UTC and the gap) (#1364).
  • The roadmap Service's job clone tracks the current release's release/vX.Y.Z while origin has that branch, and origin's default branch otherwise: between a ship and the next release's start, and while no release is open. Close and refine no longer fail with "is not a commit" while the next release has no branch. The clone fetches with --prune, so the branch a ship deleted no longer passes for a live one, and with no release open the clone follows the default branch instead of keeping the commit it last had. When a release is current but has no branch, the Service logs one INFO line naming the release and the default branch it checked out (#1366).
  • A failed fetch, remote set-head, symbolic-ref, checkout, reset or identity write in the job clone raises GitOperationError naming the git command and git's reason, credentials masked. Only a release branch that git rev-parse --verify finds absent after a successful pruning fetch selects the default branch (#1366).
  • A GraphQL budget refusal in a roadmap Service round now holds every repository's rounds until 5 s after the reset it names (#1364's clock-skew bound). Before, each worker started its next round at once against the machine account's spent budget (#1400).
    • The refusal may come from a read outside any job, such as changes_since, or from a failed job, whose reset RoadmapRunError.reset_at carries.
    • The Service logs the wait once for each new reset, and the triggers that arrive meanwhile run as one round per repository after it.
    • A drain during a pause doesn't wait for the rounds it holds, and it joins each worker it stops.
    • The rate limiter's own errors, which carry reset_epoch or #1364's reported_reset details, hold nothing. A pause time that can't be read is a warning naming its class, and the worker carries on.
  • Each Service round that sent a GraphQL request ends, however it ends, with one line naming its repository, the account's GraphQL points spent during the round, and the points left (#1400).
    • A round a refusal ended takes the line from the refusing budget and makes no closing budget read, so the pause starts at once.
    • A closing read that fails, with any error, is a warning naming the error's class and never hides the round's own error.
  • Intake places an open issue whose board card a person archived. Before, it failed every round with "Board #N does not hold card … that #N was just added as." and stopped the round's other placements with it. gh project item-add names the archived card, because the board keeps one card per issue, and the store now restores it with gh project item-archive --undo after one read of that card. It refuses below the GraphQL reserve and fails closed when gh answers with another card. A restored card keeps what it holds: one with a Priority gets no other write, keeps its milestone and gets no comment, and --confirm names it as left as it was; one without a Priority keeps its milestone and its values, and intake writes only its empty fields and a reason comment for them, so the job record never claims a value a person set (ADR 0002). devops roadmap intake --help says how to keep such an issue off the roadmap: close it as not planned. The in-memory store keeps the same contract, with archive_card for tests, and the --dry-run request plan lists the restore under the add, under its own condition (#1403).
  • A release no longer takes the roadmap Service down. The Service image a release pins now exists before main pins it, so Argo CD never rolls roadmap-service onto a missing image (#1451).
    • Each push to release/vX.Y.Z that holds version X.Y.Z and changes the image's inputs builds, smoke-tests and scans service:vX.Y.Z, pushes it by digest, attests it, and only then tags it. The release pull request shows the result as the Build & Publish Service Image check. Right before it tags, the run checks again that its commit is the branch tip and that main doesn't hold the version yet, so a release merged during the build keeps the image main pins.
    • The check fails when main has image changes the release branch lacks; main commits that leave the image alone don't count. RELEASE_CYCLE.md shows how to merge main into the branch.
    • On main, release.yml points latest at that image instead of rebuilding it, once its release.yml provenance verifies. It never replaces a tag main pins: when main's tree has other image inputs, it warns and the difference ships in the next release. It builds the image only when no image has the tag.
    • ci.yml's Service Image job no longer builds the release pull request's image a second time.

Security

  • vault status, and the MCP vault_status tool whose address the client chooses, never send X-Vault-Token, and a stored token is sent only to the Vault that issued it (#709).
  • Every Vault request, logins included, refuses a redirect to another origin, so neither the token nor a login body follows a redirect off the configured Vault address; httpx2 strips only Authorization on such a redirect (#709).
  • vault sync never writes the keyring entry that holds the login token (#709).
  • devops k8s deploy-stack --stack llm applies the LLM gateway's NetworkPolicy llm-gateway-perimeter before the gateway's Deployment, and teardown-stack deletes it. On a cluster without Argo CD the gateway used to run with no policy, so its egress was unrestricted (#795).
  • The gateway's egress no longer admits every pod on ports 11434 and 8000 through an ipBlock rule for the private address ranges, nor two pod selectors that no workload carries. It admits the Ollama tiers, Valkey and DNS, plus the vLLM rule that #820 removes. A test derives the backends from the gateway's ConfigMap and Deployment, and another fails on any wider egress (#795).
  • An HTTP client built by devops_cli.http.client checks the address it dials, at the connect and on every redirect hop, so a DNS rebinding answer can no longer slip past a check made earlier. Its connection pool is an httpcore2 pool whose network backend resolves the host once, refuses every answer when any is unsafe for the client's egress level (public, loopback or private), and dials only the vetted addresses, as IP literals, each in turn. TLS and the Host header keep the hostname. web_fetch makes one lookup per hop, where it made four on the first hop and three on each redirect (#898).
  • Cloud metadata addresses, as #897's classifier lists them (169.254.169.254, 169.254.170.2, fd00:ec2::254, fd20:ce::254, their integer, hex, NAT64, 6to4, Teredo and IPv4-mapped spellings, and the metadata host names), and scoped IPv6 addresses are refused at every level, by address as well as by name. That includes clients that reached any address before: the Kubernetes API-server proxy client and the cluster-service URLs (#898).
  • Link-local addresses other than metadata, such as the 169.254.1.2 a rootless Podman container reaches its host at, are refused at the public and loopback levels and allowed at the private level, so the documented host.docker.internal setups keep working for the Kubernetes and Vault clients and the sandbox probe (#898).
  • Clients built by the factory ignore HTTP_PROXY, HTTPS_PROXY and ALL_PROXY: a proxy would be the peer the check vets instead of the destination. web_fetch, devops install-tools downloads, the DuckDuckGo and Tavily tools, the harness documentation fetch, the GitHub and Cloudflare Statuspage fetch, Jaeger trace queries, the sandbox probe, every Vault request, and cluster-service requests stop using an environment proxy; the AI clients already did not use one. A workstation that reaches the internet only through a proxy loses those requests (#898).
  • Narrower: devops install-tools downloads are public-only, and DEVOPS_CLI_AI_ALLOW_PRIVATE_NETWORK no longer lets them reach a private address; every redirect hop must be https, where an https-to-http redirect was followed. The DuckDuckGo and Tavily tools, the harness documentation fetch and the Statuspage fetch, which checked no address, are public-only. The documentation crawler (devops ai ingest docs), which refuses private addresses with allow=False, no longer has the environment variable widen that check (#898).
  • Wider: the sandbox HTTP and TCP probes run at the private level, so a target answering a link-local host-gateway address is probed where it was refused; probe_tcp dials each vetted address in turn instead of only the first answer (#898).
  • Jaeger is queried at its configured hostname at the loopback level, as before admitting loopback and public addresses and refusing private ones whatever the flag. Its HTTPS certificate is now checked against the hostname rather than the IP address, and a URL's userinfo no longer reaches the Host header (#898).
  • A native devops k8s deploy-stack --stack infra (or all) applies the monitoring namespace's default-deny perimeter, monitoring-default-perimeter, through the monitoring kustomization right after the root kustomization creates the namespace and before any chart installs into it. Only Argo CD applied it before, so on a cluster without Argo CD every pod could reach Prometheus, Grafana, Alloy and Pyroscope (#913). A test now fails for any NetworkPolicy under k8s/ that deploy-stack does not apply, unless an exemption names the issue or the path that owns it.
  • A sandbox workspace can no longer expose a container engine socket, the user's runtime directory or its session bus (#1115). devops docker sandbox, devops test sandbox and devops sandbox deploy now run one shared workspace check, and each command keeps its own error type and exit code. It refuses a workspace that is, holds or sits under any of these directories, and the refusal names the workspace and the directory:
    • /run, and so /run/user/<uid> with its session bus, gpg-agent and keyring sockets, and /var/run where it links to /run;
    • $XDG_RUNTIME_DIR when it is set to an absolute path, for example VS Code's /tmp/user/<uid>, so a workspace of /tmp is refused there;
    • the directory of the unix socket DOCKER_HOST names (default /var/run/docker.sock), as docker-py derives it, and, when that socket is a symlink such as Docker Desktop's WSL link, the directory it leads to. Reading the endpoint makes no connection and no request, and a tcp:// endpoint names no socket. A DOCKER_HOST the client cannot parse, such as fd:// or tcp://host:abc, refuses every workspace. A socket chosen through a Docker context is not followed yet (#1108).
  • The workspace check no longer looks for the text docker.sock in the path. A directory holding podman.sock was let through before.
  • devops docker sandbox and devops test sandbox now refuse what devops sandbox deploy already refused: a workspace path containing .., and a workspace under a system directory such as /etc/ssl or /var/lib/....
  • The MCP tools docker_sandbox and sandbox_deploy run the same check on workspace before they build a command line. The client gets the refusal as a tool error and no command runs.
  • pydantic-ai moves from 2.35.0 to 2.54.0, with pydantic-ai-slim, pydantic-evals and pydantic-graph; the resolver also moves anthropic to 1.12.1, openai to 3.26.1 and google-genai to 2.29.0. This fixes three advisories: GHSA-4x9p-g9wm-8q7f (tool and agent run spans put content in exception events even with include_content=False), GHSA-fpf4-vwcp-v4hp (quadratic title extraction in web_fetch blocked the event loop) and GHSA-vmxc-h2x2-jmf3 (an IPv6 zone identifier got past the SSRF cloud-metadata blocklist, which devops-cli's URL pre-flight checks and connect-time egress check also use). uv audit passes again, and with it uv run devops ci and the pre-push gate (#1446).