Downloading a skill whose slug is non-ASCII (e.g. a Chinese name) through
the ClawHub CLI compatibility route failed: the 302 Location header was
built by string-concatenating the raw slug, and Tomcat encodes header
values as ISO-8859-1, so a character outside 0-255 makes it drop the
Location header entirely and the download breaks. The skillhub CLI path
was unaffected because it doesn't go through this redirect.
Build the Location with UriComponentsBuilder.pathSegment(...).encode(), so
each segment is percent-encoded while the '/' separators stay literal.
"需求" becomes %E9%9C%80%E6%B1%82 and the header is ISO-8859-1-writable.
Fixes#658
Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
Remove terminal review tasks before deleting an allowed skill version. Lock all versions of the aggregate in stable order so concurrent deletes preserve the last-version invariant and return business errors instead of 500 responses.
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
* fix(api): tell callers why a request was forbidden
The scope filter already computes an exact reason ("Missing API token
scope: skill:delete", "API token cannot access endpoint: /x") and the
access-denied handler discarded it, returning a bare "Forbidden" for
every case: missing scope, endpoint closed to API tokens, and paths
that simply don't exist. Clients cannot tell those apart, so they
guess — the published CLI reports every 403 as "token may lack
required scope", which sent us debugging token scopes for an hour when
the real causes were a revoked token and a mistyped namespace path.
The reason now rides in the response via a new error.forbidden.detail
message (en + zh), and is logged alongside the exception type.
Signed-off-by: Gal Eyal <gal.e@popai.health>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(api): safely expose API token denial reasons
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---------
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
The @Async rebuildSkill fix relied on a fresh thread giving a clean
transaction boundary. But skillhubEventExecutor uses CallerRunsPolicy:
under saturation the rejected task runs on the caller (request) thread,
back inside the afterCommit synchronization phase — the original failure
context where the @Transactional index write is silently dropped.
Mark SearchIndexService.index as REQUIRES_NEW so it always suspends any
lingering post-commit synchronization and commits in its own transaction,
independent of whether the async dispatch actually happened.
Add regression tests: detach removes the label keyword, and a synchronous
rebuild inside the afterCommit phase still persists the document (fails
without REQUIRES_NEW).
Signed-off-by: shychee <shychee96@gmail.com>
Attaching or detaching a skill label triggers a search index rebuild via
an afterCommit callback. Because LabelSearchSyncService.rebuildSkill ran
synchronously on the request thread, the @Transactional index write
executed inside the already-committed transaction-synchronization phase
and was silently dropped -- the search document was never written, so
label keywords never became searchable.
Move rebuildSkill onto the skillhubEventExecutor with @Async (matching the
existing rebuildSkills batch path) so the rebuild runs on a fresh thread
and transaction. Add an integration test that fails on the old synchronous
path and passes with the async fix.
Signed-off-by: shychee <shychee96@gmail.com>