Skip to content

Releases: Celmis-labs/Celmis

v0.1.29

Choose a tag to compare

@github-actions github-actions released this 31 Aug 17:19

Fixed

  • A paused session reconnected every two seconds, for as long as the tab was
    open.
    Measured on production: 36 event-stream connections in 75
    seconds
    , under an amber "Reconnecting…" badge, on a conversation that was
    perfectly healthy and simply waiting for somebody to type.

    Two causes that compound. The retry loop stopped only on final — "cannot
    continue at all" — while a paused session reports resumable, which
    continues when a person sends a turn and never on its own. And the backoff
    reset on any frame, stream_end included, so every reconnect that found
    nothing reset the counter it was supposed to grow.

    What still reconnects is the state between the two: a RUNNING session whose
    API restarted under it, which reports neither flag. That is the deploy case
    the server comment describes, and stopping on any stream_end would strand
    it. Sending a turn now wakes the stream, because stopping the loop is only
    safe if typing restarts it.

v0.1.28

Choose a tag to compare

@github-actions github-actions released this 31 Aug 16:56

Fixed

  • A notification with a relative link arrived as an empty card. Google
    Chat validates openLink.url; /claude/<session-id> fails that validation
    and the whole card is dropped, so what lands in the room is the bot's name
    with nothing under it — while the log records notif_delivered … delivered=1.
    Seen on a phone, as a hole in the feed between a firing alert and its
    recovery, at the exact minute an agent session finished.

    The alerts path already knew this rule and wrote it down; the agent path did
    not. It now lives in one place — dispatch.public_link, absolute or nothing
    — and a guard reads every notify(link_url=…) call site with ast.

  • **0** critical · **4** error reached a chat card with the asterisks in
    it.
    body_md is markdown and every other adapter is right to treat it as
    such — Slack takes mrkdwn, Discord takes markdown — but a Google Chat
    textParagraph renders a small HTML subset and prints the rest verbatim.
    Bold, italic, inline code and links are converted now, and the text is
    escaped first: a notification body carries an alert title from somebody
    else's monitoring, and that must not be able to put markup into a card sent
    under this product's branding.

Changed

  • The agent's own notification was titled "Claude Code finished a step". The
    page it links to is called Agent in all sixteen locales.

v0.1.27

Choose a tag to compare

@github-actions github-actions released this 31 Aug 15:06

Fixed

  • A recovery paged exactly as hard as the outage did. Grafana sends the
    same labels when an alert resolves as when it fires — the labels identify
    the rule, not the event — and only status says which happened. The parser
    read the labels and ignored status.

    Reproduced end to end on production, not deduced: a test gateway came back
    up at 14:45 and the workspace was paged critical with the title
    5xx rate 100% over 1m, byte for byte the card it had sent at 14:30 when
    the service actually broke. A false page is noise; a false page
    indistinguishable from the real one teaches people that a critical card
    might mean nothing. A resolved alert now says so in its title and arrives as
    info. It still arrives — coming back is worth knowing — it just stops
    impersonating an incident.

v0.1.26

Choose a tag to compare

@github-actions github-actions released this 31 Aug 14:10

Changed

  • The agent page was headed "Claude Code" while the sidebar entry that
    opens it said "Agent".
    Both were on screen at once — sidebar, breadcrumb
    and H1 in one frame — and that frame is the screenshot published in the
    guide. Two things were wrong and only one is about a trademark: a page whose
    title contradicts the entry you clicked to reach it is a bug at any name.
    claude.title now equals nav.agent in all sixteen locales.

    Every descriptive mention stays, because the terms allow saying a product
    runs Claude Code: the connection card, the token walkthrough, the engine
    pickers, the MCP client list, AI report: Claude Code beside off and
    API. The guard that was supposed to protect the permitted half had picked
    claude.title as its specimen — a heading, not a sentence — so it asserted
    the one string that had to change. It now watches claude.helpTitle, on the
    same page, and two new guards hold the heading: it must agree with its nav
    entry, and neither may carry the engine's name.

v0.1.25

Choose a tag to compare

@github-actions github-actions released this 31 Aug 10:26

Security

  • The sandbox firewall covered one of the two ways in. The INPUT rule was
    in place and the deploy logged "sandbox→host blocked"; probed from inside
    the running container, host:22 timed out and host:80 answered. A
    container port published on the host is destination-NAT'd and then
    forwarded, so INPUT never sees it.

    Nothing was exposed by it — the only 0.0.0.0 port is Caddy, which the
    internet reaches anyway, and postgres, the api and the web app are bound to
    127.0.0.1; cross-network isolation was probed too and holds. The rule is
    here because the next published port is the one nobody re-checks. A second
    rule in DOCKER-USER matches --ctstate DNAT: exactly the packets that
    arrived through a published port, leaving the sandbox's internet egress and
    the api on the sandbox network alone. Verified on production after applying
    it — 22 blocked, 80 blocked, internet open, api open.

Fixed

  • CI could not see the history it was asked about. actions/checkout
    fetches one commit and no tags. The guard that recomputes PROVENANCE.md's
    numbers failed with "the record pins v0.1.23, which is not a tag in this
    repository" — a true sentence about the clone and a false accusation against
    the file — and the guard that no commit ever weakened that record and its
    own test together walked a log of one commit and passed on nothing. Proven
    on a --depth 1 clone. fetch-depth: 0, and both now fail loudly on a
    shallow clone rather than answering a question they cannot see.

v0.1.24

Choose a tag to compare

@github-actions github-actions released this 31 Aug 09:55

Security

  • The sandbox firewall step said it was not optional and continued without
    it.
    Both failure branches logged a WARNING: and carried on, five lines
    after a real fail on a missing .env — so a host without iptables shipped
    a container that runs a tenant's own build commands with a route to the host.
    It now tries iptables, then nft, then stops, with
    CELMIS_ALLOW_UNFIREWALLED_SANDBOX=1 as a typed decision for a host isolated
    another way.

    And it did not survive a reboot. Measured on the production box: the rule was
    in place, iptables-persistent was absent and crontab was empty — so the
    gap after a restart was not until the next scheduled deploy, it was until
    somebody deployed by hand. A systemd unit ordered Before=docker.service
    reinstates it at boot.

Added

  • Publishing is a workflow now, not a person with a token.
    publish-verifier.yml uses PyPI Trusted Publishing, so upload rights belong
    to this workflow in this repository for the length of a run. The tag prefix
    is pypi-, and the first letter is the reason: release.yml triggers on the
    glob v*, which verifier-0.2.1 matches, because "verifier" starts with a
    v — that tag would have built three container images.

  • Images carry a signed build attestation. gh attestation verify now
    answers "built from which commit, by which workflow" for somebody holding
    only the image; before this the OCI revision label was the only claim, and
    a label is text anybody can write. Buildkit's own provenance: false stays —
    different mechanism, and the comment there is right.

  • packaging/pypi/DIGESTS.md records the sha256 of every published file in
    the git repository, because PyPI is the same channel that serves them. It
    says plainly what it is: the 0.1.0 and 0.2.0 digests were read from PyPI and
    written down, which is a trust-on-first-use anchor and not independent
    attestation. From the first workflow-published release the digests are
    printed before upload and the artefacts carry a PyPI attestation.

Fixed

  • The lint gate everybody believed in enforced nothing. ci.yml said the
    rules-of-hooks rule was blocking as a test in the python job; those tests
    carry skipif(not _eslint_available()) and that job never installs
    web/node_modules, so they reported SKIPPED in green — while this step ended
    in || echo. Measured for the first time: 42 findings, 31 errors, 24 of them
    in the react-hooks family whose breach took every authenticated page down.
    scripts/eslint_gate.py makes rules-of-hooks fatal at any count and ratchets
    the rest from 42.

  • The verifier read whatever an archive told it to. A zip declares each
    member's size before you read it, and neither copy checked. Measured against
    the published celmis 0.2.0: an archive of 200 KB on disk declaring 200 MB
    verified as OK with a 215 MB peak, and the number was the sender's to
    choose. The person running that got the file from the party they are
    checking, on their own laptop, precisely because they do not trust them.

    Sizes are checked before anything is read — per member, and in total, which
    is what a spread of medium files defeats — and hashing streams a megabyte at
    a time, counting what actually arrives so a header that lies in the other
    direction is refused too. Limits are three orders of magnitude above a real
    pack, and identical in both copies, held so by a test.

  • A manifest that was valid JSON and not an object crashed the verifier.
    manifest.get(...) on a list raised AttributeError: plain output reported it
    as a problem found (exit 1) and --json printed a traceback, also exit 1.
    Neither was true — nothing had been checked, which is exit 2. Both modes now
    agree, and the refusal says what the manifest actually was.

v0.1.23

Choose a tag to compare

@github-actions github-actions released this 31 Aug 09:11

Security

  • /healthz handed out the installation's configuration, to anybody, now.
    main.py copies the webhook sub-app's routes into the main app; the sub-app
    declares its own detailed /healthz, and a copied route lands ahead of the
    plain one declared below — first match wins. middleware.py exempts the path
    from authentication and Caddy proxies /backend/*, so the public answer
    carried every model name, deadline, budget, the cache size and which backends
    are configured. Observed answering exactly that on production.

    The route is no longer copied. The reason the block existed is real and is
    kept: env_file points at a .env that does not exist in the container, so
    a setting the compose file does not forward silently takes the code default
    and nothing outside could tell which had happened. It is served from
    /api/ops/review-settings, behind an admin.

  • /readyz and /metrics were public by the same route. /readyz
    returned per-dependency detail including a user count and str(exc)[:200]
    from a failed connection — for a bad DSN, a fragment of the DSN. It now
    answers {"ok": …} and the status code, which is a probe's whole contract;
    the detail is at /api/ops/readyz behind an admin.

    /metrics returned the entire Prometheus slice — queue depths, spend, review
    counts, error rates. It now needs CELMIS_METRICS_TOKEN when the request
    passed a proxy, and is served unconditionally when it did not: the bundled
    Prometheus scrapes api:8000/metrics directly over the compose network and
    keeps working. A caller can add X-Forwarded-For and lose access; they
    cannot remove Caddy's, so the public path fails closed.

v0.1.22

Choose a tag to compare

@github-actions github-actions released this 31 Aug 08:41

Added

  • A finding now says whether your own code names the package. A findings
    list is hundreds of rows and every one reads the same, so a direct dependency
    the service imports on its hot path and a transitive package pulled in four
    levels down by a build tool looked identical to whoever had to triage them.
    named_in_code carries an import-position answer with up to five
    file:line sites.

    Three states, and the third is the point. imported, not_found, and
    unknown — because a package name does not determine its module name.
    beautifulsoup4 imports as bs4, pillow as PIL; reporting those as "not
    imported" would be the silent zero this subsystem is built to refuse. The
    rule is exact for npm, Go and crates.io, so absence means something there;
    for PyPI it is not, and the answer says so.

    This is not reachability, and nothing in it is named as though it were.
    Reachability would need the dependency's own source in the index — excluded
    on purpose — advisories that name the vulnerable symbol, which OSV carries
    for a small minority, and a notion of where execution starts, which does not
    exist here. A test fails the build if any identifier in the feature is
    called reachable, because a column name reaches an API response and
    eventually a filing, where nobody reads the docstring that qualified it.

Fixed

  • Four caps bounded a dependency audit in silence. Twenty lock files, four
    thousand entries per lock file, forty manifests, six hundred transitive
    candidates — every one a plain slice or break, with nothing logged and
    nothing recorded. A monorepo with twenty-five lock files produced an SBOM
    missing five, and the evidence pack built from it read as a complete
    inventory. The caps themselves are right; an audit has to end. Not saying
    they bit is what turned a bounded read into a false statement about what is
    installed — and document.py already writes the rule down: "count of what
    was dropped is printed rather than silently truncated".

    Each now logs what it dropped and records it, and the run summary carries
    truncated per repository so the SBOM and the pack can say the list was
    short. A test asserts the caps still bound the work, because lifting them
    would turn a warning into an audit that never finishes.

  • Two texts still promised verification without trust. The README
    scenarios table and home.html were missed when the evidence-pack claim was
    corrected, and the site build reads README — so the old promise was live on
    the published docs page.

v0.1.21

Choose a tag to compare

@github-actions github-actions released this 30 Aug 22:37

Security

  • The evidence pack's hashes prove consistency, not authenticity — and the
    product said otherwise.
    MANIFEST.json records a sha256 for every other
    file and none for itself, because a file cannot contain its own hash. So
    anyone who opens the zip, edits a file and rewrites that file's entry in the
    manifest passes verification. Demonstrated against a real production pack:
    the edited archive verified as OK and exited 0.

    README, the dependencies page and the onboarding tour all said a third party
    could "check nothing was edited afterwards without trusting us". That is
    false of an unsigned manifest that does not hash itself, and it is the
    sentence a CRA filing would lean on.

    The fix is the manifest's own hash, obtained from somewhere the sender does
    not control. GET /api/deps/{run_id}/evidence now returns it in
    X-Celmis-Manifest-SHA256 and logs it; celmis verify --manifest-sha256 <hex> checks against it; and every text now says which of the two things it
    establishes. A malformed hash is a usage error (exit 2), not a mismatch
    (exit 1) — losing characters to a line wrap must not read as tampering.

v0.1.20

Choose a tag to compare

@github-actions github-actions released this 30 Aug 21:52

Added

  • celmis is on PyPI, and the pack now says so. Published 0.1.0 and
    verified from the live index: installed on Python 3.9.25 in a clean
    container, all five commands run against a real production pack, one flipped
    bit reported as findings.json: sha256 mismatch with exit 1, a missing file
    with exit 2. summary.md inside every pack said "recompute them and
    compare", which was true and had no executable form; it now names
    pip install celmis and celmis verify, and says in the same breath that
    you are not required to use it — the manifest is plain JSON and sha256 is
    sha256. A tool that made checking us require trusting us would defeat the
    point of the pack.

  • packaging/pypi/ — the evidence-pack verifier, as a standalone package.
    summary.md inside every pack tells a third party they can check it without
    trusting the tool that made it, and until now the only way to reach
    verify_pack() was to clone an AGPL repository and install forty
    dependencies under Python 3.13. The claim was true and unexecutable. The
    verifier is a few hundred lines of hashlib, io, json and zipfile, so
    it lifts out with no dependencies at all and runs from Python 3.9 — which is
    where a locked-down audit box usually is. Two commands, verify and show,
    with exit codes an auditor's CI can read: 0 intact, 1 problems found, 2 could
    not check.

    A fixture pack is committed in both trees and a drift test builds a fresh
    pack from build_evidence_pack() and runs the packaged verifier over it.
    Without it, the day somebody adds a file to the producer every installed
    celmis verify starts reporting present but not in the manifest — a false
    accusation of tampering, produced by our own tooling, against the operator.

Changed

  • The platform's distribution is celmis-platform. celmis on PyPI is the
    verifier above, and while both were called the same thing two failures were
    one command away: python -m build from the repository root would publish
    the entire server under the verifier's name, and in a shared environment
    importlib.metadata.version("celmis") would answer with the verifier's
    version — which src/vault/provenance.py stamps into every generated
    document and /api/capabilities reports as the running platform. Both
    DISTRIBUTIONS tuples now ask for celmis-platform first, and the root
    carries a Private :: Do Not Upload classifier, which is not a real
    classifier and which PyPI therefore rejects — a hard stop rather than a note.

  • Two sentences kept the mark after the labels above them lost it. The
    navigation entry and the tour heading were renamed to "Agent"; the subtitle
    and the tour's own description still said "Dispatch a bug straight to the
    Claude agent" and "Claude agent: code-editing sessions on your repos". A scan
    for "Claude agent" found seven locales of sixteen — the other nine inflect it
    ("agentovi Claude", "à l'agent Claude", "al agente de Claude"), which is why
    the first pass missed them. All thirty-two strings are fixed, and the guard
    now covers the keys that name a section of ours, not only the button.

    Naming Anthropic's product is untouched: "Claude Code agent — researches the
    code" is an engine the operator picks between, and erasing the mark there
    would leave them guessing what they chose.