* feat(my-skills): add keyword search, namespace filter and clickable pagination
Add comprehensive filtering and search capabilities to the My Skills page:
- Keyword search: search by skill name, slug, or description
- Namespace filter: filter skills by namespace
- Clickable pagination: page number buttons with smart ellipsis
- State preservation: sync search state to URL, restore when returning from detail page
- Debounced search: 300ms debounce to avoid excessive queries
- Fix: hide stale rejected preview badge when newer version is published
Backend changes:
- MySkillAppService: add keyword and namespace filtering logic
- SkillLifecycleProjectionService: only show preview versions newer than published
- MeController: add keyword and namespace query parameters
- 6 new test cases covering search and filter scenarios
Frontend changes:
- my-skills.tsx: search input, namespace dropdown, URL state sync
- pagination.tsx: clickable page numbers with ellipsis
- use-user-queries.ts: prevent flicker on query transitions
- skill-detail.tsx: remove invalid rejected badge display
- router.tsx: URL parameter validation
- i18n: add search-related translation keys
Synced from SAAS commits:
- 939fa749 (feat: search and filters)
- dc14df6c (fix: search flicker)
- 0168ea81 (fix: rejected badge)
- c9eefa93 (fix: stale preview)
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
* fix(tests): address test failures in PR #493
Backend test fixes:
- Remove unnecessary Mockito stubbing for filtered-out skills
- Add missing findBySkillIdAndStatus stub for published version lookup
- Update MeController test mocks to match new method signature (keyword, namespace params)
Frontend fixes:
- Fix TypeScript error: useMyNamespaces returns ManagedNamespace[] not PagedResponse
- Add type annotation for namespace map callback parameter
E2E test fix:
- Update URL regex to allow query parameters (returnTo from search page)
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
* fix(tests): resolve test failures in PR #493
Backend:
- Remove unnecessary mock stubbings for skillId 2 and 3 in MySkillAppServiceTest.listMySkills_combinesKeywordNamespaceAndStatusFilters
- The test filters results to only return skill with id=1, so mocks for id 2 and 3 were never called, causing UnnecessaryStubbingException
Frontend:
- Add missing mocks for useLocation, useSearch, useMyNamespaces, and useDebounce in my-skills.test.ts
- MySkillsPage component uses these hooks but the test setup didn't provide mocks, causing 'No QueryClient set' and 'No export' errors
All 4 frontend tests now pass locally.
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---------
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
The PG FK constraint fk_skill_latest_version blocks deleting a
SkillVersion whenever Skill.latest_version_id still references it.
Two services had the wrong order:
- SkillPublishService.deleteReplaceableVersionArtifacts: triggered
when re-uploading the same version (UPLOADED -> overwritten).
Reproduced by AstronClaw client retrying personal-skills upload.
- SkillGovernanceService.deleteVersion: triggered when admin deletes
a draft version that happens to be skill.latest_version_id.
Fix: clear skill.latest_version_id and flush BEFORE deleting the
SkillVersion row, so PG sees no live reference at delete time.
Synced from SAAS commit 4626f0c117d9c0544c4dc1115c3aac7468f0d277
* feat(cli,domain): support non-global namespace skill download
Parse namespace from skill name using -- separator (e.g.,
astroclaw--api-gateway) so users don't need --namespace flag.
Allow anonymous download for any PUBLIC skill regardless of namespace.
CLI changes:
- Add cli/src/shared/skill-name-parser.ts utility
- Update install and remove commands to parse skill name argument
- 10 unit tests covering edge cases
Domain changes:
- SkillDownloadService.isAnonymousDownloadAllowed: drop namespace
type check, only require PUBLIC visibility
- Update test to expect success for team-namespace public skill
Synced from SAAS commit 26c67e31b1221249cf9b73321d1b726d8ba6e6df
* fix(cli): use bun:test instead of vitest in skill-name-parser test
## Problem
Audit log timestamps displayed 8 hours later than actual time when JVM
default timezone != UTC. Root cause: `audit_log.created_at` was
`TIMESTAMP without time zone`, and `rs.getTimestamp()` interprets bare
values using JVM timezone.
## Solution
### Backend
- **V42 migration**: Upgrade `audit_log.created_at` from `TIMESTAMP` to
`TIMESTAMPTZ`, anchor historical data as UTC via `USING created_at AT
TIME ZONE 'UTC'` (same pattern as V18/V19/V23/V25/V36)
- **Read path**: `AdminAuditLogAppService.readInstant()` uses
`rs.getObject(col, OffsetDateTime.class).toInstant()`, result
independent of JVM timezone
- **Write path (filter params)**: `startTime`/`endTime` binding changed
from `Timestamp.from()` to `OffsetDateTime.ofInstant(instant,
ZoneOffset.UTC)` via `toUtcOffsetDateTime()` helper, symmetric with
read path
### Migration Safety
- `SET LOCAL lock_timeout = '30s'` (transaction-scoped, won't leak to pool)
- `DO $$ ... IF data_type = 'timestamp without time zone' THEN ... ELSE
... END $$` idempotent guard with dual-branch `RAISE NOTICE`
- Safe retry: re-running won't double-apply `AT TIME ZONE 'UTC'`
### Test Coverage (10 tests, 477 total suite)
- `rowMapper_readsCreatedAtAsInstant` — UTC offset regression
- `rowMapper_normalisesNonUtcOffsetToInstant` — Non-UTC offset (+08:00)
- `rowMapper_returnsNullTimestampWhenColumnIsNull` — Null path
- `rowMapper_isIndependentOfJvmDefaultTimezone` — JVM TZ=Asia/Shanghai
drift prevention with `verify(rs, never()).getTimestamp()`
- `@ParameterizedTest buildWhereClause_bindsTimeRangeAsOffsetDateTime` —
3 cases (both/startOnly/endOnly) for filter param binding
- `@BeforeEach setUp()` — Mock isolation to prevent cross-test stub
accumulation
## Quality Gates
- [x] `make test-backend-app` passes (477 tests, 0 failures)
- [x] No Controller changes, `make generate-api` not needed
- [x] No frontend changes, typecheck/lint/e2e not needed
## Deployment
V42 must run before new code (guaranteed by Spring Boot startup sequence
→ Flyway executes before app accepts traffic). Rolling deployment:
- New pod + migrated column: correct
- Old pod + migrated column: old code reads TIMESTAMPTZ correctly (pgjdbc
returns absolute instant)
## Related Docs
- `docs/15-backend-time-governance-plan.md` §3.1: V42 progress registered
- `docs/16-backend-time-inventory.md` §3.1: V42 listed
- Same migration pattern: V18/V19/V23/V25/V36
* refactor(cli): improve publish-cli script reliability
- Move version computation and pre-flight checks before build-and-test
to fail fast on conflicts (existing branch/tag) instead of wasting
minutes on lint/test/build
- Add INT/TERM signal handlers to cleanup trap so Ctrl+C during build
properly restores working tree state
- Update Makefile help text to reflect PR-based workflow
* fix(cli): use git checkout -f for robust cleanup
Address code review feedback from gemini-code-assist bot:
- Use `git checkout -f` in on-release and committed cleanup stages
to ensure reliable branch switching even when files are staged
but not committed (e.g., interrupted after `git add` but before
`git commit`)
- Remove redundant `git checkout -- <file>` in on-release stage
since `-f` already discards all local changes
This prevents cleanup failures when the script is interrupted
between staging and committing.
* fix(cli): address PR #441 review findings
- Fix ERR trap bypass: remove `if !` wrapper around `gh pr create` so
set -e triggers the trap and prints pushed-stage recovery instructions
- Fix command injection: all node -e/-p calls now use process.env
instead of interpolating shell variables into JS string literals
- Rewrite cli/RELEASE.md to document the new PR-based release flow
- Rewrite scripts/tests/publish-cli-test.sh with 10 tests covering
the new flow (stubs for bun/gh, pre-flight checks, happy path,
cleanup state machine stages)
* fix(cli): address PR #441 review findings from @dongmucat
- Bind release tag to origin/main: PR body, end-of-run hint, and
cli/RELEASE.md now use `git tag $TAG origin/main` so the tag is
always placed on the merged commit, regardless of local branch state
- Reject prerelease tags in version computation: if the latest cli-v*
tag contains non-X.Y.Z characters (e.g., -rc.1), exit with a clear
message instead of crashing in node parsing
- Add pr-scripts.yml workflow: runs publish-cli-test.sh on scripts/**
changes so the release script regression suite gates PRs
- Add Test 11 covering prerelease tag rejection
* fix(cli): compute publish baseline from origin tags only
A failed `git push origin cli-vX.Y.Z` after a successful local tag
leaves an orphan tag locally. The previous `git tag --list` baseline
would then treat it as the latest release, causing skipped versions or
publishes based on an unreleased tag.
Switch to `git ls-remote --tags --refs origin 'cli-v*' | sort -V` so
the baseline reflects only what is actually on origin. Local orphan
tags can still collide with the computed target tag, which fails fast
with a clear message as before.
Adds test 12 covering the orphan-tag scenario.
Vitest <4.1.0 allows arbitrary file read/execution when the UI server is
listening (GHSA-5xrq-8626-4rwp, severity: critical). Bumps vitest from
3.2.4 to 4.1.x, which also flows through to the bundled @vitest/* packages
in pnpm-lock.yaml.
Adjusts two tests for the stricter v4 mock contract: `new`-callable mocks
must be backed by a `function`/`class` implementation rather than an
arrow function (web/src/shared/lib/date-time.test.ts,
web/src/app/providers.test.ts).
Signed-off-by: dongmucat <1127093059@qq.com>
Use `namespace === 'global'` (without @ prefix) to match the actual
route parameter value. The previous check used '@global' which never
matched, causing anonymous users to be redirected to login even for
global PUBLIC skills.
Co-authored-by: dongmucat <1127093059qq.com>
* fix(runtime): pass auth environment variables to containers
The web container's envsubst in 30-runtime-config.sh only substituted
SKILLHUB_WEB_API_BASE_URL and SKILLHUB_PUBLIC_BASE_URL, leaving auth-related
variables (authDirectEnabled, authSessionBootstrapEnabled, etc.) as literal
${...} strings in runtime-config.js. Additionally, compose.release.yml did not
pass SKILLHUB_WEB_AUTH_DIRECT_ENABLED or SKILLHUB_WEB_AUTH_DIRECT_PROVIDER to
the web container, nor SKILLHUB_AUTH_DIRECT_ENABLED to the server container.
This made it impossible to enable direct (username/password) authentication
for intranet deployments without OAuth2, even though the frontend template and
backend already supported it.
Changes:
- compose.release.yml: add SKILLHUB_AUTH_DIRECT_ENABLED to server env
- compose.release.yml: add auth direct and session bootstrap vars to web env
- 30-runtime-config.sh: expand envsubst to cover all runtime-config.js template variables
- .env.release.example: document the new auth configuration variables
All new variables default to false/empty, preserving existing GitHub OAuth behavior.
* fix: remove session bootstrap frontend config from compose
Per reviewer feedback: exposing SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_* in the
compose without matching SKILLHUB_AUTH_SESSION_BOOTSTRAP_ENABLED on the server
would cause 403 errors when frontend attempts bootstrap.
Keep this PR focused on direct auth only. Bootstrap variables are still handled
in 30-runtime-config.sh with false defaults, so runtime-config.js will have
authSessionBootstrapEnabled: "false" and frontend will not trigger bootstrap.
---------
Co-authored-by: wowo <zhenggui5228@126.com>
Co-authored-by: PR Review Helper <review-helper@local>
* fix(auth): use SimpleUrlAuthenticationSuccessHandler for OAuth2 login
Replace SavedRequestAwareAuthenticationSuccessHandler with
SimpleUrlAuthenticationSuccessHandler to prevent redirecting to
saved API requests after OAuth2 login.
Previously, when a user accessed a protected API endpoint (e.g.,
/api/web/skills) without authentication, Spring Security would save
that request. After OAuth2 login, the handler would redirect back to
the API endpoint instead of the dashboard.
Now the handler only uses:
- returnTo parameter from session (if present)
- default target URL (/dashboard) as fallback
* test(auth): add regression for OAuth2 success redirect; restore clearAuthenticationAttributes
Cover the no-returnTo + cached-API-request branch with HttpSessionRequestCache so
the original bug (post-login redirect resolving to /api/web/skills) cannot be
silently reintroduced. Also restore clearAuthenticationAttributes() in the
returnTo branch so it stays symmetric with the default branch (super clears it).
---------
Co-authored-by: xiose <huyanlin@nuaa.edu.cn>
Resolve historical naming drift between protocol spec and CLI by adopting the
plural form across both docs:
- docs/07-skill-protocol.md: drop the drift caveat; the four-tier priority is
now stated as .agents/skills / ~/.agents/skills / .claude/skills /
~/.claude/skills directly.
- docs/00-product-direction.md: align with the same plural form.
The CLI already uses .agents/skills (cli/src/agents/profiles/generic-fallback.ts
and cli/src/agents/resolver.ts). No code change required.
Covers the new "access denied — token may lack required scope" error
path with a fake-registry 'forbidden' failure mode. Prevents the
improved 403 message from regressing silently.
Disable automated PR review by Gemini Code Assist for GitHub by removing
.gemini/config.yaml. The repository will no longer trigger Gemini-based
PR summaries or review comments.
- Add ariaLabel prop to CopyButton for screen reader differentiation
- Pass per-user aria-label: "Copy user ID for {username}"
- Add truncation for long userIds (max-w-[14rem] + title tooltip)
- Scope E2E copy-button assertions to userId cell to avoid false positives
- Assert on span.font-mono for userId text to exclude button text
- Use toHaveText instead of getByRole name for "Copied" feedback check
- Fix test fixture: warnings-only response now uses valid=false to
match real backend behavior (warnings make dry-run invalid)
- Distinguish 403 from 401 in CLI error messages: 403 now says
"access denied — token may lack required scope" with a hint to
regenerate the token, rather than the generic "authentication failed"
- Fix race condition: use Promise.all for goto + waitForResponse
- Remove all waitForTimeout calls, use explicit assertions/waitForResponse
- Assert clipboard content equals the actual userId (not just non-empty)
- Fix unused variable (userIdText) that would fail lint --max-warnings 0
- Trigger real search via button click instead of just filling input
- Add status filter test to cover the filter path
- Add comment explaining mock-profile approach for admin session
ApiTokenAuthenticationFilter authenticates /api/cli/** Bearer tokens
but ApiTokenScopeFilter.shouldNotFilter() previously skipped them.
The result: API token requests on CLI routes were authenticated and
authorization-policy-checked, but scope enforcement never ran. Tokens
without skill:publish or skill:delete could call /publish, /publish/validate,
and DELETE despite the policy table requiring those scopes.
Add /api/cli/ to the scope filter's covered prefixes and a filter-level
test that confirms a token missing skill:publish is rejected on the new
validate endpoint. Update the existing CLI controller tests to grant
the appropriate SCOPE_* authorities to their api_token principals so
they continue to pass under enforced scopes.
Add a userId column with one-click copy functionality to the admin
user management table to help administrators easily access user IDs
for batch operations like namespace member management.
Changes:
- Add userId column after username in admin users table
- Implement one-click copy button for each userId
- Add i18n translations for column header (en/zh)
- Add comprehensive E2E tests (6 test cases)
Closes#426
Fix three blockers and one contract drift issue surfaced in code review:
1. API token policy: add skill:publish scope policy and authentication
policy for /api/cli/v1/skills/*/publish/validate. Without these the
AntPathMatcher pattern /publish would not cover /publish/validate,
so Bearer-token requests would be rejected by the scope filter.
2. Warnings semantics: dry-run now treats warnings as making valid=false.
The CLI publish flow uses confirmWarnings=false, so the real publish
rejects any warnings; dry-run must mirror that to avoid false positives.
3. Visibility parameter: validate endpoint now accepts the same
visibility multipart field as publish. The CLI forwards --visibility
so invalid values are caught at dry-run time rather than at publish.
4. Schema drift: resolvedSlug and resolvedVersion are nullable in
practice (returned as null when validation fails before resolution).
Updated schema.d.ts to reflect string | null instead of optional string.
Tests added:
- RouteSecurityPolicyRegistryTest: validate endpoint scope check
- CliDryRunValidateTest: custom + invalid visibility cases
- publish-dry-run.test.ts: --visibility forwarded to server
- Exit non-zero (code 6) when --dry-run validation fails, enabling
CI/CD pipeline integration
- Add archived skill check: dry-run now detects when the publisher's
own skill is archived
- Add version-exists check: dry-run now detects when the resolved
version is already published
- Use StandardCharsets.UTF_8 for SKILL.md content parsing
Add a validate-only endpoint (POST /api/cli/v1/skills/{namespace}/publish/validate)
that runs the full pre-publish validation chain without persisting anything.
This allows developers to check their package locally before actual publishing.
The validation covers:
- SKILL.md existence and frontmatter parsing (name, description required)
- File extension whitelist and size limits
- Credential leak scanning with line-number precision
- Slug generation and name conflict detection
CLI usage: `skillhub publish <path> --dry-run`
Closes#429
When --agent is provided without --scope, scope was inferred via
root.startsWith(cwd), which mislabels user roots as project when
cwd === home. Use profile.userRoots(home) membership instead, so the
candidate scope reflects the profile's intent rather than path prefix
overlap. The chosen root path itself is unchanged.
- Distinguish user vs project install scope via explicit --scope flag
- Interactive mode prompts for scope when --scope/--agent/--dir not provided
- Non-interactive bare install preserves existing behavior (backward compatible)
- Mutual exclusion: --dir cannot be combined with --scope or --agent
- Symmetric fallback: --scope user falls back to ~/.agents/skills,
--scope project falls back to <cwd>/.agents/skills
- Strict TTY check requires both stdin and stdout TTY plus no --json
- Scope-aware candidate generation avoids root.startsWith(cwd) misjudgement
when cwd === home or paths overlap
- Correct gemini-cli (.gemini/skills) and kiro-cli (.kiro/skills) paths
in install path tables across README and guide docs
- Note CLI fallback uses .agents/skills (with s) in skill protocol doc