Repository: puppetlabs/wash
Pull request #298 (2019-05-25 23:17:05)

Update plugin tutorial docs with recent changes
Resolves https://github.com/puppetlabs/wash/issues/283

Contributions to this project require sign-off consistent with the [Developers Certificate of Origin](https://developercertificate.org). This can be as simple as using `git commit -s` on each commit.


Patch:
diff --git a/api/error.go b/api/error.go
index f7e094e..3815b1c 100644
--- a/api/error.go
+++ b/api/error.go
@@ -177,9 +177,9 @@ func duplicateCNameResponse(e plugin.DuplicateCNameErr) *errorResponse {
 	fields := apitypes.ErrorFields{
 		"parent_id":                           e.ParentID,
 		"first_child_name":                    e.FirstChildName,
-		"first_child_slash_replacement_char":  e.FirstChildSlashReplacementChar,
+		"first_child_slash_replacer":          e.FirstChildSlashReplacer,
 		"second_child_name":                   e.SecondChildName,
-		"second_child_slash_replacement_char": e.SecondChildSlashReplacementChar,
+		"second_child_slash_replacer":         e.SecondChildSlashReplacer,
 		"cname":                               e.CName,
 	}
 
diff --git a/docs/docs/go_plugins/index.html b/docs/docs/core_plugins/index.html
similarity index 50%
rename from docs/docs/go_plugins/index.html
rename to docs/docs/core_plugins/index.html
index 82b21ae..bac75fe 100644
--- a/docs/docs/go_plugins/index.html
+++ b/docs/docs/core_plugins/index.html
@@ -4,10 +4,10 @@
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
 
-    <title>Go Plugins | home</title>
+    <title>Core Plugins | home</title>
 
     <style>body{margin:40px auto;max-width:650px;line-height:1.6;font-size:18px;color:#444;padding:0 10px}h1,h2,h3{line-height:1.2}div.header h1{padding-top:0;padding-bottom:8px;margin-bottom:24px;font-size:18px;font-weight:400;border-bottom:1px solid}.header-menu{float:right}ul.pagination{list-style-type:none;text-align:center;padding:0}ul.pagination>li{padding:0 8px;display:inline-block}div.footer{border-top:1px solid;text-align:center}img{max-width:100%;max-height:100%;display:block;margin-left:auto;margin-right:auto}</style>
-    <link href="https://puppetlabs.github.io/wash/docs/go_plugins/index.xml" rel="alternate" type="application/rss+xml" title="home" />
+    <link href="https://puppetlabs.github.io/wash/docs/core_plugins/index.xml" rel="alternate" type="application/rss+xml" title="home" />
 
     <link rel="stylesheet" href="/wash/custom.css">
 <link href="https://fonts.googleapis.com/css?family=IBM+Plex+Mono:400,500" rel="stylesheet"> 
@@ -33,7 +33,7 @@
 
 
 <header>
-    <h1>Go Plugins</h1>
+    <h1>Core Plugins</h1>
     
 
 <div class="post-meta">
@@ -43,7 +43,7 @@
 <article>
     
 
-<h2 id="how-to-create-a-new-go-plugin">How to create a new Go plugin</h2>
+<h2 id="how-to-create-a-new-core-plugin">How to create a new core plugin</h2>
 
 <p>The <a href="https://github.com/puppetlabs/wash/tree/master/plugin">plugin</a> package defines a set of interfaces that a plugin can implement to enable specific behaviors.</p>
 
@@ -51,28 +51,26 @@
 
 <p>Create a new directory in <a href="https://github.com/puppetlabs/wash/tree/master/plugin">plugin</a> for the plugin.</p>
 
-<p>Create an object that implements the <code>Root</code> profile, as in it has:</p>
+<p>Create an object that implements the <code>Root</code> interface. This would typically be in a <code>root.go</code> file. See <a href="https://github.com/puppetlabs/wash/blob/master/plugin/docker/root.go">docker/root.go</a> for an example.</p>
 
-<ul>
-<li>a <code>Name</code> method to determine the name of the plugin when mounted</li>
-<li>an <code>Init</code> method that loads and validates credentials</li>
-<li>a <code>List</code> method that lists the first-level resources of the plugin</li>
-</ul>
+<p>NOTE: The <code>Init</code> method initializes the Root object&rsquo;s <code>EntryBase</code> configuration and any credentials.</p>
 
 <h3 id="extending-the-plugin">Extending the plugin</h3>
 
-<p>Add new types representing the types of resources the plugin can list.</p>
+<p>Each entry in the plugin&rsquo;s hierarchy should be a new type. This pattern&rsquo;s adopted by the existing core plugins (e.g. <a href="https://github.com/puppetlabs/wash/blob/master/plugin/aws/ec2Instance.go">ec2Instance</a> in AWS; <a href="https://github.com/puppetlabs/wash/blob/master/plugin/docker/container.go">container</a> in Docker). It is meant to make your plugin modular and easier to maintain.</p>
 
 <ul>
-<li>To make them appear as a directory, implement the <code>Group</code> interface.</li>
-<li>Resources with metadata should implement the <code>Resource</code> interface.</li>
-<li>To give files content, implement the <code>Readable</code> interface.</li>
-<li>Log-type files can implement the <code>Pipe</code> interface to expose a stream of new data.</li>
-<li>Files and directories that exist on a remote system or volume should implement the <code>File</code> interface. Additionally the <a href="https://github.com/puppetlabs/wash/tree/master/volume">volume</a> package provides helpers for representing these types.</li>
-<li>To support executing commands on a resource, implement the <code>Execable</code> interface.</li>
+<li>Entries with children (&ldquo;directories&rdquo;) should implement the <code>Parent</code> interface.</li>
+<li>Entries with content should implement <code>Readable</code>.</li>
+<li>Log-type entries should implement <code>Streamable</code> to expose a stream of new data.</li>
+<li>Entries that execute commands should implement the <code>Execable</code> interface.</li>
 </ul>
 
-<p>Tip: base new resources on the <code>EntryBase</code> struct to simplify creating them and enable caching.</p>
+<p>TIP: Use <a href="https://github.com/puppetlabs/wash/blob/master/activity/core.go">activity.Record</a> for all plugin-related logging.</p>
+
+<p>TIP: The <a href="https://github.com/puppetlabs/wash/tree/master/volume">volume</a> package contains useful helpers that can enumerate a given volume&rsquo;s directories and files.</p>
+
+<p>TIP: The <a href="https://github.com/puppetlabs/wash/blob/master/volume/fs.go">volume.FS</a> helper can be used to expose an <code>Execable</code> entry&rsquo;s filesystem. Currently only <code>/var/log</code> is mounted.</p>
 
 </article>
 
diff --git a/docs/docs/core_plugins/index.xml b/docs/docs/core_plugins/index.xml
new file mode 100644
index 0000000..ece5b93
--- /dev/null
+++ b/docs/docs/core_plugins/index.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
+  <channel>
+    <title>Core Plugins on home</title>
+    <link>https://puppetlabs.github.io/wash/docs/core_plugins/</link>
+    <description>Recent content in Core Plugins on home</description>
+    <generator>Hugo -- gohugo.io</generator>
+    <language>en-us</language>
+    <copyright>© 2019 — the wash maintainers</copyright>
+    
+	<atom:link href="https://puppetlabs.github.io/wash/docs/core_plugins/index.xml" rel="self" type="application/rss+xml" />
+    
+    
+  </channel>
+</rss>
\ No newline at end of file
diff --git a/docs/docs/core_plugins/page/1/index.html b/docs/docs/core_plugins/page/1/index.html
new file mode 100644
index 0000000..a351c6c
--- /dev/null
+++ b/docs/docs/core_plugins/page/1/index.html
@@ -0,0 +1,1 @@
+<!DOCTYPE html><html><head><title>https://puppetlabs.github.io/wash/docs/core_plugins/</title><link rel="canonical" href="https://puppetlabs.github.io/wash/docs/core_plugins/"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=https://puppetlabs.github.io/wash/docs/core_plugins/" /></head></html>
\ No newline at end of file
diff --git a/docs/docs/external_plugins/index.html b/docs/docs/external_plugins/index.html
index caf892f..550ea5f 100644
--- a/docs/docs/external_plugins/index.html
+++ b/docs/docs/external_plugins/index.html
@@ -46,60 +46,52 @@
 <p>External plugins let Wash talk to other things outside of the built-in plugins. They can be written in any language. To write an external plugin, you need to do the following:</p>
 
 <ol>
-<li><p>Think about what the plugin would look like if it were modeled as a filesystem. Some useful questions to ask here are:</p>
+<li><p>Write the <a href="#plugin-script">plugin script</a>. This is the script that Wash will shell out to whenever it needs to invoke a method on a specific entry within your plugin.</p></li>
 
-<ul>
-<li>What are the things (entries) that I want this plugin to represent?</li>
-<li>What things are my directories? What things are my files?</li>
-<li>What Wash actions should I support on these things?</li>
-</ul></li>
-
-<li><p>Write the <a href="#plugin-script">plugin script</a>. This is the script that Wash will shell out to whenever it needs to invoke an action on a specific entry within your plugin.</p></li>
-
-<li><p>Add the plugin to the (configurable) <code>plugins.yaml</code> file by specifying a path to the plugin script. The name will be determined by invoking the script with the <code>init</code> action. An example <code>plugins.yaml</code> file is shown below:</p>
+<li><p>Add the plugin to your <code>wash.yaml</code> file under the <code>external-plugins</code> key, and specify the path to the plugin script. The plugin&rsquo;s name will be determined by invoking the script with the <code>init</code> method. An example <code>wash.yaml</code> config is shown below:</p>
 <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
 <table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
 </span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
 </span></pre></td>
 <td style="vertical-align:top;padding:0;margin:0;border:0;">
-<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">- script: &#39;/Users/enis.inan/wash/external-plugins/external-aws.rb&#39;
-- script: &#39;/Users/enis.inan/wash/external-plugins/network.sh&#39;</pre></td></tr></table>
+<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">external-plugins:
+    - script: &#39;/Users/enis.inan/GitHub/wash/website/static/docs/external_plugins/examples/sshfs.sh&#39;</pre></td></tr></table>
 </div>
 </div></li>
 
-<li><p>Start the Wash server to see your plugin in action.</p></li>
+<li><p>Start the Wash shell to see your plugin in action.</p></li>
 </ol>
 
-<p><strong>NOTE:</strong> You can override the default <code>plugins.yaml</code> path via the <code>external-plugins</code> flag. See <code>wash help server</code> for more information.</p>
-
 <h2 id="plugin-script">Plugin Script</h2>
 
-<p>Wash shells out to the external plugin&rsquo;s script whenever it needs to invoke an action on one of its entries. The script must have the following usage:</p>
+<p>Wash shells out to the external plugin&rsquo;s script whenever it needs to invoke a method on one of its entries. The script must have the following usage:</p>
 <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
 <table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
 </span></pre></td>
 <td style="vertical-align:top;padding:0;margin:0;border:0;">
-<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;plugin_script&gt; &lt;action&gt; &lt;path&gt; &lt;state&gt; &lt;args...&gt;</pre></td></tr></table>
+<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;plugin_script&gt; &lt;method&gt; &lt;path&gt; &lt;state&gt; &lt;args...&gt;</pre></td></tr></table>
 </div>
 </div>
 <p>where</p>
 
 <ul>
-<li><code>&lt;action&gt;</code> is the Wash action that&rsquo;s being invoked</li>
+<li><code>&lt;method&gt;</code> is the Wash method that&rsquo;s being invoked</li>
 <li><code>&lt;path&gt;</code> is the entry&rsquo;s filesystem path relative to Wash&rsquo;s mountpoint</li>
 <li><code>&lt;state&gt;</code> consists of the minimum amount of information required to reconstruct the entry inside the plugin</li>
-<li><code>&lt;args...&gt;</code> are the arguments passed to the specific action.</li>
+<li><code>&lt;args...&gt;</code> are the method&rsquo;s arguments.</li>
 </ul>
 
 <p><code>&lt;path&gt;</code> and <code>&lt;state&gt;</code> can be a bit confusing. To understand them, we recommend reading the <a href="#aside">Aside</a>, and to look at the provided <a href="#bash-example">Bash</a> + Ruby external plugin examples to see how they&rsquo;re used. <strong>TODO: Link a Ruby example</strong></p>
 
-<p>The remaining sections describe all possible values of <code>&lt;action&gt;</code> that can be passed-in, including each action&rsquo;s calling and error conventions, and the expected results.</p>
+<p>The remaining sections describe all the possible Wash methods that can be passed-in, including their calling and error conventions, and the expected results.</p>
+
+<p>NOTE: Plugin script invocations run in their own process group (pgrp). Wash will send a SIGTERM signal to the pgrp on a cancelled API/filesystem request. If after five seconds the invocation process has not terminated, then Wash will send a SIGKILL signal.</p>
 
 <h2 id="init">init</h2>
 
-<p>The <code>init</code> action is special. It is invoked as <code>&lt;plugin_script&gt; init</code>, and it is invoked only once, when the external plugin is loaded. When <code>init</code> is invoked, the script must output a JSON object representing the plugin root. Here&rsquo;s an example:</p>
+<p>The <code>init</code> method is special. It is invoked as <code>&lt;plugin_script&gt; init</code>, and it is invoked only once, when the external plugin is loaded. When <code>init</code> is invoked, the script must output a JSON object representing the plugin root. Here&rsquo;s an example:</p>
 <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
 <table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
@@ -112,19 +104,20 @@
 <td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{
   &#34;name&#34;: &#34;aws&#34;,
-  &#34;supported_actions&#34;: [
+  &#34;methods&#34;: [
     &#34;list&#34;
   ]
 }</pre></td></tr></table>
 </div>
 </div>
-<p>This example shows the <em>minimum</em> amount of information required for Wash to construct the plugin root. In fact, the example shows the minimum amount of information required for Wash to construct <em>any</em> external plugin entry. This information consists of the entry&rsquo;s name and its supported actions.</p>
+<p>This example shows the <em>minimum</em> amount of information required for Wash to construct the plugin root. In fact, the example shows the minimum amount of information required for Wash to construct <em>any</em> external plugin entry. This information consists of the entry&rsquo;s name and its implemented methods.</p>
 
-<p>You can include additional keys in the printed JSON object. These keys are:</p>
+<p>You can include additional (optional) keys in the printed JSON object. These keys are:</p>
 
 <ul>
-<li><code>cache_ttls</code>. This specifies how many seconds each supported action&rsquo;s result should be cached (<code>ttl</code> is short for time to live).</li>
-<li><code>attributes</code>. This represents the entry&rsquo;s filesystem attributes, if any. These are the access time <code>atime</code>, last modified time <code>mtime</code>, creation time <code>ctime</code>, mode <code>mode</code>, and size <code>size</code>. The individual time attributes are specified in Unix seconds. Octal modes must be prefixed with the <code>0</code> delimiter (e.g. like <code>0777</code>). Hexadecimal modes must be prefixed with the <code>0x</code> delimiter (e.g. like <code>0xabcd</code>).</li>
+<li><code>cache_ttls</code>. This specifies how many seconds each method&rsquo;s result should be cached (<code>ttl</code> is short for time to live). Currently, Wash caches the result of <code>list</code>, <code>read</code>, and <code>metadata</code>.</li>
+<li><code>attributes</code>. This represents the entry&rsquo;s attributes (see the <a href="/wash/docs#attributes-metadata"><code>Attributes/Metadata</code></a> section). Time attributes are specified in Unix seconds. Octal modes must be prefixed with the <code>0</code> delimiter (e.g. like <code>0777</code>). Hexadecimal modes must be prefixed with the <code>0x</code> delimiter (e.g. like <code>0xabcd</code>).</li>
+<li><code>slash_replacer</code>. This overrides the default slash replacer <code>#</code>.</li>
 <li><code>state</code>. This corresponds to the <code>&lt;state&gt;</code> parameter in the plugin script&rsquo;s usage.</li>
 </ul>
 
@@ -148,34 +141,36 @@
 </span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
 </span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
 </span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
+</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
 </span></pre></td>
 <td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">{
   &#34;name&#34;: &#34;some_entry&#34;,
-  &#34;supported_actions&#34;: [
+  &#34;methods&#34;: [
     &#34;list&#34;
   ],
   &#34;cache_ttls&#34;: {
     &#34;list&#34;: 30
   },
   &#34;attributes&#34;: {
-    &#34;atime&#34;: 1551942012,
     &#34;mtime&#34;: 1551942012,
-    &#34;ctime&#34;: 1551942012,
-    &#34;mode&#34;: 511,
-    &#34;size&#34;: 15600
+    &#34;meta&#34;: {
+      &#34;LastModifiedTime&#34;: 1551942012,
+      &#34;Owner&#34;: &#34;Wash&#34;,
+    }
   },
-  &#34;state&#34;: &#34;{\&#34;klass\&#34;:\&#34;AWS::Profile\&#34;}&#34;
+  &#34;slash_replacer&#34;: &#34;:&#34;,
+  &#34;state&#34;: &#34;{\&#34;klass\&#34;:\&#34;SSHFS::Directory\&#34;}&#34;
 }</pre></td></tr></table>
 </div>
 </div>
-<p>We see from <code>cache_ttls</code> that the result of <code>some_entry</code>&rsquo;s <code>list</code> action will be cached for 30 seconds. We see from <code>attributes</code> that <code>some_entry</code> has some filesystem attributes that are defined (note that the mode is <code>0777</code>; <code>511</code> is its Base-10 representation). Finally, we see from <code>state</code> that <code>some_entry</code> has some state that Wash will pass-back in via the <code>&lt;state&gt;</code> parameter whenever it invokes one of its supported actions. In this case, only <code>list</code> is supported, and <code>&lt;state&gt;</code> is a stringified JSON object containing the entry&rsquo;s class (<code>AWS::Profile</code>) in whatever language the plugin script was written in.</p>
+<p>We see from <code>cache_ttls</code> that the result of <code>some_entry</code>&rsquo;s <code>list</code> method will be cached for 30 seconds. We see from <code>attributes</code> that <code>some_entry</code> has an <code>mtime</code> attribute, and that it also includes the <code>meta</code> attribute. We see from <code>slash_replacer</code> that any <code>/</code>&lsquo;es in the entry&rsquo;s returned name will be replaced by a <code>:</code> instead of a <code>#</code>. Finally, we see from <code>state</code> that <code>some_entry</code> has some state that Wash will pass-back in via the <code>&lt;state&gt;</code> parameter whenever it invokes one of its methods. In this case, only <code>list</code> is implemented, and <code>&lt;state&gt;</code> is a stringified JSON object containing the entry&rsquo;s class (<code>SSHFS::Directory</code>) in whatever language the plugin script was written in.</p>
 
-<p>The <code>init</code> action adopts the standard error conventions described in the <a href="#errors">Errors</a> section.</p>
+<p><code>init</code> adopts the standard error conventions described in the <a href="#errors">Errors</a> section.</p>
 
 <h2 id="list">list</h2>
 
-<p>The <code>list</code> action is invoked as <code>&lt;plugin_script&gt; list &lt;path&gt; &lt;state&gt;</code>. When <code>list</code> is invoked, the script must output an array of JSON objects. Each JSON object has the same schema as the JSON object described in the <code>init</code> section (hence, <code>list</code> outputs an array of entries). Below is an example of valid output from the <code>list</code> action.</p>
+<p><code>list</code> is invoked as <code>&lt;plugin_script&gt; list &lt;path&gt; &lt;state&gt;</code>. When <code>list</code> is invoked, the script must output an array of JSON objects. Each JSON object has the same schema as the JSON object described in the <code>init</code> section (hence, <code>list</code> outputs an array of entries). Below is an example of valid <code>list</code> output:</p>
 <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
 <table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
@@ -206,51 +201,63 @@
 </span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
 </span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
 </span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
+</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
+</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
+</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
+</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
+</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
+</span><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
 </span></pre></td>
 <td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">[
-	{
-		&#34;name&#34;: &#34;LARGE_FILE.txt&#34;,
-		&#34;supported_actions&#34;: [
-			&#34;metadata&#34;,
-			&#34;read&#34;,
-			&#34;stream&#34;
-		],
-		&#34;attributes&#34;: {
-			&#34;Mtime&#34;: 1551459978,
-			&#34;Size&#34;: 100000000
-		},
-		&#34;state&#34;: &#34;{\&#34;name\&#34;:\&#34;LARGE_FILE.txt\&#34;,\&#34;profile\&#34;:\&#34;default\&#34;,\&#34;region\&#34;:\&#34;us-west-1\&#34;,\&#34;bucket\&#34;:\&#34;my-stupid-bucket-enis\&#34;,\&#34;attributes\&#34;:{\&#34;Mtime\&#34;:1551459978,\&#34;Size\&#34;:100000000},\&#34;klass\&#34;:\&#34;S3Object\&#34;}&#34;
-	},
-	{
-		&#34;name&#34;: &#34;my_json_file.json&#34;,
-		&#34;supported_actions&#34;: [
-			&#34;metadata&#34;,
-			&#34;read&#34;,
-			&#34;stream&#34;
-		],
-		&#34;attributes&#34;: {
-			&#34;Mtime&#34;: 1549572670,
-			&#34;Size&#34;: 9336
-		},
-		&#34;state&#34;: &#34;{\&#34;name\&#34;:\&#34;my_json_file.json\&#34;,\&#34;profile\&#34;:\&#34;default\&#34;,\&#34;region\&#34;:\&#34;us-west-1\&#34;,\&#34;bucket\&#34;:\&#34;my-stupid-bucket-enis\&#34;,\&#34;attributes\&#34;:{\&#34;Mtime\&#34;:1549572670,\&#34;Size\&#34;:9336},\&#34;klass\&#34;:\&#34;S3Object\&#34;}&#34;
-	}
+  {
+    &#34;name&#34;: &#34;fooVM&#34;,
+    &#34;methods&#34;: [
+      &#34;list&#34;,
+      &#34;exec&#34;,
+      &#34;metadata&#34;
+    ],
+    &#34;attributes&#34;: {
+      &#34;mtime&#34;: 1558062927,
+      &#34;meta&#34;: {
+        &#34;LastModifiedTime&#34;: 1558062927,
+        &#34;Owner&#34;: &#34;Alice&#34;
+      }
+    },
+    &#34;state&#34;: &#34;{\&#34;klass\&#34;:\&#34;SSHFS::VM\&#34;}&#34;
+  },
+  {
+    &#34;name&#34;: &#34;barVM&#34;,
+    &#34;methods&#34;: [
+      &#34;list&#34;,
+      &#34;exec&#34;,
+      &#34;metadata&#34;
+    ],
+    &#34;attributes&#34;: {
+      &#34;mtime&#34;: 1558062927,
+      &#34;meta&#34;: {
+        &#34;LastModifiedTime&#34;: 1558062927,
+        &#34;Owner&#34;: &#34;Alice&#34;
+      }
+    },
+    &#34;state&#34;: &#34;{\&#34;klass\&#34;:\&#34;SSHFS::VM\&#34;}&#34;
+  }
 ]</pre></td></tr></table>
 </div>
 </div>
-<p><strong>NOTE:</strong> Remember that the state displayed here is the same <code>&lt;state&gt;</code> parameter that will be passed in to other actions invoked on that entry. For example, if the <code>read</code> action is invoked on <code>LARGE_FILE.txt</code> via <code>&lt;plugin_script&gt; read &lt;parent_path&gt;/LARGE_FILE.txt &lt;state&gt;</code>, then the value of <code>LARGE_FILE.txt</code>&rsquo;s <code>state</code> key will be passed-in for <code>&lt;state&gt;</code>.</p>
+<p><strong>NOTE:</strong> Remember that the state displayed here is the same <code>&lt;state&gt;</code> parameter that will be passed in to other methods invoked on that entry. For example, if <code>read</code> is invoked on <code>fooVM</code> via <code>&lt;plugin_script&gt; read &lt;parent_path&gt;/fooVM &lt;state&gt;</code>, then the value of <code>fooVM</code>&rsquo;s <code>state</code> key will be passed-in for <code>&lt;state&gt;</code>.</p>
 
-<p>The <code>list</code> action adopts the standard error convention described in the <a href="#errors">Errors</a> section.</p>
+<p><code>list</code> adopts the standard error convention described in the <a href="#errors">Errors</a> section.</p>
 
 <h2 id="read">read</h2>
 
-<p>The <code>read</code> action is invoked as <code>&lt;plugin_script&gt; read &lt;path&gt; &lt;state&gt;</code>. When <code>read</code> is invoked, the script must output the entry&rsquo;s content.</p>
+<p><code>read</code> is invoked as <code>&lt;plugin_script&gt; read &lt;path&gt; &lt;state&gt;</code>. When <code>read</code> is invoked, the script must output the entry&rsquo;s content.</p>
 
-<p>The <code>read</code> action adopts the standard error convention described in the <a href="#errors">Errors</a> section.</p>
+<p><code>read</code> adopts the standard error convention described in the <a href="#errors">Errors</a> section.</p>
 
 <h2 id="metadata">metadata</h2>
 
-<p>The <code>metadata</code> action is invoked as <code>&lt;plugin_script&gt; metadata &lt;path&gt; &lt;state&gt;</code>. When <code>metadata</code> is invoked, the script must output a JSON object representing the entry&rsquo;s metadata. Below is an example of acceptable <code>metadata</code> output:</p>
+<p><code>metadata</code> is invoked as <code>&lt;plugin_script&gt; metadata &lt;path&gt; &lt;state&gt;</code>. When <code>metadata</code> is invoked, the script must output a JSON object representing the entry&rsquo;s metadata. Below is an example of acceptable <code>metadata</code> output:</p>
 <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
 <table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
@@ -265,25 +272,29 @@
 }</pre></td></tr></table>
 </div>
 </div>
-<p>The <code>metadata</code> action adopts the standard error convention described in the <a href="#errors">Errors</a> section.</p>
+<p><code>metadata</code> adopts the standard error convention described in the <a href="#errors">Errors</a> section.</p>
+
+<p>NOTE: Only implement <code>metadata</code> if there is additional information about your entry that is not provided by the <code>meta</code> attribute.</p>
 
 <h2 id="stream">stream</h2>
 
-<p>The <code>stream</code> action is invoked as <code>&lt;plugin_script&gt; stream &lt;path&gt; &lt;state&gt;</code>. When <code>stream</code> is invoked, the first line of the script&rsquo;s output must contain the <code>200</code> header. This header tells Wash that the entry&rsquo;s data is about to the streamed. After it outputs the header, the script must then stream the entry&rsquo;s data. Wash will continue to poll stdout for any updates until either the streaming process exits, or the user cancels the request. In the latter case, Wash will send the <code>SIGTERM</code> signal to the streaming process.</p>
+<p><code>stream</code> is invoked as <code>&lt;plugin_script&gt; stream &lt;path&gt; &lt;state&gt;</code>. When <code>stream</code> is invoked, the first line of the script&rsquo;s output must contain the <code>200</code> header. This header tells Wash that the entry&rsquo;s data is about to the streamed. After it outputs the header, the script must then stream the entry&rsquo;s data. Wash will continue to poll stdout for any updates until either the streaming process exits, or the user cancels the request.</p>
 
-<p>The <code>stream</code> action adopts the standard error convention described in the <a href="#errors">Errors</a> section.</p>
+<p><code>stream</code> adopts the standard error convention described in the <a href="#errors">Errors</a> section.</p>
 
 <h2 id="exec">exec</h2>
 
-<p>The <code>exec</code> action is invoked as <code>&lt;plugin_script&gt; exec &lt;path&gt; &lt;state&gt; &lt;cmd&gt; &lt;args...&gt;</code>. If the <code>input</code> key is included as part of <code>opts</code> in a request to the <code>exec</code> endpoint, then its content is passed-in as stdin to the plugin script. When <code>exec</code> is invoked, the plugin script&rsquo;s stdout and stderr must be connected to <code>cmd</code>&rsquo;s stdout and stderr, and it must exit the <code>exec</code> invocation with <code>cmd</code>&rsquo;s exit code.</p>
+<p><code>exec</code> is invoked as <code>&lt;plugin_script&gt; exec &lt;path&gt; &lt;state&gt; &lt;opts&gt; &lt;cmd&gt; &lt;args...&gt;</code>. If the <code>input</code> key is included as part of <code>opts</code> in a request to the <code>exec</code> endpoint, then its content is passed-in as stdin to the plugin script. <code>&lt;opts&gt;</code> is the JSON serialization of the remaining options.</p>
+
+<p>When <code>exec</code> is invoked, the plugin script&rsquo;s stdout and stderr must be connected to <code>cmd</code>&rsquo;s stdout and stderr, and it must exit the <code>exec</code> invocation with <code>cmd</code>&rsquo;s exit code.</p>
 
-<p>Because the <code>exec</code> action effectively hijacks <code>&lt;plugin_script&gt; exec</code> with <code>&lt;cmd&gt; &lt;args...&gt;</code>, there is currently no way for external plugins to report any <code>exec</code> errors to Wash. Thus, if <code>&lt;plugin_script&gt; exec</code> fails to exec <code>&lt;cmd&gt; &lt;args...&gt;</code> (e.g. due to a failed API call to trigger the exec), then that error output will be included as part of <code>&lt;cmd&gt; &lt;args...&gt;</code>&rsquo;s output when running <code>wash exec</code>.</p>
+<p>Because <code>exec</code> effectively hijacks <code>&lt;plugin_script&gt; exec</code> with <code>&lt;cmd&gt; &lt;args...&gt;</code>, there is currently no way for external plugins to report any <code>exec</code> errors to Wash. Thus, if <code>&lt;plugin_script&gt; exec</code> fails to exec <code>&lt;cmd&gt; &lt;args...&gt;</code> (e.g. due to a failed API call to trigger the exec), then that error output will be included as part of <code>&lt;cmd&gt; &lt;args...&gt;</code>&rsquo;s output when running <code>wash exec</code>.</p>
 
 <h2 id="errors-a-name-errors-a">Errors <a name="errors"></a></h2>
 
-<p>All errors are printed to <code>stderr</code>. An action invocation is said to have errored when the plugin script returns a non-zero exit code. In that case, Wash wraps all of <code>stderr</code> into an error object, then documents that error in the process&rsquo; activity and the server logs.</p>
+<p>All errors are printed to <code>stderr</code>. A method invocation is said to have errored when the plugin script returns a non-zero exit code. In that case, Wash wraps all of <code>stderr</code> into an error object, then documents that error in the process&rsquo; activity and the server logs.</p>
 
-<p><strong>NOTE:</strong> Not all actions adopt this error handling convention (e.g. <code>exec</code>). The error handling for these &ldquo;snowflake&rdquo; actions is described in their respective sections.</p>
+<p><strong>NOTE:</strong> Not all method invocations adopt this error handling convention (e.g. <code>exec</code>). The error handling for these &ldquo;snowflake&rdquo; methods is described in their respective sections.</p>
 
 <h2 id="aside-optional-a-name-aside-a">Aside (optional)<a name="aside"></a></h2>
 
@@ -293,7 +304,7 @@
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
 </span></pre></td>
 <td style="vertical-align:top;padding:0;margin:0;border:0;">
-<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;plugin_script&gt; &lt;action&gt; &lt;path&gt; &lt;state&gt; &lt;args...&gt;</pre></td></tr></table>
+<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;plugin_script&gt; &lt;method&gt; &lt;path&gt; &lt;state&gt; &lt;args...&gt;</pre></td></tr></table>
 </div>
 </div>
 <p>If we let <code>&lt;entry&gt; = &lt;path&gt; &lt;state&gt;</code>, then our usage becomes:</p>
@@ -302,23 +313,23 @@
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
 </span></pre></td>
 <td style="vertical-align:top;padding:0;margin:0;border:0;">
-<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;plugin_script&gt; &lt;action&gt; &lt;entry&gt; &lt;args...&gt;</pre></td></tr></table>
+<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;plugin_script&gt; &lt;method&gt; &lt;entry&gt; &lt;args...&gt;</pre></td></tr></table>
 </div>
 </div>
-<p>If we ignore <code>&lt;plugin_script&gt;</code> then the above turns into <code>&lt;action&gt; &lt;entry&gt; &lt;args...&gt;</code>. When read out loud, this looks like the function call <code>&lt;action&gt;(&lt;entry&gt;, &lt;args...&gt;)</code>. If we imagine <code>&lt;entry&gt;</code> as an object in an OOP language, this is semantically equivalent to:</p>
+<p>If we ignore <code>&lt;plugin_script&gt;</code> then the above turns into <code>&lt;method&gt; &lt;entry&gt; &lt;args...&gt;</code>. When read out loud, this looks like the function call <code>&lt;method&gt;(&lt;entry&gt;, &lt;args...&gt;)</code>. If we imagine <code>&lt;entry&gt;</code> as an object in an OOP language, this is semantically equivalent to:</p>
 <div class="highlight"><div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">
 <table style="border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
 <pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><span style="margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
 </span></pre></td>
 <td style="vertical-align:top;padding:0;margin:0;border:0;">
-<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;entry&gt;.&lt;action&gt;(&lt;args...&gt;)</pre></td></tr></table>
+<pre style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;entry&gt;.&lt;method&gt;(&lt;args...&gt;)</pre></td></tr></table>
 </div>
 </div>
-<p>For example, if <code>&lt;entry&gt; = myS3Bucket</code>, <code>&lt;action&gt; = list</code>, and <code>&lt;args...&gt;</code> is empty, then we can read the above as <code>myS3Bucket.list()</code>. Thus, the plugin script&rsquo;s usage can be thought of as invoking an action (<code>&lt;action&gt;</code>) on the specified entry (<code>&lt;path&gt; &lt;state&gt;</code>) with the given arguments (<code>&lt;args...&gt;</code>).</p>
+<p>For example, if <code>&lt;entry&gt; = myS3Bucket</code>, <code>&lt;method&gt; = list</code>, and <code>&lt;args...&gt;</code> is empty, then we can read the above as <code>myS3Bucket.list()</code>. Thus, the plugin script&rsquo;s usage can be thought of as invoking a method (<code>&lt;method&gt;</code>) on the specified entry (<code>&lt;path&gt; &lt;state&gt;</code>) with the given arguments (<code>&lt;args...&gt;</code>).</p>
 
-<p>You might be wondering why we don&rsquo;t just lump <code>&lt;path&gt;</code> and <code>&lt;state&gt;</code> together into <code>&lt;entry&gt;</code> so that the plugin script&rsquo;s usage becomes <code>&lt;plugin_script&gt; &lt;action&gt; &lt;entry&gt; &lt;args...&gt;</code>. There&rsquo;s several reasons. One, having the <code>&lt;path&gt;</code> is helpful for debugging purposes. You can directly see the acting entry in the logs, which frees you from having to figure that information out yourself. Two, it mirrors the API&rsquo;s structure of <code>/fs/&lt;action&gt;/&lt;path&gt;</code>. And three, sometimes the <code>&lt;path&gt;</code> is all you need to write your plugin script. While you could always print the <code>&lt;path&gt;</code> yourself and make that the <code>&lt;state&gt;</code> parameter for Wash to pass around, it can be annoying to have to constantly do that, especially when you&rsquo;re writing simple plugins. Thus, <code>&lt;path&gt;</code> is really more of a convenience. You should use <code>&lt;path&gt;</code> if that&rsquo;s what you need to write your plugin. Otherwise, if you&rsquo;re writing a more complicated plugin that needs to maintain some state (e.g. like the entry&rsquo;s class name and its constructor arguments), then use <code>&lt;state&gt;</code>. However, try to avoid using <code>&lt;path&gt;</code> and <code>&lt;state&gt;</code> together in the same plugin script, as doing so can make it hard to reason about your code. Use either <code>&lt;path&gt;</code> or <code>&lt;state&gt;</code>, but not both.</p>
+<p>You might be wondering why we don&rsquo;t just lump <code>&lt;path&gt;</code> and <code>&lt;state&gt;</code> together into <code>&lt;entry&gt;</code> so that the plugin script&rsquo;s usage becomes <code>&lt;plugin_script&gt; &lt;method&gt; &lt;entry&gt; &lt;args...&gt;</code>. There&rsquo;s several reasons. One, having the <code>&lt;path&gt;</code> is helpful for debugging purposes. You can directly see the acting entry in the logs, which frees you from having to figure that information out yourself. Two, it mirrors the API&rsquo;s structure of <code>/fs/&lt;method&gt;/&lt;path&gt;</code>. And three, sometimes the <code>&lt;path&gt;</code> is all you need to write your plugin script. While you could always print the <code>&lt;path&gt;</code> yourself and make that the <code>&lt;state&gt;</code> parameter for Wash to pass around, it can be annoying to have to constantly do that, especially when you&rsquo;re writing simple plugins. Thus, <code>&lt;path&gt;</code> is really more of a convenience. You should use <code>&lt;path&gt;</code> if that&rsquo;s what you need to write your plugin. Otherwise, if you&rsquo;re writing a more complicated plugin that needs to maintain some state (e.g. like the entry&rsquo;s class name and its constructor arguments), then use <code>&lt;state&gt;</code>. However, try to avoid using <code>&lt;path&gt;</code> and <code>&lt;state&gt;</code> together in the same plugin script, as doing so can make it hard to reason about your code. Use either <code>&lt;path&gt;</code> or <code>&lt;state&gt;</code>, but not both.</p>
 
-<p><strong>NOTE:</strong> The <code>init</code> action is special. Its usage is <code>&lt;plugin_script&gt; init</code> &ndash; there is no <code>&lt;path&gt;</code> or <code>&lt;state</code>&gt; so there is no <code>&lt;entry&gt;</code>. Thus, the OOP call of <code>&lt;entry&gt;.&lt;action&gt;(&lt;args...&gt;)</code> doesn&rsquo;t make sense for <code>init</code>. So how do you reason about it? Why do we have an <code>init</code> action? Since every Wash plugin is modeled as a filesystem, it must have a root. Once we know the root, then it is easy to get to a specific entry by repeatedly invoking the <code>list</code> action. The <code>init</code> action is how you describe that &lsquo;root&rsquo;.</p>
+<p><strong>NOTE:</strong> The <code>init</code> method is special. Its usage is <code>&lt;plugin_script&gt; init</code> &ndash; there is no <code>&lt;path&gt;</code> or <code>&lt;state</code>&gt; so there is no <code>&lt;entry&gt;</code>. Thus, the OOP call of <code>&lt;entry&gt;.&lt;method&gt;(&lt;args...&gt;)</code> doesn&rsquo;t make sense for <code>init</code>. So how do you reason about it? Why do we have an <code>init</code> method? Since every Wash plugin is modeled as a filesystem, it must have a root. Once we know the root, then it is easy to get to a specific entry by repeatedly invoking the <code>list</code> method. The <code>init</code> method is how you describe that &lsquo;root&rsquo;.</p>
 
 <h2 id="bash-example">Bash Example</h2>
 
diff --git a/docs/docs/go_plugins/index.xml b/docs/docs/go_plugins/index.xml
deleted file mode 100644
index 3eebeb7..0000000
--- a/docs/docs/go_plugins/index.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
-<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
-  <channel>
-    <title>Go Plugins on home</title>
-    <link>https://puppetlabs.github.io/wash/docs/go_plugins/</link>
-    <description>Recent content in Go Plugins on home</description>
-    <generator>Hugo -- gohugo.io</generator>
-    <language>en-us</language>
-    <copyright>© 2019 — the wash maintainers</copyright>
-    
-	<atom:link href="https://puppetlabs.github.io/wash/docs/go_plugins/index.xml" rel="self" type="application/rss+xml" />
-    
-    
-  </channel>
-</rss>
\ No newline at end of file
diff --git a/docs/docs/go_plugins/page/1/index.html b/docs/docs/go_plugins/page/1/index.html
deleted file mode 100644
index b531ca4..0000000
--- a/docs/docs/go_plugins/page/1/index.html
+++ /dev/null
@@ -1,1 +0,0 @@
-<!DOCTYPE html><html><head><title>https://puppetlabs.github.io/wash/docs/go_plugins/</title><link rel="canonical" href="https://puppetlabs.github.io/wash/docs/go_plugins/"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=https://puppetlabs.github.io/wash/docs/go_plugins/" /></head></html>
\ No newline at end of file
diff --git a/docs/docs/index.html b/docs/docs/index.html
index 1fb9bd8..8f0fc47 100644
--- a/docs/docs/index.html
+++ b/docs/docs/index.html
@@ -70,8 +70,9 @@
 <li><a href="#plugin-concepts">Plugin Concepts</a>
 
 <ul>
+<li><a href="#attributes-metadata">Attributes/Metadata</a></li>
 <li><a href="/wash/docs/external_plugins">➠External plugins</a></li>
-<li><a href="/wash/docs/go_plugins">➠Go Plugins</a></li>
+<li><a href="/wash/docs/core_plugins">➠Core Plugins</a></li>
 <li><a href="/wash/docs/api">➠Server API</a></li>
 </ul></li>
 </ul>
@@ -181,43 +182,59 @@ to display running processes on all listed nodes. Errors on paths that don&rsquo
 
 <h2 id="plugin-concepts">Plugin Concepts</h2>
 
-<p>Wash&rsquo;s plugin system is designed around a set of primitives that resources can implement. A plugin requires a root that can list things it contains, and implements a tree structure under that where each node represents a resource or an arbitrary grouping. Wash will translate that tree structure into a file hierarchy.</p>
+<p>Everything is an entry in Wash. This includes resources like containers and volumes; organizational groups like the containers directory in the Docker plugin; read-only files like the metadata.json files for EC2 instances; and even non-infrastructure related things like Goodreads books, cooking recipes, breweries, Fandango theaters and movies, etc. (Yes, you can write a Wash plugin for Fandango. In fact, you can write a Wash plugin for anything that you can model as a filesystem.)</p>
 
-<p>Wash supports the following primitives:</p>
+<p>Plugins have their own file hierarchy that&rsquo;s described by a tree structure, where the entries are the nodes. Additionally, internal nodes (i.e. entries with children) are classified as &ldquo;parents.&rdquo; Parents represent the &ldquo;directories&rdquo; of the plugin&rsquo;s filesystem, while everything else is a &ldquo;file.&rdquo;</p>
+
+<p>Plugins are written in top-down fashion, starting with the root. All entries (and their paths) are referenced by their canonical name (<code>cname</code>). The <code>cname</code> consists of the entry&rsquo;s reported name with all <code>/</code>&lsquo;es replaced by <code>#</code>. You can override this with your own slash replacer.</p>
+
+<p>Wash entries can support the following actions:</p>
 
 <ul>
-<li><code>list</code> - lets you ask any resource what&rsquo;s contained inside of it, and what primitives it supports.
+<li><code>list</code> - lists an entry&rsquo;s children, including their supported actions
 
 <ul>
 <li><em>e.g. listing a Kubernetes pod returns its constituent containers</em></li>
 </ul></li>
-<li><code>read</code> - lets you read the contents of a given resource
+<li><code>read</code> - lets you read the entry&rsquo;s content
 
 <ul>
 <li><em>e.g. represent an EC2 instance&rsquo;s console output as a regular file you can open in a regular editor</em></li>
 </ul></li>
-<li><code>stream</code> - gives you streaming-read access to a resource
+<li><code>stream</code> - gives you streaming-read access to an entry
 
 <ul>
 <li><em>e.g. to let you follow a container&rsquo;s output as its running</em></li>
 </ul></li>
-<li><code>exec</code> - lets you execute a command against a resource
+<li><code>exec</code> - lets you execute a command against an entry
 
 <ul>
 <li><em>e.g. run a shell command inside a container, or on an EC2 vm, or on a routerOS device, etc.</em></li>
 </ul></li>
 </ul>
 
-<p>Primitives can be accessed programmatically via the Wash API, or on the CLI via <code>wash</code> subcommands and filesystem interactions.</p>
+<p>Actions can be invoked programmatically via the Wash API, or on the CLI via <code>wash</code> subcommands and filesystem interactions.</p>
 
 <p>For more on implementing plugins, see:</p>
 
 <ul>
 <li><a href="/wash/docs/external_plugins">➠External plugins</a></li>
-<li><a href="/wash/docs/go_plugins">➠Go Plugins</a></li>
+<li><a href="/wash/docs/core_plugins">➠Core Plugins</a></li>
 <li><a href="/wash/docs/api">➠Server API</a></li>
 </ul>
 
+<p>NOTE: We recommend that you read the <code>Attributes/Metadata</code> section before reading the plugin tutorials to take full advantage of Wash&rsquo;s capabilities, especially that of <code>wash find</code>&rsquo;s.</p>
+
+<h3 id="attributes-metadata">Attributes/Metadata</h3>
+
+<p>All entries have metadata, which is a JSON object containing a complete description of the entry. For example, a Docker container&rsquo;s metadata includes its labels, its state, its start time, the image it was built from, its mounted volumes, etc. <a href="#wash-find"><code>wash find</code></a> can filter on this metadata. In our example, you can use <code>find docker/containers -daystart -fullmeta -m .state .startedAt -{1d} -a .status running</code> to see a list of all running containers that started today (try it out!). Thus, metadata filtering is powerful. However, it also requires the user to query an entry&rsquo;s metadata to construct the filter. This can get annoying when a user has to filter on the same property shared by many different kinds of entries. For example, metadata filtering gets annoying when you are trying to filter on an EC2 instance&rsquo;s/Docker container&rsquo;s/Kubernetes pod&rsquo;s state due to the structural differences in their metadata (e.g. an EC2 instance&rsquo;s state is contained in the <code>.state.name</code> key, while a Kubernetes pod&rsquo;s state is contained in the <code>.status.phase</code> key). Metadata filtering is also slow. It requires O(N) API requests, where N is the number of visited entries.</p>
+
+<p>To make <code>wash find</code>&rsquo;s filtering less annoying and faster, entries can also have attributes. The attributes represent common metadata properties that people filter on. Currently, these are the traditional <code>ctime</code>, <code>mtime</code>, <code>atime</code>, <code>size</code>, and <code>mode</code> filesystem attributes, along with a special <code>meta</code> attribute representing a subset of the entry&rsquo;s metadata (useful for fast metadata filtering). The attributes are fetched in bulk when the entry&rsquo;s parent is listed. Typically, the bulk fetch is done through an API&rsquo;s <code>list</code> endpoint. This endpoint returns an array of JSON objects representing the entries. The <code>meta</code> attribute is set to this JSON object while the remaining attributes are parsed from the object&rsquo;s fields. For example, <code>list docker/containers</code> will fetch all of your containers by querying Docker&rsquo;s <code>/containers/json</code> endpoint. That endpoint&rsquo;s response is then used to create the container entry objects, where each container entry&rsquo;s <code>meta</code> attribute is set to a <code>/containers/json</code> object and the containers&rsquo; <code>ctime</code>/<code>mtime</code> attributes are parsed from it.</p>
+
+<p>NOTE: <em>All</em> attributes are optional, so set the ones that you think make sense. For example, if the <code>mode</code> or <code>size</code> attributes don&rsquo;t make sense for your entry, then feel free to ignore them. However, we recommend that you try to set the <code>meta</code> attribute when you can to take advantage of metadata filtering.</p>
+
+<p>NOTE: We plan on adding more attributes depending on user feedback (e.g. like <code>state</code> and <code>labels</code>). Thus if you find yourself metadata-filtering on a common property across a bunch of different entries, then please feel free to file an issue (or PR!) so we can consider adding that property as an attribute (and as a corresponding <code>wash find</code> primary).</p>
+
 </article>
 
 
diff --git a/docs/index.html b/docs/index.html
index 68d9eae..c6fea72 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html>
 <head>
-	<meta name="generator" content="Hugo 0.55.6" />
+	<meta name="generator" content="Hugo 0.55.5" />
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
 
diff --git a/docs/sitemap.xml b/docs/sitemap.xml
index 3bd7839..846f406 100644
--- a/docs/sitemap.xml
+++ b/docs/sitemap.xml
@@ -13,11 +13,11 @@
   </url>
   
   <url>
-    <loc>https://puppetlabs.github.io/wash/docs/external_plugins/</loc>
+    <loc>https://puppetlabs.github.io/wash/docs/core_plugins/</loc>
   </url>
   
   <url>
-    <loc>https://puppetlabs.github.io/wash/docs/go_plugins/</loc>
+    <loc>https://puppetlabs.github.io/wash/docs/external_plugins/</loc>
   </url>
   
   <url>
diff --git a/plugin/cache.go b/plugin/cache.go
index 65a2c23..b0908f4 100644
--- a/plugin/cache.go
+++ b/plugin/cache.go
@@ -92,8 +92,8 @@ type opFunc func() (interface{}, error)
 // CachedOp caches the given op's result for the duration specified by the
 // ttl. You should use it when you need more fine-grained caching than what
 // the existing CachedList, CachedOpen, and CachedMetadata methods provide.
-// For example, CachedOp could be useful to cache an API request whose response
-// lets you implement Attr() and Metadata() for the given entry.
+// For example, CachedOp could be useful to cache an API request whose
+// response lets you implement Open() and Metadata() for the given entry.
 //
 // CachedOp uses the supplied context to determine which activity to log to.
 func CachedOp(ctx context.Context, opName string, entry Entry, ttl time.Duration, op opFunc) (interface{}, error) {
@@ -119,9 +119,9 @@ func CachedOp(ctx context.Context, opName string, entry Entry, ttl time.Duration
 type DuplicateCNameErr struct {
 	ParentID                        string
 	FirstChildName                  string
-	FirstChildSlashReplacementChar  rune
+	FirstChildSlashReplacer         rune
 	SecondChildName                 string
-	SecondChildSlashReplacementChar rune
+	SecondChildSlashReplacer        rune
 	CName                           string
 }
 
@@ -133,7 +133,7 @@ func (c DuplicateCNameErr) Error() string {
 	)[0]
 
 	return fmt.Sprintf(
-		"error listing %v: children %v and %v have the same cname of %v. This means that either the %v plugin's API returns duplicate names, or that you need to use a different slash replacement character (see EntryBase#SetSlashReplacementCharar)",
+		"error listing %v: children %v and %v have the same cname of %v. This means that either the %v plugin's API returns duplicate names, or that you need to use a different slash replacer (see EntryBase#SetSlashReplacer)",
 		c.ParentID,
 		c.FirstChildName,
 		c.SecondChildName,
@@ -165,9 +165,9 @@ func CachedList(ctx context.Context, p Parent) (map[string]Entry, error) {
 				return nil, DuplicateCNameErr{
 					ParentID:                        p.id(),
 					FirstChildName:                  duplicateEntry.name(),
-					FirstChildSlashReplacementChar:  duplicateEntry.slashReplacementChar(),
+					FirstChildSlashReplacer:         duplicateEntry.slashReplacer(),
 					SecondChildName:                 entry.name(),
-					SecondChildSlashReplacementChar: entry.slashReplacementChar(),
+					SecondChildSlashReplacer:        entry.slashReplacer(),
 					CName:                           cname,
 				}
 			}
diff --git a/plugin/entryBase.go b/plugin/entryBase.go
index 0b6337b..7097479 100644
--- a/plugin/entryBase.go
+++ b/plugin/entryBase.go
@@ -37,7 +37,7 @@ to do something like
 type EntryBase struct {
 	entryName          string
 	attr               EntryAttributes
-	slashReplacementCh rune
+	slashReplacerCh    rune
 	// washID represents the entry's wash ID. It is set in CachedList.
 	washID string
 	ttl    [3]time.Duration
@@ -51,7 +51,7 @@ func NewEntry(name string) EntryBase {
 
 	e := EntryBase{
 		entryName:          name,
-		slashReplacementCh: '#',
+		slashReplacerCh: '#',
 	}
 	for op := range e.ttl {
 		e.SetTTLOf(defaultOpCode(op), 15*time.Second)
@@ -79,8 +79,8 @@ func (e *EntryBase) attributes() EntryAttributes {
 	return e.attr
 }
 
-func (e *EntryBase) slashReplacementChar() rune {
-	return e.slashReplacementCh
+func (e *EntryBase) slashReplacer() rune {
+	return e.slashReplacerCh
 }
 
 func (e *EntryBase) id() string {
@@ -120,17 +120,16 @@ func (e *EntryBase) SetAttributes(attr EntryAttributes) *EntryBase {
 }
 
 /*
-SetSlashReplacementChar overrides the default '/' replacement
-character of '#' to char. The '/' replacement character is used
-when determining the entry's cname. See plugin.CName for more
-details.
+SetSlashReplacer overrides the default '/' replacer '#' to char.
+The '/' replacer is used when determining the entry's cname. See
+plugin.CName for more details.
 */
-func (e *EntryBase) SetSlashReplacementChar(char rune) *EntryBase {
+func (e *EntryBase) SetSlashReplacer(char rune) *EntryBase {
 	if char == '/' {
-		panic("e.SetSlashReplacementChar called with '/'")
+		panic("e.SetSlashReplacer called with '/'")
 	}
 
-	e.slashReplacementCh = char
+	e.slashReplacerCh = char
 	return e
 }
 
diff --git a/plugin/externalPluginEntry.go b/plugin/externalPluginEntry.go
index 261427d..c1b020a 100644
--- a/plugin/externalPluginEntry.go
+++ b/plugin/externalPluginEntry.go
@@ -16,7 +16,7 @@ import (
 
 type decodedCacheTTLs struct {
 	List     time.Duration `json:"list"`
-	Open     time.Duration `json:"open"`
+	Read     time.Duration `json:"read"`
 	Metadata time.Duration `json:"metadata"`
 }
 
@@ -24,7 +24,7 @@ type decodedCacheTTLs struct {
 type decodedExternalPluginEntry struct {
 	Name                 string           `json:"name"`
 	Methods             []string          `json:"methods"`
-	SlashReplacementChar string           `json:"slash_replacement_char"`
+	SlashReplacer        string           `json:"slash_replacer"`
 	CacheTTLs            decodedCacheTTLs `json:"cache_ttls"`
 	Attributes           EntryAttributes  `json:"attributes"`
 	State                string           `json:"state"`
@@ -44,13 +44,13 @@ func (e decodedExternalPluginEntry) toExternalPluginEntry() (*externalPluginEntr
 	}
 	entry.SetAttributes(e.Attributes)
 	entry.setCacheTTLs(e.CacheTTLs)
-	if e.SlashReplacementChar != "" {
-		if len([]rune(e.SlashReplacementChar)) > 1 {
-			msg := fmt.Sprintf("e.SlashReplacementChar: received string %v instead of a character", e.SlashReplacementChar)
+	if e.SlashReplacer != "" {
+		if len([]rune(e.SlashReplacer)) > 1 {
+			msg := fmt.Sprintf("e.SlashReplacer: received string %v instead of a character", e.SlashReplacer)
 			panic(msg)
 		}
 
-		entry.SetSlashReplacementChar([]rune(e.SlashReplacementChar)[0])
+		entry.SetSlashReplacer([]rune(e.SlashReplacer)[0])
 	}
 	return entry, nil
 }
@@ -67,8 +67,8 @@ func (e *externalPluginEntry) setCacheTTLs(ttls decodedCacheTTLs) {
 	if ttls.List != 0 {
 		e.SetTTLOf(ListOp, ttls.List*time.Second)
 	}
-	if ttls.Open != 0 {
-		e.SetTTLOf(OpenOp, ttls.Open*time.Second)
+	if ttls.Read != 0 {
+		e.SetTTLOf(OpenOp, ttls.Read*time.Second)
 	}
 	if ttls.Metadata != 0 {
 		e.SetTTLOf(MetadataOp, ttls.Metadata*time.Second)
diff --git a/plugin/helpers.go b/plugin/helpers.go
index 51b75a4..1c41fe7 100644
--- a/plugin/helpers.go
+++ b/plugin/helpers.go
@@ -38,8 +38,8 @@ NOTE: The '#' character was chosen because it is unlikely to appear in
 a meaningful entry's name. If, however, there's a good chance that an
 entry's name can contain the '#' character, and that two entries can
 have the same cname (e.g. 'foo/bar', 'foo#bar'), then you can use
-e.SetSlashReplacementChar(<char>) to change the default slash replacement
-character from a '#' to <char>.
+e.SetSlashReplacer(<char>) to change the default slash replacer from
+a '#' to <char>.
 */
 func CName(e Entry) string {
 	// We make the CName a separate function instead of embedding it
@@ -48,7 +48,7 @@ func CName(e Entry) string {
 	return strings.Replace(
 		e.name(),
 		"/",
-		string(e.slashReplacementChar()),
+		string(e.slashReplacer()),
 		-1,
 	)
 }
diff --git a/plugin/types.go b/plugin/types.go
index 03016d6..bf654b5 100644
--- a/plugin/types.go
+++ b/plugin/types.go
@@ -42,7 +42,7 @@ type Entry interface {
 	Metadata(ctx context.Context) (JSONObject, error)
 	name() string
 	attributes() EntryAttributes
-	slashReplacementChar() rune
+	slashReplacer() rune
 	id() string
 	setID(id string)
 	getTTLOf(op defaultOpCode) time.Duration
diff --git a/website/content/docs/_index.md b/website/content/docs/_index.md
index d28c42b..4bd5b50 100644
--- a/website/content/docs/_index.md
+++ b/website/content/docs/_index.md
@@ -20,8 +20,9 @@ title= "Wash Documentation"
   * [Docker](#docker)
   * [Kubernetes](#kubernetes)
 * [Plugin Concepts](#plugin-concepts)
+  * [Attributes/Metadata](#attributes-metadata)
   * [➠External plugins]
-  * [➠Go Plugins]
+  * [➠Core Plugins]
   * [➠Server API]
 
 ## Wash Commands
@@ -121,27 +122,43 @@ NOTE: Do not override `socket` in a config file. Instead, override it via the `W
 
 ## Plugin Concepts
 
-Wash's plugin system is designed around a set of primitives that resources can implement. A plugin requires a root that can list things it contains, and implements a tree structure under that where each node represents a resource or an arbitrary grouping. Wash will translate that tree structure into a file hierarchy.
+Everything is an entry in Wash. This includes resources like containers and volumes; organizational groups like the containers directory in the Docker plugin; read-only files like the metadata.json files for EC2 instances; and even non-infrastructure related things like Goodreads books, cooking recipes, breweries, Fandango theaters and movies, etc. (Yes, you can write a Wash plugin for Fandango. In fact, you can write a Wash plugin for anything that you can model as a filesystem.)
 
-Wash supports the following primitives:
+Plugins have their own file hierarchy that's described by a tree structure, where the entries are the nodes. Additionally, internal nodes (i.e. entries with children) are classified as "parents." Parents represent the "directories" of the plugin's filesystem, while everything else is a "file."
 
-* `list` - lets you ask any resource what's contained inside of it, and what primitives it supports.
+Plugins are written in top-down fashion, starting with the root. All entries (and their paths) are referenced by their canonical name (`cname`). The `cname` consists of the entry's reported name with all `/`'es replaced by `#`. You can override this with your own slash replacer.
+
+Wash entries can support the following actions:
+
+* `list` - lists an entry's children, including their supported actions
   - _e.g. listing a Kubernetes pod returns its constituent containers_
-* `read` - lets you read the contents of a given resource
+* `read` - lets you read the entry's content
   - _e.g. represent an EC2 instance's console output as a regular file you can open in a regular editor_
-* `stream` - gives you streaming-read access to a resource
+* `stream` - gives you streaming-read access to an entry
   - _e.g. to let you follow a container's output as its running_
-* `exec` - lets you execute a command against a resource
+* `exec` - lets you execute a command against an entry
   - _e.g. run a shell command inside a container, or on an EC2 vm, or on a routerOS device, etc._
 
-Primitives can be accessed programmatically via the Wash API, or on the CLI via `wash` subcommands and filesystem interactions.
+Actions can be invoked programmatically via the Wash API, or on the CLI via `wash` subcommands and filesystem interactions.
 
 For more on implementing plugins, see:
 
 * [➠External plugins]
-* [➠Go Plugins]
+* [➠Core Plugins]
 * [➠Server API]
 
 [➠External plugins]: /wash/docs/external_plugins
-[➠Go plugins]: /wash/docs/go_plugins
-[➠Server API]: /wash/docs/api
\ No newline at end of file
+[➠Core plugins]: /wash/docs/core_plugins
+[➠Server API]: /wash/docs/api
+
+NOTE: We recommend that you read the `Attributes/Metadata` section before reading the plugin tutorials to take full advantage of Wash's capabilities, especially that of `wash find`'s.
+
+### Attributes/Metadata
+
+All entries have metadata, which is a JSON object containing a complete description of the entry. For example, a Docker container's metadata includes its labels, its state, its start time, the image it was built from, its mounted volumes, etc. [`wash find`](#wash-find) can filter on this metadata. In our example, you can use `find docker/containers -daystart -fullmeta -m .state .startedAt -{1d} -a .status running` to see a list of all running containers that started today (try it out!). Thus, metadata filtering is powerful. However, it also requires the user to query an entry's metadata to construct the filter. This can get annoying when a user has to filter on the same property shared by many different kinds of entries. For example, metadata filtering gets annoying when you are trying to filter on an EC2 instance's/Docker container's/Kubernetes pod's state due to the structural differences in their metadata (e.g. an EC2 instance's state is contained in the `.state.name` key, while a Kubernetes pod's state is contained in the `.status.phase` key). Metadata filtering is also slow. It requires O(N) API requests, where N is the number of visited entries.
+
+To make `wash find`'s filtering less annoying and faster, entries can also have attributes. The attributes represent common metadata properties that people filter on. Currently, these are the traditional `ctime`, `mtime`, `atime`, `size`, and `mode` filesystem attributes, along with a special `meta` attribute representing a subset of the entry's metadata (useful for fast metadata filtering). The attributes are fetched in bulk when the entry's parent is listed. Typically, the bulk fetch is done through an API's `list` endpoint. This endpoint returns an array of JSON objects representing the entries. The `meta` attribute is set to this JSON object while the remaining attributes are parsed from the object's fields. For example, `list docker/containers` will fetch all of your containers by querying Docker's `/containers/json` endpoint. That endpoint's response is then used to create the container entry objects, where each container entry's `meta` attribute is set to a `/containers/json` object and the containers' `ctime`/`mtime` attributes are parsed from it.
+
+NOTE: _All_ attributes are optional, so set the ones that you think make sense. For example, if the `mode` or `size` attributes don't make sense for your entry, then feel free to ignore them. However, we recommend that you try to set the `meta` attribute when you can to take advantage of metadata filtering.
+
+NOTE: We plan on adding more attributes depending on user feedback (e.g. like `state` and `labels`). Thus if you find yourself metadata-filtering on a common property across a bunch of different entries, then please feel free to file an issue (or PR!) so we can consider adding that property as an attribute (and as a corresponding `wash find` primary).
\ No newline at end of file
diff --git a/website/content/docs/core_plugins/_index.md b/website/content/docs/core_plugins/_index.md
new file mode 100644
index 0000000..99d4838
--- /dev/null
+++ b/website/content/docs/core_plugins/_index.md
@@ -0,0 +1,33 @@
++++
+title= "Core Plugins"
++++
+
+## How to create a new core plugin
+
+The [plugin] package defines a set of interfaces that a plugin can implement to enable specific behaviors.
+
+### Starting a plugin
+
+Create a new directory in [plugin] for the plugin.
+
+Create an object that implements the `Root` interface. This would typically be in a `root.go` file. See [docker/root.go](https://github.com/puppetlabs/wash/blob/master/plugin/docker/root.go) for an example.
+
+NOTE: The `Init` method initializes the Root object's `EntryBase` configuration and any credentials.
+
+### Extending the plugin
+
+Each entry in the plugin's hierarchy should be a new type. This pattern's adopted by the existing core plugins (e.g. [ec2Instance](https://github.com/puppetlabs/wash/blob/master/plugin/aws/ec2Instance.go) in AWS; [container](https://github.com/puppetlabs/wash/blob/master/plugin/docker/container.go) in Docker). It is meant to make your plugin modular and easier to maintain.
+
+- Entries with children ("directories") should implement the `Parent` interface.
+- Entries with content should implement `Readable`.
+- Log-type entries should implement `Streamable` to expose a stream of new data.
+- Entries that execute commands should implement the `Execable` interface.
+
+TIP: Use [activity.Record](https://github.com/puppetlabs/wash/blob/master/activity/core.go) for all plugin-related logging.
+
+TIP: The [volume] package contains useful helpers that can enumerate a given volume's directories and files.
+
+TIP: The [volume.FS](https://github.com/puppetlabs/wash/blob/master/volume/fs.go) helper can be used to expose an `Execable` entry's filesystem. Currently only `/var/log` is mounted.
+
+[plugin]: https://github.com/puppetlabs/wash/tree/master/plugin
+[volume]: https://github.com/puppetlabs/wash/tree/master/volume
\ No newline at end of file
diff --git a/website/content/docs/external_plugins/_index.md b/website/content/docs/external_plugins/_index.md
index 0c28312..019b4b8 100644
--- a/website/content/docs/external_plugins/_index.md
+++ b/website/content/docs/external_plugins/_index.md
@@ -4,60 +4,56 @@ title= "External Plugins"
 
 External plugins let Wash talk to other things outside of the built-in plugins. They can be written in any language. To write an external plugin, you need to do the following:
 
-1. Think about what the plugin would look like if it were modeled as a filesystem. Some useful questions to ask here are:
-    * What are the things (entries) that I want this plugin to represent?
-    * What things are my directories? What things are my files?
-    * What Wash actions should I support on these things?
+1. Write the [plugin script](#plugin-script). This is the script that Wash will shell out to whenever it needs to invoke a method on a specific entry within your plugin.
 
-2. Write the [plugin script](#plugin-script). This is the script that Wash will shell out to whenever it needs to invoke an action on a specific entry within your plugin.
-
-3. Add the plugin to the (configurable) `plugins.yaml` file by specifying a path to the plugin script. The name will be determined by invoking the script with the `init` action. An example `plugins.yaml` file is shown below:
+1. Add the plugin to your `wash.yaml` file under the `external-plugins` key, and specify the path to the plugin script. The plugin's name will be determined by invoking the script with the `init` method. An example `wash.yaml` config is shown below:
 
     ```
-    - script: '/Users/enis.inan/wash/external-plugins/external-aws.rb'
-    - script: '/Users/enis.inan/wash/external-plugins/network.sh'
+    external-plugins:
+        - script: '/Users/enis.inan/GitHub/wash/website/static/docs/external_plugins/examples/sshfs.sh'
     ```
-4. Start the Wash server to see your plugin in action.
-
-**NOTE:** You can override the default `plugins.yaml` path via the `external-plugins` flag. See `wash help server` for more information.
+1. Start the Wash shell to see your plugin in action.
 
 ## Plugin Script
 
-Wash shells out to the external plugin's script whenever it needs to invoke an action on one of its entries. The script must have the following usage:
+Wash shells out to the external plugin's script whenever it needs to invoke a method on one of its entries. The script must have the following usage:
 
 ```
-<plugin_script> <action> <path> <state> <args...>
+<plugin_script> <method> <path> <state> <args...>
 ```
 
 where
 
-* `<action>` is the Wash action that's being invoked
+* `<method>` is the Wash method that's being invoked
 * `<path>` is the entry's filesystem path relative to Wash's mountpoint
 * `<state>` consists of the minimum amount of information required to reconstruct the entry inside the plugin
-* `<args...>` are the arguments passed to the specific action.
+* `<args...>` are the method's arguments.
 
 `<path>` and `<state>` can be a bit confusing. To understand them, we recommend reading the [Aside](#aside), and to look at the provided [Bash](#bash-example) + Ruby external plugin examples to see how they're used. **TODO: Link a Ruby example**
 
-The remaining sections describe all possible values of `<action>` that can be passed-in, including each action's calling and error conventions, and the expected results.
+The remaining sections describe all the possible Wash methods that can be passed-in, including their calling and error conventions, and the expected results.
+
+NOTE: Plugin script invocations run in their own process group (pgrp). Wash will send a SIGTERM signal to the pgrp on a cancelled API/filesystem request. If after five seconds the invocation process has not terminated, then Wash will send a SIGKILL signal.
 
 ## init
-The `init` action is special. It is invoked as `<plugin_script> init`, and it is invoked only once, when the external plugin is loaded. When `init` is invoked, the script must output a JSON object representing the plugin root. Here's an example:
+The `init` method is special. It is invoked as `<plugin_script> init`, and it is invoked only once, when the external plugin is loaded. When `init` is invoked, the script must output a JSON object representing the plugin root. Here's an example:
 
 ```
 {
   "name": "aws",
-  "supported_actions": [
+  "methods": [
     "list"
   ]
 }
 ```
 
-This example shows the *minimum* amount of information required for Wash to construct the plugin root. In fact, the example shows the minimum amount of information required for Wash to construct *any* external plugin entry. This information consists of the entry's name and its supported actions.
+This example shows the *minimum* amount of information required for Wash to construct the plugin root. In fact, the example shows the minimum amount of information required for Wash to construct *any* external plugin entry. This information consists of the entry's name and its implemented methods.
 
-You can include additional keys in the printed JSON object. These keys are:
+You can include additional (optional) keys in the printed JSON object. These keys are:
 
-* `cache_ttls`. This specifies how many seconds each supported action's result should be cached (`ttl` is short for time to live).
-* `attributes`. This represents the entry's filesystem attributes, if any. These are the access time `atime`, last modified time `mtime`, creation time `ctime`, mode `mode`, and size `size`. The individual time attributes are specified in Unix seconds. Octal modes must be prefixed with the `0` delimiter (e.g. like `0777`). Hexadecimal modes must be prefixed with the `0x` delimiter (e.g. like `0xabcd`).
+* `cache_ttls`. This specifies how many seconds each method's result should be cached (`ttl` is short for time to live). Currently, Wash caches the result of `list`, `read`, and `metadata`.
+* `attributes`. This represents the entry's attributes (see the [`Attributes/Metadata`](/wash/docs#attributes-metadata) section). Time attributes are specified in Unix seconds. Octal modes must be prefixed with the `0` delimiter (e.g. like `0777`). Hexadecimal modes must be prefixed with the `0x` delimiter (e.g. like `0xabcd`).
+* `slash_replacer`. This overrides the default slash replacer `#`.
 * `state`. This corresponds to the `<state>` parameter in the plugin script's usage.
 
 Below is an example JSON object showcasing all possible keys at once.
@@ -65,72 +61,79 @@ Below is an example JSON object showcasing all possible keys at once.
 ```
 {
   "name": "some_entry",
-  "supported_actions": [
+  "methods": [
     "list"
   ],
   "cache_ttls": {
     "list": 30
   },
   "attributes": {
-    "atime": 1551942012,
     "mtime": 1551942012,
-    "ctime": 1551942012,
-    "mode": 511,
-    "size": 15600
+    "meta": {
+      "LastModifiedTime": 1551942012,
+      "Owner": "Wash",
+    }
   },
-  "state": "{\"klass\":\"AWS::Profile\"}"
+  "slash_replacer": ":",
+  "state": "{\"klass\":\"SSHFS::Directory\"}"
 }
 ```
 
-We see from `cache_ttls` that the result of `some_entry`'s `list` action will be cached for 30 seconds. We see from `attributes` that `some_entry` has some filesystem attributes that are defined (note that the mode is `0777`; `511` is its Base-10 representation). Finally, we see from `state` that `some_entry` has some state that Wash will pass-back in via the `<state>` parameter whenever it invokes one of its supported actions. In this case, only `list` is supported, and `<state>` is a stringified JSON object containing the entry's class (`AWS::Profile`) in whatever language the plugin script was written in.
+We see from `cache_ttls` that the result of `some_entry`'s `list` method will be cached for 30 seconds. We see from `attributes` that `some_entry` has an `mtime` attribute, and that it also includes the `meta` attribute. We see from `slash_replacer` that any `/`'es in the entry's returned name will be replaced by a `:` instead of a `#`. Finally, we see from `state` that `some_entry` has some state that Wash will pass-back in via the `<state>` parameter whenever it invokes one of its methods. In this case, only `list` is implemented, and `<state>` is a stringified JSON object containing the entry's class (`SSHFS::Directory`) in whatever language the plugin script was written in.
 
-The `init` action adopts the standard error conventions described in the [Errors](#errors) section.
+`init` adopts the standard error conventions described in the [Errors](#errors) section.
 
 ## list
-The `list` action is invoked as `<plugin_script> list <path> <state>`. When `list` is invoked, the script must output an array of JSON objects. Each JSON object has the same schema as the JSON object described in the `init` section (hence, `list` outputs an array of entries). Below is an example of valid output from the `list` action.
+`list` is invoked as `<plugin_script> list <path> <state>`. When `list` is invoked, the script must output an array of JSON objects. Each JSON object has the same schema as the JSON object described in the `init` section (hence, `list` outputs an array of entries). Below is an example of valid `list` output:
 
 ```
 [
-	{
-		"name": "LARGE_FILE.txt",
-		"supported_actions": [
-			"metadata",
-			"read",
-			"stream"
-		],
-		"attributes": {
-			"Mtime": 1551459978,
-			"Size": 100000000
-		},
-		"state": "{\"name\":\"LARGE_FILE.txt\",\"profile\":\"default\",\"region\":\"us-west-1\",\"bucket\":\"my-stupid-bucket-enis\",\"attributes\":{\"Mtime\":1551459978,\"Size\":100000000},\"klass\":\"S3Object\"}"
-	},
-	{
-		"name": "my_json_file.json",
-		"supported_actions": [
-			"metadata",
-			"read",
-			"stream"
-		],
-		"attributes": {
-			"Mtime": 1549572670,
-			"Size": 9336
-		},
-		"state": "{\"name\":\"my_json_file.json\",\"profile\":\"default\",\"region\":\"us-west-1\",\"bucket\":\"my-stupid-bucket-enis\",\"attributes\":{\"Mtime\":1549572670,\"Size\":9336},\"klass\":\"S3Object\"}"
-	}
+  {
+    "name": "fooVM",
+    "methods": [
+      "list",
+      "exec",
+      "metadata"
+    ],
+    "attributes": {
+      "mtime": 1558062927,
+      "meta": {
+        "LastModifiedTime": 1558062927,
+        "Owner": "Alice"
+      }
+    },
+    "state": "{\"klass\":\"SSHFS::VM\"}"
+  },
+  {
+    "name": "barVM",
+    "methods": [
+      "list",
+      "exec",
+      "metadata"
+    ],
+    "attributes": {
+      "mtime": 1558062927,
+      "meta": {
+        "LastModifiedTime": 1558062927,
+        "Owner": "Alice"
+      }
+    },
+    "state": "{\"klass\":\"SSHFS::VM\"}"
+  }
 ]
 ```
 
-**NOTE:** Remember that the state displayed here is the same `<state>` parameter that will be passed in to other actions invoked on that entry. For example, if the `read` action is invoked on `LARGE_FILE.txt` via `<plugin_script> read <parent_path>/LARGE_FILE.txt <state>`, then the value of `LARGE_FILE.txt`'s `state` key will be passed-in for `<state>`.
+**NOTE:** Remember that the state displayed here is the same `<state>` parameter that will be passed in to other methods invoked on that entry. For example, if `read` is invoked on `fooVM` via `<plugin_script> read <parent_path>/fooVM <state>`, then the value of `fooVM`'s `state` key will be passed-in for `<state>`.
 
-The `list` action adopts the standard error convention described in the [Errors](#errors) section.
+`list` adopts the standard error convention described in the [Errors](#errors) section.
 
 ## read
-The `read` action is invoked as `<plugin_script> read <path> <state>`. When `read` is invoked, the script must output the entry's content.
+`read` is invoked as `<plugin_script> read <path> <state>`. When `read` is invoked, the script must output the entry's content.
 
-The `read` action adopts the standard error convention described in the [Errors](#errors) section.
+`read` adopts the standard error convention described in the [Errors](#errors) section.
 
 ## metadata
-The `metadata` action is invoked as `<plugin_script> metadata <path> <state>`. When `metadata` is invoked, the script must output a JSON object representing the entry's metadata. Below is an example of acceptable `metadata` output:
+`metadata` is invoked as `<plugin_script> metadata <path> <state>`. When `metadata` is invoked, the script must output a JSON object representing the entry's metadata. Below is an example of acceptable `metadata` output:
 
 ```
 {
@@ -139,48 +142,52 @@ The `metadata` action is invoked as `<plugin_script> metadata <path> <state>`. W
 }
 ```
 
-The `metadata` action adopts the standard error convention described in the [Errors](#errors) section.
+`metadata` adopts the standard error convention described in the [Errors](#errors) section.
+
+NOTE: Only implement `metadata` if there is additional information about your entry that is not provided by the `meta` attribute.
 
 ## stream
-The `stream` action is invoked as `<plugin_script> stream <path> <state>`. When `stream` is invoked, the first line of the script's output must contain the `200` header. This header tells Wash that the entry's data is about to the streamed. After it outputs the header, the script must then stream the entry's data. Wash will continue to poll stdout for any updates until either the streaming process exits, or the user cancels the request. In the latter case, Wash will send the `SIGTERM` signal to the streaming process.
+`stream` is invoked as `<plugin_script> stream <path> <state>`. When `stream` is invoked, the first line of the script's output must contain the `200` header. This header tells Wash that the entry's data is about to the streamed. After it outputs the header, the script must then stream the entry's data. Wash will continue to poll stdout for any updates until either the streaming process exits, or the user cancels the request.
 
-The `stream` action adopts the standard error convention described in the [Errors](#errors) section.
+`stream` adopts the standard error convention described in the [Errors](#errors) section.
 
 ## exec
-The `exec` action is invoked as `<plugin_script> exec <path> <state> <cmd> <args...>`. If the `input` key is included as part of `opts` in a request to the `exec` endpoint, then its content is passed-in as stdin to the plugin script. When `exec` is invoked, the plugin script's stdout and stderr must be connected to `cmd`'s stdout and stderr, and it must exit the `exec` invocation with `cmd`'s exit code.
+`exec` is invoked as `<plugin_script> exec <path> <state> <opts> <cmd> <args...>`. If the `input` key is included as part of `opts` in a request to the `exec` endpoint, then its content is passed-in as stdin to the plugin script. `<opts>` is the JSON serialization of the remaining options.
+
+When `exec` is invoked, the plugin script's stdout and stderr must be connected to `cmd`'s stdout and stderr, and it must exit the `exec` invocation with `cmd`'s exit code.
 
-Because the `exec` action effectively hijacks `<plugin_script> exec` with `<cmd> <args...>`, there is currently no way for external plugins to report any `exec` errors to Wash. Thus, if `<plugin_script> exec` fails to exec `<cmd> <args...>` (e.g. due to a failed API call to trigger the exec), then that error output will be included as part of `<cmd> <args...>`'s output when running `wash exec`.
+Because `exec` effectively hijacks `<plugin_script> exec` with `<cmd> <args...>`, there is currently no way for external plugins to report any `exec` errors to Wash. Thus, if `<plugin_script> exec` fails to exec `<cmd> <args...>` (e.g. due to a failed API call to trigger the exec), then that error output will be included as part of `<cmd> <args...>`'s output when running `wash exec`.
 
 ## Errors <a name="errors"></a>
-All errors are printed to `stderr`. An action invocation is said to have errored when the plugin script returns a non-zero exit code. In that case, Wash wraps all of `stderr` into an error object, then documents that error in the process' activity and the server logs.
+All errors are printed to `stderr`. A method invocation is said to have errored when the plugin script returns a non-zero exit code. In that case, Wash wraps all of `stderr` into an error object, then documents that error in the process' activity and the server logs.
 
-**NOTE:** Not all actions adopt this error handling convention (e.g. `exec`). The error handling for these "snowflake" actions is described in their respective sections.
+**NOTE:** Not all method invocations adopt this error handling convention (e.g. `exec`). The error handling for these "snowflake" methods is described in their respective sections.
 
 
 ## Aside (optional)<a name="aside"></a>
 This section talks about the reasoning behind the plugin script's usage, shown below for convenience:
 
 ```
-<plugin_script> <action> <path> <state> <args...>
+<plugin_script> <method> <path> <state> <args...>
 ```
 
 If we let `<entry> = <path> <state>`, then our usage becomes:
 
 ```
-<plugin_script> <action> <entry> <args...>
+<plugin_script> <method> <entry> <args...>
 ```
 
-If we ignore `<plugin_script>` then the above turns into `<action> <entry> <args...>`. When read out loud, this looks like the function call `<action>(<entry>, <args...>)`. If we imagine `<entry>` as an object in an OOP language, this is semantically equivalent to:
+If we ignore `<plugin_script>` then the above turns into `<method> <entry> <args...>`. When read out loud, this looks like the function call `<method>(<entry>, <args...>)`. If we imagine `<entry>` as an object in an OOP language, this is semantically equivalent to:
 
 ```
-<entry>.<action>(<args...>)
+<entry>.<method>(<args...>)
 ```
 
-For example, if `<entry> = myS3Bucket`, `<action> = list`, and `<args...>` is empty, then we can read the above as `myS3Bucket.list()`. Thus, the plugin script's usage can be thought of as invoking an action (`<action>`) on the specified entry (`<path> <state>`) with the given arguments (`<args...>`).
+For example, if `<entry> = myS3Bucket`, `<method> = list`, and `<args...>` is empty, then we can read the above as `myS3Bucket.list()`. Thus, the plugin script's usage can be thought of as invoking a method (`<method>`) on the specified entry (`<path> <state>`) with the given arguments (`<args...>`).
 
-You might be wondering why we don't just lump `<path>` and `<state>` together into `<entry>` so that the plugin script's usage becomes `<plugin_script> <action> <entry> <args...>`. There's several reasons. One, having the `<path>` is helpful for debugging purposes. You can directly see the acting entry in the logs, which frees you from having to figure that information out yourself. Two, it mirrors the API's structure of `/fs/<action>/<path>`. And three, sometimes the `<path>` is all you need to write your plugin script. While you could always print the `<path>` yourself and make that the `<state>` parameter for Wash to pass around, it can be annoying to have to constantly do that, especially when you're writing simple plugins. Thus, `<path>` is really more of a convenience. You should use `<path>` if that's what you need to write your plugin. Otherwise, if you're writing a more complicated plugin that needs to maintain some state (e.g. like the entry's class name and its constructor arguments), then use `<state>`. However, try to avoid using `<path>` and `<state>` together in the same plugin script, as doing so can make it hard to reason about your code. Use either `<path>` or `<state>`, but not both.
+You might be wondering why we don't just lump `<path>` and `<state>` together into `<entry>` so that the plugin script's usage becomes `<plugin_script> <method> <entry> <args...>`. There's several reasons. One, having the `<path>` is helpful for debugging purposes. You can directly see the acting entry in the logs, which frees you from having to figure that information out yourself. Two, it mirrors the API's structure of `/fs/<method>/<path>`. And three, sometimes the `<path>` is all you need to write your plugin script. While you could always print the `<path>` yourself and make that the `<state>` parameter for Wash to pass around, it can be annoying to have to constantly do that, especially when you're writing simple plugins. Thus, `<path>` is really more of a convenience. You should use `<path>` if that's what you need to write your plugin. Otherwise, if you're writing a more complicated plugin that needs to maintain some state (e.g. like the entry's class name and its constructor arguments), then use `<state>`. However, try to avoid using `<path>` and `<state>` together in the same plugin script, as doing so can make it hard to reason about your code. Use either `<path>` or `<state>`, but not both.
 
-**NOTE:** The `init` action is special. Its usage is `<plugin_script> init` -- there is no `<path>` or `<state`> so there is no `<entry>`. Thus, the OOP call of `<entry>.<action>(<args...>)` doesn't make sense for `init`. So how do you reason about it? Why do we have an `init` action? Since every Wash plugin is modeled as a filesystem, it must have a root. Once we know the root, then it is easy to get to a specific entry by repeatedly invoking the `list` action. The `init` action is how you describe that 'root'.
+**NOTE:** The `init` method is special. Its usage is `<plugin_script> init` -- there is no `<path>` or `<state`> so there is no `<entry>`. Thus, the OOP call of `<entry>.<method>(<args...>)` doesn't make sense for `init`. So how do you reason about it? Why do we have an `init` method? Since every Wash plugin is modeled as a filesystem, it must have a root. Once we know the root, then it is easy to get to a specific entry by repeatedly invoking the `list` method. The `init` method is how you describe that 'root'.
 
 ## Bash Example
 
diff --git a/website/content/docs/go_plugins/_index.md b/website/content/docs/go_plugins/_index.md
deleted file mode 100644
index 1196815..0000000
--- a/website/content/docs/go_plugins/_index.md
+++ /dev/null
@@ -1,33 +0,0 @@
-+++
-title= "Go Plugins"
-+++
-
-## How to create a new Go plugin
-
-The [plugin] package defines a set of interfaces that a plugin can implement to enable specific behaviors.
-
-### Starting a plugin
-
-Create a new directory in [plugin] for the plugin.
-
-Create an object that implements the `Root` profile, as in it has:
-
-- a `Name` method to determine the name of the plugin when mounted
-- an `Init` method that loads and validates credentials
-- a `List` method that lists the first-level resources of the plugin
-
-### Extending the plugin
-
-Add new types representing the types of resources the plugin can list.
-
-- To make them appear as a directory, implement the `Group` interface.
-- Resources with metadata should implement the `Resource` interface.
-- To give files content, implement the `Readable` interface.
-- Log-type files can implement the `Pipe` interface to expose a stream of new data.
-- Files and directories that exist on a remote system or volume should implement the `File` interface. Additionally the [volume] package provides helpers for representing these types.
-- To support executing commands on a resource, implement the `Execable` interface.
-
-Tip: base new resources on the `EntryBase` struct to simplify creating them and enable caching.
-
-[plugin]: https://github.com/puppetlabs/wash/tree/master/plugin
-[volume]: https://github.com/puppetlabs/wash/tree/master/volume
\ No newline at end of file


Test patch:
diff --git a/api/helpers_test.go b/api/helpers_test.go
index 245cb0d..1444484 100644
--- a/api/helpers_test.go
+++ b/api/helpers_test.go
@@ -82,9 +82,9 @@ func (suite *HelpersTestSuite) TestFindEntry() {
 	expectedErr := plugin.DuplicateCNameErr{
 		ParentID:                        plugin.ID(parent),
 		FirstChildName:                  foo.Name(),
-		FirstChildSlashReplacementChar:  '#',
+		FirstChildSlashReplacer:         '#',
 		SecondChildName:                 duplicateFoo.Name(),
-		SecondChildSlashReplacementChar: '#',
+		SecondChildSlashReplacer:        '#',
 		CName:                           "foo#bar",
 	}
 	runTestCase(
diff --git a/plugin/cache_test.go b/plugin/cache_test.go
index 0399311..2668ab7 100644
--- a/plugin/cache_test.go
+++ b/plugin/cache_test.go
@@ -188,9 +188,9 @@ func (suite *CacheTestSuite) TestDuplicateCNameErr() {
 	err := DuplicateCNameErr{
 		ParentID:                        "/my_plugin/foo",
 		FirstChildName:                  "foo/bar/",
-		FirstChildSlashReplacementChar:  '#',
+		FirstChildSlashReplacer:         '#',
 		SecondChildName:                 "foo#bar/",
-		SecondChildSlashReplacementChar: '#',
+		SecondChildSlashReplacer:        '#',
 		CName:                           "foo#bar#",
 	}
 
@@ -284,9 +284,9 @@ func (suite *CacheTestSuite) TestCachedListCNameErrors() {
 		expectedErr := DuplicateCNameErr{
 			ParentID:                        "/my_plugin/foo",
 			FirstChildName:                  "foo/bar/",
-			FirstChildSlashReplacementChar:  '#',
+			FirstChildSlashReplacer:         '#',
 			SecondChildName:                 "foo#bar/",
-			SecondChildSlashReplacementChar: '#',
+			SecondChildSlashReplacer:        '#',
 			CName:                           "foo#bar#",
 		}
 
diff --git a/plugin/entryBase_test.go b/plugin/entryBase_test.go
index 8cea35a..c8549b6 100644
--- a/plugin/entryBase_test.go
+++ b/plugin/entryBase_test.go
@@ -66,16 +66,16 @@ func (suite *EntryBaseTestSuite) TestMetadata() {
 	suite.assertOpTTL(e, MetadataOp, "Metadata", -1)
 }
 
-func (suite *EntryBaseTestSuite) TestSetSlashReplacementChar() {
+func (suite *EntryBaseTestSuite) TestSetSlashReplacer() {
 	e := NewEntry("foo/bar")
 
 	suite.Panics(
-		func() { e.SetSlashReplacementChar('/') },
-		"e.SetSlashReplacementChar called with '/'",
+		func() { e.SetSlashReplacer('/') },
+		"e.SetSlashReplacer called with '/'",
 	)
 
-	e.SetSlashReplacementChar(':')
-	suite.Equal(e.slashReplacementChar(), ':')
+	e.SetSlashReplacer(':')
+	suite.Equal(e.slashReplacer(), ':')
 }
 
 func TestEntryBase(t *testing.T) {
diff --git a/plugin/externalPluginEntry_test.go b/plugin/externalPluginEntry_test.go
index a2b2df2..cc528d8 100644
--- a/plugin/externalPluginEntry_test.go
+++ b/plugin/externalPluginEntry_test.go
@@ -105,17 +105,17 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithCach
 	}
 }
 
-func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithSlashReplacementChar() {
+func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithSlashReplacer() {
 	decodedEntry := newMockDecodedEntry("name")
-	decodedEntry.SlashReplacementChar = "a string"
+	decodedEntry.SlashReplacer = "a string"
 	suite.Panics(
 		func() { _, _ = decodedEntry.toExternalPluginEntry() },
-		"e.SlashReplacementChar: received string a string instead of a character",
+		"e.SlashReplacer: received string a string instead of a character",
 	)
-	decodedEntry.SlashReplacementChar = ":"
+	decodedEntry.SlashReplacer = ":"
 	entry, err := decodedEntry.toExternalPluginEntry()
 	if suite.NoError(err) {
-		suite.Equal(':', entry.slashReplacementChar())
+		suite.Equal(':', entry.slashReplacer())
 	}
 }
 
@@ -134,7 +134,7 @@ func (suite *ExternalPluginEntryTestSuite) TestDecodeExternalPluginEntryWithAttr
 func (suite *ExternalPluginEntryTestSuite) TestSetCacheTTLs() {
 	decodedTTLs := decodedCacheTTLs{
 		List:     10,
-		Open:     15,
+		Read:     15,
 		Metadata: 20,
 	}
 
@@ -144,7 +144,7 @@ func (suite *ExternalPluginEntryTestSuite) TestSetCacheTTLs() {
 	entry.setCacheTTLs(decodedTTLs)
 
 	suite.Equal(decodedTTLs.List*time.Second, entry.getTTLOf(ListOp))
-	suite.Equal(decodedTTLs.Open*time.Second, entry.getTTLOf(OpenOp))
+	suite.Equal(decodedTTLs.Read*time.Second, entry.getTTLOf(OpenOp))
 	suite.Equal(decodedTTLs.Metadata*time.Second, entry.getTTLOf(MetadataOp))
 }
 
diff --git a/plugin/helpers_test.go b/plugin/helpers_test.go
index 5ad0d6f..9f7234e 100644
--- a/plugin/helpers_test.go
+++ b/plugin/helpers_test.go
@@ -30,7 +30,7 @@ func (suite *HelpersTestSuite) TestCName() {
 	e := NewEntry("foo/bar/baz")
 	suite.Equal("foo#bar#baz", CName(&e))
 
-	e.SetSlashReplacementChar(':')
+	e.SetSlashReplacer(':')
 	suite.Equal("foo:bar:baz", CName(&e))
 }