WebhookRegistering event exposes:
- register(string $type, array $spec): add a webhook type to the
registry
- types(): array — queryable post-dispatch registry
CoreServiceProvider dispatches the event at app boot and exposes the
collected registry via webhookTypes() — matches the existing
ApiRoutesRegistering / ConsoleBooting / ClientRoutesRegistering
event-driven module pattern.
Pairs with #1034 ofm.bot WebhookRegistrar (just landed) — that
service can now also be wired through this event, allowing OTHER
modules and external apps using Core to register webhook types via
the standard Core lifecycle.
Note: real Core lifecycle dispatcher lives in a sibling read-only
framework checkout. CoreServiceProvider here is a local shim that
mirrors the dispatch behaviour. Upstream patch needed when that
sibling lands.
Pest covers: instantiation + register, boot-time dispatch, post-boot
registry lookup.
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=1013
#1000 was stale-fixed: BrainService::recall() validates filter input
via the shared validator at line 489, which already bounds org,
project, type, agent_id. forget() bounds id at line 499.
These tests pin the safety claim explicitly:
- project=129 chars rejected
- agent_id=65 chars rejected
- project="core" accepted (sanity)
- project=128 chars accepted (boundary)
Note: BrainList.php (separate MCP list path) still lacks explicit
max lengths for project + agent_id — file outside this lane's allow-
list. File a follow-up if that surface needs the same bounds.
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=1000
McpContext exposes the authenticated session's authorisation scopes
via getScopes(): array and hasScope(string): bool.
Resolution order:
1. Explicit scope source passed to constructor
2. Session-like object linked to an API key
3. Authenticated Laravel request context (mcp_workspace_context,
agent_api_key, api_key)
4. Empty array (default) — never null
Dedupes scope strings, normalises separators in hasScope() matching.
Closes the OFM MCP tool gap where scope-gated tools currently return
empty/incorrect handling. No call-site stubs found needing update in
this worktree — call sites pick up the new method directly.
Pest covers: session scopes returned, hasScope present/missing, empty
session defaults to [], request-context regression against real MCP
auth shape.
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=1014
Bound input field sizes against memory/DB/Qdrant bloat (DoS-by-self):
- content: 65536 bytes via mb_strlen
- tags: max 100 entries; each tag max 128 chars
- agent_id, type: 64 chars each
- project, org: 128 chars each
- supersedes_id: ULID-shape only
validateRememberInput() throws InvalidArgumentException at every entry
point (remember, recall, forget) before any DB or upstream call. Field-
specific error messages so callers know which field violated.
Pest covers good-path, content-too-long, tags-array-too-large, tag-
length, exact-boundary cases.
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=1001
remember() now resolves a stale supersedes_id to the current live head
before writing — when X has been superseded by Y, a retried call with
supersedes_id=X automatically links the new memory to Y instead of
silently dropping the supersede.
- Walk the chain from supplied supersedes_id to find the active head
- Cap the walk at depth 100 (cycle/runaway protection)
- Throw RuntimeException("Detected cycle while resolving supersede chain")
on detected cycle, BEFORE any DB write
- Throw InvalidArgumentException("Superseded memory not found") when
the original supersedes_id never existed
- deleteSupersededMemory no longer silently no-ops once the resolved
head is expected to exist
Pest coverage extended:
- Direct chain link (X exists, succeeds with X→linked)
- Retry path (X→Y, then retry on X produces Z→Y, walks chain)
- Never-existed target (graceful error)
- Synthetic X↔Y cycle (caps walk + throws, no writes leak)
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=316
remember(), recall(), forget(), and elasticSearch() now resolve the
allowed-orgs set from the authenticated request context (mcp_workspace_context),
preferring explicit authorised_orgs/authorized_orgs, falling back to the
authenticated workspace's org/slug. A mismatched org throws
AuthorizationException BEFORE any Qdrant/Elasticsearch call or destructive
DB action — closes the horizontal-priv-escalation vector where an MCP
client could recall/remember/forget memories scoped to ANY org by
setting org="other-org" in the request body.
Pest coverage in OrgScopingTest covers good path, unauthorised recall
(asserts no HTTP), cross-org forget (asserts no DB delete), unauthorised
remember (asserts no embed/index jobs).
Note: BrainList free-form org filter is a separate ticket — outside this
lane's allowlist.
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=312
retryableHttp() now retries only 408 (Request Timeout), 429 (Too Many
Requests), and 503 (Service Unavailable). 500-and-other-5xx fail
immediately so the circuit-breaker registers them as a single
failure rather than smearing across retry attempts. Retry-After
honoured (numeric + HTTP-date), capped reasonably.
Attempt budget bumped to 6 so a burst of 5 transient 503s can recover
within ONE circuit-permitted call — the original concern from #311.
Note: CircuitBreaker is already applied OUTSIDE the logical Brain
operation by the MCP tool layer, not around each HTTP retry. The
nesting report was stale at this code shape; the real drift was the
retryableHttp() retry set + budget.
Pest coverage in CircuitBreakerTest:
- Recovered 503 burst → circuit stays closed, no failure registered
- Exhausted 503 burst → ONE breaker failure (not five)
- 429 + Retry-After 1 → sleeps 1s, no breaker failure
- 500 → immediate breaker failure, no retry
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=311
Cache::lock keyed by memory id wraps the delete path in BrainService::
forget(); supersede cleanup in remember() lifted to the same idiom.
forget() now ALWAYS queues DeleteFromIndex on a successful delete
(was previously skipped when indexed_at was null — left late writes
from stale preloaded models a window to land entries after the
underlying memory was gone).
Index write paths (qdrantUpsert / elasticIndex) re-check that the
memory row still exists before writing — defence-in-depth against any
future caller that holds a stale model reference past a forget.
Pest coverage extended in SupersedeForgetIndexCleanupTest:
- never-indexed forget queues cleanup
- late stale-model index writes are skipped after forget
- never-indexed supersede cleanup queues deletion
- late stale-model index writes are skipped after supersede
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=999
The .gitleaksignore file uses per-commit fingerprints which only match
gitleaks's default with-git mode. Developers running `gitleaks detect --no-git`
locally (e.g. for working-tree review) saw 7 false positives that the
fingerprint format couldn't address.
This .gitleaks.toml adds path-based allowlists that apply to BOTH modes,
covering the same documented placeholder/test/example sites the .gitleaksignore
covers in history form:
- .core/vm/ Traefik cert keys (mode 0600, untracked, generated for local VM)
- php/docs/ + blade.php API placeholders
- php/tests/ test fixtures
- php/Services/AgentDetection.php docblock examples
- pkg/agentic/prep_test.go t.Setenv env-clearing literal
Verified: `gitleaks detect --no-git -c .gitleaks.toml` returns "no leaks found".
Default `gitleaks detect` (with-git) still uses .gitleaksignore + this config
together — both modes now report 0 leaks for the documented false positives.
Co-authored-by: Codex <noreply@openai.com>
Mantis #324 narrowly tightened fs.go from 0644/0755→0600/0700. Athena audit
during task #20 closure-verification (2026-04-25) found sibling files in the
same directory still using 0755 for MkdirAll, leaving parent dirs world-listable
even when file content is 0600.
This commit applies the same hardening to:
- .core/reference/error.go:393 — crash-report parent dir 0755→0700
- .core/reference/embed.go:514/567/656 — workspace template extract dirs 0755→0700
- .core/reference/embed.go:595/660 — os.Create→os.OpenFile(...0600) for
template renders + standard-file copies (default umask 0644 was leaking
workspace-template content to other users on shared hosts)
- pkg/lib/workspace/default/.core/reference/error.go:414 — same crash-report fix
- pkg/lib/workspace/default/.core/reference/embed.go:518/571/660 — same template fixes
Workspace-template duplicates are kept in sync so newly-scaffolded workspaces
inherit the hardened perms instead of regressing to 0755/0644.
Closes Mantis #988.
Co-authored-by: Codex <noreply@openai.com>
Removed local sync/atomic + crypto/rand + encoding/hex based plan ID
generator from pkg/agentic/plan.go. Switched planID() to core.ID()
primitive. Preserves id-{counter}-{suffix} shape via Core's primitive.
prep.go, sync.go, pkg/brain/*.go scanned — no sync imports remained
in non-test files.
Closes tasks.lthn.sh/view.php?id=863
Co-authored-by: Codex <noreply@openai.com>
Additive-only — appended to php/Routes/api.php (existing routes
preserved). Existing /v1/fleet/{nodes,heartbeat,stats} +
/v1/agent/auth/provision left untouched.
New routes:
- /v1/agent/auth/register
- /v1/fleet/dispatch + /v1/fleet/stream
- /v1/credits/{balance,deduct,refund,ledger}
- /v1/subscription/{status,upgrade,cancel}
- /v1/agent/sync/{push,pull}
New controllers under php/Controllers/Api/{Fleet,Credits,Subscription,
Sync,AgentAuth}/. Reference FleetService/CreditService/SessionService
when available with fallbacks to current action/model layer (pre #849).
Pest Feature coverage under php/tests/Feature/Api/. pest skipped
(vendor binaries missing in sandbox).
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=848
Additive-only — no existing files modified.
- FleetOverview: node list + status badges + dispatch button + stats panel
- BrainExplorer: semantic-recall search with DB fallback + forget action
- CreditLedger: balance display + transaction list + deduct/refund actions
Flux Pro components (no vanilla Alpine). Uses existing
fleet/brain/credit actions+services in this package.
Pest Feature tests _Good/_Bad/_Ugly per AX-10 — load classes directly
since composer.json + Boot.php were left untouched per scope. Future
follow-up: wire PSR-4 + view registration in Boot.php.
pest skipped (vendor binaries missing in sandbox).
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=850
.core/reference/fs.go (canonical) + pkg/lib/workspace/default/.core/reference/fs.go (embedded copy):
- Write/WriteAtomic/Create/Append default to 0600
- Parent directories use 0700 (was 0755)
- WriteMode reapplies the requested mode after writes so overwriting an
existing file also tightens permissions
Test (pkg/lib/lib_test.go) keeps embedded fs.go synced with canonical +
asserts extracted workspaces carry the secure permission defaults.
tests/cli/extract copy not hand-edited — that flows from regeneration.
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=324
Closes Mantis #325 (agent portion).
Each fingerprint listed is a documentation placeholder, test constant, or
env-clearing call manually verified to be safe — not a real secret. The
fingerprint format anchors per-commit so a future legitimate leak in the
same file/rule will still be caught.
Categories:
- pkg/agentic/prep_test.go — t.Setenv("CORE_BRAIN_KEY", "") env-clear
- pkg/orchestrator/security_test.go — MaskToken test fixture
- php/docs/api-keys.md — curl-auth-header documentation example
- php/View/Blade/admin/api-key-manager.blade.php — same
- php/tests/Unit/ClaudeServiceTest.php — 'test-api-key' literal
- php/tests/Feature/AgentApiKeyTest.php — 'ak_test_key_*' fixture
- php/Services/AgentDetection.php — docblock example
- src/php/* — older path of same files (pre-migration commits)
Verification: gitleaks detect → 19 → 0 findings.
Co-Authored-By: Argus <argus@lthn.ai>
Co-Authored-By: Athena <athena@lthn.ai>
Co-Authored-By: Virgil <virgil@lethean.io>
Closes Mantis #863 ([agent] Phase 2: purge sync stdlib).
Per RFC plans/code/core/go/RFC.primitives-lifecycle.md §14A (landed core/go
dev 8995a80), swaps the four sync.Once usages to core.Once and the two
sync.Once{} reset-pattern callsites to core.Once.Reset():
pkg/agentic/statestore.go:
- Drop `import "sync"`.
- stateStoreRef.once: sync.Once → core.Once
- closeStateStore reset: `s.stateOnce = sync.Once{}` → `s.stateOnce.Reset()`
pkg/agentic/workspace_stats.go:
- Drop `import "sync"`.
- workspaceStatsRef.once: sync.Once → core.Once
- closeWorkspaceStatsStore reset: `s.workspaceStatsOnce = sync.Once{}` →
`s.workspaceStatsOnce.Reset()`
pkg/agentic/prep.go:
- Drop `import "sync"`.
- PrepSubsystem.stateOnce + .workspaceStatsOnce: sync.Once → core.Once
The Reset() pattern matches stdlib semantics (see RFC §14A "Tradeoff: Once.
Reset semantics") — caller serialises via the existing closeStateStore /
closeWorkspaceStatsStore structure that nests Reset inside the lifecycle
inverse, so no concurrent Do races are introduced.
Net: 3 files, +7/-11. Mechanical line-edit per RFC §16 migration plan.
Audit re-check post-commit:
grep -n '"sync"\|sync\.Once\|sync\.Mutex' pkg/agentic/{statestore,workspace_stats,prep}.go
→ empty (lib local variable named `sync` in mirror.go is unrelated; not
in scope of this ticket).
Pre-flight verification: core.Once + Reset symbols verified present on
core/go dev 8995a80. Local AX-10 build blocked by the same pre-existing
workspace forge dep break that affects all consumers (root cause: fake
v0.8.0-alpha.1 pins per task #28); CI in healthy env will validate.
Co-Authored-By: Athena <athena@lthn.ai>
Co-Authored-By: Virgil <virgil@lethean.io>
Closes the 5 PARTIAL items flagged in docs/AUDIT-openbrain-20260424.md.
- Gap A (org scoping persisted on writes): new migration adds `org`
nullable+indexed column to brain_memories; BrainMemory fillable;
RememberKnowledge action forwards org; BrainService::remember
persists it.
- Gap B (supersede/forget Elastic cleanup): BrainService::forget
dispatches DeleteFromIndex (handles both Qdrant + Elastic); supersede
path dispatches cleanup for the old memory id before replacing it.
DeleteFromIndex itself untouched — already handled both indexes.
- Gap C (brain:reindex flags): --org, --project, --stale (null OR
>14d old), --dry-run (count+stop), --elastic-only added to the
artisan command.
- Gap D (MCP schemas expose org): brain_remember, brain_recall,
brain_list now accept `org` in input schema + forward into
action/service.
- Gap E (resilience uneven): brain_list now wrapped in
withCircuitBreaker('brain', ...) matching the pattern used by
BrainRemember/Recall/Forget. BrainService gains retryableHttp()
helper — 100/300/900ms exponential backoff, retries only on 5xx +
connection errors, not on 4xx. Qdrant calls route through it;
Ollama left alone (EmbedMemory job has its own retry).
Tests (Good/Bad/Ugly per gap):
- Feature/Brain/OrgScopingTest.php
- Feature/Brain/SupersedeForgetIndexCleanupTest.php
- Feature/Brain/ReindexFlagsTest.php
- Feature/Mcp/BrainSchemaOrgTest.php
- Feature/Brain/CircuitBreakerTest.php
php -l clean on all 13 files. Pest binary not in this checkout —
CI path validates the full suite.
Closes tasks.lthn.sh/view.php?id=107
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
BrainService::http() was building a PendingRequest with no auth
header, so when Qdrant has auth enabled (the production lthn.sh
deploy does) every upsert/lookup returned 401. The circuit breaker
logged the 401 via Cache::store('file'), which was the red-herring
cache-write error chased in the first #97 iteration.
Changes:
- BrainService loads + trims a Qdrant api key from
config('brain.qdrant.api_key') in the constructor.
- New qdrantHttp() helper returns a PendingRequest with the
api-key header when the key is non-empty, or the plain client
otherwise. Ollama + Elasticsearch call sites still use http()
(separate auth shapes).
- php/config.php adds a brain.qdrant.api_key entry reading
env('BRAIN_QDRANT_API_KEY').
- Good/Bad/Ugly Pest tests cover: configured key → header sent,
unset → header absent, empty-string → header absent.
Closes tasks.lthn.sh/view.php?id=97
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
Exercises the 3 MCP handlers that work MariaDB-only (no Qdrant
dependency): brain_remember writes + returns id, brain_list
surfaces it, brain_forget removes. Negative case: brain_forget on
a non-existent id returns a proper error response (not TypeError).
brain_recall is out of scope — needs the Qdrant collection +
embedding pipeline.
Implementation note: handlers use `type` + workspace context for
scoping, not a `scope` parameter; the test matches the actual
signatures.
Closes tasks.lthn.sh/view.php?id=96
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
Replaced both docs/php-agent/RFC.openbrain-{design,impl}.md with
~12-line redirect pointers to plans/project/lthn/ai/RFC-OPENBRAIN.md
(the 728-line authoritative spec). Agents dropped into core/agent
will no longer implement the obsolete single-collection /
nomic-embed-text / synchronous model.
Closes tasks.lthn.sh/view.php?id=53
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
Fleet registration in pkg/agentic already goes through the shared
&http.Client{Timeout: 30s} at transport.go:13 — no InsecureSkipVerify,
no custom TLS transport. This audit documents that finding and adds
regression coverage so future refactors can't silently strip TLS
validation from the /v1/fleet/register path.
Verdict: OK. No production bug. Tests pass trusted TLS server case
and reject untrusted cert with a wrapped error that surfaces the
certificate / x509 / tls signal in the message.
Closes tasks.lthn.sh/view.php?id=29
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
Fleet tasks deliberately do not create AgentSession records.
AgentSession's work_log / artefacts / handoff / replay semantics are
designed for interactive, replayable, handoff-capable work — fleet
tasks are atomic assign→complete events with no in-between state
to replay. If a fleet-task handler needs session semantics, it
should start its own AgentSession via AgentSessionService when the
work begins.
Closes tasks.lthn.sh/view.php?id=94
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
Rename legacy dappcore/dAppCore/dappco.re plugin-metadata identifiers
to the canonical core namespace: marketplace name is core-agent,
plugin manifests point at https://lthn.ai and
https://forge.lthn.ai/core/agent.git. Align the two .mcp.json files
on the new "core mcp serve" command. Scaffold google/gemini-cli with
a minimal plugin.json + README so the cross-agent plugin family is
complete.
Out of this ticket's scope: live Go module import paths at
dappco.re/go/core (that's a separate migration), and a stale pip
install URL in claude/camofox_mcp/README.md (follow-up child ticket).
Closes tasks.lthn.sh/view.php?id=92
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
Creates three new Claude plugin family directories per the
plugin-restructure RFC: core-go (Go-family tooling), core-php
(PHP-family tooling), infra (ops / devops / deploy tooling). Each
carries .claude-plugin/plugin.json, YAML marketplace.yaml, README,
and commands/agents/skills stubs. Marketplace format is YAML per
RFC; the legacy JSON marketplace for core-agent is left untouched.
Appends a Resolution section to the plugin-restructure RFC recording
the YAML-over-JSON decision and noting that the dappcore→core rename
at cross-plugin metadata level is scoped to #92.
Closes tasks.lthn.sh/view.php?id=91
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
ScanForWork and ManagePullRequest now depend on the MetaReader
interface (added in #89) instead of reading raw Forgejo body /
description / PR text. Epic child-linkage comes from
EpicMeta.children, PR merge decisions come from PRMeta.state /
mergeability / checkStatuses. The returned shape drops issue_body
and replaces it with structural issue_state / issue_labels.
Adds a feature test that injects a mocked MetaReader carrying
intentionally-tainted body/description/review_text fields and
recursively asserts none of those keys appear in the output of
either action — the regression fence for the RFC rule that body
content must never reach pipeline decisions.
Closes tasks.lthn.sh/view.php?id=90
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
AgentSession::addArtifact expects ?array $metadata in the third
argument slot; the MCP tool was passing the optional description
string directly, producing a TypeError whenever a caller supplied a
non-null description. Wrap the description into a metadata array so
the call matches the model signature, and add a feature test that
exercises the MCP handler end-to-end to prevent regression.
Closes tasks.lthn.sh/view.php?id=95
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
Extend PushDispatchHistory so /v1/agent/sync writes four sync.*
workflow-progress keys into WorkspaceState (last_dispatch_at,
last_agent_type, last_findings_count, last_status) in addition to the
existing BrainMemory + SyncRecord persistence. Plan resolves via
agent_plan_id first, plan_slug fallback. Missing plan is treated as
non-fatal — state writes are skipped, BrainMemory still persists.
Adds a three-case feature test covering direct id, slug fallback, and
the missing-plan safety branch.
Closes tasks.lthn.sh/view.php?id=93
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
Introduce a pipeline metadata surface that enforces "no body content
ever reaches pipeline decisions". MetaReader is an interface with four
methods (getPRMeta, getEpicMeta, getIssueState, getCommentReactions),
each returning a readonly DTO carrying only structural fields —
state, mergeability, SHAs, branches, reaction counts, child linkage.
ForgejoMetaReader projects raw Forgejo API payloads into these DTOs
and drops body/description/review text before the caller can see it.
Unit test mocks rich Forgejo payloads containing body, description,
review_text, and comment_body, then asserts the DTO toArray output
never exposes those keys — the regression fence for the RFC rule.
Downstream callers (ScanForWork, ManagePullRequest) still use the
raw ForgejoService today; that refactor lands under Mantis #90.
Closes tasks.lthn.sh/view.php?id=89
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
Python MCP server that dispatches sandboxed Hermes runs from Claude Code.
Exposes hermes_dispatch / hermes_status / hermes_fetch as MCP tools so
Cladius can offload work to Hermes runners via `claude mcp add hermes-runner`.
Passes --agents JSON through for dynamic subagent composition.
Closes tasks.lthn.sh/view.php?id=78
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
Python MCP server wrapping the Camofox browser HTTP API on lthn.sh.
Exposes navigate/read_page/screenshot/click/fill/close_tab as MCP tools
so any Claude Code session can drive Camofox via `claude mcp add camofox`.
Closes tasks.lthn.sh/view.php?id=77
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
Two Hermes skill files that auto-register OpenBrain memory tools when
the MemoryProvider plugin loads. Each names the triggering phrases,
the tool contract, and an example invocation so Hermes can route
recall/remember prompts without coaching.
Closes tasks.lthn.sh/view.php?id=75
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
Python plugin implementing Hermes ContextEngine backed by OpenBrain.
compress() does centrality-ranked retrieval over a candidate pool
pulled via brain_recall rather than linear turn truncation. Falls
back to naive head+tail truncation when recall is unavailable so the
caller never sees a raised exception.
Closes tasks.lthn.sh/view.php?id=74
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>