feat: remote skill download and auto-sync - #468
Merged
Conversation
Download default skills from remote catalog on first setup with bundled fallback when offline. Background sync every 45 minutes checks for new/updated skills without overwriting user-customized ones. Tracks installed defaults via content hashes in a local manifest file.
Add SKILLS_CATALOG_URL env var (following CODEGEN_SERVICE_URL pattern) with fallback to the default constant. Add script to generate catalog.json from bundled defaults for static hosting.
Add upload-skills-catalog.ts that generates and uploads catalog.json to Cloudflare R2 (same infra as existing build artifacts). Update default catalog URL to cdn.browseros.com/skills/v1/catalog.json.
Contributor
Greptile SummaryThis PR adds remote skill download and background auto-sync. On first install, skills are fetched from Key changes:
Issues found:
Confidence Score: 4/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A([Server start]) --> B[seedDefaultSkills]
B --> C{hasExistingSkills?}
C -- yes --> D([Skip seeding])
C -- no --> E[seedFromRemote]
E --> F{CDN reachable?}
F -- yes --> G[Write all catalog skills]
G --> H{all written?}
H -- yes --> I([Remote seed done])
H -- no / partial --> J[Bundled fallback\nskip already-installed]
F -- no --> J
J --> K([Bundled seed done])
A --> L[startSkillSync]
L --> M[runSync immediately]
L --> N[setInterval every 45 min]
M --> O[syncRemoteSkills]
N --> O
O --> P{Fetch catalog}
P -- fail --> Q([No-op, retry next cycle])
P -- success --> R[For each catalog skill]
R --> S{Exists locally?}
S -- no --> T[Install]
S -- yes, same version --> U[Skip]
S -- yes, different version --> V[Overwrite\nremote wins]
T & U & V --> R
Prompt To Fix All With AIThis is a comment left during a code review.
Path: packages/browseros-agent/scripts/upload-skills-catalog.ts
Line: 9-12
Comment:
**Duplicate `extractVersion` function**
This function is an exact copy of `extractVersion` in `apps/server/src/skills/remote-sync.ts` (lines 13–16). Having two independent copies means a future change to the version-parsing regex (e.g. to support semver ranges or quoted values) must be applied in two places — and they can silently drift apart.
The script already imports from the server package (`../apps/server/src/skills/types`), so it can import the function directly:
```typescript
import { extractVersion } from '../apps/server/src/skills/remote-sync'
```
If keeping a dependency on server internals from a script feels wrong, the function should be moved to a shared location (e.g. `packages/shared/src/utils/skill-version.ts`) and imported from both places.
**Rule Used:** Remove unused/dead code rather than leaving it in ... ([source](https://app.greptile.com/review/custom-context?memory=9b045db4-2630-428c-95b7-ccf048d34547))
**Learnt From**
[browseros-ai/BrowserOS-agent#126](https://github.com/browseros-ai/BrowserOS-agent/pull/126)
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/tests/skills/flows.test.ts
Line: 44-45
Comment:
**Hardcoded skill count makes test brittle**
`assert.strictEqual(skills.length, 12)` ties the test to today's exact catalog size. Any addition or removal of a default skill on the CDN will fail this assertion — including the next time the catalog is expanded — without any change to this test.
A more resilient approach is to assert based on what was actually returned by `seedFromRemote`:
```suggestion
assert.ok(skills.length > 0, 'Expected at least one skill to be seeded')
```
Or, if you want to verify the count is consistent with the remote response, fetch the catalog first and compare lengths. The other CDN-bound tests (`save-page`, `summarize-page`) have the same fragility — if those skill IDs are ever renamed in the catalog, the tests at lines 55 and 70–76 will break silently with confusing errors.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: e23e850 |
- Add path traversal protection via safeSkillDir in writeSkillFile and readSkillContent (reuses existing validation from service.ts) - Add runtime type guards for catalog JSON and manifest JSON parsing - Fix seedFromRemote to return false on partial failure so bundled fallback kicks in - Add per-skill error handling in syncRemoteSkills so one bad skill doesn't crash the entire sync - Wire stopSkillSync into Application.stop() shutdown path - Extract version from frontmatter in seedFromBundled instead of hardcoding '1.0' - Consolidate duplicated logic: reuse installSkill/writeSkillFile/ contentHash/saveManifest from remote-sync.ts in seed.ts - Extract shared catalog generation into scripts/catalog-utils.ts
Drop generate-skills-catalog.ts, catalog-utils.ts, and e2e-remote-sync.test.ts (covered by flows.test.ts). Inline catalog generation into upload-skills-catalog.ts.
Tests all 7 steps of the real server lifecycle: fresh seed from CDN, no-op sync, user edit preservation, skill reinstall, custom skill protection, background timer firing, and second startup skip.
Contributor
Author
|
@greptile-ai review |
…op saves - Validate individual skill entries in catalog (id, version, content must all be strings) not just the top-level shape - Add 1MB response size limit on catalog fetch to prevent resource exhaustion from compromised/misconfigured CDN - Skip manifest save when sync cycle had no changes (avoids unnecessary disk I/O every 45 minutes) - Share extractVersion via remote-sync.ts export, remove duplicate from seed.ts
Contributor
Author
|
@greptile-ai review |
When seedFromRemote partially fails, the bundled fallback now skips skills already in the manifest (installed by the partial remote seed). Also adds Content-Length early check before downloading the full catalog response body.
Contributor
Author
|
@greptile-ai review |
Previously the first sync fired 45 minutes after boot. Now startSkillSync runs one sync immediately so returning users get skill updates right away.
Remote catalog is the source of truth. If a skill exists in the catalog, its version is compared against local frontmatter and overwritten when newer. No manifest file, no content hashes. User-created skills (IDs not in catalog) are never touched.
Contributor
Author
|
@greptile-ai review |
Contributor
Author
|
@greptile-ai review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
cdn.browseros.com/skills/v1/catalog.json) on first setup, with bundled fallback when offlineSKILLS_CATALOG_URLenv varHow it works
Sync logic (runs on startup + every 45 min):
First install (empty skills dir):
No manifest file, no content hashes. Version comparison reads directly from skill frontmatter.
Security
safeSkillDir(reused from service.ts)Files changed
apps/server/src/skills/remote-sync.tsapps/server/src/skills/seed.tsapps/server/src/skills/service.tssafeSkillDirapps/server/src/skills/types.tsRemoteSkillEntry,RemoteSkillCatalogapps/server/src/env.tsSKILLS_CATALOG_URLenv varapps/server/src/main.tspackages/shared/src/constants/limits.tsSKILLS_LIMITS.MAX_CATALOG_BYTESpackages/shared/src/constants/timeouts.tsSKILLS_FETCH,SKILLS_SYNC_INTERVALpackages/shared/src/constants/urls.tsSKILLS_CATALOGdefault URLscripts/upload-skills-catalog.tsTest plan
cdn.browseros.comCloses TKT-600
🤖 Generated with Claude Code