Skip to content

v0.7.0 feat: Recruit — browse and install community agents#22

Merged
suncommit merged 19 commits into
mainfrom
feat0528/polish
Jun 2, 2026
Merged

v0.7.0 feat: Recruit — browse and install community agents#22
suncommit merged 19 commits into
mainfrom
feat0528/polish

Conversation

@suncommit
Copy link
Copy Markdown
Contributor

Summary

Ships the Recruit feature: browse community agent packages published as tagged GitHub releases and install them in one click.

Feature

  • New Recruit browse + detail UI (src/RecruitRoute.jsx, src/RichText.jsx, src/Sidebar.jsx, src/App.jsx). Detail view renders INSTRUCTIONS.md as rich text, autolinks URLs, and links to the upstream repo.
  • Daemon install pipeline: resolve a release tag, download the GitHub tarball, validate the manifest schema, filter files through gitignore-style include/exclude globs, and atomically rotate the payload into place (daemon/internal/recruit/*, daemon/internal/store/recruit.go, daemon/internal/app/recruit.go).
  • Public daemon/recruit/schema package shared by the daemon installer and the recruit-import CLI so both enforce identical validation.
  • CREW44_AGENT_SOURCE_DIR exposed to spawned runtimes for local reads of INSTRUCTIONS.md and declared skills.
  • Agent entrypoint renamed AGENT.mdINSTRUCTIONS.md.

Hardening (from this PR's adversarial review)

  • config.json now written atomically (temp + rename); ListAgents tolerates an incomplete agent dir instead of failing the whole listing — a crash mid-install can't hide every agent.
  • Payload extraction bounds total decompressed bytes across all entries, closing a zip-bomb path through skipped binary files.

Test Coverage

Every new schema/archive/client/store module has a matching _test.go; the frontend adds src/__tests__/recruit-route.test.jsx. New regression tests this PR: TestExtractFilteredPayloadBoundsBinaryDecompression, TestListAgentsSkipsAgentDirWithoutConfig. Security-relevant paths covered: traversal, symlink escape, oversize/too-many files, decompression bound, schema/glob validation.

Pre-Landing Review

Reviewed the trust-boundary surface (archive extraction, GitHub fetch, glob/manifest validation, linkify). No critical issues — safeJoin + segment checks defend traversal, all link types are rejected, and the linkify regex only matches http(s):// so javascript: URLs can't render.

Design Review

Frontend changes are styling-consistent with the existing Pretext-native UI. No AI-slop patterns flagged.

Adversarial Review (Codex + Claude)

6 findings. 2 concrete bugs fixed in this PR (config.json atomicity + zip-bomb drain, with regression tests). 4 trust-model/concurrency edges deferred to follow-up (HEAD-vs-tag instruction display, archive manifest re-verification, concurrent-install duplicate, reinstall resurrect/clobber) — tracked in local TODOS.md as P1.

Plan Completion

No plan file for this branch — n/a.

Test plan

  • Go daemon tests pass (370 passed, 20 packages)
  • Web tests pass (297 passed, 17 files)
  • go vet clean

🤖 Generated with Claude Code

suncommit and others added 19 commits June 2, 2026 10:47
Adds a new Recruit surface that pulls a curated registry of community
agents from getcrew44/agent-registry and installs them locally. Install
fetches AGENT.md and declared SKILL.md files pinned to a release tag
that matches the manifest's version field (Obsidian-style integrity).
Re-installing the same repo updates the existing agent in place; skills
are deduped by (repo_url, in-repo path) so same-named skills from
different agents don't collide.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…th HEAD

A force-pushed tag could previously install successfully even when its
crew44-agent.json declared a different version than the HEAD manifest
that resolved it. ResolveTag now requires the pinned manifest's
version field to match the requested version, with v-prefix tolerance.
Mismatches surface as ErrVersionMismatch with both versions in the
error message.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously ErrManifestInvalid, ErrUnsafePath, ErrRegistryInvalid,
ErrVersionMismatch, and GitHub URL parse failures bubbled up unmapped
and rpc.mapError defaulted them to CodeInternalError. The UI then
showed a generic "internal error" instead of the actual problem
("missing version", "unsafe path", etc).

mapRecruitError joins ErrBadRequest with the original recruit sentinel
at the app boundary, so the RPC layer translates these to -32000 and
errors.Is still detects the underlying sentinel for tests. Adds an
ErrRepoURLInvalid sentinel so parseGitHubRepo failures route through
the same path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
validateManifest now rejects manifests with a missing or non-v1
schema_version, closing the gap between the documented "required"
status of that field and the runtime contract.

FetchRegistry filters out entries missing any of the required fields
(id, name, description, repo_url) and logs each drop, so a single
broken community submission can't blank out the Recruit list for
every user.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…load

Add SourceType, Upstream (repo_url/commit/path/imported_at), and
Payload (include/exclude) to the manifest. Add AgentSource.SourceDir
and model.RecruitedSkill so a later installer commit can record the
installed payload location and an agent-private skill index. Tighten
validation to require skills[].path to end with SKILL.md, reject
upstream-wrapper manifests without upstream.repo_url, and refuse
gitignore-negation and traversal globs in Payload.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build a focused matcher around path.Match that handles the subset of
gitignore syntax the manifest's payload spec uses: ** for variable-
depth wildcards, * and ? within a segment, and exact literal text.
Anchored to repo root (no leading /) and rejects ! negation since
v1.1 doesn't support it. Foundation for the archive extractor's
include/exclude filtering.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add codeload tar.gz fetcher with on-disk cache under
~/.crew44/cache/recruit/github/<repohash>/<tag>/, plus an extractor
that strips the wrapper directory, applies include/exclude globs,
and copies only matching entries into the destination dir. Refuse
symlinks, hard links, and any entry whose path contains a `..`
segment; cap archive download at 50 MB, extracted payload at
200 MB, per-text-file at 1 MB, and total file count at 5,000.
Skip binary entries via a null-byte heuristic on the first 8 KB
of content so wrapper repos can carry image assets upstream without
flooding the installed payload.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add per-agent path helpers (AgentDir, AgentSourceDir,
AgentSourceTmpDir, AgentSourcePrevDir, RecruitedSkillsPath) and
shared recruit cache paths (RecruitGithubCacheDir, RecruitTmpDir),
plus LoadRecruitedSkills and WriteRecruitedSkillsTmp for the
agent-private skill index. CommitAgentInstall parks the existing
source/, recruited-skills.json, and config.json under .prev
siblings, applies the new artifacts, saves config, then sweeps the
.prev parking — rolling back the swap on any failure so the agent
is never left half-installed. DeleteAgent's RemoveAll already
handles the new source/ and recruited-skills.json paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…mmit

Replace the per-file fetch path with a single tagged-archive download
followed by a filtered extract into source.tmp-<install-id>/ under
the agent dir. AGENT.md and every declared skills[].path must survive
filtering or the install aborts before any rotation. recruited-skills
metadata is generated against the final source/ path and staged
alongside, then store.CommitAgentInstall swaps source/,
recruited-skills.json, and config.json into place atomically. Agent
records carry AgentSource.SourceDir so the runtime can resolve
agent-private skill paths. resolveRunSkills merges global SkillIDs
with the recruited skill index so the chat loop can later load both.
Drops the pre-payload installer's global SkillRecord creation for
recruited skills; a one-time purge clears legacy rows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Plumb AgentSourceDir through RunRequest and inject it as
CREW44_AGENT_SOURCE_DIR into the spawned runtime's env when set.
chat.go fills the field from the agent's AgentSource and switches
to resolveRunSkills so global SkillIDs plus the agent-private
recruited skill index are both loaded for the run.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build a deterministic tool that turns an arbitrary GitHub repo into
a Crew44 wrapper agent repo: clone via tarball or copy from a local
directory, scan for SKILL.md files, generate a templated AGENT.md
and crew44-agent.json, and emit an IMPORT_REPORT.md summarizing
what was included, skipped, and detected. A Generator hook is
exposed for callers that want LLM-drafted AGENT.md, but the CLI
uses the deterministic template so the importer stays hermetic.
The generated manifest is re-validated against the daemon's own
validator before write so a published wrapper can never trip the
installer with a manifest the importer accepted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Recruit detail page now shows the upstream repo URL plus
short commit (when the manifest includes an upstream block) and
a one-line note explaining that the installed payload lives under
~/.crew44/agents/agent-<id>/source/ so the runtime can read
reference files locally.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move the crew44-agent.json manifest format and its pure helpers
(validation, payload resolution, glob matching, GitHub repo parsing) into
a new public daemon/recruit/schema package so external tooling can read
and write Crew44 agent packages against a single source of truth.

internal/recruit keeps a thin alias facade, so runtime call sites are
unchanged. The recruit-import command is removed from this repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A failed first-time install left an empty agents/agent-<id> directory
with no config.json, which ListAgents expects on every agent dir. Remove
the whole agent dir on the fresh-install failure path (existing == nil
and not committed).

Also make legacy global skill cleanup truly best-effort: log instead of
returning the error, since the agent install is already durably
committed and a cleanup failure must not surface as a false install
failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The agent's system-instruction file is now INSTRUCTIONS.md. Introduces schema.EntrypointFile as the single source of truth used by the payload resolver and the install/detail fetch path, so the daemon and the out-of-tree packager stay in lockstep. Updates tests and the Recruit UI copy.
- RichText: autolink http(s) URLs as new-tab anchors
- RecruitRoute: render agent body as RichText, make repo URL clickable,
  drop source-payload blurb, rename "Designed for" -> "Recommended runtime"
- archive: skip tar global/extended headers so GitHub codeload tarballs
  aren't misread as having multiple top-level directories

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-landing adversarial review surfaced two concrete robustness gaps:

- writeJSON now writes to a temp file and renames, so a crash mid-write
  can't leave a truncated config.json. ListAgents skips an agent dir with
  a missing/unreadable config.json instead of failing the whole listing,
  so one interrupted install no longer hides every agent from the UI.
- ExtractFilteredPayload counts all decompressed bytes pulled from gzip
  (including bytes drained from skipped binary entries) against the
  extracted-payload cap, closing a zip-bomb path where a small compressed
  tarball padded with skipped binaries forced unbounded decompression.

Regression tests cover both paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…wser

Drop the "X of Y agents" count footer and add a dedicated GitHub icon
button to each Recruit row, kept out of the stats line so it reads as an
action. Route all external links (target="_blank" and same-window
navigation to external origins) through shell.openExternal so they open
in the user's default browser — reusing an existing GitHub login — rather
than a built-in Electron window.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@suncommit suncommit merged commit 932232e into main Jun 2, 2026
14 of 15 checks passed
@suncommit suncommit deleted the feat0528/polish branch June 2, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant