diff --git a/src/applications/phid/query/PhabricatorHandleQuery.php b/src/applications/phid/query/PhabricatorHandleQuery.php
index 296116312b..2635b84298 100644
--- a/src/applications/phid/query/PhabricatorHandleQuery.php
+++ b/src/applications/phid/query/PhabricatorHandleQuery.php
@@ -1,74 +1,88 @@
 <?php
 
 final class PhabricatorHandleQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
+  private $objectCapabilities;
   private $phids = array();
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
+  public function requireObjectCapabilities(array $capabilities) {
+    $this->objectCapabilities = $capabilities;
+    return $this;
+  }
+
+  protected function getRequiredObjectCapabilities() {
+    if ($this->objectCapabilities) {
+      return $this->objectCapabilities;
+    }
+    return $this->getRequiredCapabilities();
+  }
+
   protected function loadPage() {
     $types = PhabricatorPHIDType::getAllTypes();
 
     $phids = array_unique($this->phids);
     if (!$phids) {
       return array();
     }
 
     $object_query = id(new PhabricatorObjectQuery())
       ->withPHIDs($phids)
+      ->requireCapabilities($this->getRequiredObjectCapabilities())
       ->setViewer($this->getViewer());
 
     $objects = $object_query->execute();
     $filtered = $object_query->getPolicyFilteredPHIDs();
 
     $groups = array();
     foreach ($phids as $phid) {
       $type = phid_get_type($phid);
       $groups[$type][] = $phid;
     }
 
     $results = array();
     foreach ($groups as $type => $phid_group) {
       $handles = array();
       foreach ($phid_group as $key => $phid) {
         if (isset($handles[$phid])) {
           unset($phid_group[$key]);
           // The input had a duplicate PHID; just skip it.
           continue;
         }
         $handles[$phid] = id(new PhabricatorObjectHandle())
           ->setType($type)
           ->setPHID($phid);
         if (isset($objects[$phid])) {
           $handles[$phid]->setComplete(true);
         } else if (isset($filtered[$phid])) {
           $handles[$phid]->setPolicyFiltered(true);
         }
       }
 
       if (isset($types[$type])) {
         $type_objects = array_select_keys($objects, $phid_group);
         if ($type_objects) {
           $have_object_phids = array_keys($type_objects);
           $types[$type]->loadHandles(
             $this,
             array_select_keys($handles, $have_object_phids),
             $type_objects);
         }
       }
 
       $results += $handles;
     }
 
     return $results;
   }
 
   public function getQueryApplicationClass() {
     return null;
   }
 
 }
diff --git a/src/applications/search/application/PhabricatorSearchApplication.php b/src/applications/search/application/PhabricatorSearchApplication.php
index 51d5e9780c..f350438616 100644
--- a/src/applications/search/application/PhabricatorSearchApplication.php
+++ b/src/applications/search/application/PhabricatorSearchApplication.php
@@ -1,48 +1,48 @@
 <?php
 
 final class PhabricatorSearchApplication extends PhabricatorApplication {
 
   public function getBaseURI() {
     return '/search/';
   }
 
   public function getName() {
     return pht('Search');
   }
 
   public function getShortDescription() {
     return pht('Full-Text Search');
   }
 
   public function getFlavorText() {
     return pht('Find stuff in big piles.');
   }
 
   public function getFontIcon() {
     return 'fa-search';
   }
 
   public function isLaunchable() {
     return false;
   }
 
   public function getRoutes() {
     return array(
       '/search/' => array(
         '(?:query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorSearchController',
         'attach/(?P<phid>[^/]+)/(?P<type>\w+)/(?:(?P<action>\w+)/)?'
           => 'PhabricatorSearchAttachController',
-        'select/(?P<type>\w+)/'
+        'select/(?P<type>\w+)/(?:(?P<action>\w+)/)?'
           => 'PhabricatorSearchSelectController',
         'index/(?P<phid>[^/]+)/' => 'PhabricatorSearchIndexController',
         'hovercard/(?P<mode>retrieve|test)/'
           => 'PhabricatorSearchHovercardController',
         'edit/(?P<queryKey>[^/]+)/' => 'PhabricatorSearchEditController',
         'delete/(?P<queryKey>[^/]+)/(?P<engine>[^/]+)/'
           => 'PhabricatorSearchDeleteController',
         'order/(?P<engine>[^/]+)/' => 'PhabricatorSearchOrderController',
       ),
     );
   }
 
 }
diff --git a/src/applications/search/controller/PhabricatorSearchAttachController.php b/src/applications/search/controller/PhabricatorSearchAttachController.php
index e08fd7734c..b2e0a92b9d 100644
--- a/src/applications/search/controller/PhabricatorSearchAttachController.php
+++ b/src/applications/search/controller/PhabricatorSearchAttachController.php
@@ -1,335 +1,324 @@
 <?php
 
 final class PhabricatorSearchAttachController
   extends PhabricatorSearchBaseController {
 
-  private $phid;
-  private $type;
-  private $action;
-
-  const ACTION_ATTACH       = 'attach';
-  const ACTION_MERGE        = 'merge';
-  const ACTION_DEPENDENCIES = 'dependencies';
-  const ACTION_BLOCKS       = 'blocks';
-  const ACTION_EDGE         = 'edge';
-
-  public function willProcessRequest(array $data) {
-    $this->phid = $data['phid'];
-    $this->type = $data['type'];
-    $this->action = idx($data, 'action', self::ACTION_ATTACH);
-  }
-
-  public function processRequest() {
-
-    $request = $this->getRequest();
-    $user = $request->getUser();
+  public function handleRequest(AphrontRequest $request) {
+    $user        = $request->getUser();
+    $phid        = $request->getURIData('phid');
+    $attach_type = $request->getURIData('type');
+    $action      = $request->getURIData('action', self::ACTION_ATTACH);
 
     $handle = id(new PhabricatorHandleQuery())
       ->setViewer($user)
-      ->withPHIDs(array($this->phid))
+      ->withPHIDs(array($phid))
       ->executeOne();
 
     $object_type = $handle->getType();
-    $attach_type = $this->type;
 
     $object = id(new PhabricatorObjectQuery())
       ->setViewer($user)
-      ->withPHIDs(array($this->phid))
+      ->withPHIDs(array($phid))
       ->executeOne();
 
     if (!$object) {
       return new Aphront404Response();
     }
 
     $edge_type = null;
-    switch ($this->action) {
+    switch ($action) {
       case self::ACTION_EDGE:
       case self::ACTION_DEPENDENCIES:
       case self::ACTION_BLOCKS:
       case self::ACTION_ATTACH:
         $edge_type = $this->getEdgeType($object_type, $attach_type);
         break;
     }
 
     if ($request->isFormPost()) {
       $phids = explode(';', $request->getStr('phids'));
       $phids = array_filter($phids);
       $phids = array_values($phids);
 
       if ($edge_type) {
         if (!$object instanceof PhabricatorApplicationTransactionInterface) {
           throw new Exception(
             pht(
               'Expected object ("%s") to implement interface "%s".',
               get_class($object),
               'PhabricatorApplicationTransactionInterface'));
         }
 
         $old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
-          $this->phid,
+          $phid,
           $edge_type);
         $add_phids = $phids;
         $rem_phids = array_diff($old_phids, $add_phids);
 
         $txn_editor = $object->getApplicationTransactionEditor()
           ->setActor($user)
           ->setContentSourceFromRequest($request)
           ->setContinueOnMissingFields(true)
           ->setContinueOnNoEffect(true);
 
         $txn_template = $object->getApplicationTransactionTemplate()
           ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
           ->setMetadataValue('edge:type', $edge_type)
           ->setNewValue(array(
             '+' => array_fuse($add_phids),
             '-' => array_fuse($rem_phids),
           ));
 
         try {
           $txn_editor->applyTransactions(
             $object->getApplicationTransactionObject(),
             array($txn_template));
         } catch (PhabricatorEdgeCycleException $ex) {
           $this->raiseGraphCycleException($ex);
         }
 
         return id(new AphrontReloadResponse())->setURI($handle->getURI());
       } else {
         return $this->performMerge($object, $handle, $phids);
       }
     } else {
       if ($edge_type) {
         $phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
-          $this->phid,
+          $phid,
           $edge_type);
       } else {
         // This is a merge.
         $phids = array();
       }
     }
 
-    $strings = $this->getStrings();
+    $strings = $this->getStrings($attach_type, $action);
 
     $handles = $this->loadViewerHandles($phids);
 
     $obj_dialog = new PhabricatorObjectSelectorDialog();
     $obj_dialog
       ->setUser($user)
       ->setHandles($handles)
-      ->setFilters($this->getFilters($strings))
+      ->setFilters($this->getFilters($strings, $attach_type))
       ->setSelectedFilter($strings['selected'])
-      ->setExcluded($this->phid)
+      ->setExcluded($phid)
       ->setCancelURI($handle->getURI())
-      ->setSearchURI('/search/select/'.$attach_type.'/')
+      ->setSearchURI('/search/select/'.$attach_type.'/'.$action.'/')
       ->setTitle($strings['title'])
       ->setHeader($strings['header'])
       ->setButtonText($strings['button'])
       ->setInstructions($strings['instructions']);
 
     $dialog = $obj_dialog->buildDialog();
 
     return id(new AphrontDialogResponse())->setDialog($dialog);
   }
 
   private function performMerge(
     ManiphestTask $task,
     PhabricatorObjectHandle $handle,
     array $phids) {
 
     $user = $this->getRequest()->getUser();
     $response = id(new AphrontReloadResponse())->setURI($handle->getURI());
 
     $phids = array_fill_keys($phids, true);
     unset($phids[$task->getPHID()]); // Prevent merging a task into itself.
 
     if (!$phids) {
       return $response;
     }
 
     $targets = id(new ManiphestTaskQuery())
       ->setViewer($user)
+      ->requireCapabilities(
+        array(
+          PhabricatorPolicyCapability::CAN_VIEW,
+          PhabricatorPolicyCapability::CAN_EDIT,
+        ))
       ->withPHIDs(array_keys($phids))
       ->needSubscriberPHIDs(true)
       ->needProjectPHIDs(true)
       ->execute();
 
     if (empty($targets)) {
       return $response;
     }
 
     $editor = id(new ManiphestTransactionEditor())
       ->setActor($user)
       ->setContentSourceFromRequest($this->getRequest())
       ->setContinueOnNoEffect(true)
       ->setContinueOnMissingFields(true);
 
     $cc_vector = array();
     // since we loaded this via a generic object query, go ahead and get the
     // attach the subscriber and project phids now
     $task->attachSubscriberPHIDs(
       PhabricatorSubscribersQuery::loadSubscribersForPHID($task->getPHID()));
     $task->attachProjectPHIDs(
       PhabricatorEdgeQuery::loadDestinationPHIDs($task->getPHID(),
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST));
 
     $cc_vector[] = $task->getSubscriberPHIDs();
     foreach ($targets as $target) {
       $cc_vector[] = $target->getSubscriberPHIDs();
       $cc_vector[] = array(
         $target->getAuthorPHID(),
         $target->getOwnerPHID(),
       );
 
       $merged_into_txn = id(new ManiphestTransaction())
         ->setTransactionType(ManiphestTransaction::TYPE_MERGED_INTO)
         ->setNewValue($task->getPHID());
 
       $editor->applyTransactions(
         $target,
         array($merged_into_txn));
 
     }
     $all_ccs = array_mergev($cc_vector);
     $all_ccs = array_filter($all_ccs);
     $all_ccs = array_unique($all_ccs);
 
     $add_ccs = id(new ManiphestTransaction())
       ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
       ->setNewValue(array('=' => $all_ccs));
 
     $merged_from_txn = id(new ManiphestTransaction())
       ->setTransactionType(ManiphestTransaction::TYPE_MERGED_FROM)
       ->setNewValue(mpull($targets, 'getPHID'));
 
     $editor->applyTransactions(
       $task,
       array($add_ccs, $merged_from_txn));
 
     return $response;
   }
 
-  private function getStrings() {
-    switch ($this->type) {
+  private function getStrings($attach_type, $action) {
+    switch ($attach_type) {
       case DifferentialRevisionPHIDType::TYPECONST:
         $noun = 'Revisions';
         $selected = 'created';
         break;
       case ManiphestTaskPHIDType::TYPECONST:
         $noun = 'Tasks';
         $selected = 'assigned';
         break;
       case PhabricatorRepositoryCommitPHIDType::TYPECONST:
         $noun = 'Commits';
         $selected = 'created';
         break;
       case PholioMockPHIDType::TYPECONST:
         $noun = 'Mocks';
         $selected = 'created';
         break;
     }
 
-    switch ($this->action) {
+    switch ($action) {
       case self::ACTION_EDGE:
       case self::ACTION_ATTACH:
         $dialog_title = "Manage Attached {$noun}";
         $header_text = "Currently Attached {$noun}";
         $button_text = "Save {$noun}";
         $instructions = null;
         break;
       case self::ACTION_MERGE:
         $dialog_title = 'Merge Duplicate Tasks';
         $header_text = 'Tasks To Merge';
         $button_text = "Merge {$noun}";
         $instructions =
           'These tasks will be merged into the current task and then closed. '.
           'The current task will grow stronger.';
         break;
       case self::ACTION_DEPENDENCIES:
         $dialog_title = 'Edit Dependencies';
         $header_text = 'Current Dependencies';
         $button_text = 'Save Dependencies';
         $instructions = null;
         break;
       case self::ACTION_BLOCKS:
         $dialog_title = pht('Edit Blocking Tasks');
         $header_text = pht('Current Blocking Tasks');
         $button_text = pht('Save Blocking Tasks');
         $instructions = null;
         break;
     }
 
     return array(
       'target_plural_noun'    => $noun,
       'selected'              => $selected,
       'title'                 => $dialog_title,
       'header'                => $header_text,
       'button'                => $button_text,
       'instructions'          => $instructions,
     );
   }
 
-  private function getFilters(array $strings) {
-    if ($this->type == PholioMockPHIDType::TYPECONST) {
+  private function getFilters(array $strings, $attach_type) {
+    if ($attach_type == PholioMockPHIDType::TYPECONST) {
       $filters = array(
         'created' => 'Created By Me',
         'all' => 'All '.$strings['target_plural_noun'],
       );
     } else {
       $filters = array(
         'assigned' => 'Assigned to Me',
         'created' => 'Created By Me',
         'open' => 'All Open '.$strings['target_plural_noun'],
         'all' => 'All '.$strings['target_plural_noun'],
       );
     }
 
     return $filters;
   }
 
   private function getEdgeType($src_type, $dst_type) {
     $t_cmit = PhabricatorRepositoryCommitPHIDType::TYPECONST;
     $t_task = ManiphestTaskPHIDType::TYPECONST;
     $t_drev = DifferentialRevisionPHIDType::TYPECONST;
     $t_mock = PholioMockPHIDType::TYPECONST;
 
     $map = array(
       $t_cmit => array(
         $t_task => DiffusionCommitHasTaskEdgeType::EDGECONST,
       ),
       $t_task => array(
         $t_cmit => ManiphestTaskHasCommitEdgeType::EDGECONST,
         $t_task => ManiphestTaskDependsOnTaskEdgeType::EDGECONST,
         $t_drev => ManiphestTaskHasRevisionEdgeType::EDGECONST,
         $t_mock => ManiphestTaskHasMockEdgeType::EDGECONST,
       ),
       $t_drev => array(
         $t_drev => DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST,
         $t_task => DifferentialRevisionHasTaskEdgeType::EDGECONST,
       ),
       $t_mock => array(
         $t_task => PholioMockHasTaskEdgeType::EDGECONST,
       ),
     );
 
     if (empty($map[$src_type][$dst_type])) {
       return null;
     }
 
     return $map[$src_type][$dst_type];
   }
 
   private function raiseGraphCycleException(PhabricatorEdgeCycleException $ex) {
     $cycle = $ex->getCycle();
 
     $handles = $this->loadViewerHandles($cycle);
     $names = array();
     foreach ($cycle as $cycle_phid) {
       $names[] = $handles[$cycle_phid]->getFullName();
     }
     $names = implode(" \xE2\x86\x92 ", $names);
     throw new Exception(
       "You can not create that dependency, because it would create a ".
       "circular dependency: {$names}.");
   }
 
 }
diff --git a/src/applications/search/controller/PhabricatorSearchBaseController.php b/src/applications/search/controller/PhabricatorSearchBaseController.php
index 446406dfea..86573b38bd 100644
--- a/src/applications/search/controller/PhabricatorSearchBaseController.php
+++ b/src/applications/search/controller/PhabricatorSearchBaseController.php
@@ -1,18 +1,24 @@
 <?php
 
 abstract class PhabricatorSearchBaseController extends PhabricatorController {
 
+  const ACTION_ATTACH       = 'attach';
+  const ACTION_MERGE        = 'merge';
+  const ACTION_DEPENDENCIES = 'dependencies';
+  const ACTION_BLOCKS       = 'blocks';
+  const ACTION_EDGE         = 'edge';
+
   public function buildStandardPageResponse($view, array $data) {
     $page = $this->buildStandardPageView();
 
     $page->setApplicationName('Search');
     $page->setBaseURI('/search/');
     $page->setTitle(idx($data, 'title'));
     $page->setGlyph("\xC2\xBF");
     $page->appendChild($view);
 
     $response = new AphrontWebpageResponse();
     return $response->setContent($page->render());
   }
 
 }
diff --git a/src/applications/search/controller/PhabricatorSearchSelectController.php b/src/applications/search/controller/PhabricatorSearchSelectController.php
index 03d3ca4c69..f663cd03d7 100644
--- a/src/applications/search/controller/PhabricatorSearchSelectController.php
+++ b/src/applications/search/controller/PhabricatorSearchSelectController.php
@@ -1,79 +1,86 @@
 <?php
 
 final class PhabricatorSearchSelectController
   extends PhabricatorSearchBaseController {
 
-  private $type;
-
-  public function willProcessRequest(array $data) {
-    $this->type = $data['type'];
-  }
-
-  public function processRequest() {
-    $request = $this->getRequest();
+  public function handleRequest(AphrontRequest $request) {
     $user = $request->getUser();
+    $type = $request->getURIData('type');
+    $action = $request->getURIData('action');
 
     $query = new PhabricatorSavedQuery();
     $query_str = $request->getStr('query');
 
     $query->setEngineClassName('PhabricatorSearchApplicationSearchEngine');
     $query->setParameter('query', $query_str);
-    $query->setParameter('types', array($this->type));
+    $query->setParameter('types', array($type));
 
     $status_open = PhabricatorSearchRelationship::RELATIONSHIP_OPEN;
 
     switch ($request->getStr('filter')) {
       case 'assigned':
         $query->setParameter('ownerPHIDs', array($user->getPHID()));
         $query->setParameter('statuses', array($status_open));
         break;
       case 'created';
         $query->setParameter('authorPHIDs', array($user->getPHID()));
         // TODO - if / when we allow pholio mocks to be archived, etc
         // update this
-        if ($this->type != PholioMockPHIDType::TYPECONST) {
+        if ($type != PholioMockPHIDType::TYPECONST) {
           $query->setParameter('statuses', array($status_open));
         }
         break;
       case 'open':
         $query->setParameter('statuses', array($status_open));
         break;
     }
 
     $query->setParameter('excludePHIDs', array($request->getStr('exclude')));
 
+    $capabilities = array(PhabricatorPolicyCapability::CAN_VIEW);
+    switch ($action) {
+      case self::ACTION_MERGE:
+        $capabilities[] = PhabricatorPolicyCapability::CAN_EDIT;
+        break;
+      default:
+        break;
+    }
+
     $results = id(new PhabricatorSearchDocumentQuery())
       ->setViewer($user)
+      ->requireObjectCapabilities($capabilities)
       ->withSavedQuery($query)
       ->setOffset(0)
       ->setLimit(100)
       ->execute();
 
     $phids = array_fill_keys(mpull($results, 'getPHID'), true);
-    $phids += $this->queryObjectNames($query_str);
+    $phids += $this->queryObjectNames($query_str, $capabilities);
 
     $phids = array_keys($phids);
     $handles = $this->loadViewerHandles($phids);
 
     $data = array();
     foreach ($handles as $handle) {
       $view = new PhabricatorHandleObjectSelectorDataView($handle);
       $data[] = $view->renderData();
     }
 
     return id(new AphrontAjaxResponse())->setContent($data);
   }
 
-  private function queryObjectNames($query) {
-    $viewer = $this->getRequest()->getUser();
+  private function queryObjectNames($query, $capabilities) {
+    $request = $this->getRequest();
+    $viewer = $request->getUser();
 
     $objects = id(new PhabricatorObjectQuery())
       ->setViewer($viewer)
-      ->withTypes(array($this->type))
+      ->requireCapabilities($capabilities)
+      ->withTypes(array($request->getURIData('type')))
       ->withNames(array($query))
       ->execute();
 
     return mpull($objects, 'getPHID');
   }
 
 }
diff --git a/src/applications/search/query/PhabricatorSearchDocumentQuery.php b/src/applications/search/query/PhabricatorSearchDocumentQuery.php
index b5591d8eca..903d40dd5a 100644
--- a/src/applications/search/query/PhabricatorSearchDocumentQuery.php
+++ b/src/applications/search/query/PhabricatorSearchDocumentQuery.php
@@ -1,84 +1,98 @@
 <?php
 
 final class PhabricatorSearchDocumentQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $savedQuery;
+  private $objectCapabilities;
 
   public function withSavedQuery(PhabricatorSavedQuery $query) {
     $this->savedQuery = $query;
     return $this;
   }
 
+  public function requireObjectCapabilities(array $capabilities) {
+    $this->objectCapabilities = $capabilities;
+    return $this;
+  }
+
+  protected function getRequiredObjectCapabilities() {
+    if ($this->objectCapabilities) {
+      return $this->objectCapabilities;
+    }
+    return $this->getRequiredCapabilities();
+  }
+
   protected function loadPage() {
     $phids = $this->loadDocumentPHIDsWithoutPolicyChecks();
 
     $handles = id(new PhabricatorHandleQuery())
       ->setViewer($this->getViewer())
+      ->requireObjectCapabilities($this->getRequiredObjectCapabilities())
       ->withPHIDs($phids)
       ->execute();
 
     // Retain engine order.
     $handles = array_select_keys($handles, $phids);
 
     return $handles;
   }
 
   protected function willFilterPage(array $handles) {
     // NOTE: This is used by the object selector dialog to exclude the object
     // you're looking at, so that, e.g., a task can't be set as a dependency
     // of itself in the UI.
 
     // TODO: Remove this after object selection moves to ApplicationSearch.
 
     $exclude = array();
     if ($this->savedQuery) {
       $exclude_phids = $this->savedQuery->getParameter('excludePHIDs', array());
       $exclude = array_fuse($exclude_phids);
     }
 
     foreach ($handles as $key => $handle) {
       if (!$handle->isComplete()) {
         unset($handles[$key]);
         continue;
       }
       if ($handle->getPolicyFiltered()) {
         unset($handles[$key]);
         continue;
       }
       if (isset($exclude[$handle->getPHID()])) {
         unset($handles[$key]);
         continue;
       }
     }
 
     return $handles;
   }
 
   public function loadDocumentPHIDsWithoutPolicyChecks() {
     $query = id(clone($this->savedQuery))
       ->setParameter('offset', $this->getOffset())
       ->setParameter('limit', $this->getRawResultLimit());
 
     $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
 
     return $engine->executeSearch($query);
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorSearchApplication';
   }
 
   protected function getResultCursor($result) {
     throw new Exception(
       pht(
         'This query does not support cursor paging; it must be offset '.
         'paged.'));
   }
 
   protected function nextPage(array $page) {
     $this->setOffset($this->getOffset() + count($page));
     return $this;
   }
 
 }