Feature: github-install#6
Conversation
Extend src/core/plugin_install to support git sources. Parses GitHub URL forms (https with optional .git/#ref, github: shorthand, git@github.com SSH) plus generic git+/git:/ssh:/gitlab:/bitbucket:/file:// URLs through a new pure git_source parser. Adds --ref and --path CLI flags to the plugin install command; --ref conflicting with a URL #ref is rejected as source_ambiguous, and --path is reserved as git_subdir_unsupported. The fetch path clones with --filter=blob:none --no-checkout, checks out the requested ref (or the remote's HEAD symref by default), resolves HEAD to a commit, validates the manifest and entrypoint, refuses any symlinks in the artifact tree, then copies into an atomic install directory swap. Lock entries gain resolved_ref; the update probe runs git ls-remote and flips update.available when the upstream commit moved. Telemetry: the existing plugin.install span gains hyp_source_kind=git, git_url_host/owner/repo/ref/resolved_ref, content_hash, and manifest_hash. Child spans plugin.git.clone, plugin.git.checkout, plugin.git.resolve_ref, plugin.artifact.validate, and plugin.artifact.copy each carry status and error_kind. Git stderr is redacted for credentials before being surfaced. Tests cover URL parsing variants, the --ref/URL-fragment conflict, the subdir reservation, entrypoint traversal rejection, symlink detection, and content-hash stability. New hermetic smoke plugin_install_git_url builds a local bare repo and exercises the install end-to-end through the dispatcher. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bootKernel now reads plugin-lock.json, merges bundled and installed manifest pools, resolves dependencies across the merged set, and activates installed third-party plugins when the config names them. Rejects boot with `installed_shadows_bundled` when an installed plugin tries to shadow a bundled first-party name. Adds telemetry attributes (installed_available, installed_failed, installed_shadow_collisions, installed_selected) and a plugin.installed_active log per installed-and-selected plugin. Threads installed manifest metadata into the cross-plugin config validator via a new mergeInstalledManifestsIntoKnown helper, so third-party plugin sections are no longer flagged plugin_unknown and their provides/requires participate in sink-pair and capability-ambiguity checks. Updates hyp config validate, hyp init --from-file, and daemon status to feed the merged map in. Adds boot-installed unit tests covering merged pool, dep resolution over bundled+installed, shadow rejection, and config-validator behavior, plus a hermetic plugin_boot_installed smoke that installs a git-fixture plugin, activates it via config, runs its contributed command, and asserts the boot telemetry surface.
Add a confirmation gate to `hyp plugin install` and a real update flow to `hyp plugin update <plugin>` so remote installs no longer commit without the user (or `--yes`) seeing what is about to land. Install (git sources) - New `--yes`/`-y` flag. - Non-TTY without `--yes` rejects with `remote_install_confirmation_required` (exit code 2). - TTY without `--yes` prints a summary (source URL, resolved commit, plugin name + version, permissions, entrypoint, content_hash) and prompts `Proceed? [y/N]`. Both summary and prompt land on stderr so stdout stays parseable. - Local-dir installs keep their current non-interactive behavior — the confirmation gate only fires inside `fetchGitSource`. Update - `hyp plugin update <plugin>` now re-clones with the same source, runs manifest validate, and shows old vs. new resolved_ref/version/content_hash before the artifact swap. `--yes` skips the prompt; non-TTY without `--yes` rejects as above. On rejection the prior install is left intact because the rename swap only fires once `beforeCommit` returns `proceed=true`. - The bare `hyp plugin update` (no plugin name) keeps the legacy "refresh update_check state for every plugin" behavior so `outdated` can still be recomputed without committing to a re-install. Soft warnings (stderr only) - Manifest permissions containing `network` warn about broad scope. - Branch-shaped refs (and missing refs) warn that pinning a tag or commit SHA gives reproducible installs. Telemetry - `plugin.install` and a new `plugin.update` span both carry a `confirmation` attribute drawn from `confirmed`, `auto_yes`, `rejected`, `non_tty_no_yes`. The `plugin.installed` log row mirrors the attribute when it was set. - `fetch.FetchErrorKind` / `git_fetch.GitFetchErrorKind` gain `remote_install_confirmation_required` and `remote_install_rejected`. Tests + smoke - New `test/core/plugin-install-confirm.test.js` covers the pure helpers (`decideConfirmation`, `buildWarnings`, `sourceIsUnpinnedBranch`, `renderConfirmationSummary`) and exercises the `installPlugin` + `updatePlugin` integration against a hermetic file:// bare repo. - New env-gated acceptance smoke `hypaware-core/smoke/flows/plugin_install_github_url.js` installs a real GitHub plugin fixture at a pinned tag (opt-in via `HYP_SMOKE_REAL_GITHUB=1`, with overridable URL/ref/name env vars). Skips with a clear marker when the env var is unset so CI runs of `npm run smoke` do not hit the network. - The existing hermetic git-url and boot-installed smokes now pass `--yes` (they run with non-TTY buffer streams) and assert `confirmation=auto_yes` on the install span. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dual-agent review —
|
| Source | Finding (severity, evidence) | Intersects |
|---|---|---|
| codex.md | Fix Validations / incomplete: readline interactive prompt path uses node:readline callback API as if promise-based (await rl.question(...)), so interactive confirmations can throw before returning a decision. Evidence: src/core/plugin_install/confirm.js:167, src/core/plugin_install/confirm.js:171, src/core/plugin_install/install.js:148. |
Targets (confirm.js new, install.js modified); Concurrency surface §7 (confirm.js:168-179 readline buildTtyPrompt/ask — cited lines 167, 171 sit inside the same lifecycle) |
| codex.md | Behavioral Correctness / major / high: git ls-remote parsing takes the first SHA, which is wrong for annotated tags (refs/tags/x + refs/tags/x^{}), causing false "update available" against resolved_ref. Evidence: src/core/plugin_install/update_check.js:121, src/core/plugin_install/update_check.js:137, src/core/plugin_install/update_check.js:154. |
Targets (update_check.js modified — +94 lines adding runGitProbe, execGit, parseLsRemoteOutput); same file as Concurrency surface §1 (update_check.js:180-202 execGit) — cited lines are in the new parseLsRemoteOutput helper that feeds the same probe path |
| codex.md | Security Surface / major / high: credential-bearing URLs can be persisted and echoed via source.raw (lock/confirmation), despite stderr redaction in git-error paths. Evidence: src/core/plugin_install/resolver.js:56, src/core/plugin_install/install.js:195, src/core/plugin_install/confirm.js:98, src/core/plugin_install/git_fetch.js:635. |
Targets (resolver.js / install.js / confirm.js / git_fetch.js all in PR diff); Direct callers (resolver.js:53-69 parseGitSource caller block contains the cited resolver.js:56); Concurrency surface §1 (git_fetch.js:616-641 execGit decl contains the cited git_fetch.js:635) |
| claude.md | issue 1: git clone, git checkout, and git ls-remote pass user-controlled gitUrl / ref / target as positional args without a -- end-of-options separator (CVE-2018-17456 family); neither CLI parser nor parseGitSource/applyGitSourceFlags rejects leading -. Evidence: src/core/plugin_install/git_fetch.js:238-244, src/core/plugin_install/git_fetch.js:283-289, src/core/plugin_install/update_check.js:130-140. |
Targets (git_fetch.js new, update_check.js modified); Concurrency surface §1 — every cited site is an execGit caller named in the surface map (runGitCloneSpan, runGitCheckoutSpan in git_fetch.js; runGitProbe in update_check.js) |
| claude.md | issue 2: post-install probe hangs indefinitely — after a successful git install/update, checkForPluginUpdate → runGitProbe → execGit(['ls-remote', ...]) resolves only on child 'close' with no timeout; a slow or blackholed remote hangs hyp plugin install after the artifact and lock are already written. Evidence: src/core/plugin_install/update_check.js:100-145. |
Targets (update_check.js modified); Concurrency surface §1 (execGit "no timeout / no AbortSignal — a hung 'git ls-remote' never resolves"); Risks #2 — this finding is the exact failure mode the blast-radius report flags as MEDIUM ("execGit has no timeout... a hung git ls-remote (probe path) never resolves") |
| claude.md | issue 3: applyGitSourceFlags JSDoc says the subdir value is "still recorded on the returned spec so the eventual lock entry shape stays forward compatible", but the function throws git_subdir_unsupported unconditionally when opts.subdir !== undefined, so the trailing return { ...parts, ref, ...(subdir !== undefined ? { subdir } : {}) } is unreachable when subdir is set. Evidence: src/core/plugin_install/git_source.js:97-137. |
Targets (git_source.js new — applyGitSourceFlags is the cited symbol); Direct callers (resolver.js:53-69 invokes parseGitSource/applyGitSourceFlags); Risks #6 — blast-radius report flags exactly this surface ("subdir is parsed but rejected... applyGitSourceFlags throws git_subdir_unsupported", a reserved-slot surface masquerading as a working flag) |
Blast radius
- Lock-file RMW has no serializability and the install path has a
multi-second double-write window.installPluginwrites the lock
at line 220 (entry without update state), runs
checkForPluginUpdate(which callsgit ls-remoteover the
network — seconds to minutes), then writes again at line 238 using
nextLockbuilt frominitialLock. Any concurrenthyp plugin installthat lands between those writes is silently clobbered.
Two concurrent installs of DIFFERENT plugins also lose one entry
via plain last-writer-wins. No flock / lockfile / fcntl advisory
lock anywhere on this path. Worth a follow-up bead to wrap the
RMW in an in-process mutex + file lock, especially now that the
fetch surface includes a network round-trip. execGithas no timeout and is duplicated. A hung
git ls-remote(probe path) orgit clone(install path) never
resolves; the caller awaits forever and the child PID is
reparented to PID 1 on hard exit. The implementation lives in
BOTHupdate_check.js:180-202andgit_fetch.js:616-641, so any
hardening (AbortController,signal, kill-on-cleanup) must land
in two places or the binary is only half-hardened. Probe path is
rate-limited to 24h so practical exposure is bounded; install
path runs on every install.- Daemon reload does NOT pick up newly installed plugins.
daemon/runtime.js:reload()re-reads config and per-source
reload() but does not re-calldiscoverInstalledPluginsor
re-activate. The asymmetry is invisible:hyp statuswill show
the newly installed plugin in the validation pass (because
daemon/status.jsre-discovers per-call), while the kernel's
active-plugin set is frozen at boot. Operators following the
documentedinstall → reloadritual will get a misleading "all
good" signal. - Installed-plugin discovery failures are silently swallowed in
three CLI paths.buildKnownPluginsForCtxwraps
discoverInstalledPluginsintry / catchand returns an empty
merged map on failure.daemon/status.jsdoes the same. The
underlyingplugin.installed_manifest_invalidlog row carries the
detail, but the operator runninghyp config validatesees
"everything passed" with no signal that their installed plugins
weren't considered. Worth a non-fatal warning printed to stderr
when discovery returnsfailed[]entries. - Shadow-collision is a hard boot fail with no graceful recovery.
bootKernelthrowsinstalled_shadows_bundledif any installed
plugin name matches a bundled one. The error message tells the
operator to runhyp plugin remove, but the CLI can't reach the
daemon if the daemon won't boot, and there is no "skip installed
shadows and continue" flag. A user who installed
@hypaware/local-fsfrom a fork and then ranhyp initends up
with a non-bootable kernel until they manually edit the lock file
orrm -rfthe install dir. Consistent with the Phase 7 design
intent (hy-gh-2), but worth surfacing more loudly in the error
message and the smoke surface. subdiris parsed but rejected — a "reserved" surface masquerading
as a working flag. The--pathCLI flag, thesubdirresolver
option, and thePluginSourceSpec.subdirtyped field all exist and
flow through the pipeline, but every code path that observes a
non-emptysubdirthrowsgit_subdir_unsupported. Users hitting
the CLI flag will get an error AFTER the clone has already happened.
The clone is cheap, but the failure mode is "the path was reserved,
it does not yet do anything" rather than "this flag is not yet
implemented" — a--pathflag that fast-fails in
parsePluginInstallArgswould be a less surprising UX.- Exit-code collision between usage error and confirmation
required.runPluginInstallreturns2for argv parse errors
AND forremote_install_confirmation_required. CI scripts that
treat exit2as "user typo, retry never" will misclassify a TTY
detection failure (e.g., a CI runner with an unexpected stdin)
as a permanent error. The stderr message distinguishes them; the
exit code does not. Easy to split into3for confirm-required
if scripts mature past hand-grep.
Codex review
Fix Validations
Remote install/update confirmation gate
- Status: incomplete
- Evidence: src/core/plugin_install/confirm.js:167, src/core/plugin_install/confirm.js:171, src/core/plugin_install/install.js:148
- Assessment: Non-TTY enforcement is wired, but the interactive prompt path uses
node:readlinecallback API as if it were promise-based (await rl.question(...)), so interactive confirmations can throw before returning a decision.
Installed plugins included in config validation metadata
- Status: correct
- Evidence: src/core/cli/core_commands.js:831, src/core/cli/core_commands.js:1272, src/core/config/validate.js:153
- Assessment: Installed manifests are merged into known-plugin metadata, so installed names/capabilities now participate in validation instead of being treated as unknown by default.
Boot-time installed-vs-bundled shadow rejection
- Status: correct
- Evidence: src/core/runtime/boot.js:130, src/core/runtime/boot.js:149, src/core/runtime/boot.js:176
- Assessment: Collision detection runs before activation, emits explicit telemetry, and fails boot with
installed_shadows_bundled.
Findings
[Behavioral Correctness]
- Severity: major
- Confidence: high
- Evidence: src/core/plugin_install/update_check.js:121, src/core/plugin_install/update_check.js:137, src/core/plugin_install/update_check.js:154
- Why it matters:
git ls-remoteparsing takes the first SHA, which is wrong for annotated tags (refs/tags/x+refs/tags/x^{}), causing false “update available” signals againstresolved_ref. - Suggested fix: Parse by refname and prefer the peeled
^{}commit SHA when present before comparing toentry.resolved_ref.
[Security Surface]
- Severity: major
- Confidence: high
- Evidence: src/core/plugin_install/resolver.js:56, src/core/plugin_install/install.js:195, src/core/plugin_install/confirm.js:98, src/core/plugin_install/git_fetch.js:635
- Why it matters: Credential-bearing URLs can be persisted and echoed via
source.raw(lock/confirmation), despite stderr redaction in git-error paths. - Suggested fix: Strip URL userinfo before persisting/displaying
raw, store a redacted display form, and keep any sensitive clone URL ephemeral.
No Finding
-
- Contract & Interface Fidelity
-
- Change Impact / Blast Radius
-
- Concurrency, Ordering & State Safety
-
- Error Handling & Resilience
-
- Resource Lifecycle & Cleanup
-
- Release Safety
-
- Test Evidence Quality
-
- Architectural Consistency
-
- Debuggability & Operability
Evidence Bundle
- Changed hot paths: src/core/cli/core_commands.js, src/core/plugin_install/install.js, src/core/plugin_install/git_fetch.js, src/core/plugin_install/update_check.js, src/core/runtime/boot.js, src/core/runtime/installed.js
- Impacted callers: src/core/cli/core_commands.js:832, src/core/cli/core_commands.js:987, src/core/plugin_install/fetch.js:75, src/core/plugin_install/install.js:273, src/core/daemon/status.js:257
- Impacted tests: test/core/plugin-install-confirm.test.js:18, test/core/plugin-install-git-source.test.js:14, test/core/boot-installed.test.js:88, hypaware-core/smoke/flows/plugin_install_git_url.js:1, hypaware-core/smoke/flows/plugin_install_github_url.js:1
- Unresolved uncertainty: interactive confirmation was not exercised in a real TTY test in this diff; annotated-tag upstream probe behavior is not covered by current tests/smokes.
Claude review
Code review
Note: This PR is a feature-flow integration branch (intentionally draft until ship). The code-review-quiet skill was invoked by mol-pr-dual-review with --no-comment --output, so the draft gate is overridden in this orchestrator context.
Found 3 issues:
- Git option-injection:
git clone,git checkout, andgit ls-remotepass user-controlledgitUrlandref/targetas positional args without a--end-of-options separator, and neither the CLI parser norparseGitSource/applyGitSourceFlagsrejects leading-in those values. A ref like--upload-pack=<cmd>(or a URL like--config=...) would be interpreted by git as an option rather than a value (CVE-2018-17456 family). Fix: insert--before user-controlled positionals in eachexecGitcall AND reject inputs matching^-in the resolver / CLI parser.
hypaware/src/core/plugin_install/git_fetch.js
Lines 238 to 244 in 4f69e33
hypaware/src/core/plugin_install/git_fetch.js
Lines 283 to 289 in 4f69e33
hypaware/src/core/plugin_install/update_check.js
Lines 130 to 140 in 4f69e33
- Post-install probe can hang indefinitely: after a successful git install/update, the path awaits
checkForPluginUpdate→runGitProbe→execGit(['ls-remote', '--quiet', gitUrl, ref])inside theplugin.installspan, andexecGitresolves only on the child'scloseevent with no timeout, so a slow or blackholed remote will hanghyp plugin installafter the artifact and lock are already written. Fix: either skipcheckForPluginUpdatefor fresh git installs (the just-resolvedresolved_refis by definition current) or wrap thegit ls-remotespawn with a hard timeout that emits a structuredgit_probe_timeouterror_kind.
hypaware/src/core/plugin_install/update_check.js
Lines 100 to 145 in 4f69e33
applyGitSourceFlagsJSDoc contradicts behavior: the docblock says thesubdirvalue is "still recorded on the returned spec so the eventual lock entry shape stays forward compatible", but the function throwsgit_subdir_unsupportedunconditionally whenopts.subdir !== undefined, so the trailingreturn { ...parts, ref, ...(subdir !== undefined ? { subdir } : {}) }is unreachable when subdir is set and the documented forward-compatibility promise cannot be delivered. Fix: drop the "still recorded" sentence from the JSDoc, OR record the spec before throwing.
hypaware/src/core/plugin_install/git_source.js
Lines 97 to 137 in 4f69e33
🤖 Generated with Claude Code
- If this code review was useful, please react with 👍. Otherwise, react with 👎.
Reports: /Users/phil/testcity/.gc/pr-pipeline/reviews/pr-6 · Bead: hy-z8kt · Blast-radius: hy-exum
…-lpo3) (#18) Address all blocker and major findings from the dual-review on PR #6: confirm.js — switch to node:readline/promises so the awaited rl.question() actually returns a Promise and the interactive TTY prompt path can return a decision instead of throwing. update_check.js — pickLsRemoteSha now prefers the peeled `^{}` SHA when present so annotated tags compare against entry.resolved_ref correctly instead of producing spurious "update available" signals. Also wraps the ls-remote spawn with a hard timeout (default 5s, HYP_GIT_PROBE_TIMEOUT_MS-overridable, clamped) that emits a git_probe_timeout error_kind, and the post-install path now passes freshlyResolved=true so the probe is skipped entirely when we just resolved the upstream ref — the span still emits with probe_skipped=freshly_resolved for observability. git_source.js / resolver.js / install.js — strip user:pass@ userinfo from source.raw and source.gitUrl before they reach the lock entry, the confirmation prompt, or the install span. Persistence and display are always credential-free; clones rely on the user's git credential helper. Adds redactGitUrl / redactRawSource helpers. git_fetch.js / update_check.js — insert `--` end-of-options separator before user-controlled positionals in `git clone`, `git checkout`, and `git ls-remote` so a hostile URL or ref like `--upload-pack=<cmd>` cannot be parsed as a git option (CVE-2018-17456 family). Also reject leading-dash inputs in parseGitSource, applyGitSourceFlags, and the CLI parser as belt-and-braces defense before the spawn boundary. git_source.js — applyGitSourceFlags JSDoc no longer claims the subdir value is "still recorded on the returned spec" since the function throws unconditionally; the unreachable subdir spread is removed. Tests cover redactGitUrl/redactRawSource, leading-dash rejection across parser layers, peeled-tag SHA preference in pickLsRemoteSha, and the real readline/promises behavior of buildTtyPrompt via PassThrough streams. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-generated by
/feature-launchto host the integration branch for feature github-install.Work beads file individual sub-PRs into this branch via the refinery feature-flow loop. When all work + review + ship beads complete, the ship formula flips this PR to ready-for-review.
See
/feature-flowfor the flow architecture.