Skip to content

feat: remote skill download and auto-sync - #468

Merged
shivammittal274 merged 16 commits into
mainfrom
remote-skill-updates
Mar 17, 2026
Merged

feat: remote skill download and auto-sync#468
shivammittal274 merged 16 commits into
mainfrom
remote-skill-updates

Conversation

@shivammittal274

@shivammittal274 shivammittal274 commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Default skills are fetched from a remote catalog (cdn.browseros.com/skills/v1/catalog.json) on first setup, with bundled fallback when offline
  • Background sync on startup + every 45 minutes checks for updates
  • Remote is the source of truth — if a skill exists in the catalog and the version is newer, it gets overwritten locally
  • User-created skills (IDs not in the catalog) are never touched
  • Catalog URL is configurable via SKILLS_CATALOG_URL env var

How it works

Sync logic (runs on startup + every 45 min):

For each skill in remote catalog:
  → Doesn't exist locally?       → Install
  → Exists, same version?        → Skip
  → Exists, different version?   → Overwrite (remote wins)

Skills not in the catalog?        → Never touched
CDN down?                         → Do nothing, try again next cycle

First install (empty skills dir):

Try CDN → install all 12 skills
CDN down → copy bundled defaults from binary

No manifest file, no content hashes. Version comparison reads directly from skill frontmatter.

Security

  • Path traversal protection via safeSkillDir (reused from service.ts)
  • Runtime validation of catalog JSON (top-level shape + each entry)
  • 1MB response size limit (Content-Length header + body check)
  • Per-skill error handling so one bad entry doesn't crash the sync

Files changed

File Change
apps/server/src/skills/remote-sync.ts New — fetch, sync, background timer
apps/server/src/skills/seed.ts Remote-first seeding with bundled fallback
apps/server/src/skills/service.ts Exported safeSkillDir
apps/server/src/skills/types.ts Added RemoteSkillEntry, RemoteSkillCatalog
apps/server/src/env.ts Added SKILLS_CATALOG_URL env var
apps/server/src/main.ts Start/stop sync on boot/shutdown
packages/shared/src/constants/limits.ts Added SKILLS_LIMITS.MAX_CATALOG_BYTES
packages/shared/src/constants/timeouts.ts Added SKILLS_FETCH, SKILLS_SYNC_INTERVAL
packages/shared/src/constants/urls.ts Added SKILLS_CATALOG default URL
scripts/upload-skills-catalog.ts New — generate + upload catalog to R2

Test plan

  • 17 unit tests (mocked fetch, all sync scenarios, path traversal, validation)
  • 5 E2E flow tests against live cdn.browseros.com
  • Real server test: fresh install seeded 12/12 from CDN
  • Real server test: returning startup syncs immediately on boot
  • TypeScript typecheck passes

Closes TKT-600

🤖 Generated with Claude Code

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.
@greptile-apps

greptile-apps Bot commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds remote skill download and background auto-sync. On first install, skills are fetched from cdn.browseros.com; if the CDN is unreachable, bundled defaults are written as a fallback. A background timer then syncs every 45 minutes, letting the remote catalog act as the source of truth for version-tracked skills while leaving user-created skills untouched.

Key changes:

  • remote-sync.ts — new module: catalog fetch, per-skill install/update logic, writeSkillFile, background setInterval wired through startSkillSync / stopSkillSync
  • seed.ts — remote-first seeding with a skillExists guard that correctly prevents the bundled fallback from overwriting skills already written by a partial remote seed
  • service.tssafeSkillDir exported so the path-traversal guard is reused by the new sync code
  • scripts/upload-skills-catalog.ts — developer script to generate and upload the catalog to R2

Issues found:

  • extractVersion is duplicated verbatim between upload-skills-catalog.ts and remote-sync.ts; the script should import it from the server module (or a shared location) to avoid the two copies silently drifting apart.
  • The E2E tests in flows.test.ts hardcode the expected skill count (12) and specific catalog skill IDs (summarize-page, save-page), making them brittle against any future catalog additions or renames.

Confidence Score: 4/5

  • Safe to merge with minor follow-up; no correctness or security regressions in the sync logic itself.
  • Path traversal is properly handled via the existing safeSkillDir guard. Partial-seed race conditions are mitigated by the skillExists check in the fallback path. The two flagged issues (duplicate extractVersion, brittle E2E assertions) are maintainability/test quality concerns that won't cause runtime failures today but should be cleaned up before the catalog grows further.
  • scripts/upload-skills-catalog.ts (duplicate helper) and tests/skills/flows.test.ts (hardcoded catalog assumptions)

Important Files Changed

Filename Overview
packages/browseros-agent/apps/server/src/skills/remote-sync.ts New file implementing catalog fetch, per-skill sync, remote seeding, and background timer. Path traversal is handled via safeSkillDir. No response size guard is applied to the buffered JSON body despite the PR description claiming one exists.
packages/browseros-agent/apps/server/src/skills/seed.ts Remote-first seeding with bundled fallback. The skillExists guard prevents bundled defaults from overwriting skills partially installed by a failing remote seed — correctly addresses the partial-failure scenario.
packages/browseros-agent/scripts/upload-skills-catalog.ts New catalog-generation and R2-upload script. Contains a verbatim duplicate of extractVersion from remote-sync.ts that should be shared, and the catalog-level version field is always hardcoded to 1.
packages/browseros-agent/apps/server/tests/skills/flows.test.ts E2E tests against the live CDN. Hardcoded skill count (12) and specific skill names (summarize-page, save-page) make the suite brittle against catalog changes.
packages/browseros-agent/apps/server/tests/skills/remote-sync.test.ts Comprehensive unit test suite with mocked fetch covering all sync scenarios including path traversal rejection, partial failures, version skipping, and user-created skill preservation.

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
Loading
Prompt To Fix All With AI
This 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

Comment thread packages/browseros-agent/apps/server/src/skills/remote-sync.ts Outdated
- 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.
@shivammittal274

Copy link
Copy Markdown
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
@shivammittal274

Copy link
Copy Markdown
Contributor Author

@greptile-ai review

Comment thread packages/browseros-agent/apps/server/src/skills/seed.ts
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.
@shivammittal274

Copy link
Copy Markdown
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.
@shivammittal274

Copy link
Copy Markdown
Contributor Author

@greptile-ai review

Comment thread packages/browseros-agent/apps/server/src/skills/remote-sync.ts Outdated
@shivammittal274

Copy link
Copy Markdown
Contributor Author

@greptile-ai review

@shivammittal274
shivammittal274 merged commit 59b00a6 into main Mar 17, 2026
4 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant