Commit Graph
1169 Commits
Author SHA1 Message Date
viettranx 20de0e332e feat(feishu): add /addwriter /removewriter /writers commands
Adds parity with Telegram and Discord for file-writer management
commands, closing the UX gap where users saw an error mentioning
/addwriter but the Feishu channel had no handler.

- New maybeHandleWriterCommand routes /addwriter, /removewriter,
  /writers from the Feishu inbound flow. Runs at step 5a — after
  checkGroupPolicy — so commands never bypass allowlist or pairing
  enforcement. Step 2a rejects slash commands in DM chats early so
  users get a clear hint without waking the agent pipeline.
- Target user is identified via reply-to (fetches parent message
  sender) or first non-bot @mention. A bare /addwriter with no
  target shows the usage hint instead of silently self-granting,
  preventing accidental privilege capture in empty-writer groups.
- Refuses to run while botOpenID is unresolved so a @mention of
  the bot itself cannot be mistaken for a human target.
- 10s context timeout on each handler bounds worst-case Lark API
  latency (parent message lookup, permission store access).
- feishu.New() gains variadic Option parameter with WithAgentStore
  and WithConfigPermStore mirroring Telegram's pattern. The gateway
  now wires pgStores.Agents and pgStores.ConfigPermissions into
  Feishu channel on startup.
- 12 new unit tests with fakeConfigPermStore and httptest Lark
  server cover DM rejection, nil-store graceful degradation, bootstrap
  via self-mention, bare-command usage hint, bot-probe race refusal,
  non-writer rejection, grant via mention, remove last-writer guard,
  empty and populated list output, reply-to target resolution via
  Lark im/v1/messages lookup, and non-command passthrough. 40 total
  tests in the feishu package, all green with -race.

Closes #818.
2026-04-11 21:45:37 +07:00
viettranx 934ab7e485 fix(media): route claude-cli PDFs as document blocks, scope disable_tools
read_image and read_document failed when only claude-cli was configured
because the fallback chains only listed 'anthropic'. Adding claude-cli
as fallback (PR #802) surfaced a deeper bug: buildStreamJSONInput
hardcoded 'type: image' for every content block, so PDFs routed
through claude-cli were rejected by the Anthropic API with a MIME
mismatch.

- providers/claude_cli_session.go: buildStreamJSONInput now picks the
  Anthropic block type from MIME — application/pdf -> document,
  image/* -> image.
- tools/read_image.go, read_document.go: add claude-cli to the
  fallback priority and model defaults (empty string lets the provider
  pick its own default model).
- tools/read_image.go, read_document_resolve.go: scope disable_tools to
  claude-cli only instead of leaking a CLI-specific flag into the
  shared Options map every provider in the chain receives.
- providers/claude_cli_session_test.go: table test covering png/pdf/
  mixed/unknown MIME routing plus the empty-text edge case.

Closes #801.
2026-04-11 21:39:47 +07:00
viettranx abc6d62c95 test(providers,zalo): wave A coverage push
Push two deferred packages above their coverage floors:
- internal/providers 57.0% -> 62.2% (+5.2%, target ≥60%)
  Targeted pure-logic adapter transformation code: registry,
  openai/dashscope/codex ToRequest/FromResponse/FromStreamChunk,
  Azure header branch, token source propagation, function_call +
  incomplete + reasoning parse branches.

- internal/channels/zalo 7.2% -> 65.3% (+58.1%, target ≥20%)
  Factory table-driven creds/config cases, New() defaults, callAPI
  success/error/malformed, getMe/getUpdates, sendMessage/sendPhoto,
  Send routing + photo extraction, Stop signal, processUpdate
  dispatch, handleTextMessage empty-sender drop, downloadMedia four
  branches (png/jpg fallback/404/empty).

Minimal refactor: converted package-level apiBase const to var in
internal/channels/zalo/zalo.go so tests can point at httptest server.
Zero runtime behavior change; tests restore the original value via
t.Cleanup.

Ratchet bumps (scripts/coverage_thresholds.json):
- internal/providers: 57.00 -> 62.15
- internal/channels/zalo: 7.20 -> 65.25

Race clean, dual build (default + -tags sqliteonly) clean,
personal/ sub-packages untouched.
2026-04-11 21:29:32 +07:00
viettranx 0ef3b5cd03 fix(backup): surface Detail alongside Hint in preflight warnings
Missing-status checks previously only surfaced Hint to users, leaving
them with a fix (Install postgresqlNN-client) but no explanation of the
cause (pg_dump NN cannot dump PostgreSQL MM server). Now both warning
and missing statuses append Detail before Hint.

Follow-up to #830.
2026-04-11 21:28:22 +07:00
viettranx 1bdd291fac feat(feishu): auto-fetch Lark docx URLs into agent context
When a user pastes a Lark or Feishu docx URL in chat, the channel
now detects the URL and fetches the document's raw text via the
Lark Docs API, injecting the content into the agent input inline
so the model can reason over the linked doc without a tool call.

- New Channel.resolveLarkDocs pipeline step runs before reply
  context fetch in handleMessageEvent (step 7a)
- LarkClient.GetDocRawContent calls /open-apis/docx/v1/documents
  /{id}/raw_content with the existing tenant access token;
  permission and not-found errors map to ErrDocAccessDenied
- Per-channel LRU cache (128 entries, 5 min TTL) dedupes repeat
  URL references within the window; soft failures are NOT cached
  so permission grants become visible immediately
- Rune-safe content truncation at 8000 runes handles CJK docs
  without splitting mid-rune
- Bounded concurrency (max 3 parallel fetches) and a per-message
  cap of 10 doc URLs act as spam guards
- Tight URL regex anchors the hostname class so a lazy match
  cannot bypass via query-string embedding
- 18 new unit tests cover URL extraction edge cases, cache LRU
  and TTL semantics, Lark API error code mapping, resolver
  end-to-end with nil cache, access denied soft failure,
  per-message cap, and UTF-8 truncation

Required Lark app permission: docx:document:readonly, plus
per-document access grant from the doc owner.

Partial fix for #818 (Phase 2 of 3 — thread reply + writer
commands are tracked separately).
2026-04-11 21:26:12 +07:00
viettranx bf272d8dbf fix(feishu): route thread replies via Lark reply endpoint
Bot responses to messages inside Lark topic threads were dropped
outside the thread because outbound Send always used the new-message
endpoint. This change:

- Adds LarkClient.ReplyMessage() that POSTs to
  /open-apis/im/v1/messages/{id}/reply with reply_in_thread=true
- Parses thread_id from im.message.receive_v1 events (distinct from
  root_id which fires on any quote reply) and stamps
  feishu_reply_target_id into the message metadata
- Propagates the key through cmd/gateway_consumer_normal.go and the
  new package-level routingMetaKeys var in internal/channels/events.go
  so block replies and retry notifications also land in thread
- Routes sendText, sendMarkdownCard, sendImage, sendFile, and
  sendMediaAttachment via a new deliverMessage helper that falls back
  to SendMessage with a warning log on reply endpoint errors (e.g.
  thread root deleted)
- Adds 10 unit tests covering routing, fallback, content
  double-encoding, and the thread_id gate that prevents plain quote
  replies from being silently promoted to threads

Closes #818
2026-04-11 21:22:23 +07:00
viettranx e8e00d1884 fix(telegram): preserve italic content in htmlTagToMarkdown
Go regexp's $1_ is parsed as a reference to a named group "1_" (underscores are
valid identifier chars), not $1 followed by literal underscore. This dropped the
captured italic content, turning <i>foo</i> into "__" instead of "_foo_".

Use ${1}_ with explicit braces to delimit the group reference, and update the
format_extended_test suite to assert the full round-trip for <i> and <em>.
2026-04-11 21:22:23 +07:00
viettranx eaddc68796 docs: changelog entry for coverage improvement waves
Document test coverage improvement initiative:
- 43 new test files across 3 waves
- Per-package coverage floors in coverage_thresholds.json
- CI ratchet gate prevents regression
- ~9000 lines of new test code, 61 packages covered
2026-04-11 21:22:23 +07:00
viettranx 83a6b3c68f test(cache,sessions,knowledgegraph): slim wave 3 coverage push
Add Wave 3 package coverage:
- cache/permission_cache_test.go: PermissionCache 9 methods + invalidation
- sessions/key_extra_test.go: session key builders
- sessions/manager_extra_test.go: SetHistory, Save, loadAll
- knowledgegraph/extractor_helpers_test.go: Extract with mock provider, splitChunks, mergeResults
2026-04-11 21:22:23 +07:00
viettranx bdbed9a35e test(channels): extend slack/discord/telegram/whatsapp coverage
Add channel integration handler tests:
- slack/mention_test.go: isBotMentioned, stripBotMention, user cache, sweepMaps
- slack/media_test.go: classifyMime, buildMediaTags
- discord/handler_test.go: resolveDisplayName, tryHandleCommand, classifyMediaType
- telegram/format_extended_test.go: markdown→HTML, table rendering, chunkHTML
- telegram/handlers_utils_test.go: detectMention, hasOtherMention, isServiceMessage
- telegram/channel_parse_test.go: parseChatID, isEnabled, effectiveMentionMode
- whatsapp/inbound_test.go: extractTextContent, extractQuotedText
- whatsapp/chunk_text_test.go: chunkText, markdownToWhatsApp
- whatsapp/media_utils_test.go: mimeToExt, classifyDownloadError
2026-04-11 21:22:23 +07:00
viettranx a8f0f05600 test(config,skills,backup,mcp): wave 2 integration tests
Add Wave 2 package coverage:
- config/config_extras_test.go: NormalizeAgentID, ExpandHome, ResolveAgent, env overlays
- skills/search_bm25_test.go: BM25 index/search/ranking
- skills/loader_test.go: SKILL.md loader, frontmatter, dedup, versioning
- backup/fs_archive_test.go: ArchiveDirectory, skip fn, tar prefix
- backup/manifest_dsn_test.go: SanitizeDSN, ParseDSN, WritePgpass, CleanEnv
- mcp/util_bm25_test.go: pool, manager, bridge BM25, env resolution
2026-04-11 21:22:23 +07:00
viettranx e39e97ee1b test(tasks,agent): cover TaskTicker and agent helpers
Add lifecycle and utility function tests:
- tasks/task_ticker_test.go: TaskTicker lifecycle, recoverAll, followup
- agent/pruning_test.go: resolvePruningSettings, findAssistantCutoff, takeHead/Tail
- agent/extractive_memory_test.go: ExtractiveMemoryFallback, dedup
- agent/intent_classify_test.go: quickClassify, containsWholeWord, ClassifyIntent
- agent/loop_utils_test.go: uniquifyToolCallIDs, shouldShareKG, InvalidateUserWorkspace
- agent/inject_and_misc_test.go: truncateForLog, processInjectedMessage, drainInjectChannel
2026-04-11 21:22:23 +07:00
viettranx 0494721af2 test(gateway,http): add unit tests for server/methods/handlers
Add server and RPC handler tests:
- gateway/ratelimit_test.go: rate limiter pure unit
- gateway/event_filter_test.go: event routing logic
- gateway/server_test.go: handleHealth, tokenAuth, checkOrigin, desktopCORS
- gateway/methods/sessions_test.go: sessions RPC handlers
- gateway/methods/skills_test.go: skills RPC handlers
- gateway/methods/cron_test.go: cron RPC handlers
- http/files_path_security_test.go: path traversal, workspace boundary, auth
- http/auth_helpers_test.go: extractBearerToken, tokenMatch, extractUserID/AgentID
2026-04-11 21:22:23 +07:00
viettranx 509286a8ad test(store/pg): deepen v3 integration tests — Wave 1
Add comprehensive integration tests for core v3 store APIs:
- v3_session_store_ext_test.go: pagination, isolation, rich-list
- v3_agent_store_ext_test.go: context files, profiles, overrides, types
- v3_agent_links_store_test.go: delegation links CRUD
- v3_cron_store_ext_test.go: cron CRUD, enable toggle, run log
- v3_vault_store_ext_test.go: vault docs CRUD/search/links
- v3_memory_store_ext_test.go: memory BM25 search/isolation
2026-04-11 21:22:23 +07:00
viettranx 27c9415193 build(ci): add coverage ratchet gate with per-package floors
Add automated coverage floor validation:
- scripts/check_coverage.go: parses coverage.out, compares to per-package
  thresholds, supports --update to lock current floors
- scripts/coverage_thresholds.json: 61 packages, initial floors from Phase 1
- .github/workflows/ci.yaml: new 'Coverage ratchet gate' step in CI
- internal/testutil/: test utilities (context builders, TestDB, doc.go)
2026-04-11 21:22:23 +07:00
viettranx eb2419beed fix(tests): align sqlite smoke test vault AgentID pointer type 2026-04-11 21:22:23 +07:00
viettranx 8ef11d0cbe feat(ui): vault document docType + metadata support
- Update vault.ts: add VaultDocument union type, metadata field in VaultLink
- Update vault-graph-adapter.ts: map document nodes to green color node class
- Update vault-detail-dialog.tsx: handle isBinary branch for download vs preview
- Update vault-document-sidebar.tsx: filter by type with document type icon + label
- Update vault-documents-table.tsx: render document type badge + path_basename column
- Add i18n translations: type.document + type.document_desc across en/vi/zh
2026-04-11 21:22:23 +07:00
viettranx e62c027b7d feat(vault): task + delegation auto-linking in enrich worker
- Add enrich_auto_linking.go: deterministic auto-link logic for task/delegation contexts
- Add team_task_siblings.go (PG + SQLite): find all tasks in same delegation for batch link
- Update enrich_worker.go: call enrich_auto_linking Phase 2 hook + skip re-embed for binary
- Update team_store.go: add TaskSiblings interface method + TeamStore binding
- Add context_keys.go + test: define ContextDelegationIDKey + ContextTaskIDKey for propagation
- Update vault_interceptor.go: extract + inject delegation ID from request
- Update run_context.go: propagate DelegationID from request
- Update loop_context.go: inject DelegationID from run context
- Update teams_tasks.go: DeleteTask(s) → cleanup auto-links via vault_source_cleanup
- Update teams_tasks_activity.go: DetachFileFromTask → cleanup related auto-links
- Update vault_documents_enrichment.go: populate summary from media_summary
- Update cmd/gateway.go: wire TeamStore to handler bootstrap
2026-04-11 21:22:23 +07:00
viettranx 2644a08b2b feat(store): vault media linking schema migration + document support
- Add migration 000048: vault_documents.path_basename + vault_links.metadata (PG)
- Update SQLite schema.sql: add path_basename, metadata columns + v15→v16 markers
- Add schema.go backfill: loop-patch v15→v16 + basename extraction for existing docs
- Update vault_documents store: SELECT/INSERT with path_basename (PG + SQLite)
- Update vault_links store: SELECT/INSERT with metadata (PG + SQLite)
- Add vault_source_cleanup: helper to purge orphaned sources + links by source ID
- Add basename_helper.go + test: extract filename from path for DB sync
- Update version.go: RequiredSchemaVersion → 48, SQLite SchemaVersion → 16
- Update vault_store.go: DeleteLinksBySource + BatchFindByDelegationIDs interface
2026-04-11 21:22:23 +07:00
viettranx 5623f97961 feat(vault): extension whitelist + document docType + media summary
- Add extension_whitelist.go to deterministically skip binary .bin/.exe/.dll
- Add media_summary.go: synthesize docType + generate short summary from vault fields
- Update rescan.go: detect document type for PDF/office files + propagate docType
- Update safe_walk.go test: .bin now filtered by whitelist, use .txt instead
- Update rescan_test.go: add document docType coverage
- Add link_types.go: define AUTO_LINK_TASK, AUTO_LINK_DELEGATION link types
2026-04-11 21:22:23 +07:00
viettranx 4b27a337d1 test(router): pin stale raw-UUID entry eviction on Get
Pin that a pre-hardening fragmented entry written under the raw UUID
cache key (tenantID:<uuidStr>) is still evicted by the TTL branch in
Router.Get when a UUID-form caller arrives after TTL expiry. The test
synthesizes the fragmented entry directly via a test-only map write,
then asserts the subsequent Get evicts the raw-UUID entry, re-invokes
the resolver once, and writes the canonical tenantID:agentKey entry —
leaving no fragmented entries behind.
2026-04-11 21:22:23 +07:00
viettranx 93099cc682 fix(http/agents): validate agent_key slug on update path
handleUpdate accepted any string in the agent_key allowlist field
without running it through isValidSlug. A client could rename an
agent to "weird:key", which would break router cache exact-segment
invalidation (the cache splits on the last colon for invalidation
matching). Add the slug check inline after the allowlist filter and
return MsgInvalidSlug on failure. The slug regex already rejects
colons, slashes, whitespace, and other characters that would confuse
path rendering or cache key parsing — add a dedicated predicate test
covering the full trap surface.
2026-04-11 21:22:23 +07:00
viettranx c7a93703ba test(ws): cover resolveAgentUUIDCached cache hit fast path
The existing tests exercise only the DB fallback path via errorAgentStore.
Add a test that primes the router cache with a stub implementing both
agent.Agent and agentUUIDProvider, then asserts the cache-aware helper
returns the cached UUID without touching the store — the raison d'etre
of the helper. Uses the same sentinel-error store so a broken fast path
surfaces as a sentinel error rather than a silent pass.
2026-04-11 21:22:23 +07:00
viettranx de4f742b0e fix(store/pg): error-propagate parseUUID in kg dedup insert paths
insertDedupCandidate and ScanDuplicates both swallowed uuid.Parse
errors before feeding entity IDs to the INSERT. Currently safe
because all inputs originate from DB SELECT rows or caller-validated
UUIDs, but aligning with parseUUID keeps write-path policy uniform:
bad UUID → clean Go error + skip, not silent uuid.Nil write.
2026-04-11 21:22:23 +07:00
viettranx 6c9cf321ad fix(store/pg): error-propagate parseUUID in kg EmbedEntity
The EmbedEntity helper ran UPDATE kg_entities SET embedding = $1
WHERE id = parseUUIDOrNil(entityID). A bad UUID would resolve to
uuid.Nil and the UPDATE would silently match no rows. Today's only
caller passes a freshly-minted UUID, but swapping to parseUUID with
an explicit Warn log closes the latent trap and aligns with the
hardening policy (writes must fail fast on bad input).
2026-04-11 21:22:23 +07:00
viettranx 7799a9c88a fix(router): evict stale canonical entry on double-check TTL miss
Router.Get's canonical double-check branch trusted any existing entry
under the canonical key without re-checking TTL. If an earlier
agent_key caller wrote the entry and the TTL expired, a later UUID-form
caller would resolve fresh, hit the canonical branch, find the stale
entry, and return it — indefinitely — because the raw-UUID key was
never the map key and the initial-miss eviction branch did nothing.

Re-check TTL inside the double-check branch and evict+rewrite when
stale. Regression test primes a canonical entry with cachedAt set
2×TTL in the past, then asserts a UUID-form Get re-invokes the
resolver and the returned agent reflects the fresh resolver output.
2026-04-11 21:22:23 +07:00
viettranx 6e86ba9a39 fix(agent): use agent_key in v3 workspace path resolver
The v3 workspace resolver block in injectContext was passing
l.agentUUID.String() as ResolveParams.AgentID, but the resolver uses
this value as a filesystem path segment. The v2 path in
loop_pipeline_callbacks.go and the session_key anchor both use
agent_key, so a UUID-based v3 path would produce a parallel filesystem
for the same logical agent the moment any consumer reads the v3
workspace context.

No active bug today because workspace.FromContext has zero consumers,
but this closes the landmine before the v3 wiring lands.
2026-04-11 21:22:23 +07:00
viettranx c8ebe9a789 refactor(comments): remove plan/phase refs from agent identity hardening code
Rewrite inline comments added during the agent identity hardening so
they explain the code as it stands today, rather than tying to internal
plan terminology (phase numbers, FR/NFR/H/M/C codes, PR references,
trap zone labels). Commit history already carries the plan archaeology.
Comments now keep the non-obvious invariants (cache boundaries, bypass
gaps, silent-nil traps, dual-tenant semantics) and drop the scaffolding.
Comment-only — no runtime behavior change.
2026-04-11 21:22:23 +07:00
viettranx ca7f7b4f24 docs(code): add inline comments at agent identity trap zones
Clarify the agent_key vs UUID contract inline at the four highest-ROI
trap zones: the Loop struct identity fields, DomainEvent identity
fields, the parseUUID/parseUUIDOrNil store helpers, and the WS method
resolver helpers. Each comment explains the invariant and links to
docs/agent-identity-conventions.md so future readers can find the full
rules without archaeology. Comments only — no runtime behavior change.
2026-04-11 21:22:23 +07:00
viettranx bf771a12f4 docs(agent-identity): add agent_key vs UUID convention doc
Distills the dual-identity rules from the agent identity hardening work
into a single permanent reference. Covers all 5 trap zones with post-
hardening status, batch fail-fast contract change, router cache
canonicalization, FK safety net, telemetry type-layer defense, and
dual-tenant agent_key semantics. Adds a CLAUDE.md pointer under Key
Patterns so contributors find it via the standard onboarding path.
2026-04-11 21:22:23 +07:00
viettranx dd65fbcba0 test(integration): fix pre-existing bugs + post-Phase-4 contract update
Three fixes required to run the integration suite after Phase 4:

1. v3_vault_store_test.go makeVaultDoc: AgentID is *string (nullable),
   not string — was a pre-existing type error from v3 commit 8f56ddaa
   that blocked the test binary build. Pass &agentID.

2. vault_classify_test.go imports: remove unused `store/pg` import —
   pre-existing from v3 commit 4b7a2e6e.

3. TestVaultClassify_DeleteDocLinksByTypes_MultipleTypes: fix off-by-one
   in target-doc loop. Pre-existing runtime panic from v3 commit — loop
   created 7 targets but linkTypes slice has 8 entries, accessing docs[8]
   on a len-8 slice. Changed loop to `range 8`.

4. TestVaultClassify_DeleteDocLinksByTypes_NonExistentDoc: swap
   "fake-doc-id-12345" placeholder for a well-formed but unused UUID.
   Post-Phase-4, parseUUID rejects bare strings at the caller boundary;
   the "non-existent = no-op" contract requires a syntactically valid
   UUID. Per Q7 documented contract change.

Phase 4 Step 4g integration gate.
2026-04-11 21:22:23 +07:00
viettranx d29c3dd943 refactor(store/pg): rename mustParseUUID to parseUUIDOrNil (honest name)
Final step of Phase 4: rename the silently-nil-on-error helper so its
behavior is self-documenting at the call site. No behavior change — all
remaining call sites (~24) were already classified as WARN-acceptable
(read-only SELECT WHERE paths with validated inputs from cursor
pagination or WS boundary) during sub-step 4b migration.

The parseUUID error variant stays as-is for CRITICAL writes.

Also cleans up a now-stale comment in vault_handler_upload.go.

Phase 4 Step 4d of agent identity hardening (TD-1).
2026-04-11 21:22:23 +07:00
viettranx 5ff29d051d refactor(store/pg): error-propagate parseUUID in vault_links
All 16 sites migrated. Covers BatchCreateLinks (2 sites in loop with
fail-fast per Q7), CreateLink, DeleteLink, GetOutLinks, GetOutLinksBatch,
GetBacklinks, DeleteDocLinks, DeleteDocLinksByType, DeleteDocLinksByTypes.

BatchCreateLinks previously silently dropped bad UUIDs via loop-over-
validated-docs; post-P4 the parse layer rejects the entire batch on
first bad doc_id — a documented contract change per Q7.

Phase 4 Step 4b.7 of agent identity hardening (TD-1).
2026-04-11 21:22:23 +07:00
viettranx 2aa3ca31ae refactor(store/pg): error-propagate parseUUID in remaining kg + memory files
Five files, targeted CRITICAL-site migrations:

- knowledge_graph_temporal.go: SupersedeEntity (line 71 — C1 from red-team,
  UPDATE + INSERT in transaction). ListEntitiesTemporal remains on
  mustParseUUID (WARN-acceptable SELECT WHERE).
- memory_admin.go: ListAllDocuments, GetDocumentDetail, ListChunks (3 sites,
  all return errors). Adds fmt import.
- memory_search.go: Search (1 site, agent_id resolves hybrid FTS+vector).
- knowledge_graph_traversal.go: Traverse (2 sites — agent + startEntity).
- vault_documents_enrichment.go: UpdateSummaryAndReembed (2 sites — tenant,
  doc). FindSimilarDocs was migrated earlier with optAgentUUID wrapper fix.

Phase 4 Steps 4b.6, 4b.8, 4b.9, 4b.11, 4b.12 of agent identity hardening.

Still pending:
- vault_links.go (16 sites — next commit)
- knowledge_graph_embedding.go line 124 (SAFE per scout — audit only)
- agents_export_queries.go lines 312,354 (SAFE cursor per scout — audit only)
2026-04-11 21:22:23 +07:00
viettranx 63dc84f7e8 refactor(store/pg): error-propagate parseUUID in knowledge_graph_dedup
All 8 sites migrated:
- DedupAfterExtraction, ScanDuplicates (agent)
- MergeEntities (agent, target, source — 3 sites in one transaction)
- ListDedupCandidates (agent)
- DismissCandidate (agent, candidate)

Phase 4 Step 4b.5 of agent identity hardening (TD-1).
2026-04-11 21:22:23 +07:00
viettranx e6896d2673 refactor(store/pg): error-propagate parseUUID in knowledge_graph_relations
All 11 sites migrated (counted higher than scout's 7 CRITICAL because
plan treats the file as a single commit unit):
- UpsertRelation (agent, source, target — 3 sites in one INSERT)
- DeleteRelation (agent, id)
- ListRelations, ListAllRelations (agent + entity)
- IngestExtraction (agent)
- PruneByConfidence, Stats (agent)

Phase 4 Step 4b.4 of agent identity hardening (TD-1).
2026-04-11 21:22:23 +07:00
viettranx 47bdea8259 refactor(store/pg): error-propagate parseUUID in vault_documents
CRITICAL sites migrated in the following methods (9 total):
- GetDocument, GetDocumentByID, GetDocumentsByIDs, GetDocumentByBasename
- DeleteDocument, ListDocuments, CountDocuments, UpdateHash

UpsertDocument and Search were migrated earlier as part of the
optAgentUUID wrapper fix (commit 6f6b52fb).

4 sites remain in the private helpers appendTeamFilter and
buildSearchTeamFilter — these are WARN-acceptable (team IDs from opts
structs are already validated at WS boundary). They will rename to
parseUUIDOrNil in sub-step 4d.

Phase 4 Step 4b.1 of agent identity hardening (TD-1).
2026-04-11 21:22:23 +07:00
viettranx 4ebd6698b0 refactor(store/pg): error-propagate parseUUID in knowledge_graph
5 CRITICAL sites migrated: UpsertEntity (INSERT), GetEntity (SELECT),
DeleteEntity (DELETE), ListEntities (SELECT), SearchEntities (SELECT).
All now return wrapped parse errors at the caller boundary instead of
hitting FK violations (writes) or returning empty results (reads).

Phase 4 Step 4b.3 of agent identity hardening (TD-1).
2026-04-11 21:22:23 +07:00
viettranx 642603e14e refactor(store/pg): error-propagate parseUUID in memory_docs
5 CRITICAL sites migrated from mustParseUUID to parseUUID with wrapped
errors: GetDocument, PutDocument, DeleteDocument, ListDocuments,
IndexDocument. All handle bad agent_id input at the caller boundary
instead of hitting FK violation (writes) or returning empty results
(reads).

Scout classified GetDocument as both CRITICAL and WARN — migrating all
5 is safer and consistent (M8 note).

Phase 4 Step 4b.2 of agent identity hardening (TD-1).
2026-04-11 21:22:23 +07:00
viettranx 32af80b28c refactor(store/pg): propagate error from optAgentUUID wrapper
optAgentUUID now returns (*uuid.UUID, error). All three known callers
updated to check the error and wrap with context:

1. vault_documents.go UpsertDocument — also migrates tenant_id and
   team_id to parseUUID in the same hot path.
2. vault_documents.go Search — migrates tenant_id and agent_id.
3. vault_documents_enrichment.go FindSimilarDocs — migrates tenant_id,
   agent_id, and doc_id.

Pre-Phase-4, the wrapper silently converted garbage agent_id strings to
uuid.Nil, letting the downstream INSERT hit a FK violation with cryptic
PG code 23503. Now bad input fails at the caller boundary with a clear
Go error wrapping the raw value.

Phase 4 Step 4e of agent identity hardening (TD-1).
Covers sub-step 4b migrations for vault_documents.go and
vault_documents_enrichment.go as a side effect.
2026-04-11 21:22:23 +07:00
viettranx 9a8eb396b0 refactor(store/pg): add parseUUID error variant
Two new helpers alongside existing mustParseUUID:

- parseUUID(s) (uuid.UUID, error) — use for INSERT/UPDATE/UPSERT/DELETE
  and SELECT-WHERE paths where silent nil hides bugs. Returns a wrapped
  error that includes the raw value (for debuggability) and the
  underlying uuid parse error (for errors.Is unwrap).

- parseUUIDOrNil(s) uuid.UUID — intentionally silent, for read-only
  SELECT-WHERE paths where empty result is acceptable.

mustParseUUID is kept as a transitional alias delegating to parseUUIDOrNil
so existing call sites compile unchanged. Sub-step 4d will rename it.

Phase 4 Step 4a of agent identity hardening (TD-1).
2026-04-11 21:22:23 +07:00
viettranx 6bbe1b66ff refactor(ws/teams): accept agent_key or UUID + fix handleAddMember error leak
Two teams handlers migrated to resolveAgentUUIDCached:
- handleRemoveMember (params.AgentID) accepts agent_key or UUID.
- handleTaskAssign (params.AgentID) accepts agent_key or UUID.

handleAddMember already used resolveAgentInfo, but leaked raw store
errors via "agent: " + err.Error() on resolution failure. H10 fix
replaces the leak with i18n.MsgInvalidID — no more store schema info
surfacing to WS clients.

Phase 3 of agent identity hardening.
2026-04-11 21:22:23 +07:00
viettranx ef730ac84a refactor(ws/channels): accept agent_key or UUID (adds agentStore to struct)
NFR-2 exception (Phase 3 C2): ChannelInstancesMethods struct gains an
agentStore field, required to support agent_key input via the cache-aware
resolver. Constructor signature expanded and single caller at
cmd/gateway_channels_setup.go:146 updated.

Router is passed nil — channel_instances methods are not wired to the
router today, so falls back to pure DB lookup. Acceptable given create
is a rare operation. Preserves the existing i18n key MsgInvalidID.

Phase 3 of agent identity hardening.
2026-04-11 21:22:23 +07:00
viettranx 21a9076298 refactor(ws/config): accept agent_key or UUID for permission handlers
All 3 handlers (handleList, handleGrant, handleRevoke) now accept either
a UUID or an agent_key for params.agentId via resolveAgentUUIDCached.

Adds ConfigPermissionsMethods.SetAgentRouter setter wired from
cmd/gateway_methods.go (already staged in previous commit).

Phase 3 of agent identity hardening.
2026-04-11 21:22:23 +07:00
viettranx 84babc1c3c refactor(ws/heartbeat): accept agent_key or UUID via cache-aware resolver
All 7 heartbeat handlers (get/set/toggle/test/logs/checklistGet/checklistSet)
now accept either a UUID or an agent_key for params.agentId. Behavior
additive — existing UUID-only clients keep working.

Adds HeartbeatMethods.SetAgentRouter setter wired from cmd/gateway_methods.go
so the fast path avoids a DB roundtrip for cached agents. Preserves the
literal "invalid agentId" error string (no i18n key change).

Phase 3 of agent identity hardening.
2026-04-11 21:22:23 +07:00
viettranx 6c17aad8d7 feat(gateway/methods): add cache-aware resolveAgentUUIDCached helper
Three-part addition so WS handlers can accept agent_key input without a
DB roundtrip on every request:

1. Router.GetCached — lock-free TTL cache read, no resolver fallback.
2. Loop.UUID() — canonical UUID accessor (agent_key stays on ID()).
3. resolveAgentUUIDCached — fast-path checks router cache via an
   agentUUIDProvider interface, falls back to resolveAgentUUID on miss
   or when input is a UUID string (un-cached post-canonicalization).

Helper delegates to pure DB lookup when router is nil, so handlers with
no router dependency still work. Tests cover nil-router, cache-miss, and
UUID-input paths — cache-hit coverage is exercised via downstream handler
integration tests.

Phase 3 foundation (TD-2).
2026-04-11 21:22:23 +07:00
viettranx 82dc4d3b94 chore(agents): remove dead UUID invalidation in agents_update
Post-Phase-2 canonicalization, router cache entries are always keyed as
tenantID:agent_key. The belt-and-suspenders m.agents.InvalidateAgent(ag.ID.String())
call never matched any entry — exact-segment match on the final ":"-
delimited segment cannot match a UUID as the agentKey.

Removes the dead call and the misleading comment that justified it.

Phase 2 follow-up (H7).
2026-04-11 21:22:23 +07:00
viettranx c599387248 fix(router): canonicalize cache key, exact-segment match, tenant-aware IsRunning
Three related fixes to agent router identity handling:

1. Canonicalize on resolve — Router.Get now stores entries under the
   canonical tenantID:agent_key after a successful resolver call,
   regardless of whether the caller passed a UUID or agent_key. Prevents
   fragmentation where the same logical agent would occupy two cache
   slots. Callers that pass the UUID form are now un-cached and resolve
   on every call; all production callers pass agent_key today.

2. Exact-segment match — matchAgentCacheKey helper replaces HasSuffix in
   Router.Remove and Router.InvalidateAgent. Prevents substring collisions
   like "tenantX:sub-foo" being wiped when invalidating "foo". Rejects
   empty agentKey to guard against wildcard wipes.

3. Tenant-scoped IsRunning (C6) — Router.IsRunning now accepts ctx and
   uses agentCacheKey for lookup. Pre-fix, the bare r.agents[agentID]
   lookup always returned false in tenant-scoped deployments, causing
   agents.list to report every live agent as idle.

Updates the sole caller at gateway/methods/agents.go:134.

Phase 2 of agent identity hardening (TD-3).
2026-04-11 21:22:23 +07:00
viettranx d848286b4b fix(vault): validate agent_id/team_id form params at HTTP boundary
validateTeamMembership short-circuits on owner role and nil teamAccess
(lite edition), leaving downstream mustParseUUID calls as a silent-nil
trap. Validate UUIDs at the HTTP boundary — before workspace resolution,
store upsert, or event publish — so bad form input is rejected for every
caller regardless of role or edition.

Closes the owner/lite gap identified in red-team H9.

Phase 1 Fix B of agent identity hardening.
2026-04-11 21:22:23 +07:00
viettranx 89862981e6 fix(agent): set AgentUUID in memoryflush SystemPromptConfig
memoryflush.go constructed SystemPromptConfig without AgentUUID, while
loop_history.go set both AgentID (agent_key) and AgentUUID. buildRuntimeSection
renders below the cache boundary so the omission did not bust the prompt
cache — but the runtime identity line dropped the UUID during compaction
flush turns.

Extract a pure helper buildMemoryFlushPromptConfig so tests can assert the
config shape without a full Loop fixture (M7 mitigation from red-team
review).

Phase 1 Fix A of agent identity hardening.
2026-04-11 21:22:23 +07:00