Commit Graph
1359 Commits
Author SHA1 Message Date
viettranx 7cfcbbf9db fix(vault): include shared docs in agent read paths (#917)
- Patch vault_search, ListDocuments, CountDocuments, ListTreeEntries to
  include shared docs (agent_id IS NULL) for agents in the tenant
- Keep DELETE/GetDocument/GetByBasename strict (auth-intent)
- Add CHECK invariant vault_documents_scope_consistency (PG migration
  000055 NOT VALID, SQLite triggers v24) to prevent future drift
- Update docs and changelog

Affects PostgreSQL and SQLite (desktop edition).
2026-04-16 14:17:48 +07:00
viettranx 4fe293ae1e test(integration): cover Telegram group write_file permission flow
Add 5 fixtures mirroring gateway_consumer_normal.go + commands_writers.go
context/grant shapes. Proves the ConfigPermissionStore permission flow
resolves correctly in isolation: granted sender allowed, ungranted
denied, fail-open path distinguished, DM bypass preserved, and the
"|"-delimited sender split behaves as specified.

Regression coverage for issue #915.
2026-04-16 14:17:48 +07:00
viettranx 237626b660 feat(store): add ActorIDFromContext helper
Introduce ActorIDFromContext(ctx) that returns SenderID if set, else
falls back to UserID. Separates ACTOR (acting principal) from SCOPE
(memory/namespace) semantics so permission checks, audit trails, and
ownership fields can migrate off the scope-coupled UserIDFromContext.

No callers yet; migration of existing sites is a follow-up.
2026-04-16 14:17:48 +07:00
viettranx 207bc56dbd feat(ui): persist page size preference across all list pages
Add pageSize to useUiStore (persisted in localStorage). Update
PAGE_SIZES default to [20, 50, 100, 200]. All 12 paginated pages
now read initial value from store and sync changes back globally.
2026-04-16 14:17:48 +07:00
viettranx f8a6f9363f feat(ui): hook test panel tool combobox with JSON auto-fill
Replace plain text input with single-select combobox listing builtin
tools, auto-fill JSON skeleton on selection, expand dialog sizing.
2026-04-16 14:17:48 +07:00
viettranx b097a29a61 fix(ui): heartbeat delivery Select full-width + popper positioning
Ensure Select triggers in heartbeat section use full available width
and render dropdown with popper positioning for consistent alignment.
2026-04-16 14:17:48 +07:00
viettranx ee328d4266 refactor: consolidate hooks table migration and drop deprecated agent_id column
- Consolidated PG migration 054: agent_hooks rename + junction table + deprecated column drop
- Updated SQLite schema v19 with final consolidated migration
- Removed obsolete schema rebuild files (v21, v23)
- Updated Go store layer (pg/hooks.go, sqlitestore/hooks.go)
- Updated integration tests to use new table names (hooks, hook_agents)
- Updated TypeScript protocol, UI components, and i18n strings
- Updated gateway methods to reflect schema changes

Tables: agent_hooks → hooks, agent_hook_agents → hook_agents
2026-04-16 14:17:48 +07:00
viettranx 6659ba46a8 feat(tts): consolidate TTS config UI with provider-aware pickers and synthesize endpoint
- Add 4-provider catalog (OpenAI, ElevenLabs, Edge, MiniMax) with static
  voice/model data for web and desktop
- Rewrite /tts page as 5 modular sections: provider setup, credentials,
  voice/model picker, test playground, collapsible advanced settings
- Add POST /v1/tts/synthesize endpoint with 15s timeout, 500-char cap,
  RoleOperator auth, ElevenLabs model validation, rate limit reuse
- Fix Edge TTS provider to honor per-request opts.Voice instead of
  always using construction-time default (C1)
- Gate agent detail TTS section on global provider config with
  empty-state CTA for owners, catalog-driven model select, override
  checkbox, and inline voice test button
- Remove TTS from builtin-tools dialog (STT preserved); hide tts tool
  from builtin-tools page list
- Add desktop parity: provider-aware VoicePicker, TTS config hook via
  WS config.get, empty-state component, synced i18n locales (en/vi/zh)
- Add 109 web vitest tests + 12 Go unit tests covering handler
  validation, legacy config regression, and catalog parity
2026-04-16 14:17:48 +07:00
viettranx df82fafb28 feat(hooks): name field + beta explainer modal + UI polish
- PG migration 000054 + SQLite v21→v22: add nullable name column
- HookConfig.Name in Go struct, PG/SQLite scan/insert, WS handlers
- Builtin seed writes spec ID as hook name
- UI: name input in create/edit form, displayed in hook list row
- Beta card: "Learn more" button opens modal explaining hooks×skills×MCP
- Default handler_type changed from http to script
- Script editor section: max-h-[50vh] with scroll, border always visible
- i18n: all new keys in en/vi/zh
2026-04-16 14:17:48 +07:00
viettranx c0acfa1bd9 feat(hooks): phase-08 integration tests (A-F buckets)
27 test cases across 7 files exercising real PG + SQLite backends:
- A: tenant isolation (resolver scope, cross-tenant guard)
- B: sandbox escape + per-tenant fairness (goja hardening e2e)
- C: capability tier + FireResult propagation (source-tier gate)
- D: builtin seed reconciliation + idempotency + v21 rebuild
- E: command auto-disable migration (Standard vs Lite)
- F: WS method-equivalent (Validate, Create, TestRunner)
All green with -race on PG; D6 green on sqliteonly.
2026-04-16 14:17:48 +07:00
viettranx feaf7b9e40 feat(hooks/builtin): default_disabled flag + flip pii-redactor off
Input-mutation hooks should be opt-in. Adds an explicit YAML toggle so
operators turn them on after verifying the regex / policy matches their
content, instead of getting silent mutation on first install.

- internal/hooks/builtin/loader.go: Spec.DefaultDisabled (yaml
  default_disabled). Reverse-named so zero value preserves prior
  default-on behavior for any spec that omits the field.
- internal/hooks/builtin/seed.go:
  - Fresh insert path honors the flag → row created with enabled=false
  - Version-bump path: when an UPDATE crosses the boundary that newly
    introduces default_disabled=true, flip enabled→false once. Subsequent
    boots without a version bump leave the user's manual re-enable alone
    (the switch fall-through does nothing on equal versions). Logs
    hooks.builtin_default_off_applied for audit.
- internal/hooks/builtin/builtins.yaml: pii-redactor v1 → v2 with
  default_disabled: true. Existing installs auto-flip to disabled on
  next boot via the version-bump branch above; users re-enable through
  the UI toggle when ready.
- Tests:
  - TestSeed_DefaultDisabledOnFreshInsert (insert respects flag)
  - TestSeed_DefaultDisabledFlipsOnVersionBump (one-shot flip + user
    re-enable preserved on the next no-bump boot)
2026-04-16 14:17:48 +07:00
viettranx 04354e833f feat(hooks/ui): wider form modal + dialog test panel + collapsible beta card
User-reported polish on the hooks page.

- Form modal: widen from sm:max-w-lg → sm:max-w-3xl lg:max-w-4xl. Pair
  Event/Scope and Matcher/if_expr into 2-col grids; collapse
  timeout/on_timeout/priority/enabled into a single 4-col row on lg+.
  Reads the same on mobile (single column).
- Test panel: replaced the floating bottom-right drawer with a centered
  Dialog (sm:max-w-4xl). Two-column layout — left is the sample event
  form with per-field hints, right is the result with empty-state
  illustration, prominent decision pill, separated reason / stdout /
  stderr / error / updatedInput sections. Run button is now full-width
  with a Play icon. New i18n strings cover all hints + empty/error
  states (en/vi/zh).
- Beta info card: top-of-page collapsible explainer with three "how it
  works" points (event → handler → test). Persists collapsed state in
  localStorage so dismissed users don't see it expand on every reload.
  i18n in en/vi/zh under the new beta.* namespace.
2026-04-16 14:17:48 +07:00
viettranx 7096a8e02c fix(hooks/ui): native script editor + redesigned detail + error toast
User report: hook detail page ugly, script not syntax-highlighted, edit
dialog crashes silently with no toast.

- Replace react-simple-code-editor (React 19 compat issues — likely cause
  of the silent edit-dialog crash) with a native textarea + <pre> overlay.
  Same UX: caret over highlighted source, scroll-synced. Drops the dep
  entirely and removes a render-path black box. Bundle size went DOWN
  (62 KB → was 65 KB before Phase 06; net ~+1 KB for Prism core).
- Import prismjs/themes/prism-tomorrow.css so token colors actually show.
  Editor surface uses #2d2d2d to match the Tomorrow theme palette.
- Harden HookFormDialog reset against null `initial.config` — previous
  cast threw when the row was inserted with no config payload.
- Redesign HookOverviewTab:
  - Header card with event/handler/scope chips + status pill
  - Metadata grid (timeout/on_timeout/priority/source/version/timestamps)
  - Handler-specific cards: script source rendered via the read-only
    ScriptEditor (full Prism highlight), HTTP shows url/method/body,
    prompt shows template + model. No more raw JSON dump.
  - Extract HttpConfigCard / PromptConfigCard subcomponents to keep
    types narrow at the unknown→string boundary.
- ErrorBoundary now toast.error()s when it catches — user sees the
  failure visually instead of just the generic fallback card. Full stack
  still logs to console for devs.
2026-04-16 14:17:48 +07:00
viettranx f3d6c99dda fix(hooks): close source-tier forge + i18n + concurrency hardening
Code-review findings from Wave 1 audit (commits 097776c5..f76a681e).

Critical (security):
- C1 internal/gateway/methods/hooks.go: parseHookConfigParams strips
  caller-supplied source/id/created_by/version. Without this a tenant
  admin could POST {"source":"builtin"} and escalate their UI hook into
  the dispatcher's builtin capability tier (which is allowed to mutate
  event input). Default Source resolves to "ui" via Validate.
- C2 internal/gateway/methods/hooks.go: handleUpdate also strips source +
  created_by from the patch map. Same forge surface via PATCH instead of
  POST.

High:
- H1 internal/gateway/methods/hooks.go: Update + Delete handlers wrap
  hooks.ErrBuiltinReadOnly into the i18n key MsgHookBuiltinReadOnly so
  users get a localized "builtin hooks are read-only" message instead of
  the raw English sentinel string.
- H3 internal/hooks/handlers/script.go: bound tenantSems map with
  opportunistic sweep — once map crosses 64 entries, idle slots
  (no in-flight grants AND last-used > 1h ago) are reclaimed under the
  same lock the acquire path already takes. Goroutine-free; common-path
  cost unchanged.

Medium:
- M1 internal/hooks/dispatcher.go: builtinAllowlistLookup uses
  atomic.Pointer instead of plain package var. Fixes the parallel-test
  race where one test installs a lookup while another reads.
- M3 ui/web/index.html: drop http: https: from CSP connect-src — keep
  'self' + ws:/wss: only. Same-origin XHR/WS still works; cross-origin
  HTTP is no longer wildcard-permitted.

Tests:
- internal/gateway/methods/hooks_source_strip_test.go pins the C1+C2
  strip behavior at the parse layer with a regression case that posts
  every forge field and asserts each is zeroed.
2026-04-16 14:17:47 +07:00
viettranx 31eb953508 feat(hooks/ui): script editor + builtin readonly + CSP hardening
Phase 06 UI: surface the script handler + builtin affordances that
backend Phases 02-05 already support.

- Deps: prismjs@^1.30.0 (CVE-2024-53382 patched) + react-simple-code-editor +
  @types/prismjs. pnpm override pins prismjs to ≥1.30.0 across transitives.
- CSP meta in index.html: default-src 'self', script-src 'self' (no
  'unsafe-eval' — Prism uses static highlight, never executes user JS),
  style-src allows Google Fonts + jsdelivr katex, connect-src allows ws:/wss:
  for WS, frame-ancestors 'none' + base-uri 'self'.
- Schema: HookHandlerTypeEnum adds "script"; script_source field with 32 KiB
  cap + required-when-script refinement.
- HookConfig TS type: handler_type adds "script", source adds "builtin".
- Components:
  * script-editor.tsx — Prism.highlight JS with readonly + inline compile-error
    banner. parseLineCol exported helper matches goja "Line N:M" format
    (verified against goja parser/error.go); locale-aware banner via
    t("form.compileError", {line, col}); fallback renders raw message.
  * system-badge.tsx — "System" chip rendered next to the scope on list rows
    where source==="builtin".
- hook-form-dialog.tsx: adds "script" radio; renders ScriptEditor when
  selected; readonly cascade for builtin rows (event/scope/matcher/if_expr +
  all handler sub-forms + timeout/on_timeout/priority all disabled; enabled
  switch stays actionable). Save button label flips to "Save toggle" on
  builtin. Top-of-form banner informs user.
- hook-list-row.tsx: SystemBadge + delete button disabled for builtin rows.
  New emerald color for script handler chip.
- hooks-page.tsx: buildConfig script branch → {source: data.script_source}.
- i18n: en/vi/zh get scriptSource, scriptSchemaHelp, compileError,
  builtinReadonly, saveToggle, systemBadge, reason, scriptSourceRequired.
- Tests: 5 parseLineCol cases (happy + multi-digit + null + garbage +
  multi-error first-match) — all green.
- Build: pnpm tsc --noEmit + lint + build all clean. Hooks page chunk
  65 KB / 19 KB gzip.
2026-04-16 14:17:47 +07:00
viettranx 33d188f01f feat(hooks/migration): auto-disable legacy command hooks on Standard boot
Phase 07 runtime migration. Post-Wave-1 Standard deployments that pre-dated
the edition gate may still carry enabled command-type hook rows in the DB;
those keep firing via the dispatcher after the UI stops letting users create
new ones. This helper flips them off at startup so live traffic never fires
a disabled-by-policy handler again.

- internal/hooks/migration_command_autodisable.go:
  DisableLegacyCommandHooks(ctx, hookStore, edition). Standard only; Lite
  returns early. Master-scope context + List(Enabled=true) → skip
  non-command + source='builtin' rows → Update enabled=false. Per-row
  WARN log with hook_id + tenant_id for audit; INFO summary when n>0.
  Idempotent: second boot finds nothing.
- cmd/gateway_managed.go: invoke after builtin.Seed, BEFORE handler
  registration so no HTTP/WS traffic races the migration.
- Tests (fake store, 5 cases):
  * Standard with mixed rows → only command+source=ui gets disabled
  * http + script/builtin + command/builtin rows untouched
  * Idempotent second run → n=0
  * Lite edition → n=0, nothing touched
  * nil store → n=0, no error
  * List error → propagated up

Note: Phase 04/05 never seed command-typed builtins, but the carve-out is
kept defensively so a future accidental builtin command row can't be
disabled by this migration.
2026-04-16 14:17:47 +07:00
viettranx a6592fd65f feat(hooks/builtin): ship pii-redactor exemplar — email + phone masking
First real builtin under Phase 04 infrastructure. Registers two DB rows
(user_prompt_submit + pre_tool_use) on startup, runs the embedded JS with
source-tier gate + dispatcher dotted-path allowlist.

- internal/hooks/builtin/pii-redactor.js (ES5.1, goja-compatible):
  handle(event) returns updatedInput only when email or E.164-ish phone
  actually matched. Redacts rawInput + toolInput.{command,query,content}.
- builtins.yaml: pii-redactor entry (version 1, priority 900, timeout 2s,
  on_timeout=allow — missing redaction < blocking conversation).
- Drop _placeholder.js; pii-redactor.js is now the first real //go:embed match.
- Export builtin.Source(name) so handler tests load embedded JS without
  going through Load()/cache state.
- Tests:
  * loader_test: registry contains pii-redactor after Load().
  * script_pii_redactor_test (handler-level, real goja runtime): email
    masked in rawInput; +E164 phone masked; toolInput.command redacted,
    toolInput.path left alone (not in mutable_fields); no PII → no
    mutation; redact(redact(s)) == redact(s).
  * dispatcher_test: 3 dotted-path walker cases (toolInput.command only
    lets that key through; rawInput-only doesn't let forged toolInput
    override original event keys; toolInput wildcard merges all keys).

Bench: ~242µs/op on 1 KiB Apple M4 Max, well under <1ms target.
2026-04-16 14:17:47 +07:00
viettranx 8ee540dc7e feat(hooks/builtin): embedded registry + UPSERT seed + store-layer readonly
Phase 04 of hooks Wave 1. Adds infrastructure for shipping canonical hook
rows with the binary; Phase 05 plants the first real builtin (pii-redactor).

- internal/hooks/builtin: loader (//go:embed yaml+js), Seed() with version
  reconciliation (newer embed overwrites DB; downgrade warn-only; preserves
  user's enabled toggle), AllowlistFor() for per-id mutable-field lookup.
  Stable UUIDv5 namespace 082ab084-a25f-52b4-a4a4-eb8a816bd9a8 keys rows
  across restarts so Seed is idempotent on every boot.
- internal/hooks/store.go: ErrBuiltinReadOnly sentinel + WithSeedBypass
  ctx marker (seeder bypasses the guard; users cannot).
- PG + SQLite hook stores: Update/Delete now reject any patch other than
  enabled=* on source='builtin' rows. Fail-closed on GetByID errors.
- Dispatcher: SetBuiltinAllowlistLookup setter lets the gateway wire the
  registry-backed strict allowlist; unwired tests keep Phase 03 permissive
  default. Gateway installs a strip-all lookup first so a Load() failure
  fails closed instead of opening mutation to the permissive default.
- i18n: MsgHookBuiltinReadOnly (en/vi/zh).
- yaml.v3 promoted to direct dep.
- Tests: loader parse + namespace stability + seed idempotency
  (3 boots → N rows), version bump, downgrade detection, operator
  BuiltinDisable escape hatch, PG + SQLite readonly guard round-trip.
2026-04-16 14:17:47 +07:00
viettranx d8e5fad8e4 test(store/pg): fix hook test DB setup — migrations path + UUIDv7 slug collision
Two pre-existing issues kept the PG hook tests from running under the
documented flow (TEST_DATABASE_URL="postgres://postgres:test@localhost:5433/
goclaw_test?sslmode=disable"):

1. migrate.New path had one extra `../` — resolved above the repo root.
   Fixed from `file://../../../../migrations` to `file://../../../migrations`.
2. `seedTenantAndAgent` derived slug + agent_key from `uuid[:8]`, and two
   UUIDv7 calls in the same millisecond share their 8-char prefix. The
   second tenant INSERT got silently swallowed by ON CONFLICT DO NOTHING on
   the unique slug constraint, so the follow-up agent INSERT failed the
   `agents_tenant_id_fkey` FK. Fixed by using the full UUID in the slug /
   agent_key suffix.

Verified: `TestPGHookStore_*` (6 tests) now green under the integration
DB flow; `TestHooks*` under `-tags integration` still green.
2026-04-16 14:17:47 +07:00
viettranx b7592bcacf feat(hooks/script): integrate script handler — FireResult + source-tier gate + migration
Wave 1 Phase 03. Wires the Phase 02 Goja handler into the dispatcher, widens
the DB schema for script + builtin sources, and refactors Dispatcher.Fire to
return a FireResult so builtin-source hooks can mutate event input.

Dispatcher:
- FireResult{Decision, UpdatedToolInput, UpdatedRawInput} replaces the plain
  Decision return. stdDispatcher.runSync keeps a local evMut copy so a
  builtin-source hook's updatedInput flows to downstream hooks in the chain
  and to the caller; non-builtin (source=ui) script mutations are dropped +
  WARN-logged (defense-in-depth; source tier enforced at the dispatcher, not
  only at the handler).
- applyBuiltinMutation + placeholder builtinAllowlistFor (rawInput, toolInput)
  — Phase 04 overrides the allowlist from builtins.yaml.
- noopDispatcher returns FireResult{DecisionAllow}.
- Script mutation tests: TestDispatcher_ScriptMutation_BuiltinSourceApplies,
  TestDispatcher_ScriptMutation_UISourceDenied.

Pipeline + callers (14 Fire sites refactored):
- FireHook wrapper returns FireResult; context_stage.go:52 applies
  UpdatedRawInput → state.Input.Message; tool_stage.go:52 applies
  UpdatedToolInput → tc.Arguments before ExecuteToolCall.
- delegate_bridge + delegate_tool keep the Decision branch; Updated* ignored.
- dispatcher_test / delegate_bridge_test / delegate_tool_hooks_test /
  integration test helpers updated for the new shape.

Validation:
- edition_gate allows HandlerScript on every edition (sandboxed, no shell
  escape surface).
- config.validateHandler rejects empty source, source > 32 KiB, goja compile
  error; validateTimeout rejects on_timeout=ask|defer (reserved).
- Six new config tests cover the script path.

Migrations:
- PG migration 000053 relaxes handler_type + source CHECKs and drops
  uq_hooks_{global,tenant,agent} — scripts routinely want many small hooks
  per event. RequiredSchemaVersion → 53.
- SQLite SchemaVersion → 21; patch 20 is a SELECT 1 placeholder because
  SQLite can't ALTER a CHECK — rebuildAgentHooksV21 runs outside the
  migration tx (parallel to backfillV16) to rename/recreate the table with
  widened CHECKs. schema.sql fresh-DB path matches.
- H9 fix: PGHookStore.Create + SqliteHookStore.Create honor caller-provided
  cfg.ID (uuid.Nil falls back to UUIDv7), unblocking Phase 04 idempotent
  UUIDv5 seed. TestCreateHonorsFixedID on both stores.

Config:
- config.HooksConfig{ScriptConcurrency, ScriptPerTenantConcurrency,
  ScriptCacheSize, BuiltinDisable}; buildHookHandlers wires the script
  handler with the caps from appCfg.Hooks.

Gates green: go build ./... + sqliteonly, go test -race -count=1 across
hooks/pipeline/sqlitestore.
2026-04-16 14:17:47 +07:00
viettranx a556161d0d feat(hooks/script): goja ES5.1 script handler + deny-all sandbox
Wave 1 Phase 02 — pure runtime + sandbox. Dispatcher wiring, edition gate,
and DB migration land in Phase 03.

Handler:
- internal/hooks/handlers/script.go — ScriptHandler with two-layer semaphore
  (global 10 / per-tenant 3), hashicorp/golang-lru v2 program cache (cap 500),
  InvalidateHook hook for Phase 03, watchdog goroutine interrupting on ctx
  cancel, sanitized error text (strips \n + goja " at <frame>" inline tails)
- internal/hooks/handlers/script_sandbox.go — deny-all/allowlist hardening,
  SetMaxCallStackSize(256), prototype-chain nullify running BEFORE deny pass
  so Function.prototype.constructor can still be reached
- internal/hooks/handlers/script_runtime.go — LRU compile, JSON round-trip +
  deep-freeze event binding, 4KiB truncating stdout capture, strict return
  parser enforcing {decision, reason?, updatedInput?}

Types:
- HandlerScript HandlerType = "script"
- DecisionAsk / DecisionDefer reserved (Wave 1 treats as block + warn)
- hooks.ScriptResult + WithScriptResult / ScriptResultFrom ctx helpers for
  Phase 03 dispatcher to apply builtin-source UpdatedInput

Tests (go test -race green):
- script_test.go — 12 unit cases (happy/error/timeout/ask/defer/truncate/H1)
- script_sandbox_corpus_test.go — 27 cases: escape primitives (constructor
  chain, Reflect, Proxy, Symbol, Promise, __proto__ walk, eval, Function,
  Date chain, JSON replacer, toJSON DoS), resource bombs (recursion, loop,
  memory, ReDoS), typeof sanity (Reflect/Proxy/Symbol/Promise/GoError/
  globalThis/eval/Function undefined), mutation defense (toolInput/rawInput
  frozen, Go-side map unchanged)

Deps: github.com/dop251/goja, github.com/hashicorp/golang-lru/v2.

Gates: go vet clean, go test -race clean, go build ./... + sqliteonly clean.
2026-04-16 14:17:47 +07:00
viettranx 3a25358049 feat(hooks/ui-gates): surface is_master_scope + edition, drop command handler
Wave 1 Phase 01 — UI gates before script-handler work begins.

Backend:
- WS connect response adds `is_master_scope` (store.IsMasterScope with
  role+tenant scoped ctx) and `edition` (edition.Current().Name).

Web:
- Auth store persists both values; ws-provider pipes them from ws-client
  on connect and resets on disconnect.
- Hook form: removes `command` radio + legacy tooltip; scope options
  hide `global` when caller is not master scope.
- Legacy rows with `handler_type="command"` coerce to `http` on edit
  (phase 07 auto-disables them). Schema enum narrowed to http|prompt.
- i18n: drops 4 obsolete keys (form.command, form.allowedEnvVars,
  form.cwd, form.commandDisabledTooltip) from en/vi/zh.

Security invariant: is_master_scope is an advisory UI hint only —
backend still enforces master scope on write paths.
2026-04-16 14:17:47 +07:00
viettranx 95bdb23a36 docs(hooks): user guide + example configs + changelog + Make targets
- docs/agent-hooks.md: handler reference, lifecycle events, security model
- examples/hooks/: 5 runnable JSON configs (audit, lint, block-rm-rf,
  Discord notify, context injector)
- docs/17-changelog.md: Wave 0 entry
- Makefile: hooks-specific test targets
- CLAUDE.md: cross-reference
2026-04-16 14:17:47 +07:00
viettranx f90f11a9f9 test(hooks): integration coverage — e2e, rbac, tracing, chaos
Adds cross-cutting integration tests exercising hooks RPC surface:

- hooks_e2e_test: create → dispatcher fire → audit entry → history list
- hooks_rbac_test: tenant-scope guard, master-only global scope enforcement
- hooks_tracing_test: audit entries include handler timing + outcome
- hooks_chaos_test: handler timeout behavior, on_timeout block/allow paths
- hooks_pipeline_test: extends existing pipeline coverage
2026-04-16 14:17:47 +07:00
viettranx 727f345344 feat(web): hooks management UI — list, editor, history, test panel
Adds /hooks page with tenant-scoped list, create/edit dialog (per-handler
config forms for command/http/prompt), history table with diff viewer,
and dry-run test panel that calls hooks.test RPC. Sidebar entry gated by
edition/scope through existing auth store.

i18n: new hooks.json namespace + sidebar.json additions for en/vi/zh.
Routes + query-keys wired; no new runtime deps added.
2026-04-16 14:17:47 +07:00
viettranx 1f560e15d7 feat(gateway): config.defaults RPC exposing SSoT pruning values
Adds config.defaults WS method so UI can render pruning-knob placeholders
that mirror the server's internal fallback values. Exports
agent.DefaultPruningValues() as the single source of truth used by both
the resolver and this RPC — keeps UI in sync without a parallel table.
2026-04-16 14:17:47 +07:00
viettranx aa1b2f026a feat(hooks): WS RPC surface + tenant-scope guards + shared test runner
Exposes agent hooks over WebSocket (hooks.list/create/update/delete/
toggle/test/history) behind existing auth layer. Additions:

- Gateway wiring: buildHookHandlers() factory reused by dispatcher and
  hooks.test runner, so UI test panel exercises production code paths
- GetByID tenant-scope guard: non-master callers only see own tenant +
  global rows (matches List() behavior on both PG and SQLite)
- i18n keys + catalogs (en/vi/zh) for hooks error messages
- Protocol method constants for hooks.* and config.defaults
2026-04-16 14:17:47 +07:00
viettranx 553340b3a9 feat(hooks/prompt): LLM-driven prompt handler with per-turn cap
Adds PromptHandler that runs an LLM over matched hook events (user prompts,
tool calls) to produce decisions (allow/block/modify). Features:

- Budget integration: per-tenant token spend enforced via budget.Store
- Per-turn counter seeded in ContextStage and propagated through state.Ctx
  so PreToolUse fires under the same cap as UserPromptSubmit
- prompt_template required (non-empty) at validate time to prevent
  misconfigured hooks from silently no-oping
- Injection-hardened: tool_input/raw_input are quoted into system prompt
  rather than concatenated
- Resolver abstraction (RegistryResolver) picks model from provider registry
  with SystemConfigs fallback
2026-04-16 14:17:47 +07:00
viettranx ab50e571c5 feat(hooks/budget): per-tenant token spend tracking subsystem
Atomic budget store for prompt-handler LLM spend, with PG and SQLite
implementations. Enforces per-hook token caps to prevent runaway cost
from matcher-bypass scenarios.
2026-04-16 14:17:47 +07:00
Uğur Tafralı 12cfcec59a fix(zalo): correct json field mappings (#924)
Fixes #922 - Zalo OA bot was silently dropping all incoming messages due to JSON field mapping mismatches.

- Align zaloBotInfo/zaloFrom/zaloChat JSON tags with live API (display_name, chat_type)
- Change getUpdates to decode single-object response (API returns one update, not an array)
- Add dedicated pollClient with Timeout=0 + per-call context timeout for long-polling
- Update tests accordingly
2026-04-16 14:17:07 +07:00
Plateau Nguyen 8873548102 feat(pancake): add comment auto-react (like) on Facebook (#919)
Opt-in feature (features.auto_react: true) that automatically likes
a user's Facebook comment via Pancake /likes endpoint when the comment
webhook is received, as an engagement signal.

- Add AutoReact bool to Features config struct
- Add ReactComment() to APIClient (multipart POST to /likes, auth via api_key)
- Add reactCommentAsync() to comment handler; fires before keyword filter,
  independent of comment_reply feature
- Bound with 10-slot semaphore + 5s stopCtx timeout per goroutine
- Guard: only fires on platform=facebook with non-empty convID + msgID
- Startup warning when auto_react=true without webhook_secret (F9)
- Update feature gate: exit only when BOTH auto_react AND comment_reply disabled
- Add 8 tests (ReactComment contract, error body, invalid IDs, handler variants)
2026-04-16 10:14:43 +07:00
JinwooHyunandClaude Opus 4.6 a8c01dbc49 fix(skills): add missing AS alias for COALESCE in embedding search query (#896)
SearchByEmbedding query uses COALESCE(description, '') without an AS
alias, causing PostgreSQL to name the column 'coalesce'. The sqlx scan
struct expects db:"description", so the scan fails with:

  missing destination name coalesce in *[]pg.skillEmbeddingSearchRow

The BackfillEmbeddings query on line 84 already has the correct
AS description alias — this fix makes the search query consistent.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:00:11 +07:00
Viet Tran 60ca45ec97 fix(pancake): demote hot-path webhook log to Debug; guard commentID before echo (#916)
- webhook_handler.go: demote per-event "page_id resolution" log from Info to
  Debug. Firing on every webhook was the exact anti-pattern called out in the
  routing-metadata refactor review.
- pancake.go: move reply_to_comment_id guard above rememberOutboundEcho so a
  missing-metadata error does not stamp phantom echoes that would pollute
  inbound echo dedup.
- docs/journals: drop three Pancake journals (not needed in repo).
2026-04-15 22:30:23 +07:00
Plateau Nguyen 85e6f216b5 feat(channels): Pancake comment reply fix + platform select + UI improvements (#904)
* feat(channels): make Pancake platform a required select with 11 options

- Add mandatory platform select field (11 options) to Pancake channel schema
- Add config required validation on channel instance form (create-only)
- Add i18n fieldOptions/fieldConfig for platform in en/vi/zh locales
- Add TDD tests for Pancake config schema (channel-schemas.test.ts)
- Add backend test TestFactoryExplicitPlatformPreserved
- Add slog.Debug for auto-detect path in pancake.go
- Update Platform field comment in types.go for clarity
- Update plan status to completed; add changelog entry

* feat(channels): hide comment_reply for non-social Pancake platforms

Extend showWhen to accept string | string[] values. Apply it to
features.comment_reply so the field only appears for platforms that
support public posts/comments (facebook, instagram, threads, tiktok,
youtube). E-commerce platforms (shopee, lazada, tokopedia) and
messaging-only platforms (line, google, chat_plugin) no longer show
the irrelevant Comment Reply toggle.

Also guard depValue before String() coercion to avoid the "undefined"
literal matching hazard.

* feat(channels): Pancake comment reply fix + platform select + UI improvements

- Fix comment reply: pass message_id (reply_to_comment_id) to Pancake API
  ReplyComment now requires messageID param; guard added for empty ID
- Add SendMessageRequest.MessageID field (omitempty) for reply_comment action
- Platform select: required field with 11 options, showWhen gate for comment_reply
- Webhook Page ID moved to Advanced collapsible section in channel form
  (auto-expands when existing value is configured)
- routing_metadata.go: centralize routing metadata key constants
- config-flatten.ts: flatten/unflatten nested config for form state

Closes: Pancake comment reply returns 'Missing required field: message_id'
2026-04-15 22:26:13 +07:00
viettranx c189160f91 docs: remove journals folder 2026-04-15 21:12:36 +07:00
viettranx 17b0bc5365 test(sqlitestore): regression test for agent Update null→default coercion
Covers PR #897 fix path — verifies Update() coerces null values to schema
defaults for NOT NULL columns (other_config, tools_config, self_evolve,
skill_evolve, is_default, etc.), preventing the "NOT NULL constraint failed"
crash on fresh SQLite DBs when UI sends null for disabled/unset fields.
2026-04-15 21:12:01 +07:00
viettranx fb3552f7e5 fix(hooks/security): add SSRF-safe http client missed by phase 2 commit
Phase 2 (a587231b) advertised SSRF hardening for the http hook handler
but the supporting `internal/security` package was never created, so the
production HTTPHandler fell back to a bare http.Client and admin-config
webhooks could probe loopback / link-local / RFC1918 / cloud-metadata.

- internal/security/ssrf.go: Validate(rawURL) parses + resolves once,
  rejects loopback/link-local/private/multicast/unspecified + 169.254.169.254;
  NewSafeClient(timeout) returns an http.Client whose DialContext pins the
  resolved IP from context (defense-in-depth re-checks the dialed IP) and
  refuses redirects (CheckRedirect = ErrUseLastResponse)
- internal/hooks/handlers/http_handler.go: call Validate before each request,
  attach pinned IP via security.WithPinnedIP(ctx, ip)
- cmd/gateway_managed.go: construct HTTPHandler with
  Client: security.NewSafeClient(10*time.Second)
- tests: 13 new ssrf_test.go cases (every block category + redirect refused
  + dial pinned); existing http_test.go retrofitted with
  security.SetAllowLoopbackForTest helper for httptest.NewServer
- plan: phase-02 Step 2a marked done with reference to this commit

Refs: GitHub Issue #875
2026-04-15 21:12:01 +07:00
viettranx 97f9784443 feat(hooks): phase 2 — handlers, pipeline wiring, ssrf-safe http client
- add command + http handlers (internal/hooks/handlers) with edition gating
  and Authorization-header decryption via crypto.Decrypt
- add SSRF-safe dialer (internal/security/ssrf.go): DNS-once + pinned IP,
  blocks loopback / link-local / private ranges
- wire dispatcher into pipeline stages: ContextStage fires SessionStart
  (async) + UserPromptSubmit (sync); ToolStage fires PreToolUse (sync, COW
  staging for updatedInput) + PostToolUse (async); FinalizeStage fires
  Stop (async)
- bridge delegate events to dispatcher (SubagentStart / SubagentStop)
- construct dispatcher in cmd/gateway_managed.go with both handlers and
  thread it through agent.ResolverDeps + delegateTool.SetHookDispatcher
- unit tests for both handlers + integration tests covering HTTP allow with
  audit row, HTTP block, command Lite-only gate, delegate bridge subscribe
- defer worker pool optimization (Phase 2 Step 3) to Phase 4

Refs: GitHub Issue #875
2026-04-15 21:12:01 +07:00
Duy /zuey/ 20e34deb6a feat(teams): inline rename for team name + description (#571) (#906)
* feat(teams): add inline rename for team name and description (#571)

- Add reusable InlineEditText component (click-to-edit, Enter/Esc/blur)
- Wire inline edit on team detail header (name) and info dialog (description)
- Refactor useTeams hook: updateTeamSettings → updateTeam(teamId, patch)
- Backend: fix teams.update handler to accept *map[string]any for Settings
  so partial updates (name-only / description-only) do not wipe existing
  settings (would silently downgrade v2 → v1)
- Add rename.* i18n keys for en/vi/zh
- Follow mobile rules: text-base md:text-sm, touch target ≥44px on coarse pointers
- Stale-value guard + unmount safety + snapshot-based no-op detection

Closes #571

* fix: remove obsolete team_key reference in agent-identity-conventions

* docs: add journal on rename Agent Teams feature (#571)

Captures the critical data-loss bug caught by code review — backend
teams.update handler silently wiping v2 team settings on partial patches
before the fix (Settings *map[string]any).
2026-04-15 21:11:35 +07:00
248a39ba0c This fixes a data integrity issue affecting fresh installations of Desktop Lite and any environment where agent.other_config and serveral columns are not pre-initialized. This will stop Desktop Lite edtion from saving any agent setting. (#897)
The root cause is missing default initialization of JSON field in update flow, leading to NOT NULL violations in both SQLite and Postgres.

Fix ensures consistent behavior across Lite and Standard deployments.

Co-authored-by: Viet Tran <viettranx@gmail.com>
Co-authored-by: Chunning Ha <cn@ha.td>
2026-04-15 20:49:19 +07:00
viettranx 5040fe4cbe fix(channels): suppress agent-error text on public-facing channels
Rework of #911 against dev with correctness fixes.

Public-facing channels (Facebook, Telegram, Discord, Feishu, WhatsApp,
Zalo OA, Zalo Personal, Pancake, Slack) no longer receive raw internal
error text when an agent run fails — an empty outbound is published
instead so channels still clean up placeholders / typing indicators.
Errors continue to be logged server-side at Error level.

Applied on both hot paths:
  - cmd/gateway_consumer_normal.go   (agent run failure)
  - cmd/gateway_subagent_announce_queue.go (announce lead run failure)

Fixes vs #911:
  - Whitelist uses channels.Type* constants (compile-time safe) instead
    of string literals. The original PR's literals ("zalo", "line") did
    not match any real channel type, so Zalo OA/Personal and Pancake
    leaked errors while "line" was a dead branch.
  - Added TypePancake and TypeSlack which were missing.
  - Empty Content in Facebook.Send and Pancake.Send now short-circuits
    (matching Telegram/Discord/Slack). Previously, suppressed errors
    flowed to Graph/Pancake APIs as empty payloads, triggering 400s and
    channel health degradation.
  - Added table-driven test for isExternalChannel covering all real
    channel types + empty + unknown + legacy "zalo" short form.

Closes #911
2026-04-15 20:40:25 +07:00
badgerbeesandViet Tran 770af6b1c0 Vault: handle empty delete responses (#913)
Co-authored-by: Viet Tran <viettranx@gmail.com>
2026-04-15 17:11:32 +07:00
viettranx ce620c87ef fix(channels): bound Reload Start with timeout; preserve caller ctx for long-lived goroutines
A single hung ch.Start() (e.g. Telegram GetMe stalled on DNS/TCP) blocks
InstanceLoader.mu and wedges every subsequent Reload(), leaving new
channels stuck in "checking" forever.

Wrap Start() in a goroutine bounded by reloadStartTimeout (90s) so Reload
can move on to the next instance. On timeout, Stop the channel in a
bounded window, record a typed failure for the UI, and asynchronously
drain the late-returning Start to detect channels that ignore context
cancellation.

Critically, pass the caller's ctx — not a timeout-wrapped ctx — into
ch.Start. Channels routinely derive long-running goroutines from the
ctx they receive (Telegram's pollCtx is WithCancel(ctx)). A timeout-
wrapped ctx would silently kill polling 90s after a successful Reload.

Add regression tests covering both the hang-then-timeout path and the
caller-context preservation invariant.

Closes #914
2026-04-15 15:27:18 +07:00
viettranx 3fe5ae0b50 feat(hooks): introduce agent lifecycle hooks foundation with fail-closed blocking semantics
Phase 1 of agent-hooks-system establishes:
- Hook types, matchers, and CEL evaluation engine for event-driven extensibility
- Edition-gated command handlers (Lite disabled) with dedup_key-indexed audit log
- Dual-DB store layer (PostgreSQL + SQLite) with transaction boundary enforcement
- Blocking-event semantics: hook execution failures propagate to agent loop, graceful retry
- Tenant-isolated audit trail with per-hook execution context tracing
- Integration tests verifying store correctness and hook dispatch atomicity

Lays groundwork for post-v3.0 phase-02 (per-tenant onboarding hooks).
2026-04-15 14:25:59 +07:00
viettranx 86d564967f fix(agent/title): disable thinking and raise max_tokens for Gemini
Gemini 2.5/3 default to high thinking via OpenAI-compat, consuming the
entire max_tokens budget and truncating titles to a single word (and
adding latency). Title generation is a trivial task that does not
benefit from reasoning, so set OptThinkingLevel="off" and bump
max_tokens from 50 to 256 for headroom across reasoning-capable
providers.

Also update mapGeminiReasoningEffort to forward "off" as "low" (the
minimum effort all Gemini models accept via OpenAI-compat), since not
forwarding causes the server to fall back to "high".
2026-04-15 13:37:37 +07:00
viettranx 5590b94994 fix(providers/openai): gate tool-msg name field to Gemini routes only
Red-team feedback: the new `name` field on role=tool messages was
being sent to ALL OpenAI-compat hosts including Together, Groq, vLLM.
Those hosts typically reject unknown fields with HTTP 400 — Gemini
is the only backend that requires `name` (via FunctionResponse.name
in Google's OpenAI-compat shim). Scope the field to Gemini-only via
supportsThoughtSignature, matching how we already gate
thought_signature forwarding.

Trace: 019d8f33-2de1-7ab2-9a32-9df92cd610dd
2026-04-15 12:46:48 +07:00
viettranx 666bfbaf3c fix(agent/systemprompt): soften predefined bootstrap to forbid empty-args tool calls
The previous MANDATORY phrasing ("MUST ALSO call write_file for
USER.md and BOOTSTRAP.md before your response ends") was making
small models emit write_file({}) when they had no real user info
to persist, tripping HTTP 400 on Google's Gemini OpenAI-compat shim.

Rewrite the predefined+BOOTSTRAP.md branch with softer "get to know
the user" copy plus explicit hard rules forbidding empty/placeholder
arguments and session-identifier content in USER.md. Safety net is
preserved: the USER PROFILE INCOMPLETE branch nudges the model every
subsequent turn until USER.md is populated.

Sync BOOTSTRAP_PREDEFINED.md template to remove the contradictory
MANDATORY block and replace with matching hard rules.

Trace: 019d8f33-2de1-7ab2-9a32-9df92cd610dd
2026-04-15 12:42:32 +07:00
viettranx 5689d168e6 test: failing tests for softened predefined bootstrap prompt 2026-04-15 12:41:00 +07:00
viettranx 6544999779 fix(pipeline): treat gemini tool_calls+empty-args as truncation retry
Gemini returns finish_reason="tool_calls" (not "length") even when the
thinking budget exhausted max_tokens before tool arguments could be
emitted. Extend the existing truncation retry condition to detect this
case via an allowlist of mutating tools (write_file, edit, exec,
create_image, read_file) that virtually never legitimately call with
empty args. Nullary tools (datetime, heartbeat) pass through unchanged.

Trace: 019d8f33-2de1-7ab2-9a32-9df92cd610dd
2026-04-15 12:39:56 +07:00
viettranx 5c229bed59 test: failing tests for scoped truncation retry on mutating tools 2026-04-15 12:38:55 +07:00