Clean up ExecutionRequirements

- remove BaseSpawn.Local; instead, all callers pass in the full set of
  execution requirements they want to set
- disable caching and sandboxing for the symlink tree action - it does not
  declare outputs, so it can't be cached or sandboxed (fixes #4041)
- centralize the existing execution requirements in the ExecutionRequirements
  class
- centralize checking for execution requirements in the Spawn class
  (it's possible that we may need a more decentralized, extensible design in
  the future, but for now having them in a single place is simple and
  effective)
- update the documentation
- forward the relevant tags to execution requirements in TargetUtils (progress
  on #3960)
- this also contributes to #4153

PiperOrigin-RevId: 177288598
diff --git a/src/main/java/com/google/devtools/build/docgen/templates/attributes/common/tags.html b/src/main/java/com/google/devtools/build/docgen/templates/attributes/common/tags.html
index 5ccc56b..55b73d2 100644
--- a/src/main/java/com/google/devtools/build/docgen/templates/attributes/common/tags.html
+++ b/src/main/java/com/google/devtools/build/docgen/templates/attributes/common/tags.html
@@ -21,6 +21,20 @@
 </p>
 
 <ul>
+  <li><code>no-sandbox</code> keyword results in the action or test never being
+    <a href="../user-manual.html#sandboxing">sandboxed</a>; it can still be
+    cached or run remotely - use <code>no-cache</code> or <code>no-remote</code>
+    to prevent either or both of those.
+  </li>
+
+  <li><code>no-cache</code> keyword results in the action or test never being
+    cached.
+  </li>
+
+  <li><code>no-remote</code> keyword results in the action or test never being
+    executed remotely (but it may be cached remotely).
+  </li>
+
   <li><code>local</code> keyword results in the action or test never being
     run remotely or inside the
     <a href="../user-manual.html#sandboxing">sandbox</a>.
@@ -30,7 +44,8 @@
 
   <li><code>block-network</code> keyword blocks access to the external
     network from inside the sandbox. In this case, only communication
-    with localhost is allowed.
+    with localhost is allowed. This tag only has an effect if sandboxing is
+    enabled.
   </li>
 
   <li><code>requires-fakeroot</code> runs the test or action as uid and gid 0 (i.e., the root
diff --git a/src/main/java/com/google/devtools/build/lib/actions/BaseSpawn.java b/src/main/java/com/google/devtools/build/lib/actions/BaseSpawn.java
index 901b225..bc52055 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/BaseSpawn.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/BaseSpawn.java
@@ -69,16 +69,6 @@
   }
 
   @Override
-  public boolean hasNoSandbox() {
-    return executionInfo.containsKey("nosandbox");
-  }
-
-  @Override
-  public boolean isRemotable() {
-    return !executionInfo.containsKey("local");
-  }
-
-  @Override
   public final ImmutableMap<String, String> getExecutionInfo() {
     return executionInfo;
   }
@@ -157,30 +147,4 @@
   public String getMnemonic() {
     return action.getMnemonic();
   }
-
-  /** A local spawn. */
-  public static class Local extends BaseSpawn {
-    public Local(
-        List<String> arguments, Map<String, String> environment, ActionExecutionMetadata action,
-        ResourceSet localResources) {
-      this(arguments, environment, ImmutableMap.<String, String>of(), action, localResources);
-    }
-
-    public Local(
-        List<String> arguments,
-        Map<String, String> environment,
-        Map<String, String> executionInfo,
-        ActionExecutionMetadata action,
-        ResourceSet localResources) {
-      super(arguments, environment, buildExecutionInfo(executionInfo), action, localResources);
-    }
-
-    private static ImmutableMap<String, String> buildExecutionInfo(
-        Map<String, String> additionalExecutionInfo) {
-      ImmutableMap.Builder<String, String> executionInfo = ImmutableMap.builder();
-      executionInfo.putAll(additionalExecutionInfo);
-      executionInfo.put("local", "");
-      return executionInfo.build();
-    }
-  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/DelegateSpawn.java b/src/main/java/com/google/devtools/build/lib/actions/DelegateSpawn.java
index 8341301..fd548b6 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/DelegateSpawn.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/DelegateSpawn.java
@@ -36,16 +36,6 @@
   }
 
   @Override
-  public boolean isRemotable() {
-    return spawn.isRemotable();
-  }
-
-  @Override
-  public boolean hasNoSandbox() {
-    return spawn.hasNoSandbox();
-  }
-
-  @Override
   public ImmutableList<Artifact> getFilesetManifests() {
     return spawn.getFilesetManifests();
   }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ExecutionRequirements.java b/src/main/java/com/google/devtools/build/lib/actions/ExecutionRequirements.java
index 22242de..fced368 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ExecutionRequirements.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ExecutionRequirements.java
@@ -145,8 +145,40 @@
       ImmutableMap.of(SUPPORTS_WORKERS, "1");
 
   /**
-   * Whether we should disable remote caching of an action. This can be set to force a rerun of an
-   * action even if there is a cache entry for it.
+   * Requires local execution without sandboxing for a spawn.
+   *
+   * <p>This tag is deprecated; use no-cache, no-remote, or no-sandbox instead.
+   */
+  public static final String LOCAL = "local";
+
+  /**
+   * Disables local and remote caching for a spawn, but note that the local action cache may still
+   * apply.
+   *
+   * <p>This tag can also be set on an action, in which case it completely disables all caching for
+   * that action, but note that action-generated spawns may still be cached, unless they also carry
+   * this tag.
    */
   public static final String NO_CACHE = "no-cache";
+
+  /** Disables local sandboxing of a spawn. */
+  public static final String LEGACY_NOSANDBOX = "nosandbox";
+
+  /** Disables local sandboxing of a spawn. */
+  public static final String NO_SANDBOX = "no-sandbox";
+
+  /** Disables remote execution of a spawn. */
+  public static final String NO_REMOTE = "no-remote";
+
+  /**
+   * Disables networking for a spawn if possible (only if sandboxing is enabled and if the sandbox
+   * supports it).
+   */
+  public static final String BLOCK_NETWORK = "block-network";
+
+  /**
+   * On linux, if sandboxing is enabled, ensures that a spawn is run with uid 0, i.e., root. Has no
+   * effect otherwise.
+   */
+  public static final String REQUIRES_FAKEROOT = "requires-fakeroot";
 }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/SimpleSpawn.java b/src/main/java/com/google/devtools/build/lib/actions/SimpleSpawn.java
index 564460e..3857783 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/SimpleSpawn.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/SimpleSpawn.java
@@ -82,16 +82,6 @@
   }
 
   @Override
-  public boolean hasNoSandbox() {
-    return executionInfo.containsKey("nosandbox");
-  }
-
-  @Override
-  public boolean isRemotable() {
-    return !executionInfo.containsKey("local");
-  }
-
-  @Override
   public final ImmutableMap<String, String> getExecutionInfo() {
     return executionInfo;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Spawn.java b/src/main/java/com/google/devtools/build/lib/actions/Spawn.java
index 6963b01..3a59d9f 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/Spawn.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/Spawn.java
@@ -25,17 +25,6 @@
  * of files it is expected to read and write.
  */
 public interface Spawn {
-
-  /**
-   * Returns true iff this command may be executed remotely.
-   */
-  boolean isRemotable();
-
-  /**
-   * Returns true iff this command should be executed without a sandbox.
-   */
-  boolean hasNoSandbox();
-
   /**
    * Out-of-band data for this spawn. This can be used to signal hints (hardware requirements,
    * local vs. remote) to the execution subsystem.
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Spawns.java b/src/main/java/com/google/devtools/build/lib/actions/Spawns.java
index e1156e9..4448ea8 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/Spawns.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/Spawns.java
@@ -32,6 +32,21 @@
     return !spawn.getExecutionInfo().containsKey(ExecutionRequirements.NO_CACHE);
   }
 
+  public static boolean mayBeSandboxed(Spawn spawn) {
+    return !spawn.getExecutionInfo().containsKey(ExecutionRequirements.LEGACY_NOSANDBOX)
+        && !spawn.getExecutionInfo().containsKey(ExecutionRequirements.NO_SANDBOX)
+        && !spawn.getExecutionInfo().containsKey(ExecutionRequirements.LOCAL);
+  }
+
+  public static boolean requiresNetwork(Spawn spawn) {
+    return !spawn.getExecutionInfo().containsKey(ExecutionRequirements.BLOCK_NETWORK);
+  }
+
+  public static boolean mayBeExecutedRemotely(Spawn spawn) {
+    return !spawn.getExecutionInfo().containsKey(ExecutionRequirements.LOCAL)
+        && !spawn.getExecutionInfo().containsKey(ExecutionRequirements.NO_REMOTE);
+  }
+
   /**
    * Parse the timeout key in the spawn execution info, if it exists. Otherwise, return -1.
    */
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BinTools.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BinTools.java
index deb987b..dc52388 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BinTools.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BinTools.java
@@ -33,15 +33,22 @@
  * using relative paths from the execution root.
  */
 public final class BinTools {
-  private final BlazeDirectories directories;
+  private final Path embeddedBinariesRoot;
   private final Path execrootParent;
   private final ImmutableList<String> embeddedTools;
 
   private Path binDir;  // the working bin directory under execRoot
 
   private BinTools(BlazeDirectories directories, ImmutableList<String> tools) {
-    this.directories = directories;
-    this.execrootParent = directories.getExecRoot().getParentDirectory();
+    this(
+        directories.getEmbeddedBinariesRoot(),
+        directories.getExecRoot().getParentDirectory(),
+        tools);
+  }
+
+  private BinTools(Path embeddedBinariesRoot, Path execrootParent, ImmutableList<String> tools) {
+    this.embeddedBinariesRoot = embeddedBinariesRoot;
+    this.execrootParent = execrootParent;
     ImmutableList.Builder<String> builder = ImmutableList.builder();
     // Files under embedded_tools shouldn't be copied to under _bin dir
     // They won't be used during action execution time.
@@ -69,8 +76,8 @@
    */
   @VisibleForTesting
   public static BinTools empty(BlazeDirectories directories) {
-    return new BinTools(directories, ImmutableList.<String>of()).setBinDir(
-        directories.getWorkspace().getBaseName());
+    return new BinTools(directories, ImmutableList.<String>of())
+        .setBinDir(directories.getWorkspace().getBaseName());
   }
 
   /**
@@ -80,8 +87,21 @@
    */
   @VisibleForTesting
   public static BinTools forUnitTesting(BlazeDirectories directories, Iterable<String> tools) {
-    return new BinTools(directories, ImmutableList.copyOf(tools)).setBinDir(
-        directories.getWorkspace().getBaseName());
+    return new BinTools(directories, ImmutableList.copyOf(tools))
+        .setBinDir(directories.getWorkspace().getBaseName());
+  }
+
+  /**
+   * Creates an instance for testing without actually symlinking the tools.
+   *
+   * <p>Used for tests that need a set of embedded tools to be present, but not the actual files.
+   */
+  @VisibleForTesting
+  public static BinTools forUnitTesting(Path execroot, Iterable<String> tools) {
+    return new BinTools(
+        execroot.getRelative("/fake/embedded/tools"),
+        execroot.getParentDirectory(),
+        ImmutableList.copyOf(tools)).setBinDir(execroot.getBaseName());
   }
 
   /**
@@ -168,7 +188,7 @@
 
   private void setupTool(String embeddedPath) throws ExecException {
     Preconditions.checkNotNull(binDir);
-    Path sourcePath = directories.getEmbeddedBinariesRoot().getRelative(embeddedPath);
+    Path sourcePath = embeddedBinariesRoot.getRelative(embeddedPath);
     Path linkPath = binDir.getRelative(PathFragment.create(embeddedPath).getBaseName());
     linkTool(sourcePath, linkPath);
   }
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
index b5f4b92..b9a055b 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
@@ -18,11 +18,15 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
-import com.google.devtools.build.lib.actions.AbstractAction;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
-import com.google.devtools.build.lib.actions.BaseSpawn;
+import com.google.devtools.build.lib.actions.ActionExecutionMetadata;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.ExecutionRequirements;
 import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.SimpleSpawn;
+import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.SpawnResult;
 import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.analysis.config.BinTools;
@@ -37,18 +41,17 @@
 import java.util.List;
 
 /**
- * Helper class responsible for the symlink tree creation.
- * Used to generate runfiles and fileset symlink farms.
+ * Helper class responsible for the symlink tree creation. Used to generate runfiles and fileset
+ * symlink farms.
  */
 public final class SymlinkTreeHelper {
   @VisibleForTesting
   public static final String BUILD_RUNFILES = "build-runfiles" + OsUtils.executableExtension();
 
   /**
-   * These actions run faster overall when serialized, because most of their
-   * cost is in the ext2 block allocator, and there's less seeking required if
-   * their directory creations get non-interleaved allocations. So we give them
-   * a huge resource cost.
+   * These actions run faster overall when serialized, because most of their cost is in the ext2
+   * block allocator, and there's less seeking required if their directory creations get
+   * non-interleaved allocations. So we give them a huge resource cost.
    */
   public static final ResourceSet RESOURCE_SET = ResourceSet.createWithRamCpuIo(1000, 0.5, 0.75);
 
@@ -57,16 +60,14 @@
   private final boolean filesetTree;
 
   /**
-   * Creates SymlinkTreeHelper instance. Can be used independently of
-   * SymlinkTreeAction.
+   * Creates SymlinkTreeHelper instance. Can be used independently of SymlinkTreeAction.
    *
    * @param inputManifest exec path to the input runfiles manifest
    * @param symlinkTreeRoot the root of the symlink tree to be created
    * @param filesetTree true if this is fileset symlink tree,
    *                    false if this is a runfiles symlink tree.
    */
-  public SymlinkTreeHelper(Path inputManifest, Path symlinkTreeRoot,
-      boolean filesetTree) {
+  public SymlinkTreeHelper(Path inputManifest, Path symlinkTreeRoot, boolean filesetTree) {
     this.inputManifest = inputManifest;
     this.symlinkTreeRoot = symlinkTreeRoot;
     this.filesetTree = filesetTree;
@@ -77,20 +78,19 @@
   }
 
   /**
-   * Creates a symlink tree using a CommandBuilder. This means that the symlink
-   * tree will always be present on the developer's workstation. Useful when
-   * running commands locally.
+   * Creates a symlink tree using a CommandBuilder. This means that the symlink tree will always be
+   * present on the developer's workstation. Useful when running commands locally.
    *
-   * Warning: this method REALLY executes the command on the box Blaze was
-   * run on, without any kind of synchronization, locking, or anything else.
+   * Warning: this method REALLY executes the command on the box Bazel is running on, without any
+   * kind of synchronization, locking, or anything else.
    *
    * @param config the configuration that is used for creating the symlink tree.
    * @throws CommandException
    */
-  public void createSymlinksUsingCommand(Path execRoot,
-      BuildConfiguration config, BinTools binTools) throws CommandException {
+  public void createSymlinksUsingCommand(
+      Path execRoot, BuildConfiguration config, BinTools binTools)
+          throws CommandException {
     List<String> argv = getSpawnArgumentList(execRoot, binTools);
-
     CommandBuilder builder = new CommandBuilder();
     builder.addArgs(argv);
     builder.setWorkingDir(execRoot);
@@ -101,30 +101,30 @@
    * Creates symlink tree using appropriate method. At this time tree always created using
    * build-runfiles helper application.
    *
-   * <p>Note: method may try to acquire resources - meaning that it would block for undetermined
-   * period of time. If it is interrupted during that wait, ExecException will be thrown but
-   * interrupted bit will be preserved.
-   *
-   * @param action action instance that requested symlink tree creation
+   * @param owner action instance that requested symlink tree creation
    * @param actionExecutionContext Services that are in the scope of the action.
    * @param enableRunfiles
    * @return a list of SpawnResults created during symlink creation, if any
    */
   public List<SpawnResult> createSymlinks(
-      AbstractAction action,
+      ActionExecutionMetadata owner,
       ActionExecutionContext actionExecutionContext,
       BinTools binTools,
       ImmutableMap<String, String> shellEnvironment,
+      Artifact inputManifestArtifact,
       boolean enableRunfiles)
-      throws ExecException, InterruptedException {
+          throws ExecException, InterruptedException {
+    Preconditions.checkState(inputManifestArtifact.getPath().equals(inputManifest));
     if (enableRunfiles) {
-      List<String> args =
-          getSpawnArgumentList(
-              actionExecutionContext.getExecRoot(), binTools);
       return actionExecutionContext
-          .getSpawnActionContext(action.getMnemonic())
+          .getSpawnActionContext(owner.getMnemonic())
           .exec(
-              new BaseSpawn.Local(args, shellEnvironment, action, RESOURCE_SET),
+              createSpawn(
+                  owner,
+                  actionExecutionContext.getExecRoot(),
+                  binTools,
+                  shellEnvironment,
+                  inputManifestArtifact),
               actionExecutionContext);
     } else {
       // Pretend we created the runfiles tree by copying the manifest
@@ -138,10 +138,30 @@
     }
   }
 
+  @VisibleForTesting
+  Spawn createSpawn(
+      ActionExecutionMetadata owner,
+      Path execRoot,
+      BinTools binTools,
+      ImmutableMap<String, String> environment,
+      ActionInput inputManifestArtifact) {
+    return new SimpleSpawn(
+        owner,
+        getSpawnArgumentList(execRoot, binTools),
+        environment,
+        ImmutableMap.of(
+            ExecutionRequirements.LOCAL, "",
+            ExecutionRequirements.NO_CACHE, "",
+            ExecutionRequirements.NO_SANDBOX, ""),
+        ImmutableList.of(inputManifestArtifact),
+        /*outputs=*/ ImmutableList.of(),
+        RESOURCE_SET);
+  }
+
   /**
    * Returns the complete argument list build-runfiles has to be called with.
    */
-  private List<String> getSpawnArgumentList(Path execRoot, BinTools binTools) {
+  private ImmutableList<String> getSpawnArgumentList(Path execRoot, BinTools binTools) {
     PathFragment path = binTools.getExecPath(BUILD_RUNFILES);
     Preconditions.checkNotNull(path, BUILD_RUNFILES + " not found in embedded tools");
 
@@ -156,6 +176,6 @@
     args.add(inputManifest.relativeTo(execRoot).getPathString());
     args.add(symlinkTreeRoot.relativeTo(execRoot).getPathString());
 
-    return args;
+    return ImmutableList.copyOf(args);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
index a0f87d4..14802ac 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
@@ -55,18 +55,25 @@
         AutoProfiler.logged(
             "running " + action.prettyPrint(), logger, /*minTimeForLoggingInMilliseconds=*/ 100)) {
       try {
-        SymlinkTreeHelper helper = new SymlinkTreeHelper(
-            action.getInputManifest().getPath(),
-            action.getOutputManifest().getPath().getParentDirectory(), action.isFilesetTree());
         if (outputService != null && outputService.canCreateSymlinkTree()) {
-          outputService.createSymlinkTree(action.getInputManifest().getPath(),
+          outputService.createSymlinkTree(
+              action.getInputManifest().getPath(),
               action.getOutputManifest().getPath(),
               action.isFilesetTree(),
               action.getOutputManifest().getExecPath().getParentDirectory());
           return ImmutableList.of();
         } else {
+          SymlinkTreeHelper helper = new SymlinkTreeHelper(
+              action.getInputManifest().getPath(),
+              action.getOutputManifest().getPath().getParentDirectory(),
+              action.isFilesetTree());
           return helper.createSymlinks(
-              action, actionExecutionContext, binTools, shellEnvironment, enableRunfiles);
+              action,
+              actionExecutionContext,
+              binTools,
+              shellEnvironment,
+              action.getInputManifest(),
+              enableRunfiles);
         }
       } catch (ExecException e) {
         throw e.toActionExecutionException(
diff --git a/src/main/java/com/google/devtools/build/lib/exec/TestStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/TestStrategy.java
index 85d1d1d6..e251cd6 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/TestStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/TestStrategy.java
@@ -410,7 +410,12 @@
 
     new SymlinkTreeHelper(execSettings.getInputManifest().getPath(), runfilesDir, false)
         .createSymlinks(
-            testAction, actionExecutionContext, binTools, shellEnvironment, enableRunfiles);
+            testAction,
+            actionExecutionContext,
+            binTools,
+            shellEnvironment,
+            execSettings.getInputManifest(),
+            enableRunfiles);
 
     actionExecutionContext.getEventHandler()
         .handle(Event.progress(testAction.getProgressMessage()));
diff --git a/src/main/java/com/google/devtools/build/lib/packages/TargetUtils.java b/src/main/java/com/google/devtools/build/lib/packages/TargetUtils.java
index 6ec33404..702b9d6 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/TargetUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/TargetUtils.java
@@ -203,16 +203,23 @@
   }
 
   /**
-   * Returns the execution info. These include execution requirement
-   * tags ('requires-*' as well as "local") as keys with empty values.
+   * Returns the execution info. These include execution requirement tags ('block-*', 'requires-*',
+   * 'no-*', 'supports-*', 'disable-*', 'local', and 'cpu:*') as keys with empty values.
    */
   public static Map<String, String> getExecutionInfo(Rule rule) {
     // tags may contain duplicate values.
     Map<String, String> map = new HashMap<>();
     for (String tag :
         NonconfigurableAttributeMapper.of(rule).get(CONSTRAINTS_ATTR, Type.STRING_LIST)) {
+      // We don't want to pollute the execution info with random things, and we also need to reserve
+      // some internal tags that we don't allow to be set on targets. We also don't want to
+      // exhaustively enumerate all the legal values here. Right now, only a ~small set of tags is
+      // recognized by Bazel.
       if (tag.startsWith("block-")
           || tag.startsWith("requires-")
+          || tag.startsWith("no-")
+          || tag.startsWith("supports-")
+          || tag.startsWith("disable-")
           || tag.equals("local")
           || tag.startsWith("cpu:")) {
         map.put(tag, "");
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java
index 8dae278..6c4b0a8 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnRunner.java
@@ -108,7 +108,7 @@
   @Override
   public SpawnResult exec(Spawn spawn, SpawnExecutionPolicy policy)
       throws ExecException, InterruptedException, IOException {
-    if (!spawn.isRemotable() || remoteCache == null) {
+    if (!Spawns.mayBeExecutedRemotely(spawn) || remoteCache == null) {
       return fallbackRunner.exec(spawn, policy);
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxedSpawnRunner.java
index 1f6af91..97fc677 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxedSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/DarwinSandboxedSpawnRunner.java
@@ -24,6 +24,7 @@
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.SpawnActionContext;
 import com.google.devtools.build.lib.actions.SpawnResult;
+import com.google.devtools.build.lib.actions.Spawns;
 import com.google.devtools.build.lib.exec.apple.XCodeLocalEnvProvider;
 import com.google.devtools.build.lib.exec.local.LocalEnvProvider;
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
@@ -197,7 +198,7 @@
     Map<String, String> environment =
         localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), execRoot, tmpDir, productName);
 
-    boolean allowNetworkForThisSpawn = allowNetwork || SandboxHelpers.shouldAllowNetwork(spawn);
+    boolean allowNetworkForThisSpawn = allowNetwork || Spawns.requiresNetwork(spawn);
     SandboxedSpawn sandbox = new SymlinkedSandboxedSpawn(
         sandboxPath,
         sandboxExecRoot,
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
index aee3897..a16497f 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
@@ -19,8 +19,10 @@
 import com.google.common.collect.Maps;
 import com.google.common.io.ByteStreams;
 import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.ExecutionRequirements;
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.SpawnResult;
+import com.google.devtools.build.lib.actions.Spawns;
 import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.exec.local.LocalEnvProvider;
@@ -134,8 +136,8 @@
             writableDirs,
             getTmpfsPaths(),
             getReadOnlyBindMounts(blazeDirs, sandboxExecRoot),
-            allowNetwork || SandboxHelpers.shouldAllowNetwork(spawn),
-            spawn.getExecutionInfo().containsKey("requires-fakeroot"));
+            allowNetwork || Spawns.requiresNetwork(spawn),
+            spawn.getExecutionInfo().containsKey(ExecutionRequirements.REQUIRES_FAKEROOT));
     Map<String, String> environment =
         localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), execRoot, tmpDir, productName);
 
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java
index 53173e4..ee496e8 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxActionContextProvider.java
@@ -20,6 +20,7 @@
 import com.google.devtools.build.lib.actions.ResourceManager;
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.SpawnResult;
+import com.google.devtools.build.lib.actions.Spawns;
 import com.google.devtools.build.lib.exec.ActionContextProvider;
 import com.google.devtools.build.lib.exec.SpawnRunner;
 import com.google.devtools.build.lib.exec.apple.XCodeLocalEnvProvider;
@@ -121,7 +122,7 @@
     @Override
     public SpawnResult exec(Spawn spawn, SpawnExecutionPolicy policy)
         throws InterruptedException, IOException, ExecException {
-      if (!spawn.isRemotable() || spawn.hasNoSandbox()) {
+      if (!Spawns.mayBeSandboxed(spawn)) {
         return fallbackSpawnRunner.exec(spawn, policy);
       } else {
         return sandboxSpawnRunner.exec(spawn, policy);
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java
index 94d1c11..58906fd 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java
@@ -21,6 +21,7 @@
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander;
 import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.Spawns;
 import com.google.devtools.build.lib.analysis.test.TestConfiguration;
 import com.google.devtools.build.lib.exec.SpawnInputExpander;
 import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionPolicy;
@@ -115,7 +116,7 @@
 
   /**
    * Returns true if the build options are set in a way that requires network access for all
-   * actions. This is separate from {@link #shouldAllowNetwork(Spawn)} to avoid having to keep a
+   * actions. This is separate from {@link Spawns#requiresNetwork} to avoid having to keep a
    * reference to the full set of build options (and also for performance, since this only needs to
    * be checked once-per-build).
    */
@@ -128,15 +129,4 @@
         .testArguments
         .contains("--wrapper_script_flag=--debug");
   }
-
-  /** Returns true if this specific spawn requires network access. */
-  static boolean shouldAllowNetwork(Spawn spawn) {
-    // If the Spawn requests to block network access, do so.
-    if (spawn.getExecutionInfo().containsKey("block-network")) {
-      return false;
-    }
-
-    // Network access is allowed by default.
-    return true;
-  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeHelperTest.java b/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeHelperTest.java
new file mode 100644
index 0000000..3baee79
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeHelperTest.java
@@ -0,0 +1,67 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.exec;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.ActionExecutionMetadata;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionInputHelper;
+import com.google.devtools.build.lib.actions.ExecutionRequirements;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+import com.google.devtools.build.lib.exec.util.FakeOwner;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SymlinkTreeHelper}. */
+@RunWith(JUnit4.class)
+public final class SymlinkTreeHelperTest {
+  private final FileSystem fs = new InMemoryFileSystem();
+
+  @Test
+  public void checkCreatedSpawn() {
+    ActionExecutionMetadata owner = new FakeOwner("SymlinkTree", "Creating it");
+    Path execRoot = fs.getPath("/my/workspace");
+    Path inputManifestPath = execRoot.getRelative("input_manifest");
+    ActionInput inputManifest = ActionInputHelper.fromPath(inputManifestPath.asFragment());
+    Spawn spawn =
+        new SymlinkTreeHelper(
+            inputManifestPath,
+            fs.getPath("/my/workspace/output/MANIFEST"),
+            false)
+        .createSpawn(
+            owner,
+            execRoot,
+            BinTools.forUnitTesting(execRoot, ImmutableList.of(SymlinkTreeHelper.BUILD_RUNFILES)),
+            ImmutableMap.of(),
+            inputManifest);
+    assertThat(spawn.getResourceOwner()).isSameAs(owner);
+    assertThat(spawn.getEnvironment()).isEmpty();
+    assertThat(spawn.getExecutionInfo()).containsExactly(
+        ExecutionRequirements.LOCAL, "",
+        ExecutionRequirements.NO_CACHE, "",
+        ExecutionRequirements.NO_SANDBOX, "");
+    assertThat(spawn.getInputFiles()).containsExactly(inputManifest);
+    // At this time, the spawn does not declare any output files.
+    assertThat(spawn.getOutputFiles()).isEmpty();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/TargetUtilsTest.java b/src/test/java/com/google/devtools/build/lib/packages/TargetUtilsTest.java
index 6d49de9..eb49366 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/TargetUtilsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/TargetUtilsTest.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Predicate;
 import com.google.common.collect.Lists;
 import com.google.devtools.build.lib.packages.util.PackageLoadingTestCase;
+import java.util.Map;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -75,4 +76,24 @@
     assertThat(tagFilter.apply(tag2)).isTrue();
     assertThat(tagFilter.apply(tag1b)).isFalse();
   }
+
+  @Test
+  public void testExecutionInfo() throws Exception {
+    scratch.file(
+        "tests/BUILD",
+        "sh_binary(name = 'tag1', srcs=['sh.sh'], tags=['supports-workers', 'no-cache'])",
+        "sh_binary(name = 'tag2', srcs=['sh.sh'], tags=['disable-local-prefetch'])",
+        "sh_binary(name = 'tag1b', srcs=['sh.sh'], tags=['local', 'cpu:4'])");
+
+    Rule tag1 = (Rule) getTarget("//tests:tag1");
+    Rule tag2 = (Rule) getTarget("//tests:tag2");
+    Rule tag1b = (Rule) getTarget("//tests:tag1b");
+
+    Map<String, String> execInfo = TargetUtils.getExecutionInfo(tag1);
+    assertThat(execInfo).containsExactly("supports-workers", "", "no-cache", "");
+    execInfo = TargetUtils.getExecutionInfo(tag2);
+    assertThat(execInfo).containsExactly("disable-local-prefetch", "");
+    execInfo = TargetUtils.getExecutionInfo(tag1b);
+    assertThat(execInfo).containsExactly("local", "", "cpu:4", "");
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategyTest.java b/src/test/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategyTest.java
index 342680b..d2497ff 100644
--- a/src/test/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategyTest.java
@@ -26,10 +26,10 @@
 import com.google.devtools.build.lib.actions.ActionInputPrefetcher;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander;
-import com.google.devtools.build.lib.actions.BaseSpawn;
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.ResourceManager;
 import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.SimpleSpawn;
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.SpawnActionContext;
 import com.google.devtools.build.lib.actions.SpawnResult;
@@ -58,7 +58,6 @@
 import com.google.devtools.common.options.Options;
 import com.google.devtools.common.options.OptionsParser;
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import org.junit.Before;
@@ -148,10 +147,13 @@
   }
 
   private Spawn createSpawn(String... arguments) {
-    return new BaseSpawn.Local(
-        Arrays.asList(arguments),
-        ImmutableMap.<String, String>of(),
+    return new SimpleSpawn(
         new ActionsTestUtil.NullAction(),
+        ImmutableList.copyOf(arguments),
+        /*environment=*/ ImmutableMap.of(),
+        /*executionInfo=*/ ImmutableMap.of(),
+        /*inputs=*/ ImmutableList.of(),
+        /*outputs=*/ ImmutableList.of(),
         ResourceSet.ZERO);
   }
 
@@ -230,10 +232,13 @@
       // down where that env var is coming from.
       return;
     }
-    Spawn spawn = new BaseSpawn.Local(
-        Arrays.asList("/usr/bin/env"),
-        ImmutableMap.of("foo", "bar", "baz", "boo"),
+    Spawn spawn = new SimpleSpawn(
         new ActionsTestUtil.NullAction(),
+        ImmutableList.of("/usr/bin/env"),
+        /*environment=*/ ImmutableMap.of("foo", "bar", "baz", "boo"),
+        /*executionInfo=*/ ImmutableMap.of(),
+        /*inputs=*/ ImmutableList.of(),
+        /*outputs=*/ ImmutableList.of(),
         ResourceSet.ZERO);
     run(spawn);
     assertThat(Sets.newHashSet(out().split("\n"))).isEqualTo(Sets.newHashSet("foo=bar", "baz=boo"));