* fix(gateway): admit operator.provision keys on tenants.create and tenants.users.add
The CVE #866 fail-closed hardening regressed operator.provision: the
role-only router check maps provision-only API keys to viewer and
rejects tenants.create / tenants.users.add with 'requires admin role',
even though the tenant handlers explicitly admit ScopeProvision on
exactly those two methods (issue #1524).
Restore the intended least-privilege provisioning path without any role
promotion: the router now allows credentials carrying ScopeProvision on
exactly the two tenant-provisioning RPCs (permissions.IsProvisionMethod).
Every other admin/write surface stays denied, and viewers without the
provision scope gain nothing.
Regression tests (internal/gateway/router_test.go) prove:
- provision-only succeeds on tenants.create and tenants.users.add
- provision-only stays denied on tenants.update and agents.create
- plain viewers stay denied on the provisioning methods
- unauthenticated clients stay denied
* refactor(gateway): route provisionScopeAllowed through permissions.HasProvisionScope
Address review feedback on #1584: HasProvisionScope was exported and
tested but had no production caller — the router used Client.HasScope
directly. Wire the router to the permissions helper so the provision
scope check has a single source of truth.
* 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>
Resolve the PR #8/#9/#10 stack on current dev, including Bitrix24 install callback hardening, migration renumbering, duplicate-domain fail-closed routing, UI textarea/mobile cleanup, and review hardening.
* refactor(codex-pool): remove redundant primary_first strategy
* test(tools): update tool schema fixtures to pointer form
* feat(create_image): route Codex pools through chain with member failover
Codex pool chain entries now iterate pool members per the pool's own
strategy (round_robin or priority_order) and fail over internally before
the outer chain advances to the next entry.
- ChatGPTOAuthRouter.GenerateImage implements NativeImageProvider:
iterates orderedProviders, tries each member, advances round-robin state
only on success, aggregates errors on pool exhaustion.
- media_provider_chain.wrapPoolProvider wraps a *CodexProvider in a router
when RoutingDefaults has extras or a non-primary-first strategy. Solo
Codex (no extras) stays unwrapped.
- Router exposes ProviderType() so chain telemetry records "chatgpt_oauth".
Closes#1008
* feat(ui/builtin-tools): pool badge on create_image chain entries
Chain entry card shows a read-only "Pool · <strategy>" badge when the
entry's provider is a Codex pool base. Strategy label translates in
en/vi/zh. No new toggle or selector — pool config lives on the provider
itself.
#1008
* refactor(providers): move Codex test helpers to providertest subpackage
Addresses code-review High finding: NewTestCodexProviderFast and its
staticTestTokenSource were exported from a non-_test.go file, compiling
into the production binary. Relocating to internal/providers/providertest/
keeps the helper importable from other packages' tests without leaking
test-only symbols into production.
Also tightens wrapPoolProvider — pools with zero extra members no longer
get wrapped in a router (KISS: nothing to rotate between).
- Added CodexProvider.WithRetryConfig as a legitimate fluent option (the
test helper now uses the public API).
- Dropped the internal-package test helper file.
#1008
* docs(pr-1006): add pool badge UI evidence
Force-UI style capture of the read-only "Pool · Round-robin" badge on
the create_image chain entry card when the selected provider carries
settings.codex_pool with extras. Captured against staging gateway.
* refactor(ui/builtin-tools): unify pool UX with Create Agent pattern
The create_image chain Provider dropdown was listing pool members alongside
pool owners, letting users accidentally bypass pool semantics by picking a
member directly. Matches the pattern already used by Create Agent: hide
pool members, show an inline "Pool" chip on owners.
- Filter pool members from the chain Provider dropdown via
getChatGPTOAuthPoolOwnership.ownerByMember.
- Inline "Pool" chip on owner options, reusing the existing
providers:list.poolBadge i18n key.
- Drop the separate card-level "Pool · <strategy>" badge — the dropdown chip
alone conveys the information without duplication.
- Remove the now-unused isPoolProvider / poolStrategyOf helpers and the
builtin.mediaChain.poolBadge* i18n keys we briefly introduced.
#1008
* docs(pr-1006): add pool-filtered dropdown screenshot
* fix(ui/builtin-tools): migrate stale pool-member chain entries on load
Red-team blocker: a chain saved before the pool-aware UI landed may
reference a pool member by name (e.g. openai-codex-2). After the
dropdown started hiding members, such an entry produced:
- empty Select trigger (no SelectItem matches the stored value)
- conflicting Row 1 label still showing the member name
- silent runtime misroute (bare solo call, no pool wrap)
parseInitialEntries now consults getChatGPTOAuthPoolOwnership and
rewrites any chain entry whose provider is a pool member to the owner's
name + id. The next save persists the migrated value. Safe no-op for
non-pool entries and for entries already pointing at an owner.
Also memoize enabledProviders in the parent form so the card's useMemo
boundaries actually hold (minor perf ding flagged by same review).
* docs(pr-1006): refresh HTML evidence to match filter-based UX
* fix(ui/agent-codex-pool): traffic policy reflects effective strategy under inherit
On the agent's OpenAI Account Pool page, when Agent routing mode is
Use Provider Defaults, the Traffic Policy buttons painted the draft's
placeholder strategy ("priority_order") as selected — contradicting the
top-of-page chip which already correctly shows the provider's effective
strategy. The removal of primary_first in this PR unmasked the latent
bug: the placeholder used to be a deprecated value that didn't match any
live button, so nothing appeared selected.
Derive selectedStrategy from defaultRouting when mode === "inherit":
the button highlight now mirrors what actually runs. Buttons remain
disabled in inherit mode (unchanged), but the displayed selection no
longer misleads the user.
* docs(pr-1006): add screenshot of inherit-mode Traffic Policy fix
* fix(permissions): remove duplicate MethodSessionsCompact entry
The writeExact slice listed MethodSessionsCompact twice. slices.Contains
still returned correct results so runtime behavior is unchanged, but the
duplicate entry was dead code.
Spotted during review of PR #1006.
---------
Co-authored-by: viettranx <edu@200lab.io>
Fixes a 3-vuln chain enabling unauthenticated/underprivileged access to
admin surfaces:
1. Auth bypass (router.go): invalid tokens silently fell through to
anonymous role. Now reject on token presence with invalid/expired.
2. Default-permit RBAC (policy.go): unknown methods returned a writable
role. Now fail-closed: unknown -> RoleNone.
3. Missing admin classifications: several sensitive methods weren't in
any allowlist and slipped through default-permit. Now explicitly
classified (admin/write/read).
Additional hardening:
- Remove isWriteMethod prefix fallbacks; use exact allowlist only to
prevent silent scope creep when new methods are added.
- Classify Phase-3 methods (TTS, browser, zalo, whatsapp) into
admin/write/read per sensitivity.
- Gate empty MethodRole in router dispatcher.
Tests:
- TestMethodRole_ApprovalsList_IsViewer: asserts exec.approval.list is
viewer and approve/deny are operator.
- TestMethodRole_DriftCoverage_AllProtocolMethodsClassified: AST-parses
pkg/protocol/methods.go at test time; asserts every Method* constant
resolves to a non-None role. Permanent drift guard for future adds.