20 Commits
Author SHA1 Message Date
yatulandClaude Fable 5.1 60cf79de51 feat(teams): let a human cancel and retry stuck team tasks from the dashboard (#1559)
* feat(teams): let a human cancel and retry stuck team tasks from the dashboard

A task that ended up blocked, stale, failed or cancelled could not be
recovered from the UI: the dashboard only deletes terminal tasks and
approves/rejects in_review ones. CancelTask and ResetTaskStatus existed
in the store but were reachable only through the lead agent's team_tasks
tool, which needs the full task UUID the dashboard never shows (#506).

Two WS RPCs, wired to Retry / Cancel buttons in the task detail dialog:

- teams.tasks.cancel — any task not yet completed/cancelled. Optional
  reason is stored as the result and posted as a comment so the lead
  sees it on the board. Dependents are unblocked by the store; they are
  not dispatched here because the dashboard has no agent turn.
- teams.tasks.retry — stale / failed / cancelled / in_review, plus
  blocked tasks that nothing blocks any more. A human comment is
  required: it is posted on the task and appended to the assignment
  prompt, so the assignee gets the missing answer in the same message.
  Optional agentId reassigns; the lead is refused as assignee (same
  guard as teams.tasks.assign).

ResetTaskStatus (pg + sqlite) now also accepts blocked, guarded on the
handler side by an empty blocked_by. Handler tests cover both actions
with a stub store: reason/comment persistence, required comment,
status gating, blocked_by guard, lead guard, cross-team IDOR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(permissions): classify teams.tasks.cancel and teams.tasks.retry as write methods

Every WS method must be classified in internal/permissions/policy.go,
otherwise the router answers UNAUTHORIZED for every role. Caught on a
live gateway (the drift test TestMethodRole_DriftCoverage… flags it, I
had not run that package). Both sit next to approve/reject/assign as
operator-level write methods; the write-methods test now pins them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(teams): let the retry dialog pick the assignee

A cancelled or stale task may have no owner (created unassigned, or the
member was removed). teams.tasks.retry then needs agentId, which the
dialog did not send, so Retry would fail with "task has no assignee".
The retry dialog now shows an assignee select (team members minus the
lead, defaulting to the current owner) and passes agentId only when it
differs from the owner. Retry is offered only when there is someone to
pick.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 13:30:48 +07:00
yatulandClaude Fable 5.1 d98ea89e3b feat(teams): show the full task UUID in the task detail dialog with one-click copy (#1560)
The short identifier (T-015-cc8e) carries only the last four hex chars
of the UUID, while every agent-facing surface — the team_tasks tool
(get / retry / cancel / comment) and the teams.tasks.* RPCs — takes the
full UUID. A human looking at the dashboard therefore had no way to name
a task to the lead in chat. Show the UUID next to the identifier and
status badges, copy it to the clipboard on click, with a short "copied"
confirmation.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 00:00:47 +07:00
bd5adc61c8 feat(bitrix24): imbot.v2 migration, 2-way media, openline sender-tag echo, and hardening (#1236)
* refactor(bitrix24): rename "Path B" framing to maintainer-specified naming [B24:2794]

Per maintainer hard rule #10 (no generic "Path A/B" framing) from PR #1061
review. The Bitrix24 MCP auto-onboard flow is Bitrix-specific glue
("Bitrix24 OAuth -> existing mcp_user_credentials bridge"), NOT a generic
MCP architecture pattern.

Naming convention applied consistently:
- First mention per file: full "Bitrix24 OAuth -> existing
  mcp_user_credentials bridge" (matches maintainer comment verbatim).
- Subsequent mentions in same file: shortened "mcp_user_credentials bridge".
- Test/log context referencing literal endpoint /api/auto-onboard: keep
  "auto-onboard" reference (it's the actual API endpoint name).

Changes are documentation-only:
- Rename in code comments + test descriptions + plan docs.
- Clarify framing in mcp_client.go + provisioner.go doc comments to
  emphasize Bitrix-specific glue (not generic MCP infra).
- Reuse existing mcp_user_credentials table + MCPServerStore methods
  (no schema / store / abstraction change).

Files:
- cmd/gateway.go (factory registration doc)
- internal/channels/bitrix24/{channel,factory,mcp_client,provisioner}.go
- internal/channels/bitrix24/{mcp_client,provisioner}_test.go
- plan/goclaw-mcp-integration.md (21 occurrences)

Verified: go build + MCP-related tests pass (TestProvision*,
TestInitMCPProvisioner*, TestMCPClient*).

Phase 1 of Path C execution per
plans/reports/decision-log-260519-1555-bitrix24-pr-fork-decision.md.

* fix: confine outbound media paths to agent workspace [B24:2794]

Tool MEDIA:<path> output reached channel file-upload sinks (Bitrix
imbot.v2.File.upload, Telegram sendDocument, etc.) verbatim via
parseMediaResult, with no workspace-boundary check. A malicious or buggy
tool emitting MEDIA:/etc/passwd could exfiltrate arbitrary files to chat.

Extract the EvalSymlinks+Rel containment from extractMediaFromContent into
a shared confineToWorkspace helper and apply it at the parseMediaResult
sink in processToolResult. Fixing at the source/egress boundary protects
every channel at once rather than per-channel. Paths that escape the
workspace are dropped and logged (security.media_path_rejected).

Add TestConfineToWorkspace (boundary unit) and
TestParseMediaResultConfinedToWorkspace (sink regression for H2).

* feat(bitrix24): support inbound + outbound media via imbot.v2 File API [B24:2794]

Bitrix24 channel was text-only; attachments were parsed but dropped.
- Inbound: download chat files via imbot.v2.File.download (one-time URL),
  forward to the agent with MIME preserved (internal/channels/bitrix24/download.go).
- Outbound: upload agent media to the chat via imbot.v2.File.upload
  (internal/channels/bitrix24/send_media.go).
- Add BaseChannel.HandleMessageMedia to preserve MIME/filename through the bus.
- Per-channel media_max_mb cap (default 20) applies to both directions.

Tests: 92 pass (internal/channels/bitrix24 + internal/channels), go vet clean (PG + sqliteonly).

* refactor(bitrix24): migrate messaging/bot-list/unregister to imbot v2 API [B24:2794]

Move outbound REST calls to the imbot v2 family (keeps register on v1):
- imbot.message.add -> imbot.v2.Chat.Message.send (fields.message shape, live-verified)
- imbot.bot.list (+ legacy imbot.list fallback) -> imbot.v2.Bot.list; add botListRows
  to normalize the v2 {bots:[...]} envelope, legacy array, and id-keyed map forms
- imbot.unregister -> imbot.v2.Bot.unregister

Bot registration stays on v1 imbot.register: v2 imbot.v2.Bot.register changes the
event-delivery model (per-event handler URLs -> eventMode), which would require
rewriting the inbound event parser. No user-facing behavior change.

Tests: bitrix24 package green; go vet ./... clean.

* feat(bitrix24): route whisper via v1 SKIP_CONNECTOR + add v2 replyId [B24:2794]

Bot was leaking HiddenMessage (whisper) replies to the external Zalo
connector because every outbound call went through imbot.v2.Chat.Message.send,
which has no equivalent of the v1 SKIP_CONNECTOR flag. Branch the outbound
path on inbound visibility:

  whisper → imbot.message.add + SKIP_CONNECTOR=Y  (v1, send_v1.go)
  public  → imbot.v2.Chat.Message.send + fields.replyId  (v2, send_v2.go)

Pipeline:
  events.go        parse data[PARAMS][PARAMS][COMPONENT_ID]=HiddenMessage
                   into EventParams.IsHiddenMessage (form + JSON variants)
  handle.go        set bitrix_visibility on InboundMessage.Metadata
  consumer         forward visibility + message_id into OutboundMessage
  send.go          resolveSendOptions + sendChunk dispatcher +
                   shared callWithRateLimitRetry helper
  metadata_keys.go single source of truth for the keys + values

Defaults preserve pre-refactor behaviour: callers that don't populate
bitrix_visibility still go through v2 public, and replyId is omitted
unless a numeric bitrix_message_id arrives in metadata.

Tests:
  TestParseEvent_FormURLEncoded_IsHiddenMessage  (3 cases)
  TestParseEvent_JSON_IsHiddenMessage             (3 cases)
  TestResolveSendOptions                          (8 cases)
  TestSend_BranchesOnVisibility                   (4 cases)

* feat(bitrix24): openline sender-tag echo on replies [B24:2794]

Openline sender-tag echo (this change):
- Capture the connector sender tag ("[name #id]:" or "[name] #id:") from
  inbound openline group messages, strip it from the body the agent sees,
  and re-prepend the canonical "[name] #id:" form to the reply so the Open
  Channel connector routes the answer back to the right external user.
- New sender_prefix.go helper (+ test) accepts both inbound layouts and
  emits one canonical form; scoped to messages carrying the tag, so plain
  chats are unaffected.
- metadata_keys.go: MetaKeySenderPrefix; handle.go capture/strip/stash;
  gateway_consumer_normal.go forwards the key; send.go prepends it on the
  first chunk before chunking.

Bundled bitrix24 channel-core work already on this branch:
- handle.go: @mention is the sole trigger for both staff and connector
  customers; unmentioned traffic is dropped (was: drop all connector msgs).
- isGroupMessageType: treat SONET_GROUP "B" as a group.
- handle_test.go, mcp_client_test.go: cover the above.

* feat(bitrix24): accept colon-less openline sender tag, echo [name] #id [B24:2794]

The Open Channel connector dropped the trailing colon from its sender tag:
inbound now arrives as "[Name] #id <msg>" (was "[Name] #id: <msg>"). The
id-bearing patterns required the colon, so the tag fell through to the
name-only branch and the reply echoed "[Name]" — dropping the #id the
connector needs to route the answer back.

- sender_prefix.go: make the trailing ":" optional on both id layouts
  ([name #id] / [name] #id, with or without colon) and echo the canonical
  "[name] #id" (no colon) to match the connector's current format. Bare
  "[name]" (no id) still echoes "[name]" for Open Channel only.
- handle.go: gate the bare name-only layout to Open Channel (isOpenChannel)
  so ordinary group chats starting with "[x] ..." are left untouched.
- sender_prefix_test.go: cover colon/no-colon x id-inside/id-outside, the
  name-only openline case, and the non-openline no-op.

* fix: security and robustness fixes from the bitrix24 channel review [B24:2794]

- download.go: block redirect-based SSRF on inbound media. CheckRedirect
  re-validates each hop (http(s) only, reject private/loopback/link-local
  hosts, cap hops); the initial portal-domain pin is no longer bypassable
  via a 3xx to an internal service. Public-host redirects still allowed.
- handle.go: extract/echo the openline sender tag only for Open Channel
  sessions (was: any group chat), removing bogus prefixes in CRM group
  chats and narrowing the forged-tag misroute surface.
- loop_tools.go + loop_media.go: confine result.Media to the agent / team /
  tenant-allowed roots (new confineToAnyRoot) before a channel uploads it,
  so a prompt-injected out-of-workspace path (e.g. /etc/passwd) cannot
  exfiltrate, while legitimate cross-workspace media (team files, delegatee
  output) still flows.
- send_media.go: bounded outbound read via io.LimitReader replaces the
  os.Stat + os.ReadFile pair, closing the TOCTOU size-cap bypass; cap a
  single message's outbound attachments at 10 (mirrors inbound).
- register.go: paginate imbot.v2.Bot.list (limit/offset + hasNextPage,
  capped at 40 pages) so verify/lookup see bots past the first 50.
- mcp_client.go: redact access_token / refresh_token / client_secret from an
  echoed MCP error body before it is logged or returned (+ test).

* fix(security): validate resolved dial IP on Bitrix media redirects [B24:2794]

The inbound media download redirect guard only string-checked the redirect
hostname (isPrivateOrLoopback on req.URL.Hostname()), so a redirect to a public
hostname that resolves to 127.0.0.1 / 169.254.169.254 / an RFC1918 address — or a
DNS-rebinding swap between check and dial — still passed the guard and the client
would connect. Reported in PR review.

Add security.NewRedirectFollowingSafeClient: it follows redirects but validates
the RESOLVED destination IP of every hop at dial time via net.Dialer.Control,
reusing the existing blocked-CIDR list. The IP it checks is the IP actually
dialed, so both redirect-to-internal and DNS rebinding are refused, while
legitimate public CDN redirects still succeed. download.go now uses it instead of
the hostname-string guard.

Tests: deterministic dial-control table (loopback / link-local / private /
multicast / unspecified / public, v4 + v6), malformed/non-IP addr, test bypass,
loopback-dial-blocked client wiring, and redirect cap + scheme checks.

* feat(bitrix24): per-participant Zalo openline identity from 3-token sender tag [B24:2794]

Parse the connector's "[Name] #uid #msgId" sender tag so each external
customer in a shared Open Channel group gets its own contact + USER.md
instead of collapsing onto the connector proxy id. Identity minting is
gated on IS_CONNECTOR=Y to reject operator forged tags. Echo back the
msgId only ("#msgId") on replies; keep the legacy single-number and
name-only layouts unchanged. Zero DB migration.

- sender_prefix.go: parseOpenlineSenderTag() classifies 3-token / legacy / name-only
- handle.go: synthetic senderID "openlines:{instance}:{chat}:{uid}" + participant_user_id metadata, gated on FromIsConnector
- gateway_consumer_normal.go: deriveGroupUserID() routes participant -> per-person scope, group fallback otherwise
- send.go: buildAddressMention numeric-id guard so synthetic ids don't emit invalid [USER=...] BBCode
- MetaKeyMessageID kept as Bitrix MESSAGE_ID (drives v2 fields.replyId); connector msgId surfaced only via echo prefix

---------

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
Co-authored-by: Chinh Dang <chinhdang@192.168.68.104>
2026-06-22 14:23:34 +07:00
Duy /zuey/ fdba1c2138 feat(channels): add sidecar delivery behavior overrides
Closes #144.

- Retire user-facing Tool Status Messages and deterministic tool-status channel text.
- Add sidecar-generated Quick Acknowledgement and Intermediate Replies with provider/model/timeout/token/char caps.
- Resolve delivery behavior as Channel > Agent > Workspace with agent overrides in other_config.delivery_behavior.
- Preserve legacy block_reply defaults for Intermediate Replies.
2026-06-09 22:24:24 +07:00
Duy /zuey/ 23b18fa6cd fix(channels): rely on generated intermediate progress (#143)
* fix(channels): add reasoning delivery modes

* fix(telegram): show tool status without placeholder

* docs(plans): mark issue 67 behavior plan complete

* feat(config): add behavior setting purpose tooltips

* fix(channels): rely on generated intermediate progress
2026-06-09 09:12:23 +07:00
Duy /zuey/ ed1327db16 feat(config): add behavior setting purpose tooltips (#140) 2026-06-06 07:55:55 +07:00
Duy /zuey/ f771cff77c fix(channels): coalesce multi-attachment inbounds (#63) (#90)
Implements 3 coalescing layers to handle rapid multi-attachment inbounds:
- Bus debouncer: delays inbound messages 1s, merges duplicates
- Web chat debouncer: buffers client-side inbound frames for batch RPC
- Telegram album aggregator: collects album members via AfterFunc+Stop timer

Drops media-bypass shortcut (forces 1s media floor). Aggregator enforces:
- AfterFunc+Stop timer discipline with ordered drain on stop
- 2-tuple (album_id, sender) keying for isolation
- Dual DoS caps: max 10 albums per sender, max 100 messages per album
- merged_message_ids dedup seeding across all 3 surfaces

Closes #63
2026-05-28 18:30:34 +07:00
Duy /zuey/ af10f5d708 feat(skills): add workspace-organizing built-in skill (#86)
* feat(skills): add workspace-organizing built-in skill

Enforces purpose-based folder convention for agent workspaces with two
modes (flat for ad-hoc work, project for named multi-file work),
per-agent namespacing under shared/<agent_key>/ to prevent collisions
in team workspaces, and pre-write discovery via vault_search /
memory_search / knowledge_graph_search to surface related files and
avoid duplicates. Vault integration documents which folders are
Vault-indexable and how scope mirrors workspace scope.

Closes #71

* docs(changelog): add workspace-organizing skill entry

Refs #71
2026-05-28 13:39:05 +07:00
Duy /zuey/ 532ff91d8e fix(security): harden upstream critical surfaces (#32)
* fix(security): harden upstream critical surfaces

Refs #30

* fix(security): close pre-landing review gaps

Refs #30

* fix(security): close official release blockers
2026-05-20 16:33:49 +07:00
Goon a08c626867 fix(secure-cli): resolve runtime npm binary aliases 2026-05-19 09:54:21 +07:00
Duy /zuey/ 077e81c990 Merge pull request #16 from digitopvn/fix/skill-grants-tenant-scope
fix(skills): enforce tenant scope on agent grants
2026-05-18 20:50:02 +07:00
Duy /zuey/ 52aa110e1f feat(providers): default ChatGPT OAuth to GPT-5.5
Default ChatGPT Subscription (OAuth) provider model selection to GPT-5.5 and update model metadata, tests, and docs.
2026-05-18 20:33:40 +07:00
Goon 3a62bb50e8 fix(skills): enforce tenant scope on agent grants
Reject cross-tenant skill grant and revoke operations before grant rows or skill visibility can be changed. Clean legacy invalid grant rows in PostgreSQL and SQLite migrations, hide owner IDs from skill API/UI responses, and cover the tenant-isolation cases with PG and SQLite regression tests.
2026-05-18 17:40:41 +07:00
Duy /zuey/ ba3b535286 fix(agents): handle null JSON config updates (#13)
* fix(agents): handle null JSON config updates

* docs(changelog): note agent provider switch fix

* docs(journal): record agent provider switch fix
2026-05-17 22:34:19 +07:00
Duy /zuey/ 6e5e51a18b feat(packages): Phase 2a — pip + npm update flow (#900) (#6)
* feat(packages): backend pip + npm update flow (#900)

Extend Phase 1 update infrastructure to pip + npm sources. Register
checkers/executors behind edition gate (Lite edition stays github-only).
Per-source sentinel errors + stderr classifier; strict package-name
validators reject @version suffix. Shared PackageLocker serializes
install + update paths. HTTP response surfaces per-source availability
from LookPath detection.

Closes part of #900 (Phase 2a).

* feat(packages): frontend multi-source updates UI (#900)

Unified flat updates list with source pill (github/pip/npm) + filter
dropdown. Summary bar shows per-source counts, hiding sources whose
backend availability=false. 30 i18n keys with full en/vi/zh parity.
Mobile-safe table (overflow-x-auto + min-w-[600px]).

Part of #900 (Phase 2a).

* test(packages): pip + npm integration e2e (#900)

Optional real-runtime integration test behind `pipnpm_e2e` build tag.
Skipped by default CI; exercises full check + apply cycle with real
pip3/npm in Alpine container.

Part of #900 (Phase 2a).

* docs(packages): document pip + npm update flow (#900)

Adds packages-pip-npm.md covering command matrix, exit codes, stderr
error classes, pre-release handling, availability detection, runbook
for EACCES/ERESOLVE/externally-managed, min versions, fixture regen.
Cross-link from packages-github.md. Changelogs updated.

Part of #900 (Phase 2a).

* fix(packages): set exec bit on testdata npm/pip scripts
2026-05-11 15:31:32 +07:00
Duy /zuey/ 4472c607b8 feat(workstation): Remote Workstation Runtime — SSH exec + security + audit (#4)
* feat(packages): add update flow for GitHub binaries (#900)

Closes #900. Proactive update-check + atomic swap for GitHub-installed
binaries on the Runtime & Packages page. Interfaces prepared for pip/npm/apk
extension in Phase 2.

- UpdateCache + UpdateRegistry + PackageLocker (ctx-aware keyed mutex)
- GitHubUpdateChecker: ETag-aware, distinct /latest vs /list ETag keys,
  semver-correct ordering via golang.org/x/mod/semver, non-semver fallback
  that refuses to downgrade, pre-release + stable candidate fusion for
  the v1.0.0-rc.1 -> v1.0.0 transition
- GitHubUpdateExecutor: two-phase .bak swap with hadBackup-aware rollback,
  manifest save retry (3x, 100ms/500ms/1s backoff), nil-safe meta access,
  explicit ScratchDir, 0755 set pre-rename
- HTTP: GET /v1/packages/updates (SWR), POST /v1/packages/updates/refresh,
  POST /v1/packages/update, POST /v1/packages/updates/apply-all
  (always 200, failed[] is error source). Master-scope gated.
- WS events package.update.{checked,started,succeeded,failed} forwarded to
  owner clients via event_filter.go
- Frontend: useUpdates hook + 3 components (summary bar, update-all modal,
  row button), master-scope-gated disabled state
- i18n: 8 backend keys + 17 frontend keys x en/vi/zh
- Config: packages.github_token (reserved), updates_check_ttl, scratch_dir
- 45+ new tests, race-clean, BenchmarkCheckAll10Packages ~1.1ms/op warm

* docs(packages): document update flow + Phase 1 completion

- packages-github.md: "Updating Installed Packages" section with UI + API
  contract, troubleshooting runbook (corrupt cache, rate-limit, scratch dir,
  mid-swap recovery)
- 17-changelog.md + CHANGELOG.md: Phase 1 entry
- 14-skills-runtime.md: cross-ref to update flow
- journal entry capturing CRIT fixes (double-write, lock-key mismatch,
  rollback false-alarm) + design wins (keyed locks, red-team pre-flight)

* feat(workstation): remote workstation runtime — SSH exec + security + audit

Adds generic Remote Workstation Runtime enabling agents to execute commands
on user-owned SSH workstations. Includes registry (DB + API + UI), SSH backend
with connection pool and circuit breaker, workstation.exec + claude_remote tools,
NFKC + binary-name allowlist security, and audit logging.

Standard edition only. Closes #941.

* fix(workstation): address 3 critical + 5 important code review findings

- C1: Add json:"-" to Metadata/DefaultEnv fields; use SanitizedView() in
  all API responses to prevent SSH private key leakage
- C2: Wire CheckEnv into PermCheckFn; LD_PRELOAD/PATH injection now blocked
- C3: SSH Setenv fallback — prepend `export K=V;` when server rejects Setenv
- I1: BackendCache sync.RWMutex → sync.Mutex (fix data race on lastUsed)
- I2: Validate metadata shape in handleUpdate before store write
- I3: Include command in exec-done event; activity sink uses actual cmd hash
- I4: Wrap pool release in sync.Once (idempotent double-call safety)
- I5: Verify workstation tenant ownership before adding permissions

* fix(packages): bypass HTTPS+IP validation in update executor tests

Test httptest servers bind to http://127.0.0.1 which fails both the
HTTPS scheme check and literal-IP SSRF guard. Add testSkipDownloadValidation
flag (same pattern as existing withTestDownloadHosts) to skip full URL
validation in test context.

* fix(workstation): address Claude review findings — tenant isolation + pool leak + dead code

- Activity list: add workstation ownership check before listing
  (prevents cross-tenant activity enumeration via known UUID)
- SSH pool: clean up p.sem + p.circuits maps in CloseWorkstation,
  prune, and Close to prevent unbounded map growth
- RPC handlers: return ErrInvalidRequest on JSON unmarshal failure
  instead of silently using zero-value params
- Remove unused containsControlChars function in normalize.go
- HTTP tests: add 10s context timeout to prevent CI package timeout

* fix(workstation): DefaultEnv JSON parse, backend cache leak, perm ownership check

- DefaultEnv: replace KEY=VALUE text parse with json.Unmarshal (stored as
  JSON by HTTP handler, was silently ignored)
- BackendCache: close losing backend on concurrent cache miss to prevent
  pruneLoop goroutine leak
- Backend interface: add Close() error method; SSHBackend delegates to
  pool.Close()
- handlePermList: add wsStore.GetByID ownership check (prevents cross-tenant
  UUID enumeration returning empty array vs 404)
- scanRows: log scan errors instead of silently skipping

* fix(workstation): wire activity sink shutdown + remove misleading comment

- WireActivitySink: capture cleanup func, register in gateway shutdown
  (was discarded → retention goroutine leaked + buffered rows lost)
- Add Stop() to WorkstationActivityStore interface (PG+SQLite already had it)
- wireWorkstationTools returns cleanup func; gateway.go defers it
- Remove misleading "re-validate env" comment in allowlist.go Check()

* ci: bump unit test timeout from 90s to 120s

hooks/handlers package (goja script tests) consumes ~85s on cold CI
runners, leaving insufficient headroom for HTTP retry tests with 1s
backoff. 120s provides adequate breathing room without masking real
deadlocks.

* fix: compile errors in integration tests + allowlist docstring

- packages_update_test: add missing lockKey arg to registry.Apply
- mcp_grant_revoke_test: remove unused fakeMCPClient struct
- allowlist.go: fix Check() docstring to match actual 3-step pipeline

* fix(test): relax mcp grant revoke assertion for pre-Phase02 state

Execute-time grant checking not yet wired — test correctly gets an
error but the message is "no active client" (nil clientPtr) rather
than "grant revoked". Accept any error as valid regression guard.

* chore: trigger CI on digitopvn/goclaw fork

* ci: retrigger workflows

* fix(permissions): classify workstation methods in RBAC policy
2026-05-11 14:58:19 +07:00
Duy /zuey/ 2da52cfaee feat(providers): add Google Cloud Vertex AI provider (#5)
* feat(providers): add Google Cloud Vertex AI provider (#576)

Add `vertex` built-in provider type that routes Gemini calls through Google
Cloud Vertex AI's OpenAI-compatible endpoint. Enterprises on GCP can now use
regional endpoints for data residency, consolidate AI spend under existing GCP
billing, enforce IAM/VPC-SC controls, and use committed-use discounts instead
of standalone Google AI Studio API keys.

Implementation reuses OpenAIProvider via the OpenAI-compat path; the only
provider-specific logic is OAuth2 auth wiring:

- New factory NewVertexProvider in internal/providers/vertex.go builds an
  *http.Client with oauth2.Transport, which auto-refreshes GCP access tokens
  (1-hour lifetime) transparently. Credentials precedence:
  inline SA JSON > credentials_file path > Application Default Credentials
  (works on GKE/Cloud Run/Compute Engine via metadata server).
- OpenAIProvider gets WithHTTPClient() + WithoutAuthHeader() options so the
  oauth2 transport injects Authorization rather than doRequest() setting a
  static Bearer header.
- Endpoint URL computed at registration time from project_id + region:
  https://{region}-aiplatform.googleapis.com/v1/projects/{p}/locations/{r}/endpoints/openapi
- Store: api_key column holds AES-256-GCM-encrypted SA JSON (same as other
  providers); settings JSONB holds {project_id, region, model}.
- Env vars: GOCLAW_VERTEX_{API_KEY,CREDENTIALS_FILE,PROJECT_ID,REGION,MODEL}.

Registration wired through all three paths: config-driven startup, DB-driven
startup, and HTTP CRUD in-memory registration. Vertex handled before the
generic "api_key empty" guard so ADC deployments register correctly.

Code-review fixes applied:

- H1 (correctness): Gemini thought_signature detection in openai.go now
  recognizes providerType="vertex" and apiBase suffix "aiplatform". Previously
  only worked because the default model string coincidentally contained
  "gemini"; custom model IDs or fine-tuned endpoint numeric IDs would drop the
  signature on passback and trigger HTTP 400 mid-tool-loop. Regression test
  added (TestVertexProviderForwardsThoughtSignatureOnToolCalls).
- M1 (hardening): region and project_id are regex-validated before URL
  concatenation to prevent hostname injection (e.g. region="evil.com/a?").
- M2 (hardening): APIBaseOverride must be https + *.googleapis.com host to
  prevent data exfiltration via crafted DB rows.
- M3 (documentation): CredentialsFile marked operator-only in the struct
  comment — never expose via admin UI or DB settings without path allow-list.

Tests: 17 Vertex-related unit tests. go build ./... + go build -tags sqliteonly
./... + go vet ./... all clean. Pre-existing TestSignMediaPath failure on
Windows (file_token.go uses path/filepath) is unrelated to this change.

* chore: trigger CI on digitopvn/goclaw fork

* ci: ping

* ci: retrigger workflows
2026-05-11 12:54:05 +07:00
Plateau Nguyenandviettranx 5cca9d7ac6 refactor(pancake): simplify private-reply to stateless DM (#951)
* feat(pancake): private-reply funnel — 2 modes, scope filter, DB dedup, metrics

Refactor the pancake channel's one-time DM feature from the 5-day-old minimal
`first_inbox` toggle into a complete comment → DM funnel. Breaking rename
(first_inbox → private_reply) matches Pancake + Facebook Graph terminology.

Channel logic
- Two modes: `after_reply` (default — public reply then DM) and `standalone`
  (DM-only; bypasses keyword filter, publishes synthetic outbound via
  `ch.Bus().PublishOutbound` so the LLM pipeline is skipped for template DMs).
- Post-level scope filter (`private_reply_options.allow_post_ids` /
  `deny_post_ids`, deny beats allow).
- Template vars `{{commenter_name}}` / `{{post_title}}` with pre-sanitized
  literal-replace (strips `{{`/`}}` from values — no nested substitution).
- Locale-aware default text via `i18n.T(locale, MsgPancakePrivateReplyDefault)`
  with en/vi/zh catalogs and English fallback.

DB dedup (replaces in-memory sync.Map)
- New `store.PancakePrivateReplyStore` (PG + SQLite impls), tenant-scoped via
  `store.TenantIDFromContext`, fail-closed on missing tenant.
- Atomic `TryClaim` / `Unclaim` (INSERT ... ON CONFLICT DO UPDATE WHERE stale)
  eliminates the concurrent-comment TOCTOU that would have fired duplicate
  DMs — claim first, release on API failure so the next comment can retry.
- Configurable TTL (`private_reply_ttl_days`, default 7).
- Dual-DB: migration 000056 on PG; schema v25 on SQLite.
  Bumps RequiredSchemaVersion 55 → 56.

Wiring
- `pancake.FactoryWithStores(ppReplyStore)` follows the existing
  `FactoryWithStores*` pattern (telegram, discord, whatsapp) and replaces
  `pancake.Factory` in `cmd/gateway.go`.
- `Channel.Send` injects `store.WithTenantID(ctx, ch.TenantID())` before
  dispatch so the outbound path sees a non-nil tenant (regression guard:
  without this the new store fails-closed and drops DMs in production — unit
  fakes masked this by bypassing the tenant check).
- Routing metadata whitelist extended: `private_reply_mode`,
  `private_reply_only`, `post_id`, `display_name` survive inbound → outbound.

Observability
- New `internal/metrics` package with `pancake_private_reply_total{
  page_id, result, reason}` counter and `/metrics` endpoint mounted on the
  gateway.
- Endpoint gated behind Bearer auth when `gateway.token` is configured; open
  (with warning) when unset for local dev scraping. Operators should set a
  token or bind `GOCLAW_HOST=127.0.0.1` to avoid leaking page identifiers.

UI
- 6 additive Pancake fields in `channel-schemas.ts` with `showWhen` gating on
  `features.private_reply` (itself gated on platform = fb/ig).
- 10 schema tests; full web build passes.
- i18n entries in en/vi/zh `channels.json` + 2 new Go keys
  (`MsgPancakePrivateReplyWindowExpired`, `MsgPancakePrivateReplyDefault`).

Tests
- 18 unit tests in pancake package (render/filter/mode switch/scope/dedup/
  metrics/tenant-ctx regression); 6 store-level tests; 5 E2E tests against
  live PG covering happy path, dedup-across-restart, TTL expiry via SQL,
  scope filter, FB 7-day policy error → no claim retained.
- All green on both PG and SQLite build tags with race detector.

* refactor(pancake): simplify private-reply to stateless DM

Roll back the dedup funnel from PR 951 — no table, no in-memory state,
no metrics, no modes, no scope filter. Keep only feature flag + message
template.

Rationale: each platform owning its own dedup table is an anti-pattern
that inflates multi-tenant data footprint. Use the platform itself
(Facebook Graph /comment/private_replies is per-comment idempotent)
combined with the existing webhook comment_id dedup in
comment_handler.go. Zero GoClaw state required.

Removed:
- DB table pancake_private_reply_sent (PG migration 000056, SQLite v25)
- Store interface + PG + SQLite implementations + tests
- internal/metrics package + /metrics endpoint + prometheus dep
- Two modes (after_reply/standalone) + standalone fast-path
- Scope filter (allow/deny post IDs) + PrivateReplyOptions struct
- TTL config (private_reply_ttl_days)
- Locale-aware i18n default (MsgPancakePrivateReplyDefault,
  MsgPancakePrivateReplyWindowExpired)
- FactoryWithStores wiring + tenant ctx injection in Send()
- SetHTTPClientForTest dead code

Kept:
- features.private_reply flag
- private_reply_message template with {{commenter_name}} and
  {{post_title}} vars (literal-replace, injection-safe)
- After-reply flow: comment -> public reply -> DM
- Hard-coded English fallback when message is empty

Schema: RequiredSchemaVersion 56 -> 55; SQLite SchemaVersion 25 -> 24;
schema.sql DDL block removed.

Net delta: -2661 lines across 44 files. Build (PG + sqliteonly) +
vet + race tests clean.

* test(agent): bump none-mode prompt size budget to 3100

vault_read wiring (#948) added ~95 chars to read_file tool summary,
pushing none-mode prompt from <3000 to 3075 chars. Bump budget to
3100 (~775 tokens) to match the intentional addition.

* test(integration): ensure data_migrations table exists in reset helper

The reset helper runs before RunPendingHooks, but RunPendingHooks is
what normally creates data_migrations. On a fresh CI database the
DELETE fails with 'relation does not exist'. Create the table
defensively so reset works regardless of execution order.

* chore(sqlite): remove accidental trailing blank line in schema.sql

---------

Co-authored-by: viettranx <viettranx@gmail.com>
2026-04-24 07:41:38 +07:00
viettranx 48335d7797 feat(pruning)!: faithful port of TS context pruning + backfill migration
Port goclaw context pruning to match upstream TS design in
openclaw/src/agents/pi-hooks/context-pruning/:

- Opt-in default: prune only when mode="cache-ttl" (was opt-out)
- Remove Pass 0 per-result 30% guard (duplicated Pass 1 with different
  suffix, caused wobble)
- Dedupe double prune call per iteration: PruneStage owns the single
  entry point; loop_history only runs limitHistoryTurns + sanitizeHistory
- Add cache-TTL gate for Anthropic prompt cache: skip prune while cache
  is live, scoped per-session via sync.Map
- Add context.pruned event emission for observability
- Configurable TTL as Go duration string ("5m", "30s")

BREAKING CHANGE: context pruning now opt-in. Add
contextPruning.mode: "cache-ttl" to config.agents.defaults to restore.

Migration 51 / SQLite v19 backfills mode="cache-ttl" for agents with
existing custom context_pruning config missing the mode field, so
previously-configured agents keep pruning after the opt-in flip.
NULL configs stay NULL (new opt-in default applies).

Web UI adds Cache TTL input + toggle wiring mode to cache-ttl/off.
2026-04-15 11:24:57 +07:00
Thieu Nguyenandviettranx d9dffc66e7 docs: restructure README — hub-and-spoke pattern, fix accuracy (#285)
* docs: restructure README with i18n, static diagrams, and accuracy fixes

Restructure:
- Reduce README from ~950 to ~280 lines (hub-and-spoke pattern)
- Move Project Status to CHANGELOG.md
- Add hero section: centered title, tagline, description, badges
- Add nav links: Documentation, Quick Start, Twitter/X (OpenFang pattern)
- Add Documentation table linking all docs.goclaw.sh sections
- Replace 3 mermaid diagrams with static images for consistent rendering

Accuracy fixes:
- Fix provider count: 13+ → 20+ (verified from gateway_providers.go)
- Fix "5-layer defense" → 5-layer permission system per policy.go
- Add missing features: Heartbeat, Scheduling/Cron, Observability
- Fix all docs URLs to docs.goclaw.sh hash routing
- Add VPS $5 minimum 2GB RAM note for Docker

Internationalization (30 languages, ZeroClaw coverage):
zh-CN, ja, ko, vi, tl, es, pt, it, de, fr, ar, hi, ru, bn, he,
pl, cs, nl, tr, uk, id, th, ur, ro, sv, el, hu, fi, da, nb
- Keep product feature names in English: Extended Thinking, Heartbeat
- Vietnamese README hand-tuned for natural phrasing

* docs: move translated READMEs to _readmes/ directory

Keep all 30 language translations but declutter the project root.
Fix relative links (_statics/, cross-language, main README) for new location.
Update main README language selector to point to _readmes/.

---------

Co-authored-by: viettranx <viettranx@gmail.com>
2026-03-20 07:10:47 +07:00