Skip to content

v1.7.0

Latest

Choose a tag to compare

@github-actions github-actions released this 09 Aug 01:38
· 2 commits to main since this release

Closes the design issues raised in
GHSA-xmh3-cw7j-9gp5
that 1.6.1 fixed the patchable half of and listed the rest of as Known and not
yet addressed
, plus one item from the same report that list omitted. Two of the
three witness limits documented in 1.6.0 are fixed as well.

The thread running through all of it: a control that fails open is not a control.
Authentication that switched itself off when no token was set, an operator
identity anybody could assert, profiles with no owner, a signature nothing
checked, and a sandbox flag that disabled the sandbox.

If you publish the API anywhere other than loopback, read the first entry
before upgrading
— it is the one change that can stop an existing deployment
from starting, deliberately.

Security

  • The API is no longer unauthenticated by default when it is reachable.
    Three layers were supposed to answer "is this API authenticated?" and all
    three failed open. API_BEARER_TOKEN defaulted to unset; the bearer
    middleware returned early when it was unset, so the absence of a credential
    disabled authentication instead of denying requests; and the only hard check
    lived in startup validation, which downgraded to a warning unless
    APP_ENV=production — while shipped compose set
    APP_ENV: ${APP_ENV:-development}. The default was an open control plane
    driving a browser that holds stored logins.

    The switch is now reachability rather than an environment name, because
    reachability is what decides whether an open control plane is a local
    convenience or an account takeover. New setting API_BIND_SCOPE
    (loopback | exposed) declares it, and the controller refuses to start when
    the scope is exposed without a token of at least 32 characters — in any
    APP_ENV.

    It defaults to exposed, so a deployment that declares nothing is assumed
    reachable and fails closed. The base docker-compose.yml declares loopback
    to match its 127.0.0.1:8000:8000 publish mapping, so docker compose up on
    a clean checkout still needs no token; the Codespaces overlay declares
    exposed. A hand-rolled compose file that publishes on 0.0.0.0 without
    saying so now refuses to start instead of silently serving an open API. If you
    run the controller directly, a loopback bind host in API_BIND_HOST,
    UVICORN_HOST or HOST is detected and treated as loopback.

    Authentication is decided in one place (app/auth_policy.py) for both the
    auth gate and the rate limiter, and the request path is what the tests assert:
    a reachable, tokenless controller answers 401 to /sessions while /healthz
    stays reachable for orchestrators.

    Upgrading: if you publish the API anywhere other than loopback and were
    relying on it being open, set API_BEARER_TOKEN (32+ characters). If you
    publish only on loopback with a compose file of your own, add
    API_BIND_SCOPE=loopback.

  • Two of the witness limits documented in 1.6.0 are now fixed.

    Chain forking was a correctness bug rather than a limit. Appends were
    serialised with an asyncio.Lock, which orders tasks on one event loop —
    not threads in this process, and not other processes. Two workers
    (AGENT_JOB_WORKER_COUNT > 1, several uvicorn workers, or two replicas on a
    shared volume) could read the same head and write two receipts claiming the
    same predecessor. Reading the head, signing, and appending are now one
    critical section under an OS-level lock on the chain file, with an in-process
    thread lock inside it.

    Tail truncation was undetectable: drop the last k receipts and what remains
    is a shorter chain whose every signature still verifies. Each append now also
    updates an anchor file beside the chain holding the head hash and receipt
    count, and verify() compares them — the result gains an anchor block, and
    a truncated or rewritten chain reports valid: false with the reason.
    Chains written before this release report anchor: {"status": "unanchored"}
    and keep verifying, because a missing anchor is not evidence of tampering.

    This needs no third party. An attacker who rewrites the anchor as well still
    defeats it, which is what a genuinely external anchor is for; the anchor is a
    separate artifact so one can be added later without changing this shape.
    Key compromise remains a stated limit.

    Appends also stopped re-reading the whole chain to find its head.

  • Staged skill candidates are verified on read, not just signed on write.
    Induction could sign a candidate envelope, but nothing on the read side ever
    verified one — so the signature was an assertion the artifact made about
    itself, the same shape as the pre-1.6.0 witness chain. Anyone able to write to
    the staging root could edit a staged skill, or drop a new one in, and it was
    served as a governed candidate carrying provenance.

    The registry now checks the signature before returning a candidate, and checks
    that the envelope matches the artifact it sits in — a genuine signature lifted
    from a different candidate is still a forgery. A candidate that fails is
    refused; list omits what it cannot verify. Deployments with no signer
    configured are unaffected: there is nothing to check, and signed on the
    candidate now records which case it was, so an unsigned envelope is no longer
    a dict shaped exactly like a signed one.

    Candidates induced from a mock run are marked simulated, inside the
    provenance hash and the envelope, so a skill that no browser ever executed
    cannot present itself as converged. The README's "signed provenance" claim is
    reworded to say what actually holds.

  • Codex no longer runs on the host with approvals and sandboxing disabled.
    Both Codex paths — the cli provider adapter and the host bridge — passed
    --dangerously-bypass-approvals-and-sandbox, a flag whose own help text reads
    "EXTREMELY DANGEROUS. Intended solely for running in environments that are
    externally sandboxed". A model decision could therefore run any command on the
    host.

    Nothing about the work needed it: both paths hand Codex a screenshot and a
    prompt in an ephemeral temp directory and read back a JSON decision. Codex now
    runs with -s read-only by default. CODEX_SANDBOX_MODE widens that to
    workspace-write or danger-full-access if you need it, and
    CODEX_BRIDGE_ALLOW_HOST_EXEC=true restores the bypass for the case its
    help text actually describes — an environment that is itself sandboxed. That
    opt-in logs a warning at startup and on the bridge's stderr.

  • Auth profiles now belong to the operator who saved them. Profiles lived in
    a flat root with no owner, so any caller who could reach the API could read
    one, export its cookie archive, or — the takeover that matters — open a
    session against it and drive a browser already logged in as somebody else.

    A profile saved by a caller with a proven identity records that operator as
    its owner. Reading, exporting, overwriting on import, saving over it, and
    opening a session from it then require authenticating as that operator, and
    listing hides profiles you cannot access rather than advertising which sites
    someone else holds logins for. Refusals are 403, including on export, where
    the route's catch-all previously turned any refusal into a 500.

    Ownership keys on source: "token", so claiming to be the owner in an
    X-Operator-Id header does not work. It also means ownership starts applying
    exactly when named credentials do: a profile saved without a proven identity
    records no owner, so shared-token deployments and every profile written before
    this release keep working, with no migration step and no way to be locked out
    of your own logins.

  • Operator identity can now be proven instead of asserted. X-Operator-Id
    is a request header, and until now it was the only thing that ever set the
    operator on an audit event — so the trail attributed actions to a string the
    caller chose, which reads as identity while being a label.

    New setting API_BEARER_TOKENS takes named credentials
    (alice:token-a,bob:token-b). A request authenticating with one of those has
    a verified identity recorded as source: "token", and a X-Operator-Id
    header that disagrees no longer wins — the credential does, and the false
    claim is kept on the audit record as asserted_id rather than dropped. A
    named credential also satisfies REQUIRE_OPERATOR_ID on its own.

    The shared API_BEARER_TOKEN still works and still records
    source: "header", because one shared credential genuinely cannot tell
    operators apart. That distinction is now explicit in the data, so
    authorization can require source: "token" — which is what the next release
    needs in order to scope auth profiles to an owner.

    Every named token must also clear the 32-character floor; one weak entry in
    API_BEARER_TOKENS is a way in.

  • A privately reported advisory can no longer sit unnoticed.
    GHSA-xmh3-cw7j-9gp5
    was reported on 2026-06-17 and sat in triage for seven weeks, because a
    private report appears in none of the views a maintainer opens day to day —
    not issues, not pull requests — and was found only by listing advisories
    through the API while publishing an unrelated one. SECURITY.md promises
    reporters a quick acknowledgement.

    scripts/check_open_advisories.py now fails the release audit while any
    report is still waiting in triage. Accepted drafts are reported but do not
    block, since accepted-and-being-fixed is a legitimate place to be mid-release.
    The check attaches to cutting a release because that is the one process
    guaranteed to run before users are affected by anything.

    The blocking path is what the tests cover: a gate exercised only against a
    clean repo is indistinguishable from one that always reports "all clear".

Notes

The design items listed as Known and not yet addressed under 1.6.1 are the
scope of 1.7.0, along with one more from the same report that the 1.6.1 list
omitted: skill-candidate provenance is hashed but never signed or verified.