Skip to content

v0.22.0

Choose a tag to compare

@github-actions github-actions released this 22 May 16:38
· 68 commits to main since this release
Immutable release. Only release title and notes can be modified.
v0.22.0
efe0340

Security-hardening release. Resolves ~40 findings from a codex security sweep
(plus dependency CVE bumps and OpenSSF supply-chain tooling). See the per-area
entries below.

Security — glorbo status no longer prints the token, history won't commit provider secrets, reassign flood capped (codex B-027/C-063/C-067)

  • B-027: glorbo status inlined the dashboard token (?token=…) in both --json
    and the table — operational metadata that lands in monitoring/CI/supervisor logs.
    Status now prints the bare base URL plus a non-secret pointer; the token is gone.
  • C-063: the HomeHistory git repo used a denylist .gitignore that excluded only
    config.md, so a user-controlled providers.toml carrying [env] API tokens got
    committed. providers.toml is now git-ignored by the history layer.
  • C-067: an agent reply's ACTIONS block applied every reassign_to directive
    (each = a frontmatter rewrite + handoff-chain append + audit event), so one reply
    could flood the audit log. Reassign is now capped to one effective reassign per reply
    (last-writer-wins).

Security — CLI/runtime hardening: terminal-escape, MCP DoS, poison inbox, retry spin, dep-gate (codex C-117/C-079/C-076/C-129/C-130)

  • C-117: glorbo logs printed agent stdout.log verbatim, allowing terminal-escape
    injection into the operator's terminal. Output now passes through a new linear
    Glorbo.Terminal.Sanitizer (strips C0/C1 + CSI/OSC; non-backtracking to avoid the
    C-091 ReDoS cliff); --raw opts back in for trusted debugging.
  • C-079: the MCP query_audit tool didn't bound month/year; an out-of-range
    month materialized an unbounded month stream (unauthenticated DoS). Now validated +
    capped.
  • C-076: a poison inbox message whose derived task_id failed validation made the
    agent crash-loop (raise before drain). The Agent.Server now pre-validates and
    quarantines the undispatchable message to history/rejections/ instead of looping.
  • C-129: a throttled inbox dispatch no longer re-dispatches the identical file in a
    tight spin; it re-arms the wake for the next free slot.
  • C-130: the scheduler captures depends_on authoritatively at arm time and gates on
    the union of on-disk + registered deps, so blanking/coercing depends_on on a mutable
    task file can no longer fail-open past the dependency gate (emits a tamper audit).

Security — path-approval subset check, task-edit audit, token URL strip (codex B-014/C-094/C-120)

  • B-014: the dashboard path-approval handler passed the client-supplied paths
    straight to PathRequestGate.approve without comparing them to the sentinel's
    original requested paths — a tampered LiveView payload could approve arbitrary
    host paths (confused deputy). The gate now re-reads the pending sentinel and
    enforces that granted paths are a subset (write→read downgrade only) of what was
    requested; new paths / read→write escalation are refused.
  • C-094: dashboard task edits (TaskLive + KanbanLive save_task) wrote
    frontmatter with no audit entry (only deletes were audited), bypassing the
    append-only audit log. Edits now emit a task.edit audit event (actor + changed
    keys).
  • C-120: after the session cookie is set, the DashboardToken plug now
    302-redirects to strip the ?token= from the URL and sets Referrer-Policy: no-referrer, narrowing the token's exposure in history/Referer. (Full one-time
    rotation is GEP-0049.)

Security — stado provider state relocated into the per-agent sandbox (codex A-001/B-006/B-023/B-026/B-024)

The bundled stado provider mounted the host's XDG data/state dirs
(~/.local/share/stado, ~/.local/state/stado) as read-write binds shared
across all companies, and ran stado stats on the host against that shared
state. A sandboxed agent could read/modify another company's stado sessions and
git worktrees (cross-company isolation breach), and the usage walker executed the
provider outside bwrap. Now: stado's XDG_DATA_HOME/XDG_STATE_HOME point into
the agent-private workspace (the rw host binds are dropped; only ~/.config/stado
stays RO-bound for model selection), so sessions/worktrees are per-agent and never
touch shared host state. The stado stats usage call is handed that same
per-agent XDG env (no host secrets reachable) and bounded by a hard timeout. ACP
session-id files are now lstat-guarded against symlink swaps (B-024). Note:
existing host-side stado sessions are not migrated (pre-1.0 atomic cut).

Security — dependency CVE bumps + supply-chain hardening (OpenSSF)

  • Bumped vulnerable deps within existing constraints: Phoenix 1.8.5→1.8.7
    (NDJSON long-poll memory DoS, GHSA-628h-q48j-jr6q), Bandit 1.10.4→1.11.1
    (chunked-encoding DoS, WebSocket OOM, request smuggling — 7 advisories),
    Plug 1.19.1→1.19.2 (multipart header DoS, GHSA-468c-vq7p-gh64),
    decimal 2.3.0→2.4.1. The remaining decimal advisory (GHSA-rhv4-8758-jx7v,
    MODERATE) is first patched in 3.0.0, which the ecto-family constraint
    (~> 2.0) blocks; it is ignored in CI with rationale (not reachable —
    Glorbo parses no untrusted decimals).
  • Added CI security gates: sobelow (Phoenix SAST, .sobelow-conf with a
    documented architecture-aware ignore list), mix_audit (hex advisory scan).
  • Added OpenSSF Scorecard workflow + README badge, .github/dependabot.yml
    (weekly mix + github-actions updates), and SLSA build-provenance
    attestation
    (actions/attest-build-provenance) on released binaries +
    SHA256SUMS.

Security — revise-feedback path, severity auto-flip, MCP actor provenance (codex B-008/C-062/C-077)

  • B-008: Reviews.write_revise_feedback/5 joined the task's assigned_to value
    (attacker-controllable) into the inbox path with a symlink-following File.dir?.
    It now validates the assignee + task_id slugs and refuses symlinked ancestors.
  • C-062: a string peer_review_required: "true" defeated the severity auto-flip
    (major/critical tasks silently skipped review). The flip now treats only
    boolean/"false" as opt-out, and the parser coerces the string form properly.
  • C-077: an overlong MCP client_name made safe_actor_tag/1 collapse to the
    trusted "director" provenance; it now never collapses an untrusted/overlong actor
    to director (truncates with an mcp: prefix or marks mcp:unknown).

Security — refuse agent-planted symlinks across host-side filesystem walks (codex B-007/C-041/B-016/B-019/B-010/C-037/C-098/C-070/D-146/C-040/C-058)

Several host-side (unsandboxed) code paths walked or read agent-writable trees
without refusing symlinked path components or validating slug/path inputs, so a
sandboxed agent could plant a symlink (or a traversal value) to leak/clobber files
in another company or on the host:

  • Shell Tasks view, brain-dump→task conversion, the opt-in task migrator, paperclip
    import, and the agent-retire walk now refuse symlinked ancestors (via
    AgentWritableFile.any_symlink_in_path? / :file.read_link_info), and the retire
    walk gained a recursion-depth cap.
  • Benchmark fetch/score validate the run_id (no traversal) and route reads through
    the lstat- + size-bounded AgentWritableFile.read.
  • Kanban's assignee→provider lookup validates the assigned_to slug before joining it
    into an AGENT.md path (was a cross-company existence/provider probe).

Security — bound ACP streams + stdout tee against provider-driven DoS (codex C-044/C-048/C-049/C-103)

An untrusted/looping provider could exhaust host resources through the dispatch
path: unbounded ACP frame auditing (one fsynced audit line per frame, untruncated
peer strings → audit-log/disk flood), an ACP stream with only a per-chunk-resetting
read timeout + unbounded in-memory reply accumulation, an unbounded line buffer in
the ACP framer, and a stdout tee that appended every chunk to stdout.log with no
cap. Now: ACP audited frames are capped per dispatch (5 000) with per-frame strings
truncated (256 B); the ACP client enforces an absolute conversation deadline and a
running reply-byte cap that aborts mid-stream; the framer caps line size (16 MiB);
and the stdout tee is capped at 16 MiB/dispatch with a truncation marker. drain_port
now uses a single absolute deadline instead of resetting per chunk.

Security — secret-file permission hardening (codex B-013/C-074/C-075)

Three places could expose secret-bearing files (config.md carries
secret_key_base/dashboard_token/erl_cookie; backups archive them):

  • glorbo fmt --write rewrote config.md through the generic FileSpec formatter,
    which created its temp with umask perms (0644) and renamed over the 0600 file —
    relaxing the secrets file to world-readable. The formatter now preserves the
    original file's mode.
  • The backup archive temp was created 0644 and only chmodded after secret-bearing
    content was written; it is now pre-created 0600 with O_EXCL (also defeating a
    pre-planted-symlink swap).
  • glorbo doctor's private-file check used perms > 0o600, missing group/other-
    readable low modes (e.g. 0o044); it now flags any file with group/other bits set.

Security — Homebrew-tap publish workflow no longer shell-injectable via tag name (codex B-009/C-035)

The publish-homebrew-tap CI job interpolated ${{ steps.version.outputs.version }}
(derived from the pushed git tag GITHUB_REF_NAME) raw and unquoted into run:
shell, so a tag like v1.2.3||id (which matches the v*.*.* trigger) executed
attacker commands on the runner holding HOMEBREW_TAP_TOKEN. The job now validates
the version against a strict semver regex (failing the build otherwise) and passes it
through a quoted env: var instead of template-interpolating it into the shell.

Security — task company-boundary check now collapses ../ traversal (codex D-186/E-201)

TaskDefinition.parse_file/2's company-containment check was a lexical
String.starts_with? on the raw path, so a path like
<base>/companies/<co>/projects/../../other/... still started with the company
prefix yet resolved into a sibling company (or anywhere on the host). The check
now Path.expands the path first, collapsing ./.. before testing
containment, so traversal segments can no longer escape the company tree.
(Symlinked path components were already refused by AgentWritableFile.read/1.)

Security — reserve dm-director-- channel writes to the DM owner (codex B-020, write vector)

Director DMs are stored as regular channels (channels/dm-director--<agent>.md),
and a chat write maps to the permission {"chat","write",<channel>}, so a broad
chat:write:* grant let any agent append to another agent's director DM —
and, via the mention router, notify/wake that victim. The Router now reserves the
dm-director-- prefix: an agent may only write its own dm-director--<sender>
thread; writes to any other agent's DM are refused (:reserved_dm_channel)
regardless of the wildcard. (The read vector — chat:read:* RO-mounts the whole
channels/ dir, exposing all DMs in-sandbox — is a channel-layout issue tracked
separately for a GEP.)

Security — budget enforcement wired into auto-booted agents (codex C-108)

Auto-booted agents (the heartbeat- and inbox-driven production wake path)
were started with no :dispatch_opts, so Dispatch.execute fell back to the
no-op budget_tracker_fun and record_usage_fun defaults: the per-agent
budget gate never fired AND the usage ledger was never written. Because the
company cap sums that same ledger, the SEC-05 pre-dispatch hard stop read
zero spend and never tripped — budget enforcement (a crown-jewel "no runaway
cost" guarantee) was effectively dead for every auto-booted agent.
AgentBoot now threads production dispatch_opts (the real per-company
BudgetTracker check + usage recorder, plus the resolved base) into each
agent's Agent.Server, so budget gating and usage recording are live on the
wake path. Threading base also fixes dispatch reading the wrong filesystem
root under a custom GLORBO_HOME (codex C-114).

Security — auto_dispatch now requires agents:message:<assignee> (codex B-025)

An agent filing an outbox task with auto_dispatch: true caused the Router
to write directly into agents/<assignee>/inbox — the same privileged action
a direct to: agent:<assignee> message performs — but the only authorization
checked was tasks:create/projects:write for filing the task. Since
assigned_to is attacker-controllable frontmatter, a low-privileged agent
could wake any agent and make it process attacker-authored task content
(consuming its budget, acting with its privileges). maybe_auto_dispatch now
gates the inbox write on the same agents:message:<assignee> permission a
direct message requires, emitting a task.auto_dispatch_denied audit on
refusal. Legitimate spawn-and-dispatch flows (where the spawner holds
messaging permission) are unaffected.