GitTree::read_file now takes a cap and reads the object's size from its
header first, so a blob over MAX_MANIFEST_BYTES / MAX_ITEM_BYTES is refused
without being inflated, and the blob is taken rather than cloned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An install the host skipped (pinned commit missing from the cache, cache
unreadable, item failing a tightened validation rule) never reached the
manifest, so sync.sh treated it as deselected and deleted it from the
container. The manifest now carries `held`: the state ids of such
installs ("plugin:<slug>/<key>" for plugins). The script counts them as
still selected and carries their records forward, as it already does
for items that fail inside the container. A removed marketplace is the
one skip that still removes; a malformed `held` list changes nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The slug (plugin marketplace "triple-c-<slug>", plugin tree
"plugins/<slug>/") was built from the editable display name. After a
rename the next sync registered the new marketplace, skipped the plugin
install because the state's commit matched, then removed the old
marketplace: the plugin was gone while the report said nothing changed.
marketplace_slug now takes the id only ("mp-<id8>"). With plugin state
kept per slug (I1), containers synced with the old "<name>-<id8>" slugs
move over on their next sync: plugins are installed under the new name,
the old copies uninstalled and the old registration dropped. A sync
script test covers that migration (it fails on the pre-I1 script).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The credential callback answered every credential request. gix follows
a redirect of the initial handshake and asks for credentials for the
redirect target, so the token could be sent to another host. The
callback now answers only when the request's scheme, host and port
match the marketplace URL (gix's own URL normalisation, host compared
case-insensitively); anything else gets no credential.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two marketplaces shipping a plugin of the same name shared one
"plugin:<key>" state record, so every sync reinstalled one copy and
reported it updated, and removing one marketplace never uninstalled its
copy. Plugin state ids are now "plugin:<slug>/<key>"; the slug and key
for an uninstall are derived from the id and re-validated. Older
"plugin:<key>" records are migrated using their recorded slug, so
existing installs are neither reinstalled nor orphaned. Reports keep
"plugin:<key>".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md gets a Marketplace subsection under Key Conventions (the sync
script is app-embedded and re-uploaded on every sync, never baked into
container/ — pre-flight F9) and the Settings export/import section now
covers marketplace account tokens traveling in ExportedSecrets and the
import preview's warning on global hook and plugin installs.
HOW-TO-USE.md gets a Marketplace section (placed after Shared Claude
Authentication) with its Table of Contents entry (pre-flight N13). The
spec doc's stale keychain service name, gh-login flags and
upload_bytes_to_container signature are amended to match the shipped
code (pre-flight N10).
Also fixes the new marketplace code's remaining build/clippy warnings:
BTreeMap/Sha256/Digest imports in tree.rs gated behind #[cfg(test)]
(their only uses are on MemTree, already test-only), the unused
`pub use marketplace::*` glob re-export dropped from models/mod.rs,
gh_login::strip_ansi marked #[cfg(test)] (production streams through
AnsiStripper instead), and four clippy lints in marketplace test code
(double_ended_iterator_last, cloned_ref_to_slice_refs x2,
single_match). Flushes the unresolved getMarketplaceSyncReport promise
in MarketplaceSection.test.tsx's "opens the Marketplace filtered to
this project" test to remove its act() warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gh 2.101.0 (the image's) prints "! One-time code (XXXX-XXXX) copied to
clipboard" and "Press Enter to open https://github.com/login/device in
your browser...". parse_device_prompt only knew "one-time code:", so no
code event went out and Enter was never pressed: gh sat at its prompt
until the 10-minute timeout. Match the label case-insensitively, accept
":" or "(" before the code, and require something after it so a code cut
by a frame boundary is not taken early.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 21 marketplace commands, registered and granted; marketplace fields
kept store-owned in update_settings/update_project; a background refresh
of every marketplace at app start.
- apply_marketplace_now emits marketplace-sync-finished per project (F4).
- Settings export carries marketplace account tokens in ExportedSecrets
(account id -> token) and import restores them; imported accounts,
marketplaces and global installs are validated with the commands' own
rules before anything is written. The import preview discloses the
marketplace count, token count and global hook installs, and warns on
the latter (F10).
- refresh_pins and cache removal hold the repo lock (F11).
- ops::validate_host/validate_branch delegate to auth::valid_host and
git::valid_branch (F13).
- A finished gh container login frees only its own cancel slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lost stream or a failed Enter write returned without killing the
in-container gh, leaving it polling with a temp GH_CONFIG_DIR that would
receive the token. The output loop is now drive_login (generic over the
stream, writer and emitter, so it is unit-tested without Docker), and
its result goes through cleanup_on_error, so every ending except a token
read back runs the pkill. Neutral wording for the shared ANSI stripper's
overflow warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Waits for the entrypoint, uploads the payload and the sync script, runs it
as claude and stores its report (payload skips merged in). The start hook
spawns the sync in the background; a per-project lock serialises syncs of
one project (pre-flight F11b).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drives `gh auth login --web` in a running project container over an
attached pty, with GH_CONFIG_DIR/GIT_CONFIG_GLOBAL in a temp dir that is
removed on exit (also on HUP/INT/TERM), emits the one-time code and
redacted output lines, and returns the token read back between markers.
Host validation reuses auth::valid_host plus a no-port check (F13); a
cancel or timeout also pkills the in-container login (N9). Reuses the
setup-token flow's AnsiStripper, push_capped_tail and Enter delay, made
pub(crate) without behaviour change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removal only derives a path from an exact <kind>:<key> state id with a known
kind; any other record is dropped with an error and nothing is deleted
(an id like "skill" used to remove ~/.claude/skills/skill). Invalid plugin
records are dropped too, and a failed chmod 600 on settings.json is reported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Validate manifest structure up front; malformed items are skipped with a
reason instead of aborting extraction; an unreadable manifest changes
nothing (no removals).
- Empty/whitespace settings.json reads as {}; non-object settings are left
untouched; hook installs/updates/removals are reported and recorded only
once their entries are actually merged; mv failures are checked.
- Dangling symlinks at user paths count as occupied.
- Removal paths are derived from kind+key, never taken from state.json.
- A symlinked settings.json is written through, not replaced.
- mktemp failure emits a JSON report instead of exiting silently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Constant POSIX sh + jq script (embedded via include_str!) that applies the
payload into ~/.claude, tracks ownership in state.json, never overwrites
user-owned files, merges hook entries surgically, drives claude plugin and
prints a JSON SyncReport. Also: settings.json kept 0600 (pre-flight N11),
payloads containing symlinks are refused, slugs parsed via @tsv.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds diff::item_diff (similar), MarketplaceManager with snapshots,
persisted sync reports, the gh-login slot and a repo lock held across
fetches (pre-flight F11a), refresh_marketplace, load_cached_snapshot,
compute_updates, pins_by_marketplace, head_for, and the GitFixture test
helper on top of git::test_support (F3). AppState gains marketplace.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fix round 1 for Task 5. reqwest's default redirect policy only
strips Authorization/Cookie/Proxy-Authorization/WWW-Authenticate on a
cross-host hop, so GitLab's PRIVATE-TOKEN header (and Authorization on
an https->http same-host downgrade) would have followed a redirect to
an attacker-controlled target. The client now disables redirects
outright, and a 3xx response is treated as "not this kind of host"
rather than an error. Also adds the missing N17 test for a malformed,
credential-bearing URL, and clarifies two doc comments.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Anonymous fetches of private repos map to Auth, error text drops gix
source locations and names the innermost network cause, and
valid_branch is pub(crate) for the add form (pre-flight F1, F2, F13).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix round 1 from PR review of the tree/catalog parsing:
- collect_dir now rejects an entry whose name is ".", "..", empty, or
contains "/", "\" or NUL before it becomes part of an item's rel_path —
a crafted git tree could otherwise walk a file outside the item's own
folder once that path is joined against the item root downstream.
- collect_dir caps recursion at 32 directory levels and counts
directories (not just files) toward MAX_ITEM_FILES, so a tree that is
wide or deep rather than merely file-heavy is still bounded.
- hook.json and plugins/.claude-plugin/marketplace.json are now rejected
unparsed above 1 MiB, rather than handed to serde_json regardless of
size.
A pre-read size query (checking a blob's size before reading it) is
deferred per controller ruling — this round reads the blob and checks
its length before parsing, which is enough for the JSON-parsing DoS
shape being closed here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>