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>
After the e2d1d32 → revert cycle the code moved back to php/ but
composer.json autoload paths stayed pointing at src/php/ (which
does not exist). package:discover fails with "Class
Core\\Mod\\Agentic\\Boot not found" as soon as a downstream
consumer composer-installs the dev branch.
Co-Authored-By: Virgil <virgil@lethean.io>
New endpoint GET /v1/brain/search?q=<query>&org=<>&project=<>&limit=<N>
that full-text-searches brain_memories via Elasticsearch using
BrainService::elasticSearch(). Separate from /v1/brain/recall (which is
vector/semantic via Qdrant) — this one is keyword/lexical.
Sits under the existing brain.read-auth middleware group.
Pest coverage (Http::fake for ES): Good (matches), Bad (invalid limit),
Ugly (empty query + filters).
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=64
Two introspection endpoints for OpenBrain:
- GET /v1/brain/tags — ES terms aggregation over tags.keyword, returns
{tag: count} pairs for UI filter chips
- GET /v1/brain/scopes — composite aggregation over {org, project},
returns the scope hierarchy present in the index
Sits under the existing brain.read-auth group in Routes/api.php. New
BrainService helpers for aggregation shape; reuses the elasticSearch
HTTP path added in #59.
Pest coverage with Http::fake for ES.
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=65
New artisan command brain:prune {--older-than=90} {--chunk=100} {--dry-run}
that completes the soft-delete → hard-delete lifecycle by:
1. selecting BrainMemory::onlyTrashed() where deleted_at < now - N days
2. dispatching DeleteFromIndex for each (Qdrant + ES cleanup)
3. forceDelete()'ing the rows
--dry-run counts without dispatching.
Complements brain:clean (which cleans recent soft-deletes) with a
retention-bounded terminal cleanup.
Pest coverage: Good (dispatch + forceDelete on aged trashed rows), Bad
(invalid chunk), Ugly (--dry-run skips both dispatch and delete).
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=62
New artisan command brain:clean {--chunk=100} {--dry-run} that dispatches
the DeleteFromIndex job for soft-deleted BrainMemory rows (those in
onlyTrashed scope). Cleans up orphaned Qdrant + Elasticsearch index
entries that remain after a memory is soft-deleted.
--dry-run counts without dispatching.
php/tests/Feature/Console/BrainCleanCommandTest.php covers Good
(dispatches on trashed), Bad (invalid chunk), Ugly (--dry-run prevents
dispatch).
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=61
remember() now writes the brain_memories row with indexed_at=null and
dispatches EmbedMemory::dispatch($memory->id) for async Qdrant + ES
indexing, instead of calling qdrantUpsert() synchronously. Response shape
matches the row state — caller gets the memory immediately, the Job
flips indexed_at once the Qdrant write succeeds.
Superseded rows still soft-delete synchronously (part of the remember
contract, not the indexing path).
php/tests/Feature/Services/BrainServiceRememberTest.php uses Queue::fake()
to assert EmbedMemory is dispatched and BrainService::qdrantUpsert() is
NOT called directly (subclass probe).
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=55
New artisan command brain:reindex {--all} {--chunk=100} that dispatches
the EmbedMemory job for brain memories needing (re)indexing. Without
--all, only memories where indexed_at IS NULL are dispatched; --all
re-embeds every memory (useful after a Qdrant collection wipe or
embedding model change). Uses chunkById for memory-safe iteration at
scale.
php/tests/Feature/Console/BrainReindexCommandTest.php covers Good
(unindexed-only default), Bad (invalid chunk), Ugly (--all flag).
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=60
Fills in the elasticIndex/elasticDelete stubs added by #56 and #57, plus a
new elasticSearch() method used by the upcoming /v1/brain/search endpoint
(#64).
- elasticIndex(BrainMemory) → PUT /brain_memories/_doc/{id}
- elasticDelete(string $id) → DELETE /brain_memories/_doc/{id}
- elasticSearch(string $query, array $filters) → POST /brain_memories/_search
- ES URL default http://127.0.0.1:9200 (config override via
BRAIN_ELASTICSEARCH_URL env var)
- RuntimeException on HTTP failures (same pattern as qdrantUpsert)
php/tests/Feature/Services/BrainServiceElasticTest.php covers Good/Bad/Ugly
for index, delete, and search using Http::fake.
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=59
Inverse of the EmbedMemory job (#56): removes a memory from Qdrant (and
the future Elasticsearch index) when brain_forget fires or a memory is
soft-deleted.
- php/Jobs/DeleteFromIndex.php — Laravel Job, 3 retries with backoff
- BrainService: qdrantDelete() private→public and now throws on HTTP
failure (was silent Log::warning — wouldn't trigger Job retry)
- elasticDelete() stub added (fills in with the ES integration ticket)
- php/tests/Feature/Jobs/DeleteFromIndexTest.php — success + HTTP-failure
paths via mocked Http
Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=57